Files
cursor/plans/free_credits_activity_edit_454064f6.plan.md
ray zhou f71a5c59af ok
2026-05-29 11:21:40 +08:00

248 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 调用 checkFreeCreditsExtupdate 兼容仅改状态请求
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 Creditstype=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_*` 不必新增 11C 端已通过 `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整数字段为 1Banner 显示已上传图。
- 必填校验:清空任一必填项保存,返回明确错误信息(如「首笔赢取门槛必须填写且为非负数」)。
- 业务约束:免打码提现额填 60、充值解锁门槛填 50保存被拒。
- 类型切换type 选 11→1再切回 11goods 区块不出现formData 不污染;保存 type==1 时 `_qf` 字段不会被带到 ext_config。
- C 端:调 `FreeCreditsLogic::status($uid)`,门槛 / 第一档金额 / 解锁拆分金额能读到刚配的值。
- type 字典管理后台编辑弹窗活动类型下拉出现「Free Credits首充前免费余额」选项。