feat(admin): complete post management, finance csv export, and wechat message template config

这个提交包含在:
xcbyy
2026-10-13 11:11:40 +08:00
父节点 3bb6f5a6be
当前提交 736450973e
共有 11 个文件被更改,包括 951 次插入 和 4 次删除

查看文件

@@ -436,6 +436,14 @@ export async function initDatabase(): Promise<void> {
`);
} 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<void> {
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<void> {
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) {

查看文件

@@ -2194,20 +2194,34 @@ export class MysqlRepository {
// 友圈动态相关
async getPosts(category?: string): Promise<any[]> {
async getPosts(category?: string, keyword?: string, status?: string): Promise<any[]> {
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<boolean> {
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<boolean> {
const [result]: any = await pool.query(`
UPDATE posts SET status = ? WHERE id = ?
`, [status, postId]);
return result.affectedRows > 0;
}
// 管理端彻底删除动态及下属评论
async adminDeletePost(postId: string): Promise<boolean> {
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<boolean> {
const [result]: any = await pool.query('DELETE FROM post_comments WHERE id = ?', [commentId]);
return result.affectedRows > 0;
}
async createPost(data: {
userId?: number;
author?: string;

查看文件

@@ -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 || {};

查看文件

@@ -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);
}
});
});
});