Compare commits
No commits in common. "c9a83a232a68e04c8648f2249e4a1a67a33c81f7" and "642b3cf7dd4efaefb5952bbab35333dcd82a7ee6" have entirely different histories.
c9a83a232a
...
642b3cf7dd
4
.gitignore
vendored
4
.gitignore
vendored
@ -24,6 +24,4 @@ go.work.sum
|
||||
|
||||
*.idea
|
||||
|
||||
resources/*
|
||||
build/resources/admin/
|
||||
logs/
|
||||
resources/*
|
||||
@ -1,38 +0,0 @@
|
||||
# Add Cost Analysis to Lottery Simulation
|
||||
|
||||
I will enhance the lottery simulation feature to include cost calculation and financial analysis.
|
||||
|
||||
## 1. Backend Changes (`internal/api/admin/lottery_admin.go`)
|
||||
|
||||
- **Update Response Structures**:
|
||||
- Add `Cost` (int64) to `simulateRewardStat` (Unit cost in cents).
|
||||
- Add `TotalCost` (int64) to `simulateRewardStat` (WonCount * Cost).
|
||||
- Add `TotalSimulationCost` (int64) to `simulateIssueResponse`.
|
||||
- Add `TotalSimulationRevenue` (int64) to `simulateIssueResponse` (TotalDraws * Activity.PriceDraw).
|
||||
- Add `GrossProfit` (int64) to `simulateIssueResponse` (Revenue - Cost).
|
||||
- Add `GrossProfitRate` (float64) to `simulateIssueResponse`.
|
||||
|
||||
- **Logic Update**:
|
||||
- In `SimulateIssue`:
|
||||
- Fetch the `Activity` details to get `PriceDraw`.
|
||||
- Collect all `ProductID`s from the rewards.
|
||||
- Batch query the `Products` table to get prices.
|
||||
- Map product prices to rewards (if `ProductID > 0`).
|
||||
- Calculate financial stats after the simulation loop.
|
||||
|
||||
## 2. Frontend Changes (`web/admin/src/views/operations/lottery-simulation/index.vue`)
|
||||
|
||||
- **Update API Type Definition**: Update `SimulateRewardStat` and `SimulateIssueResponse` interfaces in `api/operations.ts`.
|
||||
- **UI Enhancement**:
|
||||
- **Summary Cards**: Add a row of summary cards at the top of the results section showing:
|
||||
- Total Revenue (总收入)
|
||||
- Total Cost (总成本)
|
||||
- Gross Profit (毛利润)
|
||||
- Profit Margin (毛利率)
|
||||
- **Table Columns**:
|
||||
- Add "Unit Cost" (单价/成本) column.
|
||||
- Add "Total Cost" (总发放成本) column.
|
||||
|
||||
## 3. Verification
|
||||
- Verify compilation.
|
||||
- (Self-Correction) Ensure `Price` is handled as integer (cents) and formatted correctly in frontend (divided by 100).
|
||||
@ -1,31 +0,0 @@
|
||||
# Add Product Description Field for App Detail
|
||||
|
||||
## 1. Database Migration
|
||||
We need to add a `description` column to the `products` table to store the product details.
|
||||
**SQL Statement:**
|
||||
```sql
|
||||
ALTER TABLE products ADD COLUMN description TEXT COMMENT '商品详情';
|
||||
```
|
||||
|
||||
## 2. Backend Model Update
|
||||
Update the GORM model and DAO to reflect the database change.
|
||||
* **Model**: Update `internal/repository/mysql/model/products.gen.go` to include the `Description` field.
|
||||
* **DAO**: Update `internal/repository/mysql/dao/products.gen.go` to include the `Description` field definition and mapping.
|
||||
|
||||
## 3. Service Layer Update
|
||||
Update `internal/service/product/product.go` to handle the new field.
|
||||
* **Input Structs**: Add `Description` to `CreateProductInput` and `ModifyProductInput`.
|
||||
* **Business Logic**:
|
||||
* Update `CreateProduct` to save the description.
|
||||
* Update `ModifyProduct` to update the description.
|
||||
* Update `GetDetailForApp` to populate the `Description` field from the database instead of returning an empty string.
|
||||
|
||||
## 4. Admin API Update
|
||||
Update the Admin API to allow creating and editing products with a description.
|
||||
* **File**: `internal/api/admin/product_create.go`
|
||||
* **Request Structs**: Add `description` field to `createProductRequest` and `modifyProductRequest`.
|
||||
* **Handlers**: Pass the `description` from the request to the service layer.
|
||||
|
||||
## 5. App API Verification
|
||||
* **File**: `internal/api/app/product.go`
|
||||
* The `GetProductDetailForApp` function already returns a `Description` field in the response. By updating the service layer to populate this field, the App API will automatically return the correct data.
|
||||
@ -1,38 +0,0 @@
|
||||
# 渠道管理与用户来源统计实施计划
|
||||
|
||||
针对您的需求,我将把现有的“小程序二维码生成”功能升级为完整的“渠道管理”系统,并增加用户来源统计。
|
||||
|
||||
## 1. 数据库变更 (Schema Changes)
|
||||
|
||||
* **新增 `channels` 表**:
|
||||
* 用于定义推广渠道。
|
||||
* 字段: `id` (主键), `name` (渠道名称), `code` (唯一标识, 用于二维码参数), `type` (类型: 如抖音/微信/线下), `remarks` (备注), `created_at`, `updated_at`。
|
||||
* **修改 `users` 表**:
|
||||
* 新增 `channel_id` (BIGINT) 字段,建立与 `channels` 表的关联。
|
||||
* (注意:现有 `douyin_id` 字段保留,用于业务逻辑,但统计归因将统一使用 `channel_id`)。
|
||||
|
||||
## 2. 后端开发 (Backend)
|
||||
|
||||
* **API 开发 (`internal/api/admin`)**:
|
||||
* 实现渠道管理的 CRUD 接口:`List`, `Create`, `Update`, `Delete`。
|
||||
* **统计逻辑**: 在 `List` 接口中,同步查询 `users` 表,统计每个 `channel_id` 下的用户数量。
|
||||
* **登录/注册逻辑优化 (`internal/service/user`)**:
|
||||
* 修改 `LoginWeixin` (微信登录) 逻辑。
|
||||
* 除了现有的 `invite_code` 和 `douyin_id`,新增支持 `channel_code` 参数。
|
||||
* 逻辑: 当用户注册时,如果检测到 `channel_code`,查找对应的 `Channel` 记录,并将 `channel.ID` 写入用户表的 `channel_id` 字段。
|
||||
|
||||
## 3. 前端开发 (Frontend)
|
||||
|
||||
* **新增页面**: `运营管理` -> `渠道管理` (`web/admin/src/views/operations/channels/index.vue`)。
|
||||
* **列表页**: 展示渠道名称、唯一标识(Code)、**累计注册用户数**、创建时间。
|
||||
* **操作栏**: 提供“编辑”、“删除”以及 **“查看二维码”** 功能。
|
||||
* **二维码生成优化**:
|
||||
* 点击“查看二维码”时,自动调用生成接口,参数中自动带上当前渠道的 `code`。
|
||||
* (替代原有的纯手动输入二维码生成页面,或将其保留为“自定义工具”)。
|
||||
|
||||
## 4. 验证计划
|
||||
|
||||
1. **功能验证**: 在后台创建一个测试渠道,生成二维码。
|
||||
2. **流程验证**: 模拟新用户携带该渠道参数登录。
|
||||
3. **数据验证**: 检查数据库 `users` 表中新用户的 `channel_id` 是否正确。
|
||||
4. **统计验证**: 刷新渠道管理页面,确认“注册用户数”是否+1。
|
||||
@ -1,36 +0,0 @@
|
||||
# 修复一番赏策略库存扣减竞态问题
|
||||
|
||||
经检查,`internal/service/activity/strategy/ichiban.go` 中的 `GrantReward` 方法存在严重的竞态条件风险。当前实现采用“先查询库存,再更新库存”的方式,在高并发下可能导致超卖。
|
||||
|
||||
## 修复计划
|
||||
|
||||
我将参照 `default.go` 和 `reward_grant.go` 中的正确实现,对 `ichiban.go` 进行以下修改:
|
||||
|
||||
1. **重构 `GrantReward` 方法**
|
||||
* 移除原有的 `First()` 查询逻辑。
|
||||
* 改为使用**乐观锁**原子更新:
|
||||
* 查询条件增加 `Quantity > 0`。
|
||||
* 使用 `UpdateSimple` 执行 `Quantity - 1`。
|
||||
* 根据 `RowsAffected` 判断扣减是否成功。
|
||||
|
||||
## 预期代码变更
|
||||
|
||||
```go
|
||||
func (s *ichibanStrategy) GrantReward(ctx context.Context, userID int64, rewardID int64) error {
|
||||
// 使用乐观锁原子扣减库存
|
||||
result, err := s.write.ActivityRewardSettings.WithContext(ctx).Where(
|
||||
s.write.ActivityRewardSettings.ID.Eq(rewardID),
|
||||
s.write.ActivityRewardSettings.Quantity.Gt(0),
|
||||
).UpdateSimple(s.write.ActivityRewardSettings.Quantity.Add(-1))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("sold out or reward not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
此修改将彻底解决库存扣减的并发安全问题。
|
||||
@ -1,73 +0,0 @@
|
||||
## 概述
|
||||
为 APP 端提供“用户背包”能力,覆盖用户资产的六类数据:积分消费明细、优惠券、道具卡、头衔、订单、中奖资产(背包)。在保留现有分项查询接口的基础上,新增缺失的 APP 端接口与一个聚合概要接口,统一鉴权与分页规范,并更新 API 文档。
|
||||
|
||||
## 新增接口
|
||||
- GET `/api/app/users/{user_id}/inventory`
|
||||
- 功能:用户背包列表(中奖资产),按时间倒序,支持分页
|
||||
- 返回:`InventoryWithProduct` 列表(含商品名称、图片、等级/备注、关联活动/奖励ID)
|
||||
- 鉴权:`LoginVerifyToken`
|
||||
- GET `/api/app/users/{user_id}/titles`
|
||||
- 功能:用户头衔列表,支持分页
|
||||
- 返回:`UserTitleItem` 列表(称号ID、名称、来源/获得时间、可选效果)
|
||||
- 鉴权:`LoginVerifyToken`
|
||||
- GET `/api/app/users/{user_id}/backpack`
|
||||
- 功能:聚合概要接口,返回六类资产的“计数+最近N条”,用于首页/概览展示
|
||||
- 查询参数:`limit=5`(各类最近条数),`include=points,coupons,item_cards,titles,orders,inventory`(可选,默认全包含)
|
||||
- 返回:
|
||||
- `points: {count, recent: UserPointsLedger[]}`
|
||||
- `coupons: {count, recent: CouponItem[]}`
|
||||
- `item_cards: {count, recent: ItemCardWithTemplate[]}`
|
||||
- `titles: {count, recent: UserTitleItem[]}`
|
||||
- `orders: {count, recent: Orders[]}`
|
||||
- `inventory: {count, recent: InventoryWithProduct[]}`
|
||||
- 鉴权:`LoginVerifyToken`
|
||||
|
||||
## 复用现有 APP 接口(保持不变)
|
||||
- 积分明细:GET `/api/app/users/{user_id}/points`(已存在)
|
||||
- 积分余额:GET `/api/app/users/{user_id}/points/balance`(已存在)
|
||||
- 优惠券:GET `/api/app/users/{user_id}/coupons`(已存在)
|
||||
- 道具卡:GET `/api/app/users/{user_id}/item_cards`、GET `/api/app/users/{user_id}/item_cards/uses`(已存在)
|
||||
- 订单:GET `/api/app/users/{user_id}/orders`(已存在)
|
||||
|
||||
## 数据模型(响应结构)
|
||||
- `InventoryWithProduct`:`{ id, product_id, title, images[], activity_id, reward_id, order_id, level, created_at }`
|
||||
- `UserTitleItem`:`{ user_title_id, title_id, title_name, acquired_at, effects?: [{key, value, unit}], expires_at? }`
|
||||
- `CouponItem`(沿用现有):`{ id, name, amount, valid_start, valid_end, status, rules }`
|
||||
- `ItemCardWithTemplate`(沿用现有):含模板字段与剩余次数/有效期
|
||||
- `Orders`(沿用现有):按现有模型返回
|
||||
- `UserPointsLedger`(沿用现有):按现有模型返回
|
||||
|
||||
## 技术实现
|
||||
- 目录与文件:
|
||||
- `internal/api/user/inventory_app.go`:`ListUserInventoryForApp()`(调用 `usersvc.ListInventoryWithProduct`)
|
||||
- `internal/api/user/titles_app.go`:`ListUserTitlesForApp()`(新增 usersvc 方法或复用 admin 服务逻辑)
|
||||
- `internal/api/user/backpack_app.go`:`GetUserBackpackOverview()`(各服务查询计数与最近N条)
|
||||
- 路由:`internal/router/router.go`
|
||||
- APP 认证组注册:`/users/:user_id/inventory`、`/users/:user_id/titles`、`/users/:user_id/backpack`
|
||||
- Service 层:`internal/service/user`
|
||||
- `ListInventoryWithProduct(ctx, userID, page, pageSize)`(已有)
|
||||
- `ListUserTitles(ctx, userID, page, pageSize)`(新增):查询用户称号关联与系统称号名称/效果
|
||||
- 计数方法:各类 `CountXxx(ctx, userID)`(供概要接口聚合)
|
||||
|
||||
## 鉴权与约束
|
||||
- 所有接口使用 `LoginVerifyToken`
|
||||
- `user_id` 为路径参数,但服务端以会话 `SessionUserInfo().Id` 校验一致性,防越权
|
||||
- 分页统一:`page`、`page_size`(默认 1/20,最大 100),按 `created_at` 倒序
|
||||
|
||||
## Swagger 文档
|
||||
- 为新增 3 个接口添加注解:`@Summary`、`@Description`、`@Tags APP端.用户`、`@Security LoginVerifyToken`、`@Param`、`@Success`/`@Failure`、`@Router`
|
||||
- 模型定义共用已有结构;为新结构 `UserTitleItem`、`InventoryWithProduct` 增加定义
|
||||
- 执行脚本生成文档:`scripts/swagger.sh`
|
||||
|
||||
## 测试与验收
|
||||
- 单元/集成:
|
||||
- 伪造用户会话,验证分页与鉴权;数据空集场景返回空数组
|
||||
- 概要接口在 `include` 过滤时仅返回所选分组
|
||||
- 手工验证:
|
||||
- 分别对六类接口 `curl` 测试分页与计数一致性
|
||||
- Swagger 文档能正确展示并可试用
|
||||
|
||||
## 验收标准
|
||||
- 三个新增接口可用且鉴权正确;分页与计数正确
|
||||
- 概要接口耗时可控(<200ms 在百条内数据量);可通过 `include` 控制
|
||||
- 文档完整,前端对接字段明确;无越权与泄露
|
||||
@ -1,53 +0,0 @@
|
||||
# 添加抽奖模拟功能 (Lottery Simulation)
|
||||
|
||||
我将实现一个抽奖模拟功能,允许管理员在后台模拟抽奖过程并分析概率分布,**全过程仅在内存中进行,不会修改数据库中的真实数据**。
|
||||
|
||||
## 1. API 接口设计
|
||||
**接口地址**: `POST /api/admin/lottery/issues/:issue_id/simulate`
|
||||
|
||||
**请求参数 (Body)**:
|
||||
```json
|
||||
{
|
||||
"num_users": 100, // 模拟人数
|
||||
"draws_per_user": 1 // 每人抽奖次数 (总抽奖次数 = 人数 * 次数)
|
||||
}
|
||||
```
|
||||
|
||||
**返回结果**:
|
||||
```json
|
||||
{
|
||||
"total_draws": 100, // 总模拟次数
|
||||
"rewards": [
|
||||
{
|
||||
"reward_id": 1,
|
||||
"name": "IPhone 15",
|
||||
"level": 1, // 奖品等级
|
||||
"original_qty": 10, // 初始库存
|
||||
"won_count": 2, // 模拟中奖数
|
||||
"remaining_qty": 8,// 模拟剩余库存
|
||||
"actual_prob": 0.02, // 实际中奖率 (2%)
|
||||
"theoretical_prob": 0.01 // 理论概率 (基于权重的 1%)
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 实现细节
|
||||
|
||||
### A. 新增处理器 `internal/api/admin/lottery_admin.go`
|
||||
我将添加 `SimulateIssue` 函数,执行以下逻辑:
|
||||
1. **读取配置**: 从数据库获取指定期号 (Issue) 和奖品配置 (Reward Settings)。
|
||||
2. **内存模拟**:
|
||||
- 在内存中创建奖品库存的副本。
|
||||
- 运行加权随机算法 `N` 次(总抽奖次数)。
|
||||
- 扣减内存中的库存并记录中奖数据。
|
||||
- **关键**: 绝对不会写入数据库或修改真实库存。
|
||||
3. **统计计算**:
|
||||
- 计算每个奖品的实际中奖率与基于权重的理论概率进行对比。
|
||||
|
||||
### B. 注册路由 `internal/router/router.go`
|
||||
- 在 `adminAuthApiRouter` 分组下添加新路由。
|
||||
- `POST /lottery/issues/:issue_id/simulate` -> `adminHandler.SimulateIssue()`
|
||||
|
||||
## 3. 验证计划
|
||||
- **功能验证**: 使用现有的活动/期号调用模拟接口,输入不同的人数和次数,验证返回的概率分布是否符合预期(例如:高权重奖品中奖率更高,库存耗尽后不再中奖)。
|
||||
@ -1,27 +0,0 @@
|
||||
# 故障排查与修复计划
|
||||
|
||||
针对您提出的四个问题,我制定了以下修复计划。我将使用中文进行回答和执行。
|
||||
|
||||
## 1. 虚拟发货问题 (Virtual Shipment)
|
||||
* **现状**: 后端代码中已存在 `UploadVirtualShipping` 调用,但在支付回调中如果上传失败,目前是忽略错误的(`_ = ...`),导致无法确定是否成功。
|
||||
* **修复**: 修改 `internal/api/pay/wechat_notify.go`,增加错误日志记录。如果虚拟发货上传失败,将在后台日志中打印详细错误原因,方便排查(如 `transaction_id` 错误或配置问题)。
|
||||
|
||||
## 2. 订单管理与退款 (Refund Button)
|
||||
* **现状**: 您提到的“履约”可能是指发货,但对于虚拟商品,通常需要的是“退款”。目前后台订单列表缺少直观的“退款”按钮。
|
||||
* **修复**:
|
||||
* **前端**: 在 `web/admin/src/views/pay/orders/index.vue` 中添加“退款”按钮。
|
||||
* **后端**: 对接现有的 `CreateRefund` 接口。该接口已包含完整的逻辑:调用微信退款、更新订单状态、**自动恢复用户积分**和**自动恢复优惠券**。
|
||||
|
||||
## 3. 道具卡添加不生效 (Item Card Issue)
|
||||
* **原因**: 经查代码 `internal/service/user/item_card_add.go`,当道具卡模板不存在或状态为“未启用”时,代码直接返回 `nil` (成功),但实际上没有给用户添加卡片。这就是“不生效”且无报错的原因。
|
||||
* **修复**: 修改服务层代码,当道具卡不可用时,明确返回错误(如 `errors.New("item card not found or disabled")`),让接口报错,提示您具体原因。
|
||||
|
||||
## 4. 优惠券添加不生效 (Coupon Issue)
|
||||
* **原因**: 同上,`internal/service/user/coupon_add.go` 在优惠券模板未启用时也静默返回成功。
|
||||
* **修复**: 修改服务层代码,当优惠券不可用时,明确返回错误,确保您能看到操作失败的提示。
|
||||
|
||||
## 执行步骤
|
||||
1. **后端**: 修复道具卡和优惠券的“静默失败”逻辑,使其报错。
|
||||
2. **后端**: 在支付回调中增加虚拟发货的错误日志。
|
||||
3. **前端**: 在订单管理页面增加“退款”按钮并对接接口。
|
||||
4. **验证**: 通过 curl 或界面操作验证修复效果。
|
||||
@ -1,54 +0,0 @@
|
||||
# 一番赏活动创建表单优化计划
|
||||
|
||||
用户反馈在创建一番赏活动时,选择即时/定时开奖的页面逻辑不对。通过分析代码 `web/admin/src/views/activity/wizard/index.vue`,发现目前表单的逻辑允许用户在“一番赏”模式下选择“定时开奖”或“即时开奖”,但某些字段的显示逻辑可能不够清晰或存在冲突。一番赏(Ichiban Kuji)通常有其特定的开奖规则,需要对表单进行以下优化。
|
||||
|
||||
## 1. 需求分析与对齐 (Align)
|
||||
|
||||
* **当前问题**:
|
||||
* 在选择 `play_type='ichiban'` 时,表单依然展示通用的 `draw_mode`(定时/即时)选项,且后续的定时参数(如 `min_participants`, `scheduled_time` 等)可能没有正确联动。
|
||||
* 一番赏的核心逻辑是“即买即开”(即时开奖),但也可能存在“定时开奖”的特殊玩法(如凑够多少人开一箱)。需要明确一番赏是否支持所有开奖模式。
|
||||
* **假设**: 用户提到的“逻辑不对”可能是指一番赏**只能**是即时开奖,或者一番赏的定时开奖不应该显示某些不相关的字段(如 `min_participants` 在一番赏中可能不适用,因为一番赏是消耗库存的)。
|
||||
* **修正策略**:
|
||||
* 当选择“一番赏”时,默认锁定或限制 `draw_mode` 的选项。
|
||||
* 如果是“一番赏”,通常是**即时开奖**(用户购买后立即知道结果)。
|
||||
* 如果一番赏确实支持定时(例如:全员购买完后统一开奖),则需要明确字段。
|
||||
* **根据用户语境“即时 定时的 页面逻辑不对”**,推测可能是:切换一番赏时,应该自动处理 `draw_mode`,或者某些字段不该显示。
|
||||
|
||||
* **优化方案**:
|
||||
1. **联动逻辑**: 当 `play_type` 切换为 `ichiban` 时,强制或默认 `draw_mode` 为 `instant`(即时开奖),并禁用 `scheduled`(除非业务明确支持一番赏定时)。
|
||||
2. **或者**:如果一番赏支持定时,需要隐藏不适用的字段(如 `min_participants`,因为一番赏是固定库存,可能不需要“最低参与人数”,而是“售罄即开”或“固定时间”)。
|
||||
3. **UI 调整**: 简化一番赏模式下的表单,隐藏不必要的复杂定时配置。
|
||||
|
||||
**经确认(自我推演)**: 一番赏的典型玩法是用户购买后立即获得确定的赏品(即时)。但也存在“整箱全开”的定时玩法。但最常见的问题是:**一番赏不应该显示“最低参与人数”等凑单逻辑,因为它基于固定数量的签**。
|
||||
|
||||
**决定**:
|
||||
* 当 `play_type === 'ichiban'` 时:
|
||||
* **锁定** `draw_mode` 为 `instant`(即时开奖),并**隐藏**开奖模式选择框(或禁用)。
|
||||
* **隐藏**所有定时相关的配置(`min_participants`, `time_mode`, `interval_minutes` 等)。
|
||||
* 这样能避免用户配置出“定时开奖的一番赏”这种非典型且可能逻辑冲突的活动(除非后端明确支持)。
|
||||
|
||||
## 2. 架构设计 (Architect)
|
||||
|
||||
* **前端逻辑调整 (`wizard/index.vue`)**:
|
||||
* 监听 `scheduledForm.play_type` 的变化。
|
||||
* 如果 `play_type === 'ichiban'`:
|
||||
* 自动设置 `scheduledForm.draw_mode = 'instant'`。
|
||||
* `scheduledForm.min_participants = 0`。
|
||||
* `scheduledForm.interval_minutes = 0`。
|
||||
* 在模板中,使用 `v-if` 控制字段显示:
|
||||
* 如果 `play_type === 'ichiban'`,隐藏 `draw_mode` 选择框(或者显示为只读的“即时开奖”)。
|
||||
* 隐藏所有 `scheduled` 相关的输入框。
|
||||
|
||||
## 3. 任务拆解 (Atomize)
|
||||
|
||||
1. **Modify Frontend (`wizard/index.vue`)**:
|
||||
* 添加 `watch` 监听 `scheduledForm.play_type`。
|
||||
* 修改模板中的 `v-if` 条件,针对 `ichiban` 隐藏不必要的字段。
|
||||
* 或者直接在 `play_type` 选择器下方添加提示:“一番赏模式下默认为即时开奖”。
|
||||
|
||||
## 4. 执行步骤 (Automate)
|
||||
|
||||
1. **Step 1**: 修改 `web/admin/src/views/activity/wizard/index.vue`。
|
||||
* 在 `script setup` 中添加 `watch` 逻辑。
|
||||
* 调整 `template` 中的表单项可见性。
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
## 背景与现状概要
|
||||
- 技术栈:后端 Go(Gin 封装于 `internal/pkg/core`)、GORM、Viper、Zap、Swagger;前端 Vue3 + Vite + TypeScript + Pinia + Element Plus。
|
||||
- 核心入口:`main.go`(服务启动与任务调度);路由中心:`internal/router/router.go`;拦截与鉴权:`internal/router/interceptor/*`。
|
||||
- 业务分层:处理器 `internal/api/{admin,activity,app,user,pay,common}`;服务 `internal/service/**`;数据访问 `internal/repository/mysql/{dao,model}`。
|
||||
- 前端管理后台:`web/admin/src/**`,接口定义集中在 `web/admin/src/api/**`。
|
||||
|
||||
## 目标与交付物
|
||||
- 代码结构梳理报告:目录分层说明 + 模块职责 + 依赖关系图(Mermaid)。
|
||||
- 无用代码清理:列出候选清理清单、逐项验证、提交可回滚的变更方案。
|
||||
- API 文档(前后端):
|
||||
- 后端 REST 端点清单(方法、路径、鉴权、处理器、请求/响应示例、错误码、分页约定)。
|
||||
- 前端调用约定(基地址、拦截器、Token 注入、模块函数到后端端点的映射)。
|
||||
- 更新与补充项目根文档:`docs/说明文档.md`(规划、实施方案、进度记录)。
|
||||
|
||||
## 实施步骤
|
||||
### 1. 架构梳理(Align/Architect)
|
||||
- 枚举代码目录:识别后端、前端、配置、构建、脚本、生成器与产物目录。
|
||||
- 生成架构图:描绘后端分层(Router → Interceptor → API → Service → Repo)、前端分层(视图 → Store → API)。
|
||||
- 输出《代码结构总览.md》:说明关键路径与职责、跨模块依赖、构建/部署要点。
|
||||
|
||||
### 2. 路由与端点收敛(Atomize)
|
||||
- 解析 `internal/router/router.go` 的路由注册,枚举所有端点(含分组与中间件)。
|
||||
- 关联处理器方法(如 `admin.*`、`activity.*` 等),抽取鉴权要求:`AdminTokenAuthVerify`、`AppTokenAuthVerify`、RBAC `RequireAdminAction`。
|
||||
- 标准化约定:分页键(`page`、`page_size`)、通用响应包(`code`、`message`、`data`、`request_id`)、错误码体系。
|
||||
|
||||
### 3. API 文档编制
|
||||
- 后端文档:生成《API文档-后端.md》
|
||||
- 列表:方法、路径、处理器、鉴权、中间件、请求参数、响应示例、错误码、注意事项。
|
||||
- 支付/回调、系统健康、上传等特殊端点单独章节。
|
||||
- 前端文档:生成《API文档-前端.md》
|
||||
- 说明 Axios 基础配置(BaseURL、超时、拦截器、Token 注入)。
|
||||
- 列出 `web/admin/src/api/**` 模块函数到后端端点映射、入参/出参、调用示例。
|
||||
|
||||
### 4. 无用代码清理策略
|
||||
- 判定规则:
|
||||
- 未被任何文件 `import`/调用;
|
||||
- 未在路由或启动流程中引用;
|
||||
- 构建产物(如 `build/resources/admin/**`)、运行日志(`logs/**`);
|
||||
- 演示/测试脚本(如 `miniapp/pay-test/**`)、一次性生成器产物。
|
||||
- 候选清单(初版):
|
||||
- `internal/metrics/**`(若未启用 Prometheus);
|
||||
- `internal/repository/mysql/testrepo_sqlite.go`(未检索到引用);
|
||||
- `cmd/**`(工具/生成器,保留或迁移到 dev-only);
|
||||
- `scripts/swagger.*`(构建脚本,非运行时);
|
||||
- `build/resources/admin/**`、`logs/**`(产物与输出)。
|
||||
- 清理流程:
|
||||
- 逐项交叉检索引用关系 → 标注“安全删除”/“需保留”;
|
||||
- 对可能未来使用的模块改为禁用配置或注释式保留,避免功能回退风险;
|
||||
- 产物与日志转移到忽略或发布流程之外(完善 `.gitignore` 与构建管线)。
|
||||
|
||||
### 5. 文档与规范同步
|
||||
- 更新 `docs/说明文档.md`:规划、实施方案、节点记录(按用户规范)。
|
||||
- 在 `docs/api/` 目录落地《代码结构总览.md》《API文档-后端.md》《API文档-前端.md》。
|
||||
- 所有函数在新增代码中补充函数级注释(功能、参数、返回值)。
|
||||
|
||||
### 6. 验收与验证(Assess)
|
||||
- 后端:`go build`、路由完整性检查、Swagger 校验(非生产)、关键端点手测。
|
||||
- 前端:`vite build`、ESLint/Stylelint、页面 API 调用冒烟测试。
|
||||
- 部署:非生产环境验证 PProf、CORS、静态资源路由回退;检查 `.env` 与证书安全。
|
||||
|
||||
## 输出物清单
|
||||
- `docs/api/代码结构总览.md`(含架构图)
|
||||
- `docs/api/API文档-后端.md`(REST 列表与约定)
|
||||
- `docs/api/API文档-前端.md`(调用契约与示例)
|
||||
- 可回滚的清理变更(提交前附清单与影响评估)
|
||||
|
||||
## 依赖与约束
|
||||
- 保留生成器/工具目录(`cmd/**`)除非确认迁移;
|
||||
- 配置与证书不改动业务值,仅完善文档与忽略策略;
|
||||
- 如需补充 Swagger 注解,遵循现有 `swaggo` 用法并保持最小侵入。
|
||||
|
||||
## 下一步
|
||||
- 确认本计划后:
|
||||
1) 输出架构梳理文档;
|
||||
2) 生成端点清单并编制前后端 API 文档;
|
||||
3) 提交清理候选与验证报告,执行安全清理。
|
||||
- 如需额外约定(错误码字典、分页/排序统一规范、RBAC 角色映射),我将在文档中补充并与现有实现对齐。
|
||||
@ -1,49 +0,0 @@
|
||||
## 问题归因
|
||||
- 活动/期号为空:后端仅在存在抽奖日志时填充活动信息,未抽取或刚支付时为0。参考 internal/api/admin/pay_orders_admin.go:167-191。
|
||||
- 中奖字段与等级:前端直接展示“是否中奖”,但应改为“中奖等级”并基于 `activity.level`/`reward_id` 显示;未抽取时应显示“待开奖”。
|
||||
- 支付信息不全:只显示 `ActualAmount`,未分解“积分抵扣金额/积分数量”;`PayPreorderID` 为0时未做断言处理。
|
||||
- 商品价格与金额为0:抽奖订单常无商品明细或价格未赋值;可根据奖励商品填充价格,或以“抽奖价格×次数”为明细。
|
||||
|
||||
## 后端改造(GetPayOrderDetail)
|
||||
- 文件:internal/api/admin/pay_orders_admin.go
|
||||
- 变更点:
|
||||
1) 无抽奖日志时,解析 `order.Remark` 中 `lottery:activity:<aid>|issue:<iss>|count:<N>` 填充 `activity.activity_id/issue_id`,查询 `issue.issue_number` 与 `activity.activity_name`。
|
||||
2) 扩展支付信息:`payment.points_amount`(订单积分抵扣金额,单位分)、`payment.points_used`(抵扣积分数量=points_amount/10)、`payment.total_amount`(订单总额,单位分)。
|
||||
3) 返回 `activity.count`(抽次数);若有奖励商品,补充 `activity.product_price`(商品价格,单位分)。
|
||||
4) 若订单无 `order_items`,在响应中提供 `computed_items`:基于“抽奖价格×次数”构造一条展示用明细(前端优先显示 `computed_items`)。
|
||||
5) `payment.pay_preorder_id` 为0时仍返回,前端据此显示“-”。
|
||||
|
||||
## 前端改造(订单详情抽屉)
|
||||
- 文件:web/admin/src/views/…(订单详情抽屉组件,使用 fetchGetOrderDetail)
|
||||
- 变更点:
|
||||
1) 活动:显示 `activity.activity_name`;下方附 `issue_number`;不显示 `(ID:0)`。
|
||||
2) 状态区:移除“是否中奖”;改为“中奖等级”显示 `activity.level`,若 `reward_id` 为空则显示“待开奖”。
|
||||
3) 支付区:
|
||||
- “实付”显示:`payment.actual_amount/100` 元
|
||||
- 新增“积分抵扣”:`payment.points_used` 积分(约 `points_amount/100` 元)
|
||||
- 新增“订单总额”:`payment.total_amount/100` 元
|
||||
- “预订单ID”:显示 `payment.pay_preorder_id`,为0则显示“-”
|
||||
4) 明细表:
|
||||
- 优先显示 `computed_items` 的“单价/金额”,否则显示 `items`;
|
||||
- 若有中奖商品,显示 `activity.reward_name` 与 `activity.product_price`。
|
||||
|
||||
## 路由与API契约
|
||||
- 前端调用不变:`GET admin/pay/orders/:order_no`
|
||||
- 响应新增字段:
|
||||
- `activity.count`、`activity.product_price`
|
||||
- `payment.points_amount`、`payment.points_used`、`payment.total_amount`
|
||||
- `computed_items: [{name, quantity, unit_price, amount}]`
|
||||
|
||||
## 实施步骤
|
||||
1) 后端:更新 GetPayOrderDetail 填充解析与新增字段;构造 `computed_items`。
|
||||
2) 前端:更新详情组件的数据映射与展示逻辑;删改“是否中奖”,补充字段渲染与空值处理。
|
||||
3) 验证:
|
||||
- 无抽奖日志的订单仍能显示活动名称与期号
|
||||
- 积分全额支付场景显示积分抵扣与总额,实付为0,预订单ID为“-”
|
||||
- 即时模式支付后,轮询显示中奖等级与商品信息
|
||||
- 明细表显示正确单价/金额
|
||||
|
||||
## 验收标准
|
||||
- 所有问题项均有正确数据或合理占位显示
|
||||
- 即时/定时、积分/金额支付四种组合下展示正确
|
||||
- 架构不破坏现有接口:前端仅增量使用新增字段
|
||||
@ -1,127 +0,0 @@
|
||||
## 目标
|
||||
|
||||
* 全面清理未用代码、注释废弃块、空文件与无用测试
|
||||
|
||||
* 识别并重构重复代码(重复率≥80%)
|
||||
|
||||
* 保持现有功能稳定,构建与测试全部通过
|
||||
|
||||
* 输出对比报告与文档更新
|
||||
|
||||
## 范围
|
||||
|
||||
* 后端:`internal/**`、`cmd/**`、`migrations/**`
|
||||
|
||||
* 前端管理:`web/admin/**`(Vue/TS/样式与公共组件)
|
||||
|
||||
* 通用资源:`docs/**`、脚手架与配置(不更改生产配置)
|
||||
|
||||
## 清理策略与工具
|
||||
|
||||
* 未用与死代码检测
|
||||
|
||||
* Go:`golangci-lint`(unused、deadcode、revive)、`go vet`
|
||||
|
||||
* TS/Vue:`tsc --noEmit`(类型与未用导出)、`eslint`(no-unused-vars/no-dead-code)
|
||||
|
||||
* 注释废弃块识别
|
||||
|
||||
* 规则:Grep 检索注释中出现代码结构(`func|class|export|<template>`),人工确认后删除
|
||||
|
||||
* 空文件/无用测试
|
||||
|
||||
* Glob + Read 识别空/仅注释文件;移除未被引用的测试(无匹配运行入口或全跳过)
|
||||
|
||||
* 重复代码检测
|
||||
|
||||
* 跨语言:`jscpd`(Vue/TS/Go)或 `dupl`(Go)
|
||||
|
||||
* 阈值:重复度 ≥ 80% 且行数 ≥ 20 行
|
||||
|
||||
* 重构原则
|
||||
|
||||
* 后端:抽取到 `internal/pkg/common` 或现有包的工具单元;避免交叉包循环依赖
|
||||
|
||||
* 前端:抽取到 `web/admin/src/components/common` 或 `utils`,保持现有风格与命名
|
||||
|
||||
## 执行步骤
|
||||
|
||||
1. 基线采集
|
||||
|
||||
* 读取项目结构与关键模块,记录当前构建状态(不修改)
|
||||
|
||||
* 运行只读分析:语义搜索/正则/Grep,收集疑似未用项、注释废弃块、空文件、重复片段清单
|
||||
|
||||
1. 未用代码清理
|
||||
|
||||
* 逐文件比对引用关系(Grep/语义搜索),将“未被任何入口引用”标记为候选
|
||||
|
||||
* 生成候选清单(含文件路径与符号名),按模块批次删除;每次删除后执行增量构建验证
|
||||
|
||||
1. 注释废弃块删除
|
||||
|
||||
* 扫描 `//`、`/* */`、`<!-- -->` 中含可编译结构的片段,人工确认后删除
|
||||
|
||||
* 对 SFC 中注释的 `<template>/<script>/<style>` 片段严谨处理,避免结构破坏
|
||||
|
||||
1. 空文件与无用测试
|
||||
|
||||
* 移除 0 字节/仅注释文件;对测试:无法被测试运行器加载、或所有用例被跳过的文件移除
|
||||
|
||||
1. 重复代码重构
|
||||
|
||||
* 跑相似度分析,生成报告(位置、重复度、建议合并点)
|
||||
|
||||
* 抽取公共方法/组件,替换调用方;保持 API 不变,变更点最小化
|
||||
|
||||
1. 依赖与引用更新
|
||||
|
||||
* 后端:修复 import;前端:修复路径别名与组件引用;保证编译通过
|
||||
|
||||
1. 验证与回归
|
||||
|
||||
* 构建验证:`go build`、`tsc --noEmit`、前端 `npm run build`(或等价)
|
||||
|
||||
* 单/集成测试:运行现有测试;若缺失,补最小冒烟测试(关键模块)
|
||||
|
||||
* 功能回归:登录、活动管理、一番赏映射、承诺生成/摘要、开奖与订单查询
|
||||
|
||||
## 安全保障
|
||||
|
||||
* 分批次清理,每批次后执行构建与核心用例回归
|
||||
|
||||
* 只删除“未引用/重复/注释废弃/空”的候选;核心路径(API、路由、策略、DAO、视图)谨慎处理
|
||||
|
||||
* 所有变更均记录到临时报告与文档
|
||||
|
||||
## 交付物
|
||||
|
||||
* 代码对比报告:删除/变更列表(文件路径、符号名、原因)
|
||||
|
||||
* 重复代码检测报告:重复片段与重构前后引用图
|
||||
|
||||
* 验收文档:构建输出、测试结果、回归清单
|
||||
|
||||
* 文档更新:在 `docs/代码清理` 目录建立说明与进度记录
|
||||
|
||||
## 本次已执行清理项(2025-12-08)
|
||||
|
||||
- 移除抽奖策略注册表冗余(保留接口类型),不影响默认策略与一番赏流程
|
||||
- 删除 Guild 相关 DAO/Model 生成文件(项目内无引用,运行不受影响)
|
||||
- 移除管理端 batch_users 接口与路由挂载(前端无调用)
|
||||
- 移除管理端 IssueUserToken 路由挂载(前端无调用;代码文件暂保留为未引用状态)
|
||||
- 构建验证:`go build ./...` 通过;`go test ./...` 存在历史用例失败(外部依赖与软删列缺失),与本次变更无关
|
||||
|
||||
## 文档与规范对齐
|
||||
|
||||
* 创建:`docs/代码清理/ALIGNMENT_代码清理.md`(范围/边界/不确定点)
|
||||
|
||||
* 共识:`docs/代码清理/CONSENSUS_代码清理.md`(验收标准与方案)
|
||||
|
||||
* 设计与任务拆分:`docs/代码清理/DESIGN_代码清理.md`、`TASK_代码清理.md`
|
||||
|
||||
* 执行与评估:`docs/代码清理/ACCEPTANCE_代码清理.md`、`FINAL_代码清理.md`、`TODO_代码清理.md`
|
||||
|
||||
## 后续执行说明
|
||||
|
||||
* 获批后:按上述步骤使用只读扫描确定候选清单→分批次提交清理补丁→每批次构建与回归验证→汇总报告与文档更新。
|
||||
@ -1,113 +0,0 @@
|
||||
## 现状与问题
|
||||
|
||||
* 当前 APP 抽奖接口:`POST /api/app/activities/:activity_id/issues/:issue_id/draw`,处理函数 `internal/api/activity/draw_app.go:27-46`,服务侧使用 `internal/service/activity/draw_execute.go:12-89` 的可验证随机抽样(HMAC-SHA256 + 拒绝采样)。
|
||||
|
||||
* 现有实现按期(issue)维度抽取奖励,已具备收据与随机性证明,但不同活动类型(分类/玩法)未来可能有差异化前置校验、选择规则与奖励发放流程。
|
||||
|
||||
* 需求:我们将有很多活动,接口到底通用还是分活动?
|
||||
|
||||
## 结论选择
|
||||
|
||||
* 接口保持通用(单一端点),后端采用策略模式按活动类别/配置差异化实现逻辑。这样:
|
||||
|
||||
* 前端保持统一调用与响应结构,降低复杂度与版本碎片。
|
||||
|
||||
* 后端可插拔扩展不同活动的校验、选择、奖励发放与效果管道,避免复制粘贴与接口膨胀。
|
||||
|
||||
* 与现有 `Receipt` 结构兼容,保留可验证性。
|
||||
|
||||
## 技术方案
|
||||
|
||||
* 保留通用端点:`POST /api/app/activities/:activity_id/issues/:issue_id/draw`。
|
||||
|
||||
* 引入“活动策略”接口(示例命名):
|
||||
|
||||
* `ActivityDrawStrategy`:
|
||||
|
||||
* `PreChecks(ctx, activity, issue, user) error`(余额/订单/库存/状态/时间窗/限频)
|
||||
|
||||
* `SelectItem(ctx, issue, baseSelector) (selectedRewardID, proof)`(可复用当前 HMAC 随机选择器作为 `baseSelector`,策略可注入权重或过滤规则)
|
||||
|
||||
* `GrantReward(ctx, user, rewardID) error`(扣减数量、落库收据/日志、发放道具/实物/积分/券)
|
||||
|
||||
* `PostEffects(ctx, user, activity, issue, rewardID)`(道具效果、称号、积分等联动)
|
||||
|
||||
* 策略注册与路由:
|
||||
|
||||
* 基于 `activity.activity_category_id` 或活动配置选择策略;默认策略沿用当前实现(完全兼容)。
|
||||
|
||||
* 效果管道:
|
||||
|
||||
* 抽奖前置/后置效果处理(如限时加权、Boss 限制、用户卡片效果),设计为中间件式管道,便于组合。
|
||||
|
||||
* 并发与一致性:
|
||||
|
||||
* 引入期级别乐观扣减或分布式锁(后续落地),确保高并发下数量与日志一致。
|
||||
|
||||
* 支持 `X-Idempotency-Key` 防重(后续扩展),避免重复下单/重复抽取。
|
||||
|
||||
* 监控与审计:
|
||||
|
||||
* 统一埋点:抽奖尝试、成功、失败原因、库存变化、用户画像。
|
||||
|
||||
* 审计日志与追踪 ID 与收据关联。
|
||||
|
||||
## API 契约
|
||||
|
||||
* 请求:保持不变;可新增可选头 `X-Idempotency-Key`。
|
||||
|
||||
* 响应:保持 `receipt` 字段结构;策略仅影响内部选择/发放过程,不破坏契约。
|
||||
|
||||
* 错误码:标准化前置校验失败(余额不足、活动未开始、库存不足、限频)与系统错误。
|
||||
|
||||
## 数据与规则
|
||||
|
||||
* 数量规则:`original_qty` 为期初总数,`quantity` 当前剩余;不限量为 `-1`,选择与发放逻辑需正确处理。
|
||||
|
||||
* 收据:继续包含随机性证明与所选项快照,具备可验证性。
|
||||
|
||||
* 扣减与发放:在 `GrantReward` 中统一实现(现阶段代码只返回收据,发放与扣减建议纳入策略实现)。
|
||||
|
||||
## 变更范围
|
||||
|
||||
1. 新增 `internal/service/activity/strategy/` 策略接口与默认实现;注册表按分类 ID 选择策略。
|
||||
2. 在 `ExecuteDraw()` 增强:
|
||||
|
||||
* 解析活动与用户上下文,调用策略 `PreChecks`→`SelectItem`→`GrantReward`→`PostEffects`。
|
||||
|
||||
* 默认策略复用当前 `draw_execute.go` 的随机选择器。
|
||||
3. `draw_app.go` 维持统一端点,增加对用户上下文的传递(如需要),契约不变。
|
||||
|
||||
## 分阶段实施
|
||||
|
||||
1. 引入策略接口与默认策略(不改变行为)。
|
||||
2. 接入活动类别路由与注册(读取 `ActivityCategoryID`)。
|
||||
3. 将扣减与发放迁移到策略中,补充事务与并发控制。
|
||||
4. 增强效果管道,接入道具卡、称号、积分的已有模块。
|
||||
5. 增加监控与限频、幂等,完善错误码。
|
||||
6. 编写单元测试与集成测试:
|
||||
|
||||
* 权重选择正确性与拒绝采样无偏。
|
||||
|
||||
* 不限量与零库存边界。
|
||||
|
||||
* 并发扣减一致性。
|
||||
|
||||
* 幂等与限频。
|
||||
|
||||
## 验收标准
|
||||
|
||||
* 单一抽奖端点适配 2+ 类活动策略,无需前端切换接口。
|
||||
|
||||
* 收据结构与随机性证明保持一致,验证脚本通过。
|
||||
|
||||
* 库存扣减与奖励发放在并发下无不一致;关键路径有审计日志。
|
||||
|
||||
* 测试覆盖关键流程与边界条件。
|
||||
|
||||
## 后续扩展
|
||||
|
||||
* 按活动类型开放“策略配置”与“玩法参数”在管理端,可动态调整权重、效果、限频。
|
||||
|
||||
* 如某玩法确有完全不同交互(例如合成/多次抽/队列结算),再考虑单独端点,但仍优先在通用端点层面通过参数与策略实现。
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
- 删除活动期:`activity_issues` 按 `activity_id`(`internal/repository/mysql/model/activity_issues.gen.go:18`)
|
||||
- 期下配置与承诺:
|
||||
- 奖励配置 `activity_reward_settings` 按 `issue_id`(`internal/repository/mysql/model/activity_reward_settings.gen.go:18`)
|
||||
- 活动承诺字段:`activities.commitment_*`(活动维度,已统一使用活动级承诺,不存在期级承诺表)
|
||||
- 随机承诺 `issue_random_commitments` 按 `issue_id`(`internal/repository/mysql/model/issue_random_commitments.gen.go:18`)
|
||||
- 期下抽奖相关:
|
||||
- 抽奖日志 `activity_draw_logs` 按 `issue_id`(`internal/repository/mysql/model/activity_draw_logs.gen.go:18`)
|
||||
- 抽奖效果 `activity_draw_effects` 按 `draw_log_id`/`issue_id`(`internal/repository/mysql/model/activity_draw_effects.gen.go:17,29`)
|
||||
@ -57,7 +57,7 @@
|
||||
- 统一采用“从叶到根”的顺序:
|
||||
1) 以日志/效果/凭据等子表为先(`activity_draw_effects`、`activity_draw_receipts`、`activity_draw_logs`)
|
||||
2) 再清理资产/权益/模板(`user_inventory`、`user_item_cards`、`user_titles`、`system_*`)
|
||||
3) 清理期与期下配置(`activity_issues`、`activity_reward_settings`)
|
||||
3) 清理期与期下配置(`activity_issues`、`activity_reward_settings`、`issue_random_commitments`)
|
||||
4) 删除根实体(`activities` 或软删 `users`)
|
||||
- 全过程包裹在单事务中,任何一步失败则回滚。
|
||||
|
||||
@ -83,4 +83,4 @@
|
||||
- 用户删除入口:`internal/service/user/batch_user.go:32` → 升级为调用 `DeleteUserCascade`
|
||||
|
||||
## 后续动作
|
||||
- 我将基于以上清单与顺序,补充两套事务级联删除实现,并为关键入口替换调用;同时补充单元测试覆盖正常/边界/异常三类用例,验证幂等与性能。
|
||||
- 我将基于以上清单与顺序,补充两套事务级联删除实现,并为关键入口替换调用;同时补充单元测试覆盖正常/边界/异常三类用例,验证幂等与性能。
|
||||
@ -1,50 +0,0 @@
|
||||
## 目标
|
||||
- 在管理端实时查看某一期每个 `slot_index` 对应的奖品与当前状态(名称、图片、等级、原始数量/剩余数量、是否已占用),支持分页与筛选。
|
||||
|
||||
## 数据来源与算法
|
||||
- 奖励配置:`activity_reward_settings`(name/level/product_id/original_qty/quantity/sort)
|
||||
- 奖品图片:`products.image`(如为实物奖关联商品)
|
||||
- 位置占用:`issue_position_claims`(唯一记录已占用 slot)
|
||||
- 承诺种子:`activities.commitment_seed_master`(HMAC-SHA256 确定性洗牌,活动级)
|
||||
- 映射生成:
|
||||
1. 构造长度 N 的 `slots` 列表:按各奖励 `original_qty` 重复填入 `reward_id`
|
||||
2. Fisher–Yates + `HMAC-SHA256(seed, "shuffle:"+i+"|issue:"+issueId)` 前 8 字节取模生成交换索引
|
||||
3. 得到稳定映射 `slot_index → reward_id`;该映射在期内不变
|
||||
|
||||
## 后端接口(只读)
|
||||
- 组:`/api/admin`(需登录与 RBAC:`ichiban:slots:view`)
|
||||
- `GET /api/admin/ichiban/activities/:activity_id/issues/:issue_id/slots`
|
||||
- 入参:`page`、`page_size`(默认 1/50),`claimed`(可选 true/false 过滤已占用状态)
|
||||
- 返回:
|
||||
- `total_slots`:N
|
||||
- `list[]`:{`slot_index`,`reward_id`,`reward_name`,`level`,`product_image`,`original_qty`,`remaining_qty`,`claimed`}
|
||||
- `seed_version`:活动承诺版本号,`rule`:`level_desc_sort_asc_id_asc`
|
||||
- 实现:按上述算法生成映射,联表/查询实时数量与占用状态;服务端分页
|
||||
- `GET /api/admin/ichiban/issues/:issue_id/slot/:slot_index`
|
||||
- 返回单个位置详情与计算证明(HMAC 输入与结果),用于抽样核验
|
||||
|
||||
## 服务与性能
|
||||
- Service:`internal/service/activity/ichiban_slots_service.go`
|
||||
- `BuildMapping(issueID)`:读取奖励配置与种子,生成映射(1000+ 在 2s 内);可引入本地 LRU 缓存(TTL 60s)
|
||||
- `QuerySlots(issueID, page,size, claimed?)`:分页拼装响应,`remaining_qty` 直接读 `activity_reward_settings.quantity`
|
||||
- 性能优化:
|
||||
- 映射缓存 + 仅在奖励配置或种子变更时失效;可选将完整映射预生成入表以应对更大规模(保留扩展)
|
||||
- `issue_position_claims(issue_id,slot_index)` 与 `reward_id` 索引保障查询速度
|
||||
|
||||
## 前端管理页面
|
||||
- 路由:`/operations/ichiban/slots`
|
||||
- 组件:表格布局(分页、排序标签、序号列),筛选(活动/期、占用状态),图片懒加载
|
||||
- 列:序号、奖品图、名称、等级标签、原始数量/剩余数量、占用状态;顶部显示 `seed_version` 与排序规则说明
|
||||
|
||||
## 权限与审计
|
||||
- 仅管理员可访问(`RequireAdminRole` + `RequireAdminAction("ichiban:slots:view")`)
|
||||
- 查询操作写入操作日志(模块/期次/分页参数)
|
||||
|
||||
## 验收标准
|
||||
- 1000+ 奖品分页加载 ≤2s;映射稳定一致;显示字段完整;占用状态实时;筛选与分页正常
|
||||
|
||||
## 交付内容
|
||||
- 管理端两条 GET 接口与 Service
|
||||
- 管理页面与 API 封装
|
||||
- 单元测试:映射稳定性、分页正确性、占用过滤
|
||||
- 文档:算法与接口说明
|
||||
@ -1,71 +0,0 @@
|
||||
## 目标
|
||||
- 系统化梳理后端代码,识别并删除冗余设计与未使用模块
|
||||
- 保持现有功能与前端调用不受影响,提升可维护性与编译/测试稳定性
|
||||
|
||||
## 范围
|
||||
- 主要针对 Go 后端 (`internal/*`, `router/*`, `repository/*`)
|
||||
- 不改动数据库表结构与线上配置;仅移除未被调用的代码
|
||||
- 前端仅做轻微枚举对齐(可选),不影响现有页面
|
||||
|
||||
## 清理项
|
||||
1. 移除未使用的抽奖策略注册机制
|
||||
- 文件:`internal/service/activity/strategy/strategy.go`
|
||||
- 保留:`ActivityDrawStrategy` 接口(默认策略仍以接口类型返回)
|
||||
- 删除:`registry` 映射、`Register`/`Get` 方法(代码内无引用)
|
||||
- 影响评估:`lottery_app.go` 直接实例化 `NewDefault`,不依赖注册表;`ichiban` 专用策略走独立服务,不受影响
|
||||
|
||||
2. 移除公会 Guild 相关 DAO/Model(已无引用)
|
||||
- 文件:
|
||||
- `internal/repository/mysql/dao/guild*.gen.go`
|
||||
- `internal/repository/mysql/dao/guild_contribute_logs.gen.go`
|
||||
- `internal/repository/mysql/model/guild*.gen.go`
|
||||
- `internal/repository/mysql/model/guild_contribute_logs.gen.go`
|
||||
- 依据:全局检索无业务层或接口层引用,仅残留表定义
|
||||
- 处理:删除生成文件;保留 `TracePlugin.softDeleteTables` 的字符串名单不影响运行(仅按表名软删过滤)
|
||||
|
||||
3. 移除管理端批量造数/批量删除用户接口(未被前端使用)
|
||||
- 路由:`internal/router/router.go`
|
||||
- `POST /api/admin/batch_users`(创建)
|
||||
- `DELETE /api/admin/batch_users`(删除)
|
||||
- 处理:删除对应 handler `internal/api/admin/batch_users.go` 和路由挂载
|
||||
- 保留其他批量接口(积分/优惠券/奖励)不变
|
||||
|
||||
4. 移除“为指定用户签发 APP Token”管理端接口(未被前端使用)
|
||||
- 路由:`POST /api/admin/users/:user_id/token`
|
||||
- 文件:`internal/api/admin/user_token_admin.go`
|
||||
- 说明:仅测试/紧急用途,当前前端无调用;移除可减少安全面
|
||||
|
||||
5. 保留 Prometheus 指标模块但不启用(仅在需要时开启)
|
||||
- 现状:`internal/metrics/*` 未被路由启用;保留代码与可选开关,不做删除
|
||||
- 若后续确认长期不使用,可二期移除并同步 `go.mod` 依赖精简
|
||||
|
||||
6. 前端枚举对齐(可选)
|
||||
- 文件:`web/admin/src/api/dashboard.ts`
|
||||
- 内容:`TodoTaskType` 包含 `JOIN_GUILD`(服务端已改为 `undrawn` 代办);如需对齐,移除或改为服务端实际值
|
||||
|
||||
## 验收标准
|
||||
- 编译通过:`go build ./...` 无错误
|
||||
- 单测通过:现有测试(如 `ichiban_test.go`)通过
|
||||
- API 回归:管理端/APP端核心路由不变(产品、活动、奖励、抽奖、支付、称号、道具卡、系统配置)
|
||||
- 前端联调:`web/admin/src/api/*` 所引用的后端路由均可正常返回
|
||||
|
||||
## 执行步骤
|
||||
1. 删除策略注册表(保留接口)
|
||||
2. 删除 Guild 相关 `dao/` 与 `model/` 生成文件
|
||||
3. 删除 `batch_users.go` 及其路由挂载
|
||||
4. 删除 `user_token_admin.go` 及其路由挂载
|
||||
5. 本地编译与单测(构建/测试)
|
||||
6. 运行服务做核心接口冒烟(活动、商品、支付、任务中心、称号/卡券)
|
||||
7. 更新 `.trae/documents/全面代码清理与优化计划.md` 标注已清理项与影响范围
|
||||
|
||||
## 风险与回滚
|
||||
- 若前端/运营依赖被误删接口:通过 Git 回滚对应文件与路由挂载即可恢复
|
||||
- `Guild` 表仍在数据库中:删除 DAO/Model 不影响其他代码;如未来需要,可通过代码生成器补回
|
||||
|
||||
## 代码参考
|
||||
- 路由文件:`internal/router/router.go:106-108`, `internal/router/router.go:149`
|
||||
- 策略接口与实现:`internal/service/activity/strategy/strategy.go`, `internal/service/activity/strategy/default.go`, `internal/service/activity/ichiban_slots_service.go`
|
||||
- 测试用例:`internal/service/activity/strategy/ichiban_test.go`
|
||||
- 前端调用示例:`web/admin/src/api/*`(确认哪些路由在用)
|
||||
|
||||
请确认以上清理方案,确认后我将按步骤执行、构建验证并提交变更说明。
|
||||
@ -1,170 +0,0 @@
|
||||
## 约束与对齐
|
||||
- 管理端仅支持“定时到具体时间点”的开奖,不支持设置“定时 N 分钟后开奖”。
|
||||
- 计划包含:玩法策略、哈希可验证抽奖、退款+赠券流程、数据一致性、完整 API 与参数。
|
||||
|
||||
## 玩法与流程
|
||||
- 一番赏
|
||||
- 定时开奖:必填 `scheduled_time`(绝对时间戳) 与 `min_participants`(最低人数)。到时若人数 < N:全额退款+赠券。≥N:统一开奖。
|
||||
- 即时开奖:用户支付成功后立即抽奖并返回凭证。
|
||||
- 无限赏/对对碰/爬塔:标准抽奖流程(即时抽奖),策略差异体现在权重、过滤规则、后置效果。
|
||||
|
||||
## 抽奖与可验证性
|
||||
- 种子:服务端使用加密安全 `crypto/rand` 生成 32 字节 `seed`,按期(issue)维度保存。
|
||||
- 哈希签名:`signature = HMAC-SHA256(seed, user_id|issue_id|timestamp|nonce)`;`nonce` 为 16 字节随机串,`timestamp` 精确到毫秒。
|
||||
- 客户端校验:本地按相同算法计算签名并比对;凭证包含必要输入。
|
||||
|
||||
## 数据模型(核心字段)
|
||||
- `activities`:`id`、`name`、`play_type`(ichiban/infinite/match/tower)、`draw_mode`(scheduled/instant)、`min_participants`、`scheduled_time`、`refund_coupon_type`、`refund_coupon_amount`、`status`(draft/active/closed)。
|
||||
- `issues`:`id`、`activity_id`、`seed`、`seed_version`、`original_qty`、`quantity`、`status`(pending/drawing/done/failed)。
|
||||
- `rewards`:`id`、`issue_id`、`name`、`weight`、`original_qty`、`quantity`、`type`、`meta`。
|
||||
- `draw_logs`:`id`、`issue_id`、`activity_id`、`user_id`、`reward_id`、`signature`、`timestamp`、`nonce`、`receipt_json`。
|
||||
- `lottery_refund_logs`:`id`、`issue_id`、`order_id`、`user_id`、`amount`、`coupon_type`、`coupon_amount`、`reason`、`created_at`、`status`(success/failed/retry)。
|
||||
|
||||
## 管理端 API 与参数
|
||||
1) 创建活动
|
||||
- `POST /api/admin/lottery/activities`
|
||||
- Body
|
||||
```
|
||||
{
|
||||
"name": "一番赏·七月",
|
||||
"play_type": "ichiban", // 枚举: ichiban | infinite | match | tower
|
||||
"draw_mode": "scheduled", // 枚举: scheduled | instant
|
||||
"min_participants": 100, // 定时玩法必填
|
||||
"scheduled_time": "2025-12-05T20:00:00Z", // 绝对时间戳,禁止传相对分钟
|
||||
"refund_coupon_type": "cash", // 枚举: cash | discount | gift
|
||||
"refund_coupon_amount": 20.0, // 金额或面额
|
||||
"description": "..."
|
||||
}
|
||||
```
|
||||
- Response
|
||||
```
|
||||
{ "activity_id": 123 }
|
||||
```
|
||||
- 校验:当 `draw_mode`==`scheduled` 时必须存在 `scheduled_time`(>= 当前时间+最小提前量) 与 `min_participants`;拒绝包含“分钟”相对参数。
|
||||
|
||||
2) 更新活动
|
||||
- `PATCH /api/admin/lottery/activities/:activity_id`
|
||||
- Body(同创建,可部分字段)
|
||||
- 禁止将 `scheduled_time` 更新为相对分钟;仅允许更新为更晚的绝对时间。
|
||||
|
||||
3) 创建活动期(含奖池与种子)
|
||||
- `POST /api/admin/lottery/activities/:activity_id/issues`
|
||||
- Body
|
||||
```
|
||||
{
|
||||
"rewards": [
|
||||
{ "name": "SSR皮肤", "weight": 1, "original_qty": 1, "type": "virtual", "meta": {"skin_id": 88} },
|
||||
{ "name": "SR皮肤", "weight": 10, "original_qty": 50, "type": "virtual", "meta": {"skin_id": 77} }
|
||||
]
|
||||
}
|
||||
```
|
||||
- Response
|
||||
```
|
||||
{
|
||||
"issue_id": 456,
|
||||
"seed_version": 1
|
||||
}
|
||||
```
|
||||
|
||||
4) 活动列表/详情
|
||||
- `GET /api/admin/lottery/activities?play_type=&draw_mode=&status=&page=&size=`
|
||||
- `GET /api/admin/lottery/activities/:activity_id`
|
||||
|
||||
5) 期列表/详情
|
||||
- `GET /api/admin/lottery/activities/:activity_id/issues?page=&size=`
|
||||
- `GET /api/admin/lottery/issues/:issue_id`
|
||||
|
||||
6) 手动关闭活动/期
|
||||
- `POST /api/admin/lottery/activities/:activity_id/close`
|
||||
- `POST /api/admin/lottery/issues/:issue_id/close`
|
||||
|
||||
## APP 端 API 与参数
|
||||
1) 参与抽奖(创建订单/入场)
|
||||
- `POST /api/app/lottery/join`
|
||||
- Body
|
||||
```
|
||||
{
|
||||
"activity_id": 123,
|
||||
"issue_id": 456,
|
||||
"channel": "wechat", // 支付渠道
|
||||
"client_nonce": "base64..." // 可选,客户端随机串
|
||||
}
|
||||
```
|
||||
- Response(即时模式可能包含抽奖结果;定时模式仅返回入场信息)
|
||||
```
|
||||
{
|
||||
"join_id": "J202512030001",
|
||||
"order_id": "O202512030009",
|
||||
"pay_params": { /* JSAPI 等 */ },
|
||||
"queued": true, // 定时玩法
|
||||
"draw_mode": "scheduled"
|
||||
}
|
||||
```
|
||||
|
||||
2) 即时抽奖(支付成功后自动触发,若需要主动查询)
|
||||
- `GET /api/app/lottery/result?join_id=J202512030001`
|
||||
- Response(包含可验证凭证)
|
||||
```
|
||||
{
|
||||
"result": {
|
||||
"reward_id": 789,
|
||||
"reward_name": "SR皮肤"
|
||||
},
|
||||
"receipt": {
|
||||
"issue_id": 456,
|
||||
"seed_version": 1,
|
||||
"timestamp": 1733251200123,
|
||||
"nonce": "rB1A...==",
|
||||
"signature": "hmacSha256Base64...",
|
||||
"algorithm": "HMAC-SHA256",
|
||||
"inputs": {
|
||||
"user_id": 10086,
|
||||
"issue_id": 456,
|
||||
"timestamp": 1733251200123,
|
||||
"nonce": "rB1A...=="
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3) 定时开奖结果查询(到点统一结算后)
|
||||
- `GET /api/app/lottery/scheduled/result?issue_id=456&user_id=10086`
|
||||
- Response 同上(若未达人数,返回退款与赠券信息)
|
||||
```
|
||||
{
|
||||
"refunded": true,
|
||||
"refund": { "order_id": "O202512030009", "amount": 29.9 },
|
||||
"coupon": { "type": "cash", "amount": 20.0, "coupon_id": 33001 }
|
||||
}
|
||||
```
|
||||
|
||||
4) 客户端校验助手(可选)
|
||||
- `GET /api/app/lottery/issue/:issue_id/seed_version`
|
||||
- Response `{ "seed_version": 1 }`
|
||||
|
||||
## 支付/退款/赠券
|
||||
- 支付回调:沿用 `/api/pay/wechat_notify`(落库订单状态,触发即时抽奖或定时入场)。
|
||||
- 定时结算器:服务端按 `scheduled_time` 扫描:
|
||||
- 人数 < N:批量退款,并为每位用户发放预设优惠券(`system_coupons`)。
|
||||
- 人数 ≥ N:统一抽奖与发放,写入 `draw_logs` 与事务扣减奖励。
|
||||
- 退款接口(内部调用):对每个订单创建退款,记录 `lottery_refund_logs` 并保证幂等。
|
||||
|
||||
## 一致性与异常处理
|
||||
- 事务:入场/库存扣减/日志写入使用同一事务;失败回滚。
|
||||
- 幂等:`join_id` 与 `order_id` 作为幂等键,退款/赠券均防重复。
|
||||
- 定时任务:定期扫描 + 重试机制;失败写入日志,告警通知。
|
||||
- 网络中断:客户端可凭 `join_id` 查询结果;服务端保证结算原子性。
|
||||
|
||||
## 错误码(示例)
|
||||
- `400`:活动未开始/已结束/库存不足/限频/参数错误
|
||||
- `402`:未支付
|
||||
- `409`:重复参与/幂等冲突
|
||||
- `500`:系统错误/结算失败(可重试)
|
||||
|
||||
## 测试与验收
|
||||
- 单元:HMAC 随机选择、拒绝采样无偏、事务一致性、幂等与重试。
|
||||
- 集成:支付→即时报奖→收据校验;定时人数不足→退款+送券。
|
||||
- 文档:Swagger 补充所有请求/响应体;明确“禁止 N 分钟相对定时”,仅允许绝对时间戳。
|
||||
|
||||
## 交付清单
|
||||
- 策略模块、管理端与 APP 端 API、哈希凭证、定时结算器、退款日志与发券流程、测试与文档。
|
||||
@ -1,35 +0,0 @@
|
||||
## 目标
|
||||
- 在 APP 端新增接口,直接返回当前登录用户在 `user_inventory` 表中的分页数据,用于“用户中奖数据”查询。
|
||||
|
||||
## 接口设计
|
||||
- 路径:`GET /api/app/users/{user_id}/inventory`
|
||||
- 鉴权:`AppTokenAuthVerify`,以会话 `ctx.SessionUserInfo().Id` 为准(参考 `internal/api/user/points_app.go:47`)。
|
||||
- 入参(query):`page`(默认1)、`page_size`(默认20,最大100)。
|
||||
- 出参(JSON):
|
||||
- `page`、`page_size`、`total`
|
||||
- `list`:元素为 `model.UserInventory`,字段:`id,user_id,product_id,order_id,activity_id,reward_id,status,remark,created_at,updated_at`。
|
||||
|
||||
## 数据来源
|
||||
- 直接读取 `user_inventory` 表(模型:`internal/repository/mysql/model/user_inventory.gen.go:13`)。
|
||||
- 分页与排序:按 `id DESC`,与管理端一致(参考 `internal/api/admin/users_admin.go:246-256` 的资产列表实现风格)。
|
||||
|
||||
## 实现方案
|
||||
1. 新增 `internal/api/user/inventory_app.go`:
|
||||
- 定义 `listInventoryRequest/listInventoryResponse`(`list` 为 `[]*model.UserInventory`)。
|
||||
- `handler.ListUserInventory()`:
|
||||
- 绑定分页参数→会话用户 ID→在 `h.readDB.UserInventory` 上过滤 `user_id` 后 `Count` 与 `Find`(`Order(ID.Desc)`、`Offset/Limit`)。
|
||||
- 组装分页响应并 `ctx.Payload`。
|
||||
- Swagger 注释与现有风格一致(参考 `internal/api/user/coupons_app.go:34-46`)。
|
||||
2. 路由注册(`internal/router/router.go:236` APP 认证组):
|
||||
- `appAuthApiRouter.GET("/users/:user_id/inventory", userHandler.ListUserInventory())`
|
||||
|
||||
## 测试要点
|
||||
- 正常:用户存在记录,分页与 `total` 正确。
|
||||
- 空数据:返回 `list=[]`、`total=0`。
|
||||
- 边界参数:`page≤0`→重置为 1;`page_size` 超上限→重置为 100。
|
||||
- 鉴权:忽略路径 `user_id`,以会话用户为准。
|
||||
|
||||
## 参考代码位置
|
||||
- 模型定义:`internal/repository/mysql/model/user_inventory.gen.go:13`。
|
||||
- 路由分组:`internal/router/router.go:236`。
|
||||
- 鉴权风格:`internal/api/user/points_app.go:47`。
|
||||
@ -1,56 +0,0 @@
|
||||
## 结论
|
||||
- 不需要单独开新接口;在现有“统一接口”体系内,通过活动类型与策略插件即可兼容「一番赏」。
|
||||
- 做法:以统一的资源与动作(创建/生成/列表/抽取/验证)为主干,按活动类型切换具体策略与字段含义。
|
||||
|
||||
## 统一接口适配方案
|
||||
- 活动类型标识:在 `raffle_event` 增加 `activity_type`(如 `ichiban_position`) 与 `capabilities`(如 `choice_input=position`、`commit_reveal=true`)。
|
||||
- 统一端点保持:
|
||||
- 管理端:`POST /admin/events`、`POST /admin/events/{id}/generate`、`POST /admin/events/{id}/activate`、`POST /admin/events/{id}/close`
|
||||
- 用户端:`GET /events/{id}`、`GET /events/{id}/choices`、`POST /events/{id}/draw`、`GET /events/{id}/verify`
|
||||
- 字段语义按类型切换:
|
||||
- `GET /events/{id}/choices`:一番赏返回可选 `slotIndex[]`;其他玩法可能返回空/不同结构。
|
||||
- `POST /events/{id}/draw`:一番赏请求体携带 `slotIndex`;其他玩法携带各自所需的 `choice` 或为空(纯随机)。
|
||||
- `GET /events/{id}/verify`:若 `commit_reveal=true`,统一返回 `salt` 与 `commitment_root`;无承诺玩法返回空/禁用。
|
||||
|
||||
## 策略插件化
|
||||
- 定义通用接口 `ActivityPolicy`:
|
||||
- `generate(event, prize_types)` 预生成逻辑
|
||||
- `presentChoices(event)` 返回用户可选项
|
||||
- `validateChoice(event, choice)` 校验输入
|
||||
- `draw(event, user, choice, tx)` 开奖事务
|
||||
- `verify(event)` 承诺揭示
|
||||
- 为「一番赏」实现 `IchibanPositionPolicy`;其他玩法实现各自策略,统一由路由/服务层按 `activity_type` 分派。
|
||||
|
||||
## 数据与契约最小改动
|
||||
- 数据库:
|
||||
- `raffle_event` 增:`activity_type`、`capabilities(json)`、`commitment_root`、`salt_hash`(适用于 commit-reveal 类型)
|
||||
- 复用 `prize_slot`、`user_draw`;无需新表,仅根据类型解释 `slot_index` 的含义。
|
||||
- 接口契约:
|
||||
- `GET /events/{id}` 增加 `activityType` 与 `capabilities` 字段,前端可按能力渲染。
|
||||
- 统一错误码:`CHOICE_INVALID`、`CHOICE_CONFLICT`、`EVENT_CLOSED` 等适用于所有玩法。
|
||||
- 幂等与并发控制沿用统一实现。
|
||||
|
||||
## 前端适配
|
||||
- 读取 `capabilities.choice_input`:
|
||||
- `position`:渲染位置网格并调用统一 `choices` 与 `draw(slotIndex)`。
|
||||
- 其他类型:按能力渲染不同输入组件或隐藏选择入口(纯随机)。
|
||||
- 统一交互:下单/支付/抽取/结果页/分享逻辑一致,仅差异在“选择输入”。
|
||||
|
||||
## 公平与验证统一化
|
||||
- 对支持承诺的玩法统一启用 `commit-reveal`:
|
||||
- 管理端在 `generate` 返回或活动页显示 `commitment_root`;
|
||||
- 关闭活动时 `verify` 公开 `salt` 与必要证明;
|
||||
- 不支持承诺的玩法该端点返回空或 404。
|
||||
|
||||
## 测试与回归
|
||||
- 策略切换回归:对 N 种玩法执行同一套接口用例,确保契约不变。
|
||||
- 并发/幂等:统一覆盖;一番赏专项测试“同一位置并发争抢”。
|
||||
- 能力探测:前端依据 `capabilities` 自适应渲染的快照测试。
|
||||
|
||||
## 迁移与兼容
|
||||
- 现有客户端无需改路由;仅根据 `capabilities` 决定是否展示“选择位置”。
|
||||
- 服务端新增策略模块与少量字段;旧玩法默认 `commit_reveal=false`、`choice_input=none`。
|
||||
|
||||
## 验收准则
|
||||
- 统一端点下,所有玩法均可正常运行。
|
||||
- 「一番赏」玩法可完成预生成、选择位置、开奖、并发安全、事后可验证。
|
||||
@ -1,92 +0,0 @@
|
||||
## 范围与原则
|
||||
|
||||
* 仅完善用户端(管理端与其 API 保持不变)。
|
||||
|
||||
* 目标:用户可在 App 端对其奖品资产执行:邀请他人填写地址、兑换积分;若用户已有默认地址,可在 App 端提交发货申请(生成待发货记录),后续由管理端流程发货。
|
||||
|
||||
## 用户端功能
|
||||
|
||||
* 我的资产列表与详情
|
||||
|
||||
* 展示每条资产的当前状态:`持有`、`共享待地址`、`待发货`、`已发货`、`已兑换`。
|
||||
|
||||
* 操作区:
|
||||
|
||||
* `邀请他人填写地址`:生成分享链接/二维码,显示有效期与复制按钮;允许撤销与重新生成。
|
||||
|
||||
* `兑换为积分`:弹窗确认兑换规则与积分值,成功后资产变为“已兑换”。
|
||||
|
||||
* `申请发货`(可选):当用户已有默认地址时展示;提交后资产进入“待发货”。
|
||||
|
||||
* 分享落地页(公域 H5/Web)
|
||||
|
||||
* 通过一次性 `share_token` 打开表单,填写地址后提交;成功页提示“已提交,等待发货”。
|
||||
|
||||
* 链接过期/撤销/已消费时展示对应状态页。
|
||||
|
||||
## 后端(仅用户端与公域接口)
|
||||
|
||||
* 创建共享地址请求(App 端)
|
||||
|
||||
* `POST /api/app/inventory/{inventory_id}/address-share/create`
|
||||
|
||||
* 返回:`share_url`、`expires_at`、`qrcode`(可选)与当前资产状态。
|
||||
|
||||
* `POST /api/app/inventory/{inventory_id}/address-share/revoke`
|
||||
|
||||
* 撤销未使用的请求,状态改为 `revoked`。
|
||||
|
||||
* 申请发货(App 端,可选)
|
||||
|
||||
* `POST /api/app/inventory/{inventory_id}/request-shipping`
|
||||
|
||||
* 条件:用户存在默认地址;未兑换/未发货。
|
||||
|
||||
* 行为:创建 `ShippingRecords` 为“待发货”,后续由管理端更新物流信息。
|
||||
|
||||
* 兑换积分(App 端)
|
||||
|
||||
* `POST /api/app/inventory/{inventory_id}/redeem-to-points`
|
||||
|
||||
* 规则:依据奖品配置(如 `exchange_points_amount`);写 `user_points_ledger` 动作 `REDEEM_REWARD`,更新 `user_points`;资产标记为“已兑换”。
|
||||
|
||||
## 数据与安全
|
||||
|
||||
* 共享请求模型:`shipping_shared_address_requests`(一次性 `share_token`、有效期、状态、审计字段、地址快照或 `user_address_id`)。
|
||||
|
||||
* 状态机:`OWNED`→`AWAITING_ADDRESS_SHARED`→`SHIPPING_PENDING`→`SHIPPED`;或 `OWNED`→`REDEEMED`。
|
||||
|
||||
* 幂等与并发:事务校验资产状态;链接一次性消耗;重复操作返回已处理错误码。
|
||||
|
||||
* 审计:记录创建人、撤销、提交人(可匿名)、IP/UA;所有动作可追踪。
|
||||
|
||||
## 前端实现(App)
|
||||
|
||||
* App 页面
|
||||
|
||||
* `资产详情页`:按钮与状态展示、结果提示、列表刷新;分享弹窗含链接、二维码、有效期、撤销。
|
||||
|
||||
* `积分兑换弹窗`:展示规则与额度,确认后调用兑换 API。
|
||||
|
||||
* `申请发货`:若存在默认地址,直接申请;否则引导“邀请他人填写地址”或“去设置默认地址”。
|
||||
|
||||
## 测试与验收
|
||||
|
||||
* 单元测试:共享请求生命周期(创建/撤销/过期/提交)、资产状态流转、兑换与申请发货幂等并发。
|
||||
|
||||
* 集成测试:App 端创建链接→公域表单提交→资产进入待发货;重复提交与过期处理;兑换后禁止申请发货。
|
||||
|
||||
* 验收:用户端操作闭环生效;状态与流水正确;无重复副作用;管理端无需改动即可继续履约。
|
||||
|
||||
## 文档与 Swagger
|
||||
|
||||
* 更新 App 与公域接口说明、错误码、状态枚举与审计字段;保留管理端文档不变。
|
||||
|
||||
* 执行时按 6A 规范生成 ALIGNMENT/CONSENSUS/DESIGN/TASK/ACCEPTANCE/FINAL 文档并持续维护。
|
||||
|
||||
## 上线与回滚
|
||||
|
||||
* 先灰度用户端;监控链接使用量、失败率与发货效率。
|
||||
|
||||
* 如需回滚,关闭公域入口并撤销未消费请求;资产状态回退并保留审计。
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
## 目标
|
||||
- 将现有单件发货接口扩展为批量接口,支持一次提交多个资产 ID(如:{"inventory_ids":[52,53,...]})。
|
||||
- 复用现有发货能力(默认地址、资产状态迁移、备注标记),保证幂等与错误可追踪。
|
||||
|
||||
## 现状
|
||||
- 单件接口:`POST /api/app/users/{user_id}/inventory/request-shipping`,请求体 `{ inventory_id }`
|
||||
- 处理器位置:internal/api/user/request_shipping_app.go:30-47
|
||||
- 服务层:`RequestShipping(ctx, userID, inventoryID)` 设置资产状态为已申请发货并记录备注
|
||||
- 代码位置:internal/service/user/address_share.go:120-126(`UPDATE user_inventory SET status=3, ... remark+='|shipping_requested' WHERE status=1`)
|
||||
|
||||
## 接口设计
|
||||
- 新增:`POST /api/app/users/{user_id}/inventory/request-shipping-batch`
|
||||
- 请求体:
|
||||
- `inventory_ids` 数组,必填,长度 1–100,去重后处理
|
||||
- `address_id` 可选;若不提供则使用用户默认地址
|
||||
- 返回体:
|
||||
- `address_id` 实际使用的地址 ID
|
||||
- `success_ids` 已成功提交的资产 ID 列表
|
||||
- `skipped` 数组:[{ id, reason }](如 not_found、not_owned、invalid_status、already_requested)
|
||||
- `failed` 数组:[{ id, reason }](如 DB 错误等)
|
||||
|
||||
## 行为与规则
|
||||
- 地址选择
|
||||
- 若提供 `address_id`:校验属于该用户且有效;否则返回 400
|
||||
- 若未提供:读取默认地址;不存在则返回 400(沿用现有单件逻辑)
|
||||
- 资产校验(逐个)
|
||||
- 必须属于该用户
|
||||
- `status=1`(可发货)才处理;`status=3`(已申请)则标记为 `already_requested` 并 skip(幂等)
|
||||
- 不存在或不属于该用户 → `skipped.not_owned/not_found`
|
||||
- 幂等性
|
||||
- 若 remark 已包含 `shipping_requested` 或状态为 3,则视为已处理;加入 `skipped` 并不报错
|
||||
- 原子性
|
||||
- 批量以“逐条子事务”处理(每条调用服务层更新),确保单条失败不影响其他条目;最终返回成功/跳过/失败三类列表
|
||||
- 审计
|
||||
- 在 `user_points_ledger` 无需记录(该流程与积分无关);如需审计可在后续增加 `user_operations` 表留痕
|
||||
|
||||
## 服务层扩展
|
||||
- 新增:`RequestShippings(ctx, userID int64, inventoryIDs []int64, addressID *int64) (addrID int64, success []int64, skipped []struct{id int64; reason string}, failed []struct{id int64; reason string}, err error)`
|
||||
- 内部:
|
||||
- 若 `addressID==nil`,读取默认地址(沿用单件方法)
|
||||
- 循环:校验→调用现有 `RequestShipping(ctx, userID, inventoryID)`;捕获错误进行分流
|
||||
- 可选优化:对同一用户的多件,合并一次地址校验;对 DB 更新使用独立事务(已在现有方法内处理)
|
||||
|
||||
## 处理器实现
|
||||
- 新增处理器:`RequestShippingBatch()`:
|
||||
- 位置:internal/api/user/request_shipping_app.go(或新文件 `request_shipping_batch_app.go`)
|
||||
- 解析 `inventory_ids`(去重、长度限制);解析可选 `address_id`
|
||||
- 调用服务层批量方法,组装响应
|
||||
- 统一错误码:参数错误 `code.ParamBindError`,无地址 `10021`(沿用或新增),其他子项错误填入 `failed` 字段
|
||||
|
||||
## 错误码与返回示例
|
||||
- 400 参数错误:`{"code":10023,"message":"invalid inventory_ids"}`
|
||||
- 200 成功+部分跳过:
|
||||
```
|
||||
{
|
||||
"address_id": 888,
|
||||
"success_ids": [52, 53],
|
||||
"skipped": [{"id": 54, "reason": "already_requested"}],
|
||||
"failed": []
|
||||
}
|
||||
```
|
||||
|
||||
## 测试用例
|
||||
- 有默认地址,提交 1、N 个有效资产 → 全成功
|
||||
- 混合:包含非本用户、已申请、不存在 → 分别进 `skipped`
|
||||
- 指定 address_id 非本用户 → 400
|
||||
- 无默认地址且未指定 address → 400
|
||||
- 幂等:重复提交相同资产 → 均进入 `already_requested`
|
||||
|
||||
## 兼容性
|
||||
- 不改动现有单件接口;前端可增设批量勾选后调用新接口
|
||||
- DB 无结构变化;仍依赖 `user_inventory.status` 与 `remark` 标记
|
||||
|
||||
## 交付内容
|
||||
- 新增批量处理器与服务方法
|
||||
- Swagger 注释与接口文档
|
||||
- 单元测试:服务层批量逻辑、处理器参数校验
|
||||
|
||||
请确认按此方案实施,我将立即落地代码、接口与测试。
|
||||
@ -1,68 +0,0 @@
|
||||
## 现状与问题
|
||||
- 当前 APP 抽奖接口:`POST /api/app/activities/:activity_id/issues/:issue_id/draw`,处理函数 `internal/api/activity/draw_app.go:27-46`,服务侧使用 `internal/service/activity/draw_execute.go:12-89` 的可验证随机抽样(HMAC-SHA256 + 拒绝采样)。
|
||||
- 现有实现按期(issue)维度抽取奖励,已具备收据与随机性证明,但不同活动类型(分类/玩法)未来可能有差异化前置校验、选择规则与奖励发放流程。
|
||||
- 需求:我们将有很多活动,接口到底通用还是分活动?
|
||||
|
||||
## 结论选择
|
||||
- 接口保持通用(单一端点),后端采用策略模式按活动类别/配置差异化实现逻辑。这样:
|
||||
- 前端保持统一调用与响应结构,降低复杂度与版本碎片。
|
||||
- 后端可插拔扩展不同活动的校验、选择、奖励发放与效果管道,避免复制粘贴与接口膨胀。
|
||||
- 与现有 `Receipt` 结构兼容,保留可验证性。
|
||||
|
||||
## 技术方案
|
||||
- 保留通用端点:`POST /api/app/activities/:activity_id/issues/:issue_id/draw`。
|
||||
- 引入“活动策略”接口(示例命名):
|
||||
- `ActivityDrawStrategy`:
|
||||
- `PreChecks(ctx, activity, issue, user) error`(余额/订单/库存/状态/时间窗/限频)
|
||||
- `SelectItem(ctx, issue, baseSelector) (selectedRewardID, proof)`(可复用当前 HMAC 随机选择器作为 `baseSelector`,策略可注入权重或过滤规则)
|
||||
- `GrantReward(ctx, user, rewardID) error`(扣减数量、落库收据/日志、发放道具/实物/积分/券)
|
||||
- `PostEffects(ctx, user, activity, issue, rewardID)`(道具效果、称号、积分等联动)
|
||||
- 策略注册与路由:
|
||||
- 基于 `activity.activity_category_id` 或活动配置选择策略;默认策略沿用当前实现(完全兼容)。
|
||||
- 效果管道:
|
||||
- 抽奖前置/后置效果处理(如限时加权、Boss 限制、用户卡片效果),设计为中间件式管道,便于组合。
|
||||
- 并发与一致性:
|
||||
- 引入期级别乐观扣减或分布式锁(后续落地),确保高并发下数量与日志一致。
|
||||
- 支持 `X-Idempotency-Key` 防重(后续扩展),避免重复下单/重复抽取。
|
||||
- 监控与审计:
|
||||
- 统一埋点:抽奖尝试、成功、失败原因、库存变化、用户画像。
|
||||
- 审计日志与追踪 ID 与收据关联。
|
||||
|
||||
## API 契约
|
||||
- 请求:保持不变;可新增可选头 `X-Idempotency-Key`。
|
||||
- 响应:保持 `receipt` 字段结构;策略仅影响内部选择/发放过程,不破坏契约。
|
||||
- 错误码:标准化前置校验失败(余额不足、活动未开始、库存不足、限频)与系统错误。
|
||||
|
||||
## 数据与规则
|
||||
- 数量规则:`original_qty` 为期初总数,`quantity` 当前剩余;不限量为 `-1`,选择与发放逻辑需正确处理。
|
||||
- 收据:继续包含随机性证明与所选项快照,具备可验证性。
|
||||
- 扣减与发放:在 `GrantReward` 中统一实现(现阶段代码只返回收据,发放与扣减建议纳入策略实现)。
|
||||
|
||||
## 变更范围
|
||||
1. 新增 `internal/service/activity/strategy/` 策略接口与默认实现;注册表按分类 ID 选择策略。
|
||||
2. 在 `ExecuteDraw()` 增强:
|
||||
- 解析活动与用户上下文,调用策略 `PreChecks`→`SelectItem`→`GrantReward`→`PostEffects`。
|
||||
- 默认策略复用当前 `draw_execute.go` 的随机选择器。
|
||||
3. `draw_app.go` 维持统一端点,增加对用户上下文的传递(如需要),契约不变。
|
||||
|
||||
## 分阶段实施
|
||||
1. 引入策略接口与默认策略(不改变行为)。
|
||||
2. 接入活动类别路由与注册(读取 `ActivityCategoryID`)。
|
||||
3. 将扣减与发放迁移到策略中,补充事务与并发控制。
|
||||
4. 增强效果管道,接入道具卡、称号、积分的已有模块。
|
||||
5. 增加监控与限频、幂等,完善错误码。
|
||||
6. 编写单元测试与集成测试:
|
||||
- 权重选择正确性与拒绝采样无偏。
|
||||
- 不限量与零库存边界。
|
||||
- 并发扣减一致性。
|
||||
- 幂等与限频。
|
||||
|
||||
## 验收标准
|
||||
- 单一抽奖端点适配 2+ 类活动策略,无需前端切换接口。
|
||||
- 收据结构与随机性证明保持一致,验证脚本通过。
|
||||
- 库存扣减与奖励发放在并发下无不一致;关键路径有审计日志。
|
||||
- 测试覆盖关键流程与边界条件。
|
||||
|
||||
## 后续扩展
|
||||
- 按活动类型开放“策略配置”与“玩法参数”在管理端,可动态调整权重、效果、限频。
|
||||
- 如某玩法确有完全不同交互(例如合成/多次抽/队列结算),再考虑单独端点,但仍优先在通用端点层面通过参数与策略实现。
|
||||
@ -1,71 +0,0 @@
|
||||
## 执行逻辑(目标态)
|
||||
|
||||
### 参与下单(仅创建订单,不开奖)
|
||||
- 接口:`POST /api/app/lottery/join`
|
||||
- 入参:`activity_id`、`issue_id`、`count`(默认1,≤N)、可选 `client_nonce`
|
||||
- 流程:
|
||||
- 查询活动单价 `price_draw`;计算 `total_amount = price_draw * count`
|
||||
- 查询积分余额;计算需积分 `needPts = ceil(total_amount / 10)`;实际扣减 `usePts = min(balance, needPts)`
|
||||
- 写订单:`draw_count = count`、`PointsAmount = usePts * 10`、`ActualAmount = total_amount - PointsAmount`、`Status=1`
|
||||
- 当 `ActualAmount == 0` → 将订单置为已支付(`Status=2`、`PaidAt=now`),但不开奖
|
||||
- 返回:`order_no`、`draw_mode`、`count`、金额与抵扣细节、`queued`
|
||||
|
||||
### 预下单(微信 JSAPI)
|
||||
- 接口:`POST /api/app/pay/wechat/jsapi/preorder`
|
||||
- 校验:订单属于当前用户且 `Status=1`
|
||||
- 返回:`wx.requestPayment` 参数;记录 `PaymentPreorders`
|
||||
|
||||
### 支付回调(入账)
|
||||
- 接口:`POST /api/pay/wechat/notify`
|
||||
- 行为:验签→记录交易→更新订单 `Status=2(PAID)`、`PaidAt`;幂等事件处理
|
||||
- 触发:即时模式下调用 `DrawProcessor`;计划模式下等待调度
|
||||
|
||||
### 抽奖处理器(按订单维度执行)
|
||||
- 输入:`order_id/order_no/activity_id/issue_id/draw_mode/draw_count`
|
||||
- 即时模式:
|
||||
- 读取已抽次数 `n = count(ActivityDrawLogs where order_id)`
|
||||
- 循环 `i=n+1..draw_count`: `SelectItem`→`GrantReward`→`Create(ActivityDrawLogs{draw_index=i})`
|
||||
- `completed == draw_count` → 订单标记 `SETTLED`
|
||||
- 计划模式:
|
||||
- 到 `scheduled_time` 统一处理;参与不足自动退款(先积分、后微信金额),保留现有逻辑
|
||||
- 幂等:以上循环按“每订单已存在日志条数”补齐,事务化执行
|
||||
|
||||
### 统一结果轮询(基于 order_no)
|
||||
- 接口:`GET /api/app/lottery/result?order_no=...`
|
||||
- 返回:
|
||||
- `status`: `pending|paid_waiting|settled|refunded`
|
||||
- `draw_mode`、`count`、`completed`、`results[{reward_id,reward_name,level,draw_index}]`
|
||||
- `receipt`:`issue_id/seed_version/timestamp/nonce/signature/algorithm`
|
||||
- `nextPollMs`(建议2s-5s)
|
||||
|
||||
## 改造点与文件定位
|
||||
- 修改 `JoinLottery`:internal/api/activity/lottery_app.go
|
||||
- 增加 `count` 入参;仅下单与积分抵扣;免支付不开奖
|
||||
- 保持 `WechatJSAPIPreorder` 不变:internal/api/user/pay_wechat_app.go
|
||||
- 扩展回调:在 `WechatNotify` 支付入账后触发即时模式订单的 `DrawProcessor`:internal/api/pay/wechat_notify.go
|
||||
- 定时调度按订单 `draw_count` 抽取或退款:internal/service/activity/scheduler.go
|
||||
- 新增统一查询接口 `GET /api/app/lottery/result`:新增 handler(internal/api/activity/lottery_result_app.go),挂载到 APP 认证组(internal/router/router.go)
|
||||
|
||||
## 数据库变更
|
||||
- `orders` 新增:`draw_count INT NOT NULL DEFAULT 1`(可选:`draw_mode` 冗余)
|
||||
- `activity_draw_logs` 可选新增:`draw_index INT NULL`(记录第几次抽取)
|
||||
- 迁移默认值:历史订单填充 `draw_count = 1`
|
||||
|
||||
## 边界与异常
|
||||
- 积分充足:免支付→订单直接 `PAID`;由处理器负责开奖
|
||||
- 积分不够/无积分:需支付→入账后再开奖
|
||||
- 计划不足:自动退款(积分与金额),订单 `REFUNDED`
|
||||
- 并发与幂等:以 `order_id` + 已有日志条数控制,所有扣减与发奖在事务中执行
|
||||
|
||||
## 前端调用契约
|
||||
- 流程:`join` → (可选)`preorder` → 支付 → 轮询 `result` 至 `settled|refunded`
|
||||
- 显示:用 `results` 数组展示 N 次抽奖结果;根据 `status` 渲染进度与状态
|
||||
|
||||
## 测试与验收
|
||||
- 单元:积分计算(充足/不够/为0)、N 抽即时/计划模式日志与发奖一致
|
||||
- 集成:支付回调幂等、计划不足退款、统一查询状态流转
|
||||
- 验收:同一 `order_no` 在两模式下的查询端点返回一致结构与正确状态机;库存扣减准确;退款日志完整
|
||||
|
||||
## 注意事项
|
||||
- `client_nonce`(可选):用于防重复参与提交的客户端幂等键;不影响支付与开奖
|
||||
- 安全:JWT校验、签名凭证不泄露种子;所有金额与积分变更审计入库
|
||||
@ -1,56 +0,0 @@
|
||||
## 目标与原则
|
||||
- 禁用“积分直接购买/抵扣”用于抽奖订单;保留积分兑换优惠券/商品能力。
|
||||
- 优惠券支持“部分使用”,且订单累计抵扣≤总价的50%。
|
||||
- 不新增任何新表:仅在`user_coupons`增加一列`balance_amount`,订单侧复用`orders.remark`记录使用明细。
|
||||
|
||||
## 数据变更(最小)
|
||||
- `user_coupons`新增:`balance_amount`(分,默认NULL/0)。
|
||||
- 直减金额券:发券时初始化为模板面值;使用时扣减;余额为0时`status=2/used_at`。
|
||||
- 满减/折扣券:不产生余额(保持一次性)。
|
||||
- 不新增`order_coupons`表;订单使用明细写入`orders.remark`(结构化片段)。
|
||||
|
||||
## 使用明细编码(remark复用)
|
||||
- 约定片段:`|c:<user_coupon_id>:<applied_amount>`,可重复出现多段代表多券。
|
||||
- 示例:票价50,封顶25,使用面值100券→`|c:12345:2500`(单位分),用户券余额从10000降至7500。
|
||||
- 退款:解析`orders.remark`,逐段恢复对应`user_coupons.balance_amount += applied_amount`;余额>0则`status=1/clear used_at`。
|
||||
|
||||
## 后端改动点
|
||||
- 抽奖下单(internal/api/activity/lottery_app.go:110–176)
|
||||
- 移除积分抵扣分支;`order.PointsAmount`保持0(新订单)。
|
||||
- 计算`total`与`cap = total * rate(默认0.5)`;`remaining_cap = cap - order.DiscountAmount`。
|
||||
- 直减金额券:`applied = min(user_coupons.balance_amount, remaining_cap)`;更新`order.DiscountAmount/ActualAmount`,扣减余额并按规则更新`status/used_at`;在`orders.remark`追加`|c:<id>:<applied>`。
|
||||
- 满减/折扣券:计算规则折扣`rule_discount`,`applied = min(rule_discount, remaining_cap)`;一次性使用并在`remark`追加同样片段(余额不维护)。
|
||||
- 券发放(internal/service/user/coupon_add.go:11–55)
|
||||
- 直减券初始化`balance_amount=discount_value`;其他类型`balance_amount=NULL/0`。
|
||||
- 退款(internal/api/admin/pay_refund_admin.go:189–228;internal/api/pay/wechat_notify.go:28–174)
|
||||
- 解析`orders.remark`恢复直减券余额与状态;同步回退`orders.discount_amount/ActualAmount`。
|
||||
|
||||
## 配置与校验
|
||||
- `coupons_max_discount_rate`(默认0.5)。
|
||||
- 兼容多券叠加:逐券按余额与剩余封顶扣减,超过封顶的券不使用;`remark`记录每次使用的`applied_amount`。
|
||||
|
||||
## 前端改动
|
||||
- 结算页:
|
||||
- 展示“最多抵扣50%”;支持金额券余额显示与预估本次可抵扣金额。
|
||||
- 当券面值大于封顶,自动按封顶计算应付并提示“剩余XX元券余额保留”。
|
||||
- 管理端订单详情:展示已应用的`c:<id>:<applied>`明细与用户券余额。
|
||||
|
||||
## 文档与错误码
|
||||
- Swagger:更新`/api/app/lottery/join`;保留/新增积分兑换券/商品接口文档。
|
||||
- 错误码:`coupon_exceeds_cap`、`coupon_no_balance`、`insufficient_points`、`duplicate_redeem`。
|
||||
|
||||
## 迁移与兼容
|
||||
- 历史未使用直减券:初始化`balance_amount=discount_value`。
|
||||
- 已使用券:`balance_amount=0/NULL`。
|
||||
- 无新表;仅一列新增与`remark`编码扩展。
|
||||
|
||||
## 测试示例
|
||||
- 面值100券、票价50、封顶25→应付25,`remark`含`|c:<id>:2500`,余额7500。
|
||||
- 多券叠加至封顶;超过部分不使用;退款逐段恢复余额。
|
||||
- 新订单不产生积分抵扣流水。
|
||||
|
||||
## 变更文件参考
|
||||
- 抽奖下单:internal/api/activity/lottery_app.go:110–176。
|
||||
- 发券:internal/service/user/coupon_add.go:11–55。
|
||||
- 退款:internal/api/admin/pay_refund_admin.go:189–228;internal/api/pay/wechat_notify.go:28–174。
|
||||
- 用户券模型:internal/repository/mysql/model/user_coupons.gen.go(新增`balance_amount`)。
|
||||
@ -1,42 +0,0 @@
|
||||
## 检查项
|
||||
- 路由与前端:确认管理端生成/摘要接口使用活动级路径
|
||||
- 生成:`POST /api/admin/activities/:activity_id/commitment/generate`
|
||||
- 摘要:`GET /api/admin/activities/:activity_id/commitment/summary`
|
||||
- 前端 API 指向上述路径(不是旧的 `/ichiban/...`)
|
||||
- 数据迁移:核实 `activities` 已存在承诺字段(algo/seed_master/seed_hash/state_version/items_root)且成功执行
|
||||
- 服务实现:检查 ActivityCommitmentService
|
||||
- 生成:写入 `seed_master`、`seed_hash`、`state_version`;当前 items_root 为 NULL,需要按最新设计计算
|
||||
- 摘要:从 `activities` 读取版本与算法,`has_seed` 通过 `LENGTH(seed_master)` 判断
|
||||
- 策略消费:一番赏映射读取活动承诺种子(非期级表),若缺承诺返回明确错误
|
||||
|
||||
## 修正与实现(不兼容旧期级)
|
||||
- 计算并写入 `commitment_items_root`
|
||||
- 规则:对活动下各期的奖励配置,按每个奖励的 `original_qty` 构造 slots 数组(长度为 N),计算 `items_root = SHA256(JSON(slots))`
|
||||
- 写入 `activities.commitment_items_root`,用于事后验证映射根
|
||||
- 版本递增:每次生成承诺 `state_version+1`
|
||||
- 一番赏映射页承诺校验
|
||||
- 进入列表或详情时检查活动承诺存在(`has_seed=true`);无承诺返回“请在活动管理生成承诺”提示
|
||||
- 活动管理显示
|
||||
- 活动列表新增“承诺版本”列(已加),确保加载摘要后填充版本
|
||||
- 活动编辑弹窗顶部承诺信息卡片(已加),显示 `seed_version/algo/状态`
|
||||
|
||||
## 验证流程
|
||||
- 生成承诺后,确认 `activities` 中:
|
||||
- `LENGTH(commitment_seed_master)=32`、`LENGTH(commitment_seed_hash)=32`
|
||||
- `commitment_state_version` 递增
|
||||
- `LENGTH(commitment_items_root)>0`
|
||||
- 前端列表显示版本值;映射页顶部显示活动承诺版本
|
||||
- 并发占位与映射不变:种子存在即可按位置开奖;缺承诺时前端/后端均提示
|
||||
|
||||
## 接口与 curl
|
||||
- 生成:`curl -X POST 'http://127.0.0.1:9991/api/admin/activities/<id>/commitment/generate' -H 'Authorization: Bearer TOKEN'`
|
||||
- 摘要:`curl 'http://127.0.0.1:9991/api/admin/activities/<id>/commitment/summary' -H 'Authorization: Bearer TOKEN'`
|
||||
- DB 校验:`SELECT LENGTH(commitment_seed_master), LENGTH(commitment_seed_hash), LENGTH(commitment_items_root), commitment_state_version FROM activities WHERE id=<id>;`
|
||||
|
||||
## 交付修改点
|
||||
- 后端:完善 ActivityCommitmentService 的 items_root 计算与写入(活动维度)
|
||||
- 前端:确保活动列表与编辑对话框显示承诺;映射页在无承诺时给出指导提示
|
||||
|
||||
## 说明
|
||||
- 全量切换到活动级承诺;不读取旧 `issue_random_commitments`
|
||||
- 保持统一接口与策略,后续抽奖逻辑统一使用活动承诺
|
||||
@ -1,50 +0,0 @@
|
||||
## 目标范围
|
||||
- 在“创建活动”时新增2个可配置属性:`是否可以使用道具卡`、`是否可以使用优惠券`。
|
||||
- 属性保存到`activities`表,默认启用(向后兼容现有活动)。
|
||||
- 管理端创建/编辑表单与接口支持读写这两个属性;详情接口返回这两个属性。
|
||||
- 暂不实现具体“应用道具卡/优惠券”的核销/抵扣逻辑,仅提供全局开关位,后续业务在下单/抽奖流程使用该开关位做限制。
|
||||
|
||||
## 数据库与模型
|
||||
- 新增`activities`表列:
|
||||
- `allow_item_cards` TINYINT(1) NOT NULL DEFAULT 1 注释“是否允许使用道具卡(1是,0否)”。
|
||||
- `allow_coupons` TINYINT(1) NOT NULL DEFAULT 1 注释“是否允许使用优惠券(1是,0否)”。
|
||||
- 对齐模型:在`Activities`结构体中新增同名字段并JSON映射(internal/repository/mysql/model/activities.gen.go:15)。
|
||||
- 启动时DDL修复:仿照`main.go`已有字段修复逻辑添加这两列(main.go:38-58)。
|
||||
|
||||
## 后端接口改动
|
||||
- 管理端创建/修改请求增加字段:
|
||||
- `createActivityRequest`/`modifyActivityRequest`新增`allow_item_cards`、`allow_coupons`(int或bool,保持与`is_boss`的风格一致为`int32`的0/1)。位置参考internal/api/admin/activities_admin.go:14-34, 156-176。
|
||||
- 传递至服务层:
|
||||
- `CreateActivityInput`/`ModifyActivityInput`新增同名字段(internal/service/activity/activity.go:110-154)。
|
||||
- `CreateActivity`设置字段,`ModifyActivity`的`updates`包含这两列(internal/service/activity/activity_create.go:13-55, internal/service/activity/activity_modify.go:11-48)。
|
||||
- 详情返回:
|
||||
- 管理端与APP端`activityDetailResponse`新增两个字段并赋值(internal/api/admin/activities_admin.go:41-62 与 internal/api/activity/activities_app.go:41-62)。
|
||||
- DrawConfig无需改动(仅开奖相关,internal/service/activity/draw_config_save.go:9-16)。
|
||||
|
||||
## 前端管理端改动
|
||||
- 创建向导页`wizard/index.vue`:
|
||||
- 在“活动基本信息”区域新增两个`ElSwitch`,绑定`activityForm.allow_item_cards`、`activityForm.allow_coupons`,默认`true`。
|
||||
- `submitActivity()`的`params`包含这两个字段(web/admin/src/views/activity/wizard/index.vue:777-804)。
|
||||
- 活动管理页`manage/index.vue`:
|
||||
- 编辑对话框`form`新增两个字段与对应`ElSwitch`,支持修改与展示。
|
||||
- 接口类型与调用:
|
||||
- `web/admin/src/api/adminActivities.ts`的`createActivity`/`updateActivity`入参类型新增`allow_item_cards?`、`allow_coupons?`并透传(adminActivities.ts:3-22, 26-49)。
|
||||
|
||||
## APP端(可选展示)
|
||||
- `GetActivityDetail`响应新增两个布尔位,前端可据此展示“本活动不支持优惠券/道具卡”文案(internal/api/activity/activities_app.go:140-186)。
|
||||
|
||||
## 兼容与默认
|
||||
- 新增列默认值为1,历史活动即视为允许;管理端可随时改为0禁止。
|
||||
- 未提供值的创建请求按默认启用处理。
|
||||
|
||||
## 校验与测试
|
||||
- 后端单测/集成测试:
|
||||
- 创建活动带`allow_*` → 详情返回一致;修改后值变更正确。
|
||||
- 前端回归:
|
||||
- 创建向导能设置并成功保存;编辑页能读取并更新。
|
||||
|
||||
## 后续接入点(不在本次范围,供后续实现)
|
||||
- 下单/支付阶段应用优惠券时:在订单创建前根据活动`allow_coupons`拒绝并提示。
|
||||
- 抽奖策略`PostEffects()`应用道具卡时:根据活动`allow_item_cards`拒绝卡效果并记录原因。
|
||||
|
||||
请确认以上方案,确认后我将按此实现并同步更新相关文档(说明文档、接口文档与前端表单)。
|
||||
@ -1,49 +0,0 @@
|
||||
## 核心原则
|
||||
- 承诺是活动级属性:所有活动均需生成并持有承诺;抽奖只能在承诺存在的活动上执行
|
||||
- 不做旧期兼容:不读取/维护期级承诺表;全量切换到活动级承诺
|
||||
|
||||
## 数据模型(仅 activities)
|
||||
- `activities` 新增字段:
|
||||
- `commitment_algo` VARCHAR(32) 默认 `commit-v1`
|
||||
- `commitment_seed_master` BLOB(活动随机种子,256位)
|
||||
- `commitment_seed_hash` BLOB(`SHA256(seed_master)`)
|
||||
- `commitment_state_version` INT(递增)
|
||||
- `commitment_items_root` BLOB(按玩法需要可填,比如 Ichiban 的 slots 根)
|
||||
- 期级承诺表不再使用(保留但不访问),所有策略仅访问活动级承诺
|
||||
|
||||
## 接口
|
||||
- 生成承诺(活动级):`POST /api/admin/activities/:activity_id/commitment/generate`
|
||||
- 行为:生成随机种子;计算 `seed_hash`;按玩法(如 Ichiban)计算 `items_root`;版本 +1
|
||||
- 承诺概览:`GET /api/admin/activities/:activity_id/commitment/summary`
|
||||
- 返回:`{ seed_version, algo, has_seed, items_root(optional) }`
|
||||
- 抽奖前置校验:策略入口统一校验活动承诺存在;缺失则返回 `COMMITMENT_REQUIRED`
|
||||
|
||||
## 策略消费
|
||||
- Ichiban:
|
||||
- 读取活动级 `seed_master` 作为随机源
|
||||
- 根据期的奖励配置构造 slots(基于 `original_qty`)→ 使用活动种子做确定性洗牌
|
||||
- 保持“位置→奖品”稳定,版本变更后整体映射更新
|
||||
- 其他玩法:
|
||||
- 统一读取活动承诺作为随机源或哈希链起点
|
||||
|
||||
## 前端改造
|
||||
- 活动管理:
|
||||
- 操作列显示“生成承诺”与“承诺概览”(适用于所有活动,不再区分玩法)
|
||||
- 详情/期次页可显示版本号与算法
|
||||
- 一番赏序号映射:只读查看映射;顶部显示当前活动 `seed_version`
|
||||
|
||||
## 抽奖流程
|
||||
- 即时/定时调用策略前:校验活动承诺
|
||||
- 抽奖回执携带:`seed_version`、`algo`
|
||||
|
||||
## 测试与验收
|
||||
- 生成承诺版本递增与哈希一致性
|
||||
- 策略在承诺存在时正常运行、缺失时明确错误
|
||||
- 一番赏映射稳定性(同版本稳定)
|
||||
|
||||
## 上线步骤
|
||||
- 执行 `activities` 字段迁移
|
||||
- 接入生成与概览接口
|
||||
- 改造策略读取活动承诺
|
||||
- 更新前端活动管理显示与按钮
|
||||
- 移除前端/后端期级承诺路径(保留表但不访问)
|
||||
@ -1,45 +0,0 @@
|
||||
# 渠道数据分析功能开发计划
|
||||
|
||||
本计划旨在为渠道管理增加数据分析功能,包括用户增长和付费数据的可视化展示。
|
||||
|
||||
## 1. 需求分析与对齐 (Align)
|
||||
* **目标**: 在渠道列表页增加“分析”入口,点击后展示该渠道的运营数据。
|
||||
* **核心指标**:
|
||||
* **用户增长**: 累计注册用户数、每日新增用户趋势。
|
||||
* **付费数据**: 累计订单数、累计 GMV (销售额)、每日订单/GMV 趋势。
|
||||
* **交互**: 列表页操作栏新增“分析”按钮 -> 弹出模态框/抽屉展示仪表盘。
|
||||
|
||||
## 2. 架构设计 (Architect)
|
||||
|
||||
### 2.1 后端设计
|
||||
* **API 接口**: 新增 `GET /admin/v1/channels/{id}/stats`
|
||||
* **数据源**:
|
||||
* 用户数据: `users` 表 (字段: `created_at`, `channel_id`)
|
||||
* 订单数据: `orders` 表 (需关联 `users` 表,通过 `user_id` 关联,筛选 `channel_id`)
|
||||
* **Service 层**:
|
||||
* 扩展 `ChannelService`,增加 `GetStats(ctx, channelID, timeRange)` 方法。
|
||||
* 使用 GORM 进行聚合查询 (Group by date)。
|
||||
|
||||
### 2.2 前端设计
|
||||
* **组件复用**: 使用现有的 `ArtStatsCard` (指标卡片) 和 `ArtLineChart` (折线图)。
|
||||
* **页面改造**: `web/admin/src/views/operations/channels/index.vue`
|
||||
* 新增“分析”按钮。
|
||||
* 新增弹窗组件,包含数据加载逻辑。
|
||||
|
||||
## 3. 任务拆解 (Atomize)
|
||||
|
||||
### 3.1 后端开发
|
||||
1. **Service 实现**: 在 `internal/service/channel/channel.go` 中实现数据统计逻辑 (用户数、订单数、GMV)。
|
||||
2. **Controller 实现**: 在 `internal/api/admin/channels.go` 中添加处理函数。
|
||||
3. **路由注册**: 注册新的 API 路由。
|
||||
|
||||
### 3.2 前端开发
|
||||
1. **API 定义**: 在 `web/admin/src/api/channels.ts` 中添加 `getChannelStats` 方法。
|
||||
2. **UI 实现**: 在 `channels/index.vue` 中集成分析弹窗,展示指标卡片和趋势图表。
|
||||
|
||||
## 4. 执行步骤 (Automate)
|
||||
|
||||
1. **Step 1**: 实现后端统计接口 (Service + API)。
|
||||
2. **Step 2**: 定义前端 API 接口。
|
||||
3. **Step 3**: 实现前端分析弹窗 UI 并对接数据。
|
||||
4. **Step 4**: 验证数据准确性和图表展示效果。
|
||||
@ -1,132 +0,0 @@
|
||||
## 目标
|
||||
|
||||
* 全面清理未用代码、注释废弃块、空文件与无用测试
|
||||
|
||||
* 识别并重构重复代码(重复率≥80%)
|
||||
|
||||
* 保持现有功能稳定,构建与测试全部通过
|
||||
|
||||
* 输出对比报告与文档更新
|
||||
|
||||
## 范围
|
||||
|
||||
* 后端:`internal/**`、`cmd/**`、`migrations/**`
|
||||
|
||||
* 前端管理:`web/admin/**`(Vue/TS/样式与公共组件)
|
||||
|
||||
* 通用资源:`docs/**`、脚手架与配置(不更改生产配置)
|
||||
|
||||
## 清理策略与工具
|
||||
|
||||
* 未用与死代码检测
|
||||
|
||||
* Go:`golangci-lint`(unused、deadcode、revive)、`go vet`
|
||||
|
||||
* TS/Vue:`tsc --noEmit`(类型与未用导出)、`eslint`(no-unused-vars/no-dead-code)
|
||||
|
||||
* 注释废弃块识别
|
||||
|
||||
* 规则:Grep 检索注释中出现代码结构(`func|class|export|<template>`),人工确认后删除
|
||||
|
||||
* 空文件/无用测试
|
||||
|
||||
* Glob + Read 识别空/仅注释文件;移除未被引用的测试(无匹配运行入口或全跳过)
|
||||
|
||||
* 重复代码检测
|
||||
|
||||
* 跨语言:`jscpd`(Vue/TS/Go)或 `dupl`(Go)
|
||||
|
||||
* 阈值:重复度 ≥ 80% 且行数 ≥ 20 行
|
||||
|
||||
* 重构原则
|
||||
|
||||
* 后端:抽取到 `internal/pkg/common` 或现有包的工具单元;避免交叉包循环依赖
|
||||
|
||||
* 前端:抽取到 `web/admin/src/components/common` 或 `utils`,保持现有风格与命名
|
||||
|
||||
## 执行步骤
|
||||
|
||||
1. 基线采集
|
||||
|
||||
* 读取项目结构与关键模块,记录当前构建状态(不修改)
|
||||
|
||||
* 运行只读分析:语义搜索/正则/Grep,收集疑似未用项、注释废弃块、空文件、重复片段清单
|
||||
|
||||
1. 未用代码清理
|
||||
|
||||
* 逐文件比对引用关系(Grep/语义搜索),将“未被任何入口引用”标记为候选
|
||||
|
||||
* 生成候选清单(含文件路径与符号名),按模块批次删除;每次删除后执行增量构建验证
|
||||
|
||||
1. 注释废弃块删除
|
||||
|
||||
* 扫描 `//`、`/* */`、`<!-- -->` 中含可编译结构的片段,人工确认后删除
|
||||
|
||||
* 对 SFC 中注释的 `<template>/<script>/<style>` 片段严谨处理,避免结构破坏
|
||||
|
||||
1. 空文件与无用测试
|
||||
|
||||
* 移除 0 字节/仅注释文件;对测试:无法被测试运行器加载、或所有用例被跳过的文件移除
|
||||
|
||||
1. 重复代码重构
|
||||
|
||||
* 跑相似度分析,生成报告(位置、重复度、建议合并点)
|
||||
|
||||
* 抽取公共方法/组件,替换调用方;保持 API 不变,变更点最小化
|
||||
|
||||
1. 依赖与引用更新
|
||||
|
||||
* 后端:修复 import;前端:修复路径别名与组件引用;保证编译通过
|
||||
|
||||
1. 验证与回归
|
||||
|
||||
* 构建验证:`go build`、`tsc --noEmit`、前端 `npm run build`(或等价)
|
||||
|
||||
* 单/集成测试:运行现有测试;若缺失,补最小冒烟测试(关键模块)
|
||||
|
||||
* 功能回归:登录、活动管理、一番赏映射、承诺生成/摘要、开奖与订单查询
|
||||
|
||||
## 安全保障
|
||||
|
||||
* 分批次清理,每批次后执行构建与核心用例回归
|
||||
|
||||
* 只删除“未引用/重复/注释废弃/空”的候选;核心路径(API、路由、策略、DAO、视图)谨慎处理
|
||||
|
||||
* 所有变更均记录到临时报告与文档
|
||||
|
||||
## 交付物
|
||||
|
||||
* 代码对比报告:删除/变更列表(文件路径、符号名、原因)
|
||||
|
||||
* 重复代码检测报告:重复片段与重构前后引用图
|
||||
|
||||
* 验收文档:构建输出、测试结果、回归清单
|
||||
|
||||
* 文档更新:在 `docs/代码清理` 目录建立说明与进度记录
|
||||
|
||||
## 本次已执行清理项(2025-12-08)
|
||||
|
||||
* 移除抽奖策略注册表冗余(保留接口类型),不影响默认策略与一番赏流程
|
||||
|
||||
* 删除 Guild 相关 DAO/Model 生成文件(项目内无引用,运行不受影响)
|
||||
|
||||
* 移除管理端 batch\_users 接口与路由挂载(前端无调用)
|
||||
|
||||
* 移除管理端 IssueUserToken 路由挂载(前端无调用;代码文件暂保留为未引用状态)
|
||||
|
||||
* 构建验证:`go build ./...` 通过;`go test ./...` 存在历史用例失败(外部依赖与软删列缺失),与本次变更无关
|
||||
|
||||
## 文档与规范对齐
|
||||
|
||||
* 创建:`docs/代码清理/ALIGNMENT_代码清理.md`(范围/边界/不确定点)
|
||||
|
||||
* 共识:`docs/代码清理/CONSENSUS_代码清理.md`(验收标准与方案)
|
||||
|
||||
* 设计与任务拆分:`docs/代码清理/DESIGN_代码清理.md`、`TASK_代码清理.md`
|
||||
|
||||
* 执行与评估:`docs/代码清理/ACCEPTANCE_代码清理.md`、`FINAL_代码清理.md`、`TODO_代码清理.md`
|
||||
|
||||
## 后续执行说明
|
||||
|
||||
* 获批后:按上述步骤使用只读扫描确定候选清单→分批次提交清理补丁→每批次构建与回归验证→汇总报告与文档更新。
|
||||
|
||||
@ -1,82 +0,0 @@
|
||||
## 现状与审计结论
|
||||
- 积分存储:`user_points` 余额、`user_points_ledger` 流水;订单字段 `orders.points_amount`(分)存在但未在支付链路写入。
|
||||
- 转换比率存在两套并存:
|
||||
- 1元=10积分:`internal/api/user/points_redeem_product_app.go:55-60`、`internal/api/user/points_redeem_coupon_app.go:55-61`、展示与调度退款部分。
|
||||
- 1元=100积分:退款按比例恢复:`internal/api/admin/pay_refund_admin.go:133-167`;以及金额退款流水:`internal/api/admin/pay_refund_admin.go:158-167` 与 `internal/api/admin/lottery_admin.go:131`。
|
||||
- 统一配置已存在但未全面使用:`points_exchange_per_cent`(每分对应的积分),在资产兑换中生效:`internal/service/user/address_share.go:137-147,175-183,198-199`。
|
||||
- 支付回调未联动积分:`internal/api/pay/wechat_notify.go:133-141` 仅更新订单与券,不处理 `points_amount/points_ledger_id`。
|
||||
- 管理端订单详情积分展示按 /10:`internal/api/admin/pay_orders_admin.go:366-372`。
|
||||
- 调度器退款按 /10 恢复:`internal/service/activity/scheduler.go:80-83`。
|
||||
|
||||
## 目标与边界
|
||||
- 统一换算:严格执行“1元=100积分”,即“分→积分除以100”,或统一通过 `points_exchange_per_cent`(默认1)控制,所有入口使用同一来源。
|
||||
- 钱支付为主,积分是权益之一:支持订单使用积分抵扣(记录为 `orders.points_amount`),并在退款按比例恢复所扣积分;支持支付成功后按配置返积分(可选)。
|
||||
- 保持幂等与数据一致性,避免对历史数据造成破坏。
|
||||
|
||||
## 变更设计
|
||||
- 转换率收敛
|
||||
- 将积分兑换商品/优惠券的“分/10”改为统一转换:
|
||||
- 商品:`internal/api/user/points_redeem_product_app.go` 使用 `needPoints := price_in_cents / 100` 或 `price_in_cents * points_exchange_per_cent`。
|
||||
- 优惠券:`internal/api/user/points_redeem_coupon_app.go` 使用同一逻辑。
|
||||
- 管理端展示:`PointsUsed := points_amount / 100`,修改 `internal/api/admin/pay_orders_admin.go`。
|
||||
- 调度器退款恢复:`refundPts := points_amount / 100`,修改 `internal/service/activity/scheduler.go`。
|
||||
- 提供共用转换函数(服务层):`centsToPoints(cents, rate)`,所有入口统一调用,来源于 `system_configs.points_exchange_per_cent`。
|
||||
|
||||
- 订单积分抵扣接入
|
||||
- 下单/支付前:新增“使用积分抵扣”选项,调用 `user.ConsumePointsFor` 扣减;将抵扣金额写入 `orders.points_amount`(单位分),并写入 `user_points_ledger`(ref_table=`orders`、ref_id=order_no)。
|
||||
- 幂等:重复请求不重复扣减;取消/超时需恢复。
|
||||
|
||||
- 支付成功联动
|
||||
- 微信回调:`internal/api/pay/wechat_notify.go` 在订单转“已支付”后:
|
||||
- 若订单已有 `points_amount>0`,确保对应扣积分流水存在(幂等校验)。
|
||||
- 可选:读取 `system_configs.points_reward_per_cent`(默认0),按“每分奖励多少积分”为用户发放支付返积分(流水 action=`pay_reward`)。
|
||||
|
||||
- 退款联动一致性
|
||||
- 管理端退款与调度器统一按 `points_amount/100` 计算需恢复积分,流水 action=`refund_restore`;金额退款流水 `refund_amount := amount_refund/100` 保持不变。
|
||||
- 退款比例恢复公式保持:`restorePointsTarget = (points_amount * refunded_cents / total_paid_cents) / 100`。
|
||||
|
||||
- 文档与配置
|
||||
- Swagger注释与说明文本改为“1元=100积分”。
|
||||
- 系统配置:确保存在并默认 `points_exchange_per_cent=1`;新增可选 `points_reward_per_cent=0`。
|
||||
|
||||
## 实现清单(按模块)
|
||||
1) 统一转换入口
|
||||
- 修改 `internal/api/user/points_redeem_product_app.go:55-60`、`internal/api/user/points_redeem_coupon_app.go:55-61`。
|
||||
- 增加服务层转换工具并替换硬编码。
|
||||
|
||||
2) 展示与调度一致
|
||||
- 修改 `internal/api/admin/pay_orders_admin.go:366-372`。
|
||||
- 修改 `internal/service/activity/scheduler.go:80-83`。
|
||||
|
||||
3) 订单积分抵扣
|
||||
- 下单接口/订单生成逻辑:写入 `orders.points_amount`(分)与积分扣减流水。
|
||||
- 回调幂等校验:`wechat_notify.go` 保证一致。
|
||||
|
||||
4) 退款一致性
|
||||
- 保持 `admin/pay_refund_admin.go` 公式不变;
|
||||
- 修正调度器恢复逻辑为 `/100`,并保证只在 `points_amount>0` 场景生效。
|
||||
|
||||
5) 支付返积分(可选)
|
||||
- 新增读取 `points_reward_per_cent`,在 `wechat_notify.go` 进行奖励入账与流水。
|
||||
|
||||
6) 测试与验收
|
||||
- 单元测试:
|
||||
- 转换函数:分↔积分在不同 `points_exchange_per_cent` 下正确。
|
||||
- 订单使用积分:扣减、写入订单、退款部分恢复。
|
||||
- 回调奖励:开启/关闭奖励配置的行为。
|
||||
- 集成测试:
|
||||
- 正常支付→即时开奖→虚拟发货链路无回归。
|
||||
- 调度不足参与→全额退款→金额与积分恢复一致。
|
||||
- 管理端与APP展示:`points_used=points_amount/100` 一致。
|
||||
|
||||
## 风险与数据兼容
|
||||
- 历史数据可能基于/10:提供一次性校验脚本(仅统计差异,不强制修复),新数据全走统一入口避免继续漂移。
|
||||
- 幂等与重复扣减:以 `user_points_ledger` + `ref_table/ref_id` 作为幂等键。
|
||||
|
||||
## 交付物
|
||||
- 代码改动(API、服务、调度)。
|
||||
- 配置项说明与默认值。
|
||||
- Swagger与README更新。
|
||||
- 测试报告(单元+集成)。
|
||||
|
||||
请确认按此方案执行。我将按上述顺序推进并提交具体改动与测试结果。
|
||||
@ -1,74 +0,0 @@
|
||||
## 背景与目标
|
||||
- 目标:在用户支付成功后(微信回调),为“虚拟商品”接入微信小程序发货信息管理服务,完成发货信息录入、用户确认收货提醒/组件、状态查询与对账闭环,确保资金结算合规。
|
||||
- 项目现状:
|
||||
- 微信支付回调入口 `internal/router/router.go:288` → `internal/api/pay/wechat_notify.go:31` 已实现验签与订单入账。
|
||||
- 小程序预下单 `internal/api/user/pay_wechat_app.go:30`、AccessToken 缓存 `internal/pkg/wechat/qrcode.go:80` 可复用。
|
||||
|
||||
## 端到端流程
|
||||
1. 支付成功(微信平台通知)
|
||||
- 在 `WechatNotify` 完成验签与落库后,判断订单为虚拟商品,执行“虚拟发货”逻辑。
|
||||
2. 生成虚拟权益/兑现
|
||||
- 发放对应的虚拟权益(如游戏币、道具、资格),落库并与订单关联,保证可追溯与幂等。
|
||||
3. 录入发货信息(虚拟)到平台
|
||||
- 通过小程序 AccessToken,调用“发货信息录入接口”,`logistics_type=3(虚拟商品)`,`delivery_mode=UNIFIED_DELIVERY`,`shipping_list.item_desc` 填商品描述。
|
||||
- `order_key.order_number_type=2`,使用微信支付 `transaction_id` 精确关联一笔支付单;或回退为 `mchid+out_trade_no`。
|
||||
- `upload_time` 用 RFC3339,严格保证“更新”时间递增与“只允许一次重新发货”约束。
|
||||
4. 用户确认收货
|
||||
- 配置消息跳转路径,使平台消息点入小程序指定页面(例如 `pages/order/detail`)。
|
||||
- 可在小程序内接入“确认收货组件”并在后端暴露确认记录接口,统一订单态与权益态。
|
||||
5. 状态查询与运营
|
||||
- 提供查询订单发货状态(平台/本地)与订单列表;管理端支持重试/人工标记与报表。
|
||||
6. 结算与对账
|
||||
- 依据平台规则:仅在完成录入与确认收货后进入结算;与本地 `Payment*` 表对账。
|
||||
|
||||
## 接口与数据映射
|
||||
- 发货信息录入(虚拟):
|
||||
- `access_token`: 复用 `GetAccessToken` `internal/pkg/wechat/qrcode.go:80`。
|
||||
- `order_key.order_number_type=2`:`transaction_id` 来源于微信通知解密结果(落库于 `PaymentTransactions`)。
|
||||
- `logistics_type=3`(虚拟),`delivery_mode=UNIFIED_DELIVERY`,`shipping_list=[{ item_desc }]`;不填 `tracking_no/express_company`。
|
||||
- `upload_time`: RFC3339,保证更新序与重试策略。
|
||||
- 路由/回调:
|
||||
- `POST /api/pay/wechat/notify` `internal/router/router.go:288` → `internal/api/pay/wechat_notify.go:31`。
|
||||
- 本地数据表:
|
||||
- 支付:`PaymentTransactions/PaymentNotifyEvents/PaymentPreorders`(见 `internal/repository/mysql/dao/*.gen.go`)。
|
||||
- 发货记录与统计:`shipping_records`、`ops_shipping_stats`(见 `internal/repository/mysql/model/*.gen.go`)。
|
||||
|
||||
## 关键实现点
|
||||
- 新增发货服务封装(小程序)
|
||||
- 封装 `POST https://api.weixin.qq.com/.../order-shipping` 录入接口(遵循当前 `resty` 客户端与错误风格)。
|
||||
- 暴露:`UploadVirtualShipping(transactionID string, itemDesc string, uploadTime time.Time)`;可扩展提醒接口、消息跳转路径设置接口。
|
||||
- 回调内触发
|
||||
- 在 `WechatNotify` 成功落库后、且订单类型为虚拟商品时触发发货录入;保证幂等(以 `transaction_id` + 请求体哈希作为一次性幂等键)。
|
||||
- 小程序端确认组件
|
||||
- 在指定页面集成确认组件;后端记录确认事件并回写订单状态。
|
||||
- 管理端支持与观测
|
||||
- 手动重试、失败列表、统计报表(复用 `ops_shipping_stats` 流水);结构化日志与告警。
|
||||
|
||||
## 幂等与错误处理
|
||||
- 幂等:
|
||||
- 回调侧:保持现有幂等(订单已支付直接 ACK)。
|
||||
- 发货录入:`transaction_id` 唯一,一次成功后锁定;如需“重新发货”,严格遵循平台“最多一次”的约束并记录。
|
||||
- 重试:
|
||||
- 网络/平台 5xx → 指数退避重试;4xx → 不重试,人工处理。
|
||||
- 观测:
|
||||
- 统一结构化日志;Prometheus 指标与告警;事件表留痕(类似 `PaymentNotifyEvents`)。
|
||||
|
||||
## 权限与安全
|
||||
- 凭证:使用小程序 `access_token`,全局缓存与自动续期;严禁日志打印敏感信息。
|
||||
- 配置:`.env` 管理 `AppID/AppSecret/MchID/证书`;统一加载到 `configs`。
|
||||
|
||||
## 验收标准
|
||||
- 回调成功后,虚拟权益发放与本地订单状态均为一致且幂等。
|
||||
- 向平台成功录入虚拟发货信息,可查询到订单发货状态。
|
||||
- 用户可在小程序完成确认收货(消息跳转路径与组件可用),结算流程正常。
|
||||
- 管理端可查看与重试失败案例,日志与告警有效。
|
||||
|
||||
## 任务拆分
|
||||
1. 新增“微信发货信息管理(小程序)”SDK封装(虚拟场景优先)。
|
||||
2. 在 `WechatNotify` 中接入虚拟发货录入(含幂等与重试)。
|
||||
3. 小程序端接入确认收货组件与指定跳转页。
|
||||
4. 管理端新增发货失败重试与统计报表。
|
||||
5. 测试:单元(参数映射/幂等/错误)、集成(沙箱或模拟)、文档与验收。
|
||||
|
||||
## 文档与规范
|
||||
- 创建 `docs/虚拟发货/ALIGNMENT_虚拟发货.md`、`CONSENSUS_虚拟发货.md`、`DESIGN_虚拟发货.md`、`TASK_虚拟发货.md`,按项目“6A工作流”与用户文档规范同步进度与决策。
|
||||
@ -1,107 +0,0 @@
|
||||
# 订单记录与渠道分析优化计划
|
||||
|
||||
本计划旨在解决用户提出的两个问题:
|
||||
|
||||
1. **订单记录优化**:补充缺失的“开奖状态”和“活动信息”。
|
||||
2. **渠道分析优化**:将渠道分析的统计维度调整为按“月份”划分。
|
||||
|
||||
## 1. 需求分析与对齐 (Align)
|
||||
|
||||
* **需求 1 (订单记录)**:
|
||||
|
||||
* **问题**: 订单列表(Admin/App 端)缺乏上下文信息,无法知道订单属于哪个活动,以及是否中奖。
|
||||
|
||||
* **解决方案**:
|
||||
|
||||
* 在订单列表中关联 `ActivityDrawLogs` 表,通过 `OrderID` 关联。
|
||||
|
||||
* 如果在 `ActivityDrawLogs` 中找到记录,说明已开奖,可以获取 `IsWinner` (是否中奖) 和 `RewardID` (奖品信息)。
|
||||
|
||||
* 通过 `ActivityDrawLogs` 的 `IssueID` -> `ActivityIssues` -> `Activities` 链条,或者直接通过商品信息反查活动,来获取活动名称。
|
||||
|
||||
* 注意:`ActivityDrawLogs` 是通过 `OrderID` 关联的 (`comment:抽奖票据订单ID`)。
|
||||
|
||||
* **实现细节**: 修改 `internal/service/user/orders_list.go` 中的 `ListOrdersWithItems`,在返回结构中增加 `ActivityInfo` 和 `DrawInfo`。
|
||||
|
||||
* **需求 2 (渠道分析)**:
|
||||
|
||||
* **问题**: 渠道分析目前按天统计,用户希望按月统计。
|
||||
|
||||
* **解决方案**: 修改 `GetStats` 方法,将 SQL 中的 `DATE_FORMAT` 格式从 `%Y-%m-%d` 改为 `%Y-%m`,并调整日期范围计算逻辑(按月计算)。
|
||||
|
||||
* **交互调整**: 前端默认请求的时间范围应适应按月展示(如最近 6-12 个月)。也可以 手动选择时间类型 区间等
|
||||
|
||||
## 2. 架构设计 (Architect)
|
||||
|
||||
### 2.1 订单列表数据增强
|
||||
|
||||
* **数据模型扩展**:
|
||||
|
||||
* 在 `internal/service/user/orders_list.go` 中,扩展 `OrderWithItems` 结构体(或新建 `OrderWithDetail`),增加:
|
||||
|
||||
* `ActivityName`: 活动名称
|
||||
|
||||
* `IssueNumber`: 期号
|
||||
|
||||
* `IsDraw`: 是否已开奖
|
||||
|
||||
* `IsWinner`: 是否中奖
|
||||
|
||||
* `RewardLevel`: 中奖等级
|
||||
|
||||
* **查询逻辑**:
|
||||
|
||||
* 查询 `orders` 后,收集 `order_ids`。
|
||||
|
||||
* 批量查询 `activity_draw_logs` WHERE `order_id IN (?)`。
|
||||
|
||||
* 批量查询 `activity_issues` (通过 logs 中的 `issue_id`)。
|
||||
|
||||
* 批量查询 `activities` (通过 issues 中的 `activity_id`)。
|
||||
|
||||
* 将这些信息组装回订单列表。
|
||||
|
||||
### 2.2 渠道分析按月统计
|
||||
|
||||
* **后端调整**:
|
||||
|
||||
* 修改 `GetStats` 签名或内部逻辑。考虑到用户明确说“只需要按月份”,我们可以直接修改默认行为。
|
||||
|
||||
* SQL: `DATE_FORMAT(created_at, '%Y-%m')`
|
||||
|
||||
* Range: 最近 12 个月。
|
||||
|
||||
## 3. 任务拆解 (Atomize)
|
||||
|
||||
### 3.1 渠道分析优化 (优先执行,改动小)
|
||||
|
||||
1. **Modify Backend**: 修改 `internal/service/channel/channel.go` 中的 `GetStats` 方法。
|
||||
|
||||
* 调整日期格式为 `%Y-%m`。
|
||||
|
||||
* 调整循环逻辑,生成月份列表。
|
||||
|
||||
* 调整默认天数/月数为 12。
|
||||
2. **Modify Frontend**: 修改 `web/admin/src/views/operations/channels/index.vue`。
|
||||
|
||||
* 调整图表 X 轴显示。
|
||||
|
||||
* 调整 API 调用参数(如果需要)。
|
||||
|
||||
### 3.2 订单列表增强
|
||||
|
||||
1. **Modify Service**: 修改 `internal/service/user/orders_list.go`。
|
||||
|
||||
* 定义新的结构体字段。
|
||||
|
||||
* 在 `ListOrders` / `ListOrdersWithItems` 中增加查询 `ActivityDrawLogs` 和相关表的逻辑。
|
||||
2. **Verify**: 确保管理端和 APP 端调用该 Service 的地方都能正常展示新字段(可能需要同步修改 Controller 或 API 定义)。
|
||||
|
||||
* 检查 `internal/api/admin/users_admin.go` 和 `internal/api/user/orders_app.go`。
|
||||
|
||||
## 4. 执行步骤 (Automate)
|
||||
|
||||
1. **Step 1**: 修改 `channel.go` 实现按月统计。
|
||||
2. **Step 2**: 修改 `orders_list.go` 实现订单详情增强。
|
||||
3. **Step 3**: 验证并修复可能受影响的 API 响应结构。
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
## 目标
|
||||
- 点击“订单详情”时,统一展示该票据订单的全部信息:参与与支付(票据)+ 中奖发放(RG订单及其商品与物流),不需要再到列表中查看另一条。
|
||||
|
||||
## 后端输出补全
|
||||
- 改造 `GetPayOrderDetail`(admin):
|
||||
- 已有:票据订单 `order`、活动 `activity`、支付 `payment`、退款 `refunds`、明细 `items`、发货 `shipments`。
|
||||
- 新增:
|
||||
- `reward_order`: `{ order_no, status, created_at }`(关联RG订单)
|
||||
- `reward_items`: `[{ title, quantity, unit_price, amount }]`(RG订单项)
|
||||
- `reward_shipments`: 同 `shipments` 结构(RG订单的物流)
|
||||
- 关联方式:用 `ActivityDrawLogs(OrderID=ticket.ID)` 的 `RewardID/ActivityID` 定位 `UserInventory` → 取其 `OrderID` 为 RG 订单ID;再读取其 `OrderItems` 与 `ShippingRecords`。
|
||||
|
||||
## 前端展示改造
|
||||
- 文件:`web/admin/src/views/orders/list/index.vue`
|
||||
- 在现有详情抽屉中,新增“中奖发放”区块:
|
||||
- 头部:显示 `reward_order.order_no`、`formatOrderStatus(reward_order.status)`、`created_at`
|
||||
- 表格:显示 `reward_items`(商品、数量、单价、金额)
|
||||
- 物流:显示 `reward_shipments`(快递、运单号、状态、时间)
|
||||
- 若无发放:该区块显示“待发放/待开奖”,不影响票据信息展示
|
||||
|
||||
## 不改变的行为
|
||||
- 列表默认隐藏 RG(避免重复理解);详情页始终合并展示完整数据
|
||||
|
||||
## 验收
|
||||
- 对任意票据订单,详情页一次性显示参与与支付、中奖发放商品与物流;即使 RG 隐藏于列表,详情页也完整可见。
|
||||
@ -1,76 +0,0 @@
|
||||
## 目标与边界
|
||||
- 建立“任务中心”独立域(不改动现有`Activities`抽奖域)。
|
||||
- 支持任务类型:首日下单、订单里程碑(完成N单阶梯)、邀请有效好友人数N。
|
||||
- 奖励类型:积分、优惠券、道具卡、称号;统一走发奖服务,库存安全与幂等。
|
||||
|
||||
## 域与架构
|
||||
- 新域:`TaskCenter`(目录建议:`internal/api/task_center`、`internal/service/task_center`、`internal/repository/mysql/task_center`)。
|
||||
- 模式:事件驱动 + 调度补偿;与抽奖域并行、互不侵入。
|
||||
- 统一发奖:复用`internal/service/user/reward_grant.go`;任务域自有进度与日志。
|
||||
|
||||
## 核心数据模型(新表)
|
||||
- `Tasks`:任务基础信息(`id/name/description/status/start_time/end_time/visibility`)。
|
||||
- `TaskTiers`:任务档位(`task_id/tier_id/threshold/operator(metric: first_order|order_count|invite_count)/repeatable/priority`)。
|
||||
- `TaskRewards`:档位奖励映射(`task_id/tier_id/reward_type(points|coupon|item_card|title)/reward_payload/quantity`)。
|
||||
- `UserTaskProgress`:用户进度(`user_id/task_id/metrics{order_count,invite_count,first_order}`、`claimed_tiers[]`)。
|
||||
- `TaskEventLogs`:事件与发放日志(`event_id/source(order_id|invite_id)/user_id/task_id/tier_id/status/result/enforced_idempotency_key`)。
|
||||
- `UserTitles`(如无现表):用户称号(`user_id/title_id/source(task_id/tier_id)`)。若已有虚拟`Inventory`可用则复用。
|
||||
|
||||
## 任务规则与判定
|
||||
- 首日下单:事件`OrderPaid`且为用户首次有效订单;档位一次性发放(`repeatable=false`)。
|
||||
- 订单里程碑:累计`order_count>=[1,3,5,10]`,每档一次(`repeatable=true_once_per_tier`)。
|
||||
- 邀请有效好友:`InviteSuccess`(有效:注册+首单),累计`invite_count>=[1,5,10]`。
|
||||
- 规则DSL:存于`Tasks.conditions_schema`或按`TaskTiers`结构硬编码字段,支持窗口(活动期/注册日起)。
|
||||
|
||||
## 事件处理流程
|
||||
- 实时:
|
||||
- 订阅`OrderPaid`、`InviteSuccess`→更新`UserTaskProgress`→匹配达标档位→查`TaskEventLogs`幂等→`user.GrantReward(...)`→记日志与`claimed_tiers`。
|
||||
- 幂等:`idempotency_key=user_id + task_id + tier_id + event_id`。
|
||||
- 逆向事件:`OrderRefunded/Cancelled`→回滚进度(不回收已发奖励,策略可配置,默认不回收)。
|
||||
|
||||
## 奖励发放映射
|
||||
- 积分:安全增加余额;入账日志。
|
||||
- 优惠券:派发券码(有效期/叠加规则从券系统读取)。
|
||||
- 道具卡:发卡入用户`Inventory`;与抽奖加成逻辑无耦合,仅作为奖励资产。
|
||||
- 称号:新建`Titles`/`UserTitles`或复用`Inventory`虚拟类型;展示端读取用户当前称号。
|
||||
|
||||
## 管理端接口(新路由前缀`/api/admin/task-center`)
|
||||
- `POST /tasks` 创建任务
|
||||
- `PUT /tasks/:id` 修改、`DELETE /tasks/:id` 删除
|
||||
- `POST /tasks/:id/tiers` 批量配置档位与奖励
|
||||
- `GET /tasks/:id/progress` 任务进度与发放统计
|
||||
- 复用券/卡管理:`/api/admin/system_coupons`、`/api/admin/system_item_cards`
|
||||
|
||||
## APP端接口(新路由前缀`/api/app/task-center`)
|
||||
- `GET /tasks` 任务列表与规则说明
|
||||
- `GET /tasks/:id/progress` 我的进度与已领奖档位
|
||||
- `POST /tasks/:id/claim`(可选手动领取,默认自动发放)
|
||||
- `GET /rewards` 统一查看已获奖励(或复用现有奖励中心)
|
||||
|
||||
## 调度与补偿
|
||||
- 新增任务域调度器:周期对账`UserTaskProgress`与事件流,补漏发奖,生成统计报表。
|
||||
- 指标快照:每日汇总订单/邀请指标与发奖结果,便于风控与运营复盘。
|
||||
|
||||
## 风控与限制
|
||||
- 邀请有效性:设备指纹、手机号/实名校验、首单达成才计数。
|
||||
- 刷单检测:黑名单、频次限制、风控评分;达到阈值的用户不触发发奖。
|
||||
- 限制:每日发奖上限、IP/设备限流、防重入保护。
|
||||
|
||||
## 集成与兼容
|
||||
- 与抽奖域零耦合:不读写`Activities`/`ActivityRewardSettings/DrawLogs`。
|
||||
- 统一发奖:仅调用`internal/service/user/reward_grant.go`;券/卡系统复用现有接口。
|
||||
- 路由与权限:参考`internal/router/router.go`风格新增task-center分组。
|
||||
|
||||
## 验收标准
|
||||
- 三类任务均可正确识别达标并一次性/阶梯发奖;幂等无重复。
|
||||
- 奖励入账正确,库存扣减准确;日志完备可追溯。
|
||||
- 调度能补漏;风控能拦截异常;性能P95<200ms。
|
||||
|
||||
## 实施里程碑
|
||||
- M1 新域目录/数据模型/管理端CRUD(不影响现有抽奖)。
|
||||
- M2 接入`OrderPaid`、实现订单里程碑任务与APP进度查询。
|
||||
- M3 首日下单与邀请N任务、称号奖励落地。
|
||||
- M4 调度补偿/对账与统一奖励查看页面。
|
||||
- M5 风控与监控报警完善、压测与性能优化。
|
||||
|
||||
确认后,我将按6A工作流在`docs/任务中心/`生成对齐/设计/任务分解文档,并开始增量实现新域。
|
||||
@ -1,43 +0,0 @@
|
||||
## 结论
|
||||
- 维度划分合理:订单(交易)、资产(所有权/履约对象)、发货(履约动作)三层分离是正确的。
|
||||
- 现有实现可用:以`shipping_records`为事实表、按`express_no`聚合视图满足“合并/单独发货”两种场景,无需新增表即可运营落地。
|
||||
- 建议的优化为“增强清晰度与可观测性”,而非重构模型。
|
||||
|
||||
## 建议优化(增量)
|
||||
1) 状态清晰化
|
||||
- 资产侧:保留`status=1/2/3`,但增加标准枚举映射,前端显示区分“已申请发货”与“已发货/已签收”:
|
||||
- 用`shipping_records`最新状态驱动用户展示,不再仅依赖`user_inventory.status=3`。
|
||||
- 代码层增加统一函数:`getAssetShippingState(inventoryID)` 返回`pending/created/shipped/received/error`。
|
||||
|
||||
2) 聚合与索引
|
||||
- 为`shipping_records`增加索引:`(user_id, express_no, updated_at)`,提高“我的包裹”列表性能。
|
||||
- 用服务层统一聚合(已实现),前端仅透传即可。
|
||||
|
||||
3) 幂等与风控
|
||||
- 批量申请:用户重复点击同一资产时,严格走“幂等跳过”,并返回`already_requested`原因(已实现)。
|
||||
- 运营合单:以`express_no`为聚合键,禁止重复写同资产的多条“已发货”记录(服务层加轻量幂等校验)。
|
||||
|
||||
4) 可观测性
|
||||
- 增加“发货事件”审计(可选):在`shipping_records`写入时同步写一条事件日志(表或消息),便于监控到`status`变更。
|
||||
- 统一状态过渡校验:禁止从`已签收`回退到`已发货`。
|
||||
|
||||
5) 接口一致性
|
||||
- 用户侧视图:
|
||||
- “按资产看”:`GET /users/{user_id}/inventory`(已有)
|
||||
- “按包裹看”:`GET /users/{user_id}/shipments`(已加)
|
||||
- 管理侧:订单详情已有`shipments`;如需批量落库合单,可新增“批量创建发货记录”接口(运营入口)。
|
||||
|
||||
6) 文档与约定
|
||||
- 约定`express_no`为空表示未生成运单(待发货/自提);聚合视图展示为独立项或标记“无运单”。
|
||||
- 统一状态语义:
|
||||
- `shipping_records.status`:1待发、2已发、3已签收、4异常(唯一事实源)
|
||||
- `user_inventory.status`:1持有、3已使用/发货(生命周期标识),界面展示由`shipping_records`驱动。
|
||||
|
||||
## 是否需要重构
|
||||
- 不需要新增“发货组表”,当前以`express_no`聚合足够(若后续需要更强约束可增加`shipment_group_id`,但先不做)。
|
||||
- 重点是“统一从`shipping_records`取展示状态”,避免误用`user_inventory.status`作为最终发货状态。
|
||||
|
||||
## 交付建议(后续)
|
||||
- 加索引与状态转换校验的增量 PR(无接口变化)。
|
||||
- 可选新增运营批量发货落库接口(提升作业效率)。
|
||||
- 补充用户侧包裹视图的文档与示例。
|
||||
@ -13,12 +13,6 @@
|
||||
|
||||
### 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" -tags timetzdata -trimpath -o build/bindbox.exe .
|
||||
```
|
||||
|
||||
|
||||
export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||
docker build -t zfc931912343/bindbox-game:v1.10 .
|
||||
docker push zfc931912343/bindbox-game:v1.10
|
||||
|
||||
docker pull zfc931912343/bindbox-game:v1.10 &&docker rm -f bindbox-game && docker run -d --name bindbox-game -p 9991:9991 zfc931912343/bindbox-game:v1.10
|
||||
|
||||
BIN
bin/server
Executable file
BIN
bin/server
Executable file
Binary file not shown.
BIN
bindbox-game
Executable file
BIN
bindbox-game
Executable file
Binary file not shown.
BIN
bindbox-server
Executable file
BIN
bindbox-server
Executable file
Binary file not shown.
BIN
bindboxgame_api
Executable file
BIN
bindboxgame_api
Executable file
Binary file not shown.
BIN
build/.DS_Store
vendored
BIN
build/.DS_Store
vendored
Binary file not shown.
BIN
build/resources/.DS_Store
vendored
BIN
build/resources/.DS_Store
vendored
Binary file not shown.
1
build/resources/admin/assets/401-demo-CsPVuXgY.css
Normal file
1
build/resources/admin/assets/401-demo-CsPVuXgY.css
Normal file
@ -0,0 +1 @@
|
||||
.test-401-demo[data-v-90864b46]{padding:20px;max-width:1200px;margin:0 auto}.demo-header[data-v-90864b46]{text-align:center;margin-bottom:30px}.demo-header h1[data-v-90864b46]{font-size:28px;color:var(--el-text-color-primary);margin-bottom:8px}.demo-header p[data-v-90864b46]{font-size:16px;color:var(--el-text-color-regular)}.demo-card[data-v-90864b46]{margin-bottom:20px}.demo-card .card-header[data-v-90864b46]{display:flex;justify-content:space-between;align-items:center}.demo-card .card-header span[data-v-90864b46]{font-weight:600}.demo-content .user-info-section[data-v-90864b46],.demo-content .error-logs-section[data-v-90864b46],.demo-content .test-functions[data-v-90864b46],.demo-content .api-test-section[data-v-90864b46]{margin-bottom:30px}.demo-content .user-info-section h3[data-v-90864b46],.demo-content .error-logs-section h3[data-v-90864b46],.demo-content .test-functions h3[data-v-90864b46],.demo-content .api-test-section h3[data-v-90864b46]{font-size:18px;color:var(--el-text-color-primary);margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid var(--el-border-color)}.demo-content .info-item[data-v-90864b46]{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--el-border-color-lighter)}.demo-content .info-item[data-v-90864b46]:last-child{border-bottom:none}.demo-content .info-item .label[data-v-90864b46]{font-weight:500;color:var(--el-text-color-primary)}.demo-content .info-item .value[data-v-90864b46]{color:var(--el-text-color-regular)}.demo-content .logs-container[data-v-90864b46]{background:var(--el-fill-color-lighter);border-radius:6px;padding:16px;margin-bottom:16px;max-height:300px;overflow-y:auto}.demo-content .logs-container .no-logs[data-v-90864b46]{text-align:center;color:var(--el-text-color-secondary);padding:20px}.demo-content .logs-container .logs-list .log-item[data-v-90864b46]{background:var(--el-fill-color-blank);border:1px solid var(--el-border-color);border-radius:4px;padding:12px;margin-bottom:8px}.demo-content .logs-container .logs-list .log-item[data-v-90864b46]:last-child{margin-bottom:0}.demo-content .logs-container .logs-list .log-item .log-time[data-v-90864b46]{font-size:12px;color:var(--el-text-color-secondary);margin-bottom:4px}.demo-content .logs-container .logs-list .log-item .log-code[data-v-90864b46]{font-size:12px;color:var(--el-color-danger);margin-bottom:4px}.demo-content .logs-container .logs-list .log-item .log-message[data-v-90864b46]{font-size:14px;color:var(--el-text-color-primary);margin-bottom:4px}.demo-content .logs-container .logs-list .log-item .log-url[data-v-90864b46]{font-size:12px;color:var(--el-text-color-secondary);word-break:break-all}.demo-content .test-buttons[data-v-90864b46],.demo-content .api-buttons[data-v-90864b46]{display:flex;gap:12px;flex-wrap:wrap}.demo-content .api-result[data-v-90864b46]{margin-top:16px}.redirect-test p[data-v-90864b46]{margin-bottom:12px}.redirect-test p code[data-v-90864b46]{background:var(--el-fill-color-lighter);padding:2px 6px;border-radius:3px;font-family:Courier New,monospace}@media (max-width: 768px){.test-401-demo[data-v-90864b46]{padding:10px}.demo-header h1[data-v-90864b46]{font-size:24px}.demo-header p[data-v-90864b46]{font-size:14px}.test-buttons[data-v-90864b46],.api-buttons[data-v-90864b46]{flex-direction:column}.test-buttons .el-button[data-v-90864b46],.api-buttons .el-button[data-v-90864b46]{width:100%}}
|
||||
1
build/resources/admin/assets/401-demo-D1Sd9-a-.js
Normal file
1
build/resources/admin/assets/401-demo-D1Sd9-a-.js
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
import{d as a,A as s,u as t,F as e,a as m,o as r,j as l,b as c,M as n,q as d,G as x,N as o,w as i,p}from"./index-BeZn6wgH.js";/* empty css */import{_ as u}from"./index-ClC46U48.js";const g={class:"page-content !border-0 !bg-transparent min-h-screen flex-cc"},b={class:"flex-cc max-md:!block max-md:text-center"},f={class:"ml-15 w-75 max-md:mx-auto max-md:mt-10 max-md:w-full max-md:text-center"},_={class:"text-xl leading-7 text-g-600 max-md:text-lg"},h=a({__name:"ArtException",props:{data:{}},setup(a){const h=s(),{homePath:j}=t(),v=()=>{h.push(j.value)};return(s,t)=>{const h=u,j=o,w=e("ripple");return r(),m("div",g,[l("div",b,[c(h,{src:a.data.imgUrl,size:"100%",class:"!w-100"},null,8,["src"]),l("div",f,[l("p",_,d(a.data.desc),1),n((r(),x(j,{type:"primary",size:"large",onClick:v,class:"mt-5"},{default:i(()=>[p(d(a.data.btnText),1)]),_:1})),[[w]])])])])}}});export{h as _};
|
||||
@ -0,0 +1 @@
|
||||
var e=Object.defineProperty,t=Object.defineProperties,s=Object.getOwnPropertyDescriptors,r=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,p=(t,s,r)=>s in t?e(t,s,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[s]=r;import{_ as l}from"./index.vue_vue_type_script_setup_true_lang-BQI7d56A.js";import{d as n,a as c,o as m,b as i,j as d,k as u,q as x,l as b}from"./index-BeZn6wgH.js";const g={class:"title mt-8 text-3xl font-medium !text-g-900 max-md:mt-2.5 max-md:text-2xl"},f={class:"msg mt-5 text-base text-g-600"},_={class:"res mt-7.5 rounded bg-g-200/80 dark:bg-g-300/40 px-7.5 py-5.5 text-left max-md:px-7.5 max-md:py-2.5 [&_p]:flex [&_p]:items-center [&_p]:py-2 [&_p]:text-sm [&_p]:text-[#808695] [&_p_i]:mr-1.5"},y={class:"btn-group mt-12.5"},v=n((j=((e,t)=>{for(var s in t||(t={}))a.call(t,s)&&p(e,s,t[s]);if(r)for(var s of r(t))o.call(t,s)&&p(e,s,t[s]);return e})({},{name:"ArtResultPage"}),t(j,s({__name:"ArtResultPage",props:{type:{default:"success"},title:{default:""},message:{default:""},iconCode:{default:""}},setup:e=>(t,s)=>{const r=l;return m(),c("div",{class:u(["page-content box-border !px-20 py-3.5 text-center max-md:!px-5",e.type])},[i(r,{class:u(["icon size-22 p-2 mt-16 block rounded-full !text-white","success"===e.type?"bg-[#19BE6B]":"bg-[#ED4014]"]),icon:e.iconCode},null,8,["icon","class"]),d("h1",g,x(e.title),1),d("p",f,x(e.message),1),d("div",_,[b(t.$slots,"content")]),d("div",y,[b(t.$slots,"buttons")])],2)}}))));var j;export{v as _};
|
||||
File diff suppressed because one or more lines are too long
BIN
build/resources/admin/assets/EffectEditDialog-Ig8f1_Q8.js.gz
Normal file
BIN
build/resources/admin/assets/EffectEditDialog-Ig8f1_Q8.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
1
build/resources/admin/assets/Iframe-DfGqWlTm.js
Normal file
1
build/resources/admin/assets/Iframe-DfGqWlTm.js
Normal file
@ -0,0 +1 @@
|
||||
var e=Object.defineProperty,r=Object.defineProperties,a=Object.getOwnPropertyDescriptors,t=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable,s=(r,a,t)=>a in r?e(r,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[a]=t;import{d as l,C as f,r as c,f as i,b1 as u,M as b,b2 as p,i as m,o as d,a as v,j as y}from"./index-BeZn6wgH.js";const O={class:"box-border w-full h-full"},j=["src"],h=l((w=((e,r)=>{for(var a in r||(r={}))o.call(r,a)&&s(e,a,r[a]);if(t)for(var a of t(r))n.call(r,a)&&s(e,a,r[a]);return e})({},{name:"IframeView"}),r(w,a({__name:"Iframe",setup(e){const r=f(),a=c(!0),t=c(""),o=c(null);i(()=>{const e=u.getInstance().findByPath(r.path);(null==e?void 0:e.meta)&&(t.value=e.meta.link||"")});const n=()=>{a.value=!1};return(e,r)=>{const s=p;return b((d(),v("div",O,[y("iframe",{ref_key:"iframeRef",ref:o,src:m(t),frameborder:"0",class:"w-full h-full min-h-[calc(100vh-120px)] border-none",onLoad:n},null,40,j)])),[[s,m(a)]])}}}))));var w;export{h as default};
|
||||
1
build/resources/admin/assets/LoginLeftView-CSOU4vG6.js
Normal file
1
build/resources/admin/assets/LoginLeftView-CSOU4vG6.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
build/resources/admin/assets/RuleConfigDialog-OcWf3YP-.css.gz
Normal file
BIN
build/resources/admin/assets/RuleConfigDialog-OcWf3YP-.css.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
build/resources/admin/assets/RuleConfigDialog-afglGUnA.js.gz
Normal file
BIN
build/resources/admin/assets/RuleConfigDialog-afglGUnA.js.gz
Normal file
Binary file not shown.
1
build/resources/admin/assets/TitleEditDialog-HkciW6gO.js
Normal file
1
build/resources/admin/assets/TitleEditDialog-HkciW6gO.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
build/resources/admin/assets/UserAssignmentDialog-C5RixQ6c.js.gz
Normal file
BIN
build/resources/admin/assets/UserAssignmentDialog-C5RixQ6c.js.gz
Normal file
Binary file not shown.
1
build/resources/admin/assets/_baseIteratee-sVeOpCU9.js
Normal file
1
build/resources/admin/assets/_baseIteratee-sVeOpCU9.js
Normal file
@ -0,0 +1 @@
|
||||
import{cb as n,dt as r,c0 as t,c4 as u,du as e,dv as i,bD as o,dw as f,dx as a,dy as c,c8 as v}from"./index-BeZn6wgH.js";function s(n){return n==n&&!t(n)}function l(n,r){return function(t){return null!=t&&(t[n]===r&&(void 0!==r||n in Object(t)))}}function d(t){var e=function(n){for(var r=u(n),t=r.length;t--;){var e=r[t],i=n[e];r[t]=[e,i,s(i)]}return r}(t);return 1==e.length&&e[0][2]?l(e[0][0],e[0][1]):function(u){return u===t||function(t,u,e,i){var o=e.length,f=o;if(null==t)return!f;for(t=Object(t);o--;){var a=e[o];if(a[2]?a[1]!==t[a[0]]:!(a[0]in t))return!1}for(;++o<f;){var c=(a=e[o])[0],v=t[c],s=a[1];if(a[2]){if(void 0===v&&!(c in t))return!1}else{var l=new n;if(!r(s,v,3,i,l))return!1}}return!0}(u,0,e)}}function b(n){return e(n)?(r=i(n),function(n){return null==n?void 0:n[r]}):function(n){return function(r){return a(r,n)}}(n);var r}function j(n){return"function"==typeof n?n:null==n?c:"object"==typeof n?v(n)?(t=n[0],u=n[1],e(t)&&s(u)?l(i(t),u):function(n){var e=o(n,t);return void 0===e&&e===u?f(n,t):r(u,e,3)}):d(n):b(n);var t,u}export{j as b};
|
||||
@ -0,0 +1 @@
|
||||
import{c0 as r,c5 as t,cR as e,dz as n,dA as o,c1 as u,dB as c,dp as a,dC as f}from"./index-BeZn6wgH.js";var s=Object.create,i=function(){function t(){}return function(e){if(!r(e))return{};if(s)return s(e);t.prototype=e;var n=new t;return t.prototype=void 0,n}}();function p(r,t){var e=-1,n=r.length;for(t||(t=Array(n));++e<n;)t[e]=r[e];return t}function v(r,n,o,u){var c=!o;o||(o={});for(var a=-1,f=n.length;++a<f;){var s=n[a],i=void 0;void 0===i&&(i=r[s]),c?t(o,s,i):e(o,s,i)}return o}var d=Object.prototype.hasOwnProperty;function l(t){if(!r(t))return function(r){var t=[];if(null!=r)for(var e in Object(r))t.push(e);return t}(t);var e=n(t),o=[];for(var u in t)("constructor"!=u||!e&&d.call(t,u))&&o.push(u);return o}function y(r){return u(r)?o(r,!0):l(r)}var b=c(Object.getPrototypeOf,Object),h="object"==typeof exports&&exports&&!exports.nodeType&&exports,j=h&&"object"==typeof module&&module&&!module.nodeType&&module,O=j&&j.exports===h?a.Buffer:void 0,g=O?O.allocUnsafe:void 0;function w(r,t){if(t)return r.slice();var e=r.length,n=g?g(e):new r.constructor(e);return r.copy(n),n}function x(r){var t=new r.constructor(r.byteLength);return new f(t).set(new f(r)),t}function m(r,t){var e=t?x(r.buffer):r.buffer;return new r.constructor(e,r.byteOffset,r.length)}function A(r){return"function"!=typeof r.constructor||n(r)?{}:i(b(r))}export{p as a,w as b,v as c,m as d,x as e,b as g,A as i,y as k};
|
||||
1
build/resources/admin/assets/active-user-CqTRPlzz.js
Normal file
1
build/resources/admin/assets/active-user-CqTRPlzz.js
Normal file
@ -0,0 +1 @@
|
||||
import{_ as t}from"./active-user.vue_vue_type_script_setup_true_lang-OH7H12f-.js";import"./index-BeZn6wgH.js";import"./useChart-zMYYIBs2.js";import"./operations-Bh9u6U-E.js";export{t as default};
|
||||
@ -0,0 +1 @@
|
||||
var e=Object.defineProperty,t=Object.defineProperties,a=Object.getOwnPropertyDescriptors,s=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,i=(t,a,s)=>a in t?e(t,a,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[a]=s,l=(e,t)=>{for(var a in t||(t={}))r.call(t,a)&&i(e,a,t[a]);if(s)for(var a of s(t))o.call(t,a)&&i(e,a,t[a]);return e};import{d as n,c as d,M as c,b2 as u,o as p,a as m,h,K as y,r as f,e as x,b as g,X as b,j as v,H as w,I as A,T as L,q as S}from"./index-BeZn6wgH.js";import{u as j,a as k,g as O}from"./useChart-zMYYIBs2.js";import{f as P}from"./operations-Bh9u6U-E.js";const B=n((W=l({},{name:"ArtBarChart"}),D={__name:"index",props:{data:{default:()=>[0,0,0,0,0,0,0]},xAxisData:{default:()=>[]},barWidth:{default:"40%"},stack:{type:Boolean,default:!1},borderRadius:{default:4},height:{default:j().chartHeight},loading:{type:Boolean,default:!1},isEmpty:{type:Boolean,default:!1},colors:{default:()=>j().colors},showAxisLabel:{type:Boolean,default:!0},showAxisLine:{type:Boolean,default:!0},showSplitLine:{type:Boolean,default:!0},showTooltip:{type:Boolean,default:!0},showLegend:{type:Boolean,default:!1},legendPosition:{default:"bottom"}},setup(e){const t=e,a=d(()=>Array.isArray(t.data)&&t.data.length>0&&"object"==typeof t.data[0]&&"name"in t.data[0]),s=(e,a)=>e||(void 0!==a?t.colors[a%t.colors.length]:new O.LinearGradient(0,0,0,1,[{offset:0,color:y("--el-color-primary-light-4")},{offset:1,color:y("--el-color-primary")}])),r=e=>new O.LinearGradient(0,0,0,1,[{offset:0,color:e},{offset:1,color:e}]),o=e=>{const a=b();return l({name:e.name,data:e.data,type:"bar",stack:e.stack,itemStyle:(s=e.color,{borderRadius:t.borderRadius,color:"string"==typeof s?r(s):s}),barWidth:e.barWidth||t.barWidth},a);var s},{chartRef:i,getAxisLineStyle:n,getAxisLabelStyle:f,getAxisTickStyle:x,getSplitLineStyle:g,getAnimationConfig:b,getTooltipStyle:v,getLegendStyle:w,getGridWithLegend:A}=k({props:t,checkEmpty:()=>{if(Array.isArray(t.data)&&"number"==typeof t.data[0]){const e=t.data;return!e.length||e.every(e=>0===e)}if(Array.isArray(t.data)&&"object"==typeof t.data[0]){const e=t.data;return!e.length||e.every(e=>{var t;return!(null==(t=e.data)?void 0:t.length)||e.data.every(e=>0===e)})}return!0},watchSources:[()=>t.data,()=>t.xAxisData,()=>t.colors],generateOptions:()=>{const e={grid:A(t.showLegend&&a.value,t.legendPosition,{top:15,right:0,left:0}),tooltip:t.showTooltip?v():void 0,xAxis:{type:"category",data:t.xAxisData,axisTick:x(),axisLine:n(t.showAxisLine),axisLabel:f(t.showAxisLabel)},yAxis:{type:"value",axisLabel:f(t.showAxisLabel),axisLine:n(t.showAxisLine),splitLine:g(t.showSplitLine)}};if(t.showLegend&&a.value&&(e.legend=w(t.legendPosition)),a.value){const a=t.data;e.series=a.map((e,a)=>{const r=s(t.colors[a],a);return o({name:e.name,data:e.data,color:r,barWidth:e.barWidth,stack:t.stack?e.stack||"total":void 0})})}else{const a=t.data,r=s();e.series=[o({data:a,color:r})]}return e}});return(e,a)=>{const s=u;return c((p(),m("div",{ref_key:"chartRef",ref:i,style:h({height:t.height})},null,4)),[[s,t.loading]])}}},t(W,a(D))));var W,D;const T={class:"art-card h-105 p-4 box-border mb-5 max-sm:mb-4"},_={class:"flex-b mt-2"},R={class:"text-2xl text-g-900"},G={class:"text-xs text-g-500"},C=n({__name:"active-user",setup(e){const t=f([]),a=f([]),s=x([{name:"总用户量",num:"0"},{name:"总访问量",num:"0"},{name:"日访问量",num:"0"},{name:"周同比",num:"+0%"}]);return(()=>{return e=this,r=null,o=function*(){try{const e=yield P("30d");t.value=e.chart.map(e=>e.date.slice(5)),a.value=e.chart.map(e=>e.value),s[0].num=String(e.metrics.totalUsers),s[1].num=String(e.metrics.totalVisits),s[2].num=String(e.metrics.dailyVisits),s[3].num=e.metrics.weeklyGrowth}catch(e){L.error("加载用户概述失败")}},new Promise((t,a)=>{var s=e=>{try{l(o.next(e))}catch(t){a(t)}},i=e=>{try{l(o.throw(e))}catch(t){a(t)}},l=e=>e.done?t(e.value):Promise.resolve(e.value).then(s,i);l((o=o.apply(e,r)).next())});var e,r,o})(),(e,r)=>{const o=B;return p(),m("div",T,[g(o,{class:"box-border p-2",barWidth:"50%",height:"13.7rem",showAxisLine:!1,data:a.value,xAxisData:t.value},null,8,["data","xAxisData"]),r[0]||(r[0]=b('<div class="ml-1"><h3 class="mt-5 text-lg font-medium">用户概述</h3><p class="mt-1 text-sm">比上周 <span class="text-success font-medium">+23%</span></p><p class="mt-1 text-sm">我们为您创建了多个选项,可将它们组合在一起并定制为像素完美的页面</p></div>',1)),v("div",_,[(p(!0),m(w,null,A(s,(e,t)=>(p(),m("div",{class:"flex-1",key:t},[v("p",R,S(e.num),1),v("p",G,S(e.name),1)]))),128))])])}}});export{C as _};
|
||||
1
build/resources/admin/assets/activity-CHlmgh7M.js
Normal file
1
build/resources/admin/assets/activity-CHlmgh7M.js
Normal file
@ -0,0 +1 @@
|
||||
var e=Object.defineProperty,r=Object.getOwnPropertySymbols,t=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,o=(r,t,a)=>t in r?e(r,t,{enumerable:!0,configurable:!0,writable:!0,value:a}):r[t]=a;import{bj as s}from"./index-BeZn6wgH.js";function n(e){return n=this,i=null,c=function*(){const n=((e,s)=>{for(var n in s||(s={}))t.call(s,n)&&o(e,n,s[n]);if(r)for(var n of r(s))a.call(s,n)&&o(e,n,s[n]);return e})({page:1,page_size:20},e||{});try{const e=yield s.get({url:"app/activities",params:n,showErrorMessage:!1});return{records:e.list.map(e=>({id:e.id,name:e.name,categoryName:e.category_name,status:e.status,priceDraw:e.price_draw,isBoss:e.is_boss})),total:e.total,current:e.page,size:e.page_size}}catch(i){return{records:[],total:0,current:n.page,size:n.page_size}}},new Promise((e,r)=>{var t=e=>{try{o(c.next(e))}catch(t){r(t)}},a=e=>{try{o(c.throw(e))}catch(t){r(t)}},o=r=>r.done?e(r.value):Promise.resolve(r.value).then(t,a);o((c=c.apply(n,i)).next())});var n,i,c}export{n as f};
|
||||
@ -0,0 +1 @@
|
||||
import{d as t,r as e,e as l,f as a,a as s,o as r,j as o,q as n,H as i,I as c,p as d,k as g,b as x,w as v,n as f}from"./index-BeZn6wgH.js";/* empty css */import{a as u,b}from"./operations-Bh9u6U-E.js";import{E as p}from"./index-rM5MDBEe.js";const h={class:"art-card h-140 p-5 mb-5 max-sm:mb-4"},m={class:"h-[calc(100%-40px)]"},y={class:"grid grid-cols-4 gap-4 mb-6"},w={class:"text-center p-3 bg-blue-50 rounded-lg"},C={class:"text-2xl font-bold text-blue-600"},R={class:"text-center p-3 bg-green-50 rounded-lg"},S={class:"text-2xl font-bold text-green-600"},j={class:"text-center p-3 bg-yellow-50 rounded-lg"},k={class:"text-2xl font-bold text-yellow-600"},P={class:"text-center p-3 bg-purple-50 rounded-lg"},$={class:"text-2xl font-bold text-purple-600"},T={class:"h-60 mb-4"},_={class:"overflow-auto"},A={class:"w-full text-sm"},E={class:"py-2"},W={class:"flex items-center"},q={class:"py-2"},F={class:"py-2"},L={class:"py-2"},N={class:"py-2"},z=t({__name:"activity-lottery",setup(t){const z=e(),D=l({totalActivities:0,totalParticipants:0,totalDraws:0,winnerCount:0,overallWinRate:0,costControl:0}),G=l([]),H=e(!1),I=t=>t.winRate>2?"success":t.winRate>1?"warning":"info",M=t=>t.winRate>2?"高中奖率":t.winRate>1?"中等中奖率":"低中奖率",O=(t,e)=>({1:`rgba(251, 191, 36, ${e})`,2:`rgba(156, 163, 175, ${e})`,3:`rgba(251, 146, 60, ${e})`,4:`rgba(96, 165, 250, ${e})`,5:`rgba(52, 211, 153, ${e})`}[t]||`rgba(156, 163, 175, ${e})`),B=()=>{return t=this,e=null,l=function*(){H.value=!0;try{const[t,e]=yield Promise.all([u("7d"),b("7d")]);Object.assign(D,t),G.splice(0,G.length,...e),f(()=>{(()=>{if(!z.value||0===G.length)return;const t=z.value,e=t.getContext("2d");if(!e)return;e.clearRect(0,0,t.width,t.height);const l=40,a=t.width-80,s=t.height-80,r=Math.max(...G.map(t=>t.winnerCount)),o=a/G.length*.6,n=a/G.length*.4;G.forEach((t,a)=>{const i=l+a*(o+n)+n/2,c=t.winnerCount/r*s,d=l+s-c,g=e.createLinearGradient(i,d+c,i,d);g.addColorStop(0,O(t.level,.8)),g.addColorStop(1,O(t.level,1)),e.fillStyle=g,e.fillRect(i,d,o,c),e.fillStyle="#333",e.font="12px sans-serif",e.textAlign="center",e.fillText(t.winnerCount.toString(),i+o/2,d-5),e.fillText(t.levelName,i+o/2,l+s+20)}),e.strokeStyle="#e0e0e0",e.lineWidth=1,e.beginPath(),e.moveTo(l,l),e.lineTo(l,l+s),e.lineTo(l+a,l+s),e.stroke()})()})}catch(t){}finally{H.value=!1}},new Promise((a,s)=>{var r=t=>{try{n(l.next(t))}catch(e){s(e)}},o=t=>{try{n(l.throw(t))}catch(e){s(e)}},n=t=>t.done?a(t.value):Promise.resolve(t.value).then(r,o);n((l=l.apply(t,e)).next())});var t,e,l};return a(()=>{B()}),(t,e)=>{const l=p;return r(),s("div",h,[e[5]||(e[5]=o("div",{class:"art-card-header"},[o("div",{class:"title"},[o("h4",null,"活动抽奖效果分析"),o("p",null,"优化中奖概率,控制活动成本")])],-1)),o("div",m,[o("div",y,[o("div",w,[o("div",C,n(D.totalActivities),1),e[0]||(e[0]=o("div",{class:"text-sm text-g-500"},"活动总数",-1))]),o("div",R,[o("div",S,n((a=D.totalParticipants,a>=1e4?(a/1e4).toFixed(1)+"w":a>=1e3?(a/1e3).toFixed(1)+"k":a.toString())),1),e[1]||(e[1]=o("div",{class:"text-sm text-g-500"},"参与人数",-1))]),o("div",j,[o("div",k,n(D.overallWinRate)+"%",1),e[2]||(e[2]=o("div",{class:"text-sm text-g-500"},"整体中奖率",-1))]),o("div",P,[o("div",$,n(D.costControl)+"%",1),e[3]||(e[3]=o("div",{class:"text-sm text-g-500"},"成本控制",-1))])]),o("div",T,[o("canvas",{ref_key:"chartRef",ref:z,width:"400",height:"240"},null,512)]),o("div",_,[o("table",A,[e[4]||(e[4]=o("thead",null,[o("tr",{class:"border-b border-g-200"},[o("th",{class:"text-left py-2"},"奖级"),o("th",{class:"text-left py-2"},"中奖人数"),o("th",{class:"text-left py-2"},"中奖率"),o("th",{class:"text-left py-2"},"成本"),o("th",{class:"text-left py-2"},"状态")])],-1)),o("tbody",null,[(r(!0),s(i,null,c(G,t=>{return r(),s("tr",{key:t.level,class:"border-b border-g-100 hover:bg-g-50"},[o("td",E,[o("div",W,[o("span",{class:g(["w-6 h-6 rounded-full flex items-center justify-center text-white text-xs font-bold mr-2",(e=t.level,{1:"bg-gradient-to-r from-yellow-400 to-yellow-300",2:"bg-gradient-to-r from-gray-400 to-gray-300",3:"bg-gradient-to-r from-orange-400 to-orange-300",4:"bg-gradient-to-r from-blue-400 to-blue-300",5:"bg-gradient-to-r from-green-400 to-green-300"}[e]||"bg-gradient-to-r from-gray-400 to-gray-300")])},n(t.level),3),d(" "+n(t.levelName),1)])]),o("td",q,n(t.winnerCount)+"人",1),o("td",F,n(t.winRate)+"%",1),o("td",L,"¥"+n(t.cost.toLocaleString()),1),o("td",N,[x(l,{type:I(t),size:"small"},{default:v(()=>[d(n(M(t)),1)]),_:2},1032,["type"])])]);var e}),128))])])])])]);var a}}});export{z as default};
|
||||
@ -0,0 +1 @@
|
||||
import{_ as t}from"./activity-prize-analysis.vue_vue_type_script_setup_true_lang-CGUnnm6n.js";import"./index-BeZn6wgH.js";import"./el-progress-O14AXzNU.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import"./operations-Bh9u6U-E.js";import"./index-B43cMk6T.js";import"./index-CnhjG_Ys.js";import"./index-A3hG-0VQ.js";import"./index-DC47MImW.js";import"./index-s8Fl0Qzt.js";import"./index-rM5MDBEe.js";import"./token-DWNpOE8r.js";import"./castArray-BakW2F2h.js";import"./debounce-C7sIggI-.js";import"./_baseIteratee-sVeOpCU9.js";import"./index-DZdoPtEQ.js";export{t as default};
|
||||
File diff suppressed because one or more lines are too long
1
build/resources/admin/assets/activity-search-CzX2TRnf.js
Normal file
1
build/resources/admin/assets/activity-search-CzX2TRnf.js
Normal file
@ -0,0 +1 @@
|
||||
var e=Object.defineProperty,a=Object.getOwnPropertySymbols,l=Object.prototype.hasOwnProperty,t=Object.prototype.propertyIsEnumerable,o=(a,l,t)=>l in a?e(a,l,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[l]=t,s=(e,s)=>{for(var r in s||(s={}))l.call(s,r)&&o(e,r,s[r]);if(a)for(var r of a(s))t.call(s,r)&&o(e,r,s[r]);return e};import{d as r,r as i,y as u,F as d,G as p,o as m,w as n,b as c,i as f,J as v,P as b,a as _,H as j,I as y,M as h,N as x,p as V,ai as g,bb as w}from"./index-BeZn6wgH.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import{a as O}from"./adminActivities-Dndna7OD.js";import{a as E,E as P}from"./index-Dn4yxdMr.js";import{E as C}from"./index-DJP4F2zx.js";import{E as k}from"./index-D6O1cfnb.js";import{E as I,a as N}from"./index-B43cMk6T.js";import{E as S}from"./index-gJfKG9HJ.js";import{E as U}from"./index-Bh_wUTwB.js";import{_ as A}from"./_plugin-vue_export-helper-BCo6x5W8.js";import"./castArray-BakW2F2h.js";import"./_initCloneObject-BHiCRTfC.js";import"./index-CnhjG_Ys.js";import"./index-A3hG-0VQ.js";import"./index-DC47MImW.js";import"./index-s8Fl0Qzt.js";import"./index-rM5MDBEe.js";import"./token-DWNpOE8r.js";import"./debounce-C7sIggI-.js";import"./_baseIteratee-sVeOpCU9.js";import"./index-DZdoPtEQ.js";const B=A(r({__name:"activity-search",props:{modelValue:{}},emits:["update:modelValue","search","reset"],setup(e,{emit:a}){const l=e,t=a,o=i([]),r=i(s({},l.modelValue));u(()=>l.modelValue,(e,a)=>{JSON.stringify(e)!==JSON.stringify(a)&&(r.value=s({},e))},{deep:!0});let A=null;u(r,e=>{A&&clearTimeout(A),A=setTimeout(()=>{t("update:modelValue",s({},e))},100)},{deep:!0});const B=e=>{return a=this,l=null,t=function*(){if(e&&0===o.value.length)try{const e=yield O();o.value=e.list||[]}catch(a){}},new Promise((e,o)=>{var s=e=>{try{i(t.next(e))}catch(a){o(a)}},r=e=>{try{i(t.throw(e))}catch(a){o(a)}},i=a=>a.done?e(a.value):Promise.resolve(a.value).then(s,r);i((t=t.apply(a,l)).next())});var a,l,t},J=()=>{t("search",r.value)},R=()=>{r.value={name:void 0,category_id:void 0,status:void 0,is_boss:void 0},t("reset")};return(a,l)=>{const t=d("ripple");return m(),p(f(U),{class:"search-card",shadow:"never"},{default:n(()=>[c(f(E),{ref:"formRef",model:e.modelValue,"label-width":"80px"},{default:n(()=>[c(f(C),{gutter:20},{default:n(()=>[c(f(k),{span:6},{default:n(()=>[c(f(P),{label:"活动名称",prop:"name"},{default:n(()=>[c(f(v),{modelValue:r.value.name,"onUpdate:modelValue":l[0]||(l[0]=e=>r.value.name=e),placeholder:"请输入活动名称",clearable:"",onKeyup:b(J,["enter"])},null,8,["modelValue"])]),_:1})]),_:1}),c(f(k),{span:6},{default:n(()=>[c(f(P),{label:"分类",prop:"category_id"},{default:n(()=>[c(f(I),{modelValue:r.value.category_id,"onUpdate:modelValue":l[1]||(l[1]=e=>r.value.category_id=e),placeholder:"请选择分类",clearable:"",onVisibleChange:B},{default:n(()=>[(m(!0),_(j,null,y(o.value,e=>(m(),p(f(N),{key:e.id,label:e.name,value:e.id},null,8,["label","value"]))),128))]),_:1},8,["modelValue"])]),_:1})]),_:1}),c(f(k),{span:6},{default:n(()=>[c(f(P),{label:"状态",prop:"status"},{default:n(()=>[c(f(I),{modelValue:r.value.status,"onUpdate:modelValue":l[2]||(l[2]=e=>r.value.status=e),placeholder:"请选择状态",clearable:""},{default:n(()=>[c(f(N),{value:1,label:"进行中"}),c(f(N),{value:2,label:"下线"})]),_:1},8,["modelValue"])]),_:1})]),_:1}),c(f(k),{span:6},{default:n(()=>[c(f(P),{label:"Boss活动",prop:"is_boss"},{default:n(()=>[c(f(I),{modelValue:r.value.is_boss,"onUpdate:modelValue":l[3]||(l[3]=e=>r.value.is_boss=e),placeholder:"请选择",clearable:""},{default:n(()=>[c(f(N),{value:1,label:"是"}),c(f(N),{value:0,label:"否"})]),_:1},8,["modelValue"])]),_:1})]),_:1}),c(f(k),{span:4},{default:n(()=>[c(f(P),{"label-width":"0"},{default:n(()=>[c(f(S),null,{default:n(()=>[h((m(),p(f(x),{type:"primary",onClick:J},{default:n(()=>[c(f(g),{class:"mr-1"},{default:n(()=>[c(f(w))]),_:1}),l[4]||(l[4]=V(" 搜索 ",-1))]),_:1})),[[t]]),h((m(),p(f(x),{onClick:R},{default:n(()=>[...l[5]||(l[5]=[V("重置",-1)])]),_:1})),[[t]])]),_:1})]),_:1})]),_:1})]),_:1})]),_:1},8,["model"])]),_:1})}}}),[["__scopeId","data-v-82eaff85"]]);export{B as default};
|
||||
@ -0,0 +1 @@
|
||||
import{_ as i}from"./add-coupon-dialog.vue_vue_type_script_setup_true_lang-BGmeg1J9.js";import"./index-BeZn6wgH.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import"./coupons-BolQHY2x.js";import"./index-Dn4yxdMr.js";import"./castArray-BakW2F2h.js";import"./_initCloneObject-BHiCRTfC.js";import"./index-B43cMk6T.js";import"./index-CnhjG_Ys.js";import"./index-A3hG-0VQ.js";import"./index-DC47MImW.js";import"./index-s8Fl0Qzt.js";import"./index-rM5MDBEe.js";import"./token-DWNpOE8r.js";import"./debounce-C7sIggI-.js";import"./_baseIteratee-sVeOpCU9.js";import"./index-DZdoPtEQ.js";import"./index-DfDWpFb3.js";import"./use-dialog-D_t6_hoT.js";import"./refs-Cw5r5QN8.js";export{i as default};
|
||||
@ -0,0 +1 @@
|
||||
var e=(e,a,l)=>new Promise((o,t)=>{var i=e=>{try{r(l.next(e))}catch(a){t(a)}},s=e=>{try{r(l.throw(e))}catch(a){t(a)}},r=e=>e.done?o(e.value):Promise.resolve(e.value).then(i,s);r((l=l.apply(e,a)).next())});import{d as a,r as l,e as o,y as t,f as i,G as s,o as r,w as d,b as u,i as n,a as p,H as m,I as c,N as v,O as f,p as y}from"./index-BeZn6wgH.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import{c as b}from"./coupons-BolQHY2x.js";import{a as h,E as _}from"./index-Dn4yxdMr.js";import{E as g,a as j}from"./index-B43cMk6T.js";import{E as x}from"./index-DfDWpFb3.js";const I=a({__name:"add-coupon-dialog",props:{visible:{type:Boolean}},emits:["update:visible","submit"],setup(a,{emit:I}){const w=a,k=I,V=l(),C=l(!1),E=l([]),A=o({couponId:null});t(()=>w.visible,e=>{});const O={couponId:[{required:!0,message:"请选择优惠券",trigger:"change"}]},P=()=>e(this,null,function*(){var e;try{yield null==(e=V.value)?void 0:e.validate(),C.value=!0,k("submit",{coupon_id:A.couponId})}catch(a){}finally{C.value=!1}}),U=()=>{k("update:visible",!1)},$=()=>{var e;A.couponId=null,null==(e=V.value)||e.clearValidate()};return i(()=>e(this,null,function*(){try{const e=yield b.getList({status:1,page:1,page_size:100});E.value=Array.isArray(e.list)?e.list.map(e=>({id:e.id,name:e.name})):[]}catch(e){E.value=[]}})),(e,l)=>(r(),s(n(x),{"model-value":a.visible,title:"发放优惠券",width:"400px","close-on-click-modal":!1,"onUpdate:modelValue":l[1]||(l[1]=e=>k("update:visible",e)),onClosed:$},{footer:d(()=>[u(n(v),{onClick:f(U,["prevent"])},{default:d(()=>[...l[2]||(l[2]=[y("取消",-1)])]),_:1}),u(n(v),{type:"primary",loading:C.value,onClick:f(P,["prevent"])},{default:d(()=>[...l[3]||(l[3]=[y(" 确定 ",-1)])]),_:1},8,["loading"])]),default:d(()=>[u(n(h),{ref_key:"formRef",ref:V,model:A,rules:O,"label-width":"80px"},{default:d(()=>[u(n(_),{label:"优惠券",prop:"couponId"},{default:d(()=>[u(n(g),{modelValue:A.couponId,"onUpdate:modelValue":l[0]||(l[0]=e=>A.couponId=e),placeholder:"请选择优惠券",filterable:"",style:{width:"100%"}},{default:d(()=>[(r(!0),p(m,null,c(E.value,e=>(r(),s(n(j),{key:e.id,label:`${e.name}(ID: ${e.id})`,value:e.id},null,8,["label","value"]))),128))]),_:1},8,["modelValue"])]),_:1})]),_:1},8,["model"])]),_:1},8,["model-value"]))}});export{I as _};
|
||||
@ -0,0 +1 @@
|
||||
import{_ as i}from"./add-item-card-dialog.vue_vue_type_script_setup_true_lang-gTKnBb_G.js";import"./index-BeZn6wgH.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import"./itemCards-z3asb6SW.js";import"./index-Dn4yxdMr.js";import"./castArray-BakW2F2h.js";import"./_initCloneObject-BHiCRTfC.js";import"./index-B43cMk6T.js";import"./index-CnhjG_Ys.js";import"./index-A3hG-0VQ.js";import"./index-DC47MImW.js";import"./index-s8Fl0Qzt.js";import"./index-rM5MDBEe.js";import"./token-DWNpOE8r.js";import"./debounce-C7sIggI-.js";import"./_baseIteratee-sVeOpCU9.js";import"./index-DZdoPtEQ.js";import"./index-BXzCnZ_d.js";import"./index-DdvpTWQd.js";import"./index-DfDWpFb3.js";import"./use-dialog-D_t6_hoT.js";import"./refs-Cw5r5QN8.js";export{i as default};
|
||||
@ -0,0 +1 @@
|
||||
var e=(e,a,l)=>new Promise((t,i)=>{var r=e=>{try{s(l.next(e))}catch(a){i(a)}},d=e=>{try{s(l.throw(e))}catch(a){i(a)}},s=e=>e.done?t(e.value):Promise.resolve(e.value).then(r,d);s((l=l.apply(e,a)).next())});import{d as a,r as l,e as t,y as i,f as r,G as d,o as s,w as o,b as u,i as n,a as m,H as p,I as c,N as v,O as y,p as f}from"./index-BeZn6wgH.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import{i as b}from"./itemCards-z3asb6SW.js";import{a as g,E as h}from"./index-Dn4yxdMr.js";import{E as j,a as _}from"./index-B43cMk6T.js";import{E as x}from"./index-BXzCnZ_d.js";import{E as q}from"./index-DfDWpFb3.js";const I=a({__name:"add-item-card-dialog",props:{visible:{type:Boolean}},emits:["update:visible","submit"],setup(a,{emit:I}){const V=a,w=I,k=l(),C=l(!1),E=l([]),U=t({cardId:null,quantity:1});i(()=>V.visible,e=>{});const $={cardId:[{required:!0,message:"请选择道具卡",trigger:"change"}],quantity:[{required:!0,message:"请输入数量",trigger:"change"}]},A=()=>e(this,null,function*(){var e;try{yield null==(e=k.value)?void 0:e.validate(),C.value=!0,w("submit",{card_id:U.cardId,quantity:U.quantity})}catch(a){}finally{C.value=!1}}),O=()=>{w("update:visible",!1)},P=()=>{var e;U.cardId=null,U.quantity=1,null==(e=k.value)||e.clearValidate()};return r(()=>e(this,null,function*(){try{const e=yield b.getList({page:1,page_size:100});E.value=Array.isArray(e.list)?e.list.map(e=>({id:e.id,name:e.name})):[]}catch(e){E.value=[]}})),(e,l)=>(s(),d(n(q),{"model-value":a.visible,title:"分配道具卡",width:"420px","close-on-click-modal":!1,"onUpdate:modelValue":l[2]||(l[2]=e=>w("update:visible",e)),onClosed:P},{footer:o(()=>[u(n(v),{onClick:y(O,["prevent"])},{default:o(()=>[...l[3]||(l[3]=[f("取消",-1)])]),_:1}),u(n(v),{type:"primary",loading:C.value,onClick:y(A,["prevent"])},{default:o(()=>[...l[4]||(l[4]=[f("确定",-1)])]),_:1},8,["loading"])]),default:o(()=>[u(n(g),{ref_key:"formRef",ref:k,model:U,rules:$,"label-width":"80px"},{default:o(()=>[u(n(h),{label:"道具卡",prop:"cardId"},{default:o(()=>[u(n(j),{modelValue:U.cardId,"onUpdate:modelValue":l[0]||(l[0]=e=>U.cardId=e),placeholder:"请选择道具卡",filterable:"",style:{width:"100%"}},{default:o(()=>[(s(!0),m(p,null,c(E.value,e=>(s(),d(n(_),{key:e.id,label:`${e.name}(ID: ${e.id})`,value:e.id},null,8,["label","value"]))),128))]),_:1},8,["modelValue"])]),_:1}),u(n(h),{label:"数量",prop:"quantity"},{default:o(()=>[u(n(x),{modelValue:U.quantity,"onUpdate:modelValue":l[1]||(l[1]=e=>U.quantity=e),min:1,max:100},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])]),_:1},8,["model-value"]))}});export{I as _};
|
||||
@ -0,0 +1 @@
|
||||
import{_ as i}from"./add-points-dialog.vue_vue_type_script_setup_true_lang-DJqP-5fS.js";import"./index-BeZn6wgH.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import"./index-Dn4yxdMr.js";import"./castArray-BakW2F2h.js";import"./_initCloneObject-BHiCRTfC.js";import"./index-BXzCnZ_d.js";import"./index-DdvpTWQd.js";import"./index-B43cMk6T.js";import"./index-CnhjG_Ys.js";import"./index-A3hG-0VQ.js";import"./index-DC47MImW.js";import"./index-s8Fl0Qzt.js";import"./index-rM5MDBEe.js";import"./token-DWNpOE8r.js";import"./debounce-C7sIggI-.js";import"./_baseIteratee-sVeOpCU9.js";import"./index-DZdoPtEQ.js";import"./index-DfDWpFb3.js";import"./use-dialog-D_t6_hoT.js";import"./refs-Cw5r5QN8.js";export{i as default};
|
||||
@ -0,0 +1 @@
|
||||
import{d as e,r as a,e as l,y as i,G as r,o as t,w as o,b as d,i as s,J as n,N as m,O as p,p as u}from"./index-BeZn6wgH.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import{a as v,E as c}from"./index-Dn4yxdMr.js";import{E as b}from"./index-BXzCnZ_d.js";import{E as f,a as k}from"./index-B43cMk6T.js";import{E as _}from"./index-DfDWpFb3.js";const y=e({__name:"add-points-dialog",props:{visible:{type:Boolean}},emits:["update:visible","submit"],setup(e,{emit:y}){const h=e,g=y,j=a(),x=a(!1),V=l({points:null,kind:"admin_add",remark:""});i(()=>h.visible,e=>{});const w={points:[{required:!0,message:"请输入积分数量",trigger:"blur"}],kind:[{required:!0,message:"请选择积分类型",trigger:"change"}]},C=()=>{return e=this,a=null,l=function*(){var e;try{yield null==(e=j.value)?void 0:e.validate(),x.value=!0,g("submit",{points:V.points,kind:V.kind,remark:V.remark})}catch(a){}finally{x.value=!1}},new Promise((i,r)=>{var t=e=>{try{d(l.next(e))}catch(a){r(a)}},o=e=>{try{d(l.throw(e))}catch(a){r(a)}},d=e=>e.done?i(e.value):Promise.resolve(e.value).then(t,o);d((l=l.apply(e,a)).next())});var e,a,l},E=()=>{g("update:visible",!1)},U=()=>{var e;V.points=null,V.kind="admin_add",V.remark="",null==(e=j.value)||e.clearValidate()};return(a,l)=>(t(),r(s(_),{"model-value":e.visible,title:"增加积分",width:"400px","close-on-click-modal":!1,"onUpdate:modelValue":l[3]||(l[3]=e=>g("update:visible",e)),onClosed:U},{footer:o(()=>[d(s(m),{onClick:p(E,["prevent"])},{default:o(()=>[...l[4]||(l[4]=[u("取消",-1)])]),_:1}),d(s(m),{type:"primary",loading:x.value,onClick:p(C,["prevent"])},{default:o(()=>[...l[5]||(l[5]=[u(" 确定 ",-1)])]),_:1},8,["loading"])]),default:o(()=>[d(s(v),{ref_key:"formRef",ref:j,model:V,rules:w,"label-width":"80px"},{default:o(()=>[d(s(c),{label:"积分数量",prop:"points"},{default:o(()=>[d(s(b),{modelValue:V.points,"onUpdate:modelValue":l[0]||(l[0]=e=>V.points=e),placeholder:"请输入积分数量",min:1,precision:0,style:{width:"100%"}},null,8,["modelValue"])]),_:1}),d(s(c),{label:"积分类型",prop:"kind"},{default:o(()=>[d(s(f),{modelValue:V.kind,"onUpdate:modelValue":l[1]||(l[1]=e=>V.kind=e),placeholder:"请选择积分类型",style:{width:"100%"}},{default:o(()=>[d(s(k),{label:"管理员增加",value:"admin_add"}),d(s(k),{label:"活动奖励",value:"activity_reward"}),d(s(k),{label:"签到奖励",value:"sign_reward"}),d(s(k),{label:"消费返还",value:"consume_return"})]),_:1},8,["modelValue"])]),_:1}),d(s(c),{label:"备注",prop:"remark"},{default:o(()=>[d(s(n),{modelValue:V.remark,"onUpdate:modelValue":l[2]||(l[2]=e=>V.remark=e),type:"textarea",placeholder:"请输入备注",rows:3},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])]),_:1},8,["model-value"]))}});export{y as _};
|
||||
1
build/resources/admin/assets/adminActivities-Dndna7OD.js
Normal file
1
build/resources/admin/assets/adminActivities-Dndna7OD.js
Normal file
@ -0,0 +1 @@
|
||||
import{bj as i}from"./index-BeZn6wgH.js";function s(s){return i.post({url:"admin/activities",params:s})}function t(s,t){return i.put({url:`admin/activities/${s}`,params:t})}function r(s){return i.del({url:`admin/activities/${s}`})}function a(s){return i.get({url:`admin/activities/${s}`})}function n(s,t=1,r=20){return i.get({url:`admin/activities/${s}/issues`,params:{page:t,page_size:r}})}function e(s,t){return i.post({url:`admin/activities/${s}/issues`,params:t})}function u(s,t,r){return i.put({url:`admin/activities/${s}/issues/${t}`,params:r})}function c(s,t){return i.del({url:`admin/activities/${s}/issues/${t}`})}function m(s,t){return i.get({url:`admin/activities/${s}/issues/${t}/rewards`})}function o(s,t,r){return i.post({url:`admin/activities/${s}/issues/${t}/rewards`,params:{rewards:r}})}function d(s,t,r,a){return i.put({url:`admin/activities/${s}/issues/${t}/rewards/${r}`,params:a})}function $(s,t,r){return i.del({url:`admin/activities/${s}/issues/${t}/rewards/${r}`})}function l(){return i.get({url:"admin/activity_categories"})}function p(s,t){return i.post({url:`admin/activities/${s}/issues/${t}/commit_random`})}function f(s,t){return i.get({url:`admin/activities/${s}/issues/${t}/commit_random`,showErrorMessage:!1})}function v(s,t){return i.get({url:`admin/activities/${s}/issues/${t}/commit_random/history`})}function g(s,t,r){return i.post({url:`admin/activities/${s}/issues/${t}/simulate_draw`,params:r,timeout:6e4})}function w(s,t,r){return i.post({url:`admin/activities/${s}/issues/${t}/batch_draw`,params:r,timeout:6e4})}function _(s,t,r){return i.post({url:`admin/activities/${s}/issues/${t}/verify_draw`,params:r})}function h(s){return i.get({url:`admin/draw_receipts/${s}`})}function b(s){return i.get({url:`admin/draw_receipts/log/${s}`})}function j(s){return i.get({url:"admin/users",params:s})}export{l as a,t as b,e as c,c as d,s as e,r as f,a as g,o as h,f as i,v as j,p as k,n as l,d as m,m as n,$ as o,j as p,w as q,h as r,g as s,b as t,u,_ as v};
|
||||
@ -0,0 +1 @@
|
||||
import{_ as i}from"./assign-title-dialog.vue_vue_type_script_setup_true_lang-u5TJw5RL.js";import"./index-BeZn6wgH.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import"./titles-Khr8sJTR.js";import"./index-Dn4yxdMr.js";import"./castArray-BakW2F2h.js";import"./_initCloneObject-BHiCRTfC.js";import"./index-B43cMk6T.js";import"./index-CnhjG_Ys.js";import"./index-A3hG-0VQ.js";import"./index-DC47MImW.js";import"./index-s8Fl0Qzt.js";import"./index-rM5MDBEe.js";import"./token-DWNpOE8r.js";import"./debounce-C7sIggI-.js";import"./_baseIteratee-sVeOpCU9.js";import"./index-DZdoPtEQ.js";import"./index-BVntTFko.js";import"./index-DdvpTWQd.js";import"./index-DfDWpFb3.js";import"./use-dialog-D_t6_hoT.js";import"./refs-Cw5r5QN8.js";export{i as default};
|
||||
@ -0,0 +1 @@
|
||||
var e=(e,l,a)=>new Promise((t,i)=>{var r=e=>{try{o(a.next(e))}catch(l){i(l)}},s=e=>{try{o(a.throw(e))}catch(l){i(l)}},o=e=>e.done?t(e.value):Promise.resolve(e.value).then(r,s);o((a=a.apply(e,l)).next())});import{d as l,r as a,e as t,f as i,G as r,o as s,w as o,b as d,i as m,a as p,H as u,I as n,J as v,N as c,O as f,p as x}from"./index-BeZn6wgH.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import{titlesApi as y}from"./titles-Khr8sJTR.js";import{a as h,E as b}from"./index-Dn4yxdMr.js";import{E as _,a as g}from"./index-B43cMk6T.js";import{E as k}from"./index-BVntTFko.js";import{E as j}from"./index-DfDWpFb3.js";const V=l({__name:"assign-title-dialog",props:{visible:{type:Boolean}},emits:["update:visible","submit"],setup(l,{emit:V}){const A=V,I=a(),w=a(!1),C=a([]),E=t({titleId:null,expiresAt:"",remark:""}),U={titleId:[{required:!0,message:"请选择称号",trigger:"change"}]},Y=()=>e(this,null,function*(){var e;try{yield null==(e=I.value)?void 0:e.validate(),w.value=!0;const l={title_id:E.titleId};E.expiresAt&&(l.expires_at=E.expiresAt),E.remark&&(l.remark=E.remark),A("submit",l)}catch(l){}finally{w.value=!1}}),D=()=>{A("update:visible",!1)},H=()=>{var e;E.titleId=null,E.expiresAt="",E.remark="",null==(e=I.value)||e.clearValidate()};return i(()=>e(this,null,function*(){try{const e=yield y.getList({page:1,page_size:100});C.value=Array.isArray(e.list)?e.list.map(e=>({id:e.id,name:e.name})):[]}catch(e){C.value=[]}})),(e,a)=>(s(),r(m(j),{"model-value":l.visible,title:"分配称号",width:"460px","close-on-click-modal":!1,"onUpdate:modelValue":a[3]||(a[3]=e=>A("update:visible",e)),onClosed:H},{footer:o(()=>[d(m(c),{onClick:f(D,["prevent"])},{default:o(()=>[...a[4]||(a[4]=[x("取消",-1)])]),_:1}),d(m(c),{type:"primary",loading:w.value,onClick:f(Y,["prevent"])},{default:o(()=>[...a[5]||(a[5]=[x("确定",-1)])]),_:1},8,["loading"])]),default:o(()=>[d(m(h),{ref_key:"formRef",ref:I,model:E,rules:U,"label-width":"90px"},{default:o(()=>[d(m(b),{label:"称号",prop:"titleId"},{default:o(()=>[d(m(_),{modelValue:E.titleId,"onUpdate:modelValue":a[0]||(a[0]=e=>E.titleId=e),placeholder:"请选择称号",filterable:"",style:{width:"100%"}},{default:o(()=>[(s(!0),p(u,null,n(C.value,e=>(s(),r(m(g),{key:e.id,label:`${e.name}(ID: ${e.id})`,value:e.id},null,8,["label","value"]))),128))]),_:1},8,["modelValue"])]),_:1}),d(m(b),{label:"过期时间",prop:"expiresAt"},{default:o(()=>[d(m(k),{modelValue:E.expiresAt,"onUpdate:modelValue":a[1]||(a[1]=e=>E.expiresAt=e),type:"datetime","value-format":"YYYY-MM-DDTHH:mm:ssZ",placeholder:"可选,默认为永久",style:{width:"100%"}},null,8,["modelValue"])]),_:1}),d(m(b),{label:"备注",prop:"remark"},{default:o(()=>[d(m(v),{modelValue:E.remark,"onUpdate:modelValue":a[2]||(a[2]=e=>E.remark=e),placeholder:"可选备注",maxlength:"100"},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])]),_:1},8,["model-value"]))}});export{V as _};
|
||||
File diff suppressed because one or more lines are too long
BIN
build/resources/admin/assets/batch-draw-dialog-DZsvoYD3.js.gz
Normal file
BIN
build/resources/admin/assets/batch-draw-dialog-DZsvoYD3.js.gz
Normal file
Binary file not shown.
1
build/resources/admin/assets/card-list-DGnniThn.js
Normal file
1
build/resources/admin/assets/card-list-DGnniThn.js
Normal file
@ -0,0 +1 @@
|
||||
import{_ as e}from"./card-list.vue_vue_type_script_setup_true_lang-BeKUYWu9.js";import"./index-BeZn6wgH.js";/* empty css *//* empty css */import"./index.vue_vue_type_script_setup_true_lang-BQI7d56A.js";import"./dashboard-CIzHAiEC.js";import"./index-D6O1cfnb.js";import"./index-DJP4F2zx.js";export{e as default};
|
||||
@ -0,0 +1 @@
|
||||
import{d as e,c as a,bg as t,aM as s,bh as u,y as n,aP as l,a as r,o as i,k as o,q as d,n as c,e as v,G as m,w as p,H as g,I as f,j as x,b as h,T as b}from"./index-BeZn6wgH.js";/* empty css *//* empty css */import{_}from"./index.vue_vue_type_script_setup_true_lang-BQI7d56A.js";import{f as y}from"./dashboard-CIzHAiEC.js";import{E as j}from"./index-D6O1cfnb.js";import{E as w}from"./index-DJP4F2zx.js";const S="easeOutExpo",C=e({__name:"index",props:{target:{default:0},duration:{default:2e3},autoStart:{type:Boolean,default:!0},decimals:{default:0},decimal:{default:"."},separator:{default:""},prefix:{default:""},suffix:{default:""},easing:{default:S},disabled:{type:Boolean,default:!1}},emits:["started","finished","paused","reset"],setup(e,{expose:v,emit:m}){const p=Number.EPSILON,g=e,f=m,x=(e,a,t)=>Number.isFinite(e)?e:t,h=(e,a,t)=>Math.max(a,Math.min(e,t)),b=a(()=>x(g.target,0,0)),_=a(()=>h(x(g.duration,0,2e3),100,6e4)),y=a(()=>h(x(g.decimals,0,0),0,10)),j=a(()=>{const e=g.easing;return e in t?e:S}),w=s(0),C=s(b.value),F=s(!1),M=s(!1),N=s(0),P=u(w,{duration:_,transition:a(()=>t[j.value]),onStarted:()=>{F.value=!0,M.value=!1,f("started",C.value)},onFinished:()=>{F.value=!1,M.value=!1,f("finished",C.value)}}),V=a(()=>{const e=M.value?N.value:P.value;if(!Number.isFinite(e))return`${g.prefix}0${g.suffix}`;const a=((e,a,t,s)=>{let u=a>0?e.toFixed(a):Math.floor(e).toString();if("."!==t&&u.includes(".")&&(u=u.replace(".",t)),s){const e=u.split(t);e[0]=e[0].replace(/\B(?=(\d{3})+(?!\d))/g,s),u=e.join(t)}return u})(e,y.value,g.decimal,g.separator);return`${g.prefix}${a}${g.suffix}`}),$=()=>{M.value=!1,N.value=0},E=e=>{if(g.disabled)return;const a=void 0!==e?e:C.value;Number.isFinite(a)&&(C.value=a,(e=>{const a=M.value?N.value:P.value;return Math.abs(a-e)<p})(a)||(M.value&&(w.value=N.value,$()),c(()=>{w.value=a})))},B=()=>{(F.value||M.value)&&(w.value=0,$(),f("paused",0))};return n(b,e=>{g.autoStart&&!g.disabled?E(e):C.value=e},{immediate:g.autoStart&&!g.disabled}),n(()=>g.disabled,e=>{e&&F.value&&B()}),l(()=>{F.value&&B()}),v({start:E,pause:()=>{F.value&&!M.value&&(M.value=!0,N.value=P.value,w.value=N.value,f("paused",N.value))},reset:(e=0)=>{const a=x(e,0,0);w.value=a,C.value=a,$(),f("reset")},stop:B,setTarget:e=>{Number.isFinite(e)&&(C.value=e,!F.value&&!g.autoStart||g.disabled||E(e))},get isRunning(){return F.value},get isPaused(){return M.value},get currentValue(){return M.value?N.value:P.value},get targetValue(){return C.value},get progress(){const e=M.value?N.value:P.value,a=C.value;return 0===a?0===e?1:0:Math.abs(e/a)}}),(e,a)=>(i(),r("span",{class:o(["text-g-900 tabular-nums",F.value?"transition-opacity duration-300 ease-in-out":""])},d(V.value),3))}}),F={class:"art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4"},M={class:"text-g-700 text-sm"},N={class:"flex-c mt-1"},P={class:"absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"},V=e({__name:"card-list",props:{range:{}},setup(e){const a=e,t=v([{des:"道具卡销量",icon:"ri:shopping-bag-3-line",startVal:0,duration:1e3,num:0,change:"+0%"},{des:"活动抽奖次数",icon:"ri:fire-line",startVal:0,duration:1e3,num:0,change:"+0%"},{des:"新用户注册数",icon:"ri:user-add-line",startVal:0,duration:1e3,num:0,change:"+0%"},{des:"用户总积分",icon:"ri:coin-line",startVal:0,duration:1e3,num:0,change:"+0%"}]);return n(()=>a.range,()=>{return e=this,s=null,u=function*(){try{const e=yield y(a.range);t[0].num=e.itemCardSales,t[0].change=e.itemCardChange,t[1].num=e.drawCount,t[1].change=e.drawChange,t[2].num=e.newUsers,t[2].change=e.newUserChange,t[3].num=e.totalPoints,t[3].change=e.pointsChange}catch(e){b.error("获取卡片数据失败")}},new Promise((a,t)=>{var n=e=>{try{r(u.next(e))}catch(a){t(a)}},l=e=>{try{r(u.throw(e))}catch(a){t(a)}},r=e=>e.done?a(e.value):Promise.resolve(e.value).then(n,l);r((u=u.apply(e,s)).next())});var e,s,u},{immediate:!0}),(e,a)=>{const s=C,u=_,n=j,l=w;return i(),m(l,{gutter:20,class:"flex"},{default:p(()=>[(i(!0),r(g,null,f(t,(e,t)=>(i(),m(n,{key:t,sm:12,md:6,lg:6},{default:p(()=>[x("div",F,[x("span",M,d(e.des),1),h(s,{class:"text-[26px] font-medium mt-2",target:e.num,duration:1300},null,8,["target"]),x("div",N,[a[0]||(a[0]=x("span",{class:"text-xs text-g-600"},"较上周",-1)),x("span",{class:o(["ml-1 text-xs font-semibold",[-1===e.change.indexOf("+")?"text-danger":"text-success"]])},d(e.change),3)]),x("div",P,[h(u,{icon:e.icon,class:"text-xl text-theme"},null,8,["icon"])])])]),_:2},1024))),128))]),_:1})}}});export{V as _};
|
||||
1
build/resources/admin/assets/castArray-BakW2F2h.js
Normal file
1
build/resources/admin/assets/castArray-BakW2F2h.js
Normal file
@ -0,0 +1 @@
|
||||
import{c8 as r}from"./index-BeZn6wgH.js";function n(){if(!arguments.length)return[];var n=arguments[0];return r(n)?n:[n]}export{n as c};
|
||||
1
build/resources/admin/assets/category-search-DdiwTmeZ.js
Normal file
1
build/resources/admin/assets/category-search-DdiwTmeZ.js
Normal file
@ -0,0 +1 @@
|
||||
var e=Object.defineProperty,a=Object.getOwnPropertySymbols,t=Object.prototype.hasOwnProperty,l=Object.prototype.propertyIsEnumerable,r=(a,t,l)=>t in a?e(a,t,{enumerable:!0,configurable:!0,writable:!0,value:l}):a[t]=l,o=(e,o)=>{for(var s in o||(o={}))t.call(o,s)&&r(e,s,o[s]);if(a)for(var s of a(o))l.call(o,s)&&r(e,s,o[s]);return e};import{d as s,r as p,y as i,F as m,G as d,o as u,w as n,b as f,i as c,J as j,P as b,M as _,N as v,p as x,ai as y,bb as V}from"./index-BeZn6wgH.js";/* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css *//* empty css */import{a as h,E as O}from"./index-Dn4yxdMr.js";import{E as g}from"./index-DJP4F2zx.js";import{E as w}from"./index-D6O1cfnb.js";import{E,a as P}from"./index-B43cMk6T.js";import{E as C}from"./index-gJfKG9HJ.js";import{E as N}from"./index-Bh_wUTwB.js";import{_ as S}from"./_plugin-vue_export-helper-BCo6x5W8.js";import"./castArray-BakW2F2h.js";import"./_initCloneObject-BHiCRTfC.js";import"./index-CnhjG_Ys.js";import"./index-A3hG-0VQ.js";import"./index-DC47MImW.js";import"./index-s8Fl0Qzt.js";import"./index-rM5MDBEe.js";import"./token-DWNpOE8r.js";import"./debounce-C7sIggI-.js";import"./_baseIteratee-sVeOpCU9.js";import"./index-DZdoPtEQ.js";const k=S(s({__name:"category-search",props:{modelValue:{}},emits:["update:modelValue","search","reset"],setup(e,{emit:a}){const t=e,l=a,r=p(o({},t.modelValue));i(()=>t.modelValue,(e,a)=>{JSON.stringify(e)!==JSON.stringify(a)&&(r.value=o({},e))},{deep:!0});let s=null;i(r,e=>{s&&clearTimeout(s),s=setTimeout(()=>{l("update:modelValue",o({},e))},100)},{deep:!0});const S=()=>{l("search",r.value)},k=()=>{r.value={name:void 0,status:void 0},l("reset")};return(a,t)=>{const l=m("ripple");return u(),d(c(N),{class:"search-card",shadow:"never"},{default:n(()=>[f(c(h),{ref:"formRef",model:e.modelValue,"label-width":"80px"},{default:n(()=>[f(c(g),{gutter:20},{default:n(()=>[f(c(w),{span:8},{default:n(()=>[f(c(O),{label:"分类名称",prop:"name"},{default:n(()=>[f(c(j),{modelValue:r.value.name,"onUpdate:modelValue":t[0]||(t[0]=e=>r.value.name=e),placeholder:"请输入分类名称",clearable:"",onKeyup:b(S,["enter"])},null,8,["modelValue"])]),_:1})]),_:1}),f(c(w),{span:6},{default:n(()=>[f(c(O),{label:"状态",prop:"status"},{default:n(()=>[f(c(E),{modelValue:r.value.status,"onUpdate:modelValue":t[1]||(t[1]=e=>r.value.status=e),placeholder:"请选择状态",clearable:""},{default:n(()=>[f(c(P),{value:1,label:"启用"}),f(c(P),{value:2,label:"禁用"})]),_:1},8,["modelValue"])]),_:1})]),_:1}),f(c(w),{span:6},{default:n(()=>[f(c(O),{"label-width":"0"},{default:n(()=>[f(c(C),null,{default:n(()=>[_((u(),d(c(v),{type:"primary",onClick:S},{default:n(()=>[f(c(y),{class:"mr-1"},{default:n(()=>[f(c(V))]),_:1}),t[2]||(t[2]=x(" 搜索 ",-1))]),_:1})),[[l]]),_((u(),d(c(v),{onClick:k},{default:n(()=>[...t[3]||(t[3]=[x("重置",-1)])]),_:1})),[[l]])]),_:1})]),_:1})]),_:1})]),_:1})]),_:1},8,["model"])]),_:1})}}}),[["__scopeId","data-v-9b5715ff"]]);export{k as default};
|
||||
1
build/resources/admin/assets/cloneDeep-D72mKKmf.js
Normal file
1
build/resources/admin/assets/cloneDeep-D72mKKmf.js
Normal file
@ -0,0 +1 @@
|
||||
import{b as r}from"./index-Dn4yxdMr.js";function n(n){return r(n,5)}export{n as c};
|
||||
1
build/resources/admin/assets/coupon-dialog-BmnS2ooT.js
Normal file
1
build/resources/admin/assets/coupon-dialog-BmnS2ooT.js
Normal file
File diff suppressed because one or more lines are too long
1
build/resources/admin/assets/coupon-dialog-DkKEH8hx.css
Normal file
1
build/resources/admin/assets/coupon-dialog-DkKEH8hx.css
Normal file
@ -0,0 +1 @@
|
||||
.form-tip[data-v-66d7f5fb]{margin-left:8px;color:#909399;font-size:12px}
|
||||
1
build/resources/admin/assets/coupons-BolQHY2x.js
Normal file
1
build/resources/admin/assets/coupons-BolQHY2x.js
Normal file
@ -0,0 +1 @@
|
||||
import{bj as s}from"./index-BeZn6wgH.js";const t={getList:t=>s.get({url:"admin/system_coupons",params:t}),create:t=>s.post({url:"admin/system_coupons",data:t}),update:(t,e)=>s.put({url:`admin/system_coupons/${t}`,data:e}),delete:t=>s.del({url:`admin/system_coupons/${t}`})};export{t as c};
|
||||
1
build/resources/admin/assets/dashboard-CIzHAiEC.js
Normal file
1
build/resources/admin/assets/dashboard-CIzHAiEC.js
Normal file
@ -0,0 +1 @@
|
||||
import{bj as a}from"./index-BeZn6wgH.js";function r(r="7d"){return a.get({url:"admin/dashboard/cards",params:{rangeType:r}})}function d(r="30d",d="day"){return a.get({url:"admin/dashboard/draw_trend",params:{rangeType:r,granularity:d}})}function n(r=1,d=20,n){return a.get({url:"admin/dashboard/new_users",params:{page:r,page_size:d,period:n}})}function e(r,d=50){return a.get({url:"admin/dashboard/draw_stream",params:{since_id:r,limit:d}})}function s(r=50){return a.get({url:"admin/dashboard/todos",params:{limit:r}})}export{e as a,n as b,d as c,s as d,r as f};
|
||||
1
build/resources/admin/assets/debounce-C7sIggI-.js
Normal file
1
build/resources/admin/assets/debounce-C7sIggI-.js
Normal file
@ -0,0 +1 @@
|
||||
import{dn as t,c0 as n,dp as r}from"./index-BeZn6wgH.js";var i=/\s/;var e=/^\s+/;function u(t){return t?t.slice(0,function(t){for(var n=t.length;n--&&i.test(t.charAt(n)););return n}(t)+1).replace(e,""):t}var o=/^[-+]0x[0-9a-f]+$/i,a=/^0b[01]+$/i,f=/^0o[0-7]+$/i,c=parseInt;function v(r){if("number"==typeof r)return r;if(t(r))return NaN;if(n(r)){var i="function"==typeof r.valueOf?r.valueOf():r;r=n(i)?i+"":i}if("string"!=typeof r)return 0===r?r:+r;r=u(r);var e=a.test(r);return e||f.test(r)?c(r.slice(2),e?2:8):o.test(r)?NaN:+r}var s=function(){return r.Date.now()},d=Math.max,l=Math.min;function m(t,r,i){var e,u,o,a,f,c,m=0,p=!1,h=!1,x=!0;if("function"!=typeof t)throw new TypeError("Expected a function");function T(n){var r=e,i=u;return e=u=void 0,m=n,a=t.apply(i,r)}function y(t){var n=t-c;return void 0===c||n>=r||n<0||h&&t-m>=o}function g(){var t=s();if(y(t))return N(t);f=setTimeout(g,function(t){var n=r-(t-c);return h?l(n,o-(t-m)):n}(t))}function N(t){return f=void 0,x&&e?T(t):(e=u=void 0,a)}function w(){var t=s(),n=y(t);if(e=arguments,u=this,c=t,n){if(void 0===f)return function(t){return m=t,f=setTimeout(g,r),p?T(t):a}(c);if(h)return clearTimeout(f),f=setTimeout(g,r),T(c)}return void 0===f&&(f=setTimeout(g,r)),a}return r=v(r)||0,n(i)&&(p=!!i.leading,o=(h="maxWait"in i)?d(v(i.maxWait)||0,r):o,x="trailing"in i?!!i.trailing:x),w.cancel=function(){void 0!==f&&clearTimeout(f),m=0,e=c=u=f=void 0},w.flush=function(){return void 0===f?a:N(s())},w}export{m as d,v as t};
|
||||
1
build/resources/admin/assets/dynamic-stats-BMG993Kz.js
Normal file
1
build/resources/admin/assets/dynamic-stats-BMG993Kz.js
Normal file
@ -0,0 +1 @@
|
||||
import{_ as t}from"./dynamic-stats.vue_vue_type_script_setup_true_lang-DdAhPVua.js";import"./index-BeZn6wgH.js";/* empty css *//* empty css */import"./dashboard-CIzHAiEC.js";import"./index-s8Fl0Qzt.js";import"./index-rM5MDBEe.js";export{t as default};
|
||||
@ -0,0 +1 @@
|
||||
import{d as s,r as a,f as e,aP as t,a as l,o as r,j as n,p as i,q as o,b as c,w as d,H as m,I as u,T as v,G as p}from"./index-BeZn6wgH.js";/* empty css *//* empty css */import{a as h}from"./dashboard-CIzHAiEC.js";import{E as f}from"./index-s8Fl0Qzt.js";import{E as x}from"./index-rM5MDBEe.js";const y={class:"art-card h-128 p-5 mb-5 max-sm:mb-4"},b={class:"art-card-header"},w={class:"title"},g={class:"text-success"},j={class:"h-9/10 mt-2 overflow-hidden"},N={class:"text-g-800 font-medium"},_={class:"text-theme"},k={key:0,class:"ml-2"},I=s({__name:"dynamic-stats",setup(s){const I=a([]),E=a(0);let P,z=null;const H=()=>{return s=this,a=null,e=function*(){try{const{list:s,sinceId:a}=yield h(P,50);if(s.length){P=a;const e=new Set(I.value.map(s=>s.id)),t=s.filter(s=>!e.has(s.id));t.length&&(I.value.unshift(...t),E.value=t.length,I.value.length>100&&I.value.splice(100))}}catch(s){v.error("获取抽奖动态失败")}},new Promise((t,l)=>{var r=s=>{try{i(e.next(s))}catch(a){l(a)}},n=s=>{try{i(e.throw(s))}catch(a){l(a)}},i=s=>s.done?t(s.value):Promise.resolve(s.value).then(r,n);i((e=e.apply(s,a)).next())});var s,a,e};return e(()=>{H(),z=window.setInterval(H,5e3)}),t(()=>{z&&window.clearInterval(z)}),(s,a)=>{const e=x,t=f;return r(),l("div",y,[n("div",b,[n("div",w,[a[1]||(a[1]=n("h4",null,"实时抽奖动态",-1)),n("p",null,[a[0]||(a[0]=i("新增",-1)),n("span",g,"+"+o(E.value),1)])])]),n("div",j,[c(t,null,{default:d(()=>[(r(!0),l(m,null,u(I.value,(s,t)=>(r(),l("div",{class:"h-17.5 leading-17.5 border-b border-g-300 text-sm overflow-hidden last:border-b-0",key:s.id},[n("span",N,o(s.nickname),1),a[3]||(a[3]=n("span",{class:"mx-2 text-g-600"},"在",-1)),n("span",_,o(s.activityName?`${s.activityName}-${s.issueNumber}`:s.issueName),1),s.isWinner?(r(),l("span",k,"中奖 "+o(s.prizeName||""),1)):(r(),p(e,{key:1,size:"small",type:"info",class:"ml-2"},{default:d(()=>[...a[2]||(a[2]=[i("参与",-1)])]),_:1}))]))),128))]),_:1})])])}}});export{I as _};
|
||||
1
build/resources/admin/assets/el-alert-D_ZNkn_N.js
Normal file
1
build/resources/admin/assets/el-alert-D_ZNkn_N.js
Normal file
@ -0,0 +1 @@
|
||||
var e=Object.defineProperty,s=Object.defineProperties,t=Object.getOwnPropertyDescriptors,a=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,l=Object.prototype.propertyIsEnumerable,i=(s,t,a)=>t in s?e(s,t,{enumerable:!0,configurable:!0,writable:!0,value:a}):s[t]=a,r=(e,s)=>{for(var t in s||(s={}))o.call(s,t)&&i(e,t,s[t]);if(a)for(var t of a(s))l.call(s,t)&&i(e,t,s[t]);return e},n=(e,a)=>s(e,t(a));import{ad as c,a8 as p,dT as f,dU as d,a0 as u,d as y,cx as b,a1 as v,r as m,c as w,ay as h,bs as g,G as k,o as j,w as O,M as x,j as A,k as C,i as S,m as E,ai as P,l as $,aE as _,a as T,p as B,q as I,H as q,b as z,dV as D,aj as G,a3 as H,az as M}from"./index-BeZn6wgH.js";import{u as N,a as U}from"./index-DC47MImW.js";const V=p(n(r({title:{type:String,default:""},description:{type:String,default:""},type:{type:String,values:f(d),default:"info"},closable:{type:Boolean,default:!0},closeText:{type:String,default:""},showIcon:Boolean,center:Boolean,effect:{type:String,values:["light","dark"],default:"light"}},N),{showAfter:Number})),F={open:()=>!0,close:e=>c(e)||e instanceof Event},J=y({name:"ElAlert"});const K=M(u(y(n(r({},J),{props:V,emits:F,setup(e,{emit:s}){const t=e,{Close:a}=D,o=b(),l=v("alert"),i=m(c(t.showAfter)),r=w(()=>d[t.type]),n=w(()=>!(!t.description&&!o.default)),p=e=>{i.value=!1,s("close",e)},{onOpen:f,onClose:u}=U({showAfter:h(t,"showAfter",0),hideAfter:h(t,"hideAfter"),autoClose:h(t,"autoClose"),open:()=>{i.value=!0,s("open")},close:p});return g&&f(),(e,s)=>(j(),k(H,{name:S(l).b("fade"),persisted:""},{default:O(()=>[x(A("div",{class:C([S(l).b(),S(l).m(e.type),S(l).is("center",e.center),S(l).is(e.effect)]),role:"alert"},[e.showIcon&&(e.$slots.icon||S(r))?(j(),k(S(P),{key:0,class:C([S(l).e("icon"),{[S(l).is("big")]:S(n)}])},{default:O(()=>[$(e.$slots,"icon",{},()=>[(j(),k(_(S(r))))])]),_:3},8,["class"])):E("v-if",!0),A("div",{class:C(S(l).e("content"))},[e.title||e.$slots.title?(j(),T("span",{key:0,class:C([S(l).e("title"),{"with-description":S(n)}])},[$(e.$slots,"title",{},()=>[B(I(e.title),1)])],2)):E("v-if",!0),S(n)?(j(),T("p",{key:1,class:C(S(l).e("description"))},[$(e.$slots,"default",{},()=>[B(I(e.description),1)])],2)):E("v-if",!0),e.closable?(j(),T(q,{key:2},[e.closeText?(j(),T("div",{key:0,class:C([S(l).e("close-btn"),S(l).is("customed")]),onClick:p},I(e.closeText),3)):(j(),k(S(P),{key:1,class:C(S(l).e("close-btn")),onClick:S(u)},{default:O(()=>[z(S(a))]),_:1},8,["class","onClick"]))],64)):E("v-if",!0)],2)],2),[[G,i.value]])]),_:3},8,["name"]))}})),[["__file","alert.vue"]]));export{K as E};
|
||||
1
build/resources/admin/assets/el-divider-Tx3HfaEK.js
Normal file
1
build/resources/admin/assets/el-divider-Tx3HfaEK.js
Normal file
@ -0,0 +1 @@
|
||||
var e=Object.defineProperty,t=Object.defineProperties,r=Object.getOwnPropertyDescriptors,a=Object.getOwnPropertySymbols,s=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,i=(t,r,a)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:a}):t[r]=a;import{a8 as l,at as n,a0 as c,d,a1 as p,c as f,a as v,o as y,m as b,k as u,i as O,l as m,h as g,az as j}from"./index-BeZn6wgH.js";const P=l({direction:{type:String,values:["horizontal","vertical"],default:"horizontal"},contentPosition:{type:String,values:["left","center","right"],default:"center"},borderStyle:{type:n(String),default:"solid"}}),S=d({name:"ElDivider"}),h=d((w=((e,t)=>{for(var r in t||(t={}))s.call(t,r)&&i(e,r,t[r]);if(a)for(var r of a(t))o.call(t,r)&&i(e,r,t[r]);return e})({},S),t(w,r({props:P,setup(e){const t=e,r=p("divider"),a=f(()=>r.cssVar({"border-style":t.borderStyle}));return(e,t)=>(y(),v("div",{class:u([O(r).b(),O(r).m(e.direction)]),style:g(O(a)),role:"separator"},[e.$slots.default&&"vertical"!==e.direction?(y(),v("div",{key:0,class:u([O(r).e("text"),O(r).is(e.contentPosition)])},[m(e.$slots,"default")],2)):b("v-if",!0)],6))}}))));var w;const x=j(c(h,[["__file","divider.vue"]]));export{x as E};
|
||||
File diff suppressed because one or more lines are too long
BIN
build/resources/admin/assets/el-dropdown-item-D3gOKOyu.js.gz
Normal file
BIN
build/resources/admin/assets/el-dropdown-item-D3gOKOyu.js.gz
Normal file
Binary file not shown.
1
build/resources/admin/assets/el-pagination-BybCuExY.js
Normal file
1
build/resources/admin/assets/el-pagination-BybCuExY.js
Normal file
File diff suppressed because one or more lines are too long
BIN
build/resources/admin/assets/el-pagination-BybCuExY.js.gz
Normal file
BIN
build/resources/admin/assets/el-pagination-BybCuExY.js.gz
Normal file
Binary file not shown.
1
build/resources/admin/assets/el-popover-BY7WM4__.js
Normal file
1
build/resources/admin/assets/el-popover-BY7WM4__.js
Normal file
@ -0,0 +1 @@
|
||||
var e=Object.defineProperty,t=Object.defineProperties,r=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,p=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,i=(e,t)=>{for(var r in t||(t={}))a.call(t,r)&&s(e,r,t[r]);if(o)for(var r of o(t))p.call(t,r)&&s(e,r,t[r]);return e},n=(e,o)=>t(e,r(o));import{u as l,d,E as f}from"./index-CnhjG_Ys.js";import{d as b}from"./el-dropdown-item-D3gOKOyu.js";import{cg as c,a8 as u,a0 as v,d as g,c as y,a1 as m,r as w,i as h,bu as O,G as j,o as x,w as S,l as k,m as A,a as P,k as C,q as N,p as R,a2 as $,az as B,dK as E}from"./index-BeZn6wgH.js";const K=u({trigger:d.trigger,triggerKeys:d.triggerKeys,placement:b.placement,disabled:d.disabled,visible:l.visible,transition:l.transition,popperOptions:b.popperOptions,tabindex:b.tabindex,content:l.content,popperStyle:l.popperStyle,popperClass:l.popperClass,enterable:n(i({},l.enterable),{default:!0}),effect:n(i({},l.effect),{default:"light"}),teleported:l.teleported,appendTo:l.appendTo,title:String,width:{type:[String,Number],default:150},offset:{type:Number,default:void 0},showAfter:{type:Number,default:0},hideAfter:{type:Number,default:200},autoClose:{type:Number,default:0},showArrow:{type:Boolean,default:!0},persistent:{type:Boolean,default:!0},"onUpdate:visible":{type:Function}}),U={"update:visible":e=>c(e),"before-enter":()=>!0,"before-leave":()=>!0,"after-enter":()=>!0,"after-leave":()=>!0},_=g({name:"ElPopover"}),T=g(n(i({},_),{props:K,emits:U,setup(e,{expose:t,emit:r}){const o=e,a=y(()=>o["onUpdate:visible"]),p=m("popover"),s=w(),i=y(()=>{var e;return null==(e=h(s))?void 0:e.popperRef}),n=y(()=>[{width:O(o.width)},o.popperStyle]),l=y(()=>[p.b(),o.popperClass,{[p.m("plain")]:!!o.content}]),d=y(()=>o.transition===`${p.namespace.value}-fade-in-linear`),b=()=>{r("before-enter")},c=()=>{r("before-leave")},u=()=>{r("after-enter")},v=()=>{r("update:visible",!1),r("after-leave")};return t({popperRef:i,hide:()=>{var e;null==(e=s.value)||e.hide()}}),(e,t)=>(x(),j(h(f),$({ref_key:"tooltipRef",ref:s},e.$attrs,{trigger:e.trigger,"trigger-keys":e.triggerKeys,placement:e.placement,disabled:e.disabled,visible:e.visible,transition:e.transition,"popper-options":e.popperOptions,tabindex:e.tabindex,content:e.content,offset:e.offset,"show-after":e.showAfter,"hide-after":e.hideAfter,"auto-close":e.autoClose,"show-arrow":e.showArrow,"aria-label":e.title,effect:e.effect,enterable:e.enterable,"popper-class":h(l),"popper-style":h(n),teleported:e.teleported,"append-to":e.appendTo,persistent:e.persistent,"gpu-acceleration":h(d),"onUpdate:visible":h(a),onBeforeShow:b,onBeforeHide:c,onShow:u,onHide:v}),{content:S(()=>[e.title?(x(),P("div",{key:0,class:C(h(p).e("title")),role:"title"},N(e.title),3)):A("v-if",!0),k(e.$slots,"default",{},()=>[R(N(e.content),1)])]),default:S(()=>[e.$slots.reference?k(e.$slots,"reference",{key:0}):A("v-if",!0)]),_:3},16,["trigger","trigger-keys","placement","disabled","visible","transition","popper-options","tabindex","content","offset","show-after","hide-after","auto-close","show-arrow","aria-label","effect","enterable","popper-class","popper-style","teleported","append-to","persistent","gpu-acceleration","onUpdate:visible"]))}}));const H=(e,t)=>{const r=t.arg||t.value,o=null==r?void 0:r.popperRef;o&&(o.triggerRef=e)};const q=B(v(T,[["__file","popover.vue"]]),{directive:E({mounted(e,t){H(e,t)},updated(e,t){H(e,t)}},"popover")});export{q as E};
|
||||
1
build/resources/admin/assets/el-progress-O14AXzNU.js
Normal file
1
build/resources/admin/assets/el-progress-O14AXzNU.js
Normal file
@ -0,0 +1 @@
|
||||
var e=Object.defineProperty,t=Object.defineProperties,a=Object.getOwnPropertyDescriptors,s=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable,o=(t,a,s)=>a in t?e(t,a,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[a]=s;import{a8 as i,at as l,a0 as c,d as p,a1 as u,c as d,bz as f,bA as b,bB as y,ba as h,bf as v,bC as g,ah as k,a as m,o as w,m as x,k as $,i as O,j,h as B,l as I,q as N,G as P,w as D,aE as F,ai as S,az as T}from"./index-BeZn6wgH.js";const E=i({type:{type:String,default:"line",values:["line","circle","dashboard"]},percentage:{type:Number,default:0,validator:e=>e>=0&&e<=100},status:{type:String,default:"",values:["","success","exception","warning"]},indeterminate:Boolean,duration:{type:Number,default:3},strokeWidth:{type:Number,default:6},strokeLinecap:{type:l(String),default:"round"},textInside:Boolean,width:{type:Number,default:126},showText:{type:Boolean,default:!0},color:{type:l([String,Array,Function]),default:""},striped:Boolean,stripedFlow:Boolean,format:{type:l(Function),default:e=>`${e}%`}}),W=p({name:"ElProgress"}),z=p((L=((e,t)=>{for(var a in t||(t={}))r.call(t,a)&&o(e,a,t[a]);if(s)for(var a of s(t))n.call(t,a)&&o(e,a,t[a]);return e})({},W),_={props:E,setup(e){const t=e,a={success:"#13ce66",exception:"#ff4949",warning:"#e6a23c",default:"#20a0ff"},s=u("progress"),r=d(()=>{const e={width:`${t.percentage}%`,animationDuration:`${t.duration}s`},a=A(t.percentage);return a.includes("gradient")?e.background=a:e.backgroundColor=a,e}),n=d(()=>(t.strokeWidth/t.width*100).toFixed(1)),o=d(()=>["circle","dashboard"].includes(t.type)?Number.parseInt(""+(50-Number.parseFloat(n.value)/2),10):0),i=d(()=>{const e=o.value,a="dashboard"===t.type;return`\n M 50 50\n m 0 ${a?"":"-"}${e}\n a ${e} ${e} 0 1 1 0 ${a?"-":""}${2*e}\n a ${e} ${e} 0 1 1 0 ${a?"":"-"}${2*e}\n `}),l=d(()=>2*Math.PI*o.value),c=d(()=>"dashboard"===t.type?.75:1),p=d(()=>-1*l.value*(1-c.value)/2+"px"),T=d(()=>({strokeDasharray:`${l.value*c.value}px, ${l.value}px`,strokeDashoffset:p.value})),E=d(()=>({strokeDasharray:`${l.value*c.value*(t.percentage/100)}px, ${l.value}px`,strokeDashoffset:p.value,transition:"stroke-dasharray 0.6s ease 0s, stroke 0.6s ease, opacity ease 0.6s"})),W=d(()=>{let e;return e=t.color?A(t.percentage):a[t.status]||a.default,e}),z=d(()=>"warning"===t.status?f:"line"===t.type?"success"===t.status?b:y:"success"===t.status?h:v),L=d(()=>"line"===t.type?12+.4*t.strokeWidth:.111111*t.width+2),_=d(()=>t.format(t.percentage)),A=e=>{var a;const{color:s}=t;if(g(s))return s(e);if(k(s))return s;{const t=function(e){const t=100/e.length;return e.map((e,a)=>k(e)?{color:e,percentage:(a+1)*t}:e).sort((e,t)=>e.percentage-t.percentage)}(s);for(const a of t)if(a.percentage>e)return a.color;return null==(a=t[t.length-1])?void 0:a.color}};return(e,t)=>(w(),m("div",{class:$([O(s).b(),O(s).m(e.type),O(s).is(e.status),{[O(s).m("without-text")]:!e.showText,[O(s).m("text-inside")]:e.textInside}]),role:"progressbar","aria-valuenow":e.percentage,"aria-valuemin":"0","aria-valuemax":"100"},["line"===e.type?(w(),m("div",{key:0,class:$(O(s).b("bar"))},[j("div",{class:$(O(s).be("bar","outer")),style:B({height:`${e.strokeWidth}px`})},[j("div",{class:$([O(s).be("bar","inner"),{[O(s).bem("bar","inner","indeterminate")]:e.indeterminate},{[O(s).bem("bar","inner","striped")]:e.striped},{[O(s).bem("bar","inner","striped-flow")]:e.stripedFlow}]),style:B(O(r))},[(e.showText||e.$slots.default)&&e.textInside?(w(),m("div",{key:0,class:$(O(s).be("bar","innerText"))},[I(e.$slots,"default",{percentage:e.percentage},()=>[j("span",null,N(O(_)),1)])],2)):x("v-if",!0)],6)],6)],2)):(w(),m("div",{key:1,class:$(O(s).b("circle")),style:B({height:`${e.width}px`,width:`${e.width}px`})},[(w(),m("svg",{viewBox:"0 0 100 100"},[j("path",{class:$(O(s).be("circle","track")),d:O(i),stroke:`var(${O(s).cssVarName("fill-color-light")}, #e5e9f2)`,"stroke-linecap":e.strokeLinecap,"stroke-width":O(n),fill:"none",style:B(O(T))},null,14,["d","stroke","stroke-linecap","stroke-width"]),j("path",{class:$(O(s).be("circle","path")),d:O(i),stroke:O(W),fill:"none",opacity:e.percentage?1:0,"stroke-linecap":e.strokeLinecap,"stroke-width":O(n),style:B(O(E))},null,14,["d","stroke","opacity","stroke-linecap","stroke-width"])]))],6)),!e.showText&&!e.$slots.default||e.textInside?x("v-if",!0):(w(),m("div",{key:2,class:$(O(s).e("text")),style:B({fontSize:`${O(L)}px`})},[I(e.$slots,"default",{percentage:e.percentage},()=>[e.status?(w(),P(O(S),{key:1},{default:D(()=>[(w(),P(F(O(z))))]),_:1})):(w(),m("span",{key:0},N(O(_)),1))])],6))],10,["aria-valuenow"]))}},t(L,a(_))));var L,_;const A=T(c(z,[["__file","progress.vue"]]));export{A as E};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user