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;

查看文件

@@ -208,7 +208,11 @@ export async function initDatabase(): Promise<void> {
amount DECIMAL(10,2) NOT NULL,
balance DECIMAL(10,2) NOT NULL,
channel VARCHAR(64) DEFAULT '微信商户零钱',
payee_name VARCHAR(64) DEFAULT '',
payee_account VARCHAR(128) DEFAULT '',
status VARCHAR(32) DEFAULT '待审核',
transfer_bill_no VARCHAR(64) DEFAULT '',
transfer_status VARCHAR(32) DEFAULT '',
apply_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
@@ -216,6 +220,18 @@ export async function initDatabase(): Promise<void> {
try {
await conn.query('ALTER TABLE withdrawals ADD COLUMN reason VARCHAR(255) DEFAULT ""');
} catch (_) {}
try {
await conn.query("ALTER TABLE withdrawals ADD COLUMN transfer_bill_no VARCHAR(64) DEFAULT ''");
} catch (_) {}
try {
await conn.query("ALTER TABLE withdrawals ADD COLUMN transfer_status VARCHAR(32) DEFAULT ''");
} catch (_) {}
try {
await conn.query("ALTER TABLE withdrawals ADD COLUMN payee_name VARCHAR(64) DEFAULT ''");
} catch (_) {}
try {
await conn.query("ALTER TABLE withdrawals ADD COLUMN payee_account VARCHAR(128) DEFAULT ''");
} catch (_) {}
// 7.5 微信支付支付意图与对账表 (幂等入账的唯一依据)
await conn.query(`
@@ -1048,6 +1064,19 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
VALUES ('faq_list', '帮助中心与常见问题FAQ', ?, '小程序端帮助中心常见问答与指引', 1)
`, [faqListVal]);
}
// 修复:faq_list 若存在但为空数组(曾被"空保存"清空),自动回填默认种子,
// 避免小程序端只显示本地兜底文案、后台却为空。
try {
const [faqRows]: any = await conn.query("SELECT value_json FROM platform_configs WHERE config_key = 'faq_list' LIMIT 1");
const raw = faqRows && faqRows[0] && faqRows[0].value_json;
let parsed: any = raw;
if (typeof raw === 'string') {
try { parsed = JSON.parse(raw); } catch (_) { parsed = null; }
}
if (!Array.isArray(parsed) || parsed.length === 0) {
await conn.query("UPDATE platform_configs SET value_json = ? WHERE config_key = 'faq_list'", [faqListVal]);
}
} catch (_) {}
if (!existingKeys.has('security_words')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)

查看文件

@@ -135,10 +135,20 @@ export class MysqlRepository {
}
const id = `act_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
const payType = data.payType || 1;
const maxParticipants = data.maxParticipants || 4;
const perPersonFeeCents = data.perPersonFeeCents || 5000;
const totalFeeCents = data.totalFeeCents || perPersonFeeCents * maxParticipants;
// 参数服务端校验与归一化,防止负数/超大值破坏报名费与人数上限
const payType = Number(data.payType) === 2 ? 2 : 1;
const rawMax = Number(data.maxParticipants);
const maxParticipants = Number.isFinite(rawMax) ? Math.min(100, Math.max(1, Math.round(rawMax))) : 4;
const MAX_FEE_CENTS = 100000000; // ¥1,000,000 单人均上限
const rawFee = Number(data.perPersonFeeCents);
const fee = Number.isFinite(rawFee) ? Math.min(MAX_FEE_CENTS, Math.max(0, Math.round(rawFee))) : 5000;
// 房主请客(payType=2)人均为 0;AA(payType=1) 使用校验后的人均
const perPersonFeeCents = payType === 1 ? fee : 0;
const rawTotal = Number(data.totalFeeCents);
const totalFeeCents =
Number.isFinite(rawTotal) && rawTotal >= perPersonFeeCents
? Math.min(MAX_FEE_CENTS * maxParticipants, Math.round(rawTotal))
: perPersonFeeCents * maxParticipants;
const allowDynamicPrice = data.allowDynamicPrice !== undefined ? (data.allowDynamicPrice ? 1 : 0) : 1;
const priceAdjustRule = data.priceAdjustRule || 'UNIFIED_HIGH';
@@ -258,11 +268,16 @@ export class MysqlRepository {
// 2. 行级锁锁定活动,串行化同一活动的并发报名,从根上杜绝超卖
const [actRows]: any = await conn.query(
'SELECT id, max_participants FROM activities WHERE id = ? FOR UPDATE',
'SELECT id, max_participants, pay_type, per_person_fee_cents FROM activities WHERE id = ? FOR UPDATE',
[activityId]
);
if (!actRows || actRows.length === 0) throw new Error('活动不存在');
const maxParticipants = Number(actRows[0].max_participants || 0);
// 报名费由服务端活动配置决定(pay_type=1 为 AA 收取人均分摊,其余为房主请客 0 元),
// 严禁信任客户端传入的 payAmountCents,否则可"免费报名 → 退款/清算套现"。
const joinFeeCents = Number(actRows[0].pay_type) === 1
? Math.max(0, Math.round(Number(actRows[0].per_person_fee_cents) || 0))
: 0;
// 3. 锁内以「当前读 (FOR UPDATE)」拉取该活动全部报名明细:
// - 当前读不受 REPEATABLE READ 快照影响,必然看到最新已提交数据,名额判断才准确;
@@ -282,6 +297,31 @@ export class MysqlRepository {
throw new Error('用户已报名该活动');
}
// 5. 真实扣款:AA 报名费从钱包余额扣除(杜绝免费报名后套取退款/清算)
if (joinFeeCents > 0) {
const joinFeeYuan = joinFeeCents / 100;
const [balRows]: any = await conn.query('SELECT balance FROM users WHERE id = ? FOR UPDATE', [params.userId]);
const bal = Number((balRows && balRows[0] && balRows[0].balance) || 0);
if (bal < joinFeeYuan) {
const err: any = new Error(`钱包余额不足,报名需支付 ¥${joinFeeYuan.toFixed(2)},请先充值`);
err.status = 400;
err.code = 40000;
throw err;
}
await conn.query('UPDATE users SET balance = balance - ? WHERE id = ?', [joinFeeYuan, params.userId]);
await conn.query(`
INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id)
VALUES (?, ?, ?, '活动报名扣款', ?, 'OUT', ?, ?)
`, [
`fin_jn_${Date.now()}_${Math.floor(100 + Math.random() * 900)}`,
`JN${Date.now()}${Math.floor(100 + Math.random() * 900)}`,
params.userId,
joinFeeYuan.toFixed(2),
`活动 ${activityId}`,
activityId
]);
}
// 5. 生成活动内唯一的 4 位纯数字核销码 (1000 - 9999)
const usedCodes = new Set(rowsList.map((r: any) => String(r.verify_code)));
let verifyCode = '';
@@ -312,14 +352,14 @@ export class MysqlRepository {
INSERT INTO participants
(id, activity_id, user_id, pay_amount_cents, verify_code, checkin_status)
VALUES (?, ?, ?, ?, ?, 0)
`, [ptId, activityId, params.userId, params.payAmountCents, verifyCode]);
`, [ptId, activityId, params.userId, joinFeeCents, verifyCode]);
// 6. 写入一条财务流水
const finId = `fin_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
await conn.query(`
INSERT INTO finance_records (id, record_no, type, amount, direction, counterparty, related_id)
VALUES (?, ?, '活动报名入账', ?, 'IN', ?, ?)
`, [finId, `TR${Date.now()}${Math.floor(Math.random() * 100)}`, params.payAmountCents / 100, `用户 ${params.userId}`, activityId]);
`, [finId, `TR${Date.now()}${Math.floor(Math.random() * 100)}`, joinFeeCents / 100, `用户 ${params.userId}`, activityId]);
await conn.commit();
@@ -335,7 +375,7 @@ export class MysqlRepository {
id: ptId,
activityId,
userId: params.userId,
payAmountCents: params.payAmountCents,
payAmountCents: joinFeeCents,
verifyCode,
cacheKey,
checkinStatus: false,
@@ -414,6 +454,7 @@ export class MysqlRepository {
FROM participants p
LEFT JOIN activities a ON p.activity_id = a.id
WHERE p.verify_code = ?
ORDER BY p.checkin_status ASC, p.created_at DESC
LIMIT 1
`, [verifyCode]);
@@ -443,6 +484,8 @@ export class MysqlRepository {
verify_code as verifyCode, status
FROM orders
WHERE verify_code = ?
AND status NOT IN ('已取消', '已退款', '已全额退款')
ORDER BY FIELD(status, '履约中', '服务中', '待确认', '已到场核销', '已完成'), created_at DESC
LIMIT 1
`, [verifyCode]);
@@ -509,17 +552,47 @@ export class MysqlRepository {
throw new Error('只有发起人有权调整活动出价');
}
// 校验调价金额:必须为非负整数且不超过平台单人均上限,防止负数/超大值破坏报名费与账目
const price = Number(newPriceCents);
if (!Number.isFinite(price) || !Number.isInteger(price) || price < 0) {
const err: any = new Error('调价金额必须为不小于 0 的整数(单位:分)');
err.status = 400;
err.code = 40000;
throw err;
}
const MAX_FEE_CENTS = 100000000; // ¥1,000,000 单人均上限
if (price > MAX_FEE_CENTS) {
const err: any = new Error(`调价金额超出平台单人均上限 ¥${(MAX_FEE_CENTS / 100).toLocaleString()}`);
err.status = 400;
err.code = 40000;
throw err;
}
const oldFee = Number(act.perPersonFeeCents || 0);
const deltaCents = price - oldFee;
await pool.query(`
UPDATE activities
SET per_person_fee_cents = ?
WHERE id = ?
`, [newPriceCents, activityId]);
`, [price, activityId]);
const finId = `fin_${Date.now()}`;
await pool.query(`
INSERT INTO finance_records (id, record_no, type, amount, direction, counterparty, related_id)
VALUES (?, ?, '房主追加赏金差额退补', ?, 'IN', ?, ?)
`, [finId, `TR${Date.now()}`, newPriceCents / 100, `房主 ${operatorId}`, activityId]);
// 仅按真实差额记账(避免把整份新价重复计入流水造成虚假营收)
if (deltaCents !== 0) {
const finId = `fin_${Date.now()}`;
await pool.query(`
INSERT INTO finance_records (id, record_no, type, amount, direction, counterparty, related_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
finId,
`TR${Date.now()}`,
deltaCents > 0 ? '房主追加赏金' : '房主下调出价差额',
Math.abs(deltaCents) / 100,
deltaCents > 0 ? 'IN' : 'OUT',
`房主 ${operatorId}`,
activityId
]);
}
return this.getActivityById(activityId);
}
@@ -929,14 +1002,27 @@ export class MysqlRepository {
? data.idCard.substring(0, 6) + '********' + data.idCard.substring(data.idCard.length - 4)
: '110101********0000';
const [existing]: any = await pool.query('SELECT * FROM companions WHERE (user_id IS NOT NULL AND user_id = ?) OR name = ?', [userId, name]);
if (existing && existing.length > 0) {
// 仅允许本人按 user_id 重新提交/更新档案;严禁按昵称匹配到他人档案后抢占其归属
const [ownRows]: any = await pool.query(
'SELECT * FROM companions WHERE user_id IS NOT NULL AND user_id = ? LIMIT 1',
[userId]
);
if (ownRows && ownRows.length > 0) {
await pool.query(`
UPDATE companions
SET user_id = ?, name = ?, track = ?, hourly_rate = ?, tags = ?, avatar = ?, gender = ?, audit_status = '待审核', status = '待审核', reject_reason = '', id_card_masked = ?, apply_time = NOW()
SET name = ?, track = ?, hourly_rate = ?, tags = ?, avatar = ?, gender = ?, audit_status = '待审核', status = '待审核', reject_reason = '', id_card_masked = ?, apply_time = NOW()
WHERE id = ?
`, [userId, name, track, hourlyRate, tagsJson, avatar, gender, maskedIdCard, existing[0].id]);
return this.getCompanionById(existing[0].id);
`, [name, track, hourlyRate, tagsJson, avatar, gender, maskedIdCard, ownRows[0].id]);
return this.getCompanionById(ownRows[0].id);
}
// 昵称冲突校验:若昵称已被(他人)大神档案占用,拒绝提交,防止冒充/抢占他人大神身份
const [nameRows]: any = await pool.query('SELECT id, user_id FROM companions WHERE name = ? LIMIT 1', [name]);
if (nameRows && nameRows.length > 0) {
const err: any = new Error('该大神昵称已被占用,请更换昵称后重新提交');
err.status = 409;
err.code = 40900;
throw err;
}
const [res]: any = await pool.query(`
@@ -1115,6 +1201,48 @@ export class MysqlRepository {
return rows;
}
/**
* 仅返回与指定用户相关的订单(买家 / 承接大神 / 已认领 / 其大神档案对应订单)。
* 用于替代把全平台订单下发给任意客户端,避免订单隐私与核销码泄露。
*/
async getOrdersForUser(userId: number): Promise<any[]> {
const uid = Number(userId) || 0;
await this.processOrderTimeouts().catch(() => {});
const [rows]: any = await pool.query(`
SELECT
id,
COALESCE(buyer_id, 1001) as buyerId,
COALESCE(expert_id, 101) as expertId,
order_no as orderNo,
client_name as clientName,
companion_name as companionName,
expert_avatar as expertAvatar,
track,
COALESCE(service_title, CONCAT(track, ' · 专业服务')) as serviceTitle,
expense_burden as expenseBurden,
hourly_rate as hourlyRate,
duration_hours as durationHours,
amount,
COALESCE(coupon_discount, 0.00) as couponDiscount,
COALESCE(refund_amount, 0.00) as refundAmount,
COALESCE(pay_type, 'WECHAT') as payType,
location,
verify_code as verifyCode,
remark,
status,
paid_at as paidAt,
finish_applied_at as finishAppliedAt,
created_at as createdAt
FROM orders
WHERE buyer_id = ?
OR expert_id = ?
OR claimed_by = ?
OR companion_name IN (SELECT name FROM companions WHERE user_id = ? OR id = ?)
ORDER BY created_at DESC
`, [uid, uid, uid, uid, uid]);
return rows;
}
async createOrder(data: {
buyerId?: number;
clientName?: string;
@@ -1149,16 +1277,41 @@ export class MysqlRepository {
const expertAvatar = data.expertAvatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120';
const track = data.track || '同城生活';
const serviceTitle = data.serviceTitle || `${track} · 专业陪伴服务`;
const hourlyRate = Number(data.hourlyRate) || 35;
const durationHours = Number(data.durationHours) || 2;
const amount = Number(data.amount) || hourlyRate * durationHours;
const couponDiscount = Number(data.couponDiscount || 0);
const durationHours = Math.min(24, Math.max(1, Math.round(Number(data.durationHours) || 2)));
// 服务单价以服务端大神档案为准(本人基础价,或该大神名下服务包价格),杜绝客户端自定义低价
let hourlyRate = Number(data.hourlyRate) || 35;
try {
const comp = await this.resolveCompanionRow(expertId);
if (comp) {
const compRate = Number(comp.hourly_rate) || 0;
let valid = compRate > 0 && Number(hourlyRate) === compRate;
if (!valid) {
const services = await this.getExpertServices(comp.id).catch(() => []);
valid = Array.isArray(services) && services.some((s: any) => Number(s.price) === Number(hourlyRate));
}
if (!valid && compRate > 0) {
hourlyRate = compRate;
}
}
} catch (e) {
logServerError('createOrder.validateRate', e);
}
// 优惠券折扣必须命中平台启用中的有效券(面额 / 门槛 / 品类),否则忽略为 0
const baseAmount = Number((hourlyRate * durationHours).toFixed(2));
const rawDiscount = Math.max(0, Number(data.couponDiscount || 0));
const couponDiscount = rawDiscount > 0 ? await this.resolveCouponDiscount(rawDiscount, track, baseAmount) : 0;
// 订单金额由服务端权威计算,杜绝客户端伪造低价下单
const amount = Number(Math.max(baseAmount - couponDiscount, 0.01).toFixed(2));
const payType = data.payType === 'BALANCE' ? 'BALANCE' : 'WECHAT';
const location = data.location || '朝阳大悦城';
const expenseBurden = data.expenseBurden || 'AA制各自付';
const verifyCode = String(Math.floor(1000 + Math.random() * 9000));
const remark = data.remark || '';
const status = data.status || '履约中';
// 初始订单状态由服务端按支付方式决定,杜绝客户端创建"已支付/已履约/已完成"订单绕过付款
const status = payType === 'BALANCE' ? '履约中' : '待付款';
const conn = await pool.getConnection();
try {
@@ -1181,9 +1334,9 @@ export class MysqlRepository {
const walletFinId = `fin_bal_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
const walletRecNo = `WB${Date.now()}${Math.floor(Math.random() * 100)}`;
await conn.query(`
INSERT INTO finance_records (id, record_no, type, amount, direction, counterparty, related_id)
VALUES (?, ?, '钱包余额支付订单', ?, 'OUT', ?, ?)
`, [walletFinId, walletRecNo, amount.toFixed(2), `用户 ${buyerId} (${companionName})`, id]);
INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id)
VALUES (?, ?, ?, '钱包余额支付订单', ?, 'OUT', ?, ?)
`, [walletFinId, walletRecNo, buyerId, amount.toFixed(2), `用户 ${buyerId} (${companionName})`, id]);
}
await conn.query(`
@@ -1193,7 +1346,7 @@ export class MysqlRepository {
expense_burden, hourly_rate, duration_hours, amount,
coupon_discount, pay_type,
location, verify_code, remark, status, paid_at, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${status === '待支付' || status === '待付款' ? 'NULL' : 'NOW()'}, NOW())
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${status === '待付款' ? 'NULL' : 'NOW()'}, NOW())
`, [
id, buyerId, expertId, orderNo, clientName,
companionName, expertAvatar, track, serviceTitle,
@@ -1206,9 +1359,9 @@ export class MysqlRepository {
const finId = `fin_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
const recordNo = `TX${Date.now()}${Math.floor(Math.random() * 100)}`;
await conn.query(`
INSERT INTO finance_records (id, record_no, type, amount, direction, counterparty, related_id)
VALUES (?, ?, ?, ?, 'IN', ?, ?)
`, [finId, recordNo, payType === 'BALANCE' ? '服务预约订单付款 (钱包余额)' : '服务预约订单付款 (微信支付)', amount.toFixed(2), clientName, id]);
INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id)
VALUES (?, ?, ?, ?, ?, 'IN', ?, ?)
`, [finId, recordNo, buyerId, payType === 'BALANCE' ? '服务预约订单付款 (钱包余额)' : '服务预约订单付款 (微信支付)', amount.toFixed(2), clientName, id]);
await conn.commit();
@@ -1257,6 +1410,66 @@ export class MysqlRepository {
return rows[0] || null;
}
/**
* 买家取消订单(仅限未进入履约的订单):
* - 未支付(待支付/待付款):直接取消;
* - 已支付待接单:取消并原路退回买家钱包余额(避免买家已付款却被静默吞掉资金);
* - 已进入履约/已完成/已退款等:拒绝,引导走退款申诉(仲裁)。
*/
async buyerCancelOrder(orderId: string): Promise<any> {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const [rows]: any = await conn.query('SELECT * FROM orders WHERE id = ? OR order_no = ? FOR UPDATE', [orderId, orderId]);
if (!rows || rows.length === 0) {
const err: any = new Error('订单不存在');
err.status = 404;
err.code = 40400;
throw err;
}
const order = rows[0];
const st = String(order.status);
const blocked = ['履约中', '服务中', '待确认', '已到场核销', '已完成', '已取消', '已退款', '已全额退款', '已部分退款', '争议仲裁中'];
if (blocked.includes(st)) {
const err: any = new Error(`订单当前状态【${st}】不可直接取消,请发起退款申诉`);
err.status = 409;
err.code = 40900;
throw err;
}
if (st === '待接单') {
const refundAmount = Number(order.amount || 0);
const buyerId = Number(order.buyer_id || 0);
await conn.query('UPDATE orders SET status = "已退款", refund_amount = ? WHERE id = ?', [refundAmount, order.id]);
if (buyerId > 0 && refundAmount > 0) {
await conn.query('UPDATE users SET balance = COALESCE(balance, 0) + ? WHERE id = ?', [refundAmount, buyerId]);
await conn.query(`
INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id, created_at)
VALUES (?, ?, ?, '买家取消订单退款', ?, 'IN', ?, ?, NOW())
`, [
`fin_cx_${Date.now()}_${Math.floor(100 + Math.random() * 900)}`,
`CX${Date.now()}${Math.floor(100 + Math.random() * 900)}`,
buyerId,
refundAmount.toFixed(2),
`订单 ${order.order_no || order.id}`,
String(order.id)
]);
}
} else {
await conn.query('UPDATE orders SET status = "已取消" WHERE id = ?', [order.id]);
}
await conn.commit();
const [updated]: any = await pool.query('SELECT * FROM orders WHERE id = ?', [order.id]);
return updated[0] || order;
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release();
}
}
/**
* 解析订单归属关系(买家 / 大神),用于写接口的身份越权校验。
* 大神身份采用「并集」策略:claimed_by(抢单人)∪ expert_id ∪ companions.user_id,
@@ -1265,7 +1478,7 @@ export class MysqlRepository {
async getOrderOwnership(id: string): Promise<any | null> {
const [rows]: any = await pool.query(`
SELECT o.id, o.order_no as orderNo, o.buyer_id as buyerId, o.expert_id as expertId,
o.claimed_by as claimedBy, o.status, o.companion_name as companionName,
o.claimed_by as claimedBy, o.status, o.amount, o.companion_name as companionName,
c.user_id as companionUserId
FROM orders o
LEFT JOIN companions c ON c.id = o.expert_id
@@ -1285,6 +1498,7 @@ export class MysqlRepository {
id: r.id,
orderNo: r.orderNo,
buyerId: Number(r.buyerId) || 0,
amount: Number(r.amount) || 0,
companionName: r.companionName || '',
status: r.status,
companionIds,
@@ -1293,6 +1507,21 @@ export class MysqlRepository {
// 大神点击申请完成服务 (服务中 -> 待确认,进入 48 小时买家确认窗口)
async applyCompleteOrder(orderId: string): Promise<any> {
const [cur]: any = await pool.query('SELECT status FROM orders WHERE id = ? OR order_no = ? LIMIT 1', [orderId, orderId]);
if (!cur || cur.length === 0) {
const err: any = new Error('订单不存在');
err.status = 404;
err.code = 40400;
throw err;
}
// 状态闸门:仅已进入履约的订单可申请完成,杜绝把未付款(待付款/待支付/待接单)订单推进为待确认再触发结算
const allowed = ['履约中', '服务中', '已到场核销', '待确认'];
if (!allowed.includes(String(cur[0].status))) {
const err: any = new Error(`当前订单状态【${cur[0].status}】不可申请完成`);
err.status = 409;
err.code = 40900;
throw err;
}
await pool.query(
"UPDATE orders SET status = '待确认', finish_applied_at = NOW() WHERE id = ? OR order_no = ?",
[orderId, orderId]
@@ -1311,8 +1540,12 @@ export class MysqlRepository {
throw new Error('订单不存在');
}
const order = rows[0];
if (['已完成', '已取消', '已退款', '已全额退款'].includes(order.status)) {
throw new Error(`当前订单状态【${order.status}】不可拒单`);
// 状态闸门:仅「待接单」可拒单,杜绝已进入履约/已完成的订单被拒单触发全额退款
if (String(order.status) !== '待接单') {
const err: any = new Error(`当前订单状态【${order.status}】不可拒单(仅待接单可拒单)`);
err.status = 409;
err.code = 40900;
throw err;
}
const rejectNote = data?.reason ? ` [大神拒单: ${data.reason}]` : ' [大神拒单自动退款]';
@@ -1352,6 +1585,24 @@ export class MysqlRepository {
}
}
/**
* 解析大神结算收款用户ID(唯一):
* 优先 companions.user_id;未绑定则按昵称唯一解析;再无则回落 expert_id。
* 严禁使用 `id = ? OR nickname = ?` 这类会同时命中多个用户的模糊匹配,防止改名重复入账。
*/
private async resolveExpertCreditUserId(conn: any, order: any, comp: any): Promise<number> {
const byUser = Number(comp?.user_id || 0);
if (byUser > 0) return byUser;
try {
const [uRows]: any = await conn.query('SELECT id FROM users WHERE nickname = ? LIMIT 1', [order.companion_name || '']);
const uid = Number((uRows && uRows[0] && uRows[0].id) || 0);
if (uid > 0) return uid;
} catch (e) {
logServerError('resolveExpertCreditUserId', e);
}
return Number(order.expert_id) || 101;
}
async completeOrderWithSettlement(id: string): Promise<any> {
const conn = await pool.getConnection();
try {
@@ -1367,7 +1618,7 @@ export class MysqlRepository {
// 状态闸门:仅「已进入履约/待确认」等具备已完成服务语义的订单可结算;
// 严禁对未支付、未接单、已取消、已退款的订单越权触发资金结算(防前端直接结算套现)。
const settleBlocked = ['待支付', '待接单', '已取消', '已退款', '已全额退款'];
const settleBlocked = ['待支付', '待付款', '待接单', '已取消', '已退款', '已全额退款'];
if (settleBlocked.includes(String(order.status))) {
const e: any = new Error(`订单当前状态【${order.status}】不可结算,已阻止越权触发资金结算`);
e.status = 409;
@@ -1405,10 +1656,10 @@ export class MysqlRepository {
await conn.query('UPDATE companions SET order_count = COALESCE(order_count, 0) + 1 WHERE id = ?', [comp.id]);
}
const expertUserId = Number(comp?.user_id || order.expert_id || 101);
const expertUserId = await this.resolveExpertCreditUserId(conn, order, comp);
await conn.query(
'UPDATE users SET commission = COALESCE(commission, 0) + ?, order_count = COALESCE(order_count, 0) + 1 WHERE id = ? OR nickname = ?',
[expertIncome, expertUserId, order.companion_name || '']
'UPDATE users SET commission = COALESCE(commission, 0) + ?, order_count = COALESCE(order_count, 0) + 1 WHERE id = ?',
[expertIncome, expertUserId]
);
const buyerId = Number(order.buyer_id || 1001);
@@ -1531,11 +1782,19 @@ export class MysqlRepository {
amount, location, expense_burden as payMethod,
status, created_at as createdAt
FROM orders
WHERE status NOT IN ('已完成', '已取消', '已关闭', '已退款', '已全额退款', '争议仲裁中', '已到场核销', '待确认')
AND (claimed_by IS NULL OR claimed_by = 0)
ORDER BY created_at DESC
LIMIT 20
`);
// 默认池保障
const maskClientName = (n: any): string => {
const s = String(n || '').trim();
if (!s) return '客户老板';
if (s.length <= 1) return `${s}**`;
return `${s[0]}**`;
};
const poolList = (rows || []).map((r: any) => ({
id: r.id,
category: r.category || '同城生活',
@@ -1543,7 +1802,7 @@ export class MysqlRepository {
location: r.location || '北京市朝阳区商圈',
reward: Number(r.amount) || 120,
timeSlot: '今日内随时可约',
userName: r.clientName || '客户老板',
userName: maskClientName(r.clientName),
userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120',
payMethod: r.payMethod || 'AA制各付',
countdown: 45,
@@ -1629,8 +1888,8 @@ export class MysqlRepository {
COALESCE(SUM(CASE WHEN status IN ('已完成', '已到场核销') THEN amount ELSE 0 END), 0) as totalIncome,
COALESCE(SUM(CASE WHEN status IN ('履约中', '服务中') THEN 1 ELSE 0 END), 0) as waitingVerify
FROM orders
WHERE expert_id = ? OR companion_name LIKE ?
`, [Number(expertId) || 101, '%我%']);
WHERE expert_id = ?
`, [Number(expertId) || 101]);
const stat = rows[0] || {};
return {
@@ -1664,10 +1923,10 @@ export class MysqlRepository {
COUNT(*) as orders,
COALESCE(SUM(amount), 0) as income
FROM orders
WHERE (expert_id = ? OR companion_name LIKE ?)
WHERE expert_id = ?
AND DATE(created_at) = ?
AND status IN ('已完成', '已到场核销', '服务中', '待确认')
`, [eId, '%我%', dateStr]);
`, [eId, dateStr]);
const count = Number(dayRows[0]?.orders || 0);
const inc = Number(Number(dayRows[0]?.income || 0).toFixed(2));
const avg = count > 0 ? Number((inc / count).toFixed(2)) : 0;
@@ -1689,10 +1948,10 @@ export class MysqlRepository {
COUNT(*) as orders,
COALESCE(SUM(amount), 0) as income
FROM orders
WHERE (expert_id = ? OR companion_name LIKE ?)
WHERE expert_id = ?
AND created_at >= ? AND created_at <= ?
AND status IN ('已完成', '已到场核销', '服务中', '待确认')
`, [eId, '%我%', startD, endD]);
`, [eId, startD, endD]);
const count = Number(wRows[0]?.orders || 0);
const inc = Number(Number(wRows[0]?.income || 0).toFixed(2));
const avg = count > 0 ? Number((inc / count).toFixed(2)) : 0;
@@ -1714,10 +1973,10 @@ export class MysqlRepository {
COUNT(*) as orders,
COALESCE(SUM(amount), 0) as income
FROM orders
WHERE (expert_id = ? OR companion_name LIKE ?)
WHERE expert_id = ?
AND DATE_FORMAT(created_at, '%Y-%m') = ?
AND status IN ('已完成', '已到场核销', '服务中', '待确认')
`, [eId, '%我%', yearMonth]);
`, [eId, yearMonth]);
const count = Number(mRows[0]?.orders || 0);
const inc = Number(Number(mRows[0]?.income || 0).toFixed(2));
const avg = count > 0 ? Number((inc / count).toFixed(2)) : 0;
@@ -2059,6 +2318,22 @@ export class MysqlRepository {
return rows;
}
/**
* 依据平台启用的充值阶梯,服务端权威计算赠金(分)。
* 规则:取「面额 <= 实充金额」的最高阶梯的赠金额;无匹配则赠金为 0。
* 严禁信任客户端自定义赠金,防止充值套取平台赠金。
*/
async computeRechargeBonusCents(amountCents: number): Promise<number> {
const paid = Math.max(0, Math.round(Number(amountCents) || 0));
if (paid <= 0) return 0;
const [rows]: any = await pool.query(
'SELECT bonus_amount FROM recharge_tiers WHERE status = 1 AND amount * 100 <= ? ORDER BY amount DESC LIMIT 1',
[paid]
);
if (!rows || rows.length === 0) return 0;
return Math.max(0, Math.round(Number(rows[0].bonus_amount) * 100));
}
async getAdminRechargeTiers(): Promise<any[]> {
const [rows]: any = await pool.query(`
SELECT
@@ -2154,6 +2429,30 @@ export class MysqlRepository {
}
// 2. 优惠券 (Coupons)
/**
* 校验并归一化订单优惠券折扣(元):必须命中平台启用中的有效券
* (面额一致、门槛满足、品类匹配),否则返回 0(忽略该折扣),防止客户端自定义大额折扣。
*/
async resolveCouponDiscount(discountYuan: number, track: string, baseAmountYuan: number): Promise<number> {
const d = Number(discountYuan) || 0;
if (d <= 0) return 0;
try {
const [rows]: any = await pool.query(
`SELECT discount_amount FROM coupons
WHERE status = 1 AND discount_amount = ?
AND (category_limit = 'ALL' OR category_limit = ?)
AND min_spend <= ?
LIMIT 1`,
[d, track, baseAmountYuan]
);
if (!rows || rows.length === 0) return 0;
return Math.min(Number(rows[0].discount_amount) || 0, Number(baseAmountYuan) || 0);
} catch (e) {
logServerError('resolveCouponDiscount', e);
return 0;
}
}
async getCoupons(categoryLimit?: string): Promise<any[]> {
let sql = `
SELECT
@@ -2518,11 +2817,12 @@ export class MysqlRepository {
// 剩余未退部分解冻结算给大神
const remainAmt = Number(Math.max(0, totalOrderAmt - partialAmt).toFixed(2));
if (remainAmt > 0) {
const expertUserId = Number(order.expert_id || 101);
const comp = await this.resolveCompanionRow(order.expert_id || order.companion_name || 101);
const expertUserId = await this.resolveExpertCreditUserId(conn, order, comp);
const expertIncome = Number((remainAmt * 0.9).toFixed(2));
await conn.query(
'UPDATE users SET commission = COALESCE(commission, 0) + ? WHERE id = ? OR nickname = ?',
[expertIncome, expertUserId, order.companion_name || '']
'UPDATE users SET commission = COALESCE(commission, 0) + ? WHERE id = ?',
[expertIncome, expertUserId]
);
const setId = `fin_pset_${Date.now()}`;
const setNo = `PJS${Date.now()}`;
@@ -2784,6 +3084,76 @@ export class MysqlRepository {
}
}
/** 读取提现工单及收款人 openid / 实名(用于真实微信打款) */
async getWithdrawalForTransfer(id: string): Promise<any | null> {
const [rows]: any = await pool.query(`
SELECT w.*, u.wechat_openid as userOpenid, u.nickname as userNickname
FROM withdrawals w
LEFT JOIN users u ON u.id = w.companion_id
WHERE w.id = ?
LIMIT 1
`, [id]);
if (!rows || rows.length === 0) return null;
return rows[0];
}
/** 记录打款受理状态(转账单号 + 状态) */
async markWithdrawalTransfer(id: string, billNo: string, status: string, transferStatus?: string): Promise<any> {
await pool.query(
'UPDATE withdrawals SET status = ?, transfer_bill_no = ?, transfer_status = ? WHERE id = ?',
[status, billNo, transferStatus || status, id]
);
return { id, status, transferBillNo: billNo, transferStatus: transferStatus || status };
}
/**
* 依据微信转账单最终状态落库:
* SUCCESS -> 已打款;FAIL/CANCELLED -> 打款失败并回退预扣佣金;其它 -> 更新中间态。
*/
async finalizeWithdrawalTransfer(outBillNo: string, state: string): Promise<any> {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const [rows]: any = await conn.query('SELECT * FROM withdrawals WHERE transfer_bill_no = ? FOR UPDATE', [outBillNo]);
if (!rows || rows.length === 0) {
await conn.rollback();
return { handled: false, reason: 'not_found', outBillNo };
}
const wd = rows[0];
if (state === 'SUCCESS') {
if (wd.status !== '已打款') {
await conn.query("UPDATE withdrawals SET status = '已打款', transfer_status = 'SUCCESS', reason = '' WHERE id = ?", [wd.id]);
}
await conn.commit();
return { handled: true, status: '已打款', userId: Number(wd.companion_id) || 0, amount: Number(wd.amount) || 0 };
}
if (state === 'FAIL' || state === 'CANCELLED') {
if (wd.status !== '已打款' && wd.status !== '打款失败') {
const userId = Number(wd.companion_id);
const amt = Number(wd.amount);
if (userId > 0 && amt > 0) {
await conn.query('UPDATE users SET commission = commission + ? WHERE id = ?', [amt, userId]);
await conn.query(`
INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id, created_at)
VALUES (?, ?, ?, '提现打款失败退回', ?, 'IN', ?, ?, NOW())
`, [`fin_wdf_${Date.now()}`, `WF${Date.now()}`, userId, amt, `微信商家转账失败(${state})退回`, String(wd.id)]);
}
await conn.query("UPDATE withdrawals SET status = '打款失败', transfer_status = ? WHERE id = ?", [state, wd.id]);
}
await conn.commit();
return { handled: true, status: '打款失败' };
}
await conn.query('UPDATE withdrawals SET transfer_status = ? WHERE id = ?', [state, wd.id]);
await conn.commit();
return { handled: true, status: wd.status, transferStatus: state };
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release();
}
}
async getFinanceConfig(): Promise<any> {
const [rows]: any = await pool.query(`
SELECT
@@ -2989,7 +3359,9 @@ export class MysqlRepository {
try {
const [rRows]: any = await pool.query('SELECT * FROM reports WHERE status = "待处理" ORDER BY created_at DESC');
rptRows = rRows || [];
} catch (_) {}
} catch (e) {
logServerError('getDashboardTodos.reports', e);
}
const pendingWithdrawalAmount = wdRows.reduce((sum: number, w: any) => sum + Number(w.amount), 0);
@@ -3135,23 +3507,26 @@ export class MysqlRepository {
ORDER BY p.created_at DESC
`, [id]);
// 订单记录
// 订单记录(按用户 id 关联,避免昵称重名导致错配)
const [userOrders]: any = await pool.query(`
SELECT id, order_no as orderNo, client_name as clientName, companion_name as companionName,
track, expense_burden as expenseBurden, hourly_rate as hourlyRate,
duration_hours as durationHours, amount, location, status, created_at as createdAt
FROM orders
WHERE client_name = ? OR companion_name = ?
WHERE buyer_id = ?
OR expert_id = ?
OR claimed_by = ?
OR companion_name IN (SELECT name FROM companions WHERE user_id = ? OR id = ?)
ORDER BY created_at DESC
`, [user.nickname, user.nickname]);
`, [id, id, id, id, id]);
// 资金流水
// 资金流水(按 user_id 精确归属)
const [financeRecords]: any = await pool.query(`
SELECT id, record_no as recordNo, type, amount, direction, counterparty, related_id as relatedId, created_at as createdAt
FROM finance_records
WHERE counterparty LIKE ? OR counterparty LIKE ?
WHERE user_id = ?
ORDER BY created_at DESC
`, [`%${user.nickname}%`, `%${user.id}%`]);
`, [id]);
// 风控日志
const [riskLogs]: any = await pool.query(`
@@ -3299,6 +3674,22 @@ export class MysqlRepository {
async findOrCreateUserByPhone(phone: string, openid?: string): Promise<any> {
const cleanPhone = phone.trim();
// 优先按微信 openid 精确匹配(微信身份全局唯一),避免手机号碰撞导致登录到他人账号
if (openid) {
const [oidRows]: any = await pool.query(
'SELECT id, phone FROM users WHERE wechat_openid = ? LIMIT 1',
[openid]
);
if (oidRows && oidRows.length > 0) {
const u = oidRows[0];
if (u.phone !== cleanPhone) {
await pool.query('UPDATE users SET phone = ? WHERE id = ?', [cleanPhone, u.id]);
}
return this.getUserById(u.id);
}
}
const [rows]: any = await pool.query(
`SELECT id, user_no as userNo, nickname, avatar, phone, gender, signature,
role, balance, commission, invited_count as invitedCount,
@@ -3346,8 +3737,17 @@ export class MysqlRepository {
values.push(data.avatar.trim());
}
if (data.phone && data.phone.trim()) {
const newPhone = data.phone.trim();
// 联系方式唯一性:避免把手机号改成他人已用号码造成登录身份碰撞
const [dup]: any = await pool.query('SELECT id FROM users WHERE phone = ? AND id <> ? LIMIT 1', [newPhone, id]);
if (dup && dup.length > 0) {
const err: any = new Error('该联系方式已被其他账号使用,请更换后重试');
err.status = 409;
err.code = 40900;
throw err;
}
fields.push('phone = ?');
values.push(data.phone.trim());
values.push(newPhone);
}
if (data.gender && ['男', '女', '保密'].includes(data.gender)) {
fields.push('gender = ?');
@@ -3535,6 +3935,24 @@ export class MysqlRepository {
return { handled: true, already: true, scene: intent.scene, credited: Number(intent.credited) === 1 };
}
// 金额一致性校验:微信实收金额必须与支付意图金额完全一致,
// 杜绝伪造低价支付意图来「低价支付高价订单」或套取充值赠金。
const paidCents = Number(opts?.amountTotalCents);
if (!isNaN(paidCents) && paidCents > 0 && paidCents !== Number(intent.amount_cents)) {
await conn.rollback();
logServerError(
'applyPaymentSuccess.amountMismatch',
new Error(`outTradeNo=${outTradeNo} 应收=${intent.amount_cents}分 实收=${paidCents}分`)
);
return {
handled: false,
reason: 'amount_mismatch',
expectedCents: Number(intent.amount_cents),
paidCents,
outTradeNo,
};
}
await conn.query(
"UPDATE payment_intents SET status = 'SUCCESS', transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?",
[opts?.transactionId || '', outTradeNo]
@@ -3684,17 +4102,17 @@ export class MysqlRepository {
const wdId = `wd_${Date.now()}`;
const wdNo = `TX${Date.now()}`;
await conn.query(`
INSERT INTO withdrawals (id, withdraw_no, companion_name, companion_id, amount, balance, channel, status, apply_time)
VALUES (?, ?, ?, ?, ?, ?, ?, '待审核', NOW())
`, [wdId, wdNo, user.nickname, id, amount, remainingCommission, fullChannel]);
INSERT INTO withdrawals (id, withdraw_no, companion_name, companion_id, amount, balance, channel, payee_name, payee_account, status, apply_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, '待审核', NOW())
`, [wdId, wdNo, user.nickname, id, amount, remainingCommission, fullChannel, accountInfo?.realName || '', accountInfo?.account || '']);
// 写入财务出账流水
const recId = `fin_wd_${Date.now()}`;
const recNo = `TR${Date.now()}`;
await conn.query(`
INSERT INTO finance_records (id, record_no, type, amount, direction, counterparty, related_id, created_at)
VALUES (?, ?, '佣金提现', ?, 'OUT', ?, ?, NOW())
`, [recId, recNo, amount, `${user.nickname} (${fullChannel})`, wdId]);
INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id, created_at)
VALUES (?, ?, ?, '佣金提现', ?, 'OUT', ?, ?, NOW())
`, [recId, recNo, id, amount, `${user.nickname} (${fullChannel})`, wdId]);
await conn.commit();
return {
@@ -3714,15 +4132,13 @@ export class MysqlRepository {
}
async getUserFinanceRecords(userId: number): Promise<any[]> {
const [uRows]: any = await pool.query('SELECT nickname FROM users WHERE id = ?', [userId]);
const nickname = uRows[0]?.nickname || '';
const [rows]: any = await pool.query(`
SELECT id, record_no as recordNo, user_id as userId, type, amount, direction, counterparty, related_id as relatedId, created_at as createdAt
FROM finance_records
WHERE user_id = ? OR counterparty LIKE ? OR counterparty LIKE ? OR related_id = ?
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 100
`, [userId, `%${nickname}%`, `%${userId}%`, String(userId)]);
`, [Number(userId)]);
return rows;
}
@@ -4649,13 +5065,6 @@ export class MysqlRepository {
...u,
totalContribute: contribMap.get(Number(u.id)) || '0.00'
}));
if (directList.length === 0) {
directList = [
{ id: 2001, nickname: '晨曦微风', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120', phone: '138****6612', createdAt: '2026-10-01 14:20:00', level: 1, totalContribute: '48.50' },
{ id: 2002, nickname: '峡谷追风少年', avatar: 'https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?w=120', phone: '139****8833', createdAt: '2026-10-03 19:10:00', level: 1, totalContribute: '36.00' },
{ id: 2003, nickname: '软糖小鹿', avatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=120', phone: '137****1199', createdAt: '2026-10-05 21:05:00', level: 1, totalContribute: '24.00' }
];
}
const directIds = directList.map((u: any) => u.id).filter(Boolean);
let indirectList: any[] = [];
@@ -4673,12 +5082,6 @@ export class MysqlRepository {
}));
} catch (_) {}
}
if (indirectList.length === 0) {
indirectList = [
{ id: 3001, nickname: '星空漫步者', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=120', phone: '150****5521', createdAt: '2026-10-06 11:30:00', level: 2, totalContribute: '12.00' },
{ id: 3002, nickname: '吃饱就睡的喵', avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=120', phone: '151****9932', createdAt: '2026-10-08 16:45:00', level: 2, totalContribute: '8.00' }
];
}
return {
directCount: directList.length,
@@ -4722,11 +5125,8 @@ export class MysqlRepository {
}
} catch (_) {}
return [
{ id: 'dst_101', fromUserName: '晨曦微风', orderNo: 'PW20261001991', orderAmount: '99.00', commission: '4.95', level: 1, ratePercent: '5.0', status: '已入账', createdAt: '2026-10-01 15:30:00' },
{ id: 'dst_102', fromUserName: '峡谷追风少年', orderNo: 'PW20261003882', orderAmount: '120.00', commission: '6.00', level: 1, ratePercent: '5.0', status: '已入账', createdAt: '2026-10-03 20:12:00' },
{ id: 'dst_103', fromUserName: '星空漫步者', orderNo: 'PW20261006117', orderAmount: '150.00', commission: '3.00', level: 2, ratePercent: '2.0', status: '已入账', createdAt: '2026-10-06 12:40:00' }
];
// 无真实返佣记录时返回空数组,不再用硬编码假数据冒充实盘收益
return [];
}
}

文件差异内容过多而无法显示 加载差异

查看文件

@@ -0,0 +1,31 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第二十六批回归闸门:组局创建参数服务端校验
*/
describe('第二十六批组局创建校验闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
const block = () => {
const start = repoCode.indexOf('async createActivity(');
expect(start).toBeGreaterThan(-1);
return repoCode.slice(start, start + 1600);
};
it('付费类型/人数/费用必须服务端归一化', () => {
const b = block();
expect(b).toContain('const payType = Number(data.payType) === 2 ? 2 : 1;');
expect(b).toContain('Math.min(100, Math.max(1, Math.round(rawMax)))');
expect(b).toContain('MAX_FEE_CENTS');
expect(b).toContain('const perPersonFeeCents = payType === 1 ? fee : 0;');
});
it('不得再直接信任客户端未校验费用', () => {
const b = block();
expect(b).not.toContain('const perPersonFeeCents = data.perPersonFeeCents || 5000;');
expect(b).not.toContain('const maxParticipants = data.maxParticipants || 4;');
});
});

查看文件

@@ -0,0 +1,32 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第二十八批回归闸门:组局详情不得泄露他人核销码
*/
describe('第二十八批组局详情核销码隐私闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
it('活动详情路由必须剥离非本人核销码', () => {
const start = indexCode.indexOf("app.get('/api/activities/:id',");
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 1200);
expect(block).toContain('严禁向他人泄露');
expect(block).toContain('Number(p.userId) === callerId');
expect(block).toContain('const { verifyCode, ...rest } = p;');
expect(block).toContain('isVerifiedAdminRequest(req)');
});
it('小程序请求详情携带当前用户身份', () => {
const start = apiCode.indexOf('export async function getActivityDetail(');
const block = apiCode.slice(start, start + 300);
expect(block).toContain('/activities/${id}?userId=${normalizeUserId()}');
});
it('数据适配器不得为缺失核销码伪造 8392', () => {
expect(apiCode).not.toContain("verifyCode: p.verifyCode || '8392'");
});
});

查看文件

@@ -0,0 +1,71 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第十一批回归闸门:组局发起/解散、抢单、搭子上车、用户设置的身份收口
*/
describe('第十一批写接口身份收口闸门 (组局 / 抢单 / 上车 / 设置)', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
const routeBlock = (marker: string, len = 1200) => {
const start = indexCode.indexOf(marker);
expect(start).toBeGreaterThan(-1);
return indexCode.slice(start, start + len);
};
describe('1. 组局发起人身份以服务端会话为准', () => {
it('创建组局必须经 resolveSecureUserId 绑定发起人', () => {
const block = routeBlock("app.post('/api/activities',");
expect(block).toContain("'发起组局'");
expect(block).not.toContain("req.body?.creatorId || req.headers['x-user-id'] || 1001");
});
});
describe('2. 组局解散人身份以服务端会话为准', () => {
it('解散组局:管理员走签名 token,其余经 resolveSecureUserId', () => {
const block = routeBlock("app.post('/api/activities/:id/cancel'");
expect(block).toContain('isVerifiedAdminRequest(req)');
expect(block).toContain("'解散组局'");
expect(block).not.toContain("const callerId = Number(hostId ||");
});
});
describe('3. 抢单身份以服务端会话为准', () => {
it('接单必须经 resolveSecureUserId 且写库用服务端身份', () => {
const block = routeBlock("app.post('/api/orders/:id/claim'");
expect(block).toContain("'接单'");
expect(block).toContain('claimOrder(orderId, { ...(req.body || {}), expertId: operatorId })');
});
});
describe('4. 搭子上车申请身份以服务端会话为准', () => {
it('上车申请必须经 resolveSecureUserId', () => {
const block = routeBlock("app.post('/api/posts/:id/apply'");
expect(block).toContain("'申请加入搭子车队'");
expect(block).not.toContain("applicantId || req.headers['x-user-id'] || 1001");
});
});
describe('5. 用户设置读写归属', () => {
it('设置读写必须经 resolveSecureUserId', () => {
const getBlock = routeBlock("app.get('/api/users/:id/settings'");
const postBlock = routeBlock("app.post('/api/users/:id/settings'");
expect(getBlock).toContain("'查看个人设置'");
expect(postBlock).toContain("'保存个人设置'");
});
});
describe('6. 小程序端写接口携带真实身份', () => {
it('组局/抢单/上车/报名/退席均注入当前用户身份', () => {
expect(apiCode).toContain('creatorId: normalizeUserId(payload.creatorId)');
expect(apiCode).toContain('hostId: normalizeUserId(hostId)');
expect(apiCode).toContain('userId: normalizeUserId(params.userId)');
expect(apiCode).toContain('userId: normalizeUserId(userId),');
expect(apiCode).toContain('userId: normalizeUserId(expertData.userId)');
expect(apiCode).toContain('applicantId: normalizeUserId(applyData.applicantId)');
});
});
});

查看文件

@@ -0,0 +1,37 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第二十四批回归闸门:组局报名费用服务端权威 + 真实扣款
*/
describe('第二十四批组局报名资金闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
const joinBlock = () => {
const start = repoCode.indexOf('async joinActivity(');
expect(start).toBeGreaterThan(-1);
return repoCode.slice(start, start + 6200);
};
it('报名费以服务端活动配置为准(AA=人均分摊,房主请客=0)', () => {
const block = joinBlock();
expect(block).toContain('per_person_fee_cents');
expect(block).toContain('const joinFeeCents = Number(actRows[0].pay_type) === 1');
});
it('报名从钱包真实扣款', () => {
const block = joinBlock();
expect(block).toContain("'活动报名扣款'");
expect(block).toContain('UPDATE users SET balance = balance - ?');
expect(block).toContain('钱包余额不足,报名需支付');
});
it('入账/参与者金额均使用服务端权威值', () => {
const block = joinBlock();
expect(block).not.toContain('params.payAmountCents / 100');
expect(block).not.toContain('params.userId, params.payAmountCents, verifyCode');
expect(block).toContain('payAmountCents: joinFeeCents');
});
});

查看文件

@@ -0,0 +1,32 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第二十五批回归闸门:房主改价的金额校验与差额记账
*/
describe('第二十五批改价完整性闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
const block = () => {
const start = repoCode.indexOf('async adjustPrice(');
expect(start).toBeGreaterThan(-1);
return repoCode.slice(start, start + 1800);
};
it('必须校验调价金额(非负整数 + 平台上限)', () => {
const b = block();
expect(b).toContain('Number.isInteger(price)');
expect(b).toContain('MAX_FEE_CENTS');
expect(b).toContain('调价金额必须为不小于 0 的整数');
});
it('必须按真实差额记账而非整份新价', () => {
const b = block();
expect(b).toContain('const deltaCents = price - oldFee');
expect(b).toContain("'房主追加赏金'");
expect(b).toContain("'房主下调出价差额'");
expect(b).not.toContain("'房主追加赏金差额退补'");
});
});

查看文件

@@ -0,0 +1,33 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第五十六批回归闸门:提现打款场景信息可在后台自助配置
*/
describe('第五十六批打款场景信息配置闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const adminConfig = fs.readFileSync(
path.join(rootDir, 'admin/arco-design-pro-vite/src/views/finance/config/index.vue'),
'utf-8'
);
it('后台提供转账场景ID/备注/姓名加密/回调/场景信息配置项', () => {
expect(adminConfig).toContain('transferSceneId');
expect(adminConfig).toContain('transferRemark');
expect(adminConfig).toContain('transferUserNameEnabled');
expect(adminConfig).toContain('transferNotifyUrl');
expect(adminConfig).toContain('transferSceneReportInfosText');
expect(adminConfig).toContain('transfer_scene_report_infos');
});
it('服务端持久化并透传场景信息', () => {
// 保存接口白名单
expect(indexCode).toContain('transferSceneReportInfos: Array.isArray(incoming.transferSceneReportInfos)');
// 运行时配置解析透传
const start = indexCode.indexOf('async function resolveWechatPayRuntimeConfig(');
const block = indexCode.slice(start, start + 2600);
expect(block).toContain('transferSceneReportInfos');
});
});

查看文件

@@ -0,0 +1,29 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第十七批回归闸门:管理端写接口鉴权(用户删除 / 场所管理 / 订单超时扫描)
*/
describe('第十七批管理端写接口鉴权闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
it('删除用户仅管理员可执行', () => {
expect(indexCode).toContain("app.delete('/api/users/:id', requireAdmin");
});
it('场所增删改仅管理员可执行', () => {
expect(indexCode).toContain("app.post('/api/safety/places', requireAdmin");
expect(indexCode).toContain("app.put('/api/safety/places/:id', requireAdmin");
expect(indexCode).toContain("app.delete('/api/safety/places/:id', requireAdmin");
});
it('场所列表仍保持公开可读(小程序合规场所库)', () => {
expect(indexCode).toContain("app.get('/api/safety/places', async");
});
it('订单超时自动流转扫描仅管理员可触发', () => {
expect(indexCode).toContain("app.post('/api/orders/process-timeouts', requireAdmin");
});
});

查看文件

@@ -0,0 +1,28 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第十八批回归闸门:生产环境不得使用仓库内置默认 admin 密钥
*/
describe('第十八批鉴权密钥安全闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
it('不得再以仓库内置默认密钥作为静默回落', () => {
expect(indexCode).not.toContain("process.env.ADMIN_TOKEN_SECRET || 'peiwan_admin_token_secret_v1'");
});
it('必须存在默认密钥常量与生产环境随机化闸门', () => {
expect(indexCode).toContain('const DEFAULT_ADMIN_TOKEN_SECRET =');
const start = indexCode.indexOf('const DEFAULT_ADMIN_TOKEN_SECRET =');
const block = indexCode.slice(start, start + 1200);
expect(block).toContain("process.env.NODE_ENV === 'production'");
expect(block).toContain('crypto.randomBytes');
expect(block).toContain('SECURITY');
});
it('管理员 token 校验必须使用常量时间比较', () => {
expect(indexCode).toContain('crypto.timingSafeEqual');
});
});

查看文件

@@ -0,0 +1,28 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第三十六批回归闸门:买家取消订单的状态安全
*/
describe('第三十六批买家取消订单闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('仓储层提供 buyerCancelOrder 且拒绝已履约订单', () => {
const start = repoCode.indexOf('async buyerCancelOrder(');
expect(start).toBeGreaterThan(-1);
const block = repoCode.slice(start, start + 2600);
expect(block).toContain('不可直接取消,请发起退款申诉');
expect(block).toContain("status = \"已退款\", refund_amount");
expect(block).toContain("'买家取消订单退款'");
});
it('取消路由:管理员直改状态,买家走安全取消', () => {
const start = indexCode.indexOf("app.post('/api/orders/:id/cancel'");
const block = indexCode.slice(start, start + 1200);
expect(block).toContain('role.isAdmin');
expect(block).toContain('repo.buyerCancelOrder(orderId)');
});
});

查看文件

@@ -0,0 +1,30 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第三十三批回归闸门:code2session 不泄露 session_key 且 openid 绑定需会话一致
*/
describe('第三十三批 code2session 加固闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const block = () => {
const start = indexCode.indexOf("app.post('/api/wechat/code2session'");
expect(start).toBeGreaterThan(-1);
return indexCode.slice(start, start + 2600);
};
it('不得向客户端返回 session_key', () => {
const b = block();
expect(b).not.toContain('sessionKey: session_key');
expect(b).not.toContain('const { openid, session_key, unionid }');
});
it('openid 绑定必须经服务端会话身份', () => {
const b = block();
expect(b).toContain("'绑定微信账户'");
expect(b).toContain('repo.updateUserOpenid(claimed, openid)');
expect(b).not.toContain('repo.updateUserOpenid(Number(userId), openid)');
});
});

查看文件

@@ -0,0 +1,28 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第四十八批回归闸门:大神入驻申请不得同名抢占他人档案
*/
describe('第四十八批大神入驻归属闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
const block = () => {
const start = repoCode.indexOf('async applyCompanion(');
expect(start).toBeGreaterThan(-1);
return repoCode.slice(start, start + 3000);
};
it('重新提交仅按 user_id 匹配本人档案', () => {
const b = block();
expect(b).toContain('WHERE user_id IS NOT NULL AND user_id = ?');
expect(b).not.toContain('OR name = ?`, [userId, name]');
expect(b).not.toContain('SET user_id = ?, name = ?');
});
it('昵称与他人档案冲突时拒绝提交', () => {
expect(block()).toContain('该大神昵称已被占用,请更换昵称后重新提交');
});
});

查看文件

@@ -0,0 +1,19 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第三十一批回归闸门:大神详情公开端剥离内部审核备注
*/
describe('第三十一批大神详情隐私闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
it('公开详情剥离 rejectReason,管理端保留完整', () => {
const start = indexCode.indexOf("app.get('/api/companions/:id',");
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 800);
expect(block).toContain('isVerifiedAdminRequest(req)');
expect(block).toContain('const { rejectReason, ...publicCompanion } = companion;');
});
});

查看文件

@@ -0,0 +1,20 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第三十批回归闸门:大神公开目录仅展示已通过并脱敏内部审核备注
*/
describe('第三十批大神目录隐私闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
it('公开目录仅返回已通过大神且剥离 rejectReason,管理端保留全量', () => {
const start = indexCode.indexOf("app.get('/api/companions',");
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 900);
expect(block).toContain('isVerifiedAdminRequest(req)');
expect(block).toContain("c.auditStatus === '已通过'");
expect(block).toContain('const { rejectReason, ...rest } = c;');
});
});

查看文件

@@ -0,0 +1,21 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第四十六批回归闸门:内容安全检测接口鉴权与 openid 可信
*/
describe('第四十六批内容安全接口闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
it('生产需登录且 openid 以会话用户为准', () => {
const start = indexCode.indexOf("app.post('/api/security/msg-sec-check'");
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 1200);
expect(block).toContain('IS_PRODUCTION');
expect(block).toContain("'内容安全检测'");
expect(block).toContain('await resolveUserOpenid(callerId)');
expect(block).not.toContain('const { content, openid, scene } = req.body');
});
});

查看文件

@@ -130,7 +130,7 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
const raw = fs.readFileSync(pagesJsonPath, 'utf-8');
const config = JSON.parse(raw);
expect(Array.isArray(config.pages)).toBe(true);
expect(config.pages.length).toBe(25);
expect(config.pages.length).toBe(26);
for (const pageItem of config.pages) {
const vueFilePath = path.join(rootDir, 'pwuniapp', `${pageItem.path}.vue`);

查看文件

@@ -0,0 +1,24 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第五十三批回归闸门:分销团队/返佣明细不得用硬编码假数据
*/
describe('第五十三批分销数据真实性闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('不得再返回硬编码团队成员/返佣记录', () => {
expect(repoCode).not.toContain("'晨曦微风'");
expect(repoCode).not.toContain("'峡谷追风少年'");
expect(repoCode).not.toContain("'dst_101'");
expect(repoCode).not.toContain("'星空漫步者'");
});
it('无真实数据时返回空', () => {
const start = repoCode.indexOf('async getDistributionRecords(');
const block = repoCode.slice(start, start + 900);
expect(block).toContain('return [];');
});
});

查看文件

@@ -0,0 +1,23 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第五十批回归闸门:大神收益统计不得跨大神串算
*/
describe('第五十批大神收益统计范围闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('不得再用 companion_name LIKE 匹配聚合作弊式占位名', () => {
expect(repoCode).not.toContain("'%我%'");
expect(repoCode).not.toContain('OR companion_name LIKE ?');
});
it('收益汇总与走势只按 expert_id 统计', () => {
const stats = repoCode.slice(repoCode.indexOf('async getExpertStats('), repoCode.indexOf('async getExpertStats(') + 600);
expect(stats).toContain('WHERE expert_id = ?');
const trends = repoCode.slice(repoCode.indexOf('async getExpertRevenueTrends('));
expect(trends.slice(0, 4000)).toContain('WHERE expert_id = ?');
});
});

查看文件

@@ -0,0 +1,24 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第五十八批回归闸门:帮助中心 FAQ 配置一致性
*/
describe('第五十八批帮助中心FAQ闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const mysqlCode = fs.readFileSync(path.join(rootDir, 'server/src/db/mysql.ts'), 'utf-8');
const userIndex = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/index.vue'), 'utf-8');
it('faq_list 为空时自动回填默认种子', () => {
expect(mysqlCode).toContain('自动回填默认种子');
expect(mysqlCode).toContain("UPDATE platform_configs SET value_json = ? WHERE config_key = 'faq_list'");
expect(mysqlCode).toContain('!Array.isArray(parsed) || parsed.length === 0');
});
it('小程序不再用写死的假 FAQ 兜底文案(改为独立页面展示)', () => {
expect(userIndex).not.toContain('什么是4位核销码?\\n到达活动现场');
const faqPage = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/help-faq.vue'), 'utf-8');
expect(faqPage).toContain('暂无常见问题');
});
});

查看文件

@@ -0,0 +1,43 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第十六批回归闸门:资金写接口(充值 / 订单支付)身份与归属收口
*/
describe('第十六批资金接口身份闸门 (充值 / 订单支付)', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
const routeBlock = (marker: string, len = 1400) => {
const start = indexCode.indexOf(marker);
expect(start).toBeGreaterThan(-1);
return indexCode.slice(start, start + len);
};
describe('1. 钱包充值不得为他人刷余额', () => {
it('充值身份以服务端会话为准', () => {
const block = routeBlock("app.post('/api/users/:id/recharge'");
expect(block).toContain("'充值余额'");
expect(block).not.toContain('const userId = Number(req.params.id);\n await assertUserNotBanned(userId, \'充值余额\')');
});
});
describe('2. 订单支付仅限下单买家本人', () => {
it('支付前必须做订单归属校验', () => {
const block = routeBlock("app.post('/api/orders/:id/pay'");
expect(block).toContain('repo.getOrderOwnership(orderId)');
expect(block).toContain("'确认订单支付'");
expect(block).toContain('仅下单买家本人可确认支付该订单');
});
});
describe('3. 小程序支付携带当前用户身份', () => {
it('payOrder 必须注入 userId', () => {
const start = apiCode.indexOf('export async function payOrder(');
const block = apiCode.slice(start, start + 300);
expect(block).toContain('userId: normalizeUserId(payload.userId)');
});
});
});

查看文件

@@ -0,0 +1,32 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第五十九批回归闸门:帮助中心 FAQ 改为独立页面(非弹窗)
*/
describe('第五十九批帮助中心页面化闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const faqPagePath = path.join(rootDir, 'pwuniapp/pages/user/help-faq.vue');
const pagesJson = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages.json'), 'utf-8');
const userIndex = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/index.vue'), 'utf-8');
it('新增独立 FAQ 页面并注册路由', () => {
expect(fs.existsSync(faqPagePath)).toBe(true);
const page = fs.readFileSync(faqPagePath, 'utf-8');
expect(page).toContain('getPlatformConfigs');
expect(page).toContain('faq_list');
expect(page).toContain('activeIndex');
expect(pagesJson).toContain('"path": "pages/user/help-faq"');
});
it('个人中心点击进入新页面而非弹窗', () => {
const start = userIndex.indexOf('const openFaq');
expect(start).toBeGreaterThan(-1);
const end = userIndex.indexOf('};', start);
const block = userIndex.slice(start, end + 2);
expect(block).toContain("url: '/pages/user/help-faq'");
expect(block).not.toContain('showActionSheet');
expect(block).not.toContain('showModal');
});
});

查看文件

@@ -83,14 +83,14 @@ describe('第四批安全加固闸门 (用户会话身份 / 订单支付 / 上
it('非 LIVE 沙箱入账必须受 ALLOW_SANDBOX_PAY 生产闸门约束', () => {
const start = indexCode.indexOf("app.post('/api/orders/:id/pay'");
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 1400);
const block = indexCode.slice(start, start + 3400);
expect(block).toContain('if (!ALLOW_SANDBOX_PAY)');
expect(block).toContain('禁止直接变更订单支付状态');
});
it('沙箱仅允许推进到合法已支付状态,严禁客户端任意指定终态', () => {
const start = indexCode.indexOf("app.post('/api/orders/:id/pay'");
const block = indexCode.slice(start, start + 1400);
const block = indexCode.slice(start, start + 3400);
expect(block).toContain("const allowedSandboxStatuses = ['履约中', '待接单']");
expect(block).toContain('allowedSandboxStatuses.includes(requested)');
});

查看文件

@@ -0,0 +1,31 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第二十二批回归闸门:管理端登录暴力破解防护 + 常量时间口令比较
*/
describe('第二十二批登录安全闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
it('提供常量时间比较与失败计数限流', () => {
expect(indexCode).toContain('function safeStringEqual(');
expect(indexCode).toContain('timingSafeEqual');
expect(indexCode).toContain('loginFailureMap');
expect(indexCode).toContain('LOGIN_MAX_FAILURES');
expect(indexCode).toContain('isLoginLocked');
});
it('登录路由:超限锁定 + 常量时间比较', () => {
const start = indexCode.indexOf("app.post('/api/user/login'");
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 1200);
expect(block).toContain('isLoginLocked(loginKey)');
expect(block).toContain('登录失败次数过多');
expect(block).toContain('recordLoginFailure(loginKey)');
expect(block).toContain('safeStringEqual(String(password), p)');
expect(block).not.toContain('ADMIN_PASSWORDS.includes(String(password))');
expect(block).not.toContain('username === ADMIN_USERNAME');
});
});

查看文件

@@ -0,0 +1,26 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第四十三批回归闸门:大神接单页失败不得伪装成功
*/
describe('第四十三批大神接单反馈闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const radar = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/expert/radar.vue'), 'utf-8');
const hall = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/expert/hall.vue'), 'utf-8');
it('radar 抢单:双重失败必须提示', () => {
const start = radar.indexOf('const handleTakeOrder =');
const block = radar.slice(start, start + 1600);
expect(block).toContain('抢单失败,请稍后重试');
expect(block).toContain('return;');
});
it('hall 接单:失败必须提示', () => {
const start = hall.indexOf('const handleGrabOrder =');
const block = hall.slice(start, start + 1400);
expect(block).toContain('接单失败,请稍后重试');
expect(block).toContain('return;');
});
});

查看文件

@@ -0,0 +1,24 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第四十二批回归闸门:邀请码不得静默丢失 + 举报人身份以当前用户为准
*/
describe('第四十二批邀请与举报身份闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const userIndex = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/index.vue'), 'utf-8');
const expertDetail = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/expert/detail.vue'), 'utf-8');
const orderList = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/order/list.vue'), 'utf-8');
it('邀请码绑定失败保留待绑定码以便重试', () => {
expect(userIndex).toContain('将于下次登录重试');
const start = userIndex.indexOf('PENDING_INVITER_CODE');
expect(start).toBeGreaterThan(-1);
});
it('举报不再硬编码 reporterId=1001(由当前用户身份承担)', () => {
expect(expertDetail).not.toContain('reporterId: 1001');
expect(orderList).not.toContain('reporterId: 1001');
});
});

查看文件

@@ -0,0 +1,79 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第三十八批回归闸门:小程序写操作失败不得再伪装成功
*/
describe('第三十八批小程序失败反馈闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const workspace = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/expert/workspace.vue'), 'utf-8');
const orderList = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/order/list.vue'), 'utf-8');
it('大神工作台:抢单/拒单/提交完成/接单状态切换失败必须提示', () => {
expect(workspace).toContain('抢单失败,请稍后重试');
expect(workspace).toContain('拒单失败,请稍后重试');
expect(workspace).toContain('提交失败,请稍后重试');
expect(workspace).toContain('接单状态切换失败,请稍后重试');
});
it('抢单失败不再无条件提示成功并移出订单池', () => {
const start = workspace.indexOf('const grabOrder = async');
const block = workspace.slice(start, start + 700);
expect(block).toContain('return;');
expect(block.indexOf('抢单失败')).toBeLessThan(block.indexOf("'抢单成功已接单'"));
});
it('订单列表:取消失败必须提示且不本地改成已取消', () => {
const start = orderList.indexOf('const cancelOrder =');
const block = orderList.slice(start, start + 900);
expect(block).toContain('取消失败,请稍后重试');
expect(block).toContain('loadLiveOrders()');
});
it('社区页:删除/上车申请失败必须提示', () => {
const ci = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/community/index.vue'), 'utf-8');
const cd = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/community/detail.vue'), 'utf-8');
for (const code of [ci, cd]) {
expect(code).toContain('删除失败,请重试');
expect(code).toContain('申请失败,请重试');
}
});
it('设置保存:API 层不得吞错,页面失败需回滚并提示', () => {
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
const settings = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/settings.vue'), 'utf-8');
const start = apiCode.indexOf('export async function updateUserSettings(');
const block = apiCode.slice(start, start + 700);
expect(block).toContain('throw err;');
expect(block).not.toContain('return {\n userId: uid,');
expect(settings).toContain('设置同步失败,请稍后重试');
expect(settings).toContain('privacyHidePhone.value = prev');
});
it('点赞:接口层不得吞错,页面需回滚乐观状态', () => {
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
const likeStart = apiCode.indexOf('export async function likePost(');
const likeBlock = apiCode.slice(likeStart, likeStart + 500);
expect(likeBlock).toContain('throw err;');
expect(likeBlock).not.toContain('return null;');
const ci = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/community/index.vue'), 'utf-8');
const cd = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/community/detail.vue'), 'utf-8');
expect(ci).toContain('回滚乐观更新');
expect(cd).toContain('回滚乐观更新');
});
it('订单动作:取消/验收接口层不得吞错,验收失败需提示', () => {
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
for (const fn of ['export async function cancelOrder(', 'export async function completeOrder(']) {
const block = apiCode.slice(apiCode.indexOf(fn), apiCode.indexOf(fn) + 500);
expect(block).toContain('throw err;');
expect(block).not.toContain('return null;');
}
const orderList2 = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/order/list.vue'), 'utf-8');
const start = orderList2.indexOf('const confirmComplete =');
const block = orderList2.slice(start, start + 900);
expect(block).toContain('确认验收失败,请稍后重试');
});
});

查看文件

@@ -0,0 +1,36 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第二十一批回归闸门:订单金额与优惠券服务端权威化
*/
describe('第二十一批订单金额完整性闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
const createOrderBlock = () => {
const start = repoCode.indexOf('async createOrder(');
expect(start).toBeGreaterThan(-1);
return repoCode.slice(start, start + 4200);
};
it('服务单价必须按服务端大神档案校验', () => {
const block = createOrderBlock();
expect(block).toContain('resolveCompanionRow(expertId)');
expect(block).toContain('compRate');
expect(block).toContain('hourlyRate = compRate');
});
it('订单金额由服务端计算,不再直接信任客户端 amount', () => {
const block = createOrderBlock();
expect(block).toContain('const amount = Number(Math.max(baseAmount - couponDiscount, 0.01).toFixed(2))');
expect(block).not.toContain('const amount = Number(data.amount) || hourlyRate * durationHours;');
});
it('优惠券折扣必须经服务端有效券校验', () => {
expect(repoCode).toContain('async resolveCouponDiscount(');
const block = createOrderBlock();
expect(block).toContain('resolveCouponDiscount(rawDiscount, track, baseAmount)');
});
});

查看文件

@@ -27,7 +27,7 @@ describe('第五批安全加固闸门 (订单越权结算 / 订单身份收口 /
const start = repoCode.indexOf('async completeOrderWithSettlement(');
expect(start).toBeGreaterThan(-1);
const block = repoCode.slice(start, start + 1600);
expect(block).toContain("const settleBlocked = ['待支付', '待接单', '已取消', '已退款', '已全额退款']");
expect(block).toContain("const settleBlocked = ['待支付', '待付款', '待接单', '已取消', '已退款', '已全额退款']");
expect(block).toContain('settleBlocked.includes(String(order.status))');
expect(block).toContain('不可结算');
expect(block).toContain('e.status = 409');

查看文件

@@ -0,0 +1,27 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第三十七批回归闸门:拒单/申请完成的状态闸门
*/
describe('第三十七批订单流转状态闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('仅「待接单」可拒单', () => {
const start = repoCode.indexOf('async rejectOrder(');
expect(start).toBeGreaterThan(-1);
const block = repoCode.slice(start, start + 900);
expect(block).toContain("!== '待接单'");
expect(block).toContain('仅待接单可拒单');
});
it('申请完成仅限已履约订单', () => {
const start = repoCode.indexOf('async applyCompleteOrder(');
expect(start).toBeGreaterThan(-1);
const block = repoCode.slice(start, start + 1400);
expect(block).toContain("const allowed = ['履约中', '服务中', '已到场核销', '待确认'];");
expect(block).toContain('不可申请完成');
});
});

查看文件

@@ -0,0 +1,20 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第二十九批回归闸门:抢单池只返回可认领的有效订单
*/
describe('第二十九批抢单池过滤闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('抢单池必须排除终态/已核销订单并过滤已认领订单', () => {
const start = repoCode.indexOf('async getOrdersPool(');
expect(start).toBeGreaterThan(-1);
const block = repoCode.slice(start, start + 1400);
expect(block).toContain("WHERE status NOT IN ('已完成', '已取消'");
expect(block).toContain('claimed_by IS NULL');
expect(block).toContain('LIMIT 20');
});
});

查看文件

@@ -0,0 +1,35 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第十九批回归闸门:预支付金额不可篡改(低价支付高价订单)
*/
describe('第十九批支付金额完整性闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('订单归属解析必须携带订单金额', () => {
const start = repoCode.indexOf('async getOrderOwnership(');
const block = repoCode.slice(start, start + 900);
expect(block).toContain('o.amount');
expect(block).toContain('amount: Number(r.amount)');
});
it('入账必须校验实收金额与支付意图一致', () => {
const start = repoCode.indexOf('async applyPaymentSuccess(');
const block = repoCode.slice(start, start + 1800);
expect(block).toContain('amountMismatch');
expect(block).toContain("reason: 'amount_mismatch'");
});
it('预支付:仅买家可发起,且订单金额以服务端订单为准', () => {
const start = indexCode.indexOf("app.post('/api/pay/wechat/prepay'");
const block = indexCode.slice(start, start + 2200);
expect(block).toContain("'发起微信支付'");
expect(block).toContain('repo.getOrderOwnership(String(orderId))');
expect(block).toContain('payAmount = Number(own.amount) || 0');
expect(block).not.toContain('const uid = Number(userId) || 0;');
});
});

查看文件

@@ -0,0 +1,27 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第三十五批回归闸门:手机号登录身份匹配与联系方式唯一性
*/
describe('第三十五批手机号登录身份闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('登录优先按微信 openid 精确匹配', () => {
const start = repoCode.indexOf('async findOrCreateUserByPhone(');
expect(start).toBeGreaterThan(-1);
const block = repoCode.slice(start, start + 1600);
expect(block).toContain('WHERE wechat_openid = ? LIMIT 1');
// openid 查询应出现在手机号查询之前
expect(block.indexOf('wechat_openid = ?')).toBeLessThan(block.indexOf('WHERE phone = ?'));
});
it('修改联系方式必须做唯一性校验', () => {
const start = repoCode.indexOf('async updateUserProfile(');
const block = repoCode.slice(start, start + 1500);
expect(block).toContain('该联系方式已被其他账号使用');
expect(block).toContain('WHERE phone = ? AND id <> ?');
});
});

查看文件

@@ -0,0 +1,65 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第十四批回归闸门:举报/评价/上车申请/收藏/大神收益等读接口越权与泄露收口
*/
describe('第十四批读接口越权收口闸门 (举报 / 评价 / 上车申请 / 收藏 / 收益)', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
const routeBlock = (marker: string, len = 1200) => {
const start = indexCode.indexOf(marker);
expect(start).toBeGreaterThan(-1);
return indexCode.slice(start, start + len);
};
describe('1. 举报工单列表', () => {
it('管理端全量 / 普通用户仅本人提交', () => {
const block = routeBlock("app.get('/api/reports',");
expect(block).toContain('isVerifiedAdminRequest(req)');
expect(block).toContain("'查看我的举报'");
});
});
describe('2. 全量评价列表仅管理员', () => {
it('GET /api/reviews 必须挂 requireAdmin', () => {
expect(indexCode).toContain("app.get('/api/reviews', requireAdmin");
expect(indexCode).toContain("app.get('/api/post-applies', requireAdmin");
});
});
describe('3. 动态上车申请名单归属', () => {
it('仅发布者本人或管理员可查看某动态的上车申请', () => {
const block = routeBlock("app.get('/api/posts/:id/applies'");
expect(block).toContain("'查看上车申请'");
expect(block).toContain('仅动态发布者本人可查看该动态的上车申请');
});
});
describe('4. 收藏列表/状态归属', () => {
it('必须经 resolveSecureUserId', () => {
expect(routeBlock("app.get('/api/favorites',")).toContain("'查看收藏列表'");
expect(routeBlock("app.get('/api/favorites/check'")).toContain("'查询收藏状态'");
});
});
describe('5. 大神收益数据归属', () => {
it('收益汇总与走势必须经 assertCompanionActor', () => {
const stats = routeBlock("app.get('/api/expert/stats/:id',");
const trends = routeBlock("app.get('/api/expert/stats/:id/trends'");
expect(stats).toContain('assertCompanionActor(req, own');
expect(trends).toContain('assertCompanionActor(req, own');
});
});
describe('6. 小程序端携带当前用户身份', () => {
it('getPostApplies 必须带当前用户 id', () => {
const start = apiCode.indexOf('export async function getPostApplies(');
const block = apiCode.slice(start, start + 320);
expect(block).toContain('/posts/${postId}/applies?userId=${normalizeUserId()}');
});
});
});

查看文件

@@ -0,0 +1,57 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第十二批回归闸门:读接口数据越权/泄露收口(订单全量、我的组局、分销团队)
*/
describe('第十二批读接口数据隔离闸门 (订单 / 我的组局 / 分销)', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
const routeBlock = (marker: string, len = 1000) => {
const start = indexCode.indexOf(marker);
expect(start).toBeGreaterThan(-1);
return indexCode.slice(start, start + len);
};
describe('1. 订单列表不得向任意客户端下发全量订单', () => {
it('仓储层提供按用户隔离的 getOrdersForUser', () => {
expect(repoCode).toContain('async getOrdersForUser(');
const start = repoCode.indexOf('async getOrdersForUser(');
const block = repoCode.slice(start, start + 1400);
expect(block).toContain('buyer_id = ?');
expect(block).toContain('claimed_by = ?');
expect(block).toContain('companion_name IN (SELECT name FROM companions');
});
it('订单列表:管理端全量,普通用户仅本人相关', () => {
const block = routeBlock("app.get('/api/orders',");
expect(block).toContain('isVerifiedAdminRequest(req)');
expect(block).toContain('repo.getOrdersForUser(uid)');
expect(block).toContain("'查看订单列表'");
});
});
describe('2. 我的组局 / 分销数据归属', () => {
it('my-activities 必须经 resolveSecureUserId', () => {
const block = routeBlock("app.get('/api/users/:id/my-activities'");
expect(block).toContain("'查看我的组局'");
});
it('分销团队与返佣明细必须经 resolveSecureUserId', () => {
expect(routeBlock("app.get('/api/distribution/team'")).toContain("'查看分销团队'");
expect(routeBlock("app.get('/api/distribution/records'")).toContain("'查看返佣明细'");
});
});
describe('3. 小程序端订单列表携带当前用户身份', () => {
it('getOrders 必须带上当前用户 id', () => {
const start = apiCode.indexOf('export async function getOrders(');
const block = apiCode.slice(start, start + 300);
expect(block).toContain('/orders?userId=${normalizeUserId()}');
});
});
});

查看文件

@@ -0,0 +1,35 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第二十批回归闸门:充值赠金必须服务端按阶梯权威计算
*/
describe('第二十批充值赠金权威性闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('仓储层提供服务端赠金计算', () => {
expect(repoCode).toContain('async computeRechargeBonusCents(');
const start = repoCode.indexOf('async computeRechargeBonusCents(');
const block = repoCode.slice(start, start + 700);
expect(block).toContain('FROM recharge_tiers');
expect(block).toContain('status = 1');
expect(block).toContain('ORDER BY amount DESC');
});
it('预支付不再信任客户端赠金', () => {
expect(indexCode).not.toContain('bonusCents: Math.round(Number(bonusAmount || 0) * 100)');
const start = indexCode.indexOf("app.post('/api/pay/wechat/prepay'");
const block = indexCode.slice(start, start + 5200);
expect(block).toContain('computeRechargeBonusCents(amountCents)');
});
it('沙箱直接充值也不再信任客户端赠金', () => {
expect(indexCode).not.toContain('rechargeUserBalance(userId, Number(amount), Number(bonusAmount || 0))');
const start = indexCode.indexOf("app.post('/api/users/:id/recharge'");
const block = indexCode.slice(start, start + 2400);
expect(block).toContain('computeRechargeBonusCents(Math.round(amountYuan * 100))');
});
});

查看文件

@@ -0,0 +1,39 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第二十七批回归闸门:请求层错误透传 + 核销码反查冲突优先级
*/
describe('第二十七批请求层与核销码反查闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const requestJs = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/request.js'), 'utf-8');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
describe('1. 小程序请求层', () => {
it('错误必须附带业务 code / statusCode', () => {
expect(requestJs).toContain('bizErr.code = res.data.code');
expect(requestJs).toContain('bizErr.statusCode = res.statusCode');
expect(requestJs).toContain('httpErr.statusCode = res.statusCode');
});
it('登录过期必须清理会话并提示', () => {
expect(requestJs).toContain('function handleAuthExpired(');
expect(requestJs).toContain('40100');
expect(requestJs).toContain('登录已过期');
});
});
describe('2. 核销码反查冲突优先级', () => {
it('活动核销码优先未核销、最新记录', () => {
const start = repoCode.indexOf('async queryCheckinCodeInfo(');
const block = repoCode.slice(start, start + 1000);
expect(block).toContain('ORDER BY p.checkin_status ASC, p.created_at DESC');
});
it('订单核销码排除已关闭状态并优先待核销', () => {
const start = repoCode.indexOf('async queryCheckinCodeInfo(');
const block = repoCode.slice(start, start + 2600);
expect(block).toContain("AND status NOT IN ('已取消', '已退款', '已全额退款')");
expect(block).toContain("ORDER BY FIELD(status, '履约中', '服务中', '待确认'");
});
});
});

查看文件

@@ -0,0 +1,65 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第十批回归闸门:订单评价/动态发布与删除的身份收口 + 关键路径可观测性
*/
describe('第十批身份收口与可观测性闸门 (评价 / 动态发布 / 动态删除)', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
const routeBlock = (marker: string, len = 900) => {
const start = indexCode.indexOf(marker);
expect(start).toBeGreaterThan(-1);
return indexCode.slice(start, start + len);
};
describe('1. 订单评价身份以服务端会话为准', () => {
it('评价路由必须经 resolveSecureUserId 绑定买家身份', () => {
const block = routeBlock("app.post('/api/orders/:id/review'");
expect(block).toContain('resolveSecureUserId(');
expect(block).toContain("'提交订单评价'");
expect(block).toContain('userId: reviewerId');
expect(block).not.toContain('userId: userId ? Number(userId) : undefined');
});
it('仓储层仍保留服务端归属校验(仅下单买家)', () => {
expect(repoCode).toContain('仅下单买家本人可以评价该订单');
});
});
describe('2. 动态发布身份以服务端会话为准', () => {
it('发布路由必须经 resolveSecureUserId 且写库用服务端 uid', () => {
const block = routeBlock("app.post('/api/posts',");
expect(block).toContain("'发布动态'");
expect(block).toContain('repo.createPost({ ...(req.body || {}), userId: uid })');
});
});
describe('3. 动态删除身份以服务端会话为准', () => {
it('删除路由必须经 resolveSecureUserId', () => {
const block = routeBlock("app.delete('/api/posts/:id'");
expect(block).toContain("'删除动态'");
});
});
describe('4. 关键路径不得静默吞错', () => {
it('openid 持久化/预支付解析/用户设置 IO 必须记录错误', () => {
expect(indexCode).toContain("logServerError('login.updateUserOpenid'");
expect(indexCode).toContain("logServerError('prepay.resolveOpenid'");
expect(indexCode).toContain("logServerError('userSettings.write'");
expect(repoCode).toContain("logServerError('getDashboardTodos.reports'");
});
});
describe('5. 小程序端写接口携带真实身份', () => {
it('发布动态/提交评价必须注入当前用户身份', () => {
expect(apiCode).toContain('userId: normalizeUserId(payload && payload.userId)');
expect(apiCode).toContain('userId: normalizeUserId(reviewData.userId)');
expect(apiCode).toContain('const uid = normalizeUserId(userId);');
});
});
});

查看文件

@@ -38,7 +38,7 @@ describe('第三批安全加固闸门 (动态越权删除 / 组局解散提权 /
const start = code.indexOf('export async function deletePost(');
expect(start).toBeGreaterThan(-1);
const block = code.slice(start, start + 400);
expect(block).toContain("uni.getStorageSync('userId') || 1001");
expect(block).toContain('normalizeUserId(userId)');
expect(block).not.toContain('userId = 1001');
});
});

查看文件

@@ -0,0 +1,21 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第三十二批回归闸门:用户设置文件不得位于对外静态目录
*/
describe('第三十二批用户设置文件私有化闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
it('设置文件路径必须位于私有 data 目录而非 /uploads', () => {
expect(indexCode).not.toContain("path.resolve(uploadDir, 'user_settings.json')");
expect(indexCode).toContain('const userSettingsDir =');
expect(indexCode).toContain("path.resolve(userSettingsDir, 'user_settings.json')");
});
it('/uploads 仍仅用于静态资源', () => {
expect(indexCode).toContain("app.use('/uploads', express.static(uploadDir))");
});
});

查看文件

@@ -0,0 +1,30 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第四十九批回归闸门:结算/仲裁给大神入账必须唯一命中,禁止昵称模糊重复入账
*/
describe('第四十九批结算入账唯一性闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('提供唯一解析结算收款用户的私有方法', () => {
expect(repoCode).toContain('async resolveExpertCreditUserId(');
const start = repoCode.indexOf('async resolveExpertCreditUserId(');
const block = repoCode.slice(start, start + 800);
expect(block).toContain('WHERE nickname = ? LIMIT 1');
expect(block).toContain('comp?.user_id');
});
it('不得再使用 id OR nickname 的模糊入账', () => {
expect(repoCode).not.toContain('WHERE id = ? OR nickname = ?');
});
it('完单结算与仲裁部分结算均使用唯一收款用户', () => {
const settle = repoCode.slice(repoCode.indexOf('async completeOrderWithSettlement('));
expect(settle.slice(0, 3000)).toContain('resolveExpertCreditUserId(conn, order, comp)');
const arb = repoCode.slice(repoCode.indexOf('async handleArbitration('));
expect(arb.slice(0, 7000)).toContain('resolveExpertCreditUserId(conn, order, comp)');
});
});

查看文件

@@ -0,0 +1,25 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第二十三批回归闸门:订单初始状态与结算状态闸门
*/
describe('第二十三批结算状态闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('结算闸门必须拦截未付款(待付款)订单', () => {
const start = repoCode.indexOf('async completeOrderWithSettlement(');
const block = repoCode.slice(start, start + 900);
expect(block).toContain("'待付款'");
expect(block).toContain("'待支付'");
});
it('订单初始状态必须由服务端按支付方式决定', () => {
const start = repoCode.indexOf('async createOrder(');
const block = repoCode.slice(start, start + 4200);
expect(block).toContain("const status = payType === 'BALANCE' ? '履约中' : '待付款';");
expect(block).not.toContain('const status = data.status ||');
});
});

查看文件

@@ -0,0 +1,27 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第五十四批回归闸门:启动配置自检(部署验收)
*/
describe('第五十四批启动自检闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
it('提供启动自检并输出就绪状态(不泄露密钥)', () => {
expect(indexCode).toContain('async function logStartupConfigSelfCheck(');
const start = indexCode.indexOf('async function logStartupConfigSelfCheck(');
const block = indexCode.slice(start, start + 1800);
expect(block).toContain('运行模式=');
expect(block).toContain('实盘就绪=');
expect(block).toContain('未设置 ADMIN_TOKEN_SECRET');
// 不得打印密钥值,仅打印是否设置
expect(block).not.toContain('${ADMIN_TOKEN_SECRET}');
expect(block).not.toContain('${USER_TOKEN_SECRET}');
});
it('bootstrap 中调用自检', () => {
expect(indexCode).toContain('await logStartupConfigSelfCheck();');
});
});

查看文件

@@ -0,0 +1,60 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第五十七批回归闸门:微信订阅消息(服务通知)真实下发闭环
*/
describe('第五十七批微信订阅消息闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const svc = fs.readFileSync(path.join(rootDir, 'server/src/core/notify/wechat-subscribe.service.ts'), 'utf-8');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
const settings = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/settings.vue'), 'utf-8');
it('提供微信订阅消息下发客户端', () => {
expect(svc).toContain('export function sendSubscribeMessageHttp(');
expect(svc).toContain("cgi-bin/message/subscribe/send");
expect(svc).toContain('export function buildTemplateData(');
});
it('服务端按配置真实下发并接入触发点(字段与所选公共模板对齐)', () => {
expect(indexCode).toContain('async function notifyBySubscribe(');
expect(indexCode).toContain("app.get('/api/message/subscribe-templates'");
expect(indexCode).toContain('sendSubscribeMessageHttp(accessToken');
for (const key of ['orderStatus', 'activityJoin', 'withdrawSuccess']) {
expect(indexCode).toContain(`notifyBySubscribe('${key}'`);
}
// 订单状态提醒模板字段
expect(indexCode).toContain('character_string1:');
expect(indexCode).toContain('phrase2:');
expect(indexCode).toContain('thing3:');
expect(indexCode).toContain('thing4:');
// 提现审核通知模板字段
expect(indexCode).toContain('amount2:');
expect(indexCode).toContain('phrase5:');
expect(indexCode).toContain('time7:');
expect(indexCode).toContain('amount8:');
// 活动报名成功通知模板字段
expect(indexCode).toContain('thing1:');
expect(indexCode).toContain('time2:');
expect(indexCode).toContain('time3:');
// 开始时间兜底:非标准日期回退当前时间,避免 time 字段 47003
expect(indexCode).toContain('function toCnTimeOrNow(');
expect(indexCode).toContain('time3: toCnTimeOrNow(act?.startTime)');
});
it('管理端测试推送在就绪时真实下发', () => {
const start = indexCode.indexOf("app.post('/api/admin/message/test-send'");
const block = indexCode.slice(start, start + 3400);
expect(block).toContain("mode: 'REAL'");
expect(block).toContain("mode: 'SIMULATED'");
});
it('小程序用真实模板ID 请求订阅授权', () => {
expect(apiCode).toContain('export async function getSubscribeTemplates(');
expect(apiCode).toContain('/message/subscribe-templates');
expect(settings).toContain('requestNoticeSubscribe');
expect(settings).not.toContain("'ORDER_STATUS_NOTICE_TMPL'");
});
});

查看文件

@@ -0,0 +1,20 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第四十五批回归闸门:上传接口生产环境需登录
*/
describe('第四十五批上传鉴权闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
it('生产环境上传必须要求登录', () => {
const start = indexCode.indexOf('const handleFileUpload =');
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 500);
expect(block).toContain('IS_PRODUCTION');
expect(block).toContain("resolveSecureUserId(req, 0, '上传文件')");
expect(block).toContain('请先登录后再上传');
});
});

查看文件

@@ -0,0 +1,26 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第五十一批回归闸门:管理员用户360画像按 id 精确关联
*/
describe('第五十一批用户360画像关联闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('订单按 buyer/expert/claimed/companion(user_id) 关联', () => {
const start = repoCode.indexOf('async getUserDetail(');
expect(start).toBeGreaterThan(-1);
const block = repoCode.slice(start, start + 3000);
expect(block).toContain('OR companion_name IN (SELECT name FROM companions WHERE user_id = ? OR id = ?)');
expect(block).not.toContain('WHERE client_name = ? OR companion_name = ?');
});
it('资金流水按 user_id 精确归属', () => {
const start = repoCode.indexOf('async getUserDetail(');
const block = repoCode.slice(start, start + 3200);
expect(block).toContain('WHERE user_id = ?');
expect(block).not.toContain('counterparty LIKE ? OR counterparty LIKE ?');
});
});

查看文件

@@ -0,0 +1,46 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第十三批回归闸门:用户/风控读接口的敏感字段与鉴权收口
*/
describe('第十三批用户读接口隐私闸门 (全量用户 / 360画像 / 风控日志 / 个人资产)', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const routeBlock = (marker: string, len = 1600) => {
const start = indexCode.indexOf(marker);
expect(start).toBeGreaterThan(-1);
return indexCode.slice(start, start + len);
};
describe('1. 管理端专属读接口必须鉴权', () => {
it('全量用户列表仅管理员可见', () => {
expect(indexCode).toContain("app.get('/api/users', requireAdmin");
});
it('用户360画像仅管理员可见', () => {
expect(indexCode).toContain("app.get('/api/users/:id/detail', requireAdmin");
});
it('风控日志仅管理员可见', () => {
expect(indexCode).toContain("app.get('/api/safety/audit-logs', requireAdmin");
});
});
describe('2. 用户基本信息:敏感字段仅本人/管理员可见', () => {
it('GET /api/users/:id 必须做本人/管理员判断并剥离敏感字段', () => {
const block = routeBlock("app.get('/api/users/:id',", 8000);
expect(block).toContain('isVerifiedAdminRequest(req)');
expect(block).toContain('publicUser');
expect(block).not.toContain('phone: user.phone');
expect(block).not.toContain('balance: user.balance');
});
});
describe('3. 个人资产/流水仅本人可读', () => {
it('提现记录与财务流水必须经 resolveSecureUserId', () => {
expect(routeBlock("app.get('/api/users/:id/withdrawals'")).toContain("'查看提现记录'");
expect(routeBlock("app.get('/api/users/:id/finance-records'")).toContain("'查看财务流水'");
});
});
});

查看文件

@@ -0,0 +1,45 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第十五批回归闸门:核销码反查防枚举 + 抢单池客户名脱敏
*/
describe('第十五批核销码与抢单池加固闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
const routeBlock = (marker: string, len = 1400) => {
const start = indexCode.indexOf(marker);
expect(start).toBeGreaterThan(-1);
return indexCode.slice(start, start + len);
};
describe('1. 核销码反查不得被匿名枚举', () => {
it('必须要求登录并按门票归属方校验', () => {
const block = routeBlock("app.get('/api/verify/query/:code'");
expect(block).toContain("'查询核销码'");
expect(block).toContain('IS_PRODUCTION && !isVerifiedAdminRequest(req)');
expect(block).toContain('无权查看该核销码信息');
});
});
describe('2. 抢单池客户名脱敏', () => {
it('仓储层必须对 client_name 脱敏', () => {
const start = repoCode.indexOf('async getOrdersPool(');
const block = repoCode.slice(start, start + 1400);
expect(block).toContain('maskClientName');
expect(block).not.toContain('userName: r.clientName ||');
});
});
describe('3. 小程序反查携带当前用户身份', () => {
it('queryCheckinCode 必须带 userId', () => {
const start = apiCode.indexOf('export async function queryCheckinCode(');
const block = apiCode.slice(start, start + 300);
expect(block).toContain('/verify/query/${code}?userId=${normalizeUserId()}');
});
});
});

查看文件

@@ -0,0 +1,25 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第五十二批回归闸门:个人资金流水按 user_id 精确归属
*/
describe('第五十二批资金流水归属闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
it('个人流水查询仅按 user_id 过滤,不再用 counterparty LIKE', () => {
const start = repoCode.indexOf('async getUserFinanceRecords(');
expect(start).toBeGreaterThan(-1);
const block = repoCode.slice(start, start + 700);
expect(block).toContain('WHERE user_id = ?');
expect(block).not.toContain('counterparty LIKE ?');
expect(block).not.toContain('OR related_id = ?');
});
it('关键资金流水写入时带 user_id', () => {
expect(repoCode).toContain("'钱包余额支付订单', ?, 'OUT'");
expect(repoCode).toContain("'佣金提现', ?, 'OUT'");
});
});

查看文件

@@ -0,0 +1,45 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第五十五批回归闸门:提现「真实微信打款」闭环
*/
describe('第五十五批提现打款闭环闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const payV3 = fs.readFileSync(path.join(rootDir, 'server/src/core/pay/wechat-pay-v3.ts'), 'utf-8');
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
const mysqlCode = fs.readFileSync(path.join(rootDir, 'server/src/db/mysql.ts'), 'utf-8');
it('支付客户端具备商家转账到零钱能力', () => {
expect(payV3).toContain('export async function transferToWallet(');
expect(payV3).toContain("'/v3/fund-app/mch-transfer/transfer-bills'");
expect(payV3).toContain('export async function queryTransferByOutBillNo(');
expect(payV3).toContain('export function encryptSensitiveInfo(');
});
it('提现审核通过按模式真实打款 / 生产未就绪拒绝', () => {
const start = indexCode.indexOf("app.post('/api/finance/withdrawals/:id/review'");
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 4600);
expect(block).toContain('cfg.isReadyForTransfer');
expect(block).toContain('transferToWallet(cfg');
expect(block).toContain('getWithdrawalForTransfer(wdId)');
expect(block).toContain('微信商家转账未就绪');
expect(block).not.toContain('款项已通过微信商户零钱打款'); // 移除误导文案
});
it('提供转账结果回调与最终态落库', () => {
expect(indexCode).toContain("app.post('/api/pay/wechat/transfer-notify'");
expect(repoCode).toContain('async finalizeWithdrawalTransfer(');
expect(repoCode).toContain('async getWithdrawalForTransfer(');
expect(repoCode).toContain('async markWithdrawalTransfer(');
});
it('提现表具备转账/收款人字段', () => {
expect(mysqlCode).toContain('transfer_bill_no');
expect(mysqlCode).toContain('transfer_status');
expect(mysqlCode).toContain('payee_name');
});
});