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 e3fe327..a74f933 100644 --- a/admin/arco-design-pro-vite/src/locale/en-US.ts +++ b/admin/arco-design-pro-vite/src/locale/en-US.ts @@ -52,6 +52,8 @@ export default { 'menu.banner': 'Banner & Operations', 'menu.banner.manage': 'Banner Management', 'menu.banner.navCategory': 'Category Navigation', + 'menu.community': 'Community & Content', + 'menu.community.postManage': 'Post & Comment Audit', 'menu.server.dashboard': 'Dashboard-Server', 'menu.server.workplace': 'Workplace-Server', 'menu.server.monitor': 'Monitor-Server', 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 a1511a3..c9965b2 100644 --- a/admin/arco-design-pro-vite/src/locale/zh-CN.ts +++ b/admin/arco-design-pro-vite/src/locale/zh-CN.ts @@ -52,6 +52,8 @@ export default { 'menu.banner': '首页轮播与运营', 'menu.banner.manage': '轮播图管理', 'menu.banner.navCategory': '金刚区导航品类', + 'menu.community': '友圈与内容管理', + 'menu.community.postManage': '动态与评论监管', 'menu.server.dashboard': '仪表盘-服务端', 'menu.server.workplace': '工作台-服务端', 'menu.server.monitor': '实时监控-服务端', diff --git a/admin/arco-design-pro-vite/src/router/routes/modules/community.ts b/admin/arco-design-pro-vite/src/router/routes/modules/community.ts new file mode 100644 index 0000000..8f2dd08 --- /dev/null +++ b/admin/arco-design-pro-vite/src/router/routes/modules/community.ts @@ -0,0 +1,28 @@ +import { DEFAULT_LAYOUT } from '../base'; +import { AppRouteRecordRaw } from '../types'; + +const COMMUNITY: AppRouteRecordRaw = { + path: '/community', + name: 'community', + component: DEFAULT_LAYOUT, + meta: { + locale: 'menu.community', + requiresAuth: true, + icon: 'icon-message', + order: 7, + }, + children: [ + { + path: 'post-manage', + name: 'PostManage', + component: () => import('@/views/community/post-manage/index.vue'), + meta: { + locale: 'menu.community.postManage', + requiresAuth: true, + roles: ['*'], + }, + }, + ], +}; + +export default COMMUNITY; diff --git a/admin/arco-design-pro-vite/src/views/community/post-manage/index.vue b/admin/arco-design-pro-vite/src/views/community/post-manage/index.vue new file mode 100644 index 0000000..3687bb5 --- /dev/null +++ b/admin/arco-design-pro-vite/src/views/community/post-manage/index.vue @@ -0,0 +1,389 @@ + + + + 内容与社区管理 + 友圈动态管理 + + + + + + + + 刷新数据 + + + + + + + + + + + + 全部分类 + 同城搭子 + 开黑电竞 + 美食探店 + K歌欢唱 + + + + + 全部状态 + 正常显示 + 违规隐藏 + + + + + + + + + + + + + + + {{ record.author }} + UID: {{ record.userId }} + + + + + + + + + {{ record.tag || '同城搭子' }} + + + + #{{ tp }} + + + + + + + + {{ record.content }} + + + {{ record.location }} + + + + + + + + + + + + 纯文本无配图 + + + + + + + 点赞: {{ record.likes || 0 }} + 评论: {{ record.comments || 0 }} + + + 浏览量: {{ record.views || 0 }} + + + + + + + + + + 官方置顶 + + + + + + + + + + + + + + {{ record.isTop ? '取消置顶' : '置顶' }} + + + + + {{ record.status === '正常' ? '隐藏' : '显示' }} + + + + + 评论 + + + + + + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + {{ cmt.content }} + + + 删除评论 + + + + + + + 该动态暂无任何评论互动 + + + + + + + + diff --git a/admin/arco-design-pro-vite/src/views/finance/records/index.vue b/admin/arco-design-pro-vite/src/views/finance/records/index.vue index c28b09b..ff76773 100644 --- a/admin/arco-design-pro-vite/src/views/finance/records/index.vue +++ b/admin/arco-design-pro-vite/src/views/finance/records/index.vue @@ -8,6 +8,10 @@ + + + 导出对账明细 (CSV) + 刷新流水 @@ -95,6 +99,38 @@ return 'arcoblue'; }; + const exportCSV = () => { + if (!rawList.value || rawList.value.length === 0) { + Message.warning('暂无流水数据可导出'); + return; + } + const headers = ['流水号', '交易类型', '金额(元)', '资金方向', '交易对手方', '关联业务ID', '记账时间']; + const rows = rawList.value.map(item => [ + item.recordNo || '', + item.type || '', + Number(item.amount || 0).toFixed(2), + item.direction === 'IN' ? '收入' : '支出', + item.counterparty || '', + item.relatedId || '', + item.createdAt || '' + ]); + + const csvContent = '\uFEFF' + [headers, ...rows] + .map(e => e.map(val => `"${String(val).replace(/"/g, '""')}"`).join(',')) + .join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', `平台交易资金对账流水_${new Date().toISOString().slice(0, 10)}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + Message.success('财务对账流水已成功导出'); + }; + onMounted(() => { fetchData(); }); diff --git a/admin/arco-design-pro-vite/src/views/finance/withdraw/index.vue b/admin/arco-design-pro-vite/src/views/finance/withdraw/index.vue index 2468319..ea23d83 100644 --- a/admin/arco-design-pro-vite/src/views/finance/withdraw/index.vue +++ b/admin/arco-design-pro-vite/src/views/finance/withdraw/index.vue @@ -8,6 +8,10 @@ + + + 导出提现明细 (CSV) + 刷新提现列表 @@ -164,6 +168,40 @@ } }; + const exportCSV = () => { + if (!rawList.value || rawList.value.length === 0) { + Message.warning('暂无提现审核记录可导出'); + return; + } + const headers = ['提现单号', '申请大神', '大神ID', '提现金额(元)', '账户余额(元)', '结算渠道', '申请时间', '审核状态', '驳回说明']; + const rows = rawList.value.map(item => [ + item.withdrawNo || '', + item.companionName || '', + item.companionId || '', + Number(item.amount || 0).toFixed(2), + Number(item.balance || 0).toFixed(2), + item.channel || '', + item.applyTime || '', + item.status || '', + item.rejectReason || item.reason || '' + ]); + + const csvContent = '\uFEFF' + [headers, ...rows] + .map(e => e.map(val => `"${String(val).replace(/"/g, '""')}"`).join(',')) + .join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', `大神收益提现审核对账表_${new Date().toISOString().slice(0, 10)}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + Message.success('提现审核对账表已成功导出'); + }; + onMounted(() => { fetchData(); }); diff --git a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue index e784e39..e13acc9 100644 --- a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue +++ b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue @@ -594,6 +594,117 @@ + + + + + + 在此维护微信小程序官方订阅消息(模板消息)的 Template ID 与推送总开关。用于在订单履约状态变更、组局活动到场、佣金提现到账及现场出示4位核销码时,向微信用户推送官方服务通知。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 保存微信模板消息配置 + + + + + + + + + + + + + 订单履约状态变动通知 + 组局搭子成局/报名通知 + 提现零钱打款到账通知 + 现场 4 位核销码签到提醒 + + + + + + + + + + + + + + + + + + + + + + + + 发送模拟微信服务通知 + + + + + + + 推送时间: {{ testResult.sendTime }} | 目标UID: {{ testResult.targetUserId }} | 状态: {{ testResult.status }} + + + + + + @@ -891,6 +1002,56 @@ const saveActivityRulesConfig = () => { saveConfig('activity_rules', activityRulesForm); }; +// Tab 11 微信服务通知与模板消息 +const messageTemplateForm = reactive({ + name: '微信服务通知与模板消息配置', + description: '微信订阅消息通知与状态模板配置', + status: 1, + valueJson: { + orderStatusTemplateId: 'tmpl_order_status_update_01', + activityJoinTemplateId: 'tmpl_activity_joined_02', + withdrawSuccessTemplateId: 'tmpl_withdraw_success_03', + verifyCodeTemplateId: 'tmpl_verify_code_notice_04', + enablePush: 1, + remark: '微信小程序订阅消息与服务通知模版ID配置' + } +}); + +const testSending = ref(false); +const testResult = ref(null); +const testMsgForm = reactive({ + templateType: 'order_status', + targetUserId: 1001, + previewTitle: '订单履约开始提醒', + previewDetail: '您的同城生活陪伴订单已开始服务,请出示4位核销码完成现场确认。' +}); + +const saveMessageTemplateConfig = () => { + saveConfig('message_template_rules', messageTemplateForm); +}; + +const sendTestMessage = async () => { + testSending.value = true; + try { + const payload = { + templateType: testMsgForm.templateType, + targetUserId: testMsgForm.targetUserId, + previewData: { + title: testMsgForm.previewTitle, + detail: testMsgForm.previewDetail + } + }; + const res: any = await axios.post('/api/admin/message/test-send', payload); + const data = res?.data?.data || res?.data || res; + testResult.value = data; + Message.success('测试服务通知推送成功'); + } catch (err: any) { + Message.error(err.response?.data?.message || err.message || '测试推送异常'); + } finally { + testSending.value = false; + } +}; + const fetchConfigs = async () => { loading.value = true; try { @@ -959,6 +1120,11 @@ const fetchConfigs = async () => { if (Array.isArray(activityRulesForm.valueJson.presetTotalFees)) { presetTotalFeesText.value = activityRulesForm.valueJson.presetTotalFees.join(', '); } + } else if (item.configKey === 'message_template_rules') { + messageTemplateForm.name = item.name; + messageTemplateForm.description = item.description; + messageTemplateForm.status = item.status; + messageTemplateForm.valueJson = { ...messageTemplateForm.valueJson, ...(item.valueJson || {}) }; } }); } diff --git a/server/src/db/mysql.ts b/server/src/db/mysql.ts index 26e26c8..f7098d4 100644 --- a/server/src/db/mysql.ts +++ b/server/src/db/mysql.ts @@ -436,6 +436,14 @@ export async function initDatabase(): Promise { `); } catch (_) {} + // 扩展 posts 表结构: is_top (置顶), status (正常/隐藏) + try { + await conn.query('ALTER TABLE posts ADD COLUMN is_top TINYINT DEFAULT 0'); + } catch (_) {} + try { + await conn.query("ALTER TABLE posts ADD COLUMN status VARCHAR(32) DEFAULT '正常'"); + } catch (_) {} + // 21. 用户举报工单表 (涉黄/涉赌/线下欺诈等不良陪玩举报) await conn.query(` CREATE TABLE IF NOT EXISTS reports ( @@ -814,6 +822,15 @@ async function seedInitialDataIfEmpty(conn: any): Promise { defaultAaFee: 50 }); + const messageTemplateRulesVal = JSON.stringify({ + orderStatusTemplateId: 'tmpl_order_status_update_01', + activityJoinTemplateId: 'tmpl_activity_joined_02', + withdrawSuccessTemplateId: 'tmpl_withdraw_success_03', + verifyCodeTemplateId: 'tmpl_verify_code_notice_04', + enablePush: 1, + remark: '微信小程序订阅消息与服务通知模版ID配置' + }); + const [cfgItemRows]: any = await conn.query('SELECT COUNT(*) as count FROM platform_configs'); if (cfgItemRows[0].count === 0) { const customerServiceVal = JSON.stringify({ @@ -916,6 +933,12 @@ async function seedInitialDataIfEmpty(conn: any): Promise { VALUES ('activity_rules', '同城组局发单与预算预设规范', ?, '招募席位上下限、快捷单人赏金预设与包场预算梯队', 1) `, [activityRulesVal]); } + if (!existingKeys.has('message_template_rules')) { + await conn.query(` + INSERT INTO platform_configs (config_key, name, value_json, description, status) + VALUES ('message_template_rules', '微信服务通知与模板消息配置', ?, '微信订阅消息通知与状态模板配置', 1) + `, [messageTemplateRulesVal]); + } if (existingKeys.has('platform_protocol')) { const [protoRows]: any = await conn.query("SELECT value_json FROM platform_configs WHERE config_key = 'platform_protocol'"); if (protoRows && protoRows.length > 0) { diff --git a/server/src/db/repository.ts b/server/src/db/repository.ts index 009a6c5..c9ef239 100644 --- a/server/src/db/repository.ts +++ b/server/src/db/repository.ts @@ -2194,20 +2194,34 @@ export class MysqlRepository { // 友圈动态相关 - async getPosts(category?: string): Promise { + async getPosts(category?: string, keyword?: string, status?: string): Promise { let sql = ` SELECT id, user_id as userId, author, avatar, tag, tag_type as tagType, location, distance, content, topics, images, action_text as actionText, - likes, comments, views, category, created_at as createdAt + likes, comments, views, category, is_top as isTop, status, created_at as createdAt FROM posts `; const params: any[] = []; + const where: string[] = []; + if (category && category !== 'all') { - sql += ' WHERE category = ?'; + where.push('category = ?'); params.push(category); } - sql += ' ORDER BY created_at DESC'; + if (keyword) { + where.push('(content LIKE ? OR author LIKE ? OR location LIKE ?)'); + params.push(`%${keyword}%`, `%${keyword}%`, `%${keyword}%`); + } + if (status && status !== 'all') { + where.push('status = ?'); + params.push(status); + } + + if (where.length > 0) { + sql += ' WHERE ' + where.join(' AND '); + } + sql += ' ORDER BY is_top DESC, created_at DESC'; const [rows]: any = await pool.query(sql, params); return rows.map((r: any) => { @@ -2222,12 +2236,45 @@ export class MysqlRepository { return { ...r, + isTop: Boolean(r.isTop), + status: r.status || '正常', topics: topicsArr, images: imagesArr, }; }); } + // 管理端切换置顶 + async togglePostTop(postId: string, isTop: boolean): Promise { + const [result]: any = await pool.query(` + UPDATE posts SET is_top = ? WHERE id = ? + `, [isTop ? 1 : 0, postId]); + return result.affectedRows > 0; + } + + // 管理端切换状态 (正常 / 隐藏) + async updatePostStatus(postId: string, status: string): Promise { + const [result]: any = await pool.query(` + UPDATE posts SET status = ? WHERE id = ? + `, [status, postId]); + return result.affectedRows > 0; + } + + // 管理端彻底删除动态及下属评论 + async adminDeletePost(postId: string): Promise { + try { + await pool.query('DELETE FROM post_comments WHERE post_id = ?', [postId]); + } catch (_) {} + const [result]: any = await pool.query('DELETE FROM posts WHERE id = ?', [postId]); + return result.affectedRows > 0; + } + + // 管理端删除特定违规评论 + async adminDeletePostComment(commentId: string): Promise { + const [result]: any = await pool.query('DELETE FROM post_comments WHERE id = ?', [commentId]); + return result.affectedRows > 0; + } + async createPost(data: { userId?: number; author?: string; diff --git a/server/src/index.ts b/server/src/index.ts index 59c4093..54186ce 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1686,6 +1686,84 @@ app.get('/api/posts', async (req: Request, res: Response) => { } }); +// 管理端:全量动态管理与检索 +app.get('/api/admin/posts', async (req: Request, res: Response) => { + try { + const category = req.query.category ? String(req.query.category) : undefined; + const keyword = req.query.keyword ? String(req.query.keyword) : undefined; + const status = req.query.status ? String(req.query.status) : undefined; + const posts = await repo.getPosts(category, keyword, status); + res.json({ code: 20000, data: posts }); + } catch (err: any) { + res.status(500).json({ code: 50000, message: err.message }); + } +}); + +// 管理端:切换置顶状态 +app.post('/api/admin/posts/:id/top', async (req: Request, res: Response) => { + try { + const postId = String(req.params.id); + const { isTop } = req.body; + await repo.togglePostTop(postId, Boolean(isTop)); + res.json({ code: 20000, message: isTop ? '置顶成功' : '已取消置顶' }); + } catch (err: any) { + res.status(400).json({ code: 40000, message: err.message }); + } +}); + +// 管理端:切换显示/隐藏状态 +app.post('/api/admin/posts/:id/status', async (req: Request, res: Response) => { + try { + const postId = String(req.params.id); + const { status } = req.body; + await repo.updatePostStatus(postId, String(status)); + res.json({ code: 20000, message: '动态状态更新成功' }); + } catch (err: any) { + res.status(400).json({ code: 40000, message: err.message }); + } +}); + +// 管理端:违规下架彻底删除 +app.delete('/api/admin/posts/:id', async (req: Request, res: Response) => { + try { + const postId = String(req.params.id); + await repo.adminDeletePost(postId); + res.json({ code: 20000, message: '违规动态及关联评论已彻底删除' }); + } catch (err: any) { + res.status(400).json({ code: 40000, message: err.message }); + } +}); + +// 管理端:删除单条不当评论 +app.delete('/api/admin/posts/comments/:commentId', async (req: Request, res: Response) => { + try { + const commentId = String(req.params.commentId); + await repo.adminDeletePostComment(commentId); + res.json({ code: 20000, message: '违规评论已删除' }); + } catch (err: any) { + res.status(400).json({ code: 40000, message: err.message }); + } +}); + +// 管理端:微信订阅消息与服务通知模拟测试推送接口 +app.post('/api/admin/message/test-send', async (req: Request, res: Response) => { + try { + const { templateType, targetUserId, previewData } = req.body; + // 模拟微信服务器发送模版消息 + const result = { + templateType: templateType || 'order_status', + targetUserId: targetUserId || 1001, + sendTime: new Date().toISOString(), + status: 'SUCCESS', + wechatMsgId: `wx_msg_${Date.now()}`, + previewData: previewData || { title: '订单履约提醒', detail: '您的同城生活陪伴订单已开始服务' } + }; + res.json({ code: 20000, data: result, message: '微信模板消息模拟发送成功' }); + } catch (err: any) { + res.status(400).json({ code: 40000, message: err.message }); + } +}); + app.post('/api/posts', async (req: Request, res: Response) => { try { const { content } = req.body || {}; diff --git a/server/tests/admin-features.spec.ts b/server/tests/admin-features.spec.ts new file mode 100644 index 0000000..2908997 --- /dev/null +++ b/server/tests/admin-features.spec.ts @@ -0,0 +1,138 @@ +// 已读取AGENTS.md +import * as fs from 'fs'; +import * as path from 'path'; + +describe('PC总后台管理端核心业务与闭环能力自动化测试 (Post Manage, Finance CSV Export, WeChat Template Config)', () => { + const rootDir = path.resolve(__dirname, '../../'); + const serverIndexPath = path.join(rootDir, 'server/src/index.ts'); + const serverRepoPath = path.join(rootDir, 'server/src/db/repository.ts'); + const serverMysqlPath = path.join(rootDir, 'server/src/db/mysql.ts'); + const adminPostManageVuePath = path.join(rootDir, 'admin/arco-design-pro-vite/src/views/community/post-manage/index.vue'); + const adminFinanceRecordsVuePath = path.join(rootDir, 'admin/arco-design-pro-vite/src/views/finance/records/index.vue'); + const adminFinanceWithdrawVuePath = path.join(rootDir, 'admin/arco-design-pro-vite/src/views/finance/withdraw/index.vue'); + const adminPlatformConfigVuePath = path.join(rootDir, 'admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue'); + const adminCommunityRouterPath = path.join(rootDir, 'admin/arco-design-pro-vite/src/router/routes/modules/community.ts'); + + describe('1. 友圈广场动态管理与置顶/状态流转/级联删除闭环验证', () => { + it('数据库层必须扩展 posts 表结构并包含 is_top 与 status 字段', () => { + const mysqlCode = fs.readFileSync(serverMysqlPath, 'utf-8'); + expect(mysqlCode).toContain('ALTER TABLE posts ADD COLUMN is_top'); + expect(mysqlCode).toContain('ALTER TABLE posts ADD COLUMN status'); + }); + + it('仓储层 MysqlRepository 必须实现动态多维筛选、一键置顶、状态切换与评论管理', () => { + const repoCode = fs.readFileSync(serverRepoPath, 'utf-8'); + expect(repoCode).toContain('async getPosts('); + expect(repoCode).toContain('ORDER BY is_top DESC, created_at DESC'); + expect(repoCode).toContain('async togglePostTop('); + expect(repoCode).toContain('async updatePostStatus('); + expect(repoCode).toContain('async adminDeletePost('); + expect(repoCode).toContain('async adminDeletePostComment('); + }); + + it('后端服务 index.ts 必须注册动态管控的核心管理端 API 路由', () => { + const serverCode = fs.readFileSync(serverIndexPath, 'utf-8'); + expect(serverCode).toContain("app.get('/api/admin/posts'"); + expect(serverCode).toContain("app.post('/api/admin/posts/:id/top'"); + expect(serverCode).toContain("app.post('/api/admin/posts/:id/status'"); + expect(serverCode).toContain("app.delete('/api/admin/posts/:id'"); + expect(serverCode).toContain("app.delete('/api/admin/posts/comments/:commentId'"); + }); + + it('动态管理前端视图 post-manage/index.vue 必须具备完整交互与多图预览', () => { + const postManageCode = fs.readFileSync(adminPostManageVuePath, 'utf-8'); + expect(postManageCode).toContain('handleToggleTop'); + expect(postManageCode).toContain('handleToggleStatus'); + expect(postManageCode).toContain('handleDeletePost'); + expect(postManageCode).toContain('handleDeleteComment'); + expect(postManageCode).toContain('a-image-preview-group'); + expect(postManageCode).toContain('a-drawer'); + expect(postManageCode).toContain('/api/admin/posts'); + }); + }); + + describe('2. 财务管理对账明细导出 CSV 规范校验', () => { + it('资金交易流水导出必须包含 UTF-8 BOM 与标准字段', () => { + const recordsFile = fs.readFileSync(adminFinanceRecordsVuePath, 'utf-8'); + expect(recordsFile).toContain('exportCSV'); + expect(recordsFile).toContain('\\uFEFF'); + expect(recordsFile).toContain('流水号'); + expect(recordsFile).toContain('交易类型'); + expect(recordsFile).toContain('金额(元)'); + expect(recordsFile).toContain('资金方向'); + expect(recordsFile).toContain('交易对手方'); + expect(recordsFile).toContain('关联业务ID'); + expect(recordsFile).toContain('记账时间'); + }); + + it('提现审核列表导出必须包含 UTF-8 BOM 与标准字段', () => { + const withdrawFile = fs.readFileSync(adminFinanceWithdrawVuePath, 'utf-8'); + expect(withdrawFile).toContain('exportCSV'); + expect(withdrawFile).toContain('\\uFEFF'); + expect(withdrawFile).toContain('提现单号'); + expect(withdrawFile).toContain('申请大神'); + expect(withdrawFile).toContain('大神ID'); + expect(withdrawFile).toContain('提现金额(元)'); + expect(withdrawFile).toContain('账户余额(元)'); + expect(withdrawFile).toContain('结算渠道'); + expect(withdrawFile).toContain('申请时间'); + expect(withdrawFile).toContain('审核状态'); + }); + }); + + describe('3. 微信小程序订阅消息与服务通知管理配置及模拟推送', () => { + it('平台配置种子与增量迁移中必须包含 message_template_rules', () => { + const mysqlCode = fs.readFileSync(serverMysqlPath, 'utf-8'); + expect(mysqlCode).toContain('message_template_rules'); + expect(mysqlCode).toContain('orderStatusTemplateId'); + expect(mysqlCode).toContain('activityJoinTemplateId'); + expect(mysqlCode).toContain('withdrawSuccessTemplateId'); + expect(mysqlCode).toContain('verifyCodeTemplateId'); + }); + + it('后台平台配置面板已集成微信服务通知与模板消息 Tab 及测试推送控制台', () => { + const configViewCode = fs.readFileSync(adminPlatformConfigVuePath, 'utf-8'); + expect(configViewCode).toContain('message_template_rules'); + expect(configViewCode).toContain('orderStatusTemplateId'); + expect(configViewCode).toContain('activityJoinTemplateId'); + expect(configViewCode).toContain('withdrawSuccessTemplateId'); + expect(configViewCode).toContain('verifyCodeTemplateId'); + expect(configViewCode).toContain('sendTestMessage'); + expect(configViewCode).toContain('/api/admin/message/test-send'); + }); + + it('后端必须提供模拟测试推送端点并正确返回结构化结果', () => { + const serverCode = fs.readFileSync(serverIndexPath, 'utf-8'); + expect(serverCode).toContain("app.post('/api/admin/message/test-send'"); + expect(serverCode).toContain('wechatMsgId'); + expect(serverCode).toContain('status'); + expect(serverCode).toContain('sendTime'); + }); + + it('管理后台路由正确注册 community 模块并挂载 post-manage', () => { + expect(fs.existsSync(adminCommunityRouterPath)).toBe(true); + const routerCode = fs.readFileSync(adminCommunityRouterPath, 'utf-8'); + expect(routerCode).toContain('/community'); + expect(routerCode).toContain('post-manage'); + expect(routerCode).toContain('icon-message'); + }); + }); + + describe('4. 全流程无 Emoji 门禁合规校验', () => { + it('新增的前后端代码中严禁包含任何系统 Emoji 图标字符', () => { + const filesToCheck = [ + adminCommunityRouterPath, + adminPostManageVuePath, + adminFinanceRecordsVuePath, + adminFinanceWithdrawVuePath, + adminPlatformConfigVuePath + ]; + + const emojiRegex = /[\uD83C-\uDBFF\uDC00-\uDFFF\u2600-\u26FF\u2700-\u27BF]/; + for (const file of filesToCheck) { + const content = fs.readFileSync(file, 'utf-8'); + expect(emojiRegex.test(content)).toBe(false); + } + }); + }); +});