feat(config): add finance rules, activity rules, and real expert application flow

这个提交包含在:
xcbyy
2026-10-10 14:06:29 +08:00
父节点 d7f5c31762
当前提交 872af22c20
共有 8 个文件被更改,包括 649 次插入 和 33 次删除

查看文件

@@ -721,6 +721,25 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
]
});
const financeRulesVal = JSON.stringify({
platformRatePercent: 10.0,
firstReferralPercent: 5.0,
secondReferralPercent: 2.0,
minWithdrawAmount: 50,
maxWithdrawAmount: 20000,
withdrawFeePercent: 0.0,
withdrawDesc: '提现将自动发起微信企业付款打款至微信零钱,工作日预计2小时内到账。'
});
const activityRulesVal = JSON.stringify({
minParticipants: 2,
maxParticipants: 10,
defaultParticipants: 4,
presetPerPersonFees: [200, 300, 500, 800, 1000],
presetTotalFees: [500, 1000, 2000, 3000, 5000],
defaultAaFee: 50
});
const [cfgItemRows]: any = await conn.query('SELECT COUNT(*) as count FROM platform_configs');
if (cfgItemRows[0].count === 0) {
const customerServiceVal = JSON.stringify({
@@ -758,8 +777,15 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
('faq_list', '帮助中心与常见问题FAQ', ?, '小程序端帮助中心常见问答与指引', 1),
('security_words', '私下引流与违规自定义敏感词库', ?, '全平台发帖、评论及订单防私下交易关键词实时过滤', 1),
('home_popup', '小程序首页运营全局弹窗', ?, '节日活动推广、新用户专享首单优惠全局浮层弹窗', 1),
('expert_rules', '大神技能标签库与定价区间规范', ?, '陪玩大神入驻审核、定价上下限限制与官方推荐技能标签库', 1);
`, [customerServiceVal, safetyNoticeVal, protocolVal, communityTopicsVal, communityNoticeVal, faqListVal, securityWordsVal, homePopupVal, expertRulesVal]);
('expert_rules', '大神技能标签库与定价区间规范', ?, '陪玩大神入驻审核、定价上下限限制与官方推荐技能标签库', 1),
('finance_rules', '平台抽成与推广分销规则', ?, '平台佣金抽成比例、二级分销返润与佣金提现门槛限制', 1),
('activity_rules', '同城组局发单与预算预设规范', ?, '招募席位上下限、快捷单人赏金预设与包场预算梯队', 1);
`, [
customerServiceVal, safetyNoticeVal, protocolVal,
communityTopicsVal, communityNoticeVal, faqListVal,
securityWordsVal, homePopupVal, expertRulesVal,
financeRulesVal, activityRulesVal
]);
} else {
// 增量保证各核心配置项完整就绪
const [existingRows]: any = await conn.query('SELECT config_key FROM platform_configs');
@@ -800,5 +826,17 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
VALUES ('expert_rules', '大神技能标签库与定价区间规范', ?, '陪玩大神入驻审核、定价上下限限制与官方推荐技能标签库', 1)
`, [expertRulesVal]);
}
if (!existingKeys.has('finance_rules')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('finance_rules', '平台抽成与推广分销规则', ?, '平台佣金抽成比例、二级分销返润与佣金提现门槛限制', 1)
`, [financeRulesVal]);
}
if (!existingKeys.has('activity_rules')) {
await conn.query(`
INSERT INTO platform_configs (config_key, name, value_json, description, status)
VALUES ('activity_rules', '同城组局发单与预算预设规范', ?, '招募席位上下限、快捷单人赏金预设与包场预算梯队', 1)
`, [activityRulesVal]);
}
}
}

查看文件

@@ -549,6 +549,44 @@ export class MysqlRepository {
return this.getCompanionById(id);
}
async applyCompanion(data: {
userId: number;
name: string;
idCard: string;
track: string;
hourlyRate?: number;
tags?: string[];
avatar?: string;
gender?: string;
}): Promise<any> {
const name = data.name.trim();
const track = data.track || '同城陪吃';
const hourlyRate = Number(data.hourlyRate) || 88;
const tagsJson = JSON.stringify(data.tags || []);
const avatar = data.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300';
const gender = data.gender || '女';
const maskedIdCard = data.idCard.length >= 8
? 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]);
if (existing && existing.length > 0) {
await pool.query(`
UPDATE companions
SET track = ?, hourly_rate = ?, tags = ?, avatar = ?, gender = ?, audit_status = '待审核', status = '待审核', id_card_masked = ?
WHERE id = ?
`, [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]);
return this.getCompanionById(res.insertId);
}
async auditCompanion(id: number, action: 'approve' | 'reject', reason?: string): Promise<any> {
const auditStatus = action === 'approve' ? '已通过' : '已驳回';
const status = action === 'approve' ? '接单中' : '已驳回';
@@ -1824,6 +1862,20 @@ export class MysqlRepository {
if (isNaN(amount) || amount <= 0) {
throw new Error('提现金额必须大于0');
}
// 动态读取财务提现配置限制门槛
let minWithdraw = 50;
try {
const fConfig = await this.getPlatformConfigByKey('finance_rules');
if (fConfig?.valueJson?.minWithdrawAmount) {
minWithdraw = Number(fConfig.valueJson.minWithdrawAmount);
}
} catch (_) {}
if (amount < minWithdraw) {
throw new Error(`提现金额不能低于平台最低门槛 ¥${minWithdraw}`);
}
const conn = await pool.getConnection();
try {
await conn.beginTransaction();

查看文件

@@ -550,6 +550,57 @@ app.get('/api/companions/:id', async (req: Request, res: Response) => {
}
});
// 大神资质与实名入驻真实申请接口
app.post('/api/companions/apply', async (req: Request, res: Response) => {
try {
const { userId, name, idCard, track, hourlyRate, tags, avatar, gender } = req.body;
if (!name || !name.trim()) {
return res.status(400).json({ code: 40000, message: '真实姓名不能为空' });
}
if (!idCard || !idCard.trim()) {
return res.status(400).json({ code: 40000, message: '身份证号不能为空' });
}
// 微信内容安全检测
const sec = contentSecurityService.checkText(`${name} ${track || ''} ${(tags || []).join(' ')}`);
if (!sec.pass) {
return res.status(400).json({ code: sec.code, message: sec.message });
}
// 校验价格与标签
const expertRulesConfig = await repo.getPlatformConfigByKey('expert_rules');
const rules = expertRulesConfig?.valueJson || { minHourlyRate: 30, maxHourlyRate: 500, maxTagsCount: 6 };
if (hourlyRate) {
const rateNum = Number(hourlyRate);
if (rateNum < (rules.minHourlyRate || 30) || rateNum > (rules.maxHourlyRate || 500)) {
return res.status(400).json({
code: 40000,
message: `服务单价需在 ¥${rules.minHourlyRate || 30} - ¥${rules.maxHourlyRate || 500} 之间`
});
}
}
const companion = await repo.applyCompanion({
userId: Number(userId) || 1001,
name: name.trim(),
idCard: idCard.trim(),
track: track || '同城陪吃',
hourlyRate: Number(hourlyRate) || rules.defaultHourlyRate || 88,
tags: Array.isArray(tags) ? tags.slice(0, rules.maxTagsCount || 6) : [],
avatar,
gender
});
res.json({
code: 20000,
data: companion,
message: '大神实名认证申请已提交,等待运营人工审核'
});
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
}
});
app.post('/api/companions/:id/audit', async (req: Request, res: Response) => {
try {
const companionId = Number(req.params.id);