From 872af22c203787a4076fcb362c6505881cfa42c3 Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Sat, 10 Oct 2026 14:06:29 +0800 Subject: [PATCH] feat(config): add finance rules, activity rules, and real expert application flow --- .../views/safety/platform-config/index.vue | 178 ++++++++++++ pwuniapp/common/api.js | 13 + pwuniapp/pages/activity/create.vue | 40 ++- pwuniapp/pages/user/index.vue | 264 ++++++++++++++++-- server/src/db/mysql.ts | 42 ++- server/src/db/repository.ts | 52 ++++ server/src/index.ts | 51 ++++ server/tests/content-security.spec.ts | 42 +++ 8 files changed, 649 insertions(+), 33 deletions(-) diff --git a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue index 9c055d2..5b2027d 100644 --- a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue +++ b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue @@ -469,6 +469,115 @@ + + + +
+ + 在此灵活调控全平台陪玩订单抽成比例、二级分销裂变返润比例以及陪伴师/玩家佣金提现限制门槛,小程序端推广说明与提现流程将动态同步。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + 保存财务抽成与分销配置 + +
+
+
+
+
+ + + +
+ + 在此设定同城活动组局的招募席位上下限、AA制默认分摊基准,以及房主请客/悬赏发单时展示的快捷赏金与总预算梯队。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + 保存组局发单规范配置 + +
+
+
+
+
@@ -709,6 +818,59 @@ const saveExpertRulesConfig = () => { saveConfig('expert_rules', expertRulesForm); }; +// P1 财务抽成与分销规则 +const financeRulesForm = reactive({ + name: '平台抽成与推广分销规则', + description: '平台佣金抽成比例、二级分销返润与佣金提现门槛限制', + status: 1, + valueJson: { + platformRatePercent: 10.0, + firstReferralPercent: 5.0, + secondReferralPercent: 2.0, + minWithdrawAmount: 50, + maxWithdrawAmount: 20000, + withdrawFeePercent: 0.0, + withdrawDesc: '提现将自动发起微信企业付款打款至微信零钱,工作日预计2小时内到账。' + } +}); + +const saveFinanceRulesConfig = () => { + saveConfig('finance_rules', financeRulesForm); +}; + +// P3 组局发单与预算规则 +const activityRulesForm = reactive({ + name: '同城组局发单与预算预设规范', + description: '招募席位上下限、快捷单人赏金预设与包场预算梯队', + status: 1, + valueJson: { + minParticipants: 2, + maxParticipants: 10, + defaultParticipants: 4, + presetPerPersonFees: [200, 300, 500, 800, 1000], + presetTotalFees: [500, 1000, 2000, 3000, 5000], + defaultAaFee: 50 + } +}); + +const presetPerPersonFeesText = ref('200, 300, 500, 800, 1000'); +const presetTotalFeesText = ref('500, 1000, 2000, 3000, 5000'); + +const saveActivityRulesConfig = () => { + const pFees = presetPerPersonFeesText.value + .split(/[,,\s]+/) + .map(Number) + .filter(n => !isNaN(n) && n > 0); + const tFees = presetTotalFeesText.value + .split(/[,,\s]+/) + .map(Number) + .filter(n => !isNaN(n) && n > 0); + + if (pFees.length > 0) activityRulesForm.valueJson.presetPerPersonFees = pFees; + if (tFees.length > 0) activityRulesForm.valueJson.presetTotalFees = tFees; + saveConfig('activity_rules', activityRulesForm); +}; + const fetchConfigs = async () => { loading.value = true; try { @@ -761,6 +923,22 @@ const fetchConfigs = async () => { if (Array.isArray(expertRulesForm.valueJson.candidateTags)) { expertCandidateTags.value = expertRulesForm.valueJson.candidateTags; } + } else if (item.configKey === 'finance_rules') { + financeRulesForm.name = item.name; + financeRulesForm.description = item.description; + financeRulesForm.status = item.status; + financeRulesForm.valueJson = { ...financeRulesForm.valueJson, ...(item.valueJson || {}) }; + } else if (item.configKey === 'activity_rules') { + activityRulesForm.name = item.name; + activityRulesForm.description = item.description; + activityRulesForm.status = item.status; + activityRulesForm.valueJson = { ...activityRulesForm.valueJson, ...(item.valueJson || {}) }; + if (Array.isArray(activityRulesForm.valueJson.presetPerPersonFees)) { + presetPerPersonFeesText.value = activityRulesForm.valueJson.presetPerPersonFees.join(', '); + } + if (Array.isArray(activityRulesForm.valueJson.presetTotalFees)) { + presetTotalFeesText.value = activityRulesForm.valueJson.presetTotalFees.join(', '); + } } }); } diff --git a/pwuniapp/common/api.js b/pwuniapp/common/api.js index 50c5334..8975a6d 100644 --- a/pwuniapp/common/api.js +++ b/pwuniapp/common/api.js @@ -763,3 +763,16 @@ export async function getSafetyPlaces() { } } +/** + * 提交大神实名资质入驻认证申请 + */ +export async function applyExpertCert(data) { + try { + const res = await http.post('/companions/apply', data); + return res; + } catch (err) { + throw err; + } +} + + diff --git a/pwuniapp/pages/activity/create.vue b/pwuniapp/pages/activity/create.vue index 51201fd..27de4fe 100644 --- a/pwuniapp/pages/activity/create.vue +++ b/pwuniapp/pages/activity/create.vue @@ -80,12 +80,12 @@ 招募席位 (人) - 包含房主在内的组局总人数 + 包含房主在内的组局总人数 ({{ minParticipants }}-{{ maxParticipants }}人) - + {{ form.maxParticipants }} 人 - + @@ -138,7 +138,7 @@ 快捷赏金: 快捷预算: { try { - const [placesList, catList] = await Promise.allSettled([ + const [placesList, catList, configsRes] = await Promise.allSettled([ getSafetyPlaces(), - getNavCategories() + getNavCategories(), + getPlatformConfigs() ]); if (placesList.status === 'fulfilled' && Array.isArray(placesList.value) && placesList.value.length > 0) { @@ -368,6 +374,21 @@ const loadLivePlaces = async () => { .map((c) => c.name); if (activeCats.length > 0) { games.value = activeCats; + if (!games.value.includes(form.value.game)) { + form.value.game = games.value[0]; + } + } + } + + if (configsRes.status === 'fulfilled' && configsRes.value?.activity_rules) { + const actRules = configsRes.value.activity_rules; + if (actRules.minParticipants) minParticipants.value = Number(actRules.minParticipants); + if (actRules.maxParticipants) maxParticipants.value = Number(actRules.maxParticipants); + if (Array.isArray(actRules.presetPerPersonFees) && actRules.presetPerPersonFees.length > 0) { + presetPerPersonFees.value = actRules.presetPerPersonFees; + } + if (Array.isArray(actRules.presetTotalFees) && actRules.presetTotalFees.length > 0) { + presetTotalFees.value = actRules.presetTotalFees; } } } catch (e) { @@ -386,7 +407,10 @@ const applyRecommendZone = (zone) => { }; const changeParticipants = (delta) => { - form.value.maxParticipants = Math.max(2, Math.min(10, form.value.maxParticipants + delta)); + form.value.maxParticipants = Math.max( + minParticipants.value, + Math.min(maxParticipants.value, form.value.maxParticipants + delta) + ); }; const handleSubmit = async () => { diff --git a/pwuniapp/pages/user/index.vue b/pwuniapp/pages/user/index.vue index 1cf5dd8..3c8ba51 100644 --- a/pwuniapp/pages/user/index.vue +++ b/pwuniapp/pages/user/index.vue @@ -206,6 +206,74 @@ + + + + + 陪伴师 / 大神入驻实名申请 + + + + + + + + 提交真实有效资料,平台将在 24 小时内由运营团队人工核验后开通接单工作台。 + + + + 真实姓名 + + + + + 身份证号码 + + + + + 主营陪伴品类 + + + {{ trk }} + + + + + + 期望服务单价 (元/小时,平台限价: ¥{{ expertPriceMin }} - ¥{{ expertPriceMax }}) + + + + + 技能专长标签 (最多选6项) + + + {{ tg }} + + + + + + + + + + + + @@ -546,13 +614,22 @@ const handleRecharge = async () => { const handleWithdraw = () => { const comm = Number(user.value.commission || 0); + const finRules = platformConfigs.value.finance_rules || {}; + const minWithdraw = Number(finRules.minWithdrawAmount) || 50; + const withdrawDesc = finRules.withdrawDesc || '提现将自动发起微信企业付款打款到微信零钱。'; + if (comm <= 0) { uni.showToast({ title: '暂无可用提现佣金', icon: 'none' }); return; } + if (comm < minWithdraw) { + uni.showToast({ title: `提现金额须满 ¥${minWithdraw} 元起提`, icon: 'none' }); + return; + } + uni.showModal({ title: '佣金提现申请', - content: `当前可提现佣金为 ¥${user.value.commission},提现将自动发起微信企业付款打款到微信零钱。`, + content: `当前可提现佣金为 ¥${user.value.commission} (最低满¥${minWithdraw}起提)。\n${withdrawDesc}`, confirmText: '确认提现', confirmColor: '#10B981', success: async (res) => { @@ -579,14 +656,99 @@ const handleWithdraw = () => { }; const showInvitePoster = () => { + const finRules = platformConfigs.value.finance_rules || {}; + const firstRate = finRules.firstReferralPercent != null ? finRules.firstReferralPercent : 5.0; + const secondRate = finRules.secondReferralPercent != null ? finRules.secondReferralPercent : 2.0; + const totalRate = (Number(firstRate) + Number(secondRate)).toFixed(1); + uni.showModal({ title: '二级分销推广', - content: `您的专属邀请码为:PW${user.value.id}\n已成功邀请 ${user.value.invitedCount} 位好友!\n好友下单您将实时享受最高 15% 自动分润提成。`, + content: `您的专属邀请码为:PW${user.value.id}\n已成功邀请 ${user.value.invitedCount} 位好友!\n好友下单享受直推 ${firstRate}% + 间推 ${secondRate}%,全网自动分润最高达 ${totalRate}%!`, showCancel: false, - confirmText: '保存太阳码' + confirmText: '保存推广码' }); }; +const showExpertCertModal = ref(false); +const expertCertForm = ref({ + realName: '', + idCard: '', + track: '同城陪吃', + hourlyRate: 88, + tags: [] +}); + +const expertPriceMin = computed(() => { + return platformConfigs.value.expert_rules?.minHourlyRate || 30; +}); +const expertPriceMax = computed(() => { + return platformConfigs.value.expert_rules?.maxHourlyRate || 500; +}); +const expertDefaultRate = computed(() => { + return platformConfigs.value.expert_rules?.defaultHourlyRate || 88; +}); +const expertCandidateTags = computed(() => { + return platformConfigs.value.expert_rules?.candidateTags || [ + '国服王者', '声优达人', '幽默风趣', '随叫随到', '暖心倾听', + '百星野王', '金牌探店', '量贩麦霸', '微醺搭子' + ]; +}); + +const toggleCertTag = (tag) => { + const idx = expertCertForm.value.tags.indexOf(tag); + if (idx > -1) { + expertCertForm.value.tags.splice(idx, 1); + } else { + if (expertCertForm.value.tags.length >= 6) { + uni.showToast({ title: '最多选择6个技能标签', icon: 'none' }); + return; + } + expertCertForm.value.tags.push(tag); + } +}; + +const submitExpertCertForm = async () => { + if (!expertCertForm.value.realName || !expertCertForm.value.realName.trim()) { + uni.showToast({ title: '请输入真实姓名', icon: 'none' }); + return; + } + if (!expertCertForm.value.idCard || expertCertForm.value.idCard.length < 15) { + uni.showToast({ title: '请输入有效身份证号', icon: 'none' }); + return; + } + + const rate = Number(expertCertForm.value.hourlyRate) || expertDefaultRate.value; + if (rate < expertPriceMin.value || rate > expertPriceMax.value) { + uni.showToast({ title: `单价需在 ¥${expertPriceMin.value} - ¥${expertPriceMax.value} 之间`, icon: 'none' }); + return; + } + + uni.showLoading({ title: '正在提交认证...' }); + try { + await applyExpertCert({ + userId: user.value.id || 1001, + name: expertCertForm.value.realName.trim(), + idCard: expertCertForm.value.idCard.trim(), + track: expertCertForm.value.track, + hourlyRate: rate, + tags: expertCertForm.value.tags, + avatar: user.value.avatar, + gender: user.value.gender === 'male' ? '男' : '女' + }); + setExpertCertStatus(CERT_STATUS.PENDING); + showExpertCertModal.value = false; + uni.hideLoading(); + uni.showModal({ + title: '申请已提交', + content: '您的入驻申请已成功提交至平台审核中心,运营团队核准后即可自动开通接单工作台。', + showCancel: false, + confirmText: '我知道了' + }); + } catch (e) { + uni.hideLoading(); + uni.showToast({ title: e.message || '提交申请失败', icon: 'none' }); + } +}; const applyExpert = () => { const certStatus = getExpertCertStatus(); @@ -606,26 +768,15 @@ const applyExpert = () => { }); return; } - uni.showModal({ - title: '申请成为陪玩大神', - content: '需开通实名认证以承接线上游戏与同城生活陪伴订单。是否确认提交当前用户实名认证?', - confirmText: '提交入驻', - cancelText: '取消', - confirmColor: '#10B981', - success: (res) => { - if (res.confirm) { - setExpertCertStatus(CERT_STATUS.APPROVED); - uni.showToast({ - title: '实名认证已通过!接单权限已开通', - icon: 'success', - duration: 1500 - }); - uni.navigateTo({ - url: '/pages/expert/workspace' - }); - } - } - }); + + expertCertForm.value = { + realName: '', + idCard: '', + track: '同城陪吃', + hourlyRate: expertDefaultRate.value, + tags: [] + }; + showExpertCertModal.value = true; }; const contactService = () => { @@ -1475,6 +1626,73 @@ const navTo = (url) => { } } +.expert-cert-modal { + max-height: 85vh; + overflow-y: auto; + + .cert-alert { + background: rgba(99, 102, 241, 0.08); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 12rpx; + padding: 16rpx 20rpx; + margin-bottom: 24rpx; + + .cert-alert-text { + font-size: 24rpx; + color: #6366F1; + line-height: 1.5; + } + } + + .track-chips, .tags-chips { + display: flex; + flex-wrap: wrap; + gap: 14rpx; + margin-top: 10rpx; + + .track-chip, .tag-pill { + font-size: 24rpx; + padding: 12rpx 24rpx; + border-radius: 30rpx; + background: var(--bg-card, #1E293B); + color: #94A3B8; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.2s ease; + + &.active { + background: #6366F1; + color: #FFFFFF; + border-color: #6366F1; + box-shadow: 0 4rpx 12rpx rgba(99, 102, 241, 0.35); + } + } + } +} + +.theme-warm .expert-cert-modal { + .cert-alert { + background: #FFF7ED; + border-color: #FDBA74; + + .cert-alert-text { + color: #EA580C; + } + } + + .track-chips .track-chip, .tags-chips .tag-pill { + background: #FDFBF7; + color: #78716C; + border-color: #E7E2D8; + + &.active { + background: linear-gradient(135deg, #EA580C, #F97316); + color: #FFFFFF; + border-color: #EA580C; + box-shadow: 0 4rpx 14rpx rgba(234, 88, 12, 0.35); + } + } +} + @keyframes glowSpin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } diff --git a/server/src/db/mysql.ts b/server/src/db/mysql.ts index f9223ce..d6daafb 100644 --- a/server/src/db/mysql.ts +++ b/server/src/db/mysql.ts @@ -721,6 +721,25 @@ async function seedInitialDataIfEmpty(conn: any): Promise { ] }); + 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 { ('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 { 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]); + } } } diff --git a/server/src/db/repository.ts b/server/src/db/repository.ts index fc70708..eee7103 100644 --- a/server/src/db/repository.ts +++ b/server/src/db/repository.ts @@ -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 { + 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 { 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(); diff --git a/server/src/index.ts b/server/src/index.ts index ec28a98..a3c036e 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -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); diff --git a/server/tests/content-security.spec.ts b/server/tests/content-security.spec.ts index 8370139..f446c7a 100644 --- a/server/tests/content-security.spec.ts +++ b/server/tests/content-security.spec.ts @@ -221,5 +221,47 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安 expect(emojiRegex.test(content)).toBe(false); }); }); + + describe('7. 财务分销规则、组局预算规范与大神入驻申请闭环验证 (finance_rules & activity_rules)', () => { + it('MySQL 数据库与种子数据必须注册 finance_rules 与 activity_rules 配置项', () => { + const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'); + const content = fs.readFileSync(mysqlPath, 'utf-8'); + expect(content).toContain('finance_rules'); + expect(content).toContain('platformRatePercent'); + expect(content).toContain('firstReferralPercent'); + expect(content).toContain('activity_rules'); + expect(content).toContain('minParticipants'); + expect(content).toContain('presetPerPersonFees'); + }); + + it('管理后台 platform-config 必须包含 Tab 9 财务规则与 Tab 10 组局预算面板', () => { + const adminConfigPath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'safety', 'platform-config', 'index.vue'); + const content = fs.readFileSync(adminConfigPath, 'utf-8'); + expect(content).toContain('finance_rules_config'); + expect(content).toContain('财务抽成与分销规则'); + expect(content).toContain('activity_rules_config'); + expect(content).toContain('组局发单与预算规范'); + expect(content).toContain('financeRulesForm'); + expect(content).toContain('activityRulesForm'); + }); + + it('服务端路由必须提供 POST /api/companions/apply 真实申请接口且校验姓名/身份证/安全', () => { + const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts'); + const content = fs.readFileSync(serverIndexPath, 'utf-8'); + expect(content).toContain("app.post('/api/companions/apply'"); + expect(content).toContain('真实姓名不能为空'); + expect(content).toContain('身份证号不能为空'); + }); + + it('小程序个人中心与发起组局页面必须动态绑定配置并实现门槛保护', () => { + const userIndexContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'), 'utf-8'); + const createActContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'create.vue'), 'utf-8'); + expect(userIndexContent).toContain('applyExpertCert'); + expect(userIndexContent).toContain('showExpertCertModal'); + expect(userIndexContent).toContain('finance_rules'); + expect(createActContent).toContain('presetPerPersonFees'); + expect(createActContent).toContain('activity_rules'); + }); + }); });