fix(business-loop): complete arbitration balance refund, withdrawal rejection compensation, admin activity takedown, and companion violation alerts

这个提交包含在:
xcbyy
2026-10-13 17:18:11 +08:00
父节点 9e2c19088b
当前提交 173835dc4e
共有 5 个文件被更改,包括 168 次插入 和 22 次删除

查看文件

@@ -309,16 +309,16 @@ export class MysqlRepository {
return this.getActivityById(activityId);
}
async cancelActivity(id: string, hostId: number, reason?: string): Promise<any> {
async cancelActivity(id: string, hostId: number, reason?: string, isAdmin?: boolean): Promise<any> {
const act = await this.getActivityById(id);
if (!act) {
throw new Error('活动不存在');
}
if (Number(act.creatorId) !== Number(hostId)) {
throw new Error('只有发起人有权解散此组局');
if (!isAdmin && Number(act.creatorId) !== Number(hostId)) {
throw new Error('只有发起人或平台管理员有权解散此组局');
}
const hasCheckedIn = act.participants.some((p: any) => p.checkinStatus);
if (hasCheckedIn) {
if (!isAdmin && hasCheckedIn) {
throw new Error('聚会已有成员到场核销,不可随意解散');
}
@@ -1585,18 +1585,62 @@ export class MysqlRepository {
}
async handleArbitration(id: string, decision: string): Promise<any> {
let statusText = '申诉已驳回';
if (decision === 'refund_full') statusText = '已全额退款用户';
if (decision === 'deduct_credit') statusText = '已扣减大神信用分';
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
await pool.query('UPDATE arbitrations SET status = ? WHERE id = ?', [statusText, id]);
if (decision === 'refund_full') {
const [arbRows]: any = await pool.query('SELECT order_id FROM arbitrations WHERE id = ?', [id]);
let statusText = '申诉已驳回';
if (decision === 'refund_full') statusText = '已全额退款用户';
if (decision === 'deduct_credit') statusText = '已扣减大神信用分';
await conn.query('UPDATE arbitrations SET status = ? WHERE id = ?', [statusText, id]);
const [arbRows]: any = await conn.query('SELECT order_id FROM arbitrations WHERE id = ? FOR UPDATE', [id]);
if (arbRows.length > 0) {
await pool.query('UPDATE orders SET status = "已全额退款" WHERE id = ?', [arbRows[0].order_id]);
const orderId = arbRows[0].order_id;
const [ordRows]: any = await conn.query('SELECT * FROM orders WHERE id = ? FOR UPDATE', [orderId]);
const order = ordRows && ordRows[0];
if (decision === 'refund_full' && order) {
// 1. 更新订单为已全额退款
await conn.query('UPDATE orders SET status = "已全额退款" WHERE id = ?', [order.id]);
// 2. 将订单金额真实退回买家钱包余额
const buyerId = Number(order.buyer_id || 1001);
const refundAmount = Number(order.amount || 0);
if (buyerId > 0 && refundAmount > 0) {
await conn.query('UPDATE users SET balance = balance + ? WHERE id = ?', [refundAmount, buyerId]);
// 3. 记录财务退款入账流水
const recId = `fin_ref_${Date.now()}`;
const recNo = `TK${Date.now()}`;
await conn.query(`
INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id, created_at)
VALUES (?, ?, ?, '仲裁退款', ?, 'IN', ?, ?, NOW())
`, [recId, recNo, buyerId, refundAmount, `平台仲裁退款 (${order.order_no})`, String(order.id)]);
}
// 4. 联动扣减违规大神评分
const expertId = Number(order.expert_id);
if (expertId > 0) {
await conn.query('UPDATE companions SET rating = GREATEST(1.0, rating - 0.2) WHERE id = ?', [expertId]);
}
} else if (decision === 'deduct_credit' && order) {
const expertId = Number(order.expert_id);
if (expertId > 0) {
await conn.query('UPDATE companions SET rating = GREATEST(1.0, rating - 0.3) WHERE id = ?', [expertId]);
}
}
}
await conn.commit();
return { id, status: statusText };
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release();
}
return { id, status: statusText };
}
// 买家发起退款/售后申诉:订单流转为争议仲裁中,同时写入仲裁工单 (含理由与凭证截图)
@@ -1755,9 +1799,53 @@ export class MysqlRepository {
}
async reviewWithdrawal(id: string, action: 'approve' | 'reject', rejectReason?: string): Promise<any> {
const statusText = action === 'approve' ? '已打款' : '已驳回';
await pool.query('UPDATE withdrawals SET status = ? WHERE id = ?', [statusText, id]);
return { id, status: statusText, reason: rejectReason };
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const [rows]: any = await conn.query('SELECT * FROM withdrawals WHERE id = ? FOR UPDATE', [id]);
if (!rows || rows.length === 0) {
throw new Error('提现工单不存在');
}
const wd = rows[0];
if (wd.status !== '待审核') {
throw new Error(`当前提现工单已处理,状态为【${wd.status}】`);
}
const statusText = action === 'approve' ? '已打款' : '已驳回';
const reason = rejectReason || (action === 'reject' ? '银行/微信账号信息不匹配或未通过财务初审' : '');
await conn.query(`
UPDATE withdrawals
SET status = ?, reason = ?
WHERE id = ?
`, [statusText, reason, id]);
// 若被驳回,将预扣的提现佣金全额原路退还至用户 commission 佣金账户
if (action === 'reject') {
const userId = Number(wd.companion_id);
const refundAmt = Number(wd.amount);
if (userId > 0 && refundAmt > 0) {
await conn.query('UPDATE users SET commission = commission + ? WHERE id = ?', [refundAmt, userId]);
// 记录一条冲正入账流水
const recId = `fin_wdr_${Date.now()}`;
const recNo = `TH${Date.now()}`;
await conn.query(`
INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id, created_at)
VALUES (?, ?, ?, '提现驳回退还', ?, 'IN', ?, ?, NOW())
`, [recId, recNo, userId, refundAmt, `财务审核驳回退还 (${reason})`, id]);
}
}
await conn.commit();
return { id, status: statusText, reason };
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release();
}
}
async getFinanceConfig(): Promise<any> {

查看文件

@@ -479,16 +479,18 @@ app.post('/api/activities/:id/adjust-price', async (req: Request, res: Response)
}
});
// 房主取消/解散组局并触发全额原路退款
// 房主或平台管理员取消/违规下架组局并触发全额原路退款
app.post('/api/activities/:id/cancel', async (req: Request, res: Response) => {
try {
const actId = String(req.params.id);
const { hostId, reason } = req.body;
const result = await repo.cancelActivity(actId, Number(hostId || 1001), reason);
const { hostId, reason, operatorRole } = req.body || {};
const headerRole = req.headers['x-user-role'];
const isAdmin = operatorRole === 'admin' || headerRole === 'admin';
const result = await repo.cancelActivity(actId, Number(hostId || 1001), reason, isAdmin);
res.json({
code: 20000,
data: result,
message: `组局已成功解散${result.refundedCount > 0 ? `,已原路全额退款 ${result.refundedCount} 位搭子` : ''}`
message: `组局已成功解散下架${result.refundedCount > 0 ? `,已原路全额退款 ${result.refundedCount} 位搭子` : ''}`
});
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });