43 KiB
Design Document: Admin Frontend Business Modules
Overview
本设计文档描述了学业邑规划 MiAssessment 管理后台前端业务模块的技术架构和实现方案。系统基于 Vue 3.5 + TypeScript 5.6 + Element Plus 2.9 技术栈,在现有的前端模板基础上扩展8个核心业务模块页面。
设计目标
- 组件化设计: 页面组件独立封装,复用现有基础组件
- 类型安全: 使用 TypeScript 定义所有接口和数据类型
- 统一风格: 遵循现有代码风格和 Element Plus 设计规范
- 权限集成: 与现有权限系统无缝集成
- 可测试性: 支持单元测试和属性测试
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
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
测试分层
- 单元测试: 测试工具函数、验证逻辑、数据转换
- 组件测试: 测试 Vue 组件的渲染和交互
- 属性测试: 验证核心业务规则的正确性
测试文件结构
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 次迭代