feat(config): add finance rules, activity rules, and real expert application flow
这个提交包含在:
@@ -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);
|
||||
|
||||
在新工单中引用
屏蔽一个用户