fix: 完善全系统业务闭环(大神入驻联动用户角色、服务评价人工下架监管、友圈搭子上车审核与抽屉)

这个提交包含在:
xcbyy
2026-10-13 18:04:27 +08:00
父节点 c14a1a3474
当前提交 010edc130f
共有 9 个文件被更改,包括 520 次插入 和 12 次删除

查看文件

@@ -84,10 +84,13 @@ export async function initDatabase(): Promise<void> {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 动态为 companions 增加 tags 字段 (若历史表已存在)
// 动态为 companions 增加 tags 字段与 user_id 字段 (若历史表已存在)
try {
await conn.query('ALTER TABLE companions ADD COLUMN tags JSON AFTER id_card_masked');
} catch (_) {}
try {
await conn.query('ALTER TABLE companions ADD COLUMN user_id INT DEFAULT NULL AFTER id');
} catch (_) {}
// 4. 订单表
await conn.query(`
@@ -279,12 +282,21 @@ export async function initDatabase(): Promise<void> {
rating INT NOT NULL DEFAULT 5,
tags JSON,
content TEXT,
reply_content TEXT,
reply_time DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_order (order_id),
INDEX idx_expert (expert_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
try {
await conn.query('ALTER TABLE order_reviews ADD COLUMN reply_content TEXT AFTER content');
} catch (_) {}
try {
await conn.query('ALTER TABLE order_reviews ADD COLUMN reply_time DATETIME AFTER reply_content');
} catch (_) {}
// 14. 友圈动态互动评论表
await conn.query(`
CREATE TABLE IF NOT EXISTS post_comments (

查看文件

@@ -559,6 +559,7 @@ export class MysqlRepository {
avatar?: string;
gender?: string;
}): Promise<any> {
const userId = Number(data.userId) || 1001;
const name = data.name.trim();
const track = data.track || '同城陪吃';
const hourlyRate = Number(data.hourlyRate) || 88;
@@ -569,20 +570,20 @@ export class MysqlRepository {
? data.idCard.substring(0, 6) + '********' + data.idCard.substring(data.idCard.length - 4)
: '110101********0000';
const [existing]: any = await pool.query('SELECT * FROM companions WHERE name = ?', [name]);
const [existing]: any = await pool.query('SELECT * FROM companions WHERE name = ? OR (user_id IS NOT NULL AND user_id = ?)', [name, userId]);
if (existing && existing.length > 0) {
await pool.query(`
UPDATE companions
SET track = ?, hourly_rate = ?, tags = ?, avatar = ?, gender = ?, audit_status = '待审核', status = '待审核', id_card_masked = ?
SET user_id = ?, track = ?, hourly_rate = ?, tags = ?, avatar = ?, gender = ?, audit_status = '待审核', status = '待审核', id_card_masked = ?
WHERE id = ?
`, [track, hourlyRate, tagsJson, avatar, gender, maskedIdCard, existing[0].id]);
`, [userId, track, hourlyRate, tagsJson, avatar, gender, maskedIdCard, existing[0].id]);
return this.getCompanionById(existing[0].id);
}
const [res]: any = await pool.query(`
INSERT INTO companions (name, avatar, gender, age, track, rank_name, hourly_rate, voice_duration, voice_title, rating, order_count, status, audit_status, id_card_masked, tags, apply_time)
VALUES (?, ?, ?, 23, ?, '申请中陪伴师', ?, 15, '热情语音介绍', 5.0, 0, '待审核', '待审核', ?, ?, NOW())
`, [name, avatar, gender, track, hourlyRate, maskedIdCard, tagsJson]);
INSERT INTO companions (user_id, name, avatar, gender, age, track, rank_name, hourly_rate, voice_duration, voice_title, rating, order_count, status, audit_status, id_card_masked, tags, apply_time)
VALUES (?, ?, ?, ?, 23, ?, '申请中陪伴师', ?, 15, '热情语音介绍', 5.0, 0, '待审核', '待审核', ?, ?, NOW())
`, [userId, name, avatar, gender, track, hourlyRate, maskedIdCard, tagsJson]);
return this.getCompanionById(res.insertId);
}
@@ -597,7 +598,21 @@ export class MysqlRepository {
`, [auditStatus, status, id]);
const [rows]: any = await pool.query('SELECT * FROM companions WHERE id = ?', [id]);
return { ...rows[0], message: action === 'approve' ? '资质审核通过' : `已驳回: ${reason}` };
const comp = rows && rows[0];
// 联动更新关联用户的角色与大神状态
if (comp && comp.user_id) {
const uId = Number(comp.user_id);
if (uId > 0) {
if (action === 'approve') {
await pool.query('UPDATE users SET role = "认证大神", is_expert = 1 WHERE id = ?', [uId]);
} else {
await pool.query('UPDATE users SET role = "普通用户", is_expert = 0 WHERE id = ?', [uId]);
}
}
}
return { ...rows[0], message: action === 'approve' ? '资质审核通过,用户已获得大神接单权限' : `已驳回: ${reason}` };
}
// 订单相关
@@ -2603,10 +2618,19 @@ export class MysqlRepository {
}
async getOrderReviews(expertId?: number, orderId?: string): Promise<any[]> {
try {
await pool.query('ALTER TABLE order_reviews ADD COLUMN reply_content TEXT AFTER content');
} catch (_) {}
try {
await pool.query('ALTER TABLE order_reviews ADD COLUMN reply_time DATETIME AFTER reply_content');
} catch (_) {}
let sql = `
SELECT id, order_id as orderId, expert_id as expertId, user_id as userId,
user_nickname as userNickname, user_avatar as userAvatar,
rating, tags, content, created_at as createdAt
rating, tags, content,
COALESCE(reply_content, '') as replyContent,
reply_time as replyTime,
created_at as createdAt
FROM order_reviews
`;
const params: any[] = [];
@@ -2637,6 +2661,21 @@ export class MysqlRepository {
});
}
async deleteReview(id: string): Promise<boolean> {
const [rows]: any = await pool.query('SELECT expert_id FROM order_reviews WHERE id = ?', [id]);
const expertId = rows && rows[0]?.expert_id;
const [res]: any = await pool.query('DELETE FROM order_reviews WHERE id = ?', [id]);
if (expertId) {
try {
const [avgRows]: any = await pool.query('SELECT AVG(rating) as avgRating FROM order_reviews WHERE expert_id = ?', [expertId]);
const newRating = (avgRows && avgRows[0]?.avgRating) ? Number(Number(avgRows[0].avgRating).toFixed(2)) : 5.0;
await pool.query('UPDATE companions SET rating = ? WHERE id = ?', [newRating, expertId]);
} catch (_) {}
}
return res.affectedRows > 0;
}
// 14. 友圈动态评论
async getPostComments(postId: string): Promise<any[]> {
const [rows]: any = await pool.query(`
@@ -2709,6 +2748,27 @@ export class MysqlRepository {
};
}
async getPostApplies(postId?: string): Promise<any[]> {
let sql = `
SELECT id, post_id as postId, applicant_id as applicantId, applicant_name as applicantName,
applicant_avatar as applicantAvatar, status, created_at as createdAt
FROM post_applies
`;
const params: any[] = [];
if (postId) {
sql += ' WHERE post_id = ?';
params.push(postId);
}
sql += ' ORDER BY created_at DESC';
const [rows]: any = await pool.query(sql, params);
return rows;
}
async handlePostApply(applyId: string, status: 'ACCEPTED' | 'REJECTED'): Promise<any> {
await pool.query('UPDATE post_applies SET status = ? WHERE id = ?', [status, applyId]);
return { id: applyId, status };
}
// 16. 用户个人提现记录查询
async getUserWithdrawals(userId: number): Promise<any[]> {
const [rows]: any = await pool.query(`
@@ -2728,8 +2788,10 @@ export class MysqlRepository {
async replyOrderReview(reviewId: string, replyContent: string): Promise<any> {
const now = new Date();
try {
await pool.query('ALTER TABLE order_reviews ADD COLUMN IF NOT EXISTS reply_content TEXT');
await pool.query('ALTER TABLE order_reviews ADD COLUMN IF NOT EXISTS reply_time DATETIME');
await pool.query('ALTER TABLE order_reviews ADD COLUMN reply_content TEXT AFTER content');
} catch (_) {}
try {
await pool.query('ALTER TABLE order_reviews ADD COLUMN reply_time DATETIME AFTER reply_content');
} catch (_) {}
await pool.query(`

查看文件

@@ -1903,6 +1903,32 @@ app.post('/api/reviews/:id/reply', async (req: Request, res: Response) => {
}
});
// 管理员全量查询评价与回复列表
app.get('/api/reviews', async (req: Request, res: Response) => {
try {
const expertId = req.query.expertId ? Number(req.query.expertId) : undefined;
const orderId = req.query.orderId ? String(req.query.orderId) : undefined;
const reviews = await repo.getOrderReviews(expertId, orderId);
res.json({
code: 20000,
data: reviews
});
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 管理员违规下架/删除评价
app.delete('/api/reviews/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
await repo.deleteReview(String(id));
res.json({ code: 20000, message: '违规评价已成功删除,大神信誉评分已重新计算' });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 友圈本人动态删除接口
app.delete('/api/posts/:id', async (req: Request, res: Response) => {
try {
@@ -2003,6 +2029,41 @@ app.post('/api/posts/:id/apply', async (req: Request, res: Response) => {
}
});
// 获取指定动态或全部搭子上车申请列表
app.get('/api/posts/:id/applies', async (req: Request, res: Response) => {
try {
const postId = String(req.params.id);
const applies = await repo.getPostApplies(postId);
res.json({ code: 20000, data: applies });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
app.get('/api/post-applies', async (_req: Request, res: Response) => {
try {
const applies = await repo.getPostApplies();
res.json({ code: 20000, data: applies });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 审核/处置搭子上车申请 (同意/谢绝)
app.post('/api/post-applies/:id/handle', async (req: Request, res: Response) => {
try {
const applyId = String(req.params.id);
const { status } = req.body || {};
if (!status || !['ACCEPTED', 'REJECTED'].includes(status)) {
return res.status(400).json({ code: 40000, message: '状态参数必须为 ACCEPTED 或 REJECTED' });
}
const result = await repo.handlePostApply(applyId, status);
res.json({ code: 20000, data: result, message: status === 'ACCEPTED' ? '已同意搭子上车申请' : '已谢绝搭子上车申请' });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 7.8 陪玩收藏管理接口 (收藏/取消收藏/列表/状态探测)
app.get('/api/favorites', async (req: Request, res: Response) => {
try {