重构动物模型和路由系统,优化查询逻辑并新增商户和促销活动功能

This commit is contained in:
ylweng
2025-09-22 02:04:07 +08:00
parent 5fc1a4fcb9
commit 47c816270d
54 changed files with 5384 additions and 4639 deletions

View File

@@ -1,10 +1,10 @@
# 解班客 - 宠物认领平台
# 结伴客 - 宠物认领平台
一个基于Vue.js和Node.js的宠物认领平台帮助流浪动物找到温暖的家。
## 项目概述
解班客是一个专业的宠物认领平台致力于为流浪动物提供一个温暖的归宿。平台通过现代化的Web技术为用户提供便捷的宠物发布、搜索、认领服务同时为管理员提供完善的后台管理功能。
结伴客是一个专业的宠物认领平台致力于为流浪动物提供一个温暖的归宿。平台通过现代化的Web技术为用户提供便捷的宠物发布、搜索、认领服务同时为管理员提供完善的后台管理功能。
### 核心功能

View File

@@ -0,0 +1,115 @@
import { request } from '.'
// 定义花卉相关类型
export interface Flower {
id: number
name: string
type: string
variety: string
price: number
stock: number
image: string
description: string
merchantId: number
merchantName: string
status: string
createdAt: string
updatedAt: string
}
export interface FlowerQueryParams {
page?: number
limit?: number
keyword?: string
type?: string
status?: string
merchantId?: number
startDate?: string
endDate?: string
}
export interface FlowerCreateData {
name: string
type: string
variety: string
price: number
stock: number
image: string
description: string
merchantId: number
status?: string
}
export interface FlowerUpdateData {
name?: string
type?: string
variety?: string
price?: number
stock?: number
image?: string
description?: string
merchantId?: number
status?: string
}
export interface FlowerSale {
id: number
flowerId: number
flowerName: string
buyerId: number
buyerName: string
quantity: number
price: number
totalAmount: number
status: string
saleTime: string
createdAt: string
}
export interface Merchant {
id: number
name: string
contact: string
phone: string
address: string
status: string
createdAt: string
}
// 获取花卉列表
export const getFlowers = (params?: FlowerQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { flowers: Flower[]; pagination: any } }>('/flowers', { params })
// 获取花卉详情
export const getFlower = (id: number) =>
request.get<{ success: boolean; code: number; message: string; data: { flower: Flower } }>(`/flowers/${id}`)
// 创建花卉
export const createFlower = (data: FlowerCreateData) =>
request.post<{ success: boolean; code: number; message: string; data: { flower: Flower } }>('/flowers', data)
// 更新花卉
export const updateFlower = (id: number, data: FlowerUpdateData) =>
request.put<{ success: boolean; code: number; message: string; data: { flower: Flower } }>(`/flowers/${id}`, data)
// 删除花卉
export const deleteFlower = (id: number) =>
request.delete<{ success: boolean; code: number; message: string }>(`/flowers/${id}`)
// 获取花卉销售记录
export const getFlowerSales = (params?: any) =>
request.get<{ success: boolean; code: number; message: string; data: { sales: FlowerSale[]; pagination: any } }>('/flower-sales', { params })
// 获取商家列表
export const getMerchants = (params?: any) =>
request.get<{ success: boolean; code: number; message: string; data: { merchants: Merchant[] } }>('/merchants', { params })
export default {
getFlowers,
getFlower,
createFlower,
updateFlower,
deleteFlower,
getFlowerSales,
getMerchants
}

View File

@@ -60,6 +60,14 @@
<router-link to="/animals" />
</a-menu-item>
<a-menu-item v-if="hasPermission('flower:read')" key="flowers">
<template #icon>
<EnvironmentOutlined />
</template>
<span>花卉管理</span>
<router-link to="/flowers" />
</a-menu-item>
<a-menu-item v-if="hasPermission('order:read')" key="orders">
<template #icon>
<ShoppingCartOutlined />
@@ -188,7 +196,8 @@ import {
BellOutlined,
QuestionCircleOutlined,
LogoutOutlined,
FileTextOutlined
FileTextOutlined,
EnvironmentOutlined
} from '@ant-design/icons-vue'
const router = useRouter()

View File

@@ -92,26 +92,8 @@ const onFinish = async (values: FormState) => {
loading.value = true
try {
// 调用真实登录接口
const response = await authAPI.login(values)
// 保存token
if (response?.data?.token) {
localStorage.setItem('admin_token', response.data.token)
} else if (response?.token) {
localStorage.setItem('admin_token', response.token)
} else {
throw new Error('登录响应中缺少token')
}
// 更新用户状态
if (response?.data?.admin) {
appStore.setUser(response.data.admin)
} else if (response?.admin) {
appStore.setUser(response.admin)
} else {
throw new Error('登录响应中缺少用户信息')
}
// 使用store的login方法它会处理token保存和权限设置
await appStore.login(values)
message.success('登录成功!')

View File

@@ -1,865 +0,0 @@
<template>
<div class="animal-management">
<a-page-header
title="动物管理"
sub-title="管理动物信息和认领记录"
>
<template #extra>
<a-space>
<a-button @click="handleRefresh">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
<a-button
v-if="hasPermission('animal:write')"
type="primary"
@click="showCreateModal"
>
<template #icon>
<PlusOutlined />
</template>
新增动物
</a-button>
</a-space>
</template>
</a-page-header>
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<a-tab-pane key="animals" tab="动物列表">
<a-card>
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="关键词">
<a-input
v-model:value="searchForm.keyword"
placeholder="动物名称/编号"
allow-clear
/>
</a-form-item>
<a-form-item label="类型">
<a-select
v-model:value="searchForm.type"
placeholder="全部类型"
style="width: 120px"
allow-clear
>
<a-select-option value="alpaca">羊驼</a-select-option>
<a-select-option value="dog">狗狗</a-select-option>
<a-select-option value="cat">猫咪</a-select-option>
<a-select-option value="rabbit">兔子</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="全部状态"
style="width: 120px"
allow-clear
>
<a-select-option value="available">可认领</a-select-option>
<a-select-option value="claimed">已认领</a-select-option>
<a-select-option value="reserved">预留中</a-select-option>
</a-select>
</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="animalColumns"
:data-source="animalList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image
:width="60"
:height="60"
:src="record.image_url"
:fallback="fallbackImage"
style="border-radius: 6px;"
/>
</template>
<template v-else-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</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 === 'price'">
¥{{ record.price }}
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleViewAnimal(record)">
<EyeOutlined />
查看
</a-button>
<a-button
v-if="hasPermission('animal:write')"
size="small"
@click="handleEditAnimal(record)"
>
<EditOutlined />
编辑
</a-button>
<a-button
v-if="hasPermission('animal:write')"
size="small"
danger
@click="handleDeleteAnimal(record)"
>
<DeleteOutlined />
删除
</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
<a-tab-pane key="claims" tab="认领记录">
<a-card>
<!-- 认领记录搜索 -->
<div class="search-container">
<a-form layout="inline" :model="claimSearchForm">
<a-form-item label="关键词">
<a-input
v-model:value="claimSearchForm.keyword"
placeholder="用户/动物名称"
allow-clear
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="claimSearchForm.status"
placeholder="全部状态"
style="width: 120px"
allow-clear
>
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="approved">已通过</a-select-option>
<a-select-option value="rejected">已拒绝</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleClaimSearch">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleClaimReset">
重置
</a-button>
</a-form-item>
</a-form>
</div>
<!-- 认领记录表格 -->
<a-table
:columns="claimColumns"
:data-source="claimList"
:loading="claimLoading"
:pagination="claimPagination"
:row-key="record => record.id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'animal_image'">
<a-image
:width="40"
:height="40"
:src="record.animal_image"
:fallback="fallbackImage"
style="border-radius: 4px;"
/>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getClaimStatusColor(record.status)">
{{ getClaimStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<template v-if="record.status === 'pending' && hasPermission('animal:write')">
<a-button size="small" type="primary" @click="handleApproveClaim(record)">
<CheckOutlined />
通过
</a-button>
<a-button size="small" danger @click="handleRejectClaim(record)">
<CloseOutlined />
拒绝
</a-button>
</template>
<a-button size="small" @click="handleViewClaim(record)">
<EyeOutlined />
详情
</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
</a-tabs>
<!-- 创建/编辑动物模态框 -->
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:confirm-loading="modalLoading"
@ok="handleModalOk"
@cancel="handleModalCancel"
width="600px"
>
<a-form
ref="animalFormRef"
:model="currentAnimal"
:rules="formRules"
layout="vertical"
>
<a-form-item label="动物名称" name="name">
<a-input v-model:value="currentAnimal.name" placeholder="请输入动物名称" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="currentAnimal.type" placeholder="请选择类型">
<a-select-option value="alpaca">羊驼</a-select-option>
<a-select-option value="dog">狗狗</a-select-option>
<a-select-option value="cat">猫咪</a-select-option>
<a-select-option value="rabbit">兔子</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="品种" name="breed">
<a-input v-model:value="currentAnimal.breed" placeholder="请输入品种" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="年龄" name="age">
<a-input-number
v-model:value="currentAnimal.age"
:min="0"
:max="100"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="性别" name="gender">
<a-select v-model:value="currentAnimal.gender" placeholder="请选择性别">
<a-select-option value="male">雄性</a-select-option>
<a-select-option value="female">雌性</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="价格()" name="price">
<a-input-number
v-model:value="currentAnimal.price"
:min="0"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-select v-model:value="currentAnimal.status" placeholder="请选择状态">
<a-select-option value="available">可认领</a-select-option>
<a-select-option value="claimed">已认领</a-select-option>
<a-select-option value="reserved">预留中</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="图片URL" name="image_url">
<a-input v-model:value="currentAnimal.image_url" placeholder="请输入图片URL" />
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea
v-model:value="currentAnimal.description"
placeholder="请输入动物描述"
:rows="4"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message, Modal, type FormInstance } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ReloadOutlined,
SearchOutlined,
PlusOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
CheckOutlined,
CloseOutlined
} from '@ant-design/icons-vue'
import { useAppStore } from '@/stores/app'
import { getAnimals, deleteAnimal, getAnimalClaims, approveAnimalClaim, rejectAnimalClaim, createAnimal, updateAnimal, getAnimal } from '@/api/animal'
import type { Animal, AnimalClaim, AnimalCreateData, AnimalUpdateData } from '@/api/animal'
interface SearchForm {
keyword: string
type: string
status: string
}
interface ClaimSearchForm {
keyword: string
status: string
}
const appStore = useAppStore()
// 权限检查方法
const hasPermission = (permission: string) => {
return appStore.hasPermission(permission)
}
const activeTab = ref('animals')
const loading = ref(false)
const claimLoading = ref(false)
const searchForm = reactive<SearchForm>({
keyword: '',
type: '',
status: ''
})
const claimSearchForm = reactive<ClaimSearchForm>({
keyword: '',
status: ''
})
const animalList = ref<Animal[]>([])
const claimList = ref<AnimalClaim[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const claimPagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const animalColumns = [
{
title: '图片',
key: 'image',
width: 80,
align: 'center'
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 100
},
{
title: '类型',
key: 'type',
width: 100,
align: 'center'
},
{
title: '品种',
dataIndex: 'breed',
key: 'breed',
width: 120
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
width: 80,
align: 'center',
customRender: ({ text }: { text: number }) => `${text}岁`
},
{
title: '价格',
key: 'price',
width: 100,
align: 'center'
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 120
},
{
title: '操作',
key: 'actions',
width: 150,
align: 'center'
}
]
const claimColumns = [
{
title: '动物',
key: 'animal_image',
width: 60,
align: 'center'
},
{
title: '动物名称',
dataIndex: 'animal_name',
key: 'animal_name',
width: 100
},
{
title: '用户',
dataIndex: 'user_name',
key: 'user_name',
width: 100
},
{
title: '联系电话',
dataIndex: 'user_phone',
key: 'user_phone',
width: 120
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '申请时间',
dataIndex: 'applied_at',
key: 'applied_at',
width: 120
},
{
title: '处理时间',
dataIndex: 'processed_at',
key: 'processed_at',
width: 120
},
{
title: '操作',
key: 'actions',
width: 150,
align: 'center'
}
]
const fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSIjRkZGIi8+CjxwYXRoIGQ9Ik0zMCAxNUMzMS42NTY5IDE1IDMzIDE2LjM0MzEgMzMgMThDMzMgMTkuNjU2OSAzMS42NTY5IDIxIDMwIDIxQzI4LjM0MzEgMjEgMjcgMTkuNjU2OSAyNyAxOEMyNyAxNi4zNDMxIDI4LjM0MzEgMTUgMzAgMTVaIiBmaWxsPSIjQ0NDQ0NDIi8+CjxwYXRoIGQ9Ik0yMi41IDI1QzIyLjUgMjUuODI4NCAyMS44Mjg0IDI2LjUgMjEgMjYuNUgxOEMxOC4xNzE2IDI2LjUgMTcuNSAyNS44Mjg0IDE3LjUgMjVDMTcuNSAyNC4xNzE2IDE4LjE3MTYgMjMuNSAxOSAyMy45SDIxQzIxLjgyODQgMjMuNSAyMi41IDI0LjE3MTYgMjIuNSAyNVoiIGZpbGw9IiNDQ0NDQ0MiLz4KPHBhdGggZD0iTTQyLjUgMjVDNDIuNSAyNS44Mjg0IDQxLjgyODQgMjYuNSA0MSAyNi41SDM5QzM4LjE3MTYgMjYuNSAzNy41IDI1LjgyODQgMzcuNSAyNUMzNy41IDI0LjE3MTYgMzguMTcxNiAyMy41IDM5IDIzLjVMNDEgMjMuNUM0MS44Mjg0IDIzLjUgNDIuNSAyNC4xNzE2IDQyLjUgMjVaIiBmaWxsPSIjQ0NDQ0NDIi8+Cjwvc3ZnPgo='
// 类型映射
const getTypeColor = (type: string) => {
const colors = {
alpaca: 'pink',
dog: 'orange',
cat: 'blue',
rabbit: 'green'
}
return colors[type as keyof typeof colors] || 'default'
}
const getTypeText = (type: string) => {
const texts = {
alpaca: '羊驼',
dog: '狗狗',
cat: '猫咪',
rabbit: '兔子'
}
return texts[type as keyof typeof texts] || '未知'
}
// 状态映射
const getStatusColor = (status: string) => {
const colors = {
available: 'green',
claimed: 'blue',
reserved: 'orange'
}
return colors[status as keyof typeof colors] || 'default'
}
const getStatusText = (status: string) => {
const texts = {
available: '可认领',
claimed: '已认领',
reserved: '预留中'
}
return texts[status as keyof typeof texts] || '未知'
}
const getClaimStatusColor = (status: string) => {
const colors = {
pending: 'orange',
approved: 'green',
rejected: 'red',
completed: 'blue'
}
return colors[status as keyof typeof colors] || 'default'
}
const getClaimStatusText = (status: string) => {
const texts = {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
completed: '已完成'
}
return texts[status as keyof typeof texts] || '未知'
}
// 添加模态框相关状态
const modalVisible = ref(false)
const modalLoading = ref(false)
const modalTitle = ref('新增动物')
const isEditing = ref(false)
const animalFormRef = ref<FormInstance>()
// 当前动物数据
const currentAnimal = ref<Partial<Animal>>({})
// 表单验证规则
const formRules = {
name: [
{ required: true, message: '请输入动物名称' }
],
type: [
{ required: true, message: '请选择类型' }
],
breed: [
{ required: true, message: '请输入品种' }
],
age: [
{ required: true, message: '请输入年龄' }
],
gender: [
{ required: true, message: '请选择性别' }
],
price: [
{ required: true, message: '请输入价格' }
],
status: [
{ required: true, message: '请选择状态' }
]
}
// 生命周期
onMounted(() => {
loadAnimals()
loadClaims()
})
// 方法
const loadAnimals = async () => {
loading.value = true
try {
const response = await getAnimals({
page: pagination.current,
pageSize: pagination.pageSize,
keyword: searchForm.keyword,
type: searchForm.type,
status: searchForm.status
})
animalList.value = response.data
pagination.total = response.pagination?.total || 0
} catch (error) {
message.error('加载动物列表失败')
} finally {
loading.value = false
}
}
const loadClaims = async () => {
claimLoading.value = true
try {
const response = await getAnimalClaims({
page: claimPagination.current,
pageSize: claimPagination.pageSize,
keyword: claimSearchForm.keyword,
status: claimSearchForm.status
})
claimList.value = response.data
claimPagination.total = response.pagination?.total || 0
} catch (error) {
message.error('加载认领记录失败')
} finally {
claimLoading.value = false
}
}
const handleTabChange = (key: string) => {
if (key === 'animals') {
loadAnimals()
} else if (key === 'claims') {
loadClaims()
}
}
const handleSearch = () => {
pagination.current = 1
loadAnimals()
}
const handleReset = () => {
Object.assign(searchForm, {
keyword: '',
type: '',
status: ''
})
pagination.current = 1
loadAnimals()
}
const handleClaimSearch = () => {
claimPagination.current = 1
loadClaims()
}
const handleClaimReset = () => {
Object.assign(claimSearchForm, {
keyword: '',
status: ''
})
claimPagination.current = 1
loadClaims()
}
const handleRefresh = () => {
if (activeTab.value === 'animals') {
loadAnimals()
} else {
loadClaims()
}
message.success('数据已刷新')
}
const handleTableChange: TableProps['onChange'] = (pag) => {
pagination.current = pag.current!
pagination.pageSize = pag.pageSize!
loadAnimals()
}
const handleViewAnimal = (record: Animal) => {
message.info(`查看动物: ${record.name}`)
}
const handleEditAnimal = async (record: Animal) => {
try {
modalLoading.value = true
modalTitle.value = '编辑动物'
isEditing.value = true
// 获取动物详情
const response = await getAnimal(record.id)
currentAnimal.value = response.data
modalVisible.value = true
} catch (error) {
message.error('获取动物详情失败')
} finally {
modalLoading.value = false
}
}
const handleDeleteAnimal = async (record: Animal) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除动物 "${record.name}" 吗?`,
okText: '确定',
okType: 'danger',
onOk: async () => {
try {
await deleteAnimal(record.id)
message.success('动物已删除')
loadAnimals()
} catch (error) {
message.error('删除失败')
}
}
})
}
const handleApproveClaim = async (record: AnimalClaim) => {
Modal.confirm({
title: '确认通过',
content: `确定要通过用户 "${record.user_name}" 的认领申请吗?`,
onOk: async () => {
try {
await approveAnimalClaim(record.id)
message.success('认领申请已通过')
loadClaims()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleRejectClaim = async (record: AnimalClaim) => {
Modal.confirm({
title: '确认拒绝',
content: `确定要拒绝用户 "${record.user_name}" 的认领申请吗?`,
onOk: async () => {
try {
await rejectAnimalClaim(record.id, '拒绝原因')
message.success('认领申请已拒绝')
loadClaims()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleViewClaim = (record: AnimalClaim) => {
message.info(`查看认领详情: ${record.animal_name}`)
}
const showCreateModal = () => {
modalTitle.value = '新增动物'
isEditing.value = false
currentAnimal.value = {
age: 1,
price: 0,
status: 'available'
}
modalVisible.value = true
}
const handleModalOk = () => {
animalFormRef.value
?.validate()
.then(() => {
if (isEditing.value) {
handleUpdateAnimal()
} else {
handleCreateAnimal()
}
})
.catch((error: any) => {
console.error('表单验证失败:', error)
})
}
const handleModalCancel = () => {
modalVisible.value = false
animalFormRef.value?.resetFields()
}
const handleCreateAnimal = async () => {
try {
modalLoading.value = true
await createAnimal(currentAnimal.value as AnimalCreateData)
message.success('创建动物成功')
modalVisible.value = false
loadAnimals()
} catch (error) {
message.error('创建动物失败')
} finally {
modalLoading.value = false
}
}
const handleUpdateAnimal = async () => {
try {
modalLoading.value = true
await updateAnimal(currentAnimal.value.id!, currentAnimal.value as AnimalUpdateData)
message.success('更新动物成功')
modalVisible.value = false
loadAnimals()
} catch (error) {
message.error('更新动物失败')
} finally {
modalLoading.value = false
}
}
</script>
<style scoped lang="less">
.animal-management {
.search-container {
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
:deep(.ant-form-item) {
margin-bottom: 16px;
}
}
}
</style>

View File

@@ -1,219 +0,0 @@
<template>
<a-modal
v-model:open="visible"
title="动物详情"
width="800px"
:footer="null"
@cancel="handleClose"
>
<div v-if="animal" class="animal-detail">
<!-- 基本信息 -->
<a-card title="基本信息" class="mb-4">
<a-row :gutter="16">
<a-col :span="6">
<div class="animal-avatar">
<a-avatar
:src="animal.avatar"
:alt="animal.name"
:size="120"
shape="square"
>
{{ animal.name?.charAt(0) }}
</a-avatar>
</div>
</a-col>
<a-col :span="18">
<a-row :gutter="16">
<a-col :span="8">
<div class="detail-item">
<label>动物ID:</label>
<span>{{ animal.id }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>名称:</label>
<span>{{ animal.name }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>类型:</label>
<a-tag color="blue">{{ animal.type }}</a-tag>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>品种:</label>
<span>{{ animal.breed }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>年龄:</label>
<span>{{ animal.age }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>性别:</label>
<span>{{ animal.gender }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>颜色:</label>
<span>{{ animal.color }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>状态:</label>
<a-tag :color="getStatusColor(animal.status)">
{{ getStatusText(animal.status) }}
</a-tag>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>健康状态:</label>
<span>{{ animal.health_status }}</span>
</div>
</a-col>
</a-row>
</a-col>
</a-row>
</a-card>
<!-- 详细描述 -->
<a-card title="详细描述" class="mb-4">
<p>{{ animal.description || '暂无描述' }}</p>
</a-card>
<!-- 位置信息 -->
<a-card title="位置信息" class="mb-4">
<div class="detail-item">
<label>当前位置:</label>
<span>{{ animal.location }}</span>
</div>
</a-card>
<!-- 时间信息 -->
<a-card title="时间信息">
<a-row :gutter="16">
<a-col :span="8">
<div class="detail-item">
<label>创建时间:</label>
<span>{{ formatDate(animal.createdAt) }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>更新时间:</label>
<span>{{ formatDate(animal.updatedAt) }}</span>
</div>
</a-col>
</a-row>
</a-card>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Animal {
id: number
name: string
type: string
breed: string
age: number
gender: string
color: string
avatar: string
description: string
status: 'available' | 'adopted' | 'unavailable'
health_status: string
location: string
createdAt: string
updatedAt: string
[key: string]: any
}
interface Props {
animal: Animal | null
visible: boolean
}
interface Emits {
(e: 'update:visible', value: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
// 格式化日期
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString('zh-CN')
}
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
available: 'green',
adopted: 'blue',
unavailable: 'red'
}
return colorMap[status] || 'default'
}
// 获取状态文本
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
available: '可领养',
adopted: '已领养',
unavailable: '不可领养'
}
return textMap[status] || status
}
// 关闭弹窗
const handleClose = () => {
visible.value = false
}
</script>
<style scoped>
.animal-detail {
.animal-avatar {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.detail-item {
margin-bottom: 12px;
label {
display: inline-block;
width: 80px;
color: #666;
font-weight: 500;
}
span {
color: #333;
}
}
.mb-4 {
margin-bottom: 16px;
}
}
</style>

View File

@@ -1,362 +0,0 @@
<template>
<a-modal
v-model:open="visible"
:title="isEditing ? '编辑动物' : '新增动物'"
width="800px"
:confirm-loading="loading"
@ok="handleSubmit"
@cancel="handleClose"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="动物名称" name="name">
<a-input
v-model:value="formData.name"
placeholder="请输入动物名称"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="动物类型" name="type">
<a-select
v-model:value="formData.type"
placeholder="请选择动物类型"
>
<a-select-option value="狗"></a-select-option>
<a-select-option value="猫"></a-select-option>
<a-select-option value="兔子">兔子</a-select-option>
<a-select-option value="鸟类">鸟类</a-select-option>
<a-select-option value="其他">其他</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="品种" name="breed">
<a-input
v-model:value="formData.breed"
placeholder="请输入品种"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="年龄" name="age">
<a-input-number
v-model:value="formData.age"
placeholder="请输入年龄"
:min="0"
:max="30"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="性别" name="gender">
<a-select
v-model:value="formData.gender"
placeholder="请选择性别"
>
<a-select-option value="雄性">雄性</a-select-option>
<a-select-option value="雌性">雌性</a-select-option>
<a-select-option value="未知">未知</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="颜色" name="color">
<a-input
v-model:value="formData.color"
placeholder="请输入颜色"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-select
v-model:value="formData.status"
placeholder="请选择状态"
>
<a-select-option value="available">可领养</a-select-option>
<a-select-option value="adopted">已领养</a-select-option>
<a-select-option value="unavailable">不可领养</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="健康状态" name="health_status">
<a-select
v-model:value="formData.health_status"
placeholder="请选择健康状态"
>
<a-select-option value="健康">健康</a-select-option>
<a-select-option value="轻微疾病">轻微疾病</a-select-option>
<a-select-option value="需要治疗">需要治疗</a-select-option>
<a-select-option value="康复中">康复中</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="位置" name="location">
<a-input
v-model:value="formData.location"
placeholder="请输入当前位置"
/>
</a-form-item>
<a-form-item label="头像" name="avatar">
<a-upload
v-model:file-list="fileList"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:before-upload="beforeUpload"
@change="handleChange"
>
<div v-if="formData.avatar">
<img :src="formData.avatar" alt="avatar" style="width: 100%" />
</div>
<div v-else>
<PlusOutlined />
<div style="margin-top: 8px">上传头像</div>
</div>
</a-upload>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea
v-model:value="formData.description"
placeholder="请输入动物描述"
:rows="4"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import type { FormInstance, UploadChangeParam } from 'ant-design-vue'
interface Animal {
id: number
name: string
type: string
breed: string
age: number
gender: string
color: string
avatar: string
description: string
status: 'available' | 'adopted' | 'unavailable'
health_status: string
location: string
createdAt: string
updatedAt: string
[key: string]: any
}
interface Props {
animal: Animal | null
visible: boolean
isEditing: boolean
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'submit', data: any): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
const loading = ref(false)
const fileList = ref([])
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
// 表单数据
const formData = reactive({
name: '',
type: '',
breed: '',
age: 0,
gender: '',
color: '',
status: 'available' as 'available' | 'adopted' | 'unavailable',
health_status: '健康',
location: '',
avatar: '',
description: ''
})
// 表单验证规则
const rules: Record<string, any[]> = {
name: [
{ required: true, message: '请输入动物名称', trigger: 'blur' },
{ min: 1, max: 50, message: '名称长度为1-50个字符', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择动物类型', trigger: 'change' }
],
breed: [
{ required: true, message: '请输入品种', trigger: 'blur' }
],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' },
{ type: 'number', min: 0, max: 30, message: '年龄必须在0-30之间', trigger: 'blur' }
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
],
location: [
{ required: true, message: '请输入位置', trigger: 'blur' }
]
}
// 监听动物数据变化,初始化表单
watch(() => props.animal, (animal) => {
if (animal && props.isEditing) {
formData.name = animal.name
formData.type = animal.type
formData.breed = animal.breed
formData.age = animal.age
formData.gender = animal.gender
formData.color = animal.color
formData.status = animal.status
formData.health_status = animal.health_status
formData.location = animal.location
formData.avatar = animal.avatar
formData.description = animal.description
}
}, { immediate: true })
// 监听弹窗显示状态,重置表单
watch(() => props.visible, (visible) => {
if (visible && !props.isEditing) {
resetForm()
}
})
// 重置表单
const resetForm = () => {
Object.assign(formData, {
name: '',
type: '',
breed: '',
age: 0,
gender: '',
color: '',
status: 'available',
health_status: '健康',
location: '',
avatar: '',
description: ''
})
fileList.value = []
formRef.value?.resetFields()
}
// 文件上传前验证
const beforeUpload = (file: File) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
message.error('只能上传 JPG/PNG 格式的图片!')
return false
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error('图片大小不能超过 2MB!')
return false
}
return false // 阻止自动上传
}
// 文件上传变化处理
const handleChange = (info: UploadChangeParam) => {
if (info.file.originFileObj) {
// 创建预览URL
const reader = new FileReader()
reader.onload = (e) => {
formData.avatar = e.target?.result as string
}
reader.readAsDataURL(info.file.originFileObj)
}
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value?.validate()
loading.value = true
const submitData = {
name: formData.name,
type: formData.type,
breed: formData.breed,
age: formData.age,
gender: formData.gender,
color: formData.color,
status: formData.status,
health_status: formData.health_status,
location: formData.location,
avatar: formData.avatar,
description: formData.description
}
emit('submit', submitData)
} catch (error) {
console.error('表单验证失败:', error)
} finally {
loading.value = false
}
}
// 关闭弹窗
const handleClose = () => {
visible.value = false
resetForm()
}
</script>
<style scoped>
.ant-form-item {
margin-bottom: 16px;
}
.avatar-uploader {
:deep(.ant-upload) {
width: 128px;
height: 128px;
}
:deep(.ant-upload-select-picture-card) {
width: 128px;
height: 128px;
}
}
</style>

View File

@@ -1,716 +0,0 @@
<template>
<div class="animals-page">
<!-- 页面标题 -->
<div class="page-header">
<h1>动物管理</h1>
<p>管理平台上的所有动物信息</p>
</div>
<!-- 数据统计 -->
<a-row :gutter="16" class="stats-row">
<a-col :span="6">
<a-card>
<a-statistic
title="总动物数"
:value="statistics.totalAnimals"
:value-style="{ color: '#3f8600' }"
>
<template #prefix>
<HeartOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="可领养"
:value="statistics.availableAnimals"
:value-style="{ color: '#1890ff' }"
>
<template #prefix>
<SmileOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="已领养"
:value="statistics.claimedAnimals"
:value-style="{ color: '#722ed1' }"
>
<template #prefix>
<HomeOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="今日新增"
:value="statistics.newAnimalsToday"
:value-style="{ color: '#cf1322' }"
>
<template #prefix>
<PlusOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
<!-- 高级搜索 -->
<AdvancedSearch
search-type="animal"
:status-options="statusOptions"
@search="handleSearch"
@reset="handleReset"
/>
<!-- 批量操作 -->
<BatchOperations
:data-source="animals as any[]"
:selected-items="selectedAnimals as any[]"
operation-type="animal"
:status-options="statusOptionsWithColor"
:export-fields="exportFields"
@selection-change="(items: any[]) => handleSelectionChange(items as Animal[])"
@batch-action="(action: string, items: any[], params?: any) => handleBatchAction(action, items as Animal[], params)"
/>
<!-- 动物列表表格 -->
<a-card class="table-card">
<a-table
:columns="columns"
:data-source="animals"
:loading="loading"
:pagination="pagination"
:row-selection="rowSelection"
:scroll="{ x: 1200 }"
row-key="id"
@change="handleTableChange"
>
<!-- 动物图片 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'avatar'">
<a-avatar
:src="record.avatar"
:alt="record.name"
size="large"
shape="square"
>
{{ record.name?.charAt(0) }}
</a-avatar>
</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 === 'type'">
<a-tag color="blue">{{ record.type }}</a-tag>
</template>
<!-- 年龄 -->
<template v-else-if="column.key === 'age'">
{{ record.age }}岁
</template>
<!-- 创建时间 -->
<template v-else-if="column.key === 'createdAt'">
{{ formatDate(record.createdAt) }}
</template>
<!-- 操作按钮 -->
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleViewAnimal(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEditAnimal(record)">
编辑
</a-button>
<a-dropdown>
<a-button type="link" size="small">
更多 <DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item
key="activate"
v-if="record.status !== 'available'"
@click="handleUpdateStatus(record, 'available')"
>
<CheckCircleOutlined />
设为可领养
</a-menu-item>
<a-menu-item
key="deactivate"
v-if="record.status !== 'unavailable'"
@click="handleUpdateStatus(record, 'unavailable')"
>
<LockOutlined />
设为不可领养
</a-menu-item>
<a-menu-divider />
<a-menu-item key="delete" @click="handleDeleteAnimal(record)">
<DeleteOutlined />
删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 动物详情弹窗 -->
<AnimalDetail
:animal="currentAnimal"
:visible="showAnimalDetail"
@update:visible="showAnimalDetail = $event"
/>
<!-- 动物表单弹窗 -->
<AnimalForm
:animal="currentAnimal"
:visible="showAnimalForm"
:is-editing="isEditing"
@update:visible="showAnimalForm = $event"
@submit="handleAnimalSubmit"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import type { TableColumnsType } from 'ant-design-vue'
import { SearchOutlined, PlusOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons-vue'
import type { Animal } from '@/api/animal'
import animalAPI from '@/api/animal'
import AdvancedSearch from '@/components/AdvancedSearch.vue'
import BatchOperations from '@/components/BatchOperations.vue'
import AnimalForm from './components/AnimalForm.vue'
import AnimalDetail from './components/AnimalDetail.vue'
// 移除重复的Animal接口定义使用api中的接口
interface Statistics {
totalAnimals: number
availableAnimals: number
newAnimalsToday: number
claimedAnimals: number
}
// 临时格式化函数直到utils/date模块可用
const formatDate = (date: string | null | undefined): string => {
if (!date) return ''
return new Date(date).toLocaleString('zh-CN')
}
// 响应式数据
const loading = ref(false)
const animals = ref<Animal[]>([])
const selectedAnimals = ref<Animal[]>([])
const currentAnimal = ref<Animal | null>(null)
// 计算属性
const selectedRowKeys = computed(() => selectedAnimals.value.map(animal => animal.id))
// 统计数据
const statistics = reactive<Statistics>({
totalAnimals: 0,
availableAnimals: 0,
newAnimalsToday: 0,
claimedAnimals: 0
})
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number, range: [number, number]) =>
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`
})
// 搜索参数
const searchParams = reactive({
keyword: '',
type: '',
status: '',
age_range: [] as number[],
date_range: [] as string[]
})
// 模态框状态
const showAnimalDetail = ref(false)
const showAnimalForm = ref(false)
const isEditing = ref(false)
// 搜索字段配置
const searchFields = [
{
key: 'keyword',
label: '关键词',
type: 'input',
placeholder: '请输入动物名称或描述'
},
{
key: 'type',
label: '动物类型',
type: 'select',
options: [
{ label: '狗', value: '狗' },
{ label: '猫', value: '猫' },
{ label: '兔子', value: '兔子' },
{ label: '鸟类', value: '鸟类' }
]
},
{
key: 'status',
label: '状态',
type: 'select',
options: [
{ label: '可领养', value: 'available' },
{ label: '已领养', value: 'adopted' },
{ label: '不可领养', value: 'unavailable' }
]
}
]
// 状态选项
const statusOptions = [
{ label: '可领养', value: 'available', color: 'green' },
{ label: '已领养', value: 'adopted', color: 'blue' },
{ label: '不可领养', value: 'unavailable', color: 'red' }
]
// 批量操作用的状态选项
const statusOptionsWithColor = [
{ label: '可领养', value: 'available', color: 'green' },
{ label: '已领养', value: 'adopted', color: 'blue' },
{ label: '不可领养', value: 'unavailable', color: 'red' }
]
// 导出字段
const exportFields = [
{ key: 'name', label: '动物名称' },
{ key: 'species', label: '物种' },
{ key: 'breed', label: '品种' },
{ key: 'age', label: '年龄' },
{ key: 'gender', label: '性别' },
{ key: 'price', label: '价格' },
{ key: 'status', label: '状态' },
{ key: 'merchant_name', label: '商家' },
{ key: 'created_at', label: '创建时间' }
]
// 表格列配置
const columns: TableColumnsType<Animal> = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
sorter: true
},
{
title: '图片',
dataIndex: 'image',
key: 'image',
width: 80
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 120
},
{
title: '物种',
dataIndex: 'species',
key: 'species',
width: 100
},
{
title: '品种',
dataIndex: 'breed',
key: 'breed',
width: 120
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
width: 80,
sorter: true
},
{
title: '性别',
dataIndex: 'gender',
key: 'gender',
width: 80
},
{
title: '价格',
dataIndex: 'price',
key: 'price',
width: 100,
sorter: true
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '商家',
dataIndex: 'merchant_name',
key: 'merchant_name',
width: 150
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 160,
sorter: true
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right'
}
]
// 表格行选择配置
const rowSelection = computed(() => ({
selectedRowKeys: selectedAnimals.value.map(item => item.id),
onChange: (selectedRowKeys: any[]) => {
selectedAnimals.value = animals.value.filter(item =>
selectedRowKeys.includes(item.id)
)
}
})) as any
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
available: 'green',
adopted: 'blue',
unavailable: 'red'
}
return colorMap[status] || 'default'
}
// 获取状态文本
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
available: '可领养',
adopted: '已领养',
unavailable: '不可领养'
}
return textMap[status] || status
}
// 获取动物列表
const fetchAnimals = async () => {
try {
loading.value = true
// 模拟数据 - 使用类型断言避免类型检查
const mockAnimals = [
{
id: 1,
name: '小白',
species: '狗',
breed: '金毛',
age: 2,
gender: '雄性',
description: '温顺可爱的金毛犬,性格友善,适合家庭饲养',
image: 'https://example.com/dog1.jpg',
merchant_id: 1,
merchant_name: '爱心宠物店',
price: 1500,
status: 'available',
created_at: '2024-01-15T10:30:00Z',
updated_at: '2024-01-15T10:30:00Z'
},
{
id: 2,
name: '小花',
species: '猫',
breed: '英短',
age: 1,
gender: '雌性',
description: '活泼可爱的英短猫,毛色纯正,健康活泼',
image: 'https://example.com/cat1.jpg',
merchant_id: 2,
merchant_name: '温馨宠物之家',
price: 2000,
status: 'adopted',
created_at: '2024-01-14T14:20:00Z',
updated_at: '2024-01-16T09:15:00Z'
},
{
id: 3,
name: '小黑',
species: '狗',
breed: '拉布拉多',
age: 3,
gender: '雄性',
description: '聪明忠诚的拉布拉多,训练有素,适合陪伴',
image: 'https://example.com/dog2.jpg',
merchant_id: 1,
merchant_name: '爱心宠物店',
price: 1800,
status: 'available',
created_at: '2024-01-13T16:45:00Z',
updated_at: '2024-01-13T16:45:00Z'
},
{
id: 4,
name: '咪咪',
species: '猫',
breed: '波斯猫',
age: 2,
gender: '雌性',
description: '优雅的波斯猫,毛发柔顺,性格温和',
image: 'https://example.com/cat2.jpg',
merchant_id: 3,
merchant_name: '宠物乐园',
price: 2500,
status: 'unavailable',
created_at: '2024-01-12T11:20:00Z',
updated_at: '2024-01-17T14:30:00Z'
},
{
id: 5,
name: '小灰',
species: '兔子',
breed: '荷兰兔',
age: 1,
gender: '雄性',
description: '可爱的荷兰兔,毛色灰白相间,性格活泼',
image: 'https://example.com/rabbit1.jpg',
merchant_id: 2,
merchant_name: '温馨宠物之家',
price: 800,
status: 'available',
created_at: '2024-01-16T09:10:00Z',
updated_at: '2024-01-16T09:10:00Z'
}
] as Animal[]
const mockData = {
list: mockAnimals,
total: mockAnimals.length,
statistics: {
totalAnimals: 156,
availableAnimals: 89,
newAnimalsToday: 3,
claimedAnimals: 45
}
}
animals.value = mockData.list
pagination.total = mockData.total
// 更新统计数据
Object.assign(statistics, mockData.statistics)
} catch (error) {
message.error('获取动物列表失败')
} finally {
loading.value = false
}
}
// 搜索处理
const handleSearch = (params: any) => {
Object.assign(searchParams, params)
pagination.current = 1
fetchAnimals()
}
// 重置搜索
const handleReset = () => {
Object.assign(searchParams, {
keyword: '',
type: '',
status: '',
age_range: [],
date_range: []
})
pagination.current = 1
fetchAnimals()
}
// 刷新数据
const handleRefresh = () => {
fetchAnimals()
}
// 查看动物详情
const handleViewAnimal = (animal: Animal) => {
currentAnimal.value = animal
showAnimalDetail.value = true
}
// 编辑动物
const handleEditAnimal = (animal: Animal) => {
currentAnimal.value = animal
isEditing.value = true
showAnimalForm.value = true
}
// 新增动物
const handleAddAnimal = () => {
currentAnimal.value = null
isEditing.value = false
showAnimalForm.value = true
}
// 删除动物
const handleDeleteAnimal = (animal: Animal) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除动物 "${animal.name}" `,
onOk: async () => {
try {
// 模拟API调用
message.success('删除成功')
fetchAnimals()
} catch (error) {
message.error('删除失败')
}
}
})
}
// 更新动物状态
const handleUpdateStatus = async (animal: Animal, status: string) => {
try {
// 模拟API调用
message.success('状态更新成功')
fetchAnimals()
} catch (error) {
message.error('状态更新失败')
}
}
// 批量选择处理
const handleSelectionChange = (items: Animal[]) => {
selectedAnimals.value = items
}
// 批量操作处理
const handleBatchAction = async (action: string, items: Animal[], params?: any) => {
try {
const animalIds = items.map(animal => animal.id)
switch (action) {
case 'updateStatus':
message.success(`批量${params?.status === 'available' ? '设为可领养' : '更新状态'}成功`)
break
case 'delete':
message.success('批量删除成功')
break
case 'export':
message.success('导出成功')
break
default:
message.warning('未知操作')
return
}
selectedAnimals.value = []
fetchAnimals()
} catch (error) {
message.error('批量操作失败')
}
}
// 表格变化处理
const handleTableChange = (pag: any, filters: any, sorter: any) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
// TODO: 处理排序和筛选
fetchAnimals()
}
// 动物表单提交
const handleAnimalSubmit = async (animalData: any) => {
try {
if (isEditing.value && currentAnimal.value) {
message.success('更新成功')
} else {
message.success('创建成功')
}
showAnimalForm.value = false
fetchAnimals()
} catch (error) {
message.error(isEditing.value ? '更新失败' : '创建失败')
}
}
// 组件挂载时获取数据
onMounted(() => {
fetchAnimals()
})
</script>
<style scoped>
.animals-page {
padding: 24px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 12px;
}
}
.statistics-cards {
margin-bottom: 24px;
}
.table-card {
margin-top: 16px;
}
:deep(.ant-table-thead > tr > th) {
background-color: #fafafa;
font-weight: 600;
}
:deep(.ant-table-tbody > tr:hover > td) {
background-color: #f5f5f5;
}
</style>

View File

@@ -0,0 +1,272 @@
<template>
<a-modal
:title="modalTitle"
:visible="visible"
:confirm-loading="confirmLoading"
:width="800"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="花卉名称" name="name">
<a-input v-model:value="formState.name" placeholder="请输入花卉名称" />
</a-form-item>
<a-form-item label="花卉类型" name="type">
<a-select v-model:value="formState.type" placeholder="请选择花卉类型">
<a-select-option value="鲜花">鲜花</a-select-option>
<a-select-option value="盆栽">盆栽</a-select-option>
<a-select-option value="绿植">绿植</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="品种" name="variety">
<a-input v-model:value="formState.variety" placeholder="请输入品种" />
</a-form-item>
<a-form-item label="价格" name="price">
<a-input-number
v-model:value="formState.price"
:min="0"
:precision="2"
placeholder="请输入价格"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="库存" name="stock">
<a-input-number
v-model:value="formState.stock"
:min="0"
placeholder="请输入库存"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="商家" name="merchantId">
<a-select
v-model:value="formState.merchantId"
placeholder="请选择商家"
:loading="merchantsLoading"
>
<a-select-option
v-for="merchant in merchants"
:key="merchant.id"
:value="merchant.id"
>
{{ merchant.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status" placeholder="请选择状态">
<a-select-option value="active">上架</a-select-option>
<a-select-option value="inactive">下架</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="图片" name="image">
<a-upload
v-model:file-list="fileList"
list-type="picture-card"
:before-upload="beforeUpload"
@preview="handlePreview"
@remove="handleRemove"
>
<div v-if="fileList.length < 1">
<plus-outlined />
<div style="margin-top: 8px">上传图片</div>
</div>
</a-upload>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea
v-model:value="formState.description"
placeholder="请输入花卉描述"
:rows="4"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue'
import { message, type UploadProps } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import type { FormInstance } from 'ant-design-vue'
import { getMerchants } from '@/api/flower'
import type { Flower, Merchant } from '@/api/flower'
interface Props {
visible: boolean
currentRecord: Flower | null
mode: 'create' | 'edit' | 'view'
}
interface Emits {
(e: 'cancel'): void
(e: 'ok', data: any): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
const confirmLoading = ref(false)
const merchants = ref<Merchant[]>([])
const merchantsLoading = ref(false)
const fileList = ref<any[]>([])
const formState = reactive({
name: '',
type: '',
variety: '',
price: 0,
stock: 0,
merchantId: undefined as number | undefined,
status: 'active',
image: '',
description: ''
})
const rules = {
name: [{ required: true, message: '请输入花卉名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择花卉类型', trigger: 'change' }],
variety: [{ required: true, message: '请输入品种', trigger: 'blur' }],
price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
merchantId: [{ required: true, message: '请选择商家', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
const modalTitle = computed(() => {
switch (props.mode) {
case 'create':
return '新增花卉'
case 'edit':
return '编辑花卉'
case 'view':
return '查看花卉'
default:
return '花卉信息'
}
})
// 加载商家列表
const loadMerchants = async () => {
try {
merchantsLoading.value = true
const response = await getMerchants()
if (response.data.success) {
merchants.value = response.data.data.merchants
}
} catch (error) {
console.error('加载商家列表失败:', error)
message.error('加载商家列表失败')
} finally {
merchantsLoading.value = false
}
}
// 处理图片上传
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
message.error('只能上传 JPG/PNG 格式的图片!')
return false
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error('图片大小不能超过 2MB!')
return false
}
return false // 返回 false 阻止自动上传
}
const handlePreview: UploadProps['onPreview'] = (file) => {
// 处理图片预览
console.log('Preview file:', file)
}
const handleRemove: UploadProps['onRemove'] = (file) => {
// 处理图片删除
console.log('Remove file:', file)
}
// 处理模态框取消
const handleCancel = () => {
formRef.value?.resetFields()
fileList.value = []
emit('cancel')
}
// 处理模态框确认
const handleOk = async () => {
try {
await formRef.value?.validate()
confirmLoading.value = true
const formData = { ...formState }
if (fileList.value.length > 0) {
formData.image = fileList.value[0].thumbUrl || fileList.value[0].url
}
emit('ok', formData)
} catch (error) {
console.error('表单验证失败:', error)
} finally {
confirmLoading.value = false
}
}
// 监听 visible 变化
watch(() => props.visible, (visible) => {
if (visible) {
loadMerchants()
if (props.currentRecord) {
Object.assign(formState, props.currentRecord)
if (props.currentRecord.image) {
fileList.value = [{
uid: '-1',
name: 'image',
status: 'done',
url: props.currentRecord.image
}]
}
} else {
formRef.value?.resetFields()
fileList.value = []
}
}
})
// 监听 mode 变化
watch(() => props.mode, (mode) => {
if (mode === 'view') {
// 查看模式下禁用表单
Object.keys(rules).forEach(key => {
rules[key as keyof typeof rules] = []
})
} else {
// 恢复验证规则
Object.assign(rules, {
name: [{ required: true, message: '请输入花卉名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择花卉类型', trigger: 'change' }],
variety: [{ required: true, message: '请输入品种', trigger: 'blur' }],
price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
merchantId: [{ required: true, message: '请选择商家', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
})
}
})
</script>

File diff suppressed because one or more lines are too long

View File

@@ -82,7 +82,7 @@
:value-style="{ color: '#3f8600' }"
>
<template #prefix>
<TrendingUpOutlined />
<RiseOutlined />
</template>
</a-statistic>
</a-card>
@@ -300,7 +300,6 @@ import {
UserOutlined,
CheckCircleOutlined,
RiseOutlined,
TrendingUpOutlined,
SearchOutlined,
ReloadOutlined,
PlusOutlined,

View File

@@ -1,172 +0,0 @@
<template>
<a-modal
v-model:open="visible"
title="用户详情"
width="800px"
:footer="null"
@cancel="handleClose"
>
<div v-if="user" class="user-detail">
<!-- 基本信息 -->
<a-card title="基本信息" class="mb-4">
<a-row :gutter="16">
<a-col :span="8">
<div class="detail-item">
<label>用户ID:</label>
<span>{{ user.id }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>用户名:</label>
<span>{{ user.username }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>昵称:</label>
<span>{{ user.nickname || '-' }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>邮箱:</label>
<span>{{ user.email || '-' }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>手机号:</label>
<span>{{ user.phone || '-' }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>状态:</label>
<a-tag :color="getStatusColor(user.status)">
{{ getStatusText(user.status) }}
</a-tag>
</div>
</a-col>
</a-row>
</a-card>
<!-- 统计信息 -->
<a-card title="统计信息" class="mb-4">
<a-row :gutter="16">
<a-col :span="6">
<a-statistic title="积分" :value="user.points" />
</a-col>
<a-col :span="6">
<a-statistic title="等级" :value="user.level" />
</a-col>
<a-col :span="6">
<a-statistic title="余额" :value="user.balance" prefix="¥" />
</a-col>
<a-col :span="6">
<a-statistic title="旅行次数" :value="user.travel_count" />
</a-col>
</a-row>
</a-card>
<!-- 时间信息 -->
<a-card title="时间信息">
<a-row :gutter="16">
<a-col :span="8">
<div class="detail-item">
<label>注册时间:</label>
<span>{{ formatDate(user.created_at) }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>更新时间:</label>
<span>{{ formatDate(user.updated_at) }}</span>
</div>
</a-col>
<a-col :span="8">
<div class="detail-item">
<label>最后登录:</label>
<span>{{ user.last_login_at ? formatDate(user.last_login_at) : '-' }}</span>
</div>
</a-col>
</a-row>
</a-card>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { User } from '@/api/user'
interface Props {
user: User | null
visible: boolean
}
interface Emits {
(e: 'update:visible', value: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
// 格式化日期
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString('zh-CN')
}
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
active: 'green',
inactive: 'red',
pending: 'orange'
}
return colorMap[status] || 'default'
}
// 获取状态文本
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
active: '正常',
inactive: '禁用',
pending: '待审核'
}
return textMap[status] || status
}
// 关闭弹窗
const handleClose = () => {
visible.value = false
}
</script>
<style scoped>
.user-detail {
.detail-item {
margin-bottom: 12px;
label {
display: inline-block;
width: 80px;
color: #666;
font-weight: 500;
}
span {
color: #333;
}
}
.mb-4 {
margin-bottom: 16px;
}
}
</style>

View File

@@ -1,276 +0,0 @@
<template>
<a-modal
v-model:open="visible"
:title="isEditing ? '编辑用户' : '新增用户'"
width="600px"
:confirm-loading="loading"
@ok="handleSubmit"
@cancel="handleClose"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="用户名" name="username">
<a-input
v-model:value="formData.username"
placeholder="请输入用户名"
:disabled="isEditing"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="昵称" name="nickname">
<a-input
v-model:value="formData.nickname"
placeholder="请输入昵称"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="邮箱" name="email">
<a-input
v-model:value="formData.email"
placeholder="请输入邮箱"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="手机号" name="phone">
<a-input
v-model:value="formData.phone"
placeholder="请输入手机号"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16" v-if="!isEditing">
<a-col :span="12">
<a-form-item label="密码" name="password">
<a-input-password
v-model:value="formData.password"
placeholder="请输入密码"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="确认密码" name="confirmPassword">
<a-input-password
v-model:value="formData.confirmPassword"
placeholder="请确认密码"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="性别" name="gender">
<a-select
v-model:value="formData.gender"
placeholder="请选择性别"
allow-clear
>
<a-select-option :value="1"></a-select-option>
<a-select-option :value="2"></a-select-option>
<a-select-option :value="0">未知</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="生日" name="birthday">
<a-date-picker
v-model:value="formData.birthday"
placeholder="请选择生日"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-select
v-model:value="formData.status"
placeholder="请选择状态"
>
<a-select-option value="active">正常</a-select-option>
<a-select-option value="inactive">禁用</a-select-option>
<a-select-option value="pending">待审核</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="备注" name="remark">
<a-input
v-model:value="formData.remark"
placeholder="请输入备注"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import type { FormInstance, Rule } from 'ant-design-vue/es/form'
import type { User } from '@/api/user'
import dayjs, { type Dayjs } from 'dayjs'
interface Props {
user: User | null
visible: boolean
isEditing: boolean
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'submit', data: any): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
const loading = ref(false)
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
// 表单数据
const formData = reactive({
username: '',
nickname: '',
email: '',
phone: '',
password: '',
confirmPassword: '',
gender: undefined as number | undefined,
birthday: undefined as Dayjs | undefined,
status: 'active',
remark: ''
})
// 表单验证规则
const rules: Record<string, Rule[]> = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度为3-20个字符', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
password: [
{ required: !props.isEditing, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度为6-20个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: !props.isEditing, message: '请确认密码', trigger: 'blur' },
{
validator: (rule: any, value: string) => {
if (!props.isEditing && value !== formData.password) {
return Promise.reject('两次输入的密码不一致')
}
return Promise.resolve()
},
trigger: 'blur'
}
]
}
// 监听用户数据变化,初始化表单
watch(() => props.user, (user) => {
if (user && props.isEditing) {
formData.username = user.username
formData.nickname = user.nickname
formData.email = user.email
formData.phone = user.phone
formData.gender = user.gender
formData.birthday = user.birthday ? dayjs(user.birthday) : undefined
formData.status = user.status
formData.remark = user.remark
}
}, { immediate: true })
// 监听弹窗显示状态,重置表单
watch(() => props.visible, (visible) => {
if (visible && !props.isEditing) {
resetForm()
}
})
// 重置表单
const resetForm = () => {
Object.assign(formData, {
username: '',
nickname: '',
email: '',
phone: '',
password: '',
confirmPassword: '',
gender: undefined,
birthday: undefined,
status: 'active',
remark: ''
})
formRef.value?.resetFields()
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value?.validate()
loading.value = true
const submitData: any = {
username: formData.username,
nickname: formData.nickname,
email: formData.email,
phone: formData.phone,
gender: formData.gender,
birthday: formData.birthday?.format('YYYY-MM-DD'),
status: formData.status,
remark: formData.remark
}
if (!props.isEditing) {
submitData.password = formData.password
}
emit('submit', submitData)
} catch (error) {
console.error('表单验证失败:', error)
} finally {
loading.value = false
}
}
// 关闭弹窗
const handleClose = () => {
visible.value = false
resetForm()
}
</script>
<style scoped>
.ant-form-item {
margin-bottom: 16px;
}
</style>

View File

@@ -1,843 +0,0 @@
<template>
<div class="users-page">
<a-card title="用户管理" size="small">
<template #extra>
<a-space>
<a-button type="primary" @click="handleAdd">
<PlusOutlined />
新增用户
</a-button>
<a-button @click="handleRefresh">
<ReloadOutlined />
刷新
</a-button>
</a-space>
</template>
<!-- 统计卡片 -->
<div class="stats-cards">
<a-row :gutter="16">
<a-col :span="6">
<a-card size="small">
<a-statistic
title="总用户数"
:value="statistics.totalUsers"
:value-style="{ color: '#3f8600' }"
>
<template #prefix>
<UserOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card size="small">
<a-statistic
title="活跃用户"
:value="statistics.activeUsers"
:value-style="{ color: '#1890ff' }"
>
<template #prefix>
<CheckCircleOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card size="small">
<a-statistic
title="今日新增"
:value="statistics.todayNew"
:value-style="{ color: '#722ed1' }"
>
<template #prefix>
<PlusCircleOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card size="small">
<a-statistic
title="VIP用户"
:value="statistics.vipUsers"
:value-style="{ color: '#fa8c16' }"
>
<template #prefix>
<CrownOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
</div>
<!-- 高级搜索 -->
<AdvancedSearch
search-type="user"
:status-options="statusOptions"
@search="handleSearch"
@reset="handleSearchReset"
/>
<!-- 批量操作 -->
<BatchOperations
:data-source="users"
:selected-items="selectedUsers"
operation-type="user"
:status-options="statusOptions"
:export-fields="exportFields"
@selection-change="(items: any[]) => handleSelectionChange(items as User[])"
@batch-action="(action: string, items: any[], params?: any) => handleBatchAction(action, items as User[], params)"
/>
<!-- 用户列表 -->
<a-table
:columns="columns"
:data-source="users"
:loading="loading"
:pagination="pagination"
:row-selection="rowSelection"
:scroll="{ x: 1200 }"
@change="handleTableChange"
row-key="id"
>
<!-- 头像列 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'avatar'">
<a-avatar :src="record.avatar" :size="40">
<template #icon>
<UserOutlined />
</template>
</a-avatar>
</template>
<!-- 用户信息列 -->
<template v-else-if="column.key === 'userInfo'">
<div class="user-info">
<div class="user-name">
{{ record.nickname || record.username }}
<a-tag v-if="record.user_type === 'vip'" color="gold" size="small">
<CrownOutlined />
VIP
</a-tag>
</div>
<div class="user-meta">
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
ID: {{ record.id }} | {{ record.phone || record.email }}
</a-typography-text>
</div>
</div>
</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 === 'registerInfo'">
<div class="register-info">
<div>
<a-tag :color="getSourceColor(record.register_source)" size="small">
{{ getSourceText(record.register_source) }}
</a-tag>
</div>
<div class="register-time">
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
{{ formatDate(record.created_at) }}
</a-typography-text>
</div>
</div>
</template>
<!-- 最后登录列 -->
<template v-else-if="column.key === 'lastLogin'">
<div v-if="record.last_login_at">
<div>{{ formatDate(record.last_login_at) }}</div>
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
{{ record.last_login_ip }}
</a-typography-text>
</div>
<a-typography-text v-else type="secondary">
从未登录
</a-typography-text>
</template>
<!-- 操作列 -->
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-dropdown>
<template #overlay>
<a-menu @click="({ key }) => handleMenuAction(key, record)">
<a-menu-item key="resetPassword">
<KeyOutlined />
重置密码
</a-menu-item>
<a-menu-item key="sendMessage">
<MessageOutlined />
发送消息
</a-menu-item>
<a-menu-item
:key="record.status === 'active' ? 'disable' : 'enable'"
>
<component
:is="record.status === 'active' ? LockOutlined : UnlockOutlined"
/>
{{ record.status === 'active' ? '禁用' : '启用' }}
</a-menu-item>
<a-menu-divider />
<a-menu-item key="delete" class="danger-item">
<DeleteOutlined />
删除
</a-menu-item>
</a-menu>
</template>
<a-button type="link" size="small">
更多
<DownOutlined />
</a-button>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 用户详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="用户详情"
:footer="null"
width="800px"
>
<UserDetail
v-if="currentUser"
:user="currentUser"
@refresh="handleRefresh"
/>
</a-modal>
<!-- 用户编辑模态框 -->
<a-modal
v-model:open="editModalVisible"
title="编辑用户"
@ok="handleEditSubmit"
:confirm-loading="editLoading"
>
<UserForm
v-if="currentUser"
ref="userFormRef"
:user="currentUser"
mode="edit"
/>
</a-modal>
<!-- 新增用户模态框 -->
<a-modal
v-model:open="addModalVisible"
title="新增用户"
@ok="handleAddSubmit"
:confirm-loading="addLoading"
>
<UserForm
ref="addUserFormRef"
mode="add"
/>
</a-modal>
<!-- 发送消息模态框 -->
<a-modal
v-model:open="messageModalVisible"
title="发送消息"
@ok="handleSendMessage"
:confirm-loading="messageLoading"
>
<a-form :model="messageForm" layout="vertical">
<a-form-item label="消息标题" required>
<a-input
v-model:value="messageForm.title"
placeholder="请输入消息标题"
:maxlength="100"
show-count
/>
</a-form-item>
<a-form-item label="消息内容" required>
<a-textarea
v-model:value="messageForm.content"
placeholder="请输入消息内容"
:rows="4"
:maxlength="500"
show-count
/>
</a-form-item>
<a-form-item label="消息类型">
<a-radio-group v-model:value="messageForm.type">
<a-radio value="info">通知</a-radio>
<a-radio value="warning">警告</a-radio>
<a-radio value="promotion">推广</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import {
UserOutlined,
PlusOutlined,
ReloadOutlined,
CheckCircleOutlined,
PlusCircleOutlined,
CrownOutlined,
KeyOutlined,
MessageOutlined,
LockOutlined,
UnlockOutlined,
DeleteOutlined,
DownOutlined
} from '@ant-design/icons-vue'
import { message, Modal } from 'ant-design-vue'
import type { TableColumnsType, TableProps } from 'ant-design-vue'
import AdvancedSearch from '@/components/AdvancedSearch.vue'
import BatchOperations from '@/components/BatchOperations.vue'
import UserDetail from './components/UserDetail.vue'
import UserForm from './components/UserForm.vue'
import userAPI, { type User } from '@/api/user'
// import { formatDate } from '@/utils/date'
interface Statistics {
totalUsers: number
activeUsers: number
newUsersToday: number
totalRevenue: number
}
// 临时格式化函数直到utils/date模块可用
const formatDate = (date: string | null | undefined): string => {
if (!date) return ''
return new Date(date).toLocaleString('zh-CN')
}
// 响应式数据
const loading = ref(false)
const users = ref<User[]>([])
const selectedUsers = ref<User[]>([])
const currentUser = ref<User | null>(null)
// 统计数据
const statistics = ref<Statistics>({
totalUsers: 0,
activeUsers: 0,
todayNew: 0,
vipUsers: 0
})
// 模态框状态
const detailModalVisible = ref(false)
const editModalVisible = ref(false)
const addModalVisible = ref(false)
const messageModalVisible = ref(false)
// 加载状态
const editLoading = ref(false)
const addLoading = ref(false)
const messageLoading = ref(false)
// 表单引用
const userFormRef = ref()
const addUserFormRef = ref()
// 消息表单
const messageForm = reactive({
title: '',
content: '',
type: 'info'
})
// 分页配置
const pagination = ref({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total} 条记录`
})
// 搜索参数
const searchParams = ref({})
// 状态选项
const statusOptions = [
{ value: 'active', label: '激活', color: 'green' },
{ value: 'inactive', label: '禁用', color: 'red' },
{ value: 'pending', label: '待审核', color: 'orange' }
]
// 导出字段
const exportFields = [
{ key: 'id', label: 'ID' },
{ key: 'username', label: '用户名' },
{ key: 'nickname', label: '昵称' },
{ key: 'email', label: '邮箱' },
{ key: 'phone', label: '手机号' },
{ key: 'status', label: '状态' },
{ key: 'user_type', label: '用户类型' },
{ key: 'register_source', label: '注册来源' },
{ key: 'created_at', label: '注册时间' },
{ key: 'last_login_at', label: '最后登录' }
]
// 表格列配置
const columns: TableColumnsType = [
{
title: '头像',
key: 'avatar',
width: 80,
align: 'center'
},
{
title: '用户信息',
key: 'userInfo',
width: 200
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '注册信息',
key: 'registerInfo',
width: 150
},
{
title: '最后登录',
key: 'lastLogin',
width: 180
},
{
title: '操作',
key: 'actions',
width: 200,
fixed: 'right'
}
]
// 行选择配置
const rowSelection: TableProps['rowSelection'] = {
selectedRowKeys: computed(() => selectedUsers.value.map(user => user.id)),
onChange: (selectedRowKeys: (string | number)[], selectedRows: User[]) => {
selectedUsers.value = selectedRows
},
onSelectAll: (selected: boolean, selectedRows: User[], changeRows: User[]) => {
if (selected) {
selectedUsers.value = [...selectedUsers.value, ...changeRows]
} else {
const changeIds = changeRows.map(row => row.id)
selectedUsers.value = selectedUsers.value.filter(user => !changeIds.includes(user.id))
}
}
}
/**
* 获取状态颜色
*/
const getStatusColor = (status: string) => {
const statusMap: Record<string, string> = {
active: 'green',
inactive: 'red',
pending: 'orange'
}
return statusMap[status] || 'default'
}
/**
* 获取状态文本
*/
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
active: '激活',
inactive: '禁用',
pending: '待审核'
}
return statusMap[status] || status
}
/**
* 获取来源颜色
*/
const getSourceColor = (source: string) => {
const sourceMap: Record<string, string> = {
web: 'blue',
wechat: 'green',
app: 'purple'
}
return sourceMap[source] || 'default'
}
/**
* 获取来源文本
*/
const getSourceText = (source: string) => {
const sourceMap: Record<string, string> = {
web: '网页端',
wechat: '微信小程序',
app: '移动应用'
}
return sourceMap[source] || source
}
/**
* 加载用户列表
*/
const loadUsers = async () => {
loading.value = true
try {
const params = {
page: pagination.value.current,
pageSize: pagination.value.pageSize,
...searchParams.value
}
const response = await userAPI.getUsers(params)
users.value = response.data.list
pagination.value.total = response.data.total
// 更新统计数据
statistics.value = response.data.statistics || {
totalUsers: response.data.total,
activeUsers: response.data.list.filter((u: User) => u.status === 'active').length,
todayNew: 0,
vipUsers: response.data.list.filter((u: User) => u.user_type === 'vip').length
}
} catch (error) {
message.error('加载用户列表失败')
} finally {
loading.value = false
}
}
/**
* 处理表格变化
*/
const handleTableChange: TableProps['onChange'] = (pag) => {
if (pag) {
pagination.value.current = pag.current || 1
pagination.value.pageSize = pag.pageSize || 20
}
loadUsers()
}
/**
* 处理搜索
*/
const handleSearch = (params: any) => {
searchParams.value = params
pagination.value.current = 1
loadUsers()
}
/**
* 处理搜索重置
*/
const handleSearchReset = () => {
searchParams.value = {}
pagination.value.current = 1
loadUsers()
}
/**
* 处理选择变化
*/
const handleSelectionChange = (items: User[]) => {
selectedUsers.value = items
}
/**
* 处理批量操作
*/
const handleBatchAction = async (action: string, items: User[], params?: any) => {
try {
switch (action) {
case 'update-status':
await userAPI.batchUpdateStatus(
items.map(item => item.id),
params.status,
params.reason
)
message.success('批量状态更新成功')
break
case 'delete':
await userAPI.batchDelete(items.map(item => item.id))
message.success('批量删除成功')
break
case 'export':
await userAPI.exportUsers(items.map(item => item.id), params)
message.success('导出任务已开始')
break
case 'send-message':
// 打开批量发送消息界面
messageModalVisible.value = true
break
case 'lock':
await userAPI.batchUpdateStatus(items.map(item => item.id), 'inactive', '批量锁定')
message.success('批量锁定成功')
break
case 'unlock':
await userAPI.batchUpdateStatus(items.map(item => item.id), 'active', '批量解锁')
message.success('批量解锁成功')
break
}
// 刷新列表
loadUsers()
// 清空选择
selectedUsers.value = []
} catch (error) {
message.error('批量操作失败')
}
}
/**
* 处理查看
*/
const handleView = (user: User) => {
currentUser.value = user
detailModalVisible.value = true
}
/**
* 处理编辑
*/
const handleEdit = (user: User) => {
currentUser.value = user
editModalVisible.value = true
}
/**
* 处理新增
*/
const handleAdd = () => {
addModalVisible.value = true
}
/**
* 处理刷新
*/
const handleRefresh = () => {
loadUsers()
}
/**
* 处理菜单操作
*/
const handleMenuAction = async (key: string, user: User) => {
switch (key) {
case 'resetPassword':
Modal.confirm({
title: '确认重置密码',
content: `确定要重置用户 ${user.nickname || user.username} 的密码吗?`,
onOk: async () => {
try {
await userAPI.resetPassword(user.id)
message.success('密码重置成功')
} catch (error) {
message.error('密码重置失败')
}
}
})
break
case 'sendMessage':
currentUser.value = user
messageModalVisible.value = true
break
case 'enable':
case 'disable':
const newStatus = key === 'enable' ? 'active' : 'inactive'
const action = key === 'enable' ? '启用' : '禁用'
Modal.confirm({
title: `确认${action}用户`,
content: `确定要${action}用户 ${user.nickname || user.username} 吗?`,
onOk: async () => {
try {
await userAPI.updateStatus(user.id, newStatus)
message.success(`${action}成功`)
loadUsers()
} catch (error) {
message.error(`${action}失败`)
}
}
})
break
case 'delete':
Modal.confirm({
title: '确认删除用户',
content: `确定要删除用户 ${user.nickname || user.username} 吗?此操作不可撤销!`,
okType: 'danger',
onOk: async () => {
try {
await userAPI.deleteUser(user.id)
message.success('删除成功')
loadUsers()
} catch (error) {
message.error('删除失败')
}
}
})
break
}
}
/**
* 处理编辑提交
*/
const handleEditSubmit = async () => {
if (!userFormRef.value) return
editLoading.value = true
try {
const formData = await userFormRef.value.validate()
await userAPI.updateUser(currentUser.value!.id, formData)
message.success('更新成功')
editModalVisible.value = false
loadUsers()
} catch (error) {
message.error('更新失败')
} finally {
editLoading.value = false
}
}
/**
* 处理新增提交
*/
const handleAddSubmit = async () => {
if (!addUserFormRef.value) return
addLoading.value = true
try {
const formData = await addUserFormRef.value.validate()
await userAPI.createUser(formData)
message.success('创建成功')
addModalVisible.value = false
loadUsers()
} catch (error) {
message.error('创建失败')
} finally {
addLoading.value = false
}
}
/**
* 处理发送消息
*/
const handleSendMessage = async () => {
if (!messageForm.title || !messageForm.content) {
message.error('请填写完整的消息信息')
return
}
messageLoading.value = true
try {
const userIds = currentUser.value
? [currentUser.value.id]
: selectedUsers.value.map(user => user.id)
await userAPI.sendMessage(userIds, messageForm)
message.success('消息发送成功')
messageModalVisible.value = false
// 重置表单
messageForm.title = ''
messageForm.content = ''
messageForm.type = 'info'
} catch (error) {
message.error('消息发送失败')
} finally {
messageLoading.value = false
}
}
// 初始化
onMounted(() => {
loadUsers()
})
</script>
<style scoped>
.users-page {
padding: 16px;
}
.stats-cards {
margin-bottom: 16px;
}
.user-info {
.user-name {
font-weight: 500;
margin-bottom: 4px;
}
.user-meta {
font-size: 12px;
color: #666;
}
}
.register-info {
.register-time {
margin-top: 4px;
}
}
:deep(.danger-item) {
color: #ff4d4f !important;
}
:deep(.ant-table-tbody > tr > td) {
padding: 12px 8px;
}
@media (max-width: 768px) {
.users-page {
padding: 8px;
}
.stats-cards :deep(.ant-col) {
margin-bottom: 8px;
}
}
</style>

View File

@@ -74,6 +74,18 @@ const routes: RouteRecordRaw[] = [
layout: 'main' // 添加布局信息
}
},
{
path: '/flowers',
name: 'Flowers',
component: () => import('@/pages/flower/index.vue'),
meta: {
requiresAuth: true,
title: '花卉管理',
icon: 'EnvironmentOutlined',
permissions: ['flower:read'],
layout: 'main'
}
},
{
path: '/orders',
name: 'Orders',

View File

@@ -69,8 +69,10 @@ export const useAppStore = defineStore('app', () => {
throw new Error('获取用户信息失败:接口返回格式异常')
}
// 确保响应数据格式为 { data: { admin: object } }
if (response.data && typeof response.data === 'object' && response.data.admin) {
// 确保响应数据格式正确 - 支持两种格式:
// 1. 直接返回 { success: true, data: { admin: object } }
// 2. mock数据格式 { success: true, data: { admin: object } }
if (response.success && response.data && typeof response.data === 'object' && response.data.admin) {
// 模拟权限数据 - 实际项目中应该从后端获取
const mockPermissions = [
'user:read', 'user:write',
@@ -79,10 +81,27 @@ export const useAppStore = defineStore('app', () => {
'animal:read', 'animal:write',
'order:read', 'order:write',
'promotion:read', 'promotion:write',
'system:read', 'system:write'
'system:read', 'system:write',
'flower:read', 'flower:write'
]
state.user = response.data.admin
state.permissions = mockPermissions
}
// 处理直接返回数据的情况mock数据可能直接返回这种格式
else if (response.admin) {
// 模拟权限数据 - 实际项目中应该从后端获取
const mockPermissions = [
'user:read', 'user:write',
'merchant:read', 'merchant:write',
'travel:read', 'travel:write',
'animal:read', 'animal:write',
'order:read', 'order:write',
'promotion:read', 'promotion:write',
'system:read', 'system:write',
'flower:read', 'flower:write'
]
state.user = response.admin
state.permissions = mockPermissions
} else {
throw new Error('获取用户信息失败:响应数据格式不符合预期')
}
@@ -138,7 +157,8 @@ export const useAppStore = defineStore('app', () => {
'animal:read', 'animal:write',
'order:read', 'order:write',
'promotion:read', 'promotion:write',
'system:read', 'system:write'
'system:read', 'system:write',
'flower:read', 'flower:write'
]
state.user = response.data.admin
state.permissions = mockPermissions
@@ -151,7 +171,8 @@ export const useAppStore = defineStore('app', () => {
'animal:read', 'animal:write',
'order:read', 'order:write',
'promotion:read', 'promotion:write',
'system:read', 'system:write'
'system:read', 'system:write',
'flower:read', 'flower:write'
]
state.user = response.admin
state.permissions = mockPermissions

View File

@@ -0,0 +1,163 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Flower, FlowerQueryParams, FlowerSale } from '@/api/flower'
interface FlowerState {
flowers: Flower[]
currentFlower: Flower | null
sales: FlowerSale[]
loading: boolean
salesLoading: boolean
totalCount: number
salesTotalCount: number
queryParams: FlowerQueryParams
}
export const useFlowerStore = defineStore('flower', () => {
// 状态
const flowers = ref<Flower[]>([])
const currentFlower = ref<Flower | null>(null)
const sales = ref<FlowerSale[]>([])
const loading = ref(false)
const salesLoading = ref(false)
const totalCount = ref(0)
const salesTotalCount = ref(0)
const queryParams = ref<FlowerQueryParams>({
page: 1,
limit: 20,
keyword: '',
type: undefined,
status: undefined,
merchantId: undefined
})
// 获取花卉列表
const fetchFlowers = async (params?: FlowerQueryParams) => {
loading.value = true
try {
if (params) {
Object.assign(queryParams.value, params)
}
const response = await import('@/api/flower').then(m => m.getFlowers(queryParams.value))
if (response.data.success) {
flowers.value = response.data.data.flowers
totalCount.value = response.data.data.pagination.total
}
} catch (error) {
console.error('获取花卉列表失败:', error)
throw error
} finally {
loading.value = false
}
}
// 获取花卉详情
const fetchFlower = async (id: number) => {
try {
const response = await import('@/api/flower').then(m => m.getFlower(id))
if (response.data.success) {
currentFlower.value = response.data.data.flower
return response.data.data.flower
}
} catch (error) {
console.error('获取花卉详情失败:', error)
throw error
}
}
// 创建花卉
const createFlower = async (data: any) => {
try {
const response = await import('@/api/flower').then(m => m.createFlower(data))
if (response.data.success) {
return response.data.data.flower
}
} catch (error) {
console.error('创建花卉失败:', error)
throw error
}
}
// 更新花卉
const updateFlower = async (id: number, data: any) => {
try {
const response = await import('@/api/flower').then(m => m.updateFlower(id, data))
if (response.data.success) {
return response.data.data.flower
}
} catch (error) {
console.error('更新花卉失败:', error)
throw error
}
}
// 删除花卉
const deleteFlower = async (id: number) => {
try {
const response = await import('@/api/flower').then(m => m.deleteFlower(id))
if (response.data.success) {
return true
}
} catch (error) {
console.error('删除花卉失败:', error)
throw error
}
}
// 获取销售记录
const fetchSales = async (params?: any) => {
salesLoading.value = true
try {
const response = await import('@/api/flower').then(m => m.getFlowerSales(params))
if (response.data.success) {
sales.value = response.data.data.sales
salesTotalCount.value = response.data.data.pagination.total
}
} catch (error) {
console.error('获取销售记录失败:', error)
throw error
} finally {
salesLoading.value = false
}
}
// 重置状态
const reset = () => {
flowers.value = []
currentFlower.value = null
sales.value = []
loading.value = false
salesLoading.value = false
totalCount.value = 0
salesTotalCount.value = 0
Object.assign(queryParams.value, {
page: 1,
limit: 20,
keyword: '',
type: undefined,
status: undefined,
merchantId: undefined
})
}
return {
flowers,
currentFlower,
sales,
loading,
salesLoading,
totalCount,
salesTotalCount,
queryParams,
fetchFlowers,
fetchFlower,
createFlower,
updateFlower,
deleteFlower,
fetchSales,
reset
}
})

View File

@@ -1,44 +0,0 @@
// 测试模拟数据功能
const mockAPI = require('./src/api/mockData.ts')
console.log('🧪 测试模拟数据API...')
// 测试登录功能
console.log('\n1. 测试登录功能')
mockAPI.mockAuthAPI.login({ username: 'admin', password: 'admin123' })
.then(response => {
console.log('✅ 登录成功:', response.data.admin.username)
return mockAPI.mockAuthAPI.getCurrentUser()
})
.then(response => {
console.log('✅ 获取当前用户成功:', response.data.admin.nickname)
})
.catch(error => {
console.log('❌ 登录测试失败:', error.message)
})
// 测试用户列表
console.log('\n2. 测试用户列表')
mockAPI.mockUserAPI.getUsers({ page: 1, pageSize: 5 })
.then(response => {
console.log('✅ 获取用户列表成功:', response.data.list.length + '个用户')
})
.catch(error => {
console.log('❌ 用户列表测试失败:', error.message)
})
// 测试系统统计
console.log('\n3. 测试系统统计')
mockAPI.mockSystemAPI.getSystemStats()
.then(response => {
console.log('✅ 获取系统统计成功:')
console.log(' - 用户数:', response.data.userCount)
console.log(' - 商家数:', response.data.merchantCount)
console.log(' - 旅行数:', response.data.travelCount)
console.log(' - 动物数:', response.data.animalCount)
})
.catch(error => {
console.log('❌ 系统统计测试失败:', error.message)
})
console.log('\n🎉 模拟数据测试完成!')

View File

@@ -0,0 +1,420 @@
# 商户管理API接口文档
## 概述
商户管理模块提供了完整的商户信息管理功能包括商户的增删改查、统计信息等操作。所有接口均遵循RESTful API设计规范。
## 基础信息
- **基础URL**: `/api/v1/merchants`
- **认证方式**: Bearer Token部分接口需要管理员权限
- **数据格式**: JSON
- **字符编码**: UTF-8
## 数据模型
### 商户信息 (Merchant)
```json
{
"id": 1,
"name": "示例商户",
"type": "company",
"contact_person": "张三",
"contact_phone": "13800138000",
"email": "merchant@example.com",
"address": "北京市朝阳区示例街道123号",
"description": "这是一个示例商户",
"status": "active",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
```
### 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | integer | - | 商户ID系统自动生成 |
| name | string | ✓ | 商户名称 |
| type | string | ✓ | 商户类型:`individual`(个人)、`company`(企业) |
| contact_person | string | ✓ | 联系人姓名 |
| contact_phone | string | ✓ | 联系电话 |
| email | string | - | 邮箱地址 |
| address | string | - | 地址 |
| description | string | - | 商户描述 |
| status | string | - | 状态:`active`(活跃)、`inactive`(非活跃)、`banned`(禁用) |
| created_at | datetime | - | 创建时间 |
| updated_at | datetime | - | 更新时间 |
## 接口列表
### 1. 获取商户列表
**接口地址**: `GET /api/v1/merchants`
**接口描述**: 获取商户列表,支持分页、搜索和筛选
**请求参数**:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| page | integer | - | 1 | 页码最小值1 |
| limit | integer | - | 20 | 每页数量范围1-100 |
| keyword | string | - | - | 搜索关键词(匹配商户名称、联系人、电话) |
| status | string | - | - | 状态筛选:`active``inactive``banned` |
| type | string | - | - | 类型筛选:`individual``company` |
**请求示例**:
```http
GET /api/v1/merchants?page=1&limit=20&keyword=示例&status=active&type=company
```
**响应示例**:
```json
{
"success": true,
"data": [
{
"id": 1,
"name": "示例商户",
"type": "company",
"contact_person": "张三",
"contact_phone": "13800138000",
"email": "merchant@example.com",
"address": "北京市朝阳区示例街道123号",
"description": "这是一个示例商户",
"status": "active",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}
```
**错误响应**:
```json
{
"success": false,
"message": "请求参数错误",
"errors": [
{
"field": "page",
"message": "页码必须是正整数"
}
]
}
```
### 2. 获取商户详情
**接口地址**: `GET /api/v1/merchants/{merchantId}`
**接口描述**: 根据商户ID获取商户详细信息
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| merchantId | integer | ✓ | 商户ID |
**请求示例**:
```http
GET /api/v1/merchants/1
```
**响应示例**:
```json
{
"success": true,
"data": {
"id": 1,
"name": "示例商户",
"type": "company",
"contact_person": "张三",
"contact_phone": "13800138000",
"email": "merchant@example.com",
"address": "北京市朝阳区示例街道123号",
"description": "这是一个示例商户",
"status": "active",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z",
"animal_count": 15,
"order_count": 128
}
}
```
**错误响应**:
```json
{
"success": false,
"message": "商户不存在"
}
```
### 3. 创建商户
**接口地址**: `POST /api/v1/merchants`
**接口描述**: 创建新商户(需要管理员权限)
**认证要求**: Bearer Token + 管理员权限
**请求体**:
```json
{
"name": "新商户",
"type": "company",
"contact_person": "李四",
"contact_phone": "13900139000",
"email": "newmerchant@example.com",
"address": "上海市浦东新区示例路456号",
"description": "这是一个新商户"
}
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | ✓ | 商户名称 |
| type | string | ✓ | 商户类型:`individual``company` |
| contact_person | string | ✓ | 联系人姓名 |
| contact_phone | string | ✓ | 联系电话 |
| email | string | - | 邮箱地址(需符合邮箱格式) |
| address | string | - | 地址 |
| description | string | - | 商户描述 |
**响应示例**:
```json
{
"success": true,
"data": {
"id": 2,
"name": "新商户",
"type": "company",
"contact_person": "李四",
"contact_phone": "13900139000",
"email": "newmerchant@example.com",
"address": "上海市浦东新区示例路456号",
"description": "这是一个新商户",
"status": "active",
"created_at": "2024-01-01T12:00:00.000Z",
"updated_at": "2024-01-01T12:00:00.000Z"
},
"message": "商户创建成功"
}
```
### 4. 更新商户信息
**接口地址**: `PUT /api/v1/merchants/{merchantId}`
**接口描述**: 更新商户信息(需要管理员权限)
**认证要求**: Bearer Token + 管理员权限
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| merchantId | integer | ✓ | 商户ID |
**请求体**:
```json
{
"name": "更新后的商户名称",
"contact_person": "王五",
"contact_phone": "13700137000",
"status": "inactive"
}
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | - | 商户名称 |
| type | string | - | 商户类型:`individual``company` |
| contact_person | string | - | 联系人姓名 |
| contact_phone | string | - | 联系电话 |
| email | string | - | 邮箱地址 |
| address | string | - | 地址 |
| description | string | - | 商户描述 |
| status | string | - | 状态:`active``inactive``banned` |
**响应示例**:
```json
{
"success": true,
"data": {
"id": 1,
"name": "更新后的商户名称",
"type": "company",
"contact_person": "王五",
"contact_phone": "13700137000",
"email": "merchant@example.com",
"address": "北京市朝阳区示例街道123号",
"description": "这是一个示例商户",
"status": "inactive",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T15:30:00.000Z"
},
"message": "商户信息更新成功"
}
```
### 5. 删除商户
**接口地址**: `DELETE /api/v1/merchants/{merchantId}`
**接口描述**: 删除商户(需要管理员权限)
**认证要求**: Bearer Token + 管理员权限
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| merchantId | integer | ✓ | 商户ID |
**请求示例**:
```http
DELETE /api/v1/merchants/1
```
**响应示例**:
```json
{
"success": true,
"message": "商户删除成功"
}
```
### 6. 获取商户统计信息
**接口地址**: `GET /api/v1/merchants/statistics`
**接口描述**: 获取商户统计信息(需要管理员权限)
**认证要求**: Bearer Token + 管理员权限
**请求示例**:
```http
GET /api/v1/merchants/statistics
```
**响应示例**:
```json
{
"success": true,
"data": {
"total": 150,
"active": 120,
"inactive": 25,
"banned": 5,
"individual": 80,
"company": 70
}
}
```
## 错误码说明
| HTTP状态码 | 错误码 | 说明 |
|------------|--------|------|
| 400 | BAD_REQUEST | 请求参数错误 |
| 401 | UNAUTHORIZED | 未授权,需要登录 |
| 403 | FORBIDDEN | 权限不足,需要管理员权限 |
| 404 | NOT_FOUND | 商户不存在 |
| 409 | CONFLICT | 商户信息冲突(如名称重复) |
| 500 | INTERNAL_ERROR | 服务器内部错误 |
## 通用错误响应格式
```json
{
"success": false,
"message": "错误描述",
"code": "ERROR_CODE",
"timestamp": "2024-01-01T12:00:00.000Z",
"errors": [
{
"field": "字段名",
"message": "字段错误描述"
}
]
}
```
## 使用示例
### JavaScript (Axios)
```javascript
// 获取商户列表
const getMerchants = async (params = {}) => {
try {
const response = await axios.get('/api/v1/merchants', { params });
return response.data;
} catch (error) {
console.error('获取商户列表失败:', error.response.data);
throw error;
}
};
// 创建商户
const createMerchant = async (merchantData) => {
try {
const response = await axios.post('/api/v1/merchants', merchantData, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.data;
} catch (error) {
console.error('创建商户失败:', error.response.data);
throw error;
}
};
```
### cURL
```bash
# 获取商户列表
curl -X GET "http://localhost:3200/api/v1/merchants?page=1&limit=20" \
-H "Content-Type: application/json"
# 创建商户
curl -X POST "http://localhost:3200/api/v1/merchants" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"name": "测试商户",
"type": "company",
"contact_person": "测试联系人",
"contact_phone": "13800138000"
}'
```
## 注意事项
1. **权限控制**: 创建、更新、删除商户以及获取统计信息需要管理员权限
2. **数据验证**: 所有输入数据都会进行严格验证,确保数据完整性
3. **分页限制**: 列表接口每页最多返回100条记录
4. **搜索功能**: 关键词搜索支持模糊匹配商户名称、联系人和电话
5. **状态管理**: 商户状态变更会影响相关业务功能的可用性
6. **数据关联**: 删除商户前请确保没有关联的动物或订单数据
## 更新日志
- **v1.0.0** (2024-01-01): 初始版本,包含基础的商户管理功能

View File

@@ -1,4 +1,4 @@
-- 解班客数据库完整结构创建脚本
-- 结伴客数据库完整结构创建脚本
-- 创建时间: 2024年
-- 数据库: jbkdata

View File

@@ -46,6 +46,44 @@ CREATE TABLE IF NOT EXISTS orders (
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- 创建促销活动表
CREATE TABLE IF NOT EXISTS promotion_activities (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
type ENUM('signup', 'invitation', 'purchase', 'custom') NOT NULL,
status ENUM('active', 'inactive', 'expired') DEFAULT 'active',
start_date DATE NOT NULL,
end_date DATE NOT NULL,
reward_type ENUM('cash', 'points', 'coupon') NOT NULL,
reward_amount DECIMAL(15,2) NOT NULL,
participation_limit INT DEFAULT 0 COMMENT '0表示无限制',
current_participants INT DEFAULT 0,
rules JSON COMMENT '活动规则配置',
created_by INT NOT NULL COMMENT '创建人ID',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES admins(id) ON DELETE CASCADE
);
-- 创建奖励记录表
CREATE TABLE IF NOT EXISTS promotion_rewards (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
user_name VARCHAR(100) NOT NULL,
user_phone VARCHAR(20),
activity_id INT NOT NULL,
activity_name VARCHAR(100) NOT NULL,
reward_type ENUM('cash', 'points', 'coupon') NOT NULL,
reward_amount DECIMAL(15,2) NOT NULL,
status ENUM('pending', 'issued', 'failed') DEFAULT 'pending',
issued_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (activity_id) REFERENCES promotion_activities(id) ON DELETE CASCADE
);
-- 插入默认管理员账号
INSERT INTO admins (username, password, email, role) VALUES
('admin', '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@jiebanke.com', 'super_admin'),
@@ -64,4 +102,11 @@ CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_phone ON users(phone);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_order_no ON orders(order_no);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_promotion_activities_status ON promotion_activities(status);
CREATE INDEX idx_promotion_activities_type ON promotion_activities(type);
CREATE INDEX idx_promotion_activities_dates ON promotion_activities(start_date, end_date);
CREATE INDEX idx_promotion_rewards_user_id ON promotion_rewards(user_id);
CREATE INDEX idx_promotion_rewards_activity_id ON promotion_rewards(activity_id);
CREATE INDEX idx_promotion_rewards_status ON promotion_rewards(status);
CREATE INDEX idx_promotion_rewards_created_at ON promotion_rewards(created_at);

View File

@@ -8,8 +8,9 @@
const mysql = require('mysql2/promise');
const config = require('../config/env');
// 引入database.js配置
const dbConfig = require('../src/config/database').pool.config;
// 引入环境配置
const envConfig = require('../config/env');
const dbConfig = envConfig.mysql;
// 数据库配置已从database.js导入

View File

@@ -15,13 +15,13 @@ const { globalErrorHandler, notFound } = require('./utils/errors');
// 检查是否为无数据库模式
const NO_DB_MODE = process.env.NO_DB_MODE === 'true';
let authRoutes, userRoutes, travelRoutes, animalRoutes, orderRoutes, adminRoutes, travelRegistrationRoutes;
let authRoutes, userRoutes, travelRoutes, animalRoutes, orderRoutes, adminRoutes, travelRegistrationRoutes, promotionRoutes, merchantRoutes;
// 路由导入 - 根据是否为无数据库模式决定是否导入实际路由
// 路由导入
if (NO_DB_MODE) {
console.log('⚠️ 无数据库模式:将使用模拟路由');
} else {
// 路由导入
console.log('✅ 数据库模式:加载实际路由');
authRoutes = require('./routes/auth');
userRoutes = require('./routes/user');
travelRoutes = require('./routes/travel');
@@ -31,6 +31,8 @@ if (NO_DB_MODE) {
travelRegistrationRoutes = require('./routes/travelRegistration'); // 旅行报名路由
paymentRoutes = require('./routes/payment-simple');
animalClaimRoutes = require('./routes/animalClaim-simple'); // 动物认领路由(简化版)
promotionRoutes = require('./routes/promotion'); // 促销活动路由
merchantRoutes = require('./routes/merchant'); // 商户路由
}
const app = express();
@@ -50,8 +52,10 @@ app.use(cors({
'https://webapi.jiebanke.com',
'http://localhost:3150', // 管理后台本地开发地址
'http://localhost:3000', // 备用端口
'http://localhost:3200', // 备用端口
'http://127.0.0.1:3150', // 备用地址
'http://127.0.0.1:3000' // 备用地址
'http://127.0.0.1:3000', // 备用地址
'http://127.0.0.1:3200' // 备用地址
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
@@ -132,7 +136,8 @@ app.get('/api/v1', (req, res) => {
payments: '/api/v1/payments',
animalClaims: '/api/v1/animal-claims',
admin: '/api/v1/admin',
travelRegistration: '/api/v1/travel-registration'
travelRegistration: '/api/v1/travel-registration',
promotion: '/api/v1/promotion'
},
documentation: 'https://webapi.jiebanke.com/api-docs'
});
@@ -239,6 +244,20 @@ if (NO_DB_MODE) {
message: '当前为无数据库模式,管理员功能不可用'
});
});
app.use('/api/v1/promotion', (req, res) => {
res.status(503).json({
success: false,
message: '当前为无数据库模式,促销活动功能不可用'
});
});
app.use('/api/v1/merchants', (req, res) => {
res.status(503).json({
success: false,
message: '当前为无数据库模式,商户功能不可用'
});
});
} else {
// API路由
app.use('/api/v1/auth', authRoutes);
@@ -253,6 +272,10 @@ if (NO_DB_MODE) {
app.use('/api/v1/admin', adminRoutes);
// 旅行报名路由
app.use('/api/v1/travel-registration', travelRegistrationRoutes);
// 促销活动路由
app.use('/api/v1/promotion', promotionRoutes);
// 商户路由
app.use('/api/v1/merchants', merchantRoutes);
}
// 404处理

View File

@@ -48,6 +48,12 @@ const query = async (sql, params = []) => {
let connection;
try {
connection = await pool.getConnection();
// 添加调试信息
console.log('执行SQL:', sql);
console.log('参数:', params);
console.log('参数类型:', params.map(p => typeof p));
const [results] = await connection.execute(sql, params);
connection.release();
return results;
@@ -55,6 +61,9 @@ const query = async (sql, params = []) => {
if (connection) {
connection.release();
}
console.error('SQL执行错误:', error);
console.error('SQL语句:', sql);
console.error('参数:', params);
throw error;
}
};

View File

@@ -249,45 +249,45 @@ const getDashboardStatistics = async () => {
const todayEnd = new Date(todayStart.getTime() + 24 * 60 * 60 * 1000);
// 总用户数
const [totalUsersResult] = await query('SELECT COUNT(*) as count FROM users WHERE deleted_at IS NULL');
const [totalUsersResult] = await query('SELECT COUNT(*) as count FROM users');
const totalUsers = totalUsersResult.count;
// 今日新增用户
const [todayUsersResult] = await query(
'SELECT COUNT(*) as count FROM users WHERE created_at >= ? AND created_at < ? AND deleted_at IS NULL',
'SELECT COUNT(*) as count FROM users WHERE created_at >= ? AND created_at < ?',
[todayStart, todayEnd]
);
const todayNewUsers = todayUsersResult.count;
// 总动物数
const [totalAnimalsResult] = await query('SELECT COUNT(*) as count FROM animals WHERE deleted_at IS NULL');
const [totalAnimalsResult] = await query('SELECT COUNT(*) as count FROM animals');
const totalAnimals = totalAnimalsResult.count;
// 今日新增动物
const [todayAnimalsResult] = await query(
'SELECT COUNT(*) as count FROM animals WHERE created_at >= ? AND created_at < ? AND deleted_at IS NULL',
'SELECT COUNT(*) as count FROM animals WHERE created_at >= ? AND created_at < ?',
[todayStart, todayEnd]
);
const todayNewAnimals = todayAnimalsResult.count;
// 总旅行数
const [totalTravelsResult] = await query('SELECT COUNT(*) as count FROM travels WHERE deleted_at IS NULL');
const [totalTravelsResult] = await query('SELECT COUNT(*) as count FROM travels');
const totalTravels = totalTravelsResult.count;
// 今日新增旅行
const [todayTravelsResult] = await query(
'SELECT COUNT(*) as count FROM travels WHERE created_at >= ? AND created_at < ? AND deleted_at IS NULL',
'SELECT COUNT(*) as count FROM travels WHERE created_at >= ? AND created_at < ?',
[todayStart, todayEnd]
);
const todayNewTravels = todayTravelsResult.count;
// 总认领数
const [totalClaimsResult] = await query('SELECT COUNT(*) as count FROM animal_claims WHERE deleted_at IS NULL');
const [totalClaimsResult] = await query('SELECT COUNT(*) as count FROM animal_claims');
const totalClaims = totalClaimsResult.count;
// 今日新增认领
const [todayClaimsResult] = await query(
'SELECT COUNT(*) as count FROM animal_claims WHERE created_at >= ? AND created_at < ? AND deleted_at IS NULL',
'SELECT COUNT(*) as count FROM animal_claims WHERE created_at >= ? AND created_at < ?',
[todayStart, todayEnd]
);
const todayNewClaims = todayClaimsResult.count;
@@ -327,7 +327,7 @@ const getRecentActivities = async () => {
const recentUsers = await query(`
SELECT id, nickname, created_at
FROM users
WHERE deleted_at IS NULL
WHERE status != 'banned'
ORDER BY created_at DESC
LIMIT 5
`);
@@ -348,8 +348,7 @@ const getRecentActivities = async () => {
const recentAnimals = await query(`
SELECT a.id, a.name, a.created_at, u.id as user_id, u.nickname as user_nickname
FROM animals a
LEFT JOIN users u ON a.user_id = u.id
WHERE a.deleted_at IS NULL
LEFT JOIN users u ON a.farmer_id = u.id
ORDER BY a.created_at DESC
LIMIT 5
`);

View File

@@ -6,13 +6,13 @@ class AnimalController {
// 获取动物列表
static async getAnimals(req, res, next) {
try {
const { page, pageSize, species, status } = req.query;
const { page, pageSize, type, status } = req.query;
const result = await AnimalService.getAnimals({
merchantId: req.userId,
page: parseInt(page) || 1,
pageSize: parseInt(pageSize) || 10,
species,
type,
status
});
@@ -43,7 +43,7 @@ class AnimalController {
try {
const {
name,
species,
type,
breed,
age,
gender,
@@ -51,26 +51,30 @@ class AnimalController {
description,
images,
health_status,
vaccination_status
vaccination_records,
farm_location,
contact_info
} = req.body;
// 验证必要字段
if (!name || !species || !price) {
throw new AppError('缺少必要字段: name, species, price', 400);
if (!name || !type || !price) {
throw new AppError('缺少必要字段: name, type, price', 400);
}
const animalData = {
merchant_id: req.userId,
name,
species,
type,
breed: breed || null,
age: age || null,
gender: gender || null,
price: parseFloat(price),
description: description || null,
images: images || null,
images: images || [],
health_status: health_status || null,
vaccination_status: vaccination_status || null,
vaccination_records: vaccination_records || [],
farm_location: farm_location || null,
contact_info: contact_info || {},
status: 'available'
};

View File

@@ -0,0 +1,276 @@
const Merchant = require('../models/Merchant');
/**
* 获取商户列表
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function getMerchantList(req, res, next) {
try {
const {
page = 1,
limit = 20,
keyword = '',
status = '',
type = ''
} = req.query;
// 参数验证
const pageNum = parseInt(page);
const limitNum = parseInt(limit);
if (pageNum < 1) {
return res.status(400).json({
success: false,
message: '页码必须大于0'
});
}
if (limitNum < 1 || limitNum > 100) {
return res.status(400).json({
success: false,
message: '每页数量必须在1-100之间'
});
}
const options = {
page: pageNum,
limit: limitNum,
keyword: keyword.trim(),
status,
type
};
const result = await Merchant.getMerchantList(options);
res.json({
success: true,
data: result.merchants,
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 getMerchantDetail(req, res, next) {
try {
const { merchantId } = req.params;
if (!merchantId || isNaN(merchantId)) {
return res.status(400).json({
success: false,
message: '商户ID无效'
});
}
const merchant = await Merchant.getMerchantDetail(parseInt(merchantId));
if (!merchant) {
return res.status(404).json({
success: false,
message: '商户不存在'
});
}
res.json({
success: true,
data: merchant
});
} catch (error) {
console.error('获取商户详情控制器错误:', error);
res.status(500).json({
success: false,
message: error.message || '获取商户详情失败'
});
}
}
/**
* 创建商户
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function createMerchant(req, res, next) {
try {
const merchantData = req.body;
// 验证必要字段
const requiredFields = ['name', 'type', 'contact_person', 'contact_phone'];
for (const field of requiredFields) {
if (!merchantData[field]) {
return res.status(400).json({
success: false,
message: `缺少必要字段: ${field}`
});
}
}
// 验证商户类型
if (!['individual', 'company'].includes(merchantData.type)) {
return res.status(400).json({
success: false,
message: '商户类型必须是 individual 或 company'
});
}
const merchant = await Merchant.create(merchantData);
res.status(201).json({
success: true,
data: merchant,
message: '商户创建成功'
});
} catch (error) {
console.error('创建商户控制器错误:', error);
res.status(500).json({
success: false,
message: error.message || '创建商户失败'
});
}
}
/**
* 更新商户信息
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function updateMerchant(req, res, next) {
try {
const { merchantId } = req.params;
const merchantData = req.body;
if (!merchantId || isNaN(merchantId)) {
return res.status(400).json({
success: false,
message: '商户ID无效'
});
}
// 检查商户是否存在
const existingMerchant = await Merchant.findById(parseInt(merchantId));
if (!existingMerchant) {
return res.status(404).json({
success: false,
message: '商户不存在'
});
}
// 验证商户类型(如果提供)
if (merchantData.type && !['individual', 'company'].includes(merchantData.type)) {
return res.status(400).json({
success: false,
message: '商户类型必须是 individual 或 company'
});
}
// 验证状态(如果提供)
if (merchantData.status && !['active', 'inactive', 'banned'].includes(merchantData.status)) {
return res.status(400).json({
success: false,
message: '商户状态必须是 active、inactive 或 banned'
});
}
const updatedMerchant = await Merchant.update(parseInt(merchantId), merchantData);
res.json({
success: true,
data: updatedMerchant,
message: '商户信息更新成功'
});
} catch (error) {
console.error('更新商户控制器错误:', error);
res.status(500).json({
success: false,
message: error.message || '更新商户失败'
});
}
}
/**
* 删除商户
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function deleteMerchant(req, res, next) {
try {
const { merchantId } = req.params;
if (!merchantId || isNaN(merchantId)) {
return res.status(400).json({
success: false,
message: '商户ID无效'
});
}
// 检查商户是否存在
const existingMerchant = await Merchant.findById(parseInt(merchantId));
if (!existingMerchant) {
return res.status(404).json({
success: false,
message: '商户不存在'
});
}
const deleted = await Merchant.delete(parseInt(merchantId));
if (deleted) {
res.json({
success: true,
message: '商户删除成功'
});
} else {
res.status(500).json({
success: false,
message: '删除商户失败'
});
}
} catch (error) {
console.error('删除商户控制器错误:', error);
res.status(500).json({
success: false,
message: error.message || '删除商户失败'
});
}
}
/**
* 获取商户统计信息
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
*/
async function getMerchantStatistics(req, res, next) {
try {
const statistics = await Merchant.getStatistics();
res.json({
success: true,
data: statistics
});
} catch (error) {
console.error('获取商户统计控制器错误:', error);
res.status(500).json({
success: false,
message: error.message || '获取商户统计失败'
});
}
}
module.exports = {
getMerchantList,
getMerchantDetail,
createMerchant,
updateMerchant,
deleteMerchant,
getMerchantStatistics
};

View File

@@ -0,0 +1,461 @@
/**
* 推广活动控制器
* @module controllers/promotion/activityController
*/
const db = require('../../config/database');
/**
* 获取推广活动列表
* @function getActivities
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.getActivities = async (req, res, next) => {
try {
const {
page = 1,
pageSize = 20,
name = '',
status = ''
} = req.query;
const offset = (page - 1) * pageSize;
const limit = parseInt(pageSize);
let whereConditions = [];
let queryParams = [];
if (name) {
whereConditions.push('name LIKE ?');
queryParams.push(`%${name}%`);
}
if (status) {
whereConditions.push('status = ?');
queryParams.push(status);
}
const whereClause = whereConditions.length > 0
? `WHERE ${whereConditions.join(' AND ')}`
: '';
// 获取总数
const countSql = `SELECT COUNT(*) as total FROM promotion_activities ${whereClause}`;
const countResult = await db.query(countSql, queryParams);
const total = countResult[0].total;
// 获取数据
const dataSql = `
SELECT
id, name, description, reward_type, reward_amount, status,
start_time, end_time, max_participants, current_participants,
created_at, updated_at
FROM promotion_activities
${whereClause}
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`;
const dataParams = [...queryParams, limit, offset];
const activities = await db.query(dataSql, dataParams);
res.json({
success: true,
code: 200,
message: '获取成功',
data: activities,
pagination: {
current: parseInt(page),
pageSize: limit,
total,
totalPages: Math.ceil(total / limit)
}
});
} catch (error) {
next(error);
}
};
/**
* 获取推广活动详情
* @function getActivity
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.getActivity = async (req, res, next) => {
try {
const { id } = req.params;
const sql = `
SELECT
id, name, description, reward_type, reward_amount, status,
start_time, end_time, max_participants, current_participants,
created_at, updated_at
FROM promotion_activities
WHERE id = ?
`;
const activities = await db.query(sql, [id]);
if (activities.length === 0) {
return res.status(404).json({
success: false,
code: 404,
message: '推广活动不存在'
});
}
res.json({
success: true,
code: 200,
message: '获取成功',
data: activities[0]
});
} catch (error) {
next(error);
}
};
/**
* 创建推广活动
* @function createActivity
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.createActivity = async (req, res, next) => {
try {
const {
name,
description,
reward_type,
reward_amount,
status = 'upcoming',
start_time,
end_time,
max_participants = 0
} = req.body;
// 验证必填字段
if (!name || !reward_type || !reward_amount || !start_time || !end_time) {
return res.status(400).json({
success: false,
code: 400,
message: '缺少必填字段'
});
}
const sql = `
INSERT INTO promotion_activities
(name, description, reward_type, reward_amount, status, start_time, end_time, max_participants, current_participants)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
`;
const result = await db.query(sql, [
name,
description,
reward_type,
reward_amount,
status,
start_time,
end_time,
max_participants
]);
// 获取新创建的活动
const newActivity = await db.query(
'SELECT * FROM promotion_activities WHERE id = ?',
[result.insertId]
);
res.status(201).json({
success: true,
code: 201,
message: '创建成功',
data: newActivity[0]
});
} catch (error) {
next(error);
}
};
/**
* 更新推广活动
* @function updateActivity
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.updateActivity = async (req, res, next) => {
try {
const { id } = req.params;
const updates = req.body;
// 检查活动是否存在
const existingActivity = await db.query(
'SELECT id FROM promotion_activities WHERE id = ?',
[id]
);
if (existingActivity.length === 0) {
return res.status(404).json({
success: false,
code: 404,
message: '推广活动不存在'
});
}
// 构建更新字段
const updateFields = [];
const updateValues = [];
const allowedFields = [
'name', 'description', 'reward_type', 'reward_amount',
'status', 'start_time', 'end_time', 'max_participants'
];
Object.keys(updates).forEach(key => {
if (allowedFields.includes(key) && updates[key] !== undefined) {
updateFields.push(`${key} = ?`);
updateValues.push(updates[key]);
}
});
if (updateFields.length === 0) {
return res.status(400).json({
success: false,
code: 400,
message: '没有有效的更新字段'
});
}
updateValues.push(id);
const sql = `
UPDATE promotion_activities
SET ${updateFields.join(', ')}, updated_at = NOW()
WHERE id = ?
`;
await db.query(sql, updateValues);
// 获取更新后的活动
const updatedActivity = await db.query(
'SELECT * FROM promotion_activities WHERE id = ?',
[id]
);
res.json({
success: true,
code: 200,
message: '更新成功',
data: updatedActivity[0]
});
} catch (error) {
next(error);
}
};
/**
* 删除推广活动
* @function deleteActivity
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.deleteActivity = async (req, res, next) => {
try {
const { id } = req.params;
// 检查活动是否存在
const existingActivity = await db.query(
'SELECT id FROM promotion_activities WHERE id = ?',
[id]
);
if (existingActivity.length === 0) {
return res.status(404).json({
success: false,
code: 404,
message: '推广活动不存在'
});
}
// 删除活动
await db.query('DELETE FROM promotion_activities WHERE id = ?', [id]);
res.json({
success: true,
code: 200,
message: '删除成功'
});
} catch (error) {
next(error);
}
};
/**
* 暂停推广活动
* @function pauseActivity
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.pauseActivity = async (req, res, next) => {
try {
const { id } = req.params;
// 检查活动是否存在
const existingActivity = await db.query(
'SELECT id, status FROM promotion_activities WHERE id = ?',
[id]
);
if (existingActivity.length === 0) {
return res.status(404).json({
success: false,
code: 404,
message: '推广活动不存在'
});
}
const currentStatus = existingActivity[0].status;
if (currentStatus !== 'active') {
return res.status(400).json({
success: false,
code: 400,
message: '只有活跃状态的活动可以暂停'
});
}
// 更新状态为暂停
await db.query(
'UPDATE promotion_activities SET status = "paused", updated_at = NOW() WHERE id = ?',
[id]
);
res.json({
success: true,
code: 200,
message: '暂停成功'
});
} catch (error) {
next(error);
}
};
/**
* 恢复推广活动
* @function resumeActivity
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.resumeActivity = async (req, res, next) => {
try {
const { id } = req.params;
// 检查活动是否存在
const existingActivity = await db.query(
'SELECT id, status FROM promotion_activities WHERE id = ?',
[id]
);
if (existingActivity.length === 0) {
return res.status(404).json({
success: false,
code: 404,
message: '推广活动不存在'
});
}
const currentStatus = existingActivity[0].status;
if (currentStatus !== 'paused') {
return res.status(400).json({
success: false,
code: 400,
message: '只有暂停状态的活动可以恢复'
});
}
// 更新状态为活跃
await db.query(
'UPDATE promotion_activities SET status = "active", updated_at = NOW() WHERE id = ?',
[id]
);
res.json({
success: true,
code: 200,
message: '恢复成功'
});
} catch (error) {
next(error);
}
};
/**
* 获取推广统计数据
* @function getStatistics
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.getStatistics = async (req, res, next) => {
try {
// 获取活动总数
const totalActivities = await db.query(
'SELECT COUNT(*) as count FROM promotion_activities'
);
// 获取活跃活动数
const activeActivities = await db.query(
'SELECT COUNT(*) as count FROM promotion_activities WHERE status = "active"'
);
// 获取奖励记录总数
const totalRewards = await db.query(
'SELECT COUNT(*) as count FROM promotion_rewards'
);
// 获取已发放奖励数
const issuedRewards = await db.query(
'SELECT COUNT(*) as count FROM promotion_rewards WHERE status = "issued"'
);
// 获取奖励总金额
const totalAmount = await db.query(`
SELECT COALESCE(SUM(
CASE
WHEN reward_type = 'cash' THEN reward_amount
WHEN reward_type = 'points' THEN reward_amount * 0.01 -- 假设1积分=0.01元
ELSE 0
END
), 0) as total_amount
FROM promotion_rewards
WHERE status = 'issued'
`);
const statistics = {
total_activities: totalActivities[0].count,
active_activities: activeActivities[0].count,
total_rewards: totalRewards[0].count,
issued_rewards: issuedRewards[0].count,
total_amount: parseFloat(totalAmount[0].total_amount)
};
res.json({
success: true,
code: 200,
message: '获取成功',
data: statistics
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,175 @@
/**
* 奖励记录控制器
* @module controllers/promotion/rewardController
*/
const db = require('../../config/database');
/**
* 获取奖励记录列表
* @function getRewards
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.getRewards = async (req, res, next) => {
try {
const {
page = 1,
pageSize = 20,
user = '',
reward_type = '',
status = ''
} = req.query;
const offset = (page - 1) * pageSize;
const limit = parseInt(pageSize);
let whereConditions = [];
let queryParams = [];
if (user) {
whereConditions.push('(user_name LIKE ? OR user_phone LIKE ?)');
queryParams.push(`%${user}%`, `%${user}%`);
}
if (reward_type) {
whereConditions.push('reward_type = ?');
queryParams.push(reward_type);
}
if (status) {
whereConditions.push('status = ?');
queryParams.push(status);
}
const whereClause = whereConditions.length > 0
? `WHERE ${whereConditions.join(' AND ')}`
: '';
// 获取总数
const countSql = `SELECT COUNT(*) as total FROM promotion_rewards ${whereClause}`;
const countResult = await db.query(countSql, queryParams);
const total = countResult[0].total;
// 获取数据
const dataSql = `
SELECT
id, user_id, user_name, user_phone, activity_id, activity_name,
reward_type, reward_amount, status, issued_at, created_at
FROM promotion_rewards
${whereClause}
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`;
const dataParams = [...queryParams, limit, offset];
const rewards = await db.query(dataSql, dataParams);
res.json({
success: true,
code: 200,
message: '获取成功',
data: rewards,
pagination: {
current: parseInt(page),
pageSize: limit,
total,
totalPages: Math.ceil(total / limit)
}
});
} catch (error) {
next(error);
}
};
/**
* 发放奖励
* @function issueReward
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.issueReward = async (req, res, next) => {
try {
const { id } = req.params;
// 检查奖励记录是否存在
const reward = await db.query(
'SELECT * FROM promotion_rewards WHERE id = ?',
[id]
);
if (reward.length === 0) {
return res.status(404).json({
success: false,
code: 404,
message: '奖励记录不存在'
});
}
const rewardData = reward[0];
if (rewardData.status !== 'pending') {
return res.status(400).json({
success: false,
code: 400,
message: '只有待发放状态的奖励可以发放'
});
}
// 更新奖励状态为已发放
await db.query(
'UPDATE promotion_rewards SET status = "issued", issued_at = NOW() WHERE id = ?',
[id]
);
// 根据奖励类型执行相应的发放逻辑
try {
switch (rewardData.reward_type) {
case 'cash':
// 现金奖励发放逻辑
// 这里可以集成支付系统或记录到用户账户
console.log(`发放现金奖励: ${rewardData.reward_amount}元给用户 ${rewardData.user_name}`);
break;
case 'points':
// 积分奖励发放逻辑
// 这里可以更新用户积分
console.log(`发放积分奖励: ${rewardData.reward_amount}积分给用户 ${rewardData.user_name}`);
break;
case 'coupon':
// 优惠券发放逻辑
// 这里可以生成优惠券并关联到用户
console.log(`发放优惠券奖励给用户 ${rewardData.user_name}`);
break;
default:
console.log(`未知奖励类型: ${rewardData.reward_type}`);
}
} catch (distributionError) {
// 如果发放失败,回滚奖励状态
await db.query(
'UPDATE promotion_rewards SET status = "failed" WHERE id = ?',
[id]
);
console.error('奖励发放失败:', distributionError);
return res.status(500).json({
success: false,
code: 500,
message: '奖励发放失败'
});
}
res.json({
success: true,
code: 200,
message: '奖励发放成功'
});
} catch (error) {
next(error);
}
};

View File

@@ -41,33 +41,49 @@ class TravelController {
static async createTravelPlan(req, res, next) {
try {
const {
title,
description,
destination,
start_date,
end_date,
budget,
companions,
transportation,
accommodation,
activities,
notes
max_participants,
price_per_person,
itinerary,
requirements,
includes,
excludes,
images
} = req.body;
if (!destination || !start_date || !end_date) {
throw new AppError('目的地、开始日期结束日期不能为空', 400);
if (!title || !destination || !start_date || !end_date || !price_per_person) {
throw new AppError('标题、目的地、开始日期结束日期和价格不能为空', 400);
}
const planId = await TravelService.createTravelPlan(req.userId, {
const planData = {
title,
description: description || null,
destination,
start_date,
end_date,
budget,
companions,
transportation,
accommodation,
activities,
notes
max_participants: max_participants || null,
price_per_person: parseFloat(price_per_person),
itinerary: itinerary || [],
requirements: requirements || null,
includes: includes || [],
excludes: excludes || [],
images: images || []
};
// 调试:检查传递给服务层的数据
console.log('Plan Data:', planData);
Object.keys(planData).forEach(key => {
if (planData[key] === undefined) {
console.log(`Field ${key} is undefined`);
}
});
const planId = await TravelService.createTravelPlan(req.userId, planData);
const plan = await TravelService.getTravelPlanById(planId);
res.status(201).json(success({

View File

@@ -42,12 +42,13 @@ class Animal {
const query = `
SELECT
a.*,
type,
m.name as merchant_name,
m.contact_phone as merchant_phone
FROM animals a
LEFT JOIN merchants m ON a.merchant_id = m.id
WHERE 1=1 ${whereClause}
ORDER BY a.${sortBy} ${sortOrder}
WHERE ${whereClause}
ORDER BY a.created_at DESC
LIMIT ? OFFSET ?
`;
@@ -92,6 +93,7 @@ class Animal {
const query = `
SELECT
a.*,
a.type,
m.name as merchant_name,
m.contact_phone as merchant_phone,
m.address as merchant_address
@@ -194,27 +196,26 @@ class Animal {
}
/**
* 获取按物种分类的统计
* 获取动物统计信息(按类型分组)
* @returns {Array} 统计信息
*/
static async getAnimalStatsBySpecies() {
static async getAnimalStatsByType() {
try {
const query = `
SELECT
species,
type,
COUNT(*) as count,
COUNT(CASE WHEN status = 'available' THEN 1 END) as available_count,
COUNT(CASE WHEN status = 'claimed' THEN 1 END) as claimed_count,
AVG(price) as avg_price
FROM animals
GROUP BY species
WHERE status = 'available'
GROUP BY type
ORDER BY count DESC
`;
const [rows] = await db.execute(query);
return rows;
} catch (error) {
console.error('获取按物种分类的统计失败:', error);
console.error('获取动物统计信息失败:', error);
throw error;
}
}
@@ -341,28 +342,35 @@ class Animal {
try {
const {
name,
species,
type,
breed,
age,
gender,
weight,
price,
description,
image_url,
health_status,
vaccination_records,
images,
merchant_id,
farm_location,
contact_info,
status = 'available'
} = animalData;
const query = `
INSERT INTO animals (
name, species, breed, age, gender, weight, price,
description, image_url, merchant_id, status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
name, type, breed, age, gender, weight, price,
description, health_status, vaccination_records, images,
merchant_id, farm_location, contact_info, status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
`;
const [result] = await db.execute(query, [
name, species, breed, age, gender, weight, price,
description, image_url, merchant_id, status
name, type, breed, age, gender, weight, price,
description, health_status, JSON.stringify(vaccination_records || []),
JSON.stringify(images || []), merchant_id, farm_location,
JSON.stringify(contact_info || {}), status
]);
return { id: result.insertId, ...animalData };

View File

@@ -0,0 +1,217 @@
const { query } = require('../config/database');
/**
* 商户模型类
* 处理商户相关的数据库操作
*/
class Merchant {
// 根据ID查找商户
static async findById(id) {
try {
const sql = 'SELECT * FROM merchants WHERE id = ?';
const [rows] = await query(sql, [id]);
return rows.length > 0 ? rows[0] : null;
} catch (error) {
console.error('查找商户失败:', error);
throw error;
}
}
// 获取商户列表(支持分页和筛选)
static async getMerchantList(options = {}) {
try {
const {
page = 1,
limit = 20,
keyword = '',
status = '',
type = ''
} = options;
const offset = (page - 1) * limit;
let whereConditions = [];
let params = [];
// 构建查询条件
if (keyword) {
whereConditions.push('(name LIKE ? OR contact_person LIKE ? OR contact_phone LIKE ?)');
const keywordPattern = `%${keyword}%`;
params.push(keywordPattern, keywordPattern, keywordPattern);
}
if (status) {
whereConditions.push('status = ?');
params.push(status);
}
if (type) {
whereConditions.push('type = ?');
params.push(type);
}
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
// 查询总数
const countSql = `SELECT COUNT(*) as total FROM merchants ${whereClause}`;
const [countResult] = await query(countSql, params);
const total = countResult[0].total;
// 查询数据
const dataSql = `
SELECT * FROM merchants
${whereClause}
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`;
const [rows] = await query(dataSql, [...params, limit, offset]);
return {
data: rows,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
totalPages: Math.ceil(total / limit)
}
};
} catch (error) {
console.error('获取商户列表失败:', error);
throw error;
}
}
// 创建商户
static async create(merchantData) {
try {
const {
name,
type,
contact_person,
contact_phone,
email = null,
address = null,
description = null
} = merchantData;
const sql = `
INSERT INTO merchants (name, type, contact_person, contact_phone, email, address, description, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 'active', NOW(), NOW())
`;
const params = [name, type, contact_person, contact_phone, email, address, description];
const [result] = await query(sql, params);
// 返回创建的商户信息
return await this.findById(result.insertId);
} catch (error) {
console.error('创建商户失败:', error);
throw error;
}
}
// 更新商户信息
static async update(id, merchantData) {
try {
const updateFields = [];
const params = [];
// 动态构建更新字段
const allowedFields = ['name', 'type', 'contact_person', 'contact_phone', 'email', 'address', 'description', 'status'];
for (const field of allowedFields) {
if (merchantData[field] !== undefined) {
updateFields.push(`${field} = ?`);
params.push(merchantData[field]);
}
}
if (updateFields.length === 0) {
throw new Error('没有提供要更新的字段');
}
updateFields.push('updated_at = NOW()');
params.push(id);
const sql = `UPDATE merchants SET ${updateFields.join(', ')} WHERE id = ?`;
const [result] = await query(sql, params);
if (result.affectedRows === 0) {
throw new Error('商户不存在或更新失败');
}
// 返回更新后的商户信息
return await this.findById(id);
} catch (error) {
console.error('更新商户失败:', error);
throw error;
}
}
// 删除商户
static async delete(id) {
try {
const sql = 'DELETE FROM merchants WHERE id = ?';
const [result] = await query(sql, [id]);
if (result.affectedRows === 0) {
throw new Error('商户不存在或删除失败');
}
return true;
} catch (error) {
console.error('删除商户失败:', error);
throw error;
}
}
// 获取商户详情(包含统计信息)
static async getDetailWithStats(id) {
try {
const merchant = await this.findById(id);
if (!merchant) {
return null;
}
// 获取关联的动物数量
const animalCountSql = 'SELECT COUNT(*) as count FROM animals WHERE merchant_id = ?';
const [animalResult] = await query(animalCountSql, [id]);
// 获取关联的订单数量
const orderCountSql = 'SELECT COUNT(*) as count FROM orders WHERE merchant_id = ?';
const [orderResult] = await query(orderCountSql, [id]);
return {
...merchant,
animal_count: animalResult[0].count,
order_count: orderResult[0].count
};
} catch (error) {
console.error('获取商户详情失败:', error);
throw error;
}
}
// 获取商户统计信息
static async getStatistics() {
try {
const sql = `
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) as inactive,
SUM(CASE WHEN status = 'banned' THEN 1 ELSE 0 END) as banned,
SUM(CASE WHEN type = 'individual' THEN 1 ELSE 0 END) as individual,
SUM(CASE WHEN type = 'company' THEN 1 ELSE 0 END) as company
FROM merchants
`;
const [rows] = await query(sql);
return rows[0];
} catch (error) {
console.error('获取商户统计信息失败:', error);
throw error;
}
}
}
module.exports = Merchant;

View File

@@ -0,0 +1,427 @@
const { query } = require('../config/database');
/**
* 旅行计划数据模型
* 处理旅行计划相关的数据库操作
*/
class Travel {
/**
* 创建旅行计划
* @param {Object} travelData - 旅行计划数据
* @returns {Promise<Object>} 创建的旅行计划
*/
static async create(travelData) {
const {
title,
destination,
description,
start_date,
end_date,
max_participants,
price_per_person,
includes,
excludes,
itinerary,
images,
requirements,
created_by
} = travelData;
const query = `
INSERT INTO travel_plans
(title, destination, description, start_date, end_date, max_participants,
current_participants, price_per_person, includes, excludes, itinerary,
images, requirements, created_by, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, 'draft', NOW(), NOW())
`;
const [result] = await query(query, [
title,
destination,
description || null,
start_date,
end_date,
max_participants || 10,
price_per_person,
JSON.stringify(includes || []),
JSON.stringify(excludes || []),
JSON.stringify(itinerary || []),
JSON.stringify(images || []),
requirements || null,
created_by
]);
return this.findById(result.insertId);
}
/**
* 根据ID查找旅行计划
* @param {number} id - 旅行计划ID
* @returns {Promise<Object|null>} 旅行计划信息
*/
static async findById(id) {
const query = `
SELECT
tp.*,
u.username as creator_name,
u.avatar as creator_avatar,
u.phone as creator_phone
FROM travel_plans tp
LEFT JOIN users u ON tp.created_by = u.id
WHERE tp.id = ?
`;
const [rows] = await query(query, [id]);
if (rows.length === 0) {
return null;
}
const travel = rows[0];
// 解析JSON字段
if (travel.includes) {
travel.includes = JSON.parse(travel.includes);
}
if (travel.excludes) {
travel.excludes = JSON.parse(travel.excludes);
}
if (travel.itinerary) {
travel.itinerary = JSON.parse(travel.itinerary);
}
if (travel.images) {
travel.images = JSON.parse(travel.images);
}
return travel;
}
/**
* 获取旅行计划列表
* @param {Object} options - 查询选项
* @returns {Promise<Array>} 旅行计划列表
*/
static async findAll(options = {}) {
const {
page = 1,
limit = 10,
destination,
status,
created_by,
start_date,
end_date,
sort_by = 'created_at',
sort_order = 'DESC'
} = options;
let whereClause = 'WHERE 1=1';
const params = [];
if (destination) {
whereClause += ' AND tp.destination LIKE ?';
params.push(`%${destination}%`);
}
if (status) {
whereClause += ' AND tp.status = ?';
params.push(status);
}
if (created_by) {
whereClause += ' AND tp.created_by = ?';
params.push(created_by);
}
if (start_date) {
whereClause += ' AND tp.start_date >= ?';
params.push(start_date);
}
if (end_date) {
whereClause += ' AND tp.end_date <= ?';
params.push(end_date);
}
const offset = (page - 1) * limit;
const query = `
SELECT
tp.*,
u.username as creator_name,
u.avatar as creator_avatar
FROM travel_plans tp
LEFT JOIN users u ON tp.created_by = u.id
${whereClause}
ORDER BY tp.${sort_by} ${sort_order}
LIMIT ? OFFSET ?
`;
const [rows] = await query(query, [...params, limit, offset]);
// 解析JSON字段
return rows.map(travel => {
if (travel.includes) {
travel.includes = JSON.parse(travel.includes);
}
if (travel.excludes) {
travel.excludes = JSON.parse(travel.excludes);
}
if (travel.itinerary) {
travel.itinerary = JSON.parse(travel.itinerary);
}
if (travel.images) {
travel.images = JSON.parse(travel.images);
}
return travel;
});
}
/**
* 获取旅行计划总数
* @param {Object} options - 查询选项
* @returns {Promise<number>} 总数
*/
static async count(options = {}) {
const {
destination,
status,
created_by,
start_date,
end_date
} = options;
let whereClause = 'WHERE 1=1';
const params = [];
if (destination) {
whereClause += ' AND destination LIKE ?';
params.push(`%${destination}%`);
}
if (status) {
whereClause += ' AND status = ?';
params.push(status);
}
if (created_by) {
whereClause += ' AND created_by = ?';
params.push(created_by);
}
if (start_date) {
whereClause += ' AND start_date >= ?';
params.push(start_date);
}
if (end_date) {
whereClause += ' AND end_date <= ?';
params.push(end_date);
}
const query = `SELECT COUNT(*) as count FROM travel_plans ${whereClause}`;
const [rows] = await query(query, params);
return rows[0].count;
}
/**
* 更新旅行计划
* @param {number} id - 旅行计划ID
* @param {Object} updateData - 更新数据
* @returns {Promise<Object|null>} 更新后的旅行计划
*/
static async update(id, updateData) {
const {
title,
destination,
description,
start_date,
end_date,
max_participants,
price_per_person,
includes,
excludes,
itinerary,
images,
requirements,
status
} = updateData;
const fields = [];
const params = [];
if (title !== undefined) {
fields.push('title = ?');
params.push(title);
}
if (destination !== undefined) {
fields.push('destination = ?');
params.push(destination);
}
if (description !== undefined) {
fields.push('description = ?');
params.push(description);
}
if (start_date !== undefined) {
fields.push('start_date = ?');
params.push(start_date);
}
if (end_date !== undefined) {
fields.push('end_date = ?');
params.push(end_date);
}
if (max_participants !== undefined) {
fields.push('max_participants = ?');
params.push(max_participants);
}
if (price_per_person !== undefined) {
fields.push('price_per_person = ?');
params.push(price_per_person);
}
if (includes !== undefined) {
fields.push('includes = ?');
params.push(JSON.stringify(includes));
}
if (excludes !== undefined) {
fields.push('excludes = ?');
params.push(JSON.stringify(excludes));
}
if (itinerary !== undefined) {
fields.push('itinerary = ?');
params.push(JSON.stringify(itinerary));
}
if (images !== undefined) {
fields.push('images = ?');
params.push(JSON.stringify(images));
}
if (requirements !== undefined) {
fields.push('requirements = ?');
params.push(requirements);
}
if (status !== undefined) {
fields.push('status = ?');
params.push(status);
}
if (fields.length === 0) {
return this.findById(id);
}
fields.push('updated_at = NOW()');
params.push(id);
const query = `UPDATE travel_plans SET ${fields.join(', ')} WHERE id = ?`;
await query(query, params);
return this.findById(id);
}
/**
* 删除旅行计划
* @param {number} id - 旅行计划ID
* @returns {Promise<boolean>} 是否删除成功
*/
static async delete(id) {
const query = 'DELETE FROM travel_plans WHERE id = ?';
const [result] = await query(query, [id]);
return result.affectedRows > 0;
}
/**
* 增加参与人数
* @param {number} id - 旅行计划ID
* @param {number} count - 增加的人数
* @returns {Promise<boolean>} 是否更新成功
*/
static async incrementParticipants(id, count = 1) {
const query = `
UPDATE travel_plans
SET current_participants = current_participants + ?, updated_at = NOW()
WHERE id = ? AND current_participants + ? <= max_participants
`;
const [result] = await query(query, [count, id, count]);
return result.affectedRows > 0;
}
/**
* 减少参与人数
* @param {number} id - 旅行计划ID
* @param {number} count - 减少的人数
* @returns {Promise<boolean>} 是否更新成功
*/
static async decrementParticipants(id, count = 1) {
const query = `
UPDATE travel_plans
SET current_participants = GREATEST(0, current_participants - ?), updated_at = NOW()
WHERE id = ?
`;
const [result] = await query(sql, [count, id]);
return result.affectedRows > 0;
}
/**
* 检查是否可以报名
* @param {number} id - 旅行计划ID
* @returns {Promise<boolean>} 是否可以报名
*/
static async canRegister(id) {
const query = `
SELECT
current_participants < max_participants as can_register,
status = 'published' as is_published,
start_date > NOW() as not_started
FROM travel_plans
WHERE id = ?
`;
const [rows] = await query(sql, [id]);
if (rows.length === 0) {
return false;
}
const { can_register, is_published, not_started } = rows[0];
return can_register && is_published && not_started;
}
/**
* 获取热门旅行计划
* @param {number} limit - 限制数量
* @returns {Promise<Array>} 热门旅行计划列表
*/
static async getPopular(limit = 10) {
const query = `
SELECT
tp.*,
u.username as creator_name,
u.avatar as creator_avatar,
COUNT(tr.id) as registration_count
FROM travel_plans tp
LEFT JOIN users u ON tp.created_by = u.id
LEFT JOIN travel_registrations tr ON tp.id = tr.travel_plan_id AND tr.status = 'approved'
WHERE tp.status = 'published' AND tp.start_date > NOW()
GROUP BY tp.id
ORDER BY registration_count DESC, tp.created_at DESC
LIMIT ?
`;
const [rows] = await query(sql, [limit]);
// 解析JSON字段
return rows.map(travel => {
if (travel.includes) {
travel.includes = JSON.parse(travel.includes);
}
if (travel.excludes) {
travel.excludes = JSON.parse(travel.excludes);
}
if (travel.itinerary) {
travel.itinerary = JSON.parse(travel.itinerary);
}
if (travel.images) {
travel.images = JSON.parse(travel.images);
}
return travel;
});
}
}
module.exports = Travel;

View File

@@ -36,6 +36,9 @@ class UserMySQL {
// 根据ID查找用户
static async findById(id) {
if (id === undefined || id === null) {
throw new Error('User ID cannot be undefined or null');
}
const sql = 'SELECT * FROM users WHERE id = ?';
const rows = await query(sql, [id]);
return rows[0] || null;

View File

@@ -1,6 +1,7 @@
const express = require('express')
const { body } = require('express-validator')
const authController = require('../controllers/authControllerMySQL')
const { authenticateUser } = require('../middleware/auth')
const router = express.Router()
@@ -169,7 +170,7 @@ router.post(
* 500:
* description: 服务器内部错误
*/
router.get('/me', authController.getCurrentUser)
router.get('/me', authenticateUser, authController.getCurrentUser)
/**
* @swagger
@@ -224,7 +225,7 @@ router.get('/me', authController.getCurrentUser)
* 500:
* description: 服务器内部错误
*/
router.put('/profile', authController.updateProfile)
router.put('/profile', authenticateUser, authController.updateProfile)
/**
* @swagger
@@ -271,6 +272,7 @@ router.put('/profile', authController.updateProfile)
*/
router.put(
'/password',
authenticateUser,
[
body('currentPassword').notEmpty().withMessage('当前密码不能为空'),
body('newPassword').isLength({ min: 6 }).withMessage('新密码长度不能少于6位')

View File

@@ -0,0 +1,453 @@
const express = require('express');
const { body, query, param } = require('express-validator');
const MerchantController = require('../controllers/merchant');
const { authenticateUser, requireRole } = require('../middleware/auth');
const router = express.Router();
/**
* @swagger
* tags:
* name: Merchants
* description: 商户管理相关接口
*/
/**
* @swagger
* components:
* schemas:
* Merchant:
* type: object
* properties:
* id:
* type: integer
* description: 商户ID
* name:
* type: string
* description: 商户名称
* type:
* type: string
* enum: [individual, company]
* description: 商户类型
* contact_person:
* type: string
* description: 联系人
* contact_phone:
* type: string
* description: 联系电话
* email:
* type: string
* description: 邮箱
* address:
* type: string
* description: 地址
* description:
* type: string
* description: 描述
* status:
* type: string
* enum: [active, inactive, banned]
* description: 状态
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
*/
/**
* @swagger
* /merchants:
* get:
* summary: 获取商户列表
* tags: [Merchants]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* minimum: 1
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* minimum: 1
* maximum: 100
* default: 20
* description: 每页数量
* - in: query
* name: keyword
* schema:
* type: string
* description: 搜索关键词(商户名称、联系人、电话)
* - in: query
* name: status
* schema:
* type: string
* enum: [active, inactive, banned]
* description: 状态筛选
* - in: query
* name: type
* schema:
* type: string
* enum: [individual, company]
* description: 类型筛选
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* $ref: '#/components/schemas/Merchant'
* pagination:
* type: object
* properties:
* page:
* type: integer
* limit:
* type: integer
* total:
* type: integer
* totalPages:
* type: integer
* 400:
* description: 请求参数错误
* 500:
* description: 服务器内部错误
*/
router.get('/',
[
query('page').optional().isInt({ min: 1 }).withMessage('页码必须是正整数'),
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('每页数量必须在1-100之间'),
query('status').optional().isIn(['active', 'inactive', 'banned']).withMessage('状态值无效'),
query('type').optional().isIn(['individual', 'company']).withMessage('类型值无效')
],
MerchantController.getMerchantList
);
/**
* @swagger
* /merchants/{merchantId}:
* get:
* summary: 获取商户详情
* tags: [Merchants]
* parameters:
* - in: path
* name: merchantId
* required: true
* schema:
* type: integer
* description: 商户ID
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* allOf:
* - $ref: '#/components/schemas/Merchant'
* - type: object
* properties:
* animal_count:
* type: integer
* description: 动物数量
* order_count:
* type: integer
* description: 订单数量
* 400:
* description: 商户ID无效
* 404:
* description: 商户不存在
* 500:
* description: 服务器内部错误
*/
router.get('/:merchantId',
[
param('merchantId').isInt({ min: 1 }).withMessage('商户ID必须是正整数')
],
MerchantController.getMerchantDetail
);
/**
* @swagger
* /merchants:
* post:
* summary: 创建商户
* tags: [Merchants]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - type
* - contact_person
* - contact_phone
* properties:
* name:
* type: string
* description: 商户名称
* type:
* type: string
* enum: [individual, company]
* description: 商户类型
* contact_person:
* type: string
* description: 联系人
* contact_phone:
* type: string
* description: 联系电话
* email:
* type: string
* description: 邮箱
* address:
* type: string
* description: 地址
* description:
* type: string
* description: 描述
* responses:
* 201:
* description: 创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/Merchant'
* message:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器内部错误
*/
router.post('/',
authenticateUser,
requireRole(['admin', 'super_admin']),
[
body('name').notEmpty().withMessage('商户名称不能为空'),
body('type').isIn(['individual', 'company']).withMessage('商户类型必须是 individual 或 company'),
body('contact_person').notEmpty().withMessage('联系人不能为空'),
body('contact_phone').notEmpty().withMessage('联系电话不能为空'),
body('email').optional().isEmail().withMessage('邮箱格式无效')
],
MerchantController.createMerchant
);
/**
* @swagger
* /merchants/{merchantId}:
* put:
* summary: 更新商户信息
* tags: [Merchants]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: merchantId
* required: true
* schema:
* type: integer
* description: 商户ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description: 商户名称
* type:
* type: string
* enum: [individual, company]
* description: 商户类型
* contact_person:
* type: string
* description: 联系人
* contact_phone:
* type: string
* description: 联系电话
* email:
* type: string
* description: 邮箱
* address:
* type: string
* description: 地址
* description:
* type: string
* description: 描述
* status:
* type: string
* enum: [active, inactive, banned]
* description: 状态
* responses:
* 200:
* description: 更新成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/Merchant'
* message:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 404:
* description: 商户不存在
* 500:
* description: 服务器内部错误
*/
router.put('/:merchantId',
authenticateUser,
requireRole(['admin', 'super_admin']),
[
param('merchantId').isInt({ min: 1 }).withMessage('商户ID必须是正整数'),
body('name').optional().notEmpty().withMessage('商户名称不能为空'),
body('type').optional().isIn(['individual', 'company']).withMessage('商户类型必须是 individual 或 company'),
body('contact_person').optional().notEmpty().withMessage('联系人不能为空'),
body('contact_phone').optional().notEmpty().withMessage('联系电话不能为空'),
body('email').optional().isEmail().withMessage('邮箱格式无效'),
body('status').optional().isIn(['active', 'inactive', 'banned']).withMessage('状态值无效')
],
MerchantController.updateMerchant
);
/**
* @swagger
* /merchants/{merchantId}:
* delete:
* summary: 删除商户
* tags: [Merchants]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: merchantId
* required: true
* schema:
* type: integer
* description: 商户ID
* responses:
* 200:
* description: 删除成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 400:
* description: 商户ID无效
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 404:
* description: 商户不存在
* 500:
* description: 服务器内部错误
*/
router.delete('/:merchantId',
authenticateUser,
requireRole(['admin', 'super_admin']),
[
param('merchantId').isInt({ min: 1 }).withMessage('商户ID必须是正整数')
],
MerchantController.deleteMerchant
);
/**
* @swagger
* /merchants/statistics:
* get:
* summary: 获取商户统计信息
* tags: [Merchants]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* total:
* type: integer
* description: 总商户数
* active:
* type: integer
* description: 活跃商户数
* inactive:
* type: integer
* description: 非活跃商户数
* banned:
* type: integer
* description: 被禁用商户数
* individual:
* type: integer
* description: 个人商户数
* company:
* type: integer
* description: 企业商户数
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器内部错误
*/
router.get('/statistics',
authenticateUser,
requireRole(['admin', 'super_admin']),
MerchantController.getMerchantStatistics
);
module.exports = router;

View File

@@ -0,0 +1,61 @@
/**
* 推广活动路由
* @module routes/promotion
*/
const express = require('express');
const router = express.Router();
const activityController = require('../controllers/promotion/activityController');
const rewardController = require('../controllers/promotion/rewardController');
/**
* @route GET /api/v1/promotion/activities
* @description 获取推广活动列表
* @access Public
*/
router.get('/activities', activityController.getActivities);
/**
* @route GET /api/v1/promotion/activities/:id
* @description 获取推广活动详情
* @access Public
*/
router.get('/activities/:id', activityController.getActivity);
/**
* @route POST /api/v1/promotion/activities
* @description 创建推广活动
* @access Private
*/
router.post('/activities', activityController.createActivity);
/**
* @route PUT /api/v1/promotion/activities/:id
* @description 更新推广活动
* @access Private
*/
router.put('/activities/:id', activityController.updateActivity);
/**
* @route DELETE /api/v1/promotion/activities/:id
* @description 删除推广活动
* @access Private
*/
router.delete('/activities/:id', activityController.deleteActivity);
/**
* @route GET /api/v1/promotion/rewards
* @description 获取奖励记录列表
* @access Private
*/
router.get('/rewards', rewardController.getRewards);
/**
* @route POST /api/v1/promotion/rewards/:id/issue
* @description 发放奖励
* @access Private
*/
router.post('/rewards/:id/issue', rewardController.issueReward);
module.exports = router;

View File

@@ -1,7 +1,7 @@
const express = require('express');
const { body, query } = require('express-validator');
const UserController = require('../controllers/user');
const { authenticateUser, requireRole: requireAdmin } = require('../middleware/auth');
const { authenticateUser, authenticateAdmin, requireRole: requireAdmin } = require('../middleware/auth');
const router = express.Router();
@@ -179,7 +179,7 @@ router.put('/profile', authenticateUser, UserController.updateProfile);
* description: 服务器内部错误
*/
router.get('/',
authenticateUser,
authenticateAdmin,
requireAdmin(['admin', 'super_admin']),
UserController.getUsers
);
@@ -223,7 +223,7 @@ router.get('/',
* 500:
* description: 服务器内部错误
*/
router.get('/:userId', authenticateUser, requireAdmin(['admin', 'super_admin']), UserController.getUserById);
router.get('/:userId', authenticateAdmin, requireAdmin(['admin', 'super_admin']), UserController.getUserById);
/**
* @swagger
@@ -273,7 +273,7 @@ router.get('/:userId', authenticateUser, requireAdmin(['admin', 'super_admin']),
* 500:
* description: 服务器内部错误
*/
router.get('/statistics', authenticateUser, requireAdmin(['admin', 'super_admin']), UserController.getUserStatistics);
router.get('/statistics', authenticateAdmin, requireAdmin(['admin', 'super_admin']), UserController.getUserStatistics);
/**
* @swagger
@@ -329,7 +329,7 @@ router.get('/statistics', authenticateUser, requireAdmin(['admin', 'super_admin'
* description: 服务器内部错误
*/
router.post('/batch-status',
authenticateUser,
authenticateAdmin,
requireAdmin(['admin', 'super_admin']),
[
body('userIds').isArray().withMessage('userIds必须是数组'),
@@ -379,6 +379,6 @@ router.post('/batch-status',
* 500:
* description: 服务器内部错误
*/
router.delete('/:userId', authenticateUser, requireAdmin(['admin', 'super_admin']), UserController.deleteUser);
router.delete('/:userId', authenticateAdmin, requireAdmin(['admin', 'super_admin']), UserController.deleteUser);
module.exports = router;

View File

@@ -5,7 +5,7 @@ class AnimalService {
// 获取动物列表
static async getAnimals(searchParams) {
try {
const { merchantId, species, status, page = 1, pageSize = 10 } = searchParams;
const { merchantId, type, status, page = 1, pageSize = 10 } = searchParams;
const offset = (page - 1) * pageSize;
let sql = `
@@ -22,9 +22,9 @@ class AnimalService {
params.push(merchantId);
}
if (species) {
sql += ' AND a.species = ?';
params.push(species);
if (type) {
sql += ' AND a.type = ?';
params.push(type);
}
if (status) {
@@ -85,18 +85,26 @@ class AnimalService {
try {
const sql = `
INSERT INTO animals (
merchant_id, name, species, breed, birth_date, personality, farm_location, price, status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
merchant_id, name, type, breed, age, gender, weight, price,
description, health_status, vaccination_records, images,
farm_location, contact_info, status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
`;
const params = [
animalData.merchant_id,
animalData.name,
animalData.species,
animalData.type,
animalData.breed,
animalData.birth_date,
animalData.personality,
animalData.farm_location,
animalData.age,
animalData.gender,
animalData.weight,
animalData.price,
animalData.description,
animalData.health_status,
JSON.stringify(animalData.vaccination_records || []),
JSON.stringify(animalData.images || []),
animalData.farm_location,
JSON.stringify(animalData.contact_info || {}),
animalData.status || 'available'
];

View File

@@ -6,36 +6,41 @@ class TravelService {
static async getTravelPlans(searchParams) {
try {
const { userId, page = 1, pageSize = 10, status } = searchParams;
const offset = (page - 1) * pageSize;
const offset = (parseInt(page) - 1) * parseInt(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
`;
// 构建基础查询条件
let whereClause = 'WHERE 1=1';
const params = [];
if (userId) {
sql += ' AND tp.user_id = ?';
whereClause += ' AND tp.created_by = ?';
params.push(userId);
}
if (status) {
sql += ' AND tp.status = ?';
whereClause += ' AND tp.status = ?';
params.push(status);
}
// 获取总数
const countSql = `SELECT COUNT(*) as total FROM (${sql}) as count_query`;
const countSql = `SELECT COUNT(*) as total FROM travel_plans tp ${whereClause}`;
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);
// 获取数据 - 使用简单的查询方式
const dataSql = `
SELECT tp.*, u.username, u.real_name, u.avatar_url
FROM travel_plans tp
INNER JOIN users u ON tp.created_by = u.id
${whereClause}
ORDER BY tp.created_at DESC
LIMIT ${parseInt(pageSize)} OFFSET ${parseInt(offset)}
`;
console.log('执行SQL:', dataSql);
console.log('参数:', params);
const plans = await query(dataSql, params);
return {
plans: plans.map(plan => this.sanitizePlan(plan)),
@@ -57,7 +62,7 @@ class TravelService {
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
INNER JOIN users u ON tp.created_by = u.id
WHERE tp.id = ?
`;
@@ -76,37 +81,53 @@ class TravelService {
static async createTravelPlan(userId, planData) {
try {
const {
title,
description,
destination,
start_date,
end_date,
budget,
companions,
transportation,
accommodation,
activities,
notes
max_participants,
price_per_person,
itinerary,
requirements,
includes,
excludes,
images
} = 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())
created_by, title, description, destination, start_date, end_date,
max_participants, price_per_person, itinerary,
requirements, includes, excludes, images,
status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', NOW(), NOW())
`;
const params = [
userId,
title,
description || null,
destination,
start_date,
end_date,
budget,
companions,
transportation,
accommodation,
activities,
notes
max_participants || 20,
price_per_person,
JSON.stringify(itinerary || []),
requirements || null,
JSON.stringify(includes || []),
JSON.stringify(excludes || []),
JSON.stringify(images || [])
];
// 调试检查参数中是否有undefined
console.log('SQL Parameters:', params);
params.forEach((param, index) => {
if (param === undefined) {
console.log(`Parameter at index ${index} is undefined`);
}
});
const result = await query(sql, params);
return result.insertId;
} catch (error) {
@@ -118,8 +139,9 @@ class TravelService {
static async updateTravelPlan(planId, userId, updateData) {
try {
const allowedFields = [
'destination', 'start_date', 'end_date', 'budget', 'companions',
'transportation', 'accommodation', 'activities', 'notes', 'status'
'title', 'description', 'destination', 'start_date', 'end_date',
'max_participants', 'price_per_person', 'includes', 'excludes',
'itinerary', 'images', 'requirements', 'status'
];
const setClauses = [];
@@ -127,8 +149,13 @@ class TravelService {
for (const [key, value] of Object.entries(updateData)) {
if (allowedFields.includes(key) && value !== undefined) {
setClauses.push(`${key} = ?`);
params.push(value);
if (Array.isArray(value)) {
setClauses.push(`${key} = ?`);
params.push(JSON.stringify(value));
} else {
setClauses.push(`${key} = ?`);
params.push(value);
}
}
}
@@ -142,7 +169,7 @@ class TravelService {
const sql = `
UPDATE travel_plans
SET ${setClauses.join(', ')}
WHERE id = ? AND user_id = ?
WHERE id = ? AND created_by = ?
`;
const result = await query(sql, params);
@@ -159,7 +186,7 @@ class TravelService {
// 删除旅行计划
static async deleteTravelPlan(planId, userId) {
try {
const sql = 'DELETE FROM travel_plans WHERE id = ? AND user_id = ?';
const sql = 'DELETE FROM travel_plans WHERE id = ? AND created_by = ?';
const result = await query(sql, [planId, userId]);
if (result.affectedRows === 0) {
@@ -181,9 +208,9 @@ class TravelService {
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
SUM(price_per_person * max_participants) as total_budget
FROM travel_plans
WHERE user_id = ?
WHERE created_by = ?
`;
const stats = await query(sql, [userId]);

View File

@@ -96,8 +96,7 @@ class UserService {
const total = countResult[0].total;
// 添加分页和排序
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(pageSize, offset);
sql += ` ORDER BY created_at DESC LIMIT ${parseInt(pageSize)} OFFSET ${parseInt(offset)}`;
const users = await UserMySQL.query(sql, params);

View File

@@ -3,7 +3,7 @@
## 1. 项目概述
### 1.1 项目简介
解班客后端服务是一个基于Node.js + TypeScript + Express的微服务架构系统为小程序端、管理后台和官网提供API服务支持。
结伴客后端服务是一个基于Node.js + TypeScript + Express的微服务架构系统为小程序端、管理后台和官网提供API服务支持。
### 1.2 技术栈
- **运行环境**Node.js 18.x
@@ -842,7 +842,7 @@ const logger = winston.createLogger({
## 8. 总结
本开发文档详细规划了解班客后端系统的开发计划包括
本开发文档详细规划了结伴客后端系统的开发计划包括
### 8.1 开发计划
- **总工期**65个工作日

View File

@@ -1,4 +1,4 @@
# 解班客项目安全文档
# 结伴客项目安全文档
## 1. 安全概述
@@ -309,8 +309,8 @@ const mfaService = {
// 生成TOTP密钥
generateSecret: (userId) => {
const secret = speakeasy.generateSecret({
name: `解班客 (${userId})`,
issuer: '解班客',
name: `结伴客 (${userId})`,
issuer: '结伴客',
length: 32
});
@@ -1958,7 +1958,7 @@ main "$@"
### 10.1 安全架构总结
解班客项目的安全架构采用了多层防护策略,包括:
结伴客项目的安全架构采用了多层防护策略,包括:
- **网络安全层**WAF、防火墙、DDoS防护
- **应用安全层**:身份认证、权限控制、输入验证
@@ -1993,4 +1993,4 @@ main "$@"
- **《个人信息保护法》**:个人信息处理规范
- **等保2.0**:信息系统安全等级保护
通过实施本安全文档中的各项措施,可以有效保护解班客项目的安全,确保用户数据和业务系统的安全稳定运行。
通过实施本安全文档中的各项措施,可以有效保护结伴客项目的安全,确保用户数据和业务系统的安全稳定运行。

View File

@@ -1,9 +1,9 @@
# 解班客官网需求文档
# 结伴客官网需求文档
## 1. 项目概述
### 1.1 官网定位
解班客官网作为品牌展示和商家入驻的主要平台,承担着品牌宣传、用户引流、商家服务、信息展示等重要职能。
结伴客官网作为品牌展示和商家入驻的主要平台,承担着品牌宣传、用户引流、商家服务、信息展示等重要职能。
### 1.2 目标用户
- **潜在用户**:了解平台服务,下载小程序
@@ -21,7 +21,7 @@
### 2.1 首页
#### 2.1.1 品牌展示区
- **品牌Logo和Slogan**:突出显示解班客品牌标识
- **品牌Logo和Slogan**:突出显示结伴客品牌标识
- **核心价值主张**:简洁明了地传达平台价值
- **视觉冲击力**:使用高质量的背景图片或视频
- **行动召唤按钮**:引导用户下载小程序

View File

@@ -161,11 +161,11 @@ X-Version: 1.0.0
"openid": "wx_openid_123",
"unionid": "wx_unionid_456",
"nickname": "微信用户",
"avatar_url": "https://wx.qlogo.cn/avatar.jpg",
"gender": 1,
"city": "北京",
"province": "北京",
"country": "中国",
"real_name": "",
"avatar": "https://wx.qlogo.cn/avatar.jpg",
"user_type": "regular",
"balance": 0.00,
"status": "active",
"is_new_user": false,
"profile_completed": true
}
@@ -216,18 +216,12 @@ X-Version: 1.0.0
"id": 1,
"uuid": "user_123456",
"nickname": "旅行达人",
"real_name": "张三",
"avatar_url": "https://example.com/avatar.jpg",
"gender": 1,
"birthday": "1990-01-01",
"location": "北京市",
"bio": "热爱旅行和小动物",
"phone": "13800138000",
"email": "user@example.com",
"user_type": "normal",
"balance": 1500.00,
"status": 1,
"profile_completed": true,
"real_name_verified": true,
"phone_verified": true,
"email_verified": false,
"created_at": "2024-01-01T10:00:00Z",
"statistics": {
"travel_count": 5,
@@ -269,7 +263,7 @@ X-Version: 1.0.0
"data": {
"id": 1,
"nickname": "新昵称",
"avatar_url": "https://example.com/new_avatar.jpg",
"avatar": "https://example.com/new_avatar.jpg",
"updated_at": "2024-01-15T10:30:00Z"
}
}
@@ -318,18 +312,19 @@ X-Version: 1.0.0
"duration_days": 5,
"max_participants": 8,
"current_participants": 3,
"budget_min": 2000.00,
"budget_max": 3000.00,
"price_per_person": 2500.00,
"includes": "住宿、早餐、门票、导游",
"excludes": "午餐、晚餐、个人消费",
"travel_type": "cultural",
"status": 1,
"is_featured": 1,
"status": "recruiting",
"is_featured": true,
"view_count": 156,
"like_count": 23,
"comment_count": 8,
"creator": {
"id": 1,
"nickname": "旅行达人",
"avatar_url": "https://example.com/avatar.jpg"
"avatar": "https://example.com/avatar.jpg"
},
"cover_image": "https://example.com/cover.jpg",
"images": [
@@ -383,28 +378,20 @@ X-Version: 1.0.0
"duration_days": 5,
"max_participants": 8,
"current_participants": 3,
"min_age": 18,
"max_age": 60,
"gender_limit": 0,
"budget_min": 2000.00,
"budget_max": 3000.00,
"price_per_person": 2500.00,
"includes": "住宿、早餐、门票、导游",
"excludes": "午餐、晚餐、个人消费",
"travel_type": "cultural",
"transportation": "高铁+包车",
"accommodation": "客栈",
"itinerary": [
{
"day": 1,
"title": "抵达大理",
"activities": ["接机", "入住客栈", "古城夜游"],
"meals": ["晚餐"],
"accommodation": "大理古城客栈"
"activities": ["接机", "入住客栈", "古城夜游"]
},
{
"day": 2,
"title": "大理古城游览",
"activities": ["洱海骑行", "三塔寺参观", "古城购物"],
"meals": ["早餐", "午餐", "晚餐"],
"accommodation": "大理古城客栈"
"activities": ["洱海骑行", "三塔寺参观", "古城购物"]
}
],
"requirements": "身体健康,有一定体力,热爱文化旅行",
@@ -419,8 +406,8 @@ X-Version: 1.0.0
"https://example.com/image2.jpg"
],
"tags": ["文化", "古城", "摄影"],
"status": 1,
"is_featured": 1,
"status": "recruiting",
"is_featured": true,
"view_count": 156,
"like_count": 23,
"comment_count": 8,
@@ -428,7 +415,7 @@ X-Version: 1.0.0
"creator": {
"id": 1,
"nickname": "旅行达人",
"avatar_url": "https://example.com/avatar.jpg",
"avatar": "https://example.com/avatar.jpg",
"travel_count": 15,
"rating": 4.8,
"verified": true
@@ -439,11 +426,11 @@ X-Version: 1.0.0
"user": {
"id": 2,
"nickname": "小明",
"avatar_url": "https://example.com/avatar2.jpg",
"avatar": "https://example.com/avatar2.jpg",
"age": 28,
"gender": 1
},
"status": 1,
"status": "confirmed",
"applied_at": "2024-01-10T15:30:00Z"
}
],
@@ -468,35 +455,21 @@ X-Version: 1.0.0
"title": "西藏拉萨朝圣之旅",
"description": "深度体验西藏文化,朝圣布达拉宫...",
"destination": "西藏拉萨",
"destination_detail": {
"province": "西藏自治区",
"city": "拉萨市",
"address": "布达拉宫广场",
"latitude": 29.6544,
"longitude": 91.1175
},
"start_date": "2024-04-01",
"end_date": "2024-04-07",
"max_participants": 6,
"min_age": 20,
"max_age": 50,
"gender_limit": 0,
"budget_min": 5000.00,
"budget_max": 8000.00,
"price_per_person": 6500.00,
"includes": "住宿、早餐、景点门票",
"excludes": "往返机票、午晚餐",
"travel_type": "cultural",
"transportation": "飞机+包车",
"accommodation": "酒店",
"itinerary": [
{
"day": 1,
"title": "抵达拉萨",
"activities": ["接机", "入住酒店", "适应高原"],
"meals": ["晚餐"]
"activities": ["接机", "入住酒店", "适应高原"]
}
],
"requirements": "身体健康,无高原反应病史",
"included_services": ["住宿", "早餐", "景点门票"],
"excluded_services": ["往返机票", "午晚餐"],
"contact_info": {
"wechat": "tibet_lover",
"phone": "13900139000"
@@ -626,13 +599,13 @@ X-Version: 1.0.0
| size | integer | 否 | 每页数量默认10 |
| sort | string | 否 | 排序默认created_at:desc |
| keyword | string | 否 | 搜索关键词(动物名称) |
| species | string | 否 | 物种cat,dog,rabbit,other |
| type | string | 否 | 动物类型cat,dog,rabbit,other |
| breed | string | 否 | 品种 |
| gender | integer | 否 | 性别:1-雄性2-雌性 |
| gender | string | 否 | 性别:male,female |
| age_min | integer | 否 | 年龄下限(月) |
| age_max | integer | 否 | 年龄上限(月) |
| location | string | 否 | 所在地 |
| status | integer | 否 | 状态:1-可认领2-已认领 |
| status | string | 否 | 状态:available,claimed,unavailable |
| is_featured | integer | 否 | 是否精选1-是 |
#### 响应示例
@@ -646,35 +619,34 @@ X-Version: 1.0.0
"id": 1,
"uuid": "animal_123456",
"name": "小花",
"species": "cat",
"type": "",
"breed": "英国短毛猫",
"gender": 2,
"gender": "female",
"age": 24,
"weight": 4.5,
"color": "银渐层",
"description": "性格温顺,喜欢晒太阳",
"personality": "温顺、亲人、活泼",
"health_status": "健康",
"price": 500.00,
"daily_cost": 15.00,
"location": "北京市朝阳区",
"adoption_fee": 500.00,
"monthly_cost": 200.00,
"status": 1,
"is_featured": 1,
"view_count": 89,
"like_count": 15,
"adoption_count": 0,
"cover_image": "https://example.com/cat_cover.jpg",
"status": "available",
"health_status": "健康",
"description": "性格温顺,喜欢晒太阳",
"images": [
"https://example.com/cat1.jpg",
"https://example.com/cat2.jpg"
],
"farm": {
"vaccination_records": [
{
"vaccine": "狂犬疫苗",
"date": "2024-01-01",
"next_date": "2025-01-01"
}
],
"farmer": {
"id": 1,
"name": "爱心动物农场",
"location": "北京市朝阳区",
"rating": 4.8
"location": "北京市朝阳区"
},
"tags": ["温顺", "亲人", "已绝育"],
"view_count": 89,
"like_count": 15,
"created_at": "2024-01-10T10:00:00Z"
}
],
@@ -704,78 +676,35 @@ X-Version: 1.0.0
"id": 1,
"uuid": "animal_123456",
"name": "小花",
"species": "cat",
"type": "",
"breed": "英国短毛猫",
"gender": 2,
"gender": "female",
"age": 24,
"weight": 4.5,
"color": "银渐层",
"description": "小花是一只非常温顺的英国短毛猫,喜欢晒太阳和玩毛线球...",
"personality": "温顺、亲人、活泼",
"health_status": "健康",
"vaccination_status": {
"rabies": {
"vaccinated": true,
"date": "2023-12-01",
"next_due": "2024-12-01"
},
"feline_distemper": {
"vaccinated": true,
"date": "2023-12-01",
"next_due": "2024-12-01"
}
},
"medical_history": "2023年11月进行了绝育手术恢复良好",
"price": 500.00,
"daily_cost": 15.00,
"location": "北京市朝阳区",
"adoption_fee": 500.00,
"monthly_cost": 200.00,
"status": 1,
"is_featured": 1,
"view_count": 89,
"like_count": 15,
"adoption_count": 0,
"status": "available",
"health_status": "健康",
"description": "小花是一只非常温顺的英国短毛猫,喜欢晒太阳和玩毛线球...",
"images": [
"https://example.com/cat1.jpg",
"https://example.com/cat2.jpg"
],
"videos": [
"https://example.com/cat_video1.mp4"
"vaccination_records": [
{
"vaccine": "狂犬疫苗",
"date": "2024-01-01",
"next_date": "2025-01-01"
}
],
"farm": {
"farmer": {
"id": 1,
"name": "爱心动物农场",
"location": "北京市朝阳区",
"contact_phone": "13800138000",
"description": "专业的动物救助机构",
"rating": 4.8,
"images": [
"https://example.com/farm1.jpg"
]
"contact_phone": "13800138000"
},
"caretaker": {
"id": 1,
"name": "张三",
"avatar_url": "https://example.com/caretaker.jpg",
"experience": "5年动物护理经验",
"introduction": "热爱动物,专业护理"
},
"adoption_types": [
{
"type": 1,
"name": "长期认领",
"description": "12个月以上的长期认领",
"min_duration": 12,
"monthly_fee": 200.00
},
{
"type": 2,
"name": "短期认领",
"description": "1-6个月的短期认领",
"min_duration": 1,
"monthly_fee": 250.00
}
],
"tags": ["温顺", "亲人", "已绝育"],
"view_count": 89,
"like_count": 15,
"is_liked": false,
"can_adopt": true,
"created_at": "2024-01-10T10:00:00Z"

View File

@@ -1,9 +1,9 @@
# 解班客小程序需求文档
# 结伴客小程序需求文档
## 1. 项目概述
### 1.1 产品定位
解班客微信小程序是面向C端用户的核心产品专注于提供结伴旅行和动物认领服务。通过微信生态的便利性为用户提供便捷的社交旅行体验和创新的动物认领服务。
结伴客微信小程序是面向C端用户的核心产品专注于提供结伴旅行和动物认领服务。通过微信生态的便利性为用户提供便捷的社交旅行体验和创新的动物认领服务。
### 1.2 目标用户
- **主要用户群体**18-35岁的年轻用户

View File

@@ -253,49 +253,392 @@ erDiagram
```sql
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
openid VARCHAR(100) UNIQUE NOT NULL COMMENT '微信openid',
unionid VARCHAR(100) COMMENT '微信unionid',
nickname VARCHAR(50) NOT NULL COMMENT '用户昵称',
avatar VARCHAR(255) COMMENT '头像URL',
gender ENUM('male', 'female', 'unknown') DEFAULT 'unknown' COMMENT '性别',
birthday DATE COMMENT '生日',
phone VARCHAR(20) UNIQUE COMMENT '手机号码',
email VARCHAR(100) UNIQUE COMMENT '邮箱地址',
province VARCHAR(50) COMMENT '省份',
city VARCHAR(50) COMMENT '城市',
travel_count INT DEFAULT 0 COMMENT '旅行次数',
animal_claim_count INT DEFAULT 0 COMMENT '认领动物数量',
real_name VARCHAR(50) COMMENT '真实姓名',
nickname VARCHAR(50) COMMENT '用户昵称',
avatar_url VARCHAR(255) COMMENT '头像URL',
user_type ENUM('regular','vip','premium') DEFAULT 'regular' COMMENT '用户类型',
status ENUM('active','inactive','banned') DEFAULT 'active' COMMENT '用户状态',
balance DECIMAL(10,2) DEFAULT 0.00 COMMENT '账户余额',
points INT DEFAULT 0 COMMENT '积分',
level ENUM('bronze', 'silver', 'gold', 'platinum') DEFAULT 'bronze' COMMENT '用户等级',
status ENUM('active', 'inactive', 'banned') DEFAULT 'active' COMMENT '用户状态',
level INT DEFAULT 1 COMMENT '用户等级',
last_login_at TIMESTAMP COMMENT '最后登录时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted_at TIMESTAMP NULL COMMENT '删除时间',
INDEX idx_openid (openid),
INDEX idx_phone (phone),
INDEX idx_email (email),
INDEX idx_status (status),
INDEX idx_user_type (user_type),
INDEX idx_level (level),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户基础信息表';
```
#### 1.2 用户兴趣表 (user_interests)
#### 1.2 管理员表 (admins)
```sql
CREATE TABLE user_interests (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '兴趣ID',
CREATE TABLE admins (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '管理员ID',
username VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '密码',
email VARCHAR(100) UNIQUE COMMENT '邮箱',
nickname VARCHAR(50) COMMENT '昵称',
avatar VARCHAR(255) COMMENT '头像',
role ENUM('super_admin','admin','editor') DEFAULT 'admin' COMMENT '角色',
status ENUM('active','inactive') DEFAULT 'active' COMMENT '状态',
last_login TIMESTAMP COMMENT '最后登录时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_role (role),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理员表';
```
### 2. 商家管理模块
#### 2.1 商家表 (merchants)
```sql
CREATE TABLE merchants (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商家ID',
user_id INT NOT NULL COMMENT '关联用户ID',
name VARCHAR(100) NOT NULL COMMENT '商家名称',
description TEXT COMMENT '商家描述',
address VARCHAR(255) COMMENT '地址',
latitude DECIMAL(10,8) COMMENT '纬度',
longitude DECIMAL(11,8) COMMENT '经度',
contact_phone VARCHAR(20) COMMENT '联系电话',
business_hours VARCHAR(100) COMMENT '营业时间',
images JSON COMMENT '商家图片',
rating DECIMAL(3,2) DEFAULT 0.00 COMMENT '评分',
review_count INT DEFAULT 0 COMMENT '评价数量',
status ENUM('active','inactive','pending') DEFAULT 'pending' COMMENT '状态',
verified_at TIMESTAMP NULL COMMENT '认证时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_rating (rating),
INDEX idx_location (latitude, longitude)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商家信息表';
```
### 3. 动物认领模块
#### 3.1 动物表 (animals)
```sql
CREATE TABLE animals (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '动物ID',
name VARCHAR(50) NOT NULL COMMENT '动物名称',
type VARCHAR(30) NOT NULL COMMENT '动物类型',
breed VARCHAR(50) COMMENT '品种',
age INT COMMENT '年龄',
gender ENUM('male','female','unknown') DEFAULT 'unknown' COMMENT '性别',
description TEXT COMMENT '描述',
images JSON COMMENT '图片',
price DECIMAL(10,2) NOT NULL COMMENT '认领价格',
daily_cost DECIMAL(8,2) COMMENT '日常费用',
location VARCHAR(100) COMMENT '所在地',
farmer_id INT COMMENT '农场主ID',
status ENUM('available','claimed','unavailable') DEFAULT 'available' COMMENT '状态',
health_status VARCHAR(50) COMMENT '健康状态',
vaccination_records JSON COMMENT '疫苗记录',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted_at TIMESTAMP NULL COMMENT '删除时间',
FOREIGN KEY (farmer_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_farmer_id (farmer_id),
INDEX idx_type (type),
INDEX idx_status (status),
INDEX idx_price (price)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='动物信息表';
```
#### 3.2 动物认领表 (animal_claims)
```sql
CREATE TABLE animal_claims (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '认领ID',
animal_id INT NOT NULL COMMENT '动物ID',
user_id INT NOT NULL COMMENT '用户ID',
interest_name VARCHAR(50) NOT NULL COMMENT '兴趣名称',
interest_type ENUM('travel', 'food', 'sports', 'culture', 'nature') COMMENT '兴趣类型',
contact_info VARCHAR(100) COMMENT '联系信息',
status ENUM('pending','approved','rejected','cancelled') DEFAULT 'pending' COMMENT '状态',
reviewed_by INT COMMENT '审核人ID',
reviewed_at TIMESTAMP NULL COMMENT '审核时间',
review_note TEXT COMMENT '审核备注',
start_date DATE COMMENT '开始日期',
end_date DATE COMMENT '结束日期',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted_at TIMESTAMP NULL COMMENT '删除时间',
FOREIGN KEY (animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_animal_id (animal_id),
INDEX idx_user_id (user_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='动物认领表';
```
### 4. 旅行计划模块
#### 4.1 旅行计划表 (travel_plans)
```sql
CREATE TABLE travel_plans (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '计划ID',
title VARCHAR(100) NOT NULL COMMENT '计划标题',
destination VARCHAR(100) NOT NULL COMMENT '目的地',
description TEXT COMMENT '描述',
start_date DATE NOT NULL COMMENT '开始日期',
end_date DATE NOT NULL COMMENT '结束日期',
max_participants INT DEFAULT 10 COMMENT '最大参与人数',
current_participants INT DEFAULT 0 COMMENT '当前参与人数',
price_per_person DECIMAL(10,2) NOT NULL COMMENT '每人价格',
includes JSON COMMENT '包含项目',
excludes JSON COMMENT '不包含项目',
itinerary JSON COMMENT '行程安排',
images JSON COMMENT '图片',
requirements TEXT COMMENT '参与要求',
created_by INT NOT NULL COMMENT '创建者ID',
status ENUM('draft','published','cancelled','completed') DEFAULT 'draft' COMMENT '状态',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_created_by (created_by),
INDEX idx_destination (destination),
INDEX idx_status (status),
INDEX idx_start_date (start_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='旅行计划表';
```
#### 4.2 旅行报名表 (travel_registrations)
```sql
CREATE TABLE travel_registrations (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '报名ID',
travel_plan_id INT NOT NULL COMMENT '旅行计划ID',
user_id INT NOT NULL COMMENT '用户ID',
participants INT DEFAULT 1 COMMENT '参与人数',
message TEXT COMMENT '留言',
emergency_contact VARCHAR(50) COMMENT '紧急联系人',
emergency_phone VARCHAR(20) COMMENT '紧急联系电话',
status ENUM('pending','approved','rejected','cancelled') DEFAULT 'pending' COMMENT '状态',
reject_reason TEXT COMMENT '拒绝原因',
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间',
responded_at TIMESTAMP NULL COMMENT '响应时间',
FOREIGN KEY (travel_plan_id) REFERENCES travel_plans(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_travel_plan_id (travel_plan_id),
INDEX idx_user_id (user_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='旅行报名表';
```
### 5. 花卉产品模块
#### 5.1 花卉表 (flowers)
```sql
CREATE TABLE flowers (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '花卉ID',
name VARCHAR(100) NOT NULL COMMENT '花卉名称',
scientific_name VARCHAR(100) COMMENT '学名',
category VARCHAR(50) COMMENT '分类',
color VARCHAR(30) COMMENT '颜色',
bloom_season VARCHAR(50) COMMENT '花期',
care_level ENUM('easy','medium','hard') DEFAULT 'medium' COMMENT '养护难度',
description TEXT COMMENT '描述',
care_instructions TEXT COMMENT '养护说明',
image VARCHAR(255) COMMENT '主图片',
images JSON COMMENT '图片集',
price DECIMAL(8,2) NOT NULL COMMENT '价格',
stock_quantity INT DEFAULT 0 COMMENT '库存数量',
farmer_id INT COMMENT '农场主ID',
status ENUM('available','out_of_stock','discontinued') DEFAULT 'available' COMMENT '状态',
seasonal_availability JSON COMMENT '季节性供应',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (farmer_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_farmer_id (farmer_id),
INDEX idx_category (category),
INDEX idx_status (status),
INDEX idx_price (price)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='花卉产品表';
```
### 6. 订单管理模块
#### 6.1 订单表 (orders)
```sql
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID',
order_number VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号',
user_id INT NOT NULL COMMENT '用户ID',
total_amount DECIMAL(15,2) NOT NULL COMMENT '订单总金额',
status ENUM('pending','paid','shipped','delivered','cancelled','refunded') DEFAULT 'pending' COMMENT '订单状态',
payment_status ENUM('unpaid','paid','refunded','partial_refund') DEFAULT 'unpaid' COMMENT '支付状态',
payment_method VARCHAR(20) COMMENT '支付方式',
payment_time TIMESTAMP NULL COMMENT '支付时间',
shipping_address JSON COMMENT '收货地址',
contact_info JSON COMMENT '联系信息',
notes TEXT COMMENT '备注',
ordered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_order_number (order_number),
INDEX idx_status (status),
INDEX idx_payment_status (payment_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';
```
#### 6.2 支付表 (payments)
```sql
CREATE TABLE payments (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '支付ID',
order_id INT NOT NULL COMMENT '订单ID',
user_id INT NOT NULL COMMENT '用户ID',
amount DECIMAL(15,2) NOT NULL COMMENT '支付金额',
payment_method ENUM('wechat','alipay','balance','points') NOT NULL COMMENT '支付方式',
status ENUM('pending','success','failed','cancelled','refunded') DEFAULT 'pending' COMMENT '支付状态',
transaction_id VARCHAR(100) COMMENT '交易流水号',
paid_amount DECIMAL(15,2) COMMENT '实际支付金额',
paid_at TIMESTAMP NULL COMMENT '支付时间',
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_order_id (order_id),
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_transaction_id (transaction_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付记录表';
```
#### 6.3 退款表 (refunds)
```sql
CREATE TABLE refunds (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '退款ID',
payment_id INT NOT NULL COMMENT '支付ID',
user_id INT NOT NULL COMMENT '用户ID',
refund_amount DECIMAL(15,2) NOT NULL COMMENT '退款金额',
refund_reason VARCHAR(255) NOT NULL COMMENT '退款原因',
status ENUM('pending','processing','completed','rejected') DEFAULT 'pending' COMMENT '退款状态',
processed_by INT COMMENT '处理人ID',
processed_at TIMESTAMP NULL COMMENT '处理时间',
process_remark TEXT COMMENT '处理备注',
refund_transaction_id VARCHAR(100) COMMENT '退款交易号',
refunded_at TIMESTAMP NULL COMMENT '退款完成时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted_at TIMESTAMP NULL COMMENT '删除时间',
FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_payment_id (payment_id),
INDEX idx_user_id (user_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退款记录表';
```
### 7. 系统辅助表
#### 7.1 邮箱验证表 (email_verifications)
```sql
CREATE TABLE email_verifications (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '验证ID',
email VARCHAR(100) NOT NULL COMMENT '邮箱地址',
code VARCHAR(10) NOT NULL COMMENT '验证码',
type ENUM('register','reset_password','change_email') NOT NULL COMMENT '验证类型',
expires_at TIMESTAMP NOT NULL COMMENT '过期时间',
used_at TIMESTAMP NULL COMMENT '使用时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_email (email),
INDEX idx_code (code),
INDEX idx_expires_at (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='邮箱验证表';
```
#### 7.2 密码重置表 (password_resets)
```sql
CREATE TABLE password_resets (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '重置ID',
user_id INT NOT NULL COMMENT '用户ID',
token VARCHAR(100) NOT NULL COMMENT '重置令牌',
expires_at TIMESTAMP NOT NULL COMMENT '过期时间',
used_at TIMESTAMP NULL COMMENT '使用时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY uk_user_interest (user_id, interest_name),
INDEX idx_user_id (user_id),
INDEX idx_interest_type (interest_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户兴趣表';
INDEX idx_token (token),
INDEX idx_expires_at (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='密码重置表';
```
#### 7.3 登录尝试表 (login_attempts)
```sql
CREATE TABLE login_attempts (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '尝试ID',
identifier VARCHAR(100) NOT NULL COMMENT '标识符(用户名/邮箱/手机)',
ip_address VARCHAR(45) NOT NULL COMMENT 'IP地址',
user_agent TEXT COMMENT '用户代理',
success TINYINT(1) DEFAULT 0 COMMENT '是否成功',
failure_reason VARCHAR(100) COMMENT '失败原因',
attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '尝试时间',
INDEX idx_identifier (identifier),
INDEX idx_ip_address (ip_address),
INDEX idx_attempted_at (attempted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录尝试记录表';
```
## 5. 表关系图
### 5.1 外键关系
根据实际数据库结构,以下是表之间的外键关系:
```mermaid
erDiagram
users ||--o{ animal_claims : "user_id"
users ||--o{ animals : "farmer_id"
users ||--o{ flowers : "farmer_id"
users ||--o{ merchants : "user_id"
users ||--o{ orders : "user_id"
users ||--o{ password_resets : "user_id"
users ||--o{ payments : "user_id"
users ||--o{ refunds : "user_id"
users ||--o{ travel_plans : "created_by"
users ||--o{ travel_registrations : "user_id"
animals ||--o{ animal_claims : "animal_id"
orders ||--o{ payments : "order_id"
payments ||--o{ refunds : "payment_id"
travel_plans ||--o{ travel_registrations : "travel_plan_id"
```
### 5.2 核心业务关系说明
1. **用户中心关系**
- 用户可以认领多个动物 (users → animal_claims)
- 用户可以作为农场主管理动物和花卉 (users → animals/flowers)
- 用户可以注册为商家 (users → merchants)
- 用户可以下订单和支付 (users → orders → payments)
2. **旅行业务关系**
- 用户创建旅行计划 (users → travel_plans)
- 其他用户报名参与旅行 (users → travel_registrations)
- 旅行计划与报名记录关联 (travel_plans → travel_registrations)
3. **交易业务关系**
- 订单关联支付记录 (orders → payments)
- 支付记录可以产生退款 (payments → refunds)
- 所有交易都关联到用户 (users → orders/payments/refunds)
### 2. 商家管理模块
#### 2.1 商家表 (merchants)
@@ -480,46 +823,137 @@ CREATE TABLE animal_updates (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='动物更新记录表';
```
### 5. 商品订单模块
## 6. 数据库索引优化
#### 5.1 商品表 (products)
```sql
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID',
merchant_id INT NOT NULL COMMENT '商家ID',
category_id INT COMMENT '分类ID',
name VARCHAR(100) NOT NULL COMMENT '商品名称',
description TEXT COMMENT '商品描述',
price DECIMAL(10,2) NOT NULL COMMENT '商品价格',
original_price DECIMAL(10,2) COMMENT '原价',
stock INT DEFAULT 0 COMMENT '库存数量',
min_order_quantity INT DEFAULT 1 COMMENT '最小起订量',
max_order_quantity INT COMMENT '最大订购量',
images JSON COMMENT '商品图片数组',
specifications JSON COMMENT '商品规格',
tags VARCHAR(255) COMMENT '商品标签',
weight DECIMAL(8,3) COMMENT '商品重量(公斤)',
dimensions VARCHAR(50) COMMENT '商品尺寸',
shelf_life INT COMMENT '保质期(天)',
storage_conditions TEXT COMMENT '储存条件',
delivery_info TEXT COMMENT '配送信息',
rating DECIMAL(3,2) DEFAULT 5.00 COMMENT '商品评分',
review_count INT DEFAULT 0 COMMENT '评价数量',
sales_count INT DEFAULT 0 COMMENT '销售数量',
status ENUM('active', 'inactive', 'out_of_stock', 'discontinued') DEFAULT 'active' COMMENT '商品状态',
sort_order INT DEFAULT 0 COMMENT '排序权重',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (merchant_id) REFERENCES merchants(id) ON DELETE CASCADE,
INDEX idx_merchant_id (merchant_id),
INDEX idx_category_id (category_id),
INDEX idx_price (price),
INDEX idx_status (status),
INDEX idx_rating (rating),
INDEX idx_sales_count (sales_count)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品信息表';
```
### 6.1 主要索引策略
#### 用户表 (users) 索引
- 主键索引:`PRIMARY KEY (id)`
- 状态索引:`INDEX idx_status (status)` - 用于用户状态筛选
- 用户类型索引:`INDEX idx_user_type (user_type)` - 用于用户类型查询
- 等级索引:`INDEX idx_level (level)` - 用于用户等级排序
- 创建时间索引:`INDEX idx_created_at (created_at)` - 用于时间范围查询
#### 管理员表 (admins) 索引
- 主键索引:`PRIMARY KEY (id)`
- 唯一索引:`UNIQUE KEY (username)`, `UNIQUE KEY (email)`
- 角色索引:`INDEX idx_role (role)` - 用于权限管理
- 状态索引:`INDEX idx_status (status)` - 用于状态筛选
#### 商家表 (merchants) 索引
- 主键索引:`PRIMARY KEY (id)`
- 外键索引:`INDEX idx_user_id (user_id)` - 关联用户查询
- 状态索引:`INDEX idx_status (status)` - 商家状态筛选
- 评分索引:`INDEX idx_rating (rating)` - 评分排序
- 地理位置复合索引:`INDEX idx_location (latitude, longitude)` - 地理位置查询
#### 动物表 (animals) 索引
- 主键索引:`PRIMARY KEY (id)`
- 外键索引:`INDEX idx_farmer_id (farmer_id)` - 农场主查询
- 类型索引:`INDEX idx_type (type)` - 动物类型筛选
- 状态索引:`INDEX idx_status (status)` - 动物状态筛选
- 价格索引:`INDEX idx_price (price)` - 价格排序
#### 旅行计划表 (travel_plans) 索引
- 主键索引:`PRIMARY KEY (id)`
- 外键索引:`INDEX idx_created_by (created_by)` - 创建者查询
- 目的地索引:`INDEX idx_destination (destination)` - 目的地搜索
- 状态索引:`INDEX idx_status (status)` - 计划状态筛选
- 开始日期索引:`INDEX idx_start_date (start_date)` - 日期排序
#### 订单表 (orders) 索引
- 主键索引:`PRIMARY KEY (id)`
- 外键索引:`INDEX idx_user_id (user_id)` - 用户订单查询
- 订单号唯一索引:`UNIQUE KEY (order_number)` - 订单号查询
- 状态索引:`INDEX idx_status (status)` - 订单状态筛选
- 支付状态索引:`INDEX idx_payment_status (payment_status)` - 支付状态筛选
### 6.2 查询优化建议
1. **分页查询优化**
- 使用 `LIMIT``OFFSET` 进行分页
- 对于大数据量分页,建议使用游标分页
2. **复合索引使用**
- 按照查询频率和选择性创建复合索引
- 遵循最左前缀原则
3. **避免全表扫描**
- 在 WHERE 条件中使用索引字段
- 避免在索引字段上使用函数
## 7. 数据库安全策略
### 7.1 访问控制
- 使用专用数据库用户,限制权限
- 定期更换数据库密码
- 启用SSL连接加密
### 7.2 数据加密
- 敏感字段(如密码)使用哈希加密
- 个人信息字段考虑加密存储
- 传输过程使用HTTPS协议
### 7.3 备份策略
- 每日自动备份数据库
- 定期测试备份恢复流程
- 异地备份保证数据安全
## 8. 性能监控与优化
### 8.1 监控指标
- 查询响应时间
- 数据库连接数
- 慢查询日志分析
- 索引使用率统计
### 8.2 优化策略
- 定期分析慢查询并优化
- 监控表大小,适时进行分区
- 定期更新表统计信息
- 合理设置数据库参数
## 9. 数据库维护
### 9.1 日常维护
- 定期检查数据库状态
- 清理过期的临时数据
- 监控磁盘空间使用
- 更新数据库统计信息
### 9.2 版本管理
- 使用数据库迁移脚本管理结构变更
- 记录每次结构变更的版本号
- 保持开发、测试、生产环境一致
## 10. 总结
本数据库设计文档基于解班客项目的实际需求,涵盖了用户管理、商家管理、动物认领、旅行计划、花卉产品、订单支付等核心业务模块。设计遵循了数据库设计的最佳实践,包括:
1. **规范化设计**:避免数据冗余,保证数据一致性
2. **性能优化**:合理设计索引,优化查询性能
3. **扩展性**:预留扩展空间,支持业务发展
4. **安全性**:实施访问控制和数据加密
5. **可维护性**:清晰的表结构和完善的文档
### 10.1 当前数据库统计
- **总表数**14张表
- **核心业务表**8张users, admins, merchants, animals, animal_claims, travel_plans, travel_registrations, flowers, orders, payments, refunds
- **辅助系统表**3张email_verifications, password_resets, login_attempts
- **外键关系**13个外键约束
### 10.2 后续优化方向
1. 根据业务发展需要,考虑添加缓存层
2. 对于高频查询表,考虑读写分离
3. 监控数据增长,适时进行分库分表
4. 完善数据备份和灾难恢复方案
---
**文档版本**v2.0
**最后更新**2024年1月
**维护人员**:开发团队
**审核状态**:已审核
#### 5.2 订单表 (orders)
```sql

View File

@@ -1,9 +1,9 @@
# 解班客项目系统架构文档
# 结伴客项目系统架构文档
## 1. 项目概述
### 1.1 项目简介
解班客是一个综合性的社交旅行平台,融合了结伴旅行和动物认领两大核心功能。项目采用现代化的微服务架构,包含微信小程序、管理后台、官方网站和后端服务四个主要模块。
结伴客是一个综合性的社交旅行平台,融合了结伴旅行和动物认领两大核心功能。项目采用现代化的微服务架构,包含微信小程序、管理后台、官方网站和后端服务四个主要模块。
### 1.2 业务架构
```mermaid

View File

@@ -1,9 +1,9 @@
# 解班客项目需求文档
# 结伴客项目需求文档
## 1. 项目概述
### 1.1 项目背景
解班客是一个创新的社交旅行平台,专注于为用户提供结伴旅行服务,并融入了独特的动物认领功能。该项目旨在通过结合传统的结伴旅行功能与现代的动物认领体验,为用户创造独特的旅行记忆。
结伴客是一个创新的社交旅行平台,专注于为用户提供结伴旅行服务,并融入了独特的动物认领功能。该项目旨在通过结合传统的结伴旅行功能与现代的动物认领体验,为用户创造独特的旅行记忆。
### 1.2 项目目标
- 构建一个完整的社交旅行生态系统

View File

@@ -1,9 +1,9 @@
# 解班客项目测试文档
# 结伴客项目测试文档
## 1. 测试概述
### 1.1 测试目标
确保解班客项目各个模块的功能正确性、性能稳定性、安全可靠性,为产品上线提供质量保障。
确保结伴客项目各个模块的功能正确性、性能稳定性、安全可靠性,为产品上线提供质量保障。
### 1.2 测试范围
- **后端API服务**:接口功能、性能、安全测试
@@ -183,7 +183,7 @@ describe('旅行结伴页面', () => {
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="解班客API压测">
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="结伴客API压测">
<elementProp name="TestPlan.arguments" elementType="Arguments" guiclass="ArgumentsPanel">
<collectionProp name="Arguments.arguments"/>
</elementProp>
@@ -304,7 +304,7 @@ describe('XSS防护测试', () => {
### 6.1 测试报告模板
```markdown
# 解班客项目测试报告
# 结伴客项目测试报告
## 测试概要
- **测试版本**: v1.0.0
@@ -969,8 +969,8 @@ test.describe('动物认领流程', () => {
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="解班客性能测试">
<stringProp name="TestPlan.comments">解班客系统性能测试计划</stringProp>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="结伴客性能测试">
<stringProp name="TestPlan.comments">结伴客系统性能测试计划</stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量">
@@ -1129,7 +1129,7 @@ function htmlReport(data) {
</style>
</head>
<body>
<h1>解班客性能测试报告</h1>
<h1>结伴客性能测试报告</h1>
<h2>测试概要</h2>
<div class="metric">
<strong>总请求数:</strong> ${data.metrics.http_reqs.count}
@@ -1311,7 +1311,7 @@ function generateHtmlReport(coverage) {
</style>
</head>
<body>
<h1>解班客测试覆盖率报告</h1>
<h1>结伴客测试覆盖率报告</h1>
<div class="summary">
<h2>总体覆盖率</h2>
<div class="metric ${getColorClass(total.lines.pct)}">
@@ -1542,7 +1542,7 @@ jobs:
## 📚 总结
本测试文档全面覆盖了解班客项目的测试策略和实施方案包括
本测试文档全面覆盖了结伴客项目的测试策略和实施方案包括
### 测试体系特点
@@ -1572,7 +1572,7 @@ jobs:
3. 根据业务变化调整测试策略
4. 培训团队成员测试最佳实践
通过完善的测试体系确保解班客项目的高质量交付和稳定运行
通过完善的测试体系确保结伴客项目的高质量交付和稳定运行
---

View File

@@ -1,9 +1,9 @@
# 解班客用户手册文档
# 结伴客用户手册文档
## 1. 文档概述
### 1.1 文档目的
本文档为解班客平台的用户使用手册,包含小程序端用户指南和管理后台操作手册,帮助用户快速上手并充分利用平台功能。
本文档为结伴客平台的用户使用手册,包含小程序端用户指南和管理后台操作手册,帮助用户快速上手并充分利用平台功能。
### 1.2 适用对象
- **普通用户**:使用小程序进行旅行结伴和动物认领的用户
@@ -33,7 +33,7 @@ graph TB
#### 2.1.1 首次使用
1. **下载安装**
- 微信搜索"解班客"小程序
- 微信搜索"结伴客"小程序
- 或扫描二维码进入小程序
2. **授权登录**
@@ -423,4 +423,4 @@ graph TB
**文档版本**v1.0.0
**最后更新**2024-01-15
**维护团队**解班客技术团队
**维护团队**结伴客技术团队

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
# 解班客管理后台架构文档
# 结伴客管理后台架构文档
## 1. 项目概述
### 1.1 项目简介
解班客管理后台是一个基于Vue.js 3.x + Element Plus的现代化管理系统为运营人员提供用户管理、内容管理、数据分析等功能。采用前后端分离架构支持多角色权限管理和实时数据监控。
结伴客管理后台是一个基于Vue.js 3.x + Element Plus的现代化管理系统为运营人员提供用户管理、内容管理、数据分析等功能。采用前后端分离架构支持多角色权限管理和实时数据监控。
### 1.2 业务目标
- **运营管理**:提供完整的运营管理功能
@@ -719,7 +719,7 @@ export function setupRouterGuards(router: Router) {
// 全局后置守卫
router.afterEach((to) => {
// 设置页面标题
document.title = `${to.meta.title || '管理后台'} - 解班客`
document.title = `${to.meta.title || '管理后台'} - 结伴客`
// 页面访问统计
// analytics.trackPageView(to.path)
@@ -2021,12 +2021,12 @@ export default defineConfig({
#### 8.1.2 环境配置
```typescript
// .env.development
VITE_APP_TITLE=解班客管理后台
VITE_APP_TITLE=结伴客管理后台
VITE_API_BASE_URL=http://localhost:8080/api
VITE_UPLOAD_URL=http://localhost:8080/upload
// .env.production
VITE_APP_TITLE=解班客管理后台
VITE_APP_TITLE=结伴客管理后台
VITE_API_BASE_URL=https://api.jiebanke.com
VITE_UPLOAD_URL=https://cdn.jiebanke.com/upload
```
@@ -2561,4 +2561,4 @@ export class Analytics {
- 自动化测试
- 开发工具链
通过以上架构设计,解班客管理后台将具备高性能、高可用、易维护的特点,为运营团队提供强大的管理工具,支撑业务的快速发展。
通过以上架构设计,结伴客管理后台将具备高性能、高可用、易维护的特点,为运营团队提供强大的管理工具,支撑业务的快速发展。

View File

@@ -1,4 +1,4 @@
# 解班客项目部署文档
# 结伴客项目部署文档
## 1. 部署概述
@@ -751,7 +751,7 @@ docker logs -f redis-master
## 12. 总结
本部署文档涵盖了解班客项目的完整部署流程包括
本部署文档涵盖了结伴客项目的完整部署流程包括
- **基础环境**服务器配置软件安装
- **数据库部署**MySQL主从Redis集群