From 9e2c19088bacc2b8572b5083ae04ef6d4ad99370 Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Tue, 13 Oct 2026 17:08:25 +0800 Subject: [PATCH] fix(report): optimize wechat toast length limit and add admin violation reports management center --- .../arco-design-pro-vite/src/locale/en-US.ts | 1 + .../arco-design-pro-vite/src/locale/zh-CN.ts | 1 + .../src/router/routes/modules/safety.ts | 10 + .../src/views/safety/reports/index.vue | 299 ++++++++++++++++++ pwuniapp/pages/expert/detail.vue | 10 +- pwuniapp/pages/order/list.vue | 10 +- server/src/db/mysql.ts | 13 + server/src/db/repository.ts | 55 +++- server/src/index.ts | 19 ++ server/tests/client-features.spec.ts | 41 +++ 10 files changed, 455 insertions(+), 4 deletions(-) create mode 100644 admin/arco-design-pro-vite/src/views/safety/reports/index.vue diff --git a/admin/arco-design-pro-vite/src/locale/en-US.ts b/admin/arco-design-pro-vite/src/locale/en-US.ts index a74f933..e3a4b96 100644 --- a/admin/arco-design-pro-vite/src/locale/en-US.ts +++ b/admin/arco-design-pro-vite/src/locale/en-US.ts @@ -47,6 +47,7 @@ export default { 'menu.finance.coupons': 'Coupons Management', 'menu.safety': 'Safety & Risk Control', 'menu.safety.places': 'Approved Venues', + 'menu.safety.reports': 'Violation Reports', 'menu.safety.auditLog': 'Risk Intercept Log', 'menu.safety.platformConfig': 'Platform & Support Config', 'menu.banner': 'Banner & Operations', diff --git a/admin/arco-design-pro-vite/src/locale/zh-CN.ts b/admin/arco-design-pro-vite/src/locale/zh-CN.ts index c9965b2..890ef20 100644 --- a/admin/arco-design-pro-vite/src/locale/zh-CN.ts +++ b/admin/arco-design-pro-vite/src/locale/zh-CN.ts @@ -47,6 +47,7 @@ export default { 'menu.finance.coupons': '平台优惠券管理', 'menu.safety': '履约安全与风控', 'menu.safety.places': '线下商圈场所库', + 'menu.safety.reports': '违规举报工单处置', 'menu.safety.auditLog': '场所与内容拦截日志', 'menu.safety.platformConfig': '平台配置与协议规范 (用户协议/隐私/客服)', 'menu.banner': '首页轮播与运营', diff --git a/admin/arco-design-pro-vite/src/router/routes/modules/safety.ts b/admin/arco-design-pro-vite/src/router/routes/modules/safety.ts index de51c52..928c12c 100644 --- a/admin/arco-design-pro-vite/src/router/routes/modules/safety.ts +++ b/admin/arco-design-pro-vite/src/router/routes/modules/safety.ts @@ -22,6 +22,16 @@ const SAFETY: AppRouteRecordRaw = { roles: ['*'], }, }, + { + path: 'reports', + name: 'SafetyReports', + component: () => import('@/views/safety/reports/index.vue'), + meta: { + locale: 'menu.safety.reports', + requiresAuth: true, + roles: ['*'], + }, + }, { path: 'audit-log', name: 'SafetyAuditLog', diff --git a/admin/arco-design-pro-vite/src/views/safety/reports/index.vue b/admin/arco-design-pro-vite/src/views/safety/reports/index.vue new file mode 100644 index 0000000..c071add --- /dev/null +++ b/admin/arco-design-pro-vite/src/views/safety/reports/index.vue @@ -0,0 +1,299 @@ + + + + + diff --git a/pwuniapp/pages/expert/detail.vue b/pwuniapp/pages/expert/detail.vue index 3e29f95..25e3a0e 100644 --- a/pwuniapp/pages/expert/detail.vue +++ b/pwuniapp/pages/expert/detail.vue @@ -492,7 +492,15 @@ const submitReportTicket = async () => { }); uni.hideLoading(); closeReportModal(); - uni.showToast({ title: '举报已受理,平台将在24小时内核实处置', icon: 'success' }); + uni.showToast({ title: '举报提交成功', icon: 'success' }); + setTimeout(() => { + uni.showModal({ + title: '举报受理通知', + content: '感谢您的监督!平台安全团队将在 24 小时内核查聊天记录与履约凭据,违规属实将从严处罚。', + showCancel: false, + confirmText: '我知道了' + }); + }, 400); } catch (err) { uni.hideLoading(); uni.showToast({ title: err.message || '举报提交失败', icon: 'none' }); diff --git a/pwuniapp/pages/order/list.vue b/pwuniapp/pages/order/list.vue index 8e78a9b..ccb0cd9 100644 --- a/pwuniapp/pages/order/list.vue +++ b/pwuniapp/pages/order/list.vue @@ -731,7 +731,15 @@ const submitReportTicket = async () => { }); uni.hideLoading(); closeReportModal(); - uni.showToast({ title: '举报已提交,平台将在24小时内核实处置', icon: 'success' }); + uni.showToast({ title: '举报提交成功', icon: 'success' }); + setTimeout(() => { + uni.showModal({ + title: '举报受理通知', + content: '感谢您的监督!平台安全团队将在 24 小时内核查聊天记录与履约凭据,违规属实将从严处罚。', + showCancel: false, + confirmText: '我知道了' + }); + }, 400); } catch (err) { uni.hideLoading(); uni.showToast({ title: err.message || '举报提交失败', icon: 'none' }); diff --git a/server/src/db/mysql.ts b/server/src/db/mysql.ts index 2f0a37e..61a6c69 100644 --- a/server/src/db/mysql.ts +++ b/server/src/db/mysql.ts @@ -469,11 +469,24 @@ export async function initDatabase(): Promise { 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 ( diff --git a/server/src/db/repository.ts b/server/src/db/repository.ts index 7f5561f..dae2364 100644 --- a/server/src/db/repository.ts +++ b/server/src/db/repository.ts @@ -1669,8 +1669,19 @@ export class MysqlRepository { async getReports(): Promise { 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 { + 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 { const [rows]: any = await pool.query(` diff --git a/server/src/index.ts b/server/src/index.ts index 2b81de5..fb5750a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -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(); diff --git a/server/tests/client-features.spec.ts b/server/tests/client-features.spec.ts index 5f1510f..3564e70 100644 --- a/server/tests/client-features.spec.ts +++ b/server/tests/client-features.spec.ts @@ -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. 订单待支付倒计时与退款仲裁工单闭环验证', () => {