--- 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 ``` #### 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) 把「添加赠送」按钮与下方 `` 的 goods 卡片包成一组条件,排除 type==10 和 type==11: ```vue ...goods 卡片... ``` (保留原 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(首充前免费余额)」选项。