HaniBlindBox/.kiro/specs/admin-frontend-migration/design.md
2026-01-17 03:24:20 +08:00

17 KiB
Raw Blame History

Design Document: Admin Frontend Migration

Overview

本设计文档描述了 HoneyBox 后台管理系统前端业务模块的迁移方案。基于现有的 Vue 3 + Element Plus + TypeScript 技术栈,在 admin-web 项目中添加业务模块页面,对接已完成的后端 APIHoneyBox.Admin.Business

Architecture

整体架构

admin-web/
├── src/
│   ├── api/                    # API 服务层
│   │   ├── auth.ts             # 认证 API (已有)
│   │   ├── menu.ts             # 菜单 API (已有)
│   │   ├── role.ts             # 角色 API (已有)
│   │   ├── dashboard.ts        # 仪表盘 API (新增)
│   │   ├── user.ts             # 用户管理 API (新增)
│   │   ├── vip.ts              # VIP管理 API (新增)
│   │   ├── goods.ts            # 商品管理 API (新增)
│   │   ├── goodsType.ts        # 商品类型 API (新增)
│   │   ├── order.ts            # 订单管理 API (新增)
│   │   ├── finance.ts          # 财务管理 API (新增)
│   │   └── config.ts           # 系统配置 API (新增)
│   │
│   ├── views/
│   │   ├── dashboard/          # 仪表盘 (升级)
│   │   ├── system/             # 系统管理 (已有)
│   │   └── business/           # 业务模块 (新增)
│   │       ├── user/           # 用户管理
│   │       ├── vip/            # VIP管理
│   │       ├── goods/          # 商品管理
│   │       ├── goodsType/      # 商品类型
│   │       ├── order/          # 订单管理
│   │       ├── finance/        # 财务管理
│   │       └── config/         # 系统配置
│   │
│   ├── router/                 # 路由配置
│   ├── store/                  # 状态管理
│   └── utils/                  # 工具函数

技术栈

技术 版本 用途
Vue.js 3.5.x 前端框架
Element Plus 2.9.x UI 组件库
TypeScript 5.6.x 类型系统
Pinia 3.0.x 状态管理
Vue Router 4.5.x 路由管理
Axios 1.7.x HTTP 请求
Vite 6.0.x 构建工具

Components and Interfaces

API 服务层接口

Dashboard API (src/api/dashboard.ts)

interface DashboardOverview {
  todayUsers: number;
  todayOrders: number;
  todayAmount: number;
  totalUsers: number;
  totalOrders: number;
  totalAmount: number;
}

interface AdAccount {
  id: number;
  imgUrl: string;
  linkUrl?: string;
  sort: number;
  status: number;
  createTime: string;
}

// API Functions
export function getDashboardOverview(): Promise<ApiResponse<DashboardOverview>>;
export function getAdAccounts(): Promise<ApiResponse<AdAccount[]>>;
export function createAdAccount(data: { imgUrl: string; linkUrl?: string }): Promise<ApiResponse<{ id: number }>>;
export function deleteAdAccount(id: number): Promise<ApiResponse<void>>;

User API (src/api/user.ts)

interface UserListRequest {
  page: number;
  pageSize: number;
  userId?: number;
  nickname?: string;
  mobile?: string;
  status?: number;
}

interface UserInfo {
  id: number;
  nickname: string;
  avatar: string;
  mobile: string;
  money: number;
  integral: number;
  diamond: number;
  vipLevel: number;
  status: number;
  isTest: number;
  createTime: string;
}

interface UserDetail extends UserInfo {
  openid: string;
  inviteCode: string;
  parentId: number;
  totalConsume: number;
  totalWin: number;
  totalRecovery: number;
}

// API Functions
export function getUserList(params: UserListRequest): Promise<ApiResponse<PagedResult<UserInfo>>>;
export function getUserDetail(id: number): Promise<ApiResponse<UserDetail>>;
export function changeUserMoney(id: number, data: { type: string; amount: number; remark: string }): Promise<ApiResponse<void>>;
export function setUserStatus(id: number, status: number): Promise<ApiResponse<void>>;
export function setTestAccount(id: number, isTest: number): Promise<ApiResponse<void>>;
export function clearMobile(id: number): Promise<ApiResponse<void>>;
export function clearWeChat(id: number): Promise<ApiResponse<void>>;
export function giftCoupon(id: number, data: { couponId: number; count: number }): Promise<ApiResponse<void>>;
export function giftCard(id: number, data: { goodsId: number; prizeId: number; count: number }): Promise<ApiResponse<void>>;
export function getUserProfitLoss(id: number, startDate?: string, endDate?: string): Promise<ApiResponse<ProfitLoss>>;
export function getSubordinateUsers(id: number, page: number, pageSize: number): Promise<ApiResponse<PagedResult<UserInfo>>>;

Goods API (src/api/goods.ts)

interface GoodsListRequest {
  page: number;
  pageSize: number;
  name?: string;
  typeId?: number;
  status?: number;
}

interface GoodsInfo {
  id: number;
  name: string;
  typeId: number;
  typeName: string;
  price: number;
  image: string;
  status: number;
  sort: number;
  createTime: string;
}

interface GoodsDetail extends GoodsInfo {
  description: string;
  images: string[];
  rules: string;
  prizes: PrizeInfo[];
}

interface PrizeInfo {
  id: number;
  name: string;
  level: number;
  levelName: string;
  stock: number;
  probability: number;
  image: string;
  price: number;
}

// API Functions
export function getGoodsList(params: GoodsListRequest): Promise<ApiResponse<PagedResult<GoodsInfo>>>;
export function getGoodsDetail(id: number): Promise<ApiResponse<GoodsDetail>>;
export function createGoods(data: GoodsCreateRequest): Promise<ApiResponse<{ id: number }>>;
export function updateGoods(id: number, data: GoodsUpdateRequest): Promise<ApiResponse<void>>;
export function deleteGoods(id: number): Promise<ApiResponse<void>>;
export function setGoodsStatus(id: number, status: number): Promise<ApiResponse<void>>;
export function getPrizes(goodsId: number): Promise<ApiResponse<PrizeInfo[]>>;
export function addPrize(goodsId: number, data: PrizeCreateRequest): Promise<ApiResponse<{ id: number }>>;
export function updatePrize(id: number, data: PrizeUpdateRequest): Promise<ApiResponse<void>>;
export function deletePrize(id: number): Promise<ApiResponse<void>>;

Order API (src/api/order.ts)

interface OrderListRequest {
  page: number;
  pageSize: number;
  orderNo?: string;
  userId?: number;
  status?: number;
  startDate?: string;
  endDate?: string;
}

interface OrderInfo {
  id: number;
  orderNo: string;
  userId: number;
  nickname: string;
  goodsName: string;
  amount: number;
  status: number;
  statusText: string;
  createTime: string;
}

interface ShippingOrder {
  id: number;
  orderNo: string;
  userId: number;
  nickname: string;
  address: string;
  mobile: string;
  status: number;
  courierName?: string;
  courierNumber?: string;
  createTime: string;
}

// API Functions
export function getOrderList(params: OrderListRequest): Promise<ApiResponse<PagedResult<OrderInfo>>>;
export function getOrderDetail(id: number): Promise<ApiResponse<OrderDetail>>;
export function getStuckOrders(params: OrderListRequest): Promise<ApiResponse<PagedResult<OrderInfo>>>;
export function getRecoveryOrders(params: OrderListRequest): Promise<ApiResponse<PagedResult<OrderInfo>>>;
export function getShippingOrders(params: ShippingOrderListRequest): Promise<ApiResponse<PagedResult<ShippingOrder>>>;
export function shipOrder(id: number, data: { courierName: string; courierNumber: string }): Promise<ApiResponse<void>>;
export function cancelShippingOrder(id: number): Promise<ApiResponse<void>>;
export function exportOrders(params: OrderExportRequest): Promise<Blob>;

Finance API (src/api/finance.ts)

interface FinanceRecord {
  id: number;
  userId: number;
  nickname: string;
  type: string;
  amount: number;
  balance: number;
  remark: string;
  createTime: string;
}

interface RankingItem {
  rank: number;
  userId: number;
  nickname: string;
  avatar: string;
  totalAmount: number;
}

// API Functions
export function getConsumptionRanking(params: { page: number; pageSize: number; startDate?: string; endDate?: string }): Promise<ApiResponse<PagedResult<RankingItem>>>;
export function getMoneyRecords(params: FinanceListRequest): Promise<ApiResponse<PagedResult<FinanceRecord>>>;
export function getIntegralRecords(params: FinanceListRequest): Promise<ApiResponse<PagedResult<FinanceRecord>>>;
export function getDiamondRecords(params: FinanceListRequest): Promise<ApiResponse<PagedResult<FinanceRecord>>>;
export function getRechargeRecords(params: FinanceListRequest): Promise<ApiResponse<PagedResult<RechargeRecord>>>;

Config API (src/api/config.ts)

interface ConfigGroup {
  group: string;
  name: string;
  items: ConfigItem[];
}

interface ConfigItem {
  key: string;
  name: string;
  value: string;
  type: 'text' | 'number' | 'switch' | 'image' | 'textarea';
  description?: string;
}

// API Functions
export function getConfigGroups(): Promise<ApiResponse<ConfigGroup[]>>;
export function getConfigByGroup(group: string): Promise<ApiResponse<ConfigItem[]>>;
export function updateConfig(group: string, items: { key: string; value: string }[]): Promise<ApiResponse<void>>;

页面组件结构

用户管理页面 (src/views/business/user/index.vue)

<template>
  <div class="user-container">
    <!-- 搜索表单 -->
    <el-card class="search-card">
      <el-form :model="searchForm" inline>
        <el-form-item label="用户ID">
          <el-input v-model="searchForm.userId" />
        </el-form-item>
        <el-form-item label="昵称">
          <el-input v-model="searchForm.nickname" />
        </el-form-item>
        <el-form-item label="手机号">
          <el-input v-model="searchForm.mobile" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 用户列表 -->
    <el-card>
      <el-table :data="userList" v-loading="loading">
        <el-table-column prop="id" label="ID" width="80" />
        <el-table-column prop="nickname" label="昵称" />
        <el-table-column prop="mobile" label="手机号" />
        <el-table-column prop="money" label="余额" />
        <el-table-column prop="vipLevel" label="VIP等级" />
        <el-table-column prop="status" label="状态">
          <template #default="{ row }">
            <el-tag :type="row.status === 1 ? 'success' : 'danger'">
              {{ row.status === 1 ? '正常' : '封禁' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200">
          <template #default="{ row }">
            <el-button link @click="handleDetail(row)">详情</el-button>
            <el-button link @click="handleMoney(row)">资金</el-button>
            <el-dropdown>
              <el-button link>更多</el-button>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item @click="handleStatus(row)">
                    {{ row.status === 1 ? '封号' : '解封' }}
                  </el-dropdown-item>
                  <el-dropdown-item @click="handleTest(row)">
                    {{ row.isTest === 1 ? '取消测试' : '设为测试' }}
                  </el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
          </template>
        </el-table-column>
      </el-table>
      <el-pagination
        v-model:current-page="pagination.page"
        v-model:page-size="pagination.pageSize"
        :total="pagination.total"
        @change="fetchUserList"
      />
    </el-card>

    <!-- 资金变动对话框 -->
    <el-dialog v-model="moneyDialogVisible" title="资金变动">
      <el-form :model="moneyForm" label-width="80px">
        <el-form-item label="类型">
          <el-select v-model="moneyForm.type">
            <el-option label="余额" value="money" />
            <el-option label="积分" value="integral" />
            <el-option label="钻石" value="diamond" />
          </el-select>
        </el-form-item>
        <el-form-item label="金额">
          <el-input-number v-model="moneyForm.amount" />
        </el-form-item>
        <el-form-item label="备注">
          <el-input v-model="moneyForm.remark" type="textarea" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="moneyDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="submitMoneyChange">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

Data Models

通用响应格式

interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

interface PagedResult<T> {
  list: T[];
  total: number;
  page: number;
  pageSize: number;
}

业务数据模型

详见上述 Components and Interfaces 部分的接口定义。

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 列表页面(用户、商品、订单等),当请求分页数据时,返回的列表长度应小于等于请求的 pageSize且 total 应大于等于列表长度。

Validates: Requirements 2.1, 4.1, 6.1

Property 2: 动态菜单权限过滤

For any 菜单数据和用户权限配置,生成的导航菜单应只包含用户有权限访问的菜单项,无权限的菜单项不应出现在导航中。

Validates: Requirements 9.2, 9.4

Property 3: 请求拦截器Token注入

For any 需要认证的 API 请求,请求头中应包含有效的 Authorization Token且 Token 格式应为 "Bearer {token}"。

Validates: Requirements 10.3, 10.4

Property 4: 配置项类型渲染

For any 配置项数据,根据配置项的 type 字段应渲染对应类型的输入组件text 对应输入框number 对应数字输入框switch 对应开关image 对应图片上传)。

Validates: Requirements 8.4

Property 5: 错误响应处理

For any API 响应,当 code 不为 0 时,应显示错误提示信息,且错误信息应来自响应的 message 字段。

Validates: Requirements 1.4, 10.3

Error Handling

API 错误处理

// src/utils/request.ts
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { getToken, removeToken } from './auth'
import router from '@/router'

const request = axios.create({
  baseURL: '/api',
  timeout: 30000
})

// 请求拦截器
request.interceptors.request.use(
  config => {
    const token = getToken()
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  error => Promise.reject(error)
)

// 响应拦截器
request.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 0) {
      ElMessage.error(res.message || '请求失败')
      // Token 过期
      if (res.code === 40001) {
        removeToken()
        router.push('/login')
      }
      return Promise.reject(new Error(res.message))
    }
    return res
  },
  error => {
    ElMessage.error(error.message || '网络错误')
    return Promise.reject(error)
  }
)

export default request

表单验证错误

// 使用 Element Plus 表单验证
const rules = {
  amount: [
    { required: true, message: '请输入金额', trigger: 'blur' },
    { type: 'number', min: 0.01, message: '金额必须大于0', trigger: 'blur' }
  ],
  remark: [
    { required: true, message: '请输入备注', trigger: 'blur' }
  ]
}

Testing Strategy

单元测试

使用 Vitest 进行单元测试,测试以下内容:

  1. API 服务层测试 - 验证 API 函数正确调用 axios
  2. 工具函数测试 - 验证 Token 管理、请求拦截器等
  3. 组件测试 - 验证页面组件渲染和交互

属性测试

使用 fast-check 进行属性测试,验证以下属性:

  1. Property 1 - 分页数据一致性
  2. Property 2 - 菜单权限过滤
  3. Property 3 - Token 注入
  4. Property 4 - 配置项类型渲染
  5. Property 5 - 错误响应处理

测试配置

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true,
    coverage: {
      reporter: ['text', 'json', 'html']
    }
  }
})

测试示例

// src/api/__tests__/user.test.ts
import { describe, it, expect, vi } from 'vitest'
import fc from 'fast-check'
import { getUserList } from '../user'

describe('User API', () => {
  // Property 1: 分页数据一致性
  it('should return list length <= pageSize', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.integer({ min: 1, max: 100 }),
        fc.integer({ min: 1, max: 50 }),
        async (page, pageSize) => {
          const result = await getUserList({ page, pageSize })
          expect(result.data.list.length).toBeLessThanOrEqual(pageSize)
          expect(result.data.total).toBeGreaterThanOrEqual(result.data.list.length)
        }
      ),
      { numRuns: 100 }
    )
  })
})