feat(backend): 添加 Swagger 文档并优化认证接口

- 在 .env 文件中添加 ENABLE_SWAGGER 环境变量
- 在 app.js 中集成 Swagger UI
- 重构 auth 路由,添加请求参数验证
- 更新 API 文档,遵循 OpenAPI 3.0 规范
-优化认证接口的错误处理和响应格式
This commit is contained in:
2025-08-30 15:29:51 +08:00
parent 7f9bfbb381
commit 0cad74b06f
28 changed files with 2123 additions and 691 deletions

View File

@@ -25,6 +25,7 @@
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.0.0",
"less": "^4.4.1",
"prettier": "^2.8.0",
"typescript": "~5.0.0",
"vite": "^4.3.0",
@@ -1334,6 +1335,18 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/copy-anything": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
"dev": true,
"dependencies": {
"is-what": "^3.14.1"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/core-js": {
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
@@ -1475,6 +1488,19 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/errno": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
"dev": true,
"optional": true,
"dependencies": {
"prr": "~1.0.1"
},
"bin": {
"errno": "cli.js"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2066,6 +2092,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"optional": true
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -2126,6 +2159,19 @@
"he": "bin/he"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2135,6 +2181,19 @@
"node": ">= 4"
}
},
"node_modules/image-size": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
"dev": true,
"optional": true,
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -2224,6 +2283,12 @@
"node": ">=0.10.0"
}
},
"node_modules/is-what": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
"dev": true
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2274,6 +2339,38 @@
"json-buffer": "3.0.1"
}
},
"node_modules/less": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/less/-/less-4.4.1.tgz",
"integrity": "sha512-X9HKyiXPi0f/ed0XhgUlBeFfxrlDP3xR4M7768Zl+WXLUViuL9AOPPJP4nCV0tgRWvTYvpNmN0SFhZOQzy16PA==",
"dev": true,
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",
"tslib": "^2.3.0"
},
"bin": {
"lessc": "bin/lessc"
},
"engines": {
"node": ">=14"
},
"optionalDependencies": {
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"needle": "^3.1.0",
"source-map": "~0.6.0"
}
},
"node_modules/less/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -2337,6 +2434,30 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"dev": true,
"optional": true,
"dependencies": {
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"optional": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -2367,6 +2488,19 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"optional": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -2444,6 +2578,23 @@
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
"dev": true
},
"node_modules/needle": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
"dev": true,
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.3",
"sax": "^1.2.4"
},
"bin": {
"needle": "bin/needle"
},
"engines": {
"node": ">= 4.4.x"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -2524,6 +2675,15 @@
"node": ">=6"
}
},
"node_modules/parse-node-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
"dev": true,
"engines": {
"node": ">= 0.10"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -2583,6 +2743,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"dev": true,
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/pinia": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
@@ -2673,6 +2843,13 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
"dev": true,
"optional": true
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2781,6 +2958,20 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"optional": true
},
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"dev": true,
"optional": true
},
"node_modules/scroll-into-view-if-needed": {
"version": "2.2.31",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
@@ -2836,6 +3027,16 @@
"node": ">=8"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

View File

@@ -11,14 +11,14 @@
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"vue": "^3.3.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"@ant-design/icons-vue": "^6.1.0",
"ant-design-vue": "^4.0.0",
"axios": "^1.4.0",
"@ant-design/icons-vue": "^6.1.0",
"dayjs": "^1.11.0",
"lodash-es": "^4.17.21"
"lodash-es": "^4.17.21",
"pinia": "^2.1.0",
"vue": "^3.3.0",
"vue-router": "^4.2.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.0",
@@ -26,16 +26,17 @@
"@vitejs/plugin-vue": "^4.2.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.4.0",
"typescript": "~5.0.0",
"vite": "^4.3.0",
"vue-tsc": "^1.4.0",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.0.0",
"prettier": "^2.8.0"
"less": "^4.4.1",
"prettier": "^2.8.0",
"typescript": "~5.0.0",
"vite": "^4.3.0",
"vue-tsc": "^1.4.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
}

View File

@@ -0,0 +1,110 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
// 动物类型
export type AnimalType = 'alpaca' | 'dog' | 'cat' | 'rabbit'
// 动物状态
export type AnimalStatus = 'available' | 'claimed' | 'reserved'
// 认领状态
export type ClaimStatus = 'pending' | 'approved' | 'rejected' | 'completed'
// 动物数据结构
export interface Animal {
id: number
name: string
type: AnimalType
breed: string
age: number
price: number
status: AnimalStatus
image_url: string
description: string
created_at: string
updated_at: string
}
// 动物认领记录
export interface AnimalClaim {
id: number
animal_id: number
animal_name: string
animal_image: string
user_name: string
user_phone: string
status: ClaimStatus
applied_at: string
processed_at: string
}
// 动物查询参数
export interface AnimalQueryParams {
page?: number
pageSize?: number
keyword?: string
type?: AnimalType
status?: AnimalStatus
}
// 认领记录查询参数
export interface ClaimQueryParams {
page?: number
pageSize?: number
keyword?: string
status?: ClaimStatus
}
// API响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
pagination?: {
current: number
pageSize: number
total: number
totalPages: number
}
}
// 获取动物列表
export const getAnimals = async (params?: AnimalQueryParams): Promise<ApiResponse<Animal[]>> => {
return request.get<ApiResponse<Animal[]>>('/animals', { params })
}
// 获取动物详情
export const getAnimal = async (id: number): Promise<ApiResponse<Animal>> => {
return request.get<ApiResponse<Animal>>(`/animals/${id}`)
}
// 创建动物
export const createAnimal = async (animalData: Partial<Animal>): Promise<ApiResponse<Animal>> => {
return request.post<ApiResponse<Animal>>('/animals', animalData)
}
// 更新动物
export const updateAnimal = async (id: number, animalData: Partial<Animal>): Promise<ApiResponse<Animal>> => {
return request.put<ApiResponse<Animal>>(`/animals/${id}`, animalData)
}
// 删除动物
export const deleteAnimal = async (id: number): Promise<ApiResponse<void>> => {
return request.delete<ApiResponse<void>>(`/animals/${id}`)
}
// 获取认领记录列表
export const getAnimalClaims = async (params?: ClaimQueryParams): Promise<ApiResponse<AnimalClaim[]>> => {
return request.get<ApiResponse<AnimalClaim[]>>('/animals/claims', { params })
}
// 审核动物认领(通过)
export const approveAnimalClaim = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/animals/claims/${id}/approve`)
}
// 拒绝动物认领
export const rejectAnimalClaim = async (id: number, reason: string): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/animals/claims/${id}/reject`, { reason })
}

View File

@@ -151,189 +151,12 @@ export const authAPI = {
request.post('/auth/logout')
}
// 用户管理API
export const userAPI = {
// 获取用户列表
getUsers: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
}) => request.get('/users', { params }),
// 获取用户详情
getUser: (id: number) => request.get(`/users/${id}`),
// 创建用户
createUser: (data: any) => request.post('/users', data),
// 更新用户
updateUser: (id: number, data: any) => request.put(`/users/${id}`, data),
// 删除用户
deleteUser: (id: number) => request.delete(`/users/${id}`),
// 批量操作
batchUsers: (ids: number[], action: string) =>
request.post('/users/batch', { ids, action })
}
// 商家管理API
export const merchantAPI = {
// 获取商家列表
getMerchants: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
type?: string
}) => request.get('/merchants', { params }),
// 获取商家详情
getMerchant: (id: number) => request.get(`/merchants/${id}`),
// 审核商家
approveMerchant: (id: number, data: any) => request.post(`/merchants/${id}/approve`, data),
// 拒绝商家
rejectMerchant: (id: number, reason: string) => request.post(`/merchants/${id}/reject`, { reason }),
// 禁用商家
disableMerchant: (id: number) => request.post(`/merchants/${id}/disable`),
// 启用商家
enableMerchant: (id: number) => request.post(`/merchants/${id}/enable`)
}
// 旅行管理API
export const travelAPI = {
// 获取旅行计划列表
getTravelPlans: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
destination?: string
}) => request.get('/travel/plans', { params }),
// 获取旅行计划详情
getTravelPlan: (id: number) => request.get(`/travel/plans/${id}`),
// 审核旅行计划
approveTravelPlan: (id: number) => request.post(`/travel/plans/${id}/approve`),
// 拒绝旅行计划
rejectTravelPlan: (id: number, reason: string) => request.post(`/travel/plans/${id}/reject`, { reason }),
// 关闭旅行计划
closeTravelPlan: (id: number) => request.post(`/travel/plans/${id}/close`)
}
// 动物管理API
export const animalAPI = {
// 获取动物列表
getAnimals: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
type?: string
}) => request.get('/animals', { params }),
// 获取动物详情
getAnimal: (id: number) => request.get(`/animals/${id}`),
// 创建动物
createAnimal: (data: any) => request.post('/animals', data),
// 更新动物
updateAnimal: (id: number, data: any) => request.put(`/animals/${id}`, data),
// 删除动物
deleteAnimal: (id: number) => request.delete(`/animals/${id}`),
// 审核动物认领
approveAnimalClaim: (id: number) => request.post(`/animals/claims/${id}/approve`),
// 拒绝动物认领
rejectAnimalClaim: (id: number, reason: string) => request.post(`/animals/claims/${id}/reject`, { reason })
}
// 订单管理API
export const orderAPI = {
// 获取订单列表
getOrders: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
type?: string
startDate?: string
endDate?: string
}) => request.get('/orders', { params }),
// 获取订单详情
getOrder: (id: number) => request.get(`/orders/${id}`),
// 更新订单状态
updateOrderStatus: (id: number, status: string) => request.put(`/orders/${id}/status`, { status }),
// 导出订单
exportOrders: (params: any) => request.get('/orders/export', {
params,
responseType: 'blob'
})
}
// 推广管理API
export const promotionAPI = {
// 获取推广数据
getPromotionStats: () => request.get('/promotion/stats'),
// 获取推广记录
getPromotionRecords: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
}) => request.get('/promotion/records', { params }),
// 审核提现申请
approveWithdrawal: (id: number) => request.post(`/promotion/withdrawals/${id}/approve`),
// 拒绝提现申请
rejectWithdrawal: (id: number, reason: string) => request.post(`/promotion/withdrawals/${id}/reject`, { reason }),
// 导出推广数据
exportPromotionData: (params: any) => request.get('/promotion/export', {
params,
responseType: 'blob'
})
}
// 系统管理API
export const systemAPI = {
// 获取系统配置
getConfig: () => request.get('/system/config'),
// 更新系统配置
updateConfig: (data: any) => request.put('/system/config', data),
// 获取操作日志
getOperationLogs: (params?: {
page?: number
pageSize?: number
search?: string
action?: string
startDate?: string
endDate?: string
}) => request.get('/system/logs', { params }),
// 清理缓存
clearCache: () => request.post('/system/cache/clear'),
// 系统健康检查
healthCheck: () => request.get('/system/health')
}
export * from './user'
export * from './merchant'
export * from './travel'
export * from './animal'
export * from './order'
export * from './promotion'
export * from './system'
export default api

View File

@@ -0,0 +1,73 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
// 商家类型
export type MerchantType = 'flower_shop' | 'activity_organizer' | 'farm_owner'
// 商家状态
export type MerchantStatus = 'pending' | 'approved' | 'rejected' | 'disabled'
// 商家数据结构
export interface Merchant {
id: number
business_name: string
merchant_type: MerchantType
contact_person: string
contact_phone: string
status: MerchantStatus
created_at: string
updated_at: string
}
// 商家查询参数
export interface MerchantQueryParams {
page?: number
pageSize?: number
keyword?: string
type?: MerchantType
status?: MerchantStatus
}
// API响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
pagination?: {
current: number
pageSize: number
total: number
totalPages: number
}
}
// 获取商家列表
export const getMerchants = async (params?: MerchantQueryParams): Promise<ApiResponse<Merchant[]>> => {
return request.get<ApiResponse<Merchant[]>>('/merchants', { params })
}
// 获取商家详情
export const getMerchant = async (id: number): Promise<ApiResponse<Merchant>> => {
return request.get<ApiResponse<Merchant>>(`/merchants/${id}`)
}
// 审核商家(通过)
export const approveMerchant = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/merchants/${id}/approve`)
}
// 拒绝商家入驻申请
export const rejectMerchant = async (id: number, reason: string): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/merchants/${id}/reject`, { reason })
}
// 禁用商家
export const disableMerchant = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/merchants/${id}/disable`)
}
// 启用商家
export const enableMerchant = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/merchants/${id}/enable`)
}

View File

@@ -0,0 +1,95 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
// 订单状态
export type OrderStatus = 'pending' | 'paid' | 'shipped' | 'completed' | 'cancelled' | 'refunded'
// 支付方式
export type PaymentMethod = 'wechat' | 'alipay' | 'bank' | 'balance'
// 订单数据结构
export interface Order {
id: number
order_no: string
user_id: number
user_name: string
user_phone: string
amount: number
status: OrderStatus
payment_method: PaymentMethod
created_at: string
paid_at: string
shipped_at: string
completed_at: string
}
// 订单查询参数
export interface OrderQueryParams {
page?: number
pageSize?: number
order_no?: string
status?: OrderStatus
orderTime?: [string, string]
}
// 统计数据
export interface OrderStatistics {
today_orders: number
today_sales: number
month_orders: number
month_sales: number
}
// API响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
pagination?: {
current: number
pageSize: number
total: number
totalPages: number
}
}
// 获取订单列表
export const getOrders = async (params?: OrderQueryParams): Promise<ApiResponse<Order[]>> => {
return request.get<ApiResponse<Order[]>>('/orders', { params })
}
// 获取订单详情
export const getOrder = async (id: number): Promise<ApiResponse<Order>> => {
return request.get<ApiResponse<Order>>(`/orders/${id}`)
}
// 更新订单状态
export const updateOrderStatus = async (id: number, status: OrderStatus): Promise<ApiResponse<void>> => {
return request.put<ApiResponse<void>>(`/orders/${id}/status`, { status })
}
// 发货
export const shipOrder = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/orders/${id}/ship`)
}
// 完成订单
export const completeOrder = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/orders/${id}/complete`)
}
// 取消订单
export const cancelOrder = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/orders/${id}/cancel`)
}
// 退款
export const refundOrder = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/orders/${id}/refund`)
}
// 获取订单统计数据
export const getOrderStatistics = async (): Promise<ApiResponse<OrderStatistics>> => {
return request.get<ApiResponse<OrderStatistics>>('/orders/statistics')
}

View File

@@ -0,0 +1,133 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
// 推广活动状态
export type PromotionStatus = 'active' | 'upcoming' | 'ended' | 'paused'
// 奖励类型
export type RewardType = 'cash' | 'points' | 'coupon'
// 奖励状态
export type RewardStatus = 'pending' | 'issued' | 'failed'
// 推广活动数据结构
export interface PromotionActivity {
id: number
name: string
description: string
reward_type: RewardType
reward_amount: number
status: PromotionStatus
start_time: string
end_time: string
max_participants: number
current_participants: number
created_at: string
updated_at: string
}
// 奖励记录
export interface RewardRecord {
id: number
user_id: number
user_name: string
user_phone: string
activity_id: number
activity_name: string
reward_type: RewardType
reward_amount: number
status: RewardStatus
issued_at: string
created_at: string
}
// 推广活动查询参数
export interface PromotionQueryParams {
page?: number
pageSize?: number
name?: string
status?: PromotionStatus
activityTime?: [string, string]
}
// 奖励记录查询参数
export interface RewardQueryParams {
page?: number
pageSize?: number
user?: string
reward_type?: RewardType
status?: RewardStatus
}
// 统计数据
export interface PromotionStatistics {
total_activities: number
active_activities: number
total_rewards: number
issued_rewards: number
total_amount: number
}
// API响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
pagination?: {
current: number
pageSize: number
total: number
totalPages: number
}
}
// 获取推广活动列表
export const getPromotionActivities = async (params?: PromotionQueryParams): Promise<ApiResponse<PromotionActivity[]>> => {
return request.get<ApiResponse<PromotionActivity[]>>('/promotion/activities', { params })
}
// 获取推广活动详情
export const getPromotionActivity = async (id: number): Promise<ApiResponse<PromotionActivity>> => {
return request.get<ApiResponse<PromotionActivity>>(`/promotion/activities/${id}`)
}
// 创建推广活动
export const createPromotionActivity = async (activityData: Partial<PromotionActivity>): Promise<ApiResponse<PromotionActivity>> => {
return request.post<ApiResponse<PromotionActivity>>('/promotion/activities', activityData)
}
// 更新推广活动
export const updatePromotionActivity = async (id: number, activityData: Partial<PromotionActivity>): Promise<ApiResponse<PromotionActivity>> => {
return request.put<ApiResponse<PromotionActivity>>(`/promotion/activities/${id}`, activityData)
}
// 删除推广活动
export const deletePromotionActivity = async (id: number): Promise<ApiResponse<void>> => {
return request.delete<ApiResponse<void>>(`/promotion/activities/${id}`)
}
// 暂停推广活动
export const pausePromotionActivity = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/promotion/activities/${id}/pause`)
}
// 恢复推广活动
export const resumePromotionActivity = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/promotion/activities/${id}/resume`)
}
// 获取奖励记录列表
export const getRewardRecords = async (params?: RewardQueryParams): Promise<ApiResponse<RewardRecord[]>> => {
return request.get<ApiResponse<RewardRecord[]>>('/promotion/rewards', { params })
}
// 发放奖励
export const issueReward = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/promotion/rewards/${id}/issue`)
}
// 获取推广统计数据
export const getPromotionStatistics = async (): Promise<ApiResponse<PromotionStatistics>> => {
return request.get<ApiResponse<PromotionStatistics>>('/promotion/statistics')
}

View File

@@ -0,0 +1,120 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
// 服务类型
export type ServiceType = 'database' | 'cache' | 'mq'
// 服务状态
export type ServiceStatus = 'running' | 'stopped'
// 系统服务数据结构
export interface Service {
id: number
name: string
type: ServiceType
description: string
status: ServiceStatus
}
// 系统配置
export interface SystemSettings {
systemName: string
systemVersion: string
maintenanceMode: boolean
sessionTimeout: number
pageSize: number
enableSwagger: boolean
}
// 系统信息
export interface SystemInfo {
version: string
environment: string
uptime: string
startTime: string
}
// 数据库状态
export interface DatabaseStatus {
status: ServiceStatus
type: string
connections: string
queriesPerMinute: number
}
// 缓存状态
export interface CacheStatus {
status: ServiceStatus
memoryUsage: string
hitRate: string
keyCount: number
}
// API响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
}
// 获取系统服务列表
export const getServices = async (): Promise<ApiResponse<Service[]>> => {
return request.get<ApiResponse<Service[]>>('/system/services')
}
// 启动服务
export const startService = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/system/services/${id}/start`)
}
// 停止服务
export const stopService = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/system/services/${id}/stop`)
}
// 获取系统信息
export const getSystemInfo = async (): Promise<ApiResponse<SystemInfo>> => {
return request.get<ApiResponse<SystemInfo>>('/system/info')
}
// 获取数据库状态
export const getDatabaseStatus = async (): Promise<ApiResponse<DatabaseStatus>> => {
return request.get<ApiResponse<DatabaseStatus>>('/system/database')
}
// 获取缓存状态
export const getCacheStatus = async (): Promise<ApiResponse<CacheStatus>> => {
return request.get<ApiResponse<CacheStatus>>('/system/cache')
}
// 获取系统配置
export const getSystemSettings = async (): Promise<ApiResponse<SystemSettings>> => {
return request.get<ApiResponse<SystemSettings>>('/system/settings')
}
// 更新系统配置
export const updateSystemSettings = async (settings: SystemSettings): Promise<ApiResponse<void>> => {
return request.put<ApiResponse<void>>('/system/settings', settings)
}
// 获取操作日志
export const getOperationLogs = async (params?: {
page?: number
pageSize?: number
search?: string
startDate?: string
endDate?: string
}): Promise<ApiResponse<any[]>> => {
return request.get<ApiResponse<any[]>>('/system/logs', { params })
}
// 清理缓存
export const clearCache = async (): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>('/system/cache/clear')
}
// 系统健康检查
export const healthCheck = async (): Promise<ApiResponse<any>> => {
return request.get<ApiResponse<any>>('/system/health')
}

View File

@@ -0,0 +1,68 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
// 旅行计划状态
export type TravelStatus = 'recruiting' | 'full' | 'completed' | 'cancelled'
// 旅行计划数据结构
export interface TravelPlan {
id: number
destination: string
start_date: string
end_date: string
budget: number
max_members: number
current_members: number
status: TravelStatus
creator: string
created_at: string
updated_at: string
}
// 旅行计划查询参数
export interface TravelQueryParams {
page?: number
pageSize?: number
destination?: string
status?: TravelStatus
travelTime?: [string, string]
}
// API响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
pagination?: {
current: number
pageSize: number
total: number
totalPages: number
}
}
// 获取旅行计划列表
export const getTravelPlans = async (params?: TravelQueryParams): Promise<ApiResponse<TravelPlan[]>> => {
return request.get<ApiResponse<TravelPlan[]>>('/travel/plans', { params })
}
// 获取旅行计划详情
export const getTravelPlan = async (id: number): Promise<ApiResponse<TravelPlan>> => {
return request.get<ApiResponse<TravelPlan>>(`/travel/plans/${id}`)
}
// 审核旅行计划
export const approveTravelPlan = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/travel/plans/${id}/approve`)
}
// 拒绝旅行计划
export const rejectTravelPlan = async (id: number, reason: string): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/travel/plans/${id}/reject`, { reason })
}
// 关闭旅行计划
export const closeTravelPlan = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/travel/plans/${id}/close`)
}

View File

@@ -0,0 +1,81 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
// 用户状态类型
export type UserStatus = 'active' | 'inactive' | 'banned'
// 用户等级类型
export type UserLevel = number
// 用户数据结构
export interface User {
id: number
openid: string
nickname: string
avatar: string
gender: string
birthday: string
phone: string
email: string
status: UserStatus
level: UserLevel
points: number
created_at: string
updated_at: string
}
// 用户查询参数
export interface UserQueryParams {
page?: number
pageSize?: number
keyword?: string
status?: UserStatus
registerTime?: [string, string]
}
// API响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
pagination?: {
current: number
pageSize: number
total: number
totalPages: number
}
}
// 获取用户列表
export const getUsers = async (params?: UserQueryParams): Promise<ApiResponse<User[]>> => {
return request.get<ApiResponse<User[]>>('/users', { params })
}
// 获取用户详情
export const getUser = async (id: number): Promise<ApiResponse<User>> => {
return request.get<ApiResponse<User>>(`/users/${id}`)
}
// 创建用户
export const createUser = async (userData: Partial<User>): Promise<ApiResponse<User>> => {
return request.post<ApiResponse<User>>('/users', userData)
}
// 更新用户
export const updateUser = async (id: number, userData: Partial<User>): Promise<ApiResponse<User>> => {
return request.put<ApiResponse<User>>(`/users/${id}`, userData)
}
// 删除用户
export const deleteUser = async (id: number): Promise<ApiResponse<void>> => {
return request.delete<ApiResponse<void>>(`/users/${id}`)
}
// 批量操作用户
export const batchUsers = async (
ids: number[],
action: 'delete' | 'ban' | 'activate'
): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>('/users/batch', { ids, action })
}

View File

@@ -0,0 +1,35 @@
<template>
<div class="not-found">
<a-result
status="404"
title="404"
sub-title="抱歉您访问的页面不存在"
>
<template #extra>
<a-button type="primary" @click="goHome">
返回首页
</a-button>
</template>
</a-result>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const goHome = () => {
router.push('/')
}
</script>
<style scoped lang="less">
.not-found {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f2f5;
}
</style>

View File

@@ -243,31 +243,8 @@ import {
CheckOutlined,
CloseOutlined
} from '@ant-design/icons-vue'
interface Animal {
id: number
name: string
type: string
breed: string
age: number
price: number
status: string
image_url: string
description: string
created_at: string
}
interface AnimalClaim {
id: number
animal_id: number
animal_name: string
animal_image: string
user_name: string
user_phone: string
status: string
applied_at: string
processed_at: string
}
import { getAnimals, deleteAnimal, getAnimalClaims, approveAnimalClaim, rejectAnimalClaim } from '@/api/animal'
import type { Animal, AnimalClaim } from '@/api/animal'
interface SearchForm {
keyword: string
@@ -295,52 +272,13 @@ const claimSearchForm = reactive<ClaimSearchForm>({
status: ''
})
// 模拟数据
const animalList = ref<Animal[]>([
{
id: 1,
name: '小白',
type: 'alpaca',
breed: '苏利羊驼',
age: 2,
price: 1000,
status: 'available',
image_url: 'https://api.dicebear.com/7.x/bottts/svg?seed=alpaca',
description: '温顺可爱的羊驼',
created_at: '2024-01-10'
},
{
id: 2,
name: '旺财',
type: 'dog',
breed: '金毛寻回犬',
age: 1,
price: 800,
status: 'claimed',
image_url: 'https://api.dicebear.com/7.x/bottts/svg?seed=dog',
description: '活泼聪明的金毛',
created_at: '2024-02-15'
}
])
const claimList = ref<AnimalClaim[]>([
{
id: 1,
animal_id: 1,
animal_name: '小白',
animal_image: 'https://api.dicebear.com/7.x/bottts/svg?seed=alpaca',
user_name: '张先生',
user_phone: '13800138000',
status: 'pending',
applied_at: '2024-03-01',
processed_at: ''
}
])
const animalList = ref<Animal[]>([])
const claimList = ref<AnimalClaim[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
@@ -349,7 +287,7 @@ const pagination = reactive({
const claimPagination = reactive({
current: 1,
pageSize: 20,
total: 30,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
@@ -459,7 +397,7 @@ const claimColumns = [
},
{
title: '操作',
极速版 key: 'actions',
key: 'actions',
width: 150,
align: 'center'
}
@@ -537,7 +475,16 @@ onMounted(() => {
const loadAnimals = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
const response = await getAnimals({
page: pagination.current,
pageSize: pagination.pageSize,
keyword: searchForm.keyword,
type: searchForm.type,
status: searchForm.status
})
animalList.value = response.data
pagination.total = response.pagination?.total || 0
} catch (error) {
message.error('加载动物列表失败')
} finally {
@@ -548,7 +495,15 @@ const loadAnimals = async () => {
const loadClaims = async () => {
claimLoading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
const response = await getAnimalClaims({
page: claimPagination.current,
pageSize: claimPagination.pageSize,
keyword: claimSearchForm.keyword,
status: claimSearchForm.status
})
claimList.value = response.data
claimPagination.total = response.pagination?.total || 0
} catch (error) {
message.error('加载认领记录失败')
} finally {
@@ -616,7 +571,7 @@ const handleEditAnimal = (record: Animal) => {
message.info(`编辑动物: ${record.name}`)
}
const handleDeleteAnimal = (record: Animal) => {
const handleDeleteAnimal = async (record: Animal) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除动物 "${record.name}" 吗?`,
@@ -624,6 +579,7 @@ const handleDeleteAnimal = (record: Animal) => {
okType: 'danger',
onOk: async () => {
try {
await deleteAnimal(record.id)
message.success('动物已删除')
loadAnimals()
} catch (error) {
@@ -633,12 +589,13 @@ const handleDeleteAnimal = (record: Animal) => {
})
}
const handleApproveClaim = (record: AnimalClaim) => {
const handleApproveClaim = async (record: AnimalClaim) => {
Modal.confirm({
title: '确认通过',
content: `确定要通过用户 "${record.user_name}" 的认领申请吗?`,
onOk: async () => {
try {
await approveAnimalClaim(record.id)
message.success('认领申请已通过')
loadClaims()
} catch (error) {
@@ -648,12 +605,13 @@ const handleApproveClaim = (record: AnimalClaim) => {
})
}
const handleRejectClaim = (record: AnimalClaim) => {
const handleRejectClaim = async (record: AnimalClaim) => {
Modal.confirm({
title: '确认拒绝',
content: `确定要拒绝用户 "${record.user_name}" 的认领申请吗?`,
onOk: async () => {
try {
await rejectAnimalClaim(record.id, '拒绝原因')
message.success('认领申请已拒绝')
loadClaims()
} catch (error) {

View File

@@ -150,16 +150,8 @@ import {
StopOutlined,
PlayCircleOutlined
} from '@ant-design/icons-vue'
interface Merchant {
id: number
business_name: string
merchant_type: string
contact_person: string
contact_phone: string
status: string
created_at: string
}
import { getMerchants, approveMerchant, rejectMerchant, disableMerchant, enableMerchant } from '@/api/merchant'
import type { Merchant } from '@/api/merchant'
interface SearchForm {
keyword: string
@@ -175,32 +167,11 @@ const searchForm = reactive<SearchForm>({
status: ''
})
// 模拟商家数据
const merchantList = ref<Merchant[]>([
{
id: 1,
business_name: '鲜花坊',
merchant_type: 'flower_shop',
contact_person: '张经理',
contact_phone: '13800138000',
status: 'approved',
created_at: '2024-01-15'
},
{
id: 2,
business_name: '阳光农场',
merchant_type: 'farm_owner',
contact_person: '李场主',
contact_phone: '13800138001',
status: 'pending',
created_at: '2024-02-20'
}
])
const merchantList = ref<Merchant[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
@@ -300,8 +271,16 @@ onMounted(() => {
const loadMerchants = async () => {
loading.value = true
try {
// TODO: 调用真实API
await new Promise(resolve => setTimeout(resolve, 500))
const response = await getMerchants({
page: pagination.current,
pageSize: pagination.pageSize,
keyword: searchForm.keyword,
type: searchForm.type,
status: searchForm.status
})
merchantList.value = response.data
pagination.total = response.pagination?.total || 0
} catch (error) {
message.error('加载商家列表失败')
} finally {
@@ -345,6 +324,7 @@ const handleApprove = async (record: Merchant) => {
content: `确定要通过商家 "${record.business_name}" 的入驻申请吗?`,
onOk: async () => {
try {
await approveMerchant(record.id)
message.success('商家入驻申请已通过')
loadMerchants()
} catch (error) {
@@ -360,6 +340,7 @@ const handleReject = async (record: Merchant) => {
content: `确定要拒绝商家 "${record.business_name}" 的入驻申请吗?`,
onOk: async () => {
try {
await rejectMerchant(record.id, '拒绝原因')
message.success('商家入驻申请已拒绝')
loadMerchants()
} catch (error) {
@@ -375,6 +356,7 @@ const handleDisable = async (record: Merchant) => {
content: `确定要禁用商家 "${record.business_name}" 吗?`,
onOk: async () => {
try {
await disableMerchant(record.id)
message.success('商家已禁用')
loadMerchants()
} catch (error) {
@@ -390,6 +372,7 @@ const handleEnable = async (record: Merchant) => {
content: `确定要启用商家 "${record.business_name}" 吗?`,
onOk: async () => {
try {
await enableMerchant(record.id)
message.success('商家已启用')
loadMerchants()
} catch (error) {

View File

@@ -31,7 +31,7 @@
<a-select-option value="shipped">已发货</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
<a-select-option value极速版="refunded">已退款</a-select-option>
<a-select-option value="refunded">已退款</a-select-option>
</a-select>
</a-form-item>
@@ -92,11 +92,11 @@
<template v-if="['pending', 'paid'].includes(record.status)">
<a-button size="small" danger @click="handleCancel(record)">
<极速版CloseOutlined />取消
<CloseOutlined />取消
</a-button>
</template>
<template v-if="record.status === '极速版paid'">
<template v-if="record.status === 'paid'">
<a-button size="small" danger @click="handleRefund(record)">
<RollbackOutlined />退款
</a-button>
@@ -120,10 +120,10 @@
<a-statistic title="今日销售额" :value="statistics.today_sales" :precision="2" prefix="¥" :value-style="{ color: '#cf1322' }" />
</a-col>
<a-col :span="6">
<a-statistic title="本月订单" :value="statistics.month_orders" :precision="0" :value-style="{ color: '#1890极速版ff' }" />
<a-statistic title="本月订单" :value="statistics.month_orders" :precision="0" :value-style="{ color: '#1890ff' }" />
</a-col>
<a-col :span="6">
<a-statistic title="本月销售额" :value="statistics.month_sales" :precision="2" prefix="¥" :极速版value-style="{ color: '#722ed1' }" />
<a-statistic title="本月销售额" :value="statistics.month_sales" :precision="2" prefix="¥" :value-style="{ color: '#722ed1' }" />
</a-col>
</a-row>
</a-card>
@@ -156,21 +156,8 @@ import {
RollbackOutlined,
ShoppingOutlined
} from '@ant-design/icons-vue'
interface Order {
id: number
order_no: string
user_id: number
user_name: string
user_phone: string
amount: number
status: string
payment_method: string
created_at: string
paid_at: string
shipped_at: string
completed_at: string
}
import { getOrders, updateOrderStatus, getOrderStatistics } from '@/api/order'
import type { Order, OrderStatistics } from '@/api/order'
interface SearchForm {
order_no: string
@@ -178,13 +165,6 @@ interface SearchForm {
orderTime: any[]
}
interface Statistics {
today_orders: number
today_sales: number
month_orders: number
month_sales: number
}
const activeTab = ref('orders')
const loading = ref(false)
@@ -194,48 +174,18 @@ const searchForm = reactive<SearchForm>({
orderTime: []
})
const statistics = reactive<Statistics>({
const statistics = reactive<OrderStatistics>({
today_orders: 0,
today_sales: 0,
month_orders: 0,
month_sales: 0
})
const orderList = ref<Order[]>([
{
id: 1,
order_no: 'ORD202403150001',
user_id: 1001,
user_name: '张先生',
user_phone: '13800138000',
amount: 299.99,
status: 'paid',
payment_method: 'wechat',
created_at: '2024-03-15 10:30:00',
paid_at: '2024-03-15 10:35:00',
shipped_at: '',
completed_at: ''
},
{
id: 2,
order_no: 'ORD202403140002',
user_id: 1002,
user_name: '李女士',
user_phone: '13800138001',
amount: 极速版199.99,
status: 'shipped',
payment_method: 'alipay',
created_at: '2024-03-14 14:20:00',
paid_at: '2024-03-14 14:25:00',
shipped_at: '2024-03-15 09:00:00',
completed_at: ''
}
])
const orderList = ref<Order[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
@@ -244,7 +194,7 @@ const pagination = reactive({
const orderColumns = [
{ title: '订单号', key: 'order_no', width: 160 },
{ title: '用户', dataIndex: 'user_name', key: 'user_name', width: 100 },
{ title: '联系电话', dataIndex: 'user极速版_phone', key: 'user_phone', width: 120 },
{ title: '联系电话', dataIndex: 'user_phone', key: 'user_phone', width: 120 },
{ title: '金额', key: 'amount', width: 100, align: 'center' },
{ title: '状态', key: 'status', width: 100, align: 'center' },
{ title: '支付方式', key: 'payment_method', width: 100, align: 'center' },
@@ -294,7 +244,15 @@ onMounted(() => {
const loadOrders = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
const response = await getOrders({
page: pagination.current,
pageSize: pagination.pageSize,
order_no: searchForm.order_no,
status: searchForm.status
})
orderList.value = response.data
pagination.total = response.pagination?.total || 0
} catch (error) {
message.error('加载订单列表失败')
} finally {
@@ -304,10 +262,8 @@ const loadOrders = async () => {
const loadStatistics = async () => {
try {
statistics.today_orders = 15
statistics.today_sales = 4500.50
statistics.month_orders = 120
statistics.month_sales = 35600.80
const response = await getOrderStatistics()
Object.assign(statistics, response.data)
} catch (error) {
message.error('加载统计数据失败')
}
@@ -345,7 +301,7 @@ const handleRefresh = () => {
message.success('数据已刷新')
}
const handleTableChange: TableProps['onChange极速版'] = (pag) => {
const handleTableChange: TableProps['onChange'] = (pag) => {
pagination.current = pag.current!
pagination.pageSize = pag.pageSize!
loadOrders()
@@ -361,6 +317,7 @@ const handleShip = async (record: Order) => {
content: `确定要发货订单 "${record.order_no}" 吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'shipped')
message.success('订单已发货')
loadOrders()
} catch (error) {
@@ -376,21 +333,23 @@ const handleComplete = async (record: Order) => {
content: `确定要完成订单 "${record.order_no}" 吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'completed')
message.success('订单已完成')
loadOrders()
} catch (error) {
message.error('操作失败')
}
极速版 }
}
})
}
const handleCancel = async (record: Order) => {
Modal.confirm({
title: '确认取消',
content: `确定要取消订单 "${record.order极速版_no}" 吗?`,
on极速版Ok: async () => {
content: `确定要取消订单 "${record.order_no}" 吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'cancelled')
message.success('订单已取消')
loadOrders()
} catch (error) {
@@ -406,6 +365,7 @@ const handleRefund = async (record: Order) => {
content: `确定要退款订单 "${record.order_no}" 吗?退款金额: ¥${record.amount}`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'refunded')
message.success('退款申请已提交')
loadOrders()
} catch (error) {

View File

@@ -15,7 +15,7 @@
</template>
</a-page-header>
<a-tabs v-model:activeKey="activeTab" @change="handle极速版TabChange">
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<a-tab-pane key="activities" tab="推广活动">
<a-card>
<div class="search-container">
@@ -27,14 +27,14 @@
<a-form-item label="状态">
<a-select v-model:value="searchForm.status" placeholder="全部状态" style="width: 120px" allow-clear>
<a-select-option value="active">进行中</a-select-option>
<a-select-option value="upcoming">未开始</a-select-极速版option>
<a-select-option value="upcoming">未开始</a-select-option>
<a-select-option value="ended">已结束</a-select-option>
<a-select-option value="paused极速版">已暂停</a-select-option>
<a-select-option value="paused">已暂停</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="活动时间">
<a-range-picker v-model:value="searchForm.activityTime" :placeholder="['开始极速版时间', '结束时间']" />
<a-range-picker v-model:value="searchForm.activityTime" :placeholder="['开始时间', '结束时间']" />
</a-form-item>
<a-form-item>
@@ -67,7 +67,7 @@
<template v-else-if="column.key === 'reward_amount'">
<span v-if="record.reward_type === 'cash'">¥{{ record.reward_amount }}</span>
<span v-else-if="record.reward_type === 'points'">{{ record.reward_amount }}积分</span>
<span v-else-if="record.reward_type === 'coupon'">{{ record.re极速版ward_amount }}元券</span>
<span v-else-if="record.reward_type === 'coupon'">{{ record.reward_amount }}元券</span>
</template>
<template v-else-if="column.key === 'participants'">
@@ -89,10 +89,10 @@
<a-button size="small" @click="handleEditActivity(record)">
<EditOutlined />编辑
</极速版a-button>
</a-button>
<template v-if="record.status === 'active'">
极速版 <a-button size="small" danger @click="handlePauseActivity(record)">
<a-button size="small" danger @click="handlePauseActivity(record)">
<PauseOutlined />暂停
</a-button>
</template>
@@ -127,7 +127,7 @@
<a-select-option value="points">积分</a-select-option>
<a-select-option value="coupon">优惠券</a-select-option>
</a-select>
</极速版a-form-item>
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="rewardSearchForm.status" placeholder="全部状态" style="width: 120px" allow-clear>
@@ -152,7 +152,7 @@
:data-source="rewardList"
:loading="rewardLoading"
:pagination="rewardPagination"
:极速版row-key="record => record.id"
:row-key="record => record.id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'reward_type'">
@@ -177,7 +177,7 @@
</a-button>
</template>
<a-button size="small" @click="handleViewRew极速版ard(record)">
<a-button size="small" @click="handleViewReward(record)">
<EyeOutlined />详情
</a-button>
</a-space>
@@ -185,7 +185,7 @@
</template>
</a-table>
</a-card>
</a极速版-tab-pane>
</a-tab-pane>
</a-tabs>
</div>
</template>
@@ -206,33 +206,15 @@ import {
CheckOutlined
} from '@ant-design/icons-vue'
interface PromotionActivity {
id: number
name: string
description: string
reward_type: string极速版
reward_amount: number
status: string
start_time: string
end_time: string
max_participants:极速版 number
current_participants: number
created_at: string
}
interface RewardRecord {
id: number
user_id: number
user_name: string
user_phone: string
activity_id: number
activity_name: string
reward_type: string
reward_amount: number
status: string
issued_at: string
created_at: string
}
import {
getPromotionActivities,
deletePromotionActivity,
pausePromotionActivity,
resumePromotionActivity,
getRewardRecords,
issueReward
} from '@/api/promotion'
import type { PromotionActivity, RewardRecord } from '@/api/promotion'
interface SearchForm {
name: string
@@ -262,55 +244,13 @@ const rewardSearchForm = reactive<RewardSearchForm>({
status: ''
})
const activityList = ref<PromotionActivity[]>([
{
id: 1,
name: '邀请好友得现金',
description: '邀请好友注册即可获得现金奖励',
reward_type: 'cash',
reward_amount: 10,
status: 'active',
start_time: '极速版2024-03-01',
end_time: '2024-03-31',
max_participants: 1000极速版,
current_participants: 356,
created_at: '2024-02-20'
},
{
id: 2,
name: '分享活动得积分',
description: '分享活动页面即可获得积分奖励',
reward_type: 'points',
reward_amount: 100,
status: 'upcoming',
start_time: '2024-04-01',
end_time: '202极速版4-04-30',
max_participants: 500,
current_p极速版articipants: 0,
created_at: '2024-03-10'
}
])
const rewardList = ref<RewardRecord[]>([
{
id: 1,
user_id: 1001,
user_name: '张先生',
user_phone: '13800138000',
activity_id: 1,
activity_name: '邀请好友极速版得现金',
reward_type: 'cash',
reward_amount: 10,
status: 'issued',
issued_at: '2024-03-05 14:30:00',
created_at: '2024-03-05 14:25:00'
}
])
const activityList = ref<PromotionActivity[]>([])
const rewardList = ref<RewardRecord[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
@@ -319,15 +259,15 @@ const pagination = reactive({
const rewardPagination = reactive({
current: 1,
pageSize: 20,
total: 30,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const activityColumns = [
{ title: '活动名称', dataIndex: 'name', key: '极速版name', width: 150 },
{ title极速版: '奖励类型', key: 'reward_type', width: 100, align: 'center' },
{ title: '活动名称', dataIndex: 'name', key: 'name', width: 150 },
{ title: '奖励类型', key: 'reward_type', width: 100, align: 'center' },
{ title: '奖励金额', key: 'reward_amount', width: 100, align: 'center' },
{ title: '状态', key: 'status', width: 100, align: 'center' },
{ title: '活动时间', key: 'time', width: 200,
@@ -343,10 +283,10 @@ const rewardColumns = [
{ title: '用户', dataIndex: 'user_name', key: 'user_name', width: 100 },
{ title: '联系电话', dataIndex: 'user_phone', key: 'user_phone', width: 120 },
{ title: '活动名称', dataIndex: 'activity_name', key: 'activity_name', width: 150 },
{ title: '奖励类型', key: 'reward_type', width: 100, align: 'center极速版' },
{ title: '奖励类型', key: 'reward_type', width: 100, align: 'center' },
{ title: '奖励金额', key: 'reward_amount', width: 100, align: 'center' },
{ title: '状态', key: 'status', width: 100, align: 'center' },
{ title: '发放时间', dataIndex: 'issued_at极速版', key: 'issued_at', width: 极速版150 },
{ title: '发放时间', dataIndex: 'issued_at', key: 'issued_at', width: 150 },
{ title: '申请时间', dataIndex: 'created_at', key: 'created_at', width: 150 },
{ title: '操作', key: 'actions', width: 120, align: 'center' }
]
@@ -361,7 +301,7 @@ const getStatusColor = (status: string) => {
return colors[status as keyof typeof colors] || 'default'
}
const getStatusText = (status: string)极速版 {
const getStatusText = (status: string) => {
const texts = {
active: '进行中',
upcoming: '未开始',
@@ -382,7 +322,7 @@ const getRewardTypeText = (type: string) => {
const getRewardStatusColor = (status: string) => {
const colors = {
pending极速版: 'orange',
pending: 'orange',
issued: 'green',
failed: 'red'
}
@@ -406,7 +346,15 @@ onMounted(() => {
const loadActivities = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
const response = await getPromotionActivities({
page: pagination.current,
pageSize: pagination.pageSize,
name: searchForm.name,
status: searchForm.status
})
activityList.value = response.data
pagination.total = response.pagination?.total || 0
} catch (error) {
message.error('加载活动列表失败')
} finally {
@@ -417,7 +365,16 @@ const loadActivities = async () => {
const loadRewards = async () => {
rewardLoading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
const response = await getRewardRecords({
page: rewardPagination.current,
pageSize: rewardPagination.pageSize,
user: rewardSearchForm.user,
reward_type: rewardSearchForm.reward_type,
status: rewardSearchForm.status
})
rewardList.value = response.data
rewardPagination.total = response.pagination?.total || 0
} catch (error) {
message.error('加载奖励记录失败')
} finally {
@@ -430,7 +387,7 @@ const handleTabChange = (key: string) => {
loadActivities()
} else if (key === 'rewards') {
loadRewards()
极速版 }
}
}
const handleSearch = () => {
@@ -444,7 +401,7 @@ const handleReset = () => {
status: '',
activityTime: []
})
pagination.current = 1极速版
pagination.current = 1
loadActivities()
}
@@ -483,15 +440,16 @@ const handleViewActivity = (record: PromotionActivity) => {
}
const handleEditActivity = (record: PromotionActivity) => {
message.info(`编辑活动: ${record.name极速版}`)
message.info(`编辑活动: ${record.name}`)
}
const handlePauseActivity = async (record: PromotionActivity) => {
Modal.confirm({
title: '确认暂停',
content: `确定要暂停活动 "${record.name}" 极速版吗?`,
content: `确定要暂停活动 "${record.name}" 吗?`,
onOk: async () => {
try {
await pausePromotionActivity(record.id)
message.success('活动已暂停')
loadActivities()
} catch (error) {
@@ -507,6 +465,7 @@ const handleResumeActivity = async (record: PromotionActivity) => {
content: `确定要继续活动 "${record.name}" 吗?`,
onOk: async () => {
try {
await resumePromotionActivity(record.id)
message.success('活动已继续')
loadActivities()
} catch (error) {
@@ -524,6 +483,7 @@ const handleDeleteActivity = async (record: PromotionActivity) => {
okType: 'danger',
onOk: async () => {
try {
await deletePromotionActivity(record.id)
message.success('活动已删除')
loadActivities()
} catch (error) {
@@ -537,11 +497,12 @@ const handleIssueReward = async (record: RewardRecord) => {
Modal.confirm({
title: '确认发放',
content: `确定要发放奖励给用户 "${record.user_name}" 吗?`,
onOk: async ()极速版 => {
onOk: async () => {
try {
await issueReward(record.id)
message.success('奖励已发放')
loadRewards()
极速版 } catch (error) {
} catch (error) {
message.error('操作失败')
}
}

View File

@@ -15,10 +15,10 @@
<a-col :span="8">
<a-card title="系统信息" size="small">
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="系统版本">v1.0.0</a-descriptions-item>
<a-descriptions-item label="运行环境">Production</a-descriptions-item>
<极速版a-descriptions-item label="启动时间">2024-03-15 10:00:00</a-descriptions-item>
<a-descriptions-item label="运行时长">12天3小时</a-descriptions-item>
<a-descriptions-item label="系统版本">{{ systemInfo.version }}</a-descriptions-item>
<a-descriptions-item label="运行环境">{{ systemInfo.environment }}</a-descriptions-item>
<a-descriptions-item label="启动时间">{{ systemInfo.startTime }}</a-descriptions-item>
<a-descriptions-item label="运行时长">{{ systemInfo.uptime }}</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
@@ -27,11 +27,13 @@
<a-card title="数据库状态" size="small">
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="连接状态">
<a-tag color="green">正常</a-tag>
<a-tag :color="databaseStatus.status === 'running' ? 'green' : 'red'">
{{ databaseStatus.status === 'running' ? '正常' : '异常' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label极速版="数据库类型">MySQL</a-descriptions-item>
<a-descriptions-item label="连接数">15极速版/100</a-descriptions-item>
<a-descriptions-item label="查询次数">1,234/分钟</a-descriptions-item>
<a-descriptions-item label="数据库类型">{{ databaseStatus.type }}</a-descriptions-item>
<a-descriptions-item label="连接数">{{ databaseStatus.connections }}</a-descriptions-item>
<a-descriptions-item label="查询次数">{{ databaseStatus.queriesPerMinute }}/分钟</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
@@ -40,11 +42,13 @@
<a-card title="缓存状态" size="small">
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="Redis状态">
<a-tag color="green">正常</a-tag>
<a-tag :color="cacheStatus.status === 'running' ? 'green' : 'red'">
{{ cacheStatus.status === 'running' ? '正常' : '异常' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="内存使用">65%</a-descriptions-item>
<a-descriptions-item label="命中率">92%</a-descriptions-item>
<a-descriptions-item label="键数量">1,234</a-descriptions-item>
<a-descriptions-item label="内存使用">{{ cacheStatus.memoryUsage }}</a-descriptions-item>
<a-descriptions-item label="命中率">{{ cacheStatus.hitRate }}</a-descriptions-item>
<a-descriptions-item label="键数量">{{ cacheStatus.keyCount }}</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
@@ -90,14 +94,14 @@
<a-col :span="12">
<a-card title="系统日志" size="small">
<a-timeline>
<a-t极速版imeline-item color="green">
<a-timeline-item color="green">
<p>用户登录成功 - admin (2024-03-15 14:30:22)</p>
</a-timeline-item>
<a-timeline-item color="blue">
<p>数据库备份完成 - 备份文件: backup_20240315.sql (2024-03-15 14:00:00)</p>
</a-timeline-item>
<a-timeline-item color="orange">
<p>系统警告 - 内存使用率超过80% (2024-03-15 13:极速版45:18)</极速版p>
<p>系统警告 - 内存使用率超过80% (2024-03-15 13:45:18)</p>
</a-timeline-item>
<a-timeline-item color="green">
<p>定时任务执行 - 清理过期日志 (2024-03-15 13:30:00)</p>
@@ -112,7 +116,7 @@
<a-row :gutter="16" style="margin-top: 16px;">
<a-col :span="24">
<a-card title="系统极速版设置" size="small">
<a-card title="系统设置" size="small">
<a-form :model="systemSettings" layout="vertical">
<a-row :gutter="16">
<a-col :span="8">
@@ -122,17 +126,17 @@
</a-col>
<a-col :span="8">
<a-form-item label="系统版本">
<a-input v-model:value="systemSettings.systemVersion" placeholder="请输入系统版本极速版" />
<a-input v-model:value="systemSettings.systemVersion" placeholder="请输入系统版本" />
</a-form-item>
</极速版a-col>
</a-col>
<a-col :span="8">
<a-form-item label="维护模式">
<a-switch v-model:checked="systemSettings.maintenanceMode" />
</极速版a-form-item>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="极速版16">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="会话超时(分钟)">
<a-input-number v-model:value="systemSettings.sessionTimeout" :min="5" :max="480" style="width: 100%" />
@@ -162,7 +166,7 @@
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import {
ReloadOutlined,
@@ -170,47 +174,39 @@ import {
CloudServerOutlined,
MessageOutlined
} from '@ant-design/icons-vue'
import {
getServices,
startService,
stopService,
getSystemInfo,
getDatabaseStatus,
getCacheStatus,
getSystemSettings,
updateSystemSettings
} from '@/api/system'
import type { Service, SystemInfo, DatabaseStatus, CacheStatus, SystemSettings } from '@/api/system'
interface Service {
id: number
name: string
type: string
description: string
status: string
}
const services = ref<Service[]>([])
const systemInfo = ref<SystemInfo>({
version: 'v1.0.0',
environment: 'Production',
uptime: '12天3小时',
startTime: '2024-03-15 10:00:00'
})
interface SystemSettings {
systemName: string
systemVersion: string
maintenanceMode: boolean
sessionTimeout: number
pageSize: number
enableSwagger: boolean
}
const databaseStatus = ref<DatabaseStatus>({
status: 'running',
type: 'MySQL',
connections: '15/100',
queriesPerMinute: 1234
})
const services = ref<Service[]>([
{
id: 1,
name: 'MySQL数据库',
type: 'database',
description: '主数据库服务',
status: 'running'
},
{
id: 2,
name: 'Redis缓存',
type: 'cache',
description: '缓存服务',
status: 'running'
},
{
id: 3,
name: 'RabbitMQ',
type: 'mq',
description: '消息队列服务',
status: 'stopped'
}
])
const cacheStatus = ref<CacheStatus>({
status: 'running',
memoryUsage: '65%',
hitRate: '92%',
keyCount: 1234
})
const systemSettings = reactive<SystemSettings>({
systemName: '结伴客管理系统',
@@ -221,18 +217,76 @@ const systemSettings = reactive<SystemSettings>({
enableSwagger: true
})
onMounted(() => {
loadSystemInfo()
loadDatabaseStatus()
loadCacheStatus()
loadServices()
loadSystemSettings()
})
const loadSystemInfo = async () => {
try {
const response = await getSystemInfo()
systemInfo.value = response.data
} catch (error) {
message.error('加载系统信息失败')
}
}
const loadDatabaseStatus = async () => {
try {
const response = await getDatabaseStatus()
databaseStatus.value = response.data
} catch (error) {
message.error('加载数据库状态失败')
}
}
const loadCacheStatus = async () => {
try {
const response = await getCacheStatus()
cacheStatus.value = response.data
} catch (error) {
message.error('加载缓存状态失败')
}
}
const loadServices = async () => {
try {
const response = await getServices()
services.value = response.data
} catch (error) {
message.error('加载服务列表失败')
}
}
const loadSystemSettings = async () => {
try {
const response = await getSystemSettings()
Object.assign(systemSettings, response.data)
} catch (error) {
message.error('加载系统设置失败')
}
}
const handleRefresh = () => {
loadSystemInfo()
loadDatabaseStatus()
loadCacheStatus()
loadServices()
message.success('系统状态已刷新')
}
const handleStopService = (service: Service极速版) => {
const handleStopService = (service: Service) => {
Modal.confirm({
title: '确认停止',
content: `确定要停止服务 "${service.name}" 吗?`,
onOk: async () => {
try {
service.status = 'stopped'
await stopService(service.id)
message.success('服务已停止')
loadServices()
} catch (error) {
message.error('操作失败')
}
@@ -246,8 +300,9 @@ const handleStartService = (service: Service) => {
content: `确定要启动服务 "${service.name}" 吗?`,
onOk: async () => {
try {
service.status = 'running'
await startService(service.id)
message.success('服务已启动')
loadServices()
} catch (error) {
message.error('操作失败')
}
@@ -259,17 +314,22 @@ const viewLogs = () => {
message.info('查看系统日志功能开发中')
}
const saveSettings = () => {
message.success('系统设置已保存')
const saveSettings = async () => {
try {
await updateSystemSettings(systemSettings)
message.success('系统设置已保存')
} catch (error) {
message.error('保存设置失败')
}
}
const resetSettings = () => {
Object.assign(systemSettings, {
systemName: '结伴客管理系统',
systemVersion: 'v1.0.极速版0',
systemVersion: 'v1.0.0',
maintenanceMode: false,
sessionTimeout: 30,
pageSize: 极速版20,
pageSize: 20,
enableSwagger: true
})
message.success('设置已重置')

View File

@@ -150,19 +150,8 @@ import {
RocketOutlined,
CloseOutlined
} from '@ant-design/icons-vue'
interface TravelPlan {
id: number
destination: string
start_date: string
end_date: string
budget: number
max_members: number
current_members: number
status: string
creator: string
created_at: string
}
import { getTravelPlans, closeTravelPlan } from '@/api/travel'
import type { TravelPlan } from '@/api/travel'
interface SearchForm {
destination: string
@@ -178,38 +167,11 @@ const searchForm = reactive<SearchForm>({
travelTime: []
})
// 模拟旅行数据
const travelList = ref<TravelPlan[]>([
{
id: 1,
destination: '西藏',
start_date: '2024-07-01',
end_date: '2024-07-15',
budget: 5000,
max_members: 6,
current_members: 3,
status: 'recruiting',
creator: '旅行爱好者',
created_at: '2024-06-01'
},
{
id: 2,
destination: '云南',
start_date: '2024-08-10',
end_date: '2024-08-20',
budget: 3000,
max_members: 4,
current_members: 4,
status: 'full',
creator: '探险家',
created_at: '2024-07-15'
}
])
const travelList = ref<TravelPlan[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
@@ -289,8 +251,15 @@ onMounted(() => {
const loadTravelPlans = async () => {
loading.value = true
try {
// TODO: 调用真实API
await new Promise(resolve => setTimeout(resolve, 500))
const response = await getTravelPlans({
page: pagination.current,
pageSize: pagination.pageSize,
destination: searchForm.destination,
status: searchForm.status
})
travelList.value = response.data
pagination.total = response.pagination?.total || 0
} catch (error) {
message.error('加载旅行计划失败')
} finally {
@@ -299,7 +268,7 @@ const loadTravelPlans = async () => {
}
const handleSearch = () => {
pagination.current = 极速版1
pagination.current = 1
loadTravelPlans()
}
@@ -346,12 +315,13 @@ const handlePromote = (record: TravelPlan) => {
})
}
const handleClose = (record: TravelPlan) => {
const handleClose = async (record: TravelPlan) => {
Modal.confirm({
title: '确认关闭',
content: `确定要关闭旅行计划 "${record.destination}" 吗?`,
onOk: async () => {
try {
await closeTravelPlan(record.id)
message.success('旅行计划已关闭')
loadTravelPlans()
} catch (error) {

View File

@@ -251,20 +251,8 @@ import {
DownOutlined
} from '@ant-design/icons-vue'
import type { TableProps } from 'ant-design-vue'
interface User {
id: number
username: string
nickname: string
avatar: string
email: string
phone: string
status: string
level: number
points: number
created_at: string
updated_at: string
}
import { getUsers, updateUser, createUser, deleteUser } from '@/api/user'
import type { User } from '@/api/user'
interface SearchForm {
keyword: string
@@ -324,40 +312,11 @@ const formRules = {
]
}
// 模拟用户数据
const userList = ref<User[]>([
{
id: 1,
username: 'admin',
nickname: '系统管理员',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=admin',
email: 'admin@jiebanke.com',
phone: '13800138000',
status: 'active',
level: 10,
points: 10000,
created_at: '2024-01-01',
updated_at: '2024-01-01'
},
{
id: 2,
username: 'user001',
nickname: '旅行爱好者',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=user1',
email: 'user001@example.com',
phone: '13800138001',
status: 'active',
level: 3,
points: 1500,
created_at: '2024-02-15',
updated_at: '2024-02-15'
}
])
const userList = ref<User[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
@@ -458,15 +417,15 @@ onMounted(() => {
const loadUsers = async () => {
loading.value = true
try {
// TODO: 调用真实API
// const response = await userAPI.getUsers({
// page: pagination.current,
// pageSize: pagination.pageSize,
// ...searchForm
// })
await new Promise(resolve => setTimeout(resolve, 500))
// userList.value = response.data.users
// pagination.total = response.data.pagination.total
const response = await getUsers({
page: pagination.current,
pageSize: pagination.pageSize,
keyword: searchForm.keyword,
status: searchForm.status
})
userList.value = response.data
pagination.total = response.pagination?.total || 0
} catch (error) {
message.error('加载用户列表失败')
} finally {
@@ -539,7 +498,7 @@ const handleDisable = async (record: User) => {
content: `确定要禁用用户 "${record.nickname}" 吗?`,
onOk: async () => {
try {
// await userAPI.updateUser(record.id, { status: 'inactive' })
await updateUser(record.id, { status: 'inactive' })
message.success('用户已禁用')
loadUsers()
} catch (error) {
@@ -555,7 +514,7 @@ const handleEnable = async (record: User) => {
content: `确定要启用用户 "${record.nickname}" 吗?`,
onOk: async () => {
try {
// await userAPI.updateUser(record.id, { status: 'active' })
await updateUser(record.id, { status: 'active' })
message.success('用户已启用')
loadUsers()
} catch (error) {
@@ -571,7 +530,7 @@ const handleBan = async (record: User) => {
content: `确定要封禁用户 "${record.nickname}" 吗?`,
onOk: async () => {
try {
// await userAPI.updateUser(record.id, { status: 'banned' })
await updateUser(record.id, { status: 'banned' })
message.success('用户已封禁')
loadUsers()
} catch (error) {
@@ -590,7 +549,7 @@ const handleDelete = async (record: User) => {
cancelText: '取消',
onOk: async () => {
try {
// await userAPI.deleteUser(record.id)
await deleteUser(record.id)
message.success('用户已删除')
loadUsers()
} catch (error) {
@@ -607,11 +566,11 @@ const handleModalOk = async () => {
if (editingUser.value) {
// 编辑用户
// await userAPI.updateUser(editingUser.value.id, formState)
await updateUser(editingUser.value.id, formState)
message.success('用户信息更新成功')
} else {
// 创建用户
// await userAPI.createUser(formState)
await createUser(formState)
message.success('用户创建成功')
}

View File

@@ -2,6 +2,7 @@
NODE_ENV=development
PORT=3001
HOST=0.0.0.0
ENABLE_SWAGGER=true
# MySQL数据库配置
DB_HOST=192.168.0.240
@@ -47,8 +48,4 @@ WECHAT_SECRET=your-wechat-secret
# 文件上传配置
UPLOAD_MAX_SIZE=10485760
UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif
# 调试配置
DEBUG=jiebanke:*
LOG_LEVEL=info
UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif

View File

@@ -27,6 +27,8 @@
"multer": "^1.4.5-lts.1",
"mysql2": "^3.14.3",
"redis": "^5.8.2",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"winston": "^3.11.0",
"xss-clean": "^0.1.4"
},
@@ -53,6 +55,46 @@
"node": ">=6.0.0"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="
},
"node_modules/@apidevtools/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"@apidevtools/openapi-schemas": "^2.0.4",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"z-schema": "^5.0.1"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -1047,6 +1089,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz",
@@ -1166,6 +1213,12 @@
"@redis/client": "^5.8.2"
}
},
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
"hasInstallScript": true
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -1264,6 +1317,11 @@
"@types/istanbul-lib-report": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
},
"node_modules/@types/node": {
"version": "24.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
@@ -1451,8 +1509,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/array-flatten": {
"version": "1.1.1",
@@ -1597,8 +1654,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/basic-auth": {
"version": "2.0.1",
@@ -1673,7 +1729,6 @@
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -1801,6 +1856,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2028,6 +2088,14 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
"engines": {
"node": ">= 6"
}
},
"node_modules/component-emitter": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
@@ -2040,8 +2108,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/concat-stream": {
"version": "1.6.2",
@@ -2264,7 +2331,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
"dependencies": {
"esutils": "^2.0.2"
},
@@ -2574,7 +2640,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2919,8 +2984,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
@@ -3268,7 +3332,6 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -4059,7 +4122,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -4245,6 +4307,12 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead."
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -4255,6 +4323,12 @@
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
@@ -4281,6 +4355,11 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@@ -4465,7 +4544,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -4855,7 +4933,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
@@ -4883,6 +4960,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"peer": true
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -4990,7 +5073,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5949,6 +6031,78 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swagger-jsdoc": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
"dependencies": {
"commander": "6.2.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"lodash.mergewith": "^4.6.2",
"swagger-parser": "^10.0.3",
"yaml": "2.0.0-1"
},
"bin": {
"swagger-jsdoc": "bin/swagger-jsdoc.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/swagger-jsdoc/node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.28.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.0.tgz",
"integrity": "sha512-I9ibQtr77BPzT28WFWMVktzQOtWzoSS2J99L0Att8gDar1atl1YTRI7NUFSr4kj8VvWICgylanYHIoHjITc7iA==",
"dependencies": {
"@scarf/scarf": "=1.4.0"
}
},
"node_modules/swagger-ui-express": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
"dependencies": {
"swagger-ui-dist": ">=5.0.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -6322,8 +6476,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/write-file-atomic": {
"version": "4.0.2",
@@ -6375,6 +6528,14 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
"node_modules/yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
"engines": {
"node": ">= 6"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -6413,6 +6574,34 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/z-schema": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
"dependencies": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^13.7.0"
},
"bin": {
"z-schema": "bin/z-schema"
},
"engines": {
"node": ">=8.0.0"
},
"optionalDependencies": {
"commander": "^9.4.1"
}
},
"node_modules/z-schema/node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"optional": true,
"engines": {
"node": "^12.20.0 || >=14"
}
}
}
}

View File

@@ -37,6 +37,8 @@
"multer": "^1.4.5-lts.1",
"mysql2": "^3.14.3",
"redis": "^5.8.2",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"winston": "^3.11.0",
"xss-clean": "^0.1.4"
},
@@ -49,4 +51,4 @@
"engines": {
"node": ">=16.0.0"
}
}
}

View File

@@ -5,6 +5,8 @@ const morgan = require('morgan')
const rateLimit = require('express-rate-limit')
const xss = require('xss-clean')
const hpp = require('hpp')
const swaggerUi = require('swagger-ui-express')
const swaggerSpec = require('./config/swagger')
console.log('🔧 初始化Express应用...')
@@ -70,6 +72,12 @@ app.use(hpp({ // 防止参数污染
// 静态文件服务
app.use('/uploads', express.static('uploads'))
// Swagger文档路由
if (process.env.NODE_ENV === 'development' || process.env.ENABLE_SWAGGER === 'true') {
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))
console.log('📚 Swagger文档已启用: http://localhost:3001/api-docs')
}
// 健康检查路由
app.get('/health', (req, res) => {
res.status(200).json({

View File

@@ -0,0 +1,133 @@
const swaggerJsdoc = require('swagger-jsdoc')
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '结伴客API文档',
version: '1.0.0',
description: '结伴客小程序后端API接口文档'
},
servers: [
{
url: 'http://localhost:3001/api/v1',
description: '开发环境服务器'
},
{
url: 'https://your-domain.com/api/v1',
description: '生产环境服务器'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
},
schemas: {
User: {
type: 'object',
properties: {
id: {
type: 'integer',
description: '用户ID'
},
openid: {
type: 'string',
description: '微信openid'
},
username: {
type: 'string',
description: '用户名'
},
nickname: {
type: 'string',
description: '昵称'
},
avatar: {
type: 'string',
description: '头像URL'
},
gender: {
type: 'string',
enum: ['male', 'female', 'other'],
description: '性别'
},
birthday: {
type: 'string',
format: 'date',
description: '生日'
},
phone: {
type: 'string',
description: '手机号'
},
email: {
type: 'string',
description: '邮箱'
},
status: {
type: 'string',
enum: ['active', 'inactive', 'banned'],
description: '用户状态'
},
level: {
type: 'integer',
description: '用户等级'
},
points: {
type: 'integer',
description: '用户积分'
},
created_at: {
type: 'string',
format: 'date-time',
description: '创建时间'
},
updated_at: {
type: 'string',
format: 'date-time',
description: '更新时间'
}
}
},
ApiResponse: {
type: 'object',
properties: {
success: {
type: 'boolean',
description: '请求是否成功'
},
code: {
type: 'integer',
description: '状态码'
},
message: {
type: 'string',
description: '响应消息'
},
data: {
type: 'object',
description: '响应数据'
}
}
}
}
},
security: [
{
bearerAuth: []
}
]
},
apis: [
'./src/routes/*.js',
'./src/controllers/*.js'
]
}
const specs = swaggerJsdoc(options)
module.exports = specs

View File

@@ -1,33 +1,329 @@
const express = require('express')
const { catchAsync } = require('../utils/errors')
const { authenticate, optionalAuthenticate } = require('../middleware/auth')
const {
register,
login,
getCurrentUser,
updateProfile,
changePassword,
wechatLogin
} = require('../controllers/authControllerMySQL')
const { body } = require('express-validator')
const authController = require('../controllers/authControllerMySQL')
const router = express.Router()
// 用户注册
router.post('/register', catchAsync(register))
/**
* @swagger
* tags:
* name: Auth
* description: 用户认证相关接口
*/
// 用户登录
router.post('/login', catchAsync(login))
/**
* @swagger
* /auth/register:
* post:
* summary: 用户注册
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - username
* - password
* properties:
* username:
* type: string
* description: 用户名
* example: testuser
* password:
* type: string
* description: 密码
* example: password123
* nickname:
* type: string
* description: 昵称
* example: 测试用户
* email:
* type: string
* description: 邮箱
* example: test@example.com
* phone:
* type: string
* description: 手机号
* example: 13800000000
* responses:
* 201:
* description: 注册成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* user:
* $ref: '#/components/schemas/User'
* token:
* type: string
* message:
* type: string
* 400:
* description: 请求参数错误
* 500:
* description: 服务器内部错误
*/
router.post(
'/register',
[
body('username').notEmpty().withMessage('用户名不能为空'),
body('password').isLength({ min: 6 }).withMessage('密码长度不能少于6位')
],
authController.register
)
// 微信登录
router.post('/wechat-login', catchAsync(wechatLogin))
/**
* @swagger
* /auth/login:
* post:
* summary: 用户登录
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - username
* - password
* properties:
* username:
* type: string
* description: 用户名/邮箱/手机号
* example: testuser
* password:
* type: string
* description: 密码
* example: password123
* responses:
* 200:
* description: 登录成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* user:
* $ref: '#/components/schemas/User'
* token:
* type: string
* message:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 用户名或密码错误
* 404:
* description: 用户不存在
* 500:
* description: 服务器内部错误
*/
router.post(
'/login',
[
body('username').notEmpty().withMessage('用户名不能为空'),
body('password').notEmpty().withMessage('密码不能为空')
],
authController.login
)
// 获取当前用户信息(需要认证)
router.get('/me', authenticate, catchAsync(getCurrentUser))
/**
* @swagger
* /auth/me:
* get:
* summary: 获取当前用户信息
* tags: [Auth]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* user:
* $ref: '#/components/schemas/User'
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
router.get('/me', authController.getCurrentUser)
// 更新用户信息(需要认证)
router.put('/profile', authenticate, catchAsync(updateProfile))
/**
* @swagger
* /auth/profile:
* put:
* summary: 更新用户个人信息
* tags: [Auth]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* nickname:
* type: string
* description: 昵称
* avatar:
* type: string
* description: 头像URL
* gender:
* type: string
* enum: [male, female, other]
* description: 性别
* birthday:
* type: string
* format: date
* description: 生日
* responses:
* 200:
* description: 更新成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* user:
* $ref: '#/components/schemas/User'
* message:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
router.put('/profile', authController.updateProfile)
/**
* @swagger
* /auth/password:
* put:
* summary: 修改密码
* tags: [Auth]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - currentPassword
* - newPassword
* properties:
* currentPassword:
* type: string
* description: 当前密码
* newPassword:
* type: string
* description: 新密码
* responses:
* 200:
* description: 修改成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 当前密码错误
* 500:
* description: 服务器内部错误
*/
router.put(
'/password',
[
body('currentPassword').notEmpty().withMessage('当前密码不能为空'),
body('newPassword').isLength({ min: 6 }).withMessage('新密码长度不能少于6位')
],
authController.changePassword
)
/**
* @swagger
* /auth/wechat:
* post:
* summary: 微信登录/注册
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - code
* properties:
* code:
* type: string
* description: 微信授权码
* userInfo:
* type: object
* description: 微信用户信息
* responses:
* 200:
* description: 登录成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* user:
* $ref: '#/components/schemas/User'
* token:
* type: string
* message:
* type: string
* 400:
* description: 请求参数错误
* 500:
* description: 服务器内部错误
*/
router.post('/wechat', authController.wechatLogin)
// 修改密码(需要认证)
router.put('/password', authenticate, catchAsync(changePassword))
module.exports = router

View File

@@ -96,8 +96,8 @@ const startServer = async () => {
console.log(`📊 环境: ${process.env.NODE_ENV || 'development'}`)
console.log(`⏰ 启动时间: ${new Date().toLocaleString()}`)
console.log('💾 数据库: MySQL')
console.log(`🔴 Redis: ${redisConfig.isConnected() ? '已连接' : '未连接'}`)
console.log(`🐰 RabbitMQ: ${rabbitMQConfig.isConnected() ? '已连接' : '未连接'}`)
console.log(`🔴 Redis: ${redisConfig.isConnected ? '已连接' : '未连接'}`)
console.log(`🐰 RabbitMQ: ${rabbitMQConfig.isConnected ? '已连接' : '未连接'}`)
console.log('========================================\n')
})
@@ -130,7 +130,7 @@ const startServer = async () => {
}
// 关闭RabbitMQ连接
if (rabbitMQConfig.isConnected()) {
if (rabbitMQConfig.isConnected) {
console.log('🔐 关闭RabbitMQ连接...')
await rabbitMQConfig.close()
console.log('✅ RabbitMQ连接已关闭')

30
backend/test-swagger.js Normal file
View File

@@ -0,0 +1,30 @@
const http = require('http');
// 发送请求到Swagger UI
const options = {
hostname: 'localhost',
port: 3001,
path: '/api-docs/',
method: 'GET'
};
const req = http.request(options, (res) => {
console.log(`状态码: ${res.statusCode}`);
res.on('data', (chunk) => {
// 检查响应中是否包含Swagger UI的关键字
if (chunk.toString().includes('Swagger UI')) {
console.log('Swagger UI 已成功启动并运行');
} else {
console.log('收到响应但可能不是Swagger UI页面');
}
// 只输出前200个字符来检查内容
console.log('响应前200字符:', chunk.toString().substring(0, 200));
});
});
req.on('error', (error) => {
console.error('请求出错:', error.message);
});
req.end();

183
package-lock.json generated
View File

@@ -6,6 +6,9 @@
"": {
"dependencies": {
"mysql2": "^3.14.3"
},
"devDependencies": {
"less": "^4.4.1"
}
},
"node_modules/aws-ssl-profiles": {
@@ -16,6 +19,18 @@
"node": ">= 6.0.0"
}
},
"node_modules/copy-anything": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
"dev": true,
"dependencies": {
"is-what": "^3.14.1"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -24,6 +39,19 @@
"node": ">=0.10"
}
},
"node_modules/errno": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
"dev": true,
"optional": true,
"dependencies": {
"prr": "~1.0.1"
},
"bin": {
"errno": "cli.js"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@@ -32,6 +60,13 @@
"is-property": "^1.0.2"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"optional": true
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -43,11 +78,56 @@
"node": ">=0.10.0"
}
},
"node_modules/image-size": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
"dev": true,
"optional": true,
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
},
"node_modules/is-what": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
"dev": true
},
"node_modules/less": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/less/-/less-4.4.1.tgz",
"integrity": "sha512-X9HKyiXPi0f/ed0XhgUlBeFfxrlDP3xR4M7768Zl+WXLUViuL9AOPPJP4nCV0tgRWvTYvpNmN0SFhZOQzy16PA==",
"dev": true,
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",
"tslib": "^2.3.0"
},
"bin": {
"lessc": "bin/lessc"
},
"engines": {
"node": ">=14"
},
"optionalDependencies": {
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"needle": "^3.1.0",
"source-map": "~0.6.0"
}
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@@ -75,6 +155,33 @@
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"dev": true,
"optional": true,
"dependencies": {
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"optional": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mysql2": {
"version": "3.14.3",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.3.tgz",
@@ -105,16 +212,86 @@
"node": ">=12.0.0"
}
},
"node_modules/needle": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
"dev": true,
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.3",
"sax": "^1.2.4"
},
"bin": {
"needle": "bin/needle"
},
"engines": {
"node": ">= 4.4.x"
}
},
"node_modules/parse-node-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
"dev": true,
"engines": {
"node": ">= 0.10"
}
},
"node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"dev": true,
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
"dev": true,
"optional": true
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"dev": true,
"optional": true
},
"node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"optional": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
@@ -122,6 +299,12 @@
"engines": {
"node": ">= 0.6"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true
}
}
}

View File

@@ -1,5 +1,8 @@
{
"dependencies": {
"mysql2": "^3.14.3"
},
"devDependencies": {
"less": "^4.4.1"
}
}