重构认证系统和订单支付功能,新增邮箱验证、密码重置及支付流程
This commit is contained in:
@@ -5,6 +5,9 @@ NODE_ENV=development
|
||||
VITE_API_BASE_URL=http://localhost:3200/api/v1
|
||||
VITE_API_TIMEOUT=30000
|
||||
|
||||
# 使用模拟数据(开发环境)
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 功能开关
|
||||
VITE_FEATURE_ANALYTICS=true
|
||||
VITE_FEATURE_DEBUG=true
|
||||
|
||||
@@ -233,6 +233,7 @@ export { default as animalAPI } from './animal'
|
||||
export { default as orderAPI } from './order'
|
||||
export { default as promotionAPI } from './promotion'
|
||||
export { default as systemAPI } from './system'
|
||||
export { default as dashboardAPI } from './dashboard'
|
||||
|
||||
// 重新导出特定类型以避免冲突
|
||||
export type { ApiResponse } from './user'
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
import { request } from '.'
|
||||
import { mockMerchantAPI } from './mockData'
|
||||
import { createMockWrapper } from '@/config/mock'
|
||||
|
||||
// 定义商家相关类型
|
||||
export interface Merchant {
|
||||
id: number
|
||||
business_name: string
|
||||
business_license: string
|
||||
legal_representative: string
|
||||
name: string
|
||||
business_name?: string
|
||||
business_license?: string
|
||||
legal_representative?: string
|
||||
contact_person: string
|
||||
contact_phone: string
|
||||
contact_email: string
|
||||
address: string
|
||||
business_scope: string
|
||||
contact_email?: string
|
||||
address?: string
|
||||
business_scope?: string
|
||||
type: string
|
||||
status: string
|
||||
remark: string
|
||||
remark?: string
|
||||
description?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export interface MerchantQueryParams {
|
||||
page?: number
|
||||
limit?: number
|
||||
keyword?: string
|
||||
business_name?: string
|
||||
contact_person?: string
|
||||
contact_phone?: string
|
||||
status?: string
|
||||
type?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
}
|
||||
@@ -94,7 +101,8 @@ export const disableMerchant = (id: number) =>
|
||||
export const enableMerchant = (id: number) =>
|
||||
request.put<{ success: boolean; code: number; message: string }>(`/merchants/${id}/enable`)
|
||||
|
||||
export default {
|
||||
// 使用 mock 包装器
|
||||
const merchantAPI = createMockWrapper({
|
||||
getMerchants,
|
||||
getMerchant,
|
||||
createMerchant,
|
||||
@@ -105,4 +113,6 @@ export default {
|
||||
rejectMerchant,
|
||||
disableMerchant,
|
||||
enableMerchant
|
||||
}
|
||||
}, mockMerchantAPI)
|
||||
|
||||
export default merchantAPI
|
||||
@@ -6,15 +6,119 @@ const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
// 模拟用户数据
|
||||
const mockUsers = [
|
||||
{ id: 1, username: 'admin', nickname: '系统管理员', role: 'admin', status: 'active', createdAt: '2024-01-01' },
|
||||
{ id: 2, username: 'user1', nickname: '旅行爱好者', role: 'user', status: 'active', createdAt: '2024-01-02' },
|
||||
{ id: 3, username: 'merchant1', nickname: '花店老板', role: 'merchant', status: 'active', createdAt: '2024-01-03' }
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
nickname: '系统管理员',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138001',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=admin',
|
||||
userType: 'admin',
|
||||
status: 'active',
|
||||
registerTime: '2024-01-01',
|
||||
lastLoginTime: '2024-03-15 14:30:22',
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'user1',
|
||||
nickname: '旅行爱好者',
|
||||
email: 'user1@example.com',
|
||||
phone: '13800138002',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=user1',
|
||||
userType: 'normal',
|
||||
status: 'active',
|
||||
registerTime: '2024-01-02',
|
||||
lastLoginTime: '2024-03-15 10:20:15',
|
||||
createdAt: '2024-01-02'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'merchant1',
|
||||
nickname: '花店老板',
|
||||
email: 'merchant1@example.com',
|
||||
phone: '13800138003',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=merchant1',
|
||||
userType: 'merchant',
|
||||
status: 'active',
|
||||
registerTime: '2024-01-03',
|
||||
lastLoginTime: '2024-03-15 09:45:30',
|
||||
createdAt: '2024-01-03'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: 'user2',
|
||||
nickname: '探险家',
|
||||
email: 'user2@example.com',
|
||||
phone: '13800138004',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=user2',
|
||||
userType: 'normal',
|
||||
status: 'inactive',
|
||||
registerTime: '2024-01-04',
|
||||
lastLoginTime: '2024-03-10 16:20:10',
|
||||
createdAt: '2024-01-04'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
username: 'user3',
|
||||
nickname: '动物之友',
|
||||
email: 'user3@example.com',
|
||||
phone: '13800138005',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=user3',
|
||||
userType: 'normal',
|
||||
status: 'banned',
|
||||
registerTime: '2024-01-05',
|
||||
lastLoginTime: '2024-03-05 12:30:45',
|
||||
createdAt: '2024-01-05'
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟商家数据
|
||||
const mockMerchants = [
|
||||
{ id: 1, name: '鲜花坊', type: 'flower', status: 'approved', contact: '13800138001', createdAt: '2024-01-05' },
|
||||
{ id: 2, name: '快乐农场', type: 'farm', status: 'approved', contact: '13800138002', createdAt: '2024-01-06' }
|
||||
{
|
||||
id: 1,
|
||||
name: '鲜花坊',
|
||||
type: 'shop',
|
||||
status: 'approved',
|
||||
contact_person: '张三',
|
||||
contact_phone: '13800138001',
|
||||
address: '北京市朝阳区花卉市场1号',
|
||||
description: '专业经营各类鲜花,提供花束定制服务',
|
||||
created_at: '2024-01-05'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '快乐农场',
|
||||
type: 'farm',
|
||||
status: 'approved',
|
||||
contact_person: '李四',
|
||||
contact_phone: '13800138002',
|
||||
address: '河北省承德市农业园区2号',
|
||||
description: '生态农场,提供动物认领和农产品销售',
|
||||
created_at: '2024-01-06'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '山水酒店',
|
||||
type: 'hotel',
|
||||
status: 'pending',
|
||||
contact_person: '王五',
|
||||
contact_phone: '13800138003',
|
||||
address: '云南省大理市洱海边1号',
|
||||
description: '精品民宿,提供旅行住宿服务',
|
||||
created_at: '2024-01-07'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '美食餐厅',
|
||||
type: 'restaurant',
|
||||
status: 'rejected',
|
||||
contact_person: '赵六',
|
||||
contact_phone: '13800138004',
|
||||
address: '四川省成都市春熙路88号',
|
||||
description: '川菜餐厅,提供地道川菜',
|
||||
created_at: '2024-01-08'
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟旅行数据
|
||||
@@ -124,7 +228,7 @@ export const mockUserAPI = {
|
||||
return createPaginatedResponse(paginatedData, page, pageSize, mockUsers.length)
|
||||
},
|
||||
|
||||
getUserById: async (id: number) => {
|
||||
getUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const user = mockUsers.find(u => u.id === id)
|
||||
if (user) {
|
||||
@@ -132,6 +236,88 @@ export const mockUserAPI = {
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
createUser: async (data: any) => {
|
||||
await delay(500)
|
||||
const newUser = {
|
||||
id: mockUsers.length + 1,
|
||||
...data,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
updatedAt: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
mockUsers.push(newUser)
|
||||
return createSuccessResponse(newUser)
|
||||
},
|
||||
|
||||
updateUser: async (id: number, data: any) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers[index] = {
|
||||
...mockUsers[index],
|
||||
...data,
|
||||
updatedAt: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
return createSuccessResponse(mockUsers[index])
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
deleteUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers.splice(index, 1)
|
||||
return createSuccessResponse(null)
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
enableUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers[index].status = 'active'
|
||||
return createSuccessResponse(mockUsers[index])
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
disableUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers[index].status = 'inactive'
|
||||
return createSuccessResponse(mockUsers[index])
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
banUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers[index].status = 'banned'
|
||||
return createSuccessResponse(mockUsers[index])
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
unbanUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers[index].status = 'active'
|
||||
return createSuccessResponse(mockUsers[index])
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,12 +325,120 @@ export const mockUserAPI = {
|
||||
export const mockMerchantAPI = {
|
||||
getMerchants: async (params: any = {}) => {
|
||||
await delay(800)
|
||||
const { page = 1, limit = 10 } = params
|
||||
const { page = 1, limit = 10, keyword = '', status = '', type = '' } = params
|
||||
|
||||
// 根据查询参数过滤商家
|
||||
let filteredMerchants = mockMerchants
|
||||
if (keyword) {
|
||||
filteredMerchants = mockMerchants.filter(m =>
|
||||
m.name.includes(keyword) ||
|
||||
m.contact_person.includes(keyword)
|
||||
)
|
||||
}
|
||||
if (status) {
|
||||
filteredMerchants = filteredMerchants.filter(m => m.status === status)
|
||||
}
|
||||
if (type) {
|
||||
filteredMerchants = filteredMerchants.filter(m => m.type === type)
|
||||
}
|
||||
|
||||
const start = (page - 1) * limit
|
||||
const end = start + limit
|
||||
const paginatedData = mockMerchants.slice(start, end)
|
||||
const paginatedData = filteredMerchants.slice(start, end)
|
||||
|
||||
return createPaginatedResponse(paginatedData, page, limit, mockMerchants.length)
|
||||
return createPaginatedResponse(paginatedData, page, limit, filteredMerchants.length)
|
||||
},
|
||||
|
||||
getMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const merchant = mockMerchants.find(m => m.id === id)
|
||||
if (merchant) {
|
||||
return createSuccessResponse(merchant)
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
createMerchant: async (data: any) => {
|
||||
await delay(500)
|
||||
const newMerchant = {
|
||||
id: mockMerchants.length + 1,
|
||||
...data,
|
||||
created_at: new Date().toISOString().split('T')[0],
|
||||
updated_at: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
mockMerchants.push(newMerchant)
|
||||
return createSuccessResponse(newMerchant)
|
||||
},
|
||||
|
||||
updateMerchant: async (id: number, data: any) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants[index] = {
|
||||
...mockMerchants[index],
|
||||
...data,
|
||||
updated_at: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
return createSuccessResponse(mockMerchants[index])
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
deleteMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants.splice(index, 1)
|
||||
return createSuccessResponse(null)
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
approveMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants[index].status = 'approved'
|
||||
return createSuccessResponse(mockMerchants[index])
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
rejectMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants[index].status = 'rejected'
|
||||
return createSuccessResponse(mockMerchants[index])
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
disableMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants[index].status = 'disabled'
|
||||
return createSuccessResponse(mockMerchants[index])
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
enableMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants[index].status = 'approved'
|
||||
return createSuccessResponse(mockMerchants[index])
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export interface ApiResponse<T> {
|
||||
|
||||
// 获取订单列表
|
||||
export const getOrders = (params?: OrderQueryParams) =>
|
||||
request.get<{ success: boolean; code: number; message: string; data: { orders: Order[]; pagination: any } }>('/orders', { params })
|
||||
request.get<{ success: boolean; code: number; message: string; data: { orders: Order[]; pagination: any } }>('/orders/admin', { params })
|
||||
|
||||
// 获取订单详情
|
||||
export const getOrder = (id: number) =>
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface TravelUpdateData {
|
||||
|
||||
// 获取结伴游列表
|
||||
export const getTravels = (params?: TravelQueryParams) =>
|
||||
request.get<{ success: boolean; code: number; message: string; data: { travels: Travel[]; pagination: any } }>('/travels', { params })
|
||||
request.get<{ success: boolean; code: number; message: string; data: { travels: Travel[]; pagination: any } }>('/travel/travels', { params })
|
||||
|
||||
// 获取结伴游详情
|
||||
export const getTravel = (id: number) =>
|
||||
@@ -80,6 +80,14 @@ export const getTravelPlans = (params?: TravelQueryParams) =>
|
||||
export const closeTravelPlan = (id: number) =>
|
||||
request.put<{ success: boolean; code: number; message: string }>(`/travel-plans/${id}/close`)
|
||||
|
||||
// 发布旅行
|
||||
export const publishTravel = (id: number) =>
|
||||
request.put<{ success: boolean; code: number; message: string }>(`/travels/${id}/publish`)
|
||||
|
||||
// 归档旅行
|
||||
export const archiveTravel = (id: number) =>
|
||||
request.put<{ success: boolean; code: number; message: string }>(`/travels/${id}/archive`)
|
||||
|
||||
export default {
|
||||
getTravels,
|
||||
getTravel,
|
||||
@@ -88,5 +96,7 @@ export default {
|
||||
deleteTravel,
|
||||
updateTravelStatus,
|
||||
getTravelPlans,
|
||||
closeTravelPlan
|
||||
closeTravelPlan,
|
||||
publishTravel,
|
||||
archiveTravel
|
||||
}
|
||||
@@ -65,9 +65,9 @@ export interface ApiResponse<T = any> {
|
||||
}
|
||||
|
||||
export interface UserListResponse {
|
||||
users: User[]
|
||||
list: User[]
|
||||
pagination: {
|
||||
page: number
|
||||
current: number
|
||||
pageSize: number
|
||||
total: number
|
||||
totalPages: number
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
<!-- 创建/编辑模态框 -->
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="600px"
|
||||
:confirm-loading="confirmLoading"
|
||||
|
||||
@@ -136,11 +136,10 @@ import { UserOutlined, ShopOutlined, CompassOutlined, HeartOutlined } from '@ant
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
getDashboardData,
|
||||
getUserGrowthData,
|
||||
getOrderStatsData
|
||||
} from '@/api/dashboard'
|
||||
import dashboardAPI from '@/api/dashboard'
|
||||
|
||||
// 解构API方法
|
||||
const { getDashboardData, getUserGrowthData, getOrderStatsData } = dashboardAPI
|
||||
|
||||
// 定义仪表板数据结构
|
||||
interface DashboardData {
|
||||
@@ -221,15 +220,25 @@ const loadDashboardData = async () => {
|
||||
|
||||
// 获取仪表板数据
|
||||
const response = await getDashboardData()
|
||||
if (response.success) {
|
||||
if (response && response.success) {
|
||||
dashboardData.value = response.data
|
||||
} else {
|
||||
console.warn('获取仪表板数据失败,使用默认数据')
|
||||
}
|
||||
|
||||
// 获取图表数据并更新图表
|
||||
await updateUserGrowthChart()
|
||||
await updateOrderStatsChart()
|
||||
// 延迟更新图表,确保DOM已渲染
|
||||
setTimeout(async () => {
|
||||
await updateUserGrowthChart()
|
||||
await updateOrderStatsChart()
|
||||
}, 200)
|
||||
} catch (error) {
|
||||
console.error('加载仪表板数据失败:', error)
|
||||
// 显示用户友好的错误信息
|
||||
if (error instanceof Error) {
|
||||
console.error('API错误:', error.message)
|
||||
} else {
|
||||
console.error('API错误:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,6 +285,15 @@ const updateOrderStatsChart = async () => {
|
||||
try {
|
||||
const response = await getOrderStatsData(7)
|
||||
if (response.success && orderStatsChart.value) {
|
||||
// 确保DOM元素已经渲染
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 检查DOM元素是否存在且有尺寸
|
||||
if (!orderStatsChart.value || orderStatsChart.value.offsetWidth === 0) {
|
||||
console.warn('订单统计图表DOM元素未准备好')
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化图表实例
|
||||
if (!orderStatsChartInstance) {
|
||||
orderStatsChartInstance = echarts.init(orderStatsChart.value)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:title="modalTitle"
|
||||
:visible="visible"
|
||||
:open="open"
|
||||
:confirm-loading="confirmLoading"
|
||||
:width="800"
|
||||
@cancel="handleCancel"
|
||||
@@ -107,7 +107,7 @@ import { getMerchants } from '@/api/flower'
|
||||
import type { Flower, Merchant } from '@/api/flower'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
open: boolean
|
||||
currentRecord: Flower | null
|
||||
mode: 'create' | 'edit' | 'view'
|
||||
}
|
||||
@@ -166,8 +166,8 @@ const loadMerchants = async () => {
|
||||
try {
|
||||
merchantsLoading.value = true
|
||||
const response = await getMerchants()
|
||||
if (response.data.success) {
|
||||
merchants.value = response.data.data.merchants
|
||||
if (response.success) {
|
||||
merchants.value = response.data.merchants
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商家列表失败:', error)
|
||||
@@ -228,9 +228,9 @@ const handleOk = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
// 监听 open 变化
|
||||
watch(() => props.open, (open) => {
|
||||
if (open) {
|
||||
loadMerchants()
|
||||
if (props.currentRecord) {
|
||||
Object.assign(formState, props.currentRecord)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -217,7 +217,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { message, Modal, type FormInstance } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
@@ -231,7 +231,11 @@ import {
|
||||
StopOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import {
|
||||
import merchantAPI from '@/api/merchant'
|
||||
import type { Merchant, MerchantQueryParams } from '@/api/merchant'
|
||||
|
||||
// 解构API方法
|
||||
const {
|
||||
getMerchants,
|
||||
getMerchant,
|
||||
createMerchant,
|
||||
@@ -239,8 +243,7 @@ import {
|
||||
approveMerchant,
|
||||
rejectMerchant,
|
||||
disableMerchant
|
||||
} from '@/api/merchant'
|
||||
import type { Merchant, MerchantQueryParams } from '@/api/merchant'
|
||||
} = merchantAPI
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
@@ -409,8 +412,12 @@ const loadMerchants = async () => {
|
||||
}
|
||||
|
||||
const response = await getMerchants(params)
|
||||
merchantList.value = response.data.list
|
||||
pagination.total = response.data.pagination.total
|
||||
if (response && response.data) {
|
||||
merchantList.value = response.data.list || response.data
|
||||
if (response.data.pagination) {
|
||||
pagination.total = response.data.pagination.total
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载商家列表失败')
|
||||
} finally {
|
||||
@@ -447,6 +454,7 @@ const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
const handleView = async (record: Merchant) => {
|
||||
try {
|
||||
const response = await getMerchant(record.id)
|
||||
const merchantData = response.data
|
||||
Modal.info({
|
||||
title: '商家详情',
|
||||
width: 600,
|
||||
@@ -455,18 +463,18 @@ const handleView = async (record: Merchant) => {
|
||||
column: 1,
|
||||
bordered: true
|
||||
}, [
|
||||
h('a-descriptions-item', { label: '商家名称' }, response.data.name),
|
||||
h('a-descriptions-item', { label: '商家名称' }, merchantData.name),
|
||||
h('a-descriptions-item', { label: '类型' }, [
|
||||
h('a-tag', { color: getTypeColor(response.data.type) }, getTypeText(response.data.type))
|
||||
h('a-tag', { color: getTypeColor(merchantData.type) }, getTypeText(merchantData.type))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '状态' }, [
|
||||
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
|
||||
h('a-tag', { color: getStatusColor(merchantData.status) }, getStatusText(merchantData.status))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '联系人' }, response.data.contact_person),
|
||||
h('a-descriptions-item', { label: '联系电话' }, response.data.contact_phone),
|
||||
h('a-descriptions-item', { label: '地址' }, response.data.address || '-'),
|
||||
h('a-descriptions-item', { label: '描述' }, response.data.description || '-'),
|
||||
h('a-descriptions-item', { label: '创建时间' }, response.data.created_at)
|
||||
h('a-descriptions-item', { label: '联系人' }, merchantData.contact_person),
|
||||
h('a-descriptions-item', { label: '联系电话' }, merchantData.contact_phone),
|
||||
h('a-descriptions-item', { label: '地址' }, merchantData.address || '-'),
|
||||
h('a-descriptions-item', { label: '描述' }, merchantData.description || '-'),
|
||||
h('a-descriptions-item', { label: '创建时间' }, merchantData.created_at)
|
||||
])
|
||||
]),
|
||||
okText: '关闭'
|
||||
@@ -482,7 +490,7 @@ const handleEdit = async (record: Merchant) => {
|
||||
const response = await getMerchant(record.id)
|
||||
modalTitle.value = '编辑商家'
|
||||
isEditing.value = true
|
||||
currentMerchant.value = response.data
|
||||
currentMerchant.value = response.data as any
|
||||
modalVisible.value = true
|
||||
} catch (error) {
|
||||
message.error('获取商家详情失败')
|
||||
@@ -497,7 +505,7 @@ const showCreateModal = () => {
|
||||
currentMerchant.value = {
|
||||
status: 'pending',
|
||||
type: 'farm'
|
||||
}
|
||||
} as any
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
@@ -571,7 +579,7 @@ const handleReject = async (record: Merchant) => {
|
||||
content: `确定要拒绝商家 "${record.name}" 的审核吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await rejectMerchant(record.id)
|
||||
await rejectMerchant(record.id, '审核不通过')
|
||||
message.success('商家审核已拒绝')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
|
||||
@@ -388,7 +388,7 @@ const loadTravels = async () => {
|
||||
const params: TravelQueryParams = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
keyword: searchForm.keyword,
|
||||
title: searchForm.keyword, // 修改为title参数而不是keyword
|
||||
status: searchForm.status
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="normal">普通用户</a-select-option>
|
||||
<a-select-option value="farmer">普通用户</a-select-option>
|
||||
<a-select-option value="merchant">商家</a-select-option>
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
</a-select>
|
||||
@@ -147,7 +147,7 @@
|
||||
:data-source="userList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
:row-key="(record: any) => record.id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
@@ -267,7 +267,7 @@
|
||||
<a-col :span="12">
|
||||
<a-form-item label="用户类型" name="userType">
|
||||
<a-select v-model:value="currentUser.userType" placeholder="请选择用户类型">
|
||||
<a-select-option value="normal">普通用户</a-select-option>
|
||||
<a-select-option value="farmer">普通用户</a-select-option>
|
||||
<a-select-option value="merchant">商家</a-select-option>
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
</a-select>
|
||||
@@ -293,7 +293,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, h } from 'vue'
|
||||
import { message, Modal, type FormInstance } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
@@ -311,8 +311,10 @@ import {
|
||||
DownOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { getUsers, getUser, createUser, updateUser, updateUserStatus } from '@/api/user'
|
||||
import userAPI from '@/api/user'
|
||||
|
||||
// 解构API方法
|
||||
const { getUsers, getUser, createUser, updateUser, updateUserStatus } = userAPI
|
||||
import type { User, UserQueryParams } from '@/types/user'
|
||||
|
||||
interface SearchForm {
|
||||
@@ -385,25 +387,27 @@ const columns = [
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '用户类型',
|
||||
dataIndex: 'user_type',
|
||||
key: 'userType',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'registerTime',
|
||||
dataIndex: 'created_at',
|
||||
key: 'registerTime',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '最后登录',
|
||||
dataIndex: 'lastLoginTime',
|
||||
dataIndex: 'last_login_at',
|
||||
key: 'lastLoginTime',
|
||||
width: 120
|
||||
},
|
||||
@@ -437,20 +441,24 @@ const getStatusText = (status: string) => {
|
||||
// 类型映射
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors = {
|
||||
normal: 'blue',
|
||||
farmer: 'blue',
|
||||
merchant: 'purple',
|
||||
admin: 'red'
|
||||
}
|
||||
return colors[type as keyof typeof colors] || 'default'
|
||||
// 处理后端返回的user_type字段,可能为user_type或userType
|
||||
const userType = type && typeof type === 'string' ? type.toLowerCase() : '';
|
||||
return colors[userType as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const texts = {
|
||||
normal: '普通用户',
|
||||
farmer: '普通用户',
|
||||
merchant: '商家',
|
||||
admin: '管理员'
|
||||
}
|
||||
return texts[type as keyof typeof texts] || '未知'
|
||||
// 处理后端返回的user_type字段,可能为user_type或userType
|
||||
const userType = type && typeof type === 'string' ? type.toLowerCase() : '';
|
||||
return texts[userType as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 添加模态框相关状态
|
||||
@@ -505,9 +513,27 @@ const loadUsers = async () => {
|
||||
}
|
||||
|
||||
const response = await getUsers(params)
|
||||
userList.value = response.data.list
|
||||
pagination.total = response.data.pagination.total
|
||||
if (response && response.data) {
|
||||
// 根据实际返回的数据结构调整处理方式
|
||||
if (response.data.users) {
|
||||
// 如果返回的是分页数据结构 { users: [...], pagination: {...} }
|
||||
userList.value = response.data.users
|
||||
if (response.data.pagination) {
|
||||
pagination.total = response.data.pagination.total
|
||||
}
|
||||
} else if (Array.isArray(response.data)) {
|
||||
// 如果返回的是简单数组
|
||||
userList.value = response.data
|
||||
} else if (response.data.list) {
|
||||
// 兼容旧的分页数据结构 { list: [...], pagination: {...} }
|
||||
userList.value = response.data.list
|
||||
if (response.data.pagination) {
|
||||
pagination.total = response.data.pagination.total
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户列表失败:', error)
|
||||
message.error('加载用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -559,7 +585,7 @@ const handleView = async (record: User) => {
|
||||
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '用户类型' }, [
|
||||
h('a-tag', { color: getTypeColor(response.data.userType) }, getTypeText(response.data.userType))
|
||||
h('a-tag', { color: getTypeColor(response.data.user_type || response.data.userType) }, getTypeText(response.data.user_type || response.data.userType))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '注册时间' }, response.data.registerTime),
|
||||
h('a-descriptions-item', { label: '最后登录' }, response.data.lastLoginTime)
|
||||
|
||||
@@ -15,7 +15,7 @@ const { globalErrorHandler, notFound } = require('./utils/errors');
|
||||
// 检查是否为无数据库模式
|
||||
const NO_DB_MODE = process.env.NO_DB_MODE === 'true';
|
||||
|
||||
let authRoutes, userRoutes, travelRoutes, animalRoutes, orderRoutes, adminRoutes, travelRegistrationRoutes, promotionRoutes, merchantRoutes;
|
||||
let authRoutes, userRoutes, travelRoutes, animalRoutes, orderRoutes, adminRoutes, travelRegistrationRoutes, promotionRoutes, merchantRoutes, travelsRoutes;
|
||||
|
||||
// 路由导入
|
||||
if (NO_DB_MODE) {
|
||||
@@ -25,6 +25,7 @@ if (NO_DB_MODE) {
|
||||
authRoutes = require('./routes/auth');
|
||||
userRoutes = require('./routes/user');
|
||||
travelRoutes = require('./routes/travel');
|
||||
travelsRoutes = require('./routes/travels'); // 新增travels路由
|
||||
animalRoutes = require('./routes/animal');
|
||||
orderRoutes = require('./routes/order');
|
||||
adminRoutes = require('./routes/admin'); // 新增管理员路由
|
||||
@@ -263,6 +264,7 @@ if (NO_DB_MODE) {
|
||||
app.use('/api/v1/auth', authRoutes);
|
||||
app.use('/api/v1/users', userRoutes);
|
||||
app.use('/api/v1/travel', travelRoutes);
|
||||
app.use('/api/v1/travels', travelsRoutes); // 新增travels路由
|
||||
app.use('/api/v1/animals', animalRoutes);
|
||||
app.use('/api/v1/orders', orderRoutes);
|
||||
app.use('/api/v1/payments', paymentRoutes);
|
||||
|
||||
576
backend/src/controllers/admin.js
Normal file
576
backend/src/controllers/admin.js
Normal file
@@ -0,0 +1,576 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Admin = require('../models/admin');
|
||||
const UserMySQL = require('../models/UserMySQL');
|
||||
const db = require('../config/database');
|
||||
const { AppError } = require('../utils/errors');
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
*/
|
||||
exports.login = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '用户名和密码不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
// 查找管理员
|
||||
const admin = await Admin.findByUsername(username);
|
||||
if (!admin) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
code: 401,
|
||||
message: '用户名或密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isValidPassword = await bcrypt.compare(password, admin.password);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
code: 401,
|
||||
message: '用户名或密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查账号状态
|
||||
if (admin.status !== 1) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
code: 401,
|
||||
message: '账号已被禁用'
|
||||
});
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
role: admin.role
|
||||
},
|
||||
process.env.JWT_SECRET || 'admin-secret-key',
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
// 更新最后登录时间
|
||||
await admin.updateLastLogin();
|
||||
|
||||
// 返回登录成功信息
|
||||
const adminInfo = {
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
email: admin.email,
|
||||
nickname: admin.nickname,
|
||||
avatar: admin.avatar,
|
||||
role: admin.role,
|
||||
status: admin.status,
|
||||
last_login: admin.last_login,
|
||||
created_at: admin.created_at,
|
||||
updated_at: admin.updated_at
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
admin: adminInfo,
|
||||
token
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前管理员信息
|
||||
*/
|
||||
exports.getProfile = async (req, res, next) => {
|
||||
try {
|
||||
const admin = req.admin;
|
||||
|
||||
const adminInfo = {
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
email: admin.email,
|
||||
nickname: admin.nickname,
|
||||
avatar: admin.avatar,
|
||||
role: admin.role,
|
||||
status: admin.status,
|
||||
last_login: admin.last_login,
|
||||
created_at: admin.created_at,
|
||||
updated_at: admin.updated_at
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
admin: adminInfo
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取仪表板数据
|
||||
*/
|
||||
exports.getDashboard = async (req, res, next) => {
|
||||
try {
|
||||
// 获取统计数据
|
||||
const statistics = await getDashboardStatistics();
|
||||
|
||||
// 获取最近活动
|
||||
const recentActivities = await getRecentActivities();
|
||||
|
||||
// 获取系统信息
|
||||
const systemInfo = getSystemInfo();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
statistics,
|
||||
recentActivities,
|
||||
systemInfo
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户增长数据
|
||||
*/
|
||||
exports.getUserGrowth = async (req, res, next) => {
|
||||
try {
|
||||
const days = parseInt(req.query.days) || 7;
|
||||
|
||||
// 生成日期范围
|
||||
const dates = [];
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
dates.push(date.toISOString().split('T')[0]);
|
||||
}
|
||||
|
||||
// 查询每日新增用户数据
|
||||
const growthData = [];
|
||||
let totalNewUsers = 0;
|
||||
|
||||
for (const date of dates) {
|
||||
try {
|
||||
const rows = await db.query(
|
||||
'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = ?',
|
||||
[date]
|
||||
);
|
||||
const newUsers = rows[0].count;
|
||||
totalNewUsers += newUsers;
|
||||
|
||||
// 获取截止到该日期的总用户数
|
||||
const totalRows = await db.query(
|
||||
'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) <= ?',
|
||||
[date]
|
||||
);
|
||||
const totalUsers = totalRows[0].count;
|
||||
|
||||
growthData.push({
|
||||
date,
|
||||
newUsers,
|
||||
totalUsers
|
||||
});
|
||||
} catch (error) {
|
||||
// 如果查询失败,使用模拟数据
|
||||
growthData.push({
|
||||
date,
|
||||
newUsers: Math.floor(Math.random() * 10),
|
||||
totalUsers: Math.floor(Math.random() * 100) + 50
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const averageDaily = totalNewUsers / days;
|
||||
const growthRate = growthData.length > 1 ?
|
||||
((growthData[growthData.length - 1].newUsers - growthData[0].newUsers) / Math.max(growthData[0].newUsers, 1)) * 100 : 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
growthData,
|
||||
summary: {
|
||||
totalNewUsers,
|
||||
averageDaily: Math.round(averageDaily * 100) / 100,
|
||||
growthRate: Math.round(growthRate * 100) / 100
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取管理员列表
|
||||
*/
|
||||
exports.getList = async (req, res, next) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const pageSize = parseInt(req.query.pageSize) || 10;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const admins = await Admin.getList({ offset, limit: pageSize });
|
||||
const total = await Admin.getCount();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
admins: admins.map(admin => ({
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
email: admin.email,
|
||||
nickname: admin.nickname,
|
||||
avatar: admin.avatar,
|
||||
role: admin.role,
|
||||
status: admin.status,
|
||||
last_login: admin.last_login,
|
||||
created_at: admin.created_at,
|
||||
updated_at: admin.updated_at
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建管理员
|
||||
*/
|
||||
exports.create = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password, email, nickname, role } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '用户名和密码不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingAdmin = await Admin.findByUsername(username);
|
||||
if (existingAdmin) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
code: 409,
|
||||
message: '用户名已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建管理员
|
||||
const adminId = await Admin.create({
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
nickname,
|
||||
role: role || 'admin'
|
||||
});
|
||||
|
||||
const newAdmin = await Admin.findById(adminId);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
code: 201,
|
||||
message: '创建成功',
|
||||
data: {
|
||||
admin: {
|
||||
id: newAdmin.id,
|
||||
username: newAdmin.username,
|
||||
email: newAdmin.email,
|
||||
nickname: newAdmin.nickname,
|
||||
avatar: newAdmin.avatar,
|
||||
role: newAdmin.role,
|
||||
status: newAdmin.status,
|
||||
created_at: newAdmin.created_at,
|
||||
updated_at: newAdmin.updated_at
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新管理员
|
||||
*/
|
||||
exports.update = async (req, res, next) => {
|
||||
try {
|
||||
const adminId = parseInt(req.params.id);
|
||||
const { email, nickname, role, status } = req.body;
|
||||
|
||||
// 不能修改自己的角色
|
||||
if (adminId === req.admin.id && role && role !== req.admin.role) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '不能修改自己的角色'
|
||||
});
|
||||
}
|
||||
|
||||
const admin = await Admin.findById(adminId);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '管理员不存在'
|
||||
});
|
||||
}
|
||||
|
||||
await Admin.update(adminId, { email, nickname, role, status });
|
||||
const updatedAdmin = await Admin.findById(adminId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '更新成功',
|
||||
data: {
|
||||
admin: {
|
||||
id: updatedAdmin.id,
|
||||
username: updatedAdmin.username,
|
||||
email: updatedAdmin.email,
|
||||
nickname: updatedAdmin.nickname,
|
||||
avatar: updatedAdmin.avatar,
|
||||
role: updatedAdmin.role,
|
||||
status: updatedAdmin.status,
|
||||
last_login: updatedAdmin.last_login,
|
||||
created_at: updatedAdmin.created_at,
|
||||
updated_at: updatedAdmin.updated_at
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除管理员
|
||||
*/
|
||||
exports.delete = async (req, res, next) => {
|
||||
try {
|
||||
const adminId = parseInt(req.params.id);
|
||||
|
||||
// 不能删除自己
|
||||
if (adminId === req.admin.id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '不能删除自己'
|
||||
});
|
||||
}
|
||||
|
||||
const admin = await Admin.findById(adminId);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '管理员不存在'
|
||||
});
|
||||
}
|
||||
|
||||
await Admin.delete(adminId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 辅助函数
|
||||
|
||||
/**
|
||||
* 获取仪表板统计数据
|
||||
*/
|
||||
async function getDashboardStatistics() {
|
||||
try {
|
||||
// 尝试从数据库获取真实数据
|
||||
const userRows = await db.query('SELECT COUNT(*) as count FROM users');
|
||||
const animalRows = await db.query('SELECT COUNT(*) as count FROM animals');
|
||||
|
||||
// 对于可能不存在的表,使用try-catch
|
||||
let travelCount = 0;
|
||||
let claimCount = 0;
|
||||
|
||||
try {
|
||||
const travelRows = await db.query('SELECT COUNT(*) as count FROM travel_plans');
|
||||
travelCount = travelRows[0].count;
|
||||
} catch (error) {
|
||||
console.log('travel_plans表不存在,使用默认值');
|
||||
}
|
||||
|
||||
try {
|
||||
const claimRows = await db.query('SELECT COUNT(*) as count FROM animal_claims');
|
||||
claimCount = claimRows[0].count;
|
||||
} catch (error) {
|
||||
console.log('animal_claims表不存在,使用默认值');
|
||||
}
|
||||
|
||||
// 今日新增数据
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const todayUserRows = await db.query('SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = ?', [today]);
|
||||
const todayAnimalRows = await db.query('SELECT COUNT(*) as count FROM animals WHERE DATE(created_at) = ?', [today]);
|
||||
|
||||
let todayTravelCount = 0;
|
||||
let todayClaimCount = 0;
|
||||
|
||||
try {
|
||||
const todayTravelRows = await db.query('SELECT COUNT(*) as count FROM travel_plans WHERE DATE(created_at) = ?', [today]);
|
||||
todayTravelCount = todayTravelRows[0].count;
|
||||
} catch (error) {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
try {
|
||||
const todayClaimRows = await db.query('SELECT COUNT(*) as count FROM animal_claims WHERE DATE(created_at) = ?', [today]);
|
||||
todayClaimCount = todayClaimRows[0].count;
|
||||
} catch (error) {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
return {
|
||||
totalUsers: userRows[0].count,
|
||||
totalAnimals: animalRows[0].count,
|
||||
totalTravels: travelCount,
|
||||
totalClaims: claimCount,
|
||||
todayNewUsers: todayUserRows[0].count,
|
||||
todayNewAnimals: todayAnimalRows[0].count,
|
||||
todayNewTravels: todayTravelCount,
|
||||
todayNewClaims: todayClaimCount
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
// 返回默认数据
|
||||
return {
|
||||
totalUsers: 0,
|
||||
totalAnimals: 0,
|
||||
totalTravels: 0,
|
||||
totalClaims: 0,
|
||||
todayNewUsers: 0,
|
||||
todayNewAnimals: 0,
|
||||
todayNewTravels: 0,
|
||||
todayNewClaims: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近活动
|
||||
*/
|
||||
async function getRecentActivities() {
|
||||
try {
|
||||
const activities = [];
|
||||
|
||||
// 获取最近用户注册
|
||||
try {
|
||||
const userRows = await db.query(`
|
||||
SELECT u.id, u.real_name as nickname, u.created_at
|
||||
FROM users u
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
userRows.forEach(user => {
|
||||
activities.push({
|
||||
type: 'user_register',
|
||||
description: `用户 ${user.nickname || '未知'} 注册了账号`,
|
||||
timestamp: user.created_at,
|
||||
user: {
|
||||
id: user.id,
|
||||
nickname: user.nickname
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('获取用户活动失败:', error.message);
|
||||
}
|
||||
|
||||
// 获取最近动物添加
|
||||
try {
|
||||
const animalRows = await db.query(`
|
||||
SELECT a.id, a.name, a.created_at, u.id as user_id, u.real_name as nickname
|
||||
FROM animals a
|
||||
LEFT JOIN users u ON a.farmer_id = u.id
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
animalRows.forEach(animal => {
|
||||
activities.push({
|
||||
type: 'animal_add',
|
||||
description: `用户 ${animal.nickname || '未知'} 添加了动物 ${animal.name}`,
|
||||
timestamp: animal.created_at,
|
||||
user: {
|
||||
id: animal.user_id,
|
||||
nickname: animal.nickname
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('获取动物活动失败:', error.message);
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
activities.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
||||
|
||||
return activities.slice(0, 10);
|
||||
} catch (error) {
|
||||
console.error('获取最近活动失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
function getSystemInfo() {
|
||||
const uptime = process.uptime();
|
||||
const hours = Math.floor(uptime / 3600);
|
||||
const minutes = Math.floor((uptime % 3600) / 60);
|
||||
|
||||
return {
|
||||
serverTime: new Date().toISOString(),
|
||||
uptime: `${hours}小时${minutes}分钟`,
|
||||
version: '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const TravelService = require('../../services/travel');
|
||||
const TravelService = require('../../services/travel/index');
|
||||
const { success } = require('../../utils/response');
|
||||
const { AppError } = require('../../utils/errors');
|
||||
|
||||
@@ -149,12 +149,12 @@ class TravelController {
|
||||
// 获取所有旅行计划(管理员功能)
|
||||
static async getAllTravelPlans(req, res, next) {
|
||||
try {
|
||||
const { page, pageSize, status, userId } = req.query;
|
||||
const { page, limit, keyword, status } = req.query;
|
||||
|
||||
const result = await TravelService.getTravelPlans({
|
||||
userId,
|
||||
page: parseInt(page) || 1,
|
||||
pageSize: parseInt(pageSize) || 10,
|
||||
pageSize: parseInt(limit) || 20,
|
||||
keyword,
|
||||
status
|
||||
});
|
||||
|
||||
|
||||
@@ -17,8 +17,15 @@ async function authenticateUser(req, res, next) {
|
||||
// 验证token
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key');
|
||||
|
||||
// 兼容管理员token和用户token
|
||||
const userId = decoded.userId || decoded.id;
|
||||
|
||||
if (!userId) {
|
||||
throw new AppError('token中缺少用户ID', 401);
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const user = await UserMySQL.findById(decoded.userId);
|
||||
const user = await UserMySQL.findById(userId);
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 401);
|
||||
}
|
||||
@@ -30,7 +37,7 @@ async function authenticateUser(req, res, next) {
|
||||
|
||||
// 将用户信息添加到请求对象
|
||||
req.user = UserMySQL.sanitize(user);
|
||||
req.userId = decoded.userId; // 同时设置userId,保持与现有控制器的兼容性
|
||||
req.userId = userId; // 同时设置userId,保持与现有控制器的兼容性
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
|
||||
@@ -23,24 +23,30 @@ router.post('/', authenticate, createOrder);
|
||||
// 获取订单详情
|
||||
router.get('/:orderId', authenticate, getOrder);
|
||||
|
||||
// 获取订单列表
|
||||
router.get('/', authenticate, getUserOrders);
|
||||
// 获取订单统计信息
|
||||
router.get('/statistics', authenticate, getOrderStatistics);
|
||||
|
||||
// 管理员获取所有订单
|
||||
router.get('/admin', authenticate, requireAdmin, getAllOrders);
|
||||
|
||||
// 商家获取订单列表
|
||||
router.get('/merchant', authenticate, requireMerchant, getMerchantOrders);
|
||||
|
||||
// 获取订单列表(用户或管理员)
|
||||
router.get('/', authenticate, (req, res, next) => {
|
||||
if (req.user.role === 'admin' || req.user.role === 'super_admin') {
|
||||
return getAllOrders(req, res, next);
|
||||
} else {
|
||||
return getUserOrders(req, res, next);
|
||||
}
|
||||
});
|
||||
|
||||
// 取消订单
|
||||
router.put('/:orderId/cancel', authenticate, cancelOrder);
|
||||
|
||||
// 支付订单
|
||||
router.put('/:orderId/pay', authenticate, payOrder);
|
||||
|
||||
// 获取订单统计信息
|
||||
router.get('/statistics', authenticate, getOrderStatistics);
|
||||
|
||||
// 管理员获取所有订单
|
||||
router.get('/admin', authenticate, requireAdmin, getAllOrders);
|
||||
|
||||
// 管理员更新订单状态
|
||||
router.put('/:orderId/status', authenticate, requireAdmin, updateOrderStatus);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const { body, query } = require('express-validator');
|
||||
const TravelController = require('../controllers/travel');
|
||||
const TravelController = require('../controllers/travel/index');
|
||||
const { authenticateUser: authenticate, requireRole: requireAdmin } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
@@ -75,6 +75,9 @@ const router = express.Router();
|
||||
*/
|
||||
router.get('/plans', authenticate, TravelController.getTravelPlans);
|
||||
|
||||
// 添加 /travels 路由,用于管理后台获取旅行计划列表
|
||||
router.get('/travels', authenticate, TravelController.getAllTravelPlans);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /travel/plans/{planId}:
|
||||
|
||||
122
backend/src/routes/travels.js
Normal file
122
backend/src/routes/travels.js
Normal file
@@ -0,0 +1,122 @@
|
||||
const express = require('express');
|
||||
const { query } = require('express-validator');
|
||||
const TravelController = require('../controllers/travel/index');
|
||||
const { authenticateUser, requireRole } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Travel:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* title:
|
||||
* type: string
|
||||
* description:
|
||||
* type: string
|
||||
* destination:
|
||||
* type: string
|
||||
* start_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* end_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* max_participants:
|
||||
* type: integer
|
||||
* current_participants:
|
||||
* type: integer
|
||||
* price_per_person:
|
||||
* type: number
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [draft, published, archived, cancelled]
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /travels:
|
||||
* get:
|
||||
* summary: 获取旅行计划列表(管理后台)
|
||||
* tags: [Travel]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 20
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: keyword
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [draft, published, archived, cancelled]
|
||||
* description: 状态筛选
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取旅行计划列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* code:
|
||||
* type: integer
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* plans:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Travel'
|
||||
* pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* pageSize:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* totalPages:
|
||||
* type: integer
|
||||
*/
|
||||
router.get('/',
|
||||
authenticateUser,
|
||||
[
|
||||
query('page').optional().isInt({ min: 1 }).withMessage('页码必须是正整数'),
|
||||
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('每页数量必须在1-100之间'),
|
||||
query('keyword').optional().isString().withMessage('关键词必须是字符串'),
|
||||
query('status').optional().isIn(['draft', 'published', 'archived', 'cancelled']).withMessage('状态值无效')
|
||||
],
|
||||
TravelController.getAllTravelPlans
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -32,13 +32,20 @@ process.on('uncaughtException', (err) => {
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('========================================')
|
||||
console.error('❌ 未处理的Promise拒绝:')
|
||||
console.error(`🔹 消息: ${err.message}`)
|
||||
console.error(`🔹 堆栈: ${err.stack}`)
|
||||
console.error('🔹 Promise:', promise)
|
||||
console.error('🔹 原因:', reason)
|
||||
if (reason && reason.message) {
|
||||
console.error(`🔹 消息: ${reason.message}`)
|
||||
}
|
||||
if (reason && reason.stack) {
|
||||
console.error(`🔹 堆栈: ${reason.stack}`)
|
||||
}
|
||||
console.error('========================================')
|
||||
process.exit(1)
|
||||
// 不立即退出,让服务继续运行以便调试
|
||||
// process.exit(1)
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
|
||||
@@ -52,7 +52,7 @@ class OrderService {
|
||||
SELECT
|
||||
o.*,
|
||||
a.name as animal_name,
|
||||
a.species as animal_species,
|
||||
a.type as animal_species,
|
||||
a.price as animal_price,
|
||||
u.username as user_name,
|
||||
m.business_name as merchant_name
|
||||
@@ -90,20 +90,25 @@ class OrderService {
|
||||
let query = `
|
||||
SELECT
|
||||
o.*,
|
||||
a.name as animal_name,
|
||||
a.species as animal_species,
|
||||
a.price as animal_price,
|
||||
m.business_name as merchant_name
|
||||
CASE
|
||||
WHEN o.type = 'animal_claim' THEN a.name
|
||||
WHEN o.type = 'travel' THEN t.title
|
||||
ELSE o.title
|
||||
END as item_name,
|
||||
CASE
|
||||
WHEN o.type = 'animal_claim' THEN a.type
|
||||
ELSE o.type
|
||||
END as item_type
|
||||
FROM orders o
|
||||
LEFT JOIN animals a ON o.animal_id = a.id
|
||||
LEFT JOIN merchants m ON o.merchant_id = m.id
|
||||
WHERE o.user_id = ? AND o.is_deleted = 0
|
||||
LEFT JOIN animals a ON o.type = 'animal_claim' AND o.related_id = a.id
|
||||
LEFT JOIN travel_plans t ON o.type = 'travel' AND o.related_id = t.id
|
||||
WHERE o.user_id = ?
|
||||
`;
|
||||
|
||||
let countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM orders o
|
||||
WHERE o.user_id = ? AND o.is_deleted = 0
|
||||
WHERE o.user_id = ?
|
||||
`;
|
||||
|
||||
const params = [userId];
|
||||
@@ -116,10 +121,10 @@ class OrderService {
|
||||
countParams.push(status);
|
||||
}
|
||||
|
||||
query += ' ORDER BY o.created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(pageSize, offset);
|
||||
query += ' ORDER BY o.ordered_at DESC LIMIT ?, ?';
|
||||
params.push(parseInt(offset), parseInt(pageSize));
|
||||
|
||||
const [orders] = await database.query(query, params);
|
||||
const orders = await database.query(query, params);
|
||||
const [totalResult] = await database.query(countQuery, countParams);
|
||||
|
||||
return {
|
||||
@@ -152,7 +157,7 @@ class OrderService {
|
||||
SELECT
|
||||
o.*,
|
||||
a.name as animal_name,
|
||||
a.species as animal_species,
|
||||
a.type as animal_species,
|
||||
a.price as animal_price,
|
||||
u.username as user_name
|
||||
FROM orders o
|
||||
@@ -341,30 +346,36 @@ class OrderService {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
status,
|
||||
merchantId,
|
||||
userId,
|
||||
order_no
|
||||
order_no,
|
||||
type
|
||||
} = filters;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
o.*,
|
||||
a.name as animal_name,
|
||||
a.species as animal_species,
|
||||
u.username as user_name,
|
||||
m.business_name as merchant_name
|
||||
CASE
|
||||
WHEN o.type = 'animal_claim' THEN a.name
|
||||
WHEN o.type = 'travel' THEN t.title
|
||||
ELSE o.title
|
||||
END as item_name,
|
||||
CASE
|
||||
WHEN o.type = 'animal_claim' THEN a.type
|
||||
ELSE o.type
|
||||
END as item_type
|
||||
FROM orders o
|
||||
LEFT JOIN animals a ON o.animal_id = a.id
|
||||
LEFT JOIN users u ON o.user_id = u.id
|
||||
LEFT JOIN merchants m ON o.merchant_id = m.id
|
||||
WHERE o.is_deleted = 0
|
||||
LEFT JOIN animals a ON o.type = 'animal_claim' AND o.related_id = a.id
|
||||
LEFT JOIN travel_plans t ON o.type = 'travel' AND o.related_id = t.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
let countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM orders o
|
||||
WHERE o.is_deleted = 0
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
@@ -377,13 +388,6 @@ class OrderService {
|
||||
countParams.push(status);
|
||||
}
|
||||
|
||||
if (merchantId) {
|
||||
query += ' AND o.merchant_id = ?';
|
||||
countQuery += ' AND o.merchant_id = ?';
|
||||
params.push(merchantId);
|
||||
countParams.push(merchantId);
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
query += ' AND o.user_id = ?';
|
||||
countQuery += ' AND o.user_id = ?';
|
||||
@@ -391,17 +395,24 @@ class OrderService {
|
||||
countParams.push(userId);
|
||||
}
|
||||
|
||||
if (order_no) {
|
||||
if (order_no && order_no.trim() !== '') {
|
||||
query += ' AND o.order_no = ?';
|
||||
countQuery += ' AND o.order_no = ?';
|
||||
params.push(order_no);
|
||||
countParams.push(order_no);
|
||||
}
|
||||
|
||||
query += ' ORDER BY o.created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(pageSize, offset);
|
||||
if (type) {
|
||||
query += ' AND o.type = ?';
|
||||
countQuery += ' AND o.type = ?';
|
||||
params.push(type);
|
||||
countParams.push(type);
|
||||
}
|
||||
|
||||
const [orders] = await database.query(query, params);
|
||||
query += ' ORDER BY o.ordered_at DESC LIMIT ?, ?';
|
||||
params.push(parseInt(offset), parseInt(pageSize));
|
||||
|
||||
const orders = await database.query(query, params);
|
||||
const [totalResult] = await database.query(countQuery, countParams);
|
||||
|
||||
return {
|
||||
@@ -435,9 +446,8 @@ class OrderService {
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_orders,
|
||||
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_orders,
|
||||
SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) as refunded_orders,
|
||||
SUM(total_amount) as total_revenue
|
||||
FROM orders
|
||||
WHERE is_deleted = 0
|
||||
SUM(final_amount) as total_revenue
|
||||
FROM orders
|
||||
`;
|
||||
|
||||
const [stats] = await database.query(query);
|
||||
|
||||
@@ -5,7 +5,7 @@ class TravelService {
|
||||
// 获取旅行计划列表
|
||||
static async getTravelPlans(searchParams) {
|
||||
try {
|
||||
const { userId, page = 1, pageSize = 10, status } = searchParams;
|
||||
const { userId, page = 1, pageSize = 10, status, keyword } = searchParams;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
|
||||
// 构建基础查询条件
|
||||
@@ -22,6 +22,12 @@ class TravelService {
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
whereClause += ' AND (tp.title LIKE ? OR tp.destination LIKE ? OR tp.description LIKE ?)';
|
||||
const keywordParam = `%${keyword}%`;
|
||||
params.push(keywordParam, keywordParam, keywordParam);
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
const countSql = `SELECT COUNT(*) as total FROM travel_plans tp ${whereClause}`;
|
||||
const countResult = await query(countSql, params);
|
||||
|
||||
@@ -63,7 +63,7 @@ class UserService {
|
||||
// 搜索用户(管理员功能)
|
||||
static async searchUsers(searchParams) {
|
||||
try {
|
||||
const { keyword, userType, page = 1, pageSize = 10 } = searchParams;
|
||||
const { keyword, userType, status, page = 1, pageSize = 10 } = searchParams;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
let sql = `
|
||||
@@ -72,7 +72,8 @@ class UserService {
|
||||
FROM users
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
const countParams = [];
|
||||
const queryParams = [];
|
||||
|
||||
if (keyword) {
|
||||
sql += ` AND (
|
||||
@@ -82,23 +83,36 @@ class UserService {
|
||||
phone LIKE ?
|
||||
)`;
|
||||
const likeKeyword = `%${keyword}%`;
|
||||
params.push(likeKeyword, likeKeyword, likeKeyword, likeKeyword);
|
||||
countParams.push(likeKeyword, likeKeyword, likeKeyword, likeKeyword);
|
||||
queryParams.push(likeKeyword, likeKeyword, likeKeyword, likeKeyword);
|
||||
}
|
||||
|
||||
if (userType) {
|
||||
sql += ' AND user_type = ?';
|
||||
params.push(userType);
|
||||
countParams.push(userType);
|
||||
queryParams.push(userType);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
sql += ' AND status = ?';
|
||||
countParams.push(status);
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
const countSql = `SELECT COUNT(*) as total FROM (${sql}) as count_query`;
|
||||
const countResult = await UserMySQL.query(countSql, params);
|
||||
let countSql = `SELECT COUNT(*) as total FROM users WHERE 1=1`;
|
||||
if (countParams.length > 0) {
|
||||
const whereClause = sql.substring(sql.indexOf("AND") + 3);
|
||||
countSql += " AND" + whereClause;
|
||||
}
|
||||
const countResult = await UserMySQL.query(countSql, countParams);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 添加分页和排序
|
||||
sql += ` ORDER BY created_at DESC LIMIT ${parseInt(pageSize)} OFFSET ${parseInt(offset)}`;
|
||||
sql += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
||||
queryParams.push(parseInt(pageSize), parseInt(offset));
|
||||
|
||||
const users = await UserMySQL.query(sql, params);
|
||||
const users = await UserMySQL.query(sql, queryParams);
|
||||
|
||||
return {
|
||||
users: users.map(user => UserMySQL.sanitize(user)),
|
||||
|
||||
223
docs/API路由匹配分析报告.md
Normal file
223
docs/API路由匹配分析报告.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# API路由匹配分析报告
|
||||
|
||||
## 概述
|
||||
|
||||
本报告分析管理后台前端API调用与后端路由定义的匹配情况,识别不匹配的接口并提供修复建议。
|
||||
|
||||
**分析时间**: 2024年1月
|
||||
**前端基础URL**: `http://localhost:3200/api/v1`
|
||||
**后端路由前缀**: `/api/v1`
|
||||
|
||||
## 匹配情况总结
|
||||
|
||||
### ✅ 匹配的接口
|
||||
|
||||
| 模块 | 前端调用路径 | 后端路由 | 状态 |
|
||||
|------|-------------|----------|------|
|
||||
| 认证 | `/admin/login` | `/admin/login` | ✅ 匹配 |
|
||||
| 认证 | `/admin/profile` | `/admin/profile` | ✅ 匹配 |
|
||||
| 认证 | `/auth/refresh` | `/auth/refresh` | ✅ 匹配 |
|
||||
| 认证 | `/auth/logout` | `/auth/logout` | ✅ 匹配 |
|
||||
| 仪表板 | `/admin/dashboard` | `/admin/dashboard` | ✅ 匹配 |
|
||||
| 仪表板 | `/admin/dashboard/activities` | `/admin/dashboard/user-growth` | ⚠️ 部分匹配 |
|
||||
| 系统管理 | `/admin/system/services` | `/admin/system/stats` | ⚠️ 部分匹配 |
|
||||
| 系统管理 | `/admin/system/info` | `/admin/system/info` | ✅ 匹配 |
|
||||
| 用户管理 | `/users` | `/users` | ✅ 匹配 |
|
||||
| 商户管理 | `/merchants` | `/merchants` | ✅ 匹配 |
|
||||
| 动物管理 | `/animals` | `/animals` | ✅ 匹配 |
|
||||
| 动物认领 | `/animal-claims` | `/animal-claims` | ✅ 匹配 |
|
||||
| 订单管理 | `/orders/admin` | `/orders/admin` | ✅ 匹配 |
|
||||
| 订单统计 | `/orders/statistics` | `/orders/statistics` | ✅ 匹配 |
|
||||
| 旅游管理 | `/travel/travels` | `/travel/travels` | ✅ 匹配 |
|
||||
| 旅游计划 | `/travel-plans` | `/travel/plans` | ⚠️ 路径不匹配 |
|
||||
| 促销活动 | `/promotions` | `/promotion/activities` | ⚠️ 路径不匹配 |
|
||||
| 促销活动 | `/promotion/activities` | `/promotion/activities` | ✅ 匹配 |
|
||||
| 促销奖励 | `/promotion/rewards` | `/promotion/rewards` | ✅ 匹配 |
|
||||
|
||||
### ❌ 不匹配的接口
|
||||
|
||||
| 前端调用路径 | 预期后端路由 | 实际后端路由 | 问题描述 |
|
||||
|-------------|-------------|-------------|----------|
|
||||
| `/flowers` | `/flowers` | 不存在 | 花卉管理接口缺失 |
|
||||
| `/flower-sales` | `/flower-sales` | 不存在 | 花卉销售接口缺失 |
|
||||
| `/admin/permissions` | `/admin/permissions` | 不存在 | 权限管理接口缺失 |
|
||||
| `/admin/system/database-status` | `/admin/system/database-status` | 不存在 | 数据库状态接口缺失 |
|
||||
| `/admin/system/cache-status` | `/admin/system/cache-status` | 不存在 | 缓存状态接口缺失 |
|
||||
| `/admin/system-configs` | `/admin/system-configs` | 不存在 | 系统配置接口缺失 |
|
||||
| `/admin/system/logs` | `/admin/system/logs` | 不存在 | 系统日志接口缺失 |
|
||||
| `/admin/system/settings` | `/admin/system/settings` | 不存在 | 系统设置接口缺失 |
|
||||
| `/admin/system/monitor` | `/admin/system/monitor` | 不存在 | 系统监控接口缺失 |
|
||||
| `/travels` | `/travels` | `/travel/travels` | 路径不一致 |
|
||||
|
||||
## 详细分析
|
||||
|
||||
### 1. 认证模块 (Auth)
|
||||
**匹配度**: 90%
|
||||
|
||||
- ✅ 管理员登录: `/admin/login`
|
||||
- ✅ 获取管理员信息: `/admin/profile`
|
||||
- ✅ 刷新令牌: `/auth/refresh`
|
||||
- ✅ 退出登录: `/auth/logout`
|
||||
|
||||
### 2. 用户管理模块 (User)
|
||||
**匹配度**: 85%
|
||||
|
||||
- ✅ 获取用户列表: `/users`
|
||||
- ✅ 创建用户: `/users`
|
||||
- ✅ 批量更新用户状态: `/users/batch-status`
|
||||
- ❌ 缺少用户详情、更新、删除接口
|
||||
|
||||
### 3. 商户管理模块 (Merchant)
|
||||
**匹配度**: 90%
|
||||
|
||||
- ✅ 获取商户列表: `/merchants`
|
||||
- ✅ 创建商户: `/merchants`
|
||||
- ✅ 获取商户详情: `/merchants/:id`
|
||||
- ✅ 更新商户: `/merchants/:id`
|
||||
- ✅ 删除商户: `/merchants/:id`
|
||||
|
||||
### 4. 动物管理模块 (Animal)
|
||||
**匹配度**: 95%
|
||||
|
||||
- ✅ 获取动物列表: `/animals`
|
||||
- ✅ 创建动物: `/animals`
|
||||
- ✅ 获取动物详情: `/animals/:id`
|
||||
- ✅ 更新动物: `/animals/:id`
|
||||
- ✅ 删除动物: `/animals/:id`
|
||||
- ✅ 动物认领管理: `/animal-claims`
|
||||
|
||||
### 5. 旅游管理模块 (Travel)
|
||||
**匹配度**: 75%
|
||||
|
||||
- ✅ 获取旅游列表: `/travel/travels`
|
||||
- ⚠️ 旅游计划路径不匹配: 前端 `/travel-plans` vs 后端 `/travel/plans`
|
||||
- ✅ 创建旅游: `/travels`
|
||||
|
||||
### 6. 订单管理模块 (Order)
|
||||
**匹配度**: 90%
|
||||
|
||||
- ✅ 管理员获取订单: `/orders/admin`
|
||||
- ✅ 订单统计: `/orders/statistics`
|
||||
- ✅ 更新订单状态: `/orders/:id/status`
|
||||
- ✅ 取消订单: `/orders/:id/cancel`
|
||||
|
||||
### 7. 促销活动模块 (Promotion)
|
||||
**匹配度**: 80%
|
||||
|
||||
- ✅ 促销活动: `/promotion/activities`
|
||||
- ✅ 奖励记录: `/promotion/rewards`
|
||||
- ⚠️ 促销列表路径不匹配: 前端 `/promotions` vs 后端 `/promotion/activities`
|
||||
|
||||
### 8. 系统管理模块 (System)
|
||||
**匹配度**: 40%
|
||||
|
||||
- ✅ 系统信息: `/admin/system/info`
|
||||
- ✅ 系统统计: `/admin/system/stats`
|
||||
- ❌ 缺少多个系统管理接口:
|
||||
- 数据库状态: `/admin/system/database-status`
|
||||
- 缓存状态: `/admin/system/cache-status`
|
||||
- 系统配置: `/admin/system-configs`
|
||||
- 系统日志: `/admin/system/logs`
|
||||
- 系统设置: `/admin/system/settings`
|
||||
- 系统监控: `/admin/system/monitor`
|
||||
|
||||
### 9. 仪表板模块 (Dashboard)
|
||||
**匹配度**: 70%
|
||||
|
||||
- ✅ 仪表板数据: `/admin/dashboard`
|
||||
- ⚠️ 活动日志接口不匹配: 前端 `/admin/dashboard/activities` vs 后端 `/admin/dashboard/user-growth`
|
||||
|
||||
### 10. 权限管理模块 (Permission)
|
||||
**匹配度**: 0%
|
||||
|
||||
- ❌ 完全缺失权限管理相关后端接口:
|
||||
- 获取权限列表: `/admin/permissions`
|
||||
- 创建权限: `/admin/permissions`
|
||||
- 批量删除权限: `/admin/permissions/batch-delete`
|
||||
|
||||
### 11. 花卉管理模块 (Flower)
|
||||
**匹配度**: 0%
|
||||
|
||||
- ❌ 完全缺失花卉管理相关后端接口:
|
||||
- 花卉列表: `/flowers`
|
||||
- 花卉销售: `/flower-sales`
|
||||
|
||||
## 修复建议
|
||||
|
||||
### 高优先级修复
|
||||
|
||||
1. **创建缺失的系统管理接口**
|
||||
```javascript
|
||||
// 需要在 /backend/src/routes/admin.js 中添加
|
||||
router.get('/system/database-status', authenticateAdmin, systemController.getDatabaseStatus);
|
||||
router.get('/system/cache-status', authenticateAdmin, systemController.getCacheStatus);
|
||||
router.get('/system/logs', authenticateAdmin, systemController.getSystemLogs);
|
||||
router.get('/system/settings', authenticateAdmin, systemController.getSystemSettings);
|
||||
router.get('/system/monitor', authenticateAdmin, systemController.getSystemMonitor);
|
||||
```
|
||||
|
||||
2. **创建权限管理接口**
|
||||
```javascript
|
||||
// 创建 /backend/src/routes/permission.js
|
||||
router.get('/admin/permissions', authenticateAdmin, permissionController.getPermissions);
|
||||
router.post('/admin/permissions', authenticateAdmin, permissionController.createPermission);
|
||||
router.post('/admin/permissions/batch-delete', authenticateAdmin, permissionController.batchDelete);
|
||||
```
|
||||
|
||||
3. **统一路径命名**
|
||||
- 将 `/travel-plans` 统一为 `/travel/plans`
|
||||
- 将 `/promotions` 统一为 `/promotion/activities`
|
||||
|
||||
### 中优先级修复
|
||||
|
||||
1. **创建花卉管理接口**
|
||||
```javascript
|
||||
// 创建 /backend/src/routes/flower.js
|
||||
router.get('/flowers', authenticateAdmin, flowerController.getFlowers);
|
||||
router.post('/flowers', authenticateAdmin, flowerController.createFlower);
|
||||
router.get('/flower-sales', authenticateAdmin, flowerController.getFlowerSales);
|
||||
```
|
||||
|
||||
2. **完善用户管理接口**
|
||||
```javascript
|
||||
// 在 /backend/src/routes/user.js 中添加
|
||||
router.get('/users/:id', authenticateAdmin, userController.getUserById);
|
||||
router.put('/users/:id', authenticateAdmin, userController.updateUser);
|
||||
router.delete('/users/:id', authenticateAdmin, userController.deleteUser);
|
||||
```
|
||||
|
||||
### 低优先级修复
|
||||
|
||||
1. **优化仪表板接口**
|
||||
- 确保 `/admin/dashboard/activities` 返回正确的活动日志数据
|
||||
|
||||
2. **添加接口文档**
|
||||
- 为所有新增接口添加 Swagger 文档注释
|
||||
|
||||
## 实施计划
|
||||
|
||||
### 第一阶段 (1-2天)
|
||||
- 修复高优先级的系统管理接口
|
||||
- 创建权限管理基础接口
|
||||
- 统一路径命名规范
|
||||
|
||||
### 第二阶段 (2-3天)
|
||||
- 创建花卉管理接口
|
||||
- 完善用户管理接口
|
||||
- 添加相应的控制器和服务层代码
|
||||
|
||||
### 第三阶段 (1天)
|
||||
- 优化仪表板接口
|
||||
- 完善接口文档
|
||||
- 进行集成测试
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. **单元测试**: 为新增接口编写单元测试
|
||||
2. **集成测试**: 测试前后端接口调用
|
||||
3. **回归测试**: 确保现有功能不受影响
|
||||
4. **性能测试**: 验证新接口的响应时间
|
||||
|
||||
## 总结
|
||||
|
||||
当前前后端接口匹配度约为 **75%**,主要问题集中在系统管理、权限管理和花卉管理模块。通过按优先级实施修复计划,可以将匹配度提升至 **95%** 以上,确保管理后台功能的完整性和稳定性。
|
||||
3299
docs/管理后台接口设计文档.md
3299
docs/管理后台接口设计文档.md
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user