feat(admin): complete post management, finance csv export, and wechat message template config
这个提交包含在:
@@ -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 || {};
|
||||
|
||||
在新工单中引用
屏蔽一个用户