Files
cursor/plans/free_credits_activity_edit_454064f6.plan.md
2026-05-21 18:16:26 +08:00

12 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
free credits activity edit 在管理后台活动配置编辑表单中为活动类型 11首充前免费余额定格与分档释放 / Free Credits增加专属表单分支并补齐字典与服务端校验金额输入沿用 type==10 的「美元小数 → 千分位整数」模式,写入 ext_config 的 _qf 后缀键,与 slot_console `FreeCreditsLogic::configAmount()` 的读优先级一致。
id content status
dict-activity-type-11 字典 sm_system_dict_data 追加 code='activity_type' 的 value=11 / label='Free Credits首充前免费余额'DB 或字典管理页) completed
id content status
edit-vue-type11-form edit.vue 增加 v-if=type==11 的表单块6 字段 + Banner completed
id content status
edit-vue-skip-goods edit.vue 让 type==11 跳过 goods 区块:扩展 onlyGift / 调整「添加赠送」与 goods 卡片的 v-if completed
id content status
edit-vue-submit-conv edit.vue submit() 增加 type==11 的金额 ×1000 写入 _qf 键、清空 goods completed
id content status
edit-vue-setform-conv edit.vue setFormData() 增加 type==11 的 _qf ÷1000 回填还原 completed
id content status
validate-free-credits-ext ActivityValidate 增加 checkFreeCreditsExt(extConfig) 方法含必填、非负、max_unlock_per_recharge≥1、first_cash ≤ recharge_unlock 约束 completed
id content status
controller-trigger-ext-check ActivityController::save / update 在 checkData 后按 type==11 调用 checkFreeCreditsExtupdate 兼容仅改状态请求 completed
id content status
verify-end-to-end 本地验证:新建/编辑/必填/业务约束/类型切换/C 端 FreeCreditsLogic 读取 completed
false

Free Credits 活动后台编辑落地计划

目标

让运营在「活动管理」编辑弹窗里选活动类型 11(首充前免费余额)时,能直接编辑需求文档第 17 节列出的所有配置项;保存后落到 s_recharge_gift_config.ext_config JSON 字段C 端 FreeCreditsLogic 立即生效。

现状要点

  • slot_console 已实现 RechargeGiftConfigModel::TYPE_FREE_CREDITS = 11FreeCreditsLogic、运行时配置读取(ext_config.{key}_qf 优先)。
  • slot_admin 后端 ActivityControllerslotLib\services\ActivityService 透传到 slot_console innerapi/activity/*ext_config 已原样落库,不需要改动 slot_console
  • 管理前端 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

2.1 增加 type==11 表单块

参考 edit.vue line 108-123 type==10 的写法,新增:

<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 区块

let onlyGift = computed(() => {
  return formData.type == 7 || formData.type == 8 || formData.type == 9 || formData.type == 11;
})
  • edit.vue line 125 把「添加赠送」按钮与下方 <a-row v-else> 的 goods 卡片包成一组条件,排除 type==10 和 type==11
<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 类似 type==10 的处理,把 6 个美元字段写到 _qf 键,整数字段原样保留:

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 仿照 type==10

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

ThinkValidate 对嵌套 JSON 支持有限,采用「在 Controller 按 type 触发额外校验」+「Validate 提供专用方法」的模式,避免污染既有 scene。

3.1 ActivityValidate 增加一个公开方法

/**
 * 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 / updatecheckData() 之后追加:

if ((int)input('type') === 11) {
    $this->validate->checkFreeCreditsExt((array)input('ext_config', []));
}

update 里同样判断,且要兼容仅改状态的请求(无 updateData 时不校验 ext_config

if (!empty(input('updateData')) && (int)input('type') === 11) {
    $this->validate->checkFreeCreditsExt((array)input('ext_config', []));
}

4. 不需要改的部分

  • slot_consoleActivityConfigEntity::updateConfig 已原样写入 ext_config,无变更。
  • Consts::ACTIVITY_TYPE_* 不必新增 11C 端已通过 RechargeGiftConfigModel::TYPE_FREE_CREDITS 引用。
  • 不新增独立菜单页,复用通用「活动管理」编辑弹窗。

数据流

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首充前免费余额」选项。