refactor(backend): 重构动物相关 API 接口
- 更新了动物数据结构和相关类型定义 - 优化了动物列表、详情、创建、更新和删除接口 - 新增了更新动物状态接口 - 移除了与认领记录相关的接口 -调整了 API 响应结构
This commit is contained in:
61
add_test_admin.js
Normal file
61
add_test_admin.js
Normal 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();
|
||||
@@ -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
|
||||
|
||||
# 功能开关
|
||||
|
||||
@@ -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 %>">
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const appStore = useAppStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化应用
|
||||
appStore.initializeApp()
|
||||
appStore.initialize()
|
||||
|
||||
// 开发环境调试信息
|
||||
if (import.meta.env.DEV) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 }
|
||||
@@ -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'
|
||||
249
admin-system/src/stores/modules/activity.ts
Normal file
249
admin-system/src/stores/modules/activity.ts
Normal 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
|
||||
}
|
||||
})
|
||||
245
admin-system/src/stores/modules/animal.ts
Normal file
245
admin-system/src/stores/modules/animal.ts
Normal 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
|
||||
}
|
||||
})
|
||||
439
admin-system/src/stores/modules/content.ts
Normal file
439
admin-system/src/stores/modules/content.ts
Normal 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
|
||||
}
|
||||
})
|
||||
232
admin-system/src/stores/modules/merchant.ts
Normal file
232
admin-system/src/stores/modules/merchant.ts
Normal 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
|
||||
}
|
||||
})
|
||||
223
admin-system/src/stores/modules/order.ts
Normal file
223
admin-system/src/stores/modules/order.ts
Normal 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
|
||||
}
|
||||
})
|
||||
721
admin-system/src/stores/modules/permission.ts
Normal file
721
admin-system/src/stores/modules/permission.ts
Normal 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
|
||||
}
|
||||
})
|
||||
668
admin-system/src/stores/modules/stats.ts
Normal file
668
admin-system/src/stores/modules/stats.ts
Normal 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
|
||||
}
|
||||
})
|
||||
162
admin-system/src/stores/modules/user.ts
Normal file
162
admin-system/src/stores/modules/user.ts
Normal 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
|
||||
}
|
||||
})
|
||||
274
admin-system/src/types/activity.ts
Normal file
274
admin-system/src/types/activity.ts
Normal 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
|
||||
}
|
||||
231
admin-system/src/types/animal.ts
Normal file
231
admin-system/src/types/animal.ts
Normal 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
|
||||
}
|
||||
313
admin-system/src/types/content.ts
Normal file
313
admin-system/src/types/content.ts
Normal 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
|
||||
}
|
||||
163
admin-system/src/types/merchant.ts
Normal file
163
admin-system/src/types/merchant.ts
Normal 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
|
||||
}
|
||||
209
admin-system/src/types/order.ts
Normal file
209
admin-system/src/types/order.ts
Normal 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
|
||||
}
|
||||
402
admin-system/src/types/permission.ts
Normal file
402
admin-system/src/types/permission.ts
Normal 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>
|
||||
}
|
||||
488
admin-system/src/types/stats.ts
Normal file
488
admin-system/src/types/stats.ts
Normal 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
|
||||
}
|
||||
130
admin-system/src/types/user.ts
Normal file
130
admin-system/src/types/user.ts
Normal 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
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
# 测试环境数据库
|
||||
|
||||
@@ -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: {
|
||||
|
||||
13
backend/package-lock.json
generated
13
backend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
225
backend/src/controllers/admin/index.js
Normal file
225
backend/src/controllers/admin/index.js
Normal 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);
|
||||
}
|
||||
};
|
||||
166
backend/src/controllers/animal/index.js
Normal file
166
backend/src/controllers/animal/index.js
Normal 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;
|
||||
@@ -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
|
||||
};
|
||||
402
backend/src/controllers/order/index.js
Normal file
402
backend/src/controllers/order/index.js
Normal 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
|
||||
};
|
||||
152
backend/src/controllers/travel/index.js
Normal file
152
backend/src/controllers/travel/index.js
Normal 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;
|
||||
129
backend/src/controllers/user/index.js
Normal file
129
backend/src/controllers/user/index.js
Normal 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;
|
||||
@@ -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: '更新时间'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 安全返回用户信息(去除敏感信息)
|
||||
|
||||
88
backend/src/models/admin.js
Normal file
88
backend/src/models/admin.js
Normal 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
327
backend/src/routes/admin.js
Normal 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;
|
||||
533
backend/src/routes/animal.js
Normal file
533
backend/src/routes/animal.js
Normal 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;
|
||||
@@ -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:
|
||||
|
||||
53
backend/src/routes/order.js
Normal file
53
backend/src/routes/order.js
Normal 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;
|
||||
434
backend/src/routes/travel.js
Normal file
434
backend/src/routes/travel.js
Normal 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
380
backend/src/routes/user.js
Normal 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;
|
||||
@@ -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'
|
||||
|
||||
// 显示启动横幅
|
||||
|
||||
158
backend/src/services/admin/index.js
Normal file
158
backend/src/services/admin/index.js
Normal 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();
|
||||
262
backend/src/services/animal/index.js
Normal file
262
backend/src/services/animal/index.js
Normal 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;
|
||||
368
backend/src/services/order/index.js
Normal file
368
backend/src/services/order/index.js
Normal 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();
|
||||
206
backend/src/services/travel/index.js
Normal file
206
backend/src/services/travel/index.js
Normal 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;
|
||||
165
backend/src/services/user/index.js
Normal file
165
backend/src/services/user/index.js
Normal 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
60
check_table_structure.js
Normal 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
137
db_test.js
Normal 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
85
db_update.js
Normal 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
65
db_verify.js
Normal 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
484
docs/admin-design.md
Normal 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
@@ -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 运维风险
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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优化效果不达预期
|
||||
- 浏览器兼容性问题
|
||||
- 响应式布局在不同设备上显示异常
|
||||
- 响应式布局在不同设备上显示异常
|
||||
- **管理员后台风险**:
|
||||
- 权限控制漏洞可能导致数据泄露
|
||||
- 操作日志记录不完整影响审计
|
||||
- 数据统计准确性需要验证
|
||||
@@ -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秒)
|
||||
- 批量操作功能正常工作
|
||||
- 数据导出功能完整可用
|
||||
- 权限分级控制准确无误
|
||||
@@ -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 CURRENT极ESTAMP 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) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const config = {
|
||||
// 开发环境
|
||||
development: {
|
||||
baseURL: 'http://localhost:3000/api',
|
||||
baseURL: 'http://localhost:3100/api',
|
||||
timeout: 10000
|
||||
},
|
||||
// 生产环境
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
1
mini-program/unpackage/dist/dev/.sourcemap/mp-weixin/api/config.js.map
vendored
Normal file
1
mini-program/unpackage/dist/dev/.sourcemap/mp-weixin/api/config.js.map
vendored
Normal 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;;"}
|
||||
1
mini-program/unpackage/dist/dev/.sourcemap/mp-weixin/api/request.js.map
vendored
Normal file
1
mini-program/unpackage/dist/dev/.sourcemap/mp-weixin/api/request.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
mini-program/unpackage/dist/dev/.sourcemap/mp-weixin/api/services.js.map
vendored
Normal file
1
mini-program/unpackage/dist/dev/.sourcemap/mp-weixin/api/services.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
78
mini-program/unpackage/dist/dev/mp-weixin/api/config.js
vendored
Normal file
78
mini-program/unpackage/dist/dev/mp-weixin/api/config.js
vendored
Normal 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
|
||||
182
mini-program/unpackage/dist/dev/mp-weixin/api/request.js
vendored
Normal file
182
mini-program/unpackage/dist/dev/mp-weixin/api/request.js
vendored
Normal 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
|
||||
18
mini-program/unpackage/dist/dev/mp-weixin/api/services.js
vendored
Normal file
18
mini-program/unpackage/dist/dev/mp-weixin/api/services.js
vendored
Normal 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
293
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"axios": "^1.11.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"mysql2": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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
77
test_admin_api.js
Normal 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
60
test_db_connection.js
Normal 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();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
11
website/images/ceo-avatar.svg
Normal file
11
website/images/ceo-avatar.svg
Normal 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 |
11
website/images/cmo-avatar.svg
Normal file
11
website/images/cmo-avatar.svg
Normal 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 |
11
website/images/coo-avatar.svg
Normal file
11
website/images/coo-avatar.svg
Normal 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 |
11
website/images/cto-avatar.svg
Normal file
11
website/images/cto-avatar.svg
Normal 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 |
16
website/images/farm-case.svg
Normal file
16
website/images/farm-case.svg
Normal 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
Reference in New Issue
Block a user