refactor(backend): 重构动物相关 API 接口

- 更新了动物数据结构和相关类型定义
- 优化了动物列表、详情、创建、更新和删除接口
- 新增了更新动物状态接口
- 移除了与认领记录相关的接口
-调整了 API 响应结构
This commit is contained in:
ylweng
2025-08-31 00:45:46 +08:00
parent 0cad74b06f
commit 8e5295b572
111 changed files with 15290 additions and 1972 deletions

61
add_test_admin.js Normal file
View File

@@ -0,0 +1,61 @@
const bcrypt = require('bcryptjs');
const { query } = require('./backend/src/config/database');
// 数据库配置
const dbConfig = {
host: '129.211.213.226',
port: 9527,
user: 'root',
password: 'aiotAiot123!',
database: 'jiebandata'
};
async function addTestAdmin() {
try {
// 加密密码
const saltRounds = 10;
const plainPassword = 'admin123';
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
console.log('加密后的密码:', hashedPassword);
// 插入测试管理员账户
const insertSQL = `
INSERT INTO admins (username, password, email, nickname, role)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
password = VALUES(password),
email = VALUES(email),
nickname = VALUES(nickname),
role = VALUES(role)
`;
const params = [
'testadmin',
hashedPassword,
'testadmin@example.com',
'测试管理员',
'admin'
];
// 注意这里我们需要直接使用mysql2连接数据库因为backend/src/config/database可能依赖环境变量
const mysql = require('mysql2/promise');
const connection = await mysql.createConnection(dbConfig);
const [result] = await connection.execute(insertSQL, params);
console.log('✅ 测试管理员账户创建/更新成功');
console.log('受影响的行数:', result.affectedRows);
// 验证插入的数据
const [rows] = await connection.execute('SELECT * FROM admins WHERE username = ?', ['testadmin']);
console.log('\n插入的管理员信息:');
console.log(rows[0]);
await connection.end();
} catch (error) {
console.error('❌ 创建测试管理员账户失败:', error.message);
console.error(error.stack);
}
}
addTestAdmin();

View File

@@ -2,7 +2,7 @@
NODE_ENV=development
# API配置
VITE_API_BASE_URL=http://localhost:3000/api/v1
VITE_API_BASE_URL=http://localhost:3100/api/v1
VITE_API_TIMEOUT=30000
# 功能开关

View File

@@ -15,7 +15,7 @@
<meta name="theme-color" content="#1890ff">
<!-- 移动端优化 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="<%- VITE_APP_NAME %>">

View File

@@ -13,7 +13,7 @@ const appStore = useAppStore()
onMounted(() => {
// 初始化应用
appStore.initializeApp()
appStore.initialize()
// 开发环境调试信息
if (import.meta.env.DEV) {

View File

@@ -1,110 +1,89 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
import { request } from '.'
// 动物类型
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
species: string
breed: string
age: number
price: number
status: AnimalStatus
image_url: string
gender: string
description: string
image: string
merchant_id: number
merchant_name: string
price: number
status: 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
limit?: number
keyword?: string
type?: AnimalType
status?: AnimalStatus
species?: string
status?: string
merchant_id?: number
start_date?: string
end_date?: string
}
// 认领记录查询参数
export interface ClaimQueryParams {
page?: number
pageSize?: number
keyword?: string
status?: ClaimStatus
export interface AnimalCreateData {
name: string
species: string
breed: string
age: number
gender: string
description: string
image: string
merchant_id: number
price: number
status?: string
}
// API响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
pagination?: {
current: number
pageSize: number
total: number
totalPages: number
}
export interface AnimalUpdateData {
name?: string
species?: string
breed?: string
age?: number
gender?: string
description?: string
image?: string
merchant_id?: number
price?: number
status?: string
}
// 获取动物列表
export const getAnimals = async (params?: AnimalQueryParams): Promise<ApiResponse<Animal[]>> => {
return request.get<ApiResponse<Animal[]>>('/animals', { params })
}
export const getAnimals = (params?: AnimalQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { animals: Animal[]; pagination: any } }>('/animals', { params })
// 获取动物详情
export const getAnimal = async (id: number): Promise<ApiResponse<Animal>> => {
return request.get<ApiResponse<Animal>>(`/animals/${id}`)
}
export const getAnimal = (id: number) =>
request.get<{ success: boolean; code: number; message: string; data: { animal: Animal } }>(`/animals/${id}`)
// 创建动物
export const createAnimal = async (animalData: Partial<Animal>): Promise<ApiResponse<Animal>> => {
return request.post<ApiResponse<Animal>>('/animals', animalData)
}
export const createAnimal = (data: AnimalCreateData) =>
request.post<{ success: boolean; code: number; message: string; data: { animal: Animal } }>('/animals', data)
// 更新动物
export const updateAnimal = async (id: number, animalData: Partial<Animal>): Promise<ApiResponse<Animal>> => {
return request.put<ApiResponse<Animal>>(`/animals/${id}`, animalData)
}
export const updateAnimal = (id: number, data: AnimalUpdateData) =>
request.put<{ success: boolean; code: number; message: string; data: { animal: Animal } }>(`/animals/${id}`, data)
// 删除动物
export const deleteAnimal = async (id: number): Promise<ApiResponse<void>> => {
return request.delete<ApiResponse<void>>(`/animals/${id}`)
}
export const deleteAnimal = (id: number) =>
request.delete<{ success: boolean; code: number; message: string }>(`/animals/${id}`)
// 获取认领记录列表
export const getAnimalClaims = async (params?: ClaimQueryParams): Promise<ApiResponse<AnimalClaim[]>> => {
return request.get<ApiResponse<AnimalClaim[]>>('/animals/claims', { params })
}
// 更新动物状态
export const updateAnimalStatus = (id: number, status: string) =>
request.put<{ success: boolean; code: number; message: string }>(`/animals/${id}/status`, { status })
// 审核动物认领(通过)
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 })
export default {
getAnimals,
getAnimal,
createAnimal,
updateAnimal,
deleteAnimal,
updateAnimalStatus
}

View File

@@ -3,7 +3,7 @@ import { message } from 'ant-design-vue'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
// API基础配置
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3100/api'
const timeout = parseInt(import.meta.env.VITE_API_TIMEOUT || '10000')
// 创建axios实例
@@ -17,7 +17,7 @@ const api: AxiosInstance = axios.create({
// 请求拦截器
api.interceptors.request.use(
(config: AxiosRequestConfig) => {
(config) => {
// 添加认证token
const token = localStorage.getItem('admin_token')
if (token && config.headers) {
@@ -124,18 +124,18 @@ export const authAPI = {
success: boolean
data: {
token: string
user: any
admin: any
}
}>('/auth/admin/login', credentials),
}>('/admin/login', credentials),
// 获取当前用户信息
getCurrentUser: () =>
request.get<{
success: boolean
data: {
user: any
admin: any
}
}>('/auth/me'),
}>('/admin/profile'),
// 刷新token
refreshToken: () =>
@@ -159,4 +159,13 @@ export * from './order'
export * from './promotion'
export * from './system'
// 为避免命名冲突,单独导出模块
export { default as userAPI } from './user'
export { default as merchantAPI } from './merchant'
export { default as travelAPI } from './travel'
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 api

View File

@@ -1,73 +1,88 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
import { request } from '.'
// 商家类型
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
business_license: string
legal_representative: string
contact_person: string
contact_phone: string
status: MerchantStatus
contact_email: string
address: string
business_scope: string
status: string
remark: string
created_at: string
updated_at: string
}
// 商家查询参数
export interface MerchantQueryParams {
page?: number
pageSize?: number
keyword?: string
type?: MerchantType
status?: MerchantStatus
limit?: number
business_name?: string
contact_person?: string
contact_phone?: string
status?: string
start_date?: string
end_date?: string
}
// API响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
pagination?: {
current: number
pageSize: number
total: number
totalPages: number
}
export interface MerchantCreateData {
business_name: string
business_license: string
legal_representative: string
contact_person: string
contact_phone: string
contact_email: string
address: string
business_scope: string
status?: string
remark?: string
}
export interface MerchantUpdateData {
business_name?: string
business_license?: string
legal_representative?: string
contact_person?: string
contact_phone?: string
contact_email?: string
address?: string
business_scope?: string
status?: string
remark?: string
}
// 获取商家列表
export const getMerchants = async (params?: MerchantQueryParams): Promise<ApiResponse<Merchant[]>> => {
return request.get<ApiResponse<Merchant[]>>('/merchants', { params })
}
export const getMerchants = (params?: MerchantQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { merchants: Merchant[]; pagination: any } }>('/merchants', { params })
// 获取商家详情
export const getMerchant = async (id: number): Promise<ApiResponse<Merchant>> => {
return request.get<ApiResponse<Merchant>>(`/merchants/${id}`)
}
export const getMerchant = (id: number) =>
request.get<{ success: boolean; code: number; message: string; data: { merchant: Merchant } }>(`/merchants/${id}`)
// 审核商家(通过)
export const approveMerchant = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/merchants/${id}/approve`)
}
// 创建商家
export const createMerchant = (data: MerchantCreateData) =>
request.post<{ success: boolean; code: number; message: string; data: { merchant: Merchant } }>('/merchants', data)
// 拒绝商家入驻申请
export const rejectMerchant = async (id: number, reason: string): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/merchants/${id}/reject`, { reason })
}
// 更新商家
export const updateMerchant = (id: number, data: MerchantUpdateData) =>
request.put<{ success: boolean; code: number; message: string; data: { merchant: Merchant } }>(`/merchants/${id}`, data)
// 禁用商家
export const disableMerchant = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/merchants/${id}/disable`)
}
// 删除商家
export const deleteMerchant = (id: number) =>
request.delete<{ success: boolean; code: number; message: string }>(`/merchants/${id}`)
// 启用商家
export const enableMerchant = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/merchants/${id}/enable`)
// 更新商家状态
export const updateMerchantStatus = (id: number, status: string) =>
request.put<{ success: boolean; code: number; message: string }>(`/merchants/${id}/status`, { status })
export default {
getMerchants,
getMerchant,
createMerchant,
updateMerchant,
deleteMerchant,
updateMerchantStatus
}

View File

@@ -1,4 +1,4 @@
import { request } from '@/api'
import { request } from '.'
import type { AxiosResponse } from 'axios'
// 订单状态
@@ -13,31 +13,42 @@ export interface Order {
order_no: string
user_id: number
user_name: string
user_phone: string
amount: number
status: OrderStatus
payment_method: PaymentMethod
merchant_id: number
merchant_name: string
product_id: number
product_name: string
product_type: string
quantity: number
unit_price: number
total_amount: number
status: string
payment_method: string
payment_time: string
refund_status: string
remark: string
created_at: string
paid_at: string
shipped_at: string
completed_at: string
updated_at: string
}
// 订单查询参数
export interface OrderQueryParams {
page?: number
pageSize?: number
limit?: number
order_no?: string
status?: OrderStatus
orderTime?: [string, string]
user_name?: string
merchant_name?: string
product_type?: string
status?: string
payment_method?: string
start_date?: string
end_date?: string
}
// 统计数据
export interface OrderStatistics {
today_orders: number
today_sales: number
month_orders: number
month_sales: number
// 订单更新参数
export interface OrderUpdateData {
status?: string
refund_status?: string
remark?: string
}
// API响应结构
@@ -55,41 +66,29 @@ export interface ApiResponse<T> {
}
// 获取订单列表
export const getOrders = async (params?: OrderQueryParams): Promise<ApiResponse<Order[]>> => {
return request.get<ApiResponse<Order[]>>('/orders', { params })
}
export const getOrders = (params?: OrderQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { orders: Order[]; pagination: any } }>('/orders', { params })
// 获取订单详情
export const getOrder = async (id: number): Promise<ApiResponse<Order>> => {
return request.get<ApiResponse<Order>>(`/orders/${id}`)
}
export const getOrder = (id: number) =>
request.get<{ success: boolean; code: number; message: string; data: { order: Order } }>(`/orders/${id}`)
// 更新订单
export const updateOrder = (id: number, data: OrderUpdateData) =>
request.put<{ success: boolean; code: number; message: string; data: { order: Order } }>(`/orders/${id}`, data)
// 删除订单
export const deleteOrder = (id: number) =>
request.delete<{ success: boolean; code: number; message: string }>(`/orders/${id}`)
// 更新订单状态
export const updateOrderStatus = async (id: number, status: OrderStatus): Promise<ApiResponse<void>> => {
return request.put<ApiResponse<void>>(`/orders/${id}/status`, { status })
}
export const updateOrderStatus = (id: number, status: string) =>
request.put<{ success: boolean; code: number; message: string }>(`/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')
export default {
getOrders,
getOrder,
updateOrder,
deleteOrder,
updateOrderStatus
}

View File

@@ -1,5 +1,81 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
import { request } from '.'
// 定义促销相关类型
export interface Promotion {
id: number
title: string
description: string
type: string
discount_value: number
start_date: string
end_date: string
status: string
created_at: string
updated_at: string
}
export interface PromotionQueryParams {
page?: number
limit?: number
title?: string
type?: string
status?: string
start_date?: string
end_date?: string
}
export interface PromotionCreateData {
title: string
description: string
type: string
discount_value: number
start_date: string
end_date: string
status?: string
}
export interface PromotionUpdateData {
title?: string
description?: string
type?: string
discount_value?: number
start_date?: string
end_date?: string
status?: string
}
// 获取促销列表
export const getPromotions = (params?: PromotionQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { promotions: Promotion[]; pagination: any } }>('/promotions', { params })
// 获取促销详情
export const getPromotion = (id: number) =>
request.get<{ success: boolean; code: number; message: string; data: { promotion: Promotion } }>(`/promotions/${id}`)
// 创建促销
export const createPromotion = (data: PromotionCreateData) =>
request.post<{ success: boolean; code: number; message: string; data: { promotion: Promotion } }>('/promotions', data)
// 更新促销
export const updatePromotion = (id: number, data: PromotionUpdateData) =>
request.put<{ success: boolean; code: number; message: string; data: { promotion: Promotion } }>(`/promotions/${id}`, data)
// 删除促销
export const deletePromotion = (id: number) =>
request.delete<{ success: boolean; code: number; message: string }>(`/promotions/${id}`)
// 更新促销状态
export const updatePromotionStatus = (id: number, status: string) =>
request.put<{ success: boolean; code: number; message: string }>(`/promotions/${id}/status`, { status })
export default {
getPromotions,
getPromotion,
createPromotion,
updatePromotion,
deletePromotion,
updatePromotionStatus
}
// 推广活动状态
export type PromotionStatus = 'active' | 'upcoming' | 'ended' | 'paused'

View File

@@ -1,5 +1,4 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
import { request } from '.'
// 服务类型
export type ServiceType = 'database' | 'cache' | 'mq'
@@ -9,112 +8,79 @@ export type ServiceStatus = 'running' | 'stopped'
// 系统服务数据结构
export interface Service {
id: number
id: string
name: string
type: ServiceType
status: ServiceStatus
host: string
port: number
description: string
created_at: string
updated_at: string
}
export interface ServiceQueryParams {
page?: number
limit?: number
type?: ServiceType
status?: ServiceStatus
}
export interface ServiceUpdateData {
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 getServices = (params?: ServiceQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { services: Service[]; pagination: any } }>('/system/services', { params })
// 更新系统服务状态
export const updateServiceStatus = (id: string, data: ServiceUpdateData) =>
request.put<{ success: boolean; code: number; message: string }>(`/system/services/${id}/status`, data)
// 定义系统配置相关类型
export interface SystemConfig {
id: string
name: string
value: string
type: string
group: string
description: string
created_at: string
updated_at: string
}
// 启动服务
export const startService = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/system/services/${id}/start`)
export interface SystemConfigQueryParams {
page?: number
limit?: number
group?: string
}
// 停止服务
export const stopService = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/system/services/${id}/stop`)
export interface SystemConfigUpdateData {
value: string
}
// 获取系统信息
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 getSystemConfigs = (params?: SystemConfigQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { configs: SystemConfig[]; pagination: any } }>('/system-configs', { params })
// 更新系统配置
export const updateSystemSettings = async (settings: SystemSettings): Promise<ApiResponse<void>> => {
return request.put<ApiResponse<void>>('/system/settings', settings)
}
export const updateSystemConfig = (id: string, data: SystemConfigUpdateData) =>
request.put<{ success: boolean; code: number; message: string }>(`/system-configs/${id}`, data)
// 获取操作日志
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 getSystemStats = () =>
request.get<{ success: boolean; code: number; message: string; data: any }>('/system/stats')
// 清理缓存
export const clearCache = async (): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>('/system/cache/clear')
}
// 获取系统日志
export const getSystemLogs = (params?: { page?: number; limit?: number; level?: string }) =>
request.get<{ success: boolean; code: number; message: string; data: { logs: any[]; pagination: any } }>('/system/logs', { params })
// 系统健康检查
export const healthCheck = async (): Promise<ApiResponse<any>> => {
return request.get<ApiResponse<any>>('/system/health')
}
export default {
getServices,
updateServiceStatus,
getSystemConfigs,
updateSystemConfig,
getSystemStats,
getSystemLogs
}

View File

@@ -1,68 +1,82 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
import { request } from '.'
// 旅行计划状态
export type TravelStatus = 'recruiting' | 'full' | 'completed' | 'cancelled'
// 旅行计划数据结构
export interface TravelPlan {
// 定义结伴游相关类型
export interface Travel {
id: number
destination: string
title: string
description: string
start_date: string
end_date: string
budget: number
max_members: number
current_members: number
status: TravelStatus
creator: string
destination: string
max_participants: number
current_participants: number
price: number
status: string
created_at: string
updated_at: string
}
// 旅行计划查询参数
export interface TravelQueryParams {
page?: number
pageSize?: number
limit?: number
title?: string
destination?: string
status?: TravelStatus
travelTime?: [string, string]
status?: string
start_date?: string
end_date?: string
}
// API响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
pagination?: {
current: number
pageSize: number
total: number
totalPages: number
}
export interface TravelCreateData {
title: string
description: string
start_date: string
end_date: string
destination: string
max_participants: number
price: number
status?: string
}
// 获取旅行计划列表
export const getTravelPlans = async (params?: TravelQueryParams): Promise<ApiResponse<TravelPlan[]>> => {
return request.get<ApiResponse<TravelPlan[]>>('/travel/plans', { params })
export interface TravelUpdateData {
title?: string
description?: string
start_date?: string
end_date?: string
destination?: string
max_participants?: number
price?: number
status?: string
}
// 获取旅行计划详情
export const getTravelPlan = async (id: number): Promise<ApiResponse<TravelPlan>> => {
return request.get<ApiResponse<TravelPlan>>(`/travel/plans/${id}`)
}
// 获取结伴游列表
export const getTravels = (params?: TravelQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { travels: Travel[]; pagination: any } }>('/travels', { params })
// 审核旅行计划
export const approveTravelPlan = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/travel/plans/${id}/approve`)
}
// 获取结伴游详情
export const getTravel = (id: number) =>
request.get<{ success: boolean; code: number; message: string; data: { travel: Travel } }>(`/travels/${id}`)
// 拒绝旅行计划
export const rejectTravelPlan = async (id: number, reason: string): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/travel/plans/${id}/reject`, { reason })
}
// 创建结伴游
export const createTravel = (data: TravelCreateData) =>
request.post<{ success: boolean; code: number; message: string; data: { travel: Travel } }>('/travels', data)
// 关闭旅行计划
export const closeTravelPlan = async (id: number): Promise<ApiResponse<void>> => {
return request.post<ApiResponse<void>>(`/travel/plans/${id}/close`)
// 更新结伴游
export const updateTravel = (id: number, data: TravelUpdateData) =>
request.put<{ success: boolean; code: number; message: string; data: { travel: Travel } }>(`/travels/${id}`, data)
// 删除结伴游
export const deleteTravel = (id: number) =>
request.delete<{ success: boolean; code: number; message: string }>(`/travels/${id}`)
// 更新结伴游状态
export const updateTravelStatus = (id: number, status: string) =>
request.put<{ success: boolean; code: number; message: string }>(`/travels/${id}/status`, { status })
export default {
getTravels,
getTravel,
createTravel,
updateTravel,
deleteTravel,
updateTravelStatus
}

View File

@@ -1,81 +1,109 @@
import { request } from '@/api'
import type { AxiosResponse } from 'axios'
import { request } from '.'
// 用户状态类型
export type UserStatus = 'active' | 'inactive' | 'banned'
// 用户等级类型
export type UserLevel = number
// 用户数据结构
// 定义用户相关类型
export interface User {
id: number
openid: string
username: string
nickname: string
email: string
phone: string
avatar: string
gender: string
birthday: string
phone: string
email: string
status: UserStatus
level: UserLevel
points: number
level: number
balance: number
travel_count: number
animal_adopt_count: number
flower_order_count: number
status: string
remark: string
created_at: string
updated_at: string
last_login_at: string
}
// 用户查询参数
export interface UserQueryParams {
page?: number
pageSize?: number
keyword?: string
status?: UserStatus
registerTime?: [string, string]
limit?: number
username?: string
nickname?: string
phone?: string
status?: string
start_date?: string
end_date?: string
}
// API响应结构
export interface ApiResponse<T> {
export interface UserCreateData {
username: string
password: string
nickname?: string
email?: string
phone?: string
gender?: string
birthday?: string
status?: string
remark?: string
}
export interface UserUpdateData {
username?: string
password?: string
nickname?: string
email?: string
phone?: string
gender?: string
birthday?: string
status?: string
remark?: string
}
export interface ApiResponse<T = any> {
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 getUsers = (params?: UserQueryParams) =>
request.get<ApiResponse<{ users: User[]; pagination: any }>>('/users', { params })
// 获取用户详情
export const getUser = async (id: number): Promise<ApiResponse<User>> => {
return request.get<ApiResponse<User>>(`/users/${id}`)
}
export const getUser = (id: number) =>
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 createUser = (data: UserCreateData) =>
request.post<ApiResponse<User>>('/users', data)
// 更新用户
export const updateUser = async (id: number, userData: Partial<User>): Promise<ApiResponse<User>> => {
return request.put<ApiResponse<User>>(`/users/${id}`, userData)
}
export const updateUser = (id: number, data: UserUpdateData) =>
request.put<ApiResponse<User>>(`/users/${id}`, data)
// 删除用户
export const deleteUser = async (id: number): Promise<ApiResponse<void>> => {
return request.delete<ApiResponse<void>>(`/users/${id}`)
}
export const deleteUser = (id: number) =>
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 })
// 批量删除用户
export const batchDeleteUsers = (ids: number[]) =>
request.post<ApiResponse<void>>('/users/batch-delete', { ids })
// 更新用户状态
export const updateUserStatus = (id: number, status: string) =>
request.put<ApiResponse<User>>(`/users/${id}/status`, { status })
// 重置用户密码
export const resetUserPassword = (id: number, password: string) =>
request.put<ApiResponse<User>>(`/users/${id}/password`, { password })
export default {
getUsers,
getUser,
createUser,
updateUser,
deleteUser,
batchDeleteUsers,
updateUserStatus,
resetUserPassword
}

View File

@@ -69,6 +69,7 @@ import { useRouter } from 'vue-router'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { useAppStore } from '@/stores/app'
import { authAPI } from '@/api'
interface FormState {
username: string
@@ -91,25 +92,22 @@ const onFinish = async (values: FormState) => {
loading.value = true
try {
// 模拟登录过程
console.log('登录信息:', values)
// TODO: 调用真实登录接口
// const response = await authAPI.login(values)
// 模拟登录成功
await new Promise(resolve => setTimeout(resolve, 1000))
// 调用真实登录接口
const response = await authAPI.login(values)
// 保存token
localStorage.setItem('admin_token', 'mock_token_123456')
if (response?.data?.token) {
localStorage.setItem('admin_token', response.data.token)
} else {
throw new Error('登录响应中缺少token')
}
// 更新用户状态
appStore.setUser({
id: 1,
username: values.username,
nickname: '管理员',
role: 'admin'
})
if (response?.data?.admin) {
appStore.setUser(response.data.admin)
} else {
throw new Error('登录响应中缺少用户信息')
}
message.success('登录成功!')
@@ -117,9 +115,10 @@ const onFinish = async (values: FormState) => {
const redirect = router.currentRoute.value.query.redirect as string
router.push(redirect || '/dashboard')
} catch (error) {
} catch (error: any) {
console.error('登录失败:', error)
message.error('登录失败,请检查用户名和密码')
const errorMessage = error.message || '登录失败,请检查用户名和密码'
message.error(errorMessage)
} finally {
loading.value = false
}
@@ -174,10 +173,11 @@ const onFinishFailed = (errorInfo: any) => {
border-top: 1px solid #f0f0f0;
}
.login-footer p {
.login-footer {
text-align: center;
margin-top: 30px;
color: #999;
font-size: 12px;
margin: 0;
}
:deep(.ant-input-affix-wrapper) {

View File

@@ -137,7 +137,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
@@ -150,7 +150,7 @@ import {
StopOutlined,
PlayCircleOutlined
} from '@ant-design/icons-vue'
import { getMerchants, approveMerchant, rejectMerchant, disableMerchant, enableMerchant } from '@/api/merchant'
import { getMerchants, getMerchant, approveMerchant, rejectMerchant, disableMerchant, enableMerchant } from '@/api/merchant'
import type { Merchant } from '@/api/merchant'
interface SearchForm {
@@ -314,8 +314,33 @@ const handleTableChange: TableProps['onChange'] = (pag) => {
loadMerchants()
}
const handleView = (record: Merchant) => {
message.info(`查看商家: ${record.business_name}`)
const handleView = async (record: Merchant) => {
try {
const response = await getMerchant(record.id)
Modal.info({
title: '商家详情',
width: 600,
content: h('div', { class: 'merchant-detail-modal' }, [
h('a-descriptions', {
column: 1,
bordered: true
}, [
h('a-descriptions-item', { label: '商家ID' }, response.data.id),
h('a-descriptions-item', { label: '商家名称' }, response.data.business_name),
h('a-descriptions-item', { label: '商家类型' }, getTypeText(response.data.merchant_type)),
h('a-descriptions-item', { label: '联系人' }, response.data.contact_person),
h('a-descriptions-item', { label: '联系电话' }, response.data.contact_phone),
h('a-descriptions-item', { label: '状态' }, [
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
]),
h('a-descriptions-item', { label: '入驻时间' }, response.data.created_at),
h('a-descriptions-item', { label: '更新时间' }, response.data.updated_at)
])
])
})
} catch (error) {
message.error('获取商家详情失败')
}
}
const handleApprove = async (record: Merchant) => {

View File

@@ -1,162 +1,241 @@
<template>
<div class="order-management">
<a-page-header title="订单管理" sub-title="管理花束订单和交易记录">
<a-page-header
title="订单管理"
sub-title="管理系统订单信息"
>
<template #extra>
<a-space>
<a-button @click="handleRefresh">
<template #icon><ReloadOutlined /></template>
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
<a-button type="primary" @click="showStats">
<template #icon><BarChartOutlined /></template>
销售统计
</a-button>
</a-space>
</template>
</a-page-header>
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<a-tab-pane key="orders" tab="订单列表">
<!-- 统计卡片 -->
<a-row :gutter="16" class="stats-row">
<a-col :span="6">
<a-card>
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="订单号">
<a-input v-model:value="searchForm.order_no" placeholder="输入订单号" allow-clear />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchForm.status" placeholder="全部状态" style="width: 120px" allow-clear>
<a-select-option value="pending">待支付</a-select-option>
<a-select-option value="paid">已支付</a-select-option>
<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>
</a-form-item>
<a-form-item label="下单时间">
<a-range-picker v-model:value="searchForm.orderTime" :placeholder="['开始时间', '结束时间']" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">重置</a-button>
</a-form-item>
</a-form>
</div>
<a-table
:columns="orderColumns"
:data-source="orderList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
<a-statistic
title="今日订单"
:value="statistics.today_orders"
:precision="0"
suffix="单"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'order_no'">
<a-typography-text copyable>{{ record.order_no }}</a-typography-text>
</template>
<template v-else-if="column.key === 'amount'">¥{{ record.amount }}</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
</template>
<template v-else-if="column.key === 'payment_method'">
<span>{{ getPaymentMethodText(record.payment_method) }}</span>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleViewOrder(record)">
<EyeOutlined />详情
</a-button>
<template v-if="record.status === 'paid'">
<a-button size="small" type="primary" @click="handleShip(record)">
<CarOutlined />发货
</a-button>
</template>
<template v-if="record.status === 'shipped'">
<a-button size="small" type="primary" @click="handleComplete(record)">
<CheckCircleOutlined />完成
</a-button>
</template>
<template v-if="['pending', 'paid'].includes(record.status)">
<a-button size="small" danger @click="handleCancel(record)">
<CloseOutlined />取消
</a-button>
</template>
<template v-if="record.status === 'paid'">
<a-button size="small" danger @click="handleRefund(record)">
<RollbackOutlined />退款
</a-button>
</template>
</a-space>
</template>
<template #prefix>
<ShoppingCartOutlined />
</template>
</a-table>
</a-statistic>
</a-card>
</a-tab-pane>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="今日销售额"
:value="statistics.today_sales"
:precision="2"
suffix="元"
:value-style="{ color: '#cf1322' }"
>
<template #prefix>
<MoneyCollectOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="本月订单"
:value="statistics.month_orders"
:precision="0"
suffix="单"
>
<template #prefix>
<CalendarOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="本月销售额"
:value="statistics.month_sales"
:precision="2"
suffix="元"
:value-style="{ color: '#cf1322' }"
>
<template #prefix>
<DollarOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
<a-tab-pane key="statistics" tab="销售统计">
<a-card title="销售数据概览">
<a-row :gutter="16">
<a-col :span="6">
<a-statistic title="今日订单" :value="statistics.today_orders" :precision="0" :value-style="{ color: '#3f8600' }">
<template #prefix><ShoppingOutlined /></template>
</a-statistic>
</a-col>
<a-col :span="6">
<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: '#1890ff' }" />
</a-col>
<a-col :span="6">
<a-statistic title="本月销售额" :value="statistics.month_sales" :precision="2" prefix="¥" :value-style="{ color: '#722ed1' }" />
</a-col>
</a-row>
</a-card>
<a-card>
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="订单号">
<a-input
v-model:value="searchForm.order_no"
placeholder="请输入订单号"
allow-clear
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="全部状态"
style="width: 120px"
allow-clear
>
<a-select-option value="pending">待支付</a-select-option>
<a-select-option value="paid">已支付</a-select-option>
<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>
</a-form-item>
<a-form-item label="下单时间">
<a-range-picker
v-model:value="searchForm.orderTime"
:placeholder="['开始时间', '结束时间']"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
重置
</a-button>
</a-form-item>
</a-form>
</div>
<a-card title="销售趋势" style="margin-top: 16px;">
<div style="height: 300px;">
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #999;">
<BarChartOutlined style="font-size: 48px; margin-right: 12px;" />
<span>销售趋势图表开发中</span>
</div>
</div>
</a-card>
</a-tab-pane>
</a-tabs>
<!-- 订单表格 -->
<a-table
:columns="columns"
:data-source="orderList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'amount'">
<span class="amount-text">¥{{ record.amount }}</span>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleView(record)">
<EyeOutlined />
查看
</a-button>
<template v-if="record.status === 'pending'">
<a-button size="small" type="primary" @click="handlePay(record)">
<PayCircleOutlined />
支付
</a-button>
</template>
<template v-else-if="record.status === 'paid'">
<a-button size="small" type="primary" @click="handleShip(record)">
<SendOutlined />
发货
</a-button>
</template>
<template v-else-if="record.status === 'shipped'">
<a-button size="small" type="primary" @click="handleComplete(record)">
<CheckCircleOutlined />
完成
</a-button>
</template>
<a-dropdown>
<a-button size="small">
更多
<DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item
v-if="['pending', 'paid'].includes(record.status)"
@click="handleCancel(record)"
>
<CloseCircleOutlined />
取消
</a-menu-item>
<a-menu-item
v-if="['paid', 'shipped', 'completed'].includes(record.status)"
@click="handleRefund(record)"
>
<RedoOutlined />
退款
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ReloadOutlined,
ShoppingCartOutlined,
MoneyCollectOutlined,
CalendarOutlined,
DollarOutlined,
SearchOutlined,
BarChartOutlined,
ReloadOutlined,
EyeOutlined,
CarOutlined,
PayCircleOutlined,
SendOutlined,
CheckCircleOutlined,
CloseOutlined,
RollbackOutlined,
ShoppingOutlined
CloseCircleOutlined,
RedoOutlined,
DownOutlined
} from '@ant-design/icons-vue'
import { getOrders, updateOrderStatus, getOrderStatistics } from '@/api/order'
import {
getOrders,
getOrder,
getOrderStatistics,
updateOrderStatus,
shipOrder,
completeOrder,
cancelOrder,
refundOrder
} from '@/api/order'
import type { Order, OrderStatistics } from '@/api/order'
interface SearchForm {
@@ -165,8 +244,13 @@ interface SearchForm {
orderTime: any[]
}
const activeTab = ref('orders')
const loading = ref(false)
const statistics = ref<OrderStatistics>({
today_orders: 0,
today_sales: 0,
month_orders: 0,
month_sales: 0
})
const searchForm = reactive<SearchForm>({
order_no: '',
@@ -174,13 +258,6 @@ const searchForm = reactive<SearchForm>({
orderTime: []
})
const statistics = reactive<OrderStatistics>({
today_orders: 0,
today_sales: 0,
month_orders: 0,
month_sales: 0
})
const orderList = ref<Order[]>([])
const pagination = reactive({
current: 1,
@@ -191,25 +268,66 @@ const pagination = reactive({
showTotal: (total: number) => `共 ${total} 条记录`
})
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: '金额', key: 'amount', width: 100, align: 'center' },
{ title: '状态', key: 'status', width: 100, align: 'center' },
{ title: '支付方式', key: 'payment_method', width: 100, align: 'center' },
{ title: '下单时间', dataIndex: 'created_at', key: 'created_at', width: 150 },
{ title: '操作', key: 'actions', width: 200, align: 'center' }
const columns = [
{
title: '订单号',
dataIndex: 'order_no',
key: 'order_no',
width: 150
},
{
title: '用户',
dataIndex: 'user_name',
key: 'user_name',
width: 100
},
{
title: '联系电话',
dataIndex: 'user_phone',
key: 'user_phone',
width: 120
},
{
title: '金额',
key: 'amount',
width: 100,
align: 'right'
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '支付方式',
dataIndex: 'payment_method',
key: 'payment_method',
width: 100
},
{
title: '下单时间',
dataIndex: 'created_at',
key: 'created_at',
width: 120
},
{
title: '操作',
key: 'actions',
width: 200,
align: 'center'
}
]
// 状态映射
const getStatusColor = (status: string) => {
const colors = {
pending: 'orange',
paid: 'blue',
shipped: 'green',
completed: 'purple',
shipped: 'purple',
completed: 'green',
cancelled: 'red',
refunded: 'default'
refunded: 'red'
}
return colors[status as keyof typeof colors] || 'default'
}
@@ -226,21 +344,24 @@ const getStatusText = (status: string) => {
return texts[status as keyof typeof texts] || '未知'
}
// 支付方式映射
const getPaymentMethodText = (method: string) => {
const texts = {
wechat: '微信支付',
alipay: '支付宝',
bank: '银行',
bank: '银行转账',
balance: '余额支付'
}
return texts[method as keyof typeof texts] || '未知'
}
// 生命周期
onMounted(() => {
loadOrders()
loadStatistics()
})
// 方法
const loadOrders = async () => {
loading.value = true
try {
@@ -263,20 +384,12 @@ const loadOrders = async () => {
const loadStatistics = async () => {
try {
const response = await getOrderStatistics()
Object.assign(statistics, response.data)
statistics.value = response.data
} catch (error) {
message.error('加载统计数据失败')
}
}
const handleTabChange = (key: string) => {
if (key === 'orders') {
loadOrders()
} else if (key === 'statistics') {
loadStatistics()
}
}
const handleSearch = () => {
pagination.current = 1
loadOrders()
@@ -293,11 +406,8 @@ const handleReset = () => {
}
const handleRefresh = () => {
if (activeTab.value === 'orders') {
loadOrders()
} else {
loadStatistics()
}
loadOrders()
loadStatistics()
message.success('数据已刷新')
}
@@ -307,17 +417,60 @@ const handleTableChange: TableProps['onChange'] = (pag) => {
loadOrders()
}
const handleViewOrder = (record: Order) => {
message.info(`查看订单: ${record.order_no}`)
const handleView = async (record: Order) => {
try {
const response = await getOrder(record.id)
Modal.info({
title: '订单详情',
width: 600,
content: h('div', { class: 'order-detail-modal' }, [
h('a-descriptions', {
column: 1,
bordered: true
}, [
h('a-descriptions-item', { label: '订单号' }, response.data.order_no),
h('a-descriptions-item', { label: '用户' }, response.data.user_name),
h('a-descriptions-item', { label: '联系电话' }, response.data.user_phone),
h('a-descriptions-item', { label: '订单金额' }, `¥${response.data.amount}`),
h('a-descriptions-item', { label: '状态' }, [
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
]),
h('a-descriptions-item', { label: '支付方式' }, getPaymentMethodText(response.data.payment_method)),
h('a-descriptions-item', { label: '下单时间' }, response.data.created_at),
h('a-descriptions-item', { label: '支付时间' }, response.data.paid_at || '-'),
h('a-descriptions-item', { label: '发货时间' }, response.data.shipped_at || '-'),
h('a-descriptions-item', { label: '完成时间' }, response.data.completed_at || '-')
])
])
})
} catch (error) {
message.error('获取订单详情失败')
}
}
const handlePay = async (record: Order) => {
Modal.confirm({
title: '确认支付',
content: `确定要标记订单 "${record.order_no}" 为已支付状态吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'paid')
message.success('订单状态已更新')
loadOrders()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleShip = async (record: Order) => {
Modal.confirm({
title: '确认发货',
content: `确定要发货订单 "${record.order_no}" 吗?`,
content: `确定要标记订单 "${record.order_no}" 为已发货状态吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'shipped')
await shipOrder(record.id)
message.success('订单已发货')
loadOrders()
} catch (error) {
@@ -330,10 +483,10 @@ const handleShip = async (record: Order) => {
const handleComplete = async (record: Order) => {
Modal.confirm({
title: '确认完成',
content: `确定要完成订单 "${record.order_no}" 吗?`,
content: `确定要标记订单 "${record.order_no}" 为已完成状态吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'completed')
await completeOrder(record.id)
message.success('订单已完成')
loadOrders()
} catch (error) {
@@ -349,7 +502,7 @@ const handleCancel = async (record: Order) => {
content: `确定要取消订单 "${record.order_no}" 吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'cancelled')
await cancelOrder(record.id)
message.success('订单已取消')
loadOrders()
} catch (error) {
@@ -362,11 +515,11 @@ const handleCancel = async (record: Order) => {
const handleRefund = async (record: Order) => {
Modal.confirm({
title: '确认退款',
content: `确定要退款订单 "${record.order_no}" 吗?退款金额: ¥${record.amount}`,
content: `确定要订单 "${record.order_no}" 办理退款吗?`,
onOk: async () => {
try {
await updateOrderStatus(record.id, 'refunded')
message.success('退款申请已提交')
await refundOrder(record.id)
message.success('订单已退款')
loadOrders()
} catch (error) {
message.error('操作失败')
@@ -374,15 +527,14 @@ const handleRefund = async (record: Order) => {
}
})
}
const showStats = () => {
activeTab.value = 'statistics'
loadStatistics()
}
</script>
<style scoped lang="less">
.order-management {
.stats-row {
margin-bottom: 24px;
}
.search-container {
margin-bottom: 16px;
padding: 16px;
@@ -393,5 +545,15 @@ const showStats = () => {
margin-bottom: 16px;
}
}
.amount-text {
font-weight: 500;
color: #cf1322;
}
}
:deep(.ant-table-thead > tr > th) {
background: #fafafa;
font-weight: 600;
}
</style>

View File

@@ -236,8 +236,8 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { message, Modal, type FormInstance } from 'ant-design-vue'
import { ref, reactive, computed, onMounted, h } from 'vue'
import { message, Modal, type FormInstance, TableProps } from 'ant-design-vue'
import {
UserAddOutlined,
SearchOutlined,
@@ -248,10 +248,10 @@ import {
StopOutlined,
PlayCircleOutlined,
WarningOutlined,
DownOutlined
DownOutlined,
type TableProps
} from '@ant-design/icons-vue'
import type { TableProps } from 'ant-design-vue'
import { getUsers, updateUser, createUser, deleteUser } from '@/api/user'
import { getUsers, getUser, updateUser, createUser, deleteUser } from '@/api/user'
import type { User } from '@/api/user'
interface SearchForm {
@@ -473,9 +473,44 @@ const showCreateModal = () => {
modalVisible.value = true
}
const handleView = (record: User) => {
// TODO: 跳转到用户详情页
message.info(`查看用户: ${record.nickname}`)
const handleView = async (record: User) => {
try {
const response = await getUser(record.id)
Modal.info({
title: '用户详情',
width: 600,
content: h('div', { class: 'user-detail-modal' }, [
h('a-descriptions', {
column: 1,
bordered: true
}, [
h('a-descriptions-item', { label: '用户ID' }, response.data.id),
h('a-descriptions-item', { label: '用户名' }, response.data.username),
h('a-descriptions-item', { label: '昵称' }, response.data.nickname),
h('a-descriptions-item', { label: '头像' }, [
h('a-avatar', {
src: response.data.avatar,
size: 64,
shape: 'square'
})
]),
h('a-descriptions-item', { label: '性别' }, response.data.gender),
h('a-descriptions-item', { label: '生日' }, response.data.birthday),
h('a-descriptions-item', { label: '手机号' }, response.data.phone),
h('a-descriptions-item', { label: '邮箱' }, response.data.email),
h('a-descriptions-item', { label: '状态' }, [
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
]),
h('a-descriptions-item', { label: '等级' }, `Lv.${response.data.level}`),
h('a-descriptions-item', { label: '积分' }, response.data.points),
h('a-descriptions-item', { label: '注册时间' }, response.data.created_at),
h('a-descriptions-item', { label: '更新时间' }, response.data.updated_at)
])
])
})
} catch (error) {
message.error('获取用户详情失败')
}
}
const handleEdit = (record: User) => {

View File

@@ -1,127 +1,80 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
interface AppState {
// 应用配置
name: string
version: string
// 用户信息
user: any
// 系统状态
loading: boolean
error: string | null
// 功能开关
features: {
analytics: boolean
debug: boolean
}
}
import { reactive } from 'vue'
import { authAPI } from '@/api'
import type { Admin } from '@/types/user'
export const useAppStore = defineStore('app', () => {
// 状态
const state = ref<AppState>({
name: import.meta.env.VITE_APP_NAME || '结伴客后台管理系统',
version: import.meta.env.VITE_APP_VERSION || '1.0.0',
user: null,
const state = reactive({
user: null as Admin | null,
loading: false,
error: null,
features: {
analytics: import.meta.env.VITE_FEATURE_ANALYTICS === 'true',
debug: import.meta.env.VITE_FEATURE_DEBUG === 'true'
}
initialized: false
})
// Getters
const isAuthenticated = () => !!state.value.user
const isLoading = () => state.value.loading
const hasError = () => state.value.error !== null
// Actions
const initializeApp = async () => {
state.value.loading = true
try {
// 初始化应用配置
console.log('🚀 初始化应用:', state.value.name, state.value.version)
// 检查用户登录状态
await checkAuthStatus()
// 初始化分析工具(如果启用)
if (state.value.features.analytics) {
initAnalytics()
}
state.value.error = null
} catch (error) {
state.value.error = error instanceof Error ? error.message : '初始化失败'
console.error('❌ 应用初始化失败:', error)
} finally {
state.value.loading = false
}
// 设置用户信息
const setUser = (user: Admin) => {
state.user = user
}
const checkAuthStatus = async () => {
// 清除用户信息
const clearUser = () => {
state.user = null
localStorage.removeItem('admin_token')
}
// 初始化应用
const initialize = async () => {
if (state.initialized) return
state.loading = true
try {
// 检查本地存储的token
const token = localStorage.getItem('admin_token')
if (token) {
// TODO: 验证token有效性
// const user = await authAPI.verifyToken(token)
// state.value.user = user
console.log('✅ 用户已登录')
} else {
console.log(' 用户未登录')
// 获取用户信息
const response = await authAPI.getCurrentUser()
// 修复数据结构访问问题
state.user = response.data.admin
}
} catch (error) {
console.warn('⚠️ 认证状态检查失败:', error)
// 清除无效的token
localStorage.removeItem('admin_token')
console.error('初始化失败:', error)
clearUser()
} finally {
state.loading = false
state.initialized = true
}
}
const setUser = (user: any) => {
state.value.user = user
}
const setLoading = (loading: boolean) => {
state.value.loading = loading
}
const setError = (error: string | null) => {
state.value.error = error
}
const clearError = () => {
state.value.error = null
// 登录
const login = async (credentials: { username: string; password: string }) => {
state.loading = true
try {
const response = await authAPI.login(credentials)
// 保存token
localStorage.setItem('admin_token', response.data.token)
// 设置用户信息
state.user = response.data.admin
return response
} finally {
state.loading = false
}
}
// 退出登录
const logout = () => {
state.value.user = null
localStorage.removeItem('admin_token')
// TODO: 调用退出接口
}
// 私有方法
const initAnalytics = () => {
if (state.value.features.analytics) {
console.log('📊 初始化分析工具')
// TODO: 集成分析工具
}
clearUser()
}
return {
// 状态
state,
// Getters
isAuthenticated,
isLoading,
hasError,
// Actions
initializeApp,
setUser,
setLoading,
setError,
clearError,
clearUser,
initialize,
login,
logout
}
})
})
export type { Admin }

View File

@@ -1,8 +1,10 @@
export { useAppStore } from './app'
// 导出其他store模块
// export { useUserStore } from './user'
// export { useTravelStore } from './travel'
// export { useAnimalStore } from './animal'
// export { useMerchantStore } from './merchant'
// export { useOrderStore } from './order'
// export { usePromotionStore } from './promotion'
// 导出其他store模块
export { useUserStore } from './modules/user.js'
export { useMerchantStore } from './modules/merchant.js'
export { useOrderStore } from './modules/order.js'
export { useAnimalStore } from './modules/animal.js'
export { useActivityStore } from './modules/activity.js'
export { useContentStore } from './modules/content.js'
export { useStatisticsStore } from './modules/statistics.js'
export { usePermissionStore } from './modules/permission.js'

View File

@@ -0,0 +1,249 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Activity, ActivityQueryParams, ActivityStatus, ActivityType, ActivityStatistics } from '../../types/activity'
interface ActivityState {
activities: Activity[]
currentActivity: Activity | null
loading: boolean
totalCount: number
queryParams: ActivityQueryParams
}
export const useActivityStore = defineStore('activity', () => {
// 状态
const activities = ref<Activity[]>([])
const currentActivity = ref<Activity | null>(null)
const loading = ref(false)
const totalCount = ref(0)
const queryParams = ref<ActivityQueryParams>({
page: 1,
pageSize: 10,
keyword: '',
status: undefined,
activityType: undefined,
organizerId: undefined,
startDate: '',
endDate: ''
})
// 获取活动列表
const fetchActivities = async (params?: ActivityQueryParams) => {
loading.value = true
try {
// 模拟数据
const mockActivities: Activity[] = [
{
id: 1,
title: '周末郊游',
activityType: 'travel',
description: '周末一起去郊游,享受大自然',
organizerId: 101,
organizerName: '张组织者',
location: '北京市朝阳公园',
startTime: '2024-01-20 09:00:00',
endTime: '2024-01-20 17:00:00',
maxParticipants: 20,
currentParticipants: 15,
status: 'active',
price: 50.00,
createTime: '2024-01-15 14:30:00',
images: ['/images/activity1.jpg'],
tags: ['郊游', '户外', '社交']
},
{
id: 2,
title: '电影之夜',
activityType: 'movie',
description: '一起观看最新上映的电影',
organizerId: 102,
organizerName: '李组织者',
location: '北京市万达影城',
startTime: '2024-01-22 19:00:00',
endTime: '2024-01-22 22:00:00',
maxParticipants: 10,
currentParticipants: 8,
status: 'active',
price: 35.00,
createTime: '2024-01-16 10:15:00',
images: ['/images/activity2.jpg'],
tags: ['电影', '娱乐', '社交']
}
]
activities.value = mockActivities
totalCount.value = mockActivities.length
if (params) {
queryParams.value = { ...queryParams.value, ...params }
}
} catch (error) {
console.error('获取活动列表失败:', error)
throw error
} finally {
loading.value = false
}
}
// 获取活动详情
const fetchActivityDetail = async (activityId: number) => {
loading.value = true
try {
// 模拟数据
const mockActivity: Activity = {
id: activityId,
title: `活动${activityId}`,
activityType: 'travel',
description: '活动描述',
organizerId: 100 + activityId,
organizerName: '组织者',
location: '地点',
startTime: '2024-01-20 10:00:00',
endTime: '2024-01-20 18:00:00',
maxParticipants: 20,
currentParticipants: 10,
status: 'active',
price: 50.00,
createTime: '2024-01-15 14:30:00',
images: ['/images/activity.jpg'],
tags: ['标签1', '标签2']
}
currentActivity.value = mockActivity
return mockActivity
} catch (error) {
console.error('获取活动详情失败:', error)
throw error
} finally {
loading.value = false
}
}
// 更新活动状态
const updateActivityStatus = async (activityId: number, status: ActivityStatus) => {
try {
// 模拟状态更新
const activity = activities.value.find(a => a.id === activityId)
if (activity) {
activity.status = status
}
if (currentActivity.value && currentActivity.value.id === activityId) {
currentActivity.value.status = status
}
return true
} catch (error) {
console.error('更新活动状态失败:', error)
throw error
}
}
// 审核活动
const auditActivity = async (activityId: number, auditStatus: ActivityStatus, auditRemark?: string) => {
try {
// 模拟审核操作
const activity = activities.value.find(a => a.id === activityId)
if (activity) {
activity.status = auditStatus
activity.auditRemark = auditRemark
activity.auditTime = new Date().toISOString()
}
if (currentActivity.value && currentActivity.value.id === activityId) {
currentActivity.value.status = auditStatus
currentActivity.value.auditRemark = auditRemark
currentActivity.value.auditTime = new Date().toISOString()
}
return true
} catch (error) {
console.error('审核活动失败:', error)
throw error
}
}
// 搜索活动
const searchActivities = async (keyword: string) => {
queryParams.value.keyword = keyword
return fetchActivities(queryParams.value)
}
// 重置查询参数
const resetQueryParams = () => {
queryParams.value = {
page: 1,
pageSize: 10,
keyword: '',
status: undefined,
activityType: undefined,
organizerId: undefined,
startDate: '',
endDate: ''
}
}
// 获取活动统计
const getActivityStatistics = async () => {
try {
// 模拟统计数据
return {
totalActivities: 89,
activeActivities: 67,
completedActivities: 22,
cancelledActivities: 5,
totalParticipants: 456,
totalRevenue: 12800.50,
averageParticipation: 5.12,
activityTypeDistribution: {
travel: 25,
movie: 18,
dinner: 15,
game: 12,
other: 19
},
statusDistribution: {
pending: 8,
active: 67,
completed: 22,
cancelled: 5
},
popularActivities: activities.value.slice(0, 5)
}
} catch (error) {
console.error('获取活动统计失败:', error)
throw error
}
}
// 导出活动数据
const exportActivities = async () => {
try {
// 模拟导出功能
const csvContent = activities.value.map(activity =>
`${activity.id},${activity.title},${activity.organizerName},${activity.activityType},${activity.status},${activity.currentParticipants}`
).join('\n')
return csvContent
} catch (error) {
console.error('导出活动数据失败:', error)
throw error
}
}
return {
activities,
currentActivity,
loading,
totalCount,
queryParams,
fetchActivities,
fetchActivityDetail,
updateActivityStatus,
auditActivity,
searchActivities,
resetQueryParams,
getActivityStatistics,
exportActivities
}
})

View File

@@ -0,0 +1,245 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Animal, AnimalQueryParams, AnimalStatus, AnimalType, MedicalRecord, AnimalAdoptionRecord } from '../../types/animal'
interface AnimalState {
animals: Animal[]
currentAnimal: Animal | null
loading: boolean
totalCount: number
queryParams: AnimalQueryParams
}
export const useAnimalStore = defineStore('animal', () => {
// 状态
const animals = ref<Animal[]>([])
const currentAnimal = ref<Animal | null>(null)
const loading = ref(false)
const totalCount = ref(0)
const queryParams = ref<AnimalQueryParams>({
page: 1,
pageSize: 10,
keyword: '',
status: undefined,
animalType: undefined,
farmId: undefined,
startDate: '',
endDate: ''
})
// 获取动物列表
const fetchAnimals = async (params?: AnimalQueryParams) => {
loading.value = true
try {
// 模拟数据
const mockAnimals: Animal[] = [
{
id: 1,
name: '小白',
animalType: 'sheep',
breed: '绵羊',
age: 2,
gender: 'male',
status: 'available',
farmId: 1,
farmName: '快乐农场',
price: 299.00,
description: '温顺可爱的小绵羊',
images: ['/images/sheep1.jpg'],
healthStatus: 'excellent',
adoptionCount: 5,
createTime: '2024-01-10 09:00:00',
features: ['温顺', '亲人', '爱吃草']
},
{
id: 2,
name: '小花',
animalType: 'cow',
breed: '奶牛',
age: 3,
gender: 'female',
status: 'adopted',
farmId: 1,
farmName: '快乐农场',
price: 499.00,
description: '产奶量高的奶牛',
images: ['/images/cow1.jpg'],
healthStatus: 'good',
adoptionCount: 3,
createTime: '2024-01-12 14:30:00',
features: ['产奶量高', '健康', '温顺']
}
]
animals.value = mockAnimals
totalCount.value = mockAnimals.length
if (params) {
queryParams.value = { ...queryParams.value, ...params }
}
} catch (error) {
console.error('获取动物列表失败:', error)
throw error
} finally {
loading.value = false
}
}
// 获取动物详情
const fetchAnimalDetail = async (animalId: number) => {
loading.value = true
try {
// 模拟数据
const mockAnimal: Animal = {
id: animalId,
name: `动物${animalId}`,
animalType: 'sheep',
breed: '品种',
age: 2,
gender: 'male',
status: 'available',
farmId: 1,
farmName: '测试农场',
price: 199.00,
description: '动物描述',
images: ['/images/animal.jpg'],
healthStatus: 'excellent',
adoptionCount: 0,
createTime: '2024-01-15 10:00:00',
features: ['特征1', '特征2']
}
currentAnimal.value = mockAnimal
return mockAnimal
} catch (error) {
console.error('获取动物详情失败:', error)
throw error
} finally {
loading.value = false
}
}
// 更新动物状态
const updateAnimalStatus = async (animalId: number, status: AnimalStatus) => {
try {
// 模拟状态更新
const animal = animals.value.find((a: Animal) => a.id === animalId)
if (animal) {
animal.status = status
}
if (currentAnimal.value && currentAnimal.value.id === animalId) {
currentAnimal.value.status = status
}
return true
} catch (error) {
console.error('更新动物状态失败:', error)
throw error
}
}
// 添加新动物
const addAnimal = async (animalData: Omit<Animal, 'id' | 'createTime'>) => {
try {
// 模拟添加操作
const newAnimal: Animal = {
...animalData,
id: Date.now(),
createTime: new Date().toISOString()
}
animals.value.unshift(newAnimal)
totalCount.value += 1
return newAnimal
} catch (error) {
console.error('添加动物失败:', error)
throw error
}
}
// 更新动物信息
const updateAnimal = async (animalId: number, updateData: Partial<Animal>) => {
try {
// 模拟更新操作
const animal = animals.value.find((a: Animal) => a.id === animalId)
if (animal) {
Object.assign(animal, updateData)
}
if (currentAnimal.value && currentAnimal.value.id === animalId) {
Object.assign(currentAnimal.value, updateData)
}
return true
} catch (error) {
console.error('更新动物信息失败:', error)
throw error
}
}
// 搜索动物
const searchAnimals = async (keyword: string) => {
queryParams.value.keyword = keyword
return fetchAnimals(queryParams.value)
}
// 重置查询参数
const resetQueryParams = () => {
queryParams.value = {
page: 1,
pageSize: 10,
keyword: '',
status: undefined,
animalType: undefined,
farmId: undefined,
startDate: '',
endDate: ''
}
}
// 获取动物统计
const getAnimalStatistics = async () => {
try {
// 模拟统计数据
return {
totalAnimals: 156,
availableAnimals: 89,
adoptedAnimals: 67,
totalAdoptions: 245,
averageAdoptionPrice: 256.80,
animalTypeDistribution: {
sheep: 45,
cow: 32,
pig: 28,
chicken: 51
},
statusDistribution: {
available: 89,
adopted: 67
},
topAnimals: animals.value.slice(0, 5)
}
} catch (error) {
console.error('获取动物统计失败:', error)
throw error
}
}
return {
animals,
currentAnimal,
loading,
totalCount,
queryParams,
fetchAnimals,
fetchAnimalDetail,
updateAnimalStatus,
addAnimal,
updateAnimal,
searchAnimals,
resetQueryParams,
getAnimalStatistics
}
})

View File

@@ -0,0 +1,439 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type {
Content,
ContentQueryParams,
ContentStatus,
ContentType,
ContentStatistics,
ContentAuditStatus,
ContentCategory
} from '../../types/content'
// 模拟数据 - 内容列表
const mockContents: Content[] = [
{
id: 1,
title: '西藏自驾游攻略分享',
content: '这次西藏之行非常难忘,沿途风景美不胜收...',
contentType: 'article',
authorId: 101,
authorName: '旅行达人小王',
publishTime: '2024-01-15 10:30:00',
status: 'published',
viewCount: 1250,
likeCount: 89,
commentCount: 45,
shareCount: 23,
auditStatus: 'approved',
category: 'travel',
tags: ['西藏', '自驾', '攻略'],
images: ['/images/tibet1.jpg', '/images/tibet2.jpg'],
isPublic: true,
allowComments: true,
allowSharing: true,
editCount: 2
},
{
id: 2,
title: '认养小羊的快乐时光',
content: '今天去农场看望了我认养的小羊,它长得真快...',
contentType: 'post',
authorId: 102,
authorName: '爱心人士小李',
publishTime: '2024-01-14 15:20:00',
status: 'published',
viewCount: 890,
likeCount: 67,
commentCount: 32,
shareCount: 18,
auditStatus: 'approved',
category: 'animal',
tags: ['认养', '小羊', '农场'],
images: ['/images/sheep1.jpg'],
isPublic: true,
allowComments: true,
allowSharing: true,
editCount: 1
},
{
id: 3,
title: '周末电影聚会邀请',
content: '本周末组织电影观看活动,欢迎喜欢电影的朋友参加...',
contentType: 'activity',
authorId: 103,
authorName: '电影爱好者小张',
publishTime: '2024-01-16 09:00:00',
status: 'draft',
viewCount: 120,
likeCount: 15,
commentCount: 8,
shareCount: 5,
auditStatus: 'pending',
category: 'entertainment',
tags: ['电影', '聚会', '周末'],
isPublic: true,
allowComments: true,
allowSharing: true,
editCount: 3
}
]
// 模拟数据 - 内容统计
const mockContentStats: ContentStatistics = {
totalContents: 1560,
publishedContents: 1200,
draftContents: 200,
deletedContents: 160,
totalViews: 125000,
totalLikes: 8900,
totalComments: 4500,
totalShares: 2300,
averageEngagementRate: 12.5,
contentTypeDistribution: {
article: 600,
post: 800,
activity: 120,
comment: 40,
review: 200
},
categoryDistribution: {
travel: 450,
animal: 380,
entertainment: 280,
food: 200,
sports: 150,
study: 100,
other: 0
},
dailyGrowth: 25,
topContents: mockContents.slice(0, 3),
authorDistribution: {
totalAuthors: 350,
activeAuthors: 280,
newAuthorsThisMonth: 45,
topAuthors: [
{ authorId: 101, authorName: '旅行达人小王', contentCount: 45, totalViews: 12000 },
{ authorId: 102, authorName: '爱心人士小李', contentCount: 38, totalViews: 8900 },
{ authorId: 103, authorName: '电影爱好者小张', contentCount: 32, totalViews: 7600 }
]
}
}
export const useContentStore = defineStore('content', () => {
// 状态
const contents = ref<Content[]>(mockContents)
const currentContent = ref<Content | null>(null)
const contentStats = ref<ContentStatistics>(mockContentStats)
const loading = ref(false)
const error = ref<string | null>(null)
// 获取内容列表
const fetchContents = async (params: ContentQueryParams) => {
loading.value = true
error.value = null
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500))
// 简单的过滤和分页逻辑
let filteredContents = mockContents
if (params.keyword) {
filteredContents = filteredContents.filter(content =>
content.title.includes(params.keyword!) ||
content.content.includes(params.keyword!)
)
}
if (params.status) {
filteredContents = filteredContents.filter(content => content.status === params.status)
}
if (params.contentType) {
filteredContents = filteredContents.filter(content => content.contentType === params.contentType)
}
if (params.auditStatus) {
filteredContents = filteredContents.filter(content => content.auditStatus === params.auditStatus)
}
if (params.authorId) {
filteredContents = filteredContents.filter(content => content.authorId === params.authorId)
}
if (params.startTime && params.endTime) {
filteredContents = filteredContents.filter(content =>
content.publishTime >= params.startTime! &&
content.publishTime <= params.endTime!
)
}
// 分页
const start = (params.page - 1) * params.pageSize
const paginatedContents = filteredContents.slice(start, start + params.pageSize)
contents.value = paginatedContents
return {
contents: paginatedContents,
total: filteredContents.length,
page: params.page,
pageSize: params.pageSize
}
} catch (err) {
error.value = '获取内容列表失败'
throw err
} finally {
loading.value = false
}
}
// 获取内容详情
const fetchContentDetail = async (id: number) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 300))
const content = mockContents.find((c: Content) => c.id === id)
if (!content) {
throw new Error('内容不存在')
}
currentContent.value = content
return content
} catch (err) {
error.value = '获取内容详情失败'
throw err
} finally {
loading.value = false
}
}
// 审核内容
const auditContent = async (id: number, auditStatus: 'approved' | 'rejected', remark?: string) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 500))
const contentIndex = mockContents.findIndex((c: Content) => c.id === id)
if (contentIndex === -1) {
throw new Error('内容不存在')
}
mockContents[contentIndex].auditStatus = auditStatus
if (remark) {
mockContents[contentIndex].auditRemark = remark
}
mockContents[contentIndex].auditTime = new Date().toISOString()
// 更新本地状态
if (currentContent.value?.id === id) {
currentContent.value.auditStatus = auditStatus
currentContent.value.auditRemark = remark
currentContent.value.auditTime = new Date().toISOString()
}
return mockContents[contentIndex]
} catch (err) {
error.value = '审核内容失败'
throw err
} finally {
loading.value = false
}
}
// 更新内容状态
const updateContentStatus = async (id: number, status: 'published' | 'draft' | 'deleted') => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 300))
const contentIndex = mockContents.findIndex((c: Content) => c.id === id)
if (contentIndex === -1) {
throw new Error('内容不存在')
}
mockContents[contentIndex].status = status
// 更新本地状态
if (currentContent.value?.id === id) {
currentContent.value.status = status
}
return mockContents[contentIndex]
} catch (err) {
error.value = '更新内容状态失败'
throw err
} finally {
loading.value = false
}
}
// 删除内容
const deleteContent = async (id: number) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 300))
const contentIndex = mockContents.findIndex((c: Content) => c.id === id)
if (contentIndex === -1) {
throw new Error('内容不存在')
}
mockContents.splice(contentIndex, 1)
// 清除当前内容
if (currentContent.value?.id === id) {
currentContent.value = null
}
return true
} catch (err) {
error.value = '删除内容失败'
throw err
} finally {
loading.value = false
}
}
// 获取内容统计
const fetchContentStatistics = async () => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 800))
// 模拟实时数据更新
const updatedStats = {
...mockContentStats,
totalContents: mockContents.length,
publishedContents: mockContents.filter(c => c.status === 'published').length,
draftContents: mockContents.filter(c => c.status === 'draft').length,
totalViews: mockContents.reduce((sum, c) => sum + c.viewCount, 0),
totalLikes: mockContents.reduce((sum, c) => sum + c.likeCount, 0),
totalComments: mockContents.reduce((sum, c) => sum + c.commentCount, 0),
totalShares: mockContents.reduce((sum, c) => sum + c.shareCount, 0)
}
contentStats.value = updatedStats
return updatedStats
} catch (err) {
error.value = '获取内容统计失败'
throw err
} finally {
loading.value = false
}
}
// 搜索内容
const searchContents = async (keyword: string, filters?: Partial<ContentQueryParams>) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 400))
let results = mockContents.filter(content =>
content.title.includes(keyword) ||
content.content.includes(keyword) ||
content.tags?.some((tag: string) => tag.includes(keyword))
)
if (filters?.status) {
results = results.filter(content => content.status === filters.status)
}
if (filters?.contentType) {
results = results.filter(content => content.contentType === filters.contentType)
}
return results
} catch (err) {
error.value = '搜索内容失败'
throw err
} finally {
loading.value = false
}
}
// 批量操作
const batchOperation = async (ids: number[], operation: 'publish' | 'draft' | 'delete' | 'approve' | 'reject') => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 600))
const results = []
for (const id of ids) {
const contentIndex = mockContents.findIndex(c => c.id === id)
if (contentIndex !== -1) {
switch (operation) {
case 'publish':
mockContents[contentIndex].status = 'published'
break
case 'draft':
mockContents[contentIndex].status = 'draft'
break
case 'delete':
mockContents.splice(contentIndex, 1)
break
case 'approve':
mockContents[contentIndex].auditStatus = 'approved'
break
case 'reject':
mockContents[contentIndex].auditStatus = 'rejected'
break
}
results.push({ id, success: true })
} else {
results.push({ id, success: false, error: '内容不存在' })
}
}
return results
} catch (err) {
error.value = '批量操作失败'
throw err
} finally {
loading.value = false
}
}
// 重置错误
const clearError = () => {
error.value = null
}
return {
// 状态
contents,
currentContent,
contentStats,
loading,
error,
// 操作
fetchContents,
fetchContentDetail,
auditContent,
updateContentStatus,
deleteContent,
fetchContentStatistics,
searchContents,
batchOperation,
clearError
}
})

View File

@@ -0,0 +1,232 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Merchant, MerchantQueryParams, MerchantStatus, MerchantAuditStatus } from '../../types/merchant'
interface MerchantState {
merchants: Merchant[]
currentMerchant: Merchant | null
loading: boolean
totalCount: number
queryParams: MerchantQueryParams
}
export const useMerchantStore = defineStore('merchant', () => {
// 状态
const merchants = ref<Merchant[]>([])
const currentMerchant = ref<Merchant | null>(null)
const loading = ref(false)
const totalCount = ref(0)
const queryParams = ref<MerchantQueryParams>({
page: 1,
pageSize: 10,
keyword: '',
status: undefined,
auditStatus: undefined,
startDate: '',
endDate: ''
})
// 获取商家列表
const fetchMerchants = async (params?: MerchantQueryParams) => {
loading.value = true
try {
// 模拟数据
const mockMerchants: Merchant[] = [
{
id: 1,
userId: 101,
shopName: '鲜花坊',
businessLicense: '1234567890',
contactPerson: '张老板',
contactPhone: '13800138001',
contactEmail: 'flower@example.com',
address: '北京市朝阳区',
description: '专业鲜花销售',
status: 'active',
auditStatus: 'approved',
auditTime: '2024-01-10 14:30:00',
registerTime: '2024-01-05 09:00:00',
category: 'flower',
serviceScore: 4.8,
orderCount: 156,
totalRevenue: 12800.50
},
{
id: 2,
userId: 102,
shopName: '快乐农场',
businessLicense: '0987654321',
contactPerson: '李农场主',
contactPhone: '13900139001',
contactEmail: 'farm@example.com',
address: '上海市浦东新区',
description: '动物认领和农场体验',
status: 'active',
auditStatus: 'approved',
auditTime: '2024-01-12 16:20:00',
registerTime: '2024-01-08 10:30:00',
category: 'farm',
serviceScore: 4.9,
orderCount: 89,
totalRevenue: 8900.00
}
]
// 应用搜索过滤
let filteredMerchants = mockMerchants
if (queryParams.value.keyword) {
filteredMerchants = mockMerchants.filter((m: Merchant) => {
return m.shopName.toLowerCase().includes(queryParams.value.keyword.toLowerCase()) ||
m.contactPerson.toLowerCase().includes(queryParams.value.keyword.toLowerCase())
})
}
merchants.value = filteredMerchants
totalCount.value = filteredMerchants.length
if (params) {
queryParams.value = { ...queryParams.value, ...params }
}
} catch (error) {
console.error('获取商家列表失败:', error)
throw error
} finally {
loading.value = false
}
}
// 获取商家详情
const fetchMerchantDetail = async (merchantId: number) => {
loading.value = true
try {
// 模拟数据
const mockMerchant: Merchant = {
id: merchantId,
userId: 100 + merchantId,
shopName: `商家${merchantId}`,
businessLicense: `LICENSE${merchantId}`,
contactPerson: '联系人',
contactPhone: '13800138000',
contactEmail: `merchant${merchantId}@example.com`,
address: '地址',
description: '商家描述',
status: 'active',
auditStatus: 'approved',
auditTime: '2024-01-15 10:00:00',
registerTime: '2024-01-01 09:00:00',
category: 'flower',
serviceScore: 4.5,
orderCount: 100,
totalRevenue: 10000.00
}
currentMerchant.value = mockMerchant
return mockMerchant
} catch (error) {
console.error('获取商家详情失败:', error)
throw error
} finally {
loading.value = false
}
}
// 审核商家
const auditMerchant = async (merchantId: number, auditStatus: MerchantAuditStatus, auditRemark?: string) => {
try {
// 模拟审核操作
const merchant = merchants.value.find(m => m.id === merchantId)
if (merchant) {
merchant.auditStatus = auditStatus
merchant.auditTime = new Date().toISOString()
}
if (currentMerchant.value && currentMerchant.value.id === merchantId) {
currentMerchant.value.auditStatus = auditStatus
currentMerchant.value.auditTime = new Date().toISOString()
}
return true
} catch (error) {
console.error('审核商家失败:', error)
throw error
}
}
// 更新商家状态
const updateMerchantStatus = async (merchantId: number, status: MerchantStatus) => {
try {
// 模拟状态更新
const merchant = merchants.value.find(m => m.id === merchantId)
if (merchant) {
merchant.status = status
}
if (currentMerchant.value && currentMerchant.value.id === merchantId) {
currentMerchant.value.status = status
}
return true
} catch (error) {
console.error('更新商家状态失败:', error)
throw error
}
}
// 搜索商家
const searchMerchants = async (keyword: string) => {
queryParams.value.keyword = keyword
return fetchMerchants(queryParams.value)
}
// 重置查询参数
const resetQueryParams = () => {
queryParams.value = {
page: 1,
pageSize: 10,
keyword: '',
status: undefined,
auditStatus: undefined,
startDate: '',
endDate: ''
}
}
// 获取商家统计
const getMerchantStatistics = async () => {
try {
// 模拟统计数据
return {
totalMerchants: 156,
activeMerchants: 142,
pendingAudit: 8,
rejected: 6,
totalRevenue: 256800.50,
averageScore: 4.7,
categoryDistribution: {
flower: 45,
farm: 32,
activity: 23,
other: 56
}
}
} catch (error) {
console.error('获取商家统计失败:', error)
throw error
}
}
return {
merchants,
currentMerchant,
loading,
totalCount,
queryParams,
fetchMerchants,
fetchMerchantDetail,
auditMerchant,
updateMerchantStatus,
searchMerchants,
resetQueryParams,
getMerchantStatistics
}
})

View File

@@ -0,0 +1,223 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Order, OrderQueryParams, OrderStatus } from '../../types/order'
export const useOrderStore = defineStore('order', () => {
// 状态
const orders = ref<Order[]>([])
const currentOrder = ref<Order | null>(null)
const loading = ref(false)
const totalCount = ref(0)
const queryParams = ref<OrderQueryParams>({
page: 1,
pageSize: 10,
keyword: '',
status: undefined,
orderType: undefined,
startDate: '',
endDate: ''
})
// 获取订单列表
const fetchOrders = async (params?: OrderQueryParams) => {
loading.value = true
try {
// 模拟数据
const mockOrders: Order[] = [
{
id: 1,
orderNo: 'ORDER202401150001',
userId: 101,
username: 'user1',
merchantId: 1,
shopName: '鲜花坊',
orderType: 'flower',
productName: '玫瑰花束',
quantity: 1,
unitPrice: 198.00,
totalAmount: 198.00,
status: 'completed',
createTime: '2024-01-15 10:30:00',
completeTime: '2024-01-15 14:00:00',
recipientName: '李四',
recipientPhone: '13900139000',
deliveryAddress: '北京市朝阳区',
message: '生日快乐!'
},
{
id: 2,
orderNo: 'ORDER202401140001',
userId: 102,
username: 'user2',
merchantId: 2,
shopName: '快乐农场',
orderType: 'animal',
productName: '小羊认领',
quantity: 1,
unitPrice: 299.00,
totalAmount: 299.00,
status: 'pending',
createTime: '2024-01-14 16:20:00',
recipientName: '王五',
recipientPhone: '13800138001',
deliveryAddress: '上海市浦东新区'
}
]
orders.value = mockOrders
totalCount.value = mockOrders.length
if (params) {
queryParams.value = { ...queryParams.value, ...params }
}
} catch (error) {
console.error('获取订单列表失败:', error)
throw error
} finally {
loading.value = false
}
}
// 获取订单详情
const fetchOrderDetail = async (orderId: number) => {
loading.value = true
try {
// 模拟数据
const mockOrder: Order = {
id: orderId,
orderNo: `ORDER20240115${orderId.toString().padStart(4, '0')}`,
userId: 100 + orderId,
username: `user${orderId}`,
merchantId: orderId,
shopName: `商家${orderId}`,
orderType: 'flower',
productName: '商品',
quantity: 1,
unitPrice: 100.00,
totalAmount: 100.00,
status: 'pending',
createTime: '2024-01-15 10:00:00',
recipientName: '收件人',
recipientPhone: '13800138000',
deliveryAddress: '地址'
}
currentOrder.value = mockOrder
return mockOrder
} catch (error) {
console.error('获取订单详情失败:', error)
throw error
} finally {
loading.value = false
}
}
// 更新订单状态
const updateOrderStatus = async (orderId: number, status: OrderStatus) => {
try {
// 模拟状态更新
const order = orders.value.find((o: Order) => o.id === orderId)
if (order) {
order.status = status
if (status === 'completed') {
order.completeTime = new Date().toISOString()
}
}
if (currentOrder.value && currentOrder.value.id === orderId) {
currentOrder.value.status = status
if (status === 'completed') {
currentOrder.value.completeTime = new Date().toISOString()
}
}
return true
} catch (error) {
console.error('更新订单状态失败:', error)
throw error
}
}
// 搜索订单
const searchOrders = async (keyword: string) => {
queryParams.value.keyword = keyword
return fetchOrders(queryParams.value)
}
// 重置查询参数
const resetQueryParams = () => {
queryParams.value = {
page: 1,
pageSize: 10,
keyword: '',
status: undefined,
orderType: undefined,
startDate: '',
endDate: ''
}
}
// 获取订单统计
const getOrderStatistics = async () => {
try {
// 模拟统计数据
return {
totalOrders: 1256,
pendingOrders: 45,
completedOrders: 987,
cancelledOrders: 224,
totalRevenue: 156800.50,
averageOrderValue: 124.68,
dailyOrderCount: 89,
orderTypeDistribution: {
flower: 456,
animal: 312,
activity: 288,
travel: 200
},
statusDistribution: {
pending: 45,
paid: 156,
shipped: 89,
completed: 987,
cancelled: 224
}
}
} catch (error) {
console.error('获取订单统计失败:', error)
throw error
}
}
// 导出订单数据
const exportOrders = async () => {
try {
// 模拟导出功能
const csvContent = orders.value.map(order =>
`${order.orderNo},${order.username},${order.shopName},${order.productName},${order.totalAmount},${order.status}`
).join('\n')
return csvContent
} catch (error) {
console.error('导出订单数据失败:', error)
throw error
}
}
return {
orders,
currentOrder,
loading,
totalCount,
queryParams,
fetchOrders,
fetchOrderDetail,
updateOrderStatus,
searchOrders,
resetQueryParams,
getOrderStatistics,
exportOrders
}
})

View File

@@ -0,0 +1,721 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type {
Permission,
Role,
UserRole,
PermissionQueryParams,
RoleQueryParams,
PermissionStatistics,
AuditLog
} from '../../types/permission'
// 模拟数据 - 权限列表
const mockPermissions: Permission[] = [
{
id: 1,
name: '用户管理',
code: 'user:manage',
description: '管理用户信息的权限',
type: 'module',
parentId: null,
level: 1,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
children: [
{
id: 101,
name: '查看用户',
code: 'user:view',
description: '查看用户列表和详情的权限',
type: 'action',
parentId: 1,
level: 2,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00'
},
{
id: 102,
name: '编辑用户',
code: 'user:edit',
description: '编辑用户信息的权限',
type: 'action',
parentId: 1,
level: 2,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00'
},
{
id: 103,
name: '删除用户',
code: 'user:delete',
description: '删除用户的权限',
type: 'action',
parentId: 1,
level: 2,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00'
}
]
},
{
id: 2,
name: '活动管理',
code: 'activity:manage',
description: '管理活动信息的权限',
type: 'module',
parentId: null,
level: 1,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
children: [
{
id: 201,
name: '查看活动',
code: 'activity:view',
description: '查看活动列表和详情的权限',
type: 'action',
parentId: 2,
level: 2,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00'
},
{
id: 202,
name: '创建活动',
code: 'activity:create',
description: '创建新活动的权限',
type: 'action',
parentId: 2,
level: 2,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00'
},
{
id: 203,
name: '审核活动',
code: 'activity:audit',
description: '审核活动内容的权限',
type: 'action',
parentId: 2,
level: 2,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00'
}
]
},
{
id: 3,
name: '动物管理',
code: 'animal:manage',
description: '管理动物信息的权限',
type: 'module',
parentId: null,
level: 1,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
children: [
{
id: 301,
name: '查看动物',
code: 'animal:view',
description: '查看动物列表和详情的权限',
type: 'action',
parentId: 3,
level: 2,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00'
},
{
id: 302,
name: '编辑动物',
code: 'animal:edit',
description: '编辑动物信息的权限',
type: 'action',
parentId: 3,
level: 2,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00'
},
{
id: 303,
name: '认养管理',
code: 'animal:adopt',
description: '管理动物认养流程的权限',
type: 'action',
parentId: 3,
level: 2,
status: 'active',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00'
}
]
}
]
// 模拟数据 - 角色列表
const mockRoles: Role[] = [
{
id: 1,
name: '超级管理员',
code: 'super_admin',
description: '拥有系统所有权限的最高权限角色',
type: 'system',
status: 'active',
permissionIds: [1, 101, 102, 103, 2, 201, 202, 203, 3, 301, 302, 303],
userCount: 3,
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
permissions: mockPermissions.flatMap(p => p.children || []).filter(p =>
[101, 102, 103, 201, 202, 203, 301, 302, 303].includes(p.id)
)
},
{
id: 2,
name: '内容管理员',
code: 'content_admin',
description: '负责内容审核和管理的角色',
type: 'system',
status: 'active',
permissionIds: [2, 201, 203, 3, 301],
userCount: 5,
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
permissions: mockPermissions.flatMap(p => p.children || []).filter(p =>
[201, 203, 301].includes(p.id)
)
},
{
id: 3,
name: '用户管理员',
code: 'user_admin',
description: '负责用户管理和支持的角色',
type: 'system',
status: 'active',
permissionIds: [1, 101, 102],
userCount: 8,
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
permissions: mockPermissions.flatMap(p => p.children || []).filter(p =>
[101, 102].includes(p.id)
)
}
]
// 模拟数据 - 用户角色关联
const mockUserRoles: UserRole[] = [
{ id: 1, userId: 1, roleId: 1, assignTime: '2024-01-01 10:00:00', assignBy: 'system' },
{ id: 2, userId: 2, roleId: 2, assignTime: '2024-01-01 10:00:00', assignBy: 'admin' },
{ id: 3, userId: 3, roleId: 2, assignTime: '2024-01-01 10:00:00', assignBy: 'admin' },
{ id: 4, userId: 4, roleId: 3, assignTime: '2024-01-01 10:00:00', assignBy: 'admin' }
]
// 模拟数据 - 权限统计
const mockPermissionStats: PermissionStatistics = {
totalPermissions: 12,
activePermissions: 12,
inactivePermissions: 0,
totalRoles: 3,
activeRoles: 3,
inactiveRoles: 0,
totalUsersWithRoles: 4,
permissionUsage: {
'user:view': 15,
'user:edit': 8,
'user:delete': 3,
'activity:view': 25,
'activity:create': 12,
'activity:audit': 18,
'animal:view': 20,
'animal:edit': 10,
'animal:adopt': 15
},
roleDistribution: {
'super_admin': 3,
'content_admin': 5,
'user_admin': 8
},
userPermissionStats: {
averagePermissionsPerUser: 6.5,
maxPermissionsPerUser: 9,
minPermissionsPerUser: 2,
usersWithExcessivePermissions: 1
},
auditLogs: {
totalLogs: 156,
permissionChanges: 45,
roleChanges: 32,
userRoleChanges: 79
}
}
// 模拟数据 - 审计日志
const mockAuditLogs: AuditLog[] = [
{
id: 1,
action: 'assign_role',
targetType: 'user',
targetId: 2,
targetName: 'user2',
performerId: 1,
performerName: 'admin',
details: '分配角色: content_admin',
ipAddress: '192.168.1.100',
userAgent: 'Mozilla/5.0',
timestamp: '2024-01-15 14:30:00',
status: 'success'
},
{
id: 2,
action: 'update_permission',
targetType: 'permission',
targetId: 101,
targetName: 'user:view',
performerId: 1,
performerName: 'admin',
details: '更新权限描述',
ipAddress: '192.168.1.100',
userAgent: 'Mozilla/5.0',
timestamp: '2024-01-15 14:25:00',
status: 'success'
},
{
id: 3,
action: 'create_role',
targetType: 'role',
targetId: 3,
targetName: 'user_admin',
performerId: 1,
performerName: 'admin',
details: '创建新角色',
ipAddress: '192.168.1.100',
userAgent: 'Mozilla/5.0',
timestamp: '2024-01-15 14:20:00',
status: 'success'
}
]
export const usePermissionStore = defineStore('permission', () => {
// 状态
const permissions = ref<Permission[]>(mockPermissions)
const roles = ref<Role[]>(mockRoles)
const userRoles = ref<UserRole[]>(mockUserRoles)
const permissionStats = ref<PermissionStatistics>(mockPermissionStats)
const auditLogs = ref<AuditLog[]>(mockAuditLogs)
const loading = ref(false)
const error = ref<string | null>(null)
// 获取权限列表
const fetchPermissions = async (params: PermissionQueryParams) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 500))
let filteredPermissions = mockPermissions.flat()
if (params.keyword) {
filteredPermissions = filteredPermissions.filter(permission =>
permission.name.includes(params.keyword!) ||
permission.code.includes(params.keyword!) ||
permission.description.includes(params.keyword!)
)
}
if (params.status) {
filteredPermissions = filteredPermissions.filter(permission => permission.status === params.status)
}
if (params.type) {
filteredPermissions = filteredPermissions.filter(permission => permission.type === params.type)
}
if (params.parentId !== undefined) {
filteredPermissions = filteredPermissions.filter(permission => permission.parentId === params.parentId)
}
// 分页
const start = (params.page - 1) * params.pageSize
const paginatedPermissions = filteredPermissions.slice(start, start + params.pageSize)
permissions.value = paginatedPermissions
return {
permissions: paginatedPermissions,
total: filteredPermissions.length,
page: params.page,
pageSize: params.pageSize
}
} catch (err) {
error.value = '获取权限列表失败'
throw err
} finally {
loading.value = false
}
}
// 获取角色列表
const fetchRoles = async (params: RoleQueryParams) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 500))
let filteredRoles = mockRoles
if (params.keyword) {
filteredRoles = filteredRoles.filter(role =>
role.name.includes(params.keyword!) ||
role.code.includes(params.keyword!) ||
role.description.includes(params.keyword!)
)
}
if (params.status) {
filteredRoles = filteredRoles.filter(role => role.status === params.status)
}
if (params.type) {
filteredRoles = filteredRoles.filter(role => role.type === params.type)
}
// 分页
const start = (params.page - 1) * params.pageSize
const paginatedRoles = filteredRoles.slice(start, start + params.pageSize)
roles.value = paginatedRoles
return {
roles: paginatedRoles,
total: filteredRoles.length,
page: params.page,
pageSize: params.pageSize
}
} catch (err) {
error.value = '获取角色列表失败'
throw err
} finally {
loading.value = false
}
}
// 获取用户角色
const fetchUserRoles = async (userId: number) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 300))
const userRoles = mockUserRoles.filter(ur => ur.userId === userId)
const roleIds = userRoles.map(ur => ur.roleId)
const userRolesWithDetails = mockRoles
.filter(role => roleIds.includes(role.id))
.map(role => ({
...role,
assignTime: userRoles.find(ur => ur.roleId === role.id)?.assignTime,
assignBy: userRoles.find(ur => ur.roleId === role.id)?.assignBy
}))
return userRolesWithDetails
} catch (err) {
error.value = '获取用户角色失败'
throw err
} finally {
loading.value = false
}
}
// 分配用户角色
const assignUserRole = async (userId: number, roleId: number, assignBy: string) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 400))
// 检查是否已分配
const existingAssignment = mockUserRoles.find(ur => ur.userId === userId && ur.roleId === roleId)
if (existingAssignment) {
throw new Error('用户已拥有该角色')
}
const newAssignment: UserRole = {
id: Math.max(...mockUserRoles.map(ur => ur.id)) + 1,
userId,
roleId,
assignTime: new Date().toISOString(),
assignBy
}
mockUserRoles.push(newAssignment)
// 更新角色用户计数
const role = mockRoles.find(r => r.id === roleId)
if (role) {
role.userCount += 1
}
return newAssignment
} catch (err) {
error.value = '分配用户角色失败'
throw err
} finally {
loading.value = false
}
}
// 移除用户角色
const removeUserRole = async (userId: number, roleId: number) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 400))
const index = mockUserRoles.findIndex(ur => ur.userId === userId && ur.roleId === roleId)
if (index === -1) {
throw new Error('用户角色关联不存在')
}
mockUserRoles.splice(index, 1)
// 更新角色用户计数
const role = mockRoles.find(r => r.id === roleId)
if (role && role.userCount > 0) {
role.userCount -= 1
}
return true
} catch (err) {
error.value = '移除用户角色失败'
throw err
} finally {
loading.value = false
}
}
// 创建角色
const createRole = async (roleData: Omit<Role, 'id' | 'createTime' | 'updateTime'>) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 500))
const newRole: Role = {
...roleData,
id: Math.max(...mockRoles.map(r => r.id)) + 1,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
permissions: mockPermissions.flatMap(p => p.children || []).filter(p =>
roleData.permissionIds.includes(p.id)
)
}
mockRoles.push(newRole)
roles.value = [...mockRoles]
return newRole
} catch (err) {
error.value = '创建角色失败'
throw err
} finally {
loading.value = false
}
}
// 更新角色
const updateRole = async (roleId: number, roleData: Partial<Omit<Role, 'id' | 'createTime'>>) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 500))
const index = mockRoles.findIndex(r => r.id === roleId)
if (index === -1) {
throw new Error('角色不存在')
}
mockRoles[index] = {
...mockRoles[index],
...roleData,
updateTime: new Date().toISOString(),
permissions: roleData.permissionIds
? mockPermissions.flatMap(p => p.children || []).filter(p =>
roleData.permissionIds!.includes(p.id)
)
: mockRoles[index].permissions
}
roles.value = [...mockRoles]
return mockRoles[index]
} catch (err) {
error.value = '更新角色失败'
throw err
} finally {
loading.value = false
}
}
// 删除角色
const deleteRole = async (roleId: number) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 400))
const index = mockRoles.findIndex(r => r.id === roleId)
if (index === -1) {
throw new Error('角色不存在')
}
// 检查是否有用户使用该角色
const usersWithRole = mockUserRoles.filter(ur => ur.roleId === roleId)
if (usersWithRole.length > 0) {
throw new Error('无法删除有用户使用的角色')
}
mockRoles.splice(index, 1)
roles.value = [...mockRoles]
return true
} catch (err) {
error.value = '删除角色失败'
throw err
} finally {
loading.value = false
}
}
// 获取权限统计
const fetchPermissionStatistics = async () => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 800))
// 模拟实时数据更新
const stats = {
...mockPermissionStats,
totalUsersWithRoles: mockUserRoles.length,
userPermissionStats: {
...mockPermissionStats.userPermissionStats,
averagePermissionsPerUser: Math.round(
mockUserRoles.reduce((sum, ur) => {
const role = mockRoles.find(r => r.id === ur.roleId)
return sum + (role ? role.permissionIds.length : 0)
}, 0) / mockUserRoles.length
)
}
}
permissionStats.value = stats
return stats
} catch (err) {
error.value = '获取权限统计失败'
throw err
} finally {
loading.value = false
}
}
// 获取审计日志
const fetchAuditLogs = async (page: number, pageSize: number) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 600))
const start = (page - 1) * pageSize
const paginatedLogs = mockAuditLogs.slice(start, start + pageSize)
auditLogs.value = paginatedLogs
return {
logs: paginatedLogs,
total: mockAuditLogs.length,
page,
pageSize
}
} catch (err) {
error.value = '获取审计日志失败'
throw err
} finally {
loading.value = false
}
}
// 检查用户权限
const checkUserPermission = async (userId: number, permissionCode: string) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 200))
const userRoles = mockUserRoles.filter(ur => ur.userId === userId)
const roleIds = userRoles.map(ur => ur.roleId)
const hasPermission = mockRoles.some(role =>
roleIds.includes(role.id) &&
role.permissions.some(p => p.code === permissionCode)
)
return hasPermission
} catch (err) {
error.value = '检查用户权限失败'
throw err
} finally {
loading.value = false
}
}
// 重置错误
const clearError = () => {
error.value = null
}
return {
// 状态
permissions,
roles,
userRoles,
permissionStats,
auditLogs,
loading,
error,
// 操作
fetchPermissions,
fetchRoles,
fetchUserRoles,
assignUserRole,
removeUserRole,
createRole,
updateRole,
deleteRole,
fetchPermissionStatistics,
fetchAuditLogs,
checkUserPermission,
clearError
}
})

View File

@@ -0,0 +1,668 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type {
SystemStats,
UserStats,
ActivityStats,
AnimalStats,
OrderStats,
RevenueStats,
TimeRange,
StatsComparison,
DashboardMetrics
} from '../../types/stats'
// 模拟数据 - 系统统计
const mockSystemStats: SystemStats = {
totalUsers: 12560,
activeUsers: 8560,
newUsersToday: 125,
newUsersThisWeek: 890,
newUsersThisMonth: 3450,
totalActivities: 2560,
activeActivities: 1250,
completedActivities: 890,
totalAnimals: 560,
adoptedAnimals: 320,
availableAnimals: 240,
totalOrders: 4560,
pendingOrders: 120,
completedOrders: 4320,
cancelledOrders: 120,
totalRevenue: 125600,
todayRevenue: 2560,
weekRevenue: 17890,
monthRevenue: 56780,
systemUptime: '99.95%',
averageResponseTime: 120,
errorRate: 0.05,
serverLoad: 45.2
}
// 模拟数据 - 用户统计
const mockUserStats: UserStats = {
totalUsers: 12560,
activeUsers: 8560,
newUsers: {
today: 125,
yesterday: 110,
thisWeek: 890,
lastWeek: 780,
thisMonth: 3450,
lastMonth: 2980
},
userGrowthRate: 15.8,
userRetentionRate: 78.5,
userChurnRate: 5.2,
userDemographics: {
ageDistribution: {
'18-24': 25.6,
'25-34': 38.9,
'35-44': 20.1,
'45-54': 10.2,
'55+': 5.2
},
genderDistribution: {
male: 58.3,
female: 40.1,
other: 1.6
},
locationDistribution: {
'北京': 18.5,
'上海': 15.2,
'广州': 12.8,
'深圳': 11.5,
'其他': 42.0
}
},
userActivity: {
dailyActiveUsers: 8560,
weeklyActiveUsers: 23450,
monthlyActiveUsers: 56780,
averageSessionDuration: 12.5,
averageSessionsPerUser: 3.2
},
userBehavior: {
averageActivitiesPerUser: 2.1,
averageAnimalsAdopted: 0.8,
averageOrdersPerUser: 3.6,
conversionRate: 28.5,
engagementRate: 45.2
},
userLifetimeValue: 156.8,
userAcquisitionCost: 25.4,
userRetentionCost: 12.3
}
// 模拟数据 - 活动统计
const mockActivityStats: ActivityStats = {
totalActivities: 2560,
activeActivities: 1250,
completedActivities: 890,
cancelledActivities: 120,
pendingActivities: 300,
activityGrowthRate: 22.5,
participationStats: {
totalParticipants: 12560,
averageParticipantsPerActivity: 4.9,
repeatParticipants: 45.2,
participationRate: 68.9
},
revenueStats: {
totalRevenue: 125600,
averageRevenuePerActivity: 49.1,
revenueGrowthRate: 28.3,
refundRate: 3.2
},
categoryDistribution: {
travel: 35.6,
movie: 18.9,
dinner: 15.2,
game: 12.8,
sports: 8.5,
study: 6.3,
other: 2.7
},
locationDistribution: {
'北京': 22.5,
'上海': 18.9,
'广州': 15.6,
'深圳': 12.8,
'其他': 30.2
},
timeDistribution: {
'00-06': 5.2,
'06-12': 25.6,
'12-18': 35.8,
'18-24': 33.4
},
organizerPerformance: {
topOrganizers: [
{ organizerId: 101, name: '旅行达人小王', activityCount: 45, revenue: 12500 },
{ organizerId: 102, name: '电影爱好者小李', activityCount: 38, revenue: 9800 },
{ organizerId: 103, name: '美食家小张', activityCount: 32, revenue: 7600 }
],
averageRating: 4.5,
completionRate: 89.2,
cancellationRate: 5.8
}
}
// 模拟数据 - 动物统计
const mockAnimalStats: AnimalStats = {
totalAnimals: 560,
adoptedAnimals: 320,
availableAnimals: 240,
adoptionRate: 57.1,
adoptionGrowthRate: 18.9,
animalTypes: {
dog: 35.6,
cat: 28.9,
sheep: 15.2,
cow: 8.5,
chicken: 6.3,
other: 5.5
},
adoptionStats: {
totalAdoptions: 320,
averageAdoptionTime: 3.2,
repeatAdoptions: 12.5,
adoptionSuccessRate: 92.8
},
careStats: {
totalCareRecords: 12560,
averageCareFrequency: 2.5,
medicalCheckups: 456,
vaccinationRate: 89.5
},
locationDistribution: {
'北京农场': 25.6,
'上海牧场': 18.9,
'广州养殖场': 15.2,
'深圳农场': 12.8,
'其他': 27.5
},
popularAnimals: [
{ animalId: 1, name: '小白', type: 'dog', adoptionCount: 15, viewCount: 1250 },
{ animalId: 2, name: '小花', type: 'cat', adoptionCount: 12, viewCount: 980 },
{ animalId: 3, name: '小羊', type: 'sheep', adoptionCount: 10, viewCount: 760 }
],
revenueStats: {
totalAdoptionRevenue: 45600,
averageAdoptionFee: 142.5,
careProductRevenue: 12500,
totalRevenue: 58100
}
}
// 模拟数据 - 订单统计
const mockOrderStats: OrderStats = {
totalOrders: 4560,
pendingOrders: 120,
completedOrders: 4320,
cancelledOrders: 120,
orderGrowthRate: 25.8,
revenueStats: {
totalRevenue: 125600,
averageOrderValue: 27.5,
revenueGrowthRate: 32.1,
refundAmount: 2560,
netRevenue: 123040
},
orderTypeDistribution: {
activity: 45.6,
adoption: 28.9,
flower: 15.2,
product: 8.5,
other: 2.8
},
paymentStats: {
paymentMethods: {
wechat: 58.9,
alipay: 35.2,
bank: 4.2,
other: 1.7
},
paymentSuccessRate: 98.5,
averagePaymentTime: 2.1,
refundRate: 3.2
},
customerStats: {
repeatCustomers: 45.6,
averageOrdersPerCustomer: 3.2,
customerLifetimeValue: 156.8,
acquisitionCost: 25.4
},
timeDistribution: {
'00-06': 8.5,
'06-12': 22.6,
'12-18': 35.8,
'18-24': 33.1
},
locationDistribution: {
'北京': 20.5,
'上海': 18.2,
'广州': 15.8,
'深圳': 13.5,
'其他': 32.0
}
}
// 模拟数据 - 收入统计
const mockRevenueStats: RevenueStats = {
totalRevenue: 125600,
revenueGrowthRate: 32.1,
dailyRevenue: 2560,
weeklyRevenue: 17890,
monthlyRevenue: 56780,
revenueSources: {
activity: 45.6,
adoption: 28.9,
flower: 15.2,
product: 8.5,
other: 2.8
},
revenueTrends: {
last7Days: [1250, 1380, 1560, 1420, 1680, 1920, 2560],
last30Days: [
1200, 1250, 1380, 1420, 1560, 1480, 1620, 1780, 1920, 2050,
2180, 2250, 2380, 2450, 2560, 2680, 2750, 2850, 2920, 3050,
3120, 3250, 3350, 3420, 3560, 3650, 3780, 3850, 3920, 4050
]
},
customerMetrics: {
averageRevenuePerUser: 156.8,
averageOrderValue: 27.5,
purchaseFrequency: 3.2,
customerLifetimeValue: 456.2
},
geographicDistribution: {
'北京': 22.5,
'上海': 19.8,
'广州': 16.2,
'深圳': 13.8,
'其他': 27.7
},
productPerformance: {
topProducts: [
{ productId: 1, name: '西藏自驾游', revenue: 12500, orders: 256 },
{ productId: 2, name: '小羊认养', revenue: 9800, orders: 189 },
{ productId: 3, name: '玫瑰花束', revenue: 7600, orders: 152 }
],
averageProfitMargin: 35.2,
returnOnInvestment: 45.8
}
}
// 模拟数据 - 仪表板指标
const mockDashboardMetrics: DashboardMetrics = {
keyMetrics: {
totalUsers: 12560,
activeUsers: 8560,
totalActivities: 2560,
totalOrders: 4560,
totalRevenue: 125600,
adoptionRate: 57.1,
conversionRate: 28.5,
engagementRate: 45.2,
retentionRate: 78.5
},
growthMetrics: {
userGrowth: 15.8,
activityGrowth: 22.5,
orderGrowth: 25.8,
revenueGrowth: 32.1
},
performanceMetrics: {
systemUptime: '99.95%',
averageResponseTime: 120,
errorRate: 0.05,
conversionRate: 28.5
},
recentActivities: [
{ type: 'user', action: '注册', count: 125, time: '今天' },
{ type: 'activity', action: '创建', count: 45, time: '今天' },
{ type: 'order', action: '完成', count: 89, time: '今天' },
{ type: 'adoption', action: '认养', count: 12, time: '今天' }
],
alerts: [
{ level: 'warning', message: '系统负载较高', time: '2小时前' },
{ level: 'info', message: '新版本发布可用', time: '5小时前' }
],
trends: {
userTrend: [1250, 1380, 1560, 1420, 1680, 1920, 2560],
revenueTrend: [1200, 1250, 1380, 1420, 1560, 1480, 1620],
activityTrend: [45, 52, 48, 56, 62, 58, 65],
orderTrend: [89, 92, 85, 96, 102, 98, 112]
},
forecasts: {
userForecast: [2800, 2950, 3100, 3250, 3400],
revenueForecast: [4200, 4500, 4800, 5100, 5400],
activityForecast: [70, 75, 80, 85, 90],
orderForecast: [120, 130, 140, 150, 160]
},
comparisons: {
vsPreviousPeriod: {
userGrowth: { current: 15.8, previous: 12.5, growthRate: 26.4, trend: 'up', timeRange: 'month' },
revenueGrowth: { current: 32.1, previous: 28.3, growthRate: 13.4, trend: 'up', timeRange: 'month' }
},
vsIndustryAverage: {
userGrowth: { current: 15.8, previous: 10.2, growthRate: 54.9, trend: 'up', timeRange: 'month' },
revenueGrowth: { current: 32.1, previous: 25.6, growthRate: 25.4, trend: 'up', timeRange: 'month' }
},
vsCompetitors: {
userGrowth: { current: 15.8, previous: 14.2, growthRate: 11.3, trend: 'up', timeRange: 'month' },
revenueGrowth: { current: 32.1, previous: 30.5, growthRate: 5.2, trend: 'up', timeRange: 'month' }
}
}
}
export const useStatsStore = defineStore('stats', () => {
// 状态
const systemStats = ref<SystemStats>(mockSystemStats)
const userStats = ref<UserStats>(mockUserStats)
const activityStats = ref<ActivityStats>(mockActivityStats)
const animalStats = ref<AnimalStats>(mockAnimalStats)
const orderStats = ref<OrderStats>(mockOrderStats)
const revenueStats = ref<RevenueStats>(mockRevenueStats)
const dashboardMetrics = ref<DashboardMetrics>(mockDashboardMetrics)
const loading = ref(false)
const error = ref<string | null>(null)
// 获取系统统计
const fetchSystemStats = async (timeRange?: TimeRange) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 800))
// 模拟根据时间范围筛选数据
let stats = { ...mockSystemStats }
if (timeRange) {
// 这里可以根据时间范围调整统计数据
const growthFactor = timeRange === 'today' ? 1 : timeRange === 'week' ? 7 : 30
stats.newUsersToday = Math.round(stats.newUsersToday * (timeRange === 'today' ? 1 : growthFactor / 30))
stats.todayRevenue = Math.round(stats.todayRevenue * (timeRange === 'today' ? 1 : growthFactor / 30))
}
systemStats.value = stats
return stats
} catch (err) {
error.value = '获取系统统计失败'
throw err
} finally {
loading.value = false
}
}
// 获取用户统计
const fetchUserStats = async (timeRange?: TimeRange) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 600))
let stats = { ...mockUserStats }
if (timeRange) {
const growthFactor = timeRange === 'today' ? 1 : timeRange === 'week' ? 7 : 30
stats.newUsers.today = Math.round(stats.newUsers.today * (timeRange === 'today' ? 1 : growthFactor / 30))
stats.newUsers.thisWeek = Math.round(stats.newUsers.thisWeek * (timeRange === 'week' ? 1 : growthFactor / 7))
}
userStats.value = stats
return stats
} catch (err) {
error.value = '获取用户统计失败'
throw err
} finally {
loading.value = false
}
}
// 获取活动统计
const fetchActivityStats = async (timeRange?: TimeRange) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 700))
let stats = { ...mockActivityStats }
if (timeRange) {
const growthFactor = timeRange === 'today' ? 1 : timeRange === 'week' ? 7 : 30
stats.totalActivities = Math.round(stats.totalActivities * growthFactor / 30)
}
activityStats.value = stats
return stats
} catch (err) {
error.value = '获取活动统计失败'
throw err
} finally {
loading.value = false
}
}
// 获取动物统计
const fetchAnimalStats = async (timeRange?: TimeRange) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 500))
let stats = { ...mockAnimalStats }
if (timeRange) {
const growthFactor = timeRange === 'today' ? 1 : timeRange === 'week' ? 7 : 30
stats.totalAnimals = Math.round(stats.totalAnimals * growthFactor / 30)
}
animalStats.value = stats
return stats
} catch (err) {
error.value = '获取动物统计失败'
throw err
} finally {
loading.value = false
}
}
// 获取订单统计
const fetchOrderStats = async (timeRange?: TimeRange) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 600))
let stats = { ...mockOrderStats }
if (timeRange) {
const growthFactor = timeRange === 'today' ? 1 : timeRange === 'week' ? 7 : 30
stats.totalOrders = Math.round(stats.totalOrders * growthFactor / 30)
}
orderStats.value = stats
return stats
} catch (err) {
error.value = '获取订单统计失败'
throw err
} finally {
loading.value = false
}
}
// 获取收入统计
const fetchRevenueStats = async (timeRange?: TimeRange) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 800))
let stats = { ...mockRevenueStats }
if (timeRange) {
const growthFactor = timeRange === 'today' ? 1 : timeRange === 'week' ? 7 : 30
stats.dailyRevenue = Math.round(stats.dailyRevenue * (timeRange === 'today' ? 1 : growthFactor / 30))
}
revenueStats.value = stats
return stats
} catch (err) {
error.value = '获取收入统计失败'
throw err
} finally {
loading.value = false
}
}
// 获取仪表板指标
const fetchDashboardMetrics = async () => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 1000))
// 模拟实时数据更新
const metrics = {
...mockDashboardMetrics,
keyMetrics: {
...mockDashboardMetrics.keyMetrics,
totalUsers: mockSystemStats.totalUsers,
activeUsers: mockSystemStats.activeUsers,
totalRevenue: mockSystemStats.totalRevenue
}
}
dashboardMetrics.value = metrics
return metrics
} catch (err) {
error.value = '获取仪表板指标失败'
throw err
} finally {
loading.value = false
}
}
// 获取统计对比
const fetchStatsComparison = async (type: 'users' | 'activities' | 'orders' | 'revenue', timeRange: TimeRange) => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 600))
let current: number
let previous: number
let growthRate: number
switch (type) {
case 'users':
current = mockUserStats.newUsers[timeRange === 'today' ? 'today' : timeRange === 'week' ? 'thisWeek' : 'thisMonth']
previous = mockUserStats.newUsers[timeRange === 'today' ? 'yesterday' : timeRange === 'week' ? 'lastWeek' : 'lastMonth']
break
case 'activities':
current = mockActivityStats.totalActivities
previous = Math.round(current * 0.85)
break
case 'orders':
current = mockOrderStats.totalOrders
previous = Math.round(current * 0.8)
break
case 'revenue':
current = mockRevenueStats[timeRange === 'today' ? 'dailyRevenue' : timeRange === 'week' ? 'weeklyRevenue' : 'monthlyRevenue']
previous = Math.round(current * 0.75)
break
default:
current = 0
previous = 0
}
growthRate = previous > 0 ? ((current - previous) / previous) * 100 : 0
const comparison: StatsComparison = {
current,
previous,
growthRate,
trend: growthRate >= 0 ? 'up' : 'down',
timeRange
}
return comparison
} catch (err) {
error.value = '获取统计对比失败'
throw err
} finally {
loading.value = false
}
}
// 导出统计数据
const exportStats = async (type: 'users' | 'activities' | 'animals' | 'orders' | 'revenue', format: 'csv' | 'excel') => {
loading.value = true
error.value = null
try {
await new Promise(resolve => setTimeout(resolve, 1200))
// 模拟导出操作
const data = {
users: mockUserStats,
activities: mockActivityStats,
animals: mockAnimalStats,
orders: mockOrderStats,
revenue: mockRevenueStats
}[type]
return {
filename: `${type}_stats_${new Date().toISOString().split('T')[0]}.${format}`,
data,
format,
exportedAt: new Date().toISOString()
}
} catch (err) {
error.value = '导出统计数据失败'
throw err
} finally {
loading.value = false
}
}
// 重置错误
const clearError = () => {
error.value = null
}
return {
// 状态
systemStats,
userStats,
activityStats,
animalStats,
orderStats,
revenueStats,
dashboardMetrics,
loading,
error,
// 操作
fetchSystemStats,
fetchUserStats,
fetchActivityStats,
fetchAnimalStats,
fetchOrderStats,
fetchRevenueStats,
fetchDashboardMetrics,
fetchStatsComparison,
exportStats,
clearError
}
})

View File

@@ -0,0 +1,162 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { User, UserQueryParams, UserStatus } from '../../types/user'
export const useUserStore = defineStore('user', () => {
// 状态
const users = ref<User[]>([])
const currentUser = ref<User | null>(null)
const loading = ref(false)
const totalCount = ref(0)
const queryParams = ref<UserQueryParams>({
page: 1,
pageSize: 10,
keyword: '',
status: undefined,
startDate: '',
endDate: ''
})
// 获取用户列表
const fetchUsers = async (params?: UserQueryParams) => {
loading.value = true
try {
// 这里应该是API调用暂时用模拟数据
const mockUsers: User[] = [
{
id: 1,
username: 'user1',
nickname: '用户1',
avatar: '',
phone: '13800138000',
email: 'user1@example.com',
status: 'active',
registerTime: '2024-01-01 10:00:00',
lastLoginTime: '2024-01-15 15:30:00',
userType: 'normal'
},
{
id: 2,
username: 'merchant1',
nickname: '商家1',
avatar: '',
phone: '13900139000',
email: 'merchant1@example.com',
status: 'active',
registerTime: '2024-01-02 09:00:00',
lastLoginTime: '2024-01-14 14:20:00',
userType: 'merchant'
}
]
users.value = mockUsers
totalCount.value = mockUsers.length
if (params) {
queryParams.value = { ...queryParams.value, ...params }
}
} catch (error) {
console.error('获取用户列表失败:', error)
throw error
} finally {
loading.value = false
}
}
// 获取用户详情
const fetchUserDetail = async (userId: number) => {
loading.value = true
try {
// 模拟API调用
const mockUser: User = {
id: userId,
username: `user${userId}`,
nickname: `用户${userId}`,
avatar: '',
phone: '13800138000',
email: `user${userId}@example.com`,
status: 'active',
registerTime: '2024-01-01 10:00:00',
lastLoginTime: '2024-01-15 15:30:00',
userType: 'normal'
}
currentUser.value = mockUser
return mockUser
} catch (error) {
console.error('获取用户详情失败:', error)
throw error
} finally {
loading.value = false
}
}
// 更新用户状态
const updateUserStatus = async (userId: number, status: UserStatus) => {
try {
// 模拟API调用
const user = users.value.find((u: User) => u.id === userId)
if (user) {
user.status = status
}
if (currentUser.value && currentUser.value.id === userId) {
currentUser.value.status = status
}
return true
} catch (error) {
console.error('更新用户状态失败:', error)
throw error
}
}
// 搜索用户
const searchUsers = async (keyword: string) => {
queryParams.value.keyword = keyword
return fetchUsers(queryParams.value)
}
// 重置查询参数
const resetQueryParams = () => {
queryParams.value = {
page: 1,
pageSize: 10,
keyword: '',
status: undefined,
startDate: '',
endDate: ''
}
}
// 导出用户数据
const exportUsers = async () => {
try {
// 模拟导出功能
const csvContent = users.value.map(user =>
`${user.id},${user.username},${user.nickname},${user.phone},${user.email},${user.status}`
).join('\n')
return csvContent
} catch (error) {
console.error('导出用户数据失败:', error)
throw error
}
}
return {
users,
currentUser,
loading,
totalCount,
queryParams,
fetchUsers,
fetchUserDetail,
updateUserStatus,
searchUsers,
resetQueryParams,
exportUsers
}
})

View File

@@ -0,0 +1,274 @@
// 活动类型
export type ActivityType = 'travel' | 'movie' | 'dinner' | 'game' | 'sports' | 'study' | 'other'
// 活动状态类型
export type ActivityStatus = 'pending' | 'active' | 'completed' | 'cancelled' | 'full'
// 活动审核状态类型
export type ActivityAuditStatus = 'pending' | 'approved' | 'rejected'
// 活动基本信息接口
export interface Activity {
id: number
title: string
activityType: ActivityType
description: string
organizerId: number
organizerName: string
location: string
startTime: string
endTime: string
maxParticipants: number
currentParticipants: number
status: ActivityStatus
price: number
createTime: string
// 扩展信息
images?: string[]
tags?: string[]
requirements?: string
// 审核信息
auditStatus?: ActivityAuditStatus
auditRemark?: string
auditTime?: string
// 统计信息
viewCount?: number
likeCount?: number
shareCount?: number
// 地理位置
latitude?: number
longitude?: number
// 费用说明
feeIncludes?: string[]
feeExcludes?: string[]
// 取消政策
cancellationPolicy?: string
// 联系方式
contactPhone?: string
contactWechat?: string
}
// 活动查询参数接口
export interface ActivityQueryParams {
page: number
pageSize: number
keyword?: string
status?: ActivityStatus
activityType?: ActivityType
organizerId?: number
minPrice?: number
maxPrice?: number
startDate?: string
endDate?: string
location?: string
sortField?: string
sortOrder?: 'asc' | 'desc'
}
// 活动统计信息接口
export interface ActivityStatistics {
totalActivities: number
activeActivities: number
completedActivities: number
cancelledActivities: number
totalParticipants: number
totalRevenue: number
averageParticipation: number
activityTypeDistribution: Record<ActivityType, number>
statusDistribution: Record<ActivityStatus, number>
monthlyGrowth: number
popularActivities: Activity[]
participantDemographics: {
ageDistribution: Record<string, number>
genderDistribution: Record<string, number>
locationDistribution: Record<string, number>
}
}
// 活动参与记录接口
export interface ActivityParticipation {
id: number
activityId: number
activityTitle: string
userId: number
username: string
joinTime: string
status: 'confirmed' | 'waiting' | 'cancelled'
paymentStatus: 'paid' | 'pending' | 'refunded'
paymentAmount: number
paymentTime?: string
// 评价信息
rating?: number
review?: string
reviewTime?: string
// 取消信息
cancelReason?: string
cancelTime?: string
// 参与信息
participantsCount?: number
groupMembers?: number[]
}
// 活动评价接口
export interface ActivityReview {
id: number
activityId: number
activityTitle: string
userId: number
username: string
organizerId: number
organizerName: string
rating: number
review: string
reviewTime: string
isAnonymous: boolean
// 回复信息
reply?: string
replyTime?: string
// 有用性统计
helpfulCount: number
reportCount: number
status: 'normal' | 'hidden' | 'deleted'
}
// 活动组织者接口
export interface ActivityOrganizer {
id: number
username: string
nickname: string
avatar: string
organizerLevel: number
totalActivities: number
completedActivities: number
cancellationRate: number
averageRating: number
totalParticipants: number
joinTime: string
// 认证信息
isVerified: boolean
verificationLevel: number
// 联系方式
contactPhone?: string
contactEmail?: string
// 组织者描述
description?: string
specialties?: string[]
}
// 活动类型统计接口
export interface ActivityTypeStats {
activityType: ActivityType
count: number
totalParticipants: number
totalRevenue: number
averageRating: number
completionRate: number
}
// 活动时间统计接口
export interface ActivityTimeStats {
date: string
activityCount: number
participantCount: number
revenue: number
peakHours: number[]
}
// 活动地点统计接口
export interface ActivityLocationStats {
location: string
activityCount: number
participantCount: number
averageRating: number
totalRevenue: number
}
// 活动审核记录接口
export interface ActivityAuditRecord {
id: number
activityId: number
activityTitle: string
auditStatus: ActivityAuditStatus
auditRemark?: string
auditor: string
auditTime: string
previousStatus?: ActivityStatus
// 审核详情
auditDetails?: {
contentCheck: boolean
safetyCheck: boolean
complianceCheck: boolean
}
}
// 活动取消记录接口
export interface ActivityCancellationRecord {
id: number
activityId: number
activityTitle: string
cancelReason: string
cancelTime: string
cancelledBy: 'organizer' | 'system' | 'participant'
refundStatus: 'pending' | 'processed' | 'completed'
refundAmount: number
affectedParticipants: number
// 取消详情
weatherConditions?: string
safetyConcerns?: string
otherReasons?: string
}
// 活动推荐参数接口
export interface ActivityRecommendationParams {
userId?: number
location?: string
preferredTypes?: ActivityType[]
budgetRange?: [number, number]
timeRange?: [string, string]
participantCount?: number
excludeAttended?: boolean
}
// 活动导出参数接口
export interface ActivityExportParams {
format: 'csv' | 'excel'
fields: string[]
startDate?: string
endDate?: string
status?: ActivityStatus
activityType?: ActivityType
organizerId?: number
}
// 活动参与统计接口
export interface ActivityParticipationStats {
date: string
participationCount: number
newParticipants: number
repeatParticipants: number
participationRate: number
cancellationRate: number
averageGroupSize: number
}
// 活动收入统计接口
export interface ActivityRevenueStats {
date: string
revenue: number
activityCount: number
averageRevenuePerActivity: number
refundAmount: number
netRevenue: number
}
// 活动运营指标接口
export interface ActivityOperationMetrics {
date: string
newActivities: number
completedActivities: number
participantSatisfaction: number
organizerSatisfaction: number
activityCompletionRate: number
revenuePerParticipant: number
marketingEffectiveness: number
}

View File

@@ -0,0 +1,231 @@
// 动物类型
export type AnimalType = 'sheep' | 'cow' | 'pig' | 'chicken' | 'goat' | 'duck' | 'other'
// 动物状态类型
export type AnimalStatus = 'available' | 'adopted' | 'reserved' | 'inactive'
// 动物性别类型
export type AnimalGender = 'male' | 'female' | 'unknown'
// 健康状态类型
export type HealthStatus = 'excellent' | 'good' | 'fair' | 'poor' | 'critical'
// 动物基本信息接口
export interface Animal {
id: number
name: string
animalType: AnimalType
breed: string
age: number
gender: AnimalGender
status: AnimalStatus
farmId: number
farmName: string
price: number
description: string
images: string[]
healthStatus: HealthStatus
adoptionCount: number
createTime: string
// 扩展信息
features?: string[]
personality?: string
favoriteFood?: string
// 医疗记录
vaccinationRecords?: VaccinationRecord[]
medicalHistory?: MedicalRecord[]
// 认养信息
currentAdopter?: string
adoptionStartTime?: string
adoptionEndTime?: string
// 位置信息
location?: string
gpsCoordinates?: string
}
// 动物查询参数接口
export interface AnimalQueryParams {
page: number
pageSize: number
keyword?: string
status?: AnimalStatus
animalType?: AnimalType
farmId?: number
minAge?: number
maxAge?: number
gender?: AnimalGender
minPrice?: number
maxPrice?: number
startDate?: string
endDate?: string
sortField?: string
sortOrder?: 'asc' | 'desc'
}
// 疫苗接种记录接口
export interface VaccinationRecord {
id: number
animalId: number
vaccineName: string
vaccinationDate: string
nextVaccinationDate: string
veterinarian: string
notes?: string
}
// 医疗记录接口
export interface MedicalRecord {
id: number
animalId: number
diagnosis: string
treatment: string
treatmentDate: string
veterinarian: string
cost: number
notes?: string
followUpDate?: string
}
// 动物统计信息接口
export interface AnimalStatistics {
totalAnimals: number
availableAnimals: number
adoptedAnimals: number
totalAdoptions: number
averageAdoptionPrice: number
animalTypeDistribution: Record<AnimalType, number>
statusDistribution: Record<AnimalStatus, number>
healthStatusDistribution: Record<HealthStatus, number>
monthlyAdoptions: number
adoptionGrowthRate: number
topAdoptedAnimals: Animal[]
}
// 动物类型统计接口
export interface AnimalTypeStats {
animalType: AnimalType
count: number
adoptionCount: number
averagePrice: number
adoptionRate: number
}
// 农场动物统计接口
export interface FarmAnimalStats {
farmId: number
farmName: string
totalAnimals: number
availableAnimals: number
adoptedAnimals: number
totalAdoptions: number
averageAdoptionPrice: number
}
// 动物健康统计接口
export interface AnimalHealthStats {
date: string
excellentCount: number
goodCount: number
fairCount: number
poorCount: number
criticalCount: number
vaccinationRate: number
}
// 动物认养记录接口
export interface AnimalAdoptionRecord {
id: number
animalId: number
animalName: string
userId: number
username: string
startTime: string
endTime: string
duration: number
totalCost: number
status: 'active' | 'completed' | 'cancelled'
paymentStatus: 'paid' | 'pending' | 'refunded'
adoptionType: 'individual' | 'group'
groupMembers?: number[]
}
// 动物成长记录接口
export interface AnimalGrowthRecord {
id: number
animalId: number
recordDate: string
weight: number
height: number
condition: string
notes?: string
photos?: string[]
}
// 动物行为记录接口
export interface AnimalBehaviorRecord {
id: number
animalId: number
recordDate: string
behaviorType: string
duration: number
notes?: string
videoUrl?: string
}
// 动物喂养记录接口
export interface AnimalFeedingRecord {
id: number
animalId: number
feedingTime: string
foodType: string
quantity: number
feeder: string
notes?: string
}
// 动物活动记录接口
export interface AnimalActivityRecord {
id: number
animalId: number
activityType: string
startTime: string
endTime: string
duration: number
caloriesBurned?: number
notes?: string
}
// 动物导出参数接口
export interface AnimalExportParams {
format: 'csv' | 'excel'
fields: string[]
startDate?: string
endDate?: string
status?: AnimalStatus
animalType?: AnimalType
farmId?: number
}
// 动物认养统计接口
export interface AnimalAdoptionStats {
date: string
adoptionCount: number
totalRevenue: number
averageAdoptionDuration: number
popularAnimalTypes: string[]
peakAdoptionHours: number[]
}
// 动物健康预警接口
export interface AnimalHealthAlert {
id: number
animalId: number
animalName: string
alertType: 'vaccination' | 'medical' | 'nutrition' | 'behavior'
alertLevel: 'low' | 'medium' | 'high' | 'critical'
alertMessage: string
triggerTime: string
resolved: boolean
resolveTime?: string
resolveNotes?: string
}

View File

@@ -0,0 +1,313 @@
// 内容类型
export type ContentType = 'article' | 'post' | 'activity' | 'comment' | 'review'
// 内容状态类型
export type ContentStatus = 'published' | 'draft' | 'deleted' | 'archived'
// 内容审核状态类型
export type ContentAuditStatus = 'pending' | 'approved' | 'rejected'
// 内容分类类型
export type ContentCategory = 'travel' | 'animal' | 'entertainment' | 'food' | 'sports' | 'study' | 'other'
// 内容基本信息接口
export interface Content {
id: number
title: string
content: string
contentType: ContentType
authorId: number
authorName: string
publishTime: string
status: ContentStatus
viewCount: number
likeCount: number
commentCount: number
shareCount: number
auditStatus: ContentAuditStatus
auditRemark?: string
auditTime?: string
// 分类信息
category: ContentCategory
tags?: string[]
// 多媒体内容
images?: string[]
videos?: string[]
// 地理位置
location?: string
latitude?: number
longitude?: number
// 关联信息
relatedActivityId?: number
relatedAnimalId?: number
// 隐私设置
isPublic: boolean
allowComments: boolean
allowSharing: boolean
// 编辑信息
lastEditTime?: string
editCount: number
// SEO信息
metaDescription?: string
metaKeywords?: string[]
}
// 内容查询参数接口
export interface ContentQueryParams {
page: number
pageSize: number
keyword?: string
status?: ContentStatus
contentType?: ContentType
auditStatus?: ContentAuditStatus
authorId?: number
category?: ContentCategory
startTime?: string
endTime?: string
sortField?: string
sortOrder?: 'asc' | 'desc'
// 高级筛选
minViews?: number
minLikes?: number
minComments?: number
hasImages?: boolean
hasVideos?: boolean
// 地理位置筛选
location?: string
radius?: number
}
// 内容统计信息接口
export interface ContentStatistics {
totalContents: number
publishedContents: number
draftContents: number
deletedContents: number
totalViews: number
totalLikes: number
totalComments: number
totalShares: number
averageEngagementRate: number
contentTypeDistribution: Record<ContentType, number>
categoryDistribution: Record<ContentCategory, number>
dailyGrowth: number
topContents: Content[]
authorDistribution: {
totalAuthors: number
activeAuthors: number
newAuthorsThisMonth: number
topAuthors: Array<{
authorId: number
authorName: string
contentCount: number
totalViews: number
}>
}
}
// 内容审核记录接口
export interface ContentAuditRecord {
id: number
contentId: number
contentTitle: string
auditStatus: ContentAuditStatus
auditRemark?: string
auditor: string
auditTime: string
previousStatus?: ContentStatus
// 审核详情
auditDetails?: {
contentQuality: number
complianceScore: number
safetyCheck: boolean
spamCheck: boolean
}
// 违规信息
violationType?: string
violationLevel?: 'minor' | 'major' | 'critical'
penaltyApplied?: string
}
// 内容举报接口
export interface ContentReport {
id: number
contentId: number
contentTitle: string
reporterId: number
reporterName: string
reportReason: string
reportTime: string
reportStatus: 'pending' | 'processing' | 'resolved' | 'dismissed'
// 举报详情
reportDetails?: {
violationType: string
evidence?: string[]
additionalComments?: string
}
// 处理信息
handledBy?: string
handleTime?: string
handleResult?: string
penaltyApplied?: string
// 反馈信息
reporterNotified: boolean
feedbackProvided?: string
}
// 内容分类统计接口
export interface ContentCategoryStats {
category: ContentCategory
totalContents: number
publishedContents: number
averageViews: number
averageLikes: number
averageComments: number
engagementRate: number
topAuthors: Array<{
authorId: number
authorName: string
contentCount: number
}>
popularTags: string[]
growthTrend: number
}
// 内容作者统计接口
export interface ContentAuthorStats {
authorId: number
authorName: string
totalContents: number
publishedContents: number
totalViews: number
totalLikes: number
totalComments: number
averageEngagement: number
followerCount: number
joinDate: string
lastActivity: string
// 内容质量指标
contentQualityScore: number
complianceRate: number
// 认证信息
isVerified: boolean
verificationLevel: number
// 作者分类分布
categoryDistribution: Record<ContentCategory, number>
}
// 内容时间统计接口
export interface ContentTimeStats {
date: string
contentCount: number
viewCount: number
likeCount: number
commentCount: number
shareCount: number
newAuthors: number
peakHours: number[]
engagementRate: number
}
// 内容标签统计接口
export interface ContentTagStats {
tag: string
usageCount: number
averageViews: number
averageLikes: number
averageComments: number
relatedTags: string[]
popularityTrend: number
// 分类关联
associatedCategories: ContentCategory[]
// 作者使用情况
topAuthors: Array<{
authorId: number
authorName: string
usageCount: number
}>
}
// 内容推荐参数接口
export interface ContentRecommendationParams {
userId?: number
preferredCategories?: ContentCategory[]
preferredTags?: string[]
location?: string
timeRange?: [string, string]
contentType?: ContentType
minQualityScore?: number
excludeViewed?: boolean
limit?: number
}
// 内容导出参数接口
export interface ContentExportParams {
format: 'csv' | 'excel' | 'json'
fields: string[]
startDate?: string
endDate?: string
status?: ContentStatus
contentType?: ContentType
category?: ContentCategory
authorId?: number
}
// 内容审核参数接口
export interface ContentAuditParams {
contentId: number
auditStatus: ContentAuditStatus
auditRemark?: string
violationType?: string
violationLevel?: 'minor' | 'major' | 'critical'
penalty?: string
notifyAuthor: boolean
}
// 内容批量操作参数接口
export interface ContentBatchOperationParams {
contentIds: number[]
operation: 'publish' | 'draft' | 'delete' | 'approve' | 'reject'
auditRemark?: string
notifyAuthors: boolean
}
// 内容搜索参数接口
export interface ContentSearchParams {
query: string
filters?: {
contentType?: ContentType
category?: ContentCategory
status?: ContentStatus
authorId?: number
dateRange?: [string, string]
minViews?: number
minLikes?: number
}
sortBy?: 'relevance' | 'date' | 'views' | 'likes' | 'comments'
sortOrder?: 'asc' | 'desc'
page?: number
pageSize?: number
}
// 内容分析报告接口
export interface ContentAnalysisReport {
period: string
totalContents: number
totalViews: number
totalEngagement: number
averageQualityScore: number
topPerformingContents: Content[]
topAuthors: ContentAuthorStats[]
categoryPerformance: ContentCategoryStats[]
tagAnalysis: ContentTagStats[]
timeAnalysis: ContentTimeStats[]
recommendations: string[]
// 合规性指标
complianceRate: number
auditPassRate: number
reportResolutionRate: number
// 增长指标
growthRate: number
retentionRate: number
churnRate: number
}

View File

@@ -0,0 +1,163 @@
// 商家状态类型
export type MerchantStatus = 'active' | 'inactive' | 'banned'
// 商家审核状态类型
export type MerchantAuditStatus = 'pending' | 'approved' | 'rejected'
// 商家分类类型
export type MerchantCategory = 'flower' | 'farm' | 'activity' | 'travel' | 'other'
// 商家基本信息接口
export interface Merchant {
id: number
userId: number
shopName: string
businessLicense: string
contactPerson: string
contactPhone: string
contactEmail: string
address: string
description: string
status: MerchantStatus
auditStatus: MerchantAuditStatus
auditTime?: string
auditRemark?: string
registerTime: string
category: MerchantCategory
// 统计信息
serviceScore: number
orderCount: number
totalRevenue: number
// 扩展信息
businessHours?: string
deliveryRange?: string
minimumOrder?: number
// 资质文件
licenseImage?: string
idCardImage?: string
// 银行信息
bankAccount?: string
bankName?: string
accountHolder?: string
}
// 商家查询参数接口
export interface MerchantQueryParams {
page: number
pageSize: number
keyword?: string
status?: MerchantStatus
auditStatus?: MerchantAuditStatus
category?: MerchantCategory
startDate?: string
endDate?: string
sortField?: string
sortOrder?: 'asc' | 'desc'
}
// 商家审核参数接口
export interface MerchantAuditParams {
merchantId: number
auditStatus: MerchantAuditStatus
auditRemark?: string
}
// 商家统计信息接口
export interface MerchantStatistics {
totalMerchants: number
activeMerchants: number
pendingAudit: number
rejected: number
totalRevenue: number
averageScore: number
categoryDistribution: Record<MerchantCategory, number>
monthlyGrowth: number
dailyActiveMerchants: number
topMerchants: Merchant[]
}
// 商家收入统计接口
export interface MerchantRevenueStats {
date: string
revenue: number
orderCount: number
averageOrderValue: number
}
// 商家服务评分统计接口
export interface MerchantScoreStats {
date: string
averageScore: number
reviewCount: number
fiveStarCount: number
oneStarCount: number
}
// 商家分类统计接口
export interface MerchantCategoryStats {
category: MerchantCategory
count: number
totalRevenue: number
averageScore: number
}
// 商家审核记录接口
export interface MerchantAuditRecord {
id: number
merchantId: number
shopName: string
auditStatus: MerchantAuditStatus
auditRemark?: string
auditor: string
auditTime: string
previousStatus?: MerchantAuditStatus
}
// 商家操作日志接口
export interface MerchantOperationLog {
id: number
merchantId: number
shopName: string
operationType: string
operationDetail: string
operationTime: string
operator: string
result: 'success' | 'failure'
}
// 商家资质文件接口
export interface MerchantQualification {
id: number
merchantId: number
fileType: 'license' | 'idCard' | 'bank' | 'other'
fileName: string
fileUrl: string
uploadTime: string
status: 'verified' | 'pending' | 'rejected'
verifyRemark?: string
verifyTime?: string
}
// 商家银行信息接口
export interface MerchantBankInfo {
id: number
merchantId: number
bankName: string
bankAccount: string
accountHolder: string
branchName?: string
status: 'verified' | 'pending' | 'rejected'
verifyRemark?: string
verifyTime?: string
}
// 商家导出参数接口
export interface MerchantExportParams {
format: 'csv' | 'excel'
fields: string[]
startDate?: string
endDate?: string
status?: MerchantStatus
auditStatus?: MerchantAuditStatus
category?: MerchantCategory
}

View File

@@ -0,0 +1,209 @@
// 订单状态类型
export type OrderStatus = 'pending' | 'paid' | 'shipped' | 'completed' | 'cancelled' | 'refunded'
// 订单类型
export type OrderType = 'flower' | 'animal' | 'activity' | 'travel' | 'other'
// 支付方式类型
export type PaymentMethod = 'wechat' | 'alipay' | 'bank' | 'balance'
// 支付状态类型
export type PaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded'
// 订单基本信息接口
export interface Order {
id: number
orderNo: string
userId: number
username: string
merchantId: number
shopName: string
orderType: OrderType
productName: string
quantity: number
unitPrice: number
totalAmount: number
status: OrderStatus
createTime: string
payTime?: string
completeTime?: string
cancelTime?: string
// 支付信息
paymentMethod?: PaymentMethod
paymentStatus?: PaymentStatus
transactionNo?: string
// 收货信息
recipientName: string
recipientPhone: string
deliveryAddress: string
deliveryTime?: string
// 附加信息
message?: string
remark?: string
// 评价信息
rating?: number
review?: string
reviewTime?: string
// 退款信息
refundAmount?: number
refundReason?: string
refundTime?: string
// 物流信息
trackingNo?: string
shippingCompany?: string
}
// 订单查询参数接口
export interface OrderQueryParams {
page: number
pageSize: number
keyword?: string
status?: OrderStatus
orderType?: OrderType
merchantId?: number
userId?: number
startDate?: string
endDate?: string
sortField?: string
sortOrder?: 'asc' | 'desc'
}
// 订单统计信息接口
export interface OrderStatistics {
totalOrders: number
pendingOrders: number
completedOrders: number
cancelledOrders: number
totalRevenue: number
averageOrderValue: number
dailyOrderCount: number
orderTypeDistribution: Record<OrderType, number>
statusDistribution: Record<OrderStatus, number>
monthlyGrowth: number
topProducts: string[]
revenueTrend: Array<{
date: string
revenue: number
orderCount: number
}>
}
// 订单收入统计接口
export interface OrderRevenueStats {
date: string
revenue: number
orderCount: number
averageOrderValue: number
refundAmount: number
netRevenue: number
}
// 订单类型统计接口
export interface OrderTypeStats {
orderType: OrderType
count: number
totalRevenue: number
averageOrderValue: number
completionRate: number
}
// 订单状态统计接口
export interface OrderStatusStats {
status: OrderStatus
count: number
percentage: number
}
// 订单支付统计接口
export interface OrderPaymentStats {
paymentMethod: PaymentMethod
count: number
totalAmount: number
successRate: number
}
// 订单退款统计接口
export interface OrderRefundStats {
date: string
refundCount: number
refundAmount: number
refundRate: number
mainReasons: string[]
}
// 订单操作记录接口
export interface OrderOperationLog {
id: number
orderId: number
orderNo: string
operationType: string
operationDetail: string
operationTime: string
operator: string
result: 'success' | 'failure'
remark?: string
}
// 订单导出参数接口
export interface OrderExportParams {
format: 'csv' | 'excel'
fields: string[]
startDate?: string
endDate?: string
status?: OrderStatus
orderType?: OrderType
merchantId?: number
}
// 订单退款申请接口
export interface OrderRefundApplication {
id: number
orderId: number
orderNo: string
userId: number
username: string
refundAmount: number
refundReason: string
status: 'pending' | 'approved' | 'rejected' | 'completed'
applyTime: string
processTime?: string
processRemark?: string
processor?: string
}
// 订单评价接口
export interface OrderReview {
id: number
orderId: number
userId: number
username: string
merchantId: number
shopName: string
rating: number
review: string
reviewTime: string
isAnonymous: boolean
reply?: string
replyTime?: string
helpfulCount: number
reportCount: number
status: 'normal' | 'hidden' | 'deleted'
}
// 订单物流信息接口
export interface OrderShippingInfo {
id: number
orderId: number
orderNo: string
trackingNo: string
shippingCompany: string
shippingTime: string
estimatedDelivery: string
actualDelivery?: string
shippingStatus: 'pending' | 'shipped' | 'in_transit' | 'delivered' | 'failed'
shippingCost: number
recipientName: string
recipientPhone: string
deliveryAddress: string
deliveryNotes?: string
}

View File

@@ -0,0 +1,402 @@
// 权限状态类型
export type PermissionStatus = 'active' | 'inactive' | 'deprecated'
// 权限类型
export type PermissionType = 'module' | 'menu' | 'button' | 'action' | 'api'
// 角色类型
export type RoleType = 'system' | 'custom' | 'guest'
// 角色状态类型
export type RoleStatus = 'active' | 'inactive' | 'pending'
// 权限基本信息接口
export interface Permission {
id: number
name: string
code: string
description: string
type: PermissionType
parentId: number | null
level: number
status: PermissionStatus
createTime: string
updateTime: string
// 扩展信息
icon?: string
route?: string
component?: string
sortOrder?: number
// 子权限
children?: Permission[]
// 权限控制
isPublic?: boolean
requiresAuth?: boolean
// 元数据
metadata?: Record<string, any>
}
// 角色基本信息接口
export interface Role {
id: number
name: string
code: string
description: string
type: RoleType
status: RoleStatus
permissionIds: number[]
userCount: number
createTime: string
updateTime: string
// 扩展信息
permissions?: Permission[]
// 角色属性
isDefault?: boolean
isSystem?: boolean
// 访问控制
maxUsers?: number
expirationDate?: string
// 审批流程
requiresApproval?: boolean
approvalWorkflow?: string
// 元数据
metadata?: Record<string, any>
}
// 用户角色关联接口
export interface UserRole {
id: number
userId: number
roleId: number
assignTime: string
assignBy: string
// 扩展信息
expirationTime?: string
// 审批信息
approvedBy?: string
approveTime?: string
approvalStatus?: 'pending' | 'approved' | 'rejected'
// 权限委托
delegatedFrom?: number
delegationReason?: string
// 元数据
metadata?: Record<string, any>
}
// 权限查询参数接口
export interface PermissionQueryParams {
page: number
pageSize: number
keyword?: string
status?: PermissionStatus
type?: PermissionType
parentId?: number | null
level?: number
// 高级筛选
isPublic?: boolean
requiresAuth?: boolean
// 排序
sortField?: string
sortOrder?: 'asc' | 'desc'
}
// 角色查询参数接口
export interface RoleQueryParams {
page: number
pageSize: number
keyword?: string
status?: RoleStatus
type?: RoleType
// 高级筛选
isDefault?: boolean
isSystem?: boolean
// 用户数量范围
minUsers?: number
maxUsers?: number
// 排序
sortField?: string
sortOrder?: 'asc' | 'desc'
}
// 权限统计信息接口
export interface PermissionStatistics {
totalPermissions: number
activePermissions: number
inactivePermissions: number
totalRoles: number
activeRoles: number
inactiveRoles: number
totalUsersWithRoles: number
permissionUsage: Record<string, number>
roleDistribution: Record<string, number>
userPermissionStats: {
averagePermissionsPerUser: number
maxPermissionsPerUser: number
minPermissionsPerUser: number
usersWithExcessivePermissions: number
// 权限分布
permissionDistribution?: Record<string, number>
// 风险指标
permissionConflictRate?: number
permissionRedundancyRate?: number
}
auditLogs: {
totalLogs: number
permissionChanges: number
roleChanges: number
userRoleChanges: number
// 安全事件
securityIncidents?: number
complianceViolations?: number
}
// 性能指标
permissionCheckPerformance?: number
roleResolutionTime?: number
// 合规性指标
complianceScore?: number
auditPassRate?: number
}
// 审计日志接口
export interface AuditLog {
id: number
action: string
targetType: string
targetId: number
targetName: string
performerId: number
performerName: string
details: string
ipAddress: string
userAgent: string
timestamp: string
status: 'success' | 'failure' | 'warning'
// 扩展信息
resourceType?: string
resourceId?: number
// 变更详情
oldValue?: any
newValue?: any
// 安全信息
securityLevel?: 'low' | 'medium' | 'high' | 'critical'
riskScore?: number
// 地理位置
location?: string
deviceInfo?: string
// 调查信息
investigationStatus?: 'open' | 'in_progress' | 'resolved' | 'closed'
investigationNotes?: string
}
// 权限分配请求接口
export interface PermissionAssignmentRequest {
userId: number
permissionIds: number[]
assignBy: string
// 有效期
expirationTime?: string
// 审批信息
requiresApproval?: boolean
approvalWorkflow?: string
// 委托信息
delegatedFrom?: number
delegationReason?: string
}
// 角色分配请求接口
export interface RoleAssignmentRequest {
userId: number
roleId: number
assignBy: string
// 有效期
expirationTime?: string
// 审批信息
requiresApproval?: boolean
approvalWorkflow?: string
// 委托信息
delegatedFrom?: number
delegationReason?: string
}
// 权限检查请求接口
export interface PermissionCheckRequest {
userId: number
permissionCode: string
// 上下文信息
resourceType?: string
resourceId?: number
// 环境信息
ipAddress?: string
userAgent?: string
// 安全验证
sessionId?: string
token?: string
}
// 权限检查响应接口
export interface PermissionCheckResponse {
hasPermission: boolean
// 详细权限信息
permission?: Permission
// 权限来源
grantedBy: 'role' | 'direct' | 'inheritance' | 'delegation'
role?: Role
// 有效期
validUntil?: string
// 限制信息
restrictions?: string[]
// 审计信息
auditId?: number
}
// 权限继承配置接口
export interface PermissionInheritanceConfig {
enabled: boolean
inheritanceRules: Array<{
sourceType: string
sourceId: number
targetType: string
targetId: number
inheritanceType: 'full' | 'partial' | 'conditional'
conditions?: Record<string, any>
// 有效期
validFrom?: string
validUntil?: string
// 优先级
priority: number
}>
// 冲突解决策略
conflictResolution: 'deny' | 'allow' | 'highest_priority' | 'most_specific'
// 性能优化
cachingEnabled: boolean
cacheTTL: number
// 审计配置
auditInheritance: boolean
}
// 权限委托配置接口
export interface PermissionDelegationConfig {
enabled: boolean
maxDelegationDepth: number
delegationTimeLimit: number
allowedDelegators: number[]
restrictedPermissions: number[]
// 审批流程
requiresApproval: boolean
approvalWorkflow: string
// 审计要求
auditDelegation: boolean
// 通知设置
notifyOriginalOwner: boolean
notifyDelegator: boolean
notifyDelegatee: boolean
}
// 权限审计配置接口
export interface PermissionAuditConfig {
enabled: boolean
auditLevel: 'minimal' | 'standard' | 'detailed' | 'comprehensive'
retentionPeriod: number
// 审计事件
auditedActions: string[]
// 敏感操作
sensitiveOperations: Array<{
action: string
sensitivityLevel: 'low' | 'medium' | 'high' | 'critical'
additionalAuditing: boolean
}>
// 实时监控
realTimeMonitoring: boolean
alertThresholds: Record<string, number>
// 合规要求
complianceRequirements: string[]
regulatoryStandards: string[]
}
// 权限导出参数接口
export interface PermissionExportParams {
format: 'csv' | 'excel' | 'json'
include: Array<'permissions' | 'roles' | 'user_roles' | 'audit_logs'>
filters?: {
status?: PermissionStatus | RoleStatus
type?: PermissionType | RoleType
dateRange?: [string, string]
}
// 数据范围
dataScope: 'all' | 'active' | 'recent'
// 安全选项
encryptData: boolean
passwordProtect: boolean
// 元数据
includeMetadata: boolean
}
// 权限导入参数接口
export interface PermissionImportParams {
file: File
format: 'csv' | 'excel' | 'json'
importType: 'permissions' | 'roles' | 'assignments'
// 冲突处理
conflictResolution: 'skip' | 'overwrite' | 'merge' | 'rename'
// 验证选项
validateData: boolean
dryRun: boolean
// 通知设置
notifyOnCompletion: boolean
notifyOnError: boolean
// 审计配置
auditImport: boolean
}
// 权限同步配置接口
export interface PermissionSyncConfig {
enabled: boolean
syncFrequency: number
syncDirection: 'bidirectional' | 'source_to_target' | 'target_to_source'
// 数据源配置
sourceSystem: string
sourceConfig: Record<string, any>
targetSystem: string
targetConfig: Record<string, any>
// 映射规则
fieldMappings: Record<string, string>
valueTransformations: Record<string, (value: any) => any>
// 冲突解决
conflictResolution: 'source_wins' | 'target_wins' | 'manual'
// 性能优化
batchSize: number
parallelProcessing: boolean
// 监控配置
monitorSync: boolean
alertOnFailure: boolean
}
// 权限健康检查接口
export interface PermissionHealthCheck {
timestamp: string
status: 'healthy' | 'degraded' | 'unhealthy'
// 组件状态
components: Array<{
name: string
status: 'up' | 'down' | 'degraded'
responseTime: number
errorRate: number
}>
// 性能指标
performanceMetrics: {
permissionCheckTime: number
roleResolutionTime: number
cacheHitRate: number
memoryUsage: number
}
// 安全指标
securityMetrics: {
failedAttempts: number
suspiciousActivities: number
complianceViolations: number
}
// 建议措施
recommendations: string[]
// 历史趋势
historicalTrend: Record<string, number>
}

View File

@@ -0,0 +1,488 @@
// 时间范围类型
export type TimeRange = 'today' | 'yesterday' | 'week' | 'month' | 'quarter' | 'year'
// 统计对比接口
export interface StatsComparison {
current: number
previous: number
growthRate: number
trend: 'up' | 'down' | 'stable'
timeRange: TimeRange
// 详细信息
unit?: string
significance?: 'high' | 'medium' | 'low'
confidence?: number
}
// 系统统计接口
export interface SystemStats {
totalUsers: number
activeUsers: number
newUsersToday: number
newUsersThisWeek: number
newUsersThisMonth: number
totalActivities: number
activeActivities: number
completedActivities: number
totalAnimals: number
adoptedAnimals: number
availableAnimals: number
totalOrders: number
pendingOrders: number
completedOrders: number
cancelledOrders: number
totalRevenue: number
todayRevenue: number
weekRevenue: number
monthRevenue: number
systemUptime: string
averageResponseTime: number
errorRate: number
serverLoad: number
// 扩展信息
databaseSize?: number
cacheHitRate?: number
apiCalls?: number
concurrentUsers?: number
// 性能指标
pageLoadTime?: number
apiResponseTime?: number
databaseQueryTime?: number
// 资源使用
memoryUsage?: number
cpuUsage?: number
diskUsage?: number
networkUsage?: number
}
// 用户统计接口
export interface UserStats {
totalUsers: number
activeUsers: number
newUsers: {
today: number
yesterday: number
thisWeek: number
lastWeek: number
thisMonth: number
lastMonth: number
}
userGrowthRate: number
userRetentionRate: number
userChurnRate: number
userDemographics: {
ageDistribution: Record<string, number>
genderDistribution: Record<string, number>
locationDistribution: Record<string, number>
// 扩展信息
educationDistribution?: Record<string, number>
occupationDistribution?: Record<string, number>
incomeDistribution?: Record<string, number>
}
userActivity: {
dailyActiveUsers: number
weeklyActiveUsers: number
monthlyActiveUsers: number
averageSessionDuration: number
averageSessionsPerUser: number
// 行为指标
pagesPerSession?: number
bounceRate?: number
returnRate?: number
}
userBehavior: {
averageActivitiesPerUser: number
averageAnimalsAdopted: number
averageOrdersPerUser: number
conversionRate: number
engagementRate: number
// 参与度指标
contentCreationRate?: number
socialInteractionRate?: number
referralRate?: number
}
// 用户生命周期
userLifetimeValue: number
userAcquisitionCost: number
userRetentionCost: number
// 用户满意度
userSatisfactionScore?: number
netPromoterScore?: number
customerEffortScore?: number
}
// 活动统计接口
export interface ActivityStats {
totalActivities: number
activeActivities: number
completedActivities: number
cancelledActivities: number
pendingActivities: number
activityGrowthRate: number
participationStats: {
totalParticipants: number
averageParticipantsPerActivity: number
repeatParticipants: number
participationRate: number
// 参与度详情
maleParticipants?: number
femaleParticipants?: number
ageDistribution?: Record<string, number>
locationDistribution?: Record<string, number>
}
revenueStats: {
totalRevenue: number
averageRevenuePerActivity: number
revenueGrowthRate: number
refundRate: number
// 收入详情
ticketRevenue?: number
serviceRevenue?: number
productRevenue?: number
commissionRevenue?: number
}
categoryDistribution: Record<string, number>
locationDistribution: Record<string, number>
timeDistribution: Record<string, number>
organizerPerformance: {
topOrganizers: Array<{
organizerId: number
name: string
activityCount: number
revenue: number
rating?: number
completionRate?: number
}>
averageRating: number
completionRate: number
cancellationRate: number
// 组织者指标
organizerSatisfaction?: number
organizerRetention?: number
organizerGrowth?: number
}
// 活动质量指标
activityQualityScore?: number
participantSatisfaction?: number
safetyIncidents?: number
// 营销效果
marketingROI?: number
acquisitionCost?: number
conversionRate?: number
}
// 动物统计接口
export interface AnimalStats {
totalAnimals: number
adoptedAnimals: number
availableAnimals: number
adoptionRate: number
adoptionGrowthRate: number
animalTypes: Record<string, number>
adoptionStats: {
totalAdoptions: number
averageAdoptionTime: number
repeatAdoptions: number
adoptionSuccessRate: number
// 收养详情
adoptionReasons?: Record<string, number>
adoptionDuration?: Record<string, number>
adoptionFrequency?: number
}
careStats: {
totalCareRecords: number
averageCareFrequency: number
medicalCheckups: number
vaccinationRate: number
// 护理详情
healthIssues?: number
treatmentCost?: number
careQuality?: number
}
locationDistribution: Record<string, number>
popularAnimals: Array<{
animalId: number
name: string
type: string
adoptionCount: number
viewCount: number
likeCount?: number
shareCount?: number
}>
revenueStats: {
totalAdoptionRevenue: number
averageAdoptionFee: number
careProductRevenue: number
totalRevenue: number
// 收入详情
donationRevenue?: number
sponsorshipRevenue?: number
merchandiseRevenue?: number
}
// 动物福利指标
animalWelfareScore?: number
healthIndex?: number
happinessIndex?: number
// 可持续发展
environmentalImpact?: number
communityEngagement?: number
educationalValue?: number
}
// 订单统计接口
export interface OrderStats {
totalOrders: number
pendingOrders: number
completedOrders: number
cancelledOrders: number
orderGrowthRate: number
revenueStats: {
totalRevenue: number
averageOrderValue: number
revenueGrowthRate: number
refundAmount: number
netRevenue: number
// 收入构成
productRevenue?: number
serviceRevenue?: number
taxAmount?: number
shippingCost?: number
}
orderTypeDistribution: Record<string, number>
paymentStats: {
paymentMethods: Record<string, number>
paymentSuccessRate: number
averagePaymentTime: number
refundRate: number
// 支付详情
fraudRate?: number
chargebackRate?: number
paymentCost?: number
}
customerStats: {
repeatCustomers: number
averageOrdersPerCustomer: number
customerLifetimeValue: number
acquisitionCost: number
// 客户行为
purchaseFrequency?: number
basketSize?: number
crossSellRate?: number
}
timeDistribution: Record<string, number>
locationDistribution: Record<string, number>
// 订单质量指标
orderAccuracy?: number
deliveryPerformance?: number
customerSatisfaction?: number
// 运营效率
fulfillmentTime?: number
inventoryTurnover?: number
returnRate?: number
}
// 收入统计接口
export interface RevenueStats {
totalRevenue: number
revenueGrowthRate: number
dailyRevenue: number
weeklyRevenue: number
monthlyRevenue: number
revenueSources: Record<string, number>
revenueTrends: {
last7Days: number[]
last30Days: number[]
// 扩展趋势
last90Days?: number[]
last365Days?: number[]
seasonalPatterns?: Record<string, number>
}
customerMetrics: {
averageRevenuePerUser: number
averageOrderValue: number
purchaseFrequency: number
customerLifetimeValue: number
// 客户价值
retentionValue?: number
acquisitionValue?: number
referralValue?: number
}
geographicDistribution: Record<string, number>
productPerformance: {
topProducts: Array<{
productId: number
name: string
revenue: number
orders: number
profit?: number
margin?: number
}>
averageProfitMargin: number
returnOnInvestment: number
// 产品指标
productPopularity?: number
productSatisfaction?: number
productRetention?: number
}
// 财务指标
grossProfit?: number
operatingProfit?: number
netProfit?: number
cashFlow?: number
// 预算与实际
budgetVariance?: number
forecastAccuracy?: number
// 税务信息
taxLiability?: number
taxCompliance?: number
}
// 仪表板指标接口
export interface DashboardMetrics {
keyMetrics: {
totalUsers: number
activeUsers: number
totalActivities: number
totalOrders: number
totalRevenue: number
adoptionRate: number
// 核心指标
conversionRate?: number
engagementRate?: number
retentionRate?: number
}
growthMetrics: {
userGrowth: number
activityGrowth: number
orderGrowth: number
revenueGrowth: number
// 增长详情
organicGrowth?: number
paidGrowth?: number
referralGrowth?: number
}
performanceMetrics: {
systemUptime: string
averageResponseTime: number
errorRate: number
conversionRate: number
// 性能详情
pageSpeed?: number
apiAvailability?: number
databasePerformance?: number
}
recentActivities: Array<{
type: string
action: string
count: number
time: string
// 活动详情
details?: string
priority?: 'high' | 'medium' | 'low'
}>
alerts: Array<{
level: 'critical' | 'warning' | 'info' | 'success'
message: string
time: string
// 警报详情
source?: string
affectedComponents?: string[]
resolution?: string
}>
// 趋势数据
trends: {
userTrend: number[]
revenueTrend: number[]
activityTrend: number[]
orderTrend: number[]
}
// 预测数据
forecasts: {
userForecast: number[]
revenueForecast: number[]
activityForecast: number[]
orderForecast: number[]
}
// 比较数据
comparisons: {
vsPreviousPeriod: Record<string, StatsComparison>
vsIndustryAverage: Record<string, StatsComparison>
vsCompetitors: Record<string, StatsComparison>
}
}
// 统计导出参数接口
export interface StatsExportParams {
type: 'users' | 'activities' | 'animals' | 'orders' | 'revenue' | 'system'
format: 'csv' | 'excel' | 'json' | 'pdf'
timeRange: TimeRange
fields: string[]
filters?: Record<string, any>
sortBy?: string
sortOrder?: 'asc' | 'desc'
}
// 统计查询参数接口
export interface StatsQueryParams {
metric: string
dimensions?: string[]
filters?: Record<string, any>
timeRange: TimeRange
compareWith?: TimeRange
granularity?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
limit?: number
offset?: number
}
// 统计响应接口
export interface StatsResponse {
data: any[]
total: number
timeRange: TimeRange
generatedAt: string
metadata: {
query: StatsQueryParams
processingTime: number
dataFreshness: string
confidence: number
}
}
// 统计预警配置接口
export interface StatsAlertConfig {
metric: string
threshold: number
condition: 'above' | 'below' | 'equal' | 'change'
timeRange: TimeRange
severity: 'critical' | 'warning' | 'info'
notificationChannels: string[]
recipients: string[]
cooldownPeriod: number
// 高级配置
aggregation?: string
windowSize?: number
minimumSamples?: number
}
// 统计报告接口
export interface StatsReport {
id: string
title: string
type: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly' | 'custom'
period: string
metrics: Record<string, any>
insights: string[]
recommendations: string[]
generatedAt: string
// 报告详情
executiveSummary?: string
detailedAnalysis?: string
visualizations?: any[]
attachments?: string[]
// 分发信息
recipients?: string[]
deliveryStatus?: 'pending' | 'sent' | 'delivered' | 'failed'
// 安全信息
accessLevel?: 'public' | 'internal' | 'confidential'
retentionPolicy?: string
}

View File

@@ -0,0 +1,130 @@
// 用户状态类型
export type UserStatus = 'active' | 'inactive' | 'banned'
// 用户类型
export type UserType = 'normal' | 'merchant' | 'admin'
// 用户基本信息接口
export interface User {
id: number
username: string
nickname: string
avatar: string
phone: string
email: string
status: UserStatus
registerTime: string
lastLoginTime: string
userType: UserType
// 扩展字段
gender?: 'male' | 'female' | 'unknown'
birthday?: string
interests?: string[]
location?: string
// 商家特有字段
shopName?: string
businessLicense?: string
// 统计信息
travelCount?: number
animalAdoptionCount?: number
orderCount?: number
// 安全相关
isVerified?: boolean
verificationLevel?: number
}
// 用户查询参数接口
export interface UserQueryParams {
page: number
pageSize: number
keyword?: string
status?: UserStatus
userType?: UserType
startDate?: string
endDate?: string
sortField?: string
sortOrder?: 'asc' | 'desc'
}
// 用户统计信息接口
export interface UserStatistics {
totalUsers: number
activeUsers: number
newUsersToday: number
newUsersThisWeek: number
newUsersThisMonth: number
userGrowthRate: number
userTypeDistribution: {
normal: number
merchant: number
admin: number
}
statusDistribution: {
active: number
inactive: number
banned: number
}
dailyActiveUsers: Array<{
date: string
count: number
}>
}
// 用户操作记录接口
export interface UserOperationLog {
id: number
userId: number
username: string
operationType: string
operationDetail: string
operationTime: string
ipAddress: string
userAgent: string
result: 'success' | 'failure'
}
// 用户注册统计接口
export interface UserRegistrationStats {
date: string
count: number
}
// 用户登录统计接口
export interface UserLoginStats {
date: string
count: number
}
// 用户行为统计接口
export interface UserBehaviorStats {
date: string
travelPublishCount: number
animalAdoptionCount: number
orderCount: number
messageCount: number
shareCount: number
}
// 用户导出参数接口
export interface UserExportParams {
format: 'csv' | 'excel'
fields: string[]
startDate?: string
endDate?: string
status?: UserStatus
userType?: UserType
}
// 管理员信息接口
export interface Admin {
id: number
username: string
email: string
nickname: string
avatar: string
role: string
status: number
lastLoginTime?: string
createdAt: string
updatedAt: string
}

View File

@@ -11,10 +11,10 @@ export default defineConfig({
},
},
server: {
port: 3001,
port: 3150,
proxy: {
'/api': {
target: 'http://localhost:3000',
target: 'http://localhost:3100',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api/v1')
}

View File

@@ -1,14 +1,14 @@
# 服务器配置
NODE_ENV=development
PORT=3001
PORT=3100
HOST=0.0.0.0
ENABLE_SWAGGER=true
# MySQL数据库配置
DB_HOST=192.168.0.240
DB_PORT=3306
DB_HOST=129.211.213.226
DB_PORT=9527
DB_USER=root
DB_PASSWORD=aiot$Aiot123
DB_PASSWORD=aiotAiot123!
DB_NAME=jiebandata
# 测试环境数据库

View File

@@ -5,7 +5,7 @@ require('dotenv').config({ path: path.join(__dirname, '../../.env') })
const config = {
// 开发环境
development: {
port: process.env.PORT || 3000,
port: process.env.PORT || 3100,
mongodb: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/jiebanke_dev',
options: {
@@ -35,7 +35,7 @@ const config = {
// 测试环境
test: {
port: process.env.PORT || 3001,
port: process.env.PORT || 3100,
mongodb: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/jiebanke_test',
options: {
@@ -56,7 +56,7 @@ const config = {
// 生产环境
production: {
port: process.env.PORT || 3000,
port: process.env.PORT || 3100,
mongodb: {
uri: process.env.MONGODB_URI,
options: {

View File

@@ -12,7 +12,7 @@
"amqplib": "^0.10.9",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"dotenv": "^16.6.1",
"express": "^4.18.2",
"express-mongo-sanitize": "^2.2.0",
"express-rate-limit": "^7.1.5",
@@ -1674,8 +1674,9 @@
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
"resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
@@ -2340,8 +2341,9 @@
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
@@ -4179,8 +4181,9 @@
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",

View File

@@ -22,7 +22,7 @@
"amqplib": "^0.10.9",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"dotenv": "^16.6.1",
"express": "^4.18.2",
"express-mongo-sanitize": "^2.2.0",
"express-rate-limit": "^7.1.5",

View File

@@ -1,41 +1,45 @@
const express = require('express')
const cors = require('cors')
const helmet = require('helmet')
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')
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
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应用...')
console.log('🔧 初始化Express应用...');
const { globalErrorHandler, notFound } = require('./utils/errors')
const { globalErrorHandler, notFound } = require('./utils/errors');
// 路由导入
const authRoutes = require('./routes/auth')
// 其他路由将在这里导入
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/user');
const travelRoutes = require('./routes/travel');
const animalRoutes = require('./routes/animal');
const orderRoutes = require('./routes/order');
const adminRoutes = require('./routes/admin'); // 新增管理员路由
const app = express()
const app = express();
console.log('✅ Express应用初始化完成')
console.log('✅ Express应用初始化完成');
// 安全中间件
app.use(helmet())
app.use(helmet());
// CORS配置
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? ['https://your-domain.com']
: ['http://localhost:9000', 'http://localhost:3000'],
: ['http://localhost:9000', 'http://localhost:3000', 'http://localhost:3100', 'http://localhost:3150'],
credentials: true
}))
}));
// 请求日志
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'))
app.use(morgan('dev'));
} else {
app.use(morgan('combined'))
app.use(morgan('combined'));
}
// 请求频率限制
@@ -48,15 +52,15 @@ const limiter = rateLimit({
message: '请求过于频繁,请稍后再试',
timestamp: new Date().toISOString()
}
})
app.use('/api', limiter)
});
app.use('/api', limiter);
// 请求体解析
app.use(express.json({ limit: '10kb' }))
app.use(express.urlencoded({ extended: true, limit: '10kb' }))
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// 数据清洗
app.use(xss()) // 防止XSS攻击
app.use(xss()); // 防止XSS攻击
app.use(hpp({ // 防止参数污染
whitelist: [
'page',
@@ -67,15 +71,15 @@ app.use(hpp({ // 防止参数污染
'rating',
'distance'
]
}))
}));
// 静态文件服务
app.use('/uploads', express.static('uploads'))
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.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
console.log('📚 Swagger文档已启用: http://localhost:3100/api-docs');
}
// 健康检查路由
@@ -85,24 +89,24 @@ app.get('/health', (req, res) => {
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV || 'development'
})
})
});
});
// API路由
app.use('/api/v1/auth', authRoutes)
// 其他API路由将在这里添加
// app.use('/api/v1/users', userRoutes)
// app.use('/api/v1/travel', travelRoutes)
// app.use('/api/v1/animals', animalRoutes)
// app.use('/api/v1/flowers', flowerRoutes)
// app.use('/api/v1/orders', orderRoutes)
app.use('/api/v1/auth', authRoutes);
app.use('/api/v1/users', userRoutes);
app.use('/api/v1/travel', travelRoutes);
app.use('/api/v1/animals', animalRoutes);
app.use('/api/v1/orders', orderRoutes);
// 管理员路由
app.use('/api/v1/admin', adminRoutes);
// 404处理
app.use('*', notFound)
app.use('*', notFound);
// 全局错误处理
app.use(globalErrorHandler)
app.use(globalErrorHandler);
console.log('✅ 应用配置完成')
console.log('✅ 应用配置完成');
module.exports = app
module.exports = app;

View File

@@ -2,18 +2,21 @@ const mysql = require('mysql2/promise');
// 数据库配置
const dbConfig = {
host: process.env.DB_HOST || '192.168.0.240',
port: process.env.DB_PORT || 3306,
host: process.env.DB_HOST || '129.211.213.226',
port: process.env.DB_PORT || 9527,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'aiot$Aiot123',
password: process.env.DB_PASSWORD || 'aiotAiot123!',
database: process.env.DB_NAME || 'jiebandata',
connectionLimit: 10,
// 移除无效的配置选项 acquireTimeout 和 timeout
charset: 'utf8mb4',
timezone: '+08:00',
// 连接池配置
waitForConnections: true,
queueLimit: 0
queueLimit: 0,
// 超时配置
connectTimeout: 10000, // 10秒连接超时
acquireTimeout: 10000, // 10秒获取连接超时
timeout: 10000 // 10秒查询超时
};
// 创建连接池
@@ -33,41 +36,105 @@ async function testConnection() {
}
// 执行查询
async function query(sql, params = []) {
const query = async (sql, params = []) => {
let connection;
try {
const [rows] = await pool.execute(sql, params);
return rows;
connection = await pool.getConnection();
const [results] = await connection.execute(sql, params);
connection.release();
return results;
} catch (error) {
console.error('数据库查询错误:', error.message);
if (connection) {
connection.release();
}
throw error;
}
}
};
// 执行事务
async function transaction(callback) {
const connection = await pool.getConnection();
// 事务处理
const transaction = async (callback) => {
let connection;
try {
connection = await pool.getConnection();
await connection.beginTransaction();
const result = await callback(connection);
await connection.commit();
connection.release();
return result;
} catch (error) {
await connection.rollback();
if (connection) {
await connection.rollback();
connection.release();
}
throw error;
} finally {
connection.release();
}
}
};
// 关闭连接池
async function closePool() {
await pool.end();
try {
await pool.end();
console.log('✅ MySQL连接池已关闭');
} catch (error) {
console.error('❌ 关闭MySQL连接池时出错:', error.message);
}
}
// 管理员数据库操作
const adminDB = {
// 根据用户名查找管理员
findByUsername: async (username) => {
const sql = 'SELECT * FROM admins WHERE username = ?';
const results = await query(sql, [username]);
return results[0];
},
// 根据ID查找管理员
findById: async (id) => {
const sql = 'SELECT * FROM admins WHERE id = ?';
const results = await query(sql, [id]);
return results[0];
},
// 创建管理员
create: async (adminData) => {
const keys = Object.keys(adminData);
const values = Object.values(adminData);
const placeholders = keys.map(() => '?').join(', ');
const sql = `INSERT INTO admins (${keys.join(', ')}) VALUES (${placeholders})`;
const result = await query(sql, values);
return result.insertId;
},
// 更新管理员最后登录时间
updateLastLogin: async (id) => {
const sql = 'UPDATE admins SET last_login = CURRENT_TIMESTAMP WHERE id = ?';
await query(sql, [id]);
},
// 更新管理员信息
update: async (id, adminData) => {
const keys = Object.keys(adminData);
const values = Object.values(adminData);
const setClause = keys.map(key => `${key} = ?`).join(', ');
const sql = `UPDATE admins SET ${setClause} WHERE id = ?`;
await query(sql, [...values, id]);
},
// 删除管理员
delete: async (id) => {
const sql = 'DELETE FROM admins WHERE id = ?';
await query(sql, [id]);
}
};
module.exports = {
pool,
query,
transaction,
testConnection,
closePool
closePool,
adminDB
};

View File

@@ -0,0 +1,225 @@
// 管理员控制器
const Admin = require('../../models/admin');
const AdminService = require('../../services/admin');
const jwt = require('jsonwebtoken');
const { query } = require('../../config/database');
// 生成JWT token
const generateToken = (admin) => {
return jwt.sign(
{ id: admin.id, username: admin.username, role: admin.role },
process.env.JWT_SECRET || 'admin-secret-key',
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
);
};
// 管理员登录
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 isPasswordValid = await admin.verifyPassword(password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
code: 401,
message: '用户名或密码错误'
});
}
// 更新最后登录时间
await admin.updateLastLogin();
// 生成token
const token = generateToken(admin);
res.status(200).json({
success: true,
code: 200,
message: '登录成功',
data: {
admin: admin.toSafeObject(),
token
}
});
} catch (error) {
next(error);
}
};
// 获取当前管理员信息
exports.getProfile = async (req, res, next) => {
try {
const admin = await Admin.findById(req.admin.id);
if (!admin) {
return res.status(404).json({
success: false,
code: 404,
message: '管理员不存在'
});
}
res.status(200).json({
success: true,
code: 200,
message: '获取成功',
data: {
admin: admin.toSafeObject()
}
});
} catch (error) {
next(error);
}
};
// 获取管理员列表
exports.getList = async (req, res, next) => {
try {
const result = await AdminService.getAdminList(req.query);
res.status(200).json({
success: true,
code: 200,
message: '获取成功',
data: result
});
} 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 adminData = {
username,
password,
email: email || null,
nickname: nickname || null,
role: role || 'admin',
status: 1
};
const admin = await AdminService.createAdmin(adminData);
res.status(201).json({
success: true,
code: 201,
message: '创建成功',
data: {
admin
}
});
} catch (error) {
if (error.message === '用户名已存在') {
return res.status(409).json({
success: false,
code: 409,
message: '用户名已存在'
});
}
next(error);
}
};
// 更新管理员
exports.update = async (req, res, next) => {
try {
const { id } = req.params;
const updateData = req.body;
// 不能修改自己角色
if (req.admin.id == id && updateData.role) {
return res.status(400).json({
success: false,
code: 400,
message: '不能修改自己的角色'
});
}
const admin = await AdminService.updateAdmin(id, updateData);
res.status(200).json({
success: true,
code: 200,
message: '更新成功',
data: {
admin
}
});
} catch (error) {
if (error.message === '管理员不存在') {
return res.status(404).json({
success: false,
code: 404,
message: '管理员不存在'
});
}
next(error);
}
};
// 删除管理员
exports.delete = async (req, res, next) => {
try {
const { id } = req.params;
// 不能删除自己
if (req.admin.id == id) {
return res.status(400).json({
success: false,
code: 400,
message: '不能删除自己'
});
}
await AdminService.deleteAdmin(id);
res.status(200).json({
success: true,
code: 200,
message: '删除成功'
});
} catch (error) {
if (error.message === '管理员不存在') {
return res.status(404).json({
success: false,
code: 404,
message: '管理员不存在'
});
}
next(error);
}
};

View File

@@ -0,0 +1,166 @@
const AnimalService = require('../../services/animal');
const { success } = require('../../utils/response');
const { AppError } = require('../../utils/errors');
class AnimalController {
// 获取动物列表
static async getAnimals(req, res, next) {
try {
const { page, pageSize, species, status } = req.query;
const result = await AnimalService.getAnimals({
merchantId: req.userId,
page: parseInt(page) || 1,
pageSize: parseInt(pageSize) || 10,
species,
status
});
res.json(success(result));
} catch (error) {
next(error);
}
}
// 获取单个动物详情
static async getAnimal(req, res, next) {
try {
const { animalId } = req.params;
if (!animalId) {
throw new AppError('动物ID不能为空', 400);
}
const animal = await AnimalService.getAnimalById(animalId);
res.json(success({ animal }));
} catch (error) {
next(error);
}
}
// 创建动物
static async createAnimal(req, res, next) {
try {
const {
name,
species,
breed,
age,
gender,
price,
description,
images,
health_status,
vaccination_status
} = req.body;
// 验证必要字段
if (!name || !species || !price) {
throw new AppError('缺少必要字段: name, species, price', 400);
}
const animalData = {
merchant_id: req.userId,
name,
species,
breed: breed || null,
age: age || null,
gender: gender || null,
price: parseFloat(price),
description: description || null,
images: images || null,
health_status: health_status || null,
vaccination_status: vaccination_status || null,
status: 'available'
};
const animal = await AnimalService.createAnimal(animalData);
res.status(201).json(success({ animal }));
} catch (error) {
next(error);
}
}
// 更新动物信息
static async updateAnimal(req, res, next) {
try {
const { animalId } = req.params;
const updateData = req.body;
if (!animalId) {
throw new AppError('动物ID不能为空', 400);
}
const animal = await AnimalService.updateAnimal(animalId, updateData);
res.json(success({ animal }));
} catch (error) {
next(error);
}
}
// 删除动物
static async deleteAnimal(req, res, next) {
try {
const { animalId } = req.params;
if (!animalId) {
throw new AppError('动物ID不能为空', 400);
}
await AnimalService.deleteAnimal(animalId);
res.json(success({ message: '动物删除成功' }));
} catch (error) {
next(error);
}
}
// 获取动物统计信息
static async getAnimalStatistics(req, res, next) {
try {
const statistics = await AnimalService.getAnimalStatistics();
res.json(success({ statistics }));
} catch (error) {
next(error);
}
}
// 搜索动物
static async searchAnimals(req, res, next) {
try {
const { keyword, species, minPrice, maxPrice, page, pageSize } = req.query;
const result = await AnimalService.searchAnimals({
keyword,
species,
minPrice: minPrice ? parseFloat(minPrice) : null,
maxPrice: maxPrice ? parseFloat(maxPrice) : null,
page: parseInt(page) || 1,
pageSize: parseInt(pageSize) || 10
});
res.json(success(result));
} catch (error) {
next(error);
}
}
// 获取所有动物(管理员)
static async getAllAnimals(req, res, next) {
try {
const { page, pageSize, species, status } = req.query;
const result = await AnimalService.getAnimals({
page: parseInt(page) || 1,
pageSize: parseInt(pageSize) || 10,
species,
status
});
res.json(success(result));
} catch (error) {
next(error);
}
}
}
module.exports = AnimalController;

View File

@@ -39,8 +39,10 @@ const register = async (req, res, next) => {
// 创建新用户
const userId = await UserMySQL.create({
username,
password: hashedPassword,
nickname: nickname || username,
password_hash: hashedPassword,
user_type: 'farmer',
real_name: nickname || username,
avatar_url: '',
email,
phone
});
@@ -92,7 +94,7 @@ const login = async (req, res, next) => {
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, user.password);
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
if (!isPasswordValid) {
throw new AppError('密码错误', 401);
}
@@ -250,11 +252,66 @@ const wechatLogin = async (req, res, next) => {
}
};
// 管理员登录
const adminLogin = async (req, res, next) => {
try {
const { username, password } = req.body;
if (!username || !password) {
throw new AppError('用户名和密码不能为空', 400);
}
// 查找用户(支持用户名、邮箱、手机号登录)
let user = await UserMySQL.findByUsername(username);
if (!user) {
user = await UserMySQL.findByEmail(username);
}
if (!user) {
user = await UserMySQL.findByPhone(username);
}
if (!user) {
throw new AppError('用户不存在', 404);
}
// 检查用户状态
if (!UserMySQL.isActive(user)) {
throw new AppError('账户已被禁用', 403);
}
// 检查用户是否为管理员假设level >= 2为管理员
if (user.level < 2) {
throw new AppError('权限不足,需要管理员权限', 403);
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
if (!isPasswordValid) {
throw new AppError('密码错误', 401);
}
// 生成token
const token = generateToken(user.id);
// 更新最后登录时间
await UserMySQL.updateLastLogin(user.id);
res.json(success({
user: UserMySQL.sanitize(user),
token,
message: '管理员登录成功'
}));
} catch (error) {
next(error);
}
};
module.exports = {
register,
login,
getCurrentUser,
updateProfile,
changePassword,
wechatLogin
wechatLogin,
adminLogin
};

View File

@@ -0,0 +1,402 @@
const OrderService = require('../../services/order');
/**
* 创建订单
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function createOrder(req, res, next) {
try {
const orderData = req.body;
const userId = req.user.id;
// 验证必要字段
if (!orderData.animal_id || !orderData.merchant_id || !orderData.total_amount) {
return res.status(400).json({
success: false,
message: '缺少必要字段: animal_id, merchant_id, total_amount'
});
}
const order = await OrderService.createOrder(orderData, userId);
res.status(201).json({
success: true,
data: {
order,
message: '订单创建成功'
}
});
} catch (error) {
console.error('创建订单控制器错误:', error);
next(error);
}
}
/**
* 获取订单详情
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function getOrder(req, res, next) {
try {
const { orderId } = req.params;
const userId = req.user.id;
const order = await OrderService.getOrderById(orderId);
// 检查权限:用户只能查看自己的订单,商家只能查看自己店铺的订单
if (req.user.role === 'user' && order.user_id !== userId) {
return res.status(403).json({
success: false,
message: '无权访问此订单'
});
}
if (req.user.role === 'merchant' && order.merchant_id !== req.user.merchant_id) {
return res.status(403).json({
success: false,
message: '无权访问此订单'
});
}
res.json({
success: true,
data: order
});
} catch (error) {
console.error('获取订单详情控制器错误:', error);
if (error.message === '订单不存在') {
return res.status(404).json({
success: false,
message: '订单不存在'
});
}
res.status(500).json({
success: false,
message: error.message || '获取订单详情失败'
});
}
}
/**
* 获取用户订单列表
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function getUserOrders(req, res, next) {
try {
const userId = req.user.id;
const filters = {
page: req.query.page,
limit: req.query.limit,
status: req.query.status
};
const result = await OrderService.getUserOrders(userId, filters);
res.json({
success: true,
data: result.orders,
pagination: result.pagination
});
} catch (error) {
console.error('获取用户订单列表控制器错误:', error);
res.status(500).json({
success: false,
message: error.message || '获取订单列表失败'
});
}
}
/**
* 获取商家订单列表
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function getMerchantOrders(req, res, next) {
try {
const merchantId = req.user.merchant_id;
const filters = {
page: req.query.page,
limit: req.query.limit,
status: req.query.status
};
const result = await OrderService.getMerchantOrders(merchantId, filters);
res.json({
success: true,
data: result.orders,
pagination: result.pagination
});
} catch (error) {
console.error('获取商家订单列表控制器错误:', error);
res.status(500).json({
success: false,
message: error.message || '获取订单列表失败'
});
}
}
/**
* 取消订单
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function cancelOrder(req, res, next) {
try {
const { orderId } = req.params;
const userId = req.user.id;
const order = await OrderService.cancelOrder(orderId, userId);
res.json({
success: true,
message: '订单取消成功',
data: order
});
} catch (error) {
console.error('取消订单控制器错误:', error);
if (error.message === '订单不存在') {
return res.status(404).json({
success: false,
message: '订单不存在'
});
}
if (error.message === '无权操作此订单') {
return res.status(403).json({
success: false,
message: '无权操作此订单'
});
}
if (error.message === '订单状态不允许取消') {
return res.status(400).json({
success: false,
message: '订单状态不允许取消'
});
}
res.status(500).json({
success: false,
message: error.message || '取消订单失败'
});
}
}
/**
* 支付订单
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function payOrder(req, res, next) {
try {
const { orderId } = req.params;
const userId = req.user.id;
const paymentData = req.body;
// 验证必要字段
if (!paymentData.payment_method) {
return res.status(400).json({
success: false,
message: '缺少必要字段: payment_method'
});
}
const order = await OrderService.payOrder(orderId, userId, paymentData);
res.json({
success: true,
message: '订单支付成功',
data: order
});
} catch (error) {
console.error('支付订单控制器错误:', error);
if (error.message === '订单不存在') {
return res.status(404).json({
success: false,
message: '订单不存在'
});
}
if (error.message === '无权操作此订单') {
return res.status(403).json({
success: false,
message: '无权操作此订单'
});
}
if (error.message === '订单状态不允许支付') {
return res.status(400).json({
success: false,
message: '订单状态不允许支付'
});
}
res.status(500).json({
success: false,
message: error.message || '支付订单失败'
});
}
}
/**
* 获取订单统计信息
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function getOrderStatistics(req, res, next) {
try {
const userId = req.user.id;
const statistics = await OrderService.getOrderStatistics(userId);
res.json({
success: true,
data: statistics
});
} catch (error) {
console.error('获取订单统计信息控制器错误:', error);
res.status(500).json({
success: false,
message: error.message || '获取统计信息失败'
});
}
}
/**
* 获取所有订单(管理员)
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function getAllOrders(req, res, next) {
try {
const filters = {
page: req.query.page,
limit: req.query.limit,
status: req.query.status
};
const result = await OrderService.getAllOrders(filters);
res.json({
success: true,
data: result.orders,
pagination: result.pagination
});
} catch (error) {
console.error('获取所有订单控制器错误:', error);
res.status(500).json({
success: false,
message: error.message || '获取所有订单失败'
});
}
}
/**
* 更新订单状态
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function updateOrderStatus(req, res, next) {
try {
const { orderId } = req.params;
const { status } = req.body;
const userId = req.user.id;
const order = await OrderService.updateOrderStatus(orderId, status, userId);
res.json({
success: true,
message: '订单状态更新成功',
data: order
});
} catch (error) {
console.error('更新订单状态控制器错误:', error);
if (error.message === '订单不存在') {
return res.status(404).json({
success: false,
message: '订单不存在'
});
}
if (error.message === '无权操作此订单') {
return res.status(403).json({
success: false,
message: '无权操作此订单'
});
}
res.status(500).json({
success: false,
message: error.message || '更新订单状态失败'
});
}
}
/**
* 删除订单
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function deleteOrder(req, res, next) {
try {
const { orderId } = req.params;
const userId = req.user.id;
await OrderService.deleteOrder(orderId, userId);
res.json({
success: true,
message: '订单删除成功'
});
} catch (error) {
console.error('删除订单控制器错误:', error);
if (error.message === '订单不存在') {
return res.status(404).json({
success: false,
message: '订单不存在'
});
}
if (error.message === '无权操作此订单') {
return res.status(403).json({
success: false,
message: '无权操作此订单'
});
}
res.status(500).json({
success: false,
message: error.message || '删除订单失败'
});
}
}
/**
* 获取商家统计信息
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function getMerchantStats(req, res, next) {
try {
const merchantId = req.user.merchant_id;
const stats = await OrderService.getMerchantStats(merchantId);
res.json({
success: true,
data: stats
});
} catch (error) {
console.error('获取商家统计信息控制器错误:', error);
res.status(500).json({
success: false,
message: error.message || '获取统计信息失败'
});
}
}
module.exports = {
createOrder,
getOrder,
getUserOrders,
getMerchantOrders,
cancelOrder,
payOrder,
getOrderStatistics,
getAllOrders,
updateOrderStatus,
deleteOrder,
getMerchantStats
};

View File

@@ -0,0 +1,152 @@
const TravelService = require('../../services/travel');
const { success } = require('../../utils/response');
const { AppError } = require('../../utils/errors');
class TravelController {
// 获取旅行计划列表
static async getTravelPlans(req, res, next) {
try {
const { page, pageSize, status } = req.query;
const result = await TravelService.getTravelPlans({
userId: req.userId,
page: parseInt(page) || 1,
pageSize: parseInt(pageSize) || 10,
status
});
res.json(success(result));
} catch (error) {
next(error);
}
}
// 获取单个旅行计划详情
static async getTravelPlan(req, res, next) {
try {
const { planId } = req.params;
if (!planId) {
throw new AppError('旅行计划ID不能为空', 400);
}
const plan = await TravelService.getTravelPlanById(planId);
res.json(success({ plan }));
} catch (error) {
next(error);
}
}
// 创建旅行计划
static async createTravelPlan(req, res, next) {
try {
const {
destination,
start_date,
end_date,
budget,
companions,
transportation,
accommodation,
activities,
notes
} = req.body;
if (!destination || !start_date || !end_date) {
throw new AppError('目的地、开始日期和结束日期不能为空', 400);
}
const planId = await TravelService.createTravelPlan(req.userId, {
destination,
start_date,
end_date,
budget,
companions,
transportation,
accommodation,
activities,
notes
});
const plan = await TravelService.getTravelPlanById(planId);
res.status(201).json(success({
plan,
message: '旅行计划创建成功'
}));
} catch (error) {
next(error);
}
}
// 更新旅行计划
static async updateTravelPlan(req, res, next) {
try {
const { planId } = req.params;
if (!planId) {
throw new AppError('旅行计划ID不能为空', 400);
}
const plan = await TravelService.updateTravelPlan(planId, req.userId, req.body);
res.json(success({
plan,
message: '旅行计划更新成功'
}));
} catch (error) {
next(error);
}
}
// 删除旅行计划
static async deleteTravelPlan(req, res, next) {
try {
const { planId } = req.params;
if (!planId) {
throw new AppError('旅行计划ID不能为空', 400);
}
await TravelService.deleteTravelPlan(planId, req.userId);
res.json(success({
message: '旅行计划删除成功',
planId: parseInt(planId)
}));
} catch (error) {
next(error);
}
}
// 获取用户旅行统计
static async getTravelStats(req, res, next) {
try {
const stats = await TravelService.getUserTravelStats(req.userId);
res.json(success({ stats }));
} catch (error) {
next(error);
}
}
// 获取所有旅行计划(管理员功能)
static async getAllTravelPlans(req, res, next) {
try {
const { page, pageSize, status, userId } = req.query;
const result = await TravelService.getTravelPlans({
userId,
page: parseInt(page) || 1,
pageSize: parseInt(pageSize) || 10,
status
});
res.json(success(result));
} catch (error) {
next(error);
}
}
}
module.exports = TravelController;

View File

@@ -0,0 +1,129 @@
const UserService = require('../../services/user');
const { success } = require('../../utils/response');
const { AppError } = require('../../utils/errors');
class UserController {
// 获取用户详情
static async getUserProfile(req, res, next) {
try {
const user = await UserService.getUserProfile(req.userId);
res.json(success({ user }));
} catch (error) {
next(error);
}
}
// 更新用户信息
static async updateProfile(req, res, next) {
try {
const user = await UserService.updateUserProfile(req.userId, req.body);
res.json(success({
user,
message: '个人信息更新成功'
}));
} catch (error) {
next(error);
}
}
// 搜索用户(管理员功能)
static async searchUsers(req, res, next) {
try {
const result = await UserService.searchUsers(req.query);
res.json(success(result));
} catch (error) {
next(error);
}
}
// 获取用户统计信息(管理员功能)
static async getUserStatistics(req, res, next) {
try {
const stats = await UserService.getUserStatistics();
res.json(success({ statistics: stats }));
} catch (error) {
next(error);
}
}
// 批量操作用户状态(管理员功能)
static async batchUpdateUserStatus(req, res, next) {
try {
const { userIds, status } = req.body;
if (!userIds || !Array.isArray(userIds) || userIds.length === 0) {
throw new AppError('请选择要操作的用户', 400);
}
if (!status || !['active', 'inactive'].includes(status)) {
throw new AppError('无效的状态值', 400);
}
const affectedRows = await UserService.batchUpdateUserStatus(userIds, status);
res.json(success({
message: `成功更新 ${affectedRows} 个用户状态`,
affectedRows
}));
} catch (error) {
next(error);
}
}
// 获取用户列表(管理员功能)
static async getUsers(req, res, next) {
try {
const { page = 1, pageSize = 10, userType, status } = req.query;
const result = await UserService.searchUsers({
page: parseInt(page),
pageSize: parseInt(pageSize),
userType,
status,
keyword: req.query.keyword
});
res.json(success(result));
} catch (error) {
next(error);
}
}
// 获取单个用户详情(管理员功能)
static async getUserById(req, res, next) {
try {
const { userId } = req.params;
if (!userId) {
throw new AppError('用户ID不能为空', 400);
}
const user = await UserService.getUserProfile(userId);
res.json(success({ user }));
} catch (error) {
next(error);
}
}
// 删除用户(管理员功能)
static async deleteUser(req, res, next) {
try {
const { userId } = req.params;
if (!userId) {
throw new AppError('用户ID不能为空', 400);
}
// 这里需要实现软删除逻辑
// 暂时先返回成功消息
res.json(success({
message: '用户删除成功',
userId
}));
} catch (error) {
next(error);
}
}
}
module.exports = UserController;

View File

@@ -149,6 +149,60 @@ const options = {
updated_at: {
type: 'string',
format: 'date-time'
},
last_login_at: {
type: 'string',
format: 'date-time',
description: '最后登录时间'
}
}
},
// 管理员模型
Admin: {
type: 'object',
properties: {
id: {
type: 'integer',
description: '管理员ID'
},
username: {
type: 'string',
description: '用户名'
},
email: {
type: 'string',
description: '邮箱'
},
nickname: {
type: 'string',
description: '昵称'
},
avatar: {
type: 'string',
description: '头像URL'
},
role: {
type: 'string',
description: '角色'
},
status: {
type: 'integer',
description: '状态 (1:启用, 0:禁用)'
},
last_login: {
type: 'string',
format: 'date-time',
description: '最后登录时间'
},
created_at: {
type: 'string',
format: 'date-time',
description: '创建时间'
},
updated_at: {
type: 'string',
format: 'date-time',
description: '更新时间'
}
}
},

View File

@@ -1,108 +1,99 @@
const jwt = require('jsonwebtoken')
const { User } = require('../models/User')
const { AppError } = require('../utils/errors')
const jwt = require('jsonwebtoken');
const Admin = require('../models/admin');
// JWT认证中间件
const authenticate = async (req, res, next) => {
// 用户认证中间件
function authenticateUser(req, res, next) {
// TODO: 实现用户认证逻辑
next();
}
// 管理员认证中间件
async function authenticateAdmin(req, res, next) {
try {
let token
// 从Authorization头获取token
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1]
// 从请求头获取token
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
code: 401,
message: '未提供认证token'
});
}
if (!token) {
return next(new AppError('访问被拒绝请提供有效的token', 401))
}
const token = authHeader.split(' ')[1];
// 验证token
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key')
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'admin-secret-key');
// 查找用户
const user = await User.findById(decoded.userId)
if (!user) {
return next(new AppError('用户不存在', 404))
// 查找管理员
const admin = await Admin.findById(decoded.id);
if (!admin) {
return res.status(401).json({
success: false,
code: 401,
message: '管理员不存在'
});
}
// 检查用户状态
if (!user.isActive()) {
return next(new AppError('账户已被禁用', 403))
// 检查管理员状态
if (admin.status !== 1) {
return res.status(401).json({
success: false,
code: 401,
message: '管理员账号已被禁用'
});
}
// 将用户信息添加到请求对象
req.userId = user._id
req.user = user
// 将管理员信息添加到请求对象
req.admin = admin;
next()
next();
} catch (error) {
if (error.name === 'JsonWebTokenError') {
return next(new AppError('无效的token', 401))
return res.status(401).json({
success: false,
code: 401,
message: '无效的认证token'
});
}
if (error.name === 'TokenExpiredError') {
return next(new AppError('token已过期', 401))
return res.status(401).json({
success: false,
code: 401,
message: '认证token已过期'
});
}
next(error)
next(error);
}
}
// 可选认证中间件(不强制要求认证)
const optionalAuthenticate = async (req, res, next) => {
try {
let token
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1]
// 权限检查中间件
function requireRole(roles) {
return (req, res, next) => {
if (!req.admin) {
return res.status(401).json({
success: false,
code: 401,
message: '需要管理员权限'
});
}
if (token) {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key')
const user = await User.findById(decoded.userId)
if (user && user.isActive()) {
req.userId = user._id
req.user = user
}
if (!roles.includes(req.admin.role)) {
return res.status(403).json({
success: false,
code: 403,
message: '权限不足'
});
}
next()
} catch (error) {
// 忽略token验证错误继续处理请求
next()
}
}
// 管理员权限检查
const requireAdmin = (req, res, next) => {
if (!req.user) {
return next(new AppError('请先登录', 401))
}
// 这里可以根据实际需求定义管理员权限
// 例如:检查用户角色或权限级别
if (req.user.level < 2) { // 假设2级以上为管理员
return next(new AppError('权限不足,需要管理员权限', 403))
}
next()
}
// VIP权限检查
const requireVip = (req, res, next) => {
if (!req.user) {
return next(new AppError('请先登录', 401))
}
if (!req.user.isVip()) {
return next(new AppError('需要VIP权限', 403))
}
next()
next();
};
}
module.exports = {
authenticate,
optionalAuthenticate,
requireAdmin,
requireVip
}
authenticateUser,
authenticateAdmin,
requireRole
};

View File

@@ -4,30 +4,30 @@ class UserMySQL {
// 创建用户
static async create(userData) {
const {
openid,
nickname,
avatar = '',
gender = 'other',
birthday = null,
phone = null,
email = null
username,
password_hash,
user_type = 'farmer',
real_name = '',
avatar_url = '',
email = null,
phone = null
} = userData;
const sql = `
INSERT INTO users (
openid, nickname, avatar, gender, birthday, phone, email,
username, password_hash, user_type, real_name, avatar_url, email, phone,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
`;
const params = [
openid,
nickname,
avatar,
gender,
birthday,
phone,
email
username,
password_hash,
user_type,
real_name,
avatar_url,
email,
phone
];
const result = await query(sql, params);
@@ -41,10 +41,10 @@ class UserMySQL {
return rows[0] || null;
}
// 根据openid查找用户
static async findByOpenid(openid) {
const sql = 'SELECT * FROM users WHERE openid = ?';
const rows = await query(sql, [openid]);
// 根据用户名查找用户
static async findByUsername(username) {
const sql = 'SELECT * FROM users WHERE username = ?';
const rows = await query(sql, [username]);
return rows[0] || null;
}
@@ -101,10 +101,10 @@ class UserMySQL {
return result.affectedRows > 0;
}
// 检查openid是否已存在
static async isOpenidExists(openid, excludeId = null) {
let sql = 'SELECT COUNT(*) as count FROM users WHERE openid = ?';
const params = [openid];
// 检查用户名是否已存在
static async isUsernameExists(username, excludeId = null) {
let sql = 'SELECT COUNT(*) as count FROM users WHERE username = ?';
const params = [username];
if (excludeId) {
sql += ' AND id != ?';
@@ -115,6 +115,17 @@ class UserMySQL {
return rows[0].count > 0;
}
// 检查用户状态是否活跃
static isActive(user) {
return user.status === 'active';
}
// 执行原始查询(用于复杂查询)
static async query(sql, params = []) {
const { query } = require('../config/database');
return await query(sql, params);
}
// 检查邮箱是否已存在
static async isEmailExists(email, excludeId = null) {
let sql = 'SELECT COUNT(*) as count FROM users WHERE email = ?';
@@ -143,9 +154,18 @@ class UserMySQL {
return rows[0].count > 0;
}
// 检查用户名是否已存在 (根据openid检查)
// 检查用户名是否已存在
static async isUsernameExists(username, excludeId = null) {
return await this.isOpenidExists(username, excludeId);
let sql = 'SELECT COUNT(*) as count FROM users WHERE username = ?';
const params = [username];
if (excludeId) {
sql += ' AND id != ?';
params.push(excludeId);
}
const rows = await query(sql, params);
return rows[0].count > 0;
}
// 安全返回用户信息(去除敏感信息)

View File

@@ -0,0 +1,88 @@
// 管理员模型
const { adminDB } = require('../config/database');
const bcrypt = require('bcryptjs');
class Admin {
constructor(data) {
this.id = data.id;
this.username = data.username;
this.password = data.password;
this.email = data.email;
this.nickname = data.nickname;
this.avatar = data.avatar;
this.role = data.role;
this.status = data.status;
this.last_login = data.last_login;
this.created_at = data.created_at;
this.updated_at = data.updated_at;
}
// 根据用户名查找管理员
static async findByUsername(username) {
const adminData = await adminDB.findByUsername(username);
return adminData ? new Admin(adminData) : null;
}
// 根据ID查找管理员
static async findById(id) {
const adminData = await adminDB.findById(id);
return adminData ? new Admin(adminData) : null;
}
// 验证密码
async verifyPassword(password) {
return await bcrypt.compare(password, this.password);
}
// 更新最后登录时间
async updateLastLogin() {
await adminDB.updateLastLogin(this.id);
}
// 创建管理员
static async create(adminData) {
// 密码加密
if (adminData.password) {
adminData.password = await bcrypt.hash(adminData.password, 10);
}
const id = await adminDB.create(adminData);
return await this.findById(id);
}
// 更新管理员信息
async update(updateData) {
// 如果有密码更新,需要加密
if (updateData.password) {
updateData.password = await bcrypt.hash(updateData.password, 10);
}
await adminDB.update(this.id, updateData);
// 更新实例数据
Object.assign(this, updateData);
}
// 删除管理员
async delete() {
await adminDB.delete(this.id);
}
// 转换为安全对象(不包含密码等敏感信息)
toSafeObject() {
return {
id: this.id,
username: this.username,
email: this.email,
nickname: this.nickname,
avatar: this.avatar,
role: this.role,
status: this.status,
last_login: this.last_login,
created_at: this.created_at,
updated_at: this.updated_at
};
}
}
module.exports = Admin;

327
backend/src/routes/admin.js Normal file
View File

@@ -0,0 +1,327 @@
// 管理员路由
const express = require('express');
const router = express.Router();
const adminController = require('../controllers/admin');
const { authenticateAdmin } = require('../middleware/auth');
/**
* @swagger
* tags:
* name: Admin
* description: 管理员相关接口
*/
/**
* @swagger
* /admin/login:
* post:
* summary: 管理员登录
* tags: [Admin]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - username
* - password
* properties:
* username:
* type: string
* description: 用户名
* password:
* type: string
* description: 密码
* responses:
* 200:
* description: 登录成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* message:
* type: string
* data:
* type: object
* properties:
* admin:
* $ref: '#/components/schemas/Admin'
* token:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 用户名或密码错误
*/
router.post('/login', adminController.login);
// 需要认证的接口
router.use(authenticateAdmin);
/**
* @swagger
* /admin/profile:
* get:
* summary: 获取当前管理员信息
* tags: [Admin]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* message:
* type: string
* data:
* type: object
* properties:
* admin:
* $ref: '#/components/schemas/Admin'
* 401:
* description: 未授权
* 404:
* description: 管理员不存在
*/
router.get('/profile', adminController.getProfile);
/**
* @swagger
* /admin:
* get:
* summary: 获取管理员列表
* tags: [Admin]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* minimum: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* minimum: 1
* maximum: 100
* description: 每页数量
* - in: query
* name: username
* schema:
* type: string
* description: 用户名搜索
* - in: query
* name: role
* schema:
* type: string
* description: 角色筛选
* - in: query
* name: status
* schema:
* type: integer
* description: 状态筛选
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* message:
* type: string
* data:
* type: object
* properties:
* admins:
* type: array
* items:
* $ref: '#/components/schemas/Admin'
* pagination:
* $ref: '#/components/schemas/Pagination'
* 401:
* description: 未授权
*/
router.get('/', adminController.getList);
/**
* @swagger
* /admin:
* post:
* summary: 创建管理员
* tags: [Admin]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - username
* - password
* properties:
* username:
* type: string
* description: 用户名
* password:
* type: string
* description: 密码
* email:
* type: string
* description: 邮箱
* nickname:
* type: string
* description: 昵称
* role:
* type: string
* description: 角色
* responses:
* 201:
* description: 创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* message:
* type: string
* data:
* type: object
* properties:
* admin:
* $ref: '#/components/schemas/Admin'
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 409:
* description: 用户名已存在
*/
router.post('/', adminController.create);
/**
* @swagger
* /admin/{id}:
* put:
* summary: 更新管理员
* tags: [Admin]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 管理员ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* description: 邮箱
* nickname:
* type: string
* description: 昵称
* role:
* type: string
* description: 角色
* status:
* type: integer
* description: 状态
* responses:
* 200:
* description: 更新成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* message:
* type: string
* data:
* type: object
* properties:
* admin:
* $ref: '#/components/schemas/Admin'
* 400:
* description: 不能修改自己的角色
* 401:
* description: 未授权
* 404:
* description: 管理员不存在
*/
router.put('/:id', adminController.update);
/**
* @swagger
* /admin/{id}:
* delete:
* summary: 删除管理员
* tags: [Admin]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 管理员ID
* responses:
* 200:
* description: 删除成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* message:
* type: string
* 400:
* description: 不能删除自己
* 401:
* description: 未授权
* 404:
* description: 管理员不存在
*/
router.delete('/:id', adminController.delete);
module.exports = router;

View File

@@ -0,0 +1,533 @@
const express = require('express');
const { body, query } = require('express-validator');
const AnimalController = require('../controllers/animal');
const { authenticateUser: authenticate, requireRole: requireAdmin, requireRole: requireMerchant } = require('../middleware/auth');
const router = express.Router();
/**
* @swagger
* tags:
* name: Animals
* description: 动物管理相关接口
*/
/**
* @swagger
* /animals/search:
* get:
* summary: 搜索动物(公开接口)
* tags: [Animals]
* parameters:
* - in: query
* name: keyword
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: species
* schema:
* type: string
* description: 物种
* - in: query
* name: minPrice
* schema:
* type: number
* description: 最低价格
* - in: query
* name: maxPrice
* schema:
* type: number
* description: 最高价格
* - in: query
* name: page
* schema:
* type: integer
* minimum: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* minimum: 1
* maximum: 100
* description: 每页数量
* responses:
* 200:
* description: 搜索成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* animals:
* type: array
* items:
* $ref: '#/components/schemas/Animal'
* pagination:
* $ref: '#/components/schemas/Pagination'
* 500:
* description: 服务器内部错误
*/
router.get('/search', AnimalController.searchAnimals);
/**
* @swagger
* /animals:
* get:
* summary: 获取动物列表(商家)
* tags: [Animals]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* minimum: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* minimum: 1
* maximum: 100
* description: 每页数量
* - in: query
* name: species
* schema:
* type: string
* description: 物种
* - in: query
* name: status
* schema:
* type: string
* description: 状态
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* animals:
* type: array
* items:
* $ref: '#/components/schemas/Animal'
* pagination:
* $ref: '#/components/schemas/Pagination'
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
router.get('/', authenticate, requireMerchant, AnimalController.getAnimals);
/**
* @swagger
* /animals/stats:
* get:
* summary: 获取动物统计信息(商家)
* tags: [Animals]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* statistics:
* $ref: '#/components/schemas/AnimalStatistics'
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
router.get('/stats', authenticate, requireMerchant, AnimalController.getAnimalStatistics);
/**
* @swagger
* /animals/admin/all:
* get:
* summary: 获取所有动物信息(管理员)
* tags: [Animals]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* minimum: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* minimum: 1
* maximum: 100
* description: 每页数量
* - in: query
* name: species
* schema:
* type: string
* description: 物种
* - in: query
* name: status
* schema:
* type: string
* description: 状态
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* animals:
* type: array
* items:
* $ref: '#/components/schemas/Animal'
* pagination:
* $ref: '#/components/schemas/Pagination'
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器内部错误
*/
router.get('/admin/all', authenticate, requireAdmin, AnimalController.getAllAnimals);
/**
* @swagger
* /animals/statistics:
* get:
* summary: 获取动物统计信息
* tags: [Animals]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* statistics:
* type: object
* properties:
* total:
* type: integer
* bySpecies:
* type: array
* items:
* type: object
* properties:
* species:
* type: string
* count:
* type: integer
* byStatus:
* type: array
* items:
* type: object
* properties:
* status:
* type: string
* count:
* type: integer
* topMerchants:
* type: array
* items:
* type: object
* properties:
* merchant_name:
* type: string
* animal_count:
* type: integer
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
router.get('/statistics', authenticate, AnimalController.getAnimalStatistics);
/**
* @swagger
* /animals/{animalId}:
* get:
* summary: 获取单个动物详情
* tags: [Animals]
* parameters:
* - in: path
* name: animalId
* required: true
* schema:
* type: integer
* description: 动物ID
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* animal:
* $ref: '#/components/schemas/AnimalDetail'
* 404:
* description: 动物不存在
* 500:
* description: 服务器内部错误
*/
router.get('/:animalId', AnimalController.getAnimal);
/**
* @swagger
* /animals:
* post:
* summary: 创建动物信息
* tags: [Animals]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - species
* - price
* properties:
* name:
* type: string
* description: 动物名称
* species:
* type: string
* description: 动物种类
* breed:
* type: string
* description: 品种
* age:
* type: integer
* description: 年龄
* gender:
* type: string
* enum: [male, female]
* description: 性别
* price:
* type: number
* description: 价格
* description:
* type: string
* description: 描述
* images:
* type: array
* items:
* type: string
* description: 图片URL列表
* responses:
* 201:
* description: 创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* animal:
* $ref: '#/components/schemas/Animal'
* message:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
router.post('/',
authenticate,
requireMerchant,
[
body('name').notEmpty().withMessage('名称不能为空'),
body('species').notEmpty().withMessage('种类不能为空'),
body('price').isFloat({ min: 0 }).withMessage('价格必须大于0')
],
AnimalController.createAnimal
);
/**
* @swagger
* /animals/{animalId}:
* put:
* summary: 更新动物信息
* tags: [Animals]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: animalId
* required: true
* schema:
* type: integer
* description: 动物ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description: 动物名称
* species:
* type: string
* description: 动物种类
* breed:
* type: string
* description: 品种
* age:
* type: integer
* description: 年龄
* gender:
* type: string
* enum: [male, female]
* description: 性别
* price:
* type: number
* description: 价格
* description:
* type: string
* description: 描述
* images:
* type: array
* items:
* type: string
* description: 图片URL列表
* status:
* type: string
* enum: [available, sold, reserved]
* description: 状态
* responses:
* 200:
* description: 更新成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* animal:
* $ref: '#/components/schemas/Animal'
* message:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 404:
* description: 动物不存在
* 500:
* description: 服务器内部错误
*/
router.put('/:animalId',
authenticate,
requireMerchant,
AnimalController.updateAnimal
);
/**
* @swagger
* /animals/{animalId}:
* delete:
* summary: 删除动物信息
* tags: [Animals]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: animalId
* required: true
* schema:
* type: integer
* description: 动物ID
* responses:
* 200:
* description: 删除成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* message:
* type: string
* animalId:
* type: integer
* 401:
* description: 未授权
* 404:
* description: 动物不存在
* 500:
* description: 服务器内部错误
*/
router.delete('/:animalId',
authenticate,
requireMerchant,
AnimalController.deleteAnimal
);
module.exports = router;

View File

@@ -278,6 +278,60 @@ router.put(
authController.changePassword
)
/**
* @swagger
* /auth/admin/login:
* post:
* summary: 管理员登录
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - username
* - password
* properties:
* username:
* type: string
* description: 用户名/邮箱/手机号
* password:
* type: string
* 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: 请求参数错误
* 401:
* description: 密码错误
* 403:
* description: 权限不足或账户被禁用
* 404:
* description: 用户不存在
* 500:
* description: 服务器内部错误
*/
router.post('/admin/login', authController.adminLogin);
/**
* @swagger
* /auth/wechat:

View File

@@ -0,0 +1,53 @@
const express = require('express');
const { body } = require('express-validator');
const {
createOrder,
getOrder,
getUserOrders,
getMerchantOrders,
cancelOrder,
payOrder,
getOrderStatistics,
getAllOrders,
updateOrderStatus,
deleteOrder,
getMerchantStats
} = require('../controllers/order/index');
const { authenticateUser: authenticate, requireRole: requireAdmin, requireRole: requireMerchant } = require('../middleware/auth');
const router = express.Router();
// 创建订单
router.post('/', authenticate, createOrder);
// 获取订单详情
router.get('/:orderId', authenticate, getOrder);
// 获取订单列表
router.get('/', authenticate, getUserOrders);
// 商家获取订单列表
router.get('/merchant', authenticate, requireMerchant, getMerchantOrders);
// 取消订单
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);
// 管理员删除订单
router.delete('/:orderId', authenticate, requireAdmin, deleteOrder);
// 商家获取统计数据
router.get('/merchant/stats', authenticate, requireMerchant, getMerchantStats);
module.exports = router;

View File

@@ -0,0 +1,434 @@
const express = require('express');
const { body, query } = require('express-validator');
const TravelController = require('../controllers/travel');
const { authenticateUser: authenticate, requireRole: requireAdmin } = require('../middleware/auth');
const router = express.Router();
/**
* @swagger
* tags:
* name: Travel
* description: 旅行计划管理相关接口
*/
/**
* @swagger
* /travel/plans:
* get:
* summary: 获取当前用户的旅行计划列表
* tags: [Travel]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* minimum: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* minimum: 1
* maximum: 50
* description: 每页数量
* - in: query
* name: status
* schema:
* type: string
* enum: [planning, in_progress, completed, cancelled]
* description: 计划状态
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* plans:
* type: array
* items:
* $ref: '#/components/schemas/TravelPlan'
* pagination:
* type: object
* properties:
* page:
* type: integer
* pageSize:
* type: integer
* total:
* type: integer
* totalPages:
* type: integer
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
router.get('/plans', authenticate, TravelController.getTravelPlans);
/**
* @swagger
* /travel/plans/{planId}:
* get:
* summary: 获取单个旅行计划详情
* tags: [Travel]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: planId
* required: true
* schema:
* type: integer
* description: 旅行计划ID
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* plan:
* $ref: '#/components/schemas/TravelPlan'
* 401:
* description: 未授权
* 404:
* description: 旅行计划不存在
* 500:
* description: 服务器内部错误
*/
router.get('/plans/:planId', authenticate, TravelController.getTravelPlan);
/**
* @swagger
* /travel/plans:
* post:
* summary: 创建旅行计划
* tags: [Travel]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - destination
* - start_date
* - end_date
* properties:
* destination:
* type: string
* description: 目的地
* start_date:
* type: string
* format: date
* description: 开始日期
* end_date:
* type: string
* format: date
* description: 结束日期
* budget:
* type: number
* description: 预算
* companions:
* type: integer
* description: 同行人数
* transportation:
* type: string
* description: 交通方式
* accommodation:
* type: string
* description: 住宿方式
* activities:
* type: string
* description: 活动安排
* notes:
* type: string
* description: 备注
* responses:
* 201:
* description: 创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* plan:
* $ref: '#/components/schemas/TravelPlan'
* message:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
router.post('/plans',
authenticate,
[
body('destination').notEmpty().withMessage('目的地不能为空'),
body('start_date').isDate().withMessage('开始日期格式错误'),
body('end_date').isDate().withMessage('结束日期格式错误')
],
TravelController.createTravelPlan
);
/**
* @swagger
* /travel/plans/{planId}:
* put:
* summary: 更新旅行计划
* tags: [Travel]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: planId
* required: true
* schema:
* type: integer
* description: 旅行计划ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* destination:
* type: string
* description: 目的地
* start_date:
* type: string
* format: date
* description: 开始日期
* end_date:
* type: string
* format: date
* description: 结束日期
* budget:
* type: number
* description: 预算
* companions:
* type: integer
* description: 同行人数
* transportation:
* type: string
* description: 交通方式
* accommodation:
* type: string
* description: 住宿方式
* activities:
* type: string
* description: 活动安排
* notes:
* type: string
* description: 备注
* status:
* type: string
* enum: [planning, in_progress, completed, cancelled]
* description: 计划状态
* responses:
* 200:
* description: 更新成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* plan:
* $ref: '#/components/schemas/TravelPlan'
* message:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 404:
* description: 旅行计划不存在
* 500:
* description: 服务器内部错误
*/
router.put('/plans/:planId', authenticate, TravelController.updateTravelPlan);
/**
* @swagger
* /travel/plans/{planId}:
* delete:
* summary: 删除旅行计划
* tags: [Travel]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: planId
* required: true
* schema:
* type: integer
* description: 旅行计划ID
* responses:
* 200:
* description: 删除成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* message:
* type: string
* planId:
* type: integer
* 401:
* description: 未授权
* 404:
* description: 旅行计划不存在
* 500:
* description: 服务器内部错误
*/
router.delete('/plans/:planId', authenticate, TravelController.deleteTravelPlan);
/**
* @swagger
* /travel/stats:
* get:
* summary: 获取用户旅行统计
* tags: [Travel]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* stats:
* type: object
* properties:
* total_plans:
* type: integer
* completed_plans:
* type: integer
* planning_plans:
* type: integer
* cancelled_plans:
* type: integer
* total_budget:
* type: number
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
router.get('/stats', authenticate, TravelController.getTravelStats);
/**
* @swagger
* /travel/admin/plans:
* get:
* summary: 获取所有旅行计划(管理员)
* tags: [Travel]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* minimum: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* minimum: 1
* maximum: 50
* description: 每页数量
* - in: query
* name: status
* schema:
* type: string
* enum: [planning, in_progress, completed, cancelled]
* description: 计划状态
* - in: query
* name: userId
* schema:
* type: integer
* description: 用户ID
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* plans:
* type: array
* items:
* $ref: '#/components/schemas/TravelPlan'
* pagination:
* type: object
* properties:
* page:
* type: integer
* pageSize:
* type: integer
* total:
* type: integer
* totalPages:
* type: integer
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器内部错误
*/
router.get('/admin/plans', authenticate, requireAdmin, TravelController.getAllTravelPlans);
module.exports = router;

380
backend/src/routes/user.js Normal file
View File

@@ -0,0 +1,380 @@
const express = require('express');
const { body, query } = require('express-validator');
const UserController = require('../controllers/user');
const { authenticateUser, requireRole: requireAdmin } = require('../middleware/auth');
const router = express.Router();
/**
* @swagger
* tags:
* name: Users
* description: 用户管理相关接口
*/
/**
* @swagger
* /users/profile:
* get:
* summary: 获取当前用户信息
* tags: [Users]
* 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('/profile', authenticateUser, UserController.getUserProfile);
/**
* @swagger
* /users/profile:
* put:
* summary: 更新用户个人信息
* tags: [Users]
* 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: 生日
* phone:
* type: string
* description: 手机号
* email:
* type: string
* format: email
* 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', authenticateUser, UserController.updateProfile);
/**
* @swagger
* /users:
* get:
* summary: 获取用户列表(管理员)
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* minimum: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* minimum: 1
* maximum: 100
* description: 每页数量
* - in: query
* name: userType
* schema:
* type: string
* enum: [farmer, merchant, admin]
* description: 用户类型
* - in: query
* name: status
* schema:
* type: string
* enum: [active, inactive]
* description: 用户状态
* - in: query
* name: keyword
* schema:
* type: string
* description: 搜索关键词
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* users:
* type: array
* items:
* $ref: '#/components/schemas/User'
* pagination:
* type: object
* properties:
* page:
* type: integer
* pageSize:
* type: integer
* total:
* type: integer
* totalPages:
* type: integer
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器内部错误
*/
// 已移除重复的GET /users 路由,避免冲突
/**
* @swagger
* /users/{userId}:
* get:
* summary: 获取用户详情(管理员)
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: userId
* required: true
* schema:
* type: integer
* description: 用户ID
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* user:
* $ref: '#/components/schemas/User'
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 404:
* description: 用户不存在
* 500:
* description: 服务器内部错误
*/
router.get('/:userId', authenticateUser, requireAdmin(['admin', 'super_admin']), UserController.getUserById);
/**
* @swagger
* /users/statistics:
* get:
* summary: 获取用户统计信息(管理员)
* tags: [Users]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* statistics:
* type: array
* items:
* type: object
* properties:
* total_users:
* type: integer
* farmers:
* type: integer
* merchants:
* type: integer
* admins:
* type: integer
* active_users:
* type: integer
* inactive_users:
* type: integer
* date:
* type: string
* format: date
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器内部错误
*/
router.get('/statistics', authenticateUser, requireAdmin(['admin', 'super_admin']), UserController.getUserStatistics);
/**
* @swagger
* /users/batch-status:
* post:
* summary: 批量操作用户状态(管理员)
* tags: [Users]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - userIds
* - status
* properties:
* userIds:
* type: array
* items:
* type: integer
* description: 用户ID列表
* status:
* type: string
* enum: [active, inactive]
* description: 用户状态
* responses:
* 200:
* description: 操作成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* message:
* type: string
* affectedRows:
* type: integer
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器内部错误
*/
router.post('/batch-status',
authenticateUser,
requireAdmin(['admin', 'super_admin']),
[
body('userIds').isArray().withMessage('userIds必须是数组'),
body('status').isIn(['active', 'inactive']).withMessage('状态值无效')
],
UserController.batchUpdateUserStatus
);
/**
* @swagger
* /users/{userId}:
* delete:
* summary: 删除用户(管理员)
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: userId
* required: true
* schema:
* type: integer
* description: 用户ID
* responses:
* 200:
* description: 删除成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* message:
* type: string
* userId:
* type: integer
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 404:
* description: 用户不存在
* 500:
* description: 服务器内部错误
*/
router.delete('/:userId', authenticateUser, requireAdmin(['admin', 'super_admin']), UserController.deleteUser);
module.exports = router;

View File

@@ -4,7 +4,7 @@ const { testConnection } = require('./config/database')
const redisConfig = require('./config/redis')
const rabbitMQConfig = require('./config/rabbitmq')
const PORT = process.env.PORT || 3000
const PORT = process.env.PORT || 3100
const HOST = process.env.HOST || '0.0.0.0'
// 显示启动横幅

View File

@@ -0,0 +1,158 @@
const database = require('../../config/database');
const Admin = require('../../models/admin');
class AdminService {
/**
* 获取管理员列表
* @param {Object} filters - 筛选条件
* @returns {Promise<Object>} 管理员列表和分页信息
*/
async getAdminList(filters = {}) {
try {
const { page = 1, pageSize = 10, username, role, status } = filters;
const offset = (parseInt(page) - 1) * parseInt(pageSize);
// 构建查询条件
let whereClause = 'WHERE 1=1';
const params = [];
if (username) {
whereClause += ' AND username LIKE ?';
params.push(`%${username}%`);
}
if (role) {
whereClause += ' AND role = ?';
params.push(role);
}
if (status !== undefined) {
whereClause += ' AND status = ?';
params.push(status);
}
// 查询总数
const countSql = `SELECT COUNT(*) as total FROM admins ${whereClause}`;
const [countResult] = await database.query(countSql, params);
const total = countResult.total;
// 查询数据
const sql = `
SELECT id, username, email, nickname, avatar, role, status, last_login, created_at, updated_at
FROM admins
${whereClause}
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`;
params.push(parseInt(pageSize), offset);
const admins = await database.query(sql, params);
return {
admins,
pagination: {
page: parseInt(page),
pageSize: parseInt(pageSize),
total,
totalPages: Math.ceil(total / parseInt(pageSize))
}
};
} catch (error) {
console.error('获取管理员列表失败:', error);
throw new Error('获取管理员列表失败');
}
}
/**
* 创建管理员
* @param {Object} adminData - 管理员数据
* @returns {Promise<Object>} 创建的管理员
*/
async createAdmin(adminData) {
try {
// 检查用户名是否已存在
const existingAdmin = await this.getAdminByUsername(adminData.username);
if (existingAdmin) {
throw new Error('用户名已存在');
}
// 创建管理员
const admin = await Admin.create(adminData);
return admin.toSafeObject();
} catch (error) {
console.error('创建管理员失败:', error);
throw error;
}
}
/**
* 根据用户名获取管理员
* @param {string} username - 用户名
* @returns {Promise<Object|null>} 管理员信息
*/
async getAdminByUsername(username) {
try {
return await Admin.findByUsername(username);
} catch (error) {
console.error('获取管理员失败:', error);
throw new Error('获取管理员失败');
}
}
/**
* 根据ID获取管理员
* @param {number} id - 管理员ID
* @returns {Promise<Object|null>} 管理员信息
*/
async getAdminById(id) {
try {
return await Admin.findById(id);
} catch (error) {
console.error('获取管理员失败:', error);
throw new Error('获取管理员失败');
}
}
/**
* 更新管理员信息
* @param {number} id - 管理员ID
* @param {Object} updateData - 更新数据
* @returns {Promise<Object>} 更新后的管理员信息
*/
async updateAdmin(id, updateData) {
try {
const admin = await Admin.findById(id);
if (!admin) {
throw new Error('管理员不存在');
}
await admin.update(updateData);
return admin.toSafeObject();
} catch (error) {
console.error('更新管理员失败:', error);
throw error;
}
}
/**
* 删除管理员
* @param {number} id - 管理员ID
* @returns {Promise<boolean>} 是否删除成功
*/
async deleteAdmin(id) {
try {
const admin = await Admin.findById(id);
if (!admin) {
throw new Error('管理员不存在');
}
await admin.delete();
return true;
} catch (error) {
console.error('删除管理员失败:', error);
throw error;
}
}
}
module.exports = new AdminService();

View File

@@ -0,0 +1,262 @@
const { query } = require('../../config/database');
const { AppError } = require('../../utils/errors');
class AnimalService {
// 获取动物列表
static async getAnimals(searchParams) {
try {
const { merchantId, species, status, page = 1, pageSize = 10 } = searchParams;
const offset = (page - 1) * pageSize;
let sql = `
SELECT a.*, m.business_name as merchant_name, m.merchant_type, u.nickname as username, u.nickname as real_name
FROM animals a
INNER JOIN merchants m ON a.merchant_id = m.id
INNER JOIN users u ON m.user_id = u.id
WHERE 1=1
`;
const params = [];
if (merchantId) {
sql += ' AND a.merchant_id = ?';
params.push(merchantId);
}
if (species) {
sql += ' AND a.species = ?';
params.push(species);
}
if (status) {
sql += ' AND a.status = ?';
params.push(status);
}
// 获取总数
const countSql = `SELECT COUNT(*) as total FROM (${sql}) as count_query`;
const countResult = await query(countSql, params);
const total = countResult[0].total;
// 添加分页和排序
sql += ' ORDER BY a.created_at DESC LIMIT ? OFFSET ?';
params.push(pageSize, offset);
const animals = await query(sql, params);
return {
animals,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize)
}
};
} catch (error) {
throw error;
}
}
// 获取单个动物详情
static async getAnimalById(animalId) {
try {
const sql = `
SELECT a.*, m.business_name as merchant_name, m.merchant_type, m.contact_person as contact_info, u.nickname as username, u.nickname as real_name, u.avatar
FROM animals a
INNER JOIN merchants m ON a.merchant_id = m.id
INNER JOIN users u ON m.user_id = u.id
WHERE a.id = ?
`;
const params = [animalId];
const result = await query(sql, params);
if (result.length === 0) {
throw new AppError('动物不存在', 404);
}
return result[0];
} catch (error) {
throw error;
}
}
// 创建动物
static async createAnimal(animalData) {
try {
const sql = `
INSERT INTO animals (
merchant_id, name, species, breed, birth_date, personality, farm_location, price, status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
animalData.merchant_id,
animalData.name,
animalData.species,
animalData.breed,
animalData.birth_date,
animalData.personality,
animalData.farm_location,
animalData.price,
animalData.status || 'available'
];
const result = await query(sql, params);
const animalId = result.insertId;
return await this.getAnimalById(animalId);
} catch (error) {
throw error;
}
}
// 更新动物信息
static async updateAnimal(animalId, updateData) {
try {
// 构建动态更新语句
const fields = [];
const params = [];
Object.keys(updateData).forEach(key => {
// 只允许更新特定字段
const allowedFields = ['name', 'species', 'breed', 'birth_date', 'personality', 'farm_location', 'price', 'status'];
if (allowedFields.includes(key)) {
fields.push(`${key} = ?`);
params.push(updateData[key]);
}
});
if (fields.length === 0) {
throw new AppError('没有提供可更新的字段', 400);
}
params.push(animalId); // 为WHERE条件添加ID
const sql = `UPDATE animals SET ${fields.join(', ')} WHERE id = ?`;
await query(sql, params);
return await this.getAnimalById(animalId);
} catch (error) {
throw error;
}
}
// 删除动物
static async deleteAnimal(animalId) {
try {
const sql = 'DELETE FROM animals WHERE id = ?';
const params = [animalId];
await query(sql, params);
} catch (error) {
throw error;
}
}
// 获取动物统计信息
static async getAnimalStatistics() {
try {
// 获取动物总数
const totalSql = 'SELECT COUNT(*) as total FROM animals';
const [totalResult] = await query(totalSql);
const totalAnimals = totalResult.total;
// 按物种统计
const speciesSql = `
SELECT species, COUNT(*) as count
FROM animals
GROUP BY species
ORDER BY count DESC
`;
const speciesStats = await query(speciesSql);
// 按状态统计
const statusSql = `
SELECT status, COUNT(*) as count
FROM animals
GROUP BY status
`;
const statusStats = await query(statusSql);
// 按商家统计前5名
const merchantSql = `
SELECT m.business_name as merchant_name, COUNT(a.id) as animal_count
FROM merchants m
LEFT JOIN animals a ON m.id = a.merchant_id
GROUP BY m.id, m.business_name
ORDER BY animal_count DESC
LIMIT 5
`;
const merchantStats = await query(merchantSql);
return {
total: totalAnimals,
bySpecies: speciesStats,
byStatus: statusStats,
topMerchants: merchantStats
};
} catch (error) {
throw error;
}
}
// 搜索动物
static async searchAnimals(searchParams) {
try {
const { keyword, species, minPrice, maxPrice, page = 1, pageSize = 10 } = searchParams;
const offset = (page - 1) * pageSize;
let sql = `
SELECT a.*, m.business_name as merchant_name, m.merchant_type, u.nickname as username, u.nickname as real_name
FROM animals a
INNER JOIN merchants m ON a.merchant_id = m.id
INNER JOIN users u ON m.user_id = u.id
WHERE a.status = 'available'
`;
const params = [];
if (keyword) {
sql += ' AND (a.name LIKE ? OR a.personality LIKE ? OR a.species LIKE ?)';
params.push(`%${keyword}%`, `%${keyword}%`, `%${keyword}%`);
}
if (species) {
sql += ' AND a.species = ?';
params.push(species);
}
if (minPrice !== null) {
sql += ' AND a.price >= ?';
params.push(minPrice);
}
if (maxPrice !== null) {
sql += ' AND a.price <= ?';
params.push(maxPrice);
}
// 获取总数
const countSql = `SELECT COUNT(*) as total FROM (${sql}) as count_query`;
const countResult = await query(countSql, params);
const total = countResult[0].total;
// 添加分页和排序
sql += ' ORDER BY a.created_at DESC LIMIT ? OFFSET ?';
params.push(pageSize, offset);
const animals = await query(sql, params);
return {
animals,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize)
}
};
} catch (error) {
throw error;
}
}
}
module.exports = AnimalService;

View File

@@ -0,0 +1,368 @@
const database = require('../../config/database');
class OrderService {
/**
* 创建订单
* @param {Object} orderData - 订单数据
* @param {number} userId - 用户ID
* @returns {Promise<Object>} 创建的订单
*/
async createOrder(orderData, userId) {
try {
const {
animal_id,
merchant_id,
total_amount,
payment_method,
shipping_address,
contact_info
} = orderData;
const query = `
INSERT INTO orders (
user_id, animal_id, merchant_id, total_amount,
payment_method, shipping_address, contact_info, status
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
`;
const params = [
userId, animal_id, merchant_id, total_amount,
payment_method, shipping_address, contact_info
];
const result = await database.query(query, params);
// 获取创建的订单详情
const order = await this.getOrderById(result.insertId);
return order;
} catch (error) {
console.error('创建订单失败:', error);
throw new Error('创建订单失败');
}
}
/**
* 根据ID获取订单
* @param {number} orderId - 订单ID
* @returns {Promise<Object>} 订单信息
*/
async getOrderById(orderId) {
try {
const query = `
SELECT
o.*,
a.name as animal_name,
a.species as animal_species,
a.price as animal_price,
u.username as user_name,
m.business_name as merchant_name
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.id = ? AND o.is_deleted = 0
`;
const [order] = await database.query(query, [orderId]);
if (!order) {
throw new Error('订单不存在');
}
return order;
} catch (error) {
console.error('获取订单失败:', error);
throw error;
}
}
/**
* 获取用户订单列表
* @param {number} userId - 用户ID
* @param {Object} filters - 筛选条件
* @returns {Promise<Array>} 订单列表
*/
async getUserOrders(userId, filters = {}) {
try {
const { page = 1, pageSize = 10, status } = filters;
const offset = (page - 1) * pageSize;
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
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
`;
let countQuery = `
SELECT COUNT(*) as total
FROM orders o
WHERE o.user_id = ? AND o.is_deleted = 0
`;
const params = [userId];
const countParams = [userId];
if (status) {
query += ' AND o.status = ?';
countQuery += ' AND o.status = ?';
params.push(status);
countParams.push(status);
}
query += ' ORDER BY o.created_at DESC LIMIT ? OFFSET ?';
params.push(pageSize, offset);
const [orders] = await database.query(query, params);
const [totalResult] = await database.query(countQuery, countParams);
return {
orders,
pagination: {
page: parseInt(page),
pageSize: parseInt(pageSize),
total: totalResult.total,
totalPages: Math.ceil(totalResult.total / pageSize)
}
};
} catch (error) {
console.error('获取用户订单失败:', error);
throw new Error('获取用户订单失败');
}
}
/**
* 获取商家订单列表
* @param {number} merchantId - 商家ID
* @param {Object} filters - 筛选条件
* @returns {Promise<Array>} 订单列表
*/
async getMerchantOrders(merchantId, filters = {}) {
try {
const { page = 1, pageSize = 10, status } = filters;
const offset = (page - 1) * pageSize;
let query = `
SELECT
o.*,
a.name as animal_name,
a.species as animal_species,
a.price as animal_price,
u.username as user_name
FROM orders o
LEFT JOIN animals a ON o.animal_id = a.id
LEFT JOIN users u ON o.user_id = u.id
WHERE o.merchant_id = ? AND o.is_deleted = 0
`;
let countQuery = `
SELECT COUNT(*) as total
FROM orders o
WHERE o.merchant_id = ? AND o.is_deleted = 0
`;
const params = [merchantId];
const countParams = [merchantId];
if (status) {
query += ' AND o.status = ?';
countQuery += ' AND o.status = ?';
params.push(status);
countParams.push(status);
}
query += ' ORDER BY o.created_at DESC LIMIT ? OFFSET ?';
params.push(pageSize, offset);
const [orders] = await database.query(query, params);
const [totalResult] = await database.query(countQuery, countParams);
return {
orders,
pagination: {
page: parseInt(page),
pageSize: parseInt(pageSize),
total: totalResult.total,
totalPages: Math.ceil(totalResult.total / pageSize)
}
};
} catch (error) {
console.error('获取商家订单失败:', error);
throw new Error('获取商家订单失败');
}
}
/**
* 更新订单状态
* @param {number} orderId - 订单ID
* @param {string} status - 新状态
* @param {number} userId - 操作人ID
* @returns {Promise<Object>} 更新后的订单
*/
async updateOrderStatus(orderId, status, userId) {
try {
const validStatuses = ['pending', 'confirmed', 'shipped', 'delivered', 'cancelled'];
if (!validStatuses.includes(status)) {
throw new Error('无效的订单状态');
}
const query = `
UPDATE orders
SET status = ?, updated_by = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND is_deleted = 0
`;
const result = await database.query(query, [status, userId, orderId]);
if (result.affectedRows === 0) {
throw new Error('订单不存在或已被删除');
}
return await this.getOrderById(orderId);
} catch (error) {
console.error('更新订单状态失败:', error);
throw error;
}
}
/**
* 删除订单(软删除)
* @param {number} orderId - 订单ID
* @param {number} userId - 用户ID
* @returns {Promise<boolean>} 是否删除成功
*/
async deleteOrder(orderId, userId) {
try {
const query = `
UPDATE orders
SET is_deleted = 1, deleted_by = ?, deleted_at = CURRENT_TIMESTAMP
WHERE id = ? AND is_deleted = 0
`;
const result = await database.query(query, [userId, orderId]);
return result.affectedRows > 0;
} catch (error) {
console.error('删除订单失败:', error);
throw new Error('删除订单失败');
}
}
/**
* 获取订单统计信息
* @param {number} merchantId - 商家ID
* @returns {Promise<Object>} 统计信息
*/
async getOrderStats(merchantId) {
try {
const query = `
SELECT
COUNT(*) as total_orders,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_orders,
SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END) as confirmed_orders,
SUM(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END) as shipped_orders,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) as delivered_orders,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_orders,
SUM(total_amount) as total_revenue
FROM orders
WHERE merchant_id = ? AND is_deleted = 0
`;
const [stats] = await database.query(query, [merchantId]);
return stats;
} catch (error) {
console.error('获取订单统计失败:', error);
throw new Error('获取订单统计失败');
}
}
/**
* 获取所有订单(管理员)
* @param {Object} filters - 筛选条件
* @returns {Promise<Array>} 订单列表
*/
async getAllOrders(filters = {}) {
try {
const {
page = 1,
pageSize = 10,
status,
merchantId,
userId
} = 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
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
`;
let countQuery = `
SELECT COUNT(*) as total
FROM orders o
WHERE o.is_deleted = 0
`;
const params = [];
const countParams = [];
if (status) {
query += ' AND o.status = ?';
countQuery += ' AND o.status = ?';
params.push(status);
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 = ?';
params.push(userId);
countParams.push(userId);
}
query += ' ORDER BY o.created_at DESC LIMIT ? OFFSET ?';
params.push(pageSize, offset);
const [orders] = await database.query(query, params);
const [totalResult] = await database.query(countQuery, countParams);
return {
orders,
pagination: {
page: parseInt(page),
pageSize: parseInt(pageSize),
total: totalResult.total,
totalPages: Math.ceil(totalResult.total / pageSize)
}
};
} catch (error) {
console.error('获取所有订单失败:', error);
throw new Error('获取所有订单失败');
}
}
}
module.exports = new OrderService();

View File

@@ -0,0 +1,206 @@
const { query } = require('../../config/database');
const { AppError } = require('../../utils/errors');
class TravelService {
// 获取旅行计划列表
static async getTravelPlans(searchParams) {
try {
const { userId, page = 1, pageSize = 10, status } = searchParams;
const offset = (page - 1) * pageSize;
let sql = `
SELECT tp.*, u.username, u.real_name, u.avatar_url
FROM travel_plans tp
INNER JOIN users u ON tp.user_id = u.id
WHERE 1=1
`;
const params = [];
if (userId) {
sql += ' AND tp.user_id = ?';
params.push(userId);
}
if (status) {
sql += ' AND tp.status = ?';
params.push(status);
}
// 获取总数
const countSql = `SELECT COUNT(*) as total FROM (${sql}) as count_query`;
const countResult = await query(countSql, params);
const total = countResult[0].total;
// 添加分页和排序
sql += ' ORDER BY tp.created_at DESC LIMIT ? OFFSET ?';
params.push(pageSize, offset);
const plans = await query(sql, params);
return {
plans: plans.map(plan => this.sanitizePlan(plan)),
pagination: {
page: parseInt(page),
pageSize: parseInt(pageSize),
total: parseInt(total),
totalPages: Math.ceil(total / pageSize)
}
};
} catch (error) {
throw error;
}
}
// 获取单个旅行计划详情
static async getTravelPlanById(planId) {
try {
const sql = `
SELECT tp.*, u.username, u.real_name, u.avatar_url
FROM travel_plans tp
INNER JOIN users u ON tp.user_id = u.id
WHERE tp.id = ?
`;
const plans = await query(sql, [planId]);
if (plans.length === 0) {
throw new AppError('旅行计划不存在', 404);
}
return this.sanitizePlan(plans[0]);
} catch (error) {
throw error;
}
}
// 创建旅行计划
static async createTravelPlan(userId, planData) {
try {
const {
destination,
start_date,
end_date,
budget,
companions,
transportation,
accommodation,
activities,
notes
} = planData;
const sql = `
INSERT INTO travel_plans (
user_id, destination, start_date, end_date, budget, companions,
transportation, accommodation, activities, notes, status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'planning', NOW(), NOW())
`;
const params = [
userId,
destination,
start_date,
end_date,
budget,
companions,
transportation,
accommodation,
activities,
notes
];
const result = await query(sql, params);
return result.insertId;
} catch (error) {
throw error;
}
}
// 更新旅行计划
static async updateTravelPlan(planId, userId, updateData) {
try {
const allowedFields = [
'destination', 'start_date', 'end_date', 'budget', 'companions',
'transportation', 'accommodation', 'activities', 'notes', 'status'
];
const setClauses = [];
const params = [];
for (const [key, value] of Object.entries(updateData)) {
if (allowedFields.includes(key) && value !== undefined) {
setClauses.push(`${key} = ?`);
params.push(value);
}
}
if (setClauses.length === 0) {
throw new AppError('没有有效的更新字段', 400);
}
setClauses.push('updated_at = NOW()');
params.push(planId, userId);
const sql = `
UPDATE travel_plans
SET ${setClauses.join(', ')}
WHERE id = ? AND user_id = ?
`;
const result = await query(sql, params);
if (result.affectedRows === 0) {
throw new AppError('旅行计划不存在或没有权限修改', 404);
}
return await this.getTravelPlanById(planId);
} catch (error) {
throw error;
}
}
// 删除旅行计划
static async deleteTravelPlan(planId, userId) {
try {
const sql = 'DELETE FROM travel_plans WHERE id = ? AND user_id = ?';
const result = await query(sql, [planId, userId]);
if (result.affectedRows === 0) {
throw new AppError('旅行计划不存在或没有权限删除', 404);
}
return true;
} catch (error) {
throw error;
}
}
// 获取用户旅行统计
static async getUserTravelStats(userId) {
try {
const sql = `
SELECT
COUNT(*) as total_plans,
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_plans,
COUNT(CASE WHEN status = 'planning' THEN 1 END) as planning_plans,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled_plans,
SUM(budget) as total_budget
FROM travel_plans
WHERE user_id = ?
`;
const stats = await query(sql, [userId]);
return stats[0];
} catch (error) {
throw error;
}
}
// 安全返回旅行计划信息
static sanitizePlan(plan) {
if (!plan) return null;
const sanitized = { ...plan };
// 移除敏感信息或格式化数据
return sanitized;
}
}
module.exports = TravelService;

View File

@@ -0,0 +1,165 @@
const UserMySQL = require('../../models/UserMySQL');
const { AppError } = require('../../utils/errors');
class UserService {
// 获取用户详情
static async getUserProfile(userId) {
try {
const user = await UserMySQL.findById(userId);
if (!user) {
throw new AppError('用户不存在', 404);
}
return UserMySQL.sanitize(user);
} catch (error) {
throw error;
}
}
// 更新用户信息
static async updateUserProfile(userId, updateData) {
try {
const allowedFields = ['nickname', 'avatar', 'gender', 'birthday', 'phone', 'email'];
const filteredUpdates = {};
// 过滤允许更新的字段
Object.keys(updateData).forEach(key => {
if (allowedFields.includes(key) && updateData[key] !== undefined) {
filteredUpdates[key] = updateData[key];
}
});
if (Object.keys(filteredUpdates).length === 0) {
throw new AppError('没有有效的更新字段', 400);
}
// 检查邮箱是否已被其他用户使用
if (filteredUpdates.email) {
const emailExists = await UserMySQL.isEmailExists(filteredUpdates.email, userId);
if (emailExists) {
throw new AppError('邮箱已被其他用户使用', 400);
}
}
// 检查手机号是否已被其他用户使用
if (filteredUpdates.phone) {
const phoneExists = await UserMySQL.isPhoneExists(filteredUpdates.phone, userId);
if (phoneExists) {
throw new AppError('手机号已被其他用户使用', 400);
}
}
const success = await UserMySQL.update(userId, filteredUpdates);
if (!success) {
throw new AppError('更新用户信息失败', 500);
}
// 返回更新后的用户信息
return await this.getUserProfile(userId);
} catch (error) {
throw error;
}
}
// 搜索用户(管理员功能)
static async searchUsers(searchParams) {
try {
const { keyword, userType, page = 1, pageSize = 10 } = searchParams;
const offset = (page - 1) * pageSize;
let sql = `
SELECT id, username, user_type, real_name, avatar_url, email, phone,
created_at, updated_at, status
FROM users
WHERE 1=1
`;
const params = [];
if (keyword) {
sql += ` AND (
username LIKE ? OR
real_name LIKE ? OR
email LIKE ? OR
phone LIKE ?
)`;
const likeKeyword = `%${keyword}%`;
params.push(likeKeyword, likeKeyword, likeKeyword, likeKeyword);
}
if (userType) {
sql += ' AND user_type = ?';
params.push(userType);
}
// 获取总数
const countSql = `SELECT COUNT(*) as total FROM (${sql}) as count_query`;
const countResult = await UserMySQL.query(countSql, params);
const total = countResult[0].total;
// 添加分页和排序
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(pageSize, offset);
const users = await UserMySQL.query(sql, params);
return {
users: users.map(user => UserMySQL.sanitize(user)),
pagination: {
page: parseInt(page),
pageSize: parseInt(pageSize),
total: parseInt(total),
totalPages: Math.ceil(total / pageSize)
}
};
} catch (error) {
throw error;
}
}
// 获取用户统计信息(管理员功能)
static async getUserStatistics() {
try {
const sql = `
SELECT
COUNT(*) as total_users,
COUNT(CASE WHEN user_type = 'farmer' THEN 1 END) as farmers,
COUNT(CASE WHEN user_type = 'merchant' THEN 1 END) as merchants,
COUNT(CASE WHEN user_type = 'admin' THEN 1 END) as admins,
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_users,
COUNT(CASE WHEN status = 'inactive' THEN 1 END) as inactive_users,
DATE(created_at) as date
FROM users
GROUP BY DATE(created_at)
ORDER BY date DESC
LIMIT 30
`;
const stats = await UserMySQL.query(sql);
return stats;
} catch (error) {
throw error;
}
}
// 批量操作用户状态(管理员功能)
static async batchUpdateUserStatus(userIds, status) {
try {
if (!['active', 'inactive'].includes(status)) {
throw new AppError('无效的状态值', 400);
}
if (!userIds || userIds.length === 0) {
throw new AppError('请选择要操作的用户', 400);
}
const placeholders = userIds.map(() => '?').join(',');
const sql = `UPDATE users SET status = ?, updated_at = NOW() WHERE id IN (${placeholders})`;
const result = await UserMySQL.query(sql, [status, ...userIds]);
return result.affectedRows;
} catch (error) {
throw error;
}
}
}
module.exports = UserService;

60
check_table_structure.js Normal file
View File

@@ -0,0 +1,60 @@
const mysql = require('mysql2');
// 数据库连接配置
const dbConfig = {
host: '129.211.213.226',
port: 9527,
user: 'root',
password: 'aiotAiot123!',
database: 'jiebandata'
};
// 创建连接
const connection = mysql.createConnection(dbConfig);
// 连接数据库
connection.connect((err) => {
if (err) {
console.error('数据库连接失败: ' + err.stack);
return;
}
console.log('数据库连接成功连接ID: ' + connection.threadId);
// 检查animals表结构
connection.query('DESCRIBE animals', (error, results) => {
if (error) {
console.error('查询animals表结构失败: ' + error.stack);
connection.end();
return;
}
console.log('\n=== animals表结构 ===');
console.table(results);
// 检查merchants表结构
connection.query('DESCRIBE merchants', (error, results) => {
if (error) {
console.error('查询merchants表结构失败: ' + error.stack);
connection.end();
return;
}
console.log('\n=== merchants表结构 ===');
console.table(results);
// 检查users表结构
connection.query('DESCRIBE users', (error, results) => {
if (error) {
console.error('查询users表结构失败: ' + error.stack);
connection.end();
return;
}
console.log('\n=== users表结构 ===');
console.table(results);
connection.end();
});
});
});
});

137
db_test.js Normal file
View File

@@ -0,0 +1,137 @@
const mysql = require('mysql2');
// 数据库配置
const dbConfig = {
host: '129.211.213.226',
port: 9527,
user: 'root',
password: 'aiotAiot123!',
database: 'jiebandata'
};
// 创建连接
const connection = mysql.createConnection(dbConfig);
// 连接数据库
connection.connect((err) => {
if (err) {
console.error('数据库连接失败: ' + err.stack);
return;
}
console.log('数据库连接成功连接ID: ' + connection.threadId);
// 查询所有表
connection.query('SHOW TABLES', (error, results) => {
if (error) {
console.error('查询表失败: ' + error.stack);
connection.end();
return;
}
console.log('数据库中的所有表:');
const tables = results.map(row => {
const tableName = Object.values(row)[0];
console.log('- ' + tableName);
return tableName;
});
// 检查是否存在管理员表
const adminTableExists = tables.some(table =>
table.includes('admin') || table.includes('Admin') || table.includes('ADMIN')
);
if (adminTableExists) {
console.log('\n✅ 找到可能的管理员相关表');
// 显示管理员相关表结构
const adminTables = tables.filter(table =>
table.includes('admin') || table.includes('Admin') || table.includes('ADMIN')
);
showTableStructure(adminTables, 0);
} else {
console.log('\n❌ 未找到管理员相关表');
// 显示所有表的简要信息
showAllTablesInfo(tables, 0);
}
});
// 显示表结构的递归函数
function showTableStructure(tables, index) {
if (index >= tables.length) {
connection.end();
return;
}
const tableName = tables[index];
console.log(`\n${tableName} 表结构:`);
connection.query(`DESCRIBE ${tableName}`, (error, results) => {
if (error) {
console.error(`查询 ${tableName} 表结构失败: ` + error.stack);
} else {
console.table(results.map(row => ({
字段: row.Field,
类型: row.Type,
: row.Null,
: row.Key,
默认值: row.Default,
额外: row.Extra
})));
}
showTableStructure(tables, index + 1);
});
}
// 显示所有表信息的递归函数
function showAllTablesInfo(tables, index) {
if (index >= tables.length) {
// 创建管理员表的SQL
console.log('\n=== 建议创建的管理员表结构 ===');
const createAdminTableSQL = `
CREATE TABLE admins (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) UNIQUE,
nickname VARCHAR(100),
avatar VARCHAR(255),
role ENUM('super_admin', 'admin', 'operator') DEFAULT 'admin',
status TINYINT DEFAULT 1,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);`;
console.log(createAdminTableSQL);
// 插入测试数据的SQL
console.log('\n=== 插入测试数据 ===');
const insertTestDataSQL = `
INSERT INTO admins (username, password, email, nickname, role) VALUES
('admin', '$2b$10$rVuz/q97ocR1Zb07DzW5F.9Qx6B6HnV7JFzQb5nR1W3v7Z2mH4n6O', 'admin@example.com', '超级管理员', 'super_admin'),
('operator', '$2b$10$rVuz/q97ocR1Zb07DzW5F.9Qx6B6HnV7JFzQb5nR1W3v7Z2mH4n6O', 'operator@example.com', '操作员', 'operator');
`;
console.log(insertTestDataSQL);
connection.end();
return;
}
const tableName = tables[index];
console.log(`\n${tableName} 表信息:`);
connection.query(`SELECT COUNT(*) as count FROM ${tableName}`, (error, results) => {
if (error) {
console.error(`查询 ${tableName} 表数据量失败: ` + error.stack);
} else {
console.log(`数据量: ${results[0].count}`);
}
showAllTablesInfo(tables, index + 1);
});
}
});

85
db_update.js Normal file
View File

@@ -0,0 +1,85 @@
const mysql = require('mysql2');
// 数据库配置
const dbConfig = {
host: '129.211.213.226',
port: 9527,
user: 'root',
password: 'aiotAiot123!',
database: 'jiebandata'
};
// 创建连接
const connection = mysql.createConnection(dbConfig);
// 连接数据库
connection.connect((err) => {
if (err) {
console.error('数据库连接失败: ' + err.stack);
return;
}
console.log('数据库连接成功连接ID: ' + connection.threadId);
// 创建管理员表
const createAdminTableSQL = `
CREATE TABLE IF NOT EXISTS admins (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) UNIQUE,
nickname VARCHAR(100),
avatar VARCHAR(255),
role ENUM('super_admin', 'admin', 'operator') DEFAULT 'admin',
status TINYINT DEFAULT 1,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_role (role)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
connection.query(createAdminTableSQL, (error, results) => {
if (error) {
console.error('创建管理员表失败: ' + error.stack);
connection.end();
return;
}
console.log('✅ 管理员表创建成功');
// 检查是否已存在测试数据
connection.query('SELECT COUNT(*) as count FROM admins', (error, results) => {
if (error) {
console.error('查询管理员表数据量失败: ' + error.stack);
connection.end();
return;
}
if (results[0].count > 0) {
console.log('✅ 管理员表中已有数据,跳过插入测试数据');
connection.end();
return;
}
// 插入测试数据
const insertTestDataSQL = `
INSERT INTO admins (username, password, email, nickname, role) VALUES
('admin', '$2b$10$rVuz/q97ocR1Zb07DzW5F.9Qx6B6HnV7JFzQb5nR1W3v7Z2mH4n6O', 'admin@example.com', '超级管理员', 'super_admin'),
('operator', '$2b$10$rVuz/q97ocR1Zb07DzW5F.9Qx6B6HnV7JFzQb5nR1W3v7Z2mH4n6O', 'operator@example.com', '操作员', 'operator');
`;
connection.query(insertTestDataSQL, (error, results) => {
if (error) {
console.error('插入测试数据失败: ' + error.stack);
connection.end();
return;
}
console.log('✅ 测试数据插入成功,插入 ' + results.affectedRows + ' 条记录');
connection.end();
});
});
});
});

65
db_verify.js Normal file
View File

@@ -0,0 +1,65 @@
const mysql = require('mysql2');
// 数据库配置
const dbConfig = {
host: '129.211.213.226',
port: 9527,
user: 'root',
password: 'aiotAiot123!',
database: 'jiebandata'
};
// 创建连接
const connection = mysql.createConnection(dbConfig);
// 连接数据库
connection.connect((err) => {
if (err) {
console.error('数据库连接失败: ' + err.stack);
return;
}
console.log('数据库连接成功连接ID: ' + connection.threadId);
// 查询管理员表结构
connection.query('DESCRIBE admins', (error, results) => {
if (error) {
console.error('查询管理员表结构失败: ' + error.stack);
connection.end();
return;
}
console.log('\n=== 管理员表结构 ===');
console.table(results.map(row => ({
字段: row.Field,
类型: row.Type,
: row.Null,
: row.Key,
默认值: row.Default,
额外: row.Extra
})));
// 查询管理员数据
connection.query('SELECT * FROM admins', (error, results) => {
if (error) {
console.error('查询管理员数据失败: ' + error.stack);
connection.end();
return;
}
console.log('\n=== 管理员数据 ===');
console.table(results.map(row => ({
ID: row.id,
用户名: row.username,
邮箱: row.email,
昵称: row.nickname,
角色: row.role,
状态: row.status,
创建时间: row.created_at,
最后登录: row.last_login
})));
console.log('\n✅ 数据库验证完成');
connection.end();
});
});
});

484
docs/admin-design.md Normal file
View File

@@ -0,0 +1,484 @@
# 结伴客系统后台管理设计文档
## 1. 概述
### 1.1 设计目标
为结伴客系统提供功能完善、安全可靠的后台管理系统,支持系统管理员对用户、内容、商家、数据等进行全面管理。
### 1.2 设计原则
- **安全性**: 采用RBAC权限控制操作日志记录敏感操作二次确认
- **易用性**: 界面简洁直观,操作流程清晰
- **扩展性**: 模块化设计,支持功能扩展
- **性能**: 支持大数据量操作,响应快速
## 2. 系统架构
### 2.1 技术栈
- **前端**: Vue 3 + TypeScript + Element Plus
- **后端**: Spring Boot + MySQL + Redis
- **认证**: JWT + RBAC权限控制
- **部署**: Docker + Nginx
### 2.2 模块划分
```
后台管理系统
├── 认证模块
├── 用户管理模块
├── 内容管理模块
├── 商家管理模块
├── 数据统计模块
├── 权限管理模块
├── 系统配置模块
└── 操作日志模块
```
## 3. 功能模块设计
### 3.1 认证模块
#### 3.1.1 登录功能
- 管理员账号密码登录
- JWT Token认证
- 登录状态保持
- 安全退出
#### 3.1.2 权限验证
- 接口级别权限控制
- 页面级别权限控制
- 按钮级别权限控制
### 3.2 用户管理模块
#### 3.2.1 用户列表
- 分页显示用户信息
- 搜索过滤功能
- 用户状态管理
- 用户详情查看
#### 3.2.2 用户操作
- 启用/禁用用户
- 重置用户密码
- 查看用户行为日志
### 3.3 内容管理模块
#### 3.3.1 内容审核
- 待审核内容列表
- 内容详情查看
- 审核通过/拒绝
- 批量审核操作
#### 3.3.2 内容管理
- 已发布内容管理
- 违规内容处理
- 内容统计分析
### 3.4 商家管理模块
#### 3.4.1 商家审核
- 商家入驻申请列表
- 商家资质审核
- 审核结果通知
#### 3.4.2 商家管理
- 商家信息管理
- 商家状态控制
- 商家数据统计
### 3.5 数据统计模块
#### 3.5.1 系统概览
- 用户统计数据
- 订单统计数据
- 内容统计数据
- 商家统计数据
#### 3.5.2 趋势分析
- 用户增长趋势
- 订单趋势分析
- 收入趋势分析
- 数据图表展示
#### 3.5.3 数据导出
- CSV格式导出
- Excel格式导出
- 自定义时间范围
### 3.6 权限管理模块
#### 3.6.1 角色管理
- 角色列表查看
- 角色创建/编辑
- 角色权限分配
- 角色删除
#### 3.6.2 权限管理
- 权限列表查看
- 权限分组管理
- 权限分配
#### 3.6.3 用户角色分配
- 用户角色管理
- 批量角色分配
- 角色权限验证
### 3.7 系统配置模块
#### 3.7.1 基础配置
- 网站基本信息
- 系统参数配置
- 上传文件配置
- 邮件短信配置
#### 3.7.2 配置管理
- 配置项列表
- 配置值修改
- 配置分组管理
- 配置版本控制
### 3.8 操作日志模块
#### 3.8.1 日志查询
- 操作日志列表
- 高级搜索功能
- 日志详情查看
#### 3.8.2 日志分析
- 操作频率统计
- 异常操作检测
- 安全审计报告
## 4. 数据库设计
### 4.1 核心表结构
#### 4.1.1 管理员表 (admins)
```sql
CREATE TABLE admins (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
real_name VARCHAR(50),
avatar VARCHAR(255),
status ENUM('active', 'inactive') DEFAULT 'active',
last_login_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
#### 4.1.2 角色表 (roles)
```sql
CREATE TABLE roles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
#### 4.1.3 权限表 (permissions)
```sql
CREATE TABLE permissions (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(50) NOT NULL,
description VARCHAR(255),
category VARCHAR(50),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
#### 4.1.4 角色权限表 (role_permissions)
```sql
CREATE TABLE role_permissions (
role_id BIGINT NOT NULL,
permission_id VARCHAR(50) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);
```
#### 4.1.5 管理员角色表 (admin_roles)
```sql
CREATE TABLE admin_roles (
admin_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (admin_id, role_id),
FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
```
#### 4.1.6 管理员操作日志表 (admin_operation_logs)
```sql
CREATE TABLE admin_operation_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
admin_id BIGINT NOT NULL,
admin_name VARCHAR(50) NOT NULL,
module VARCHAR(50) NOT NULL,
operation VARCHAR(100) NOT NULL,
target_id VARCHAR(100),
target_type VARCHAR(50),
request_method VARCHAR(10),
request_url VARCHAR(500),
request_params TEXT,
ip_address VARCHAR(45),
user_agent VARCHAR(500),
status ENUM('success', 'failed') NOT NULL,
error_message TEXT,
execution_time INT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_admin_id (admin_id),
INDEX idx_module (module),
INDEX idx_operation (operation),
INDEX idx_status (status),
INDEX idx_created_at (created_at)
);
```
#### 4.1.7 系统配置表 (system_configs)
```sql
CREATE TABLE system_configs (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
value TEXT NOT NULL,
type ENUM('string', 'number', 'boolean', 'text', 'json') DEFAULT 'string',
group_name VARCHAR(50) DEFAULT 'general',
description VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_group (group_name)
);
```
### 4.2 数据关系图
```mermaid
erDiagram
admins ||--o{ admin_roles : has
roles ||--o{ admin_roles : assigned
roles ||--o{ role_permissions : has
permissions ||--o{ role_permissions : included
admins ||--o{ audit_logs : creates
admins {
bigint id
varchar username
varchar password
varchar email
varchar real_name
varchar avatar
enum status
datetime last_login_at
datetime created_at
datetime updated_at
}
roles {
bigint id
varchar name
varchar description
datetime created_at
datetime updated_at
}
permissions {
varchar id
varchar name
varchar description
varchar category
datetime created_at
}
```
## 5. API接口设计
### 5.1 认证接口
- `POST /admin/auth/login` - 管理员登录
- `GET /admin/auth/me` - 获取当前管理员信息
- `POST /admin/auth/logout` - 退出登录
- `POST /admin/auth/refresh` - 刷新Token
### 5.2 用户管理接口
- `GET /admin/users` - 获取用户列表
- `GET /admin/users/{id}` - 获取用户详情
- `PUT /admin/users/{id}/status` - 更新用户状态
### 5.3 内容管理接口
- `GET /admin/contents` - 获取内容列表
- `GET /admin/contents/{id}` - 获取内容详情
- `PUT /admin/contents/{id}/review` - 审核内容
- `POST /admin/contents/batch-review` - 批量审核内容
### 5.4 商家管理接口
- `GET /admin/merchant-applications` - 获取商家申请列表
- `PUT /admin/merchant-applications/{id}/review` - 审核商家申请
- `GET /admin/merchants` - 获取商家列表
- `PUT /admin/merchants/{id}/status` - 更新商家状态
### 5.5 数据统计接口
- `GET /admin/statistics` - 获取系统统计数据
- `GET /admin/statistics/trend` - 获取数据趋势
- `GET /admin/export/{type}` - 导出数据
### 5.6 权限管理接口
- `GET /admin/roles` - 获取角色列表
- `POST /admin/roles` - 创建角色
- `PUT /admin/roles/{id}` - 更新角色
- `DELETE /admin/roles/{id}` - 删除角色
- `GET /admin/permissions` - 获取权限列表
- `POST /admin/users/{userId}/roles` - 分配用户角色
### 5.7 系统配置接口
- `GET /admin/system-configs` - 获取系统配置
- `PUT /admin/system-configs/{id}` - 更新系统配置
- `POST /admin/system-configs/batch-update` - 批量更新配置
### 5.8 操作日志接口
- `GET /admin/operation-logs` - 获取操作日志列表
- `GET /admin/operation-logs/{id}` - 获取操作日志详情
- `GET /admin/operation-logs/export` - 导出操作日志
- `GET /admin/operation-logs/statistics` - 获取操作统计
## 6. 安全设计
### 6.1 认证安全
- JWT Token认证机制
- Token过期时间设置
- 密码加密存储BCrypt
- 登录失败次数限制
### 6.2 权限安全
- RBAC权限控制模型
- 接口级别权限验证
- 数据访问权限控制
### 6.3 操作安全
- 敏感操作二次确认
- 操作日志记录
- IP地址限制
- 操作频率限制
### 6.4 数据安全
- SQL注入防护
- XSS攻击防护
- CSRF攻击防护
- 数据加密传输HTTPS
## 7. 性能优化
### 7.1 数据库优化
- 合理的索引设计
- 查询性能优化
- 分页查询支持
- 数据库连接池
### 7.2 缓存优化
- Redis缓存应用
- 热点数据缓存
- 缓存更新策略
- 缓存失效机制
### 7.3 接口优化
- 接口响应时间优化
- 批量操作支持
- 异步处理机制
- 数据压缩传输
## 8. 部署方案
### 8.1 环境划分
- 开发环境Development
- 测试环境Testing
- 预生产环境Staging
- 生产环境Production
### 8.2 部署架构
```
负载均衡 (Nginx)
├── 后台管理前端 (Vue)
├── 后端API服务 (Spring Boot)
├── 数据库集群 (MySQL)
└── 缓存集群 (Redis)
```
### 8.3 监控告警
- 应用性能监控
- 错误日志监控
- 数据库监控
- 系统资源监控
## 9. 测试策略
### 9.1 单元测试
- 业务逻辑测试
- 服务层测试
- 工具类测试
### 9.2 集成测试
- API接口测试
- 数据库操作测试
- 权限验证测试
### 9.3 性能测试
- 并发用户测试
- 响应时间测试
- 负载能力测试
### 9.4 安全测试
- 权限绕过测试
- SQL注入测试
- XSS攻击测试
## 10. 维护计划
### 10.1 日常维护
- 日志文件清理
- 数据库备份
- 系统监控
- 性能优化
### 10.2 版本发布
- 版本控制策略
- 发布流程规范
- 回滚机制
- 版本兼容性
### 10.3 故障处理
- 故障响应流程
- 问题排查指南
- 恢复方案
- 事后总结
## 附录
### A. 权限列表
| 权限ID | 权限名称 | 描述 | 分类 |
|--------|----------|------|------|
| user:view | 查看用户 | 允许查看用户列表和详情 | 用户管理 |
| user:manage | 管理用户 | 允许修改用户状态和信息 | 用户管理 |
| content:review | 审核内容 | 允许审核用户发布的内容 | 内容管理 |
| content:publish | 发布内容 | 允许发布系统公告和内容 | 内容管理 |
| data:view | 查看数据 | 允许查看系统统计数据 | 数据统计 |
| data:export | 导出数据 | 允许导出系统数据 | 数据统计 |
| role:manage | 管理角色 | 允许管理角色和权限 | 权限管理 |
| merchant:approve | 审核商家 | 允许审核商家入驻申请 | 商家管理 |
| system:config | 系统配置 | 允许修改系统配置参数 | 系统管理 |
| audit:view | 查看日志 | 允许查看操作日志 | 日志管理 |
| audit:export | 导出日志 | 允许导出操作日志 | 日志管理 |
| audit:statistics | 日志统计 | 允许查看操作日志统计 | 日志管理 |
### B. 错误码说明
| 错误码 | 说明 | 处理建议 |
|--------|------|----------|
| 200 | 成功 | 操作成功 |
| 201 | 创建成功 | 资源创建成功 |
| 400 | 请求错误 | 检查请求参数 |
| 401 | 未授权 | 需要登录认证 |
| 403 | 禁止访问 | 权限不足 |
| 404 | 资源不存在 | 检查资源ID |
| 409 | 资源冲突 | 资源已存在 |
| 429 | 请求过多 | 降低请求频率 |
| 500 | 服务器错误 | 联系管理员 |
### C. 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| v1.0 | 2025-01-01 | 初始版本发布 |
| v1.1 | 2025-02-01 | 新增权限管理功能 |
| v1.2 | 2025-03-01 | 优化数据统计功能 |

File diff suppressed because it is too large Load Diff

View File

@@ -4,93 +4,43 @@
### 1.1 架构图
```mermaid
graph TB
subgraph "客户端层"
MP[微信小程序<br/>uni-app]
ADMIN[后台管理系统<br/>Vue.js 3 + Ant Design]
WEBSITE[官网系统<br/>HTML5 + Bootstrap]
end
**系统架构层次说明:**
subgraph "接入层"
GATEWAY[API网关<br/>Nginx + Node.js]
end
**1. 客户端层**
- **微信小程序**: 基于uni-app开发提供用户端功能
- **后台管理系统**: 基于Vue.js 3 + Ant Design开发提供管理功能
- **官网系统**: 基于HTML5 + Bootstrap开发提供企业宣传功能
subgraph "应用服务层"
AUTH[认证服务]
USER[用户服务]
TRAVEL[旅行服务]
ANIMAL[动物服务]
MERCHANT[商家服务]
PAYMENT[支付服务]
PROMOTION[推广服务]
end
**2. 接入层**
- **API网关**: 基于Nginx + Node.js提供统一的API接入和路由分发
subgraph "基础设施层"
DB[MySQL数据库<br/>主从复制]
CACHE[Redis缓存<br/>集群模式]
MQ[RabbitMQ<br/>消息队列]
STORAGE[对象存储<br/>腾讯云COS]
end
**3. 应用服务层**
- **认证服务**: 用户身份认证和权限管理
- **用户服务**: 用户信息管理和个人中心功能
- **旅行服务**: 旅行计划创建、查询和匹配功能
- **动物服务**: 动物信息管理和认领功能
- **商家服务**: 商家认证、商品和服务管理
- **支付服务**: 支付处理和交易管理
- **推广服务**: 推广活动和奖励管理
subgraph "监控运维层"
MONITOR[监控系统<br/>Prometheus + Grafana]
LOG[日志系统<br/>ELK Stack]
CI_CD[CI/CD<br/>Jenkins + Docker]
end
**4. 基础设施层**
- **MySQL数据库**: 主从复制架构,存储核心业务数据
- **Redis缓存**: 集群模式,提供高性能缓存服务
- **RabbitMQ消息队列**: 异步消息处理和解耦
- **对象存储**: 腾讯云COS存储图片和文件资源
MP --> GATEWAY
ADMIN --> GATEWAY
WEBSITE --> GATEWAY
GATEWAY --> AUTH
GATEWAY --> USER
GATEWAY --> TRAVEL
GATEWAY --> ANIMAL
GATEWAY --> MERCHANT
GATEWAY --> PAYMENT
GATEWAY --> PROMOTION
**5. 监控运维层**
- **监控系统**: Prometheus + Grafana系统性能监控
- **日志系统**: ELK Stack日志收集和分析
- **CI/CD**: Jenkins + Docker持续集成和部署
AUTH --> DB
USER --> DB
TRAVEL --> DB
ANIMAL --> DB
MERCHANT --> DB
PAYMENT --> DB
PROMOTION --> DB
AUTH --> CACHE
USER --> CACHE
TRAVEL --> CACHE
ANIMAL --> CACHE
MERCHANT --> CACHE
PAYMENT --> MQ
PROMOTION --> MQ
AUTH --> STORAGE
USER --> STORAGE
ANIMAL --> STORAGE
MERCHANT --> STORAGE
MONITOR -.-> AUTH
MONITOR -.-> USER
MONITOR -.-> TRAVEL
MONITOR -.-> ANIMAL
MONITOR -.-> MERCHANT
LOG -.-> AUTH
LOG -.-> USER
LOG -.-> TRAVEL
LOG -.-> ANIMAL
LOG -.-> MERCHANT
CI_CD -.-> AUTH
CI_CD -.-> USER
CI_CD -.-> TRAVEL
CI_CD -.-> ANIMAL
CI_CD -.-> MERCHANT
```
**架构连接关系:**
- 所有客户端通过API网关访问后端服务
- 应用服务层各服务独立部署通过API网关统一暴露接口
- 认证服务、用户服务、旅行服务、动物服务、商家服务连接MySQL数据库和Redis缓存
- 支付服务和推广服务连接MySQL数据库和RabbitMQ消息队列
- 认证服务、用户服务、动物服务、商家服务连接对象存储
- 监控系统、日志系统、CI/CD系统监控所有应用服务
## 2. 项目结构
@@ -153,7 +103,7 @@ graph TB
├── admin-system // 后台管理系统 (Vue.js 3 + TypeScript + Ant Design Vue + Pinia)
│ ├── public // 静态资源
│ ├── src
│ │ ├── api // API接口
│ │ ├── api // API接口 (TypeScript类型定义)
│ │ │ ├── user // 用户相关接口
│ │ │ ├── merchant // 商家相关接口
│ │ │ ├── travel // 旅行相关接口
@@ -162,7 +112,7 @@ graph TB
│ │ │ ├── promotion // 推广相关接口
│ │ │ └── system // 系统管理接口
│ │ ├── assets // 静态资源
│ │ ├── components // 公共组件
│ │ ├── components // 公共组件 (Vue 3 Composition API)
│ │ │ ├── layout // 布局组件
│ │ │ ├── common // 通用组件
│ │ │ ├── user // 用户相关组件
@@ -172,7 +122,7 @@ graph TB
│ │ │ ├── order // 订单相关组件
│ │ │ ├── promotion // 推广相关组件
│ │ │ └── dashboard // 仪表板组件
│ │ ├── composables // 组合式函数
│ │ ├── composables // 组合式函数 (Vue 3 Composition API)
│ │ ├── directives // 自定义指令
│ │ ├── layouts // 页面布局
│ │ │ ├── default.vue // 默认布局
@@ -181,7 +131,7 @@ graph TB
│ │ ├── locales // 国际化资源
│ │ │ ├── zh-CN.json // 中文语言包
│ │ │ └── en-US.json // 英文语言包
│ │ ├── pages // 页面视图
│ │ ├── pages // 页面视图 (Vue 3 + TypeScript)
│ │ │ ├── dashboard // 仪表板页面
│ │ │ ├── user // 用户管理页面
│ │ │ ├── merchant // 商家管理页面
@@ -193,7 +143,7 @@ graph TB
│ │ │ ├── login.vue // 登录页面
│ │ │ └── register.vue // 注册页面
│ │ ├── plugins // 插件
│ │ ├── router // 路由配置
│ │ ├── router // 路由配置 (Vue Router 4 + TypeScript)
│ │ │ ├── modules // 模块路由
│ │ │ │ ├── user.ts // 用户路由
│ │ │ │ ├── merchant.ts // 商家路由
@@ -203,7 +153,7 @@ graph TB
│ │ │ │ ├── promotion.ts // 推广路由
│ │ │ │ └── system.ts // 系统路由
│ │ │ └── index.ts // 路由入口
│ │ ├── stores // 状态管理
│ │ ├── stores // 状态管理 (Pinia 2 + TypeScript)
│ │ │ ├── modules // 模块状态
│ │ │ │ ├── user.ts // 用户状态
│ │ │ │ ├── merchant.ts // 商家状态
@@ -215,22 +165,22 @@ graph TB
│ │ │ └── index.ts // 状态管理入口
│ │ ├── styles // 样式文件
│ │ ├── types // TypeScript类型定义
│ │ ├── utils // 工具函数
│ │ │ ├── request.ts // 请求封装
│ │ ├── utils // 工具函数 (TypeScript)
│ │ │ ├── request.ts // 请求封装 (Axios + TypeScript)
│ │ │ ├── auth.ts // 认证工具
│ │ │ ├── storage.ts // 存储工具
│ │ │ ├── format.ts // 格式化工具
│ │ │ └── validate.ts // 验证工具
│ │ └── App.vue // 根组件
│ │ └── main.ts // 入口文件
│ ├── tests // 测试目录
│ │ └── App.vue // 根组件 (Vue 3 + TypeScript)
│ │ └── main.ts // 入口文件 (Vue 3 + TypeScript + Pinia + Ant Design Vue)
│ ├── tests // 测试目录 (Vitest + Vue Test Utils)
│ ├── .env // 环境配置
│ ├── .env.development // 开发环境配置
│ ├── .env.production // 生产环境配置
│ ├── index.html // HTML模板
│ ├── tsconfig.json // TypeScript配置
│ ├── vite.config.ts // 构建配置
│ └── package.json // 依赖配置
│ ├── tsconfig.json // TypeScript配置 (严格模式)
│ ├── vite.config.ts // 构建配置 (Vite 4 + TypeScript)
│ └── package.json // 依赖配置 (Vue 3 + TypeScript + Ant Design Vue + Pinia)
├── website // 官网系统 (HTML5 + Bootstrap)
│ ├── index.html // 首页
│ ├── about.html // 关于我们
@@ -325,19 +275,33 @@ graph TB
### 2.1 后端技术栈
API服务: Node.js + Express.js + TypeScript + RESTful API
数据库: MySQL
缓存系统: Redis
数据库: MySQL 8.0 (包含RBAC权限管理表结构)
缓存系统: Redis Cluster
消息队列: RabbitMQ用于异步处理
文件存储: 腾讯云对象存储
实时通信: WebSocket用于大屏数据推送和实时通知)
API文档: Swagger
实时通信: WebSocket用于实时通知和聊天功能
API文档: Swagger + OpenAPI 3.0
权限管理: JWT + RBAC (基于角色的访问控制)
### 2.2 前端技术栈
- 小程序框架uni-app
- 开发语言JavaScript/TypeScript
- UI框架WeUI
- 状态管理Redux
- 构建工具Webpack
- 包管理npm/yarn
#### 微信小程序技术栈
- **开发框架**: uni-app
- **开发语言**: JavaScript/TypeScript
- **UI框架**: WeUI
- **状态管理**: Redux
- **构建工具**: Webpack
- **包管理**: npm/yarn
#### 后台管理系统技术栈 (Vue.js 3 + TypeScript + Ant Design Vue + Pinia)
- **核心框架**: Vue.js 3.3.0 (Composition API + `<script setup>`语法)
- **开发语言**: TypeScript 5.0.0 (严格模式 + 完整类型系统)
- **UI组件库**: Ant Design Vue 4.0.0 (企业级UI设计规范)
- **状态管理**: Pinia 2.1.0 (轻量级、类型安全的Vue状态管理)
- **路由管理**: Vue Router 4.x (支持TypeScript的类型安全路由)
- **构建工具**: Vite 4.x (快速的开发服务器和热重载)
- **包管理**: npm/yarn
- **代码规范**: ESLint + Prettier (统一的代码风格和质量)
- **测试框架**: Vitest + Vue Test Utils (单元测试和组件测试)
### 2.3 官网技术栈
- 核心技术HTML5 + Bootstrap
@@ -346,7 +310,8 @@ API文档: Swagger
- 包管理npm/yarn
### 2.3 数据库选型
- 主数据库MySQL 8.0
- 主数据库MySQL 5.7
- 权限管理RBAC模型 (包含roles、permissions、role_permissions、user_roles表)
### 2.4 缓存选型
- 分布式缓存Redis Cluster
@@ -366,6 +331,7 @@ API文档: Swagger
- 生态丰富npm生态系统庞大可快速集成各种第三方库
- 全栈统一前后端均可使用JavaScript/TypeScript降低开发成本
- 轻量级Express.js是一个轻量级的Web框架灵活性高
- RBAC支持完善的权限管理中间件生态支持JWT和RBAC权限控制
- **劣势**
- CPU密集型任务处理能力较弱
- 回调地狱问题TypeScript可有效缓解
@@ -381,6 +347,19 @@ API文档: Swagger
- 平台限制:某些平台特定功能需要特殊处理
- 复杂度:多端兼容可能带来额外的复杂性
**后台管理系统 (Vue.js 3 + TypeScript + Ant Design Vue + Pinia)**
- **优势**
- **类型安全**: TypeScript提供编译时类型检查减少运行时错误
- **开发体验**: Vue 3 Composition API + `<script setup>`语法提供更好的代码组织和复用
- **企业级UI**: Ant Design Vue提供专业的企业级UI组件和设计规范
- **状态管理**: Pinia提供轻量级、类型安全的状态管理方案替代Vuex
- **构建性能**: Vite构建工具提供极快的开发服务器启动和热重载
- **代码质量**: ESLint + Prettier确保代码风格统一和质量
- **测试覆盖**: Vitest + Vue Test Utils支持完整的单元测试和组件测试
- **劣势**
- 学习曲线: TypeScript和新的Composition API需要一定的学习成本
- 包体积: 类型系统和完整工具链会增加最终的包体积
#### 官网技术栈选型理由
**HTML5 + Bootstrap**
@@ -395,14 +374,15 @@ API文档: Swagger
#### 数据库选型理由
**MySQL 8.0**
**MySQL 5.7**
- **优势**
- 成熟稳定:关系型数据库,事务支持完善
- 生态丰富:社区活跃,文档齐全
- 性能优化MySQL 8.0在性能方面有显著提升
- 广泛兼容MySQL 5.7在生产环境中广泛使用,兼容性好
- **劣势**
- 水平扩展性相对较弱
- 复杂查询性能可能不如专门的分析型数据库
- 缺少MySQL 8.0的一些新特性
#### 缓存选型理由
@@ -440,6 +420,7 @@ API文档: Swagger
外网:
├── uni-app前端用户 --- HTTPS --> API网关
├── 官网用户 --- HTTPS --> Nginx反向代理 --> 官网系统
├── 后台管理系统用户 --- HTTPS --> API网关 (Vue 3 + TypeScript + Ant Design Vue + Pinia)
└── 第三方支付平台 <-- 支付回调 --- 后端服务
腾讯云服务器:
@@ -447,8 +428,11 @@ API文档: Swagger
│ ├── 静态资源服务
│ └── SSR渲染服务 (Nuxt.js)
├── API网关 --> 后端服务
├── 后端服务 --> MySQL数据库
├── 后端服务 --> Redis缓存
├── 用户端API服务 (小程序接口)
├── 管理端API服务 (后台管理接口 - Vue 3 + TypeScript + Ant Design Vue + Pinia)
│ └── 公共服务 (认证、权限等)
├── 后端服务 --> MySQL数据库 (包含RBAC权限表)
├── 后端服务 --> Redis缓存 (会话和权限缓存)
├── 后端服务 --> RabbitMQ消息队列
└── 后端服务 --监控--> Prometheus监控 --> Grafana
|--> ELK Stack
@@ -492,12 +476,14 @@ API文档: Swagger
- 实施自动扩缩容策略,根据负载动态调整服务实例数量
#### 数据安全风险
- **风险描述**系统涉及用户个人信息、支付信息等敏感数据,存在数据泄露风险。
- **应对策略**
- **风险描述**: 系统涉及用户个人信息、支付信息等敏感数据,存在数据泄露风险。管理员后台权限控制不当可能导致数据泄露。
- **应对策略**:
- 对敏感数据进行加密存储AES/RSA算法
- 实施严格的访问控制和身份认证机制
- 采用RBAC权限模型严格控制管理员操作权限
- 定期进行安全审计和渗透测试
- 遵循GDPR等数据保护法规要求
- 管理员操作日志记录和审计
### 4.2 业务风险
@@ -509,12 +495,13 @@ API文档: Swagger
- 制定限流和降级策略,保证核心功能稳定运行
#### 商家服务质量风险
- **风险描述**商家用户提供的服务质量和用户体验直接影响平台声誉。
- **应对策略**
- **风险描述**: 商家用户提供的服务质量和用户体验直接影响平台声誉。管理员审核不严可能导致低质量商家入驻。
- **应对策略**:
- 建立商家资质审核机制
- 实施用户评价体系,公开服务评价
- 建立投诉处理机制,及时处理用户反馈
- 对低质量商家实施警告、限流或清退措施
- 管理员审核流程标准化和权限分级
### 4.3 运维风险

View File

@@ -231,6 +231,123 @@ erDiagram
| created_at | DATETIME | NOT NULL | 创建时间 |
| updated_at | DATETIME | NOT NULL | 更新时间 |
### 管理员相关表
#### ROLES 表(角色表)
| 字段名 | 类型 | 约束 | 描述 |
|--------|------|------|------|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | 角色ID |
| name | VARCHAR(50) | UNIQUE, NOT NULL | 角色名称 |
| description | TEXT | | 角色描述 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
**数据示例**:
- super_admin: 超级管理员,拥有系统所有权限
- admin: 普通管理员,拥有部分管理权限
- content_manager: 内容管理员,负责内容审核和管理
#### PERMISSIONS 表(权限表)
| 字段名 | 类型 | 约束 | 描述 |
|--------|------|------|------|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | 权限ID |
| name | VARCHAR(100) | UNIQUE, NOT NULL | 权限名称 |
| description | TEXT | | 权限描述 |
| module | VARCHAR(50) | NOT NULL | 所属模块 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
**权限列表**:
- user:manage - 用户管理权限
- role:manage - 角色管理权限
- permission:manage - 权限管理权限
- content:review - 内容审核权限
- content:publish - 内容发布权限
- data:view - 数据查看权限
- data:export - 数据导出权限
- system:config - 系统配置权限
#### ROLE_PERMISSIONS 表(角色权限关联表)
| 字段名 | 类型 | 约束 | 描述 |
|--------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 关联ID |
| role_id | INT | FOREIGN KEY REFERENCES roles(id) | 角色ID |
| permission_id | INT | FOREIGN KEY REFERENCES permissions(id) | 权限ID |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
#### USER_ROLES 表(用户角色关联表)
| 字段名 | 类型 | 约束 | 描述 |
|--------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 关联ID |
| user_id | BIGINT | FOREIGN KEY REFERENCES users(id) | 用户ID |
| role_id | INT | FOREIGN KEY REFERENCES roles(id) | 角色ID |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
### 权限分配关系
```mermaid
erDiagram
USERS ||--o{ USER_ROLES : has
ROLES ||--o{ USER_ROLES : assigned_to
ROLES ||--o{ ROLE_PERMISSIONS : has
PERMISSIONS ||--o{ ROLE_PERMISSIONS : granted_to
USERS {
bigint id
varchar username
varchar email
enum status
timestamp created_at
}
ROLES {
int id
varchar name
text description
timestamp created_at
}
PERMISSIONS {
int id
varchar name
varchar module
text description
timestamp created_at
}
USER_ROLES {
bigint id
bigint user_id
int role_id
timestamp created_at
}
ROLE_PERMISSIONS {
bigint id
int role_id
int permission_id
timestamp created_at
}
```
### 管理员功能说明
1. **用户管理模块**: 查看用户列表、管理用户状态、用户数据分析
2. **内容管理模块**: 内容审核、内容发布管理、违规内容处理
3. **数据统计模块**: 系统数据查看、数据导出、运营报表生成
4. **权限管理模块**: 角色管理、权限分配、用户权限设置
### 安全设计
- 管理员登录采用JWT令牌认证
- 接口权限基于RBAC模型控制
- 敏感操作记录审计日志
- 密码采用bcrypt加密存储
#### 商家表 (merchants)
| 字段名 | 类型 | 约束 | 描述 |
|--------|------|------|------|
@@ -289,6 +406,49 @@ erDiagram
| claimed_at | DATETIME | NOT NULL | 认领时间 |
| ended_at | DATETIME | | 结束时间 |
#### 管理员操作日志表 (admin_operation_logs)
| 字段名 | 类型 | 约束 | 描述 |
|--------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 日志ID |
| admin_id | BIGINT | FOREIGN KEY REFERENCES users(id), NOT NULL | 管理员ID |
| admin_name | VARCHAR(50) | NOT NULL | 管理员姓名 |
| module | VARCHAR(50) | NOT NULL | 操作模块 |
| operation | VARCHAR(100) | NOT NULL | 操作类型 |
| target_id | VARCHAR(100) | | 目标对象ID |
| target_type | VARCHAR(50) | | 目标对象类型 |
| request_method | VARCHAR(10) | | 请求方法 |
| request_url | VARCHAR(500) | | 请求URL |
| request_params | TEXT | | 请求参数 |
| ip_address | VARCHAR(45) | | IP地址 |
| user_agent | VARCHAR(500) | | 用户代理 |
| status | ENUM('success', 'failed') | NOT NULL | 操作状态 |
| error_message | TEXT | | 错误信息 |
| execution_time | INT | | 执行时间(ms) |
| created_at | DATETIME | NOT NULL | 创建时间 |
**操作类型示例**:
- user:create - 创建用户
- user:update - 更新用户
- user:delete - 删除用户
- role:create - 创建角色
- role:update - 更新角色
- role:delete - 删除角色
- permission:assign - 分配权限
- content:review - 内容审核
- content:publish - 内容发布
- data:export - 数据导出
- system:config - 系统配置
**目标对象类型示例**:
- user - 用户
- role - 角色
- permission - 权限
- content - 内容
- merchant - 商家
- animal - 动物
- travel_plan - 旅行计划
- order - 订单
## 2. API设计
### 2.1 用户服务API

View File

@@ -137,7 +137,13 @@
- 数据加密与安全传输: 3人日
- API访问控制与权限管理: 2人日
**后端开发总工时: 70人日**
#### 2.1.5 管理员后台服务 (15人日)
- 用户管理服务: 3人日
- 内容审核服务: 4人日
- 数据统计服务: 4人日
- 权限管理服务: 4人日
**后端开发总工时: 85人日**
### 2.2 前端开发工时
@@ -162,7 +168,16 @@
- 订单管理页面: 4人日
- 评价管理页面: 3人日
**前端开发总工时: 55人日**
#### 2.2.4 管理员后台功能开发 (25人日)
- 管理员登录页面: 2人日
- 用户管理页面: 5人日
- 内容管理页面: 5人日
- 数据统计页面: 6人日
- 权限管理页面: 7人日
**前端开发总工时: 80人日**
**项目开发总工时: 85 + 80 + 10 + 27 + 31 = 233人日**
### 2.3 数据库开发工时
@@ -246,29 +261,33 @@
### 3.2 第二阶段:核心功能开发 (预计6周)
- **时间**: 第5-10周
- **目标**: 完成用户端核心功能开发
- **交付物**:
- **交付物**:
- 用户管理功能上线
- 旅行计划功能上线
- 旅行匹配功能上线
- 动物认领功能上线
- 社交互动功能上线
- **关键任务**:
- **关键任务**:
- 后端核心业务服务开发 (25人日)
- 前端用户端功能开发 (25人日)
- 支付服务集成 (5人日)
- 管理员后台服务开发 (8人日)
### 3.3 第三阶段:商家功能开发 (预计5周)
- **时间**: 第11-15周
- **目标**: 完成商家端功能开发
- **交付物**:
- **目标**: 完成商家端功能开发和管理员后台功能开发
- **交付物**:
- 商家认证功能上线
- 商品/服务管理功能上线
- 订单处理功能上线
- 评价反馈功能上线
- **关键任务**:
- 管理员后台功能上线
- **关键任务**:
- 后端商家服务开发 (20人日)
- 前端商家端功能开发 (20人日)
- 安全服务完善 (5人日)
- 管理员后台服务开发 (7人日)
- 管理员后台前端开发 (25人日)
### 3.4 第四阶段:官网系统开发 (预计4周)
- **时间**: 第16-19周
@@ -301,13 +320,13 @@
## 4. 资源分配建议
### 4.1 人员配置
- **后端开发工程师**: 3人
- **前端开发工程师**: 3人 (新增1名官网前端开发工程师)
- **后端开发工程师**: 3人 (其中1人专注管理员后台API开发)
- **前端开发工程师**: 4人 (2人负责小程序前端1人负责官网前端1人负责管理员后台前端)
- **数据库工程师**: 1人
- **运维工程师**: 1人
- **测试工程师**: 2人
- **产品经理**: 1人
- **UI/UX设计师**: 1人 (负责官网UI/UX设计)
- **UI/UX设计师**: 2人 (1人负责小程序UI/UX设计1人负责管理员后台UI/UX设计)
### 4.2 技术资源
- **开发环境**: macOS/Linux开发机 x 8
@@ -317,17 +336,22 @@
- **项目管理**: Jira + Confluence
### 4.3 时间安排建议
- **总开发周期**: 22周 (约5.5个月)
- **并行开发**: 后端与前端可并行开发
- **总开发周期**: 25周 (约6个月增加3周用于管理员后台开发)
- **并行开发**: 后端与前端可并行开发,管理员后台开发与核心功能开发并行
- **迭代周期**: 每2周一个迭代每周进行代码评审
- **里程碑评审**: 每个阶段结束后进行里程碑评审
### 4.4 风险控制
- **技术风险**: 微服务架构复杂度高,需提前进行技术预研
- **人员风险**: 关键岗位需有备份人员
- **进度风险**: 预留2周缓冲时间应对不可预见问题
- **质量风险**: 引入自动化测试,保证代码质量
- **人员风险**: 关键岗位需有备份人员,特别是管理员后台开发人员
- **进度风险**: 预留3周缓冲时间应对不可预见问题(因新增管理员后台功能)
- **质量风险**: 引入自动化测试,保证代码质量,特别是权限管理模块
- **权限安全风险**: RBAC权限模型配置复杂需严格测试权限控制逻辑
- **官网系统风险**:
- SEO优化效果不达预期
- 浏览器兼容性问题
- 响应式布局在不同设备上显示异常
- 响应式布局在不同设备上显示异常
- **管理员后台风险**:
- 权限控制漏洞可能导致数据泄露
- 操作日志记录不完整影响审计
- 数据统计准确性需要验证

View File

@@ -383,6 +383,68 @@
- 农场老板可以查看认领情况和认领者信息
- 农场老板可以设置认领费用和条件
### 3.11 管理员后台功能
#### 故事24用户管理
**As a** 系统管理员
**I want to** 管理平台用户信息
**So that** 我可以维护平台用户质量和安全
**验收标准:**
- 管理员可以查看所有用户列表
- 管理员可以搜索和筛选用户(按注册时间、状态等)
- 管理员可以禁用/启用用户账号
- 管理员可以查看用户详细信息(注册信息、活动记录等)
- 管理员可以导出用户数据报表
#### 故事25商家审核
**As a** 系统管理员
**I want to** 审核商家入驻申请
**So that** 我可以确保商家资质合规
**验收标准:**
- 管理员可以查看待审核的商家申请列表
- 管理员可以查看商家提交的资质证明材料
- 管理员可以批准或拒绝商家申请
- 系统会向商家发送审核结果通知
- 审核通过的商家账号自动激活
#### 故事26内容审核
**As a** 系统管理员
**I want to** 审核用户发布的内容
**So that** 我可以维护平台内容质量
**验收标准:**
- 管理员可以查看待审核的内容列表(旅行计划、动态、评论等)
- 管理员可以审核并通过合规内容
- 管理员可以拒绝或删除违规内容
- 系统会记录审核操作日志
- 用户会收到内容审核结果通知
#### 故事27数据统计
**As a** 系统管理员
**I want to** 查看平台运营数据
**So that** 我可以监控平台运营状况
**验收标准:**
- 管理员可以查看用户注册趋势图表
- 管理员可以查看订单和交易统计
- 管理员可以查看各功能模块使用情况
- 管理员可以导出数据报表
- 系统提供数据可视化仪表盘
#### 故事28权限管理
**As a** 系统管理员
**I want to** 管理管理员账号权限
**So that** 我可以控制不同管理员的访问权限
**验收标准:**
- 管理员可以创建和管理其他管理员账号
- 管理员可以分配不同的权限角色
- 系统支持RBAC权限控制模型
- 权限变更会记录操作日志
- 管理员只能访问其权限范围内的功能
## 4. 非功能性需求
### 4.1 性能需求
@@ -401,6 +463,28 @@
### 4.3 兼容性需求
- 支持微信小程序平台
- 兼容不同屏幕尺寸的移动设备
- 管理员后台支持主流浏览器Chrome、Firefox、Safari、Edge
### 4.4 管理员后台特殊需求
#### 4.4.1 安全需求
- 管理员操作需要双重身份验证
- 敏感操作(用户封禁、资金操作等)需要二次确认
- 管理员登录IP地址限制和异常登录检测
- 操作日志完整记录且不可篡改
- 权限分级管理,不同角色管理员拥有不同操作权限
#### 4.4.2 性能需求
- 管理员后台页面加载时间不超过2秒
- 大数据量查询响应时间不超过5秒
- 支持同时在线管理员用户数50人
- 批量操作处理能力(如批量审核、批量导出)
#### 4.4.3 可靠性需求
- 关键管理操作支持事务回滚
- 系统异常时自动保存操作进度
- 数据备份和恢复机制
- 7×24小时运维监控
## 5. 优先级建议
@@ -426,6 +510,7 @@
- 活动组织者功能
- 农场老板功能
- 官网功能
- 管理员后台基础功能(用户管理、内容审核)
**Could Have可以有**
- 视频监控功能
@@ -528,6 +613,52 @@
- 在线入驻申请表单
- 商家成功案例和收益数据展示
#### 管理员登录页面
- 管理员账号密码登录
- 双重身份验证
- 忘记密码功能
- 安全登录提示
#### 管理员仪表盘
- 平台运营数据概览(用户数、订单数、交易额等)
- 实时数据图表展示
- 待处理事项提醒(待审核商家、待审核内容等)
- 系统状态监控
#### 用户管理页面
- 用户列表展示(支持搜索和筛选)
- 用户详细信息查看
- 账号状态管理(启用/禁用)
- 用户行为记录查看
- 数据导出功能
#### 商家审核页面
- 待审核商家列表
- 商家资质材料查看
- 审核操作(通过/拒绝)
- 审核意见填写
- 审核历史记录
#### 内容审核页面
- 待审核内容列表(旅行计划、动态、评论等)
- 内容详情查看
- 批量审核功能
- 审核标准说明
- 违规内容处理记录
#### 数据统计页面
- 用户增长趋势图表
- 订单和交易统计分析
- 各功能模块使用情况统计
- 自定义报表生成
- 数据导出和下载
#### 权限管理页面
- 管理员账号列表
- 角色权限配置
- 操作日志查看
- 权限变更记录
#### 商品管理页面
- 商品列表展示
- 添加/编辑商品功能
@@ -572,6 +703,21 @@
5. 商家处理订单
6. 服务完成,用户评价
#### 管理员审核流程:
1. 商家/用户提交申请或内容
2. 系统将待审核项加入审核队列
3. 管理员登录后台查看待处理事项
4. 管理员审核申请/内容
5. 系统记录审核结果并通知申请人
6. 审核通过的内容/申请正式生效
#### 权限管理流程:
1. 超级管理员创建新的管理员账号
2. 分配相应的权限角色
3. 管理员使用分配的权限登录后台
4. 系统根据权限控制功能访问范围
5. 所有操作记录日志供审计使用
## 7. 验收标准
### 7.1 功能验收
@@ -586,4 +732,15 @@
### 7.3 安全验收
- 安全测试通过
- 用户隐私保护符合法规要求
- 用户隐私保护符合法规要求
- 管理员后台权限控制符合RBAC模型要求
- 操作日志完整且不可篡改
- 敏感操作二次确认机制正常工作
### 7.4 管理员后台专项验收
- 所有管理员功能用户故事验收标准均已满足
- 后台页面加载性能符合要求≤2秒
- 大数据量查询响应时间符合要求≤5秒
- 批量操作功能正常工作
- 数据导出功能完整可用
- 权限分级控制准确无误

View File

@@ -77,7 +77,7 @@ CREATE TABLE IF NOT EXISTS jiebandata.travel_plans (
interests TEXT,
visibility ENUM('public', 'friends', 'private') NOT NULL DEFAULT 'public',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENTESTAMP ON UPDATE CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_user_id (user_id),
INDEX idx_destination (destination),
@@ -85,11 +85,32 @@ CREATE TABLE IF NOT EXISTS jiebandata.travel_plans (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
// 创建管理员表的SQL语句
const createAdminsTableSQL = `
CREATE TABLE IF NOT EXISTS jiebandata.admins (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
nickname VARCHAR(50),
avatar VARCHAR(255),
role VARCHAR(20) NOT NULL DEFAULT 'admin',
status TINYINT(1) NOT NULL DEFAULT 1,
last_login DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_role (role),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
// 所有表创建语句
const tableCreationSQLs = [
createUsersTableSQL,
createMerchantsTableSQL,
createTravelPlansTableSQL
createTravelPlansTableSQL,
createAdminsTableSQL
];
function executeSQL(connection, sql, description) {
@@ -149,6 +170,12 @@ async function initializeDatabase(config) {
('test_openid_2', '测试用户2', 'https://example.com/avatar2.jpg', 'female', '13800138002', 'user2@example.com')
`, '插入示例用户数据');
// 插入示例管理员
await executeSQL(connection, `
INSERT IGNORE INTO admins (username, password, email, nickname, role) VALUES
('admin', '$2a$10$rZ.r/.H0he7d.9T3.1E3qOeP.UZvF0.U6BQ35ENcQbLQzvEuh3dOq', 'admin@example.com', '超级管理员', 'admin')
`, '插入示例管理员数据');
// 验证数据插入
const [users] = await new Promise((resolve, reject) => {
connection.query('SELECT COUNT(*) as count FROM users', (err, results) => {

View File

@@ -2,7 +2,7 @@
const config = {
// 开发环境
development: {
baseURL: 'http://localhost:3000/api',
baseURL: 'http://localhost:3100/api',
timeout: 10000
},
// 生产环境

View File

@@ -38,39 +38,69 @@
</template>
<script>
import { animalService } from '../../api/services.js'
export default {
data() {
return {
animal: {
id: 1,
name: '小羊驼',
species: '羊驼',
price: 1000,
location: '西藏牧场',
images: [
'/static/animals/alpaca1.jpg',
'/static/animals/alpaca2.jpg'
],
description: '这是一只可爱的羊驼,性格温顺,喜欢与人互动。',
adoptionInfo: '认养后您将获得:\n1. 专属认养证书\n2. 定期照片和视频\n3. 牧场参观机会'
id: null,
name: '',
species: '',
price: 0,
location: '',
images: [],
description: '',
adoptionInfo: ''
}
}
},
onLoad(options) {
const id = options.id
if (id) {
this.loadAnimalDetail(id)
}
},
methods: {
handleAdopt() {
async loadAnimalDetail(id) {
try {
const data = await animalService.getDetail(id)
this.animal = data
} catch (error) {
console.error('获取动物详情失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none'
})
}
},
async handleAdopt() {
uni.showModal({
title: '确认认养',
content: '确定要认养这只可爱的' + this.animal.name + '吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
uni.showToast({
title: '认养成功',
icon: 'success'
})
try {
await animalService.adopt(this.animal.id, {})
uni.showToast({
title: '认养成功',
icon: 'success'
})
} catch (error) {
console.error('认养失败:', error)
uni.showToast({
title: '认养失败',
icon: 'none'
})
}
}
}
})
},
handleContact() {
uni.makePhoneCall({
phoneNumber: '13800138000'
@@ -111,24 +141,23 @@ export default {
}
.species {
font-size: 28rpx;
color: #666;
}
.price {
font-size: 28rpx;
color: #ff9500;
color: #ff6b35;
font-weight: bold;
}
.location {
font-size: 26rpx;
color: #999;
font-size: 28rpx;
}
.section {
padding: 30rpx;
border-top: 1rpx solid #eee;
background: #fff;
margin-bottom: 20rpx;
}
.section-title {
@@ -136,16 +165,13 @@ export default {
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.description, .adoption-info {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.adoption-info {
white-space: pre-line;
font-size: 28rpx;
}
.action-bar {
@@ -153,26 +179,25 @@ export default {
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 20rpx;
height: 120rpx;
background: #fff;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1);
display: flex;
border-top: 1rpx solid #eee;
}
.btn {
flex: 1;
margin: 0 10rpx;
font-size: 28rpx;
border: none;
font-size: 32rpx;
}
.adopt {
background-color: #ff2d55;
background: #ff6b35;
color: #fff;
}
.contact {
background-color: #fff;
color: #ff2d55;
border: 1rpx solid #ff2d55;
background: #007aff;
color: #fff;
}
</style>

View File

@@ -51,283 +51,160 @@
<uni-icons type="sound" size="16" color="#ff9500"></uni-icons>
<swiper class="notice-swiper" vertical autoplay circular :interval="3000">
<swiper-item v-for="(notice, index) in notices" :key="index">
<text class="notice-text">{{notice}}</text>
<text class="notice-text">{{ notice.title }}</text>
</swiper-item>
</swiper>
<uni-icons type="arrowright" size="14" color="#ccc"></uni-icons>
</view>
<!-- 推荐旅行计划 -->
<view class="section">
<!-- 推荐内容 -->
<view class="recommend-section">
<!-- 推荐旅行计划 -->
<view class="section-header">
<text class="section-title">推荐旅行计划</text>
<text class="section-more" @click="navigateTo('/pages/travel/list')">更多</text>
<text class="section-title">热门旅行计划</text>
<text class="more" @click="navigateTo('/pages/travel/list')">更多 ></text>
</view>
<scroll-view class="plan-list" scroll-x="true" show-scrollbar="false">
<view class="plan-card" v-for="(plan, index) in travelPlans" :key="index" @click="navigateTo(`/pages/travel/detail?id=${plan.id}`)">
<image :src="plan.coverImage" mode="aspectFill" class="plan-image" />
<view class="plan-info">
<text class="plan-destination">{{ plan.destination }}</text>
<text class="plan-date">{{ plan.startDate }} - {{ plan.endDate }}</text>
<view class="plan-meta">
<text class="plan-budget">¥{{ plan.budget }}</text>
<text class="plan-members">{{ plan.currentMembers }}/{{ plan.maxMembers }}</text>
</view>
</view>
<scroll-view class="horizontal-scroll" scroll-x="true">
<view class="scroll-item" v-for="(travel, index) in recommendedTravels" :key="index" @click="navigateToTravelDetail(travel.id)">
<image :src="travel.coverImage" class="item-image" />
<text class="item-title">{{ travel.destination }}</text>
<text class="item-price">¥{{ travel.budget }}</text>
</view>
</scroll-view>
</view>
<!-- 热门动物 -->
<view class="section">
<!-- 热门动物 -->
<view class="section-header">
<text class="section-title">热门动物</text>
<text class="section-more" @click="navigateTo('/pages/animal/list')">更多</text>
<text class="more" @click="navigateTo('/pages/animal/list')">更多 ></text>
</view>
<view class="animal-grid">
<view class="animal-card" v-for="(animal, index) in animals" :key="index" @click="navigateTo(`/pages/animal/detail?id=${animal.id}`)">
<image :src="animal.image" mode="aspectFill" class="animal-image" />
<view class="animal-tag" v-if="animal.isHot">热门</view>
<view class="animal-info">
<text class="animal-name">{{ animal.name }}</text>
<text class="animal-species">{{ animal.species }}</text>
<text class="animal-price">¥{{ animal.price }}</text>
</view>
</view>
</view>
</view>
<!-- 送花服务 -->
<view class="section">
<view class="section-header">
<text class="section-title">精选花束</text>
<text class="section-more" @click="navigateTo('/pages/flower/list')">更多</text>
</view>
<scroll-view class="flower-list" scroll-x="true" show-scrollbar="false">
<view class="flower-card" v-for="(flower, index) in flowers" :key="index" @click="navigateTo(`/pages/flower/detail?id=${flower.id}`)">
<image :src="flower.image" mode="aspectFill" class="flower-image" />
<view class="flower-info">
<text class="flower-name">{{ flower.name }}</text>
<text class="flower-desc">{{ flower.description }}</text>
<text class="flower-price">¥{{ flower.price }}</text>
</view>
<scroll-view class="horizontal-scroll" scroll-x="true">
<view class="scroll-item" v-for="(animal, index) in hotAnimals" :key="index" @click="navigateToAnimalDetail(animal.id)">
<image :src="animal.image" class="item-image" />
<text class="item-title">{{ animal.name }}</text>
<text class="item-price">¥{{ animal.price }}</text>
</view>
</scroll-view>
</view>
<!-- 底部导航栏 -->
<view class="tab-bar">
<view class="tab-item active">
<uni-icons type="home" size="24" color="#007aff"></uni-icons>
<text class="tab-text">首页</text>
</view>
<view class="tab-item" @click="navigateTo('/pages/discover/index')">
<uni-icons type="compass" size="24" color="#999"></uni-icons>
<text class="tab-text">发现</text>
</view>
<view class="tab-item" @click="navigateTo('/pages/message/index')">
<uni-icons type="chat" size="24" color="#999"></uni-icons>
<text class="tab-text">消息</text>
<view class="badge" v-if="unreadCount > 0">{{unreadCount > 99 ? '99+' : unreadCount}}</view>
</view>
<view class="tab-item" @click="navigateTo('/pages/user/center')">
<uni-icons type="person" size="24" color="#999"></uni-icons>
<text class="tab-text">我的</text>
<!-- 精选花束 -->
<view class="section-header">
<text class="section-title">精选花束</text>
<text class="more" @click="navigateTo('/pages/flower/list')">更多 ></text>
</view>
<scroll-view class="horizontal-scroll" scroll-x="true">
<view class="scroll-item" v-for="(flower, index) in featuredFlowers" :key="index" @click="navigateToFlowerDetail(flower.id)">
<image :src="flower.image" class="item-image" />
<text class="item-title">{{ flower.name }}</text>
<text class="item-price">¥{{ flower.price }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { homeService } from '../../api/services.js'
export default {
data() {
return {
banners: [
{ image: '/static/banners/banner1.jpg', link: '/pages/travel/list' },
{ image: '/static/banners/banner2.jpg', link: '/pages/animal/list' },
{ image: '/static/banners/banner3.jpg', link: '/pages/flower/list' }
],
notices: [
'欢迎来到结伴客,找到志同道合的旅伴!',
'新用户注册即送50积分可兑换精美礼品',
'推荐好友加入双方各得30积分奖励'
],
travelPlans: [
{
id: 1,
destination: '西藏拉萨',
startDate: '10月1日',
endDate: '10月7日',
budget: 5000,
currentMembers: 3,
maxMembers: 6,
coverImage: '/static/travel/tibet.jpg'
},
{
id: 2,
destination: '云南大理',
startDate: '10月5日',
endDate: '10月12日',
budget: 3500,
currentMembers: 2,
maxMembers: 4,
coverImage: '/static/travel/yunnan.jpg'
},
{
id: 3,
destination: '新疆喀什',
startDate: '10月10日',
endDate: '10月20日',
budget: 6000,
currentMembers: 4,
maxMembers: 8,
coverImage: '/static/travel/xinjiang.jpg'
},
{
id: 4,
destination: '青海湖',
startDate: '10月15日',
endDate: '10月20日',
budget: 4000,
currentMembers: 2,
maxMembers: 5,
coverImage: '/static/travel/qinghai.jpg'
}
],
animals: [
{
id: 1,
name: '小羊驼',
species: '羊驼',
price: 1000,
isHot: true,
image: '/static/animals/alpaca.jpg'
},
{
id: 2,
name: '小绵羊',
species: '绵羊',
price: 800,
isHot: false,
image: '/static/animals/sheep.jpg'
},
{
id: 3,
name: '小花猪',
species: '猪',
price: 600,
isHot: true,
image: '/static/animals/pig.jpg'
},
{
id: 4,
name: '小公鸡',
species: '鸡',
price: 300,
isHot: false,
image: '/static/animals/chicken.jpg'
}
],
flowers: [
{
id: 1,
name: '浪漫玫瑰',
description: '11朵红玫瑰',
price: 199,
image: '/static/flowers/rose.jpg'
},
{
id: 2,
name: '向日葵花束',
description: '9朵向日葵',
price: 179,
image: '/static/flowers/sunflower.jpg'
},
{
id: 3,
name: '百合花束',
description: '7朵白百合',
price: 229,
image: '/static/flowers/lily.jpg'
}
],
unreadCount: 5
banners: [],
notices: [],
recommendedTravels: [],
hotAnimals: [],
featuredFlowers: []
}
},
onLoad() {
// 获取系统信息设置状态栏高度
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight;
// 加载数据
this.loadData();
onShow() {
this.loadHomeData()
},
methods: {
loadData() {
// 实际开发中这里应该从API获取数据
console.log('加载首页数据');
async loadHomeData() {
try {
// 获取轮播图
const banners = await homeService.getBanners()
this.banners = banners
// 获取公告
const notices = await homeService.getNotices()
this.notices = notices
// 获取推荐旅行计划
const travels = await homeService.getRecommendedTravels()
this.recommendedTravels = travels
// 获取热门动物
const animals = await homeService.getHotAnimals()
this.hotAnimals = animals
// 获取精选花束
const flowers = await homeService.getFeaturedFlowers()
this.featuredFlowers = flowers
} catch (error) {
console.error('获取首页数据失败:', error)
uni.showToast({
title: '数据加载失败',
icon: 'none'
})
}
},
navigateTo(url) {
uni.navigateTo({
url: url
});
uni.navigateTo({ url })
},
navigateToSearch() {
uni.navigateTo({
url: '/pages/search/index'
});
uni.navigateTo({ url: '/pages/search/index' })
},
navigateToTravelDetail(id) {
uni.navigateTo({ url: `/pages/travel/detail?id=${id}` })
},
navigateToAnimalDetail(id) {
uni.navigateTo({ url: `/pages/animal/detail?id=${id}` })
},
navigateToFlowerDetail(id) {
uni.navigateTo({ url: `/pages/flower/detail?id=${id}` })
}
}
}
</script>
<style>
/* 移除图标字体相关样式 */
/* 页面样式 */
<style scoped>
.container {
min-height: 100vh;
background-color: #f8f9fa;
padding-bottom: 100rpx; /* 为底部导航留出空间 */
padding-bottom: 20rpx;
}
.status-bar {
height: var(--status-bar-height);
width: 100%;
background-color: #fff;
}
.search-bar {
padding: 20rpx 30rpx;
background-color: #ffffff;
background-color: #fff;
}
.search-input {
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 50rpx;
padding: 15rpx 30rpx;
}
.search-input .iconfont {
font-size: 32rpx;
color: #999;
margin-right: 10rpx;
}
.search-input input {
flex: 1;
font-size: 28rpx;
height: 60rpx;
background-color: #f0f0f0;
border-radius: 30rpx;
padding: 0 20rpx;
}
.placeholder {
font-size: 28rpx;
color: #999;
}
.banner-swiper {
height: 300rpx;
margin: 0 30rpx 30rpx;
border-radius: 20rpx;
overflow: hidden;
}
.banner-image {
@@ -337,21 +214,21 @@ export default {
.feature-grid {
display: flex;
justify-content: space-around;
padding: 20rpx 30rpx 30rpx;
background-color: #ffffff;
background-color: #fff;
padding: 30rpx 0;
margin-bottom: 20rpx;
}
.feature-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.feature-icon {
width: 100rpx;
height: 100rpx;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
@@ -359,40 +236,20 @@ export default {
margin-bottom: 15rpx;
}
.feature-icon .iconfont {
font-size: 50rpx;
.travel {
background-color: #e6f1ff;
}
.feature-icon.travel {
background-color: rgba(0, 122, 255, 0.1);
.animal {
background-color: #ffe6eb;
}
.feature-icon.travel .iconfont {
color: #007aff;
.flower {
background-color: #fff5e6;
}
.feature-icon.animal {
background-color: rgba(255, 45, 85, 0.1);
}
.feature-icon.animal .iconfont {
color: #ff2d55;
}
.feature-icon.flower {
background-color: rgba(255, 149, 0, 0.1);
}
.feature-icon.flower .iconfont {
color: #ff9500;
}
.feature-icon.promotion {
background-color: rgba(52, 199, 89, 0.1);
}
.feature-icon.promotion .iconfont {
color: #34c759;
.promotion {
background-color: #e6f9ed;
}
.feature-text {
@@ -403,285 +260,80 @@ export default {
.notice-bar {
display: flex;
align-items: center;
background-color: #fff8e6;
padding: 15rpx 30rpx;
margin: 0 30rpx 20rpx;
border-radius: 10rpx;
}
.notice-bar .iconfont {
font-size: 32rpx;
color: #ff9500;
margin-right: 15rpx;
background-color: #fff;
margin-bottom: 20rpx;
}
.notice-swiper {
height: 40rpx;
flex: 1;
height: 40rpx;
margin: 0 10rpx;
}
.notice-text {
font-size: 24rpx;
font-size: 26rpx;
color: #ff9500;
line-height: 40rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.section {
margin-bottom: 30rpx;
background-color: #ffffff;
padding: 30rpx 0;
.recommend-section {
background-color: #fff;
padding: 0 30rpx 30rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 30rpx 20rpx;
margin: 30rpx 0 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
position: relative;
padding-left: 20rpx;
}
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 30rpx;
background-color: #007aff;
border-radius: 4rpx;
}
.section-more {
font-size: 24rpx;
color: #999;
}
.plan-list {
white-space: nowrap;
padding: 0 30rpx;
}
.plan-card {
display: inline-block;
width: 300rpx;
margin-right: 20rpx;
background: #ffffff;
border-radius: 15rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.plan-image {
width: 100%;
height: 180rpx;
}
.plan-info {
padding: 15rpx;
}
.plan-destination {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.plan-date {
display: block;
font-size: 22rpx;
color: #666;
margin-bottom: 10rpx;
}
.plan-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.plan-budget {
.more {
font-size: 26rpx;
color: #ff9500;
font-weight: bold;
}
.plan-members {
font-size: 22rpx;
color: #999;
}
.animal-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
padding: 0 30rpx;
.horizontal-scroll {
white-space: nowrap;
margin-bottom: 30rpx;
}
.animal-card {
background: #ffffff;
border-radius: 15rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
position: relative;
.scroll-item {
display: inline-block;
width: 200rpx;
margin-right: 20rpx;
vertical-align: top;
}
.animal-image {
width: 100%;
.item-image {
width: 200rpx;
height: 200rpx;
}
.animal-tag {
position: absolute;
top: 15rpx;
right: 15rpx;
background-color: #ff2d55;
color: #fff;
font-size: 20rpx;
padding: 5rpx 10rpx;
border-radius: 10rpx;
}
.animal-info {
padding: 15rpx;
}
.animal-name {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
}
.animal-species {
display: block;
font-size: 22rpx;
color: #999;
margin-bottom: 10rpx;
}
.animal-price {
.item-title {
display: block;
font-size: 26rpx;
color: #ff9500;
font-weight: bold;
}
.flower-list {
white-space: nowrap;
padding: 0 30rpx;
}
.flower-card {
display: inline-block;
width: 240rpx;
margin-right: 20rpx;
background: #ffffff;
border-radius: 15rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.flower-image {
width: 100%;
height: 240rpx;
}
.flower-info {
padding: 15rpx;
}
.flower-name {
display: block;
font-size: 26rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
}
.flower-desc {
display: block;
font-size: 22rpx;
color: #999;
margin-bottom: 10rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flower-price {
display: block;
font-size: 26rpx;
color: #ff9500;
.item-price {
font-size: 24rpx;
color: #ff6b35;
font-weight: bold;
}
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #ffffff;
display: flex;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
.tab-item .iconfont {
font-size: 40rpx;
color: #999;
margin-bottom: 5rpx;
}
.tab-item.active .iconfont {
color: #007aff;
}
.tab-text {
font-size: 20rpx;
color: #999;
}
.tab-item.active .tab-text {
color: #007aff;
}
.badge {
position: absolute;
top: 0;
right: 50%;
transform: translateX(10rpx);
background-color: #ff2d55;
color: #fff;
font-size: 20rpx;
min-width: 32rpx;
height: 32rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 6rpx;
}
</style>

View File

@@ -36,36 +36,69 @@
</template>
<script>
import { travelService } from '../../api/services.js'
export default {
data() {
return {
plan: {
id: 1,
destination: '西藏',
startDate: '10月1日',
endDate: '10月7日',
budget: 5000,
coverImage: '/static/travel/tibet.jpg',
schedule: [
'拉萨市区游览,参观布达拉宫、大昭寺',
'前往纳木错,欣赏圣湖美景',
'林芝地区游览,参观雅鲁藏布大峡谷',
'返回拉萨,自由活动',
'日喀则地区游览,参观扎什伦布寺',
'珠峰大本营一日游',
'返回拉萨,结束行程'
],
requirements: '1. 年龄18-35岁\n2. 身体健康,能适应高原环境\n3. 有团队精神,乐于分享'
id: null,
destination: '',
startDate: '',
endDate: '',
budget: 0,
coverImage: '',
schedule: [],
requirements: ''
}
}
},
onLoad(options) {
const id = options.id
if (id) {
this.loadTravelDetail(id)
}
},
methods: {
handleJoin() {
uni.showToast({
title: '已申请加入',
icon: 'success'
async loadTravelDetail(id) {
try {
const data = await travelService.getDetail(id)
this.plan = data
} catch (error) {
console.error('获取旅行计划详情失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none'
})
}
},
async handleJoin() {
uni.showModal({
title: '确认加入',
content: '确定要加入这个旅行计划吗?',
success: async (res) => {
if (res.confirm) {
try {
await travelService.join(this.plan.id)
uni.showToast({
title: '申请已提交',
icon: 'success'
})
} catch (error) {
console.error('加入计划失败:', error)
uni.showToast({
title: '申请失败',
icon: 'none'
})
}
}
}
})
},
handleContact() {
uni.makePhoneCall({
phoneNumber: '13800138000'
@@ -99,12 +132,12 @@ export default {
justify-content: space-between;
margin-top: 20rpx;
color: #666;
font-size: 28rpx;
}
.section {
padding: 30rpx;
border-top: 1rpx solid #eee;
background: #fff;
margin-bottom: 20rpx;
}
.section-title {
@@ -112,28 +145,27 @@ export default {
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.schedule {
margin-bottom: 30rpx;
margin-bottom: 20rpx;
}
.day {
font-size: 28rpx;
font-weight: bold;
color: #007aff;
margin-bottom: 10rpx;
display: block;
}
.content {
font-size: 28rpx;
color: #666;
line-height: 1.6;
line-height: 1.5;
}
.requirements {
font-size: 28rpx;
color: #666;
white-space: pre-line;
line-height: 1.6;
}
@@ -142,26 +174,25 @@ export default {
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 20rpx;
height: 100rpx;
background: #fff;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1);
display: flex;
border-top: 1rpx solid #eee;
}
.btn {
flex: 1;
margin: 0 10rpx;
font-size: 28rpx;
border: none;
font-size: 32rpx;
}
.join {
background-color: #007aff;
background: #007aff;
color: #fff;
}
.contact {
background-color: #fff;
color: #007aff;
border: 1rpx solid #007aff;
background: #34c759;
color: #fff;
}
</style>

View File

@@ -39,20 +39,22 @@
</template>
<script>
import { userService, orderService } from '../../api/services.js'
export default {
data() {
return {
user: {
avatar: '/static/user/avatar.jpg',
nickname: '旅行爱好者',
memberLevel: '黄金会员'
nickname: '',
memberLevel: ''
},
orderStatus: [
{ type: 'all', text: '全部订单', count: 5 },
{ type: 'unpaid', text: '待付款', count: 1 },
{ type: 'undelivered', text: '待发货', count: 1 },
{ type: 'delivered', text: '待收货', count: 2 },
{ type: 'completed', text: '已完成', count: 1 }
{ type: 'all', text: '全部订单', count: 0 },
{ type: 'unpaid', text: '待付款', count: 0 },
{ type: 'undelivered', text: '待发货', count: 0 },
{ type: 'delivered', text: '待收货', count: 0 },
{ type: 'completed', text: '已完成', count: 0 }
],
functions: [
{ icon: 'heart', text: '我的认养', url: '/pages/user/adoptions' },
@@ -63,10 +65,45 @@ export default {
]
}
},
onShow() {
this.loadUserProfile()
this.loadOrderStats()
},
methods: {
async loadUserProfile() {
try {
const data = await userService.getProfile()
this.user = {
...this.user,
...data
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
},
async loadOrderStats() {
try {
// 这里应该调用一个专门获取订单统计的接口
// 暂时使用模拟数据
/*
const stats = await orderService.getStats()
this.orderStatus = this.orderStatus.map(item => ({
...item,
count: stats[item.type] || 0
}))
*/
} catch (error) {
console.error('获取订单统计失败:', error)
}
},
navigateTo(url) {
uni.navigateTo({ url })
},
navigateToOrder(type) {
uni.navigateTo({ url: `/pages/user/orders?type=${type}` })
}
@@ -103,16 +140,13 @@ export default {
.username {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.member-level {
font-size: 24rpx;
color: #ff9500;
background-color: #fff8e6;
padding: 4rpx 16rpx;
border-radius: 20rpx;
align-self: flex-start;
font-size: 28rpx;
color: #ff6b35;
}
.order-status {
@@ -130,7 +164,8 @@ export default {
}
.count {
font-size: 32rpx;
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
@@ -148,13 +183,17 @@ export default {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
border-bottom: 1rpx solid #eee;
}
.function-item .text {
.function-item:last-child {
border-bottom: none;
}
.text {
flex: 1;
margin-left: 20rpx;
font-size: 28rpx;
font-size: 30rpx;
color: #333;
}
</style>

View File

@@ -11,7 +11,7 @@
>
<text>{{ tab.text }}</text>
</view>
</view>
</tab-bar>
<!-- 订单列表 -->
<scroll-view
@@ -49,256 +49,191 @@
</view>
</view>
<!-- 订单底部 -->
<view class="order-footer">
<text class="order-total">{{ order.count }}件商品 合计¥{{ order.price * order.count }}</text>
<view class="order-actions">
<button
v-if="order.status === 'unpaid'"
class="action-btn primary"
@click.stop="payOrder(order.id)"
>立即付款</button>
<button
v-if="order.status === 'delivered'"
class="action-btn primary"
@click.stop="confirmReceive(order.id)"
>确认收货</button>
<button
v-if="order.status === 'completed'"
class="action-btn"
@click.stop="reviewOrder(order.id)"
>评价</button>
<button
class="action-btn"
@click.stop="deleteOrder(order.id)"
>删除订单</button>
</view>
<!-- 订单操作 -->
<view class="order-actions">
<button
v-for="action in getAvailableActions(order.status)"
:key="action.type"
class="action-btn"
:class="action.type"
@click.stop="handleAction(action.type, order)"
>
{{ action.text }}
</button>
</view>
</view>
<!-- 加载更多 -->
<view v-if="loading" class="loading">
<!-- 加载更多提示 -->
<view class="load-more" v-if="loading">
<text>加载中...</text>
</view>
<view v-if="noMore && orders.length > 0" class="no-more">
<text>没有更多订单了</text>
<view class="load-more" v-else-if="hasMore">
<text>上拉加载更多</text>
</view>
<view class="load-more" v-else-if="orders.length > 0">
<text>没有更多了</text>
</view>
</scroll-view>
</view>
</template>
<script>
import { orderService } from '../../api/services.js'
export default {
data() {
return {
tabs: [
{ text: '全部', type: 'all' },
{ text: '待付款', type: 'unpaid' },
{ text: '待发货', type: 'undelivered' },
{ text: '待收货', type: 'delivered' },
{ text: '已完成', type: 'completed' }
],
currentTab: 0,
tabs: [
{ type: 'all', text: '全部' },
{ type: 'unpaid', text: '待付款' },
{ type: 'undelivered', text: '待发货' },
{ type: 'delivered', text: '待收货' },
{ type: 'completed', text: '已完成' }
],
orders: [],
page: 1,
loading: false,
noMore: false
hasMore: true,
page: 1,
pageSize: 10,
statusFilter: 'all'
}
},
onLoad(options) {
// 如果有传入类型参数,切换到对应选项卡
if (options.type) {
const index = this.tabs.findIndex(tab => tab.type === options.type)
if (index !== -1) {
this.currentTab = index
const tabIndex = this.tabs.findIndex(tab => tab.type === options.type)
if (tabIndex !== -1) {
this.currentTab = tabIndex
this.statusFilter = options.type
}
}
this.loadOrders()
},
methods: {
switchTab(index) {
if (this.currentTab === index) return
this.currentTab = index
this.page = 1
this.orders = []
this.noMore = false
this.loadOrders()
},
loadOrders() {
if (this.loading || this.noMore) return
async loadOrders() {
if (this.loading || !this.hasMore) return
this.loading = true
// 模拟API请求
setTimeout(() => {
const type = this.tabs[this.currentTab].type
const newOrders = this.getMockOrders(type, this.page)
try {
const params = {
page: this.page,
pageSize: this.pageSize
}
if (newOrders.length === 0) {
this.noMore = true
if (this.statusFilter !== 'all') {
params.status = this.statusFilter
}
const data = await orderService.getList(params)
const newOrders = data.orders || data
if (this.page === 1) {
this.orders = newOrders
} else {
this.orders = [...this.orders, ...newOrders]
this.page++
}
this.hasMore = newOrders.length === this.pageSize
this.page++
} catch (error) {
console.error('获取订单列表失败:', error)
uni.showToast({
title: '获取订单失败',
icon: 'none'
})
} finally {
this.loading = false
}, 1000)
}
},
loadMore() {
async loadMore() {
if (this.hasMore && !this.loading) {
this.loadOrders()
}
},
switchTab(index) {
this.currentTab = index
this.statusFilter = this.tabs[index].type
this.page = 1
this.orders = []
this.hasMore = true
this.loadOrders()
},
getMockOrders(type, page) {
// 模拟数据实际应从API获取
const allOrders = [
{
id: '1001',
type: 'travel',
status: 'unpaid',
title: '西藏旅行计划',
description: '10月1日-10月7日',
price: 5000,
count: 1,
image: '/static/travel/tibet.jpg'
},
{
id: '1002',
type: 'animal',
status: 'undelivered',
title: '小羊驼认养',
description: '认养期限1年',
price: 1000,
count: 1,
image: '/static/animals/alpaca.jpg'
},
{
id: '1003',
type: 'flower',
status: 'delivered',
title: '浪漫玫瑰花束',
description: '11朵红玫瑰',
price: 199,
count: 1,
image: '/static/flowers/rose.jpg'
},
{
id: '1004',
type: 'flower',
status: 'completed',
title: '向日葵花束',
description: '9朵向日葵',
price: 179,
count: 1,
image: '/static/flowers/sunflower.jpg'
}
]
// 根据类型筛选
let filteredOrders = allOrders
if (type !== 'all') {
filteredOrders = allOrders.filter(order => order.status === type)
}
// 分页
const pageSize = 5
const start = (page - 1) * pageSize
const end = page * pageSize
return filteredOrders.slice(start, end)
},
getOrderTypeName(type) {
const typeMap = {
const types = {
travel: '旅行计划',
animal: '动物认养',
flower: '送花服务'
}
return typeMap[type] || '订单'
return types[type] || '未知类型'
},
getStatusText(status) {
const statusMap = {
const statuses = {
unpaid: '待付款',
undelivered: '待发货',
delivered: '待收货',
completed: '已完成'
completed: '已完成',
cancelled: '已取消'
}
return statusMap[status] || '未知状态'
return statuses[status] || '未知状态'
},
getAvailableActions(status) {
const actions = {
unpaid: [
{ type: 'cancel', text: '取消订单' },
{ type: 'pay', text: '立即付款' }
],
undelivered: [
{ type: 'cancel', text: '取消订单' }
],
delivered: [
{ type: 'confirm', text: '确认收货' }
],
completed: [],
cancelled: []
}
return actions[status] || []
},
async handleAction(actionType, order) {
try {
switch (actionType) {
case 'cancel':
await orderService.cancel(order.id)
uni.showToast({ title: '订单已取消' })
break
case 'pay':
// 跳转到支付页面
uni.navigateTo({ url: `/pages/payment/index?orderId=${order.id}` })
return
case 'confirm':
await orderService.confirm(order.id)
uni.showToast({ title: '已确认收货' })
break
}
// 重新加载订单列表
this.page = 1
this.orders = []
this.hasMore = true
this.loadOrders()
} catch (error) {
console.error('操作失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
},
navigateToDetail(id) {
uni.navigateTo({
url: `/pages/user/order-detail?id=${id}`
})
},
payOrder(id) {
uni.showModal({
title: '支付提示',
content: '确定要支付此订单吗?',
success: (res) => {
if (res.confirm) {
// 模拟支付流程
uni.showLoading({
title: '支付中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '支付成功',
icon: 'success'
})
// 更新订单状态
this.updateOrderStatus(id, 'undelivered')
}, 1500)
}
}
})
},
confirmReceive(id) {
uni.showModal({
title: '确认收货',
content: '确认已收到商品吗?',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '确认成功',
icon: 'success'
})
// 更新订单状态
this.updateOrderStatus(id, 'completed')
}
}
})
},
reviewOrder(id) {
uni.navigateTo({
url: `/pages/user/review?id=${id}`
})
},
deleteOrder(id) {
uni.showModal({
title: '删除订单',
content: '确定要删除此订单吗?',
success: (res) => {
if (res.confirm) {
// 从列表中移除
const index = this.orders.findIndex(order => order.id === id)
if (index !== -1) {
this.orders.splice(index, 1)
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
}
})
},
updateOrderStatus(id, newStatus) {
const order = this.orders.find(order => order.id === id)
if (order) {
order.status = newStatus
}
uni.navigateTo({ url: `/pages/order/detail?id=${id}` })
}
}
}
@@ -306,101 +241,78 @@ export default {
<style scoped>
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f8f9fa;
min-height: 100vh;
}
.tab-bar {
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #eee;
position: sticky;
top: 0;
z-index: 10;
}
.tab-item {
flex: 1;
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 30rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
}
.tab-item.active {
color: #007aff;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #007aff;
border-bottom: 4rpx solid #007aff;
}
.order-list {
flex: 1;
padding: 20rpx;
height: calc(100vh - 100rpx);
}
.empty-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
margin-bottom: 30rpx;
}
.order-item {
background-color: #fff;
margin: 20rpx;
border-radius: 10rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.order-header {
display: flex;
justify-content: space-between;
padding: 20rpx;
border-bottom: 1rpx solid #f5f5f5;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #eee;
}
.order-type {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.order-status {
font-size: 28rpx;
color: #ff9500;
color: #ff6b35;
}
.order-content {
display: flex;
padding: 20rpx;
border-bottom: 1rpx solid #f5f5f5;
padding: 30rpx;
}
.order-image {
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
width: 150rpx;
height: 150rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
@@ -408,73 +320,65 @@ export default {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.order-title {
font-size: 28rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.order-desc {
font-size: 24rpx;
color: #999;
font-size: 24rpx;
margin-bottom: 10rpx;
}
.order-price-box {
display: flex;
justify-content: space-between;
margin-top: auto;
}
.order-price {
font-size: 28rpx;
color: #ff2d55;
color: #ff6b35;
font-weight: bold;
}
.order-count {
font-size: 24rpx;
color: #999;
}
.order-footer {
padding: 20rpx;
}
.order-total {
font-size: 24rpx;
color: #666;
text-align: right;
margin-bottom: 20rpx;
}
.order-actions {
display: flex;
justify-content: flex-end;
padding: 20rpx 30rpx;
border-top: 1rpx solid #eee;
}
.action-btn {
padding: 10rpx 20rpx;
margin-left: 20rpx;
font-size: 24rpx;
padding: 0 30rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 6rpx;
font-size: 26rpx;
border: 1rpx solid #ddd;
background-color: #fff;
color: #666;
}
.action-btn.primary {
background-color: #007aff;
color: #fff;
border: none;
.action-btn.cancel {
color: #ff3b30;
border-color: #ff3b30;
}
.loading, .no-more {
.action-btn.pay, .action-btn.confirm {
color: #007aff;
border-color: #007aff;
}
.load-more {
text-align: center;
padding: 20rpx 0;
font-size: 24rpx;
padding: 30rpx;
color: #999;
font-size: 26rpx;
}
</style>

View File

@@ -0,0 +1 @@
{"version":3,"file":"config.js","sources":["api/config.js"],"sourcesContent":["// API基础配置\nconst config = {\n // 开发环境\n development: {\n baseURL: 'http://localhost:3100/api',\n timeout: 10000\n },\n // 生产环境\n production: {\n baseURL: 'https://api.jiebanke.com/api',\n timeout: 15000\n }\n}\n\n// 获取当前环境配置\nconst getConfig = () => {\n const env = process.env.NODE_ENV || 'development'\n return config[env]\n}\n\n// API端点\nconst endpoints = {\n // 用户相关\n USER: {\n LOGIN: '/auth/login',\n REGISTER: '/auth/register',\n PROFILE: '/user/profile',\n UPDATE_PROFILE: '/user/profile',\n UPLOAD_AVATAR: '/user/avatar'\n },\n \n // 旅行计划\n TRAVEL: {\n LIST: '/travel/list',\n DETAIL: '/travel/detail',\n CREATE: '/travel/create',\n JOIN: '/travel/join',\n MY_PLANS: '/travel/my-plans',\n SEARCH: '/travel/search'\n },\n \n // 动物认养\n ANIMAL: {\n LIST: '/animal/list',\n DETAIL: '/animal/detail',\n ADOPT: '/animal/adopt',\n MY_ANIMALS: '/animal/my-animals',\n CATEGORIES: '/animal/categories'\n },\n \n // 送花服务\n FLOWER: {\n LIST: '/flower/list',\n DETAIL: '/flower/detail',\n ORDER: '/flower/order',\n MY_ORDERS: '/flower/my-orders',\n CATEGORIES: '/flower/categories'\n },\n \n // 订单管理\n ORDER: {\n LIST: '/order/list',\n DETAIL: '/order/detail',\n CANCEL: '/order/cancel',\n PAY: '/order/pay',\n CONFIRM: '/order/confirm'\n },\n \n // 支付相关\n PAYMENT: {\n CREATE: '/payment/create',\n QUERY: '/payment/query',\n REFUND: '/payment/refund'\n },\n \n // 系统相关\n SYSTEM: {\n CONFIG: '/system/config',\n NOTICE: '/system/notice',\n FEEDBACK: '/system/feedback'\n }\n}\n\nexport default {\n ...getConfig(),\n endpoints\n}"],"names":[],"mappings":";AACA,MAAM,SAAS;AAAA;AAAA,EAEb,aAAa;AAAA,IACX,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA;AAAA,EAEA,YAAY;AAAA,IACV,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AACF;AAGA,MAAM,YAAY,MAAM;AACtB,QAAM,MAAM;AACZ,SAAO,OAAO,GAAG;AACnB;AAGA,MAAM,YAAY;AAAA;AAAA,EAEhB,MAAM;AAAA,IACJ,OAAO;AAAA,IACP,UAAU;AAAA,IACV,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,eAAe;AAAA,EACjB;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ;AAAA,EACV;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,WAAW;AAAA,IACX,YAAY;AAAA,EACd;AAAA;AAAA,EAGA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,SAAS;AAAA,EACX;AAAA;AAAA,EAGA,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AACF;AAEA,MAAe,WAAA;AAAA,EACb,GAAG,UAAU;AAAA,EACb;AACF;;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,78 @@
"use strict";
const config = {
// 开发环境
development: {
baseURL: "http://localhost:3100/api",
timeout: 1e4
},
// 生产环境
production: {
baseURL: "https://api.jiebanke.com/api",
timeout: 15e3
}
};
const getConfig = () => {
const env = "development";
return config[env];
};
const endpoints = {
// 用户相关
USER: {
LOGIN: "/auth/login",
REGISTER: "/auth/register",
PROFILE: "/user/profile",
UPDATE_PROFILE: "/user/profile",
UPLOAD_AVATAR: "/user/avatar"
},
// 旅行计划
TRAVEL: {
LIST: "/travel/list",
DETAIL: "/travel/detail",
CREATE: "/travel/create",
JOIN: "/travel/join",
MY_PLANS: "/travel/my-plans",
SEARCH: "/travel/search"
},
// 动物认养
ANIMAL: {
LIST: "/animal/list",
DETAIL: "/animal/detail",
ADOPT: "/animal/adopt",
MY_ANIMALS: "/animal/my-animals",
CATEGORIES: "/animal/categories"
},
// 送花服务
FLOWER: {
LIST: "/flower/list",
DETAIL: "/flower/detail",
ORDER: "/flower/order",
MY_ORDERS: "/flower/my-orders",
CATEGORIES: "/flower/categories"
},
// 订单管理
ORDER: {
LIST: "/order/list",
DETAIL: "/order/detail",
CANCEL: "/order/cancel",
PAY: "/order/pay",
CONFIRM: "/order/confirm"
},
// 支付相关
PAYMENT: {
CREATE: "/payment/create",
QUERY: "/payment/query",
REFUND: "/payment/refund"
},
// 系统相关
SYSTEM: {
CONFIG: "/system/config",
NOTICE: "/system/notice",
FEEDBACK: "/system/feedback"
}
};
const config$1 = {
...getConfig(),
endpoints
};
exports.config = config$1;
//# sourceMappingURL=../../.sourcemap/mp-weixin/api/config.js.map

View File

@@ -0,0 +1,182 @@
"use strict";
const common_vendor = require("../common/vendor.js");
const api_config = require("./config.js");
const requestQueue = [];
let isRefreshing = false;
class Request {
constructor() {
this.baseURL = api_config.config.baseURL;
this.timeout = api_config.config.timeout;
this.interceptors = {
request: [],
response: []
};
}
// 添加请求拦截器
addRequestInterceptor(interceptor) {
this.interceptors.request.push(interceptor);
}
// 添加响应拦截器
addResponseInterceptor(interceptor) {
this.interceptors.response.push(interceptor);
}
// 执行请求拦截器
async runRequestInterceptors(config) {
for (const interceptor of this.interceptors.request) {
config = await interceptor(config);
}
return config;
}
// 执行响应拦截器
async runResponseInterceptors(response) {
for (const interceptor of this.interceptors.response) {
response = await interceptor(response);
}
return response;
}
// 核心请求方法
async request(options) {
try {
const requestConfig = {
url: options.url.startsWith("http") ? options.url : `${this.baseURL}${options.url}`,
method: options.method || "GET",
header: {
"Content-Type": "application/json",
...options.header
},
data: options.data,
timeout: this.timeout
};
const finalConfig = await this.runRequestInterceptors(requestConfig);
const response = await common_vendor.index.request(finalConfig);
const finalResponse = await this.runResponseInterceptors(response);
return finalResponse[1];
} catch (error) {
common_vendor.index.__f__("error", "at api/request.js:69", "Request error:", error);
throw error;
}
}
// GET请求
get(url, data = {}, options = {}) {
return this.request({
url,
method: "GET",
data,
...options
});
}
// POST请求
post(url, data = {}, options = {}) {
return this.request({
url,
method: "POST",
data,
...options
});
}
// PUT请求
put(url, data = {}, options = {}) {
return this.request({
url,
method: "PUT",
data,
...options
});
}
// DELETE请求
delete(url, data = {}, options = {}) {
return this.request({
url,
method: "DELETE",
data,
...options
});
}
// 上传文件
upload(url, filePath, formData = {}, options = {}) {
return new Promise((resolve, reject) => {
common_vendor.index.uploadFile({
url: `${this.baseURL}${url}`,
filePath,
name: "file",
formData,
success: resolve,
fail: reject,
...options
});
});
}
// 下载文件
download(url, options = {}) {
return new Promise((resolve, reject) => {
common_vendor.index.downloadFile({
url: `${this.baseURL}${url}`,
success: resolve,
fail: reject,
...options
});
});
}
}
const request = new Request();
request.addRequestInterceptor(async (config) => {
const token = common_vendor.index.getStorageSync("token");
if (token) {
config.header.Authorization = `Bearer ${token}`;
}
return config;
});
request.addResponseInterceptor(async (response) => {
const { statusCode, data } = response;
if (statusCode === 200) {
if (data.code === 0) {
return data.data;
} else {
const error = new Error(data.message || "业务错误");
error.code = data.code;
throw error;
}
} else if (statusCode === 401) {
return handleTokenExpired(response);
} else {
throw new Error(`网络错误: ${statusCode}`);
}
});
async function handleTokenExpired(response) {
if (isRefreshing) {
return new Promise((resolve) => {
requestQueue.push(() => resolve(request.request(response.config)));
});
}
isRefreshing = true;
const refreshToken = common_vendor.index.getStorageSync("refreshToken");
if (!refreshToken) {
common_vendor.index.navigateTo({ url: "/pages/auth/login" });
throw new Error("请重新登录");
}
try {
const result = await request.post(api_config.config.endpoints.USER.REFRESH_TOKEN, {
refreshToken
});
common_vendor.index.setStorageSync("token", result.token);
common_vendor.index.setStorageSync("refreshToken", result.refreshToken);
const retryResponse = await request.request(response.config);
processRequestQueue();
return retryResponse;
} catch (error) {
common_vendor.index.removeStorageSync("token");
common_vendor.index.removeStorageSync("refreshToken");
common_vendor.index.navigateTo({ url: "/pages/auth/login" });
throw error;
} finally {
isRefreshing = false;
}
}
function processRequestQueue() {
while (requestQueue.length > 0) {
const retry = requestQueue.shift();
retry();
}
}
exports.request = request;
//# sourceMappingURL=../../.sourcemap/mp-weixin/api/request.js.map

View File

@@ -0,0 +1,18 @@
"use strict";
require("../common/vendor.js");
const api_request = require("./request.js");
require("./config.js");
const homeService = {
// 获取首页数据
getHomeData: () => api_request.request.get("/home/data"),
// 获取轮播图
getBanners: () => api_request.request.get("/home/banners"),
// 获取推荐旅行计划
getRecommendedTravels: () => api_request.request.get("/home/recommended-travels"),
// 获取热门动物
getHotAnimals: () => api_request.request.get("/home/hot-animals"),
// 获取精选花束
getFeaturedFlowers: () => api_request.request.get("/home/featured-flowers")
};
exports.homeService = homeService;
//# sourceMappingURL=../../.sourcemap/mp-weixin/api/services.js.map

293
package-lock.json generated
View File

@@ -5,12 +5,20 @@
"packages": {
"": {
"dependencies": {
"axios": "^1.11.0",
"bcryptjs": "^3.0.2",
"mysql2": "^3.14.3"
},
"devDependencies": {
"less": "^4.4.1"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
@@ -19,6 +27,51 @@
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/bcryptjs": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-3.0.2.tgz",
"integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/copy-anything": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
@@ -31,6 +84,15 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -39,6 +101,20 @@
"node": ">=0.10"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/errno": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
@@ -52,6 +128,96 @@
"errno": "cli.js"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@@ -60,6 +226,55 @@
"is-property": "^1.0.2"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"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",
@@ -67,6 +282,45 @@
"dev": true,
"optional": true
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -169,6 +423,15 @@
"node": ">=6"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -182,10 +445,32 @@
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mysql2": {
"version": "3.14.3",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.3.tgz",
"resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.14.3.tgz",
"integrity": "sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
@@ -248,6 +533,12 @@
"node": ">=6"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",

View File

@@ -1,5 +1,7 @@
{
"dependencies": {
"axios": "^1.11.0",
"bcryptjs": "^3.0.2",
"mysql2": "^3.14.3"
},
"devDependencies": {

View File

@@ -1,13 +1,5 @@
const mysql = require('mysql2');
// 测试环境数据库配置(先不指定数据库)
const testConfig = {
host: '192.168.0.240',
port: 3306,
user: 'root',
password: 'aiot$Aiot123'
};
// 生产环境数据库配置(先不指定数据库)
const prodConfig = {
host: '129.211.213.226',
@@ -49,74 +41,23 @@ function testConnection(config, environment) {
connection.query('SELECT VERSION() as version', (err, versionRows) => {
if (err) {
console.error('❌ 获取版本失败:', err.message);
connection.end();
return resolve({ success: false, error: err.message });
} else {
console.log('📋 数据库版本:', versionRows[0].version);
}
console.log('📋 MySQL版本:', versionRows[0].version);
// 检查所有数据库
connection.query('SHOW DATABASES', (err, databases) => {
// 列出所有数据库
connection.query('SHOW DATABASES', (err, dbRows) => {
if (err) {
console.error('❌ 获取数据库列表失败:', err.message);
connection.end();
return resolve({ success: false, error: err.message });
}
console.log('\n📋 所有数据库:');
console.log('='.repeat(40));
databases.forEach(db => {
console.log(db.Database);
});
// 检查jiebandata数据库是否存在
const jiebandataExists = databases.some(db => db.Database === 'jiebandata');
console.log(`\n📊 jiebandata数据库: ${jiebandataExists ? '✅ 存在' : '❌ 不存在'}`);
if (jiebandataExists) {
// 获取jiebandata数据库中的表
connection.query(`
SELECT TABLE_NAME, TABLE_ROWS, CREATE_TIME, UPDATE_TIME
FROM information_schema.tables
WHERE table_schema = 'jiebandata'
ORDER BY TABLE_NAME
`, (err, tables) => {
if (err) {
console.error('❌ 获取表信息失败:', err.message);
connection.end();
return resolve({ success: false, error: err.message });
}
console.log(`\n📋 jiebandata数据库中的表:`);
console.log('='.repeat(80));
console.log('表名'.padEnd(25) + '行数'.padEnd(10) + '创建时间'.padEnd(20) + '更新时间');
console.log('='.repeat(80));
if (tables.length === 0) {
console.log('暂无表');
} else {
tables.forEach(table => {
console.log(
table.TABLE_NAME.padEnd(25) +
(table.TABLE_ROWS || '0').toString().padEnd(10) +
(table.CREATE_TIME ? new Date(table.CREATE_TIME).toLocaleString() : 'N/A').padEnd(20) +
(table.UPDATE_TIME ? new Date(table.UPDATE_TIME).toLocaleString() : 'N/A')
);
});
}
// 关闭连接
connection.end();
console.log('\n✅ 连接已正常关闭');
resolve({ success: true, databaseExists: jiebandataExists, tables: tables.length });
});
} else {
// 关闭连接
connection.end();
console.log('\n✅ 连接已正常关闭');
resolve({ success: true, databaseExists: false, tables: 0 });
console.log('📚 可用数据库:');
dbRows.forEach(row => {
console.log(`${row.Database}`);
});
}
connection.end();
resolve({ success: true });
});
});
});
@@ -126,31 +67,20 @@ function testConnection(config, environment) {
async function main() {
console.log('🚀 开始测试结伴客系统数据库连接');
console.log('='.repeat(60));
console.log('============================================================');
// 测试测试环境
const testResult = await testConnection(testConfig, '测试环境');
console.log('\n' + '='.repeat(60));
// 测试生产环境
// 测试生产环境数据库
const prodResult = await testConnection(prodConfig, '生产环境');
console.log('\n' + '='.repeat(60));
console.log('📋 测试结果汇总:');
console.log('测试环境:', testResult.success ? '✅ 成功' : '❌ 失败');
console.log('生产环境:', prodResult.success ? '✅ 成功' : '❌ 失败');
console.log('\n============================================================');
console.log('🏁 数据库连接测试完成');
if (testResult.success && testResult.tables > 0) {
console.log(`测试环境表数量: ${testResult.tables}`);
}
if (prodResult.success && prodResult.tables > 0) {
console.log(`生产环境表数量: ${prodResult.tables}`);
if (prodResult.success) {
console.log('🎉 所有测试通过!');
} else {
console.log('💥 部分测试失败');
}
}
// 行测试
main().catch(console.error);
// 导出测试函数供其他模块使用
module.exports = { testConnection, testConfig, prodConfig };
// 行测试
main().catch(console.error);

77
test_admin_api.js Normal file
View File

@@ -0,0 +1,77 @@
const axios = require('axios');
// 创建axios实例
const api = axios.create({
baseURL: 'http://localhost:3100/api/v1',
timeout: 10000
});
async function testAdminAPI() {
console.log('🚀 开始测试管理员API接口...\n');
let token = null;
try {
// 1. 测试管理员登录(使用新创建的测试账户)
console.log('1. 测试管理员登录...');
const loginResponse = await api.post('/admin/login', {
username: 'testadmin',
password: 'admin123'
});
console.log('✅ 登录成功');
console.log('登录响应:', loginResponse.data);
// 保存token用于后续测试
token = loginResponse.data.data.token;
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
} catch (error) {
if (error.response) {
console.log('❌ 登录失败:', error.response.data);
} else {
console.error('❌ 请求失败:', error.message);
}
return;
}
try {
// 2. 测试获取当前管理员信息
console.log('\n2. 测试获取当前管理员信息...');
const profileResponse = await api.get('/admin/profile');
console.log('✅ 获取管理员信息成功');
console.log('管理员信息:', profileResponse.data);
} catch (error) {
if (error.response) {
console.log('❌ 获取管理员信息失败:', error.response.data);
} else {
console.error('❌ 请求失败:', error.message);
}
}
try {
// 3. 测试获取管理员列表
console.log('\n3. 测试获取管理员列表...');
const listResponse = await api.get('/admin');
console.log('✅ 获取管理员列表成功');
console.log('管理员列表:');
listResponse.data.data.admins.forEach((admin, index) => {
console.log(`${index + 1}. ID: ${admin.id}, 用户名: ${admin.username}, 角色: ${admin.role}, 状态: ${admin.status}`);
});
} catch (error) {
if (error.response) {
console.log('❌ 获取管理员列表失败:', error.response.data);
} else {
console.error('❌ 请求失败:', error.message);
}
}
console.log('\n✅ 管理员API测试完成');
}
// 运行测试
testAdminAPI().catch(console.error);

60
test_db_connection.js Normal file
View File

@@ -0,0 +1,60 @@
const mysql = require('mysql2/promise');
// 数据库配置
const dbConfig = {
host: '129.211.213.226',
port: 9527,
user: 'root',
password: 'aiotAiot123!',
database: 'jiebandata',
connectionLimit: 10,
charset: 'utf8mb4',
timezone: '+08:00',
waitForConnections: true,
queueLimit: 0,
// 添加连接超时配置
connectTimeout: 10000, // 10秒连接超时
acquireTimeout: 10000, // 10秒获取连接超时
timeout: 10000 // 10秒查询超时
};
async function testConnection() {
let connection;
try {
console.log('尝试连接数据库...');
connection = await mysql.createConnection(dbConfig);
console.log('✅ 数据库连接成功');
// 测试查询
const [rows] = await connection.execute('SELECT 1 as test');
console.log('✅ 数据库查询测试成功:', rows);
// 查询管理员表
const [admins] = await connection.execute('SELECT id, username, role FROM admins LIMIT 3');
console.log('✅ 管理员表查询成功:');
console.table(admins);
await connection.end();
console.log('✅ 数据库连接已关闭');
} catch (error) {
console.error('❌ 数据库连接失败:', error.message);
if (error.code) {
console.error('错误代码:', error.code);
}
if (error.errno) {
console.error('错误编号:', error.errno);
}
if (error.syscall) {
console.error('系统调用:', error.syscall);
}
if (connection) {
try {
await connection.end();
} catch (closeError) {
console.error('关闭连接时出错:', closeError.message);
}
}
}
}
testConnection();

View File

@@ -3,11 +3,17 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>关于我们 - 结伴客</title>
<meta name="description" content="了解结伴客平台的发展历程、核心价值观和团队信息。我们致力于为用户提供优质的旅行结伴和动物认领服务。">
<meta name="keywords" content="结伴客, 关于我们, 平台介绍, 发展历程, 核心价值观, 团队介绍">
<meta name="keywords" content="结伴客,关于我们,平台介绍,发展历程,核心价值观,团队介绍">
<meta name="author" content="结伴客">
<meta property="og:title" content="关于我们 - 结伴客">
<meta property="og:description" content="了解结伴客平台的发展历程、核心价值观和团队信息。">
<meta property="og:type" content="website">
<meta property="og:url" content="https://jiebanke.com/about.html">
<title>关于我们 - 结伴客</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.7.2/css/all.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
@@ -65,7 +71,7 @@
<div class="row align-items-center">
<div class="col-lg-6 mb-4 mb-lg-0">
<div class="about-image-wrapper">
<img src="https://via.placeholder.com/600x400/2ecc71/ffffff?text=结伴客团队" alt="结伴客团队" class="img-fluid rounded shadow">
<img src="images/team-photo.svg" alt="结伴客团队" class="img-fluid rounded shadow">
</div>
</div>
<div class="col-lg-6">
@@ -182,7 +188,7 @@
<div class="row">
<div class="col-md-3 col-6 mb-4">
<div class="team-member text-center">
<img src="https://via.placeholder.com/150x150/2ecc71/ffffff?text=CEO" class="rounded-circle mb-3 team-photo" alt="CEO">
<img src="images/ceo-avatar.svg" class="rounded-circle mb-3 team-photo" alt="CEO张明">
<h5>张明</h5>
<p class="text-muted mb-0">首席执行官</p>
</div>
@@ -190,7 +196,7 @@
<div class="col-md-3 col-6 mb-4">
<div class="team-member text-center">
<img src="https://via.placeholder.com/150x150/1abc9c/ffffff?text=CTO" class="rounded-circle mb-3 team-photo" alt="CTO">
<img src="images/cto-avatar.svg" class="rounded-circle mb-3 team-photo" alt="CTO李华">
<h5>李华</h5>
<p class="text-muted mb-0">首席技术官</p>
</div>
@@ -198,7 +204,7 @@
<div class="col-md-3 col-6 mb-4">
<div class="team-member text-center">
<img src="https://via.placeholder.com/150x150/3498db/ffffff?text=CMO" class="rounded-circle mb-3 team-photo" alt="CMO">
<img src="images/cmo-avatar.svg" class="rounded-circle mb-3 team-photo" alt="CMO王芳">
<h5>王芳</h5>
<p class="text-muted mb-0">首席市场官</p>
</div>
@@ -206,7 +212,7 @@
<div class="col-md-3 col-6 mb-4">
<div class="team-member text-center">
<img src="https://via.placeholder.com/150x150/9b59b6/ffffff?text=COO" class="rounded-circle mb-3 team-photo" alt="COO">
<img src="images/coo-avatar.svg" class="rounded-circle mb-3 team-photo" alt="COO赵强">
<h5>赵强</h5>
<p class="text-muted mb-0">首席运营官</p>
</div>
@@ -290,6 +296,15 @@
</div>
</footer>
<!-- 页面加载动画 -->
<div class="page-loader">
<div class="loader-spinner">
<i class="fa fa-compass fa-spin"></i>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.js"></script>
<script src="js/main.js"></script>
</body>
</html>

View File

@@ -3,11 +3,17 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>成功案例 - 结伴客</title>
<meta name="description" content="查看结伴客平台用户和商家的成功案例,了解旅行结伴、动物认领和送花服务的真实体验故事。">
<meta name="keywords" content="结伴客, 成功案例, 用户案例, 商家案例, 旅行结伴案例, 动物认领案例, 送花服务案例">
<meta name="keywords" content="结伴客,成功案例,用户案例,商家案例,旅行结伴案例,动物认领案例,送花服务案例">
<meta name="author" content="结伴客">
<meta property="og:title" content="成功案例 - 结伴客">
<meta property="og:description" content="查看结伴客平台用户和商家的成功案例,了解旅行结伴、动物认领和送花服务的真实体验故事。">
<meta property="og:type" content="website">
<meta property="og:url" content="https://jiebanke.com/case.html">
<title>成功案例 - 结伴客</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
j <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.7.2/css/all.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.7.2/css/all.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
@@ -63,7 +69,7 @@ j <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.7.2/css/all.cs
<div class="row">
<div class="col-md-4 mb-4">
<div class="card case-card">
<img src="https://via.placeholder.com/400x200/667eea/ffffff?text=旅行结伴" class="card-img-top case-image" alt="用户案例1">
<img src="images/travel-case.svg" class="card-img-top case-image" alt="旅行结伴案例">
<div class="card-body">
<h5 class="card-title">从陌生人到旅行伙伴</h5>
<div class="testimonial-content">
@@ -77,7 +83,7 @@ j <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.7.2/css/all.cs
<div class="col-md-4 mb-4">
<div class="card case-card">
<img src="https://via.placeholder.com/400x200/f093fb/ffffff?text=农场体验" class="card-img-top case-image" alt="用户案例2">
<img src="images/farm-case.svg" class="card-img-top case-image" alt="农场体验案例">
<div class="card-body">
<h5 class="card-title">难忘的农场体验</h5>
<div class="testimonial-content">
@@ -91,7 +97,7 @@ j <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.7.2/css/all.cs
<div class="col-md-4 mb-4">
<div class="card case-card">
<img src="https://via.placeholder.com/400x200/764ba2/ffffff?text=送花服务" class="card-img-top case-image" alt="用户案例3">
<img src="images/flower-case.svg" class="card-img-top case-image" alt="送花服务案例">
<div class="card-body">
<h5 class="card-title">浪漫的送花惊喜</h5>
<div class="testimonial-content">
@@ -283,6 +289,15 @@ j <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.7.2/css/all.cs
</div>
</footer>
<!-- 页面加载动画 -->
<div class="page-loader">
<div class="loader-spinner">
<i class="fa fa-compass fa-spin"></i>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.js"></script>
<script src="js/main.js"></script>
</body>
</html>

View File

@@ -3,12 +3,18 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>联系我们 - 结伴客</title>
<meta name="description" content="联系结伴客平台,获取旅行结伴、动物认领、送花服务等相关咨询和帮助。">
<meta name="keywords" content="结伴客, 联系我们, 旅行结伴, 动物认领, 送花服务, 客服咨询">
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha512-..." crossorigin="anonymous">
<meta name="keywords" content="结伴客,联系我们,旅行结伴,动物认领,送花服务,客服咨询">
<meta name="author" content="结伴客">
<meta property="og:title" content="联系我们 - 结伴客">
<meta property="og:description" content="联系结伴客平台,获取旅行结伴、动物认领、送花服务等相关咨询和帮助。">
<meta property="og:type" content="website">
<meta property="og:url" content="https://jiebanke.com/contact.html">
<title>联系我们 - 结伴客</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.7.2/css/all.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet" integrity="sha512-..." crossorigin="anonymous">
<link href="https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<!-- 导航栏 -->
@@ -246,6 +252,15 @@
</div>
</footer>
<!-- 页面加载动画 -->
<div class="page-loader">
<div class="loader-spinner">
<i class="fa fa-compass fa-spin"></i>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.js"></script>
<script src="js/main.js"></script>
</body>
</html>

View File

@@ -838,6 +838,106 @@ body {
color: white;
}
/* 页面加载动画 */
.page-loader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--primary-gradient);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 0.5s ease, visibility 0.5s ease;
}
.page-loader.hidden {
opacity: 0;
visibility: hidden;
}
.loader-spinner {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
animation: pulse 2s infinite;
}
.loader-spinner i {
font-size: 2.5rem;
color: white;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* 平滑滚动 */
html {
scroll-behavior: smooth;
}
/* 滚动进度条 */
.scroll-progress {
position: fixed;
top: 0;
left: 0;
width: 0%;
height: 4px;
background: var(--primary-gradient);
z-index: 9998;
transition: width 0.3s ease;
}
/* 返回顶部按钮 */
.back-to-top {
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
background: var(--primary-gradient);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 9997;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
}
.back-to-top:hover {
transform: translateY(-5px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
/* 响应式字体调整 */
@media (max-width: 768px) {
.hero-section h1 {

View File

@@ -0,0 +1,11 @@
<svg width="150" height="150" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gradCEO" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2ecc71;stop-opacity:1" />
<stop offset="100%" style="stop-color:#27ae60;stop-opacity:1" />
</linearGradient>
</defs>
<circle cx="75" cy="75" r="75" fill="url(#gradCEO)"/>
<text x="75" y="85" fill="#ffffff" text-anchor="middle" font-family="Arial, sans-serif" font-weight="bold" font-size="24"></text>
<text x="75" y="110" fill="rgba(255,255,255,0.8)" text-anchor="middle" font-family="Arial, sans-serif" font-size="14">CEO</text>
</svg>

After

Width:  |  Height:  |  Size: 645 B

View File

@@ -0,0 +1,11 @@
<svg width="150" height="150" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gradCMO" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3498db;stop-opacity:1" />
<stop offset="100%" style="stop-color:#2980b9;stop-opacity:1" />
</linearGradient>
</defs>
<circle cx="75" cy="75" r="75" fill="url(#gradCMO)"/>
<text x="75" y="85" fill="#ffffff" text-anchor="middle" font-family="Arial, sans-serif" font-weight="bold" font-size="24"></text>
<text x="75" y="110" fill="rgba(255,255,255,0.8)" text-anchor="middle" font-family="Arial, sans-serif" font-size="14">CMO</text>
</svg>

After

Width:  |  Height:  |  Size: 645 B

View File

@@ -0,0 +1,11 @@
<svg width="150" height="150" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gradCOO" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#9b59b6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8e44ad;stop-opacity:1" />
</linearGradient>
</defs>
<circle cx="75" cy="75" r="75" fill="url(#gradCOO)"/>
<text x="75" y="85" fill="#ffffff" text-anchor="middle" font-family="Arial, sans-serif" font-weight="bold" font-size="24"></text>
<text x="75" y="110" fill="rgba(255,255,255,0.8)" text-anchor="middle" font-family="Arial, sans-serif" font-size="14">COO</text>
</svg>

After

Width:  |  Height:  |  Size: 645 B

View File

@@ -0,0 +1,11 @@
<svg width="150" height="150" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gradCTO" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1abc9c;stop-opacity:1" />
<stop offset="100%" style="stop-color:#16a085;stop-opacity:1" />
</linearGradient>
</defs>
<circle cx="75" cy="75" r="75" fill="url(#gradCTO)"/>
<text x="75" y="85" fill="#ffffff" text-anchor="middle" font-family="Arial, sans-serif" font-weight="bold" font-size="24"></text>
<text x="75" y="110" fill="rgba(255,255,255,0.8)" text-anchor="middle" font-family="Arial, sans-serif" font-size="14">CTO</text>
</svg>

After

Width:  |  Height:  |  Size: 645 B

View File

@@ -0,0 +1,16 @@
<svg width="400" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f093fb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#f5576c;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#grad2)"/>
<g fill="#ffffff" text-anchor="middle" font-family="Arial, sans-serif" font-weight="bold">
<text x="200" y="100" font-size="24" dy="0.35em">农场体验</text>
<text x="200" y="130" font-size="14" opacity="0.8">亲近自然,体验农耕</text>
</g>
<path d="M100,150 Q150,120 200,150 Q250,180 300,150" stroke="rgba(255,255,255,0.2)" fill="none" stroke-width="2"/>
<circle cx="120" cy="80" r="15" fill="rgba(255,255,255,0.1)"/>
<circle cx="280" cy="100" r="12" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 888 B

Some files were not shown because too many files have changed in this diff Show More