248 lines
12 KiB
Markdown
248 lines
12 KiB
Markdown
---
|
||
name: free credits activity edit
|
||
overview: 在管理后台活动配置编辑表单中为活动类型 11(首充前免费余额定格与分档释放 / Free Credits)增加专属表单分支,并补齐字典与服务端校验;金额输入沿用 type==10 的「美元小数 → 千分位整数」模式,写入 ext_config 的 _qf 后缀键,与 slot_console `FreeCreditsLogic::configAmount()` 的读优先级一致。
|
||
todos:
|
||
- id: dict-activity-type-11
|
||
content: 字典 sm_system_dict_data 追加 code='activity_type' 的 value=11 / label='Free Credits(首充前免费余额)'(DB 或字典管理页)
|
||
status: completed
|
||
- id: edit-vue-type11-form
|
||
content: edit.vue 增加 v-if=type==11 的表单块(6 字段 + Banner)
|
||
status: completed
|
||
- id: edit-vue-skip-goods
|
||
content: edit.vue 让 type==11 跳过 goods 区块:扩展 onlyGift / 调整「添加赠送」与 goods 卡片的 v-if
|
||
status: completed
|
||
- id: edit-vue-submit-conv
|
||
content: edit.vue submit() 增加 type==11 的金额 ×1000 写入 _qf 键、清空 goods
|
||
status: completed
|
||
- id: edit-vue-setform-conv
|
||
content: edit.vue setFormData() 增加 type==11 的 _qf ÷1000 回填还原
|
||
status: completed
|
||
- id: validate-free-credits-ext
|
||
content: ActivityValidate 增加 checkFreeCreditsExt(extConfig) 方法,含必填、非负、max_unlock_per_recharge≥1、first_cash ≤ recharge_unlock 约束
|
||
status: completed
|
||
- id: controller-trigger-ext-check
|
||
content: ActivityController::save / update 在 checkData 后按 type==11 调用 checkFreeCreditsExt;update 兼容仅改状态请求
|
||
status: completed
|
||
- id: verify-end-to-end
|
||
content: 本地验证:新建/编辑/必填/业务约束/类型切换/C 端 FreeCreditsLogic 读取
|
||
status: completed
|
||
isProject: false
|
||
---
|
||
|
||
# Free Credits 活动后台编辑落地计划
|
||
|
||
## 目标
|
||
|
||
让运营在「活动管理」编辑弹窗里选活动类型 `11`(首充前免费余额)时,能直接编辑需求文档第 17 节列出的所有配置项;保存后落到 `s_recharge_gift_config.ext_config` JSON 字段,C 端 `FreeCreditsLogic` 立即生效。
|
||
|
||
## 现状要点
|
||
|
||
- `slot_console` 已实现 `RechargeGiftConfigModel::TYPE_FREE_CREDITS = 11`、`FreeCreditsLogic`、运行时配置读取(`ext_config.{key}_qf` 优先)。
|
||
- `slot_admin` 后端 [ActivityController](backend/slot_admin/app/game/controller/ActivityController.php) 走 `slotLib\services\ActivityService` 透传到 `slot_console innerapi/activity/*`,`ext_config` 已原样落库,**不需要改动 slot_console**。
|
||
- 管理前端 [edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue) 现仅对 6 / 9 / 10 做了 `v-if` 分支,`type==11` 无任何 UI;字典 `activity_type` 也无 value=11 条目。
|
||
|
||
## 字段映射(type=11 专属 `ext_config`)
|
||
|
||
UI 输入用美元小数,提交时 ×1000 写入 `_qf` 键(与 type==10 模式一致;`FreeCreditsLogic::configAmount()` 优先读 `_qf`):
|
||
|
||
- `win_threshold_qf` ← 首笔赢取门槛(默认 $50)
|
||
- `recharge_unlock_amount_qf` ← 累计充值解锁第一档(默认 $50)
|
||
- `first_cash_amount_qf` ← 第一档免打码金额(默认 $20)
|
||
- `package_amount_qf` ← 后续每档拆分金额(默认 $10)
|
||
- `subsequent_min_recharge_qf` ← 解锁下一档单笔充值下限(默认 $10)
|
||
- `max_unlock_per_recharge` ← 整数,每笔充值最多解锁档数(默认 1,**不走 _qf**)
|
||
- `banner_image` ← Free Play to Go 弹窗 Banner 图片 URL(可空)
|
||
|
||
## 改动清单
|
||
|
||
### 1. 字典:追加 `activity_type` value=11
|
||
|
||
在系统管理「字典管理 → activity_type」以有,不需要再执行了
|
||
|
||
### 2. 前端:[backend/slot_admin_vue/src/views/game/activity/edit.vue](backend/slot_admin_vue/src/views/game/activity/edit.vue)
|
||
|
||
#### 2.1 增加 type==11 表单块
|
||
|
||
参考 [edit.vue line 108-123](backend/slot_admin_vue/src/views/game/activity/edit.vue) type==10 的写法,新增:
|
||
|
||
```vue
|
||
<template v-if="formData.type == 11">
|
||
<a-col :span="24">
|
||
<a-form-item label="首笔赢取门槛($)" help="免费余额曾达到该值后解锁首页 Withdraw" :rules="[{ required: true, message: '必填' }]">
|
||
<a-input-number v-model="formData.ext_config.win_threshold" placeholder="如 50" :min="0" />
|
||
</a-form-item>
|
||
<a-form-item label="充值解锁门槛($)" help="累计真实充值满该金额释放第一档" :rules="[{ required: true, message: '必填' }]">
|
||
<a-input-number v-model="formData.ext_config.recharge_unlock_amount" placeholder="如 50" :min="0" />
|
||
</a-form-item>
|
||
<a-form-item label="免打码提现额($)" help="第一档可免打码直接提现金额" :rules="[{ required: true, message: '必填' }]">
|
||
<a-input-number v-model="formData.ext_config.first_cash_amount" placeholder="如 20" :min="0" />
|
||
</a-form-item>
|
||
<a-form-item label="解锁拆分金额($)" help="后续每档释放金额" :rules="[{ required: true, message: '必填' }]">
|
||
<a-input-number v-model="formData.ext_config.package_amount" placeholder="如 10" :min="0" />
|
||
</a-form-item>
|
||
<a-form-item label="后续解锁最小充值($)" help="单笔充值达到该金额才解锁下一档">
|
||
<a-input-number v-model="formData.ext_config.subsequent_min_recharge" placeholder="如 10" :min="0" />
|
||
</a-form-item>
|
||
<a-form-item label="每笔最多解锁档数" help="防止一笔充值解锁多档">
|
||
<a-input-number v-model="formData.ext_config.max_unlock_per_recharge" placeholder="如 1" :min="1" :precision="0" />
|
||
</a-form-item>
|
||
<a-form-item label="Banner 图片" help="Free Play to Go 底部弹窗 Banner">
|
||
<sa-upload-image v-model="formData.ext_config.banner_image" :limit="1" :multiple="false" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</template>
|
||
```
|
||
|
||
#### 2.2 让 type==11 跳过 goods 区块
|
||
|
||
- [edit.vue line 60](backend/slot_admin_vue/src/views/game/activity/edit.vue) `onlyGift` computed 追加 `|| formData.type == 11`:
|
||
|
||
```js
|
||
let onlyGift = computed(() => {
|
||
return formData.type == 7 || formData.type == 8 || formData.type == 9 || formData.type == 11;
|
||
})
|
||
```
|
||
|
||
- [edit.vue line 125](backend/slot_admin_vue/src/views/game/activity/edit.vue) 把「添加赠送」按钮与下方 `<a-row v-else>` 的 goods 卡片包成一组条件,排除 type==10 和 type==11:
|
||
|
||
```vue
|
||
<template v-if="formData.type !== 10 && formData.type !== 11">
|
||
<a-button type="primary" ... @click="add()">添加赠送</a-button>
|
||
</template>
|
||
|
||
<a-row :gutter="20" v-if="formData.type !== 9 && formData.type !== 10 && formData.type !== 11">
|
||
...goods 卡片...
|
||
</a-row>
|
||
```
|
||
|
||
(保留原 type==9 的 exchange 表格 v-if 不变。)
|
||
|
||
#### 2.3 `submit()` 中追加 type==11 的金额 ×1000 转换
|
||
|
||
[edit.vue line 370](backend/slot_admin_vue/src/views/game/activity/edit.vue) 类似 type==10 的处理,把 6 个美元字段写到 `_qf` 键,整数字段原样保留:
|
||
|
||
```js
|
||
if (formData.type === 11) {
|
||
const e = data.ext_config || {};
|
||
const toQf = (v) => v === '' || v == null ? undefined : Math.round(Number(v) * 1000);
|
||
data.ext_config = {
|
||
win_threshold_qf: toQf(e.win_threshold),
|
||
recharge_unlock_amount_qf: toQf(e.recharge_unlock_amount),
|
||
first_cash_amount_qf: toQf(e.first_cash_amount),
|
||
package_amount_qf: toQf(e.package_amount),
|
||
subsequent_min_recharge_qf: toQf(e.subsequent_min_recharge),
|
||
max_unlock_per_recharge: e.max_unlock_per_recharge == null ? undefined : Math.round(Number(e.max_unlock_per_recharge)),
|
||
banner_image: e.banner_image || '',
|
||
};
|
||
data.goods = [];
|
||
}
|
||
```
|
||
|
||
#### 2.4 `setFormData()` 中追加 type==11 的回填还原
|
||
|
||
[edit.vue line 344-351](backend/slot_admin_vue/src/views/game/activity/edit.vue) 仿照 type==10:
|
||
|
||
```js
|
||
if (data.type === 11 && data.ext_config) {
|
||
const e = data.ext_config;
|
||
formData.ext_config = {
|
||
win_threshold: e.win_threshold_qf != null ? e.win_threshold_qf / 1000 : e.win_threshold,
|
||
recharge_unlock_amount: e.recharge_unlock_amount_qf != null ? e.recharge_unlock_amount_qf / 1000 : e.recharge_unlock_amount,
|
||
first_cash_amount: e.first_cash_amount_qf != null ? e.first_cash_amount_qf / 1000 : e.first_cash_amount,
|
||
package_amount: e.package_amount_qf != null ? e.package_amount_qf / 1000 : e.package_amount,
|
||
subsequent_min_recharge: e.subsequent_min_recharge_qf != null ? e.subsequent_min_recharge_qf / 1000 : e.subsequent_min_recharge,
|
||
max_unlock_per_recharge: e.max_unlock_per_recharge ?? 1,
|
||
banner_image: e.banner_image || '',
|
||
};
|
||
}
|
||
```
|
||
|
||
### 3. 后端校验:[backend/slot_admin/app/game/validate/ActivityValidate.php](backend/slot_admin/app/game/validate/ActivityValidate.php)
|
||
|
||
ThinkValidate 对嵌套 JSON 支持有限,采用「在 Controller 按 `type` 触发额外校验」+「Validate 提供专用方法」的模式,避免污染既有 scene。
|
||
|
||
#### 3.1 ActivityValidate 增加一个公开方法
|
||
|
||
```php
|
||
/**
|
||
* Free Credits(type=11)专用 ext_config 校验。
|
||
*
|
||
* @param array $extConfig 前端提交的 ext_config,金额已 ×1000 写入 _qf 键
|
||
* @throws \think\exception\ValidateException
|
||
*/
|
||
public function checkFreeCreditsExt(array $extConfig): void
|
||
{
|
||
$required = [
|
||
'win_threshold_qf' => '首笔赢取门槛',
|
||
'recharge_unlock_amount_qf' => '充值解锁门槛',
|
||
'first_cash_amount_qf' => '免打码提现额',
|
||
'package_amount_qf' => '解锁拆分金额',
|
||
'subsequent_min_recharge_qf' => '后续解锁最小充值',
|
||
'max_unlock_per_recharge' => '每笔最多解锁档数',
|
||
];
|
||
foreach ($required as $key => $label) {
|
||
if (!isset($extConfig[$key]) || $extConfig[$key] === '' || (int)$extConfig[$key] < 0) {
|
||
throw new \think\exception\ValidateException("{$label}必须填写且为非负数");
|
||
}
|
||
}
|
||
if ((int)$extConfig['max_unlock_per_recharge'] < 1) {
|
||
throw new \think\exception\ValidateException('每笔最多解锁档数必须 ≥ 1');
|
||
}
|
||
// 业务约束:免打码提现额 ≤ 累计充值解锁门槛
|
||
if ((int)$extConfig['first_cash_amount_qf'] > (int)$extConfig['recharge_unlock_amount_qf']) {
|
||
throw new \think\exception\ValidateException('免打码提现额不能超过充值解锁门槛');
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.2 ActivityController save / update 触发
|
||
|
||
[ActivityController::save / update](backend/slot_admin/app/game/controller/ActivityController.php) 在 `checkData()` 之后追加:
|
||
|
||
```php
|
||
if ((int)input('type') === 11) {
|
||
$this->validate->checkFreeCreditsExt((array)input('ext_config', []));
|
||
}
|
||
```
|
||
|
||
`update` 里同样判断,且要兼容仅改状态的请求(无 `updateData` 时不校验 ext_config):
|
||
|
||
```php
|
||
if (!empty(input('updateData')) && (int)input('type') === 11) {
|
||
$this->validate->checkFreeCreditsExt((array)input('ext_config', []));
|
||
}
|
||
```
|
||
|
||
### 4. 不需要改的部分
|
||
|
||
- `slot_console` 侧 `ActivityConfigEntity::updateConfig` 已原样写入 `ext_config`,无变更。
|
||
- `Consts::ACTIVITY_TYPE_*` 不必新增 11;C 端已通过 `RechargeGiftConfigModel::TYPE_FREE_CREDITS` 引用。
|
||
- 不新增独立菜单页,复用通用「活动管理」编辑弹窗。
|
||
|
||
## 数据流
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
edit[edit.vue type=11 form] -->|"美元×1000 → _qf"| save["POST /game/activity/save|update"]
|
||
save --> ctrl[ActivityController]
|
||
ctrl -->|"checkFreeCreditsExt"| validate[ActivityValidate]
|
||
ctrl --> svc[ActivityService HTTP]
|
||
svc --> console["slot_console innerapi/activity/update"]
|
||
console --> entity[ActivityConfigEntity::updateConfig]
|
||
entity --> db["s_recharge_gift_config.ext_config"]
|
||
db --> fc["FreeCreditsLogic::configAmount key_qf 优先"]
|
||
```
|
||
|
||
|
||
|
||
## 验收
|
||
|
||
- 新建 type=11 活动:填写 6 字段 + Banner,保存成功;DB `ext_config` 是 `*_qf` 整数 + `max_unlock_per_recharge` + `banner_image`。
|
||
- 编辑回填:再次打开同一条活动,美元字段显示为原值(如 50 / 20 / 10),整数字段为 1,Banner 显示已上传图。
|
||
- 必填校验:清空任一必填项保存,返回明确错误信息(如「首笔赢取门槛必须填写且为非负数」)。
|
||
- 业务约束:免打码提现额填 60、充值解锁门槛填 50,保存被拒。
|
||
- 类型切换:type 选 11→1,再切回 11,goods 区块不出现,formData 不污染;保存 type==1 时 `_qf` 字段不会被带到 ext_config。
|
||
- C 端:调 `FreeCreditsLogic::status($uid)`,门槛 / 第一档金额 / 解锁拆分金额能读到刚配的值。
|
||
- type 字典:管理后台编辑弹窗活动类型下拉出现「Free Credits(首充前免费余额)」选项。
|
||
|