重构动物模型和路由系统,优化查询逻辑并新增商户和促销活动功能
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
# 解班客 - 宠物认领平台
|
||||
# 结伴客 - 宠物认领平台
|
||||
|
||||
一个基于Vue.js和Node.js的宠物认领平台,帮助流浪动物找到温暖的家。
|
||||
|
||||
## 项目概述
|
||||
|
||||
解班客是一个专业的宠物认领平台,致力于为流浪动物提供一个温暖的归宿。平台通过现代化的Web技术,为用户提供便捷的宠物发布、搜索、认领服务,同时为管理员提供完善的后台管理功能。
|
||||
结伴客是一个专业的宠物认领平台,致力于为流浪动物提供一个温暖的归宿。平台通过现代化的Web技术,为用户提供便捷的宠物发布、搜索、认领服务,同时为管理员提供完善的后台管理功能。
|
||||
|
||||
### 核心功能
|
||||
|
||||
|
||||
115
admin-system/src/api/flower.ts
Normal file
115
admin-system/src/api/flower.ts
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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('登录成功!')
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
272
admin-system/src/pages/flower/components/FlowerModal.vue
Normal file
272
admin-system/src/pages/flower/components/FlowerModal.vue
Normal 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>
|
||||
223
admin-system/src/pages/flower/index.vue
Normal file
223
admin-system/src/pages/flower/index.vue
Normal file
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
163
admin-system/src/stores/modules/flower.ts
Normal file
163
admin-system/src/stores/modules/flower.ts
Normal 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
|
||||
}
|
||||
})
|
||||
@@ -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🎉 模拟数据测试完成!')
|
||||
420
backend/docs/商户管理API接口文档.md
Normal file
420
backend/docs/商户管理API接口文档.md
Normal 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): 初始版本,包含基础的商户管理功能
|
||||
@@ -1,4 +1,4 @@
|
||||
-- 解班客数据库完整结构创建脚本
|
||||
-- 结伴客数据库完整结构创建脚本
|
||||
-- 创建时间: 2024年
|
||||
-- 数据库: jbkdata
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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导入
|
||||
|
||||
|
||||
@@ -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处理
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
`);
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
276
backend/src/controllers/merchant.js
Normal file
276
backend/src/controllers/merchant.js
Normal 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
|
||||
};
|
||||
461
backend/src/controllers/promotion/activityController.js
Normal file
461
backend/src/controllers/promotion/activityController.js
Normal 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);
|
||||
}
|
||||
};
|
||||
175
backend/src/controllers/promotion/rewardController.js
Normal file
175
backend/src/controllers/promotion/rewardController.js
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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({
|
||||
|
||||
@@ -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 };
|
||||
|
||||
217
backend/src/models/Merchant.js
Normal file
217
backend/src/models/Merchant.js
Normal 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;
|
||||
427
backend/src/models/Travel.js
Normal file
427
backend/src/models/Travel.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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位')
|
||||
|
||||
453
backend/src/routes/merchant.js
Normal file
453
backend/src/routes/merchant.js
Normal 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;
|
||||
61
backend/src/routes/promotion.js
Normal file
61
backend/src/routes/promotion.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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个工作日
|
||||
|
||||
10
docs/安全文档.md
10
docs/安全文档.md
@@ -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**:信息系统安全等级保护
|
||||
|
||||
通过实施本安全文档中的各项措施,可以有效保护解班客项目的安全,确保用户数据和业务系统的安全稳定运行。
|
||||
通过实施本安全文档中的各项措施,可以有效保护结伴客项目的安全,确保用户数据和业务系统的安全稳定运行。
|
||||
@@ -1,9 +1,9 @@
|
||||
# 解班客官网需求文档
|
||||
# 结伴客官网需求文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 官网定位
|
||||
解班客官网作为品牌展示和商家入驻的主要平台,承担着品牌宣传、用户引流、商家服务、信息展示等重要职能。
|
||||
结伴客官网作为品牌展示和商家入驻的主要平台,承担着品牌宣传、用户引流、商家服务、信息展示等重要职能。
|
||||
|
||||
### 1.2 目标用户
|
||||
- **潜在用户**:了解平台服务,下载小程序
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
### 2.1 首页
|
||||
#### 2.1.1 品牌展示区
|
||||
- **品牌Logo和Slogan**:突出显示解班客品牌标识
|
||||
- **品牌Logo和Slogan**:突出显示结伴客品牌标识
|
||||
- **核心价值主张**:简洁明了地传达平台价值
|
||||
- **视觉冲击力**:使用高质量的背景图片或视频
|
||||
- **行动召唤按钮**:引导用户下载小程序
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 解班客小程序需求文档
|
||||
# 结伴客小程序需求文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 产品定位
|
||||
解班客微信小程序是面向C端用户的核心产品,专注于提供结伴旅行和动物认领服务。通过微信生态的便利性,为用户提供便捷的社交旅行体验和创新的动物认领服务。
|
||||
结伴客微信小程序是面向C端用户的核心产品,专注于提供结伴旅行和动物认领服务。通过微信生态的便利性,为用户提供便捷的社交旅行体验和创新的动物认领服务。
|
||||
|
||||
### 1.2 目标用户
|
||||
- **主要用户群体**:18-35岁的年轻用户
|
||||
|
||||
562
docs/数据库设计文档.md
562
docs/数据库设计文档.md
@@ -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
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 解班客项目系统架构文档
|
||||
# 结伴客项目系统架构文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 项目简介
|
||||
解班客是一个综合性的社交旅行平台,融合了结伴旅行和动物认领两大核心功能。项目采用现代化的微服务架构,包含微信小程序、管理后台、官方网站和后端服务四个主要模块。
|
||||
结伴客是一个综合性的社交旅行平台,融合了结伴旅行和动物认领两大核心功能。项目采用现代化的微服务架构,包含微信小程序、管理后台、官方网站和后端服务四个主要模块。
|
||||
|
||||
### 1.2 业务架构
|
||||
```mermaid
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 解班客项目需求文档
|
||||
# 结伴客项目需求文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 项目背景
|
||||
解班客是一个创新的社交旅行平台,专注于为用户提供结伴旅行服务,并融入了独特的动物认领功能。该项目旨在通过结合传统的结伴旅行功能与现代的动物认领体验,为用户创造独特的旅行记忆。
|
||||
结伴客是一个创新的社交旅行平台,专注于为用户提供结伴旅行服务,并融入了独特的动物认领功能。该项目旨在通过结合传统的结伴旅行功能与现代的动物认领体验,为用户创造独特的旅行记忆。
|
||||
|
||||
### 1.2 项目目标
|
||||
- 构建一个完整的社交旅行生态系统
|
||||
|
||||
20
docs/测试文档.md
20
docs/测试文档.md
@@ -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. 培训团队成员测试最佳实践
|
||||
|
||||
通过完善的测试体系,确保解班客项目的高质量交付和稳定运行。
|
||||
通过完善的测试体系,确保结伴客项目的高质量交付和稳定运行。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
**维护团队**:解班客技术团队
|
||||
**维护团队**:结伴客技术团队
|
||||
1955
docs/管理后台接口设计文档.md
1955
docs/管理后台接口设计文档.md
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
- 自动化测试
|
||||
- 开发工具链
|
||||
|
||||
通过以上架构设计,解班客管理后台将具备高性能、高可用、易维护的特点,为运营团队提供强大的管理工具,支撑业务的快速发展。
|
||||
通过以上架构设计,结伴客管理后台将具备高性能、高可用、易维护的特点,为运营团队提供强大的管理工具,支撑业务的快速发展。
|
||||
@@ -1,4 +1,4 @@
|
||||
# 解班客项目部署文档
|
||||
# 结伴客项目部署文档
|
||||
|
||||
## 1. 部署概述
|
||||
|
||||
@@ -751,7 +751,7 @@ docker logs -f redis-master
|
||||
|
||||
## 12. 总结
|
||||
|
||||
本部署文档涵盖了解班客项目的完整部署流程,包括:
|
||||
本部署文档涵盖了结伴客项目的完整部署流程,包括:
|
||||
|
||||
- **基础环境**:服务器配置、软件安装
|
||||
- **数据库部署**:MySQL主从、Redis集群
|
||||
|
||||
Reference in New Issue
Block a user