1544 lines
43 KiB
Markdown
1544 lines
43 KiB
Markdown
# 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
|
||
|
||
### 系统架构图
|
||
|
||
```mermaid
|
||
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 服务层设计
|
||
|
||
#### 通用类型定义
|
||
|
||
```typescript
|
||
// 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)
|
||
|
||
```typescript
|
||
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)
|
||
|
||
```typescript
|
||
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)
|
||
|
||
```typescript
|
||
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)
|
||
|
||
```typescript
|
||
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)
|
||
|
||
```typescript
|
||
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)
|
||
|
||
```typescript
|
||
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)
|
||
|
||
```typescript
|
||
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)
|
||
|
||
```typescript
|
||
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. 路由配置设计
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
### 页面组件数据模型
|
||
|
||
#### 通用列表页面状态
|
||
|
||
```typescript
|
||
// 通用列表页面状态接口
|
||
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> {
|
||
// 额外的页面特定状态
|
||
}
|
||
```
|
||
|
||
#### 仪表盘页面数据模型
|
||
|
||
```typescript
|
||
interface DashboardState {
|
||
loading: boolean
|
||
overview: DashboardOverview | null
|
||
trends: TrendsData | null
|
||
pendingItems: PendingItems | null
|
||
dateRange: [string, string]
|
||
}
|
||
```
|
||
|
||
#### 配置页面数据模型
|
||
|
||
```typescript
|
||
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
|
||
|
||
### 前端错误处理策略
|
||
|
||
```typescript
|
||
// 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**
|
||
|
||
### Property 4: Banner Link Type Validation
|
||
|
||
*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
|
||
```
|
||
|
||
### 属性测试配置
|
||
|
||
```typescript
|
||
// 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']
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
### 属性测试示例
|
||
|
||
```typescript
|
||
// 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 }
|
||
)
|
||
})
|
||
})
|
||
```
|
||
|
||
### 组件测试示例
|
||
|
||
```typescript
|
||
// 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 次迭代
|