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

17 KiB
Raw Blame History

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 (类型编辑弹窗)

数据模型

盒子类型枚举

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]: '特殊盒子',
};

盒子类型字段配置

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,
  },
  // ... 其他类型配置
};

盒子数据模型

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;
}

奖品数据模型

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接口设计

// 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 盒子新增弹窗

核心逻辑:根据盒子类型动态显示/隐藏字段

// 计算属性:根据类型获取字段配置
const fieldConfig = computed(() => {
  return GoodsTypeFieldConfigs[formData.type] || defaultFieldConfig;
});

// 监听类型变化,重置相关字段
watch(() => formData.type, (newType) => {
  resetTypeSpecificFields();
});

PrizeListDialog 奖品列表弹窗

核心逻辑:根据盒子类型动态显示不同列

// 计算属性:动态列配置
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;
});

路由配置

// 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' }
},

属性测试设计

测试用例

// 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

// 盒子列表状态
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,
});

错误处理

// 统一错误处理
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. 端到端测试

    • 测试盒子列表页面的完整功能流程
    • 测试盒子新增的类型条件字段逻辑
    • 测试奖品管理的完整操作流程