feat(称号系统): 新增称号管理功能与抽奖效果集成
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 39s
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 39s
- 新增系统称号模板与效果配置表及相关CRUD接口 - 实现用户称号分配与抽奖效果应用逻辑 - 优化抽奖接口支持用户ID参数以应用称号效果 - 新增称号管理前端页面与分配功能 - 修复Windows时区错误与JSON字段初始化问题 - 移除无用管理接口代码并更新文档说明
This commit is contained in:
parent
81e2fb5a75
commit
8141a47690
37
.trae/documents/优化双倍卡配置与作用范围选择的实施计划.md
Normal file
37
.trae/documents/优化双倍卡配置与作用范围选择的实施计划.md
Normal file
@ -0,0 +1,37 @@
|
||||
## 使用指导(立即可用)
|
||||
- 叠加策略(stacking_strategy)
|
||||
- 对“双倍概率”(effect_type=6)当前不生效;代码仅按“概率累加+统一封顶”处理(internal/service/activity/draw_with_effects.go:133-149)。
|
||||
- 只有一个双倍效果时,保持默认即可(推荐填 1“累加封顶”或 0“最大值”,不影响现状)。
|
||||
- 统一封顶(cap_value_x1000)
|
||||
- 约束“多个双倍效果合并后的总概率”的上限(单位千分)。
|
||||
- 只有一个效果:填 0(不封顶)或填业务上限(如 1000=100%)。
|
||||
- 多个效果:按业务上限填写,如最多 30% 则填 300(避免叠加超过 30%)。
|
||||
|
||||
## 作用范围简化(改造目标)
|
||||
- 将前端“作用范围”改为:
|
||||
- 下拉选择“活动 activity_ids”(多选)
|
||||
- 依赖选择“期 issue_ids”(多选,基于已选活动加载期)
|
||||
- 可选“排除期 exclude.issue_ids”(多选)
|
||||
- 不再要求手写 ID,不展示分类/时间等无关选项。
|
||||
|
||||
## 前端改造
|
||||
- EffectEditDialog.vue
|
||||
- 为 effect_type=6 添加三组下拉:活动、期、排除期。
|
||||
- 提交时构造简化版 scopes_json:`{ activity_ids, issue_ids, exclude: { issue_ids } }`。
|
||||
- 在 UI 上为 stacking_strategy/cap_value_x1000 提供简短说明(不改后端字段)。
|
||||
- EffectManagerDialog.vue
|
||||
- 在列表新增“作用范围”列,展示活动/期/排除期标签(已完成范围列,可保留)。
|
||||
|
||||
## 数据来源与接口(不改后端)
|
||||
- 活动/期下拉数据:
|
||||
- 如果已有活动与期的接口:直接请求填充;
|
||||
- 如暂无接口:先用静态选项或从现有管理页可获取的列表填充,下轮再对接接口。
|
||||
- 不改后端:效果保存仍通过 `admin/system_titles/:title_id/effects`,携带 `params_json` 与简化 `scopes_json`。
|
||||
|
||||
## 验收
|
||||
- 在管理端为“双倍概率”新增/编辑效果:
|
||||
- 可用下拉选择活动与期,不用手写;
|
||||
- 保存后列表显示范围标签;
|
||||
- 抽奖按选定的期过滤生效(draw_with_effects.go:85-103)。
|
||||
|
||||
确认后我将实施前端下拉改造与表单说明文本,并保持后端兼容。
|
||||
53
.trae/documents/修复 Admin 前端构建 TypeScript 错误.md
Normal file
53
.trae/documents/修复 Admin 前端构建 TypeScript 错误.md
Normal file
@ -0,0 +1,53 @@
|
||||
## 问题概览
|
||||
- 缺失模块:`@/mock/temp/commentDetail` 导致 TS2307(src/components/business/comment-widget/index.vue:46)。
|
||||
- 事件类型不匹配:`emit('search', ...)` 未在 `defineEmits` 中声明载荷(activity-search、player-search)。
|
||||
- 缺少类型声明:`crypto-js/md5` 导致 TS7016(login)。
|
||||
- 返回类型不一致:登录接口不含 `refreshToken` 导致 TS2339(login)。
|
||||
- 表格数据类型为 `unknown[]` 与组件期望 `Record<string, any>[]` 不匹配(guild、banner、product)。
|
||||
|
||||
## 修复方案(不改接口协议,最小改动)
|
||||
- 缺失模块补齐:新增 `web/admin/src/mock/temp/commentDetail.ts`,导出 `Comment` 类型与 `commentList: Ref<Comment[]>`。
|
||||
- 使 `src/components/business/comment-widget/index.vue:46` 的导入可解析。
|
||||
- 事件类型声明完善:在以下文件的 `defineEmits` 中为 `search` 声明载荷类型 `Props['modelValue']`:
|
||||
- `src/views/activity/manage/modules/activity-search.vue:93-101,148-151`
|
||||
- `src/views/player-manage/modules/player-search.vue:82-90,136-139`
|
||||
- 为 `crypto-js/md5` 提供类型:新增 `web/admin/src/types/shims-crypto-js.d.ts` 内容:`declare module 'crypto-js/md5';`
|
||||
- 保证 `src/views/auth/login/index.vue:117` 类型可解析。
|
||||
- 登录返回类型与使用一致化:修改登录页不解构 `refreshToken`,仅解构 `token` 并调用 `userStore.setToken(token)`:
|
||||
- `src/views/auth/login/index.vue:223-236`;当前后端返回结构为 `{ token, is_super }`(参考后端 `internal/service/admin/login.go:22-25,62-63` 与前端 `src/api/auth.ts:8-14`)。
|
||||
- 表格数据类型注解:在各视图的 `apiFn` 显式标注返回类型为 `Promise<Api.Common.PaginatedResponse<...>>`,使 `useTable` 能推导出 `TRecord`,从而 `data` 类型为记录数组:
|
||||
- Guild 列表:`src/views/guild/manage/index.vue:137-173`,返回 `PaginatedResponse<{ id; name; owner_id; ... }>`。
|
||||
- Banner 列表:`src/views/operations/banner/index.vue:77-98`,返回 `PaginatedResponse<BannerItem>`(`src/api/banner.ts:3-10`)。
|
||||
- Product 分类:`src/views/product/categories/index.vue:80-102`,返回 `PaginatedResponse<{ id; name; parent_id; status }>`。
|
||||
- Product 列表:`src/views/product/list/index.vue`(同样模式)。
|
||||
- 若不方便标注,临时方案:在模板传参处显式断言 `:data="data as Record<string, any>[]"`(不推荐,先按注解方案处理)。
|
||||
|
||||
## 具体改动明细
|
||||
- 新增 `src/mock/temp/commentDetail.ts`:
|
||||
- 导出 `export interface Comment { id:number; author:string; content:string; timestamp:string; replies: Comment[] }`
|
||||
- `export const commentList = ref<Comment[]>([])`(保持与组件 `comments.value.push(...)` 一致)。
|
||||
- 更新事件声明:
|
||||
- `activity-search.vue` `defineEmits<Emits>()` 中将 `(e: 'search'): void` 改为 `(e: 'search', value: Props['modelValue']): void`。
|
||||
- `player-search.vue` 同步调整。
|
||||
- 新增类型声明文件:`src/types/shims-crypto-js.d.ts` 写入 `declare module 'crypto-js/md5';`。
|
||||
- 登录页使用调整:
|
||||
- 改为 `const { token } = await fetchLogin(...)`,并调用 `userStore.setToken(token)`。
|
||||
- useTable 返回类型注解:在上述 4 个视图 `apiFn` 的返回 `then` 中显式声明返回值类型为 `Api.Common.PaginatedResponse<...>`。
|
||||
|
||||
## 验证步骤
|
||||
- 安装依赖后(若需要):`npm i --save-dev @types/crypto-js`(可省略,因已加 shims)。
|
||||
- 执行 `npm run build`,expect:无 TS 错误;资源正常打包。
|
||||
- 运行本地:`npm run dev`,检查各页面:
|
||||
- 评论组件能正常发布与回复。
|
||||
- 搜索模块能正常触发,父组件收到带载荷的 `search` 事件。
|
||||
- 登录页能正常登录并存储 `token`。
|
||||
- Guild、Banner、Product 列表渲染正常,分页操作无类型报错。
|
||||
|
||||
## 影响面与风险
|
||||
- 均为类型与前端视图层改动,不涉及后端接口或数据结构变更。
|
||||
- `crypto-js` 类型通过 shims 解决,不强依赖 `@types` 包。
|
||||
- `useTable` 类型注解只提升类型推导,不改变运行时行为。
|
||||
|
||||
## 后续优化建议
|
||||
- 统一在 `api/*` 层将后端分页响应封装为 `Api.Common.PaginatedResponse<T>`,视图层的 `apiFn` 直接返回该类型,减少重复 `then` 转换与类型注解。
|
||||
- 为常见搜索子组件抽取 `Emits` 模板,避免事件签名遗漏。
|
||||
@ -0,0 +1,34 @@
|
||||
**问题定位**
|
||||
- 触发点在 `internal/pkg/timeutil/timeutil.go:13-22`:`time.LoadLocation("Asia/Shanghai")` 在 `init` 中执行,失败则 `panic`,并将 `time.Local` 设为中国时区。
|
||||
- 在 Windows 无系统 IANA tzdata、且以 `CGO_ENABLED=0` 交叉编译的静态二进制中,`time` 包找不到 `Asia/Shanghai`,导致启动时报错。
|
||||
|
||||
**解决方案(按优先级)**
|
||||
- 方案A(推荐):嵌入 tzdata 到二进制
|
||||
- 在应用入口 `main.go` 增加空导入:`_ "time/tzdata"`,使 `time` 包使用内嵌 IANA 数据库。
|
||||
- 或在打包时添加构建标签:`-tags timetzdata`(效果等同),无需改代码。
|
||||
- 方案B(无代码变更):随程序分发 `zoneinfo.zip`
|
||||
- 复制 `$GOROOT/lib/time/zoneinfo.zip` 到可执行文件同目录,运行前设置环境变量 `ZONEINFO` 指向该文件,例如:`set ZONEINFO=%CD%\zoneinfo.zip`。
|
||||
- 方案C(防御补充):去除 `panic` 并提供容错
|
||||
- 在 `timeutil` 初始化失败时,回退到固定东八区:`time.FixedZone("CST", 8*3600)`,并设置 `time.Local`,避免启动失败。
|
||||
|
||||
**打包命令更新**
|
||||
- 将 README 中 Windows 打包命令更新为:
|
||||
- `CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -tags timetzdata -trimpath -o build/bindbox.exe .`
|
||||
- 若采用方案B:在 `build` 目录放置 `zoneinfo.zip`,并在启动脚本/说明中加入 `set ZONEINFO=%~dp0zoneinfo.zip`。
|
||||
|
||||
**验证计划**
|
||||
- 本地编译并运行单测:覆盖 `internal/pkg/timeutil` 的格式化与解析函数,确认 `Asia/Shanghai` 可加载且不崩溃。
|
||||
- 人工验证:在 Windows 主机上执行 `bindbox.exe` 并访问 `http://127.0.0.1:9991/system/health`,字段 `time` 正常返回中国时区时间(参考 `internal/pkg/core/core.go:418-441` 与 `timeutil.CSTLayoutString()`)。
|
||||
|
||||
**将实施的具体改动**
|
||||
- 若选A:
|
||||
- 在 `main.go` 增加 `_ "time/tzdata"` 导入。
|
||||
- 更新 README 的 Windows 构建命令,加入 `-tags timetzdata`。
|
||||
- 若选C(可选增强):
|
||||
- 修改 `internal/pkg/timeutil/timeutil.go`,失败时不 `panic`,改为固定时区回退并日志记录。
|
||||
|
||||
**回滚与兼容**
|
||||
- 方案A仅增加约 ~800KB 体积,无行为回滚风险。
|
||||
- 方案C为防御性修改,兼容现有逻辑;若需恢复原严格行为,可还原为 `panic`。
|
||||
|
||||
请确认采用的方案(推荐选A;可附加选C增强健壮性)。确认后我将按上述步骤实施并提交更改、完成打包与验证。
|
||||
22
.trae/documents/修复称号初始化JSON字段并重新初始化.md
Normal file
22
.trae/documents/修复称号初始化JSON字段并重新初始化.md
Normal file
@ -0,0 +1,22 @@
|
||||
## 问题原因
|
||||
- 数据库报错 `Invalid JSON text: "The document is empty."`,说明往 `system_titles.obtain_rules_json` 写入了空字符串 `""`。
|
||||
- 在 MySQL JSON 列中,空字符串不是合法 JSON;需使用 `"{}"` 或 `"[]"` 等合法文本。
|
||||
- 代码位置:`internal/api/admin/titles_seed.go` 初始化 `SystemTitles` 时将 `ObtainRulesJSON`、`ScopesJSON` 设为了空字符串。
|
||||
|
||||
## 修复方案
|
||||
- 修改 `internal/api/admin/titles_seed.go` 的种子逻辑:
|
||||
- 将 `ObtainRulesJSON` 默认值改为 `"{}"`(或 `{"type":"manual"}` 作为占位规则)。
|
||||
- 将 `ScopesJSON` 默认值改为 `"{}"`(空作用域表示全局生效)。
|
||||
- 保持 `SystemTitleEffects.ParamsJSON` 使用 `json.Marshal(d.Params)`(已为合法 JSON)。
|
||||
|
||||
## 执行步骤
|
||||
1. 更新代码:将空字符串替换为合法 JSON 默认值 `"{}"`。
|
||||
2. 编译检查:`go build ./...`。
|
||||
3. 运行初始化:`POST /api/admin/system_titles/seed_default`,预期返回 6 个称号(新建或已存在)。
|
||||
4. 验证列表:登录后调用 `GET /api/admin/system_titles?page=1&page_size=20`,应返回 `total=6`。
|
||||
|
||||
## 风险与回滚
|
||||
- 仅修改种子默认值,不影响既有数据;若此前插入失败,无需回滚。
|
||||
- 如你希望 `obtain_rules_json` 有具体规则,我可以将默认值改为 `{"type":"manual"}` 或按你的要求填充。
|
||||
|
||||
确认后我将立即修复代码、重新初始化并验证页面数据。
|
||||
31
.trae/documents/初始化并排查称号管理列表为空.md
Normal file
31
.trae/documents/初始化并排查称号管理列表为空.md
Normal file
@ -0,0 +1,31 @@
|
||||
## 现象与猜测
|
||||
- 运营端“称号管理”页面为空,极可能是尚未调用初始化接口或请求被鉴权拦截。
|
||||
|
||||
## 执行步骤
|
||||
- 后端健康检查
|
||||
- 确认服务已启动且数据库连接正常(启动日志不报错)。
|
||||
- 管理员登录获取 Token
|
||||
- 账号:`admin`;密码:`chat2025` → 先做 MD5:`39f841df6f9a4768330b00d3ab9f1b4d`
|
||||
- `POST /api/admin/login`,记录返回 `token`
|
||||
- 初始化 6 个称号与效果
|
||||
- `POST /api/admin/system_titles/seed_default`
|
||||
- 期望返回:`{ created: N, exists: M, ids: [...] }`(总数 6)
|
||||
- 菜单补齐(后端动态菜单模式)
|
||||
- `POST /api/menu/ensure_titles`
|
||||
- 期望返回:`{ ensured: true, parent_id: <Operations>, menu_id: <Titles> }`
|
||||
- 验证后端数据
|
||||
- `GET /api/admin/system_titles?page=1&page_size=20`,请求头加 `Authorization: Bearer <token>`
|
||||
- 期望:`list` 含 6 条数据,`total=6`
|
||||
- 前端验证
|
||||
- 进入“运营管理 → 称号管理”,若仍为空:
|
||||
- 打开浏览器网络面板检查 `admin/system_titles` 响应码与返回体:401 表示未登录;200 且 `list=[]` 表示数据未初始化或失败(重试初始化)。
|
||||
|
||||
## 注意点
|
||||
- 初始化接口在非认证组,便于快速拉起;列表接口需要登录。
|
||||
- 之前的路由重复注册已修复(避免 Gin panic)。
|
||||
- 若你希望把占位参数(券模板 `template_id`、奖品 `target_prize_ids`)替换为真实运营配置,我将更新 `params_json` 并再次初始化。
|
||||
|
||||
## 验收标准
|
||||
- 初始化后,`GET /api/admin/system_titles` 返回 6 条记录;前端列表显示 6 条。
|
||||
|
||||
确认后我将按照以上步骤执行初始化与验证,确保页面数据正常展示。
|
||||
46
.trae/documents/双倍概率效果详解与作用范围简化方案.md
Normal file
46
.trae/documents/双倍概率效果详解与作用范围简化方案.md
Normal file
@ -0,0 +1,46 @@
|
||||
## 字段与行为详解(以 effect_type=6 双倍概率为例)
|
||||
- 目标ID(target_prize_ids)
|
||||
- 含义:指定哪些奖品可触发双倍;为空数组表示对所有奖品生效。
|
||||
- 应用代码:合并目标集 `doubleTargets`(internal/service/activity/draw_with_effects.go:143-145)。
|
||||
- 概率(chance_x1000)
|
||||
- 含义:双倍触发的概率,千分制(1000=100%、200=20%)。多个效果的概率会累加,并受统一封顶限制。
|
||||
- 应用代码:累加并按 `CapValueX1000` 统一封顶(internal/service/activity/draw_with_effects.go:146-149);最终判定(199-207)。
|
||||
- 周期内次数上限(period_cap_times)
|
||||
- 业务含义:一个统计周期内最多能触发几次双倍(例如:自然日内最多1次)。
|
||||
- 当前实现:字段已解析(internal/service/activity/draw_with_effects.go:137),但暂未做持久化计数与强制限制;建议后续以“用户+周期”维度计数(DB/Redis),命中后递减或阻断。
|
||||
- 叠加策略(stacking_strategy)
|
||||
- 业务含义:多效果如何合并(0最大值/1累加封顶/2首个匹配)。
|
||||
- 当前实现:对“概率加成(type=5)”生效(internal/service/activity/draw_with_effects.go:117-132);对“双倍概率(type=6)”未使用(合并逻辑仅累加+封顶)。
|
||||
- 统一封顶(cap_value_x1000)
|
||||
- 含义:对累计概率(或加成)的统一上限(单位:千分)。
|
||||
- 应用代码:双倍概率封顶(internal/service/activity/draw_with_effects.go:147-149);概率加成默认分支封顶(127-131)。
|
||||
- 包含期(issue_ids)/排除期(exclude.issue_ids)
|
||||
- 含义:限定效果只在指定期生效,或在某些期不生效。
|
||||
- 应用代码:抽奖路径按期过滤(internal/service/activity/draw_with_effects.go:85-103);更全面的范围匹配在效果解析器(internal/service/title/effects_resolver.go:133-155)。
|
||||
- 包含活动(activity_ids)
|
||||
- 含义:限定效果只在指定活动生效。
|
||||
- 应用代码:效果解析器支持(internal/service/title/effects_resolver.go:60-69, 133-155);抽奖路径当前只按期(issue)做简化过滤。
|
||||
- 排序(sort)
|
||||
- 含义:效果的展示与应用顺序;不同策略可能受顺序影响(如取最大值时先后无影响,叠加时顺序影响可忽略)。
|
||||
- 应用代码:读取效果按 `Sort` 排序(internal/api/admin/titles_admin.go:163;internal/service/activity/draw_with_effects.go:47-49)。
|
||||
|
||||
## 作用范围简化(仅“活动”和“期”)
|
||||
- 目标:将前端“作用范围”精简为:
|
||||
- 包含活动 `activity_ids`
|
||||
- 包含期 `issue_ids`
|
||||
- 排除期 `exclude.issue_ids`(可选)
|
||||
- 不再展示/保存:分类(category_ids)、时间/地区等复杂范围。
|
||||
|
||||
## 实施步骤(前端改造)
|
||||
1. 效果编辑对话框(EffectEditDialog.vue)
|
||||
- 保留 effect_type=6 的参数:`target_prize_ids/chance_x1000/period_cap_times`。
|
||||
- 精简作用范围面板,仅保留:`activity_ids`、`issue_ids`、`exclude.issue_ids`。
|
||||
- 提交时 `scopes_json` 只包含上述三个字段。
|
||||
2. 效果列表(EffectManagerDialog.vue)
|
||||
- 参数展示映射只显示:目标奖品、概率、周期上限;范围只显示包含/排除期与活动。
|
||||
3. 文档更新
|
||||
- 在《说明文档.md》中标注“作用范围简化”,并更新字段速查表。
|
||||
|
||||
## 注意与后续
|
||||
- 周期计数未实现:若您需要严格的“周期内次数上限”,我将增加一次触发计数的服务(用户+周期维度),并在抽奖路径强制限制。
|
||||
- 抽奖路径按期过滤:如需按活动过滤,请在抽奖服务中引入效果解析器返回的范围匹配结果,或将 `issue` 与 `activity` 进行绑定映射。
|
||||
73
.trae/documents/后台管理接口审计与无用代码清理.md
Normal file
73
.trae/documents/后台管理接口审计与无用代码清理.md
Normal file
@ -0,0 +1,73 @@
|
||||
# 后台管理接口审计与无用代码清理
|
||||
|
||||
## 目标
|
||||
- 全面梳理管理端(`/api/admin/*`)后端接口与前端页面调用关系,排除 App 端接口
|
||||
- 标注未被前端使用的管理端接口与文件,制定安全清理方案
|
||||
|
||||
## 范围
|
||||
- 后端:`internal/router/router.go` 管理端分组与 `internal/api/admin/*`
|
||||
- 前端:`web/admin/src/api/*` 与 `web/admin/src/views/*` 中使用的管理端接口
|
||||
- 排除:`/api/app/*` 与模板系统管理接口(`/api/user/*`, `/api/role/*`, `/api/v3/system/menus/simple`)
|
||||
|
||||
## 关键发现
|
||||
- 路由注册位置:`internal/router/router.go` 管理端非鉴权与鉴权分组
|
||||
- 非鉴权:
|
||||
- `GET /api/admin/license/status`(内联处理器)· `internal/router/router.go:53`
|
||||
- `POST /api/admin/login` → `Login()` · `internal/router/router.go:57`,`internal/api/admin/login.go`
|
||||
- 鉴权组主要接口(节选,完整见路由文件):
|
||||
- 活动与期次/奖励/随机承诺/抽奖:`/api/admin/activities/*` · `internal/router/router.go:72-90`
|
||||
- 抽奖凭据:`GET /draw_receipts/:draw_id`、`GET /draw_receipts/log/:log_id` · `internal/router/router.go:91-92`
|
||||
- 批量用户:`POST/DELETE /batch_users` · `internal/router/router.go:93-94`
|
||||
- 工会:`/guilds/*` · `internal/router/router.go:96-100`
|
||||
- 商品与分类:`/product_categories/*`、`/products/*` · `internal/router/router.go:103-110`
|
||||
- 轮播图:`/banners/*` · `internal/router/router.go:113-116`
|
||||
- 玩家:`/users/*`(邀请、订单、优惠券、积分、库存、道具卡、奖励发放)· `internal/router/router.go:119-129,141`
|
||||
- 系统道具卡/优惠券:`/system_item_cards/*`、`/system_coupons/*` · `internal/router/router.go:132-140`
|
||||
- 示例后端处理函数位置:
|
||||
- 抽奖凭据:`GetDrawReceipt()` 与 `GetDrawReceiptByLogID()` · `internal/api/admin/draw_receipt.go:31-75,77-121`
|
||||
|
||||
- 前端管理端接口使用(以 `VITE_API_URL=/api` 为前缀,`url: 'admin/...'` → `/api/admin/...`)
|
||||
- 登录:`POST admin/login` · `web/admin/src/api/auth.ts:11`
|
||||
- 活动/期次/奖励/随机承诺/抽奖:`admin/activities/*` · `web/admin/src/api/adminActivities.ts:13,29,33,37,47,57,68,75,93,112,133,140,146,159,172,188,210,247,274,299,323,347`
|
||||
- 批量用户:`admin/batch_users` · `web/admin/src/api/adminActivities.ts:220,227`
|
||||
- 抽奖凭据:`admin/draw_receipts/:id`、`admin/draw_receipts/log/:log_id` · `web/admin/src/api/adminActivities.ts:299,323`
|
||||
- 玩家管理:`admin/users/*`(邀请、订单、优惠券、积分、余额、库存、道具卡、奖励发放)· `web/admin/src/api/player-manage.ts:25,48,81,106,130,157,188,198,210,220,239`
|
||||
- 轮播图:`admin/banners/*` · `web/admin/src/api/banner.ts:14,26,39,43`
|
||||
- 商品与分类:`admin/product_categories/*`、`admin/products/*` · `web/admin/src/api/product.ts:15,22,32,38,63,76,90,94`
|
||||
- 系统道具卡/优惠券:`admin/system_item_cards/*`、`admin/system_coupons/*` · `web/admin/src/api/itemCards.ts:81,86,92,96,101`,`web/admin/src/api/coupons.ts:59,64,70,74`
|
||||
- 工会管理(管理端):`admin/guilds/*`、成员:`admin/guilds/:guild_id/members` · `web/admin/src/api/adminGuild.ts:12,27,31,35`,`web/admin/src/api/guild.ts:61`
|
||||
|
||||
## 待清理项(未发现前端使用)
|
||||
- `GET /api/admin/license/status` · `internal/router/router.go:53`(前端无调用)
|
||||
- 管理员账号维护接口:
|
||||
- `POST /api/admin/create` · `internal/router/router.go:66`,`internal/api/admin/admin_create.go`
|
||||
- `PUT /api/admin/:id` · `internal/router/router.go:67`,`internal/api/admin/admin_modify.go`
|
||||
- `POST /api/admin/delete` · `internal/router/router.go:68`,`internal/api/admin/admin_delete.go`
|
||||
- `GET /api/admin/list` · `internal/router/router.go:69`,`internal/api/admin/admin_list.go`
|
||||
- 以上清单根据前端 `web/admin/src/api/*` 的全面检索得出;如后续发现隐藏使用,则可回滚。
|
||||
|
||||
## 实施步骤
|
||||
1. 冻结与确认
|
||||
- 在代码评审中确认上述待清理项确实不需要;标注使用范围仅限管理端
|
||||
2. 路由层清理
|
||||
- 从 `internal/router/router.go` 删除或注释未用路由注册行:`53, 66-69`
|
||||
- 核对编译影响:移除后需同时删除对应 handler 引用,避免未引用编译警告
|
||||
3. 处理器与文件清理
|
||||
- 删除对应的处理器文件:`internal/api/admin/admin_create.go`、`admin_modify.go`、`admin_delete.go`、`admin_list.go`
|
||||
- 若 `license/status` 为内联处理器,无独立文件,仅移除路由与调用
|
||||
4. 依赖与构建
|
||||
- 执行模块级编译,确保无引用残留(如 `admin.New` 构造体内未使用成员)
|
||||
- 运行单元/集成用例(如存在),并做最小回归:登录、活动、玩家、商品、运营、工会等主流程
|
||||
5. 文档与变更说明
|
||||
- 记录变更影响范围与回滚策略,标注接口移除清单
|
||||
|
||||
## 验收标准
|
||||
- 管理端页面所有功能可正常使用与构建通过
|
||||
- 删除的接口在全仓内无调用引用(前端/后端均为 0)
|
||||
- 关键页面 API 映射完整且无 404/500
|
||||
|
||||
## 回滚方案
|
||||
- 路由注册保留注释备份;如发现遗漏使用,按文件恢复并重新注册路由
|
||||
|
||||
## 下一步请求
|
||||
- 若确认以上清理项与步骤,开始实施清理并提交差异,附带验证报告与引用检索证明
|
||||
74
.trae/documents/哈希抽奖逻辑与控制策略说明.md
Normal file
74
.trae/documents/哈希抽奖逻辑与控制策略说明.md
Normal file
@ -0,0 +1,74 @@
|
||||
# 抽奖控制策略选择与执行
|
||||
|
||||
## 背景与约束
|
||||
|
||||
* 不改变算法逻辑(承诺→HMAC+拒绝采样→验算一致),仅通过“输入/配置/时机”达到运营控制。
|
||||
|
||||
* 验算用户能看到的是回执中池快照与承诺哈希一致性,不能证明你在抽后操纵,只能确认抽前配置。
|
||||
|
||||
## 方案A:权重/库存门控(推荐)
|
||||
|
||||
* 做法:在承诺前将目标奖品 `weight=0` 或 `quantity=0`,前 N 次(或直到达到阈值)不进入抽取集合;到期后恢复权重/库存并重新承诺。
|
||||
|
||||
* 基于代码:
|
||||
|
||||
* 排除条件:`weight>0 && (quantity==-1 || quantity>0)` 才参与抽取(`internal/service/activity/draw_execute.go:31-35,56-58`)。
|
||||
|
||||
* 承诺快照:包含每个奖励的 `{id,name,weight,quantity_before}`(`internal/service/activity/random_commit.go:53-61,67-73`)。
|
||||
|
||||
* 优点:简单直接、无需改算法;前 N 次绝不命中;验算完全通过。
|
||||
|
||||
* 缺点:恢复后需生成新承诺(`state_version` 增加),不同时间段 `items_root` 不同,运维需记录策略切换。
|
||||
|
||||
## 方案B:承诺版本切换
|
||||
|
||||
* 做法:用 `state_version` 管理期的承诺版本:
|
||||
|
||||
* v1:不含目标奖品或其权重为 0 → 前 N 次抽使用 v1。
|
||||
|
||||
* v2:目标奖品恢复权重/库存 → 达到 N 后切换到 v2。
|
||||
|
||||
* 基于代码:承诺生成与历史查询(`internal/service/activity/random_commit.go:74-97,121-146`)。
|
||||
|
||||
* 优点:语义清晰、审计友好;对不同用户批次可严格区分承诺。
|
||||
|
||||
* 缺点:运维复杂度稍高;用户若横向对比可能看到承诺变化,但单次验算仍通过。
|
||||
|
||||
## 方案C:直接发放替代抽奖
|
||||
|
||||
* 做法:对需要“必中/避中”的个体,使用管理端发放接口 `POST /api/admin/users/:user_id/rewards/grant`(`internal/router/router.go:127`)。
|
||||
|
||||
* 优点:精确可控,零风险。
|
||||
|
||||
* 缺点:不产生抽奖回执;不适合需要“抽奖体验”的场景。
|
||||
|
||||
## 方案对比与推荐
|
||||
|
||||
* 目标“前 N 次不出现”且保留抽奖体验:优先选 **方案A(权重/库存门控)**,用 `quantity=0` 或 `weight=0` 让奖品在 N 次前不参与集合;到期后恢复并重新承诺。
|
||||
|
||||
* 若需批次化与清晰审计边界:选 **方案B(承诺版本切换)**,以 `state_version` 驱动切换,N 次阈值以抽奖日志计数实现运维。
|
||||
|
||||
* 个体定向控制:用 **方案C(直接发放)** 替代抽奖。
|
||||
|
||||
## 验算与用户感知
|
||||
|
||||
* 验算会确认:回执中的 `server_seed_hash/items_root/weights_total/selected_index/rand_proof` 与承诺一致(`internal/api/admin/verify_draw.go:50-66,104-138`)。
|
||||
|
||||
* 用户能“看到”:当次承诺的奖池快照与权重(若回执包含快照,管理端/APP均有:`internal/api/admin/draw_receipt.go:55-73`,`internal/api/activity/draw_app.go:28-34`)。
|
||||
|
||||
* 用户“感知不到”:你通过前置配置与时机实现“前 N 次不出现”的意图;只要在承诺前已固化,抽后不会被判定为操纵。
|
||||
|
||||
## 执行建议(不改代码)
|
||||
|
||||
1. 选定期次与目标奖品,设置前置配置:`quantity=0` 或 `weight=0`。
|
||||
2. 生成承诺(`commit_random`)并上线;开始计数抽奖日志,达到 N 次后恢复配置并生成新承诺。
|
||||
3. 记录操作与版本切换,必要时在活动规则中说明奖池/期的切换策略。
|
||||
|
||||
## 参考位置
|
||||
|
||||
* 参与判定与选取:`internal/service/activity/draw_execute.go:31-35,50-66,131-145`
|
||||
|
||||
* 承诺生成与版本:`internal/service/activity/random_commit.go:67-85,74-97,121-146`
|
||||
|
||||
* 管理端验证:`internal/api/admin/verify_draw.go:50-66,104-138`
|
||||
|
||||
206
.trae/documents/头衔与权益效果方案设计.md
Normal file
206
.trae/documents/头衔与权益效果方案设计.md
Normal file
@ -0,0 +1,206 @@
|
||||
## 目标与范围
|
||||
|
||||
* 支持用户持有多个头衔,并在各类业务事件中正确生效与结算。
|
||||
|
||||
* 覆盖六类效果:领取优惠券、抽奖折扣、签到双倍积分、领取道具卡、概率加成、双倍奖励卡。
|
||||
|
||||
* 与既有四张表对齐:`system_titles`、`system_title_effects`、`user_titles`、`user_title_effect_claims`。
|
||||
|
||||
## 整体架构
|
||||
|
||||
* 配置层:运营通过`system_titles`与`system_title_effects`完成模板与效果配置。
|
||||
|
||||
* 持有层:`user_titles`记录用户持有与激活状态、有效期与来源。
|
||||
|
||||
* 事件引擎:在`SIGNIN`、`DRAW_PURCHASE`、`DRAW_EXECUTE`与`CLAIM`类接口中查询并结算效果。
|
||||
|
||||
* 防重限流:`user_title_effect_claims`按周期唯一键实现并发防重与频次限制。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[用户行为事件] --> B[查询激活头衔 user_titles]
|
||||
B --> C[加载效果 system_title_effects]
|
||||
C --> D{按scopes/effect_type过滤}
|
||||
D --> E[叠加/上限策略计算]
|
||||
E --> F[结算(积分/折扣/概率/发放)]
|
||||
F --> G[必要时写 user_title_effect_claims]
|
||||
```
|
||||
|
||||
## 效果模型与参数规范
|
||||
|
||||
* 统一字段:`effect_type`、`params_json`、`stacking_strategy`、`cap_value_x1000`、`scopes_json`、`status`。
|
||||
|
||||
* 固定小数:所有比例/倍数/折扣统一用`*_x1000`(例如 10% = 100,2倍 = 2000)。
|
||||
|
||||
* `params_json`建议结构:
|
||||
|
||||
* 领取优惠券(`COUPON_CLAIM`): `{template_id, frequency:{period:'day|week|month', times:int}, start_at?, end_at?}`
|
||||
|
||||
* 抽奖折扣(`DRAW_DISCOUNT`): `{discount_type:'percentage|fixed', value_x1000, min_price?, max_discount?}`
|
||||
|
||||
* 签到倍数(`SIGNIN_MULTIPLIER`): `{multiplier_x1000, daily_cap_points?}`
|
||||
|
||||
* 领取道具卡(`ITEM_CARD_CLAIM`): `{template_id, frequency:{period, times}}`
|
||||
|
||||
* 概率加成(`PROBABILITY_BOOST`): `{target_pool_ids?:[], target_prize_ids?:[], boost_x1000, combine:'sum|max', cap_x1000?}`
|
||||
|
||||
* 双倍奖励卡(`DOUBLE_REWARD`): `{chance_x1000, target_prize_ids?:[], period_cap_times?:int}`
|
||||
|
||||
## 叠加与上限策略
|
||||
|
||||
* `stacking_strategy`枚举:
|
||||
|
||||
* `none`:不叠加,取单一最高优先级或最大值。
|
||||
|
||||
* `sum_with_cap`:求和后受`cap_value_x1000`或业务cap约束。
|
||||
|
||||
* `max_only`:取最大项。
|
||||
|
||||
* `multiply_with_cap`:倍数相乘后再cap(适用于少数特殊配置)。
|
||||
|
||||
* `priority_order`:按`priority`字段逐项应用,遇到冲突按优先级覆盖。
|
||||
|
||||
* 推荐缺省:
|
||||
|
||||
* 抽奖折扣:`max_only`(防止过度优惠),可选`sum_with_cap`。
|
||||
|
||||
* 签到倍数:`sum_with_cap`(如多头衔加总倍数,上限控制)。
|
||||
|
||||
* 概率加成:`sum_with_cap`(保持可解释性与稳定性)。
|
||||
|
||||
* 领取型权益:同模板同周期`sum_with_cap`或`max_only`,按运营策略选择。
|
||||
|
||||
* 双倍奖励卡:多卡按`chance_x1000`合并使用`sum_with_cap`,并设`period_cap_times`。
|
||||
|
||||
## 作用域与生效判定
|
||||
|
||||
* `scopes_json`建议:`{activity_ids?:[], phase_ids?:[], category_ids?:[], exclude?:{...}}`。
|
||||
|
||||
* 判定顺序:事件上下文→按`activity/phase/category`过滤→`status=active`→用户头衔`active=1`且未过期→计算叠加与cap。
|
||||
|
||||
* 冲突处理:包含与排除并存时优先排除;多效果同类型按`stacking_strategy`解决。
|
||||
|
||||
## 事件结算流程
|
||||
|
||||
* 签到(`SIGNIN`):
|
||||
|
||||
* 载入`SIGNIN_MULTIPLIER`→合并倍数→应用上限→计算积分→落账。
|
||||
|
||||
* 抽奖购票(`DRAW_PURCHASE`):
|
||||
|
||||
* 载入`DRAW_DISCOUNT`→取最大或合并后cap→计算优惠→生成支付单。
|
||||
|
||||
* 抽奖执行(`DRAW_EXECUTE`):
|
||||
|
||||
* 载入`PROBABILITY_BOOST`→对目标池/奖品加权(总概率归一)→进行抽取→载入`DOUBLE_REWARD`测试加倍机会→如命中,倍增奖品数量/价值并校验当期可用次数。
|
||||
|
||||
* 领取(`COUPON_CLAIM`/`ITEM_CARD_CLAIM`):
|
||||
|
||||
* 解析`frequency.period`生成`period_key`(`YYYYMMDD|YYYYWW|YYYYMM`)→检查唯一键是否已存在→未存在则发放并写`user_title_effect_claims`;已存在直接返回已领取。
|
||||
|
||||
## 防重与限流实现
|
||||
|
||||
* 唯一键:`user_id + title_id + effect_type + target_template_id + period_key`。
|
||||
|
||||
* 并发:使用数据库唯一约束天然防重;接口层加幂等键`Idempotency-Key`进一步防抖。
|
||||
|
||||
* 频次:根据`frequency.times`控制当期可用次数;若需累计多效果,按`stacking_strategy`决定合并或取最大。
|
||||
|
||||
## 接口设计(示例)
|
||||
|
||||
* 头衔管理
|
||||
|
||||
* `POST /titles/assign`:分配或续期;参数:`user_id, title_id, obtained_at, expires_at, source`。
|
||||
|
||||
* `PATCH /titles/:user_id/:title_id/activate`:激活/停用。
|
||||
|
||||
* `GET /titles/user/:user_id?active=1`:查询用户激活头衔与效果摘要。
|
||||
|
||||
* 事件接口
|
||||
|
||||
* `POST /events/signin`:入参`user_id, context`;出参`points, applied_effects`。
|
||||
|
||||
* `POST /events/draw/purchase`:入参`user_id, base_price, context`;出参`final_price, discount_breakdown`。
|
||||
|
||||
* `POST /events/draw/execute`:入参`user_id, context`;出参`prize, boost_detail, double_reward_applied, audit_id`。
|
||||
|
||||
* `POST /claims/coupon`:入参`user_id, template_id, context`;出参`claimed, period_key, remaining_times`。
|
||||
|
||||
* `POST /claims/item-card`:入参同上。
|
||||
|
||||
* 诊断/审计
|
||||
|
||||
* `GET /effects/preview`:给定`user_id+context`预览本次将应用的合并效果,用于运营核对。
|
||||
|
||||
## 数据与索引建议
|
||||
|
||||
* 复用现有索引:
|
||||
|
||||
* `user_titles`: `idx_user_titles_user`。
|
||||
|
||||
* `system_title_effects`: `idx_title_effects_title` + 按`effect_type/status`过滤。
|
||||
|
||||
* `user_title_effect_claims`: 周期唯一索引。
|
||||
|
||||
* 读优化:
|
||||
|
||||
* 缓存`system_title_effects`(按`title_id`与`scopes_json`分片,TTL 5\~15 分钟)。
|
||||
|
||||
* 针对`DRAW_EXECUTE`的概率表做内存映射,命中后快速归一化计算。
|
||||
|
||||
## 风险与边界处理
|
||||
|
||||
* 浮点误差:统一`*_x1000`避免精度问题。
|
||||
|
||||
* 过度优惠/过度概率:必须设置`cap_value_x1000`或业务cap。
|
||||
|
||||
* 多模板冲突:同类型不同模板时采用`priority_order`或业务白名单优先级。
|
||||
|
||||
* 过期与停用:事件前统一校验有效期与`status`,过期/停用不生效。
|
||||
|
||||
* 诊断可视化:提供`preview`接口给运营验证,防误配置上线。
|
||||
|
||||
## 测试与验收标准
|
||||
|
||||
* 单元测试:
|
||||
|
||||
* 每类效果的合并逻辑与cap测试;周期键生成与唯一约束;概率加成归一化与双倍卡应用顺序。
|
||||
|
||||
* 并发/幂等:
|
||||
|
||||
* 高并发领取/购票/抽奖的防重与一致性测试。
|
||||
|
||||
* 集成测试:
|
||||
|
||||
* 端到端场景:
|
||||
|
||||
* 多头衔叠加签到倍数,限制生效。
|
||||
|
||||
* 两个折扣叠加取最大或cap限。
|
||||
|
||||
* 抽奖概率加成后命中率变更,双倍卡当期次数受限。
|
||||
|
||||
* 同模板每日领取限 1 次,多效果按策略合并。
|
||||
|
||||
* 验收标准:
|
||||
|
||||
* 用户可持有多个头衔并正确生效;
|
||||
|
||||
* 六类效果在对应事件中结算正确,含叠加与上限;
|
||||
|
||||
* 领取型权益防重与限流稳定;
|
||||
|
||||
* 审计日志完整,能输出`applied_effects`明细;
|
||||
|
||||
* 性能满足并发指标(按业务要求)。
|
||||
|
||||
## 交付物
|
||||
|
||||
* 结算引擎与效果合并模块的实现方案说明与接口契约。
|
||||
|
||||
* 管理/诊断接口的API说明与示例。
|
||||
|
||||
* 测试用例清单与通过报告。
|
||||
|
||||
* 运营使用指引:效果配置、叠加策略与风险提示。
|
||||
|
||||
81
.trae/documents/头衔系统与抽奖增益设计方案.md
Normal file
81
.trae/documents/头衔系统与抽奖增益设计方案.md
Normal file
@ -0,0 +1,81 @@
|
||||
# 头衔加概率与折扣的可验证设计
|
||||
|
||||
## 结论
|
||||
|
||||
* 采用“权重修饰”的方式为持有头衔的用户提升抽奖概率。
|
||||
|
||||
* 哈希算法(HMAC-SHA256 随机熵)保持不变;只扩展承诺与收据的字段以固化规则与有效权重,保证可验证性与不可篡改。
|
||||
|
||||
## 必要扩展
|
||||
|
||||
* 承诺扩展(IssueRandomCommitment)
|
||||
|
||||
* 新增 `AlgoRulesHash`:对“权重修饰规则(由头衔配置确定的纯函数)”做哈希并随期承诺固化。服务端在抽取前不能更改规则。
|
||||
|
||||
* 收据扩展(Receipt)
|
||||
|
||||
* 新增 `UserId`:明确本次抽取的用户。
|
||||
|
||||
* 新增 `WeightsTotalEffective`:按用户头衔修饰后的总权重,用于位置采样与第三方复核。
|
||||
|
||||
* 新增 `UserWeightFactorsRoot`:以 `reward_id -> factor` 做 Merkle 根,第三方可据此对 `Items(snapshot)` 重建有效权重。
|
||||
|
||||
* 编码消息扩展(不改算法,仅改输入)
|
||||
|
||||
* 仍使用 `HMAC(serverSubSeed, encodeMessage(...))`;`encodeMessage` 增加 `UserId` 与 `WeightsTotalEffective` 字段(保持确定性)。
|
||||
|
||||
## 规则与确定性
|
||||
|
||||
* 规则来源:头衔模板配置(百分比或加点),以及叠加策略(none|max|multiply)。
|
||||
|
||||
* 纯函数:`factor = F(user_titles, reward_id)`,只依赖用户持有的头衔与静态规则;不依赖随机数或服务端临时状态。
|
||||
|
||||
* 可复核:第三方得到 `ItemsRoot`、`AlgoRulesHash`、`UserWeightFactorsRoot`、`UserId` 与收据内快照,即可重建每个 `reward_id` 的有效权重与 `WeightsTotalEffective`,据同样的消息编码与哈希熵验证 `SelectedIndex`。
|
||||
|
||||
## 核心流程(抽奖不变,仅插入修饰步骤)
|
||||
|
||||
1. 读取承诺:`GetIssueRandomCommit(...)`(`internal/service/activity/random_commit.go:99`)。
|
||||
2. 载入奖励池快照:同现有实现(`internal/service/activity/draw_execute.go:21-35`)。
|
||||
3. 计算用户权重修饰:`effective_weight[i] = base_weight[i] * factor(user, reward_id[i])`;汇总 `WeightsTotalEffective`。
|
||||
4. 使用 HMAC-SHA256 生成熵(算法不变),对 `encodeMessage(..., UserId, WeightsTotalEffective)` 取样位点。
|
||||
5. 按有效权重的累加区间选中 `SelectedIndex/SelectedItemId`。
|
||||
6. 返回收据,并包含 `UserWeightFactorsRoot` 与证明材料。
|
||||
|
||||
## 折扣集成(独立不影响随机)
|
||||
|
||||
* 抽奖订单创建(新增):以活动门票价 `PriceDraw`(`internal/repository/mysql/model/activities.gen.go:22`)为基价。
|
||||
|
||||
* 头衔折扣:按叠加策略计算 `discount_amount`,落到订单字段 `DiscountAmount/ActualAmount`(`internal/repository/mysql/model/orders.gen.go:22`)。
|
||||
|
||||
* 与优惠券/积分叠加:默认“优惠券优先 + 头衔折上折”,可通过 `stack_policy` 切到“取最大”。
|
||||
|
||||
## 接口与服务
|
||||
|
||||
* 管理端:头衔模板 CRUD + 发放;配置权重修饰参数与叠加策略。
|
||||
|
||||
* APP:
|
||||
|
||||
* 抽奖订单创建接口(新):返回计算后的金额。
|
||||
|
||||
* 抽奖接口:保持路径不变(`internal/api/activity/draw_app.go:28`),服务内执行权重修饰。
|
||||
|
||||
* 模拟与校验:
|
||||
|
||||
* 模拟接口增加 `user_id`,输出期望概率与观测分布。
|
||||
|
||||
* 校验接口以收据扩展字段重建有效权重,验证 `SelectedIndex`。
|
||||
|
||||
## 验收标准
|
||||
|
||||
* 头衔发放后,持有用户的抽奖订单能正确展示并结算折扣。
|
||||
|
||||
* 抽奖收据包含新增字段,第三方使用承诺与收据可重放选中过程;不持有头衔的用户与持有者的分布差异符合规则。
|
||||
|
||||
* 哈希熵算法未变,安全与不可预测性不受影响。
|
||||
|
||||
## 说明:关于“加概率是否影响哈希算法”
|
||||
|
||||
* 不影响哈希算法本身(仍是 HMAC-SHA256)。
|
||||
|
||||
* 影响的是“采样空间”——由原始 `weights_total` 改为 `WeightsTotalEffective`,并且将该值与用户标识作为消息输入的一部分来固定采样过程;承诺和收据的扩展确保服务端无法事后调参。
|
||||
|
||||
127
.trae/documents/头衔表DDL(含字段备注与事务).md
Normal file
127
.trae/documents/头衔表DDL(含字段备注与事务).md
Normal file
@ -0,0 +1,127 @@
|
||||
# 头衔权益系统设计与开发方案
|
||||
|
||||
## 目标与范围
|
||||
- 目标:构建“用户头衔”体系,使用户可拥有多个头衔,并在签到、抽奖、领取等事件中触发对应权益。
|
||||
- 权益范围:
|
||||
1. 领取优惠券
|
||||
2. 抽奖折扣(票价优惠)
|
||||
3. 签到双倍积分
|
||||
4. 领取道具卡
|
||||
5. 抽奖概率加成
|
||||
6. 奖品双倍概率(命中后按概率倍增奖励)
|
||||
- 管理端:支持头衔模板与效果配置、用户分配与启停;用户端:展示头衔、领取入口、页面显示加成提示。
|
||||
|
||||
## 现有能力复用
|
||||
- 优惠券:`system_coupons/user_coupons` 与管理端发券接口。
|
||||
- 积分与流水:`user_points/user_points_ledger`,新增“签到”服务即可接入倍数规则。
|
||||
- 抽奖:`activity_issues/activity_reward_settings` 与执行流程,按设计接入概率加成与双倍概率试验。
|
||||
- 道具卡:`system_item_cards/user_item_cards` 与相关服务。
|
||||
|
||||
## 数据模型(已提供DDL)
|
||||
- `system_titles`:头衔模板主表,定义名称、状态、获得规则与作用范围。
|
||||
- `system_title_effects`:头衔-效果配置,一条记录代表一个效果项(含类型、参数、叠加策略与封顶)。
|
||||
- `user_titles`:用户持有头衔资产,含激活状态、有效期与来源。
|
||||
- `user_title_effect_claims`:领取型权益的周期限流与防重(如每日领券一次)。
|
||||
- 注:不改动 `activity_draw_effects` 结构;抽奖效果的来源审计通过“效果应用层”记录到日志或 `Remark` 字段,保持表结构不变。
|
||||
|
||||
## 效果类型与参数约定
|
||||
- `effect_type`(整型枚举):
|
||||
- 1=COUPON_CLAIM(领取优惠券)
|
||||
- 2=DRAW_DISCOUNT(抽奖折扣)
|
||||
- 3=SIGNIN_MULTIPLIER(签到积分倍数)
|
||||
- 4=ITEM_CARD_CLAIM(领取道具卡)
|
||||
- 5=DRAW_PROBABILITY_BONUS(抽奖概率加成)
|
||||
- 6=DRAW_REWARD_DOUBLE_CHANCE(奖品双倍概率)
|
||||
- `params_json` 约定:
|
||||
- COUPON_CLAIM:`{template_id, frequency: 'once|daily|weekly|monthly', max_per_period}`
|
||||
- DRAW_DISCOUNT:`{discount_type: 'percent|fixed', value, max_cap, applicable_activities, applicable_issues}`
|
||||
- SIGNIN_MULTIPLIER:`{multiplier_x1000: 2000, cap_value_x1000: 3000}`
|
||||
- ITEM_CARD_CLAIM:`{template_id, frequency, max_per_period}`
|
||||
- DRAW_PROBABILITY_BONUS:`{probability_delta_x1000, cap_value_x1000, applicable_activities, applicable_issues}`
|
||||
- DRAW_REWARD_DOUBLE_CHANCE:`{multiplier_x1000: 2000, chance_x1000: 150, max_multiplier_cap_x1000: 3000, limit_per_day: null, applicable_activities, applicable_issues}`
|
||||
- `stacking_strategy`:0=取最大(MAX);1=累加并封顶(STACK_CAPPED);2=按排序首个(FIRST_MATCH)。
|
||||
|
||||
## 叠加与封顶规则
|
||||
- 抽奖折扣:同类折扣按策略(默认MAX)合并;若`STACK_CAPPED`则累加至`max_cap`。
|
||||
- 签到倍数:乘法叠加但有总封顶(如≤3x)。
|
||||
- 概率加成:相加叠加后按上限封顶(如≤500‰)。
|
||||
- 奖品双倍概率:
|
||||
- 概率合并:`p_total = 1 - ∏(1 - p_i)`,并设置上限(如≤50%)。
|
||||
- 倍数合并:与其他奖励倍数(如道具卡)做乘法,但最终乘积受`max_multiplier_cap_x1000`封顶(如≤3x)。
|
||||
|
||||
## 事件与规则引擎(EffectEngine)
|
||||
- 统一事件:
|
||||
- `SIGNIN`:加载用户激活头衔,应用`SIGNIN_MULTIPLIER`,写积分与流水。
|
||||
- `COUPON_CLAIM/ITEM_CARD_CLAIM`:校验`user_title_effect_claims`限流后发放。
|
||||
- `DRAW_PURCHASE`:计算`DRAW_DISCOUNT`,作用于抽奖票价。
|
||||
- `DRAW_EXECUTE`:在“中奖项选择”后进行概率加成与“双倍概率”试验,计算最终奖励倍数;将结果影响在发奖/展示层体现,同时保留来源信息到日志或回执扩展结构(不改表)。
|
||||
- 引擎职责:读取`user_titles → system_title_effects`,按叠加策略合并同类项并输出结算指令;保障并发可重入与审计信息完备(在不改动抽奖效果表前提下记录来源)。
|
||||
|
||||
## 接口设计
|
||||
- 管理端
|
||||
- `POST /api/admin/system_titles` 头衔模板创建
|
||||
- `PUT /api/admin/system_titles/:id` 头衔模板修改
|
||||
- `GET /api/admin/system_titles` 列表检索
|
||||
- `DELETE /api/admin/system_titles/:id` 删除(级联效果)
|
||||
- `POST /api/admin/system_titles/:id/effects` 效果创建(含JSON校验)
|
||||
- `PUT /api/admin/system_titles/:id/effects/:effect_id` 效果修改
|
||||
- `DELETE /api/admin/system_titles/:id/effects/:effect_id` 效果删除
|
||||
- `POST /api/admin/users/:user_id/titles/:title_id/assign` 分配头衔
|
||||
- `PUT /api/admin/users/:user_id/titles/:title_id/toggle` 激活/停用
|
||||
- 用户端
|
||||
- `GET /api/app/users/:user_id/titles` 我的头衔与效果摘要(含可领取提示)
|
||||
- `POST /api/app/users/:user_id/titles/:title_id/effects/coupon/claim` 领券
|
||||
- `POST /api/app/users/:user_id/titles/:title_id/effects/item_card/claim` 领卡
|
||||
- `POST /api/app/users/:user_id/signin` 签到(生成流水,应用倍数)
|
||||
- 抽奖购票与执行:在现有接口内接入EffectEngine,不改外部路由签名。
|
||||
|
||||
## 业务流程
|
||||
- 领取型权益:校验`user_title_effect_claims`唯一键(周期),通过则发券/发卡并写入计数;拒绝则返回已达上限。
|
||||
- 签到:读取倍数并写入`user_points_ledger(action=signin)`;倍数叠加与封顶在引擎完成。
|
||||
- 抽奖购票:计算折扣并返回应付价;可在订单侧抵扣。
|
||||
- 抽奖执行:
|
||||
1. 按现有算法选中奖励项
|
||||
2. 计算概率加成后的总概率与“双倍概率”的`p_total`
|
||||
3. 使用受控RNG进行一次试验,命中则将奖励倍数乘以`multiplier_x1000`
|
||||
4. 将来源信息(title/effect参数)记录到日志或回执扩展结构(不改动表),用于运营分析和复核
|
||||
|
||||
## 风控与审计
|
||||
- 限流与防重:依赖`user_title_effect_claims`唯一键与事务保证。
|
||||
- 并发安全:接口层加幂等键(如`period_key`)与事务处理,避免重复发放。
|
||||
- 来源审计:在不改表的前提下,将来源写入日志/回执的备注域或独立运营日志表(后续若需要可扩展)。
|
||||
|
||||
## 开发方案与里程碑
|
||||
- M1 数据层与代码生成
|
||||
- 执行DDL(事务+先删后建),新增四张表
|
||||
- 更新`cmd/gormgen`配置并生成DAO/Model
|
||||
- 管理端:头衔模板与效果CRUD接口与页面
|
||||
- M2 规则引擎与签到
|
||||
- 实现EffectEngine(加载、合并、封顶、指令输出)
|
||||
- 新增`POST /api/app/users/:user_id/signin`服务,应用签到倍数
|
||||
- 领取型权益限流服务与接口(领券/领卡)
|
||||
- M3 抽奖集成
|
||||
- 在购票结算接入`DRAW_DISCOUNT`
|
||||
- 在抽奖执行接入`DRAW_PROBABILITY_BONUS`与`DRAW_REWARD_DOUBLE_CHANCE`
|
||||
- 在不改表前提下,于日志/回执的备注域记录来源参数
|
||||
- M4 前端联动与运营
|
||||
- 管理端:头衔与效果配置表单、分配头衔入口
|
||||
- 用户端:我的头衔页、领取入口、抽奖页加成展示
|
||||
- M5 测试与验收
|
||||
- 单元/集成测试:限流、叠加、封顶、并发
|
||||
- 压测与随机性验证:抽奖公平性与概率准确性
|
||||
- 文档与Swagger更新
|
||||
|
||||
## 测试用例要点
|
||||
- 单头衔:15%双倍,抽样命中率约等于15%,倍数为2x
|
||||
- 多头衔概率合并:10%与20% → 总概率≈28%
|
||||
- 倍数合并:与道具卡1.5x叠加,最终乘积≤封顶
|
||||
- 领取限流:同周期重复领取被拒绝,计数与时间正确更新
|
||||
- 并发:高并发领取与抽奖下无重复发放与越界叠加
|
||||
|
||||
## 验收标准
|
||||
- 多头衔并存下,五类权益+双倍概率在对应事件正确触发与叠加、封顶生效
|
||||
- 管理端与用户端流程完整、接口稳定、文档齐备
|
||||
- 概率与倍数验证通过,随机性与公平性符合预期
|
||||
- 审计信息可追踪(在现有表结构前提下)
|
||||
|
||||
请确认以上“设计方案与开发计划”,确认后我将按里程碑开始实现。
|
||||
102
.trae/documents/头衔表设计方案.md
Normal file
102
.trae/documents/头衔表设计方案.md
Normal file
@ -0,0 +1,102 @@
|
||||
# 头衔表设计DDL(含事务与字段说明)
|
||||
|
||||
## 字段说明
|
||||
|
||||
* system\_titles:`id`、`name`、`description`、`status`、`obtain_rules_json`、`scopes_json`、`created_at`、`updated_at`
|
||||
|
||||
* system\_title\_effects:`id`、`title_id`、`effect_type`、`params_json`、`stacking_strategy`、`cap_value_x1000`、`scopes_json`、`sort`、`status`、`created_at`
|
||||
|
||||
* user\_titles:`id`、`user_id`、`title_id`、`active`、`obtained_at`、`expires_at`、`source`、`remark`、`created_at`
|
||||
|
||||
* user\_title\_effect\_claims:`id`、`user_id`、`title_id`、`effect_type`、`target_template_id`、`period_key`、`claim_count`、`last_claim_at`、`created_at`
|
||||
|
||||
* activity\_draw\_effects 扩展:`source_type`、`source_id`
|
||||
|
||||
## 事务化SQL(先删表再新建,含COMMIT)
|
||||
|
||||
```sql
|
||||
START TRANSACTION;
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
DROP TABLE IF EXISTS `system_title_effects`;
|
||||
DROP TABLE IF EXISTS `user_title_effect_claims`;
|
||||
DROP TABLE IF EXISTS `user_titles`;
|
||||
DROP TABLE IF EXISTS `system_titles`;
|
||||
|
||||
CREATE TABLE `system_titles` (
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`name` VARCHAR(64) NOT NULL,
|
||||
`description` VARCHAR(255) NULL,
|
||||
`status` TINYINT NOT NULL DEFAULT 1,
|
||||
`obtain_rules_json` JSON NULL,
|
||||
`scopes_json` JSON NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
UNIQUE KEY `uk_system_titles_name` (`name`),
|
||||
INDEX `idx_system_titles_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `system_title_effects` (
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`title_id` BIGINT NOT NULL,
|
||||
`effect_type` INT NOT NULL,
|
||||
`params_json` JSON NOT NULL,
|
||||
`stacking_strategy` INT NOT NULL DEFAULT 0,
|
||||
`cap_value_x1000` INT NULL,
|
||||
`scopes_json` JSON NULL,
|
||||
`sort` INT NOT NULL DEFAULT 0,
|
||||
`status` TINYINT NOT NULL DEFAULT 1,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
CONSTRAINT `fk_title_effects_title` FOREIGN KEY (`title_id`) REFERENCES `system_titles`(`id`) ON DELETE CASCADE,
|
||||
INDEX `idx_title_effects_title` (`title_id`),
|
||||
INDEX `idx_title_effects_type` (`effect_type`),
|
||||
INDEX `idx_title_effects_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `user_titles` (
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`user_id` BIGINT NOT NULL,
|
||||
`title_id` BIGINT NOT NULL,
|
||||
`active` TINYINT NOT NULL DEFAULT 1,
|
||||
`obtained_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`expires_at` DATETIME(3) NULL,
|
||||
`source` VARCHAR(64) NULL,
|
||||
`remark` VARCHAR(255) NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
CONSTRAINT `fk_user_titles_title` FOREIGN KEY (`title_id`) REFERENCES `system_titles`(`id`) ON DELETE CASCADE,
|
||||
UNIQUE KEY `uk_user_title_unique` (`user_id`, `title_id`),
|
||||
INDEX `idx_user_titles_user` (`user_id`),
|
||||
INDEX `idx_user_titles_active` (`active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `user_title_effect_claims` (
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`user_id` BIGINT NOT NULL,
|
||||
`title_id` BIGINT NOT NULL,
|
||||
`effect_type` INT NOT NULL,
|
||||
`target_template_id` BIGINT NULL,
|
||||
`period_key` VARCHAR(16) NOT NULL,
|
||||
`claim_count` INT NOT NULL DEFAULT 0,
|
||||
`last_claim_at` DATETIME(3) NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
CONSTRAINT `fk_claims_title` FOREIGN KEY (`title_id`) REFERENCES `system_titles`(`id`) ON DELETE CASCADE,
|
||||
UNIQUE KEY `uk_title_claim_period` (`user_id`, `title_id`, `effect_type`, `target_template_id`, `period_key`),
|
||||
INDEX `idx_title_claims_user` (`user_id`),
|
||||
INDEX `idx_title_claims_effect` (`effect_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
ALTER TABLE `activity_draw_effects`
|
||||
ADD COLUMN `source_type` INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN `source_id` BIGINT NULL,
|
||||
ADD INDEX `idx_draw_effects_source` (`source_type`, `source_id`);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
## 说明
|
||||
|
||||
* `period_key`:按策略生成(如每日`YYYYMMDD`、每周`YYYYWW`、每月`YYYYMM`)。
|
||||
|
||||
* 事务与外键检查切换保障迁移原子性与顺序删除/创建。
|
||||
|
||||
55
.trae/documents/头衔规则与设计思路.md
Normal file
55
.trae/documents/头衔规则与设计思路.md
Normal file
@ -0,0 +1,55 @@
|
||||
## 目标与范围
|
||||
- 设计并梳理六类头衔效果:1领取优惠券、2抽奖折扣、3签到双倍积分、4领取道具卡、5概率加成、6奖励双倍。
|
||||
- 输出参数规范、叠加与封顶策略、作用域策略,以及标准化新建流程(管理端接口与数据约束)。
|
||||
|
||||
## 效果类型与参数规范
|
||||
- 1 领取优惠券:`template_id`(券模板ID)、`frequency.period`(day|week|month)、`frequency.times`(周期次数)。示例:`{"template_id":123,"frequency":{"period":"day","times":1}}`(种子示例 `internal/api/admin/titles_seed.go:31`)。
|
||||
- 2 抽奖折扣:`discount_type`(percentage|fixed)、`value_x1000`(千分比或固定额)、`max_discount_x1000`(折扣封顶)。示例:`{"discount_type":"percentage","value_x1000":100,"max_discount_x1000":300}`(`internal/api/admin/titles_seed.go:32`)。
|
||||
- 3 签到双倍积分:`multiplier_x1000`(如2000表示x2)、`daily_cap_points`(每日积分上限)。示例:`{"multiplier_x1000":2000,"daily_cap_points":3000}`(`internal/api/admin/titles_seed.go:33`)。
|
||||
- 4 领取道具卡:`template_id`、`frequency.period/times`(同“领券”频控)。示例:`{"template_id":456,"frequency":{"period":"week","times":2}}`(`internal/api/admin/titles_seed.go:34`)。
|
||||
- 5 概率加成:`target_prize_ids[]`(目标奖品ID)、`boost_x1000`(加成千分比)、`cap_x1000`(单项封顶,可选)。示例:`{"target_prize_ids":[1,2],"boost_x1000":100,"cap_x1000":300}`;应用参考 `internal/service/activity/draw_with_effects.go:104-131`。
|
||||
- 6 奖励双倍:`target_prize_ids[]`、`chance_x1000`(命中概率千分比)、`period_cap_times`(期内命中上限,可选)。示例:`{"target_prize_ids":[3],"chance_x1000":150}`;命中判定参考 `internal/service/activity/draw_with_effects.go:197-208`。
|
||||
|
||||
## 叠加与封顶策略
|
||||
- `StackingStrategy`:
|
||||
- `0` max_only:取效果最大值。
|
||||
- `1` sum_with_cap:累加并按封顶值限制(优先使用效果内的 `cap_x1000`,否则用 `CapValueX1000`)。
|
||||
- `2` first_match:首个命中后忽略后续。
|
||||
- 默认:累加并按 `CapValueX1000` 封顶(抽奖已实现 `internal/service/activity/draw_with_effects.go:117-131`、`internal/service/activity/draw_with_effects.go:146-149`)。
|
||||
- 封顶单位统一为 `x1000` 千分比,避免浮点误差。
|
||||
|
||||
## 作用域策略(ScopesJSON)
|
||||
- 字段:`activity_ids/issue_ids/category_ids` 与 `exclude` 子段;解析与匹配参考 `internal/service/title/effects_resolver.go:59-69`、`internal/service/title/effects_resolver.go:133-155`。
|
||||
- 生效原则:先排除再包含;未配置视为全局生效。
|
||||
|
||||
## 新建流程(管理端)
|
||||
- 步骤1:创建称号模板
|
||||
- `POST /api/admin/system_titles`(`internal/router/router.go:128`;处理 `internal/api/admin/titles_admin.go:81-103`)。
|
||||
- 请求体:`name/description/status/obtain_rules_json/scopes_json`(空JSON默认填 `{}`)。
|
||||
- 步骤2:为称号模板添加效果
|
||||
- `POST /api/admin/system_titles/:title_id/effects`(`internal/router/router.go:132-133`;处理 `internal/api/admin/titles_admin.go:183-213`)。
|
||||
- 请求体:`effect_type/params_json/stacking_strategy/cap_value_x1000/scopes_json/sort/status`。
|
||||
- 步骤3:分配给用户(可选,含有效期)
|
||||
- `POST /api/admin/users/:user_id/titles`(`internal/router/router.go:127`;处理 `internal/api/admin/titles_admin.go:291-353`)。
|
||||
- 请求体:`title_id/expires_at(RFC3339)/remark`。
|
||||
- 步骤4:校验
|
||||
- 列表查询与检查:`GET /api/admin/system_titles`、`GET /api/admin/system_titles/:title_id/effects`(`internal/router/router.go:126`、`internal/router/router.go:131`;处理 `internal/api/admin/titles_admin.go:27-59`、`internal/api/admin/titles_admin.go:156-171`)。
|
||||
|
||||
## 数据与规则约束
|
||||
- 名称唯一与幂等:同名不重复创建(`internal/api/admin/titles_seed.go:41-48`)。
|
||||
- 状态控制:模板与效果需同时启用才会生效(抽奖过滤 `internal/service/activity/draw_with_effects.go:46-49`、`internal/service/activity/draw_with_effects.go:80-83`)。
|
||||
- 有效期与激活:仅激活且未过期的用户称号参与计算(`internal/service/activity/draw_with_effects.go:54-67`)。
|
||||
- 参数校验:`params_json` 必填(`internal/api/admin/titles_admin.go:195-206`)。
|
||||
|
||||
## 示例:新增“签到双倍积分”称号
|
||||
- 创建模板:`name=签到达人`、`status=1`、`scopes_json={}`。
|
||||
- 添加效果:`effect_type=3`、`params_json={"multiplier_x1000":2000,"daily_cap_points":3000}`、`stacking_strategy=1`、`cap_value_x1000=3000`、`status=1`。
|
||||
- 分配给用户:设置 `expires_at`(如7天后)并激活。
|
||||
|
||||
## 示例:新增“抽奖折扣”称号
|
||||
- 创建模板:`name=折扣官`、`status=1`、`scopes_json={"activity_ids":[1001]}`。
|
||||
- 添加效果:`effect_type=2`、`params_json={"discount_type":"percentage","value_x1000":150,"max_discount_x1000":500}`、`stacking_strategy=1`、`cap_value_x1000=500`、`status=1`。
|
||||
|
||||
## 后续实施建议
|
||||
- 在签到与购票模块接入统一解析器与效果计算器,实现(2)(3)的实际应用;领取型(1)(4)接入 `UserTitleEffectClaims` 做周期限流与资产发放;抽奖效果写入审计日志。
|
||||
- 你确认后,我将按上述流程与参数规范为每种类型提供可执行的创建脚手架(接口调用示例与测试用例),并补齐未接入业务的效果逻辑。
|
||||
41
.trae/documents/安全审计结论与修复方案(称号_特效_优惠券_抽奖).md
Normal file
41
.trae/documents/安全审计结论与修复方案(称号_特效_优惠券_抽奖).md
Normal file
@ -0,0 +1,41 @@
|
||||
## 审计重点
|
||||
- 配置与密钥:敏感信息明文存储在 `configs/*.toml`(数据库密码、JWT密钥、COS密钥、微信AppSecret、commit_master_key)。
|
||||
- 路由与鉴权:管理端多数接口已启用 `AdminTokenAuthVerify`;但存在非鉴权管理端接口(如 `seed_default`、`ensure_titles`)。
|
||||
- 特效参数:`SystemTitleEffects.ParamsJSON` 后端仅做“非空”校验,运行期解析失败静默忽略;缺少数值范围与集合规模约束。
|
||||
- 抽奖随机性:主密钥未打印日志;HMAC与拒绝采样实现合理;但密钥来源为明文配置文件,需迁移至环境变量。
|
||||
- 优惠券/道具卡:分配/发放缺少配额与上限控制、幂等与审计;优惠券发放未限制超管。
|
||||
|
||||
## 具体风险与代码位置
|
||||
- 明文密钥:`configs/fat_configs.toml:32`、`configs/*_configs.toml` 多处(JWT/COS/MySQL/Wechat/commit_master_key)。
|
||||
- 管理端非鉴权路由:`internal/router/router.go`(`/api/admin/system_titles/seed_default`、`/api/admin/menu/ensure_titles`)。
|
||||
- 特效参数缺失校验:`internal/api/admin/titles_admin.go:173-213, 225-260`(仅非空);运行期解析:`internal/service/activity/draw_with_effects.go:104-149`(失败忽略)。
|
||||
- 优惠券发放权限与配额:`internal/api/admin/users_admin.go::AddUserCoupon`(无超管校验、无模板配额扣减);道具卡分配虽限超管,但无上限/审计:`internal/api/admin/item_cards_admin.go::AssignUserItemCard`。
|
||||
|
||||
## 修复方案(分阶段)
|
||||
1) 配置与密钥治理
|
||||
- 将敏感配置迁移到环境变量;`configs/*.toml`改为占位与示例;确保不在日志输出敏感值。
|
||||
- 在密钥读取处加入检测:若为空或默认值,拒绝启动并记录安全告警。
|
||||
|
||||
2) 鉴权与权限
|
||||
- 将所有管理端变更型接口统一置于鉴权组;保留非鉴权的仅限登录与必要的只读接口。
|
||||
- 优惠券发放接口增加超管校验或细粒度权限;引入操作审计(记录操作者、时间、对象、数量)。
|
||||
|
||||
3) 特效参数强校验
|
||||
- 后端在 `Create/ModifySystemTitleEffect` 按 `effect_type` 反序列化到明确 struct,启用 `DisallowUnknownFields`;
|
||||
- 数值边界:`boost_x1000/chance_x1000/cap_x1000` 禁止负数,限制最大值;`target_prize_ids` 长度限制与去重;
|
||||
- 运行期解析失败不再静默:记录结构化告警日志,并在管理端提示配置异常。
|
||||
|
||||
4) 优惠券/道具卡防滥用
|
||||
- 模板配额与上限:发放时扣减可用数量,事务内完成;超量拒绝。
|
||||
- 用户持有上限与重复发放策略:同模板在时间窗口内单用户限制;引入幂等键防重复请求。
|
||||
- 操作审计与限流:关键接口限流、审批或二次确认;记录审计日志。
|
||||
|
||||
5) 抽奖安全稳健
|
||||
- 保持现有 HMAC 与拒绝采样;确保 `commit_master_key` 仅从安全环境加载;周期性轮换与版本化控制。
|
||||
|
||||
## 交付内容
|
||||
- 安全问题清单(带文件与行号)。
|
||||
- 修复清单与实施顺序(先密钥与鉴权→后特效参数校验→最后配额/审计/限流)。
|
||||
- 修复后的验证方案:单元测试、接口防护测试、管理端提示验证。
|
||||
|
||||
如确认,我将按上述方案逐项实施修复并提交相应代码与测试。
|
||||
40
.trae/documents/排查并初始化“称号管理”数据的执行计划.md
Normal file
40
.trae/documents/排查并初始化“称号管理”数据的执行计划.md
Normal file
@ -0,0 +1,40 @@
|
||||
## 问题现象
|
||||
- 前端“运营→称号管理”页面列表为空。
|
||||
- 可能原因:后端未创建称号数据、未登录导致接口被拦截、数据库未连接、或菜单未展示导致误判。
|
||||
|
||||
## 状态检查
|
||||
- 后端运行:确认服务已启动并连接数据库(`go run ./main.go -env dev` 日志无报错)。
|
||||
- 登录状态:确认在管理端已登录并持有有效 Token(接口返回 `token`,在请求头 `Authorization: Bearer <token>`)。
|
||||
- 路由与接口:
|
||||
- 列表接口:`GET /api/admin/system_titles`(需要登录)。
|
||||
- 初始化接口:`POST /api/admin/system_titles/seed_default`(非认证组,便于快速初始化)。
|
||||
- 菜单补齐:`POST /api/menu/ensure_titles`(非认证组,确保“称号管理”菜单出现)。
|
||||
|
||||
## 初始化步骤
|
||||
1. 管理员登录(你提供的账号密码)
|
||||
- 账号:`admin`
|
||||
- 密码:`chat2025`(需 MD5 后传输:`39f841df6f9a4768330b00d3ab9f1b4d`)
|
||||
- `curl -s -X POST http://localhost:9991/api/admin/login -H 'Content-Type: application/json' -d '{"username":"admin","password":"39f841df6f9a4768330b00d3ab9f1b4d"}'`
|
||||
- 记录返回 `token`
|
||||
2. 初始化 6 个称号及效果
|
||||
- `curl -s -X POST http://localhost:9991/api/admin/system_titles/seed_default`
|
||||
- 预期返回:`{ created: N, exists: M, ids: [...] }`(`created+exists=6`)
|
||||
3. 菜单补齐(后端动态菜单模式)
|
||||
- `curl -s -X POST http://localhost:9991/api/menu/ensure_titles`
|
||||
- 预期返回:`{ ensured: true, parent_id: <Operations>, menu_id: <Titles> }`
|
||||
4. 验证列表
|
||||
- `curl -s 'http://localhost:9991/api/admin/system_titles?page=1&page_size=20' -H 'Authorization: Bearer <token>'`
|
||||
- 预期返回:`{ list: [...], total: 6, page: 1, page_size: 20 }`
|
||||
|
||||
## 前端验证
|
||||
- 在管理端左侧菜单进入“运营管理 → 称号管理”,页面应显示 6 条数据。
|
||||
- 若仍为空:
|
||||
- 打开浏览器“网络”面板,检查 `admin/system_titles` 响应码与返回体,是否 401(未登录)或 200 但 `list=[]`。
|
||||
- 若 `list=[]`,说明初始化未成功或数据库无数据(重复调用步骤2)。
|
||||
|
||||
## 说明与已修复事项
|
||||
- 之前的路由重复注册导致 Gin panic,已修复为仅在“非认证组”保留 `POST /api/admin/system_titles/seed_default`;构建通过。
|
||||
- 初始化的 6 个称号及效果已内置参数,后续可按你的运营实际替换 `template_id` 与 `target_prize_ids` 等占位参数。
|
||||
|
||||
## 后续扩展
|
||||
- 如需完善“称号模板与效果”的 CRUD 管理页,我将新增后端 CRUD 与前端编辑表单,支持运营在页面上直接增删改查。
|
||||
27
.trae/documents/新增“运营_头衔管理”菜单与页面接入计划.md
Normal file
27
.trae/documents/新增“运营_头衔管理”菜单与页面接入计划.md
Normal file
@ -0,0 +1,27 @@
|
||||
## 目标
|
||||
- 在运营管理中新增“头衔管理”菜单与页面 `/operations/titles`,支持称号列表与搜索、并提供为玩家分配称号的入口。
|
||||
- 兼容当前两种菜单控制模式:前端模块路由与后端动态菜单;确保至少前端可见并可用。
|
||||
|
||||
## 变更项
|
||||
|
||||
### 前端路由与页面
|
||||
- 路由:在 `web/admin/src/router/modules/operations.ts` 新增子路由 `titles`(`name: 'Titles'`,`component: '/operations/titles'`,`meta.title: '称号管理'`)。
|
||||
- 页面:新增 `web/admin/src/views/operations/titles/index.vue`:
|
||||
- 列表:调用 `titlesApi.getList` 展示称号名称、状态、创建时间、描述;支持名称与状态筛选。
|
||||
- 操作:
|
||||
- 行内“分配称号”:弹出 `assign-title-dialog.vue`,为指定玩家分配称号。
|
||||
- (占位)创建/编辑/删除:预留按钮,后端 CRUD 未完成则禁用或隐藏。
|
||||
|
||||
### 前端 API
|
||||
- 已有 `web/admin/src/api/titles.ts` 支持 `getList`、`assignToUser`;页面直接复用。
|
||||
|
||||
### 菜单显示(后端动态菜单模式)
|
||||
- 后端菜单接口:`/api/v3/system/menus/simple` 已存在。
|
||||
- 如需在后端模式下展示菜单:可在后台菜单管理中新增一条菜单记录(父节点为运营),`component: '/operations/titles'`,`name: 'Titles'`,`icon: 'ri:medal-line'`,`status: true`;当前计划不强制修改后端菜单数据,仅完成前端路由与页面以确保可见与可用。
|
||||
|
||||
## 验收
|
||||
- 运营管理左侧菜单出现“称号管理”,进入后能搜索与查看称号列表。
|
||||
- 行内“分配称号”打开弹窗,提交后接口返回成功并刷新列表。
|
||||
- 前端构建通过;后端接口已就绪(列表与分配)。
|
||||
|
||||
确认后我将立即实施以上前端路由与页面接入,并在必要时协助配置后端菜单以在“后端控制菜单模式”下显示。
|
||||
41
.trae/documents/用户头衔体系与权益设计.md
Normal file
41
.trae/documents/用户头衔体系与权益设计.md
Normal file
@ -0,0 +1,41 @@
|
||||
# 奖品双倍头衔设计补充
|
||||
|
||||
## 目标
|
||||
- 为“头衔”新增一种权益:抽奖命中后有一定概率将奖励倍数提升(默认双倍)。
|
||||
|
||||
## 模型与配置
|
||||
- `system_title_effects.effect_type = DRAW_REWARD_DOUBLE_CHANCE`
|
||||
- `params_json` 示例:
|
||||
- `multiplier_x1000`: 2000(默认2x,允许配置3x等)
|
||||
- `chance_x1000`: 150(表示15%)
|
||||
- `max_multiplier_cap_x1000`: 3000(总倍数上限,例如≤3x)
|
||||
- `limit_per_day`: null 或数值(每日最多触发次数)
|
||||
- `applicable_activities/issues`: 作用范围限定列表
|
||||
- 叠加策略:
|
||||
- 概率合并:`p_total = 1 - Π(1 - p_i)`,并设置上限(如≤50%)。
|
||||
- 倍数合并:与其他“奖励倍数”效果(如道具卡)相乘,最终倍数受 `max_multiplier_cap_x1000` 封顶。
|
||||
|
||||
## 抽奖流程接入
|
||||
- 触发点:`DRAW_EXECUTE` 阶段,在完成“中奖项选择”之后执行一次“是否倍增”的伯努利试验。
|
||||
- 步骤:
|
||||
1. EffectEngine 汇总用户激活头衔的 `DRAW_REWARD_DOUBLE_CHANCE`,计算 `p_total` 与期望倍数(通常为2x)。
|
||||
2. 使用抽奖同一随机源或受控RNG进行试验,命中则将当前中奖奖励按倍数提升。
|
||||
3. 将结果写入 `activity_draw_effects.reward_multiplier_x1000`,并记录 `source_type=TITLE`、`source_id=title_id`、命中与否、概率参数。
|
||||
4. 回执与发奖按倍数后的值结算(库存/道具类型按现有规则处理)。
|
||||
|
||||
## 审计与风控
|
||||
- 落盘:在抽奖效果表记录每次试验的参数与结果,便于复核与分析。
|
||||
- 上限:概率与倍数均受可配置总封顶约束,防止过度放大。
|
||||
- 并发:同一抽奖回合仅进行一次合并后的试验,避免重复计算。
|
||||
|
||||
## 管理端与前端
|
||||
- 管理端在“头衔效果”配置表单中提供:倍数、概率、封顶、作用范围、每日上限等字段。
|
||||
- 用户端抽奖页面展示“我的双倍概率与倍数”提示;在中奖结果处标注“头衔加成(双倍)”。
|
||||
|
||||
## 测试与验收
|
||||
- 用例:
|
||||
- 单头衔15%双倍 → 约15%回合出现倍增且倍数=2x。
|
||||
- 多头衔概率合并(10%与20%)→ 总概率约28%,倍数与其他倍数效果(如1.5x道具卡)相乘并≤总封顶。
|
||||
- 封顶与每日上限有效;审计记录完整。
|
||||
|
||||
确认后,我将把该效果纳入头衔体系的实现与抽奖流程接入。
|
||||
157
.trae/documents/称号管理重构对齐后端模型的实施计划.md
Normal file
157
.trae/documents/称号管理重构对齐后端模型的实施计划.md
Normal file
@ -0,0 +1,157 @@
|
||||
## 目标
|
||||
|
||||
* 移除无关的“客服组”等错误效果类型与字段,严格对齐后端模型定义(SystemTitles/SystemTitleEffects/UserTitles)。
|
||||
|
||||
* 统一“效果类型/参数/叠加策略/封顶/作用范围”配置为后端可用的结构,确保抽奖逻辑(effect\_type=5/6)准确生效。
|
||||
|
||||
* 优化前端管理界面与 API 交互,保证运营可视化配置与后端运行一致。
|
||||
|
||||
## 范围
|
||||
|
||||
* 前端文件:
|
||||
|
||||
* `web/admin/src/views/operations/titles/components/EffectEditDialog.vue`
|
||||
|
||||
* `web/admin/src/views/operations/titles/components/EffectManagerDialog.vue`
|
||||
|
||||
* `web/admin/src/views/operations/titles/components/RuleConfigDialog.vue`
|
||||
|
||||
* `web/admin/src/views/operations/titles/index.vue`
|
||||
|
||||
* `web/admin/src/api/titles.ts`
|
||||
|
||||
* 后端仅对齐说明,不改动逻辑(模型/服务已正确):
|
||||
|
||||
* `internal/service/activity/draw_with_effects.go`(抽奖应用)
|
||||
|
||||
* `internal/service/title/effects_resolver.go`(效果范围)
|
||||
|
||||
* `internal/api/admin/titles_admin.go`(创建/修改/分配)
|
||||
|
||||
## 重构设计(按模型对齐)
|
||||
|
||||
### 效果类型与参数(替换现有 UI 与默认值)
|
||||
|
||||
* 1 领券(优惠券使者)
|
||||
|
||||
* `params_json`:`{ "template_id": number, "frequency": { "period": "day|week|month", "times": number } }`
|
||||
|
||||
* 2 抽奖折扣
|
||||
|
||||
* `params_json`:`{ "discount_type": "percentage|fixed", "value_x1000": number, "max_discount_x1000": number }`
|
||||
|
||||
* 3 签到倍数
|
||||
|
||||
* `params_json`:`{ "multiplier_x1000": number, "daily_cap_points": number }`
|
||||
|
||||
* 4 领道具卡
|
||||
|
||||
* `params_json`:`{ "template_id": number, "frequency": { "period": "week|month", "times": number } }`
|
||||
|
||||
* 5 概率加成(抽奖)
|
||||
|
||||
* `params_json`:`{ "target_prize_ids": number[], "boost_x1000": number, "cap_x1000": number? }`
|
||||
|
||||
* 6 奖品双倍概率(抽奖)
|
||||
|
||||
* `params_json`:`{ "target_prize_ids": number[], "chance_x1000": number, "period_cap_times": number? }`
|
||||
|
||||
* 移除:`生日特权/专属客服` 效果类型与全部相关字段(UI/默认值/展示/说明)。
|
||||
|
||||
### 叠加策略与封顶(严格按模型)
|
||||
|
||||
* `stacking_strategy` 取值与文案:
|
||||
|
||||
* 0:最大值(max\_only)
|
||||
|
||||
* 1:累加封顶(sum\_with\_cap)
|
||||
|
||||
* 2:首个匹配(first\_match)
|
||||
|
||||
* `cap_value_x1000`:统一封顶,单位千分比(用于 5/6 合并时的全局封顶)。
|
||||
|
||||
* 统一前端控件与默认值(默认建议:1 累加封顶;`cap_value_x1000`=0 表示不封顶)。
|
||||
|
||||
### 效果作用范围(效果级 scopes\_json)
|
||||
|
||||
* 字段:`activity_ids/issue_ids/category_ids` 与 `exclude.*`
|
||||
|
||||
* 在效果编辑对话框中新增“作用范围”折叠面板:
|
||||
|
||||
* 多选输入上述集合;支持排除列表;空集合视为全局。
|
||||
|
||||
* 展示格式:在效果列表中将 `scopes_json` 可视化为“包含/排除”标签。
|
||||
|
||||
### 获得规则(标题级 obtain\_rules\_json)
|
||||
|
||||
* 保留 UI(方便运营标注),明确说明其仅存储,不参与当前自动授予;后续如接入事件评估器再使用。
|
||||
|
||||
* JSON 结构:`{ methods: string[], conditions: object }`;维持现有 RuleConfigDialog 保存逻辑。
|
||||
|
||||
## 具体改动点(不执行,供审核)
|
||||
|
||||
* `EffectEditDialog.vue`
|
||||
|
||||
* 替换效果类型下拉项为 1\~6 正确枚举;删除“生日特权/专属客服”。
|
||||
|
||||
* 重写各类型的表单字段与 `getDefaultParams()` 返回值,改为上述模型参数结构。
|
||||
|
||||
* 调整 `stacking_strategy` 单选值为 0/1/2;文案与后端一致。
|
||||
|
||||
* 新增“作用范围”字段并在提交时携带 `scopes_json`。
|
||||
|
||||
* `EffectManagerDialog.vue`
|
||||
|
||||
* 更新 `effectTypes` 名称与 Tag 映射为正确文案:`领券/抽奖折扣/签到倍数/领道具卡/概率加成/双倍概率`。
|
||||
|
||||
* 调整 `stackingStrategies` 文案为 0/1/2;去掉无模型项。
|
||||
|
||||
* 更新参数格式化 `formatParam()`,支持 `template_id/frequency/discount_type/value_x1000/...`。
|
||||
|
||||
* 增加对 `scopes_json` 的展示:包含/排除集合标签。
|
||||
|
||||
* `RuleConfigDialog.vue`
|
||||
|
||||
* 保留现有 UI;在文档中明确“标题级范围不参与运行时判定”,避免误解。
|
||||
|
||||
* `titles.ts`
|
||||
|
||||
* 校验请求参数与后端一致:`effect_type/params_json/stacking_strategy(0|1|2)/cap_value_x1000/scopes_json/sort/status`。
|
||||
|
||||
## 数据迁移与兼容
|
||||
|
||||
* 已存在的效果记录:
|
||||
|
||||
* 运营检查并手动改为新结构(通过管理端编辑),或提供一次性迁移脚本:按 `effect_type` 将旧 `params_json` 映射为新字段。
|
||||
|
||||
* 前端:
|
||||
|
||||
* 旧页面中“生日特权/客服组”等效果无法再新建;编辑时提示需切换为模型定义效果。
|
||||
|
||||
## 测试与验收
|
||||
|
||||
* 配置与展示:创建每一种效果,检查列表显示与 JSON 持久化。
|
||||
|
||||
* 抽奖逻辑:
|
||||
|
||||
* `effect_type=5`:目标奖品权重按 `boost_x1000` 调整,叠加与封顶正确;参见 `internal/service/activity/draw_with_effects.go`。
|
||||
|
||||
* `effect_type=6`:双倍概率按 `chance_x1000` 合并封顶;命中后倍数 x2;名称后缀标识;参见同文件。
|
||||
|
||||
* 作用范围:在 issueID 不同期下验证包含/排除命中行为;参见 `effects_resolver.go` 与抽奖文件。
|
||||
|
||||
* 运营页面每个功能或者配置字段需要详细描述
|
||||
|
||||
## 交付物
|
||||
|
||||
* 代码层面的重构(上述文件修改)。
|
||||
|
||||
* 文档更新:已在 `docs/称号系统/说明文档.md` 完成字段级详解与代码绑定点,重构后同步补充“字段校验与默认值”章节与新 UI 截图位点。
|
||||
|
||||
## 风险与注意
|
||||
|
||||
* 现有错误效果类型的数据需运营确认是否迁移或删除。
|
||||
|
||||
* 前端表单字段与后端参数必须完全对齐,否则抽奖效果不会生效。
|
||||
|
||||
确认后我将开始实施重构并提供测试用例与验证记录。
|
||||
67
.trae/documents/称号系统字段级说明与代码绑定文档增强计划.md
Normal file
67
.trae/documents/称号系统字段级说明与代码绑定文档增强计划.md
Normal file
@ -0,0 +1,67 @@
|
||||
## 目标
|
||||
- 交付一份字段级的《说明文档》增强版,逐一解释:
|
||||
1) 获得规则(ObtainRulesJSON)字段字典、用途、代码绑定点;
|
||||
2) 使用范围(ScopesJSON)字段字典、用途、代码绑定点;
|
||||
3) 效果数量与叠加策略(多效果、排序、封顶、组合规则);
|
||||
4) 规则助手(前端可视化配置)字段映射与校验;
|
||||
- 所有字段给出示例与“在代码块哪里起作用”的精确文件/行号。
|
||||
|
||||
## 文档结构
|
||||
- 《docs/称号系统/说明文档.md》新增章节:
|
||||
- 获得规则字段详解(ObtainRulesJSON)
|
||||
- 使用范围字段详解(ScopesJSON:标题&效果)
|
||||
- 效果数量、叠加策略与封顶(StackingStrategy/CapValueX1000)
|
||||
- 规则助手详解(UI字段 → JSON 映射 → 后端字段)
|
||||
- 代码绑定点总览(带 file_path:line_number 引用)
|
||||
|
||||
## 字段级内容(概要)
|
||||
- 获得规则(ObtainRulesJSON)
|
||||
- 字段:`methods: string[]`、`conditions: object`(如 `lottery_type`, `min_amount`, `time_range` 等)
|
||||
- 用途:当前仅“存取”,未参与后端自动授予逻辑(需后续服务层接入)
|
||||
- 代码绑定:
|
||||
- 写入:`internal/api/admin/titles_admin.go:88/94/125`
|
||||
- 预置:`internal/api/admin/titles_seed.go:54`
|
||||
- DAO映射:`internal/repository/mysql/dao/system_titles.gen.go:34/53/77/102`
|
||||
- 示例与建议:提供多场景 JSON 模板与后续接入建议(事件钩子/规则评估器)
|
||||
- 使用范围(ScopesJSON)
|
||||
- 标题层:`SystemTitles.ScopesJSON`(目前仅存取,不参与运行时判定)
|
||||
- 效果层:`SystemTitleEffects.ScopesJSON`(运行时生效,活动/期/分类的包含/排除)
|
||||
- 字段字典:`activity_ids, issue_ids, category_ids, exclude.{...}`
|
||||
- 代码绑定:
|
||||
- 解析与匹配:`internal/service/title/effects_resolver.go`(scopeMatch)
|
||||
- 抽奖应用:`internal/service/activity/draw_with_effects.go`(issue 过滤)
|
||||
- 管理端读写:`internal/api/admin/titles_admin.go`、预置:`titles_seed.go`
|
||||
- 示例:包含与排除的组合配置(带示意流程)
|
||||
- 效果数量与叠加策略
|
||||
- 每个称号可挂载多个效果;按 `sort` 排序;`status` 控制启用;
|
||||
- `StackingStrategy`:0最大值/1累加封顶/2首个匹配/默认累加封顶;
|
||||
- `CapValueX1000`:统一封顶(千分比)
|
||||
- 代码绑定:
|
||||
- 抽奖应用:`internal/service/activity/draw_with_effects.go`(effect_type=5/6 合并规则)
|
||||
- 模型定义:`internal/repository/mysql/model/system_title_effects.gen.go`
|
||||
- 示例:多效果组合的实际影响(权重调整/双倍合并概率)
|
||||
- 规则助手(前端)
|
||||
- 位置:`web/admin/src/views/operations/titles/components/RuleConfigDialog.vue`
|
||||
- 字段映射:UI → `obtain_rules_json`/`scopes_json`
|
||||
- 校验/预览:JSON 校验、示例模板、保存策略
|
||||
- 注意:当前 effect_type=5/6 的前端参数与后端不一致(计划列出对齐方案与字段映射表,不改代码,先文档说明)
|
||||
|
||||
## 代码绑定点(示例引用)
|
||||
- ObtainRulesJSON 写入:`internal/api/admin/titles_admin.go:88, 94, 125`
|
||||
- ScopesJSON 应用匹配:`internal/service/title/effects_resolver.go:line`(具体行号在交付时标注)
|
||||
- 抽奖效果应用:`internal/service/activity/draw_with_effects.go:line`(包含 5/6 的 switch 分支具体行号)
|
||||
- 种子数据:`internal/api/admin/titles_seed.go:31-37, 54-56, 70-80`
|
||||
|
||||
## 交付步骤
|
||||
1. 使用只读工具确认所有字段与代码绑定点的具体行号;
|
||||
2. 更新《说明文档.md》,按上述结构补充完备的字段级说明与代码引用;
|
||||
3. 提供前端与后端参数对齐表(仅文档,不改代码);
|
||||
4. 给出后续建议:自动授予逻辑接入方案(服务层/事件钩子/任务调度)。
|
||||
|
||||
## 验收标准
|
||||
- 文档包含所有字段的用途、类型、取值范围、示例;
|
||||
- 每个字段“在哪里起作用”都给出精确文件/行号;
|
||||
- 覆盖四项:获得规则、使用范围、效果数量、规则助手;
|
||||
- 补充参数对齐提醒与后续接入建议;
|
||||
|
||||
确认后我将开始更新文档并提交增强内容。
|
||||
116
.trae/documents/称号系统:完整创建流程与使用说明交付计划.md
Normal file
116
.trae/documents/称号系统:完整创建流程与使用说明交付计划.md
Normal file
@ -0,0 +1,116 @@
|
||||
## 目标
|
||||
- 为“抽奖后获得双倍奖励,并具备时效”的业务提供可执行的创建流程与说明文档。
|
||||
- 输出一份可直接用于运营与研发的《说明文档.md》,包含:项目规划、实施方案、进度记录,以及完整的操作与集成指南。
|
||||
|
||||
## 核心概念
|
||||
- SystemTitles:系统称号主数据,含名称、描述、获取规则、适用范围等。参考 internal/repository/mysql/model/system_titles.gen.go
|
||||
- SystemTitleEffects:称号效果,含效果类型、参数、叠加策略、封顶值等。参考 internal/repository/mysql/model/system_title_effects.gen.go
|
||||
- UserTitles:用户称号记录,含生效状态与过期时间,用于管控时效。参考 internal/repository/mysql/model/user_titles.gen.go
|
||||
- 效果类型示例:在 admin/titles_seed.go 已内置6类,含“双倍之王(EffectType=6)”。参考 internal/api/admin/titles_seed.go:18-37
|
||||
|
||||
## 后端接口一览(供前后端与运维对齐)
|
||||
- 系统称号(管理员)
|
||||
- GET/POST/PUT/DELETE `/api/admin/system_titles`
|
||||
- GET/POST/PUT/DELETE `/api/admin/system_titles/:title_id/effects`
|
||||
- 用户称号分配(管理员)
|
||||
- POST `/api/admin/users/:user_id/titles`(分配或更新有效期与激活状态)
|
||||
- 路由参考:internal/router/router.go
|
||||
- 管理端控制器参考:internal/api/admin/titles_admin.go(列表/创建/修改/删除/分配)
|
||||
|
||||
## 创建流程(运营页面)
|
||||
1. 进入管理端:`/operations/titles`(确保菜单已存在,见 EnsureTitlesMenu)
|
||||
2. 创建称号:
|
||||
- 名称:例如“抽奖双倍达人”
|
||||
- 描述:例如“抽奖奖励翻倍,限时24小时”
|
||||
- 状态:启用
|
||||
3. 配置获取规则(ObtainRulesJSON):
|
||||
- methods:`["lottery"]`
|
||||
- conditions:可选,如抽奖类型 `lottery_type="normal"`
|
||||
- 示例:
|
||||
```json
|
||||
{
|
||||
"methods": ["lottery"],
|
||||
"conditions": {"lottery_type": "normal"}
|
||||
}
|
||||
```
|
||||
4. 配置适用范围(ScopesJSON):可为空或限定品类/场次/用户等级,按需填写
|
||||
5. 添加效果(SystemTitleEffects):选择“EffectType=6(双倍概率)”,并设置参数:
|
||||
- `target_prize_ids`: [](空表示所有奖品)
|
||||
- `chance_x1000`: 1000(100% 双倍;或按需改为 200=20%)
|
||||
- `period_cap_times`: 1(周期内最多触发次数,建议与日/活动周期对齐)
|
||||
- 示例:
|
||||
```json
|
||||
{
|
||||
"target_prize_ids": [],
|
||||
"chance_x1000": 1000,
|
||||
"period_cap_times": 1
|
||||
}
|
||||
```
|
||||
6. 保存称号与效果,确认列表中显示。
|
||||
|
||||
## 分配与时效(运营或自动)
|
||||
- 运营手动分配(页面“分配用户”对话框):
|
||||
- 选择用户,设定有效期类型:
|
||||
- `permanent` 永久
|
||||
- `period` 指定开始/结束时间(填写 `start_time`/`end_time`)
|
||||
- `days` 按天数有效(例如 1 天)
|
||||
- 勾选“覆盖现有/发送通知”按需配置,提交后生效
|
||||
- 抽奖后自动分配(后端集成点):
|
||||
- 在抽奖流程成功后,调用分配接口或服务方法为用户写入 UserTitles(设置 `expires_at`)
|
||||
- 建议在抽奖服务中统一封装:检查用户是否已拥有称号;若没有则分配,有则延长或保持不变(按业务策略)
|
||||
|
||||
## 抽奖流程应用双倍效果(后端逻辑)
|
||||
- 在发奖时:
|
||||
1. 读取用户当前激活且未过期的 UserTitles
|
||||
2. 聚合其 SystemTitleEffects(EffectType=6),解析 `chance_x1000` 与 `period_cap_times`
|
||||
3. 按概率/次数限制判断是否触发翻倍,将奖品数量或价值翻倍(或对指定 `target_prize_ids` 生效)
|
||||
4. 记录一次触发(用于控制当期 `period_cap_times`)
|
||||
- 注意:叠加策略(StackingStrategy)与封顶(CapValueX1000)需一并考虑,避免超限
|
||||
|
||||
## 配置示例(可直接复用)
|
||||
- 称号获取规则(让称号只在抽奖场景下获得)
|
||||
```json
|
||||
{
|
||||
"methods": ["lottery"],
|
||||
"conditions": {"lottery_type": "normal"}
|
||||
}
|
||||
```
|
||||
- 双倍效果参数(对所有奖品 100% 双倍,且周期内最多 1 次)
|
||||
```json
|
||||
{
|
||||
"target_prize_ids": [],
|
||||
"chance_x1000": 1000,
|
||||
"period_cap_times": 1
|
||||
}
|
||||
```
|
||||
- 用户分配有效期(按天)
|
||||
```json
|
||||
{
|
||||
"expire_type": "days",
|
||||
"days": 1,
|
||||
"remark": "抽奖获得双倍称号",
|
||||
"override_existing": true,
|
||||
"send_notification": true
|
||||
}
|
||||
```
|
||||
|
||||
## 测试与验收
|
||||
- 管理端:
|
||||
- 创建称号与效果 → 列表校验 → 为测试用户分配(1天有效)
|
||||
- 抽奖流程:
|
||||
- 模拟用户抽奖 → 校验发奖结果是否翻倍 → 检查 period_cap_times 限制是否生效
|
||||
- 到期后再次抽奖:不再翻倍(验证 `expires_at` 生效)
|
||||
- 数据核查:
|
||||
- 检查 UserTitles 记录及过期时间
|
||||
- 检查效果触发记录(若有)与叠加/封顶逻辑
|
||||
|
||||
## 说明文档结构(交付内容)
|
||||
- 文件:`docs/称号系统/说明文档.md`
|
||||
- 项目规划:业务目标、范围、时间节点
|
||||
- 实施方案:模型与接口、创建与分配流程、抽奖集成方案
|
||||
- 进度记录:每次变更与验证结果
|
||||
- 操作指南:页面操作步骤、配置示例、常见问题
|
||||
- 验收标准:用例列表与通过条件
|
||||
|
||||
## 后续工作
|
||||
- 我将依据上述结构生成《说明文档.md》,并补充操作截图位点与接口示例;如需我同时在抽奖服务中添加自动分配与双倍应用的集成代码,请确认后我直接实现并提交测试用例。
|
||||
12
.trae/documents/简化双倍概率作用范围并对齐前端配置的实施计划.md
Normal file
12
.trae/documents/简化双倍概率作用范围并对齐前端配置的实施计划.md
Normal file
@ -0,0 +1,12 @@
|
||||
## 目标
|
||||
- 将效果编辑对话框的作用范围精简为“包含活动(activity_ids)”“包含期(issue_ids)”与“排除期(exclude.issue_ids)”三项,移除分类等无关选项。
|
||||
- 在效果列表中新增“作用范围”列,直观展示包含/排除的活动与期。
|
||||
- 保持与后端抽奖逻辑一致,确保 effect_type=6 的参数与范围保存后可被正确解析与应用。
|
||||
|
||||
## 改动点
|
||||
- 编辑对话框:只保留 activity_ids、issue_ids、exclude.issue_ids 的输入;提交时构建简化的 scopes_json。
|
||||
- 效果列表:新增范围列并格式化展示。
|
||||
- API 类型:已支持 scopes_json(无需后端改动)。
|
||||
|
||||
## 验收
|
||||
- 可在管理端创建或编辑双倍效果,查看列表范围列;抽奖时按期过滤生效。
|
||||
66
.trae/documents/运营端称号规则与效果配置页面实现计划.md
Normal file
66
.trae/documents/运营端称号规则与效果配置页面实现计划.md
Normal file
@ -0,0 +1,66 @@
|
||||
## 目标
|
||||
- 在运营管理中新增“称号模板与规则”页面,支持创建/编辑/删除称号(含获取规则 obtain_rules_json 与作用域 scopes_json)。
|
||||
- 新增“称号效果配置”页面(或同页二级标签),支持为每个称号新增/编辑效果项,覆盖六类功能:领取优惠券、抽奖折扣、签到倍数、领取道具卡、概率加成、双倍奖励。
|
||||
- 所有配置直写到既有表:`system_titles`、`system_title_effects`,并保证JSON合法与可视化表单输入。
|
||||
|
||||
## 后端接口(CRUD)
|
||||
- 系统称号(Templates)
|
||||
- `GET /api/admin/system_titles`:分页检索(name/status),返回 list/total。
|
||||
- `POST /api/admin/system_titles`:创建称号,入参:`name, description?, status, obtain_rules_json(字符串JSON), scopes_json(字符串JSON)`。
|
||||
- `PUT /api/admin/system_titles/:title_id`:修改;字段同上。
|
||||
- `DELETE /api/admin/system_titles/:title_id`:删除(可选:级联删除该称号的效果项,需二次确认)。
|
||||
- 称号效果(Effects)
|
||||
- `GET /api/admin/system_titles/:title_id/effects`:列表(status/effect_type筛选)。
|
||||
- `POST /api/admin/system_titles/:title_id/effects`:新增效果;入参:`effect_type, params_json(字符串JSON), stacking_strategy, cap_value_x1000?, scopes_json?, sort, status`。
|
||||
- `PUT /api/admin/system_titles/:title_id/effects/:effect_id`:修改;字段同上。
|
||||
- `DELETE /api/admin/system_titles/:title_id/effects/:effect_id`:删除。
|
||||
- 校验与约束
|
||||
- `obtain_rules_json`/`scopes_json`/`params_json`必须为合法JSON(默认 `{}`),否则拒绝并提示。
|
||||
- `stacking_strategy` 枚举校验:0 最大值、1 累加封顶、2 首个匹配。
|
||||
- `cap_value_x1000`、各类 `*_x1000` 必须为非负整数;必要时设上限。
|
||||
|
||||
## 前端页面
|
||||
- 菜单位置:`运营管理 → 称号管理`(保持现有入口),页面内使用二级标签:
|
||||
- Tab1:称号模板(列表 + 新建/编辑弹窗)
|
||||
- Tab2:效果配置(左侧选择称号或从行操作进入,右侧效果列表 + 新建/编辑弹窗)
|
||||
- 表单设计
|
||||
- 称号模板弹窗:
|
||||
- 基本:名称、描述、状态(启用/停用)
|
||||
- 获取规则 obtain_rules_json:可视化构建器(单选/多选项生成JSON):
|
||||
- 任务达成:`{"type":"mission","mission_id":...,"times":...}`
|
||||
- 等级达成:`{"type":"level","level":...}`
|
||||
- 支付购买:`{"type":"purchase","product_id":...,"price":...}`
|
||||
- 手动发放:`{"type":"manual"}`(缺省)
|
||||
- 作用域 scopes_json:选择活动分类/活动/期次,生成:`{"activity_ids":[],"issue_ids":[],"category_ids":[],"exclude":{...}}`
|
||||
- 效果配置弹窗(按效果类型动态渲染表单):
|
||||
- 领取优惠券:`template_id`、`period(day|week|month)`、`times`(生成 `{template_id,frequency:{period,times}}`)
|
||||
- 抽奖折扣:`discount_type(percentage|fixed)`、`value_x1000`、`max_discount_x1000?`、`min_price?`
|
||||
- 签到倍数:`multiplier_x1000`、`daily_cap_points?`
|
||||
- 领取道具卡:`template_id`、`period`、`times`
|
||||
- 概率加成:`target_prize_ids[]`(从奖励列表选择)、`boost_x1000`、`cap_x1000?`、`combine(sum|max)`
|
||||
- 双倍奖励:`target_prize_ids[]`、`chance_x1000`、`period_cap_times?`
|
||||
- 通用:`stacking_strategy`、`cap_value_x1000?`、`scopes_json`(可与模板一致或另设)
|
||||
- 预览与校验
|
||||
- JSON编辑器只读预览框,展示最终写库JSON;前置校验拒绝非法JSON。
|
||||
- “合并效果预览”按钮:传 `user_id+context` 调用预览接口(可后续实现),便于运营核对。
|
||||
|
||||
## 前端技术点
|
||||
- 组件复用:沿用 `ArtTable/ArtSearchBar/ArtButtonTable` 模式,仿照“道具卡/优惠券”页面实现。
|
||||
- 表单动态:根据 `effect_type` 切换字段与校验;内置枚举与字典展示中文标签。
|
||||
- 交互:创建成功后刷新列表;编辑表单带现有JSON -> 反序列化填入表单。
|
||||
|
||||
## 数据与合法性
|
||||
- 所有 JSON 列默认值使用 `{}`,避免“空文档”错误。
|
||||
- 金额/比例/倍数统一使用 `x1000` 固定小数避免精度误差。
|
||||
- 删除称号时,如启用“级联删除效果”需弹窗确认;否则保留效果项以供审计。
|
||||
|
||||
## 验收
|
||||
- 运营可在页面新建/编辑称号模板(含获取规则与作用域),保存成功并后端入库。
|
||||
- 运营可在页面为指定称号新增/编辑各类效果,保存后生效(后续事件引擎读取)。
|
||||
- 列表检索与状态切换可用;所有JSON合法校验通过;非法输入有明确错误提示。
|
||||
|
||||
## 后续可选
|
||||
- 效果预览接口:`GET /api/admin/effects/preview?user_id&context`,返回按叠加策略后的生效明细。
|
||||
- 审计日志:记录模板与效果的变更历史、操作者与时间。
|
||||
|
||||
确认后我将按上述计划补充后端CRUD接口与前端页面(两个弹窗/二级标签),并完成联调与校验。
|
||||
62
.trae/documents/运营管理前端接入与玩家称号分配实现计划.md
Normal file
62
.trae/documents/运营管理前端接入与玩家称号分配实现计划.md
Normal file
@ -0,0 +1,62 @@
|
||||
## 目标
|
||||
- 在运营管理中接入“批量抽奖”并确保对话框正确对接后端接口(含用户搜索、期数选择、执行与验证)。
|
||||
- 在玩家管理页新增“分配称号”功能:支持搜索玩家、选择系统称号并分配(含有效期与备注)。
|
||||
|
||||
## 改造与新增点
|
||||
|
||||
### 1. 批量抽奖对话框对接
|
||||
- 位置:`web/admin/src/views/activity/rewards/modules/batch-draw-dialog.vue`
|
||||
- 接口统一由 `@/api/adminActivities` 提供;若需要使用“带效果抽奖”后端路径,保持 URL 不变(后端已在同一路由替换为带效果执行),前端无需改参。
|
||||
- 校验与交互:
|
||||
- 期数下拉:数据源 `listActivityIssues(activityId)`(已存在)。
|
||||
- 玩家搜索与选择:数据源 `listAdminUsers(params)`(已存在),保留分页与勾选逻辑。
|
||||
- 批量执行:`batchDrawForUsers(activityId, issueId, { user_ids })`,展示收据与中奖汇总。
|
||||
- 结果验证:`verifyDrawReceipt(activityId, issueId, body)` 保持结构一致。
|
||||
- UI 微调:在结果区增加“效果标识”展示(若收据选中项名称带 `(x2)` 则以标签提示“奖励翻倍”)。
|
||||
|
||||
### 2. 玩家管理页新增“分配称号”
|
||||
- 入口:`web/admin/src/views/player-manage/index.vue` 操作列新增按钮。
|
||||
- 枚举:`web/admin/src/enums/tableActionEnum.ts` 增加 `ASSIGN_TITLE`,映射中文标签与图标。
|
||||
- 弹窗组件(新增):`web/admin/src/views/player-manage/modules/assign-title-dialog.vue`
|
||||
- 表单:
|
||||
- 选择系统称号(下拉,数据源:`titlesApi.getList({page,page_size,name,status})`)。
|
||||
- 可选:有效期(日期时间)、备注(文本)。
|
||||
- 提交:`titlesApi.assignToUser(userId, { title_id, expires_at?, remark? })`,成功后 toast 并关闭。
|
||||
- 前端 API(新增):`web/admin/src/api/titles.ts`
|
||||
- `getList(params)` → `GET /api/admin/system_titles`(分页检索)。
|
||||
- `assignToUser(userId, payload)` → `POST /api/admin/users/:user_id/titles`。
|
||||
- 玩家搜索集成:保留现有搜索栏 `player-search.vue`;在结果表行的操作列加入“分配称号”按钮,点击打开弹窗并携带当前 `user_id`。
|
||||
|
||||
### 3. 后端路由与处理(需一并落地)
|
||||
- 路由:`internal/router/router.go`
|
||||
- 系统称号:`GET/POST/PUT/DELETE /api/admin/system_titles`
|
||||
- 用户分配称号:`POST /api/admin/users/:user_id/titles`
|
||||
- Handler:`internal/api/admin/titles_admin.go`
|
||||
- 列表、创建、修改、删除系统称号。
|
||||
- 用户分配:校验头衔存在与启用状态,写入或更新 `user_titles`(保持 `user_id + title_id` 唯一,激活状态与有效期)。
|
||||
- 复用服务/DAO:`internal/repository/mysql/dao/*` 与 `internal/service/title/effects_resolver.go`(后续用于预览用户生效效果)。
|
||||
|
||||
## 交互与校验
|
||||
- 分配称号:
|
||||
- 重复分配处理:若用户已持有该称号,则更新 `expires_at`/`remark`/`active`。
|
||||
- 时间校验:`expires_at` 可空;若传入需晚于当前时间。
|
||||
- 批量抽奖:
|
||||
- 期数必选、用户列表非空;执行后展示收据与命中明细。
|
||||
- 收据中的 `(x2)` 标签用于前端提示“奖励翻倍”。
|
||||
|
||||
## 验收与测试
|
||||
- 前端:
|
||||
- 玩家搜索能正常检索与分页;操作列新增“分配称号”按钮弹窗正常。
|
||||
- 批量抽奖弹窗从运营管理入口可用,执行与验证接口成功,结果展示清晰。
|
||||
- 后端:
|
||||
- 系统称号列表与 CRUD 可用;用户分配称号接口成功写 `user_titles`。
|
||||
- 集成:
|
||||
- 用户分配称号后,管理端批量抽奖能体现概率/双倍效果(已有后端支持)。
|
||||
|
||||
## 交付物
|
||||
- 新增前端 API `titles.ts` 与 `assign-title-dialog.vue`。
|
||||
- 更新 `player-manage/index.vue` 与 `tableActionEnum.ts` 增加入口。
|
||||
- 如需对批量抽奖对话框做 `(x2)` 标签显示,补充小型 UI 更新。
|
||||
- 后端路由与 Handler 实现(称号 CRUD 与分配)。
|
||||
|
||||
确认后我将立即实施上述前后端改造,并进行编译与交互联调。
|
||||
@ -14,5 +14,5 @@
|
||||
### MAC
|
||||
```
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-w -s' -trimpath -o MINI
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o build/bindbox.exe .
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -tags timetzdata -trimpath -o build/bindbox.exe .
|
||||
```
|
||||
|
||||
@ -36,6 +36,6 @@ eg :
|
||||
# 根目录下执行
|
||||
go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,guild,guild_boxes,guild_contribute_logs,guild_members,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger"
|
||||
|
||||
go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,guild,guild_boxes,guild_contribute_logs,guild_members,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger,users,user_addresses,menu_actions,menus,role_actions,role_menus,role_users,roles,order_items,orders,products,shipping_records,product_categories,user_invites,system_item_cards,user_item_cards,activity_draw_effects,banner,issue_random_commitments,activity_draw_receipts"
|
||||
go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,guild,guild_boxes,guild_contribute_logs,guild_members,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger,users,user_addresses,menu_actions,menus,role_actions,role_menus,role_users,roles,order_items,orders,products,shipping_records,product_categories,user_invites,system_item_cards,user_item_cards,activity_draw_effects,banner,issue_random_commitments,activity_draw_receipts,system_titles,system_title_effects,user_titles,user_title_effect_claims"
|
||||
|
||||
```
|
||||
281
docs/称号系统/说明文档.md
Normal file
281
docs/称号系统/说明文档.md
Normal file
@ -0,0 +1,281 @@
|
||||
# 称号系统使用说明文档
|
||||
|
||||
> 版本:v1.0
|
||||
> 日期:2025-11-16
|
||||
> 适用范围:运营、后端、前端
|
||||
|
||||
---
|
||||
|
||||
## 一、项目规划
|
||||
- 业务目标:实现“用户抽奖后获得双倍奖励(可限时)”的称号能力,运营可视化配置,后端安全可控,前端可观测。
|
||||
- 范围:系统称号主数据、称号效果配置(双倍概率)、用户称号分配与时效管理、抽奖流程双倍应用。
|
||||
- 时间节点:
|
||||
- 2025-11-16:建立说明文档,完成创建流程与使用指南。
|
||||
- 2025-11-17:完成抽奖流程集成验证与用例测试(预期)。
|
||||
|
||||
---
|
||||
|
||||
## 二、实施方案
|
||||
|
||||
### 2.1 核心模型
|
||||
- SystemTitles:称号主数据(名称、描述、获取规则、适用范围)。
|
||||
- SystemTitleEffects:称号效果(类型、参数、叠加策略、封顶值、排序、状态)。
|
||||
- UserTitles:用户称号记录(生效状态、获得时间、过期时间、来源、备注)。
|
||||
- 参考文件:
|
||||
- internal/repository/mysql/model/system_titles.gen.go
|
||||
- internal/repository/mysql/model/system_title_effects.gen.go
|
||||
- internal/repository/mysql/model/user_titles.gen.go
|
||||
|
||||
### 2.2 现有预置效果
|
||||
- 已内置6类效果,含“双倍之王(EffectType=6)”。
|
||||
- 参考:internal/api/admin/titles_seed.go:18-37
|
||||
|
||||
### 2.3 后端接口
|
||||
- 系统称号(管理员):
|
||||
- GET/POST/PUT/DELETE `/api/admin/system_titles`
|
||||
- GET/POST/PUT/DELETE `/api/admin/system_titles/:title_id/effects`
|
||||
- 用户称号分配(管理员):
|
||||
- POST `/api/admin/users/:user_id/titles`
|
||||
- 路由参考:internal/router/router.go
|
||||
- 控制器参考:internal/api/admin/titles_admin.go
|
||||
|
||||
---
|
||||
|
||||
## 三、创建流程(运营页面)
|
||||
|
||||
### 3.1 进入页面
|
||||
- 管理端路径:`/operations/titles`
|
||||
- 若菜单缺失,可调用 EnsureTitlesMenu(参考 internal/api/admin/titles_seed.go)
|
||||
|
||||
### 3.2 创建称号
|
||||
- 基本信息:
|
||||
- 名称:例如“抽奖双倍达人”
|
||||
- 描述:例如“抽奖奖励翻倍,限时24小时”
|
||||
- 状态:启用
|
||||
- 获取规则(ObtainRulesJSON)建议:
|
||||
```json
|
||||
{
|
||||
"methods": ["lottery"],
|
||||
"conditions": {"lottery_type": "normal"}
|
||||
}
|
||||
```
|
||||
- 适用范围(ScopesJSON):可为空或限定具体用户等级/品类/区域/时段。
|
||||
|
||||
### 3.3 添加效果(双倍概率)
|
||||
- 选择效果类型:EffectType=6(双倍概率)
|
||||
- 参数示例(对所有奖品100%双倍,周期最多1次):
|
||||
```json
|
||||
{
|
||||
"target_prize_ids": [],
|
||||
"chance_x1000": 1000,
|
||||
"period_cap_times": 1
|
||||
}
|
||||
```
|
||||
- 叠加与封顶:按需设置 `StackingStrategy` 与 `CapValueX1000`,用于限制叠加与封顶行为。
|
||||
|
||||
### 3.4 保存与检查
|
||||
- 保存称号与效果后,在列表中确认显示;可进入“效果管理”查看参数是否正确。
|
||||
|
||||
---
|
||||
|
||||
## 四、分配与时效
|
||||
|
||||
### 4.1 运营手动分配
|
||||
- 在称号列表中点击“分配用户”:
|
||||
- 有效期类型:`permanent`(永久)、`period`(开始/结束时间)、`days`(按天数)
|
||||
- 建议设置:`days=1`(24小时有效)
|
||||
- 备注:例如“抽奖获得双倍称号”
|
||||
- 可选:覆盖现有相同称号、发送通知
|
||||
|
||||
### 4.2 抽奖后自动分配(后端集成)
|
||||
- 在抽奖成功后调用用户称号分配接口或服务方法:
|
||||
- 若用户未持有称号 → 写入 UserTitles,设定 `expires_at`(例如当前时间+24小时)
|
||||
- 若已持有且未过期 → 按策略决定是否延长或保持(可由业务规则决定)
|
||||
|
||||
---
|
||||
|
||||
## 五、抽奖流程应用双倍效果
|
||||
|
||||
### 5.1 判定逻辑
|
||||
1. 读取用户激活且未过期的 UserTitles。
|
||||
2. 聚合其中类型为 EffectType=6 的 SystemTitleEffects。
|
||||
3. 按 `chance_x1000`(概率)与 `period_cap_times`(周期次数)判定是否触发。
|
||||
4. 若触发:将奖品数量或价值翻倍(对指定 `target_prize_ids` 生效或全部奖品)。
|
||||
5. 记录当期触发次数,避免超过 `period_cap_times`。
|
||||
|
||||
### 5.2 参数含义
|
||||
- `target_prize_ids`:限定生效的奖品ID集合,空数组表示对所有奖品生效。
|
||||
- `chance_x1000`:千分制概率;1000=100%、200=20%。
|
||||
- `period_cap_times`:周期内最多触发次数(建议结合自然日或活动期定义周期)。
|
||||
|
||||
---
|
||||
|
||||
## 六、配置示例集
|
||||
|
||||
### 6.1 获取规则(抽奖获得)
|
||||
```json
|
||||
{
|
||||
"methods": ["lottery"],
|
||||
"conditions": {"lottery_type": "normal"}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 双倍效果(100%/限1次)
|
||||
```json
|
||||
{
|
||||
"target_prize_ids": [],
|
||||
"chance_x1000": 1000,
|
||||
"period_cap_times": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 用户分配有效期(按天)
|
||||
```json
|
||||
{
|
||||
"expire_type": "days",
|
||||
"days": 1,
|
||||
"remark": "抽奖获得双倍称号",
|
||||
"override_existing": true,
|
||||
"send_notification": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、测试与验收
|
||||
|
||||
### 7.1 管理端用例
|
||||
- 创建称号与效果 → 列表校验
|
||||
- 为测试用户分配(days=1)→ 显示生效与过期时间
|
||||
|
||||
### 7.2 抽奖用例
|
||||
- 用户持有称号时抽奖 → 奖励翻倍,次数受 `period_cap_times` 控制
|
||||
- 称号过期后抽奖 → 不再翻倍
|
||||
|
||||
### 7.3 数据核查
|
||||
- 检查 UserTitles:`active/expires_at/remark`
|
||||
- 检查效果:`SystemTitleEffects.params_json` 是否符合配置
|
||||
|
||||
### 7.4 验收标准
|
||||
- 正常流程:在有效期内至少一次成功翻倍
|
||||
- 边界:过期后不生效;超过 `period_cap_times` 不再触发
|
||||
- 失败路径:接口异常可回滚或重试,日志可追踪
|
||||
|
||||
---
|
||||
|
||||
## 八、进度记录
|
||||
- 2025-11-16:建立说明文档;完成创建流程、分配与时效、抽奖双倍应用、配置示例与验收标准。
|
||||
- 2025-11-17(计划):完成抽奖流程集成代码的验证与单元测试。
|
||||
|
||||
---
|
||||
|
||||
## 九、常见问题
|
||||
- Q:为什么双倍没有触发?
|
||||
- A:检查有效期是否过期;确认 `chance_x1000` 是否为期望值;校验 `period_cap_times` 是否已用尽;确认奖品是否在 `target_prize_ids` 范围内。
|
||||
- Q:如何限制只对特定奖品翻倍?
|
||||
- A:在效果参数中填入 `target_prize_ids` 指定的奖品ID数组。
|
||||
- Q:如何延长用户称号有效期?
|
||||
- A:重新分配该称号并设置新的有效期(覆盖现有)。
|
||||
|
||||
---
|
||||
|
||||
## 十、后续工作
|
||||
- 如需我在抽奖服务中添加自动分配与双倍应用的具体代码,请告知周期定义(按自然日/活动期)与触发记录存储位置,我将补充实现与测试用例并更新本说明文档进度。
|
||||
|
||||
---
|
||||
|
||||
## 十一、字段级详解与代码绑定点
|
||||
|
||||
### 11.1 获得规则(ObtainRulesJSON)
|
||||
- 字段结构:
|
||||
- `methods: string[]` 获得方式集合(如 `register/consume/invite/activity/manual/lottery`)
|
||||
- `conditions: object` 按不同方式的约束条件(示例键:`lottery_type`, `min_amount`, `time_range`, `participate_times` 等)
|
||||
- 用途说明:当前后端仅进行“存取”,尚未在服务层解析并自动授予头衔;需后续集成到具体事件(抽奖、消费、邀请等)的规则评估器。
|
||||
- 代码绑定:
|
||||
- 创建时写入默认空对象:`internal/api/admin/titles_admin.go:88`
|
||||
- 创建落库:`internal/api/admin/titles_admin.go:94-96`
|
||||
- 修改时更新:`internal/api/admin/titles_admin.go:125`
|
||||
- 预置种子:`internal/api/admin/titles_seed.go:54`
|
||||
- DAO 字段映射:`internal/repository/mysql/dao/system_titles.gen.go:34, 53, 77, 102`
|
||||
- 前端规则助手来源:
|
||||
- 载入与保存:`web/admin/src/views/operations/titles/components/RuleConfigDialog.vue:331-393, 395-450`
|
||||
- JSON 预览构造:`web/admin/src/views/operations/titles/components/RuleConfigDialog.vue:283-322`
|
||||
- 示例配置:
|
||||
```json
|
||||
{
|
||||
"methods": ["lottery"],
|
||||
"conditions": {"lottery_type": "normal", "participate_times": 1}
|
||||
}
|
||||
```
|
||||
(说明:目前该 JSON 仅随标题存储,不会自动触发授予;若需要自动授予,需新增服务层解析器与事件钩子。)
|
||||
|
||||
### 11.2 使用范围(ScopesJSON)
|
||||
- 标题层(SystemTitles.ScopesJSON):
|
||||
- 用途:目前仅存取,并未在运行时判定中直接使用。
|
||||
- 创建写入:`internal/api/admin/titles_admin.go:89, 95-96`
|
||||
- 修改更新:`internal/api/admin/titles_admin.go:126`
|
||||
- 效果层(SystemTitleEffects.ScopesJSON):
|
||||
- 用途:运行时用于过滤效果的生效范围(活动/期/分类等),排除优先,包含其次,未配置则视为全局。
|
||||
- 解析结构:`internal/service/title/effects_resolver.go:60-69`
|
||||
- `activity_ids`, `issue_ids`, `category_ids`
|
||||
- `exclude.activity_ids`, `exclude.issue_ids`, `exclude.category_ids`
|
||||
- 匹配逻辑:`internal/service/title/effects_resolver.go:133-155`
|
||||
- 抽奖期过滤(简化版,仅按 issue):`internal/service/activity/draw_with_effects.go:85-103`
|
||||
- 管理端写入:`internal/api/admin/titles_admin.go:196-206, 251`
|
||||
- 示例配置:
|
||||
```json
|
||||
{
|
||||
"activity_ids": [1001, 1002],
|
||||
"issue_ids": [2001],
|
||||
"category_ids": [],
|
||||
"exclude": { "issue_ids": [2003] }
|
||||
}
|
||||
```
|
||||
(说明:当事件上下文命中 `exclude` 列表则不生效;若配置了包含列表且不命中则不生效;都未配置视为全局。)
|
||||
|
||||
### 11.3 效果数量与叠加策略(StackingStrategy/CapValueX1000)
|
||||
- 多效果:一个称号可挂载多个效果,列表接口返回总数(`internal/api/admin/titles_admin.go:156-170`),`sort` 控制同内顺序,`status` 控制启用。
|
||||
- 叠加策略(`SystemTitleEffects.StackingStrategy`):`internal/repository/mysql/model/system_title_effects.gen.go:19`
|
||||
- `0` 最大值(max_only):取每个目标的最大增益
|
||||
- `1` 累加封顶(sum_with_cap):叠加并用 cap 限制
|
||||
- `2` 首个匹配(first_match):只在当前为 0 时赋值
|
||||
- 默认:累加并使用 `CapValueX1000` 统一封顶
|
||||
- 统一封顶(`SystemTitleEffects.CapValueX1000`):千分比封顶值,用于限制概率/倍数累积的上限。
|
||||
- 抽奖应用点:`internal/service/activity/draw_with_effects.go:104-149, 152-166, 197-233`
|
||||
- `effect_type=5` 概率加成:解析 `target_prize_ids/boost_x1000/cap_x1000` 后调整权重
|
||||
- `effect_type=6` 双倍奖励:合并 `target_prize_ids` 与 `chance_x1000` 后概率判定,命中则倍数 `x2`
|
||||
|
||||
### 11.4 规则助手(前端可视化配置)
|
||||
- 入口位置:`web/admin/src/views/operations/titles/components/RuleConfigDialog.vue`
|
||||
- 获得规则 UI → JSON 映射:`methods/consume/invite/activity`(`RuleConfigDialog.vue:225-242, 283-297`)
|
||||
- 使用范围 UI → JSON 映射:`user_level/category_ids/region/time`(`RuleConfigDialog.vue:245-257, 298-318`)
|
||||
- 加载现有规则:`RuleConfigDialog.vue:331-393`
|
||||
- 保存规则:`RuleConfigDialog.vue:395-450`
|
||||
- 注意事项:
|
||||
- 效果编辑表单与后端字段对齐(effect_type=5/6):
|
||||
- 5(概率加成)后端期望:`target_prize_ids, boost_x1000, cap_x1000`(`internal/service/activity/draw_with_effects.go:104-133`)
|
||||
- 6(双倍奖励)后端期望:`target_prize_ids, chance_x1000, period_cap_times`(`internal/service/activity/draw_with_effects.go:133-149`)
|
||||
- 当前前端默认参数返回与后端不一致:`web/admin/src/views/operations/titles/components/EffectEditDialog.vue:244-261`
|
||||
- 建议在运营使用时,按后端字段结构填写 `params_json`,或由后续改造对齐表单字段;本文档已标注对齐需求,待前端改造确认后更新。
|
||||
|
||||
---
|
||||
|
||||
## 十二、字段参考速查表
|
||||
|
||||
- ObtainRulesJSON
|
||||
- `methods`: string[](注册/消费/邀请/活动/抽奖/手动)
|
||||
- `conditions`: object(按场景扩展键,如 `lottery_type/min_amount/time_range/participate_times` 等)
|
||||
- 绑定点:创建/修改/预置/DAO 映射(见 11.1)
|
||||
|
||||
- ScopesJSON(效果层)
|
||||
- `activity_ids/issue_ids/category_ids`: number[]
|
||||
- `exclude.activity_ids/issue_ids/category_ids`: number[]
|
||||
- 绑定点:解析/匹配/抽奖过滤/写入(见 11.2)
|
||||
|
||||
- SystemTitleEffects(效果通用)
|
||||
- `effect_type`: 1~6(见模型注释)
|
||||
- `params_json`: 按效果类型字段定义
|
||||
- `stacking_strategy`: 0/1/2/默认
|
||||
- `cap_value_x1000`: 千分封顶
|
||||
- `sort/status`: 顺序与启用
|
||||
- 绑定点:创建/修改/抽奖应用(见 11.3)
|
||||
@ -10,6 +10,7 @@ import (
|
||||
productsvc "bindbox-game/internal/service/product"
|
||||
bannersvc "bindbox-game/internal/service/banner"
|
||||
usersvc "bindbox-game/internal/service/user"
|
||||
titlesvc "bindbox-game/internal/service/title"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
@ -22,6 +23,7 @@ type handler struct {
|
||||
product productsvc.Service
|
||||
user usersvc.Service
|
||||
banner bannersvc.Service
|
||||
title titlesvc.Service
|
||||
}
|
||||
|
||||
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
|
||||
@ -35,5 +37,6 @@ func New(logger logger.CustomLogger, db mysql.Repo) *handler {
|
||||
product: productsvc.New(logger, db),
|
||||
user: usersvc.New(logger, db),
|
||||
banner: bannersvc.New(logger, db),
|
||||
title: titlesvc.New(logger, db),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
adminsvc "bindbox-game/internal/service/admin"
|
||||
)
|
||||
|
||||
type createAdminRequest struct {
|
||||
UserName string `json:"username" binding:"required"` // 用户名
|
||||
NickName string `json:"nickname" binding:"required"` // 昵称
|
||||
Mobile string `json:"mobile"` // 手机号
|
||||
Password string `json:"password" binding:"required"` // 密码
|
||||
Avatar string `json:"avatar"` // 头像
|
||||
}
|
||||
|
||||
type createAdminResponse struct {
|
||||
Message string `json:"message"` // 提示信息
|
||||
}
|
||||
|
||||
// CreateAdmin 新增客服
|
||||
// @Summary 新增客服
|
||||
// @Description 新增客服
|
||||
// @Tags 管理端.客服管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param RequestBody body createAdminRequest true "请求参数"
|
||||
// @Success 200 {object} createAdminResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/create [post]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) CreateAdmin() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(createAdminRequest)
|
||||
res := new(createAdminResponse)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ParamBindError,
|
||||
validation.Error(err)),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.CreateAdminError,
|
||||
fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), "禁止操作")),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.Create(ctx.RequestContext(), adminsvc.CreateInput{
|
||||
Username: req.UserName,
|
||||
Nickname: req.NickName,
|
||||
Mobile: req.Mobile,
|
||||
Password: req.Password,
|
||||
Avatar: req.Avatar,
|
||||
CreatedBy: ctx.SessionUserInfo().UserName,
|
||||
}); err != nil {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.CreateAdminError,
|
||||
fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), err.Error())),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
res.Message = "操作成功"
|
||||
ctx.Payload(res)
|
||||
}
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
)
|
||||
|
||||
type deleteAdminRequest struct {
|
||||
Ids string `json:"ids" binding:"required"` // 编号(多个用,分割)
|
||||
}
|
||||
|
||||
type deleteAdminResponse struct {
|
||||
Message string `json:"message"` // 提示信息
|
||||
}
|
||||
|
||||
// DeleteAdmin 删除客服
|
||||
// @Summary 删除客服
|
||||
// @Description 删除客服
|
||||
// @Tags 管理端.客服管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param RequestBody body deleteAdminRequest true "请求参数"
|
||||
// @Success 200 {object} deleteAdminResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/delete [post]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) DeleteAdmin() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(deleteAdminRequest)
|
||||
res := new(deleteAdminResponse)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ParamBindError,
|
||||
validation.Error(err)),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.DeleteAdminError,
|
||||
fmt.Sprintf("%s: %s", code.Text(code.DeleteAdminError), "禁止操作")),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
idList := strings.Split(req.Ids, ",")
|
||||
if len(idList) == 0 || (len(idList) == 1 && idList[0] == "") {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ParamBindError,
|
||||
"编号不能为空"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
var ids []int32
|
||||
|
||||
for _, strID := range idList {
|
||||
if strID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(strID)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ParamBindError,
|
||||
fmt.Sprintf("无效的编号: %s", strID)),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
ids = append(ids, int32(id))
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ParamBindError,
|
||||
"编号不能为空"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.Delete(ctx.RequestContext(), ids); err != nil {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.DeleteAdminError,
|
||||
fmt.Sprintf("%s: %s", code.Text(code.DeleteAdminError), err.Error())),
|
||||
)
|
||||
return
|
||||
}
|
||||
res.Message = "操作成功"
|
||||
ctx.Payload(res)
|
||||
}
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/timeutil"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
adminsvc "bindbox-game/internal/service/admin"
|
||||
)
|
||||
|
||||
type listRequest struct {
|
||||
Username string `form:"username"` // 用户名
|
||||
Nickname string `form:"nickname"` // 昵称
|
||||
Page int `form:"page"` // 当前页码,默认为第一页
|
||||
PageSize int `form:"page_size"` // 每页返回的数据量
|
||||
}
|
||||
|
||||
type listData struct {
|
||||
ID int32 `json:"id"` // 编号
|
||||
UserName string `json:"username"` // 用户名
|
||||
NickName string `json:"nickname"` // 昵称
|
||||
Mobile string `json:"mobile"` // 手机号
|
||||
Avatar string `json:"avatar"` // 头像
|
||||
CreatedAt string `json:"created_at"` // 创建时间
|
||||
UpdatedAt string `json:"updated_at"` // 更新时间
|
||||
}
|
||||
|
||||
type listResponse struct {
|
||||
Page int `json:"page"` // 当前页码
|
||||
PageSize int `json:"page_size"` // 每页返回的数据量
|
||||
Total int64 `json:"total"` // 符合查询条件的总记录数
|
||||
List []listData `json:"list"`
|
||||
}
|
||||
|
||||
// PageList 客服列表
|
||||
// @Summary 客服列表
|
||||
// @Description 客服列表
|
||||
// @Tags 管理端.客服管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param username query string false "用户名"
|
||||
// @Param nickname query string false "昵称"
|
||||
// @Param page query int true "当前页码" default(1)
|
||||
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
|
||||
// @Success 200 {object} listResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/list [get]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) PageList() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(listRequest)
|
||||
res := new(listResponse)
|
||||
|
||||
if err := ctx.ShouldBindForm(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ParamBindError,
|
||||
validation.Error(err)),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Page == 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
|
||||
if req.PageSize == 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
if req.PageSize > 100 {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ListAdminError,
|
||||
fmt.Sprintf("%s: 一次最多只能查询 100 条", code.Text(code.ListAdminError)),
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ListAdminError,
|
||||
fmt.Sprintf("%s: %s", code.Text(code.ListAdminError), "禁止操作")),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
items, total, err := h.svc.List(ctx.RequestContext(), adminsvc.ListInput{
|
||||
Username: req.Username,
|
||||
Nickname: req.Nickname,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ListAdminError,
|
||||
fmt.Sprintf("%s:%s", code.Text(code.ListAdminError), err.Error())),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
res.Page = req.Page
|
||||
res.PageSize = req.PageSize
|
||||
res.Total = total
|
||||
res.List = make([]listData, len(items))
|
||||
|
||||
for k, v := range items {
|
||||
res.List[k] = listData{
|
||||
ID: v.ID,
|
||||
UserName: v.Username,
|
||||
NickName: v.Nickname,
|
||||
Mobile: v.Mobile,
|
||||
Avatar: v.Avatar,
|
||||
CreatedAt: timeutil.FriendlyTime(v.CreatedAt),
|
||||
UpdatedAt: timeutil.FriendlyTime(v.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Payload(res)
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
adminsvc "bindbox-game/internal/service/admin"
|
||||
)
|
||||
|
||||
type modifyAdminRequest struct {
|
||||
UserName string `json:"username" binding:"required"` // 用户名
|
||||
NickName string `json:"nickname" binding:"required"` // 昵称
|
||||
Mobile string `json:"mobile"` // 手机号
|
||||
Password string `json:"password"` // 密码
|
||||
Avatar string `json:"avatar"` // 头像
|
||||
}
|
||||
|
||||
type modifyAdminResponse struct {
|
||||
Message string `json:"message"` // 提示信息
|
||||
}
|
||||
|
||||
// ModifyAdmin 编辑客服
|
||||
// @Summary 编辑客服
|
||||
// @Description 编辑客服
|
||||
// @Tags 管理端.客服管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "编号ID"
|
||||
// @Param RequestBody body modifyAdminRequest true "请求参数"
|
||||
// @Success 200 {object} modifyAdminResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/{id} [put]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) ModifyAdmin() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(modifyAdminRequest)
|
||||
res := new(modifyAdminResponse)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ParamBindError,
|
||||
validation.Error(err)),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ModifyAdminError,
|
||||
fmt.Sprintf("%s: %s", code.Text(code.ModifyAdminError), "禁止操作")),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if req.UserName == "" && req.NickName == "" {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ParamBindError,
|
||||
"用户名、昵称为必填"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(ctx.Param("id"))
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ParamBindError,
|
||||
"未传递编号ID"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.Modify(ctx.RequestContext(), id, adminsvc.ModifyInput{
|
||||
Username: req.UserName,
|
||||
Nickname: req.NickName,
|
||||
Mobile: req.Mobile,
|
||||
Password: req.Password,
|
||||
Avatar: req.Avatar,
|
||||
UpdatedBy: ctx.SessionUserInfo().UserName,
|
||||
}); err != nil {
|
||||
ctx.AbortWithError(core.Error(
|
||||
http.StatusBadRequest,
|
||||
code.ModifyAdminError,
|
||||
fmt.Sprintf("%s: %s", code.Text(code.ModifyAdminError), err.Error())),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
res.Message = "操作成功"
|
||||
ctx.Payload(res)
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,11 @@ type drawResultItem struct {
|
||||
|
||||
func (h *handler) BatchDrawForUsers() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
activityIDStr := ctx.Param("activity_id")
|
||||
if activityIDStr == "" {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
|
||||
return
|
||||
}
|
||||
issueIDStr := ctx.Param("issue_id")
|
||||
if issueIDStr == "" {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID"))
|
||||
@ -41,6 +46,7 @@ func (h *handler) BatchDrawForUsers() core.HandlerFunc {
|
||||
return
|
||||
}
|
||||
issueID, _ := strconv.ParseInt(issueIDStr, 10, 64)
|
||||
_, _ = strconv.ParseInt(activityIDStr, 10, 64)
|
||||
var req batchDrawRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
@ -71,8 +77,8 @@ func (h *handler) BatchDrawForUsers() core.HandlerFunc {
|
||||
|
||||
draws := make([]drawResultItem, 0, len(req.UserIDs))
|
||||
for _, uid := range req.UserIDs {
|
||||
// 执行抽奖
|
||||
rec, err := h.activity.ExecuteDraw(ctx.RequestContext(), issueID)
|
||||
// 执行抽奖(应用头衔效果)
|
||||
rec, err := h.activity.ExecuteDrawWithEffects(ctx.RequestContext(), issueID, uid)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ExecuteDrawError, err.Error()))
|
||||
|
||||
386
internal/api/admin/titles_admin.go
Normal file
386
internal/api/admin/titles_admin.go
Normal file
@ -0,0 +1,386 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
type listSystemTitlesRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
Name string `form:"name"`
|
||||
Status *int32 `form:"status"`
|
||||
}
|
||||
type listSystemTitlesResponse struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
List []*model.SystemTitles `json:"list"`
|
||||
}
|
||||
|
||||
// ListSystemTitles 系统称号列表
|
||||
func (h *handler) ListSystemTitles() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(listSystemTitlesRequest)
|
||||
rsp := new(listSystemTitlesResponse)
|
||||
if err := ctx.ShouldBindForm(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
if req.Page <= 0 { req.Page = 1 }
|
||||
if req.PageSize <= 0 { req.PageSize = 20 }
|
||||
if req.PageSize > 100 { req.PageSize = 100 }
|
||||
q := h.readDB.SystemTitles.WithContext(ctx.RequestContext()).ReadDB()
|
||||
if req.Name != "" { q = q.Where(h.readDB.SystemTitles.Name.Like("%" + req.Name + "%")) }
|
||||
if req.Status != nil { q = q.Where(h.readDB.SystemTitles.Status.Eq(*req.Status)) }
|
||||
total, err := q.Count()
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 30101, err.Error()))
|
||||
return
|
||||
}
|
||||
rows, err := q.Order(h.readDB.SystemTitles.ID.Desc()).
|
||||
Offset((req.Page-1)*req.PageSize).Limit(req.PageSize).Find()
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 30102, err.Error()))
|
||||
return
|
||||
}
|
||||
rsp.Page = req.Page
|
||||
rsp.PageSize = req.PageSize
|
||||
rsp.Total = total
|
||||
rsp.List = rows
|
||||
ctx.Payload(rsp)
|
||||
}
|
||||
}
|
||||
|
||||
type createSystemTitleRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1"`
|
||||
Description string `json:"description"`
|
||||
Status int32 `json:"status"`
|
||||
ObtainRulesJSON string `json:"obtain_rules_json"`
|
||||
ScopesJSON string `json:"scopes_json"`
|
||||
}
|
||||
|
||||
type modifySystemTitleRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status *int32 `json:"status"`
|
||||
ObtainRulesJSON string `json:"obtain_rules_json"`
|
||||
ScopesJSON string `json:"scopes_json"`
|
||||
}
|
||||
|
||||
type simpleMessageResponseTitle struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (h *handler) CreateSystemTitle() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
var req createSystemTitleRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
if req.ObtainRulesJSON == "" { req.ObtainRulesJSON = "{}" }
|
||||
if req.ScopesJSON == "" { req.ScopesJSON = "{}" }
|
||||
it := &model.SystemTitles{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Status: req.Status,
|
||||
ObtainRulesJSON: req.ObtainRulesJSON,
|
||||
ScopesJSON: req.ScopesJSON,
|
||||
}
|
||||
if err := h.writeDB.SystemTitles.WithContext(ctx.RequestContext()).Create(it); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30106, "创建称号失败"))
|
||||
return
|
||||
}
|
||||
ctx.Payload(&simpleMessageResponseTitle{Message: "创建成功"})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) ModifySystemTitle() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64)
|
||||
if err != nil || titleID <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID"))
|
||||
return
|
||||
}
|
||||
var req modifySystemTitleRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
row, err := h.readDB.SystemTitles.WithContext(ctx.RequestContext()).Where(h.readDB.SystemTitles.ID.Eq(titleID)).First()
|
||||
if err != nil || row == nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 30107, "称号不存在"))
|
||||
return
|
||||
}
|
||||
if req.Name != "" { row.Name = req.Name }
|
||||
row.Description = req.Description
|
||||
if req.Status != nil { row.Status = *req.Status }
|
||||
if req.ObtainRulesJSON != "" { row.ObtainRulesJSON = req.ObtainRulesJSON }
|
||||
if req.ScopesJSON != "" { row.ScopesJSON = req.ScopesJSON }
|
||||
if err := h.writeDB.SystemTitles.Save(row); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30108, "修改称号失败"))
|
||||
return
|
||||
}
|
||||
ctx.Payload(&simpleMessageResponseTitle{Message: "修改成功"})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) DeleteSystemTitle() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64)
|
||||
if err != nil || titleID <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID"))
|
||||
return
|
||||
}
|
||||
del := &model.SystemTitles{ID: titleID}
|
||||
if _, err := h.writeDB.SystemTitles.Delete(del); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30109, "删除称号失败"))
|
||||
return
|
||||
}
|
||||
ctx.Payload(&simpleMessageResponseTitle{Message: "删除成功"})
|
||||
}
|
||||
}
|
||||
|
||||
type listEffectsResponse struct {
|
||||
List []*model.SystemTitleEffects `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
func (h *handler) ListSystemTitleEffects() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64)
|
||||
if err != nil || titleID <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID"))
|
||||
return
|
||||
}
|
||||
rows, err := h.readDB.SystemTitleEffects.WithContext(ctx.RequestContext()).Where(h.readDB.SystemTitleEffects.TitleID.Eq(titleID)).Order(h.readDB.SystemTitleEffects.Sort).Find()
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 30110, err.Error()))
|
||||
return
|
||||
}
|
||||
total := int64(len(rows))
|
||||
ctx.Payload(&listEffectsResponse{List: rows, Total: total})
|
||||
}
|
||||
}
|
||||
|
||||
type createEffectRequest struct {
|
||||
EffectType int32 `json:"effect_type" binding:"required"`
|
||||
ParamsJSON string `json:"params_json" binding:"required"`
|
||||
StackingStrategy int32 `json:"stacking_strategy"`
|
||||
CapValueX1000 int32 `json:"cap_value_x1000"`
|
||||
ScopesJSON string `json:"scopes_json"`
|
||||
Sort int32 `json:"sort"`
|
||||
Status int32 `json:"status"`
|
||||
}
|
||||
|
||||
func (h *handler) CreateSystemTitleEffect() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64)
|
||||
if err != nil || titleID <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID"))
|
||||
return
|
||||
}
|
||||
var req createEffectRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
if req.ParamsJSON == "" { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "params_json不能为空")); return }
|
||||
if req.ScopesJSON == "" { req.ScopesJSON = "{}" }
|
||||
existed, _ := h.readDB.SystemTitleEffects.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.SystemTitleEffects.TitleID.Eq(titleID)).
|
||||
Where(h.readDB.SystemTitleEffects.EffectType.Eq(req.EffectType)).First()
|
||||
if existed != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 30115, "同类型效果已存在"))
|
||||
return
|
||||
}
|
||||
ef := &model.SystemTitleEffects{
|
||||
TitleID: titleID,
|
||||
EffectType: req.EffectType,
|
||||
ParamsJSON: req.ParamsJSON,
|
||||
StackingStrategy: req.StackingStrategy,
|
||||
CapValueX1000: req.CapValueX1000,
|
||||
ScopesJSON: req.ScopesJSON,
|
||||
Sort: req.Sort,
|
||||
Status: req.Status,
|
||||
}
|
||||
if err := h.writeDB.SystemTitleEffects.WithContext(ctx.RequestContext()).Create(ef); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30111, "创建效果失败"))
|
||||
return
|
||||
}
|
||||
ctx.Payload(&simpleMessageResponseTitle{Message: "创建成功"})
|
||||
}
|
||||
}
|
||||
|
||||
type modifyEffectRequest struct {
|
||||
EffectType *int32 `json:"effect_type"`
|
||||
ParamsJSON string `json:"params_json"`
|
||||
StackingStrategy *int32 `json:"stacking_strategy"`
|
||||
CapValueX1000 *int32 `json:"cap_value_x1000"`
|
||||
ScopesJSON string `json:"scopes_json"`
|
||||
Sort *int32 `json:"sort"`
|
||||
Status *int32 `json:"status"`
|
||||
}
|
||||
|
||||
func (h *handler) ModifySystemTitleEffect() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64)
|
||||
if err != nil || titleID <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID"))
|
||||
return
|
||||
}
|
||||
effectID, err := strconv.ParseInt(ctx.Param("effect_id"), 10, 64)
|
||||
if err != nil || effectID <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递效果ID"))
|
||||
return
|
||||
}
|
||||
var req modifyEffectRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
row, err := h.readDB.SystemTitleEffects.WithContext(ctx.RequestContext()).Where(h.readDB.SystemTitleEffects.ID.Eq(effectID)).Where(h.readDB.SystemTitleEffects.TitleID.Eq(titleID)).First()
|
||||
if err != nil || row == nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 30112, "效果不存在"))
|
||||
return
|
||||
}
|
||||
if req.EffectType != nil {
|
||||
existed, _ := h.readDB.SystemTitleEffects.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.SystemTitleEffects.TitleID.Eq(titleID)).
|
||||
Where(h.readDB.SystemTitleEffects.EffectType.Eq(*req.EffectType)).
|
||||
Where(h.readDB.SystemTitleEffects.ID.Neq(effectID)).First()
|
||||
if existed != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 30116, "同类型效果已存在"))
|
||||
return
|
||||
}
|
||||
row.EffectType = *req.EffectType
|
||||
}
|
||||
if req.ParamsJSON != "" { row.ParamsJSON = req.ParamsJSON }
|
||||
if req.StackingStrategy != nil { row.StackingStrategy = *req.StackingStrategy }
|
||||
if req.CapValueX1000 != nil { row.CapValueX1000 = *req.CapValueX1000 }
|
||||
if req.ScopesJSON != "" { row.ScopesJSON = req.ScopesJSON }
|
||||
if req.Sort != nil { row.Sort = *req.Sort }
|
||||
if req.Status != nil { row.Status = *req.Status }
|
||||
if err := h.writeDB.SystemTitleEffects.Save(row); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30113, "修改效果失败"))
|
||||
return
|
||||
}
|
||||
ctx.Payload(&simpleMessageResponseTitle{Message: "修改成功"})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) DeleteSystemTitleEffect() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64)
|
||||
if err != nil || titleID <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递称号ID"))
|
||||
return
|
||||
}
|
||||
effectID, err := strconv.ParseInt(ctx.Param("effect_id"), 10, 64)
|
||||
if err != nil || effectID <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递效果ID"))
|
||||
return
|
||||
}
|
||||
del := &model.SystemTitleEffects{ID: effectID, TitleID: titleID}
|
||||
if _, err := h.writeDB.SystemTitleEffects.Delete(del); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30114, "删除效果失败"))
|
||||
return
|
||||
}
|
||||
ctx.Payload(&simpleMessageResponseTitle{Message: "删除成功"})
|
||||
}
|
||||
}
|
||||
type assignUserTitleRequest struct {
|
||||
TitleID int64 `json:"title_id" binding:"required,min=1"`
|
||||
ExpiresAt *string `json:"expires_at"` // RFC3339 字符串,可空
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
type assignUserTitleResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// AssignUserTitle 给用户分配称号(存在则更新有效期与备注,并激活)
|
||||
func (h *handler) AssignUserTitle() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
|
||||
if err != nil || userID <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
|
||||
return
|
||||
}
|
||||
var req assignUserTitleRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
// 校验称号存在且启用
|
||||
title, err := h.readDB.SystemTitles.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.SystemTitles.ID.Eq(req.TitleID)).First()
|
||||
if err != nil || title == nil || title.Status != 1 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 30103, "称号不存在或未启用"))
|
||||
return
|
||||
}
|
||||
// 解析过期时间
|
||||
var exp time.Time
|
||||
if req.ExpiresAt != nil && *req.ExpiresAt != "" {
|
||||
t, perr := time.Parse(time.RFC3339, *req.ExpiresAt)
|
||||
if perr != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "expires_at格式错误"))
|
||||
return
|
||||
}
|
||||
exp = t
|
||||
}
|
||||
// upsert 用户称号
|
||||
// 先查是否存在
|
||||
ut, err := h.readDB.UserTitles.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.UserTitles.UserID.Eq(userID)).
|
||||
Where(h.readDB.UserTitles.TitleID.Eq(req.TitleID)).First()
|
||||
if err == nil && ut != nil {
|
||||
// 更新
|
||||
ut.Active = 1
|
||||
ut.Remark = req.Remark
|
||||
if !exp.IsZero() { ut.ExpiresAt = exp }
|
||||
if err := h.writeDB.UserTitles.Save(ut); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30104, "更新称号失败"))
|
||||
return
|
||||
}
|
||||
others, _ := h.readDB.UserTitles.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.UserTitles.UserID.Eq(userID)).
|
||||
Where(h.readDB.UserTitles.TitleID.Neq(req.TitleID)).
|
||||
Where(h.readDB.UserTitles.Active.Eq(1)).Find()
|
||||
for _, o := range others {
|
||||
o.Active = 0
|
||||
_ = h.writeDB.UserTitles.Save(o)
|
||||
}
|
||||
} else {
|
||||
now := time.Now()
|
||||
newUT := &model.UserTitles{
|
||||
UserID: userID,
|
||||
TitleID: req.TitleID,
|
||||
Active: 1,
|
||||
ObtainedAt: now,
|
||||
Source: "admin_assign",
|
||||
Remark: req.Remark,
|
||||
}
|
||||
if !exp.IsZero() { newUT.ExpiresAt = exp }
|
||||
if err := h.writeDB.UserTitles.Create(newUT); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 30105, "分配称号失败"))
|
||||
return
|
||||
}
|
||||
others, _ := h.readDB.UserTitles.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.UserTitles.UserID.Eq(userID)).
|
||||
Where(h.readDB.UserTitles.TitleID.Neq(req.TitleID)).
|
||||
Where(h.readDB.UserTitles.Active.Eq(1)).Find()
|
||||
for _, o := range others {
|
||||
o.Active = 0
|
||||
_ = h.writeDB.UserTitles.Save(o)
|
||||
}
|
||||
}
|
||||
ctx.Payload(&assignUserTitleResponse{Message: "分配称号成功"})
|
||||
}
|
||||
}
|
||||
178
internal/api/admin/titles_seed.go
Normal file
178
internal/api/admin/titles_seed.go
Normal file
@ -0,0 +1,178 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
type seedDefaultTitlesResponse struct {
|
||||
Created int `json:"created"`
|
||||
Exists int `json:"exists"`
|
||||
IDs []int64 `json:"ids"`
|
||||
}
|
||||
|
||||
// SeedDefaultTitles 内置6个称号与基础效果
|
||||
func (h *handler) SeedDefaultTitles() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
// 定义默认称号与效果
|
||||
type def struct {
|
||||
Name string
|
||||
Description string
|
||||
EffectType int32
|
||||
Params map[string]interface{}
|
||||
Stack int32
|
||||
CapX1000 int32
|
||||
}
|
||||
defs := []def{
|
||||
{Name: "优惠券使者", Description: "可领取优惠券", EffectType: 1, Params: map[string]interface{}{ "template_id": 0, "frequency": map[string]interface{}{"period": "day", "times": 1}}, Stack: 1, CapX1000: 0},
|
||||
{Name: "折扣官", Description: "抽奖购票折扣", EffectType: 2, Params: map[string]interface{}{ "discount_type": "percentage", "value_x1000": 100, "max_discount_x1000": 300}, Stack: 0, CapX1000: 300},
|
||||
{Name: "签到达人", Description: "签到双倍积分", EffectType: 3, Params: map[string]interface{}{ "multiplier_x1000": 2000, "daily_cap_points": 0}, Stack: 1, CapX1000: 3000},
|
||||
{Name: "卡牌使者", Description: "可领取道具卡", EffectType: 4, Params: map[string]interface{}{ "template_id": 0, "frequency": map[string]interface{}{"period": "week", "times": 2}}, Stack: 1, CapX1000: 0},
|
||||
{Name: "幸运加成者", Description: "抽奖概率加成", EffectType: 5, Params: map[string]interface{}{ "target_prize_ids": []int64{}, "boost_x1000": 100, "cap_x1000": 300}, Stack: 1, CapX1000: 300},
|
||||
{Name: "双倍之王", Description: "奖品双倍概率", EffectType: 6, Params: map[string]interface{}{ "target_prize_ids": []int64{}, "chance_x1000": 200, "period_cap_times": 1}, Stack: 1, CapX1000: 500},
|
||||
}
|
||||
created := 0
|
||||
exists := 0
|
||||
var ids []int64
|
||||
for _, d := range defs {
|
||||
// 是否存在同名称号
|
||||
row, _ := h.readDB.SystemTitles.WithContext(ctx.RequestContext()).Where(h.readDB.SystemTitles.Name.Eq(d.Name)).First()
|
||||
if row != nil {
|
||||
exists++
|
||||
ids = append(ids, row.ID)
|
||||
continue
|
||||
}
|
||||
// 创建称号(使用模型类型)
|
||||
newRow := &model.SystemTitles{
|
||||
Name: d.Name,
|
||||
Description: d.Description,
|
||||
Status: 1,
|
||||
ObtainRulesJSON: "{}",
|
||||
ScopesJSON: "{}",
|
||||
}
|
||||
if err := h.writeDB.SystemTitles.WithContext(ctx.RequestContext()).Create(newRow); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "创建称号失败"))
|
||||
return
|
||||
}
|
||||
// 重新读取ID
|
||||
trow, _ := h.readDB.SystemTitles.WithContext(ctx.RequestContext()).Where(h.readDB.SystemTitles.Name.Eq(d.Name)).First()
|
||||
if trow == nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "创建称号失败(读取)"))
|
||||
return
|
||||
}
|
||||
ids = append(ids, trow.ID)
|
||||
created++
|
||||
// 创建效果
|
||||
paramsBytes, _ := json.Marshal(d.Params)
|
||||
eff := &model.SystemTitleEffects{
|
||||
TitleID: trow.ID,
|
||||
EffectType: d.EffectType,
|
||||
ParamsJSON: string(paramsBytes),
|
||||
StackingStrategy: d.Stack,
|
||||
CapValueX1000: d.CapX1000,
|
||||
ScopesJSON: "{}",
|
||||
Sort: 1,
|
||||
Status: 1,
|
||||
}
|
||||
if err := h.writeDB.SystemTitleEffects.WithContext(ctx.RequestContext()).Create(eff); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "创建称号效果失败"))
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Payload(&seedDefaultTitlesResponse{Created: created, Exists: exists, IDs: ids})
|
||||
}
|
||||
}
|
||||
|
||||
type ensureTitlesMenuResponse struct {
|
||||
Ensured bool `json:"ensured"`
|
||||
Parent int64 `json:"parent_id"`
|
||||
MenuID int64 `json:"menu_id"`
|
||||
}
|
||||
|
||||
// EnsureTitlesMenu 确保运营菜单下存在“称号管理”子菜单
|
||||
func (h *handler) EnsureTitlesMenu() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
// 查找运营菜单父节点
|
||||
parent, _ := h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Name.Eq("Operations")).First()
|
||||
var parentID int64
|
||||
if parent == nil {
|
||||
// 创建运营父菜单
|
||||
pm := &model.Menus{
|
||||
ParentID: 0,
|
||||
Path: "/operations",
|
||||
Name: "Operations",
|
||||
Component: "/index/index",
|
||||
Icon: "ri:tools-line",
|
||||
Sort: 10,
|
||||
Status: true,
|
||||
KeepAlive: true,
|
||||
IsHide: false,
|
||||
IsHideTab: false,
|
||||
CreatedUser: "system",
|
||||
UpdatedUser: "system",
|
||||
}
|
||||
if err := h.writeDB.Menus.WithContext(ctx.RequestContext()).Create(pm); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "创建运营菜单失败"))
|
||||
return
|
||||
}
|
||||
// 读取
|
||||
parent, _ = h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Name.Eq("Operations")).First()
|
||||
if parent == nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "创建运营菜单失败(读取)"))
|
||||
return
|
||||
}
|
||||
}
|
||||
parentID = parent.ID
|
||||
// 查找称号菜单
|
||||
titlesMenu, _ := h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Path.Eq("titles")).Where(h.readDB.Menus.ParentID.Eq(parentID)).First()
|
||||
if titlesMenu == nil {
|
||||
tm := &struct {
|
||||
ParentID int64
|
||||
Path string
|
||||
Name string
|
||||
Component string
|
||||
Icon string
|
||||
Sort int32
|
||||
Status bool
|
||||
KeepAlive bool
|
||||
IsHide bool
|
||||
IsHideTab bool
|
||||
}{
|
||||
ParentID: parentID,
|
||||
Path: "titles",
|
||||
Name: "Titles",
|
||||
Component: "/operations/titles",
|
||||
Icon: "ri:medal-line",
|
||||
Sort: 50,
|
||||
Status: true,
|
||||
KeepAlive: true,
|
||||
IsHide: false,
|
||||
IsHideTab: false,
|
||||
}
|
||||
mm := &model.Menus{
|
||||
ParentID: tm.ParentID,
|
||||
Path: tm.Path,
|
||||
Name: tm.Name,
|
||||
Component: tm.Component,
|
||||
Icon: tm.Icon,
|
||||
Sort: tm.Sort,
|
||||
Status: tm.Status,
|
||||
KeepAlive: tm.KeepAlive,
|
||||
IsHide: tm.IsHide,
|
||||
IsHideTab: tm.IsHideTab,
|
||||
CreatedUser: "system",
|
||||
UpdatedUser: "system",
|
||||
}
|
||||
if err := h.writeDB.Menus.WithContext(ctx.RequestContext()).Create(mm); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "创建称号菜单失败"))
|
||||
return
|
||||
}
|
||||
titlesMenu, _ = h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Path.Eq("titles")).Where(h.readDB.Menus.ParentID.Eq(parentID)).First()
|
||||
}
|
||||
ctx.Payload(&ensureTitlesMenuResponse{Ensured: true, Parent: parentID, MenuID: titlesMenu.ID})
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,8 @@ var (
|
||||
ShippingRecords *shippingRecords
|
||||
SystemCoupons *systemCoupons
|
||||
SystemItemCards *systemItemCards
|
||||
SystemTitleEffects *systemTitleEffects
|
||||
SystemTitles *systemTitles
|
||||
UserAddresses *userAddresses
|
||||
UserCoupons *userCoupons
|
||||
UserInventory *userInventory
|
||||
@ -54,6 +56,8 @@ var (
|
||||
UserItemCards *userItemCards
|
||||
UserPoints *userPoints
|
||||
UserPointsLedger *userPointsLedger
|
||||
UserTitleEffectClaims *userTitleEffectClaims
|
||||
UserTitles *userTitles
|
||||
Users *users
|
||||
)
|
||||
|
||||
@ -88,6 +92,8 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
||||
ShippingRecords = &Q.ShippingRecords
|
||||
SystemCoupons = &Q.SystemCoupons
|
||||
SystemItemCards = &Q.SystemItemCards
|
||||
SystemTitleEffects = &Q.SystemTitleEffects
|
||||
SystemTitles = &Q.SystemTitles
|
||||
UserAddresses = &Q.UserAddresses
|
||||
UserCoupons = &Q.UserCoupons
|
||||
UserInventory = &Q.UserInventory
|
||||
@ -96,6 +102,8 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
||||
UserItemCards = &Q.UserItemCards
|
||||
UserPoints = &Q.UserPoints
|
||||
UserPointsLedger = &Q.UserPointsLedger
|
||||
UserTitleEffectClaims = &Q.UserTitleEffectClaims
|
||||
UserTitles = &Q.UserTitles
|
||||
Users = &Q.Users
|
||||
}
|
||||
|
||||
@ -131,6 +139,8 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
||||
ShippingRecords: newShippingRecords(db, opts...),
|
||||
SystemCoupons: newSystemCoupons(db, opts...),
|
||||
SystemItemCards: newSystemItemCards(db, opts...),
|
||||
SystemTitleEffects: newSystemTitleEffects(db, opts...),
|
||||
SystemTitles: newSystemTitles(db, opts...),
|
||||
UserAddresses: newUserAddresses(db, opts...),
|
||||
UserCoupons: newUserCoupons(db, opts...),
|
||||
UserInventory: newUserInventory(db, opts...),
|
||||
@ -139,6 +149,8 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
||||
UserItemCards: newUserItemCards(db, opts...),
|
||||
UserPoints: newUserPoints(db, opts...),
|
||||
UserPointsLedger: newUserPointsLedger(db, opts...),
|
||||
UserTitleEffectClaims: newUserTitleEffectClaims(db, opts...),
|
||||
UserTitles: newUserTitles(db, opts...),
|
||||
Users: newUsers(db, opts...),
|
||||
}
|
||||
}
|
||||
@ -175,6 +187,8 @@ type Query struct {
|
||||
ShippingRecords shippingRecords
|
||||
SystemCoupons systemCoupons
|
||||
SystemItemCards systemItemCards
|
||||
SystemTitleEffects systemTitleEffects
|
||||
SystemTitles systemTitles
|
||||
UserAddresses userAddresses
|
||||
UserCoupons userCoupons
|
||||
UserInventory userInventory
|
||||
@ -183,6 +197,8 @@ type Query struct {
|
||||
UserItemCards userItemCards
|
||||
UserPoints userPoints
|
||||
UserPointsLedger userPointsLedger
|
||||
UserTitleEffectClaims userTitleEffectClaims
|
||||
UserTitles userTitles
|
||||
Users users
|
||||
}
|
||||
|
||||
@ -220,6 +236,8 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
||||
ShippingRecords: q.ShippingRecords.clone(db),
|
||||
SystemCoupons: q.SystemCoupons.clone(db),
|
||||
SystemItemCards: q.SystemItemCards.clone(db),
|
||||
SystemTitleEffects: q.SystemTitleEffects.clone(db),
|
||||
SystemTitles: q.SystemTitles.clone(db),
|
||||
UserAddresses: q.UserAddresses.clone(db),
|
||||
UserCoupons: q.UserCoupons.clone(db),
|
||||
UserInventory: q.UserInventory.clone(db),
|
||||
@ -228,6 +246,8 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
||||
UserItemCards: q.UserItemCards.clone(db),
|
||||
UserPoints: q.UserPoints.clone(db),
|
||||
UserPointsLedger: q.UserPointsLedger.clone(db),
|
||||
UserTitleEffectClaims: q.UserTitleEffectClaims.clone(db),
|
||||
UserTitles: q.UserTitles.clone(db),
|
||||
Users: q.Users.clone(db),
|
||||
}
|
||||
}
|
||||
@ -272,6 +292,8 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
||||
ShippingRecords: q.ShippingRecords.replaceDB(db),
|
||||
SystemCoupons: q.SystemCoupons.replaceDB(db),
|
||||
SystemItemCards: q.SystemItemCards.replaceDB(db),
|
||||
SystemTitleEffects: q.SystemTitleEffects.replaceDB(db),
|
||||
SystemTitles: q.SystemTitles.replaceDB(db),
|
||||
UserAddresses: q.UserAddresses.replaceDB(db),
|
||||
UserCoupons: q.UserCoupons.replaceDB(db),
|
||||
UserInventory: q.UserInventory.replaceDB(db),
|
||||
@ -280,6 +302,8 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
||||
UserItemCards: q.UserItemCards.replaceDB(db),
|
||||
UserPoints: q.UserPoints.replaceDB(db),
|
||||
UserPointsLedger: q.UserPointsLedger.replaceDB(db),
|
||||
UserTitleEffectClaims: q.UserTitleEffectClaims.replaceDB(db),
|
||||
UserTitles: q.UserTitles.replaceDB(db),
|
||||
Users: q.Users.replaceDB(db),
|
||||
}
|
||||
}
|
||||
@ -314,6 +338,8 @@ type queryCtx struct {
|
||||
ShippingRecords *shippingRecordsDo
|
||||
SystemCoupons *systemCouponsDo
|
||||
SystemItemCards *systemItemCardsDo
|
||||
SystemTitleEffects *systemTitleEffectsDo
|
||||
SystemTitles *systemTitlesDo
|
||||
UserAddresses *userAddressesDo
|
||||
UserCoupons *userCouponsDo
|
||||
UserInventory *userInventoryDo
|
||||
@ -322,6 +348,8 @@ type queryCtx struct {
|
||||
UserItemCards *userItemCardsDo
|
||||
UserPoints *userPointsDo
|
||||
UserPointsLedger *userPointsLedgerDo
|
||||
UserTitleEffectClaims *userTitleEffectClaimsDo
|
||||
UserTitles *userTitlesDo
|
||||
Users *usersDo
|
||||
}
|
||||
|
||||
@ -356,6 +384,8 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
||||
ShippingRecords: q.ShippingRecords.WithContext(ctx),
|
||||
SystemCoupons: q.SystemCoupons.WithContext(ctx),
|
||||
SystemItemCards: q.SystemItemCards.WithContext(ctx),
|
||||
SystemTitleEffects: q.SystemTitleEffects.WithContext(ctx),
|
||||
SystemTitles: q.SystemTitles.WithContext(ctx),
|
||||
UserAddresses: q.UserAddresses.WithContext(ctx),
|
||||
UserCoupons: q.UserCoupons.WithContext(ctx),
|
||||
UserInventory: q.UserInventory.WithContext(ctx),
|
||||
@ -364,6 +394,8 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
||||
UserItemCards: q.UserItemCards.WithContext(ctx),
|
||||
UserPoints: q.UserPoints.WithContext(ctx),
|
||||
UserPointsLedger: q.UserPointsLedger.WithContext(ctx),
|
||||
UserTitleEffectClaims: q.UserTitleEffectClaims.WithContext(ctx),
|
||||
UserTitles: q.UserTitles.WithContext(ctx),
|
||||
Users: q.Users.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
356
internal/repository/mysql/dao/system_title_effects.gen.go
Normal file
356
internal/repository/mysql/dao/system_title_effects.gen.go
Normal file
@ -0,0 +1,356 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
|
||||
"gorm.io/plugin/dbresolver"
|
||||
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
func newSystemTitleEffects(db *gorm.DB, opts ...gen.DOOption) systemTitleEffects {
|
||||
_systemTitleEffects := systemTitleEffects{}
|
||||
|
||||
_systemTitleEffects.systemTitleEffectsDo.UseDB(db, opts...)
|
||||
_systemTitleEffects.systemTitleEffectsDo.UseModel(&model.SystemTitleEffects{})
|
||||
|
||||
tableName := _systemTitleEffects.systemTitleEffectsDo.TableName()
|
||||
_systemTitleEffects.ALL = field.NewAsterisk(tableName)
|
||||
_systemTitleEffects.ID = field.NewInt64(tableName, "id")
|
||||
_systemTitleEffects.TitleID = field.NewInt64(tableName, "title_id")
|
||||
_systemTitleEffects.EffectType = field.NewInt32(tableName, "effect_type")
|
||||
_systemTitleEffects.ParamsJSON = field.NewString(tableName, "params_json")
|
||||
_systemTitleEffects.StackingStrategy = field.NewInt32(tableName, "stacking_strategy")
|
||||
_systemTitleEffects.CapValueX1000 = field.NewInt32(tableName, "cap_value_x1000")
|
||||
_systemTitleEffects.ScopesJSON = field.NewString(tableName, "scopes_json")
|
||||
_systemTitleEffects.Sort = field.NewInt32(tableName, "sort")
|
||||
_systemTitleEffects.Status = field.NewInt32(tableName, "status")
|
||||
_systemTitleEffects.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
|
||||
_systemTitleEffects.fillFieldMap()
|
||||
|
||||
return _systemTitleEffects
|
||||
}
|
||||
|
||||
// systemTitleEffects 头衔效果配置表
|
||||
type systemTitleEffects struct {
|
||||
systemTitleEffectsDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.Int64 // 头衔效果配置主键ID
|
||||
TitleID field.Int64 // 归属头衔ID(system_titles.id)
|
||||
EffectType field.Int32 // 效果类型:1领券 2抽奖折扣 3签到倍数 4领道具卡 5概率加成 6奖品双倍概率
|
||||
ParamsJSON field.String // 效果参数JSON(倍数、概率、折扣值、模板ID、频次等)
|
||||
StackingStrategy field.Int32 // 叠加策略:0最大值 1累加封顶 2首个匹配
|
||||
CapValueX1000 field.Int32 // 封顶值(千分比;如倍数或概率总封顶)
|
||||
ScopesJSON field.String // 作用范围JSON(活动/期/分类等)
|
||||
Sort field.Int32 // 效果排序(同一头衔内)
|
||||
Status field.Int32 // 状态:1启用 0停用
|
||||
CreatedAt field.Time // 创建时间
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
|
||||
func (s systemTitleEffects) Table(newTableName string) *systemTitleEffects {
|
||||
s.systemTitleEffectsDo.UseTable(newTableName)
|
||||
return s.updateTableName(newTableName)
|
||||
}
|
||||
|
||||
func (s systemTitleEffects) As(alias string) *systemTitleEffects {
|
||||
s.systemTitleEffectsDo.DO = *(s.systemTitleEffectsDo.As(alias).(*gen.DO))
|
||||
return s.updateTableName(alias)
|
||||
}
|
||||
|
||||
func (s *systemTitleEffects) updateTableName(table string) *systemTitleEffects {
|
||||
s.ALL = field.NewAsterisk(table)
|
||||
s.ID = field.NewInt64(table, "id")
|
||||
s.TitleID = field.NewInt64(table, "title_id")
|
||||
s.EffectType = field.NewInt32(table, "effect_type")
|
||||
s.ParamsJSON = field.NewString(table, "params_json")
|
||||
s.StackingStrategy = field.NewInt32(table, "stacking_strategy")
|
||||
s.CapValueX1000 = field.NewInt32(table, "cap_value_x1000")
|
||||
s.ScopesJSON = field.NewString(table, "scopes_json")
|
||||
s.Sort = field.NewInt32(table, "sort")
|
||||
s.Status = field.NewInt32(table, "status")
|
||||
s.CreatedAt = field.NewTime(table, "created_at")
|
||||
|
||||
s.fillFieldMap()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *systemTitleEffects) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
_f, ok := s.fieldMap[fieldName]
|
||||
if !ok || _f == nil {
|
||||
return nil, false
|
||||
}
|
||||
_oe, ok := _f.(field.OrderExpr)
|
||||
return _oe, ok
|
||||
}
|
||||
|
||||
func (s *systemTitleEffects) fillFieldMap() {
|
||||
s.fieldMap = make(map[string]field.Expr, 10)
|
||||
s.fieldMap["id"] = s.ID
|
||||
s.fieldMap["title_id"] = s.TitleID
|
||||
s.fieldMap["effect_type"] = s.EffectType
|
||||
s.fieldMap["params_json"] = s.ParamsJSON
|
||||
s.fieldMap["stacking_strategy"] = s.StackingStrategy
|
||||
s.fieldMap["cap_value_x1000"] = s.CapValueX1000
|
||||
s.fieldMap["scopes_json"] = s.ScopesJSON
|
||||
s.fieldMap["sort"] = s.Sort
|
||||
s.fieldMap["status"] = s.Status
|
||||
s.fieldMap["created_at"] = s.CreatedAt
|
||||
}
|
||||
|
||||
func (s systemTitleEffects) clone(db *gorm.DB) systemTitleEffects {
|
||||
s.systemTitleEffectsDo.ReplaceConnPool(db.Statement.ConnPool)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s systemTitleEffects) replaceDB(db *gorm.DB) systemTitleEffects {
|
||||
s.systemTitleEffectsDo.ReplaceDB(db)
|
||||
return s
|
||||
}
|
||||
|
||||
type systemTitleEffectsDo struct{ gen.DO }
|
||||
|
||||
func (s systemTitleEffectsDo) Debug() *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Debug())
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) WithContext(ctx context.Context) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.WithContext(ctx))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) ReadDB() *systemTitleEffectsDo {
|
||||
return s.Clauses(dbresolver.Read)
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) WriteDB() *systemTitleEffectsDo {
|
||||
return s.Clauses(dbresolver.Write)
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Session(config *gorm.Session) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Session(config))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Clauses(conds ...clause.Expression) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Clauses(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Returning(value interface{}, columns ...string) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Returning(value, columns...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Not(conds ...gen.Condition) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Not(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Or(conds ...gen.Condition) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Or(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Select(conds ...field.Expr) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Select(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Where(conds ...gen.Condition) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Where(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Order(conds ...field.Expr) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Order(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Distinct(cols ...field.Expr) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Distinct(cols...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Omit(cols ...field.Expr) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Omit(cols...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Join(table schema.Tabler, on ...field.Expr) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Join(table, on...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) LeftJoin(table schema.Tabler, on ...field.Expr) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.LeftJoin(table, on...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) RightJoin(table schema.Tabler, on ...field.Expr) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.RightJoin(table, on...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Group(cols ...field.Expr) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Group(cols...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Having(conds ...gen.Condition) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Having(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Limit(limit int) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Limit(limit))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Offset(offset int) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Offset(offset))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Scopes(funcs...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Unscoped() *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Unscoped())
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Create(values ...*model.SystemTitleEffects) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.DO.Create(values)
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) CreateInBatches(values []*model.SystemTitleEffects, batchSize int) error {
|
||||
return s.DO.CreateInBatches(values, batchSize)
|
||||
}
|
||||
|
||||
// Save : !!! underlying implementation is different with GORM
|
||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
||||
func (s systemTitleEffectsDo) Save(values ...*model.SystemTitleEffects) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.DO.Save(values)
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) First() (*model.SystemTitleEffects, error) {
|
||||
if result, err := s.DO.First(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SystemTitleEffects), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Take() (*model.SystemTitleEffects, error) {
|
||||
if result, err := s.DO.Take(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SystemTitleEffects), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Last() (*model.SystemTitleEffects, error) {
|
||||
if result, err := s.DO.Last(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SystemTitleEffects), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Find() ([]*model.SystemTitleEffects, error) {
|
||||
result, err := s.DO.Find()
|
||||
return result.([]*model.SystemTitleEffects), err
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.SystemTitleEffects, err error) {
|
||||
buf := make([]*model.SystemTitleEffects, 0, batchSize)
|
||||
err = s.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
||||
defer func() { results = append(results, buf...) }()
|
||||
return fc(tx, batch)
|
||||
})
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) FindInBatches(result *[]*model.SystemTitleEffects, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
||||
return s.DO.FindInBatches(result, batchSize, fc)
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Attrs(attrs ...field.AssignExpr) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Attrs(attrs...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Assign(attrs ...field.AssignExpr) *systemTitleEffectsDo {
|
||||
return s.withDO(s.DO.Assign(attrs...))
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Joins(fields ...field.RelationField) *systemTitleEffectsDo {
|
||||
for _, _f := range fields {
|
||||
s = *s.withDO(s.DO.Joins(_f))
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Preload(fields ...field.RelationField) *systemTitleEffectsDo {
|
||||
for _, _f := range fields {
|
||||
s = *s.withDO(s.DO.Preload(_f))
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) FirstOrInit() (*model.SystemTitleEffects, error) {
|
||||
if result, err := s.DO.FirstOrInit(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SystemTitleEffects), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) FirstOrCreate() (*model.SystemTitleEffects, error) {
|
||||
if result, err := s.DO.FirstOrCreate(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SystemTitleEffects), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) FindByPage(offset int, limit int) (result []*model.SystemTitleEffects, count int64, err error) {
|
||||
result, err = s.Offset(offset).Limit(limit).Find()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
||||
count = int64(size + offset)
|
||||
return
|
||||
}
|
||||
|
||||
count, err = s.Offset(-1).Limit(-1).Count()
|
||||
return
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
||||
count, err = s.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Offset(offset).Limit(limit).Scan(result)
|
||||
return
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Scan(result interface{}) (err error) {
|
||||
return s.DO.Scan(result)
|
||||
}
|
||||
|
||||
func (s systemTitleEffectsDo) Delete(models ...*model.SystemTitleEffects) (result gen.ResultInfo, err error) {
|
||||
return s.DO.Delete(models)
|
||||
}
|
||||
|
||||
func (s *systemTitleEffectsDo) withDO(do gen.Dao) *systemTitleEffectsDo {
|
||||
s.DO = *do.(*gen.DO)
|
||||
return s
|
||||
}
|
||||
348
internal/repository/mysql/dao/system_titles.gen.go
Normal file
348
internal/repository/mysql/dao/system_titles.gen.go
Normal file
@ -0,0 +1,348 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
|
||||
"gorm.io/plugin/dbresolver"
|
||||
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
func newSystemTitles(db *gorm.DB, opts ...gen.DOOption) systemTitles {
|
||||
_systemTitles := systemTitles{}
|
||||
|
||||
_systemTitles.systemTitlesDo.UseDB(db, opts...)
|
||||
_systemTitles.systemTitlesDo.UseModel(&model.SystemTitles{})
|
||||
|
||||
tableName := _systemTitles.systemTitlesDo.TableName()
|
||||
_systemTitles.ALL = field.NewAsterisk(tableName)
|
||||
_systemTitles.ID = field.NewInt64(tableName, "id")
|
||||
_systemTitles.Name = field.NewString(tableName, "name")
|
||||
_systemTitles.Description = field.NewString(tableName, "description")
|
||||
_systemTitles.Status = field.NewInt32(tableName, "status")
|
||||
_systemTitles.ObtainRulesJSON = field.NewString(tableName, "obtain_rules_json")
|
||||
_systemTitles.ScopesJSON = field.NewString(tableName, "scopes_json")
|
||||
_systemTitles.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_systemTitles.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
|
||||
_systemTitles.fillFieldMap()
|
||||
|
||||
return _systemTitles
|
||||
}
|
||||
|
||||
// systemTitles 头衔模板表
|
||||
type systemTitles struct {
|
||||
systemTitlesDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.Int64 // 头衔模板主键ID
|
||||
Name field.String // 头衔名称(唯一)
|
||||
Description field.String // 头衔描述
|
||||
Status field.Int32 // 状态:1启用 0停用
|
||||
ObtainRulesJSON field.String // 获得条件规则JSON(任务、等级、付费等)
|
||||
ScopesJSON field.String // 作用范围配置JSON(活动/期/分类等)
|
||||
CreatedAt field.Time // 创建时间
|
||||
UpdatedAt field.Time // 更新时间
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
|
||||
func (s systemTitles) Table(newTableName string) *systemTitles {
|
||||
s.systemTitlesDo.UseTable(newTableName)
|
||||
return s.updateTableName(newTableName)
|
||||
}
|
||||
|
||||
func (s systemTitles) As(alias string) *systemTitles {
|
||||
s.systemTitlesDo.DO = *(s.systemTitlesDo.As(alias).(*gen.DO))
|
||||
return s.updateTableName(alias)
|
||||
}
|
||||
|
||||
func (s *systemTitles) updateTableName(table string) *systemTitles {
|
||||
s.ALL = field.NewAsterisk(table)
|
||||
s.ID = field.NewInt64(table, "id")
|
||||
s.Name = field.NewString(table, "name")
|
||||
s.Description = field.NewString(table, "description")
|
||||
s.Status = field.NewInt32(table, "status")
|
||||
s.ObtainRulesJSON = field.NewString(table, "obtain_rules_json")
|
||||
s.ScopesJSON = field.NewString(table, "scopes_json")
|
||||
s.CreatedAt = field.NewTime(table, "created_at")
|
||||
s.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
|
||||
s.fillFieldMap()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *systemTitles) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
_f, ok := s.fieldMap[fieldName]
|
||||
if !ok || _f == nil {
|
||||
return nil, false
|
||||
}
|
||||
_oe, ok := _f.(field.OrderExpr)
|
||||
return _oe, ok
|
||||
}
|
||||
|
||||
func (s *systemTitles) fillFieldMap() {
|
||||
s.fieldMap = make(map[string]field.Expr, 8)
|
||||
s.fieldMap["id"] = s.ID
|
||||
s.fieldMap["name"] = s.Name
|
||||
s.fieldMap["description"] = s.Description
|
||||
s.fieldMap["status"] = s.Status
|
||||
s.fieldMap["obtain_rules_json"] = s.ObtainRulesJSON
|
||||
s.fieldMap["scopes_json"] = s.ScopesJSON
|
||||
s.fieldMap["created_at"] = s.CreatedAt
|
||||
s.fieldMap["updated_at"] = s.UpdatedAt
|
||||
}
|
||||
|
||||
func (s systemTitles) clone(db *gorm.DB) systemTitles {
|
||||
s.systemTitlesDo.ReplaceConnPool(db.Statement.ConnPool)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s systemTitles) replaceDB(db *gorm.DB) systemTitles {
|
||||
s.systemTitlesDo.ReplaceDB(db)
|
||||
return s
|
||||
}
|
||||
|
||||
type systemTitlesDo struct{ gen.DO }
|
||||
|
||||
func (s systemTitlesDo) Debug() *systemTitlesDo {
|
||||
return s.withDO(s.DO.Debug())
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) WithContext(ctx context.Context) *systemTitlesDo {
|
||||
return s.withDO(s.DO.WithContext(ctx))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) ReadDB() *systemTitlesDo {
|
||||
return s.Clauses(dbresolver.Read)
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) WriteDB() *systemTitlesDo {
|
||||
return s.Clauses(dbresolver.Write)
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Session(config *gorm.Session) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Session(config))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Clauses(conds ...clause.Expression) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Clauses(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Returning(value interface{}, columns ...string) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Returning(value, columns...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Not(conds ...gen.Condition) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Not(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Or(conds ...gen.Condition) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Or(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Select(conds ...field.Expr) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Select(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Where(conds ...gen.Condition) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Where(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Order(conds ...field.Expr) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Order(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Distinct(cols ...field.Expr) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Distinct(cols...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Omit(cols ...field.Expr) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Omit(cols...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Join(table schema.Tabler, on ...field.Expr) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Join(table, on...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) LeftJoin(table schema.Tabler, on ...field.Expr) *systemTitlesDo {
|
||||
return s.withDO(s.DO.LeftJoin(table, on...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) RightJoin(table schema.Tabler, on ...field.Expr) *systemTitlesDo {
|
||||
return s.withDO(s.DO.RightJoin(table, on...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Group(cols ...field.Expr) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Group(cols...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Having(conds ...gen.Condition) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Having(conds...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Limit(limit int) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Limit(limit))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Offset(offset int) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Offset(offset))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Scopes(funcs...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Unscoped() *systemTitlesDo {
|
||||
return s.withDO(s.DO.Unscoped())
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Create(values ...*model.SystemTitles) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.DO.Create(values)
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) CreateInBatches(values []*model.SystemTitles, batchSize int) error {
|
||||
return s.DO.CreateInBatches(values, batchSize)
|
||||
}
|
||||
|
||||
// Save : !!! underlying implementation is different with GORM
|
||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
||||
func (s systemTitlesDo) Save(values ...*model.SystemTitles) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.DO.Save(values)
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) First() (*model.SystemTitles, error) {
|
||||
if result, err := s.DO.First(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SystemTitles), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Take() (*model.SystemTitles, error) {
|
||||
if result, err := s.DO.Take(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SystemTitles), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Last() (*model.SystemTitles, error) {
|
||||
if result, err := s.DO.Last(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SystemTitles), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Find() ([]*model.SystemTitles, error) {
|
||||
result, err := s.DO.Find()
|
||||
return result.([]*model.SystemTitles), err
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.SystemTitles, err error) {
|
||||
buf := make([]*model.SystemTitles, 0, batchSize)
|
||||
err = s.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
||||
defer func() { results = append(results, buf...) }()
|
||||
return fc(tx, batch)
|
||||
})
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) FindInBatches(result *[]*model.SystemTitles, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
||||
return s.DO.FindInBatches(result, batchSize, fc)
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Attrs(attrs ...field.AssignExpr) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Attrs(attrs...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Assign(attrs ...field.AssignExpr) *systemTitlesDo {
|
||||
return s.withDO(s.DO.Assign(attrs...))
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Joins(fields ...field.RelationField) *systemTitlesDo {
|
||||
for _, _f := range fields {
|
||||
s = *s.withDO(s.DO.Joins(_f))
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Preload(fields ...field.RelationField) *systemTitlesDo {
|
||||
for _, _f := range fields {
|
||||
s = *s.withDO(s.DO.Preload(_f))
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) FirstOrInit() (*model.SystemTitles, error) {
|
||||
if result, err := s.DO.FirstOrInit(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SystemTitles), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) FirstOrCreate() (*model.SystemTitles, error) {
|
||||
if result, err := s.DO.FirstOrCreate(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SystemTitles), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) FindByPage(offset int, limit int) (result []*model.SystemTitles, count int64, err error) {
|
||||
result, err = s.Offset(offset).Limit(limit).Find()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
||||
count = int64(size + offset)
|
||||
return
|
||||
}
|
||||
|
||||
count, err = s.Offset(-1).Limit(-1).Count()
|
||||
return
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
||||
count, err = s.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Offset(offset).Limit(limit).Scan(result)
|
||||
return
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Scan(result interface{}) (err error) {
|
||||
return s.DO.Scan(result)
|
||||
}
|
||||
|
||||
func (s systemTitlesDo) Delete(models ...*model.SystemTitles) (result gen.ResultInfo, err error) {
|
||||
return s.DO.Delete(models)
|
||||
}
|
||||
|
||||
func (s *systemTitlesDo) withDO(do gen.Dao) *systemTitlesDo {
|
||||
s.DO = *do.(*gen.DO)
|
||||
return s
|
||||
}
|
||||
352
internal/repository/mysql/dao/user_title_effect_claims.gen.go
Normal file
352
internal/repository/mysql/dao/user_title_effect_claims.gen.go
Normal file
@ -0,0 +1,352 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
|
||||
"gorm.io/plugin/dbresolver"
|
||||
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
func newUserTitleEffectClaims(db *gorm.DB, opts ...gen.DOOption) userTitleEffectClaims {
|
||||
_userTitleEffectClaims := userTitleEffectClaims{}
|
||||
|
||||
_userTitleEffectClaims.userTitleEffectClaimsDo.UseDB(db, opts...)
|
||||
_userTitleEffectClaims.userTitleEffectClaimsDo.UseModel(&model.UserTitleEffectClaims{})
|
||||
|
||||
tableName := _userTitleEffectClaims.userTitleEffectClaimsDo.TableName()
|
||||
_userTitleEffectClaims.ALL = field.NewAsterisk(tableName)
|
||||
_userTitleEffectClaims.ID = field.NewInt64(tableName, "id")
|
||||
_userTitleEffectClaims.UserID = field.NewInt64(tableName, "user_id")
|
||||
_userTitleEffectClaims.TitleID = field.NewInt64(tableName, "title_id")
|
||||
_userTitleEffectClaims.EffectType = field.NewInt32(tableName, "effect_type")
|
||||
_userTitleEffectClaims.TargetTemplateID = field.NewInt64(tableName, "target_template_id")
|
||||
_userTitleEffectClaims.PeriodKey = field.NewString(tableName, "period_key")
|
||||
_userTitleEffectClaims.ClaimCount = field.NewInt32(tableName, "claim_count")
|
||||
_userTitleEffectClaims.LastClaimAt = field.NewTime(tableName, "last_claim_at")
|
||||
_userTitleEffectClaims.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
|
||||
_userTitleEffectClaims.fillFieldMap()
|
||||
|
||||
return _userTitleEffectClaims
|
||||
}
|
||||
|
||||
// userTitleEffectClaims 领取型权益限流表
|
||||
type userTitleEffectClaims struct {
|
||||
userTitleEffectClaimsDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.Int64 // 头衔领取型权益限流主键ID
|
||||
UserID field.Int64 // 用户ID(users.id)
|
||||
TitleID field.Int64 // 头衔ID(system_titles.id)
|
||||
EffectType field.Int32 // 效果类型(与system_title_effects.effect_type一致)
|
||||
TargetTemplateID field.Int64 // 目标模板ID(券模板/卡模板等)
|
||||
PeriodKey field.String // 周期键(如每日YYYYMMDD、每周YYYYWW、每月YYYYMM)
|
||||
ClaimCount field.Int32 // 当期已领取次数
|
||||
LastClaimAt field.Time // 最近领取时间
|
||||
CreatedAt field.Time // 创建时间
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaims) Table(newTableName string) *userTitleEffectClaims {
|
||||
u.userTitleEffectClaimsDo.UseTable(newTableName)
|
||||
return u.updateTableName(newTableName)
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaims) As(alias string) *userTitleEffectClaims {
|
||||
u.userTitleEffectClaimsDo.DO = *(u.userTitleEffectClaimsDo.As(alias).(*gen.DO))
|
||||
return u.updateTableName(alias)
|
||||
}
|
||||
|
||||
func (u *userTitleEffectClaims) updateTableName(table string) *userTitleEffectClaims {
|
||||
u.ALL = field.NewAsterisk(table)
|
||||
u.ID = field.NewInt64(table, "id")
|
||||
u.UserID = field.NewInt64(table, "user_id")
|
||||
u.TitleID = field.NewInt64(table, "title_id")
|
||||
u.EffectType = field.NewInt32(table, "effect_type")
|
||||
u.TargetTemplateID = field.NewInt64(table, "target_template_id")
|
||||
u.PeriodKey = field.NewString(table, "period_key")
|
||||
u.ClaimCount = field.NewInt32(table, "claim_count")
|
||||
u.LastClaimAt = field.NewTime(table, "last_claim_at")
|
||||
u.CreatedAt = field.NewTime(table, "created_at")
|
||||
|
||||
u.fillFieldMap()
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *userTitleEffectClaims) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
_f, ok := u.fieldMap[fieldName]
|
||||
if !ok || _f == nil {
|
||||
return nil, false
|
||||
}
|
||||
_oe, ok := _f.(field.OrderExpr)
|
||||
return _oe, ok
|
||||
}
|
||||
|
||||
func (u *userTitleEffectClaims) fillFieldMap() {
|
||||
u.fieldMap = make(map[string]field.Expr, 9)
|
||||
u.fieldMap["id"] = u.ID
|
||||
u.fieldMap["user_id"] = u.UserID
|
||||
u.fieldMap["title_id"] = u.TitleID
|
||||
u.fieldMap["effect_type"] = u.EffectType
|
||||
u.fieldMap["target_template_id"] = u.TargetTemplateID
|
||||
u.fieldMap["period_key"] = u.PeriodKey
|
||||
u.fieldMap["claim_count"] = u.ClaimCount
|
||||
u.fieldMap["last_claim_at"] = u.LastClaimAt
|
||||
u.fieldMap["created_at"] = u.CreatedAt
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaims) clone(db *gorm.DB) userTitleEffectClaims {
|
||||
u.userTitleEffectClaimsDo.ReplaceConnPool(db.Statement.ConnPool)
|
||||
return u
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaims) replaceDB(db *gorm.DB) userTitleEffectClaims {
|
||||
u.userTitleEffectClaimsDo.ReplaceDB(db)
|
||||
return u
|
||||
}
|
||||
|
||||
type userTitleEffectClaimsDo struct{ gen.DO }
|
||||
|
||||
func (u userTitleEffectClaimsDo) Debug() *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Debug())
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) WithContext(ctx context.Context) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.WithContext(ctx))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) ReadDB() *userTitleEffectClaimsDo {
|
||||
return u.Clauses(dbresolver.Read)
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) WriteDB() *userTitleEffectClaimsDo {
|
||||
return u.Clauses(dbresolver.Write)
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Session(config *gorm.Session) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Session(config))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Clauses(conds ...clause.Expression) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Clauses(conds...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Returning(value interface{}, columns ...string) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Returning(value, columns...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Not(conds ...gen.Condition) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Not(conds...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Or(conds ...gen.Condition) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Or(conds...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Select(conds ...field.Expr) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Select(conds...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Where(conds ...gen.Condition) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Where(conds...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Order(conds ...field.Expr) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Order(conds...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Distinct(cols ...field.Expr) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Distinct(cols...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Omit(cols ...field.Expr) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Omit(cols...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Join(table schema.Tabler, on ...field.Expr) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Join(table, on...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) LeftJoin(table schema.Tabler, on ...field.Expr) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.LeftJoin(table, on...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) RightJoin(table schema.Tabler, on ...field.Expr) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.RightJoin(table, on...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Group(cols ...field.Expr) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Group(cols...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Having(conds ...gen.Condition) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Having(conds...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Limit(limit int) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Limit(limit))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Offset(offset int) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Offset(offset))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Scopes(funcs...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Unscoped() *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Unscoped())
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Create(values ...*model.UserTitleEffectClaims) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return u.DO.Create(values)
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) CreateInBatches(values []*model.UserTitleEffectClaims, batchSize int) error {
|
||||
return u.DO.CreateInBatches(values, batchSize)
|
||||
}
|
||||
|
||||
// Save : !!! underlying implementation is different with GORM
|
||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
||||
func (u userTitleEffectClaimsDo) Save(values ...*model.UserTitleEffectClaims) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return u.DO.Save(values)
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) First() (*model.UserTitleEffectClaims, error) {
|
||||
if result, err := u.DO.First(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.UserTitleEffectClaims), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Take() (*model.UserTitleEffectClaims, error) {
|
||||
if result, err := u.DO.Take(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.UserTitleEffectClaims), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Last() (*model.UserTitleEffectClaims, error) {
|
||||
if result, err := u.DO.Last(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.UserTitleEffectClaims), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Find() ([]*model.UserTitleEffectClaims, error) {
|
||||
result, err := u.DO.Find()
|
||||
return result.([]*model.UserTitleEffectClaims), err
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.UserTitleEffectClaims, err error) {
|
||||
buf := make([]*model.UserTitleEffectClaims, 0, batchSize)
|
||||
err = u.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
||||
defer func() { results = append(results, buf...) }()
|
||||
return fc(tx, batch)
|
||||
})
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) FindInBatches(result *[]*model.UserTitleEffectClaims, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
||||
return u.DO.FindInBatches(result, batchSize, fc)
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Attrs(attrs ...field.AssignExpr) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Attrs(attrs...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Assign(attrs ...field.AssignExpr) *userTitleEffectClaimsDo {
|
||||
return u.withDO(u.DO.Assign(attrs...))
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Joins(fields ...field.RelationField) *userTitleEffectClaimsDo {
|
||||
for _, _f := range fields {
|
||||
u = *u.withDO(u.DO.Joins(_f))
|
||||
}
|
||||
return &u
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Preload(fields ...field.RelationField) *userTitleEffectClaimsDo {
|
||||
for _, _f := range fields {
|
||||
u = *u.withDO(u.DO.Preload(_f))
|
||||
}
|
||||
return &u
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) FirstOrInit() (*model.UserTitleEffectClaims, error) {
|
||||
if result, err := u.DO.FirstOrInit(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.UserTitleEffectClaims), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) FirstOrCreate() (*model.UserTitleEffectClaims, error) {
|
||||
if result, err := u.DO.FirstOrCreate(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.UserTitleEffectClaims), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) FindByPage(offset int, limit int) (result []*model.UserTitleEffectClaims, count int64, err error) {
|
||||
result, err = u.Offset(offset).Limit(limit).Find()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
||||
count = int64(size + offset)
|
||||
return
|
||||
}
|
||||
|
||||
count, err = u.Offset(-1).Limit(-1).Count()
|
||||
return
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
||||
count, err = u.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = u.Offset(offset).Limit(limit).Scan(result)
|
||||
return
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Scan(result interface{}) (err error) {
|
||||
return u.DO.Scan(result)
|
||||
}
|
||||
|
||||
func (u userTitleEffectClaimsDo) Delete(models ...*model.UserTitleEffectClaims) (result gen.ResultInfo, err error) {
|
||||
return u.DO.Delete(models)
|
||||
}
|
||||
|
||||
func (u *userTitleEffectClaimsDo) withDO(do gen.Dao) *userTitleEffectClaimsDo {
|
||||
u.DO = *do.(*gen.DO)
|
||||
return u
|
||||
}
|
||||
352
internal/repository/mysql/dao/user_titles.gen.go
Normal file
352
internal/repository/mysql/dao/user_titles.gen.go
Normal file
@ -0,0 +1,352 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
|
||||
"gorm.io/plugin/dbresolver"
|
||||
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
func newUserTitles(db *gorm.DB, opts ...gen.DOOption) userTitles {
|
||||
_userTitles := userTitles{}
|
||||
|
||||
_userTitles.userTitlesDo.UseDB(db, opts...)
|
||||
_userTitles.userTitlesDo.UseModel(&model.UserTitles{})
|
||||
|
||||
tableName := _userTitles.userTitlesDo.TableName()
|
||||
_userTitles.ALL = field.NewAsterisk(tableName)
|
||||
_userTitles.ID = field.NewInt64(tableName, "id")
|
||||
_userTitles.UserID = field.NewInt64(tableName, "user_id")
|
||||
_userTitles.TitleID = field.NewInt64(tableName, "title_id")
|
||||
_userTitles.Active = field.NewInt32(tableName, "active")
|
||||
_userTitles.ObtainedAt = field.NewTime(tableName, "obtained_at")
|
||||
_userTitles.ExpiresAt = field.NewTime(tableName, "expires_at")
|
||||
_userTitles.Source = field.NewString(tableName, "source")
|
||||
_userTitles.Remark = field.NewString(tableName, "remark")
|
||||
_userTitles.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
|
||||
_userTitles.fillFieldMap()
|
||||
|
||||
return _userTitles
|
||||
}
|
||||
|
||||
// userTitles 用户持有头衔表
|
||||
type userTitles struct {
|
||||
userTitlesDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.Int64 // 用户持有头衔主键ID
|
||||
UserID field.Int64 // 用户ID(users.id)
|
||||
TitleID field.Int64 // 头衔ID(system_titles.id)
|
||||
Active field.Int32 // 是否激活:1激活 0停用
|
||||
ObtainedAt field.Time // 获得时间
|
||||
ExpiresAt field.Time // 过期时间(可空)
|
||||
Source field.String // 来源标识(活动、运营发放等)
|
||||
Remark field.String // 备注信息
|
||||
CreatedAt field.Time // 创建时间
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
|
||||
func (u userTitles) Table(newTableName string) *userTitles {
|
||||
u.userTitlesDo.UseTable(newTableName)
|
||||
return u.updateTableName(newTableName)
|
||||
}
|
||||
|
||||
func (u userTitles) As(alias string) *userTitles {
|
||||
u.userTitlesDo.DO = *(u.userTitlesDo.As(alias).(*gen.DO))
|
||||
return u.updateTableName(alias)
|
||||
}
|
||||
|
||||
func (u *userTitles) updateTableName(table string) *userTitles {
|
||||
u.ALL = field.NewAsterisk(table)
|
||||
u.ID = field.NewInt64(table, "id")
|
||||
u.UserID = field.NewInt64(table, "user_id")
|
||||
u.TitleID = field.NewInt64(table, "title_id")
|
||||
u.Active = field.NewInt32(table, "active")
|
||||
u.ObtainedAt = field.NewTime(table, "obtained_at")
|
||||
u.ExpiresAt = field.NewTime(table, "expires_at")
|
||||
u.Source = field.NewString(table, "source")
|
||||
u.Remark = field.NewString(table, "remark")
|
||||
u.CreatedAt = field.NewTime(table, "created_at")
|
||||
|
||||
u.fillFieldMap()
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *userTitles) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
_f, ok := u.fieldMap[fieldName]
|
||||
if !ok || _f == nil {
|
||||
return nil, false
|
||||
}
|
||||
_oe, ok := _f.(field.OrderExpr)
|
||||
return _oe, ok
|
||||
}
|
||||
|
||||
func (u *userTitles) fillFieldMap() {
|
||||
u.fieldMap = make(map[string]field.Expr, 9)
|
||||
u.fieldMap["id"] = u.ID
|
||||
u.fieldMap["user_id"] = u.UserID
|
||||
u.fieldMap["title_id"] = u.TitleID
|
||||
u.fieldMap["active"] = u.Active
|
||||
u.fieldMap["obtained_at"] = u.ObtainedAt
|
||||
u.fieldMap["expires_at"] = u.ExpiresAt
|
||||
u.fieldMap["source"] = u.Source
|
||||
u.fieldMap["remark"] = u.Remark
|
||||
u.fieldMap["created_at"] = u.CreatedAt
|
||||
}
|
||||
|
||||
func (u userTitles) clone(db *gorm.DB) userTitles {
|
||||
u.userTitlesDo.ReplaceConnPool(db.Statement.ConnPool)
|
||||
return u
|
||||
}
|
||||
|
||||
func (u userTitles) replaceDB(db *gorm.DB) userTitles {
|
||||
u.userTitlesDo.ReplaceDB(db)
|
||||
return u
|
||||
}
|
||||
|
||||
type userTitlesDo struct{ gen.DO }
|
||||
|
||||
func (u userTitlesDo) Debug() *userTitlesDo {
|
||||
return u.withDO(u.DO.Debug())
|
||||
}
|
||||
|
||||
func (u userTitlesDo) WithContext(ctx context.Context) *userTitlesDo {
|
||||
return u.withDO(u.DO.WithContext(ctx))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) ReadDB() *userTitlesDo {
|
||||
return u.Clauses(dbresolver.Read)
|
||||
}
|
||||
|
||||
func (u userTitlesDo) WriteDB() *userTitlesDo {
|
||||
return u.Clauses(dbresolver.Write)
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Session(config *gorm.Session) *userTitlesDo {
|
||||
return u.withDO(u.DO.Session(config))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Clauses(conds ...clause.Expression) *userTitlesDo {
|
||||
return u.withDO(u.DO.Clauses(conds...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Returning(value interface{}, columns ...string) *userTitlesDo {
|
||||
return u.withDO(u.DO.Returning(value, columns...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Not(conds ...gen.Condition) *userTitlesDo {
|
||||
return u.withDO(u.DO.Not(conds...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Or(conds ...gen.Condition) *userTitlesDo {
|
||||
return u.withDO(u.DO.Or(conds...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Select(conds ...field.Expr) *userTitlesDo {
|
||||
return u.withDO(u.DO.Select(conds...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Where(conds ...gen.Condition) *userTitlesDo {
|
||||
return u.withDO(u.DO.Where(conds...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Order(conds ...field.Expr) *userTitlesDo {
|
||||
return u.withDO(u.DO.Order(conds...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Distinct(cols ...field.Expr) *userTitlesDo {
|
||||
return u.withDO(u.DO.Distinct(cols...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Omit(cols ...field.Expr) *userTitlesDo {
|
||||
return u.withDO(u.DO.Omit(cols...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Join(table schema.Tabler, on ...field.Expr) *userTitlesDo {
|
||||
return u.withDO(u.DO.Join(table, on...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) LeftJoin(table schema.Tabler, on ...field.Expr) *userTitlesDo {
|
||||
return u.withDO(u.DO.LeftJoin(table, on...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) RightJoin(table schema.Tabler, on ...field.Expr) *userTitlesDo {
|
||||
return u.withDO(u.DO.RightJoin(table, on...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Group(cols ...field.Expr) *userTitlesDo {
|
||||
return u.withDO(u.DO.Group(cols...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Having(conds ...gen.Condition) *userTitlesDo {
|
||||
return u.withDO(u.DO.Having(conds...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Limit(limit int) *userTitlesDo {
|
||||
return u.withDO(u.DO.Limit(limit))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Offset(offset int) *userTitlesDo {
|
||||
return u.withDO(u.DO.Offset(offset))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *userTitlesDo {
|
||||
return u.withDO(u.DO.Scopes(funcs...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Unscoped() *userTitlesDo {
|
||||
return u.withDO(u.DO.Unscoped())
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Create(values ...*model.UserTitles) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return u.DO.Create(values)
|
||||
}
|
||||
|
||||
func (u userTitlesDo) CreateInBatches(values []*model.UserTitles, batchSize int) error {
|
||||
return u.DO.CreateInBatches(values, batchSize)
|
||||
}
|
||||
|
||||
// Save : !!! underlying implementation is different with GORM
|
||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
||||
func (u userTitlesDo) Save(values ...*model.UserTitles) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return u.DO.Save(values)
|
||||
}
|
||||
|
||||
func (u userTitlesDo) First() (*model.UserTitles, error) {
|
||||
if result, err := u.DO.First(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.UserTitles), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Take() (*model.UserTitles, error) {
|
||||
if result, err := u.DO.Take(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.UserTitles), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Last() (*model.UserTitles, error) {
|
||||
if result, err := u.DO.Last(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.UserTitles), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Find() ([]*model.UserTitles, error) {
|
||||
result, err := u.DO.Find()
|
||||
return result.([]*model.UserTitles), err
|
||||
}
|
||||
|
||||
func (u userTitlesDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.UserTitles, err error) {
|
||||
buf := make([]*model.UserTitles, 0, batchSize)
|
||||
err = u.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
||||
defer func() { results = append(results, buf...) }()
|
||||
return fc(tx, batch)
|
||||
})
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (u userTitlesDo) FindInBatches(result *[]*model.UserTitles, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
||||
return u.DO.FindInBatches(result, batchSize, fc)
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Attrs(attrs ...field.AssignExpr) *userTitlesDo {
|
||||
return u.withDO(u.DO.Attrs(attrs...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Assign(attrs ...field.AssignExpr) *userTitlesDo {
|
||||
return u.withDO(u.DO.Assign(attrs...))
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Joins(fields ...field.RelationField) *userTitlesDo {
|
||||
for _, _f := range fields {
|
||||
u = *u.withDO(u.DO.Joins(_f))
|
||||
}
|
||||
return &u
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Preload(fields ...field.RelationField) *userTitlesDo {
|
||||
for _, _f := range fields {
|
||||
u = *u.withDO(u.DO.Preload(_f))
|
||||
}
|
||||
return &u
|
||||
}
|
||||
|
||||
func (u userTitlesDo) FirstOrInit() (*model.UserTitles, error) {
|
||||
if result, err := u.DO.FirstOrInit(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.UserTitles), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u userTitlesDo) FirstOrCreate() (*model.UserTitles, error) {
|
||||
if result, err := u.DO.FirstOrCreate(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.UserTitles), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u userTitlesDo) FindByPage(offset int, limit int) (result []*model.UserTitles, count int64, err error) {
|
||||
result, err = u.Offset(offset).Limit(limit).Find()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
||||
count = int64(size + offset)
|
||||
return
|
||||
}
|
||||
|
||||
count, err = u.Offset(-1).Limit(-1).Count()
|
||||
return
|
||||
}
|
||||
|
||||
func (u userTitlesDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
||||
count, err = u.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = u.Offset(offset).Limit(limit).Scan(result)
|
||||
return
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Scan(result interface{}) (err error) {
|
||||
return u.DO.Scan(result)
|
||||
}
|
||||
|
||||
func (u userTitlesDo) Delete(models ...*model.UserTitles) (result gen.ResultInfo, err error) {
|
||||
return u.DO.Delete(models)
|
||||
}
|
||||
|
||||
func (u *userTitlesDo) withDO(do gen.Dao) *userTitlesDo {
|
||||
u.DO = *do.(*gen.DO)
|
||||
return u
|
||||
}
|
||||
30
internal/repository/mysql/model/system_title_effects.gen.go
Normal file
30
internal/repository/mysql/model/system_title_effects.gen.go
Normal file
@ -0,0 +1,30 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameSystemTitleEffects = "system_title_effects"
|
||||
|
||||
// SystemTitleEffects 头衔效果配置表
|
||||
type SystemTitleEffects struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:头衔效果配置主键ID" json:"id"` // 头衔效果配置主键ID
|
||||
TitleID int64 `gorm:"column:title_id;not null;comment:归属头衔ID(system_titles.id)" json:"title_id"` // 归属头衔ID(system_titles.id)
|
||||
EffectType int32 `gorm:"column:effect_type;not null;comment:效果类型:1领券 2抽奖折扣 3签到倍数 4领道具卡 5概率加成 6奖品双倍概率" json:"effect_type"` // 效果类型:1领券 2抽奖折扣 3签到倍数 4领道具卡 5概率加成 6奖品双倍概率
|
||||
ParamsJSON string `gorm:"column:params_json;not null;comment:效果参数JSON(倍数、概率、折扣值、模板ID、频次等)" json:"params_json"` // 效果参数JSON(倍数、概率、折扣值、模板ID、频次等)
|
||||
StackingStrategy int32 `gorm:"column:stacking_strategy;not null;comment:叠加策略:0最大值 1累加封顶 2首个匹配" json:"stacking_strategy"` // 叠加策略:0最大值 1累加封顶 2首个匹配
|
||||
CapValueX1000 int32 `gorm:"column:cap_value_x1000;comment:封顶值(千分比;如倍数或概率总封顶)" json:"cap_value_x1000"` // 封顶值(千分比;如倍数或概率总封顶)
|
||||
ScopesJSON string `gorm:"column:scopes_json;comment:作用范围JSON(活动/期/分类等)" json:"scopes_json"` // 作用范围JSON(活动/期/分类等)
|
||||
Sort int32 `gorm:"column:sort;not null;comment:效果排序(同一头衔内)" json:"sort"` // 效果排序(同一头衔内)
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态:1启用 0停用" json:"status"` // 状态:1启用 0停用
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
}
|
||||
|
||||
// TableName SystemTitleEffects's table name
|
||||
func (*SystemTitleEffects) TableName() string {
|
||||
return TableNameSystemTitleEffects
|
||||
}
|
||||
28
internal/repository/mysql/model/system_titles.gen.go
Normal file
28
internal/repository/mysql/model/system_titles.gen.go
Normal file
@ -0,0 +1,28 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameSystemTitles = "system_titles"
|
||||
|
||||
// SystemTitles 头衔模板表
|
||||
type SystemTitles struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:头衔模板主键ID" json:"id"` // 头衔模板主键ID
|
||||
Name string `gorm:"column:name;not null;comment:头衔名称(唯一)" json:"name"` // 头衔名称(唯一)
|
||||
Description string `gorm:"column:description;comment:头衔描述" json:"description"` // 头衔描述
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态:1启用 0停用" json:"status"` // 状态:1启用 0停用
|
||||
ObtainRulesJSON string `gorm:"column:obtain_rules_json;comment:获得条件规则JSON(任务、等级、付费等)" json:"obtain_rules_json"` // 获得条件规则JSON(任务、等级、付费等)
|
||||
ScopesJSON string `gorm:"column:scopes_json;comment:作用范围配置JSON(活动/期/分类等)" json:"scopes_json"` // 作用范围配置JSON(活动/期/分类等)
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||
}
|
||||
|
||||
// TableName SystemTitles's table name
|
||||
func (*SystemTitles) TableName() string {
|
||||
return TableNameSystemTitles
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameUserTitleEffectClaims = "user_title_effect_claims"
|
||||
|
||||
// UserTitleEffectClaims 领取型权益限流表
|
||||
type UserTitleEffectClaims struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:头衔领取型权益限流主键ID" json:"id"` // 头衔领取型权益限流主键ID
|
||||
UserID int64 `gorm:"column:user_id;not null;comment:用户ID(users.id)" json:"user_id"` // 用户ID(users.id)
|
||||
TitleID int64 `gorm:"column:title_id;not null;comment:头衔ID(system_titles.id)" json:"title_id"` // 头衔ID(system_titles.id)
|
||||
EffectType int32 `gorm:"column:effect_type;not null;comment:效果类型(与system_title_effects.effect_type一致)" json:"effect_type"` // 效果类型(与system_title_effects.effect_type一致)
|
||||
TargetTemplateID int64 `gorm:"column:target_template_id;comment:目标模板ID(券模板/卡模板等)" json:"target_template_id"` // 目标模板ID(券模板/卡模板等)
|
||||
PeriodKey string `gorm:"column:period_key;not null;comment:周期键(如每日YYYYMMDD、每周YYYYWW、每月YYYYMM)" json:"period_key"` // 周期键(如每日YYYYMMDD、每周YYYYWW、每月YYYYMM)
|
||||
ClaimCount int32 `gorm:"column:claim_count;not null;comment:当期已领取次数" json:"claim_count"` // 当期已领取次数
|
||||
LastClaimAt time.Time `gorm:"column:last_claim_at;comment:最近领取时间" json:"last_claim_at"` // 最近领取时间
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
}
|
||||
|
||||
// TableName UserTitleEffectClaims's table name
|
||||
func (*UserTitleEffectClaims) TableName() string {
|
||||
return TableNameUserTitleEffectClaims
|
||||
}
|
||||
29
internal/repository/mysql/model/user_titles.gen.go
Normal file
29
internal/repository/mysql/model/user_titles.gen.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameUserTitles = "user_titles"
|
||||
|
||||
// UserTitles 用户持有头衔表
|
||||
type UserTitles struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:用户持有头衔主键ID" json:"id"` // 用户持有头衔主键ID
|
||||
UserID int64 `gorm:"column:user_id;not null;comment:用户ID(users.id)" json:"user_id"` // 用户ID(users.id)
|
||||
TitleID int64 `gorm:"column:title_id;not null;comment:头衔ID(system_titles.id)" json:"title_id"` // 头衔ID(system_titles.id)
|
||||
Active int32 `gorm:"column:active;not null;default:1;comment:是否激活:1激活 0停用" json:"active"` // 是否激活:1激活 0停用
|
||||
ObtainedAt time.Time `gorm:"column:obtained_at;not null;default:CURRENT_TIMESTAMP(3);comment:获得时间" json:"obtained_at"` // 获得时间
|
||||
ExpiresAt time.Time `gorm:"column:expires_at;comment:过期时间(可空)" json:"expires_at"` // 过期时间(可空)
|
||||
Source string `gorm:"column:source;comment:来源标识(活动、运营发放等)" json:"source"` // 来源标识(活动、运营发放等)
|
||||
Remark string `gorm:"column:remark;comment:备注信息" json:"remark"` // 备注信息
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
}
|
||||
|
||||
// TableName UserTitles's table name
|
||||
func (*UserTitles) TableName() string {
|
||||
return TableNameUserTitles
|
||||
}
|
||||
@ -11,9 +11,8 @@ import (
|
||||
"bindbox-game/internal/dblogger"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/pkg/startup"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/router/interceptor"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/router/interceptor"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -47,15 +46,14 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
||||
commonHandler := commonapi.New(logger, db)
|
||||
intc := interceptor.New(logger, db)
|
||||
|
||||
// 管理端非认证接口路由组
|
||||
adminNonAuthApiRouter := mux.Group("/api/admin")
|
||||
{
|
||||
adminNonAuthApiRouter.GET("/license/status", func(ctx core.Context) {
|
||||
ctx.Payload(startup.Info())
|
||||
})
|
||||
|
||||
adminNonAuthApiRouter.POST("/login", adminHandler.Login()) // 登录
|
||||
}
|
||||
// 管理端非认证接口路由组
|
||||
adminNonAuthApiRouter := mux.Group("/api/admin")
|
||||
{
|
||||
adminNonAuthApiRouter.POST("/login", adminHandler.Login()) // 登录
|
||||
// 开发便捷:无认证的初始化接口(仅用于快速配置,生产环境请关闭或加权限)
|
||||
adminNonAuthApiRouter.POST("/system_titles/seed_default", adminHandler.SeedDefaultTitles())
|
||||
adminNonAuthApiRouter.POST("/menu/ensure_titles", adminHandler.EnsureTitlesMenu())
|
||||
}
|
||||
|
||||
// 管理端认证接口路由组
|
||||
adminAuthApiRouter := mux.Group("/api/admin", core.WrapAuthHandler(intc.AdminTokenAuthVerify))
|
||||
@ -63,10 +61,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
||||
// 系统管理接口(为前端模板路径兼容,挂载到 /api)
|
||||
systemApiRouter := mux.Group("/api", core.WrapAuthHandler(intc.AdminTokenAuthVerify))
|
||||
{
|
||||
adminAuthApiRouter.POST("/create", adminHandler.CreateAdmin())
|
||||
adminAuthApiRouter.PUT("/:id", adminHandler.ModifyAdmin())
|
||||
adminAuthApiRouter.POST("/delete", adminHandler.DeleteAdmin())
|
||||
adminAuthApiRouter.GET("/list", adminHandler.PageList())
|
||||
// 管理员账号维护接口移除(未被前端使用)
|
||||
|
||||
adminAuthApiRouter.GET("/activity_categories", adminHandler.ListActivityCategories())
|
||||
adminAuthApiRouter.POST("/activities", adminHandler.CreateActivity())
|
||||
@ -127,6 +122,16 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
||||
adminAuthApiRouter.POST("/users/:user_id/rewards/grant", adminHandler.GrantReward())
|
||||
adminAuthApiRouter.GET("/users/:user_id/inventory", adminHandler.ListUserInventory())
|
||||
adminAuthApiRouter.GET("/users/:user_id/item_cards", adminHandler.ListUserItemCards())
|
||||
// 系统称号与分配
|
||||
adminAuthApiRouter.GET("/system_titles", adminHandler.ListSystemTitles())
|
||||
adminAuthApiRouter.POST("/users/:user_id/titles", adminHandler.AssignUserTitle())
|
||||
adminAuthApiRouter.POST("/system_titles", adminHandler.CreateSystemTitle())
|
||||
adminAuthApiRouter.PUT("/system_titles/:title_id", adminHandler.ModifySystemTitle())
|
||||
adminAuthApiRouter.DELETE("/system_titles/:title_id", adminHandler.DeleteSystemTitle())
|
||||
adminAuthApiRouter.GET("/system_titles/:title_id/effects", adminHandler.ListSystemTitleEffects())
|
||||
adminAuthApiRouter.POST("/system_titles/:title_id/effects", adminHandler.CreateSystemTitleEffect())
|
||||
adminAuthApiRouter.PUT("/system_titles/:title_id/effects/:effect_id", adminHandler.ModifySystemTitleEffect())
|
||||
adminAuthApiRouter.DELETE("/system_titles/:title_id/effects/:effect_id", adminHandler.DeleteSystemTitleEffect())
|
||||
|
||||
// 道具卡管理
|
||||
adminAuthApiRouter.POST("/system_item_cards", adminHandler.CreateSystemItemCard())
|
||||
@ -141,6 +146,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
||||
adminAuthApiRouter.POST("/users/:user_id/item_cards", adminHandler.AssignUserItemCard())
|
||||
// 通用上传
|
||||
systemApiRouter.POST("/common/upload/wangeditor", commonHandler.UploadWangEditorImage())
|
||||
systemApiRouter.POST("/menu/ensure_titles", adminHandler.EnsureTitlesMenu())
|
||||
}
|
||||
|
||||
// 系统管理:用户/角色/菜单
|
||||
|
||||
@ -34,6 +34,7 @@ type Service interface {
|
||||
GetIssueRandomCommitHistory(ctx context.Context, issueID int64) ([]*IssueRandomCommitment, error)
|
||||
|
||||
ExecuteDraw(ctx context.Context, issueID int64) (*Receipt, error)
|
||||
ExecuteDrawWithEffects(ctx context.Context, issueID int64, userID int64) (*Receipt, error)
|
||||
|
||||
GetCategoryNames(ctx context.Context, ids []int64) (map[int64]string, error)
|
||||
}
|
||||
|
||||
233
internal/service/activity/draw_with_effects.go
Normal file
233
internal/service/activity/draw_with_effects.go
Normal file
@ -0,0 +1,233 @@
|
||||
package activity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExecuteDrawWithEffects 执行抽奖(应用用户头衔效果:概率加成/双倍奖励)
|
||||
// Params:
|
||||
// - ctx: 上下文
|
||||
// - issueID: 期ID
|
||||
// - userID: 用户ID
|
||||
// Returns:
|
||||
// - 抽奖收据(已按效果调整权重并处理双倍奖励)
|
||||
func (s *service) ExecuteDrawWithEffects(ctx context.Context, issueID int64, userID int64) (*Receipt, error) {
|
||||
cm, err := s.GetIssueRandomCommit(ctx, issueID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cm == nil {
|
||||
return nil, nil
|
||||
}
|
||||
master := unmaskSeed(cm.ServerSeedMaster, cm.IssueID, cm.StateVersion)
|
||||
items, err := s.readDB.ActivityRewardSettings.WithContext(ctx).
|
||||
Where(s.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).
|
||||
Order(s.readDB.ActivityRewardSettings.Sort).
|
||||
Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 初始权重与快照
|
||||
var snapshot []ReceiptItem
|
||||
baseWeights := make(map[int64]int32, len(items))
|
||||
for _, it := range items {
|
||||
snapshot = append(snapshot, ReceiptItem{ID: it.ID, Name: it.Name, Weight: it.Weight, QuantityBefore: it.Quantity})
|
||||
if it.Weight > 0 && (it.Quantity == -1 || it.Quantity > 0) {
|
||||
baseWeights[it.ID] = it.Weight
|
||||
}
|
||||
}
|
||||
// 解析用户头衔效果(仅过滤到当前issue)
|
||||
effects, err := s.readDB.SystemTitleEffects.WithContext(ctx).
|
||||
Where(s.readDB.SystemTitleEffects.Status.Eq(1)).
|
||||
Order(s.readDB.SystemTitleEffects.Sort).
|
||||
Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 仅保留用户激活的头衔效果
|
||||
now := time.Now()
|
||||
uts, err := s.readDB.UserTitles.WithContext(ctx).
|
||||
Where(s.readDB.UserTitles.UserID.Eq(userID)).
|
||||
Where(s.readDB.UserTitles.Active.Eq(1)).
|
||||
Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
titleSet := make(map[int64]struct{}, len(uts))
|
||||
for _, ut := range uts {
|
||||
if ut.ExpiresAt.IsZero() || ut.ExpiresAt.After(now) {
|
||||
titleSet[ut.TitleID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// 作用域过滤:issueID
|
||||
type scopePayload struct {
|
||||
IssueIDs []int64 `json:"issue_ids"`
|
||||
Exclude struct{ IssueIDs []int64 `json:"issue_ids"` } `json:"exclude"`
|
||||
}
|
||||
|
||||
// 累计概率加成与双倍奖励参数
|
||||
boostPerItemX1000 := make(map[int64]int32)
|
||||
var doubleChanceX1000 int32
|
||||
var doubleTargets map[int64]struct{}
|
||||
|
||||
for _, ef := range effects {
|
||||
if _, ok := titleSet[ef.TitleID]; !ok {
|
||||
continue
|
||||
}
|
||||
// scope过滤
|
||||
if ef.ScopesJSON != "" {
|
||||
var sc scopePayload
|
||||
if err := json.Unmarshal([]byte(ef.ScopesJSON), &sc); err == nil {
|
||||
if len(sc.Exclude.IssueIDs) > 0 {
|
||||
for _, ex := range sc.Exclude.IssueIDs {
|
||||
if ex == issueID {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(sc.IssueIDs) > 0 {
|
||||
matched := false
|
||||
for _, id := range sc.IssueIDs {
|
||||
if id == issueID { matched = true; break }
|
||||
}
|
||||
if !matched { continue }
|
||||
}
|
||||
}
|
||||
}
|
||||
switch ef.EffectType {
|
||||
case 5: // 概率加成
|
||||
var p struct {
|
||||
TargetPrizeIDs []int64 `json:"target_prize_ids"`
|
||||
BoostX1000 int32 `json:"boost_x1000"`
|
||||
CapX1000 *int32 `json:"cap_x1000"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(ef.ParamsJSON), &p); err != nil {
|
||||
continue
|
||||
}
|
||||
// 累加或取最大(1累加封顶,0最大值,2首个匹配)
|
||||
for _, tid := range p.TargetPrizeIDs {
|
||||
curr := boostPerItemX1000[tid]
|
||||
switch ef.StackingStrategy {
|
||||
case 0: // max_only
|
||||
if p.BoostX1000 > curr { boostPerItemX1000[tid] = p.BoostX1000 }
|
||||
case 1: // sum_with_cap
|
||||
nxt := curr + p.BoostX1000
|
||||
if p.CapX1000 != nil && nxt > *p.CapX1000 { nxt = *p.CapX1000 }
|
||||
boostPerItemX1000[tid] = nxt
|
||||
case 2: // first_match
|
||||
if curr == 0 { boostPerItemX1000[tid] = p.BoostX1000 }
|
||||
default:
|
||||
nxt := curr + p.BoostX1000
|
||||
cap := ef.CapValueX1000
|
||||
if cap > 0 && nxt > cap { nxt = cap }
|
||||
boostPerItemX1000[tid] = nxt
|
||||
}
|
||||
}
|
||||
case 6: // 双倍奖励卡
|
||||
var p struct {
|
||||
TargetPrizeIDs []int64 `json:"target_prize_ids"`
|
||||
ChanceX1000 int32 `json:"chance_x1000"`
|
||||
PeriodCapTimes *int32 `json:"period_cap_times"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(ef.ParamsJSON), &p); err != nil {
|
||||
continue
|
||||
}
|
||||
// 合并双倍奖励命中率(sum_with_cap 缺省)
|
||||
if doubleTargets == nil { doubleTargets = make(map[int64]struct{}) }
|
||||
for _, tid := range p.TargetPrizeIDs { doubleTargets[tid] = struct{}{} }
|
||||
// 累加并封顶
|
||||
doubleChanceX1000 += p.ChanceX1000
|
||||
cap := ef.CapValueX1000
|
||||
if cap > 0 && doubleChanceX1000 > cap { doubleChanceX1000 = cap }
|
||||
}
|
||||
}
|
||||
|
||||
// 应用概率加成:调整权重
|
||||
var total int64
|
||||
adjWeights := make(map[int64]int32, len(baseWeights))
|
||||
for _, it := range items {
|
||||
if it.Weight <= 0 || !(it.Quantity == -1 || it.Quantity > 0) { continue }
|
||||
w := baseWeights[it.ID]
|
||||
if inc, ok := boostPerItemX1000[it.ID]; ok && inc > 0 {
|
||||
// w * (1 + inc/1000)
|
||||
w = int32(int64(w) * (1000 + int64(inc)) / 1000)
|
||||
if w < 1 { w = 1 }
|
||||
}
|
||||
adjWeights[it.ID] = w
|
||||
total += int64(w)
|
||||
}
|
||||
if total <= 0 { return nil, nil }
|
||||
|
||||
// 随机选择(按调整后权重)
|
||||
drawId := time.Now().UnixNano()
|
||||
clientSeed := make([]byte, 32)
|
||||
_, _ = rand.Read(clientSeed)
|
||||
nonce := uint64(1)
|
||||
subInput := make([]byte, 16)
|
||||
binary.BigEndian.PutUint64(subInput[:8], uint64(issueID))
|
||||
binary.BigEndian.PutUint64(subInput[8:16], uint64(drawId))
|
||||
mac := hmac.New(sha256.New, master)
|
||||
mac.Write(subInput)
|
||||
serverSubSeed := mac.Sum(nil)
|
||||
enc := encodeMessage(cm.AlgoVersion, issueID, drawId, 0, clientSeed, nonce, cm.ItemsRoot[:], uint64(total))
|
||||
entropy := hmacSha256(serverSubSeed, enc)
|
||||
pos, proof := rejectSample(entropy, serverSubSeed, enc, uint64(total))
|
||||
var acc uint64
|
||||
var selIndex int
|
||||
var selID int64
|
||||
var iIdx int
|
||||
for i, it := range items {
|
||||
if it.Weight <= 0 || !(it.Quantity == -1 || it.Quantity > 0) { continue }
|
||||
w := uint64(adjWeights[it.ID])
|
||||
if pos < acc+w {
|
||||
selIndex = i
|
||||
selID = it.ID
|
||||
iIdx = i
|
||||
break
|
||||
}
|
||||
acc += w
|
||||
}
|
||||
// 双倍奖励判定(若目标命中)
|
||||
rewardMultiplierX1000 := int32(1000)
|
||||
if selID > 0 && doubleChanceX1000 > 0 {
|
||||
_, eligible := doubleTargets[selID]
|
||||
if eligible {
|
||||
// 使用另一次哈希派生随机判定
|
||||
check := hmacSha256(serverSubSeed, []byte("double:"))
|
||||
rv := int32(binary.BigEndian.Uint32(check[:4]) % 1000)
|
||||
if rv < doubleChanceX1000 {
|
||||
rewardMultiplierX1000 = 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
rec := &Receipt{
|
||||
AlgoVersion: cm.AlgoVersion,
|
||||
RoundId: issueID,
|
||||
DrawId: drawId,
|
||||
ClientId: 0,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
ServerSeedHash: cm.ServerSeedHash[:],
|
||||
ServerSubSeed: serverSubSeed,
|
||||
ClientSeed: clientSeed,
|
||||
Nonce: nonce,
|
||||
Items: snapshot,
|
||||
ItemsRoot: cm.ItemsRoot[:],
|
||||
WeightsTotal: uint64(total),
|
||||
SelectedIndex: selIndex,
|
||||
SelectedItemId: selID,
|
||||
RandProof: proof,
|
||||
Signature: nil,
|
||||
}
|
||||
// 将倍数编码回选中项的名称后缀以便上层识别(非侵入式)
|
||||
if iIdx >= 0 && iIdx < len(rec.Items) && rewardMultiplierX1000 > 1000 {
|
||||
rec.Items[iIdx].Name = rec.Items[iIdx].Name + "(x2)"
|
||||
}
|
||||
return rec, nil
|
||||
}
|
||||
177
internal/service/title/effects_resolver.go
Normal file
177
internal/service/title/effects_resolver.go
Normal file
@ -0,0 +1,177 @@
|
||||
package title
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
// Service 提供头衔效果解析与领取型权益限流的服务
|
||||
// Params:
|
||||
// - ctx: 上下文
|
||||
// - 依赖通过构造函数注入
|
||||
// Returns:
|
||||
// - 头衔相关功能的服务实例
|
||||
type Service interface {
|
||||
// ResolveActiveEffects 解析用户在指定上下文(issue/activity/category)下的激活头衔效果
|
||||
// Params:
|
||||
// - ctx: 上下文
|
||||
// - userID: 用户ID
|
||||
// - scope: 事件作用域(可空字段用于精细过滤)
|
||||
// Returns:
|
||||
// - 解析后的效果列表(已按scopes过滤并仅保留启用效果)
|
||||
ResolveActiveEffects(ctx context.Context, userID int64, scope EffectScope) ([]*model.SystemTitleEffects, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger logger.CustomLogger
|
||||
readDB *dao.Query
|
||||
writeDB *dao.Query
|
||||
}
|
||||
|
||||
// New 创建头衔服务实例
|
||||
// Params:
|
||||
// - l: 日志器
|
||||
// - db: 数据库仓库
|
||||
// Returns:
|
||||
// - 头衔服务实例
|
||||
func New(l logger.CustomLogger, db mysql.Repo) Service {
|
||||
return &service{
|
||||
logger: l,
|
||||
readDB: dao.Use(db.GetDbR()),
|
||||
writeDB: dao.Use(db.GetDbW()),
|
||||
}
|
||||
}
|
||||
|
||||
// EffectScope 事件上下文作用域
|
||||
// 用于按活动/期/分类过滤效果生效范围
|
||||
type EffectScope struct {
|
||||
ActivityID *int64
|
||||
IssueID *int64
|
||||
ActivityCategory *int64
|
||||
}
|
||||
|
||||
// scopesPayload 用于解析SystemTitleEffects.ScopesJSON
|
||||
type scopesPayload struct {
|
||||
ActivityIDs []int64 `json:"activity_ids"`
|
||||
IssueIDs []int64 `json:"issue_ids"`
|
||||
CategoryIDs []int64 `json:"category_ids"`
|
||||
Exclude struct {
|
||||
ActivityIDs []int64 `json:"activity_ids"`
|
||||
IssueIDs []int64 `json:"issue_ids"`
|
||||
CategoryIDs []int64 `json:"category_ids"`
|
||||
} `json:"exclude"`
|
||||
}
|
||||
|
||||
// ResolveActiveEffects 查询用户激活头衔并解析效果,返回在给定scope内生效的效果
|
||||
// Params:
|
||||
// - ctx: 上下文
|
||||
// - userID: 用户ID
|
||||
// - scope: 事件作用域
|
||||
// Returns:
|
||||
// - 生效效果列表
|
||||
func (s *service) ResolveActiveEffects(ctx context.Context, userID int64, scope EffectScope) ([]*model.SystemTitleEffects, error) {
|
||||
now := time.Now()
|
||||
titles, err := s.readDB.UserTitles.WithContext(ctx).
|
||||
Where(s.readDB.UserTitles.UserID.Eq(userID)).
|
||||
Where(s.readDB.UserTitles.Active.Eq(1)).
|
||||
Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(titles) == 0 {
|
||||
return []*model.SystemTitleEffects{}, nil
|
||||
}
|
||||
var selID int64
|
||||
var selAt time.Time
|
||||
for _, ut := range titles {
|
||||
if ut.ExpiresAt.IsZero() || ut.ExpiresAt.After(now) {
|
||||
if selID == 0 || ut.ObtainedAt.After(selAt) {
|
||||
selID = ut.TitleID
|
||||
selAt = ut.ObtainedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
if selID == 0 {
|
||||
return []*model.SystemTitleEffects{}, nil
|
||||
}
|
||||
effects, err := s.readDB.SystemTitleEffects.WithContext(ctx).
|
||||
Where(s.readDB.SystemTitleEffects.TitleID.Eq(selID)).
|
||||
Where(s.readDB.SystemTitleEffects.Status.Eq(1)).
|
||||
Order(s.readDB.SystemTitleEffects.Sort).
|
||||
Find()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(effects) == 0 {
|
||||
return []*model.SystemTitleEffects{}, nil
|
||||
}
|
||||
var result []*model.SystemTitleEffects
|
||||
for _, ef := range effects {
|
||||
if ef.ScopesJSON == "" {
|
||||
result = append(result, ef)
|
||||
continue
|
||||
}
|
||||
var sc scopesPayload
|
||||
if err := json.Unmarshal([]byte(ef.ScopesJSON), &sc); err != nil {
|
||||
// 解析失败时按“全局生效”处理
|
||||
result = append(result, ef)
|
||||
continue
|
||||
}
|
||||
if !scopeMatch(scope, sc) {
|
||||
continue
|
||||
}
|
||||
result = append(result, ef)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// scopeMatch 判断效果scope是否命中当前事件上下文
|
||||
// Params:
|
||||
// - scope: 事件作用域
|
||||
// - sc: 效果配置的scope载荷
|
||||
// Returns:
|
||||
// - 是否命中
|
||||
func scopeMatch(scope EffectScope, sc scopesPayload) bool {
|
||||
// 排除优先
|
||||
if scope.ActivityID != nil && containsInt64(sc.Exclude.ActivityIDs, *scope.ActivityID) {
|
||||
return false
|
||||
}
|
||||
if scope.IssueID != nil && containsInt64(sc.Exclude.IssueIDs, *scope.IssueID) {
|
||||
return false
|
||||
}
|
||||
if scope.ActivityCategory != nil && containsInt64(sc.Exclude.CategoryIDs, *scope.ActivityCategory) {
|
||||
return false
|
||||
}
|
||||
// 包含判定(未配置即视为全局)
|
||||
if scope.ActivityID != nil && len(sc.ActivityIDs) > 0 && !containsInt64(sc.ActivityIDs, *scope.ActivityID) {
|
||||
return false
|
||||
}
|
||||
if scope.IssueID != nil && len(sc.IssueIDs) > 0 && !containsInt64(sc.IssueIDs, *scope.IssueID) {
|
||||
return false
|
||||
}
|
||||
if scope.ActivityCategory != nil && len(sc.CategoryIDs) > 0 && !containsInt64(sc.CategoryIDs, *scope.ActivityCategory) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// containsInt64 判断切片是否包含指定值
|
||||
// Params:
|
||||
// - arr: 切片
|
||||
// - v: 值
|
||||
// Returns:
|
||||
// - 是否包含
|
||||
func containsInt64(arr []int64, v int64) bool {
|
||||
for _, x := range arr {
|
||||
if x == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -7,3 +7,5 @@
|
||||
{"level":"fatal","time":"2025-11-15 12:26:06","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
|
||||
{"level":"fatal","time":"2025-11-15 12:28:06","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
|
||||
{"level":"fatal","time":"2025-11-15 13:05:26","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
|
||||
{"level":"fatal","time":"2025-11-15 23:04:52","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
|
||||
{"level":"fatal","time":"2025-11-16 00:14:50","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user