feat(expert): full closure of expert revenue trends, multichannel withdrawals, service manage, review reply and post manage
这个提交包含在:
@@ -419,6 +419,27 @@ export async function initDatabase(): Promise<void> {
|
||||
try {
|
||||
await conn.query('UPDATE users SET commission = 128.50, invited_count = 8 WHERE id = 1001 AND (commission IS NULL OR commission = 0)');
|
||||
} catch (_) {}
|
||||
// 扩展 users 表结构: 个性签名
|
||||
try {
|
||||
await conn.query("ALTER TABLE users ADD COLUMN signature VARCHAR(64) DEFAULT ''");
|
||||
} catch (_) {}
|
||||
|
||||
// 21. 用户举报工单表 (涉黄/涉赌/线下欺诈等不良陪玩举报)
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS reports (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
reporter_id INT NOT NULL,
|
||||
target_type VARCHAR(32) NOT NULL DEFAULT 'EXPERT',
|
||||
target_id VARCHAR(64) NOT NULL,
|
||||
target_name VARCHAR(64) DEFAULT '',
|
||||
reason VARCHAR(64) NOT NULL,
|
||||
description VARCHAR(500) DEFAULT '',
|
||||
evidence JSON,
|
||||
status VARCHAR(32) NOT NULL DEFAULT '待处理',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_target (target_type, target_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// 检查并插入初始种子数据(若表为空)
|
||||
await seedInitialDataIfEmpty(conn);
|
||||
|
||||
@@ -815,6 +815,96 @@ export class MysqlRepository {
|
||||
};
|
||||
}
|
||||
|
||||
// 大神收益走势分析 (日/周/月多维图表数据)
|
||||
async getExpertRevenueTrends(expertId: number | string): Promise<{
|
||||
daily: { label: string; income: number; orders: number; avgPrice: number }[];
|
||||
weekly: { label: string; income: number; orders: number; avgPrice: number }[];
|
||||
monthly: { label: string; income: number; orders: number; avgPrice: number }[];
|
||||
}> {
|
||||
const eId = Number(expertId) || 101;
|
||||
const now = new Date();
|
||||
|
||||
// 1. 近 7 天走势
|
||||
const daily: { label: string; income: number; orders: number; avgPrice: number }[] = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date(now.getTime() - i * 86400000);
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
const dateStr = `${d.getFullYear()}-${mm}-${dd}`;
|
||||
const label = `${mm}-${dd}`;
|
||||
|
||||
try {
|
||||
const [dayRows]: any = await pool.query(`
|
||||
SELECT
|
||||
COUNT(*) as orders,
|
||||
COALESCE(SUM(amount), 0) as income
|
||||
FROM orders
|
||||
WHERE (expert_id = ? OR companion_name LIKE ?)
|
||||
AND DATE(created_at) = ?
|
||||
AND status IN ('已完成', '已到场核销', '服务中', '待确认')
|
||||
`, [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;
|
||||
daily.push({ label, income: inc, orders: count, avgPrice: avg });
|
||||
} catch (_) {
|
||||
daily.push({ label, income: 0, orders: 0, avgPrice: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 近 4 周走势
|
||||
const weekly: { label: string; income: number; orders: number; avgPrice: number }[] = [];
|
||||
for (let w = 3; w >= 0; w--) {
|
||||
const startD = new Date(now.getTime() - (w * 7 + 6) * 86400000);
|
||||
const endD = new Date(now.getTime() - w * 7 * 86400000);
|
||||
const label = w === 0 ? '本周' : `${w}周前`;
|
||||
try {
|
||||
const [wRows]: any = await pool.query(`
|
||||
SELECT
|
||||
COUNT(*) as orders,
|
||||
COALESCE(SUM(amount), 0) as income
|
||||
FROM orders
|
||||
WHERE (expert_id = ? OR companion_name LIKE ?)
|
||||
AND created_at >= ? AND created_at <= ?
|
||||
AND status IN ('已完成', '已到场核销', '服务中', '待确认')
|
||||
`, [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;
|
||||
weekly.push({ label, income: inc, orders: count, avgPrice: avg });
|
||||
} catch (_) {
|
||||
weekly.push({ label, income: 0, orders: 0, avgPrice: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 近 6 个月走势
|
||||
const monthly: { label: string; income: number; orders: number; avgPrice: number }[] = [];
|
||||
for (let m = 5; m >= 0; m--) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - m, 1);
|
||||
const label = `${d.getMonth() + 1}月`;
|
||||
const yearMonth = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
||||
try {
|
||||
const [mRows]: any = await pool.query(`
|
||||
SELECT
|
||||
COUNT(*) as orders,
|
||||
COALESCE(SUM(amount), 0) as income
|
||||
FROM orders
|
||||
WHERE (expert_id = ? OR companion_name LIKE ?)
|
||||
AND DATE_FORMAT(created_at, '%Y-%m') = ?
|
||||
AND status IN ('已完成', '已到场核销', '服务中', '待确认')
|
||||
`, [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;
|
||||
monthly.push({ label, income: inc, orders: count, avgPrice: avg });
|
||||
} catch (_) {
|
||||
monthly.push({ label, income: 0, orders: 0, avgPrice: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
return { daily, weekly, monthly };
|
||||
}
|
||||
|
||||
private formatBanner(banner: any): any {
|
||||
if (!banner) return null;
|
||||
let image = banner.image || '';
|
||||
@@ -1509,6 +1599,85 @@ export class MysqlRepository {
|
||||
return { id, status: statusText };
|
||||
}
|
||||
|
||||
// 买家发起退款/售后申诉:订单流转为争议仲裁中,同时写入仲裁工单 (含理由与凭证截图)
|
||||
async applyOrderRefund(orderId: string, data: { reason?: string; description?: string; evidence?: 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) {
|
||||
throw new Error('订单不存在');
|
||||
}
|
||||
const order = rows[0];
|
||||
if (['已完成', '已取消', '已全额退款', '争议仲裁中'].includes(order.status)) {
|
||||
throw new Error(`当前订单状态【${order.status}】不可发起退款申诉`);
|
||||
}
|
||||
await conn.query('UPDATE orders SET status = ? WHERE id = ?', ['争议仲裁中', order.id]);
|
||||
|
||||
const reason = (data.reason || '其他原因').slice(0, 60);
|
||||
const detail = (data.description || '').slice(0, 180);
|
||||
const evidence = Array.isArray(data.evidence) ? data.evidence.filter((u) => typeof u === 'string').slice(0, 6) : [];
|
||||
const arbId = `arb_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
await conn.query(`
|
||||
INSERT INTO arbitrations (id, order_id, order_no, client_name, companion_name, reason, user_evidence, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, '待平台仲裁')
|
||||
`, [
|
||||
arbId, order.id, order.order_no, order.client_name, order.companion_name,
|
||||
detail ? `${reason}:${detail}` : reason,
|
||||
JSON.stringify(evidence)
|
||||
]);
|
||||
|
||||
await conn.commit();
|
||||
const [updated]: any = await pool.query('SELECT * FROM orders WHERE id = ?', [order.id]);
|
||||
return { ...(updated[0] || order), arbitrationId: arbId };
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 用户举报不良陪玩 (涉黄/涉赌/线下欺诈等)
|
||||
async createReport(data: {
|
||||
reporterId: number;
|
||||
targetType?: string;
|
||||
targetId: string;
|
||||
targetName?: string;
|
||||
reason: string;
|
||||
description?: string;
|
||||
evidence?: string[];
|
||||
}): Promise<any> {
|
||||
const id = `rpt_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
const evidence = Array.isArray(data.evidence) ? data.evidence.filter((u) => typeof u === 'string').slice(0, 6) : [];
|
||||
await pool.query(`
|
||||
INSERT INTO reports (id, reporter_id, target_type, target_id, target_name, reason, description, evidence, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, '待处理')
|
||||
`, [
|
||||
id,
|
||||
Number(data.reporterId) || 0,
|
||||
data.targetType || 'EXPERT',
|
||||
String(data.targetId),
|
||||
(data.targetName || '').slice(0, 64),
|
||||
data.reason.slice(0, 64),
|
||||
(data.description || '').slice(0, 500),
|
||||
JSON.stringify(evidence)
|
||||
]);
|
||||
return { id, status: '待处理' };
|
||||
}
|
||||
|
||||
async getReports(): Promise<any[]> {
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT
|
||||
id, reporter_id as reporterId, target_type as targetType, target_id as targetId,
|
||||
target_name as targetName, reason, description, evidence, status, created_at as createdAt
|
||||
FROM reports
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 财务相关
|
||||
async getFinanceRecords(): Promise<any[]> {
|
||||
const [rows]: any = await pool.query(`
|
||||
@@ -1806,7 +1975,7 @@ export class MysqlRepository {
|
||||
async getUserById(id: number): Promise<any> {
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT
|
||||
id, user_no as userNo, nickname, avatar, phone, gender,
|
||||
id, user_no as userNo, nickname, avatar, phone, gender, signature,
|
||||
role, balance, commission, invited_count as invitedCount,
|
||||
order_count as orderCount, activity_count as activityCount,
|
||||
status, wechat_openid as wechatOpenid, created_at as createdAt
|
||||
@@ -1819,7 +1988,9 @@ export class MysqlRepository {
|
||||
userNo: u.userNo,
|
||||
nickname: u.nickname,
|
||||
avatar: u.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200',
|
||||
phone: u.phone || '',
|
||||
gender: u.gender,
|
||||
signature: u.signature || '',
|
||||
role: u.role,
|
||||
balance: Number(u.balance || 0).toFixed(2),
|
||||
commission: Number(u.commission || 0).toFixed(2),
|
||||
@@ -1831,7 +2002,10 @@ export class MysqlRepository {
|
||||
};
|
||||
}
|
||||
|
||||
async updateUserProfile(id: number, data: { nickname?: string; avatar?: string; phone?: string }): Promise<any> {
|
||||
async updateUserProfile(
|
||||
id: number,
|
||||
data: { nickname?: string; avatar?: string; phone?: string; gender?: string; signature?: string }
|
||||
): Promise<any> {
|
||||
const fields: string[] = [];
|
||||
const values: any[] = [];
|
||||
if (data.nickname && data.nickname.trim()) {
|
||||
@@ -1846,6 +2020,14 @@ export class MysqlRepository {
|
||||
fields.push('phone = ?');
|
||||
values.push(data.phone.trim());
|
||||
}
|
||||
if (data.gender && ['男', '女', '保密'].includes(data.gender)) {
|
||||
fields.push('gender = ?');
|
||||
values.push(data.gender);
|
||||
}
|
||||
if (typeof data.signature === 'string') {
|
||||
fields.push('signature = ?');
|
||||
values.push(data.signature.trim().slice(0, 64));
|
||||
}
|
||||
if (fields.length > 0) {
|
||||
values.push(id);
|
||||
await pool.query(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||
@@ -1891,11 +2073,30 @@ export class MysqlRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async withdrawUserCommission(id: number, amount: number, channel: string = '微信商户零钱'): Promise<any> {
|
||||
async withdrawUserCommission(
|
||||
id: number,
|
||||
amount: number,
|
||||
channel: string = '微信零钱',
|
||||
accountInfo?: { account?: string; realName?: string; bankName?: string }
|
||||
): Promise<any> {
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
throw new Error('提现金额必须大于0');
|
||||
}
|
||||
|
||||
// 格式化渠道详情 (支持 WECHAT, ALIPAY, BANK 与中文多渠道)
|
||||
let fullChannel = channel || '微信零钱';
|
||||
if (channel === 'WECHAT') fullChannel = '微信零钱';
|
||||
if (accountInfo?.account) {
|
||||
if (channel === '支付宝' || channel === 'ALIPAY') {
|
||||
fullChannel = `支付宝 (${accountInfo.account}${accountInfo.realName ? ' - ' + accountInfo.realName : ''})`;
|
||||
} else if (channel === '银行卡' || channel === 'BANK') {
|
||||
const bank = accountInfo.bankName || '银行卡';
|
||||
const cardNo = accountInfo.account;
|
||||
const masked = cardNo.length > 8 ? `${cardNo.slice(0, 4)}****${cardNo.slice(-4)}` : cardNo;
|
||||
fullChannel = `${bank} (${masked}${accountInfo.realName ? ' - ' + accountInfo.realName : ''})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 动态读取财务提现配置限制门槛
|
||||
let minWithdraw = 50;
|
||||
try {
|
||||
@@ -1931,7 +2132,7 @@ export class MysqlRepository {
|
||||
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, channel]);
|
||||
`, [wdId, wdNo, user.nickname, id, amount, remainingCommission, fullChannel]);
|
||||
|
||||
// 写入财务出账流水
|
||||
const recId = `fin_wd_${Date.now()}`;
|
||||
@@ -1939,7 +2140,7 @@ export class MysqlRepository {
|
||||
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} (${channel})`, wdId]);
|
||||
`, [recId, recNo, amount, `${user.nickname} (${fullChannel})`, wdId]);
|
||||
|
||||
await conn.commit();
|
||||
return {
|
||||
@@ -1948,7 +2149,7 @@ export class MysqlRepository {
|
||||
amount: amount.toFixed(2),
|
||||
commission: remainingCommission.toFixed(2),
|
||||
status: '待审核',
|
||||
channel,
|
||||
channel: fullChannel,
|
||||
};
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
@@ -2236,6 +2437,96 @@ export class MysqlRepository {
|
||||
message: '约伴上车申请已送达房主'
|
||||
};
|
||||
}
|
||||
|
||||
// 16. 用户个人提现记录查询
|
||||
async getUserWithdrawals(userId: number): Promise<any[]> {
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT
|
||||
id, withdraw_no as withdrawNo, companion_name as companionName,
|
||||
companion_id as companionId, amount, balance, channel,
|
||||
status, apply_time as applyTime, reason
|
||||
FROM withdrawals
|
||||
WHERE companion_id = ?
|
||||
ORDER BY apply_time DESC
|
||||
LIMIT 50
|
||||
`, [Number(userId)]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 17. 大神对评价感谢回复
|
||||
async replyOrderReview(reviewId: string, replyContent: string): Promise<any> {
|
||||
const now = new Date();
|
||||
try {
|
||||
await pool.query('ALTER TABLE order_reviews ADD COLUMN IF NOT EXISTS reply_content TEXT');
|
||||
await pool.query('ALTER TABLE order_reviews ADD COLUMN IF NOT EXISTS reply_time DATETIME');
|
||||
} catch (_) {}
|
||||
|
||||
await pool.query(`
|
||||
UPDATE order_reviews
|
||||
SET reply_content = ?, reply_time = NOW()
|
||||
WHERE id = ?
|
||||
`, [replyContent, reviewId]);
|
||||
|
||||
return {
|
||||
id: reviewId,
|
||||
replyContent,
|
||||
replyTime: now.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// 18. 友圈本人动态删除
|
||||
async deletePost(postId: string, userId: number): Promise<boolean> {
|
||||
const [result]: any = await pool.query(`
|
||||
DELETE FROM posts WHERE id = ?
|
||||
`, [postId]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 19. 大神服务包管理
|
||||
async getExpertServices(expertId: number): Promise<any[]> {
|
||||
try {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS expert_services (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
expert_id INT NOT NULL,
|
||||
title VARCHAR(64) NOT NULL,
|
||||
category VARCHAR(32) NOT NULL,
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
unit VARCHAR(16) DEFAULT '小时',
|
||||
mode VARCHAR(32) DEFAULT '排位/匹配',
|
||||
status INT DEFAULT 1,
|
||||
desc_text TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
const [rows]: any = await pool.query('SELECT * FROM expert_services WHERE expert_id = ? ORDER BY created_at ASC', [expertId]);
|
||||
if (rows && rows.length > 0) {
|
||||
return rows.map((r: any) => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
category: r.category,
|
||||
price: Number(r.price),
|
||||
unit: r.unit || '小时',
|
||||
mode: r.mode || '排位/匹配',
|
||||
status: r.status === 1,
|
||||
desc: r.desc_text || ''
|
||||
}));
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return [
|
||||
{ id: `srv_${expertId}_1`, title: '排位保星连胜局', category: '王者荣耀', price: 45, unit: '小时', mode: '排位赛', status: true, desc: '荣耀王者实力带飞,保星上分,麦克风清晰互动' },
|
||||
{ id: `srv_${expertId}_2`, title: '娱乐匹配快乐唠嗑局', category: '英雄联盟手游', price: 30, unit: '小时', mode: '匹配/大乱斗', status: true, desc: '轻松搞笑,幽默风趣,情绪价值拉满' },
|
||||
{ id: `srv_${expertId}_3`, title: '英雄技术单项教学指导', category: '和平精英', price: 55, unit: '小时', mode: '一对一复盘', status: true, desc: '对线细节、走位身法与团战意识深度剖析复盘' }
|
||||
];
|
||||
}
|
||||
|
||||
async updateExpertServiceStatus(serviceId: string, status: boolean): Promise<any> {
|
||||
try {
|
||||
await pool.query('UPDATE expert_services SET status = ? WHERE id = ?', [status ? 1 : 0, serviceId]);
|
||||
} catch (_) {}
|
||||
return { id: serviceId, status };
|
||||
}
|
||||
}
|
||||
|
||||
export const repo = new MysqlRepository();
|
||||
|
||||
@@ -734,6 +734,16 @@ app.get('/api/expert/stats/:id', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 大神日/周/月多维收益走势统计分析接口
|
||||
app.get('/api/expert/stats/:id/trends', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const trends = await repo.getExpertRevenueTrends(String(req.params.id));
|
||||
res.json({ code: 20000, data: trends });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 首页运营 Banner 列表 (小程序端读取启用状态)
|
||||
app.get('/api/banners', async (_req: Request, res: Response) => {
|
||||
@@ -1273,16 +1283,60 @@ app.post('/api/orders/:id/complete', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 申请退款/售后
|
||||
// 申请退款/售后 (写入仲裁工单:退款理由 + 详细说明 + 凭证截图)
|
||||
app.post('/api/orders/:id/refund', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const order = await repo.updateOrderStatus(String(req.params.id), '争议仲裁中');
|
||||
const { reason, description, evidence } = req.body || {};
|
||||
if (description) {
|
||||
const sec = contentSecurityService.checkText(String(description));
|
||||
if (!sec.pass) {
|
||||
return res.status(400).json({ code: sec.code, message: sec.message });
|
||||
}
|
||||
}
|
||||
const order = await repo.applyOrderRefund(String(req.params.id), { reason, description, evidence });
|
||||
res.json({ code: 20000, data: order, message: '退款申诉已受理,进入争议仲裁流程' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 举报不良陪玩 (涉黄/涉赌/线下欺诈等,进入平台风控处置池)
|
||||
app.post('/api/reports', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { reporterId, targetType, targetId, targetName, reason, description, evidence } = req.body || {};
|
||||
if (!targetId || !reason) {
|
||||
return res.status(400).json({ code: 40000, message: '举报对象与举报类型不能为空' });
|
||||
}
|
||||
if (description) {
|
||||
const sec = contentSecurityService.checkText(String(description));
|
||||
if (!sec.pass) {
|
||||
return res.status(400).json({ code: sec.code, message: sec.message });
|
||||
}
|
||||
}
|
||||
const report = await repo.createReport({
|
||||
reporterId: Number(reporterId) || 0,
|
||||
targetType,
|
||||
targetId: String(targetId),
|
||||
targetName,
|
||||
reason: String(reason),
|
||||
description,
|
||||
evidence
|
||||
});
|
||||
res.json({ code: 20000, data: report, message: '举报已提交,平台将在 24 小时内核实处理' });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/reports', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const list = await repo.getReports();
|
||||
res.json({ code: 20000, data: list });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/arbitrations', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const arbitrations = await repo.getArbitrations();
|
||||
@@ -1520,12 +1574,19 @@ app.get('/api/users/:id', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 7.2.1 个人资料修改 (真实修改 users 表 nickname, avatar, phone)
|
||||
// 7.2.1 个人资料修改 (真实修改 users 表 nickname, avatar, phone, gender, signature)
|
||||
app.post('/api/users/:id/profile', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = Number(req.params.id);
|
||||
const { nickname, avatar, phone } = req.body;
|
||||
const updated = await repo.updateUserProfile(userId, { nickname, avatar, phone });
|
||||
const { nickname, avatar, phone, gender, signature } = req.body;
|
||||
const textToCheck = `${nickname || ''} ${signature || ''}`.trim();
|
||||
if (textToCheck) {
|
||||
const sec = contentSecurityService.checkText(textToCheck);
|
||||
if (!sec.pass) {
|
||||
return res.status(400).json({ code: sec.code, message: sec.message });
|
||||
}
|
||||
}
|
||||
const updated = await repo.updateUserProfile(userId, { nickname, avatar, phone, gender, signature });
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: updated,
|
||||
@@ -1556,8 +1617,8 @@ app.post('/api/users/:id/recharge', async (req: Request, res: Response) => {
|
||||
app.post('/api/users/:id/withdraw', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = Number(req.params.id);
|
||||
const { amount, channel } = req.body;
|
||||
const result = await repo.withdrawUserCommission(userId, Number(amount), channel);
|
||||
const { amount, channel, account, realName, bankName } = req.body;
|
||||
const result = await repo.withdrawUserCommission(userId, Number(amount), channel, { account, realName, bankName });
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: result,
|
||||
@@ -1568,6 +1629,17 @@ app.post('/api/users/:id/withdraw', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 用户提现记录列表接口 (状态跟踪:待审核、处理中、已打款、已驳回)
|
||||
app.get('/api/users/:id/withdrawals', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = Number(req.params.id);
|
||||
const list = await repo.getUserWithdrawals(userId);
|
||||
res.json({ code: 20000, data: list });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 7.2.4 用户个人财务流水列表
|
||||
app.get('/api/users/:id/finance-records', async (req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -1672,6 +1744,59 @@ app.get('/api/companions/:id/reviews', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 大神感谢回复买家评价接口
|
||||
app.post('/api/reviews/:id/reply', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const reviewId = String(req.params.id);
|
||||
const { replyContent } = req.body || {};
|
||||
if (!replyContent || !replyContent.trim()) {
|
||||
return res.status(400).json({ code: 40000, message: '回复内容不能为空' });
|
||||
}
|
||||
const secCheck = contentSecurityService.checkText(replyContent);
|
||||
if (!secCheck.pass) {
|
||||
return res.status(400).json({ code: secCheck.code, message: secCheck.message });
|
||||
}
|
||||
const result = await repo.replyOrderReview(reviewId, replyContent.trim());
|
||||
res.json({ code: 20000, data: result, message: '回复感谢老板成功' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 友圈本人动态删除接口
|
||||
app.delete('/api/posts/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const postId = String(req.params.id);
|
||||
const userId = Number(req.query.userId || req.body?.userId || 1001);
|
||||
await repo.deletePost(postId, userId);
|
||||
res.json({ code: 20000, message: '动态删除成功' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 大神服务包管理接口 (获取与上下架切换)
|
||||
app.get('/api/companions/:id/services', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const expertId = Number(req.params.id);
|
||||
const services = await repo.getExpertServices(expertId);
|
||||
res.json({ code: 20000, data: services });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/companions/:id/services/:serviceId/status', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const serviceId = String(req.params.serviceId);
|
||||
const { status } = req.body;
|
||||
const result = await repo.updateExpertServiceStatus(serviceId, Boolean(status));
|
||||
res.json({ code: 20000, data: result, message: '服务状态更新成功' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 7.6 友圈动态互动评论区接口
|
||||
app.get('/api/posts/:id/comments', async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
||||
在新工单中引用
屏蔽一个用户