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 次删除

查看文件

@@ -469,6 +469,115 @@
</a-card>
</div>
</a-tab-pane>
<!-- Tab 9: 财务抽成与推广分销规则 -->
<a-tab-pane key="finance_rules_config" title="财务抽成与分销规则">
<div class="tab-pane-content">
<a-alert type="warning" style="margin-bottom: 20px">
在此灵活调控全平台陪玩订单抽成比例、二级分销裂变返润比例以及陪伴师/玩家佣金提现限制门槛,小程序端推广说明与提现流程将动态同步。
</a-alert>
<a-card title="平台抽成与裂变分销费率" :bordered="false" style="background: var(--color-fill-2)">
<a-form :model="financeRulesForm.valueJson" layout="vertical">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="平台订单抽成比例 (%)">
<a-input-number v-model="financeRulesForm.valueJson.platformRatePercent" :min="0" :max="50" :precision="1" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="一级邀请直接返佣比例 (%)">
<a-input-number v-model="financeRulesForm.valueJson.firstReferralPercent" :min="0" :max="30" :precision="1" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="二级邀请间接返佣比例 (%)">
<a-input-number v-model="financeRulesForm.valueJson.secondReferralPercent" :min="0" :max="20" :precision="1" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="单笔提现最低门槛 (元)">
<a-input-number v-model="financeRulesForm.valueJson.minWithdrawAmount" :min="1" :max="5000" placeholder="如 50" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="单笔提现最高上限 (元)">
<a-input-number v-model="financeRulesForm.valueJson.maxWithdrawAmount" :min="100" :max="100000" placeholder="如 20000" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="提现手续费率 (%)">
<a-input-number v-model="financeRulesForm.valueJson.withdrawFeePercent" :min="0" :max="10" :precision="1" placeholder="如 0" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="小程序提现提示与到账说明文案">
<a-textarea v-model="financeRulesForm.valueJson.withdrawDesc" :auto-size="{ minRows: 2, maxRows: 4 }" />
</a-form-item>
<div style="margin-top: 16px">
<a-button type="primary" :loading="saving" @click="saveFinanceRulesConfig">
保存财务抽成与分销配置
</a-button>
</div>
</a-form>
</a-card>
</div>
</a-tab-pane>
<!-- Tab 10: 组局发单与预算预设规范 -->
<a-tab-pane key="activity_rules_config" title="组局发单与预算规范">
<div class="tab-pane-content">
<a-alert type="info" style="margin-bottom: 20px">
在此设定同城活动组局的招募席位上下限、AA制默认分摊基准,以及房主请客/悬赏发单时展示的快捷赏金与总预算梯队。
</a-alert>
<a-card title="活动席位与预设金额梯队" :bordered="false" style="background: var(--color-fill-2)">
<a-form :model="activityRulesForm.valueJson" layout="vertical">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="最小招募人数 (人)">
<a-input-number v-model="activityRulesForm.valueJson.minParticipants" :min="2" :max="10" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="最大招募人数 (人)">
<a-input-number v-model="activityRulesForm.valueJson.maxParticipants" :min="2" :max="50" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="默认推荐招募人数 (人)">
<a-input-number v-model="activityRulesForm.valueJson.defaultParticipants" :min="2" :max="20" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="单人出价快捷赏金梯队 (多个金额用逗号隔开,单位: 元)">
<a-input v-model="presetPerPersonFeesText" placeholder="如:200, 300, 500, 800, 1000" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="包场总预算快捷梯队 (多个金额用逗号隔开,单位: 元)">
<a-input v-model="presetTotalFeesText" placeholder="如:500, 1000, 2000, 3000, 5000" />
</a-form-item>
</a-col>
</a-row>
<div style="margin-top: 16px">
<a-button type="primary" :loading="saving" @click="saveActivityRulesConfig">
保存组局发单规范配置
</a-button>
</div>
</a-form>
</a-card>
</div>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
@@ -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(', ');
}
}
});
}

查看文件

@@ -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;
}
}

查看文件

@@ -80,12 +80,12 @@
<view class="stepper-row">
<view class="stepper-info">
<text class="row-label">招募席位 (人)</text>
<text class="sub-label">包含房主在内的组局总人数</text>
<text class="sub-label">包含房主在内的组局总人数 ({{ minParticipants }}-{{ maxParticipants }}人)</text>
</view>
<view class="stepper-box">
<button class="step-btn btn-active" :disabled="form.maxParticipants <= 2" @click="changeParticipants(-1)">-</button>
<button class="step-btn btn-active" :disabled="form.maxParticipants <= minParticipants" @click="changeParticipants(-1)">-</button>
<text class="step-val">{{ form.maxParticipants }} 人</text>
<button class="step-btn btn-active" :disabled="form.maxParticipants >= 10" @click="changeParticipants(1)">+</button>
<button class="step-btn btn-active" :disabled="form.maxParticipants >= maxParticipants" @click="changeParticipants(1)">+</button>
</view>
</view>
@@ -138,7 +138,7 @@
<text class="preset-label">快捷赏金:</text>
<view class="preset-chips">
<view
v-for="amt in [200, 300, 500, 800, 1000]"
v-for="amt in presetPerPersonFees"
:key="amt"
class="preset-chip btn-active"
:class="{ selected: Number(form.perPersonFee) === amt }"
@@ -165,7 +165,7 @@
<text class="preset-label">快捷预算:</text>
<view class="preset-chips">
<view
v-for="amt in [500, 1000, 2000, 3000, 5000]"
v-for="amt in presetTotalFees"
:key="amt"
class="preset-chip btn-active"
:class="{ selected: Number(form.totalFee) === amt }"
@@ -349,11 +349,17 @@ const recommendPublicZones = ref([
'北京市海淀区五道口购物中心 (高校商圈)'
]);
const minParticipants = ref(2);
const maxParticipants = ref(10);
const presetPerPersonFees = ref([200, 300, 500, 800, 1000]);
const presetTotalFees = ref([500, 1000, 2000, 3000, 5000]);
const loadLivePlaces = async () => {
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 () => {

查看文件

@@ -206,6 +206,74 @@
</view>
</view>
<!-- 大神实名认证与资质入驻申请弹窗 (任务四:真实资料与后台审核) -->
<view class="profile-edit-modal-mask" v-if="showExpertCertModal" @click="showExpertCertModal = false">
<view class="profile-edit-modal expert-cert-modal" @click.stop>
<view class="modal-header">
<text class="modal-title">陪伴师 / 大神入驻实名申请</text>
<view class="btn-close btn-active" @click="showExpertCertModal = false">
<image class="close-svg" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<view class="modal-body">
<view class="cert-alert">
<text class="cert-alert-text">提交真实有效资料,平台将在 24 小时内由运营团队人工核验后开通接单工作台。</text>
</view>
<view class="edit-field">
<text class="field-label">真实姓名</text>
<input class="field-input" v-model="expertCertForm.realName" placeholder="如:张三 (须与身份证一致)" placeholder-class="field-placeholder" />
</view>
<view class="edit-field">
<text class="field-label">身份证号码</text>
<input class="field-input" v-model="expertCertForm.idCard" maxlength="18" placeholder="18位二代身份证号" placeholder-class="field-placeholder" />
</view>
<view class="edit-field">
<text class="field-label">主营陪伴品类</text>
<view class="track-chips">
<view
v-for="trk in ['同城陪吃', 'KTV陪唱', '同城陪喝', '线上电竞', '同城桌游']"
:key="trk"
class="track-chip btn-active"
:class="{ active: expertCertForm.track === trk }"
@click="expertCertForm.track = trk"
>
{{ trk }}
</view>
</view>
</view>
<view class="edit-field">
<text class="field-label">期望服务单价 (元/小时,平台限价: ¥{{ expertPriceMin }} - ¥{{ expertPriceMax }})</text>
<input class="field-input price-num-input" type="number" v-model="expertCertForm.hourlyRate" :placeholder="`推荐单价 ¥${expertDefaultRate}`" placeholder-class="field-placeholder" />
</view>
<view class="edit-field" v-if="expertCandidateTags.length > 0">
<text class="field-label">技能专长标签 (最多选6项)</text>
<view class="tags-chips">
<view
v-for="tg in expertCandidateTags"
:key="tg"
class="tag-pill btn-active"
:class="{ active: expertCertForm.tags.includes(tg) }"
@click="toggleCertTag(tg)"
>
{{ tg }}
</view>
</view>
</view>
</view>
<view class="modal-footer">
<button class="btn-modal btn-cancel btn-active" @click="showExpertCertModal = false">取消</button>
<button class="btn-modal btn-save btn-active" @click="submitExpertCertForm">提交审核</button>
</view>
</view>
</view>
<!-- 底部自定义导航栏 (动态自适应发单人4项 / 接单人3项) -->
<custom-tab-bar current="user" />
@@ -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); }

查看文件

@@ -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);

查看文件

@@ -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');
});
});
});