fix(report): optimize wechat toast length limit and add admin violation reports management center

这个提交包含在:
xcbyy
2026-10-13 17:08:25 +08:00
父节点 56e672219e
当前提交 9e2c19088b
共有 10 个文件被更改,包括 455 次插入 和 4 次删除

查看文件

@@ -469,11 +469,24 @@ export async function initDatabase(): Promise<void> {
description VARCHAR(500) DEFAULT '',
evidence JSON,
status VARCHAR(32) NOT NULL DEFAULT '待处理',
handle_result VARCHAR(64) DEFAULT '',
handle_note VARCHAR(500) DEFAULT '',
handled_at DATETIME DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_target (target_type, target_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
try {
await conn.query('ALTER TABLE reports ADD COLUMN handle_result VARCHAR(64) DEFAULT ""');
} catch (_) {}
try {
await conn.query('ALTER TABLE reports ADD COLUMN handle_note VARCHAR(500) DEFAULT ""');
} catch (_) {}
try {
await conn.query('ALTER TABLE reports ADD COLUMN handled_at DATETIME DEFAULT NULL');
} catch (_) {}
// 22. 陪玩收藏表 (收藏喜欢的陪玩,实时高亮在接单状态)
await conn.query(`
CREATE TABLE IF NOT EXISTS favorites (

查看文件

@@ -1669,8 +1669,19 @@ export class MysqlRepository {
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
id,
reporter_id as reporterId,
target_type as targetType,
target_id as targetId,
target_name as targetName,
reason,
description,
evidence,
status,
COALESCE(handle_result, '') as handleResult,
COALESCE(handle_note, '') as handleNote,
handled_at as handledAt,
created_at as createdAt
FROM reports
ORDER BY created_at DESC
LIMIT 200
@@ -1678,6 +1689,46 @@ export class MysqlRepository {
return rows;
}
async handleReport(
id: string,
action: 'punish' | 'reject',
handleNote?: string,
penaltyOptions?: { deductCredit?: number; banDays?: number }
): Promise<any> {
const status = action === 'punish' ? '已核实处罚' : '已驳回';
const handleResult = action === 'punish' ? '违规属实,已给予处分' : '证据不足,予以驳回';
const note = handleNote || (action === 'punish' ? '经平台安全与风控团队核查,违规行为属实,已给予处分并记录违规档案。' : '经安全团队核验,现有证据不足以判定违规,予以驳回。');
await pool.query(`
UPDATE reports
SET status = ?, handle_result = ?, handle_note = ?, handled_at = NOW()
WHERE id = ?
`, [status, handleResult, note, id]);
try {
const [rows]: any = await pool.query('SELECT * FROM reports WHERE id = ?', [id]);
const report = rows && rows[0];
if (report && action === 'punish' && report.target_type === 'EXPERT') {
const expertId = Number(report.target_id);
if (expertId > 0) {
await pool.query(`
UPDATE companions
SET rating = GREATEST(1.0, rating - 0.2)
WHERE id = ?
`, [expertId]);
}
}
} catch (_) {}
return {
id,
status,
handleResult,
handleNote: note,
handledAt: new Date().toISOString()
};
}
// 财务相关
async getFinanceRecords(): Promise<any[]> {
const [rows]: any = await pool.query(`

查看文件

@@ -1337,6 +1337,25 @@ app.get('/api/reports', async (_req: Request, res: Response) => {
}
});
// 管理员处置不良违规举报工单
app.post('/api/reports/:id/handle', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { action, handleNote, penaltyOptions } = req.body || {};
if (!action || !['punish', 'reject'].includes(action)) {
return res.status(400).json({ code: 40000, message: '请指定合法的处理动作 (punish 或 reject)' });
}
const result = await repo.handleReport(id, action, handleNote, penaltyOptions);
res.json({
code: 20000,
data: result,
message: action === 'punish' ? '已核实举报并执行违规处置' : '已驳回该违规举报工单'
});
} 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();

查看文件

@@ -35,6 +35,47 @@ describe('ClientFeatures (TDD 测试套件: 用户端功能闭环)', () => {
expect(checkResult.code).toBe(87014);
expect(checkResult.suggest).toBe('block');
});
it('后端 repository 必须实现 handleReport 处置流转并支持处分与驳回', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('handleReport');
expect(content).toContain('handle_result');
expect(content).toContain('handle_note');
expect(content).toContain('handled_at');
});
it('后端 index.ts 必须暴露 POST /api/reports/:id/handle 路由', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.post('/api/reports/:id/handle'");
expect(content).toContain('repo.handleReport');
});
it('管理后台必须挂载 SafetyReports 路由且存在对应处理页面视图', () => {
const routePath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'router', 'routes', 'modules', 'safety.ts');
const routeContent = fs.readFileSync(routePath, 'utf-8');
expect(routeContent).toContain("path: 'reports'");
expect(routeContent).toContain("name: 'SafetyReports'");
const viewPath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'safety', 'reports', 'index.vue');
expect(fs.existsSync(viewPath)).toBe(true);
const viewContent = fs.readFileSync(viewPath, 'utf-8');
expect(viewContent).toContain('/api/reports');
expect(viewContent).toContain('handleAction');
});
it('小程序端 Toast 提示必须在 7 汉字以内以防被微信小程序方块图标截断', () => {
const orderListPath = path.join(uniPagesDir, 'order', 'list.vue');
const orderContent = fs.readFileSync(orderListPath, 'utf-8');
expect(orderContent).toContain("title: '举报提交成功'");
const detailPath = path.join(uniPagesDir, 'expert', 'detail.vue');
const detailContent = fs.readFileSync(detailPath, 'utf-8');
expect(detailContent).toContain("title: '举报提交成功'");
});
});
describe('2. 订单待支付倒计时与退款仲裁工单闭环验证', () => {