From f2ee678a2f3cbca4bc275be62b45603124579ce2 Mon Sep 17 00:00:00 2001 From: zpc Date: Fri, 30 Jan 2026 16:59:53 +0800 Subject: [PATCH] 32 --- docs/1.1.0/1.1.0文档.md | 53 +++ .../指定用户中奖功能设计文档.md | 330 ++++++++++++++++++ 2 files changed, 383 insertions(+) create mode 100644 docs/1.1.0/1.1.0文档.md create mode 100644 docs/1.1.0/指定用户中奖功能设计文档.md diff --git a/docs/1.1.0/1.1.0文档.md b/docs/1.1.0/1.1.0文档.md new file mode 100644 index 00000000..142e5330 --- /dev/null +++ b/docs/1.1.0/1.1.0文档.md @@ -0,0 +1,53 @@ +哈尼盲盒 需求文档 +一、需求简介 +1.以现有盲盒源码为基础,进行换皮 + 增加新功能。 +1.1.老功能除BUG外不做改动。 +2.项目名:哈尼盲盒。 +3.开发版本:微信小程序。 +4.本次新加功能如下,其他功能都为待定功能。 +4.1.指定用户抽中大奖 +4.2.待领取优惠券固定入口 +4.3.首页中大奖公告 +4.4.奖品等级名称更换 +二、指定用户抽中大奖 +1.每个盒子,可配置某个奖品由指定用户抽中。 +2.指定中奖,只是该用户能中,但不修改奖品本身的概率。 +2.1.该用户真想中奖,还是得按奖品的概率来抽。 +2.2.该用户能多次抽中指定大奖。 +3.若指定用户一直不抽: +3.1.在类似“无限赏”这种没有奖品数量限制的赏中,该奖品一直存在。 +3.2.在有奖品数量限制的活动中,奖品可能会被其他用户抽走。 +三、待领取优惠券固定入口 +1.首页右上角,增加待领取优惠券固定入口。 +2.当有待领取的优惠券时,此入口一直展示。 +2.1.优惠券都领完时,入口隐藏。 +四、首页中大奖公告 + +1.首页顶部展示某用户抽中某等级某大奖的公告。 +1.1.假公告,后台配置。 +1.2.展示用户头像。 +2.后台可配置该公告,可配置多条内容,循环展示。 +2.1.每条展示4秒。 +2.2.每条公告切换时,有进出展示效果。 +3.点击公告,弹出“抽中大奖弹窗”。 + +叮当盲盒 参考图 +3.1.展示中奖的奖品名称 +3.2.点击【我也要玩】按钮,关闭弹窗。 +五、奖品等级名称更换 +1.替换现有的奖品等级名称: +1.1.“超神”替换为“无上”。 +1.2.“欧皇”替换为“传说”。 +1.3.“黄金”替换为“史诗”。 +1.4.“普通”替换为“稀有”。 +2.替换每个奖品等级的icon。 + +六、【待定】音乐播放 +1.屏幕右侧增加全页面悬浮球,控制音乐播放。 + +2.默认关闭,点击播放音乐。 +2.1.全页面播放音乐。 +2.2.点击时切换音乐开关状态icon。 + +3.开启状态下,icon自旋转。 +4.后台可配置播放的音乐列表。 \ No newline at end of file diff --git a/docs/1.1.0/指定用户中奖功能设计文档.md b/docs/1.1.0/指定用户中奖功能设计文档.md new file mode 100644 index 00000000..3c42f33e --- /dev/null +++ b/docs/1.1.0/指定用户中奖功能设计文档.md @@ -0,0 +1,330 @@ +# 指定用户中奖功能设计文档 + +## 一、需求概述 + +允许后台为每个盒子配置"指定用户中奖"规则: +- 某个奖品只能由指定用户抽中 +- 不修改奖品本身的概率,指定用户仍需按概率抽奖 +- 指定用户可多次抽中指定奖品 +- 非指定用户无法抽中该奖品(但在有限赏中可能抽走库存) + +## 二、数据库设计 + +### 新增表:goods_designated_prizes(指定中奖配置表) + +```sql +CREATE TABLE goods_designated_prizes ( + id INT PRIMARY KEY IDENTITY(1,1), + goods_id INT NOT NULL, -- 盒子ID + goods_item_id INT NOT NULL, -- 奖品ID + user_id BIGINT NOT NULL, -- 指定用户ID + is_active BIT NOT NULL DEFAULT 1, -- 是否启用 + remark NVARCHAR(200), -- 备注说明 + created_at DATETIME2 NOT NULL DEFAULT GETDATE(), + updated_at DATETIME2, + + -- 索引 + INDEX IX_designated_goods_id (goods_id), + INDEX IX_designated_user_id (user_id), + INDEX IX_designated_goods_item (goods_item_id), + + -- 唯一约束:同一盒子的同一奖品只能指定给一个用户 + CONSTRAINT UQ_goods_item_user UNIQUE (goods_id, goods_item_id) +); +``` + +### 字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| goods_id | INT | 盒子ID,关联 goods 表 | +| goods_item_id | INT | 奖品ID,关联 goods_items 表 | +| user_id | BIGINT | 指定用户ID,关联 users 表 | +| is_active | BIT | 是否启用,0=禁用 1=启用 | +| remark | NVARCHAR(200) | 备注,如"VIP用户专属" | + +## 三、抽奖逻辑改造 + +### 3.1 盒子类型分类 + +根据代码分析,系统有两类抽奖模式: + +| 类型 | 抽奖方法 | 特点 | 包含的赏品类型 | +|------|----------|------|----------------| +| 有限赏 | `DrawAsync` | 基于库存概率,抽完即止 | 一番赏(1)、福袋(5)、幸运赏(6)、盲盒(10)、幸运赏新(11) | +| 无限赏 | `DrawInfiniteAsync` | 基于固定概率(real_pro),无限池 | 无限赏(2)、领主赏(8)、连击赏(9)、翻倍赏(16)、福利屋(15) | + +### 3.2 有限赏抽奖流程改造(一番赏等) + +**特殊性**:一番赏是"每抽必中,抽完即止"的机制,不能因为指定中奖配置导致用户抽不到东西。 + +``` +原流程: +1. 获取可用奖品池(库存>0) +2. 按库存计算概率 +3. 权重随机选择 +4. 扣减库存 +5. 创建记录 + +新流程: +1. 获取可用奖品池(库存>0) +2. 查询指定中奖配置 +3. 【新增】分离奖品池: + - 普通奖品池:没有指定用户配置的奖品 + - 指定奖品池:有指定用户配置的奖品 +4. 【新增】选择抽奖池: + - 如果当前用户是某个指定奖品的指定用户 → 使用【全部奖品池】 + - 如果当前用户是普通用户: + - 普通奖品池不为空 → 使用【普通奖品池】 + - 普通奖品池为空(只剩指定奖品)→ 使用【全部奖品池】(兜底保证必中) +5. 按库存计算概率 +6. 权重随机选择 +7. 扣减库存 +8. 创建记录 +``` + +**兜底机制说明**: +- 正常情况:普通用户从普通奖品池抽,抽不到指定奖品 +- 极端情况:当盒子只剩下指定奖品时,普通用户也能抽走,保证一番赏"必中"机制 +- 这意味着:指定中奖只是"优先权",不是"绝对保证" + +### 3.3 无限赏抽奖流程改造 + +``` +原流程: +1. 获取奖品池(real_pro>0) +2. 按 real_pro 计算概率 +3. 权重随机选择 +4. 创建记录(不扣库存) + +新流程: +1. 获取奖品池(real_pro>0) +2. 查询当前用户的指定中奖配置 +3. 【新增】过滤奖品池: + - 如果奖品有指定用户配置 且 当前用户不是指定用户 → 从池中移除 + - 如果奖品有指定用户配置 且 当前用户是指定用户 → 保留在池中 +4. 按 real_pro 计算概率 +5. 权重随机选择 +6. 创建记录 +``` + +### 3.4 核心代码改造点 + +#### LotteryEngine.cs + +```csharp +// 新增方法:获取指定中奖配置 +private async Task> GetDesignatedPrizesAsync(int goodsId) +{ + return await _dbContext.GoodsDesignatedPrizes + .Where(dp => dp.GoodsId == goodsId && dp.IsActive) + .ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId); +} + +// 新增方法:过滤奖品池(无限赏用,严格过滤) +private List FilterPrizePoolStrict( + List prizePool, + int userId, + Dictionary designatedPrizes) +{ + return prizePool.Where(prize => + { + // 如果该奖品没有指定用户配置,所有人都可以抽 + if (!designatedPrizes.TryGetValue(prize.Id, out var designatedUserId)) + return true; + + // 如果有指定用户配置,只有指定用户可以抽 + return designatedUserId == userId; + }).ToList(); +} + +// 新增方法:过滤奖品池(有限赏用,带兜底机制) +private List FilterPrizePoolWithFallback( + List prizePool, + int userId, + Dictionary designatedPrizes) +{ + // 分离普通奖品和指定奖品 + var normalPrizes = prizePool.Where(prize => + !designatedPrizes.ContainsKey(prize.Id)).ToList(); + + var userDesignatedPrizes = prizePool.Where(prize => + designatedPrizes.TryGetValue(prize.Id, out var uid) && uid == userId).ToList(); + + // 如果当前用户有指定奖品,返回全部奖品池(包含其指定奖品) + if (userDesignatedPrizes.Any()) + { + return prizePool.Where(prize => + { + if (!designatedPrizes.TryGetValue(prize.Id, out var designatedUserId)) + return true; // 普通奖品 + return designatedUserId == userId; // 只包含自己的指定奖品 + }).ToList(); + } + + // 普通用户:优先从普通奖品池抽 + if (normalPrizes.Any()) + { + return normalPrizes; + } + + // 兜底:普通奖品池为空,只剩指定奖品,允许抽走(保证一番赏必中) + _logger.LogWarning("Fallback: Only designated prizes left, allowing normal user to draw. GoodsId={GoodsId}, UserId={UserId}", + prizePool.FirstOrDefault()?.GoodsId, userId); + return prizePool; +} +``` + +#### DrawAsync 改造(有限赏) + +```csharp +public async Task DrawAsync(LotteryDrawRequest request) +{ + // 1. 获取可用奖品池 + var prizePool = await _inventoryManager.GetAvailablePrizePoolAsync(request.GoodsId, request.Num); + + // 2. 【新增】获取指定中奖配置 + var designatedPrizes = await GetDesignatedPrizesAsync(request.GoodsId); + + // 3. 【新增】过滤奖品池(带兜底机制) + if (designatedPrizes.Any()) + { + prizePool = FilterPrizePoolWithFallback(prizePool, request.UserId, designatedPrizes); + } + + if (!prizePool.Any()) + { + result.ErrorMessage = "暂无可抽奖品"; + return result; + } + + // 后续流程不变... +} +``` + +#### DrawInfiniteAsync 改造(无限赏) + +```csharp +public async Task DrawInfiniteAsync(LotteryDrawRequest request) +{ + // 1. 获取奖品池 + var prizePool = await GetInfinitePrizePoolAsync(request.GoodsId, request.Num); + + // 2. 【新增】获取指定中奖配置 + var designatedPrizes = await GetDesignatedPrizesAsync(request.GoodsId); + + // 3. 【新增】严格过滤奖品池(无限赏不需要兜底) + if (designatedPrizes.Any()) + { + prizePool = FilterPrizePoolStrict(prizePool, request.UserId, designatedPrizes); + } + + if (!prizePool.Any()) + { + result.ErrorMessage = "暂无可抽奖品"; + return result; + } + + // 后续流程不变... +} +``` + +## 四、业务场景分析 + +### 4.1 无限赏场景 + +| 场景 | 处理方式 | +|------|----------| +| 指定用户抽奖 | 奖品池包含指定奖品,按概率正常抽 | +| 指定用户抽中指定奖品 | 正常中奖,可多次中奖 | +| 指定用户没抽中 | 抽到其他奖品 | +| 非指定用户抽奖 | 奖品池不包含指定奖品,永远抽不到 | +| 指定奖品一直存在 | ✅ 无限赏不扣库存,奖品永远在 | + +### 4.2 有限赏场景(一番赏等) + +| 场景 | 处理方式 | +|------|----------| +| 指定用户抽奖 | 奖品池包含指定奖品,按概率正常抽 | +| 指定用户抽中指定奖品 | 正常中奖,扣减库存 | +| 普通用户抽奖(有普通奖品) | 只从普通奖品池抽,抽不到指定奖品 | +| 普通用户抽奖(只剩指定奖品) | 兜底机制生效,可以抽走指定奖品 | +| 指定用户一直不抽 | ⚠️ 当只剩指定奖品时,会被其他用户抽走 | + +**重要说明**: +- 指定中奖是"优先权"而非"绝对保证" +- 如果指定用户迟迟不来抽,当盒子只剩指定奖品时,其他用户会抽走 +- 这是为了保证一番赏"每抽必中"的核心机制 + +### 4.3 边界情况 + +| 情况 | 处理 | +|------|------| +| 指定用户不存在 | 配置生效,但该用户永远不会来抽 | +| 指定奖品不存在 | 配置无效,不影响抽奖 | +| 同一奖品指定多个用户 | 数据库唯一约束阻止,一个奖品只能指定一个用户 | +| 同一用户被指定多个奖品 | 允许,用户可以抽中多个指定奖品 | +| 禁用配置 | is_active=0,等同于没有配置 | + +## 五、后台管理功能 + +### 5.1 功能入口 + +在盒子管理 → 奖品列表页面,每个奖品行增加"指定中奖"操作按钮。 + +### 5.2 配置弹窗 + +``` +┌─────────────────────────────────────┐ +│ 指定中奖配置 │ +├─────────────────────────────────────┤ +│ 奖品:【SP赏】限定手办 │ +│ │ +│ 指定用户:[用户搜索框] │ +│ 用户ID: 10001 │ +│ 昵称: 张三 │ +│ │ +│ 备注:[VIP专属奖品] │ +│ │ +│ 状态:○ 启用 ○ 禁用 │ +│ │ +│ [取消] [保存] │ +└─────────────────────────────────────┘ +``` + +### 5.3 API 接口 + +``` +POST /api/admin/goods/{goodsId}/designated-prizes # 添加/更新配置 +GET /api/admin/goods/{goodsId}/designated-prizes # 获取配置列表 +DELETE /api/admin/goods/{goodsId}/designated-prizes/{id} # 删除配置 +``` + +## 六、实施步骤 + +### 第一阶段:数据库 + +1. 创建 `goods_designated_prizes` 表 +2. 创建 `GoodsDesignatedPrize` 实体类 +3. 更新 `HoneyBoxDbContext` + +### 第二阶段:抽奖逻辑 + +1. 在 `LotteryEngine` 中添加指定中奖过滤逻辑 +2. 改造 `DrawAsync` 方法(有限赏) +3. 改造 `DrawInfiniteAsync` 方法(无限赏) +4. 添加单元测试 + +### 第三阶段:后台管理 + +1. 添加后台 API 接口 +2. 前端页面开发(如需要) + +## 七、注意事项 + +1. **性能考虑**:指定中奖配置查询会增加一次数据库访问,建议加缓存 +2. **概率不变**:指定用户的中奖概率不变,只是普通用户的奖品池被缩小 +3. **有限赏兜底**:当只剩指定奖品时,普通用户也能抽走,保证必中机制 +4. **无限赏严格**:无限赏没有兜底,指定奖品只有指定用户能抽到 +5. **日志记录**:建议记录指定中奖的命中日志和兜底触发日志,便于运营追踪 +6. **运营提示**:后台配置时应提示"有限赏的指定奖品可能被其他用户抽走"