feat(security): 三端闭环加固 + 微信提现打款/订阅消息落地

后端: 身份与归属鉴权收口; 资金安全(结算闸门/支付防篡改/报名费真实扣款/赠金/结算唯一入账/提现打款); 数据泄露与隐私; 登录与密钥安全(默认密钥闸门/登录限流); 可观测性; 启动自检.

微信: 商家转账到零钱提现打款闭环(含回调/查单/失败退回佣金); 订阅消息真实下发闭环与后台模板字段对齐.

后台: 财务/微信支付配置读取失败锁定; 看板/待办/走势去假数据; 打款与测试推送文案修正.

小程序: 请求层错误透传与登录过期提示; 多页失败反馈与乐观回滚; 帮助中心FAQ页面化.

测试: 新增 40+ 回归护栏与真实 MySQL 冒烟(75 套件/599 用例全绿).
这个提交包含在:
xcbyy
2026-10-17 18:00:09 +08:00
父节点 78117bb4e1
当前提交 2599dce315
共有 70 个文件被更改,包括 3806 次插入 和 330 次删除

查看文件

@@ -0,0 +1,66 @@
// 已读取AGENTS.md
/**
* 微信小程序「订阅消息」(服务通知) 下发客户端。
* 说明:订阅消息为一次性订阅,用户每次授权仅可下发一条;调用需有效 access_token。
* 该模块为纯函数/无状态,便于离线单元校验与复用。
*/
import https from 'https';
export interface SubscribeMessagePayload {
touser: string;
template_id: string;
page?: string;
data: Record<string, { value: string }>;
miniprogram_state?: 'developer' | 'trial' | 'formal';
lang?: string;
}
/**
* 组装微信订阅消息 data 字段:入参为 { 字段名: 值 },输出 { 字段名: { value } }。
* 微信各字段有长度限制(thing 类通常 ≤20 字符),此处统一截断到 20,避免 47003 参数错误。
*/
export function buildTemplateData(input: Record<string, string | number | undefined | null>): Record<string, { value: string }> {
const out: Record<string, { value: string }> = {};
for (const [key, val] of Object.entries(input || {})) {
if (val === undefined || val === null) continue;
out[key] = { value: String(val).slice(0, 20) };
}
return out;
}
/** 调用微信 cgi-bin/message/subscribe/send 真实下发 */
export function sendSubscribeMessageHttp(
accessToken: string,
payload: SubscribeMessagePayload
): Promise<{ errcode: number; errmsg: string }> {
const url = `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${encodeURIComponent(accessToken)}`;
const body = JSON.stringify(payload);
return new Promise((resolve, reject) => {
const req = https.request(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
},
(resp) => {
let data = '';
resp.on('data', (chunk) => {
data += chunk;
});
resp.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(e);
}
});
}
);
req.on('error', reject);
req.write(body);
req.end();
});
}

查看文件

@@ -168,6 +168,99 @@ export async function queryOrderByOutTradeNo(
return JSON.parse(resp.body || '{}');
}
/**
* RSA-OAEP(SHA-1) 加密敏感信息。
* 微信支付「商家转账到零钱」在部分转账场景下要求收款人真实姓名(user_name)以平台公钥加密后提交。
*/
export function encryptSensitiveInfo(platformPublicKey: string, plain: string): string {
const key = resolvePlatformKey(platformPublicKey);
if (!key) {
throw new Error('缺少有效的微信支付平台公钥,无法加密敏感信息(收款人姓名)');
}
const encrypted = crypto.publicEncrypt(
{ key, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha1' },
Buffer.from(String(plain), 'utf8')
);
return encrypted.toString('base64');
}
export interface TransferParams {
/** 商户侧转账单号(全局唯一,建议直接用提现单号,天然幂等) */
outBillNo: string;
openid: string;
amountCents: number;
/** 转账场景ID(商户平台「商家转账到零钱」配置的转账场景) */
sceneId: string;
remark: string;
notifyUrl?: string;
/** 收款人真实姓名(明文,调用内部会按平台公钥加密) */
userName?: string;
/** 部分转账场景必填的场景信息(如岗位类型/报酬说明) */
sceneReportInfos?: Array<{ info_type: string; info_content: string }>;
}
/** 商家转账到零钱(V3):向指定 openid 发起转账。成功受理返回 { out_bill_no, state, ... } */
export async function transferToWallet(cfg: WechatPayConfig, p: TransferParams): Promise<any> {
const urlPath = '/v3/fund-app/mch-transfer/transfer-bills';
const bodyObj: any = {
appid: cfg.appId,
out_bill_no: p.outBillNo,
transfer_scene_id: p.sceneId,
openid: p.openid,
transfer_amount: Math.round(Number(p.amountCents) || 0),
transfer_remark: String(p.remark || '').slice(0, 32),
};
if (p.notifyUrl) bodyObj.notify_url = p.notifyUrl;
if (p.userName && cfg.platformPublicKey) {
bodyObj.user_name = encryptSensitiveInfo(cfg.platformPublicKey, p.userName);
}
if (Array.isArray(p.sceneReportInfos) && p.sceneReportInfos.length > 0) {
bodyObj.transfer_scene_report_infos = p.sceneReportInfos;
}
const body = JSON.stringify(bodyObj);
const authorization = buildAuthorization({ method: 'POST', urlPath, body, cfg });
const resp = await httpsRequest({
method: 'POST',
urlPath,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: authorization,
'User-Agent': 'peiwan-server/1.0',
},
body,
});
let parsed: any = {};
try {
parsed = JSON.parse(resp.body || '{}');
} catch (_) {
parsed = {};
}
if (resp.statusCode >= 400) {
throw new Error(`微信商家转账失败 [${resp.statusCode}]: ${parsed.message || resp.body}`);
}
return parsed;
}
/** 查询商家转账单结果(按商户转账单号)。state: ACCEPTED/PROCESSING/SUCCESS/FAIL/CANCELING/CANCELLED */
export async function queryTransferByOutBillNo(cfg: WechatPayConfig, outBillNo: string): Promise<any> {
const urlPath = `/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/${encodeURIComponent(outBillNo)}`;
const authorization = buildAuthorization({ method: 'GET', urlPath, body: '', cfg });
const resp = await httpsRequest({
method: 'GET',
urlPath,
headers: {
Accept: 'application/json',
Authorization: authorization,
'User-Agent': 'peiwan-server/1.0',
},
});
if (resp.statusCode >= 400) {
throw new Error(`微信商家转账查单失败 [${resp.statusCode}]: ${resp.body}`);
}
return JSON.parse(resp.body || '{}');
}
/** 校验微信支付回调签名(使用平台公钥/平台证书),时间戳容差 5 分钟 */
export function verifyNotifySignature(params: {
cfg: WechatPayConfig;