chore: 清理无用文件与优化代码结构

refactor(utils): 修复密码哈希比较逻辑错误
feat(user): 新增按状态筛选优惠券接口
docs: 添加虚拟发货与任务中心相关文档
fix(wechat): 修正Code2Session上下文传递问题
test: 补充订单折扣与积分转换测试用例
build: 更新配置文件与构建脚本
style: 清理多余的空行与注释
This commit is contained in:
邹方成 2025-12-18 17:35:55 +08:00
parent 642b3cf7dd
commit 45815bfb7d
574 changed files with 31550 additions and 15118 deletions

BIN
.DS_Store vendored

Binary file not shown.

2
.gitignore vendored
View File

@ -25,3 +25,5 @@ go.work.sum
*.idea *.idea
resources/* resources/*
build/resources/admin/
logs/

View File

@ -0,0 +1,38 @@
# 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).

View File

@ -0,0 +1,31 @@
# 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.

View File

@ -0,0 +1,38 @@
# 渠道管理与用户来源统计实施计划
针对您的需求,我将把现有的“小程序二维码生成”功能升级为完整的“渠道管理”系统,并增加用户来源统计。
## 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。

View File

@ -0,0 +1,36 @@
# 修复一番赏策略库存扣减竞态问题
经检查,`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
}
```
此修改将彻底解决库存扣减的并发安全问题。

View File

@ -0,0 +1,73 @@
## 概述
为 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` 控制
- 文档完整,前端对接字段明确;无越权与泄露

View File

@ -0,0 +1,53 @@
# 添加抽奖模拟功能 (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. 验证计划
- **功能验证**: 使用现有的活动/期号调用模拟接口,输入不同的人数和次数,验证返回的概率分布是否符合预期(例如:高权重奖品中奖率更高,库存耗尽后不再中奖)。

View File

@ -0,0 +1,27 @@
# 故障排查与修复计划
针对您提出的四个问题,我制定了以下修复计划。我将使用中文进行回答和执行。
## 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 或界面操作验证修复效果。

View File

@ -0,0 +1,54 @@
# 一番赏活动创建表单优化计划
用户反馈在创建一番赏活动时,选择即时/定时开奖的页面逻辑不对。通过分析代码 `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` 中的表单项可见性。

View File

@ -0,0 +1,77 @@
## 背景与现状概要
- 技术栈:后端 GoGin 封装于 `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 角色映射),我将在文档中补充并与现有实现对齐。

View File

@ -0,0 +1,49 @@
## 问题归因
- 活动/期号为空后端仅在存在抽奖日志时填充活动信息未抽取或刚支付时为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为“-”
- 即时模式支付后,轮询显示中奖等级与商品信息
- 明细表显示正确单价/金额
## 验收标准
- 所有问题项均有正确数据或合理占位显示
- 即时/定时、积分/金额支付四种组合下展示正确
- 架构不破坏现有接口:前端仅增量使用新增字段

View File

@ -0,0 +1,127 @@
## 目标
* 全面清理未用代码、注释废弃块、空文件与无用测试
* 识别并重构重复代码重复率≥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`
## 后续执行说明
* 获批后:按上述步骤使用只读扫描确定候选清单→分批次提交清理补丁→每批次构建与回归验证→汇总报告与文档更新。

View File

@ -0,0 +1,113 @@
## 现状与问题
* 当前 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+ 类活动策略,无需前端切换接口。
* 收据结构与随机性证明保持一致,验证脚本通过。
* 库存扣减与奖励发放在并发下无不一致;关键路径有审计日志。
* 测试覆盖关键流程与边界条件。
## 后续扩展
* 按活动类型开放“策略配置”与“玩法参数”在管理端,可动态调整权重、效果、限频。
* 如某玩法确有完全不同交互(例如合成/多次抽/队列结算),再考虑单独端点,但仍优先在通用端点层面通过参数与策略实现。

View File

@ -11,7 +11,7 @@
- 删除活动期:`activity_issues``activity_id``internal/repository/mysql/model/activity_issues.gen.go:18` - 删除活动期:`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` - 奖励配置 `activity_reward_settings``issue_id``internal/repository/mysql/model/activity_reward_settings.gen.go:18`
- 随机承诺 `issue_random_commitments``issue_id``internal/repository/mysql/model/issue_random_commitments.gen.go:18` - 活动承诺字段:`activities.commitment_*`(活动维度,已统一使用活动级承诺,不存在期级承诺表
- 期下抽奖相关: - 期下抽奖相关:
- 抽奖日志 `activity_draw_logs``issue_id``internal/repository/mysql/model/activity_draw_logs.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` - 抽奖效果 `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` 1) 以日志/效果/凭据等子表为先(`activity_draw_effects``activity_draw_receipts``activity_draw_logs`
2) 再清理资产/权益/模板(`user_inventory``user_item_cards``user_titles``system_*` 2) 再清理资产/权益/模板(`user_inventory``user_item_cards``user_titles``system_*`
3) 清理期与期下配置(`activity_issues``activity_reward_settings``issue_random_commitments` 3) 清理期与期下配置(`activity_issues``activity_reward_settings`
4) 删除根实体(`activities` 或软删 `users` 4) 删除根实体(`activities` 或软删 `users`
- 全过程包裹在单事务中,任何一步失败则回滚。 - 全过程包裹在单事务中,任何一步失败则回滚。

View File

@ -0,0 +1,50 @@
## 目标
- 在管理端实时查看某一期每个 `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. FisherYates + `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 封装
- 单元测试:映射稳定性、分页正确性、占用过滤
- 文档:算法与接口说明

View File

@ -0,0 +1,71 @@
## 目标
- 系统化梳理后端代码,识别并删除冗余设计与未使用模块
- 保持现有功能与前端调用不受影响,提升可维护性与编译/测试稳定性
## 范围
- 主要针对 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/*`(确认哪些路由在用)
请确认以上清理方案,确认后我将按步骤执行、构建验证并提交变更说明。

View File

@ -0,0 +1,170 @@
## 约束与对齐
- 管理端仅支持“定时到具体时间点”的开奖,不支持设置“定时 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、哈希凭证、定时结算器、退款日志与发券流程、测试与文档。

View File

@ -0,0 +1,35 @@
## 目标
- 在 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`

View File

@ -0,0 +1,56 @@
## 结论
- 不需要单独开新接口;在现有“统一接口”体系内,通过活动类型与策略插件即可兼容「一番赏」。
- 做法:以统一的资源与动作(创建/生成/列表/抽取/验证)为主干,按活动类型切换具体策略与字段含义。
## 统一接口适配方案
- 活动类型标识:在 `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`
## 验收准则
- 统一端点下,所有玩法均可正常运行。
- 「一番赏」玩法可完成预生成、选择位置、开奖、并发安全、事后可验证。

View File

@ -0,0 +1,92 @@
## 范围与原则
* 仅完善用户端(管理端与其 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 文档并持续维护。
## 上线与回滚
* 先灰度用户端;监控链接使用量、失败率与发货效率。
* 如需回滚,关闭公域入口并撤销未消费请求;资产状态回退并保留审计。

View File

@ -0,0 +1,79 @@
## 目标
- 将现有单件发货接口扩展为批量接口,支持一次提交多个资产 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` 数组,必填,长度 1100去重后处理
- `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 注释与接口文档
- 单元测试:服务层批量逻辑、处理器参数校验
请确认按此方案实施,我将立即落地代码、接口与测试。

View File

@ -0,0 +1,68 @@
## 现状与问题
- 当前 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+ 类活动策略,无需前端切换接口。
- 收据结构与随机性证明保持一致,验证脚本通过。
- 库存扣减与奖励发放在并发下无不一致;关键路径有审计日志。
- 测试覆盖关键流程与边界条件。
## 后续扩展
- 按活动类型开放“策略配置”与“玩法参数”在管理端,可动态调整权重、效果、限频。
- 如某玩法确有完全不同交互(例如合成/多次抽/队列结算),再考虑单独端点,但仍优先在通用端点层面通过参数与策略实现。

View File

@ -0,0 +1,71 @@
## 执行逻辑(目标态)
### 参与下单(仅创建订单,不开奖)
- 接口:`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`:新增 handlerinternal/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校验、签名凭证不泄露种子所有金额与积分变更审计入库

View File

@ -0,0 +1,56 @@
## 目标与原则
- 禁用“积分直接购买/抵扣”用于抽奖订单;保留积分兑换优惠券/商品能力。
- 优惠券支持“部分使用”且订单累计抵扣≤总价的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:110176
- 移除积分抵扣分支;`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:1155
- 直减券初始化`balance_amount=discount_value`;其他类型`balance_amount=NULL/0`
- 退款internal/api/admin/pay_refund_admin.go:189228internal/api/pay/wechat_notify.go:28174
- 解析`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:110176。
- 发券internal/service/user/coupon_add.go:1155。
- 退款internal/api/admin/pay_refund_admin.go:189228internal/api/pay/wechat_notify.go:28174。
- 用户券模型internal/repository/mysql/model/user_coupons.gen.go新增`balance_amount`)。

View File

@ -0,0 +1,42 @@
## 检查项
- 路由与前端:确认管理端生成/摘要接口使用活动级路径
- 生成:`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`
- 保持统一接口与策略,后续抽奖逻辑统一使用活动承诺

View File

@ -0,0 +1,50 @@
## 目标范围
- 在“创建活动”时新增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`拒绝卡效果并记录原因。
请确认以上方案,确认后我将按此实现并同步更新相关文档(说明文档、接口文档与前端表单)。

View File

@ -0,0 +1,49 @@
## 核心原则
- 承诺是活动级属性:所有活动均需生成并持有承诺;抽奖只能在承诺存在的活动上执行
- 不做旧期兼容:不读取/维护期级承诺表;全量切换到活动级承诺
## 数据模型(仅 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` 字段迁移
- 接入生成与概览接口
- 改造策略读取活动承诺
- 更新前端活动管理显示与按钮
- 移除前端/后端期级承诺路径(保留表但不访问)

View File

@ -0,0 +1,45 @@
# 渠道数据分析功能开发计划
本计划旨在为渠道管理增加数据分析功能,包括用户增长和付费数据的可视化展示。
## 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**: 验证数据准确性和图表展示效果。

View File

@ -0,0 +1,132 @@
## 目标
* 全面清理未用代码、注释废弃块、空文件与无用测试
* 识别并重构重复代码重复率≥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`
## 后续执行说明
* 获批后:按上述步骤使用只读扫描确定候选清单→分批次提交清理补丁→每批次构建与回归验证→汇总报告与文档更新。

View File

@ -0,0 +1,82 @@
## 现状与审计结论
- 积分存储:`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更新。
- 测试报告(单元+集成)。
请确认按此方案执行。我将按上述顺序推进并提交具体改动与测试结果。

View File

@ -0,0 +1,74 @@
## 背景与目标
- 目标:在用户支付成功后(微信回调),为“虚拟商品”接入微信小程序发货信息管理服务,完成发货信息录入、用户确认收货提醒/组件、状态查询与对账闭环,确保资金结算合规。
- 项目现状:
- 微信支付回调入口 `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工作流”与用户文档规范同步进度与决策。

View File

@ -0,0 +1,107 @@
# 订单记录与渠道分析优化计划
本计划旨在解决用户提出的两个问题:
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 响应结构。

View File

@ -0,0 +1,25 @@
## 目标
- 点击“订单详情”时,统一展示该票据订单的全部信息:参与与支付(票据)+ 中奖发放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 隐藏于列表,详情页也完整可见。

View File

@ -0,0 +1,76 @@
## 目标与边界
- 建立“任务中心”独立域(不改动现有`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/任务中心/`生成对齐/设计/任务分解文档,并开始增量实现新域。

View File

@ -0,0 +1,43 @@
## 结论
- 维度划分合理:订单(交易)、资产(所有权/履约对象)、发货(履约动作)三层分离是正确的。
- 现有实现可用:以`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无接口变化
- 可选新增运营批量发货落库接口(提升作业效率)。
- 补充用户侧包裹视图的文档与示例。

View File

@ -13,6 +13,10 @@
### MAC ### 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 . 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.6 .
docker push zfc931912343/bindbox-game:v1.6

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
build.zip

Binary file not shown.

BIN
build/.DS_Store vendored

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
.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%}}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
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 _};

View File

@ -1 +0,0 @@
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

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
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};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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 _};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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 _};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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 _};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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 _};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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 _};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
import{b as r}from"./index-Dn4yxdMr.js";function n(n){return r(n,5)}export{n as c};

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.form-tip[data-v-66d7f5fb]{margin-left:8px;color:#909399;font-size:12px}

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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 _};

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
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};

View File

@ -1 +0,0 @@
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