|
|
|
|
@@ -424,14 +424,46 @@ export class MysqlRepository {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 大神相关
|
|
|
|
|
private async resolveCompanionRow(id: number | string): Promise<any | null> {
|
|
|
|
|
const rawId = String(id).trim();
|
|
|
|
|
const numId = Number(rawId);
|
|
|
|
|
if (!isNaN(numId) && numId > 0) {
|
|
|
|
|
const [res]: any = await pool.query(
|
|
|
|
|
'SELECT * FROM companions WHERE id = ? OR user_id = ? OR name = ? ORDER BY id ASC LIMIT 1',
|
|
|
|
|
[numId, numId, rawId]
|
|
|
|
|
);
|
|
|
|
|
if (res && res.length > 0) return res[0];
|
|
|
|
|
if (numId > 100 && numId < 200) {
|
|
|
|
|
const [mappedRes]: any = await pool.query(
|
|
|
|
|
'SELECT * FROM companions WHERE id = ? LIMIT 1',
|
|
|
|
|
[numId - 100]
|
|
|
|
|
);
|
|
|
|
|
if (mappedRes && mappedRes.length > 0) return mappedRes[0];
|
|
|
|
|
}
|
|
|
|
|
if (numId === 2006) {
|
|
|
|
|
const [susuRes]: any = await pool.query(
|
|
|
|
|
"SELECT * FROM companions WHERE name = '苏苏学姐' OR id = 1 LIMIT 1"
|
|
|
|
|
);
|
|
|
|
|
if (susuRes && susuRes.length > 0) return susuRes[0];
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const [res]: any = await pool.query('SELECT * FROM companions WHERE name = ? LIMIT 1', [rawId]);
|
|
|
|
|
if (res && res.length > 0) return res[0];
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getCompanions(): Promise<any[]> {
|
|
|
|
|
try {
|
|
|
|
|
await pool.query("ALTER TABLE companions ADD COLUMN reject_reason VARCHAR(255) DEFAULT '' AFTER audit_status");
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
const [rows]: any = await pool.query(`
|
|
|
|
|
SELECT
|
|
|
|
|
id, name, avatar, gender, age, track, rank_name as \`rank\`,
|
|
|
|
|
id, user_id as userId, name, avatar, gender, age, track, rank_name as \`rank\`,
|
|
|
|
|
hourly_rate as hourlyRate, voice_duration as voiceDuration,
|
|
|
|
|
voice_title as voiceTitle, rating, order_count as orderCount,
|
|
|
|
|
status, audit_status as auditStatus, id_card_masked as idCardMasked,
|
|
|
|
|
tags, apply_time as applyTime
|
|
|
|
|
status, audit_status as auditStatus, COALESCE(reject_reason, '') as rejectReason,
|
|
|
|
|
id_card_masked as idCardMasked, tags, apply_time as applyTime
|
|
|
|
|
FROM companions
|
|
|
|
|
ORDER BY id ASC
|
|
|
|
|
`);
|
|
|
|
|
@@ -448,19 +480,9 @@ export class MysqlRepository {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getCompanionById(id: number | string): Promise<any> {
|
|
|
|
|
const rawId = String(id);
|
|
|
|
|
const numId = Number(rawId);
|
|
|
|
|
let rows: any[] = [];
|
|
|
|
|
if (!isNaN(numId)) {
|
|
|
|
|
const [res]: any = await pool.query('SELECT * FROM companions WHERE id = ? OR name = ?', [numId, rawId]);
|
|
|
|
|
rows = res;
|
|
|
|
|
} else {
|
|
|
|
|
const [res]: any = await pool.query('SELECT * FROM companions WHERE name = ?', [rawId]);
|
|
|
|
|
rows = res;
|
|
|
|
|
}
|
|
|
|
|
if (!rows || rows.length === 0) return null;
|
|
|
|
|
const c = rows[0];
|
|
|
|
|
const game = c.track === 'OFFLINE_MEAL' ? '美食陪吃' : (c.track === 'OFFLINE_SING' ? '同城陪唱' : (c.track === 'OFFLINE_DRINK' ? '下午茶陪喝' : (c.track === 'OFFLINE_PLAY' ? '同城陪玩' : (c.game || '同城生活陪伴'))));
|
|
|
|
|
const c = await this.resolveCompanionRow(id);
|
|
|
|
|
if (!c) return null;
|
|
|
|
|
const game = c.track === 'OFFLINE_MEAL' ? '美食陪吃' : (c.track === 'OFFLINE_SING' ? '同城陪唱' : (c.track === 'OFFLINE_DRINK' ? '下午茶陪喝' : (c.track === 'OFFLINE_PLAY' ? '同城陪玩' : (c.track || c.game || '同城生活陪伴'))));
|
|
|
|
|
const price = Number(c.hourly_rate) || 68;
|
|
|
|
|
|
|
|
|
|
let parsedTags = ['高颜值', '性格好', '秒回消息', '文明健康'];
|
|
|
|
|
@@ -473,9 +495,53 @@ export class MysqlRepository {
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let customServices: any[] = [];
|
|
|
|
|
try {
|
|
|
|
|
const displayExpertId = Number(c.id) < 100 ? Number(c.id) + 100 : Number(c.id);
|
|
|
|
|
const [srvRows]: any = await pool.query(
|
|
|
|
|
'SELECT * FROM expert_services WHERE expert_id IN (?, ?) AND status = 1 ORDER BY created_at ASC',
|
|
|
|
|
[Number(c.id), displayExpertId]
|
|
|
|
|
);
|
|
|
|
|
if (srvRows && srvRows.length > 0) {
|
|
|
|
|
customServices = srvRows.map((s: any) => ({
|
|
|
|
|
id: s.id,
|
|
|
|
|
name: s.title,
|
|
|
|
|
title: s.title,
|
|
|
|
|
category: s.category,
|
|
|
|
|
price: Number(s.price) || price,
|
|
|
|
|
unit: s.unit || '小时',
|
|
|
|
|
desc: s.desc_text || '正规公开商圈同城陪伴,AA制或协商消费'
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
const defaultServices = [
|
|
|
|
|
{
|
|
|
|
|
id: 'srv_1',
|
|
|
|
|
name: `${game} · 基础陪伴 (2小时)`,
|
|
|
|
|
title: `${game} · 基础陪伴 (2小时)`,
|
|
|
|
|
price: price,
|
|
|
|
|
unit: '小时',
|
|
|
|
|
desc: '正规公开商圈同城陪伴,AA制或协商消费'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'srv_2',
|
|
|
|
|
name: `${game} · 半日畅享 (3小时)`,
|
|
|
|
|
title: `${game} · 半日畅享 (3小时)`,
|
|
|
|
|
price: Math.round(price * 2.8),
|
|
|
|
|
unit: '次',
|
|
|
|
|
desc: '深度互动与陪伴交流,赠送专属点歌或语音祝福'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const auditStatus = c.audit_status || '已通过';
|
|
|
|
|
const certStatus = auditStatus === '已通过' ? 2 : (auditStatus === '待审核' ? 1 : (auditStatus === '已驳回' ? 3 : 0));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: String(c.id),
|
|
|
|
|
userId: c.user_id || null,
|
|
|
|
|
nickname: c.name,
|
|
|
|
|
name: c.name,
|
|
|
|
|
avatar: c.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300',
|
|
|
|
|
coverImage: c.avatar || 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800',
|
|
|
|
|
gender: c.gender === '男' ? 'male' : 'female',
|
|
|
|
|
@@ -486,8 +552,13 @@ export class MysqlRepository {
|
|
|
|
|
rankTag: c.rank_name || '认证大神',
|
|
|
|
|
levelTag: '金牌认证',
|
|
|
|
|
price: price,
|
|
|
|
|
hourlyRate: price,
|
|
|
|
|
rating: Number(c.rating) || 5.0,
|
|
|
|
|
orderCount: Number(c.order_count) || 120,
|
|
|
|
|
status: c.status || '接单中',
|
|
|
|
|
auditStatus,
|
|
|
|
|
certStatus,
|
|
|
|
|
rejectReason: c.reject_reason || '',
|
|
|
|
|
isOnline: c.status === '接单中',
|
|
|
|
|
voiceDuration: Number(c.voice_duration) || 15,
|
|
|
|
|
voiceType: 'voice',
|
|
|
|
|
@@ -499,28 +570,14 @@ export class MysqlRepository {
|
|
|
|
|
'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400',
|
|
|
|
|
'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=400'
|
|
|
|
|
],
|
|
|
|
|
services: [
|
|
|
|
|
{
|
|
|
|
|
id: 'srv_1',
|
|
|
|
|
name: `${game} · 基础陪伴 (2小时)`,
|
|
|
|
|
title: `${game} · 基础陪伴 (2小时)`,
|
|
|
|
|
price: price,
|
|
|
|
|
unit: '小时',
|
|
|
|
|
desc: '正规公开商圈同城陪伴,AA制或协商消费'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'srv_2',
|
|
|
|
|
name: `${game} · 半日畅享 (3小时)`,
|
|
|
|
|
title: `${game} · 半日畅享 (3小时)`,
|
|
|
|
|
price: Math.round(price * 2.8),
|
|
|
|
|
unit: '次',
|
|
|
|
|
desc: '深度互动与陪伴交流,赠送专属点歌或语音祝福'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
services: customServices.length > 0 ? customServices : defaultServices
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateCompanionProfile(id: number | string, data: { hourlyRate?: number; tags?: string[]; rankName?: string; status?: string }): Promise<any> {
|
|
|
|
|
const comp = await this.resolveCompanionRow(id);
|
|
|
|
|
if (!comp) return null;
|
|
|
|
|
|
|
|
|
|
const fields: string[] = [];
|
|
|
|
|
const values: any[] = [];
|
|
|
|
|
|
|
|
|
|
@@ -542,11 +599,11 @@ export class MysqlRepository {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fields.length > 0) {
|
|
|
|
|
values.push(id);
|
|
|
|
|
values.push(comp.id);
|
|
|
|
|
await pool.query(`UPDATE companions SET ${fields.join(', ')} WHERE id = ?`, values);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.getCompanionById(id);
|
|
|
|
|
return this.getCompanionById(comp.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async applyCompanion(data: {
|
|
|
|
|
@@ -559,6 +616,9 @@ export class MysqlRepository {
|
|
|
|
|
avatar?: string;
|
|
|
|
|
gender?: string;
|
|
|
|
|
}): Promise<any> {
|
|
|
|
|
try {
|
|
|
|
|
await pool.query("ALTER TABLE companions ADD COLUMN reject_reason VARCHAR(255) DEFAULT '' AFTER audit_status");
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
const userId = Number(data.userId) || 1001;
|
|
|
|
|
const name = data.name.trim();
|
|
|
|
|
const track = data.track || '同城陪吃';
|
|
|
|
|
@@ -570,32 +630,41 @@ 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 = ? OR (user_id IS NOT NULL AND user_id = ?)', [name, userId]);
|
|
|
|
|
const [existing]: any = await pool.query('SELECT * FROM companions WHERE (user_id IS NOT NULL AND user_id = ?) OR name = ?', [userId, name]);
|
|
|
|
|
if (existing && existing.length > 0) {
|
|
|
|
|
await pool.query(`
|
|
|
|
|
UPDATE companions
|
|
|
|
|
SET user_id = ?, track = ?, hourly_rate = ?, tags = ?, avatar = ?, gender = ?, audit_status = '待审核', status = '待审核', id_card_masked = ?
|
|
|
|
|
SET user_id = ?, name = ?, track = ?, hourly_rate = ?, tags = ?, avatar = ?, gender = ?, audit_status = '待审核', status = '待审核', reject_reason = '', id_card_masked = ?, apply_time = NOW()
|
|
|
|
|
WHERE id = ?
|
|
|
|
|
`, [userId, track, hourlyRate, tagsJson, avatar, gender, maskedIdCard, existing[0].id]);
|
|
|
|
|
`, [userId, name, track, hourlyRate, tagsJson, avatar, gender, maskedIdCard, existing[0].id]);
|
|
|
|
|
return this.getCompanionById(existing[0].id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [res]: any = await pool.query(`
|
|
|
|
|
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())
|
|
|
|
|
INSERT INTO companions (user_id, name, avatar, gender, age, track, rank_name, hourly_rate, voice_duration, voice_title, rating, order_count, status, audit_status, reject_reason, 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async auditCompanion(id: number, action: 'approve' | 'reject', reason?: string): Promise<any> {
|
|
|
|
|
try {
|
|
|
|
|
await pool.query("ALTER TABLE companions ADD COLUMN reject_reason VARCHAR(255) DEFAULT '' AFTER audit_status");
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
try {
|
|
|
|
|
await pool.query('ALTER TABLE users ADD COLUMN is_expert TINYINT DEFAULT 0');
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
const auditStatus = action === 'approve' ? '已通过' : '已驳回';
|
|
|
|
|
const status = action === 'approve' ? '接单中' : '已驳回';
|
|
|
|
|
const rejectReason = action === 'reject' ? (reason || '提交的资质或语音未通过人工核验,请修改后重新提交') : '';
|
|
|
|
|
|
|
|
|
|
await pool.query(`
|
|
|
|
|
UPDATE companions
|
|
|
|
|
SET audit_status = ?, status = ?
|
|
|
|
|
SET audit_status = ?, status = ?, reject_reason = ?
|
|
|
|
|
WHERE id = ?
|
|
|
|
|
`, [auditStatus, status, id]);
|
|
|
|
|
`, [auditStatus, status, rejectReason, id]);
|
|
|
|
|
|
|
|
|
|
const [rows]: any = await pool.query('SELECT * FROM companions WHERE id = ?', [id]);
|
|
|
|
|
const comp = rows && rows[0];
|
|
|
|
|
@@ -604,15 +673,30 @@ export class MysqlRepository {
|
|
|
|
|
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]);
|
|
|
|
|
try {
|
|
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {
|
|
|
|
|
if (action === 'approve') {
|
|
|
|
|
await pool.query('UPDATE users SET role = "认证大神" WHERE id = ?', [uId]);
|
|
|
|
|
} else {
|
|
|
|
|
await pool.query('UPDATE users SET role = "普通用户" WHERE id = ?', [uId]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { ...rows[0], message: action === 'approve' ? '资质审核通过,用户已获得大神接单权限' : `已驳回: ${reason}` };
|
|
|
|
|
return {
|
|
|
|
|
...(comp || {}),
|
|
|
|
|
auditStatus,
|
|
|
|
|
status,
|
|
|
|
|
rejectReason,
|
|
|
|
|
certStatus: action === 'approve' ? 2 : 3,
|
|
|
|
|
message: action === 'approve' ? '资质审核通过,用户已获得大神接单权限' : `已驳回: ${rejectReason}`
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 订单相关
|
|
|
|
|
@@ -766,11 +850,158 @@ export class MysqlRepository {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateOrderStatus(id: string, newStatus: string): Promise<any> {
|
|
|
|
|
if (newStatus === '已完成') {
|
|
|
|
|
return this.completeOrderWithSettlement(id);
|
|
|
|
|
}
|
|
|
|
|
await pool.query('UPDATE orders SET status = ? WHERE id = ? OR order_no = ?', [newStatus, id, id]);
|
|
|
|
|
const [rows]: any = await pool.query('SELECT * FROM orders WHERE id = ? OR order_no = ?', [id, id]);
|
|
|
|
|
return rows[0] || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async completeOrderWithSettlement(id: string): Promise<any> {
|
|
|
|
|
await pool.query("UPDATE orders SET status = '已完成' WHERE id = ? OR order_no = ?", [id, id]);
|
|
|
|
|
const [rows]: any = await pool.query('SELECT * FROM orders WHERE id = ? OR order_no = ?', [id, id]);
|
|
|
|
|
const order = rows && rows[0];
|
|
|
|
|
if (!order) return null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const [settledRows]: any = await pool.query(
|
|
|
|
|
"SELECT id FROM finance_records WHERE related_id = ? AND type = '大神完单收益结算' LIMIT 1",
|
|
|
|
|
[String(order.id)]
|
|
|
|
|
);
|
|
|
|
|
const alreadySettled = settledRows && settledRows.length > 0;
|
|
|
|
|
|
|
|
|
|
if (!alreadySettled) {
|
|
|
|
|
const [cfgRows]: any = await pool.query('SELECT * FROM finance_config WHERE id = 1 LIMIT 1');
|
|
|
|
|
const cfg = (cfgRows && cfgRows[0]) || {
|
|
|
|
|
platform_rate_percent: 10,
|
|
|
|
|
first_referral_percent: 5,
|
|
|
|
|
second_referral_percent: 2
|
|
|
|
|
};
|
|
|
|
|
const platformPct = Number(cfg.platform_rate_percent ?? 10);
|
|
|
|
|
const firstRefPct = Number(cfg.first_referral_percent ?? 5);
|
|
|
|
|
const secondRefPct = Number(cfg.second_referral_percent ?? 2);
|
|
|
|
|
|
|
|
|
|
const orderAmount = Number(order.amount) || 0;
|
|
|
|
|
if (orderAmount > 0) {
|
|
|
|
|
const platformFee = Number(((orderAmount * platformPct) / 100).toFixed(2));
|
|
|
|
|
const expertIncome = Number(Math.max(0, orderAmount - platformFee).toFixed(2));
|
|
|
|
|
|
|
|
|
|
const comp = await this.resolveCompanionRow(order.expert_id || order.companion_name || 101);
|
|
|
|
|
if (comp) {
|
|
|
|
|
await pool.query('UPDATE companions SET order_count = COALESCE(order_count, 0) + 1 WHERE id = ?', [comp.id]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const expertUserId = Number(comp?.user_id || order.expert_id || 101);
|
|
|
|
|
await pool.query(
|
|
|
|
|
'UPDATE users SET commission = COALESCE(commission, 0) + ?, order_count = COALESCE(order_count, 0) + 1 WHERE id = ? OR nickname = ?',
|
|
|
|
|
[expertIncome, expertUserId, order.companion_name || '']
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const buyerId = Number(order.buyer_id || 1001);
|
|
|
|
|
await pool.query(
|
|
|
|
|
'UPDATE users SET order_count = COALESCE(order_count, 0) + 1 WHERE id = ?',
|
|
|
|
|
[buyerId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const ts = Date.now();
|
|
|
|
|
const rand = () => Math.floor(100 + Math.random() * 900);
|
|
|
|
|
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id, created_at)
|
|
|
|
|
VALUES (?, ?, ?, '大神完单收益结算', ?, 'IN', ?, ?, NOW())`,
|
|
|
|
|
[
|
|
|
|
|
`fin_settle_${ts}_${rand()}`,
|
|
|
|
|
`JS${ts}${rand()}`,
|
|
|
|
|
expertUserId,
|
|
|
|
|
expertIncome,
|
|
|
|
|
order.client_name || '买家客户',
|
|
|
|
|
String(order.id)
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (platformFee > 0) {
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id, created_at)
|
|
|
|
|
VALUES (?, ?, 0, ?, ?, 'IN', ?, ?, NOW())`,
|
|
|
|
|
[
|
|
|
|
|
`fin_plat_${ts}_${rand()}`,
|
|
|
|
|
`PT${ts}${rand()}`,
|
|
|
|
|
`平台抽成 (${platformPct}%)`,
|
|
|
|
|
platformFee,
|
|
|
|
|
order.companion_name || '认证大神',
|
|
|
|
|
String(order.id)
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 二级分销自动返佣结算
|
|
|
|
|
const [buyerRows]: any = await pool.query('SELECT id, nickname, inviter_id FROM users WHERE id = ? LIMIT 1', [buyerId]);
|
|
|
|
|
const buyer = buyerRows && buyerRows[0];
|
|
|
|
|
const fromUserName = buyer?.nickname || order.client_name || '客户玩家';
|
|
|
|
|
const orderNo = order.order_no || String(order.id);
|
|
|
|
|
|
|
|
|
|
if (buyer && buyer.inviter_id && firstRefPct > 0) {
|
|
|
|
|
const lv1UserId = Number(buyer.inviter_id);
|
|
|
|
|
const comm1 = Number(((orderAmount * firstRefPct) / 100).toFixed(2));
|
|
|
|
|
if (comm1 > 0 && lv1UserId > 0 && lv1UserId !== buyerId) {
|
|
|
|
|
await pool.query('UPDATE users SET commission = COALESCE(commission, 0) + ? WHERE id = ?', [comm1, lv1UserId]);
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO distribution_records (id, user_id, from_user_id, from_user_name, order_no, order_amount, commission, level, rate_percent, status, created_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, '已入账', NOW())`,
|
|
|
|
|
[`dst_1_${ts}_${rand()}`, lv1UserId, buyerId, fromUserName, orderNo, orderAmount, comm1, firstRefPct]
|
|
|
|
|
);
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id, created_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, 'IN', ?, ?, NOW())`,
|
|
|
|
|
[
|
|
|
|
|
`fin_ref1_${ts}_${rand()}`,
|
|
|
|
|
`FX1${ts}${rand()}`,
|
|
|
|
|
lv1UserId,
|
|
|
|
|
`一级分销佣金 (${firstRefPct}%)`,
|
|
|
|
|
comm1,
|
|
|
|
|
fromUserName,
|
|
|
|
|
String(order.id)
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [lv1Rows]: any = await pool.query('SELECT id, inviter_id FROM users WHERE id = ? LIMIT 1', [lv1UserId]);
|
|
|
|
|
const lv1User = lv1Rows && lv1Rows[0];
|
|
|
|
|
if (lv1User && lv1User.inviter_id && secondRefPct > 0) {
|
|
|
|
|
const lv2UserId = Number(lv1User.inviter_id);
|
|
|
|
|
const comm2 = Number(((orderAmount * secondRefPct) / 100).toFixed(2));
|
|
|
|
|
if (comm2 > 0 && lv2UserId > 0 && lv2UserId !== buyerId && lv2UserId !== lv1UserId) {
|
|
|
|
|
await pool.query('UPDATE users SET commission = COALESCE(commission, 0) + ? WHERE id = ?', [comm2, lv2UserId]);
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO distribution_records (id, user_id, from_user_id, from_user_name, order_no, order_amount, commission, level, rate_percent, status, created_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 2, ?, '已入账', NOW())`,
|
|
|
|
|
[`dst_2_${ts}_${rand()}`, lv2UserId, buyerId, fromUserName, orderNo, orderAmount, comm2, secondRefPct]
|
|
|
|
|
);
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id, created_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, 'IN', ?, ?, NOW())`,
|
|
|
|
|
[
|
|
|
|
|
`fin_ref2_${ts}_${rand()}`,
|
|
|
|
|
`FX2${ts}${rand()}`,
|
|
|
|
|
lv2UserId,
|
|
|
|
|
`二级分销佣金 (${secondRefPct}%)`,
|
|
|
|
|
comm2,
|
|
|
|
|
fromUserName,
|
|
|
|
|
String(order.id)
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
const [updatedRows]: any = await pool.query('SELECT * FROM orders WHERE id = ?', [order.id]);
|
|
|
|
|
return updatedRows[0] || order;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getOrdersPool(): Promise<any[]> {
|
|
|
|
|
const [rows]: any = await pool.query(`
|
|
|
|
|
SELECT
|
|
|
|
|
@@ -1601,6 +1832,7 @@ export class MysqlRepository {
|
|
|
|
|
|
|
|
|
|
async handleArbitration(id: string, decision: string): Promise<any> {
|
|
|
|
|
const conn = await pool.getConnection();
|
|
|
|
|
let shouldSettleOrderId = '';
|
|
|
|
|
try {
|
|
|
|
|
await conn.beginTransaction();
|
|
|
|
|
|
|
|
|
|
@@ -1638,24 +1870,41 @@ export class MysqlRepository {
|
|
|
|
|
// 4. 联动扣减违规大神评分
|
|
|
|
|
const expertId = Number(order.expert_id);
|
|
|
|
|
if (expertId > 0) {
|
|
|
|
|
await conn.query('UPDATE companions SET rating = GREATEST(1.0, rating - 0.2) WHERE id = ?', [expertId]);
|
|
|
|
|
const altId = expertId > 100 && expertId < 200 ? expertId - 100 : expertId;
|
|
|
|
|
await conn.query('UPDATE companions SET rating = GREATEST(1.0, rating - 0.2) WHERE id IN (?, ?) OR user_id = ?', [expertId, altId, expertId]);
|
|
|
|
|
}
|
|
|
|
|
} else if (decision === 'deduct_credit' && order) {
|
|
|
|
|
const expertId = Number(order.expert_id);
|
|
|
|
|
if (expertId > 0) {
|
|
|
|
|
await conn.query('UPDATE companions SET rating = GREATEST(1.0, rating - 0.3) WHERE id = ?', [expertId]);
|
|
|
|
|
} else if (order) {
|
|
|
|
|
// 驳回退款申诉或仅扣减大神信用分:订单恢复为已完成并进入完单结算闭环
|
|
|
|
|
await conn.query('UPDATE orders SET status = "已完成" WHERE id = ?', [order.id]);
|
|
|
|
|
shouldSettleOrderId = String(order.id);
|
|
|
|
|
if (decision === 'deduct_credit') {
|
|
|
|
|
const expertId = Number(order.expert_id);
|
|
|
|
|
if (expertId > 0) {
|
|
|
|
|
const altId = expertId > 100 && expertId < 200 ? expertId - 100 : expertId;
|
|
|
|
|
await conn.query('UPDATE companions SET rating = GREATEST(1.0, rating - 0.3) WHERE id IN (?, ?) OR user_id = ?', [expertId, altId, expertId]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await conn.commit();
|
|
|
|
|
return { id, status: statusText };
|
|
|
|
|
} catch (err) {
|
|
|
|
|
await conn.rollback();
|
|
|
|
|
throw err;
|
|
|
|
|
} finally {
|
|
|
|
|
conn.release();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shouldSettleOrderId) {
|
|
|
|
|
try {
|
|
|
|
|
await this.completeOrderWithSettlement(shouldSettleOrderId);
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let statusText = '申诉已驳回';
|
|
|
|
|
if (decision === 'refund_full') statusText = '已全额退款用户';
|
|
|
|
|
if (decision === 'deduct_credit') statusText = '已扣减大神信用分';
|
|
|
|
|
return { id, status: statusText, orderStatus: decision === 'refund_full' ? '已全额退款' : '已完成' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 买家发起退款/售后申诉:订单流转为争议仲裁中,同时写入仲裁工单 (含理由与凭证截图)
|
|
|
|
|
@@ -1725,8 +1974,8 @@ export class MysqlRepository {
|
|
|
|
|
return { id, status: '待处理' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getReports(): Promise<any[]> {
|
|
|
|
|
const [rows]: any = await pool.query(`
|
|
|
|
|
async getReports(reporterId?: number): Promise<any[]> {
|
|
|
|
|
let sql = `
|
|
|
|
|
SELECT
|
|
|
|
|
id,
|
|
|
|
|
reporter_id as reporterId,
|
|
|
|
|
@@ -1742,9 +1991,14 @@ export class MysqlRepository {
|
|
|
|
|
handled_at as handledAt,
|
|
|
|
|
created_at as createdAt
|
|
|
|
|
FROM reports
|
|
|
|
|
ORDER BY created_at DESC
|
|
|
|
|
LIMIT 200
|
|
|
|
|
`);
|
|
|
|
|
`;
|
|
|
|
|
const params: any[] = [];
|
|
|
|
|
if (reporterId !== undefined && !isNaN(Number(reporterId)) && Number(reporterId) > 0) {
|
|
|
|
|
sql += ' WHERE reporter_id = ?';
|
|
|
|
|
params.push(Number(reporterId));
|
|
|
|
|
}
|
|
|
|
|
sql += ' ORDER BY created_at DESC LIMIT 200';
|
|
|
|
|
const [rows]: any = await pool.query(sql, params);
|
|
|
|
|
return rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1770,11 +2024,12 @@ export class MysqlRepository {
|
|
|
|
|
if (report && action === 'punish' && report.target_type === 'EXPERT') {
|
|
|
|
|
const expertId = Number(report.target_id);
|
|
|
|
|
if (expertId > 0) {
|
|
|
|
|
const altId = expertId > 100 && expertId < 200 ? expertId - 100 : expertId;
|
|
|
|
|
await pool.query(`
|
|
|
|
|
UPDATE companions
|
|
|
|
|
SET rating = GREATEST(1.0, rating - 0.2)
|
|
|
|
|
WHERE id = ?
|
|
|
|
|
`, [expertId]);
|
|
|
|
|
WHERE id IN (?, ?) OR user_id = ?
|
|
|
|
|
`, [expertId, altId, expertId]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
@@ -1988,6 +2243,11 @@ export class MysqlRepository {
|
|
|
|
|
const [arbRows]: any = await pool.query('SELECT * FROM arbitrations WHERE status = "待平台仲裁"');
|
|
|
|
|
const [wdRows]: any = await pool.query('SELECT * FROM withdrawals WHERE status = "待审核"');
|
|
|
|
|
const [secRows]: any = await pool.query('SELECT * FROM safety_audit_logs ORDER BY id DESC LIMIT 5');
|
|
|
|
|
let rptRows: any[] = [];
|
|
|
|
|
try {
|
|
|
|
|
const [rRows]: any = await pool.query('SELECT * FROM reports WHERE status = "待处理" ORDER BY created_at DESC');
|
|
|
|
|
rptRows = rRows || [];
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
const pendingWithdrawalAmount = wdRows.reduce((sum: number, w: any) => sum + Number(w.amount), 0);
|
|
|
|
|
|
|
|
|
|
@@ -1996,6 +2256,7 @@ export class MysqlRepository {
|
|
|
|
|
pendingArbitrationsCount: arbRows.length,
|
|
|
|
|
pendingWithdrawalsCount: wdRows.length,
|
|
|
|
|
pendingWithdrawalAmount,
|
|
|
|
|
pendingReportsCount: rptRows.length,
|
|
|
|
|
riskInterceptionsCount: secRows.length,
|
|
|
|
|
pendingAudits: auditRows.map((c: any) => ({
|
|
|
|
|
id: c.id,
|
|
|
|
|
@@ -2015,6 +2276,13 @@ export class MysqlRepository {
|
|
|
|
|
amount: w.amount,
|
|
|
|
|
channel: w.channel,
|
|
|
|
|
})),
|
|
|
|
|
pendingReports: rptRows.map((r: any) => ({
|
|
|
|
|
id: r.id,
|
|
|
|
|
reporterId: r.reporter_id,
|
|
|
|
|
targetName: r.target_name || r.target_id,
|
|
|
|
|
reason: r.reason,
|
|
|
|
|
description: r.description || '',
|
|
|
|
|
})),
|
|
|
|
|
recentRiskLogs: secRows,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
@@ -2130,7 +2398,7 @@ export class MysqlRepository {
|
|
|
|
|
const [rows]: any = await pool.query(`
|
|
|
|
|
SELECT
|
|
|
|
|
id, user_no as userNo, nickname, avatar, phone, gender, signature,
|
|
|
|
|
role, balance, commission, invited_count as invitedCount,
|
|
|
|
|
role, balance, commission, invited_count as invitedCount, inviter_id as inviterId,
|
|
|
|
|
order_count as orderCount, activity_count as activityCount,
|
|
|
|
|
status, wechat_openid as wechatOpenid, created_at as createdAt
|
|
|
|
|
FROM users
|
|
|
|
|
@@ -2153,6 +2421,10 @@ export class MysqlRepository {
|
|
|
|
|
orderCount: 88,
|
|
|
|
|
activityCount: 16,
|
|
|
|
|
isExpert: true,
|
|
|
|
|
certStatus: 2,
|
|
|
|
|
auditStatus: '已通过',
|
|
|
|
|
rejectReason: '',
|
|
|
|
|
companionStatus: '接单中',
|
|
|
|
|
status: '正常'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
@@ -2172,12 +2444,42 @@ export class MysqlRepository {
|
|
|
|
|
orderCount: 120,
|
|
|
|
|
activityCount: 35,
|
|
|
|
|
isExpert: true,
|
|
|
|
|
certStatus: 2,
|
|
|
|
|
auditStatus: '已通过',
|
|
|
|
|
rejectReason: '',
|
|
|
|
|
companionStatus: '接单中',
|
|
|
|
|
status: '正常'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const u = rows[0];
|
|
|
|
|
let companionRow: any = null;
|
|
|
|
|
try {
|
|
|
|
|
const [compRows]: any = await pool.query(
|
|
|
|
|
'SELECT * FROM companions WHERE user_id = ? OR name = ? ORDER BY id DESC LIMIT 1',
|
|
|
|
|
[u.id, u.nickname]
|
|
|
|
|
);
|
|
|
|
|
if (compRows && compRows.length > 0) {
|
|
|
|
|
companionRow = compRows[0];
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
const roleHasExpert = (u.role || '').includes('大神');
|
|
|
|
|
const auditStatus = companionRow
|
|
|
|
|
? (companionRow.audit_status || '已通过')
|
|
|
|
|
: (roleHasExpert ? '已通过' : '未申请');
|
|
|
|
|
const certStatus =
|
|
|
|
|
auditStatus === '已通过'
|
|
|
|
|
? 2
|
|
|
|
|
: auditStatus === '待审核'
|
|
|
|
|
? 1
|
|
|
|
|
: auditStatus === '已驳回'
|
|
|
|
|
? 3
|
|
|
|
|
: 0;
|
|
|
|
|
const rejectReason = companionRow?.reject_reason || '';
|
|
|
|
|
const companionStatus = companionRow ? (companionRow.status || '接单中') : (certStatus === 2 ? '接单中' : '');
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: u.id,
|
|
|
|
|
userNo: u.userNo,
|
|
|
|
|
@@ -2190,9 +2492,15 @@ export class MysqlRepository {
|
|
|
|
|
balance: Number(u.balance || 0).toFixed(2),
|
|
|
|
|
commission: Number(u.commission || 0).toFixed(2),
|
|
|
|
|
invitedCount: Number(u.invitedCount || 0),
|
|
|
|
|
inviterId: u.inviterId ? Number(u.inviterId) : null,
|
|
|
|
|
orderCount: Number(u.orderCount || 0),
|
|
|
|
|
activityCount: Number(u.activityCount || 0),
|
|
|
|
|
isExpert: (u.role || '').includes('大神') || (u.role || '').includes('房主'),
|
|
|
|
|
companionId: companionRow ? Number(companionRow.id) : null,
|
|
|
|
|
auditStatus,
|
|
|
|
|
certStatus,
|
|
|
|
|
rejectReason,
|
|
|
|
|
companionStatus,
|
|
|
|
|
isExpert: certStatus === 2 || roleHasExpert,
|
|
|
|
|
status: u.status,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
@@ -2382,6 +2690,25 @@ export class MysqlRepository {
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
const applyStatsMap = new Map<string, { total: number; pending: number; myStatus: string }>();
|
|
|
|
|
try {
|
|
|
|
|
const [applyRows]: any = await pool.query(
|
|
|
|
|
'SELECT post_id, applicant_id, status FROM post_applies ORDER BY created_at DESC'
|
|
|
|
|
);
|
|
|
|
|
if (Array.isArray(applyRows)) {
|
|
|
|
|
for (const ar of applyRows) {
|
|
|
|
|
const pid = String(ar.post_id);
|
|
|
|
|
const cur = applyStatsMap.get(pid) || { total: 0, pending: 0, myStatus: '' };
|
|
|
|
|
cur.total += 1;
|
|
|
|
|
if (ar.status === 'PENDING') cur.pending += 1;
|
|
|
|
|
if (Number(ar.applicant_id) === currentUserId && !cur.myStatus) {
|
|
|
|
|
cur.myStatus = ar.status || 'PENDING';
|
|
|
|
|
}
|
|
|
|
|
applyStatsMap.set(pid, cur);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
let sql = `
|
|
|
|
|
SELECT
|
|
|
|
|
id, user_id as userId, author, avatar, tag, tag_type as tagType,
|
|
|
|
|
@@ -2421,6 +2748,8 @@ export class MysqlRepository {
|
|
|
|
|
imagesArr = typeof r.images === 'string' ? JSON.parse(r.images) : (r.images || []);
|
|
|
|
|
} catch (_) { imagesArr = []; }
|
|
|
|
|
|
|
|
|
|
const appStat = applyStatsMap.get(String(r.id)) || { total: 0, pending: 0, myStatus: '' };
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...r,
|
|
|
|
|
liked: likedPostIdSet.has(String(r.id)),
|
|
|
|
|
@@ -2428,6 +2757,9 @@ export class MysqlRepository {
|
|
|
|
|
status: r.status || '正常',
|
|
|
|
|
topics: topicsArr,
|
|
|
|
|
images: imagesArr,
|
|
|
|
|
applyCount: appStat.total,
|
|
|
|
|
pendingApplyCount: appStat.pending,
|
|
|
|
|
myApplyStatus: appStat.myStatus,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
@@ -2592,14 +2924,19 @@ export class MysqlRepository {
|
|
|
|
|
`, [id, orderId, expertId, userId, userNickname, userAvatar, rating, tagsJson, content]);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await pool.query("UPDATE orders SET status = '已完成' WHERE id = ? OR order_no = ?", [orderId, orderId]);
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
await this.completeOrderWithSettlement(orderId);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
try {
|
|
|
|
|
await pool.query("UPDATE orders SET status = '已完成' WHERE id = ? OR order_no = ?", [orderId, orderId]);
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const [avgRows]: any = await pool.query('SELECT AVG(rating) as avgRating FROM order_reviews WHERE expert_id = ?', [expertId]);
|
|
|
|
|
const altId = Number(expertId) > 100 && Number(expertId) < 200 ? Number(expertId) - 100 : (Number(expertId) < 100 ? Number(expertId) + 100 : Number(expertId));
|
|
|
|
|
const [avgRows]: any = await pool.query('SELECT AVG(rating) as avgRating FROM order_reviews WHERE expert_id IN (?, ?)', [expertId, altId]);
|
|
|
|
|
if (avgRows && avgRows[0]?.avgRating) {
|
|
|
|
|
const newRating = Number(Number(avgRows[0].avgRating).toFixed(2));
|
|
|
|
|
await pool.query('UPDATE companions SET rating = ? WHERE id = ?', [newRating, expertId]);
|
|
|
|
|
await pool.query('UPDATE companions SET rating = ? WHERE id IN (?, ?) OR user_id = ?', [newRating, expertId, altId, expertId]);
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
@@ -2636,8 +2973,10 @@ export class MysqlRepository {
|
|
|
|
|
const params: any[] = [];
|
|
|
|
|
const where: string[] = [];
|
|
|
|
|
if (expertId) {
|
|
|
|
|
where.push('expert_id = ?');
|
|
|
|
|
params.push(expertId);
|
|
|
|
|
const numExp = Number(expertId);
|
|
|
|
|
const altId = numExp > 100 && numExp < 200 ? numExp - 100 : (numExp < 100 ? numExp + 100 : numExp);
|
|
|
|
|
where.push('expert_id IN (?, ?)');
|
|
|
|
|
params.push(numExp, altId);
|
|
|
|
|
}
|
|
|
|
|
if (orderId) {
|
|
|
|
|
where.push('order_id = ?');
|
|
|
|
|
@@ -2668,9 +3007,11 @@ export class MysqlRepository {
|
|
|
|
|
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 numExp = Number(expertId);
|
|
|
|
|
const altId = numExp > 100 && numExp < 200 ? numExp - 100 : (numExp < 100 ? numExp + 100 : numExp);
|
|
|
|
|
const [avgRows]: any = await pool.query('SELECT AVG(rating) as avgRating FROM order_reviews WHERE expert_id IN (?, ?)', [numExp, altId]);
|
|
|
|
|
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]);
|
|
|
|
|
await pool.query('UPDATE companions SET rating = ? WHERE id IN (?, ?) OR user_id = ?', [newRating, numExp, altId, numExp]);
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
return res.affectedRows > 0;
|
|
|
|
|
@@ -2726,12 +3067,46 @@ export class MysqlRepository {
|
|
|
|
|
applicantName?: string;
|
|
|
|
|
applicantAvatar?: string;
|
|
|
|
|
}): Promise<any> {
|
|
|
|
|
const id = `apply_${Date.now()}_${Math.floor(100 + Math.random() * 900)}`;
|
|
|
|
|
const postId = data.postId;
|
|
|
|
|
const applicantId = Number(data.applicantId) || 1001;
|
|
|
|
|
const applicantName = data.applicantName || '搭子玩家';
|
|
|
|
|
const applicantAvatar = data.applicantAvatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const [existRows]: any = await pool.query(
|
|
|
|
|
'SELECT * FROM post_applies WHERE post_id = ? AND applicant_id = ? ORDER BY created_at DESC LIMIT 1',
|
|
|
|
|
[postId, applicantId]
|
|
|
|
|
);
|
|
|
|
|
if (existRows && existRows.length > 0) {
|
|
|
|
|
const exist = existRows[0];
|
|
|
|
|
if (exist.status === 'REJECTED') {
|
|
|
|
|
await pool.query(
|
|
|
|
|
"UPDATE post_applies SET status = 'PENDING', applicant_name = ?, applicant_avatar = ?, created_at = NOW() WHERE id = ?",
|
|
|
|
|
[applicantName, applicantAvatar, exist.id]
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
id: exist.id,
|
|
|
|
|
postId,
|
|
|
|
|
applicantId,
|
|
|
|
|
applicantName,
|
|
|
|
|
applicantAvatar,
|
|
|
|
|
status: 'PENDING',
|
|
|
|
|
message: '已重新发起约伴上车申请,等待房主确认'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
id: exist.id,
|
|
|
|
|
postId,
|
|
|
|
|
applicantId,
|
|
|
|
|
applicantName: exist.applicant_name || applicantName,
|
|
|
|
|
applicantAvatar: exist.applicant_avatar || applicantAvatar,
|
|
|
|
|
status: exist.status || 'PENDING',
|
|
|
|
|
message: exist.status === 'ACCEPTED' ? '房主已同意您的上车申请' : '约伴上车申请已送达,请等待房主确认'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
const id = `apply_${Date.now()}_${Math.floor(100 + Math.random() * 900)}`;
|
|
|
|
|
await pool.query(`
|
|
|
|
|
INSERT INTO post_applies (id, post_id, applicant_id, applicant_name, applicant_avatar, status, created_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, 'PENDING', NOW())
|
|
|
|
|
@@ -2817,6 +3192,11 @@ export class MysqlRepository {
|
|
|
|
|
|
|
|
|
|
// 19. 大神服务包管理
|
|
|
|
|
async getExpertServices(expertId: number): Promise<any[]> {
|
|
|
|
|
const defaultList = [
|
|
|
|
|
{ id: `srv_${expertId}_1`, title: '排位保星连胜局', category: '王者荣耀', price: 45, unit: '小时', mode: '排位赛', status: true, desc: '荣耀王者实力带飞,保星上分,麦克风清晰互动' },
|
|
|
|
|
{ id: `srv_${expertId}_2`, title: '娱乐匹配快乐唠嗑局', category: '英雄联盟手游', price: 30, unit: '小时', mode: '匹配/大乱斗', status: true, desc: '轻松搞笑,幽默风趣,情绪价值拉满' },
|
|
|
|
|
{ id: `srv_${expertId}_3`, title: '英雄技术单项教学指导', category: '和平精英', price: 55, unit: '小时', mode: '一对一复盘', status: true, desc: '对线细节、走位身法与团战意识深度剖析复盘' }
|
|
|
|
|
];
|
|
|
|
|
try {
|
|
|
|
|
await pool.query(`
|
|
|
|
|
CREATE TABLE IF NOT EXISTS expert_services (
|
|
|
|
|
@@ -2845,13 +3225,17 @@ export class MysqlRepository {
|
|
|
|
|
desc: r.desc_text || ''
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
// 首次读取时自动将默认服务包落库,确保后续上下架切换真实持久化
|
|
|
|
|
for (const item of defaultList) {
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT IGNORE INTO expert_services (id, expert_id, title, category, price, unit, mode, status, desc_text)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
|
|
|
|
[item.id, Number(expertId), item.title, item.category, item.price, item.unit, item.mode, item.desc]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
{ id: `srv_${expertId}_1`, title: '排位保星连胜局', category: '王者荣耀', price: 45, unit: '小时', mode: '排位赛', status: true, desc: '荣耀王者实力带飞,保星上分,麦克风清晰互动' },
|
|
|
|
|
{ id: `srv_${expertId}_2`, title: '娱乐匹配快乐唠嗑局', category: '英雄联盟手游', price: 30, unit: '小时', mode: '匹配/大乱斗', status: true, desc: '轻松搞笑,幽默风趣,情绪价值拉满' },
|
|
|
|
|
{ id: `srv_${expertId}_3`, title: '英雄技术单项教学指导', category: '和平精英', price: 55, unit: '小时', mode: '一对一复盘', status: true, desc: '对线细节、走位身法与团战意识深度剖析复盘' }
|
|
|
|
|
];
|
|
|
|
|
return defaultList;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateExpertServiceStatus(serviceId: string, status: boolean): Promise<any> {
|
|
|
|
|
@@ -2867,8 +3251,9 @@ export class MysqlRepository {
|
|
|
|
|
const [rows]: any = await pool.query(`
|
|
|
|
|
SELECT
|
|
|
|
|
f.id as favoriteId,
|
|
|
|
|
f.expert_id as favExpertId,
|
|
|
|
|
f.created_at as favoritedAt,
|
|
|
|
|
c.id,
|
|
|
|
|
c.id as compId,
|
|
|
|
|
c.name as nickname,
|
|
|
|
|
c.avatar,
|
|
|
|
|
c.gender,
|
|
|
|
|
@@ -2881,7 +3266,11 @@ export class MysqlRepository {
|
|
|
|
|
c.status,
|
|
|
|
|
c.tags
|
|
|
|
|
FROM favorites f
|
|
|
|
|
JOIN companions c ON f.expert_id = c.id
|
|
|
|
|
JOIN companions c ON (
|
|
|
|
|
f.expert_id = c.id
|
|
|
|
|
OR (f.expert_id > 100 AND f.expert_id < 200 AND (f.expert_id - 100) = c.id)
|
|
|
|
|
OR (c.user_id IS NOT NULL AND f.expert_id = c.user_id)
|
|
|
|
|
)
|
|
|
|
|
WHERE f.user_id = ?
|
|
|
|
|
ORDER BY f.created_at DESC
|
|
|
|
|
`, [Number(userId)]);
|
|
|
|
|
@@ -2892,8 +3281,11 @@ export class MysqlRepository {
|
|
|
|
|
try {
|
|
|
|
|
tagsArr = typeof r.tags === 'string' ? JSON.parse(r.tags) : (r.tags || []);
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
const displayId = Number(r.compId) < 100 ? Number(r.compId) + 100 : Number(r.compId);
|
|
|
|
|
return {
|
|
|
|
|
...r,
|
|
|
|
|
id: displayId,
|
|
|
|
|
dbId: Number(r.compId),
|
|
|
|
|
tags: tagsArr,
|
|
|
|
|
isOnline: r.status === '接单中'
|
|
|
|
|
};
|
|
|
|
|
@@ -2918,10 +3310,12 @@ export class MysqlRepository {
|
|
|
|
|
|
|
|
|
|
async removeFavorite(userId: number, expertId: number): Promise<{ success: boolean; message: string }> {
|
|
|
|
|
try {
|
|
|
|
|
const numExp = Number(expertId);
|
|
|
|
|
const altId = numExp > 100 && numExp < 200 ? numExp - 100 : (numExp < 100 ? numExp + 100 : numExp);
|
|
|
|
|
await pool.query(`
|
|
|
|
|
DELETE FROM favorites
|
|
|
|
|
WHERE user_id = ? AND expert_id = ?
|
|
|
|
|
`, [Number(userId), Number(expertId)]);
|
|
|
|
|
WHERE user_id = ? AND expert_id IN (?, ?)
|
|
|
|
|
`, [Number(userId), numExp, altId]);
|
|
|
|
|
return { success: true, message: '已取消收藏' };
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
return { success: false, message: err.message || '取消收藏失败' };
|
|
|
|
|
@@ -2930,11 +3324,13 @@ export class MysqlRepository {
|
|
|
|
|
|
|
|
|
|
async isFavorite(userId: number, expertId: number): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const numExp = Number(expertId);
|
|
|
|
|
const altId = numExp > 100 && numExp < 200 ? numExp - 100 : (numExp < 100 ? numExp + 100 : numExp);
|
|
|
|
|
const [rows]: any = await pool.query(`
|
|
|
|
|
SELECT id FROM favorites
|
|
|
|
|
WHERE user_id = ? AND expert_id = ?
|
|
|
|
|
WHERE user_id = ? AND expert_id IN (?, ?)
|
|
|
|
|
LIMIT 1
|
|
|
|
|
`, [Number(userId), Number(expertId)]);
|
|
|
|
|
`, [Number(userId), numExp, altId]);
|
|
|
|
|
return Boolean(rows && rows.length > 0);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return false;
|
|
|
|
|
@@ -2942,9 +3338,52 @@ export class MysqlRepository {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 21. 分销下级团队与返佣记录
|
|
|
|
|
async bindInviteCode(userId: number, inviteCode: string): Promise<any> {
|
|
|
|
|
const uid = Number(userId);
|
|
|
|
|
const cleaned = String(inviteCode || '').trim().toUpperCase().replace(/^PW/, '').replace(/^U/, '');
|
|
|
|
|
const inviterId = Number(cleaned);
|
|
|
|
|
if (!inviterId || isNaN(inviterId) || inviterId <= 0) {
|
|
|
|
|
throw new Error('请输入有效的邀请码 (如 PW1001)');
|
|
|
|
|
}
|
|
|
|
|
if (inviterId === uid) {
|
|
|
|
|
throw new Error('不能绑定自己的邀请码');
|
|
|
|
|
}
|
|
|
|
|
const [invRows]: any = await pool.query('SELECT id, nickname FROM users WHERE id = ? LIMIT 1', [inviterId]);
|
|
|
|
|
if (!invRows || invRows.length === 0) {
|
|
|
|
|
throw new Error('邀请码对应的推荐人不存在');
|
|
|
|
|
}
|
|
|
|
|
const inviter = invRows[0];
|
|
|
|
|
const [curRows]: any = await pool.query('SELECT id, inviter_id FROM users WHERE id = ? LIMIT 1', [uid]);
|
|
|
|
|
if (!curRows || curRows.length === 0) {
|
|
|
|
|
throw new Error('当前用户不存在');
|
|
|
|
|
}
|
|
|
|
|
if (curRows[0].inviter_id && Number(curRows[0].inviter_id) === inviterId) {
|
|
|
|
|
return { userId: uid, inviterId, inviterName: inviter.nickname, message: `已绑定推荐人:${inviter.nickname}` };
|
|
|
|
|
}
|
|
|
|
|
await pool.query('UPDATE users SET inviter_id = ? WHERE id = ?', [inviterId, uid]);
|
|
|
|
|
await pool.query('UPDATE users SET invited_count = COALESCE(invited_count, 0) + 1 WHERE id = ?', [inviterId]);
|
|
|
|
|
return {
|
|
|
|
|
userId: uid,
|
|
|
|
|
inviterId,
|
|
|
|
|
inviterName: inviter.nickname,
|
|
|
|
|
message: `成功绑定推荐人:${inviter.nickname}`
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getDistributionTeam(userId: number): Promise<any> {
|
|
|
|
|
const uid = Number(userId);
|
|
|
|
|
try {
|
|
|
|
|
const [contribRows]: any = await pool.query(
|
|
|
|
|
'SELECT from_user_id as fromUserId, COALESCE(SUM(commission), 0) as total FROM distribution_records WHERE user_id = ? GROUP BY from_user_id',
|
|
|
|
|
[uid]
|
|
|
|
|
);
|
|
|
|
|
const contribMap = new Map<number, string>();
|
|
|
|
|
if (Array.isArray(contribRows)) {
|
|
|
|
|
contribRows.forEach((r: any) => {
|
|
|
|
|
contribMap.set(Number(r.fromUserId), Number(r.total || 0).toFixed(2));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [directRows]: any = await pool.query(`
|
|
|
|
|
SELECT id, nickname, avatar, phone, created_at as createdAt, 1 as level
|
|
|
|
|
FROM users
|
|
|
|
|
@@ -2952,7 +3391,10 @@ export class MysqlRepository {
|
|
|
|
|
ORDER BY created_at DESC
|
|
|
|
|
`, [uid]);
|
|
|
|
|
|
|
|
|
|
let directList = directRows || [];
|
|
|
|
|
let directList = (directRows || []).map((u: any) => ({
|
|
|
|
|
...u,
|
|
|
|
|
totalContribute: contribMap.get(Number(u.id)) || '0.00'
|
|
|
|
|
}));
|
|
|
|
|
if (directList.length === 0) {
|
|
|
|
|
directList = [
|
|
|
|
|
{ id: 2001, nickname: '晨曦微风', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120', phone: '138****6612', createdAt: '2026-10-01 14:20:00', level: 1, totalContribute: '48.50' },
|
|
|
|
|
@@ -2971,7 +3413,10 @@ export class MysqlRepository {
|
|
|
|
|
WHERE inviter_id IN (?)
|
|
|
|
|
ORDER BY created_at DESC
|
|
|
|
|
`, [directIds]);
|
|
|
|
|
indirectList = indRows || [];
|
|
|
|
|
indirectList = (indRows || []).map((u: any) => ({
|
|
|
|
|
...u,
|
|
|
|
|
totalContribute: contribMap.get(Number(u.id)) || '0.00'
|
|
|
|
|
}));
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
if (indirectList.length === 0) {
|
|
|
|
|
|