mi-assessment/.kiro/specs/admin-frontend/design.md
2026-02-03 19:17:48 +08:00

43 KiB
Raw Blame History

Design Document: Admin Frontend Business Modules

Overview

本设计文档描述了学业邑规划 MiAssessment 管理后台前端业务模块的技术架构和实现方案。系统基于 Vue 3.5 + TypeScript 5.6 + Element Plus 2.9 技术栈在现有的前端模板基础上扩展8个核心业务模块页面。

设计目标

  1. 组件化设计: 页面组件独立封装,复用现有基础组件
  2. 类型安全: 使用 TypeScript 定义所有接口和数据类型
  3. 统一风格: 遵循现有代码风格和 Element Plus 设计规范
  4. 权限集成: 与现有权限系统无缝集成
  5. 可测试性: 支持单元测试和属性测试

Architecture

系统架构图

graph TB
    subgraph "Vue Application"
        A[Router] --> B[Layout]
        B --> C[Business Pages]
        
        subgraph "Business Pages"
            C1[Dashboard]
            C2[Config]
            C3[Content - Banner/Promotion]
            C4[Assessment - Type/Question/Category/Conclusion]
            C5[User]
            C6[Order]
            C7[Planner - Info/Booking]
            C8[Distribution - InviteCode/Commission/Withdrawal]
        end
    end
    
    subgraph "API Layer"
        D[API Services]
        D1[dashboard.ts]
        D2[config.ts]
        D3[content.ts]
        D4[assessment.ts]
        D5[user.ts]
        D6[order.ts]
        D7[planner.ts]
        D8[distribution.ts]
    end
    
    subgraph "Shared Components"
        E1[ImageUpload]
        E2[DictSelect]
        E3[DictRadio]
        E4[DictCheckbox]
    end
    
    C --> D
    C --> E1
    C --> E2
    C --> E3

项目结构

server/MiAssessment/src/MiAssessment.Admin/admin-web/src/
├── api/
│   └── business/                    # 业务模块 API
│       ├── dashboard.ts             # 仪表盘 API
│       ├── config.ts                # 系统配置 API
│       ├── content.ts               # 内容管理 API (轮播图、宣传图)
│       ├── assessment.ts            # 测评管理 API
│       ├── user.ts                  # 用户管理 API
│       ├── order.ts                 # 订单管理 API
│       ├── planner.ts               # 规划师管理 API
│       └── distribution.ts          # 分销管理 API
├── views/
│   ├── dashboard/
│   │   └── index.vue                # 仪表盘页面 (已存在,需扩展)
│   └── business/                    # 业务模块页面
│       ├── config/
│       │   └── index.vue            # 系统配置页面
│       ├── content/
│       │   ├── banner/
│       │   │   └── index.vue        # 轮播图管理页面
│       │   └── promotion/
│       │       └── index.vue        # 宣传图管理页面
│       ├── assessment/
│       │   ├── type/
│       │   │   └── index.vue        # 测评类型管理页面
│       │   ├── question/
│       │   │   └── index.vue        # 题库管理页面
│       │   ├── category/
│       │   │   └── index.vue        # 报告分类管理页面
│       │   └── conclusion/
│       │       └── index.vue        # 报告结论管理页面
│       ├── user/
│       │   └── index.vue            # 用户管理页面
│       ├── order/
│       │   └── index.vue            # 订单管理页面
│       ├── planner/
│       │   ├── index.vue            # 规划师管理页面
│       │   └── booking/
│       │       └── index.vue        # 预约记录管理页面
│       └── distribution/
│           ├── invite-code/
│           │   └── index.vue        # 邀请码管理页面
│           ├── commission/
│           │   └── index.vue        # 佣金记录管理页面
│           └── withdrawal/
│               └── index.vue        # 提现审核管理页面
└── router/
    └── index.ts                     # 路由配置 (需扩展)

Components and Interfaces

1. API 服务层设计

通用类型定义

// types/common.ts
export interface PagedRequest {
  pageIndex: number
  pageSize: number
}

export interface PagedResult<T> {
  items: T[]
  total: number
  pageIndex: number
  pageSize: number
}

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

export interface UpdateStatusRequest {
  id: number
  status: number
}

export interface SortItem {
  id: number
  sort: number
}

export interface UpdateSortRequest {
  items: SortItem[]
}

Dashboard API (api/business/dashboard.ts)

import request from '@/utils/request'

export interface TodayStatistics {
  newUsers: number
  newOrders: number
  revenue: number
}

export interface TotalStatistics {
  totalUsers: number
  totalOrders: number
  totalRevenue: number
}

export interface DashboardOverview {
  today: TodayStatistics
  total: TotalStatistics
}

export interface TrendItem {
  date: string
  value: number
}

export interface TrendsData {
  userTrend: TrendItem[]
  orderTrend: TrendItem[]
  revenueTrend: TrendItem[]
}

export interface TrendsQuery {
  startDate: string
  endDate: string
}

export interface PendingItems {
  pendingWithdrawals: number
  pendingBookings: number
}

// 获取概览数据
export function getOverview() {
  return request.get<DashboardOverview>('/api/admin/dashboard/getOverview')
}

// 获取趋势数据
export function getTrends(params: TrendsQuery) {
  return request.get<TrendsData>('/api/admin/dashboard/getTrends', { params })
}

// 获取待办事项
export function getPendingItems() {
  return request.get<PendingItems>('/api/admin/dashboard/getPendingItems')
}

Config API (api/business/config.ts)

import request from '@/utils/request'

export interface ConfigItem {
  id: number
  configKey: string
  configValue: string
  configType: string
  description: string
}

export interface ConfigGroup {
  configType: string
  typeName: string
  items: ConfigItem[]
}

export interface UpdateConfigRequest {
  configKey: string
  configValue: string
}

// 获取配置列表
export function getConfigList() {
  return request.get<ConfigGroup[]>('/api/admin/config/getList')
}

// 更新配置
export function updateConfig(data: UpdateConfigRequest) {
  return request.post<boolean>('/api/admin/config/update', data)
}

// 根据Key获取配置
export function getConfigByKey(configKey: string) {
  return request.get<ConfigItem>('/api/admin/config/getByKey', { params: { configKey } })
}

Content API (api/business/content.ts)

import request from '@/utils/request'
import type { PagedRequest, PagedResult, UpdateStatusRequest, UpdateSortRequest } from '@/types/common'

// ==================== Banner 类型定义 ====================
export interface BannerItem {
  id: number
  title: string
  imageUrl: string
  linkType: number
  linkTypeName: string
  linkUrl: string
  appId: string
  sort: number
  status: number
  statusName: string
  createTime: string
}

export interface BannerQuery extends PagedRequest {
  title?: string
  status?: number
}

export interface CreateBannerRequest {
  title?: string
  imageUrl: string
  linkType: number
  linkUrl?: string
  appId?: string
  sort: number
  status: number
}

export interface UpdateBannerRequest extends CreateBannerRequest {
  id: number
}

// ==================== Promotion 类型定义 ====================
export interface PromotionItem {
  id: number
  title: string
  imageUrl: string
  position: number
  positionName: string
  sort: number
  status: number
  statusName: string
  createTime: string
}

export interface PromotionQuery extends PagedRequest {
  title?: string
  position?: number
  status?: number
}

export interface CreatePromotionRequest {
  title?: string
  imageUrl: string
  position: number
  sort: number
  status: number
}

export interface UpdatePromotionRequest extends CreatePromotionRequest {
  id: number
}

// ==================== Banner API ====================
export function getBannerList(params: BannerQuery) {
  return request.get<PagedResult<BannerItem>>('/api/admin/content/banner/getList', { params })
}

export function createBanner(data: CreateBannerRequest) {
  return request.post<number>('/api/admin/content/banner/create', data)
}

export function updateBanner(data: UpdateBannerRequest) {
  return request.post<boolean>('/api/admin/content/banner/update', data)
}

export function deleteBanner(id: number) {
  return request.post<boolean>('/api/admin/content/banner/delete', { id })
}

export function updateBannerStatus(data: UpdateStatusRequest) {
  return request.post<boolean>('/api/admin/content/banner/updateStatus', data)
}

export function updateBannerSort(data: UpdateSortRequest) {
  return request.post<boolean>('/api/admin/content/banner/updateSort', data)
}

// ==================== Promotion API ====================
export function getPromotionList(params: PromotionQuery) {
  return request.get<PagedResult<PromotionItem>>('/api/admin/content/promotion/getList', { params })
}

export function createPromotion(data: CreatePromotionRequest) {
  return request.post<number>('/api/admin/content/promotion/create', data)
}

export function updatePromotion(data: UpdatePromotionRequest) {
  return request.post<boolean>('/api/admin/content/promotion/update', data)
}

export function deletePromotion(id: number) {
  return request.post<boolean>('/api/admin/content/promotion/delete', { id })
}

export function updatePromotionStatus(data: UpdateStatusRequest) {
  return request.post<boolean>('/api/admin/content/promotion/updateStatus', data)
}

Assessment API (api/business/assessment.ts)

import request from '@/utils/request'
import type { PagedRequest, PagedResult, UpdateStatusRequest } from '@/types/common'

// ==================== Assessment Type 类型定义 ====================
export interface AssessmentTypeItem {
  id: number
  name: string
  code: string
  imageUrl: string
  introContent: string
  price: number
  questionCount: number
  sort: number
  status: number
  statusName: string
  createTime: string
}

export interface AssessmentTypeQuery extends PagedRequest {
  name?: string
  code?: string
  status?: number
}

export interface CreateAssessmentTypeRequest {
  name: string
  code: string
  imageUrl?: string
  introContent?: string
  price: number
  sort: number
  status: number
}

export interface UpdateAssessmentTypeRequest extends CreateAssessmentTypeRequest {
  id: number
}

// ==================== Question 类型定义 ====================
export interface QuestionItem {
  id: number
  assessmentTypeId: number
  assessmentTypeName: string
  questionNo: number
  content: string
  sort: number
  status: number
  categoryIds: number[]
}

export interface QuestionQuery extends PagedRequest {
  assessmentTypeId?: number
  questionNo?: number
  status?: number
}

export interface CreateQuestionRequest {
  assessmentTypeId: number
  questionNo: number
  content: string
  sort: number
  status: number
}

export interface UpdateQuestionRequest extends CreateQuestionRequest {
  id: number
}

export interface BatchImportResult {
  successCount: number
  failCount: number
  errors: { row: number; message: string }[]
}

// ==================== Category 类型定义 ====================
export interface CategoryTreeNode {
  id: number
  parentId: number
  name: string
  code: string
  categoryType: number
  categoryTypeName: string
  scoreRule: number
  scoreRuleName: string
  sort: number
  children: CategoryTreeNode[]
}

export interface CreateCategoryRequest {
  assessmentTypeId: number
  parentId?: number
  name: string
  code: string
  categoryType: number
  scoreRule: number
  sort: number
}

export interface UpdateCategoryRequest extends CreateCategoryRequest {
  id: number
}

// ==================== Mapping 类型定义 ====================
export interface BatchUpdateMappingsRequest {
  questionId: number
  categoryIds: number[]
}

// ==================== Conclusion 类型定义 ====================
export interface ConclusionItem {
  id: number
  categoryId: number
  categoryName: string
  conclusionType: number
  conclusionTypeName: string
  title: string
  content: string
}

export interface CreateConclusionRequest {
  categoryId: number
  conclusionType: number
  title: string
  content: string
}

export interface UpdateConclusionRequest extends CreateConclusionRequest {
  id: number
}

// ==================== Assessment Type API ====================
export function getAssessmentTypeList(params: AssessmentTypeQuery) {
  return request.get<PagedResult<AssessmentTypeItem>>('/api/admin/assessment/type/getList', { params })
}

export function createAssessmentType(data: CreateAssessmentTypeRequest) {
  return request.post<number>('/api/admin/assessment/type/create', data)
}

export function updateAssessmentType(data: UpdateAssessmentTypeRequest) {
  return request.post<boolean>('/api/admin/assessment/type/update', data)
}

export function deleteAssessmentType(id: number) {
  return request.post<boolean>('/api/admin/assessment/type/delete', { id })
}

export function updateAssessmentTypeStatus(data: UpdateStatusRequest) {
  return request.post<boolean>('/api/admin/assessment/type/updateStatus', data)
}

// ==================== Question API ====================
export function getQuestionList(params: QuestionQuery) {
  return request.get<PagedResult<QuestionItem>>('/api/admin/assessment/question/getList', { params })
}

export function createQuestion(data: CreateQuestionRequest) {
  return request.post<number>('/api/admin/assessment/question/create', data)
}

export function updateQuestion(data: UpdateQuestionRequest) {
  return request.post<boolean>('/api/admin/assessment/question/update', data)
}

export function deleteQuestion(id: number) {
  return request.post<boolean>('/api/admin/assessment/question/delete', { id })
}

export function batchImportQuestions(data: FormData) {
  return request.post<BatchImportResult>('/api/admin/assessment/question/batchImport', data)
}

// ==================== Category API ====================
export function getCategoryTree(assessmentTypeId: number) {
  return request.get<CategoryTreeNode[]>('/api/admin/assessment/category/getTree', { params: { assessmentTypeId } })
}

export function createCategory(data: CreateCategoryRequest) {
  return request.post<number>('/api/admin/assessment/category/create', data)
}

export function updateCategory(data: UpdateCategoryRequest) {
  return request.post<boolean>('/api/admin/assessment/category/update', data)
}

export function deleteCategory(id: number) {
  return request.post<boolean>('/api/admin/assessment/category/delete', { id })
}

// ==================== Mapping API ====================
export function getMappingsByQuestion(questionId: number) {
  return request.get<number[]>('/api/admin/assessment/mapping/getByQuestion', { params: { questionId } })
}

export function batchUpdateMappings(data: BatchUpdateMappingsRequest) {
  return request.post<boolean>('/api/admin/assessment/mapping/batchUpdate', data)
}

// ==================== Conclusion API ====================
export function getConclusionList(categoryId: number) {
  return request.get<ConclusionItem[]>('/api/admin/assessment/conclusion/getList', { params: { categoryId } })
}

export function createConclusion(data: CreateConclusionRequest) {
  return request.post<number>('/api/admin/assessment/conclusion/create', data)
}

export function updateConclusion(data: UpdateConclusionRequest) {
  return request.post<boolean>('/api/admin/assessment/conclusion/update', data)
}

export function deleteConclusion(id: number) {
  return request.post<boolean>('/api/admin/assessment/conclusion/delete', { id })
}

User API (api/business/user.ts)

import request from '@/utils/request'
import type { PagedRequest, PagedResult, UpdateStatusRequest } from '@/types/common'

export interface UserItem {
  id: number
  uid: string
  phone: string
  nickname: string
  avatar: string
  userLevel: number
  userLevelName: string
  balance: number
  totalIncome: number
  status: number
  statusName: string
  createTime: string
  lastLoginTime: string | null
}

export interface UserDetail extends UserItem {
  parentUserId: number | null
  parentUserNickname: string | null
  parentUserUid: string | null
  withdrawnAmount: number
  orderCount: number
  assessmentCount: number
  inviteCount: number
}

export interface UserQuery extends PagedRequest {
  uid?: string
  phone?: string
  nickname?: string
  userLevel?: number
  status?: number
  createTimeStart?: string
  createTimeEnd?: string
}

export interface UpdateUserLevelRequest {
  id: number
  userLevel: number
}

// 获取用户列表
export function getUserList(params: UserQuery) {
  return request.get<PagedResult<UserItem>>('/api/admin/user/getList', { params })
}

// 获取用户详情
export function getUserDetail(id: number) {
  return request.get<UserDetail>('/api/admin/user/getDetail', { params: { id } })
}

// 更新用户状态
export function updateUserStatus(data: UpdateStatusRequest) {
  return request.post<boolean>('/api/admin/user/updateStatus', data)
}

// 更新用户等级
export function updateUserLevel(data: UpdateUserLevelRequest) {
  return request.post<boolean>('/api/admin/user/updateLevel', data)
}

// 导出用户列表
export function exportUsers(params: UserQuery) {
  return request.get('/api/admin/user/export', { params, responseType: 'blob' })
}

Order API (api/business/order.ts)

import request from '@/utils/request'
import type { PagedRequest, PagedResult } from '@/types/common'

export interface OrderItem {
  id: number
  orderNo: string
  userId: number
  userNickname: string
  userPhone: string
  orderType: number
  orderTypeName: string
  productName: string
  amount: number
  payAmount: number
  payType: number | null
  payTypeName: string | null
  status: number
  statusName: string
  payTime: string | null
  createTime: string
}

export interface OrderDetail extends OrderItem {
  productId: number
  inviteCodeId: number | null
  inviteCode: string | null
  transactionId: string | null
  refundTime: string | null
  refundAmount: number | null
  refundReason: string | null
  remark: string | null
  relatedRecord: unknown
}

export interface OrderQuery extends PagedRequest {
  orderNo?: string
  userId?: number
  orderType?: number
  status?: number
  payType?: number
  createTimeStart?: string
  createTimeEnd?: string
}

export interface RefundRequest {
  orderId: number
  refundAmount: number
  refundReason: string
}

// 获取订单列表
export function getOrderList(params: OrderQuery) {
  return request.get<PagedResult<OrderItem>>('/api/admin/order/getList', { params })
}

// 获取订单详情
export function getOrderDetail(id: number) {
  return request.get<OrderDetail>('/api/admin/order/getDetail', { params: { id } })
}

// 退款
export function refundOrder(data: RefundRequest) {
  return request.post<boolean>('/api/admin/order/refund', data)
}

// 导出订单列表
export function exportOrders(params: OrderQuery) {
  return request.get('/api/admin/order/export', { params, responseType: 'blob' })
}

Planner API (api/business/planner.ts)

import request from '@/utils/request'
import type { PagedRequest, PagedResult, UpdateStatusRequest, UpdateSortRequest } from '@/types/common'

// ==================== Planner 类型定义 ====================
export interface PlannerItem {
  id: number
  name: string
  avatar: string
  title: string
  intro: string
  price: number
  sort: number
  status: number
  statusName: string
  createTime: string
}

export interface PlannerQuery extends PagedRequest {
  name?: string
  status?: number
}

export interface CreatePlannerRequest {
  name: string
  avatar: string
  title?: string
  intro?: string
  price: number
  sort: number
  status: number
}

export interface UpdatePlannerRequest extends CreatePlannerRequest {
  id: number
}

// ==================== Booking 类型定义 ====================
export interface BookingItem {
  id: number
  userId: number
  userNickname: string
  userPhone: string
  plannerId: number
  plannerName: string
  bookingDate: string
  studentName: string
  studentGrade: number
  studentGradeName: string
  status: number
  statusName: string
  createTime: string
}

export interface BookingDetail extends BookingItem {
  plannerAvatar: string
  plannerTitle: string
  studentSchool: string
  studentScores: string
  remark: string | null
  orderId: number
  orderNo: string
}

export interface BookingQuery extends PagedRequest {
  plannerId?: number
  userId?: number
  bookingDateStart?: string
  bookingDateEnd?: string
  status?: number
}

export interface UpdateBookingStatusRequest {
  id: number
  status: number
}

// ==================== Planner API ====================
export function getPlannerList(params: PlannerQuery) {
  return request.get<PagedResult<PlannerItem>>('/api/admin/planner/getList', { params })
}

export function createPlanner(data: CreatePlannerRequest) {
  return request.post<number>('/api/admin/planner/create', data)
}

export function updatePlanner(data: UpdatePlannerRequest) {
  return request.post<boolean>('/api/admin/planner/update', data)
}

export function deletePlanner(id: number) {
  return request.post<boolean>('/api/admin/planner/delete', { id })
}

export function updatePlannerStatus(data: UpdateStatusRequest) {
  return request.post<boolean>('/api/admin/planner/updateStatus', data)
}

export function updatePlannerSort(data: UpdateSortRequest) {
  return request.post<boolean>('/api/admin/planner/updateSort', data)
}

// ==================== Booking API ====================
export function getBookingList(params: BookingQuery) {
  return request.get<PagedResult<BookingItem>>('/api/admin/planner/booking/getList', { params })
}

export function getBookingDetail(id: number) {
  return request.get<BookingDetail>('/api/admin/planner/booking/getDetail', { params: { id } })
}

export function updateBookingStatus(data: UpdateBookingStatusRequest) {
  return request.post<boolean>('/api/admin/planner/booking/updateStatus', data)
}

export function exportBookings(params: BookingQuery) {
  return request.get('/api/admin/planner/booking/export', { params, responseType: 'blob' })
}

Distribution API (api/business/distribution.ts)

import request from '@/utils/request'
import type { PagedRequest, PagedResult } from '@/types/common'

// ==================== InviteCode 类型定义 ====================
export interface InviteCodeItem {
  id: number
  code: string
  batchNo: string
  assignUserId: number | null
  assignUserNickname: string | null
  assignTime: string | null
  useUserId: number | null
  useUserNickname: string | null
  useOrderId: number | null
  useTime: string | null
  status: number
  statusName: string
  createTime: string
}

export interface InviteCodeQuery extends PagedRequest {
  code?: string
  batchNo?: string
  assignUserId?: number
  status?: number
}

export interface GenerateInviteCodesRequest {
  count: number
}

export interface GenerateResult {
  batchNo: string
  count: number
  codes: string[]
}

export interface AssignInviteCodesRequest {
  codeIds: number[]
  userId: number
}

// ==================== Commission 类型定义 ====================
export interface CommissionItem {
  id: number
  userId: number
  userNickname: string
  fromUserId: number
  fromUserNickname: string
  orderId: number
  orderNo: string
  orderAmount: number
  commissionRate: number
  commissionAmount: number
  level: number
  levelName: string
  status: number
  statusName: string
  settleTime: string | null
  createTime: string
}

export interface CommissionQuery extends PagedRequest {
  userId?: number
  fromUserId?: number
  orderId?: number
  level?: number
  status?: number
  createTimeStart?: string
  createTimeEnd?: string
}

export interface CommissionStatistics {
  totalAmount: number
  pendingAmount: number
  settledAmount: number
  totalCount: number
  pendingCount: number
  settledCount: number
}

// ==================== Withdrawal 类型定义 ====================
export interface WithdrawalItem {
  id: number
  withdrawalNo: string
  userId: number
  userNickname: string
  userPhone: string
  amount: number
  beforeBalance: number
  afterBalance: number
  status: number
  statusName: string
  auditUserId: number | null
  auditUserName: string | null
  auditTime: string | null
  auditRemark: string | null
  payTime: string | null
  payTransactionId: string | null
  createTime: string
}

export interface WithdrawalDetail extends WithdrawalItem {
  userBalance: number
  userTotalIncome: number
  userWithdrawnAmount: number
}

export interface WithdrawalQuery extends PagedRequest {
  withdrawalNo?: string
  userId?: number
  status?: number
  createTimeStart?: string
  createTimeEnd?: string
}

export interface ApproveWithdrawalRequest {
  id: number
}

export interface RejectWithdrawalRequest {
  id: number
  auditRemark: string
}

export interface CompleteWithdrawalRequest {
  id: number
  payTransactionId: string
}

// ==================== InviteCode API ====================
export function getInviteCodeList(params: InviteCodeQuery) {
  return request.get<PagedResult<InviteCodeItem>>('/api/admin/distribution/inviteCode/getList', { params })
}

export function generateInviteCodes(data: GenerateInviteCodesRequest) {
  return request.post<GenerateResult>('/api/admin/distribution/inviteCode/generate', data)
}

export function assignInviteCodes(data: AssignInviteCodesRequest) {
  return request.post<boolean>('/api/admin/distribution/inviteCode/assign', data)
}

export function exportInviteCodes(params: InviteCodeQuery) {
  return request.get('/api/admin/distribution/inviteCode/export', { params, responseType: 'blob' })
}

// ==================== Commission API ====================
export function getCommissionList(params: CommissionQuery) {
  return request.get<PagedResult<CommissionItem>>('/api/admin/distribution/commission/getList', { params })
}

export function getCommissionDetail(id: number) {
  return request.get<CommissionItem>('/api/admin/distribution/commission/getDetail', { params: { id } })
}

export function getCommissionStatistics(params?: { userId?: number }) {
  return request.get<CommissionStatistics>('/api/admin/distribution/commission/getStatistics', { params })
}

export function exportCommissions(params: CommissionQuery) {
  return request.get('/api/admin/distribution/commission/export', { params, responseType: 'blob' })
}

// ==================== Withdrawal API ====================
export function getWithdrawalList(params: WithdrawalQuery) {
  return request.get<PagedResult<WithdrawalItem>>('/api/admin/distribution/withdrawal/getList', { params })
}

export function getWithdrawalDetail(id: number) {
  return request.get<WithdrawalDetail>('/api/admin/distribution/withdrawal/getDetail', { params: { id } })
}

export function approveWithdrawal(data: ApproveWithdrawalRequest) {
  return request.post<boolean>('/api/admin/distribution/withdrawal/approve', data)
}

export function rejectWithdrawal(data: RejectWithdrawalRequest) {
  return request.post<boolean>('/api/admin/distribution/withdrawal/reject', data)
}

export function completeWithdrawal(data: CompleteWithdrawalRequest) {
  return request.post<boolean>('/api/admin/distribution/withdrawal/complete', data)
}

export function exportWithdrawals(params: WithdrawalQuery) {
  return request.get('/api/admin/distribution/withdrawal/export', { params, responseType: 'blob' })
}

2. 路由配置设计

// router/business.ts - 业务模块路由配置
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/layout/index.vue'

export const businessRoutes: RouteRecordRaw[] = [
  {
    path: '/business',
    component: Layout,
    redirect: '/business/config',
    meta: { title: '业务管理', icon: 'business' },
    children: [
      {
        path: 'config',
        name: 'Config',
        component: () => import('@/views/business/config/index.vue'),
        meta: { title: '系统配置', permission: 'config:view' }
      },
      {
        path: 'content',
        name: 'Content',
        redirect: '/business/content/banner',
        meta: { title: '内容管理' },
        children: [
          {
            path: 'banner',
            name: 'Banner',
            component: () => import('@/views/business/content/banner/index.vue'),
            meta: { title: '轮播图管理', permission: 'banner:view' }
          },
          {
            path: 'promotion',
            name: 'Promotion',
            component: () => import('@/views/business/content/promotion/index.vue'),
            meta: { title: '宣传图管理', permission: 'promotion:view' }
          }
        ]
      },
      {
        path: 'assessment',
        name: 'Assessment',
        redirect: '/business/assessment/type',
        meta: { title: '测评管理' },
        children: [
          {
            path: 'type',
            name: 'AssessmentType',
            component: () => import('@/views/business/assessment/type/index.vue'),
            meta: { title: '测评类型', permission: 'assessment:view' }
          },
          {
            path: 'question',
            name: 'Question',
            component: () => import('@/views/business/assessment/question/index.vue'),
            meta: { title: '题库管理', permission: 'question:view' }
          },
          {
            path: 'category',
            name: 'Category',
            component: () => import('@/views/business/assessment/category/index.vue'),
            meta: { title: '报告分类', permission: 'category:view' }
          },
          {
            path: 'conclusion',
            name: 'Conclusion',
            component: () => import('@/views/business/assessment/conclusion/index.vue'),
            meta: { title: '报告结论', permission: 'conclusion:view' }
          }
        ]
      },
      {
        path: 'user',
        name: 'BusinessUser',
        component: () => import('@/views/business/user/index.vue'),
        meta: { title: '用户管理', permission: 'user:view' }
      },
      {
        path: 'order',
        name: 'Order',
        component: () => import('@/views/business/order/index.vue'),
        meta: { title: '订单管理', permission: 'order:view' }
      },
      {
        path: 'planner',
        name: 'Planner',
        redirect: '/business/planner/list',
        meta: { title: '规划师管理' },
        children: [
          {
            path: 'list',
            name: 'PlannerList',
            component: () => import('@/views/business/planner/index.vue'),
            meta: { title: '规划师列表', permission: 'planner:view' }
          },
          {
            path: 'booking',
            name: 'Booking',
            component: () => import('@/views/business/planner/booking/index.vue'),
            meta: { title: '预约记录', permission: 'booking:view' }
          }
        ]
      },
      {
        path: 'distribution',
        name: 'Distribution',
        redirect: '/business/distribution/invite-code',
        meta: { title: '分销管理' },
        children: [
          {
            path: 'invite-code',
            name: 'InviteCode',
            component: () => import('@/views/business/distribution/invite-code/index.vue'),
            meta: { title: '邀请码管理', permission: 'inviteCode:view' }
          },
          {
            path: 'commission',
            name: 'Commission',
            component: () => import('@/views/business/distribution/commission/index.vue'),
            meta: { title: '佣金记录', permission: 'commission:view' }
          },
          {
            path: 'withdrawal',
            name: 'Withdrawal',
            component: () => import('@/views/business/distribution/withdrawal/index.vue'),
            meta: { title: '提现审核', permission: 'withdrawal:view' }
          }
        ]
      }
    ]
  }
]

Data Models

页面组件数据模型

通用列表页面状态

// 通用列表页面状态接口
interface ListPageState<T, Q> {
  loading: boolean
  tableData: T[]
  total: number
  queryParams: Q
  dialogVisible: boolean
  dialogTitle: string
  formData: Partial<T>
  formLoading: boolean
}

// 使用示例 - Banner 页面
interface BannerPageState extends ListPageState<BannerItem, BannerQuery> {
  // 额外的页面特定状态
}

仪表盘页面数据模型

interface DashboardState {
  loading: boolean
  overview: DashboardOverview | null
  trends: TrendsData | null
  pendingItems: PendingItems | null
  dateRange: [string, string]
}

配置页面数据模型

interface ConfigPageState {
  loading: boolean
  configGroups: ConfigGroup[]
  editingKey: string | null
  editValue: string
  saving: boolean
}

字典类型定义

需要在后台配置的字典类型:

字典编码 字典名称 字典项
banner_link_type 轮播图跳转类型 1-内部页面, 2-外部链接, 3-小程序
common_status 通用状态 0-禁用, 1-启用
promotion_position 宣传图位置 1-首页底部, 2-团队页面
assessment_status 测评状态 0-下线, 1-上线, 2-即将上线
user_level 用户等级 1-普通用户, 2-合伙人, 3-渠道商
order_type 订单类型 1-测评订单, 2-规划订单
order_status 订单状态 1-待支付, 2-已支付, 3-已完成, 4-退款中, 5-已退款, 6-已取消
pay_type 支付方式 1-微信支付
booking_status 预约状态 1-待确认, 2-已确认, 3-已完成, 4-已取消
invite_code_status 邀请码状态 1-未分配, 2-已分配, 3-已使用
commission_level 佣金层级 1-直接下级, 2-间接下级
commission_status 佣金状态 1-待结算, 2-已结算
withdrawal_status 提现状态 1-待审核, 2-处理中, 3-已完成, 4-已取消
category_type 分类类型 1-八大智能, 2-个人特质, 3-细分能力, 4-先天学习, 5-学习能力, 6-大脑类型, 7-性格类型, 8-未来能力
score_rule 计分规则 1-累加(1-10), 2-二值(0/1)
conclusion_type 结论类型 1-最强, 2-较强, 3-较弱, 4-最弱
student_grade 学生年级 1-小学, 2-初中, 3-高中

Error Handling

前端错误处理策略

// utils/error-handler.ts
import { ElMessage, ElMessageBox } from 'element-plus'

// API 错误码映射
const errorMessages: Record<number, string> = {
  1001: '参数错误',
  1002: '未授权,请重新登录',
  1003: 'Token已过期请重新登录',
  1004: '没有权限执行此操作',
  2001: '业务处理失败',
  2002: '数据不存在',
  2003: '数据已存在',
  2004: '数据正在使用中,无法删除',
  2005: '无效的操作',
  5000: '系统错误,请稍后重试'
}

// 处理 API 错误
export function handleApiError(code: number, message?: string) {
  const errorMessage = message || errorMessages[code] || '未知错误'
  
  if (code === 1002 || code === 1003) {
    // Token 相关错误,跳转登录
    ElMessageBox.alert(errorMessage, '提示', {
      confirmButtonText: '重新登录',
      callback: () => {
        // 清除 token 并跳转登录页
        localStorage.removeItem('token')
        window.location.href = '/login'
      }
    })
  } else if (code === 1004) {
    // 权限错误
    ElMessage.warning(errorMessage)
  } else {
    ElMessage.error(errorMessage)
  }
}

// 表单验证错误处理
export function handleValidationError(errors: Record<string, string[]>) {
  const firstError = Object.values(errors)[0]?.[0]
  if (firstError) {
    ElMessage.warning(firstError)
  }
}

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.

Based on the prework analysis, the following correctness properties have been identified for property-based testing:

Property 1: Statistics Display Correctness

For any valid API response containing statistics data (today's or cumulative), the Dashboard component SHALL render the exact values from the response in the corresponding statistics cards without modification.

Validates: Requirements 1.1, 1.2

Property 2: Config Grouping Correctness

For any list of configuration items with various ConfigType values, the Config page SHALL group all items by their ConfigType and display each group in a separate collapsible panel.

Validates: Requirements 2.1

Property 3: Config Value Validation

For any configuration update where ConfigType is 'price', the validation SHALL reject non-positive numbers. For any configuration update where ConfigType is 'commission', the validation SHALL reject values outside the range [0, 1].

Validates: Requirements 2.3, 2.4, 2.5

For any Banner form submission where LinkType is 1 (internal page) or 2 (external link), the form validation SHALL require LinkUrl to be non-empty. For any Banner form submission where LinkType is 3 (mini-program), the form validation SHALL require both LinkUrl and AppId to be non-empty.

Validates: Requirements 3.3, 3.4

Property 5: Price Field Validation

For any form containing a price field (Assessment Type, Planner), the validation SHALL reject non-positive numbers and accept only positive decimal values.

Validates: Requirements 5.3, 12.3

Property 6: Query Parameter Handling

For any list page with search/filter functionality, when the user enters search criteria, the page SHALL pass all non-empty filter values as query parameters to the corresponding API endpoint.

Validates: Requirements 9.2, 1.4

Property 7: Order Status Color Mapping

For any order status value, the Order page SHALL map it to a consistent, predefined color according to the status-color mapping configuration.

Validates: Requirements 10.6

Property 8: Refund Amount Validation

For any refund form submission, the validation SHALL reject refund amounts that exceed the original pay amount of the order.

Validates: Requirements 11.3

Property 9: Permission-Based UI Rendering

For any UI element protected by v-permission directive and any user permission set, the element SHALL be visible if and only if the user has the required permission.

Validates: Requirements 19.1

Property 10: Dictionary Fallback Behavior

For any field using dictionary components (DictSelect, DictRadio) where the dictionary data is unavailable or the value is not found in the dictionary, the component SHALL display the raw value with a visual warning indicator.

Validates: Requirements 20.4

Testing Strategy

测试框架

  • 单元测试: Vitest + Vue Test Utils
  • 属性测试: fast-check
  • 组件测试: @vue/test-utils

测试分层

  1. 单元测试: 测试工具函数、验证逻辑、数据转换
  2. 组件测试: 测试 Vue 组件的渲染和交互
  3. 属性测试: 验证核心业务规则的正确性

测试文件结构

src/
├── api/business/__tests__/
│   ├── dashboard.test.ts
│   ├── config.test.ts
│   ├── content.test.ts
│   └── ...
├── views/business/__tests__/
│   ├── config.test.ts
│   ├── banner.test.ts
│   └── ...
└── utils/__tests__/
    ├── validation.test.ts
    └── error-handler.test.ts

属性测试配置

// 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: {
      provider: 'v8',
      reporter: ['text', 'json', 'html']
    }
  }
})

属性测试示例

// src/utils/__tests__/validation.property.test.ts
import { describe, it, expect } from 'vitest'
import * as fc from 'fast-check'
import { validatePrice, validateCommissionRate } from '../validation'

describe('Validation Properties', () => {
  // Feature: admin-frontend, Property 3: Config Value Validation
  it('should reject non-positive prices', () => {
    fc.assert(
      fc.property(
        fc.double({ max: 0, noNaN: true }),
        (price) => {
          expect(validatePrice(price)).toBe(false)
        }
      ),
      { numRuns: 100 }
    )
  })

  // Feature: admin-frontend, Property 3: Config Value Validation
  it('should accept positive prices', () => {
    fc.assert(
      fc.property(
        fc.double({ min: 0.01, max: 1000000, noNaN: true }),
        (price) => {
          expect(validatePrice(price)).toBe(true)
        }
      ),
      { numRuns: 100 }
    )
  })

  // Feature: admin-frontend, Property 3: Config Value Validation
  it('should reject commission rates outside [0, 1]', () => {
    fc.assert(
      fc.property(
        fc.oneof(
          fc.double({ max: -0.01, noNaN: true }),
          fc.double({ min: 1.01, noNaN: true })
        ),
        (rate) => {
          expect(validateCommissionRate(rate)).toBe(false)
        }
      ),
      { numRuns: 100 }
    )
  })

  // Feature: admin-frontend, Property 3: Config Value Validation
  it('should accept commission rates within [0, 1]', () => {
    fc.assert(
      fc.property(
        fc.double({ min: 0, max: 1, noNaN: true }),
        (rate) => {
          expect(validateCommissionRate(rate)).toBe(true)
        }
      ),
      { numRuns: 100 }
    )
  })
})

组件测试示例

// src/views/business/__tests__/banner.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import * as fc from 'fast-check'
import BannerForm from '../content/banner/components/BannerForm.vue'

describe('Banner Form Properties', () => {
  // Feature: admin-frontend, Property 4: Banner Link Type Validation
  it('should require linkUrl when linkType is 1 or 2', () => {
    fc.assert(
      fc.property(
        fc.constantFrom(1, 2),
        (linkType) => {
          const wrapper = mount(BannerForm, {
            props: { formData: { linkType, linkUrl: '', appId: '' } }
          })
          const form = wrapper.vm
          expect(form.rules.linkUrl[0].required).toBe(true)
        }
      ),
      { numRuns: 100 }
    )
  })

  // Feature: admin-frontend, Property 4: Banner Link Type Validation
  it('should require both linkUrl and appId when linkType is 3', () => {
    const wrapper = mount(BannerForm, {
      props: { formData: { linkType: 3, linkUrl: '', appId: '' } }
    })
    const form = wrapper.vm
    expect(form.rules.linkUrl[0].required).toBe(true)
    expect(form.rules.appId[0].required).toBe(true)
  })
})

测试覆盖要求

  • 工具函数覆盖率 > 90%
  • 验证逻辑 100% 覆盖
  • 核心业务组件覆盖率 > 80%
  • 每个属性测试至少运行 100 次迭代