fix: 完善全系统业务闭环(大神入驻联动用户角色、服务评价人工下架监管、友圈搭子上车审核与抽屉)
这个提交包含在:
@@ -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 {
|
||||
|
||||
在新工单中引用
屏蔽一个用户