diff --git a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue index 9e0f6a4..3cdcb85 100644 --- a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue +++ b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue @@ -724,7 +724,7 @@ - + @@ -1394,7 +1394,7 @@ const loadingTemplates = ref(false); const wechatTemplates = ref(null); const testMsgForm = reactive({ templateType: 'order_status', - targetUserId: 1001, + targetUserId: null, previewTitle: '订单履约开始提醒', previewDetail: '您的同城生活陪伴订单已开始服务,请出示4位核销码完成现场确认。' }); diff --git a/server/src/index.ts b/server/src/index.ts index e358254..4ef7d6c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -250,12 +250,20 @@ async function assertUserNotBanned(userId: number, actionName: string = '操作' } } -// 读取用户微信 openid(用于微信官方内容安全 security.msgSecCheck 溯源),失败返回 undefined +// 微信 openid 形如 28 位字母数字(o 开头);演示种子数据里的 wx_openid_1001 不是真实 openid +function isPlausibleWechatOpenid(openid: any): boolean { + const value = String(openid || '').trim(); + if (!value) return false; + if (/^wx_openid_/i.test(value)) return false; + return /^[A-Za-z0-9_-]{20,64}$/.test(value); +} + +// 读取用户微信 openid(用于微信官方内容安全 security.msgSecCheck 溯源),失败或非真实 openid 返回 undefined async function resolveUserOpenid(userId: number): Promise { try { const [rows]: any = await pool.query('SELECT wechat_openid FROM users WHERE id = ? LIMIT 1', [Number(userId)]); const openid = rows && rows[0] && rows[0].wechat_openid; - return openid ? String(openid) : undefined; + return isPlausibleWechatOpenid(openid) ? String(openid) : undefined; } catch (_) { return undefined; } @@ -2789,7 +2797,10 @@ async function notifyBySubscribe( const [uRows]: any = await pool.query('SELECT wechat_openid FROM users WHERE id = ? LIMIT 1', [uid]); const openid = uRows && uRows[0] && uRows[0].wechat_openid; - if (!openid) return; + if (!isPlausibleWechatOpenid(openid)) { + logServerError('notifyBySubscribe', new Error(`订阅消息[${templateKey}]跳过:用户 ${uid} 没有有效的微信 openid`)); + return; + } const accessToken = await getWechatAccessToken(appId, appSecret); const result = await sendSubscribeMessageHttp(accessToken, { @@ -4444,12 +4455,15 @@ app.post('/api/admin/message/test-send', async (req: Request, res: Response) => const cfg = await resolveWechatPayRuntimeConfig(); const [uRows]: any = await pool.query('SELECT wechat_openid FROM users WHERE id = ? LIMIT 1', [Number(targetUserId) || 0]); - const openid = uRows && uRows[0] && uRows[0].wechat_openid; + const openidRaw = uRows && uRows[0] && uRows[0].wechat_openid; + const openidValid = isPlausibleWechatOpenid(openidRaw); + const openid = openidValid ? String(openidRaw) : ''; const diagnostics = { enablePush, hasTemplateId: Boolean(templateId), - hasOpenid: Boolean(openid), + hasOpenid: Boolean(openidRaw), + openidValid, hasAppId: Boolean(cfg.appId), hasAppSecret: Boolean(cfg.appSecret), appIdMasked: maskTail(cfg.appId), @@ -4517,9 +4531,11 @@ app.post('/api/admin/message/test-send', async (req: Request, res: Response) => ? '订阅消息总开关未开启' : (!templateId ? `未配置「${typeLabels[tKey] || tKey}」模板ID${reasonDetail}${tKey === 'verifyCode' ? '(该类型为可选:核销码已随「订单状态提醒」的温馨提示下发)' : ''}` - : (!openid + : (!openidRaw ? `目标用户(ID=${targetUserId || 0})没有 wechat_openid(该用户需通过微信登录过小程序)` - : '小程序 appId/appSecret 未配置')); + : (!openidValid + ? `目标用户(ID=${targetUserId || 0})的 openid 不是有效微信 openid(演示种子账号),请改用真实微信登录用户测试` + : '小程序 appId/appSecret 未配置'))); const result = { templateType: tKey, targetUserId: targetUserId || 1001, diff --git a/server/tests/system-alerts-audit.spec.ts b/server/tests/system-alerts-audit.spec.ts index 1ddf275..c109df1 100644 --- a/server/tests/system-alerts-audit.spec.ts +++ b/server/tests/system-alerts-audit.spec.ts @@ -53,4 +53,11 @@ describe('第一百批系统告警与后台审计', () => { expect(backupScript).toContain('gzip -t'); expect(backupScript).toContain("-mtime +14 -delete"); }); + + it('订阅消息必须识别演示账号的假 openid,避免 40003', () => { + expect(indexCode).toContain('function isPlausibleWechatOpenid'); + expect(indexCode).toContain("if (/^wx_openid_/i.test(value)) return false;"); + expect(indexCode).toContain('openidValid'); + expect(indexCode).toContain('请改用真实微信登录用户测试'); + }); });