HaniBlindBox/.kiro/specs/goods-management-frontend/design.md
2026-01-17 18:29:17 +08:00

587 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design Document
## Overview
本设计文档描述了HoneyBox后台管理系统商品管理模块前端迁移的技术方案。该模块将老项目PHP ThinkPHP + Layui的商品管理前端页面迁移到新项目ASP.NET Core + Vue 3 + Element Plus
主要工作包括:
1. 补充缺失的后端API接口盒子类型CRUD、盒子扩展设置、复制盒子、清空抽奖
2. 创建Vue 3前端页面和组件
3. 实现API调用层
4. 实现复杂的盒子类型条件字段逻辑
商品管理模块是系统核心模块涉及10+种盒子类型,每种类型有不同的字段配置,是本次迁移中复杂度最高的模块。
## Architecture
### 前端架构
```
admin-web/src/
├── api/
│ └── business/
│ └── goods.ts # 商品管理API
├── views/
│ └── business/
│ └── goods/
│ ├── index.vue # 盒子列表主页面
│ ├── type.vue # 盒子类型管理页面
│ ├── config/
│ │ └── typeFieldConfig.ts # 盒子类型字段配置
│ └── components/
│ ├── GoodsSearchForm.vue # 搜索表单组件
│ ├── GoodsTable.vue # 盒子列表表格
│ ├── GoodsAddDialog.vue # 盒子新增弹窗
│ ├── GoodsEditDialog.vue # 盒子编辑弹窗
│ ├── GoodsExtendDialog.vue # 盒子扩展设置弹窗
│ ├── PrizeListDialog.vue # 奖品列表弹窗
│ ├── PrizeAddDialog.vue # 奖品新增弹窗
│ ├── PrizeEditDialog.vue # 奖品编辑弹窗
│ └── TypeFormDialog.vue # 类型新增/编辑弹窗
└── router/
└── modules/
└── business.ts # 业务模块路由
```
### 后端架构
```
HoneyBox.Admin.Business/
├── Controllers/
│ ├── GoodsController.cs # 商品管理控制器补充API
│ ├── GoodsTypesController.cs # 盒子类型控制器补充API
│ └── PrizesController.cs # 奖品管理控制器
├── Services/
│ ├── GoodsService.cs # 商品业务服务(补充方法)
│ └── Interfaces/
│ └── IGoodsService.cs
└── Models/
└── Goods/
├── GoodsModels.cs # 商品相关模型
├── PrizeModels.cs # 奖品相关模型
├── GoodsTypeModels.cs # 盒子类型模型(新增)
└── GoodsExtendModels.cs # 盒子扩展模型(新增)
```
### 目录结构
```
src/views/business/goods/
├── index.vue # 盒子列表页面
├── type.vue # 盒子类型管理页面
├── components/
│ ├── GoodsSearchForm.vue # 盒子搜索表单
│ ├── GoodsTable.vue # 盒子列表表格
│ ├── GoodsAddDialog.vue # 盒子新增弹窗
│ ├── GoodsEditDialog.vue # 盒子编辑弹窗
│ ├── GoodsExtendDialog.vue # 盒子扩展设置弹窗
│ ├── PrizeListDialog.vue # 奖品列表弹窗
│ ├── PrizeAddDialog.vue # 奖品新增弹窗
│ ├── PrizeEditDialog.vue # 奖品编辑弹窗
│ ├── TypeAddDialog.vue # 类型新增弹窗
│ └── TypeEditDialog.vue # 类型编辑弹窗
src/api/business/
├── goods.ts # 商品API接口
```
### 组件关系图
```
index.vue (盒子列表)
├── GoodsSearchForm.vue (搜索表单)
├── GoodsTable.vue (列表表格)
│ ├── GoodsAddDialog.vue (新增弹窗)
│ ├── GoodsEditDialog.vue (编辑弹窗)
│ ├── GoodsExtendDialog.vue (扩展设置弹窗)
│ └── PrizeListDialog.vue (奖品列表弹窗)
│ ├── PrizeAddDialog.vue (奖品新增弹窗)
│ └── PrizeEditDialog.vue (奖品编辑弹窗)
type.vue (类型管理)
├── TypeAddDialog.vue (类型新增弹窗)
└── TypeEditDialog.vue (类型编辑弹窗)
```
## 数据模型
### 盒子类型枚举
```typescript
export enum GoodsType {
YiFanShang = 1, // 一番赏
WuXianShang = 2, // 无限赏
LeiTaiShang = 3, // 擂台赏
FuDai = 5, // 福袋
XingYunShang = 6, // 幸运赏
LingZhuShang = 8, // 领主赏
LianJiShang = 9, // 连击赏
MangHe = 10, // 盲盒
XingYunShangNew = 11,// 幸运赏(新)
FuLiWu = 15, // 福利屋
FanBeiShang = 16, // 翻倍赏
TeShuHeZi = 17, // 特殊盒子
}
export const GoodsTypeLabels: Record<GoodsType, string> = {
[GoodsType.YiFanShang]: '一番赏',
[GoodsType.WuXianShang]: '无限赏',
[GoodsType.LeiTaiShang]: '擂台赏',
[GoodsType.FuDai]: '福袋',
[GoodsType.XingYunShang]: '幸运赏',
[GoodsType.LingZhuShang]: '领主赏',
[GoodsType.LianJiShang]: '连击赏',
[GoodsType.MangHe]: '盲盒',
[GoodsType.XingYunShangNew]: '幸运赏(新)',
[GoodsType.FuLiWu]: '福利屋',
[GoodsType.FanBeiShang]: '翻倍赏',
[GoodsType.TeShuHeZi]: '特殊盒子',
};
```
### 盒子类型字段配置
```typescript
export interface GoodsTypeFieldConfig {
showStock: boolean; // 显示套数
showLock: boolean; // 显示锁箱配置
showDailyLimit: boolean; // 显示每日限购
showRage: boolean; // 显示怒气值
showItemCard: boolean; // 显示道具卡
showLingzhu: boolean; // 显示领主配置
showLianji: boolean; // 显示连击配置
showTimeConfig: boolean; // 显示时间配置(福利屋)
showAutoXiajia: boolean; // 显示自动下架
showCoupon: boolean; // 显示发券开关
showIntegral: boolean; // 显示发积分开关
showDescription: boolean; // 显示盒子描述
showQuanjuXiangou: boolean; // 显示限购次数
}
export const GoodsTypeFieldConfigs: Record<GoodsType, GoodsTypeFieldConfig> = {
[GoodsType.YiFanShang]: {
showStock: true, showLock: true, showDailyLimit: true,
showRage: false, showItemCard: false, showLingzhu: false,
showLianji: false, showTimeConfig: false, showAutoXiajia: true,
showCoupon: true, showIntegral: true, showDescription: false,
showQuanjuXiangou: false,
},
[GoodsType.WuXianShang]: {
showStock: false, showLock: false, showDailyLimit: false,
showRage: true, showItemCard: true, showLingzhu: false,
showLianji: false, showTimeConfig: false, showAutoXiajia: true,
showCoupon: true, showIntegral: true, showDescription: false,
showQuanjuXiangou: false,
},
// ... 其他类型配置
};
```
### 盒子数据模型
```typescript
export interface GoodsItem {
id: number;
type: GoodsType;
title: string;
price: number;
stock: number;
imgUrl: string;
imgUrlDetail: string;
status: number; // 0-下架 1-上架 2-售罄
sort: number;
isNew: boolean;
isShouZhe: boolean; // 首抽五折
lockIs: boolean; // 锁箱开关
lockTime: number; // 锁箱时间(秒)
dailyXiangou: number; // 每日限购
quanjuXiangou: number; // 限购次数
rageIs: boolean; // 怒气值开关
rage: number; // 怒气值
itemCardId: number; // 道具卡ID
couponIs: boolean; // 发券开关
integralIs: boolean; // 发积分开关
isAutoXiajia: boolean; // 自动下架开关
xiajiaLirun: number; // 下架利润值(%)
xiajiaAutoCoushu: number; // 下架抽数阈值
xiajiaJine: number; // 下架金额
unlockAmount: number; // 解锁金额
choujiangXianzhi: number; // 抽奖门槛
goodsDescribe: string; // 盒子描述
// 福利屋专用
flwStartTime: string; // 开始时间
flwEndTime: string; // 结束时间
openTime: string; // 开奖时间
// 领主赏专用
lingzhuIs: boolean;
lingzhuFan: number;
lingzhuShangId: number;
// 连击赏专用
lianJiNum: number;
lianJiShangId: number;
// 时间戳
createTime: string;
updateTime: string;
}
```
### 奖品数据模型
```typescript
export enum PrizeCategory {
XianHuo = 1, // 现货
YuShou = 2, // 预售
HuoBi = 3, // 货币
BaoXiang = 4, // 宝箱
}
export interface PrizeItem {
id: number;
goodsId: number;
title: string;
category: PrizeCategory;
level: string; // A/B/C/D/E等
price: number; // 售价
costPrice: number; // 采购价
referencePrice: number; // 参考价
quantity: number; // 数量(一番赏等)
probability: number; // 概率%(无限赏等)
giftMultiple: number; // 赠送倍率(翻倍赏)
isLingzhu: boolean; // 是否领主(领主赏)
preSaleTime: string; // 预售时间
sort: number;
imgUrl: string;
detailImgUrl: string;
giftCurrency: GiftCurrency[]; // 赠送货币配置
children?: PrizeItem[]; // 宝箱子奖品
createTime: string;
updateTime: string;
}
export interface GiftCurrency {
type: string; // 货币类型
amount: number; // 数量
}
```
## API接口设计
```typescript
// src/api/business/goods.ts
import request from '@/utils/request';
const BASE_URL = '/api/admin/business/goods';
// 盒子管理
export const goodsApi = {
// 获取盒子列表
getList: (params: GoodsListParams) =>
request.get<PageResult<GoodsItem>>(`${BASE_URL}`, { params }),
// 获取盒子详情
getDetail: (id: number) =>
request.get<GoodsItem>(`${BASE_URL}/${id}`),
// 创建盒子
create: (data: GoodsCreateRequest) =>
request.post<{ id: number }>(`${BASE_URL}`, data),
// 更新盒子
update: (id: number, data: GoodsUpdateRequest) =>
request.put(`${BASE_URL}/${id}`, data),
// 删除盒子
delete: (id: number) =>
request.delete(`${BASE_URL}/${id}`),
// 设置盒子状态
setStatus: (id: number, status: number) =>
request.put(`${BASE_URL}/${id}/status`, { status }),
// 获取盒子奖品列表
getPrizes: (goodsId: number) =>
request.get<PrizeItem[]>(`${BASE_URL}/${goodsId}/prizes`),
// 添加奖品
addPrize: (goodsId: number, data: PrizeCreateRequest) =>
request.post<{ id: number }>(`${BASE_URL}/${goodsId}/prizes`, data),
};
// 奖品管理
export const prizesApi = {
// 获取奖品详情
getDetail: (id: number) =>
request.get<PrizeItem>(`/api/admin/business/prizes/${id}`),
// 更新奖品
update: (id: number, data: PrizeUpdateRequest) =>
request.put(`/api/admin/business/prizes/${id}`, data),
// 删除奖品
delete: (id: number) =>
request.delete(`/api/admin/business/prizes/${id}`),
};
// 盒子类型管理
export const goodsTypeApi = {
// 获取类型列表
getList: () =>
request.get<GoodsTypeItem[]>(`${BASE_URL}/types`),
// 创建类型
create: (data: GoodsTypeCreateRequest) =>
request.post(`${BASE_URL}/types`, data),
// 更新类型
update: (id: number, data: GoodsTypeUpdateRequest) =>
request.put(`${BASE_URL}/types/${id}`, data),
// 删除类型
delete: (id: number) =>
request.delete(`${BASE_URL}/types/${id}`),
};
```
## 组件设计
### GoodsAddDialog 盒子新增弹窗
核心逻辑:根据盒子类型动态显示/隐藏字段
```typescript
// 计算属性:根据类型获取字段配置
const fieldConfig = computed(() => {
return GoodsTypeFieldConfigs[formData.type] || defaultFieldConfig;
});
// 监听类型变化,重置相关字段
watch(() => formData.type, (newType) => {
resetTypeSpecificFields();
});
```
### PrizeListDialog 奖品列表弹窗
核心逻辑:根据盒子类型动态显示不同列
```typescript
// 计算属性:动态列配置
const dynamicColumns = computed(() => {
const type = props.goodsType;
const columns = [...baseColumns];
// 一番赏/擂台赏/福袋等显示数量列
if ([1, 3, 5, 6, 10, 11, 15, 17].includes(type)) {
columns.push({ prop: 'quantity', label: '奖品数量' });
}
// 无限赏/翻倍赏等显示概率列
if ([2, 8, 9, 16, 17].includes(type)) {
columns.push({ prop: 'probability', label: '真实概率(%)' });
}
// 翻倍赏显示赠送倍率
if ([16, 17].includes(type)) {
columns.push({ prop: 'giftMultiple', label: '赠送倍率' });
}
return columns;
});
```
## 路由配置
```typescript
// src/router/modules/business.ts
{
path: 'goods',
name: 'BusinessGoods',
component: () => import('@/views/business/goods/index.vue'),
meta: { title: '盒子管理', icon: 'goods', permission: 'goods:list' }
},
{
path: 'goods/type',
name: 'BusinessGoodsType',
component: () => import('@/views/business/goods/type.vue'),
meta: { title: '盒子类型', icon: 'type', permission: 'goods:type' }
},
```
## 属性测试设计
### 测试用例
```csharp
// GoodsManagementFrontendPropertyTests.cs
[Property]
public Property GoodsTypeFieldConfig_ShouldBeConsistent()
{
// 验证每种盒子类型都有对应的字段配置
}
[Property]
public Property GoodsCreate_WithValidData_ShouldSucceed()
{
// 验证有效数据创建盒子成功
}
[Property]
public Property GoodsCreate_WithInvalidType_ShouldFail()
{
// 验证无效类型创建盒子失败
}
[Property]
public Property PrizeCreate_WithValidData_ShouldSucceed()
{
// 验证有效数据创建奖品成功
}
[Property]
public Property PrizeProbability_ShouldNotExceed100()
{
// 验证奖品概率总和不超过100%
}
```
## 状态管理
使用组合式API管理状态不需要Pinia store
```typescript
// 盒子列表状态
const goodsListState = reactive({
loading: false,
list: [] as GoodsItem[],
total: 0,
searchParams: {
title: '',
status: null,
type: null,
page: 1,
pageSize: 20,
},
});
// 奖品列表状态
const prizeListState = reactive({
loading: false,
list: [] as PrizeItem[],
goodsId: 0,
goodsType: 0,
});
```
## 错误处理
```typescript
// 统一错误处理
const handleApiError = (error: any, defaultMessage: string) => {
const message = error.response?.data?.message || defaultMessage;
ElMessage.error(message);
};
// 使用示例
try {
await goodsApi.create(formData);
ElMessage.success('创建成功');
} catch (error) {
handleApiError(error, '创建失败');
}
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: 盒子类型字段配置一致性
*For any* 盒子类型,前端字段配置应与后端数据模型保持一致,确保根据类型显示/隐藏的字段与数据库字段对应。
**Validates: Requirements 2.2, 2.8, 2.9, 2.10**
### Property 2: 盒子创建参数验证
*For any* 盒子创建请求,当必填字段缺失或数据格式无效时,系统应该返回错误提示而不是创建盒子。
**Validates: Requirements 2.3, 2.4**
### Property 3: 盒子状态切换一致性
*For any* 盒子状态变更操作上架操作应该将status设为1下架操作应该将status设为0且操作后列表应该显示正确的状态。
**Validates: Requirements 1.3**
### Property 4: 奖品概率总和验证
*For any* 概率类型盒子无限赏、翻倍赏等所有奖品的概率总和不应超过100%。
**Validates: Requirements 4.5**
### Property 5: 盒子扩展设置继承
*For any* 盒子扩展设置查询,当盒子没有独立扩展配置时,应该返回其所属盒子类型的默认支付配置。
**Validates: Requirements 7.1, 7.4**
### Property 6: API响应格式一致性
*For any* 后端API响应响应格式应该符合统一的ApiResponse结构{ code: number, message: string, data: T }其中code为0表示成功。
**Validates: Requirements 8.1-8.9**
## Error Handling
### 前端错误处理
1. **网络错误**: 显示"网络连接失败"提示,允许用户重试
2. **401未授权**: 自动刷新token或跳转登录页
3. **403无权限**: 显示"没有操作权限"提示
4. **业务错误**: 显示后端返回的错误消息
5. **表单验证错误**: 在表单字段下方显示验证错误信息
6. **危险操作**: 清空抽奖等危险操作需要二次确认
### 后端错误处理
1. **参数验证失败**: 返回400状态码和验证错误详情
2. **资源不存在**: 返回404状态码和"盒子不存在"消息
3. **业务规则违反**: 返回业务错误码和描述消息如概率超过100%
4. **服务器错误**: 返回500状态码和通用错误消息
## Testing Strategy
### 单元测试
1. **前端组件测试**
- 测试搜索表单组件的参数收集
- 测试盒子类型字段配置的正确性
- 测试奖品列表动态列的渲染
2. **后端服务测试**
- 测试GoodsService的各个方法
- 测试盒子类型CRUD操作
- 测试盒子扩展设置的继承逻辑
### 属性测试
使用属性测试验证以下属性:
- 盒子类型字段配置一致性
- 盒子创建参数验证
- 奖品概率总和验证
- 盒子扩展设置继承
- API响应格式一致性
### 集成测试
1. **API集成测试**
- 测试完整的API请求-响应流程
- 测试权限验证
2. **端到端测试**
- 测试盒子列表页面的完整功能流程
- 测试盒子新增的类型条件字段逻辑
- 测试奖品管理的完整操作流程