优化公众号回复
This commit is contained in:
@@ -29,8 +29,8 @@
|
||||
<div v-else class="bind-box">
|
||||
<p class="tip">绑定微信后,可通过公众号接收系统提醒与公告推送。</p>
|
||||
<el-steps :active="bindStep" simple class="bind-steps">
|
||||
<el-step title="扫码关注公众号" />
|
||||
<el-step title="公众号回复验证码" />
|
||||
<el-step title="关注公众号" />
|
||||
<el-step title="获取验证码(扫码或发送「验证码」)" />
|
||||
<el-step title="输入验证码完成绑定" />
|
||||
</el-steps>
|
||||
<div class="bind-area">
|
||||
@@ -45,7 +45,7 @@
|
||||
<el-button type="primary" size="small" :loading="confirming" @click="handleConfirm">
|
||||
完成绑定
|
||||
</el-button>
|
||||
<p class="tip">扫码关注后公众号会自动回复 6 位验证码({{ ttlText }})。</p>
|
||||
<p class="tip">两种方式任选:① 扫描左侧二维码(未关注会自动关注);② 在公众号内发送「验证码」。都会回复 6 位验证码({{ ttlText }}),在上方输入即可绑定。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,10 +133,7 @@ const handleStart = async () => {
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!scene.value) {
|
||||
ElMessage.warning("请先获取绑定二维码并扫码关注");
|
||||
return;
|
||||
}
|
||||
// scene 可为空:在公众号内发送「验证码」获取时无需二维码,后端按验证码认领会话
|
||||
if (!code.value.trim()) {
|
||||
ElMessage.warning("请输入公众号回复的验证码");
|
||||
return;
|
||||
|
||||
@@ -83,7 +83,7 @@ func (p *mcpServerPayload) validate() error {
|
||||
return fmt.Errorf("请填写服务地址 URL")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("不支持的传输类型: %s(可选 stdio/http/sse)")
|
||||
return fmt.Errorf("不支持的传输类型: %s(可选 stdio/http/sse)", p.Transport)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -142,13 +142,24 @@ func (c *BackendWechatMpController) BindConfirm() {
|
||||
c.jsonErr(400, 400, "请输入公众号返回的验证码")
|
||||
return
|
||||
}
|
||||
follower, err := wechatmp.ConfirmVerifyCode(
|
||||
models.WechatBindTypeTenantUser,
|
||||
uint64(claims.UserID),
|
||||
uint64(claims.TenantId),
|
||||
p.Scene,
|
||||
p.Code,
|
||||
)
|
||||
var follower *models.WechatMpFollower
|
||||
if strings.TrimSpace(p.Scene) == "" {
|
||||
// 未展示二维码(在公众号内发送「验证码」获取):按验证码认领待认领会话
|
||||
follower, err = wechatmp.ConfirmVerifyCodeByCode(
|
||||
models.WechatBindTypeTenantUser,
|
||||
uint64(claims.UserID),
|
||||
uint64(claims.TenantId),
|
||||
p.Code,
|
||||
)
|
||||
} else {
|
||||
follower, err = wechatmp.ConfirmVerifyCode(
|
||||
models.WechatBindTypeTenantUser,
|
||||
uint64(claims.UserID),
|
||||
uint64(claims.TenantId),
|
||||
p.Scene,
|
||||
p.Code,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
|
||||
@@ -257,7 +257,13 @@ func (c *PlatformWechatMpController) BindConfirm() {
|
||||
c.jsonErr(400, 400, "请输入公众号返回的验证码")
|
||||
return
|
||||
}
|
||||
follower, err := wechatmp.ConfirmVerifyCode(models.WechatBindTypePlatformUser, uint64(claims.UserID), 0, p.Scene, p.Code)
|
||||
var follower *models.WechatMpFollower
|
||||
if strings.TrimSpace(p.Scene) == "" {
|
||||
// 未展示二维码(在公众号内发送「验证码」获取):按验证码认领待认领会话
|
||||
follower, err = wechatmp.ConfirmVerifyCodeByCode(models.WechatBindTypePlatformUser, uint64(claims.UserID), 0, p.Code)
|
||||
} else {
|
||||
follower, err = wechatmp.ConfirmVerifyCode(models.WechatBindTypePlatformUser, uint64(claims.UserID), 0, p.Scene, p.Code)
|
||||
}
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
|
||||
@@ -26,7 +26,7 @@ var (
|
||||
ErrSceneNotScanned = errors.New("尚未检测到扫码关注,请先使用微信扫码并关注公众号")
|
||||
ErrCodeUsed = errors.New("该验证码已使用,请重新获取二维码")
|
||||
ErrCodeExpired = errors.New("二维码已过期,请重新获取")
|
||||
ErrCodeMismatch = errors.New("验证码不正确")
|
||||
ErrCodeNotPending = errors.New("验证码不正确或已过期:请在公众号内发送「验证码」重新获取")
|
||||
ErrSceneInvalid = errors.New("二维码无效或已失效")
|
||||
)
|
||||
|
||||
@@ -105,7 +105,9 @@ func ConfirmVerifyCode(bindType string, bindID, bindTid uint64, scene, code stri
|
||||
return nil, ErrCodeExpired
|
||||
}
|
||||
if strings.TrimSpace(code) != row.Code {
|
||||
return nil, ErrCodeMismatch
|
||||
// 验证码不属于该二维码会话(用户在公众号内单独发送「验证码」取得):
|
||||
// 回退按验证码认领,保证「不扫码直接取码」的绑定路径可用
|
||||
return ConfirmVerifyCodeByCode(bindType, bindID, bindTid, code)
|
||||
}
|
||||
now := time.Now()
|
||||
if _, err := models.Orm.QueryTable(new(models.WechatMpVerifyCode)).
|
||||
@@ -116,6 +118,78 @@ func ConfirmVerifyCode(bindType string, bindID, bindTid uint64, scene, code stri
|
||||
return BindFollower(row.OpenID, bindType, bindID, bindTid)
|
||||
}
|
||||
|
||||
// CreateOpenIDVerify 由公众号消息发起绑定会话:openid 已知(消息发送者),
|
||||
// 绑定目标(bind_type/bind_id)为空,待系统页面登录用户输入验证码后认领。
|
||||
// 与扫码会话共用一张表,区别仅在发起方与归属信息。
|
||||
func CreateOpenIDVerify(openid string, ttl time.Duration) (*models.WechatMpVerifyCode, error) {
|
||||
openid = strings.TrimSpace(openid)
|
||||
if openid == "" {
|
||||
return nil, errors.New("openid 为空")
|
||||
}
|
||||
if ttl <= 0 {
|
||||
ttl = DefaultVerifyTTL
|
||||
}
|
||||
now := time.Now()
|
||||
expireAt := now.Add(ttl)
|
||||
row := &models.WechatMpVerifyCode{
|
||||
Scene: randomScene(),
|
||||
Code: randomCode6(),
|
||||
OpenID: openid,
|
||||
Status: models.WechatVerifyStatusScanned, // 微信号已知,等同「已扫码待核销」
|
||||
ExpireAt: &expireAt,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
// ConfirmVerifyCodeByCode 按验证码认领「公众号发起」的绑定会话(系统页面未展示二维码时使用):
|
||||
// 匹配最近一条 bind_id=0、openid 非空、未核销、未过期的会话,用 CAS 更新占位后绑定到当前账号。
|
||||
// 只认领无归属的会话,不会影响他人扫码发起的会话,避免绑定串号。
|
||||
func ConfirmVerifyCodeByCode(bindType string, bindID, bindTid uint64, code string) (*models.WechatMpFollower, error) {
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
return nil, ErrCodeNotPending
|
||||
}
|
||||
var row models.WechatMpVerifyCode
|
||||
err := models.Orm.QueryTable(new(models.WechatMpVerifyCode)).
|
||||
Filter("code", code).
|
||||
Filter("bind_id", 0).
|
||||
Filter("openid__ne", "").
|
||||
Filter("status", models.WechatVerifyStatusScanned).
|
||||
Filter("expire_at__gt", time.Now()).
|
||||
OrderBy("-id").
|
||||
One(&row)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, ErrCodeNotPending
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
// CAS:仅当仍是「待核销」时更新,避免同一验证码被并发认领
|
||||
affected, uerr := models.Orm.QueryTable(new(models.WechatMpVerifyCode)).
|
||||
Filter("id", row.ID).
|
||||
Filter("status", models.WechatVerifyStatusScanned).
|
||||
Update(map[string]interface{}{
|
||||
"bind_type": bindType,
|
||||
"bind_id": bindID,
|
||||
"bind_tid": bindTid,
|
||||
"status": models.WechatVerifyStatusUsed,
|
||||
"update_time": now,
|
||||
})
|
||||
if uerr != nil {
|
||||
return nil, uerr
|
||||
}
|
||||
if affected == 0 {
|
||||
return nil, ErrCodeUsed
|
||||
}
|
||||
return BindFollower(row.OpenID, bindType, bindID, bindTid)
|
||||
}
|
||||
|
||||
// BindFollower 将 openid 绑定到指定账号。
|
||||
// 同一账号只保留一个微信:旧的 openid 绑定关系会被清除。
|
||||
func BindFollower(openid, bindType string, bindID, bindTid uint64) (*models.WechatMpFollower, error) {
|
||||
@@ -198,6 +272,47 @@ func GetFollowerByUser(bindType string, bindID uint64) (*models.WechatMpFollower
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// GetFollowerByOpenID 按 openid 查询粉丝记录(不存在返回 nil, nil)
|
||||
func GetFollowerByOpenID(openid string) (*models.WechatMpFollower, error) {
|
||||
openid = strings.TrimSpace(openid)
|
||||
if openid == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var row models.WechatMpFollower
|
||||
err := models.Orm.QueryTable(new(models.WechatMpFollower)).Filter("openid", openid).One(&row)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// LatestPendingVerifyByOpenID 查询该微信号最近一条「未核销且未过期」的绑定会话,用于重发验证码。
|
||||
// 命中范围:扫码会话(扫码时写入 openid)与公众号内发起的待认领会话(创建即带 openid)。
|
||||
// 他人发起的、尚未扫码的会话 openid 为空,不会被命中,避免绑定串号。
|
||||
func LatestPendingVerifyByOpenID(openid string) (*models.WechatMpVerifyCode, error) {
|
||||
openid = strings.TrimSpace(openid)
|
||||
if openid == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var row models.WechatMpVerifyCode
|
||||
err := models.Orm.QueryTable(new(models.WechatMpVerifyCode)).
|
||||
Filter("openid", openid).
|
||||
Filter("status__in", models.WechatVerifyStatusWaiting, models.WechatVerifyStatusScanned).
|
||||
Filter("expire_at__gt", time.Now()).
|
||||
OrderBy("-id").
|
||||
One(&row)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// UpsertSubscribe 关注事件:创建/恢复粉丝记录
|
||||
func UpsertSubscribe(openid string, at time.Time) (*models.WechatMpFollower, error) {
|
||||
openid = strings.TrimSpace(openid)
|
||||
@@ -285,7 +400,7 @@ func HandleInbound(cfg *Config, msg *InboundMessage) string {
|
||||
if scene != "" {
|
||||
return handleScanScene(cfg, scene, openid)
|
||||
}
|
||||
return "欢迎关注云泽平台!\n如需接收系统提醒、公告推送,请回到系统页面点击「绑定微信」,使用微信扫码获取专属验证码。"
|
||||
return "欢迎关注云泽平台!\n如需接收系统提醒、公告推送,请回到系统页面点击「绑定微信」,使用微信扫码获取专属验证码;已扫码但未收到时可发送「验证码」重新获取。"
|
||||
case "scan":
|
||||
return handleScanScene(cfg, strings.TrimSpace(msg.EventKey), openid)
|
||||
case "unsubscribe":
|
||||
@@ -293,11 +408,58 @@ func HandleInbound(cfg *Config, msg *InboundMessage) string {
|
||||
return ""
|
||||
}
|
||||
case "text":
|
||||
return "如需绑定账号:请回到系统页面点击「绑定微信」,展示专属二维码后使用微信扫码,公众号会自动回复验证码。\n绑定成功后即可接收系统提醒、公告推送。"
|
||||
return handleTextIntent(openid, msg.Content)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// handleTextIntent 处理公众号文本消息:
|
||||
// - 发送「验证码」等关键词:返回该微信号的验证码(复用未核销会话;没有则在公众号侧
|
||||
// 新建「待认领」会话),用户回系统页面输入即完成绑定,无需先扫码;
|
||||
// - 其余文本:给出绑定引导。
|
||||
func handleTextIntent(openid, content string) string {
|
||||
if !isBindKeyword(strings.TrimSpace(content)) {
|
||||
return textBindGuide()
|
||||
}
|
||||
// 1) 复用该微信号未核销、未过期的会话(扫码会话或此前公众号发起的会话)
|
||||
row, err := LatestPendingVerifyByOpenID(openid)
|
||||
if err != nil || row == nil {
|
||||
// 2) 公众号侧直接发起:新建待认领会话,绑定目标由系统页面登录用户输入验证码后认领
|
||||
row, err = CreateOpenIDVerify(openid, 0)
|
||||
if err != nil {
|
||||
return "系统繁忙,请稍后重新发送。"
|
||||
}
|
||||
}
|
||||
minutes := 1
|
||||
if row.ExpireAt != nil {
|
||||
if m := int(time.Until(*row.ExpireAt).Minutes()); m > 1 {
|
||||
minutes = m
|
||||
}
|
||||
}
|
||||
tip := ""
|
||||
if f, ferr := GetFollowerByOpenID(openid); ferr == nil && f != nil && strings.TrimSpace(f.BindType) != "" {
|
||||
tip = "\n提示:当前微信已绑定账号,输入本验证码将更换绑定。"
|
||||
}
|
||||
return fmt.Sprintf("您的验证码是:%s\n请在系统页面「绑定微信」处输入完成绑定(%d 分钟内有效)。%s", row.Code, minutes, tip)
|
||||
}
|
||||
|
||||
// isBindKeyword 判断文本是否为「获取验证码 / 绑定」类意图
|
||||
func isBindKeyword(text string) bool {
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
switch text {
|
||||
case "验证码", "获取验证码", "重新获取验证码", "绑定", "绑定微信", "码":
|
||||
return true
|
||||
}
|
||||
return strings.Contains(text, "验证码")
|
||||
}
|
||||
|
||||
// textBindGuide 非关键词文本的绑定引导
|
||||
func textBindGuide() string {
|
||||
return "如需绑定账号:请在系统页面点击「绑定微信」,然后任选一种方式——① 扫描页面上的二维码;② 直接在本公众号发送「验证码」。\n两种方式都会回复 6 位验证码,回到系统页面输入即可完成绑定。"
|
||||
}
|
||||
|
||||
// handleScanScene 扫码(关注/已关注)后的统一处理:写入验证码会话 + 回复验证码
|
||||
func handleScanScene(cfg *Config, scene, openid string) string {
|
||||
if strings.TrimSpace(scene) == "" {
|
||||
|
||||
@@ -410,8 +410,8 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-steps :active="bindStep" simple class="bind-steps">
|
||||
<el-step title="微信扫码关注" />
|
||||
<el-step title="公众号回复验证码" />
|
||||
<el-step title="关注公众号" />
|
||||
<el-step title="获取验证码(扫码或发送「验证码」)" />
|
||||
<el-step title="输入验证码完成绑定" />
|
||||
</el-steps>
|
||||
<div class="bind-area">
|
||||
@@ -422,7 +422,7 @@
|
||||
<div class="bind-form">
|
||||
<el-input v-model="bindCode" placeholder="输入公众号回复的验证码" maxlength="8" class="bind-input" />
|
||||
<el-button type="primary" :loading="bindConfirming" @click="confirmBind">完成绑定</el-button>
|
||||
<p class="form-tip">扫码关注公众号后,公众号会自动回复 6 位验证码({{ bindTtlText }})。</p>
|
||||
<p class="form-tip">两种方式任选:① 微信扫描左侧二维码(未关注会自动关注);② 在公众号内直接发送「验证码」。都会回复 6 位验证码({{ bindTtlText }}),在上方输入即可完成绑定。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1101,10 +1101,7 @@ const startBind = async () => {
|
||||
};
|
||||
|
||||
const confirmBind = async () => {
|
||||
if (!bindScene.value) {
|
||||
ElMessage.warning("请先获取绑定二维码");
|
||||
return;
|
||||
}
|
||||
// scene 可为空:在公众号内发送「验证码」获取时无需二维码,后端按验证码认领会话
|
||||
if (!bindCode.value.trim()) {
|
||||
ElMessage.warning("请输入公众号回复的验证码");
|
||||
return;
|
||||
@@ -1292,10 +1289,6 @@ watch(activeSubTab, (tab) => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sub-tabs-container {
|
||||
/* height: 520px; */
|
||||
}
|
||||
|
||||
/* Make tabs look extra sleek */
|
||||
:deep(.el-tabs--left .el-tabs__header.is-left) {
|
||||
margin-right: 0;
|
||||
|
||||
Reference in New Issue
Block a user