587 lines
17 KiB
Markdown
587 lines
17 KiB
Markdown
# 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. **端到端测试**
|
||
- 测试盒子列表页面的完整功能流程
|
||
- 测试盒子新增的类型条件字段逻辑
|
||
- 测试奖品管理的完整操作流程
|