feat: 资金冻结规则改为选项式条件与可增减分期
去掉 JSON 手填,支持勾选条件、自由添加分期步,并配置用户可见冻结说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="默认关闭。未开启时结算全额进可提现,与现网一致。仅对当前顶部所选俱乐部生效。"
|
||||
title="打开「启用冻结」并保存后,该角色新入账立即按规则冻结;旧单仍按当时快照解冻。关闭则新入账全额可提现。定时任务约每 5 分钟扫描到期/条件解冻。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<div v-if="clubId" class="club-hint">当前俱乐部:{{ clubId }}</div>
|
||||
@@ -24,16 +24,17 @@
|
||||
</el-tabs>
|
||||
|
||||
<div v-if="form" class="form-card">
|
||||
<el-form label-width="140px">
|
||||
<el-form label-width="148px">
|
||||
<el-divider content-position="left">开关与冻结额</el-divider>
|
||||
<el-form-item label="启用冻结">
|
||||
<el-switch v-model="form.enabled" />
|
||||
<span class="tip">关闭=该角色入账不冻结</span>
|
||||
<el-switch v-model="form.enabled" active-text="已开启" inactive-text="关闭" />
|
||||
<span class="tip">保存后立即对新入账生效</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="冻结方式">
|
||||
<el-radio-group v-model="form.freeze_mode">
|
||||
<el-radio :label="1">按比例</el-radio>
|
||||
<el-radio :label="2">固定额</el-radio>
|
||||
<el-radio :label="3">比例+上限</el-radio>
|
||||
<el-radio :label="3">比例 + 单笔上限</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.freeze_mode !== 2" label="冻结比例">
|
||||
@@ -45,7 +46,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.freeze_mode === 3" label="单笔上限">
|
||||
<el-input-number v-model="form.freeze_cap" :min="0" :precision="2" />
|
||||
<span class="tip">0=不限</span>
|
||||
<span class="tip">0 = 不限</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="最低入账才冻">
|
||||
<el-input-number v-model="form.min_credit_to_freeze" :min="0" :precision="2" />
|
||||
@@ -57,50 +58,173 @@
|
||||
<el-checkbox label="kaohe_fee">考核代收费</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户可见说明">
|
||||
<el-input
|
||||
v-model="form.display_reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
placeholder="例:为保障订单售后与纠纷处理,部分佣金将暂存为冻结保障金,按规则分期解冻后自动转入可提现。"
|
||||
/>
|
||||
<div class="tip block">小程序冻结旁「?」点开后展示这段文字</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">解冻触发</el-divider>
|
||||
<el-form-item label="解冻触发">
|
||||
<el-select v-model="form.unfreeze_trigger" style="width: 260px">
|
||||
<el-option label="仅到期" value="time_only" />
|
||||
<el-select v-model="form.unfreeze_trigger" style="width: 280px">
|
||||
<el-option label="仅到期(到天数就解)" value="time_only" />
|
||||
<el-option label="仅条件达标" value="condition_only" />
|
||||
<el-option label="到期且达标" value="time_and_condition" />
|
||||
<el-option label="到期或达标" value="time_or_condition" />
|
||||
<el-option label="仅手动" value="manual_only" />
|
||||
<el-option label="到期且条件达标" value="time_and_condition" />
|
||||
<el-option label="到期或条件达标(先到先解)" value="time_or_condition" />
|
||||
<el-option label="仅手动(后台解)" value="manual_only" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="最短冻结天数">
|
||||
<el-input-number v-model="form.freeze_min_days" :min="0" />
|
||||
<span class="tip">一次全解 / 仅条件模式:未满此天数不解</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="解冻节奏">
|
||||
<el-radio-group v-model="form.schedule_mode">
|
||||
<el-radio label="all_at_once">一次全解</el-radio>
|
||||
<el-radio label="installment">分期解冻</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="最短冻结天数">
|
||||
<el-input-number v-model="form.freeze_min_days" :min="0" />
|
||||
<span class="tip">一次全解时生效;分期以各步 after_days 为准</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.schedule_mode === 'installment'" label="分期 JSON">
|
||||
<el-input
|
||||
v-model="installmentText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder='{"steps":[{"seq":1,"after_days":7,"type":"ratio","value":0.3,"need_condition":false}]}'
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="条件 JSON">
|
||||
<el-input
|
||||
v-model="conditionText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder='{"min_completed_orders":30,"max_fine_rate":0.05,"fine_rate_window_days":30}'
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" maxlength="200" />
|
||||
|
||||
<template v-if="form.schedule_mode === 'installment'">
|
||||
<el-form-item label="分期计划">
|
||||
<div class="steps-wrap">
|
||||
<div class="steps-toolbar">
|
||||
<el-button type="primary" link @click="addStep">+ 添加一期</el-button>
|
||||
<span class="tip">可任意加减;最后一期建议选「剩余全部」吃干净尾差</span>
|
||||
</div>
|
||||
<el-table :data="steps" border size="small" style="width: 100%">
|
||||
<el-table-column label="期数" width="70">
|
||||
<template #default="{ $index }">第{{ $index + 1 }}期</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="满多少天可解" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.after_days" :min="0" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="解冻方式" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.type" size="small">
|
||||
<el-option label="按比例" value="ratio" />
|
||||
<el-option label="固定金额" value="fixed" />
|
||||
<el-option label="剩余全部" value="remain" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数值" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="row.type !== 'remain'"
|
||||
v-model="row.value"
|
||||
:min="0"
|
||||
:max="row.type === 'ratio' ? 1 : 999999"
|
||||
:step="row.type === 'ratio' ? 0.05 : 1"
|
||||
:precision="row.type === 'ratio' ? 2 : 2"
|
||||
size="small"
|
||||
/>
|
||||
<span v-else class="tip">全部剩余</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="本步需达标" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.need_condition" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="steps.length <= 1"
|
||||
@click="removeStep($index)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-divider content-position="left">达标条件(选项)</el-divider>
|
||||
<el-form-item label="启用条件">
|
||||
<div class="cond-list">
|
||||
<div class="cond-row">
|
||||
<el-checkbox v-model="cond.enable_min_completed_orders">最低完成单量</el-checkbox>
|
||||
<el-input-number
|
||||
v-model="cond.min_completed_orders"
|
||||
:min="0"
|
||||
:disabled="!cond.enable_min_completed_orders"
|
||||
size="small"
|
||||
/>
|
||||
<span class="tip">窗口内完成单 ≥ 此值</span>
|
||||
</div>
|
||||
<div class="cond-row">
|
||||
<el-checkbox v-model="cond.enable_max_fine_rate">最高罚款率</el-checkbox>
|
||||
<el-input-number
|
||||
v-model="cond.max_fine_rate"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
:precision="4"
|
||||
:disabled="!cond.enable_max_fine_rate"
|
||||
size="small"
|
||||
/>
|
||||
<span class="tip">0.05 = 罚款率不超过 5%</span>
|
||||
<span class="tip">窗口</span>
|
||||
<el-input-number
|
||||
v-model="cond.fine_rate_window_days"
|
||||
:min="1"
|
||||
:disabled="!cond.enable_max_fine_rate"
|
||||
size="small"
|
||||
/>
|
||||
<span class="tip">天</span>
|
||||
</div>
|
||||
<div class="cond-row">
|
||||
<el-checkbox v-model="cond.enable_max_complaint_rate">最高投诉率</el-checkbox>
|
||||
<el-input-number
|
||||
v-model="cond.max_complaint_rate"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
:precision="4"
|
||||
:disabled="!cond.enable_max_complaint_rate"
|
||||
size="small"
|
||||
/>
|
||||
<span class="tip">窗口</span>
|
||||
<el-input-number
|
||||
v-model="cond.complaint_rate_window_days"
|
||||
:min="1"
|
||||
:disabled="!cond.enable_max_complaint_rate"
|
||||
size="small"
|
||||
/>
|
||||
<span class="tip">天</span>
|
||||
</div>
|
||||
<div class="cond-row">
|
||||
<el-checkbox v-model="cond.enable_min_valid_invites">最低有效邀请</el-checkbox>
|
||||
<el-input-number
|
||||
v-model="cond.min_valid_invites"
|
||||
:min="0"
|
||||
:disabled="!cond.enable_min_valid_invites"
|
||||
size="small"
|
||||
/>
|
||||
<span class="tip">管事/组长更适用</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="试算(入账100)">
|
||||
<span>可提现 {{ preview.available }} / 冻结 {{ preview.frozen }}</span>
|
||||
<el-button link type="primary" @click="refreshPreview">重算</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存该角色规则</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">
|
||||
{{ form.enabled ? '保存并启用' : '保存(保持关闭)' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -121,10 +245,66 @@ const clubId = ref('')
|
||||
const roles = ref([])
|
||||
const activeRole = ref('dashou')
|
||||
const form = ref(null)
|
||||
const installmentText = ref('')
|
||||
const conditionText = ref('')
|
||||
const steps = ref([])
|
||||
const cond = reactive({
|
||||
enable_min_completed_orders: false,
|
||||
min_completed_orders: 30,
|
||||
enable_max_fine_rate: false,
|
||||
max_fine_rate: 0.05,
|
||||
fine_rate_window_days: 30,
|
||||
enable_max_complaint_rate: false,
|
||||
max_complaint_rate: 0.05,
|
||||
complaint_rate_window_days: 30,
|
||||
enable_min_valid_invites: false,
|
||||
min_valid_invites: 0,
|
||||
})
|
||||
const preview = reactive({ available: '100', frozen: '0' })
|
||||
|
||||
function defaultStep() {
|
||||
return { after_days: 7, type: 'ratio', value: 0.3, need_condition: false }
|
||||
}
|
||||
|
||||
function syncCondFromJson(cj) {
|
||||
const c = cj || {}
|
||||
const minOrders = Number(c.min_completed_orders || 0)
|
||||
const maxFine = Number(c.max_fine_rate ?? 1)
|
||||
const maxComp = Number(c.max_complaint_rate ?? 1)
|
||||
const minInv = Number(c.min_valid_invites || 0)
|
||||
cond.enable_min_completed_orders = c.enable_min_completed_orders != null
|
||||
? !!c.enable_min_completed_orders
|
||||
: minOrders > 0
|
||||
cond.min_completed_orders = minOrders || 30
|
||||
cond.enable_max_fine_rate = c.enable_max_fine_rate != null
|
||||
? !!c.enable_max_fine_rate
|
||||
: maxFine >= 0 && maxFine < 1
|
||||
cond.max_fine_rate = maxFine < 1 ? maxFine : 0.05
|
||||
cond.fine_rate_window_days = Number(c.fine_rate_window_days || 30)
|
||||
cond.enable_max_complaint_rate = c.enable_max_complaint_rate != null
|
||||
? !!c.enable_max_complaint_rate
|
||||
: maxComp >= 0 && maxComp < 1
|
||||
cond.max_complaint_rate = maxComp < 1 ? maxComp : 0.05
|
||||
cond.complaint_rate_window_days = Number(c.complaint_rate_window_days || 30)
|
||||
cond.enable_min_valid_invites = c.enable_min_valid_invites != null
|
||||
? !!c.enable_min_valid_invites
|
||||
: minInv > 0
|
||||
cond.min_valid_invites = minInv
|
||||
}
|
||||
|
||||
function buildConditionJson() {
|
||||
return {
|
||||
enable_min_completed_orders: !!cond.enable_min_completed_orders,
|
||||
min_completed_orders: Number(cond.min_completed_orders || 0),
|
||||
enable_max_fine_rate: !!cond.enable_max_fine_rate,
|
||||
max_fine_rate: Number(cond.max_fine_rate || 0),
|
||||
fine_rate_window_days: Number(cond.fine_rate_window_days || 30),
|
||||
enable_max_complaint_rate: !!cond.enable_max_complaint_rate,
|
||||
max_complaint_rate: Number(cond.max_complaint_rate || 0),
|
||||
complaint_rate_window_days: Number(cond.complaint_rate_window_days || 30),
|
||||
enable_min_valid_invites: !!cond.enable_min_valid_invites,
|
||||
min_valid_invites: Number(cond.min_valid_invites || 0),
|
||||
}
|
||||
}
|
||||
|
||||
function syncFormFromRole(role) {
|
||||
const item = roles.value.find((r) => r.role === role)
|
||||
if (!item) {
|
||||
@@ -139,9 +319,18 @@ function syncFormFromRole(role) {
|
||||
min_credit_to_freeze: Number(item.min_credit_to_freeze || 0),
|
||||
freeze_min_days: Number(item.freeze_min_days || 0),
|
||||
source_scope: Array.isArray(item.source_scope) ? [...item.source_scope] : ['order_settle'],
|
||||
display_reason: item.display_reason || item.remark || '',
|
||||
}
|
||||
installmentText.value = JSON.stringify(item.installment_json || {}, null, 2)
|
||||
conditionText.value = JSON.stringify(item.condition_json || {}, null, 2)
|
||||
const rawSteps = item.installment_json?.steps
|
||||
steps.value = Array.isArray(rawSteps) && rawSteps.length
|
||||
? rawSteps.map((s) => ({
|
||||
after_days: Number(s.after_days || 0),
|
||||
type: s.type || 'ratio',
|
||||
value: Number(s.value ?? (s.type === 'remain' ? 1 : 0.3)),
|
||||
need_condition: !!s.need_condition,
|
||||
}))
|
||||
: [defaultStep(), { after_days: 14, type: 'ratio', value: 0.3, need_condition: true }, { after_days: 30, type: 'remain', value: 1, need_condition: true }]
|
||||
syncCondFromJson(item.condition_json)
|
||||
preview.available = item.preview_100?.available || '100'
|
||||
preview.frozen = item.preview_100?.frozen || '0'
|
||||
}
|
||||
@@ -150,6 +339,15 @@ function onTabChange(name) {
|
||||
syncFormFromRole(name)
|
||||
}
|
||||
|
||||
function addStep() {
|
||||
steps.value.push(defaultStep())
|
||||
}
|
||||
|
||||
function removeStep(index) {
|
||||
if (steps.value.length <= 1) return
|
||||
steps.value.splice(index, 1)
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -204,20 +402,14 @@ watch(
|
||||
|
||||
async function save() {
|
||||
if (!form.value) return
|
||||
let installment_json = {}
|
||||
let condition_json = {}
|
||||
try {
|
||||
installment_json = installmentText.value ? JSON.parse(installmentText.value) : {}
|
||||
} catch {
|
||||
ElMessage.error('分期 JSON 格式错误')
|
||||
return
|
||||
}
|
||||
try {
|
||||
condition_json = conditionText.value ? JSON.parse(conditionText.value) : {}
|
||||
} catch {
|
||||
ElMessage.error('条件 JSON 格式错误')
|
||||
if (!form.value.source_scope?.length) {
|
||||
ElMessage.error('请至少选择一个入账来源')
|
||||
return
|
||||
}
|
||||
const installment_json = form.value.schedule_mode === 'installment'
|
||||
? { steps: steps.value.map((s, i) => ({ ...s, seq: i + 1 })) }
|
||||
: { steps: [{ seq: 1, after_days: form.value.freeze_min_days || 0, type: 'remain', value: 1, need_condition: false }] }
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await request.post(JITUAN_API.fundFreezeConfig, {
|
||||
@@ -235,11 +427,14 @@ async function save() {
|
||||
schedule_mode: form.value.schedule_mode,
|
||||
freeze_min_days: form.value.freeze_min_days,
|
||||
installment_json,
|
||||
condition_json,
|
||||
remark: form.value.remark,
|
||||
condition_json: buildConditionJson(),
|
||||
display_reason: form.value.display_reason,
|
||||
remark: form.value.display_reason,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('已保存')
|
||||
ElMessage.success(form.value.enabled
|
||||
? '已保存并启用:新入账将按规则冻结,解冻由定时任务自动处理'
|
||||
: '已保存(未启用):新入账仍全额可提现')
|
||||
await loadConfig()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '保存失败')
|
||||
@@ -279,4 +474,27 @@ onMounted(loadConfig)
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
}
|
||||
.tip.block {
|
||||
display: block;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
.steps-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
.steps-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.cond-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.cond-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user