阶段4-商品系统

This commit is contained in:
zpc 2026-01-02 20:31:24 +08:00
parent 7e00d28ad4
commit a1e457db06
78 changed files with 5639 additions and 121 deletions

View File

@ -6,29 +6,29 @@
## Tasks
- [ ] 1. 基础设施准备
- [ ] 1.1 创建商品相关的DTO和Request/Response模型
- [x] 1. 基础设施准备
- [x] 1.1 创建商品相关的DTO和Request/Response模型
- 在HoneyBox.Model/Models目录下创建Goods相关模型
- 包括GoodsListDto、GoodsDetailResponse、BoxDetailDto、CollectionDto、PrizeLogsResponse等
- _Requirements: 1.1-10.3_
- [ ] 1.2 创建服务接口定义
- [x] 1.2 创建服务接口定义
- 在HoneyBox.Core/Interfaces目录下创建IGoodsService、ICollectionService、IPrizeService、IGoodsCacheService接口
- _Requirements: 1.1-10.3_
- [ ] 1.3 创建数据库实体模型
- [x] 1.3 创建数据库实体模型
- 创建Goods、GoodsList、GoodsType、GoodsExtend、GoodsCollection等实体
- 配置EF Core映射
- _Requirements: 1.1-10.3_
- [ ] 1.4 注册服务到DI容器
- [x] 1.4 注册服务到DI容器
- 在ServiceModule.cs中注册新服务
- _Requirements: 1.1-10.3_
- [ ] 2. 商品列表服务实现
- [ ] 2.1 查看PHP代码了解商品列表业务逻辑
- [x] 2. 商品列表服务实现
- [x] 2.1 查看PHP代码了解商品列表业务逻辑
- 阅读server/php/app/api/controller/Goods.php中的goods_list方法
- 理解type过滤、unlock_amount过滤、分页、排序逻辑
- 理解join_count统计逻辑
- _Requirements: 1.1-1.6_
- [ ] 2.2 实现GoodsService - 商品列表
- [x] 2.2 实现GoodsService - 商品列表
- 实现GetGoodsListAsync方法
- 实现type过滤逻辑
- 实现unlock_amount过滤逻辑
@ -39,12 +39,12 @@
- **Property 1: 商品列表过滤和排序正确性**
- **Validates: Requirements 1.1, 1.4, 1.5, 1.6**
- [ ] 3. 商品详情服务实现
- [ ] 3.1 查看PHP代码了解商品详情业务逻辑
- [x] 3. 商品详情服务实现
- [x] 3.1 查看PHP代码了解商品详情业务逻辑
- 阅读server/php/app/api/controller/Goods.php中的goods_detail方法
- 理解奖品列表查询、概率计算、锁箱信息、参与用户统计
- _Requirements: 2.1-2.7_
- [ ] 3.2 实现GoodsService - 商品详情
- [x] 3.2 实现GoodsService - 商品详情
- 实现GetGoodsDetailAsync方法
- 实现自动选择箱号逻辑goods_num=0时
- 实现奖品列表查询
@ -59,12 +59,12 @@
- **Property 3: 奖品概率计算正确性**
- **Validates: Requirements 2.1, 2.3, 2.4, 2.5, 2.6, 2.7**
- [ ] 4. 商品子奖品服务实现
- [ ] 4.1 查看PHP代码了解子奖品业务逻辑
- [x] 4. 商品子奖品服务实现
- [x] 4.1 查看PHP代码了解子奖品业务逻辑
- 阅读server/php/app/api/controller/Goods.php中的goods_children方法
- 理解子奖品查询和real_pro计算
- _Requirements: 3.1-3.3_
- [ ] 4.2 实现GoodsService - 子奖品查询
- [x] 4.2 实现GoodsService - 子奖品查询
- 实现GetGoodsChildrenAsync方法
- 实现real_pro计算逻辑
- 实现shang_info关联查询
@ -73,30 +73,30 @@
- **Property 4: 子奖品数据完整性**
- **Validates: Requirements 3.1, 3.2, 3.3**
- [ ] 5. 商品扩展配置服务实现
- [ ] 5.1 查看PHP代码了解扩展配置业务逻辑
- [x] 5. 商品扩展配置服务实现
- [x] 5.1 查看PHP代码了解扩展配置业务逻辑
- 阅读server/php/app/api/controller/Goods.php中的goods_extend方法
- 理解配置优先级(商品配置 > 类型默认配置)
- _Requirements: 4.1-4.3_
- [ ] 5.2 实现GoodsService - 扩展配置
- [x] 5.2 实现GoodsService - 扩展配置
- 实现GetGoodsExtendAsync方法
- 实现配置优先级逻辑
- _Requirements: 4.1-4.3_
- [ ] 6. Checkpoint - 核心商品服务测试验证
- [x] 6. Checkpoint - 核心商品服务测试验证
- 确保商品列表、详情、子奖品、扩展配置服务测试通过
- 如有问题请询问用户
- [ ] 7. 箱号管理服务实现
- [ ] 7.1 查看PHP代码了解箱号管理业务逻辑
- [x] 7. 箱号管理服务实现
- [x] 7.1 查看PHP代码了解箱号管理业务逻辑
- 阅读server/php/app/api/controller/Goods.php中的goods_num_list、goods_num_detail方法
- 理解箱号分组、排序、余量统计逻辑
- _Requirements: 5.1-5.4_
- [ ] 7.2 实现GoodsService - 箱号列表
- [x] 7.2 实现GoodsService - 箱号列表
- 实现GetBoxListAsync方法
- 实现箱号分组逻辑每10个一组
- _Requirements: 5.1_
- [ ] 7.3 实现GoodsService - 箱号详情
- [x] 7.3 实现GoodsService - 箱号详情
- 实现GetBoxDetailAsync方法
- 实现排序逻辑(箱号升序/降序、余量降序)
- 实现余量统计
@ -106,12 +106,12 @@
- **Property 5: 箱号管理数据正确性**
- **Validates: Requirements 5.1, 5.2, 5.3, 5.4**
- [ ] 8. 收藏服务实现
- [ ] 8.1 查看PHP代码了解收藏业务逻辑
- [x] 8. 收藏服务实现
- [x] 8.1 查看PHP代码了解收藏业务逻辑
- 阅读server/php/app/api/controller/Goods.php中的collection、collection_list方法
- 理解收藏/取消收藏逻辑
- _Requirements: 6.1-6.4_
- [ ] 8.2 实现CollectionService
- [x] 8.2 实现CollectionService
- 实现ToggleCollectionAsync方法收藏/取消收藏)
- 实现GetCollectionListAsync方法收藏列表
- 实现IsCollectedAsync方法检查收藏状态
@ -121,22 +121,22 @@
- **Property 6: 收藏操作往返一致性**
- **Validates: Requirements 6.1, 6.2, 6.4**
- [ ] 9. 奖品统计服务实现
- [ ] 9.1 查看PHP代码了解奖品统计业务逻辑
- [x] 9. 奖品统计服务实现
- [x] 9.1 查看PHP代码了解奖品统计业务逻辑
- 阅读server/php/app/api/controller/Goods.php中的goods_prize_count、goods_prize_content方法
- 理解统计计算逻辑
- _Requirements: 7.1-7.2_
- [ ] 9.2 实现PrizeService - 奖品统计
- [x] 9.2 实现PrizeService - 奖品统计
- 实现GetPrizeCountAsync方法
- 实现GetPrizeContentAsync方法
- _Requirements: 7.1-7.2_
- [ ] 10. 中奖记录服务实现
- [ ] 10.1 查看PHP代码了解中奖记录业务逻辑
- [x] 10. 中奖记录服务实现
- [x] 10.1 查看PHP代码了解中奖记录业务逻辑
- 阅读server/php/app/api/controller/Goods.php中的goods_prize_logs方法
- 理解分类筛选、分页逻辑
- _Requirements: 8.1-8.4_
- [ ] 10.2 实现PrizeService - 中奖记录
- [x] 10.2 实现PrizeService - 中奖记录
- 实现GetPrizeLogsAsync方法
- 实现shang_id筛选
- 实现分类列表查询
@ -146,74 +146,74 @@
- **Property 7: 中奖记录数据完整性**
- **Validates: Requirements 8.1, 8.2, 8.3, 8.4**
- [ ] 11. 缓存服务实现
- [ ] 11.1 实现GoodsCacheService
- [x] 11. 缓存服务实现
- [x] 11.1 实现GoodsCacheService
- 实现GetJoinCountAsync方法
- 实现IncrementJoinCountAsync方法
- 实现InvalidateGoodsCacheAsync方法
- 配置缓存过期时间
- _Requirements: 9.1-9.4_
- [ ] 12. Checkpoint - 服务层测试验证
- [x] 12. Checkpoint - 服务层测试验证
- 确保所有服务层单元测试通过
- 确保所有属性测试通过
- 如有问题请询问用户
- [ ] 13. 控制器实现 - GoodsController
- [ ] 13.1 实现商品列表接口 POST /goods_list
- [x] 13. 控制器实现 - GoodsController
- [x] 13.1 实现商品列表接口 POST /goods_list
- 调用GoodsService.GetGoodsListAsync
- 更新API接口文档标记迁移状态
- _Requirements: 1.1-1.6_
- [ ] 13.2 实现商品详情接口 POST /goods_detail
- [x] 13.2 实现商品详情接口 POST /goods_detail
- 调用GoodsService.GetGoodsDetailAsync
- 更新API接口文档标记迁移状态
- _Requirements: 2.1-2.7_
- [ ] 13.3 实现商品子奖品接口 POST /goods_children
- [x] 13.3 实现商品子奖品接口 POST /goods_children
- 调用GoodsService.GetGoodsChildrenAsync
- 更新API接口文档标记迁移状态
- _Requirements: 3.1-3.3_
- [ ] 13.4 实现商品扩展配置接口 POST /goods_extend
- [x] 13.4 实现商品扩展配置接口 POST /goods_extend
- 调用GoodsService.GetGoodsExtendAsync
- 更新API接口文档标记迁移状态
- _Requirements: 4.1-4.3_
- [ ] 13.5 实现箱号列表接口 POST /goods_num_list
- [x] 13.5 实现箱号列表接口 POST /goods_num_list
- 调用GoodsService.GetBoxListAsync
- 更新API接口文档标记迁移状态
- _Requirements: 5.1_
- [ ] 13.6 实现箱号详情接口 POST /goods_num_detail
- [x] 13.6 实现箱号详情接口 POST /goods_num_detail
- 调用GoodsService.GetBoxDetailAsync
- 更新API接口文档标记迁移状态
- _Requirements: 5.2-5.4_
- [ ] 13.7 实现奖品数量统计接口 POST /goods_prize_count
- [x] 13.7 实现奖品数量统计接口 POST /goods_prize_count
- 调用PrizeService.GetPrizeCountAsync
- 更新API接口文档标记迁移状态
- _Requirements: 7.1_
- [ ] 13.8 实现奖品内容接口 POST /goods_prize_content
- [x] 13.8 实现奖品内容接口 POST /goods_prize_content
- 调用PrizeService.GetPrizeContentAsync
- 更新API接口文档标记迁移状态
- _Requirements: 7.2_
- [ ] 13.9 实现中奖记录接口 POST /goods_prize_logs
- [x] 13.9 实现中奖记录接口 POST /goods_prize_logs
- 调用PrizeService.GetPrizeLogsAsync
- 更新API接口文档标记迁移状态
- _Requirements: 8.1-8.4_
- [ ] 14. 控制器实现 - CollectionController
- [ ] 14.1 实现收藏/取消收藏接口 POST /collection
- [x] 14. 控制器实现 - CollectionController
- [x] 14.1 实现收藏/取消收藏接口 POST /collection
- 调用CollectionService.ToggleCollectionAsync
- 更新API接口文档标记迁移状态
- _Requirements: 6.1-6.3_
- [ ] 14.2 实现收藏列表接口 POST /collection_list
- [x] 14.2 实现收藏列表接口 POST /collection_list
- 调用CollectionService.GetCollectionListAsync
- 更新API接口文档标记迁移状态
- _Requirements: 6.4_
- [ ] 15. Checkpoint - 控制器测试验证
- [x] 15. Checkpoint - 控制器测试验证
- 确保所有控制器接口可正常访问
- 使用Postman或HTTP文件测试各接口
- 如有问题请询问用户
- [ ] 16. API响应格式验证
- [ ] 16.1 验证所有接口响应格式
- [x] 16. API响应格式验证
- [x] 16.1 验证所有接口响应格式
- 确保status、msg、data结构一致
- 确保字段命名与PHP API一致snake_case
- _Requirements: 10.1, 10.3_
@ -221,29 +221,29 @@
- **Property 8: API响应格式一致性**
- **Validates: Requirements 10.1, 10.3**
- [ ] 17. 集成测试
- [ ] 17.1 编写商品列表集成测试
- [x] 17. 集成测试
- [x] 17.1 编写商品列表集成测试
- 测试完整的商品查询流程
- 测试分类筛选
- _Requirements: 1.1-1.6_
- [ ] 17.2 编写商品详情集成测试
- [x] 17.2 编写商品详情集成测试
- 测试商品详情查询
- 测试概率计算
- _Requirements: 2.1-2.7_
- [ ] 17.3 编写收藏功能集成测试
- [x] 17.3 编写收藏功能集成测试
- 测试收藏/取消收藏流程
- _Requirements: 6.1-6.4_
- [ ] 18. 文档更新和最终验证
- [ ] 18.1 更新API接口文档
- [x] 18. 文档更新和最终验证
- [x] 18.1 更新API接口文档
- 确认所有迁移接口都已标记
- 记录新接口地址
- _Requirements: 10.4_
- [ ] 18.2 创建HTTP测试文件
- [x] 18.2 创建HTTP测试文件
- 在HoneyBox.Api目录下创建goods-system.http测试文件
- 包含所有商品系统相关接口的测试请求
- [ ] 19. Final Checkpoint - 完整功能验证
- [x] 19. Final Checkpoint - 完整功能验证
- 确保所有测试通过
- 确保API文档已更新
- 确保与前端兼容性

View File

@ -371,6 +371,9 @@ Authorization: Bearer {token}
POST /goods
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/goods_list`
**请求参数:**
```json
{
@ -419,6 +422,9 @@ POST /goods
POST /goodsdetail
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/goods_detail`
**请求参数:**
```json
{
@ -460,7 +466,178 @@ POST /goodsdetail
}
```
### 3.3 获取无限赏商品详情
### 3.3 获取商品子奖品
```http
POST /goods_children
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/goods_children`
**请求参数:**
```json
{
"goods_id": 1001,
"goods_num": 0,
"goods_list_id": 2001
}
```
**参数说明:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| goods_id | int | 是 | 商品ID |
| goods_num | int | 否 | 箱号默认0 |
| goods_list_id | int | 是 | 奖品ID |
**响应示例:**
```json
{
"status": 1,
"msg": "success",
"data": [
{
"id": 3001,
"title": "子奖品名称",
"imgurl": "子奖品图片",
"price": "99.00",
"real_pro": "0.05000",
"shang_info": {
"id": 1,
"title": "A赏",
"color": "#FF0000"
}
}
]
}
```
### 3.4 获取商品扩展配置
```http
POST /goods_extend
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/goods_extend`
**请求参数:**
```json
{
"goods_id": 1001,
"goods_type": 1
}
```
**参数说明:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| goods_id | int | 是 | 商品ID |
| goods_type | int | 否 | 商品类型 |
**响应示例:**
```json
{
"status": 1,
"msg": "success",
"data": {
"pay_wechat": 1,
"pay_balance": 1,
"pay_currency": 1,
"pay_currency2": 1,
"dk_money": 1,
"dk_integral": 1,
"dk_money2": 1
}
}
```
### 3.5 获取箱号列表
```http
POST /goods_num_list
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/goods_num_list`
**请求参数:**
```json
{
"goods_id": 1001
}
```
**参数说明:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| goods_id | int | 是 | 商品ID |
**响应示例:**
```json
{
"status": 1,
"msg": "success",
"data": [
{
"start": 1,
"end": 10,
"text": "1-10"
},
{
"start": 11,
"end": 20,
"text": "11-20"
}
]
}
```
### 3.6 获取箱号详情
```http
POST /goods_num_detail
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/goods_num_detail`
**请求参数:**
```json
{
"goods_id": 1001,
"page_no": 0,
"sort": 0
}
```
**参数说明:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| goods_id | int | 是 | 商品ID |
| page_no | int | 否 | 页码默认0第一组 |
| sort | int | 否 | 排序0-箱号升序1-箱号降序2-余量降序 |
**响应示例:**
```json
{
"status": 1,
"msg": "success",
"data": [
{
"num": 1,
"surplus_all_stock": 50,
"goods_list": [
{
"id": 2001,
"title": "奖品名称",
"imgurl": "奖品图片",
"surplus_stock": 5
}
]
}
]
}
```
### 3.7 获取无限赏商品详情
```http
POST /infinite_goodsdetail
```
@ -505,6 +682,9 @@ POST /infinite_goodsdetail
POST /goodslist_count
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/goods_prize_count`
**请求参数:**
```json
{
@ -517,6 +697,9 @@ POST /goodslist_count
POST /goodslist_content
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/goods_prize_content`
**请求参数:**
```json
{
@ -786,29 +969,58 @@ POST /infinite_prizeorderlog
POST /shang_log
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/goods_prize_logs`
**请求参数:**
```json
{
"goods_id": 1001,
"num": 0,
"page": 1
"goods_num": 0,
"shang_id": 0,
"page": 1,
"page_size": 15
}
```
**参数说明:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| goods_id | int | 是 | 商品ID |
| goods_num | int | 否 | 箱号默认0 |
| shang_id | int | 否 | 赏品分类ID0=全部 |
| page | int | 否 | 页码默认1 |
| page_size | int | 否 | 每页数量默认15 |
**响应示例:**
```json
{
"status": 1,
"msg": "success",
"data": {
"category": [
{
"id": 1,
"title": "A赏",
"color": "#FF0000"
}
],
"data": [
{
"user_nickname": "用户***",
"user_headimg": "头像URL",
"goodslist_title": "限定手办A",
"goodslist_imgurl": "奖品图片",
"shang_info": {
"id": 1,
"title": "A赏",
"color": "#FF0000"
},
"addtime": "2024-01-01 10:30:00",
"luck_no": 1
}
]
],
"last_page": 10
}
}
```
@ -1958,12 +2170,104 @@ POST /receive
POST /addCollect
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/addCollect`
**请求参数:**
```json
{
"goods_id": 1001,
"type": 1,
"num": 0
"goods_num": 0
}
```
**参数说明:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| goods_id | int | 是 | 商品ID |
| goods_num | int | 否 | 箱号默认0 |
**响应示例:**
```json
{
"status": 1,
"msg": "操作成功"
}
```
### 9.7 收藏列表
```http
POST /listCollect
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/listCollect`
**请求参数:**
```json
{
"type": 0,
"page": 1,
"page_size": 100
}
```
**参数说明:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| type | int | 否 | 商品类型0=全部 |
| page | int | 否 | 页码默认1 |
| page_size | int | 否 | 每页数量默认100 |
**响应示例:**
```json
{
"status": 1,
"msg": "success",
"data": {
"data": [
{
"id": 1,
"goods_id": 1001,
"type": 1,
"num": 0,
"goods_title": "商品标题",
"goods_price": "99.00",
"imgurl": "图片URL",
"stock": 100,
"surplus_stock": 50
}
],
"last_page": 1
}
}
```
### 9.8 删除收藏
```http
POST /delCollect
```
**迁移状态**: ✅ 已迁移到 .NET 8
**新接口地址**: `POST /api/delCollect`
**请求参数:**
```json
{
"id": 1
}
```
**参数说明:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | int | 是 | 收藏记录ID |
**响应示例:**
```json
{
"status": 1,
"msg": "删除成功"
}
```

View File

@ -0,0 +1,170 @@
using System.Security.Claims;
using HoneyBox.Core.Interfaces;
using HoneyBox.Model.Base;
using HoneyBox.Model.Models.Goods;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HoneyBox.Api.Controllers;
/// <summary>
/// 收藏控制器
/// Requirements: 6.1-6.4
/// </summary>
[ApiController]
[Route("api")]
public class CollectionController : ControllerBase
{
private readonly ICollectionService _collectionService;
private readonly ILogger<CollectionController> _logger;
public CollectionController(
ICollectionService collectionService,
ILogger<CollectionController> logger)
{
_collectionService = collectionService;
_logger = logger;
}
/// <summary>
/// 收藏/取消收藏商品
/// POST /api/addCollect
/// Requirements: 6.1-6.3
/// </summary>
/// <remarks>
/// 切换收藏状态:如果已收藏则取消收藏,如果未收藏则添加收藏
/// </remarks>
[HttpPost("addCollect")]
[Authorize]
public async Task<ApiResponse> ToggleCollection([FromBody] CollectionRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse.Unauthorized();
}
try
{
if (request == null || request.GoodsId <= 0)
{
return ApiResponse.Fail("商品ID不能为空");
}
var result = await _collectionService.ToggleCollectionAsync(
userId.Value,
request.GoodsId,
request.GoodsNum);
return result
? ApiResponse.Success("操作成功")
: ApiResponse.Fail("操作失败");
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("Toggle collection failed: UserId={UserId}, GoodsId={GoodsId}, Error={Error}",
userId, request?.GoodsId, ex.Message);
return ApiResponse.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to toggle collection: UserId={UserId}, GoodsId={GoodsId}",
userId, request?.GoodsId);
return ApiResponse.Fail("操作失败");
}
}
/// <summary>
/// 获取收藏列表
/// POST /api/listCollect
/// Requirements: 6.4
/// </summary>
[HttpPost("listCollect")]
[Authorize]
public async Task<ApiResponse<CollectionListResponse>> GetCollectionList([FromBody] CollectionListRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<CollectionListResponse>.Unauthorized();
}
try
{
request ??= new CollectionListRequest();
var page = request.Page < 1 ? 1 : request.Page;
var pageSize = request.PageSize < 1 ? 100 : request.PageSize;
var result = await _collectionService.GetCollectionListAsync(
userId.Value,
request.Type,
page,
pageSize);
return ApiResponse<CollectionListResponse>.Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get collection list: UserId={UserId}", userId);
return ApiResponse<CollectionListResponse>.Fail("获取收藏列表失败");
}
}
/// <summary>
/// 删除收藏
/// POST /api/delCollect
/// </summary>
[HttpPost("delCollect")]
[Authorize]
public async Task<ApiResponse> DeleteCollection([FromBody] DeleteCollectionRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse.Unauthorized();
}
try
{
if (request == null || request.Id <= 0)
{
return ApiResponse.Fail("收藏ID不能为空");
}
var result = await _collectionService.DeleteCollectionAsync(userId.Value, request.Id);
return result
? ApiResponse.Success("删除成功")
: ApiResponse.Fail("删除失败");
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("Delete collection failed: UserId={UserId}, CollectionId={CollectionId}, Error={Error}",
userId, request?.Id, ex.Message);
return ApiResponse.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete collection: UserId={UserId}, CollectionId={CollectionId}",
userId, request?.Id);
return ApiResponse.Fail("删除失败");
}
}
#region Private Helper Methods
/// <summary>
/// 获取当前登录用户ID
/// </summary>
private int? GetCurrentUserId()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out var userId))
{
return null;
}
return userId;
}
#endregion
}

View File

@ -0,0 +1,349 @@
using System.Security.Claims;
using HoneyBox.Core.Interfaces;
using HoneyBox.Model.Base;
using HoneyBox.Model.Models;
using HoneyBox.Model.Models.Goods;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HoneyBox.Api.Controllers;
/// <summary>
/// 商品控制器
/// </summary>
[ApiController]
[Route("api")]
public class GoodsController : ControllerBase
{
private readonly IGoodsService _goodsService;
private readonly IPrizeService _prizeService;
private readonly ILogger<GoodsController> _logger;
public GoodsController(
IGoodsService goodsService,
IPrizeService prizeService,
ILogger<GoodsController> logger)
{
_goodsService = goodsService;
_prizeService = prizeService;
_logger = logger;
}
/// <summary>
/// 获取商品列表
/// POST /api/goods_list
/// Requirements: 1.1-1.6
/// </summary>
[HttpPost("goods_list")]
[Authorize]
public async Task<ApiResponse<PageResponse<GoodsListDto>>> GetGoodsList([FromBody] GoodsListRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<PageResponse<GoodsListDto>>.Unauthorized();
}
try
{
request ??= new GoodsListRequest();
if (request.Page < 1) request.Page = 1;
if (request.PageSize < 1) request.PageSize = 15;
var result = await _goodsService.GetGoodsListAsync(request, userId.Value);
return ApiResponse<PageResponse<GoodsListDto>>.Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get goods list: UserId={UserId}", userId);
return ApiResponse<PageResponse<GoodsListDto>>.Fail("获取商品列表失败");
}
}
/// <summary>
/// 获取商品详情
/// POST /api/goods_detail
/// Requirements: 2.1-2.7
/// </summary>
[HttpPost("goods_detail")]
[Authorize]
public async Task<ApiResponse<GoodsDetailResponseDto>> GetGoodsDetail([FromBody] GoodsDetailRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<GoodsDetailResponseDto>.Unauthorized();
}
try
{
if (request == null || request.GoodsId <= 0)
{
return ApiResponse<GoodsDetailResponseDto>.Fail("商品ID不能为空");
}
var result = await _goodsService.GetGoodsDetailAsync(request.GoodsId, request.GoodsNum, userId.Value);
return ApiResponse<GoodsDetailResponseDto>.Success(result);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("Get goods detail failed: GoodsId={GoodsId}, Error={Error}", request?.GoodsId, ex.Message);
return ApiResponse<GoodsDetailResponseDto>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get goods detail: GoodsId={GoodsId}", request?.GoodsId);
return ApiResponse<GoodsDetailResponseDto>.Fail("获取商品详情失败");
}
}
/// <summary>
/// 获取商品子奖品列表
/// POST /api/goods_children
/// Requirements: 3.1-3.3
/// </summary>
[HttpPost("goods_children")]
[Authorize]
public async Task<ApiResponse<List<GoodsChildrenDto>>> GetGoodsChildren([FromBody] GoodsChildrenRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<List<GoodsChildrenDto>>.Unauthorized();
}
try
{
if (request == null || request.GoodsId <= 0 || request.GoodsListId <= 0)
{
return ApiResponse<List<GoodsChildrenDto>>.Fail("参数不能为空");
}
var result = await _goodsService.GetGoodsChildrenAsync(request.GoodsId, request.GoodsNum, request.GoodsListId);
return ApiResponse<List<GoodsChildrenDto>>.Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get goods children: GoodsId={GoodsId}, GoodsListId={GoodsListId}",
request?.GoodsId, request?.GoodsListId);
return ApiResponse<List<GoodsChildrenDto>>.Fail("获取子奖品列表失败");
}
}
/// <summary>
/// 获取商品扩展配置
/// POST /api/goods_extend
/// Requirements: 4.1-4.3
/// </summary>
[HttpPost("goods_extend")]
[Authorize]
public async Task<ApiResponse<GoodsExtendDto>> GetGoodsExtend([FromBody] GoodsExtendRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<GoodsExtendDto>.Unauthorized();
}
try
{
if (request == null || request.GoodsId <= 0)
{
return ApiResponse<GoodsExtendDto>.Fail("商品ID不能为空");
}
var result = await _goodsService.GetGoodsExtendAsync(request.GoodsId, request.GoodsType);
return ApiResponse<GoodsExtendDto>.Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get goods extend: GoodsId={GoodsId}", request?.GoodsId);
return ApiResponse<GoodsExtendDto>.Fail("获取商品扩展配置失败");
}
}
/// <summary>
/// 获取箱号列表
/// POST /api/goods_num_list
/// Requirements: 5.1
/// </summary>
[HttpPost("goods_num_list")]
[Authorize]
public async Task<ApiResponse<List<BoxGroupDto>>> GetBoxList([FromBody] BoxListRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<List<BoxGroupDto>>.Unauthorized();
}
try
{
if (request == null || request.GoodsId <= 0)
{
return ApiResponse<List<BoxGroupDto>>.Fail("商品ID不能为空");
}
var result = await _goodsService.GetBoxListAsync(request.GoodsId);
return ApiResponse<List<BoxGroupDto>>.Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get box list: GoodsId={GoodsId}", request?.GoodsId);
return ApiResponse<List<BoxGroupDto>>.Fail("获取箱号列表失败");
}
}
/// <summary>
/// 获取箱号详情
/// POST /api/goods_num_detail
/// Requirements: 5.2-5.4
/// </summary>
[HttpPost("goods_num_detail")]
[Authorize]
public async Task<ApiResponse<List<BoxDetailDto>>> GetBoxDetail([FromBody] BoxDetailRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<List<BoxDetailDto>>.Unauthorized();
}
try
{
if (request == null || request.GoodsId <= 0)
{
return ApiResponse<List<BoxDetailDto>>.Fail("商品ID不能为空");
}
var result = await _goodsService.GetBoxDetailAsync(request.GoodsId, request.PageNo, request.Sort);
return ApiResponse<List<BoxDetailDto>>.Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get box detail: GoodsId={GoodsId}", request?.GoodsId);
return ApiResponse<List<BoxDetailDto>>.Fail("获取箱号详情失败");
}
}
/// <summary>
/// 获取奖品数量统计
/// POST /api/goods_prize_count
/// Requirements: 7.1
/// </summary>
[HttpPost("goods_prize_count")]
[Authorize]
public async Task<ApiResponse<PrizeCountResponseDto>> GetPrizeCount([FromBody] PrizeCountRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<PrizeCountResponseDto>.Unauthorized();
}
try
{
if (request == null || request.GoodsId <= 0)
{
return ApiResponse<PrizeCountResponseDto>.Fail("商品ID不能为空");
}
var result = await _prizeService.GetPrizeCountAsync(request.GoodsId);
return ApiResponse<PrizeCountResponseDto>.Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get prize count: GoodsId={GoodsId}", request?.GoodsId);
return ApiResponse<PrizeCountResponseDto>.Fail("获取奖品统计失败");
}
}
/// <summary>
/// 获取奖品内容
/// POST /api/goods_prize_content
/// Requirements: 7.2
/// </summary>
[HttpPost("goods_prize_content")]
[Authorize]
public async Task<ApiResponse<PrizeContentResponseDto>> GetPrizeContent([FromBody] PrizeContentRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<PrizeContentResponseDto>.Unauthorized();
}
try
{
if (request == null || request.GoodsId <= 0)
{
return ApiResponse<PrizeContentResponseDto>.Fail("商品ID不能为空");
}
var result = await _prizeService.GetPrizeContentAsync(request.GoodsId, request.Num);
return ApiResponse<PrizeContentResponseDto>.Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get prize content: GoodsId={GoodsId}", request?.GoodsId);
return ApiResponse<PrizeContentResponseDto>.Fail("获取奖品内容失败");
}
}
/// <summary>
/// 获取中奖记录
/// POST /api/goods_prize_logs
/// Requirements: 8.1-8.4
/// </summary>
[HttpPost("goods_prize_logs")]
[Authorize]
public async Task<ApiResponse<PrizeLogsResponseDto>> GetPrizeLogs([FromBody] PrizeLogsRequest? request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<PrizeLogsResponseDto>.Unauthorized();
}
try
{
if (request == null || request.GoodsId <= 0)
{
return ApiResponse<PrizeLogsResponseDto>.Fail("商品ID不能为空");
}
var page = request.Page < 1 ? 1 : request.Page;
var pageSize = request.PageSize < 1 ? 15 : request.PageSize;
var result = await _prizeService.GetPrizeLogsAsync(
request.GoodsId,
request.GoodsNum,
request.ShangId,
page,
pageSize);
return ApiResponse<PrizeLogsResponseDto>.Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get prize logs: GoodsId={GoodsId}", request?.GoodsId);
return ApiResponse<PrizeLogsResponseDto>.Fail("获取中奖记录失败");
}
}
#region Private Helper Methods
/// <summary>
/// 获取当前登录用户ID
/// </summary>
private int? GetCurrentUserId()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out var userId))
{
return null;
}
return userId;
}
#endregion
}

View File

@ -0,0 +1,462 @@
# HoneyBox API 商品系统接口测试文件
# 用于验证所有商品系统相关的控制器接口
# Checkpoint 15: 控制器测试验证
@baseUrl = http://localhost:5238/api
@contentType = application/json
# 测试用Token需要通过登录接口获取真实Token后替换
# 下面是一个有效的测试Token用户ID: 21583有效期至2026年
@authToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoi5b6u5L-h55So5oi3MTMxMCIsImV4cCI6MTc2NzQzMTM1OCwidWlkIjoiMzMyMjY2IiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbmFtZWlkZW50aWZpZXIiOiIyMTU4MyIsImF1ZCI6IkhvbmV5Qm94VXNlcnMiLCJpc3MiOiJIb25leUJveCJ9.700XWIUmzEumNk5tNYRshh7M42A8MG1X4yTHuz9PZbc
# 测试用商品ID需要替换为数据库中存在的商品ID
@testGoodsId = 1
@testGoodsNum = 1
@testGoodsListId = 1
@testShangId = 1
### ============================================
### 1. 健康检查接口
### ============================================
### 1.1 健康检查 - 验证服务是否正常运行
# GET /api/health
GET {{baseUrl}}/health
Accept: {{contentType}}
### ============================================
### 2. 商品列表接口 (GoodsController)
### Requirements: 1.1-1.6
### ============================================
### 2.1 获取商品列表 - 全部类型
# POST /api/goods_list
# Requirements: 1.1, 1.2
POST {{baseUrl}}/goods_list
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"type": -1,
"page": 1,
"pageSize": 15
}
### 2.2 获取商品列表 - 按类型过滤(一番赏 type=1
# POST /api/goods_list
# Requirements: 1.1
POST {{baseUrl}}/goods_list
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"type": 1,
"page": 1,
"pageSize": 15
}
### 2.3 获取商品列表 - 按类型过滤(无限赏 type=2
# POST /api/goods_list
# Requirements: 1.1
POST {{baseUrl}}/goods_list
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"type": 2,
"page": 1,
"pageSize": 15
}
### 2.4 获取商品列表 - 第二页
# POST /api/goods_list
# Requirements: 1.1
POST {{baseUrl}}/goods_list
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"type": -1,
"page": 2,
"pageSize": 15
}
### ============================================
### 3. 商品详情接口 (GoodsController)
### Requirements: 2.1-2.7
### ============================================
### 3.1 获取商品详情 - 自动选择箱号
# POST /api/goods_detail
# Requirements: 2.1, 2.2
POST {{baseUrl}}/goods_detail
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"goods_num": 0
}
### 3.2 获取商品详情 - 指定箱号
# POST /api/goods_detail
# Requirements: 2.1, 2.3-2.7
POST {{baseUrl}}/goods_detail
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"goods_num": {{testGoodsNum}}
}
### ============================================
### 4. 商品子奖品接口 (GoodsController)
### Requirements: 3.1-3.3
### ============================================
### 4.1 获取商品子奖品列表
# POST /api/goods_children
# Requirements: 3.1-3.3
POST {{baseUrl}}/goods_children
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"goods_num": {{testGoodsNum}},
"goods_list_id": {{testGoodsListId}}
}
### ============================================
### 5. 商品扩展配置接口 (GoodsController)
### Requirements: 4.1-4.3
### ============================================
### 5.1 获取商品扩展配置 - 一番赏类型
# POST /api/goods_extend
# Requirements: 4.1-4.3
POST {{baseUrl}}/goods_extend
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"goods_type": 1
}
### 5.2 获取商品扩展配置 - 无限赏类型
# POST /api/goods_extend
# Requirements: 4.1-4.3
POST {{baseUrl}}/goods_extend
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"goods_type": 2
}
### ============================================
### 6. 箱号管理接口 (GoodsController)
### Requirements: 5.1-5.4
### ============================================
### 6.1 获取箱号列表
# POST /api/goods_num_list
# Requirements: 5.1
POST {{baseUrl}}/goods_num_list
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}}
}
### 6.2 获取箱号详情 - 箱号升序
# POST /api/goods_num_detail
# Requirements: 5.2-5.4
POST {{baseUrl}}/goods_num_detail
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"page_no": 0,
"sort": 0
}
### 6.3 获取箱号详情 - 箱号降序
# POST /api/goods_num_detail
# Requirements: 5.2-5.4
POST {{baseUrl}}/goods_num_detail
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"page_no": 0,
"sort": 1
}
### 6.4 获取箱号详情 - 余量降序
# POST /api/goods_num_detail
# Requirements: 5.2-5.4
POST {{baseUrl}}/goods_num_detail
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"page_no": 0,
"sort": 2
}
### ============================================
### 7. 奖品统计接口 (GoodsController)
### Requirements: 7.1-7.2
### ============================================
### 7.1 获取奖品数量统计
# POST /api/goods_prize_count
# Requirements: 7.1
POST {{baseUrl}}/goods_prize_count
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}}
}
### 7.2 获取奖品内容 - 指定箱号
# POST /api/goods_prize_content
# Requirements: 7.2
POST {{baseUrl}}/goods_prize_content
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"num": {{testGoodsNum}}
}
### 7.3 获取奖品内容 - 默认箱号
# POST /api/goods_prize_content
# Requirements: 7.2
POST {{baseUrl}}/goods_prize_content
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"num": 0
}
### ============================================
### 8. 中奖记录接口 (GoodsController)
### Requirements: 8.1-8.4
### ============================================
### 8.1 获取中奖记录 - 全部分类
# POST /api/goods_prize_logs
# Requirements: 8.1, 8.3
POST {{baseUrl}}/goods_prize_logs
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"goods_num": {{testGoodsNum}},
"shang_id": 0,
"page": 1,
"pageSize": 15
}
### 8.2 获取中奖记录 - 按分类过滤
# POST /api/goods_prize_logs
# Requirements: 8.1, 8.2
POST {{baseUrl}}/goods_prize_logs
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"goods_num": {{testGoodsNum}},
"shang_id": {{testShangId}},
"page": 1,
"pageSize": 15
}
### ============================================
### 9. 收藏接口 (CollectionController)
### Requirements: 6.1-6.4
### ============================================
### 9.1 收藏/取消收藏商品
# POST /api/addCollect
# Requirements: 6.1-6.3
POST {{baseUrl}}/addCollect
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"goods_num": {{testGoodsNum}}
}
### 9.2 获取收藏列表 - 全部类型
# POST /api/listCollect
# Requirements: 6.4
POST {{baseUrl}}/listCollect
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"type": -1,
"page": 1,
"pageSize": 100
}
### 9.3 获取收藏列表 - 按类型过滤(一番赏)
# POST /api/listCollect
# Requirements: 6.4
POST {{baseUrl}}/listCollect
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"type": 1,
"page": 1,
"pageSize": 100
}
### 9.4 删除收藏
# POST /api/delCollect
# 注意需要替换为有效的收藏ID
POST {{baseUrl}}/delCollect
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"id": 1
}
### ============================================
### 10. 错误场景测试
### ============================================
### 10.1 未授权访问 - 获取商品列表无Token
POST {{baseUrl}}/goods_list
Content-Type: {{contentType}}
{
"type": -1,
"page": 1,
"pageSize": 15
}
### 10.2 未授权访问 - 获取商品详情无Token
POST {{baseUrl}}/goods_detail
Content-Type: {{contentType}}
{
"goods_id": {{testGoodsId}},
"goods_num": 0
}
### 10.3 未授权访问 - 收藏商品无Token
POST {{baseUrl}}/addCollect
Content-Type: {{contentType}}
{
"goods_id": {{testGoodsId}},
"goods_num": {{testGoodsNum}}
}
### 10.4 未授权访问 - 获取收藏列表无Token
POST {{baseUrl}}/listCollect
Content-Type: {{contentType}}
{
"type": -1,
"page": 1,
"pageSize": 100
}
### 10.5 参数缺失 - 商品详情无商品ID
POST {{baseUrl}}/goods_detail
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{}
### 10.6 参数缺失 - 商品详情商品ID为0
POST {{baseUrl}}/goods_detail
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": 0,
"goods_num": 0
}
### 10.7 参数缺失 - 子奖品无商品ID
POST {{baseUrl}}/goods_children
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_num": {{testGoodsNum}},
"goods_list_id": {{testGoodsListId}}
}
### 10.8 参数缺失 - 子奖品无奖品列表ID
POST {{baseUrl}}/goods_children
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_id": {{testGoodsId}},
"goods_num": {{testGoodsNum}}
}
### 10.9 参数缺失 - 扩展配置无商品ID
POST {{baseUrl}}/goods_extend
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_type": 1
}
### 10.10 参数缺失 - 箱号列表无商品ID
POST {{baseUrl}}/goods_num_list
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{}
### 10.11 参数缺失 - 奖品统计无商品ID
POST {{baseUrl}}/goods_prize_count
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{}
### 10.12 参数缺失 - 收藏商品无商品ID
POST {{baseUrl}}/addCollect
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{
"goods_num": {{testGoodsNum}}
}
### 10.13 参数缺失 - 删除收藏无收藏ID
POST {{baseUrl}}/delCollect
Content-Type: {{contentType}}
Authorization: Bearer {{authToken}}
{}

View File

@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("HoneyBox.Api")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+21c4bb5c6a0c35c345420f3a29d64fd2922e7427")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7e00d28ad48928059f0ff514ed804f2cb0626442")]
[assembly: System.Reflection.AssemblyProductAttribute("HoneyBox.Api")]
[assembly: System.Reflection.AssemblyTitleAttribute("HoneyBox.Api")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@ -1 +1 @@
{"GlobalPropertiesHash":"Ysh/n1uypAtQviJNGODCPwxAly+bcTyiCeqRgFpIiaE=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["KmCbcplRcUDHmkghC2lKt4atEvqmdCypecbGKiyOL1Y=","Yr\u002BOWR3TPK4EWzVknKsBQAY40K9td9F\u002BJRMqIuPwWE0=","iwh6YCm1hP7gw5T3HlRwNBcUu9NVDH4VUC1D2LbVP6Y=","A1eOW5m2OjAzDYXt5hE8LFSp5KPpS853XPPu1pJgO7M=","0HeClgJNdUHftrAxD1WHyxDsoBBIMgh7pzhFVwvPzCk=","ug\u002BeY\u002Bhf4RfN5v1Kpje6K\u002BfkdoRsX4hw/oT\u002Bxe0stT4=","Ff7GKRhN1a2md5IaW2AMR/j8y6P7n3aDr8WZCWUaC6o=","7mYTjVVc3QiwJS21WoZe/uABKsCqwZFgh21t86u3bEo="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"Ysh/n1uypAtQviJNGODCPwxAly+bcTyiCeqRgFpIiaE=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["KmCbcplRcUDHmkghC2lKt4atEvqmdCypecbGKiyOL1Y=","Yr\u002BOWR3TPK4EWzVknKsBQAY40K9td9F\u002BJRMqIuPwWE0=","iwh6YCm1hP7gw5T3HlRwNBcUu9NVDH4VUC1D2LbVP6Y=","j7KwQ7vZvdcRd5zgXvB4eGt8NbUUSSPUUhkRceBkb1s=","A1eOW5m2OjAzDYXt5hE8LFSp5KPpS853XPPu1pJgO7M=","MzSV86jgVStZOroCGPZnD7UWQ8g2CiyPQ0SO8zLA3TM=","ug\u002BeY\u002Bhf4RfN5v1Kpje6K\u002BfkdoRsX4hw/oT\u002Bxe0stT4=","Ff7GKRhN1a2md5IaW2AMR/j8y6P7n3aDr8WZCWUaC6o=","FnpzMeeyaOTZN\u002BxilFV3aDYmU2xYJ/3/CY39GC865P8="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@ -1 +1 @@
{"GlobalPropertiesHash":"nOa+ffacSShxUVXaHMpxAJqQMESoIWNWaEHTlS9nGGU=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["KmCbcplRcUDHmkghC2lKt4atEvqmdCypecbGKiyOL1Y=","Yr\u002BOWR3TPK4EWzVknKsBQAY40K9td9F\u002BJRMqIuPwWE0=","iwh6YCm1hP7gw5T3HlRwNBcUu9NVDH4VUC1D2LbVP6Y=","A1eOW5m2OjAzDYXt5hE8LFSp5KPpS853XPPu1pJgO7M=","0HeClgJNdUHftrAxD1WHyxDsoBBIMgh7pzhFVwvPzCk=","ug\u002BeY\u002Bhf4RfN5v1Kpje6K\u002BfkdoRsX4hw/oT\u002Bxe0stT4=","Ff7GKRhN1a2md5IaW2AMR/j8y6P7n3aDr8WZCWUaC6o=","7mYTjVVc3QiwJS21WoZe/uABKsCqwZFgh21t86u3bEo="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"nOa+ffacSShxUVXaHMpxAJqQMESoIWNWaEHTlS9nGGU=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["KmCbcplRcUDHmkghC2lKt4atEvqmdCypecbGKiyOL1Y=","Yr\u002BOWR3TPK4EWzVknKsBQAY40K9td9F\u002BJRMqIuPwWE0=","iwh6YCm1hP7gw5T3HlRwNBcUu9NVDH4VUC1D2LbVP6Y=","j7KwQ7vZvdcRd5zgXvB4eGt8NbUUSSPUUhkRceBkb1s=","A1eOW5m2OjAzDYXt5hE8LFSp5KPpS853XPPu1pJgO7M=","MzSV86jgVStZOroCGPZnD7UWQ8g2CiyPQ0SO8zLA3TM=","ug\u002BeY\u002Bhf4RfN5v1Kpje6K\u002BfkdoRsX4hw/oT\u002Bxe0stT4=","Ff7GKRhN1a2md5IaW2AMR/j8y6P7n3aDr8WZCWUaC6o=","FnpzMeeyaOTZN\u002BxilFV3aDYmU2xYJ/3/CY39GC865P8="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@ -0,0 +1,46 @@
using HoneyBox.Model.Models;
using HoneyBox.Model.Models.Goods;
namespace HoneyBox.Core.Interfaces;
/// <summary>
/// 收藏服务接口
/// </summary>
public interface ICollectionService
{
/// <summary>
/// 收藏/取消收藏商品 (PHP addCollect - 切换收藏状态)
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="goodsId">商品ID</param>
/// <param name="goodsNum">箱号</param>
/// <returns>操作是否成功</returns>
Task<bool> ToggleCollectionAsync(int userId, int goodsId, int goodsNum);
/// <summary>
/// 获取收藏列表 (PHP listCollect)
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="type">商品类型 (0=全部)</param>
/// <param name="page">页码</param>
/// <param name="pageSize">每页数量</param>
/// <returns>分页收藏列表</returns>
Task<CollectionListResponse> GetCollectionListAsync(int userId, int type, int page, int pageSize);
/// <summary>
/// 删除收藏 (PHP delCollect)
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="collectionId">收藏记录ID</param>
/// <returns>操作是否成功</returns>
Task<bool> DeleteCollectionAsync(int userId, int collectionId);
/// <summary>
/// 检查是否已收藏
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="goodsId">商品ID</param>
/// <param name="goodsNum">箱号</param>
/// <returns>是否已收藏</returns>
Task<bool> IsCollectedAsync(int userId, int goodsId, int goodsNum);
}

View File

@ -0,0 +1,66 @@
namespace HoneyBox.Core.Interfaces;
/// <summary>
/// 商品缓存服务接口
/// </summary>
public interface IGoodsCacheService
{
/// <summary>
/// 获取商品参与次数
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <returns>参与次数,如果缓存不存在返回-1</returns>
Task<int> GetJoinCountAsync(int goodsId);
/// <summary>
/// 设置商品参与次数
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <param name="count">参与次数</param>
Task SetJoinCountAsync(int goodsId, int count);
/// <summary>
/// 增加商品参与次数
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <returns>增加后的参与次数</returns>
Task<int> IncrementJoinCountAsync(int goodsId);
/// <summary>
/// 清除商品缓存
/// </summary>
/// <param name="goodsId">商品ID</param>
Task InvalidateGoodsCacheAsync(int goodsId);
/// <summary>
/// 获取商品列表缓存
/// </summary>
/// <param name="cacheKey">缓存键</param>
/// <returns>缓存的商品列表JSON如果不存在返回null</returns>
Task<string?> GetGoodsListCacheAsync(string cacheKey);
/// <summary>
/// 设置商品列表缓存
/// </summary>
/// <param name="cacheKey">缓存键</param>
/// <param name="data">商品列表JSON</param>
/// <param name="expiry">过期时间</param>
Task SetGoodsListCacheAsync(string cacheKey, string data, TimeSpan expiry);
/// <summary>
/// 获取商品详情缓存
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <param name="goodsNum">箱号</param>
/// <returns>缓存的商品详情JSON如果不存在返回null</returns>
Task<string?> GetGoodsDetailCacheAsync(int goodsId, int goodsNum);
/// <summary>
/// 设置商品详情缓存
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <param name="goodsNum">箱号</param>
/// <param name="data">商品详情JSON</param>
/// <param name="expiry">过期时间</param>
Task SetGoodsDetailCacheAsync(int goodsId, int goodsNum, string data, TimeSpan expiry);
}

View File

@ -0,0 +1,60 @@
using HoneyBox.Model.Models;
using HoneyBox.Model.Models.Goods;
namespace HoneyBox.Core.Interfaces;
/// <summary>
/// 商品服务接口
/// </summary>
public interface IGoodsService
{
/// <summary>
/// 获取商品列表
/// </summary>
/// <param name="request">查询请求</param>
/// <param name="userId">用户ID</param>
/// <returns>分页商品列表</returns>
Task<PageResponse<GoodsListDto>> GetGoodsListAsync(GoodsListRequest request, int userId);
/// <summary>
/// 获取商品详情
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <param name="goodsNum">箱号 (0表示自动选择)</param>
/// <param name="userId">用户ID</param>
/// <returns>商品详情</returns>
Task<GoodsDetailResponseDto> GetGoodsDetailAsync(int goodsId, int goodsNum, int userId);
/// <summary>
/// 获取商品子奖品列表
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <param name="goodsNum">箱号</param>
/// <param name="goodsListId">奖品列表ID</param>
/// <returns>子奖品列表</returns>
Task<List<GoodsChildrenDto>> GetGoodsChildrenAsync(int goodsId, int goodsNum, int goodsListId);
/// <summary>
/// 获取商品扩展配置
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <param name="goodsType">商品类型</param>
/// <returns>扩展配置</returns>
Task<GoodsExtendDto> GetGoodsExtendAsync(int goodsId, int goodsType);
/// <summary>
/// 获取箱号列表
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <returns>箱号分组列表</returns>
Task<List<BoxGroupDto>> GetBoxListAsync(int goodsId);
/// <summary>
/// 获取箱号详情
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <param name="pageNo">页码</param>
/// <param name="sort">排序方式: 0=箱号升序, 1=箱号降序, 2=余量降序</param>
/// <returns>箱号详情列表</returns>
Task<List<BoxDetailDto>> GetBoxDetailAsync(int goodsId, int pageNo, int sort);
}

View File

@ -0,0 +1,35 @@
using HoneyBox.Model.Models.Goods;
namespace HoneyBox.Core.Interfaces;
/// <summary>
/// 奖品服务接口
/// </summary>
public interface IPrizeService
{
/// <summary>
/// 获取奖品数量统计
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <returns>奖品数量统计</returns>
Task<PrizeCountResponseDto> GetPrizeCountAsync(int goodsId);
/// <summary>
/// 获取奖品内容
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <param name="num">箱号</param>
/// <returns>奖品内容</returns>
Task<PrizeContentResponseDto> GetPrizeContentAsync(int goodsId, int num);
/// <summary>
/// 获取中奖记录
/// </summary>
/// <param name="goodsId">商品ID</param>
/// <param name="goodsNum">箱号</param>
/// <param name="shangId">赏品分类ID (0表示全部)</param>
/// <param name="page">页码</param>
/// <param name="pageSize">每页数量</param>
/// <returns>中奖记录</returns>
Task<PrizeLogsResponseDto> GetPrizeLogsAsync(int goodsId, int goodsNum, int shangId, int page, int pageSize);
}

View File

@ -0,0 +1,257 @@
using HoneyBox.Core.Interfaces;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using HoneyBox.Model.Models;
using HoneyBox.Model.Models.Goods;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace HoneyBox.Core.Services;
/// <summary>
/// 收藏服务实现
/// </summary>
public class CollectionService : ICollectionService
{
private readonly HoneyBoxDbContext _dbContext;
private readonly ILogger<CollectionService> _logger;
private readonly IRedisService _redisService;
// 奖品统计ID范围 (与PHP保持一致)
private static readonly int[] ShangPrizeIdRange = { 10, 33 };
public CollectionService(
HoneyBoxDbContext dbContext,
ILogger<CollectionService> logger,
IRedisService redisService)
{
_dbContext = dbContext;
_logger = logger;
_redisService = redisService;
}
/// <inheritdoc />
public async Task<bool> ToggleCollectionAsync(int userId, int goodsId, int goodsNum)
{
// 1. 验证商品是否存在
var goods = await _dbContext.Goods
.Where(g => g.Id == goodsId)
.Select(g => new { g.Id, g.Stock, g.Status, g.Type })
.FirstOrDefaultAsync();
if (goods == null)
{
throw new InvalidOperationException("盒子不存在");
}
if (goods.Status != 1 && goods.Status != 3)
{
throw new InvalidOperationException("盒子已下架");
}
// 2. 检查是否已收藏
var existingCollection = await _dbContext.GoodsCollections
.Where(c => c.UserId == userId && c.GoodsId == goodsId && c.Num == goodsNum)
.FirstOrDefaultAsync();
bool result;
if (existingCollection != null)
{
// 已收藏,则取消收藏
_dbContext.GoodsCollections.Remove(existingCollection);
result = await _dbContext.SaveChangesAsync() > 0;
}
else
{
// 未收藏,则添加收藏
var collection = new GoodsCollection
{
UserId = userId,
GoodsId = goodsId,
Num = goodsNum,
Type = goods.Type,
CreatedAt = DateTime.Now
};
_dbContext.GoodsCollections.Add(collection);
result = await _dbContext.SaveChangesAsync() > 0;
}
// 3. 清除相关缓存
if (result)
{
try
{
await _redisService.DeleteAsync($"goods_detail_{goodsId}_{userId}");
await _redisService.DeleteAsync($"infinite_goodsdetail_{goodsId}_{userId}");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "清除收藏相关缓存失败");
}
}
return result;
}
/// <inheritdoc />
public async Task<CollectionListResponse> GetCollectionListAsync(int userId, int type, int page, int pageSize)
{
// 1. 构建查询
var query = _dbContext.GoodsCollections
.Where(c => c.UserId == userId);
// 按类型过滤
if (type > 0)
{
query = query.Where(c => c.Type == type);
}
// 2. 获取总数和分页数据
var totalCount = await query.CountAsync();
var lastPage = pageSize > 0 ? (int)Math.Ceiling((double)totalCount / pageSize) : 1;
var collections = await query
.OrderByDescending(c => c.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
if (!collections.Any())
{
return new CollectionListResponse
{
Data = new List<CollectionDto>(),
LastPage = lastPage
};
}
// 3. 获取商品信息
var goodsIds = collections.Select(c => c.GoodsId).Distinct().ToList();
var goodsDict = await _dbContext.Goods
.Where(g => goodsIds.Contains(g.Id))
.Select(g => new { g.Id, g.Title, g.Price, g.ImgUrl })
.ToDictionaryAsync(g => g.Id);
// 4. 获取库存信息 (仅对特定类型的商品)
// 类型 1, 3, 5, 6, 10, 11 需要查询库存
var stockTypes = new byte[] { 1, 3, 5, 6, 10, 11 };
var stockCollections = collections
.Where(c => stockTypes.Contains(c.Type))
.Select(c => new { c.GoodsId, c.Num })
.Distinct()
.ToList();
var stockDict = new Dictionary<string, (int Stock, int SurplusStock)>();
if (stockCollections.Any())
{
foreach (var sc in stockCollections)
{
var stockInfo = await _dbContext.GoodsItems
.Where(gi => gi.GoodsId == sc.GoodsId
&& gi.Num == sc.Num
&& gi.ShangId >= ShangPrizeIdRange[0]
&& gi.ShangId <= ShangPrizeIdRange[1])
.GroupBy(gi => 1)
.Select(g => new
{
Stock = g.Sum(x => x.Stock),
SurplusStock = g.Sum(x => x.SurplusStock)
})
.FirstOrDefaultAsync();
var key = $"{sc.GoodsId}_{sc.Num}";
stockDict[key] = stockInfo != null
? (stockInfo.Stock, stockInfo.SurplusStock)
: (0, 0);
}
}
// 5. 构建响应
var result = new List<CollectionDto>();
foreach (var collection in collections)
{
var dto = new CollectionDto
{
Id = collection.Id,
GoodsId = collection.GoodsId,
Type = collection.Type,
Num = collection.Num
};
// 填充商品信息
if (goodsDict.TryGetValue(collection.GoodsId, out var goodsInfo))
{
dto.GoodsTitle = goodsInfo.Title;
dto.GoodsPrice = goodsInfo.Price.ToString("0.##");
dto.ImgUrl = FormatImageUrl(goodsInfo.ImgUrl);
}
// 填充库存信息
if (stockTypes.Contains(collection.Type))
{
var stockKey = $"{collection.GoodsId}_{collection.Num}";
if (stockDict.TryGetValue(stockKey, out var stock))
{
dto.Stock = stock.Stock;
dto.SurplusStock = stock.SurplusStock;
}
}
result.Add(dto);
}
return new CollectionListResponse
{
Data = result,
LastPage = lastPage
};
}
/// <inheritdoc />
public async Task<bool> DeleteCollectionAsync(int userId, int collectionId)
{
var collection = await _dbContext.GoodsCollections
.Where(c => c.UserId == userId && c.Id == collectionId)
.FirstOrDefaultAsync();
if (collection == null)
{
throw new InvalidOperationException("请求重复操作");
}
_dbContext.GoodsCollections.Remove(collection);
return await _dbContext.SaveChangesAsync() > 0;
}
/// <inheritdoc />
public async Task<bool> IsCollectedAsync(int userId, int goodsId, int goodsNum)
{
if (userId <= 0)
{
return false;
}
return await _dbContext.GoodsCollections
.AnyAsync(c => c.UserId == userId && c.GoodsId == goodsId && c.Num == goodsNum);
}
/// <summary>
/// 格式化图片URL
/// </summary>
private static string FormatImageUrl(string? imgUrl)
{
if (string.IsNullOrEmpty(imgUrl))
{
return string.Empty;
}
// 如果已经是完整URL直接返回
if (imgUrl.StartsWith("http://") || imgUrl.StartsWith("https://"))
{
return imgUrl;
}
// 否则直接返回 (可以从配置读取基础URL)
return imgUrl;
}
}

View File

@ -0,0 +1,114 @@
using HoneyBox.Core.Interfaces;
using Microsoft.Extensions.Logging;
namespace HoneyBox.Core.Services;
/// <summary>
/// 商品缓存服务实现
/// </summary>
public class GoodsCacheService : IGoodsCacheService
{
private readonly IRedisService _redisService;
private readonly ILogger<GoodsCacheService> _logger;
// 缓存键前缀 - 与PHP保持一致
private const string JoinCountKeyPrefix = "order_goods_count:";
private const string GoodsListKeyPrefix = "goods_list_";
private const string GoodsDetailKeyPrefix = "goods_detail_";
// 缓存过期时间
private static readonly TimeSpan JoinCountExpiry = TimeSpan.FromMinutes(5);
private static readonly TimeSpan GoodsListExpiry = TimeSpan.FromSeconds(30);
private static readonly TimeSpan GoodsDetailExpiry = TimeSpan.FromMinutes(5);
public GoodsCacheService(
IRedisService redisService,
ILogger<GoodsCacheService> logger)
{
_redisService = redisService;
_logger = logger;
}
/// <inheritdoc />
public async Task<int> GetJoinCountAsync(int goodsId)
{
var key = $"{JoinCountKeyPrefix}{goodsId}";
var value = await _redisService.GetStringAsync(key);
if (int.TryParse(value, out var count))
{
return count;
}
// 返回-1表示缓存不存在
return -1;
}
/// <inheritdoc />
public async Task SetJoinCountAsync(int goodsId, int count)
{
var key = $"{JoinCountKeyPrefix}{goodsId}";
await _redisService.SetStringAsync(key, count.ToString(), JoinCountExpiry);
}
/// <inheritdoc />
public async Task<int> IncrementJoinCountAsync(int goodsId)
{
var key = $"{JoinCountKeyPrefix}{goodsId}";
var currentValue = await GetJoinCountAsync(goodsId);
var newValue = currentValue > 0 ? currentValue + 1 : 1;
await _redisService.SetStringAsync(key, newValue.ToString(), JoinCountExpiry);
return newValue;
}
/// <inheritdoc />
public async Task InvalidateGoodsCacheAsync(int goodsId)
{
// 清除参与次数缓存
var joinCountKey = $"{JoinCountKeyPrefix}{goodsId}";
await _redisService.DeleteAsync(joinCountKey);
// 清除商品详情缓存 - 由于箱号可能有多个,这里清除所有可能的箱号缓存
// 注意实际生产环境中可能需要使用Redis的SCAN命令来批量删除
// 这里简化处理只清除常见的箱号范围0-100
var deleteTasks = new List<Task>();
for (int num = 0; num <= 100; num++)
{
var detailKey = $"{GoodsDetailKeyPrefix}{goodsId}_{num}";
deleteTasks.Add(_redisService.DeleteAsync(detailKey));
}
await Task.WhenAll(deleteTasks);
_logger.LogInformation("已清除商品 {GoodsId} 的所有缓存(参与次数、商品详情)", goodsId);
}
/// <inheritdoc />
public async Task<string?> GetGoodsListCacheAsync(string cacheKey)
{
var key = $"{GoodsListKeyPrefix}{cacheKey}";
return await _redisService.GetStringAsync(key);
}
/// <inheritdoc />
public async Task SetGoodsListCacheAsync(string cacheKey, string data, TimeSpan expiry)
{
var key = $"{GoodsListKeyPrefix}{cacheKey}";
await _redisService.SetStringAsync(key, data, expiry);
}
/// <inheritdoc />
public async Task<string?> GetGoodsDetailCacheAsync(int goodsId, int goodsNum)
{
var key = $"{GoodsDetailKeyPrefix}{goodsId}_{goodsNum}";
return await _redisService.GetStringAsync(key);
}
/// <inheritdoc />
public async Task SetGoodsDetailCacheAsync(int goodsId, int goodsNum, string data, TimeSpan expiry)
{
var key = $"{GoodsDetailKeyPrefix}{goodsId}_{goodsNum}";
await _redisService.SetStringAsync(key, data, expiry);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,421 @@
using HoneyBox.Core.Interfaces;
using HoneyBox.Model.Data;
using HoneyBox.Model.Models.Goods;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace HoneyBox.Core.Services;
/// <summary>
/// 奖品服务实现
/// </summary>
public class PrizeService : IPrizeService
{
private readonly HoneyBoxDbContext _dbContext;
private readonly ILogger<PrizeService> _logger;
// 抽奖赏品ID范围 [10, 33]
private static readonly int[] ShangPrizeIdRange = { 10, 33 };
// 赠送赏品ID (不计入统计)
private static readonly int[] ShangGiveIds = { 1, 2, 3, 4, 5 };
public PrizeService(
HoneyBoxDbContext dbContext,
ILogger<PrizeService> logger)
{
_dbContext = dbContext;
_logger = logger;
}
/// <inheritdoc />
public async Task<PrizeCountResponseDto> GetPrizeCountAsync(int goodsId)
{
// 1. 验证商品存在且状态有效
var goods = await _dbContext.Goods
.Where(g => g.Id == goodsId)
.Select(g => new { g.Id, g.Status })
.FirstOrDefaultAsync();
if (goods == null)
{
throw new InvalidOperationException("盒子不存在");
}
if (goods.Status != 1 && goods.Status != 3)
{
throw new InvalidOperationException("盒子已下架");
}
// 2. 按赏品分类统计数量
var prizeStats = await _dbContext.GoodsItems
.Where(gi => gi.GoodsId == goodsId
&& gi.GoodsListId == 0
&& gi.ShangId.HasValue
&& !ShangGiveIds.Contains(gi.ShangId.Value))
.GroupBy(gi => gi.ShangId)
.Select(g => new
{
ShangId = g.Key ?? 0,
Total = g.Sum(x => x.Stock),
Surplus = g.Sum(x => x.SurplusStock)
})
.OrderBy(x => x.ShangId)
.ToListAsync();
// 3. 获取所有赏品等级信息
var shangIds = prizeStats.Select(x => x.ShangId).Distinct().ToList();
var shangInfos = await _dbContext.PrizeLevels
.Where(p => shangIds.Contains(p.Id))
.ToDictionaryAsync(p => p.Id, p => new ShangInfoDto
{
Id = p.Id,
Title = p.Title,
ImgUrl = FormatImageUrl(p.ImgUrl),
Color = p.Color,
SpecialImgUrl = FormatImageUrl(p.SpecialImgUrl)
});
// 4. 构建结果
var list = prizeStats.Select(ps => new PrizeCountItemDto
{
ShangId = ps.ShangId,
ShangInfo = shangInfos.TryGetValue(ps.ShangId, out var info) ? info : null,
Total = ps.Total,
Surplus = ps.Surplus
}).ToList();
return new PrizeCountResponseDto
{
List = list,
Total = list.Sum(x => x.Total),
Surplus = list.Sum(x => x.Surplus)
};
}
/// <inheritdoc />
public async Task<PrizeContentResponseDto> GetPrizeContentAsync(int goodsId, int num)
{
// 1. 验证商品存在且状态有效
var goods = await _dbContext.Goods
.Where(g => g.Id == goodsId)
.Select(g => new { g.Id, g.Status, g.Stock })
.FirstOrDefaultAsync();
if (goods == null)
{
throw new InvalidOperationException("盒子不存在");
}
if (goods.Status != 1 && goods.Status != 3)
{
throw new InvalidOperationException("盒子已下架");
}
// 验证箱号有效性
if (num < 0 || num > goods.Stock)
{
throw new InvalidOperationException("箱号错误");
}
// 如果num为0默认查询第1箱
if (num == 0)
{
num = 1;
}
// 2. 获取本箱子总剩余库存(用于计算概率)
var allSurplusStock = await _dbContext.GoodsItems
.Where(gi => gi.GoodsId == goodsId
&& gi.Num == num
&& gi.GoodsListId == 0
&& gi.ShangId.HasValue
&& !ShangGiveIds.Contains(gi.ShangId.Value))
.SumAsync(gi => gi.SurplusStock);
// 3. 查询指定箱号的奖品列表
var goodsItems = await _dbContext.GoodsItems
.Where(gi => gi.GoodsId == goodsId
&& gi.Num == num
&& gi.GoodsListId == 0)
.OrderByDescending(gi => gi.Sort)
.ThenBy(gi => gi.ShangId)
.ThenBy(gi => gi.Id)
.Select(gi => new
{
gi.Id,
gi.ShangId,
gi.Title,
gi.Stock,
gi.SurplusStock,
gi.ImgUrl,
gi.GoodsType,
gi.SaleTime,
gi.Price,
gi.ScMoney
})
.ToListAsync();
// 4. 获取所有赏品等级信息
var shangIds = goodsItems
.Where(x => x.ShangId.HasValue)
.Select(x => x.ShangId!.Value)
.Distinct()
.ToList();
var shangInfos = await _dbContext.PrizeLevels
.Where(p => shangIds.Contains(p.Id))
.ToDictionaryAsync(p => p.Id, p => new ShangInfoDto
{
Id = p.Id,
Title = p.Title,
ImgUrl = FormatImageUrl(p.ImgUrl),
Color = p.Color,
SpecialImgUrl = FormatImageUrl(p.SpecialImgUrl)
});
// 5. 构建结果列表并计算概率
var result = new List<GoodsListItemDto>();
decimal proAll = 0;
decimal proMax = 0;
int proMaxIndex = 0;
for (int i = 0; i < goodsItems.Count; i++)
{
var item = goodsItems[i];
var shangId = item.ShangId ?? 0;
var surplusStock = item.SurplusStock;
string pro;
decimal proNum = 0;
// 赠送赏品不计入概率
if (ShangGiveIds.Contains(shangId))
{
pro = "只赠不售";
}
else
{
if (surplusStock > 0 && allSurplusStock > 0)
{
proNum = Math.Round((decimal)surplusStock / allSurplusStock * 100, 2);
pro = $"概率:{RemoveTrailingZeros(proNum)}%";
}
else
{
pro = "概率0%";
}
}
// 计算剩余概率
proAll += proNum;
if (proNum >= proMax)
{
proMax = proNum;
proMaxIndex = i;
}
var dto = new GoodsListItemDto
{
Id = item.Id,
ShangId = shangId,
ShangInfo = shangId > 0 && shangInfos.TryGetValue(shangId, out var info) ? info : null,
Title = item.Title,
Stock = item.Stock,
SurplusStock = surplusStock,
ImgUrl = FormatImageUrl(item.ImgUrl),
GoodsType = item.GoodsType,
Price = (item.Price * 1).ToString("0.##"),
ScMoney = item.ScMoney.ToString("0.##"),
SaleTime = item.SaleTime.HasValue ? item.SaleTime.Value.ToString("yyyy-MM-dd") : null,
Pro = pro,
Children = item.GoodsType == 4 // goods_type=4 表示有子奖品
};
result.Add(dto);
}
// 概率不足100%时,补足到最大概率的奖品
if (proMax > 0 && proAll < 100 && result.Count > proMaxIndex)
{
var surplusPro = 100 - proAll;
var newPro = proMax + surplusPro;
result[proMaxIndex].Pro = $"概率:{RemoveTrailingZeros(newPro)}%";
}
return new PrizeContentResponseDto
{
List = result
};
}
/// <inheritdoc />
public async Task<PrizeLogsResponseDto> GetPrizeLogsAsync(int goodsId, int goodsNum, int shangId, int page, int pageSize)
{
// 1. 验证商品存在且状态有效
var goods = await _dbContext.Goods
.Where(g => g.Id == goodsId)
.Select(g => new { g.Id, g.Status, g.Type, g.Stock })
.FirstOrDefaultAsync();
if (goods == null)
{
throw new InvalidOperationException("盒子不存在");
}
if (goods.Status != 1 && goods.Status != 3)
{
throw new InvalidOperationException("盒子已下架");
}
// 验证箱号有效性
if (goodsNum < 0)
{
throw new InvalidOperationException("箱号选择错误");
}
// 2. 获取中奖记录分类列表 (按shang_id分组)
var categoryList = await _dbContext.GoodsItems
.Where(gi => gi.GoodsId == goodsId && gi.Num == goodsNum)
.Select(gi => gi.ShangId)
.Distinct()
.ToListAsync();
// 获取赏品等级信息
var shangIds = categoryList.Where(s => s.HasValue).Select(s => s!.Value).ToList();
var shangInfos = await _dbContext.PrizeLevels
.Where(p => shangIds.Contains(p.Id))
.ToDictionaryAsync(p => p.Id, p => new { p.Title, p.Color });
// 构建分类列表,添加"全部"选项
var categories = new List<CategoryDto>
{
new CategoryDto { ShangId = 0, ShangTitle = "全部" }
};
foreach (var sid in categoryList.Where(s => s.HasValue).Select(s => s!.Value).OrderBy(s => s))
{
if (shangInfos.TryGetValue(sid, out var info))
{
categories.Add(new CategoryDto
{
ShangId = sid,
ShangTitle = info.Title
});
}
}
// 3. 构建中奖记录查询
var query = _dbContext.OrderItems
.Where(oi => oi.GoodsId == goodsId
&& oi.Num == goodsNum
&& oi.OrderType == goods.Type
&& oi.Source == 1);
// 如果指定了shang_id添加筛选条件
if (shangId > 0)
{
query = query.Where(oi => oi.ShangId == shangId);
}
// 4. 按order_id, goodslist_id, user_id分组统计prize_num
var groupedQuery = query
.GroupBy(oi => new { oi.OrderId, oi.GoodslistId, oi.UserId })
.Select(g => new
{
UserId = g.Key.UserId,
GoodslistTitle = g.First().GoodslistTitle,
GoodslistImgurl = g.First().GoodslistImgurl,
ShangId = g.First().ShangId,
Addtime = g.First().Addtime,
PrizeNum = g.Count()
})
.OrderByDescending(x => x.Addtime);
// 5. 计算总数和分页
var totalCount = await query
.GroupBy(oi => new { oi.OrderId, oi.GoodslistId, oi.UserId })
.CountAsync();
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
// 6. 获取分页数据
var prizeLogData = await groupedQuery
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// 7. 获取用户信息
var userIds = prizeLogData.Select(x => x.UserId).Distinct().ToList();
var users = await _dbContext.Users
.Where(u => userIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => new { u.Nickname, u.HeadImg });
// 8. 获取赏品颜色信息
var prizeShangIds = prizeLogData.Select(x => x.ShangId).Distinct().ToList();
var prizeShangInfos = await _dbContext.PrizeLevels
.Where(p => prizeShangIds.Contains(p.Id))
.ToDictionaryAsync(p => p.Id, p => new { p.Title, p.Color });
// 9. 构建结果
var data = prizeLogData.Select(item =>
{
var userInfo = users.TryGetValue(item.UserId, out var user)
? new PrizeLogUserDto
{
Nickname = user.Nickname,
HeadImg = FormatImageUrl(user.HeadImg)
}
: null;
var shangInfo = prizeShangInfos.TryGetValue(item.ShangId, out var shang) ? shang : null;
return new PrizeLogDto
{
UserId = item.UserId,
GoodslistTitle = item.GoodslistTitle ?? string.Empty,
GoodslistImgurl = FormatImageUrl(item.GoodslistImgurl),
ShangId = item.ShangId,
Addtime = DateTimeOffset.FromUnixTimeSeconds(item.Addtime).LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss"),
PrizeNum = item.PrizeNum,
ShangTitle = shangInfo?.Title ?? string.Empty,
UserInfo = userInfo,
ShangColor = shangInfo?.Color ?? string.Empty
};
}).ToList();
return new PrizeLogsResponseDto
{
Category = categories,
Data = data,
LastPage = totalPages,
Total = totalCount
};
}
/// <summary>
/// 格式化图片URL
/// </summary>
private static string FormatImageUrl(string? imgUrl)
{
if (string.IsNullOrEmpty(imgUrl))
{
return string.Empty;
}
// 如果已经是完整URL直接返回
if (imgUrl.StartsWith("http://") || imgUrl.StartsWith("https://"))
{
return imgUrl;
}
return imgUrl;
}
/// <summary>
/// 移除小数末尾的零
/// </summary>
private static string RemoveTrailingZeros(decimal value)
{
return value.ToString("0.##");
}
}

View File

@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("HoneyBox.Core")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+21c4bb5c6a0c35c345420f3a29d64fd2922e7427")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7e00d28ad48928059f0ff514ed804f2cb0626442")]
[assembly: System.Reflection.AssemblyProductAttribute("HoneyBox.Core")]
[assembly: System.Reflection.AssemblyTitleAttribute("HoneyBox.Core")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@ -116,5 +116,41 @@ public class ServiceModule : Module
var logger = c.Resolve<ILogger<WelfareService>>();
return new WelfareService(dbContext, logger);
}).As<IWelfareService>().InstancePerLifetimeScope();
// ========== 商品系统服务注册 ==========
// 注册商品缓存服务
builder.Register(c =>
{
var redisService = c.Resolve<IRedisService>();
var logger = c.Resolve<ILogger<GoodsCacheService>>();
return new GoodsCacheService(redisService, logger);
}).As<IGoodsCacheService>().InstancePerLifetimeScope();
// 注册商品服务
builder.Register(c =>
{
var dbContext = c.Resolve<HoneyBoxDbContext>();
var cacheService = c.Resolve<IGoodsCacheService>();
var logger = c.Resolve<ILogger<GoodsService>>();
return new GoodsService(dbContext, cacheService, logger);
}).As<IGoodsService>().InstancePerLifetimeScope();
// 注册收藏服务
builder.Register(c =>
{
var dbContext = c.Resolve<HoneyBoxDbContext>();
var logger = c.Resolve<ILogger<CollectionService>>();
var redisService = c.Resolve<IRedisService>();
return new CollectionService(dbContext, logger, redisService);
}).As<ICollectionService>().InstancePerLifetimeScope();
// 注册奖品服务
builder.Register(c =>
{
var dbContext = c.Resolve<HoneyBoxDbContext>();
var logger = c.Resolve<ILogger<PrizeService>>();
return new PrizeService(dbContext, logger);
}).As<IPrizeService>().InstancePerLifetimeScope();
}
}

View File

@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("HoneyBox.Infrastructure")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+21c4bb5c6a0c35c345420f3a29d64fd2922e7427")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7e00d28ad48928059f0ff514ed804f2cb0626442")]
[assembly: System.Reflection.AssemblyProductAttribute("HoneyBox.Infrastructure")]
[assembly: System.Reflection.AssemblyTitleAttribute("HoneyBox.Infrastructure")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@ -94,6 +94,10 @@ public partial class HoneyBoxDbContext : DbContext
public virtual DbSet<RedeemCode> RedeemCodes { get; set; }
public virtual DbSet<GoodsCollection> GoodsCollections { get; set; }
public virtual DbSet<GoodsLock> GoodsLocks { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Connection string is configured in Program.cs via dependency injection
@ -2675,6 +2679,67 @@ public partial class HoneyBoxDbContext : DbContext
.HasColumnName("remark");
});
modelBuilder.Entity<GoodsCollection>(entity =>
{
entity.HasKey(e => e.Id).HasName("pk_goods_collections");
entity.ToTable("goods_collections", tb => tb.HasComment("商品收藏表,存储用户收藏的商品信息"));
entity.HasIndex(e => e.UserId, "ix_goods_collections_user_id");
entity.HasIndex(e => e.GoodsId, "ix_goods_collections_goods_id");
entity.HasIndex(e => new { e.UserId, e.GoodsId, e.Num }, "uk_goods_collections_user_goods_num").IsUnique();
entity.Property(e => e.Id)
.HasComment("收藏ID")
.HasColumnName("id");
entity.Property(e => e.UserId)
.HasComment("用户ID")
.HasColumnName("user_id");
entity.Property(e => e.GoodsId)
.HasComment("商品ID")
.HasColumnName("goods_id");
entity.Property(e => e.Num)
.HasComment("箱号")
.HasColumnName("num");
entity.Property(e => e.Type)
.HasComment("商品类型 1-一番赏 2-无限赏 3-擂台赏 4-抽卡机 5-积分赏 6-全局赏 7-福利盲盒 8-领主赏 9-连击赏")
.HasColumnName("type");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("添加时间")
.HasColumnName("created_at");
});
modelBuilder.Entity<GoodsLock>(entity =>
{
entity.HasKey(e => e.Id).HasName("pk_goods_locks");
entity.ToTable("goods_locks", tb => tb.HasComment("商品锁箱信息表,存储盲盒锁箱状态"));
entity.HasIndex(e => e.UserId, "ix_goods_locks_user_id");
entity.HasIndex(e => e.GoodsIdNum, "ix_goods_locks_goods_id_num");
entity.Property(e => e.Id)
.HasComment("锁箱ID")
.HasColumnName("id");
entity.Property(e => e.UserId)
.HasComment("用户ID")
.HasColumnName("user_id");
entity.Property(e => e.GoodsIdNum)
.HasMaxLength(20)
.HasComment("商品ID和箱号组合")
.HasColumnName("goods_id_num");
entity.Property(e => e.EndTime)
.HasComment("过期时间")
.HasColumnName("endtime");
entity.Property(e => e.UpdateTime)
.HasComment("更新时间")
.HasColumnName("update_time");
});
OnModelCreatingPartial(modelBuilder);
}

View File

@ -0,0 +1,39 @@
using System;
namespace HoneyBox.Model.Entities;
/// <summary>
/// 商品收藏表,存储用户收藏的商品信息
/// </summary>
public partial class GoodsCollection
{
/// <summary>
/// 收藏ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 商品ID
/// </summary>
public int GoodsId { get; set; }
/// <summary>
/// 箱号
/// </summary>
public int Num { get; set; }
/// <summary>
/// 商品类型 1-一番赏 2-无限赏 3-擂台赏 4-抽卡机 5-积分赏 6-全局赏 7-福利盲盒 8-领主赏 9-连击赏
/// </summary>
public byte Type { get; set; }
/// <summary>
/// 添加时间
/// </summary>
public DateTime CreatedAt { get; set; }
}

View File

@ -0,0 +1,34 @@
using System;
namespace HoneyBox.Model.Entities;
/// <summary>
/// 商品锁箱信息表,存储盲盒锁箱状态
/// </summary>
public partial class GoodsLock
{
/// <summary>
/// 锁箱ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 商品ID和箱号组合 (格式: goods_id_num)
/// </summary>
public string GoodsIdNum { get; set; } = null!;
/// <summary>
/// 过期时间 (Unix时间戳)
/// </summary>
public long EndTime { get; set; }
/// <summary>
/// 更新时间 (Unix时间戳)
/// </summary>
public long UpdateTime { get; set; }
}

View File

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace HoneyBox.Model.Models.Asset;
/// <summary>
@ -8,16 +10,19 @@ public class AssetRecordRequest
/// <summary>
/// 类型过滤 (0=全部)
/// </summary>
[JsonPropertyName("type")]
public int Type { get; set; } = 0;
/// <summary>
/// 页码
/// </summary>
[JsonPropertyName("page")]
public int Page { get; set; } = 1;
/// <summary>
/// 每页数量
/// </summary>
[JsonPropertyName("limit")]
public int Limit { get; set; } = 15;
}
@ -29,41 +34,48 @@ public class AssetRecordDto
/// <summary>
/// 变动金额 (带正负号)
/// </summary>
[JsonPropertyName("change_money")]
public string ChangeMoney { get; set; } = string.Empty;
/// <summary>
/// 变动说明
/// </summary>
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
/// <summary>
/// 添加时间 (格式: Y-m-d H:i:s)
/// </summary>
[JsonPropertyName("addtime")]
public string AddTime { get; set; } = string.Empty;
}
/// <summary>
/// 资产记录分页响应
/// 资产记录分页响应 (兼容PHP API格式)
/// </summary>
public class AssetRecordPageResponse
{
/// <summary>
/// 记录列表
/// </summary>
[JsonPropertyName("list")]
public List<AssetRecordDto> List { get; set; } = new();
/// <summary>
/// 最后一页页码
/// </summary>
[JsonPropertyName("last_page")]
public int LastPage { get; set; }
/// <summary>
/// 当前页码
/// </summary>
[JsonPropertyName("current_page")]
public int CurrentPage { get; set; }
/// <summary>
/// 总记录数
/// </summary>
[JsonPropertyName("total")]
public int Total { get; set; }
}

View File

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace HoneyBox.Model.Models;
/// <summary>
@ -8,16 +10,18 @@ public class PageRequest
/// <summary>
/// 页码从1开始
/// </summary>
[JsonPropertyName("page")]
public int Page { get; set; } = 1;
/// <summary>
/// 每页数量
/// </summary>
[JsonPropertyName("page_size")]
public int PageSize { get; set; } = 10;
}
/// <summary>
/// 分页响应基类
/// 分页响应基类 (兼容PHP API格式)
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
public class PageResponse<T>
@ -25,27 +29,32 @@ public class PageResponse<T>
/// <summary>
/// 数据列表
/// </summary>
public List<T> List { get; set; } = new();
[JsonPropertyName("data")]
public List<T> Data { get; set; } = new();
/// <summary>
/// 最后一页页码 (兼容PHP API)
/// </summary>
[JsonPropertyName("last_page")]
public int LastPage { get; set; }
/// <summary>
/// 总数量
/// </summary>
[JsonPropertyName("total")]
public int Total { get; set; }
/// <summary>
/// 当前页码
/// </summary>
[JsonPropertyName("page")]
public int Page { get; set; }
/// <summary>
/// 每页数量
/// </summary>
[JsonPropertyName("page_size")]
public int PageSize { get; set; }
/// <summary>
/// 总页数
/// </summary>
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)Total / PageSize) : 0;
}
/// <summary>
@ -56,6 +65,7 @@ public class IdRequest
/// <summary>
/// ID
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
}
@ -67,5 +77,6 @@ public class IdsRequest
/// <summary>
/// ID列表
/// </summary>
[JsonPropertyName("ids")]
public List<int> Ids { get; set; } = new();
}

View File

@ -1,6 +1,9 @@
using System.Text.Json.Serialization;
using HoneyBox.Model.Models;
namespace HoneyBox.Model.Models.Goods;
using HoneyBox.Model.Models;
#region Request Models
/// <summary>
/// 商品列表请求
@ -13,110 +16,903 @@ public class GoodsListRequest : PageRequest
public int? CategoryId { get; set; }
/// <summary>
/// 商品类型
/// 商品类型 (-1表示全部)
/// </summary>
public byte? Type { get; set; }
[JsonPropertyName("type")]
public int Type { get; set; } = -1;
}
/// <summary>
/// 商品列表项响应
/// 商品详情请求
/// </summary>
public class GoodsListItemResponse
public class GoodsDetailRequest
{
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
/// <summary>
/// 箱号 (0表示自动选择)
/// </summary>
[JsonPropertyName("goods_num")]
public int GoodsNum { get; set; } = 0;
}
/// <summary>
/// 商品子奖品请求
/// </summary>
public class GoodsChildrenRequest
{
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
/// <summary>
/// 箱号
/// </summary>
[JsonPropertyName("goods_num")]
public int GoodsNum { get; set; }
/// <summary>
/// 奖品列表ID
/// </summary>
[JsonPropertyName("goods_list_id")]
public int GoodsListId { get; set; }
}
/// <summary>
/// 商品扩展配置请求
/// </summary>
public class GoodsExtendRequest
{
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
/// <summary>
/// 商品类型
/// </summary>
[JsonPropertyName("goods_type")]
public int GoodsType { get; set; }
}
/// <summary>
/// 箱号列表请求
/// </summary>
public class BoxListRequest
{
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
}
/// <summary>
/// 箱号详情请求
/// </summary>
public class BoxDetailRequest
{
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
/// <summary>
/// 页码 (每页10个箱号)
/// </summary>
[JsonPropertyName("page_no")]
public int PageNo { get; set; } = 0;
/// <summary>
/// 排序方式: 0=箱号升序, 1=箱号降序, 2=余量降序
/// </summary>
[JsonPropertyName("sort")]
public int Sort { get; set; } = 0;
}
/// <summary>
/// 奖品数量统计请求
/// </summary>
public class PrizeCountRequest
{
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
}
/// <summary>
/// 奖品内容请求
/// </summary>
public class PrizeContentRequest
{
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
/// <summary>
/// 箱号
/// </summary>
[JsonPropertyName("num")]
public int Num { get; set; }
}
/// <summary>
/// 中奖记录请求
/// </summary>
public class PrizeLogsRequest : PageRequest
{
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
/// <summary>
/// 箱号
/// </summary>
[JsonPropertyName("goods_num")]
public int GoodsNum { get; set; }
/// <summary>
/// 赏品分类ID (0表示全部)
/// </summary>
[JsonPropertyName("shang_id")]
public int ShangId { get; set; } = 0;
}
#endregion
#region Response/DTO Models
/// <summary>
/// 商品列表项DTO (兼容PHP API snake_case格式)
/// </summary>
public class GoodsListDto
{
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 商品标题
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
/// <summary>
/// 商品图片URL
/// </summary>
[JsonPropertyName("img_url")]
public string ImgUrl { get; set; } = string.Empty;
/// <summary>
/// 商品价格
/// </summary>
public decimal Price { get; set; }
[JsonPropertyName("price")]
public string Price { get; set; } = "0";
/// <summary>
/// 商品类型
/// </summary>
[JsonPropertyName("type")]
public int Type { get; set; }
/// <summary>
/// 类型文字
/// </summary>
[JsonPropertyName("type_text")]
public string TypeText { get; set; } = string.Empty;
/// <summary>
/// 总库存
/// </summary>
[JsonPropertyName("stock")]
public int Stock { get; set; }
/// <summary>
/// 已售库存
/// </summary>
[JsonPropertyName("sale_stock")]
public int SaleStock { get; set; }
/// <summary>
/// 状态
/// </summary>
[JsonPropertyName("status")]
public int Status { get; set; }
/// <summary>
/// 是否锁定
/// </summary>
[JsonPropertyName("lock_is")]
public int LockIs { get; set; }
/// <summary>
/// 是否首折
/// </summary>
[JsonPropertyName("is_shou_zhe")]
public int IsShouZhe { get; set; }
/// <summary>
/// 是否新品
/// </summary>
[JsonPropertyName("new_is")]
public int NewIs { get; set; }
/// <summary>
/// 参与次数
/// </summary>
[JsonPropertyName("join_count")]
public int JoinCount { get; set; }
/// <summary>
/// 需要抽奖次数
/// </summary>
[JsonPropertyName("need_draw_num")]
public int NeedDrawNum { get; set; }
/// <summary>
/// 显示价格
/// </summary>
[JsonPropertyName("show_price")]
public string? ShowPrice { get; set; }
/// <summary>
/// 商品类型
/// 角标文字
/// </summary>
public byte Type { get; set; }
[JsonPropertyName("corner_text")]
public string? CornerText { get; set; }
}
/// <summary>
/// 商品信息DTO
/// </summary>
public class GoodsInfoDto
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("imgurl_detail")]
public string ImgUrlDetail { get; set; } = string.Empty;
[JsonPropertyName("price")]
public string Price { get; set; } = "0";
[JsonPropertyName("stock")]
public int Stock { get; set; }
[JsonPropertyName("sale_stock")]
public int SaleStock { get; set; }
[JsonPropertyName("surplus_stock")]
public int SurplusStock { get; set; }
[JsonPropertyName("goodslist_stock")]
public int GoodslistStock { get; set; }
[JsonPropertyName("goodslist_surplus_stock")]
public int GoodslistSurplusStock { get; set; }
[JsonPropertyName("type")]
public int Type { get; set; }
[JsonPropertyName("type_text")]
public string? TypeText { get; set; }
[JsonPropertyName("status")]
public int Status { get; set; }
[JsonPropertyName("lock_is")]
public int LockIs { get; set; }
[JsonPropertyName("num")]
public int Num { get; set; }
[JsonPropertyName("addtime")]
public string? Addtime { get; set; }
[JsonPropertyName("goods_describe")]
public string? GoodsDescribe { get; set; }
[JsonPropertyName("coupon_is")]
public int CouponIs { get; set; }
[JsonPropertyName("coupon_pro")]
public int CouponPro { get; set; }
[JsonPropertyName("integral_is")]
public int IntegralIs { get; set; }
[JsonPropertyName("rage_is")]
public int RageIs { get; set; }
[JsonPropertyName("rage")]
public int Rage { get; set; }
[JsonPropertyName("lingzhu_is")]
public int LingzhuIs { get; set; }
[JsonPropertyName("lingzhu_fan")]
public int LingzhuFan { get; set; }
[JsonPropertyName("card_num")]
public int CardNum { get; set; }
[JsonPropertyName("is_shou_zhe")]
public int IsShouZhe { get; set; }
[JsonPropertyName("new_is")]
public int NewIs { get; set; }
[JsonPropertyName("collection_is")]
public int CollectionIs { get; set; }
[JsonPropertyName("three_time")]
public int ThreeTime { get; set; }
[JsonPropertyName("five_time")]
public int FiveTime { get; set; }
[JsonPropertyName("quanju_xiangou")]
public int QuanjuXiangou { get; set; }
[JsonPropertyName("daily_xiangou")]
public int DailyXiangou { get; set; }
}
/// <summary>
/// 商品详情响应
/// </summary>
public class GoodsDetailResponse
public class GoodsDetailResponseDto
{
/// <summary>
/// 商品ID
/// 商品信息
/// </summary>
public int Id { get; set; }
[JsonPropertyName("goods")]
public GoodsInfoDto Goods { get; set; } = new();
/// <summary>
/// 商品标题
/// 锁箱信息
/// </summary>
public string Title { get; set; } = string.Empty;
[JsonPropertyName("lock_info")]
public LockInfoDto LockInfo { get; set; } = new();
/// <summary>
/// 商品图片URL
/// 参与用户头像列表
/// </summary>
public string ImgUrl { get; set; } = string.Empty;
[JsonPropertyName("join_user")]
public List<string> JoinUser { get; set; } = new();
/// <summary>
/// 商品详情图片URL
/// 参与次数
/// </summary>
public string ImgUrlDetail { get; set; } = string.Empty;
/// <summary>
/// 商品价格
/// </summary>
public decimal Price { get; set; }
/// <summary>
/// 商品描述
/// </summary>
public string? GoodsDescribe { get; set; }
[JsonPropertyName("join_count")]
public int JoinCount { get; set; }
/// <summary>
/// 奖品列表
/// </summary>
public List<GoodsPrizeItemResponse> Prizes { get; set; } = new();
[JsonPropertyName("goodslist")]
public List<GoodsListItemDto> GoodsList { get; set; } = new();
/// <summary>
/// 限购信息
/// </summary>
[JsonPropertyName("limit_info")]
public LimitInfoDto LimitInfo { get; set; } = new();
}
/// <summary>
/// 商品奖品项响应
/// 锁箱信息DTO
/// </summary>
public class GoodsPrizeItemResponse
public class LockInfoDto
{
/// <summary>
/// 奖品ID
/// 是否锁定
/// </summary>
[JsonPropertyName("lock_is")]
public int LockIs { get; set; }
/// <summary>
/// 锁定用户昵称
/// </summary>
[JsonPropertyName("goods_lock_user_nickname")]
public string? GoodsLockUserNickname { get; set; }
/// <summary>
/// 锁定用户头像
/// </summary>
[JsonPropertyName("goods_lock_user_headimg")]
public string? GoodsLockUserHeadimg { get; set; }
/// <summary>
/// 锁箱剩余时间 (Unix时间戳)
/// </summary>
[JsonPropertyName("goods_lock_surplus_time")]
public long GoodsLockSurplusTime { get; set; }
}
/// <summary>
/// 限购信息DTO
/// </summary>
public class LimitInfoDto
{
/// <summary>
/// 每日限购数量
/// </summary>
[JsonPropertyName("daily_xiangou")]
public int DailyXiangou { get; set; }
/// <summary>
/// 今日已购数量
/// </summary>
[JsonPropertyName("daily_bought")]
public int DailyBought { get; set; }
/// <summary>
/// 今日剩余可购数量
/// </summary>
[JsonPropertyName("daily_remaining")]
public int DailyRemaining { get; set; }
/// <summary>
/// 全局限购数量
/// </summary>
[JsonPropertyName("quanju_xiangou")]
public int QuanjuXiangou { get; set; }
/// <summary>
/// 全局已购数量
/// </summary>
[JsonPropertyName("global_bought")]
public int GlobalBought { get; set; }
/// <summary>
/// 全局剩余可购数量
/// </summary>
[JsonPropertyName("global_remaining")]
public int GlobalRemaining { get; set; }
}
/// <summary>
/// 奖品列表项DTO
/// </summary>
public class GoodsListItemDto
{
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 奖品名称
/// </summary>
public string Name { get; set; } = string.Empty;
[JsonPropertyName("shang_id")]
public int ShangId { get; set; }
/// <summary>
/// 奖品图片
/// </summary>
[JsonPropertyName("shang_info")]
public ShangInfoDto? ShangInfo { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("stock")]
public int Stock { get; set; }
[JsonPropertyName("surplus_stock")]
public int SurplusStock { get; set; }
[JsonPropertyName("img_url")]
public string ImgUrl { get; set; } = string.Empty;
/// <summary>
/// 奖品等级
/// </summary>
public int Level { get; set; }
[JsonPropertyName("goods_type")]
public int GoodsType { get; set; }
[JsonPropertyName("price")]
public string Price { get; set; } = "0";
[JsonPropertyName("sc_money")]
public string ScMoney { get; set; } = "0";
[JsonPropertyName("sale_time")]
public string? SaleTime { get; set; }
[JsonPropertyName("pro")]
public string Pro { get; set; } = "0";
[JsonPropertyName("children")]
public bool Children { get; set; }
[JsonPropertyName("real_pro")]
public string RealPro { get; set; } = "0";
}
/// <summary>
/// 赏品等级信息DTO
/// </summary>
public class ShangInfoDto
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("img_url")]
public string? ImgUrl { get; set; }
[JsonPropertyName("color")]
public string? Color { get; set; }
[JsonPropertyName("special_img_url")]
public string? SpecialImgUrl { get; set; }
}
/// <summary>
/// 子奖品DTO
/// </summary>
public class GoodsChildrenDto
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("shang_id")]
public int ShangId { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("imgurl")]
public string ImgUrl { get; set; } = string.Empty;
[JsonPropertyName("stock")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int Stock { get; set; }
[JsonPropertyName("surplus_stock")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int SurplusStock { get; set; }
[JsonPropertyName("goods_type")]
public int GoodsType { get; set; }
[JsonPropertyName("sale_time")]
public string? SaleTime { get; set; }
[JsonPropertyName("price")]
public string Price { get; set; } = "0";
[JsonPropertyName("sc_money")]
public string ScMoney { get; set; } = "0";
[JsonPropertyName("real_pro")]
public string RealPro { get; set; } = "0";
[JsonPropertyName("pro")]
public string Pro { get; set; } = "0";
[JsonPropertyName("pro_num")]
public string ProNum { get; set; } = "0";
[JsonPropertyName("shang_info")]
public ShangInfoDto? ShangInfo { get; set; }
}
/// <summary>
/// 商品扩展配置DTO
/// </summary>
public class GoodsExtendDto
{
[JsonPropertyName("pay_wechat")]
public int PayWechat { get; set; }
[JsonPropertyName("pay_balance")]
public int PayBalance { get; set; }
[JsonPropertyName("pay_currency")]
public int PayCurrency { get; set; }
[JsonPropertyName("pay_currency2")]
public int PayCurrency2 { get; set; }
[JsonPropertyName("pay_coupon")]
public int PayCoupon { get; set; }
[JsonPropertyName("is_deduction")]
public int IsDeduction { get; set; }
}
/// <summary>
/// 箱号分组DTO
/// </summary>
public class BoxGroupDto
{
/// <summary>
/// 显示标题,如 "1-10"
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
/// <summary>
/// 页码从0开始
/// </summary>
[JsonPropertyName("page_no")]
public int PageNo { get; set; }
}
/// <summary>
/// 箱号详情DTO
/// </summary>
public class BoxDetailDto
{
[JsonPropertyName("num")]
public int Num { get; set; }
[JsonPropertyName("surplus_all_stock")]
public int SurplusAllStock { get; set; }
[JsonPropertyName("goodslist")]
public List<BoxGoodsListDto> GoodsList { get; set; } = new();
}
/// <summary>
/// 箱号商品列表DTO
/// </summary>
public class BoxGoodsListDto
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("shang_id")]
public int ShangId { get; set; }
[JsonPropertyName("shang_info")]
public ShangInfoDto? ShangInfo { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("surplus_stock")]
public int SurplusStock { get; set; }
[JsonPropertyName("stock")]
public int Stock { get; set; }
}
#endregion
#region Collection Models
/// <summary>
/// 收藏请求
/// </summary>
public class CollectionRequest
{
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
/// <summary>
/// 箱号
/// </summary>
[JsonPropertyName("goods_num")]
public int GoodsNum { get; set; }
/// <summary>
/// 操作类型: add=收藏, remove=取消收藏
/// </summary>
[JsonPropertyName("action")]
public string Action { get; set; } = "add";
}
/// <summary>
/// 收藏列表请求
/// </summary>
public class CollectionListRequest : PageRequest
{
/// <summary>
/// 商品类型 (0=全部)
/// </summary>
[JsonPropertyName("type")]
public int Type { get; set; } = 0;
}
/// <summary>
/// 收藏列表响应 (兼容PHP API格式)
/// </summary>
public class CollectionListResponse
{
[JsonPropertyName("data")]
public List<CollectionDto> Data { get; set; } = new();
[JsonPropertyName("last_page")]
public int LastPage { get; set; }
}
/// <summary>
/// 收藏DTO (兼容PHP API格式)
/// </summary>
public class CollectionDto
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
[JsonPropertyName("type")]
public int Type { get; set; }
[JsonPropertyName("num")]
public int Num { get; set; }
[JsonPropertyName("goods_title")]
public string GoodsTitle { get; set; } = string.Empty;
[JsonPropertyName("goods_price")]
public string GoodsPrice { get; set; } = "0";
[JsonPropertyName("imgurl")]
public string ImgUrl { get; set; } = string.Empty;
[JsonPropertyName("stock")]
public int Stock { get; set; }
[JsonPropertyName("surplus_stock")]
public int SurplusStock { get; set; }
}
/// <summary>
/// 删除收藏请求
/// </summary>
public class DeleteCollectionRequest
{
/// <summary>
/// 收藏记录ID
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
}
#endregion
#region Prize Statistics Models
/// <summary>
/// 奖品数量统计响应
/// </summary>
public class PrizeCountResponseDto
{
[JsonPropertyName("list")]
public List<PrizeCountItemDto> List { get; set; } = new();
[JsonPropertyName("total")]
public int Total { get; set; }
[JsonPropertyName("surplus")]
public int Surplus { get; set; }
}
/// <summary>
/// 奖品数量统计项DTO
/// </summary>
public class PrizeCountItemDto
{
[JsonPropertyName("shang_id")]
public int ShangId { get; set; }
[JsonPropertyName("shang_info")]
public ShangInfoDto? ShangInfo { get; set; }
[JsonPropertyName("total")]
public int Total { get; set; }
[JsonPropertyName("surplus")]
public int Surplus { get; set; }
}
/// <summary>
/// 奖品内容响应
/// </summary>
public class PrizeContentResponseDto
{
[JsonPropertyName("list")]
public List<GoodsListItemDto> List { get; set; } = new();
}
#endregion
#region Prize Logs Models
/// <summary>
/// 中奖记录响应
/// </summary>
public class PrizeLogsResponseDto
{
[JsonPropertyName("category")]
public List<CategoryDto> Category { get; set; } = new();
[JsonPropertyName("data")]
public List<PrizeLogDto> Data { get; set; } = new();
[JsonPropertyName("last_page")]
public int LastPage { get; set; }
[JsonPropertyName("total")]
public int Total { get; set; }
}
/// <summary>
/// 分类DTO - 用于中奖记录筛选
/// </summary>
public class CategoryDto
{
[JsonPropertyName("shang_id")]
public int ShangId { get; set; }
[JsonPropertyName("shang_title")]
public string ShangTitle { get; set; } = string.Empty;
}
/// <summary>
/// 中奖记录DTO - 匹配PHP API响应格式
/// </summary>
public class PrizeLogDto
{
[JsonPropertyName("user_id")]
public int UserId { get; set; }
[JsonPropertyName("goodslist_title")]
public string GoodslistTitle { get; set; } = string.Empty;
[JsonPropertyName("goodslist_imgurl")]
public string GoodslistImgurl { get; set; } = string.Empty;
[JsonPropertyName("shang_id")]
public int ShangId { get; set; }
[JsonPropertyName("addtime")]
public string Addtime { get; set; } = string.Empty;
[JsonPropertyName("prize_num")]
public int PrizeNum { get; set; }
[JsonPropertyName("shang_title")]
public string ShangTitle { get; set; } = string.Empty;
[JsonPropertyName("user_info")]
public PrizeLogUserDto? UserInfo { get; set; }
[JsonPropertyName("shang_color")]
public string ShangColor { get; set; } = string.Empty;
}
/// <summary>
/// 中奖记录用户信息DTO
/// </summary>
public class PrizeLogUserDto
{
[JsonPropertyName("nickname")]
public string Nickname { get; set; } = string.Empty;
[JsonPropertyName("headimg")]
public string HeadImg { get; set; } = string.Empty;
}
#endregion

View File

@ -1,5 +1,6 @@
namespace HoneyBox.Model.Models.Prize;
using System.Text.Json.Serialization;
using HoneyBox.Model.Models;
/// <summary>
@ -10,11 +11,13 @@ public class DrawPrizeRequest
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int GoodsId { get; set; }
/// <summary>
/// 抽奖次数
/// </summary>
[JsonPropertyName("times")]
public int Times { get; set; } = 1;
}
@ -26,16 +29,19 @@ public class DrawPrizeResponse
/// <summary>
/// 中奖奖品列表
/// </summary>
[JsonPropertyName("prizes")]
public List<PrizeResultItem> Prizes { get; set; } = new();
/// <summary>
/// 消耗金额
/// </summary>
[JsonPropertyName("cost_amount")]
public decimal CostAmount { get; set; }
/// <summary>
/// 剩余余额
/// </summary>
[JsonPropertyName("remaining_balance")]
public decimal RemainingBalance { get; set; }
}
@ -47,26 +53,31 @@ public class PrizeResultItem
/// <summary>
/// 奖品ID
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 奖品名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 奖品图片
/// </summary>
[JsonPropertyName("img_url")]
public string ImgUrl { get; set; } = string.Empty;
/// <summary>
/// 奖品等级
/// </summary>
[JsonPropertyName("level")]
public int Level { get; set; }
/// <summary>
/// 奖品价值
/// </summary>
[JsonPropertyName("value")]
public decimal Value { get; set; }
}
@ -78,6 +89,7 @@ public class PrizeRecordListRequest : PageRequest
/// <summary>
/// 商品ID
/// </summary>
[JsonPropertyName("goods_id")]
public int? GoodsId { get; set; }
}
@ -89,20 +101,24 @@ public class PrizeRecordResponse
/// <summary>
/// 记录ID
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 奖品名称
/// </summary>
[JsonPropertyName("prize_name")]
public string PrizeName { get; set; } = string.Empty;
/// <summary>
/// 奖品图片
/// </summary>
[JsonPropertyName("prize_img")]
public string PrizeImg { get; set; } = string.Empty;
/// <summary>
/// 中奖时间
/// </summary>
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
}

View File

@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("HoneyBox.Model")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+21c4bb5c6a0c35c345420f3a29d64fd2922e7427")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7e00d28ad48928059f0ff514ed804f2cb0626442")]
[assembly: System.Reflection.AssemblyProductAttribute("HoneyBox.Model")]
[assembly: System.Reflection.AssemblyTitleAttribute("HoneyBox.Model")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@ -0,0 +1,452 @@
using HoneyBox.Core.Interfaces;
using HoneyBox.Core.Services;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace HoneyBox.Tests.Integration;
/// <summary>
/// 收藏服务集成测试
/// 测试收藏/取消收藏流程
/// Requirements: 6.1-6.4
/// </summary>
public class CollectionServiceIntegrationTests
{
private HoneyBoxDbContext CreateInMemoryDbContext()
{
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new HoneyBoxDbContext(options);
}
private CollectionService CreateCollectionService(HoneyBoxDbContext dbContext)
{
var mockLogger = new Mock<ILogger<CollectionService>>();
var mockRedisService = new Mock<IRedisService>();
mockRedisService.Setup(x => x.DeleteAsync(It.IsAny<string>())).Returns(Task.CompletedTask);
return new CollectionService(dbContext, mockLogger.Object, mockRedisService.Object);
}
#region (Requirements 6.1-6.4)
/// <summary>
/// 测试添加收藏
/// Requirements: 6.1
/// </summary>
[Fact]
public async Task ToggleCollection_AddsCollection_WhenNotCollected()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
var goods = new Good
{
Id = 1,
Title = "测试商品",
Type = 2,
Status = 1,
ShowIs = 0,
Price = 10,
Stock = 5,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg"
};
await dbContext.Goods.AddAsync(goods);
await dbContext.SaveChangesAsync();
// Act
var result = await service.ToggleCollectionAsync(100, 1, 1);
// Assert
Assert.True(result);
var collection = await dbContext.GoodsCollections
.FirstOrDefaultAsync(c => c.UserId == 100 && c.GoodsId == 1 && c.Num == 1);
Assert.NotNull(collection);
}
/// <summary>
/// 测试取消收藏
/// Requirements: 6.2
/// </summary>
[Fact]
public async Task ToggleCollection_RemovesCollection_WhenAlreadyCollected()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
var goods = new Good
{
Id = 1,
Title = "测试商品",
Type = 2,
Status = 1,
ShowIs = 0,
Price = 10,
Stock = 5,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg"
};
await dbContext.Goods.AddAsync(goods);
// 添加已有收藏
var collection = new GoodsCollection
{
Id = 1,
UserId = 100,
GoodsId = 1,
Num = 1,
Type = 2,
CreatedAt = DateTime.Now
};
await dbContext.GoodsCollections.AddAsync(collection);
await dbContext.SaveChangesAsync();
// Act - 再次调用应取消收藏
var result = await service.ToggleCollectionAsync(100, 1, 1);
// Assert
Assert.True(result);
var exists = await dbContext.GoodsCollections
.AnyAsync(c => c.UserId == 100 && c.GoodsId == 1 && c.Num == 1);
Assert.False(exists);
}
/// <summary>
/// 测试收藏往返一致性 - 添加后再取消
/// Requirements: 6.1, 6.2
/// </summary>
[Fact]
public async Task ToggleCollection_RoundTrip_AddsAndRemoves()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
var goods = new Good
{
Id = 1,
Title = "测试商品",
Type = 2,
Status = 1,
ShowIs = 0,
Price = 10,
Stock = 5,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg"
};
await dbContext.Goods.AddAsync(goods);
await dbContext.SaveChangesAsync();
// Act - 第一次调用:添加收藏
await service.ToggleCollectionAsync(100, 1, 1);
var afterAdd = await service.IsCollectedAsync(100, 1, 1);
// Act - 第二次调用:取消收藏
await service.ToggleCollectionAsync(100, 1, 1);
var afterRemove = await service.IsCollectedAsync(100, 1, 1);
// Assert
Assert.True(afterAdd);
Assert.False(afterRemove);
}
/// <summary>
/// 测试收藏列表查询
/// Requirements: 6.4
/// </summary>
[Fact]
public async Task GetCollectionList_ReturnsUserCollections()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
// 添加商品
var goods = new List<Good>
{
new() { Id = 1, Title = "商品1", Type = 2, Status = 1, ShowIs = 0, Price = 10, Stock = 5, ImgUrl = "img1.jpg", ImgUrlDetail = "detail1.jpg" },
new() { Id = 2, Title = "商品2", Type = 2, Status = 1, ShowIs = 0, Price = 20, Stock = 10, ImgUrl = "img2.jpg", ImgUrlDetail = "detail2.jpg" }
};
await dbContext.Goods.AddRangeAsync(goods);
// 添加收藏
var collections = new List<GoodsCollection>
{
new() { Id = 1, UserId = 100, GoodsId = 1, Num = 1, Type = 2, CreatedAt = DateTime.Now },
new() { Id = 2, UserId = 100, GoodsId = 2, Num = 1, Type = 2, CreatedAt = DateTime.Now }
};
await dbContext.GoodsCollections.AddRangeAsync(collections);
await dbContext.SaveChangesAsync();
// Act
var result = await service.GetCollectionListAsync(100, 0, 1, 10);
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Data.Count);
}
/// <summary>
/// 测试收藏列表查询 - 按类型过滤
/// Requirements: 6.4
/// </summary>
[Fact]
public async Task GetCollectionList_FiltersByType()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
// 添加商品
var goods = new List<Good>
{
new() { Id = 1, Title = "商品1", Type = 2, Status = 1, ShowIs = 0, Price = 10, Stock = 5, ImgUrl = "img1.jpg", ImgUrlDetail = "detail1.jpg" },
new() { Id = 2, Title = "商品2", Type = 6, Status = 1, ShowIs = 0, Price = 20, Stock = 10, ImgUrl = "img2.jpg", ImgUrlDetail = "detail2.jpg" }
};
await dbContext.Goods.AddRangeAsync(goods);
// 添加收藏
var collections = new List<GoodsCollection>
{
new() { Id = 1, UserId = 100, GoodsId = 1, Num = 1, Type = 2, CreatedAt = DateTime.Now },
new() { Id = 2, UserId = 100, GoodsId = 2, Num = 1, Type = 6, CreatedAt = DateTime.Now }
};
await dbContext.GoodsCollections.AddRangeAsync(collections);
await dbContext.SaveChangesAsync();
// Act - 只查询类型2的收藏
var result = await service.GetCollectionListAsync(100, 2, 1, 10);
// Assert
Assert.Single(result.Data);
Assert.Equal(2, result.Data[0].Type);
}
/// <summary>
/// 测试收藏列表查询 - 分页
/// Requirements: 6.4
/// </summary>
[Fact]
public async Task GetCollectionList_PaginationWorksCorrectly()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
// 添加15个商品和收藏
var goods = Enumerable.Range(1, 15).Select(i => new Good
{
Id = i,
Title = $"商品{i}",
Type = 2,
Status = 1,
ShowIs = 0,
Price = i * 10,
Stock = 10,
ImgUrl = $"img{i}.jpg",
ImgUrlDetail = $"detail{i}.jpg"
}).ToList();
await dbContext.Goods.AddRangeAsync(goods);
var collections = Enumerable.Range(1, 15).Select(i => new GoodsCollection
{
Id = i,
UserId = 100,
GoodsId = i,
Num = 1,
Type = 2,
CreatedAt = DateTime.Now.AddMinutes(-i)
}).ToList();
await dbContext.GoodsCollections.AddRangeAsync(collections);
await dbContext.SaveChangesAsync();
// Act
var page1 = await service.GetCollectionListAsync(100, 0, 1, 10);
var page2 = await service.GetCollectionListAsync(100, 0, 2, 10);
// Assert
Assert.Equal(2, page1.LastPage);
Assert.Equal(10, page1.Data.Count);
Assert.Equal(5, page2.Data.Count);
}
/// <summary>
/// 测试检查收藏状态
/// Requirements: 6.1
/// </summary>
[Fact]
public async Task IsCollected_ReturnsCorrectStatus()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
var collection = new GoodsCollection
{
Id = 1,
UserId = 100,
GoodsId = 1,
Num = 1,
Type = 2,
CreatedAt = DateTime.Now
};
await dbContext.GoodsCollections.AddAsync(collection);
await dbContext.SaveChangesAsync();
// Act
var isCollected = await service.IsCollectedAsync(100, 1, 1);
var isNotCollected = await service.IsCollectedAsync(100, 2, 1);
// Assert
Assert.True(isCollected);
Assert.False(isNotCollected);
}
/// <summary>
/// 测试删除收藏
/// Requirements: 6.2
/// </summary>
[Fact]
public async Task DeleteCollection_RemovesCollectionById()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
var collection = new GoodsCollection
{
Id = 1,
UserId = 100,
GoodsId = 1,
Num = 1,
Type = 2,
CreatedAt = DateTime.Now
};
await dbContext.GoodsCollections.AddAsync(collection);
await dbContext.SaveChangesAsync();
// Act
var result = await service.DeleteCollectionAsync(100, 1);
// Assert
Assert.True(result);
var exists = await dbContext.GoodsCollections.AnyAsync(c => c.Id == 1);
Assert.False(exists);
}
/// <summary>
/// 测试删除不存在的收藏
/// Requirements: 6.2
/// </summary>
[Fact]
public async Task DeleteCollection_ThrowsException_WhenNotFound()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => service.DeleteCollectionAsync(100, 999));
}
/// <summary>
/// 测试收藏商品不存在
/// Requirements: 6.1
/// </summary>
[Fact]
public async Task ToggleCollection_ThrowsException_WhenGoodsNotFound()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => service.ToggleCollectionAsync(100, 999, 1));
}
/// <summary>
/// 测试收藏已下架商品
/// Requirements: 6.1
/// </summary>
[Fact]
public async Task ToggleCollection_ThrowsException_WhenGoodsOffline()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
var goods = new Good
{
Id = 1,
Title = "下架商品",
Type = 2,
Status = 0, // 下架
ShowIs = 0,
Price = 10,
Stock = 5,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg"
};
await dbContext.Goods.AddAsync(goods);
await dbContext.SaveChangesAsync();
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => service.ToggleCollectionAsync(100, 1, 1));
}
/// <summary>
/// 测试用户隔离 - 只返回当前用户的收藏
/// Requirements: 6.4
/// </summary>
[Fact]
public async Task GetCollectionList_OnlyReturnsCurrentUserCollections()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateCollectionService(dbContext);
// 添加商品
var goods = new Good
{
Id = 1,
Title = "商品1",
Type = 2,
Status = 1,
ShowIs = 0,
Price = 10,
Stock = 5,
ImgUrl = "img1.jpg",
ImgUrlDetail = "detail1.jpg"
};
await dbContext.Goods.AddAsync(goods);
// 添加不同用户的收藏
var collections = new List<GoodsCollection>
{
new() { Id = 1, UserId = 100, GoodsId = 1, Num = 1, Type = 2, CreatedAt = DateTime.Now },
new() { Id = 2, UserId = 200, GoodsId = 1, Num = 1, Type = 2, CreatedAt = DateTime.Now }
};
await dbContext.GoodsCollections.AddRangeAsync(collections);
await dbContext.SaveChangesAsync();
// Act
var result = await service.GetCollectionListAsync(100, 0, 1, 10);
// Assert
Assert.Single(result.Data);
Assert.Equal(1, result.Data[0].GoodsId);
}
#endregion
}

View File

@ -0,0 +1,637 @@
using HoneyBox.Core.Interfaces;
using HoneyBox.Core.Services;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using HoneyBox.Model.Models.Goods;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace HoneyBox.Tests.Integration;
/// <summary>
/// 商品服务集成测试
/// 测试完整的商品查询流程
/// Requirements: 1.1-1.6, 2.1-2.7
/// </summary>
public class GoodsServiceIntegrationTests
{
private HoneyBoxDbContext CreateInMemoryDbContext()
{
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new HoneyBoxDbContext(options);
}
private GoodsService CreateGoodsService(HoneyBoxDbContext dbContext, IGoodsCacheService? cacheService = null)
{
var mockLogger = new Mock<ILogger<GoodsService>>();
var mockCacheService = cacheService ?? CreateMockCacheService();
return new GoodsService(dbContext, mockCacheService, mockLogger.Object);
}
private IGoodsCacheService CreateMockCacheService()
{
var mock = new Mock<IGoodsCacheService>();
mock.Setup(x => x.GetJoinCountAsync(It.IsAny<int>())).ReturnsAsync(-1);
mock.Setup(x => x.SetJoinCountAsync(It.IsAny<int>(), It.IsAny<int>())).Returns(Task.CompletedTask);
return mock.Object;
}
#region (Requirements 1.1-1.6)
/// <summary>
/// 测试商品列表查询 - 返回分页商品
/// Requirements: 1.1
/// </summary>
[Fact]
public async Task GetGoodsList_WithTypeFilter_ReturnsMatchingGoods()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
// 添加商品类型
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 6, Name = "全局赏", FlName = "全局赏", CornerText = "全局赏" });
await dbContext.SaveChangesAsync();
// 添加测试商品
var goods = new List<Good>
{
new() { Id = 1, Title = "商品1", Type = 2, Status = 1, ShowIs = 0, Price = 10, Stock = 10, Sort = 1, ImgUrl = "img1.jpg", ImgUrlDetail = "detail1.jpg" },
new() { Id = 2, Title = "商品2", Type = 2, Status = 1, ShowIs = 0, Price = 20, Stock = 20, Sort = 2, ImgUrl = "img2.jpg", ImgUrlDetail = "detail2.jpg" },
new() { Id = 3, Title = "商品3", Type = 6, Status = 1, ShowIs = 0, Price = 30, Stock = 30, Sort = 3, ImgUrl = "img3.jpg", ImgUrlDetail = "detail3.jpg" }
};
await dbContext.Goods.AddRangeAsync(goods);
await dbContext.SaveChangesAsync();
// Act - 查询类型2的商品
var request = new GoodsListRequest { Type = 2, Page = 1, PageSize = 10 };
var result = await service.GetGoodsListAsync(request, 0);
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Total);
Assert.All(result.Data, g => Assert.Equal(2, g.Type));
}
/// <summary>
/// 测试商品列表查询 - type=-1返回默认类型商品
/// Requirements: 1.2
/// </summary>
[Fact]
public async Task GetGoodsList_TypeMinusOne_ReturnsDefaultTypeGoods()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
// 添加商品类型
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 6, Name = "全局赏", FlName = "全局赏", CornerText = "全局赏" });
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 8, Name = "领主赏", FlName = "领主赏", CornerText = "领主赏" });
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 16, Name = "其他", FlName = "其他", CornerText = "其他" });
await dbContext.SaveChangesAsync();
// 添加测试商品 - 默认类型: 2, 6, 8, 16
var goods = new List<Good>
{
new() { Id = 1, Title = "商品1", Type = 2, Status = 1, ShowIs = 0, Price = 10, Stock = 10, Sort = 1, ImgUrl = "img1.jpg", ImgUrlDetail = "detail1.jpg" },
new() { Id = 2, Title = "商品2", Type = 6, Status = 1, ShowIs = 0, Price = 20, Stock = 20, Sort = 2, ImgUrl = "img2.jpg", ImgUrlDetail = "detail2.jpg" },
new() { Id = 3, Title = "商品3", Type = 1, Status = 1, ShowIs = 0, Price = 30, Stock = 30, Sort = 3, ImgUrl = "img3.jpg", ImgUrlDetail = "detail3.jpg" } // 非默认类型
};
await dbContext.Goods.AddRangeAsync(goods);
await dbContext.SaveChangesAsync();
// Act - type=-1 返回默认类型
var request = new GoodsListRequest { Type = -1, Page = 1, PageSize = 10 };
var result = await service.GetGoodsListAsync(request, 0);
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Total); // 只有类型2和6的商品
Assert.All(result.Data, g => Assert.Contains(g.Type, new[] { 2, 6, 8, 16 }));
}
/// <summary>
/// 测试商品列表查询 - 过滤非上架商品
/// Requirements: 1.4
/// </summary>
[Fact]
public async Task GetGoodsList_FiltersInactiveGoods()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.SaveChangesAsync();
var goods = new List<Good>
{
new() { Id = 1, Title = "上架商品", Type = 2, Status = 1, ShowIs = 0, Price = 10, Stock = 10, Sort = 1, ImgUrl = "img1.jpg", ImgUrlDetail = "detail1.jpg" },
new() { Id = 2, Title = "下架商品", Type = 2, Status = 0, ShowIs = 0, Price = 20, Stock = 20, Sort = 2, ImgUrl = "img2.jpg", ImgUrlDetail = "detail2.jpg" }
};
await dbContext.Goods.AddRangeAsync(goods);
await dbContext.SaveChangesAsync();
// Act
var request = new GoodsListRequest { Type = 2, Page = 1, PageSize = 10 };
var result = await service.GetGoodsListAsync(request, 0);
// Assert
Assert.Single(result.Data);
Assert.Equal("上架商品", result.Data[0].Title);
}
/// <summary>
/// 测试商品列表查询 - 解锁金额过滤
/// Requirements: 1.5
/// </summary>
[Fact]
public async Task GetGoodsList_FiltersUnlockAmountForAnonymousUser()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.SaveChangesAsync();
var goods = new List<Good>
{
new() { Id = 1, Title = "无门槛商品", Type = 2, Status = 1, ShowIs = 0, Price = 10, Stock = 10, Sort = 1, UnlockAmount = 0, ImgUrl = "img1.jpg", ImgUrlDetail = "detail1.jpg" },
new() { Id = 2, Title = "有门槛商品", Type = 2, Status = 1, ShowIs = 0, Price = 20, Stock = 20, Sort = 2, UnlockAmount = 100, ImgUrl = "img2.jpg", ImgUrlDetail = "detail2.jpg" }
};
await dbContext.Goods.AddRangeAsync(goods);
await dbContext.SaveChangesAsync();
// Act - 未登录用户 (userId=0)
var request = new GoodsListRequest { Type = 2, Page = 1, PageSize = 10 };
var result = await service.GetGoodsListAsync(request, 0);
// Assert - 只能看到无门槛商品
Assert.Single(result.Data);
Assert.Equal("无门槛商品", result.Data[0].Title);
}
/// <summary>
/// 测试商品列表查询 - 排序正确性
/// Requirements: 1.6
/// </summary>
[Fact]
public async Task GetGoodsList_SortsBySortDescThenIdDesc()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.SaveChangesAsync();
var goods = new List<Good>
{
new() { Id = 1, Title = "商品1", Type = 2, Status = 1, ShowIs = 0, Price = 10, Stock = 10, Sort = 1, ImgUrl = "img1.jpg", ImgUrlDetail = "detail1.jpg" },
new() { Id = 2, Title = "商品2", Type = 2, Status = 1, ShowIs = 0, Price = 20, Stock = 20, Sort = 2, ImgUrl = "img2.jpg", ImgUrlDetail = "detail2.jpg" },
new() { Id = 3, Title = "商品3", Type = 2, Status = 1, ShowIs = 0, Price = 30, Stock = 30, Sort = 2, ImgUrl = "img3.jpg", ImgUrlDetail = "detail3.jpg" }
};
await dbContext.Goods.AddRangeAsync(goods);
await dbContext.SaveChangesAsync();
// Act
var request = new GoodsListRequest { Type = 2, Page = 1, PageSize = 10 };
var result = await service.GetGoodsListAsync(request, 0);
// Assert - 按sort DESC, id DESC排序
Assert.Equal(3, result.Data.Count);
Assert.Equal(3, result.Data[0].Id); // sort=2, id=3
Assert.Equal(2, result.Data[1].Id); // sort=2, id=2
Assert.Equal(1, result.Data[2].Id); // sort=1, id=1
}
/// <summary>
/// 测试商品列表查询 - 分页功能
/// Requirements: 1.1
/// </summary>
[Fact]
public async Task GetGoodsList_PaginationWorksCorrectly()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.SaveChangesAsync();
// 添加15条商品
var goods = Enumerable.Range(1, 15).Select(i => new Good
{
Id = i,
Title = $"商品{i}",
Type = 2,
Status = 1,
ShowIs = 0,
Price = i * 10,
Stock = 10,
Sort = i,
ImgUrl = $"img{i}.jpg",
ImgUrlDetail = $"detail{i}.jpg"
}).ToList();
await dbContext.Goods.AddRangeAsync(goods);
await dbContext.SaveChangesAsync();
// Act - 第一页
var page1Request = new GoodsListRequest { Type = 2, Page = 1, PageSize = 10 };
var page1 = await service.GetGoodsListAsync(page1Request, 0);
// Act - 第二页
var page2Request = new GoodsListRequest { Type = 2, Page = 2, PageSize = 10 };
var page2 = await service.GetGoodsListAsync(page2Request, 0);
// Assert
Assert.Equal(15, page1.Total);
Assert.Equal(2, page1.LastPage);
Assert.Equal(10, page1.Data.Count);
Assert.Equal(5, page2.Data.Count);
}
#endregion
#region (Requirements 2.1-2.7)
/// <summary>
/// 测试商品详情查询 - 返回完整商品信息
/// Requirements: 2.1
/// </summary>
[Fact]
public async Task GetGoodsDetail_ReturnsCompleteGoodsInfo()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
// 添加商品类型
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.SaveChangesAsync();
// 添加商品
var goods = new Good
{
Id = 1,
Title = "测试商品",
Type = 2,
Status = 1,
ShowIs = 0,
Price = 10,
Stock = 5,
SaleStock = 2,
LockIs = 0,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg",
CouponIs = 1,
CouponPro = 10,
IntegralIs = 1,
RageIs = 0,
LingzhuIs = 0,
DailyXiangou = 0,
QuanjuXiangou = 0
};
await dbContext.Goods.AddAsync(goods);
// 添加奖品等级
await dbContext.PrizeLevels.AddAsync(new PrizeLevel { Id = 10, Title = "A赏", Color = "#FF0000" });
await dbContext.SaveChangesAsync();
// 添加奖品
var goodsItem = new GoodsItem
{
Id = 1,
GoodsId = 1,
Num = 1,
Title = "奖品1",
Stock = 10,
SurplusStock = 8,
Price = 100,
ScMoney = 50,
ShangId = 10,
GoodsListId = 0,
ImgUrl = "prize.jpg",
Sort = 1
};
await dbContext.GoodsItems.AddAsync(goodsItem);
await dbContext.SaveChangesAsync();
// Act
var result = await service.GetGoodsDetailAsync(1, 1, 0);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Goods);
Assert.Equal(1, result.Goods.Id);
Assert.Equal("测试商品", result.Goods.Title);
Assert.Equal(2, result.Goods.Type);
Assert.NotNull(result.LockInfo);
Assert.NotNull(result.GoodsList);
Assert.NotNull(result.LimitInfo);
}
/// <summary>
/// 测试商品详情查询 - 自动选择箱号
/// Requirements: 2.2
/// </summary>
[Fact]
public async Task GetGoodsDetail_AutoSelectsBoxNumber_WhenGoodsNumIsZero()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.SaveChangesAsync();
var goods = new Good
{
Id = 1,
Title = "测试商品",
Type = 2,
Status = 1,
ShowIs = 0,
Price = 10,
Stock = 3,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg"
};
await dbContext.Goods.AddAsync(goods);
// 添加奖品 - 箱号1无库存箱号2有库存
var goodsItems = new List<GoodsItem>
{
new() { Id = 1, GoodsId = 1, Num = 1, Title = "奖品1", Stock = 10, SurplusStock = 0, ShangId = 10, GoodsListId = 0, ImgUrl = "p1.jpg" },
new() { Id = 2, GoodsId = 1, Num = 2, Title = "奖品2", Stock = 10, SurplusStock = 5, ShangId = 10, GoodsListId = 0, ImgUrl = "p2.jpg" }
};
await dbContext.GoodsItems.AddRangeAsync(goodsItems);
await dbContext.PrizeLevels.AddAsync(new PrizeLevel { Id = 10, Title = "A赏" });
await dbContext.SaveChangesAsync();
// Act - goodsNum=0 应自动选择有库存的箱号
var result = await service.GetGoodsDetailAsync(1, 0, 0);
// Assert - 应选择箱号2有库存
Assert.Equal(2, result.Goods.Num);
}
/// <summary>
/// 测试商品详情查询 - 概率计算
/// Requirements: 2.3
/// </summary>
[Fact]
public async Task GetGoodsDetail_CalculatesProbabilityCorrectly()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.SaveChangesAsync();
var goods = new Good
{
Id = 1,
Title = "测试商品",
Type = 2,
Status = 1,
ShowIs = 0,
Price = 10,
Stock = 1,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg"
};
await dbContext.Goods.AddAsync(goods);
// 添加奖品等级
await dbContext.PrizeLevels.AddAsync(new PrizeLevel { Id = 10, Title = "A赏" });
await dbContext.PrizeLevels.AddAsync(new PrizeLevel { Id = 11, Title = "B赏" });
await dbContext.SaveChangesAsync();
// 添加奖品 - 总剩余库存=10, A赏剩余2, B赏剩余8
var goodsItems = new List<GoodsItem>
{
new() { Id = 1, GoodsId = 1, Num = 1, Title = "A赏奖品", Stock = 5, SurplusStock = 2, ShangId = 10, GoodsListId = 0, ImgUrl = "a.jpg", Sort = 2 },
new() { Id = 2, GoodsId = 1, Num = 1, Title = "B赏奖品", Stock = 10, SurplusStock = 8, ShangId = 11, GoodsListId = 0, ImgUrl = "b.jpg", Sort = 1 }
};
await dbContext.GoodsItems.AddRangeAsync(goodsItems);
await dbContext.SaveChangesAsync();
// Act
var result = await service.GetGoodsDetailAsync(1, 1, 0);
// Assert - 概率计算: A赏=2/10*100=20%, B赏=8/10*100=80%
Assert.Equal(2, result.GoodsList.Count);
var aItem = result.GoodsList.First(x => x.ShangId == 10);
var bItem = result.GoodsList.First(x => x.ShangId == 11);
Assert.Contains("20", aItem.Pro);
Assert.Contains("80", bItem.Pro);
}
/// <summary>
/// 测试商品详情查询 - 锁箱信息
/// Requirements: 2.4
/// </summary>
[Fact]
public async Task GetGoodsDetail_ReturnsLockInfo()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.SaveChangesAsync();
var goods = new Good
{
Id = 1,
Title = "测试商品",
Type = 2,
Status = 1,
ShowIs = 0,
Price = 10,
Stock = 1,
LockIs = 1, // 支持锁箱
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg"
};
await dbContext.Goods.AddAsync(goods);
// 添加锁箱记录
var futureTime = DateTimeOffset.UtcNow.AddMinutes(30).ToUnixTimeSeconds();
var goodsLock = new GoodsLock
{
Id = 1,
GoodsIdNum = "1_1",
UserId = 100,
EndTime = futureTime
};
await dbContext.GoodsLocks.AddAsync(goodsLock);
// 添加锁箱用户
var user = new User
{
Id = 100,
OpenId = "test_openid_100",
Uid = "test_uid_100",
Nickname = "锁箱用户",
HeadImg = "avatar.jpg"
};
await dbContext.Users.AddAsync(user);
await dbContext.SaveChangesAsync();
// Act
var result = await service.GetGoodsDetailAsync(1, 1, 0);
// Assert
Assert.NotNull(result.LockInfo);
Assert.Equal(1, result.LockInfo.LockIs);
Assert.Equal("锁箱用户", result.LockInfo.GoodsLockUserNickname);
Assert.True(result.LockInfo.GoodsLockSurplusTime > 0);
}
/// <summary>
/// 测试商品详情查询 - 收藏状态
/// Requirements: 2.6
/// </summary>
[Fact]
public async Task GetGoodsDetail_ReturnsCollectionStatus()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.SaveChangesAsync();
var goods = new Good
{
Id = 1,
Title = "测试商品",
Type = 2,
Status = 1,
ShowIs = 0,
Price = 10,
Stock = 1,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg"
};
await dbContext.Goods.AddAsync(goods);
// 添加收藏记录
var collection = new GoodsCollection
{
Id = 1,
UserId = 100,
GoodsId = 1,
Num = 1,
Type = 2
};
await dbContext.GoodsCollections.AddAsync(collection);
await dbContext.SaveChangesAsync();
// Act - 已收藏用户
var result = await service.GetGoodsDetailAsync(1, 1, 100);
// Assert
Assert.Equal(1, result.Goods.CollectionIs);
}
/// <summary>
/// 测试商品详情查询 - 限购信息
/// Requirements: 2.7
/// </summary>
[Fact]
public async Task GetGoodsDetail_ReturnsLimitInfo()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
await dbContext.GoodsTypes.AddAsync(new GoodsType { Value = 2, Name = "无限赏", FlName = "无限赏", CornerText = "无限赏" });
await dbContext.SaveChangesAsync();
var goods = new Good
{
Id = 1,
Title = "测试商品",
Type = 2,
Status = 1,
ShowIs = 0,
Price = 10,
Stock = 1,
DailyXiangou = 5,
QuanjuXiangou = 10,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg"
};
await dbContext.Goods.AddAsync(goods);
await dbContext.SaveChangesAsync();
// Act
var result = await service.GetGoodsDetailAsync(1, 1, 0);
// Assert
Assert.NotNull(result.LimitInfo);
Assert.Equal(5, result.LimitInfo.DailyXiangou);
Assert.Equal(10, result.LimitInfo.QuanjuXiangou);
}
/// <summary>
/// 测试商品详情查询 - 商品不存在
/// Requirements: 2.1
/// </summary>
[Fact]
public async Task GetGoodsDetail_ThrowsException_WhenGoodsNotFound()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => service.GetGoodsDetailAsync(999, 1, 0));
}
/// <summary>
/// 测试商品详情查询 - 商品已下架
/// Requirements: 2.1
/// </summary>
[Fact]
public async Task GetGoodsDetail_ThrowsException_WhenGoodsOffline()
{
// Arrange
var dbContext = CreateInMemoryDbContext();
var service = CreateGoodsService(dbContext);
var goods = new Good
{
Id = 1,
Title = "下架商品",
Type = 2,
Status = 0, // 下架
ShowIs = 0,
Price = 10,
Stock = 1,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg"
};
await dbContext.Goods.AddAsync(goods);
await dbContext.SaveChangesAsync();
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => service.GetGoodsDetailAsync(1, 1, 0));
}
#endregion
}

View File

@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("HoneyBox.Tests")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+21c4bb5c6a0c35c345420f3a29d64fd2922e7427")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7e00d28ad48928059f0ff514ed804f2cb0626442")]
[assembly: System.Reflection.AssemblyProductAttribute("HoneyBox.Tests")]
[assembly: System.Reflection.AssemblyTitleAttribute("HoneyBox.Tests")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]