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

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

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

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

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

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

查看文件

@@ -136,11 +136,11 @@
const quickApproveWithdraw = async (id: string) => {
try {
await axios.post(`/api/finance/withdrawals/${id}/review`, { action: 'approve' });
Message.success('已审核通过并触发微信商户零钱打款');
const res: any = await axios.post(`/api/finance/withdrawals/${id}/review`, { action: 'approve' });
Message.success(res?.data?.message || '提现审核已处理');
fetchQuickTodos();
} catch (err: any) {
Message.error(err.message || '打款失败');
Message.error(err?.response?.data?.message || err.message || '打款失败');
}
};

查看文件

@@ -287,6 +287,48 @@
</a-col>
</a-row>
<a-divider orientation="left">提现打款(微信商家转账到零钱)</a-divider>
<a-alert type="info" style="margin-bottom: 12px">
提现审核"通过"将对大神发起真实的微信商家转账到零钱。需商户号已开通该产品并配置「转账场景ID」;未配置时生产环境会拒绝打款(不会只改状态、不发钱)。
</a-alert>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item
label="转账场景ID (transfer_scene_id)"
help="商户平台「商家转账到零钱」配置的转账场景编号"
>
<a-input v-model="wxPayForm.transferSceneId" placeholder="如 佣金报酬场景ID" allow-clear />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="转账备注 (transfer_remark)" help="展示给收款用户的转账备注,≤32字符">
<a-input v-model="wxPayForm.transferRemark" placeholder="佣金提现" allow-clear />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="收款人姓名加密" help="启用后按平台公钥加密提交收款人姓名(部分场景必填)">
<a-switch v-model="wxPayForm.transferUserNameEnabled" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="转账结果回调地址 (transfer_notify_url,选填)">
<a-input
v-model="wxPayForm.transferNotifyUrl"
placeholder="https://peiww.gynhc.com/api/pay/wechat/transfer-notify"
allow-clear
/>
</a-form-item>
<a-form-item
label="转账场景信息 (transfer_scene_report_infos,部分场景必填)"
help='JSON 数组,元素形如 {"info_type":"岗位类型","info_content":"外卖骑手"};不同转账场景要求的 info_type 不同,请以微信商户平台「商家转账到零钱」该场景说明为准。留空则不提交。'
>
<a-textarea
v-model="wxPayForm.transferSceneReportInfosText"
:auto-size="{ minRows: 2, maxRows: 6 }"
placeholder='[{"info_type":"岗位类型","info_content":"外卖骑手"}]'
/>
</a-form-item>
<input
ref="pemFileInputRef"
type="file"
@@ -375,6 +417,12 @@
platformPublicKeyId: '',
platformPublicKey: '',
notifyUrl: 'https://peiww.gynhc.com/api/pay/wechat/notify',
transferSceneId: '',
transferRemark: '佣金提现',
transferNotifyUrl: 'https://peiww.gynhc.com/api/pay/wechat/transfer-notify',
transferUserNameEnabled: false,
transferSceneReportInfos: [] as Array<{ info_type: string; info_content: string }>,
transferSceneReportInfosText: '',
remark: '微信支付V3直连商户参数配置'
});
@@ -399,6 +447,11 @@
...wxPayForm.value,
...data
};
wxPayForm.value.transferSceneReportInfosText = JSON.stringify(
wxPayForm.value.transferSceneReportInfos || [],
null,
2
);
if (data.effectiveMode) {
wxPayEffectiveMode.value = data.effectiveMode;
}
@@ -458,6 +511,22 @@
}
wxPaySaving.value = true;
try {
// 解析"转账场景信息"JSON(部分转账场景必填)
const rawSceneInfos = String(wxPayForm.value.transferSceneReportInfosText || '').trim();
let sceneInfos: Array<{ info_type: string; info_content: string }> = [];
if (rawSceneInfos) {
try {
const parsed = JSON.parse(rawSceneInfos);
if (!Array.isArray(parsed)) throw new Error('not_array');
sceneInfos = parsed;
} catch (e) {
Message.error('转账场景信息 (transfer_scene_report_infos) 需为合法 JSON 数组');
wxPaySaving.value = false;
return;
}
}
wxPayForm.value.transferSceneReportInfos = sceneInfos;
const res: any = await axios.post('/api/admin/pay/wechat/config', wxPayForm.value);
const data = res?.data?.data || res?.data || res;
if (data && data.effectiveMode) {