Compare commits
10 Commits
10aca89915
...
44aee19acc
| Author | SHA1 | Date | |
|---|---|---|---|
| 44aee19acc | |||
| 98f81840f2 | |||
|
|
6876683d80 | ||
|
|
47c816270d | ||
|
|
5fc1a4fcb9 | ||
|
|
14aca938de | ||
| 467a4ead10 | |||
| 68a96b7e82 | |||
| 62d122cff6 | |||
| 4352f613fc |
35
.codebuddy/.rules/my-rule.mdc
Normal file
35
.codebuddy/.rules/my-rule.mdc
Normal file
@@ -0,0 +1,35 @@
|
||||
# my-rule
|
||||
|
||||
这是一个规则文件,用于帮助 AI 理解您的代码库和遵循项目约定。
|
||||
1. 请保持对话语言为中文
|
||||
2. 我的系统为 macos
|
||||
3. 远程服务器为centos10 64位
|
||||
4. 项目文件夹结构为:
|
||||
- docs 文档目录
|
||||
- admin-system 管理后台目录
|
||||
- mini-program 小程序app目录
|
||||
- backend 后端服务目录
|
||||
- website 官网目录
|
||||
- scripts 脚本目录 放置一些脚本,如:
|
||||
- 数据库脚本
|
||||
- 部署脚本
|
||||
- 测试脚本
|
||||
- 运维脚本
|
||||
5. 整个项目入口文档为根目录下的readme.md,其他文档请放在docs目录下
|
||||
6. 请使用markdown格式编写文档,整个项目文档包括:
|
||||
- 需求文档:整个项目需求文档.md 官网需求文档.md 后端管理需求文档.md 管理后台需求文档.md 小程序app需求文档.md
|
||||
- 架构文档:整个项目的架构文档.md 后端架构文档.md 小程序架构文档.md 管理后台架构文档.md
|
||||
- 详细设计文档:
|
||||
- 数据库设计文档.md
|
||||
- 管理后台接口设计文档.md
|
||||
- 小程序app接口设计文档.md
|
||||
- 开发文档:
|
||||
- 后端开发文档.md 包含:细分到每个子任务的开发计划
|
||||
- 小程序app开发文档.md 包含:细分到每个子任务的开发计划
|
||||
- 管理后台开发文档.md 包含:细分到每个子任务的开发计划
|
||||
- 后端管理开发文档.md 包含:细分到每个子任务的开发计划
|
||||
- 测试文档.md
|
||||
- 部署文档.md
|
||||
- 运维文档.md
|
||||
- 安全文档.md
|
||||
- 用户手册文档.md
|
||||
97
CHANGELOG.md
Normal file
97
CHANGELOG.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# 变更日志
|
||||
|
||||
本文档记录了结伴客项目的所有重要变更。
|
||||
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [未发布]
|
||||
|
||||
### 新增
|
||||
- 完善项目文档体系
|
||||
- 创建中文版核心文档
|
||||
- 优化文档结构和索引
|
||||
|
||||
### 变更
|
||||
- 更新README.md文档索引
|
||||
- 统一文档命名规范
|
||||
|
||||
### 移除
|
||||
- 删除冗余的英文文档
|
||||
|
||||
## [1.0.0] - 2024-01-15
|
||||
|
||||
### 新增
|
||||
- 🎉 项目初始版本发布
|
||||
- 后端API服务 (Node.js + Express)
|
||||
- 后台管理系统 (Vue 3 + Element Plus)
|
||||
- 官方网站 (Vue 3)
|
||||
- 微信小程序矩阵
|
||||
- 用户管理模块
|
||||
- 旅行结伴功能
|
||||
- 动物认领功能
|
||||
- 商品订单系统
|
||||
- 活动管理功能
|
||||
|
||||
### 技术特性
|
||||
- RESTful API 设计
|
||||
- JWT 身份认证
|
||||
- MySQL 数据库
|
||||
- Redis 缓存
|
||||
- Docker 容器化部署
|
||||
- 自动化部署脚本
|
||||
|
||||
### 安全特性
|
||||
- 用户权限管理
|
||||
- 数据加密存储
|
||||
- API 访问限制
|
||||
- 安全审计日志
|
||||
|
||||
## [0.9.0] - 2023-12-20
|
||||
|
||||
### 新增
|
||||
- 项目架构设计
|
||||
- 数据库表结构设计
|
||||
- API 接口规范定义
|
||||
- 开发环境搭建
|
||||
|
||||
### 变更
|
||||
- 确定技术栈选型
|
||||
- 制定开发规范
|
||||
- 设计系统架构
|
||||
|
||||
## [0.1.0] - 2023-11-01
|
||||
|
||||
### 新增
|
||||
- 项目立项
|
||||
- 需求分析
|
||||
- 技术调研
|
||||
- 团队组建
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
### 版本号格式
|
||||
采用语义化版本号:`主版本号.次版本号.修订号`
|
||||
|
||||
- **主版本号**:不兼容的API修改
|
||||
- **次版本号**:向下兼容的功能性新增
|
||||
- **修订号**:向下兼容的问题修正
|
||||
|
||||
### 变更类型
|
||||
- **新增** - 新功能
|
||||
- **变更** - 对现有功能的变更
|
||||
- **弃用** - 即将移除的功能
|
||||
- **移除** - 已移除的功能
|
||||
- **修复** - 问题修复
|
||||
- **安全** - 安全相关的修复
|
||||
|
||||
### 发布周期
|
||||
- **主版本**:每年1-2次重大更新
|
||||
- **次版本**:每月1-2次功能更新
|
||||
- **修订版本**:根据需要随时发布
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2024年1月*
|
||||
110
LICENSE.md
Normal file
110
LICENSE.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# MIT License
|
||||
|
||||
Copyright (c) 2024 结伴客项目团队
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
# MIT 许可证
|
||||
|
||||
版权所有 (c) 2024 结伴客项目团队
|
||||
|
||||
特此免费授予任何获得本软件副本和相关文档文件(下称"软件")的人不受限制地处置该软件的权利,包括不受限制地使用、复制、修改、合并、发布、分发、转授许可和/或出售该软件副本,以及再授权被配发了本软件的人如上的权利,须在下列条件下:
|
||||
|
||||
上述版权声明和本许可声明应包含在该软件的所有副本或实质成分中。
|
||||
|
||||
本软件是"如此"提供的,没有任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和不侵权的保证。在任何情况下,作者或版权持有人都不对任何索赔、损害或其他责任负责,无论这些追责来自合同、侵权或其它行为中,还是产生于、源于或有关于本软件以及本软件的使用或其它处置。
|
||||
|
||||
## 许可证说明
|
||||
|
||||
### 您可以:
|
||||
- ✅ **商业使用** - 可以将软件用于商业目的
|
||||
- ✅ **修改** - 可以修改软件源代码
|
||||
- ✅ **分发** - 可以分发原始或修改后的软件
|
||||
- ✅ **私人使用** - 可以私人使用软件
|
||||
- ✅ **专利使用** - 许可证提供专利权的明确授权
|
||||
|
||||
### 您必须:
|
||||
- ⚠️ **包含许可证** - 在软件副本中包含许可证和版权声明
|
||||
- ⚠️ **包含版权声明** - 在软件副本中包含原始版权声明
|
||||
|
||||
### 您不能:
|
||||
- ❌ **追究责任** - 软件作者不承担任何责任
|
||||
- ❌ **使用商标** - 许可证不授予使用贡献者姓名、商标或标志的权利
|
||||
|
||||
## 第三方许可证
|
||||
|
||||
本项目使用了以下开源软件,它们各自遵循相应的许可证:
|
||||
|
||||
### 后端依赖
|
||||
- **Express.js** - MIT License
|
||||
- **Sequelize** - MIT License
|
||||
- **jsonwebtoken** - MIT License
|
||||
- **bcryptjs** - MIT License
|
||||
- **cors** - MIT License
|
||||
- **dotenv** - BSD-2-Clause License
|
||||
- **mysql2** - MIT License
|
||||
- **redis** - BSD-3-Clause License
|
||||
|
||||
### 前端依赖
|
||||
- **Vue.js** - MIT License
|
||||
- **Element Plus** - MIT License
|
||||
- **Vue Router** - MIT License
|
||||
- **Pinia** - MIT License
|
||||
- **Axios** - MIT License
|
||||
- **Vite** - MIT License
|
||||
- **TypeScript** - Apache-2.0 License
|
||||
|
||||
### 开发工具
|
||||
- **ESLint** - MIT License
|
||||
- **Prettier** - MIT License
|
||||
- **Jest** - MIT License
|
||||
- **Cypress** - MIT License
|
||||
- **Nodemon** - MIT License
|
||||
|
||||
## 贡献者协议
|
||||
|
||||
通过向本项目提交代码,您同意:
|
||||
|
||||
1. **授权许可** - 您的贡献将在MIT许可证下发布
|
||||
2. **原创性** - 您确认贡献是您的原创作品
|
||||
3. **权利声明** - 您有权在MIT许可证下授权您的贡献
|
||||
4. **免责声明** - 您的贡献按"现状"提供,不提供任何保证
|
||||
|
||||
## 商标和品牌
|
||||
|
||||
"结伴客"名称和标志是结伴客项目团队的商标。MIT许可证不授予使用这些商标的权利。如需使用项目商标,请联系项目维护者获得明确许可。
|
||||
|
||||
## 联系信息
|
||||
|
||||
如对许可证有任何疑问,请联系:
|
||||
|
||||
- **项目维护者**: dev@jiebanke.com
|
||||
- **法务咨询**: legal@jiebanke.com
|
||||
- **GitHub**: https://github.com/jiebanke/jiebanke
|
||||
|
||||
## 许可证历史
|
||||
|
||||
- **v1.0** (2024-01-15) - 初始MIT许可证发布
|
||||
- **v1.1** (2024-01-20) - 添加中文翻译和第三方许可证说明
|
||||
|
||||
---
|
||||
|
||||
*本许可证文档最后更新于 2024年1月*
|
||||
327
README.md
327
README.md
@@ -1,207 +1,214 @@
|
||||
# 🏗️ 结伴客项目
|
||||
# 结伴客 - 宠物认领平台
|
||||
|
||||
## 📋 项目概述
|
||||
结伴客是一个综合性的管理系统,包含后台管理、微信小程序和官方网站三个主要模块。
|
||||
一个基于Vue.js和Node.js的宠物认领平台,帮助流浪动物找到温暖的家。
|
||||
|
||||
## 项目概述
|
||||
|
||||
结伴客是一个专业的宠物认领平台,致力于为流浪动物提供一个温暖的归宿。平台通过现代化的Web技术,为用户提供便捷的宠物发布、搜索、认领服务,同时为管理员提供完善的后台管理功能。
|
||||
|
||||
### 核心功能
|
||||
|
||||
- **用户系统**: 完整的用户注册、登录、个人信息管理
|
||||
- **动物管理**: 动物信息发布、编辑、状态管理
|
||||
- **认领流程**: 在线认领申请、审核、跟踪
|
||||
- **地图定位**: 基于地理位置的动物搜索和展示
|
||||
- **管理后台**: 用户管理、动物管理、数据统计、文件管理
|
||||
- **消息通知**: 实时消息推送和邮件通知
|
||||
- **数据统计**: 详细的业务数据分析和报表
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 前端技术栈
|
||||
- **框架**: Vue.js 3.x + Composition API
|
||||
- **UI组件**: Element Plus
|
||||
- **状态管理**: Pinia
|
||||
- **路由**: Vue Router 4
|
||||
- **构建工具**: Vite
|
||||
- **HTTP客户端**: Axios
|
||||
- **样式**: SCSS + CSS Modules
|
||||
|
||||
### 后端技术栈
|
||||
- **运行时**: Node.js 18+
|
||||
- **框架**: Express.js
|
||||
- **数据库**: MySQL 8.0
|
||||
- **缓存**: Redis 6.0
|
||||
- **认证**: JWT + Passport
|
||||
- **文件处理**: Multer + Sharp
|
||||
- **日志**: Winston
|
||||
- **测试**: Jest + Supertest
|
||||
|
||||
### 基础设施
|
||||
- **容器化**: Docker + Docker Compose
|
||||
- **反向代理**: Nginx
|
||||
- **进程管理**: PM2
|
||||
- **监控**: Prometheus + Grafana
|
||||
- **日志收集**: ELK Stack
|
||||
- **CI/CD**: GitHub Actions
|
||||
|
||||
## 🗂️ 项目结构
|
||||
|
||||
```
|
||||
jiebanke/
|
||||
├── 📁 backend/ # 后端服务 (Node.js + Express)
|
||||
├── 📁 admin-system/ # 后台管理系统 (Vue 3 + Element Plus)
|
||||
├── 📁 website/ # 官方网站 (Vue 3)
|
||||
├── 📁 mini-program/ # 微信小程序矩阵 (原生小程序)
|
||||
├── 📁 docs/ # 项目文档
|
||||
├── 📁 scripts/ # 工具脚本
|
||||
├── 📁 test/ # 测试文件目录
|
||||
└── 📄 README.md # 项目说明 (当前文件)
|
||||
├── README.md # 项目入口文档
|
||||
├── docs/ # 项目文档目录
|
||||
│ ├── 整个项目需求文档.md # 项目整体需求
|
||||
│ ├── 官网需求文档.md # 官网需求
|
||||
│ ├── 后端管理需求文档.md # 后端管理需求
|
||||
│ ├── 管理后台需求文档.md # 管理后台需求
|
||||
│ ├── 小程序app需求文档.md # 小程序需求
|
||||
│ ├── 整个项目的架构文档.md # 项目整体架构
|
||||
│ ├── 后端架构文档.md # 后端架构
|
||||
│ ├── 小程序架构文档.md # 小程序架构
|
||||
│ ├── 管理后台架构文档.md # 管理后台架构
|
||||
│ ├── 数据库设计文档.md # 数据库设计
|
||||
│ ├── 管理后台接口设计文档.md # 管理后台接口
|
||||
│ ├── 小程序app接口设计文档.md # 小程序接口
|
||||
│ ├── 后端开发文档.md # 后端开发指南
|
||||
│ ├── 小程序app开发文档.md # 小程序开发指南
|
||||
│ ├── 管理后台开发文档.md # 管理后台开发指南
|
||||
│ ├── 后端管理开发文档.md # 后端管理开发指南
|
||||
│ ├── 测试文档.md # 测试策略和规范
|
||||
│ ├── 部署文档.md # 部署指南
|
||||
│ ├── 运维文档.md # 运维手册
|
||||
│ ├── 安全文档.md # 安全规范
|
||||
│ └── 用户手册文档.md # 用户操作手册
|
||||
├── admin-system/ # 管理后台目录
|
||||
├── mini-program/ # 小程序app目录
|
||||
├── backend/ # 后端服务目录
|
||||
├── website/ # 官网目录
|
||||
└── scripts/ # 脚本目录
|
||||
├── database/ # 数据库脚本
|
||||
├── deploy/ # 部署脚本
|
||||
├── test/ # 测试脚本
|
||||
└── ops/ # 运维脚本
|
||||
```
|
||||
|
||||
## 📚 文档导航
|
||||
|
||||
### 需求文档
|
||||
- [整个项目需求文档](./docs/整个项目需求文档.md) - 项目整体需求和功能规划
|
||||
- [官网需求文档](./docs/官网需求文档.md) - 官网功能需求
|
||||
- [后端管理需求文档](./docs/后端管理需求文档.md) - 后端管理系统需求
|
||||
- [管理后台需求文档](./docs/管理后台需求文档.md) - 管理后台功能需求
|
||||
- [小程序app需求文档](./docs/小程序app需求文档.md) - 小程序功能需求
|
||||
|
||||
### 架构文档
|
||||
- [整个项目的架构文档](./docs/整个项目的架构文档.md) - 项目整体架构设计
|
||||
- [后端架构文档](./docs/后端架构文档.md) - 后端服务架构
|
||||
- [小程序架构文档](./docs/小程序架构文档.md) - 小程序架构设计
|
||||
- [管理后台架构文档](./docs/管理后台架构文档.md) - 管理后台架构
|
||||
|
||||
### 详细设计文档
|
||||
- [数据库设计文档](./docs/数据库设计文档.md) - 数据库表结构和关系设计
|
||||
- [管理后台接口设计文档](./docs/管理后台接口设计文档.md) - 管理后台API接口
|
||||
- [小程序app接口设计文档](./docs/小程序app接口设计文档.md) - 小程序API接口
|
||||
|
||||
### 开发文档
|
||||
- [后端开发文档](./docs/后端开发文档.md) - 后端开发指南和任务计划
|
||||
- [小程序app开发文档](./docs/小程序app开发文档.md) - 小程序开发指南和任务计划
|
||||
- [管理后台开发文档](./docs/管理后台开发文档.md) - 管理后台开发指南和任务计划
|
||||
- [后端管理开发文档](./docs/后端管理开发文档.md) - 后端管理开发指南和任务计划
|
||||
|
||||
### 运维文档
|
||||
- [测试文档](./docs/测试文档.md) - 测试策略、用例和自动化测试
|
||||
- [部署文档](./docs/部署文档.md) - 环境部署和配置指南
|
||||
- [运维文档](./docs/运维文档.md) - 系统监控、维护和故障处理
|
||||
- [安全文档](./docs/安全文档.md) - 安全策略和防护措施
|
||||
- [用户手册文档](./docs/用户手册文档.md) - 用户操作指南
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
- Node.js 16.x+
|
||||
- Node.js 18+
|
||||
- MySQL 8.0+
|
||||
- npm 8.x+
|
||||
- Redis 6.0+
|
||||
- Docker & Docker Compose (可选)
|
||||
|
||||
### 安装依赖
|
||||
### 本地开发
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
# 安装后端依赖
|
||||
cd backend && npm install
|
||||
|
||||
# 安装后台管理依赖
|
||||
cd admin-system && npm install
|
||||
|
||||
# 安装官网依赖
|
||||
cd website && npm install
|
||||
|
||||
# 安装小程序依赖
|
||||
cd mini-program && npm install
|
||||
git clone https://github.com/your-org/jiebanke.git
|
||||
cd jiebanke
|
||||
```
|
||||
|
||||
### 启动开发环境
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
# 启动后端服务
|
||||
cd backend && npm run dev
|
||||
# 后端服务
|
||||
cd backend
|
||||
npm install
|
||||
|
||||
# 启动后台管理 (新终端)
|
||||
cd admin-system && npm run dev
|
||||
# 管理后台
|
||||
cd ../admin-system
|
||||
npm install
|
||||
|
||||
# 启动官方网站 (新终端)
|
||||
cd website && npm run dev
|
||||
# 小程序
|
||||
cd ../mini-program
|
||||
npm install
|
||||
```
|
||||
|
||||
## 📖 项目文档
|
||||
|
||||
所有详细文档位于 `docs/` 目录:
|
||||
|
||||
- 📄 [架构设计](docs/architecture.md) - 系统架构和技术栈
|
||||
- 📄 [API文档](docs/API_DOCS.md) - 完整的API接口说明
|
||||
- 📄 [部署指南](docs/DEPLOYMENT.md) - 开发、测试、生产环境部署
|
||||
- 📄 [数据库设计](docs/database-design.md) - 数据库表结构和关系
|
||||
- 📄 [需求文档](docs/requirements.md) - 业务需求和功能说明
|
||||
- 📄 [详细设计](docs/detailed_design.md) - 系统详细设计方案
|
||||
- 📄 [开发计划](docs/development_plan.md) - 项目开发进度计划
|
||||
|
||||
## 🛠️ 开发工具
|
||||
|
||||
### 脚本工具
|
||||
项目提供了一些有用的开发脚本:
|
||||
|
||||
3. **配置环境**
|
||||
```bash
|
||||
# 数据库连接测试
|
||||
cd backend && npm run test-db
|
||||
|
||||
# API接口测试
|
||||
cd backend && npm run test-api
|
||||
|
||||
# 数据库初始化
|
||||
cd backend && npm run db:reset
|
||||
|
||||
# 部署脚本 (Linux/Mac)
|
||||
cd scripts && ./deploy.sh all
|
||||
|
||||
# 部署脚本 (Windows PowerShell)
|
||||
cd scripts && .\deploy.ps1 all
|
||||
```
|
||||
|
||||
### 环境配置
|
||||
复制环境变量模板并配置:
|
||||
|
||||
```bash
|
||||
# 后端环境配置
|
||||
# 复制环境配置文件
|
||||
cp backend/.env.example backend/.env
|
||||
|
||||
# 后台管理环境配置
|
||||
cp admin-system/.env.example admin-system/.env
|
||||
```
|
||||
|
||||
## ☁️ 部署
|
||||
|
||||
项目支持多种部署方式:
|
||||
|
||||
### 自动部署脚本
|
||||
在 `scripts/` 目录中提供了自动部署脚本,支持 Linux/Mac 和 Windows 系统:
|
||||
|
||||
4. **启动服务**
|
||||
```bash
|
||||
# Linux/Mac 部署所有模块
|
||||
cd scripts && chmod +x deploy.sh && ./deploy.sh all
|
||||
# 启动后端服务
|
||||
cd backend
|
||||
npm run dev
|
||||
|
||||
# Windows PowerShell 部署所有模块
|
||||
cd scripts && .\deploy.ps1 all
|
||||
# 启动管理后台
|
||||
cd admin-system
|
||||
npm run dev
|
||||
|
||||
# 启动小程序开发工具
|
||||
cd mini-program
|
||||
npm run dev
|
||||
```
|
||||
|
||||
支持的部署选项:
|
||||
- `all` - 部署所有模块
|
||||
- `backend` - 部署后端服务
|
||||
- `admin` - 部署后台管理系统
|
||||
- `website` - 部署官方网站
|
||||
- `mini-program` - 构建微信小程序
|
||||
|
||||
### Docker 容器化部署
|
||||
每个模块都提供了 Docker 配置文件,可以使用 docker-compose 进行部署:
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
# 启动所有服务
|
||||
# 构建并启动所有服务
|
||||
docker-compose up -d
|
||||
|
||||
# 启动指定服务
|
||||
docker-compose up -d backend
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### 手动部署
|
||||
每个模块也可以手动部署到服务器,具体说明请参考各模块目录中的 DEPLOYMENT.md 文件。
|
||||
## 📋 开发规范
|
||||
|
||||
## 🌐 访问地址
|
||||
- **代码规范**: 遵循 ESLint + Prettier 配置
|
||||
- **提交规范**: 使用 Conventional Commits 格式
|
||||
- **分支策略**: Git Flow 工作流
|
||||
- **测试要求**: 单元测试覆盖率 ≥ 80%
|
||||
- **文档更新**: 代码变更需同步更新文档
|
||||
|
||||
- **后端API**: https://webapi.jiebanke.com
|
||||
- **后台管理**: https://admin.jiebanke.com
|
||||
- **官方网站**: https://www.jiebanke.com
|
||||
- **小程序**: 使用微信开发者工具打开 `mini-program/` 目录
|
||||
## 🤝 贡献指南
|
||||
|
||||
## 📦 依赖管理
|
||||
1. Fork 项目
|
||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
|
||||
### 主要技术栈
|
||||
## 📄 许可证
|
||||
|
||||
**后端**:
|
||||
- Node.js + Express.js
|
||||
- Sequelize ORM
|
||||
- JWT 认证
|
||||
- MySQL 数据库
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
|
||||
|
||||
**前端**:
|
||||
- Vue 3 + TypeScript
|
||||
- Element Plus UI
|
||||
- Vite 构建工具
|
||||
- Pinia 状态管理
|
||||
## 📞 联系我们
|
||||
|
||||
**小程序**:
|
||||
- 微信原生小程序
|
||||
- Vant Weapp UI
|
||||
- Uni-app 框架
|
||||
|
||||
## 🔧 开发规范
|
||||
|
||||
### 代码风格
|
||||
- 使用 ESLint + Prettier 统一代码风格
|
||||
- 遵循 Git Commit 消息规范
|
||||
- 实行代码审查流程
|
||||
|
||||
### 分支策略
|
||||
- 采用 Git Flow 工作流
|
||||
- 功能分支开发
|
||||
- 发布分支管理
|
||||
|
||||
## 🚀 部署说明
|
||||
|
||||
详细部署指南请参考 [DEPLOYMENT.md](docs/DEPLOYMENT.md),包含:
|
||||
|
||||
- 开发环境部署
|
||||
- 测试环境部署
|
||||
- 生产环境部署
|
||||
- 容器化部署 (Docker)
|
||||
- 安全配置指南
|
||||
|
||||
## 📞 支持与维护
|
||||
|
||||
### 开发团队
|
||||
- 后端开发: backend@jiebanke.com
|
||||
- 前端开发: frontend@jiebanke.com
|
||||
- 小程序开发: miniprogram@jiebanke.com
|
||||
|
||||
### 运维支持
|
||||
- 运维团队: ops@jiebanke.com
|
||||
- 紧急联系: +86-138-0013-8000
|
||||
|
||||
## 📊 版本信息
|
||||
|
||||
- **当前版本**: v1.0.0
|
||||
- **Node.js**: 16.20.2
|
||||
- **Vue**: 3.3.4
|
||||
- **MySQL**: 8.0.33
|
||||
- 项目维护者: [团队名称]
|
||||
- 邮箱: contact@jiebanke.com
|
||||
- 项目地址: https://github.com/your-org/jiebanke
|
||||
- 文档地址: https://docs.jiebanke.com
|
||||
|
||||
---
|
||||
*最后更新: 2024年* 📅
|
||||
|
||||
**注意**: 详细的开发、部署和使用说明请参考 `docs/` 目录下的相关文档。
|
||||
@@ -2,8 +2,8 @@
|
||||
VITE_APP_NAME=结伴客后台管理系统
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# API配置
|
||||
VITE_API_BASE_URL=https://webapi.jiebanke.com/api
|
||||
# API配置 - 修改为本地测试地址
|
||||
VITE_API_BASE_URL=http://localhost:3200/api/v1
|
||||
VITE_API_TIMEOUT=10000
|
||||
|
||||
# 功能开关
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# 开发环境配置
|
||||
NODE_ENV=development
|
||||
|
||||
# API配置
|
||||
VITE_API_BASE_URL=https://webapi.jiebanke.com/api/v1
|
||||
# API配置 - 修改为本地测试地址
|
||||
VITE_API_BASE_URL=http://localhost:3200/api/v1
|
||||
VITE_API_TIMEOUT=30000
|
||||
|
||||
# 使用模拟数据(开发环境)
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 功能开关
|
||||
VITE_FEATURE_ANALYTICS=true
|
||||
VITE_FEATURE_DEBUG=true
|
||||
|
||||
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
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { mockAPI } from './mockData'
|
||||
import { createMockWrapper } from '@/config/mock'
|
||||
|
||||
// API基础配置
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3100/api'
|
||||
// API基础配置 - 修改为本地测试地址
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3200/api/v1'
|
||||
const timeout = parseInt(import.meta.env.VITE_API_TIMEOUT || '10000')
|
||||
|
||||
// 检查是否使用模拟数据(注释掉未使用的变量)
|
||||
@@ -42,6 +42,23 @@ api.interceptors.request.use(
|
||||
}
|
||||
)
|
||||
|
||||
// 用于防止重复刷新token的标志
|
||||
let isRefreshing = false
|
||||
let failedQueue: Array<{ resolve: Function; reject: Function }> = []
|
||||
|
||||
// 处理队列中的请求
|
||||
const processQueue = (error: any, token: string | null = null) => {
|
||||
failedQueue.forEach(({ resolve, reject }) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(token)
|
||||
}
|
||||
})
|
||||
|
||||
failedQueue = []
|
||||
}
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
@@ -62,19 +79,71 @@ api.interceptors.response.use(
|
||||
return Promise.reject(new Error(errorMsg))
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
async (error) => {
|
||||
// 处理错误响应
|
||||
console.error('❌ API错误:', error)
|
||||
|
||||
const originalRequest = error.config
|
||||
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
// 未授权,跳转到登录页
|
||||
message.error('登录已过期,请重新登录')
|
||||
localStorage.removeItem('admin_token')
|
||||
window.location.href = '/login'
|
||||
// 如果是登录接口或者已经在刷新token,直接返回错误
|
||||
if (originalRequest.url?.includes('/login') || originalRequest._retry) {
|
||||
message.error('登录已过期,请重新登录')
|
||||
localStorage.removeItem('admin_token')
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 标记请求已重试
|
||||
originalRequest._retry = true
|
||||
|
||||
if (isRefreshing) {
|
||||
// 如果正在刷新token,将请求加入队列
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject })
|
||||
}).then(token => {
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`
|
||||
return api(originalRequest)
|
||||
}).catch(err => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
isRefreshing = true
|
||||
|
||||
try {
|
||||
// 尝试刷新token
|
||||
const refreshToken = localStorage.getItem('admin_refresh_token')
|
||||
const response = await authAPI.refreshToken(refreshToken)
|
||||
const newToken = response.data.token
|
||||
|
||||
// 更新localStorage中的token
|
||||
localStorage.setItem('admin_token', newToken)
|
||||
|
||||
// 更新默认请求头
|
||||
api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`
|
||||
|
||||
// 处理队列中的请求
|
||||
processQueue(null, newToken)
|
||||
|
||||
// 重试原始请求
|
||||
originalRequest.headers['Authorization'] = `Bearer ${newToken}`
|
||||
return api(originalRequest)
|
||||
} catch (refreshError) {
|
||||
// 刷新失败,清除所有token并跳转登录页
|
||||
processQueue(refreshError, null)
|
||||
message.error('登录已过期,请重新登录')
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_refresh_token')
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(refreshError)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
break
|
||||
case 403:
|
||||
message.error('权限不足,无法访问')
|
||||
@@ -143,13 +212,13 @@ export const authAPI = createMockWrapper({
|
||||
}>('/admin/profile'),
|
||||
|
||||
// 刷新token
|
||||
refreshToken: () =>
|
||||
refreshToken: (refreshToken: string) =>
|
||||
request.post<{
|
||||
success: boolean
|
||||
data: {
|
||||
token: string
|
||||
}
|
||||
}>('/auth/refresh'),
|
||||
}>('/auth/refresh', { refreshToken }),
|
||||
|
||||
// 退出登录
|
||||
logout: () =>
|
||||
@@ -164,6 +233,7 @@ export { default as animalAPI } from './animal'
|
||||
export { default as orderAPI } from './order'
|
||||
export { default as promotionAPI } from './promotion'
|
||||
export { default as systemAPI } from './system'
|
||||
export { default as dashboardAPI } from './dashboard'
|
||||
|
||||
// 重新导出特定类型以避免冲突
|
||||
export type { ApiResponse } from './user'
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
import { request } from '.'
|
||||
import { mockMerchantAPI } from './mockData'
|
||||
import { createMockWrapper } from '@/config/mock'
|
||||
|
||||
// 定义商家相关类型
|
||||
export interface Merchant {
|
||||
id: number
|
||||
business_name: string
|
||||
business_license: string
|
||||
legal_representative: string
|
||||
name: string
|
||||
business_name?: string
|
||||
business_license?: string
|
||||
legal_representative?: string
|
||||
contact_person: string
|
||||
contact_phone: string
|
||||
contact_email: string
|
||||
address: string
|
||||
business_scope: string
|
||||
contact_email?: string
|
||||
address?: string
|
||||
business_scope?: string
|
||||
type: string
|
||||
status: string
|
||||
remark: string
|
||||
remark?: string
|
||||
description?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export interface MerchantQueryParams {
|
||||
page?: number
|
||||
limit?: number
|
||||
keyword?: string
|
||||
business_name?: string
|
||||
contact_person?: string
|
||||
contact_phone?: string
|
||||
status?: string
|
||||
type?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
}
|
||||
@@ -94,7 +101,8 @@ export const disableMerchant = (id: number) =>
|
||||
export const enableMerchant = (id: number) =>
|
||||
request.put<{ success: boolean; code: number; message: string }>(`/merchants/${id}/enable`)
|
||||
|
||||
export default {
|
||||
// 使用 mock 包装器
|
||||
const merchantAPI = createMockWrapper({
|
||||
getMerchants,
|
||||
getMerchant,
|
||||
createMerchant,
|
||||
@@ -105,4 +113,6 @@ export default {
|
||||
rejectMerchant,
|
||||
disableMerchant,
|
||||
enableMerchant
|
||||
}
|
||||
}, mockMerchantAPI)
|
||||
|
||||
export default merchantAPI
|
||||
@@ -6,15 +6,119 @@ const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
// 模拟用户数据
|
||||
const mockUsers = [
|
||||
{ id: 1, username: 'admin', nickname: '系统管理员', role: 'admin', status: 'active', createdAt: '2024-01-01' },
|
||||
{ id: 2, username: 'user1', nickname: '旅行爱好者', role: 'user', status: 'active', createdAt: '2024-01-02' },
|
||||
{ id: 3, username: 'merchant1', nickname: '花店老板', role: 'merchant', status: 'active', createdAt: '2024-01-03' }
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
nickname: '系统管理员',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138001',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=admin',
|
||||
userType: 'admin',
|
||||
status: 'active',
|
||||
registerTime: '2024-01-01',
|
||||
lastLoginTime: '2024-03-15 14:30:22',
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'user1',
|
||||
nickname: '旅行爱好者',
|
||||
email: 'user1@example.com',
|
||||
phone: '13800138002',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=user1',
|
||||
userType: 'normal',
|
||||
status: 'active',
|
||||
registerTime: '2024-01-02',
|
||||
lastLoginTime: '2024-03-15 10:20:15',
|
||||
createdAt: '2024-01-02'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'merchant1',
|
||||
nickname: '花店老板',
|
||||
email: 'merchant1@example.com',
|
||||
phone: '13800138003',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=merchant1',
|
||||
userType: 'merchant',
|
||||
status: 'active',
|
||||
registerTime: '2024-01-03',
|
||||
lastLoginTime: '2024-03-15 09:45:30',
|
||||
createdAt: '2024-01-03'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: 'user2',
|
||||
nickname: '探险家',
|
||||
email: 'user2@example.com',
|
||||
phone: '13800138004',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=user2',
|
||||
userType: 'normal',
|
||||
status: 'inactive',
|
||||
registerTime: '2024-01-04',
|
||||
lastLoginTime: '2024-03-10 16:20:10',
|
||||
createdAt: '2024-01-04'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
username: 'user3',
|
||||
nickname: '动物之友',
|
||||
email: 'user3@example.com',
|
||||
phone: '13800138005',
|
||||
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=user3',
|
||||
userType: 'normal',
|
||||
status: 'banned',
|
||||
registerTime: '2024-01-05',
|
||||
lastLoginTime: '2024-03-05 12:30:45',
|
||||
createdAt: '2024-01-05'
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟商家数据
|
||||
const mockMerchants = [
|
||||
{ id: 1, name: '鲜花坊', type: 'flower', status: 'approved', contact: '13800138001', createdAt: '2024-01-05' },
|
||||
{ id: 2, name: '快乐农场', type: 'farm', status: 'approved', contact: '13800138002', createdAt: '2024-01-06' }
|
||||
{
|
||||
id: 1,
|
||||
name: '鲜花坊',
|
||||
type: 'shop',
|
||||
status: 'approved',
|
||||
contact_person: '张三',
|
||||
contact_phone: '13800138001',
|
||||
address: '北京市朝阳区花卉市场1号',
|
||||
description: '专业经营各类鲜花,提供花束定制服务',
|
||||
created_at: '2024-01-05'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '快乐农场',
|
||||
type: 'farm',
|
||||
status: 'approved',
|
||||
contact_person: '李四',
|
||||
contact_phone: '13800138002',
|
||||
address: '河北省承德市农业园区2号',
|
||||
description: '生态农场,提供动物认领和农产品销售',
|
||||
created_at: '2024-01-06'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '山水酒店',
|
||||
type: 'hotel',
|
||||
status: 'pending',
|
||||
contact_person: '王五',
|
||||
contact_phone: '13800138003',
|
||||
address: '云南省大理市洱海边1号',
|
||||
description: '精品民宿,提供旅行住宿服务',
|
||||
created_at: '2024-01-07'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '美食餐厅',
|
||||
type: 'restaurant',
|
||||
status: 'rejected',
|
||||
contact_person: '赵六',
|
||||
contact_phone: '13800138004',
|
||||
address: '四川省成都市春熙路88号',
|
||||
description: '川菜餐厅,提供地道川菜',
|
||||
created_at: '2024-01-08'
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟旅行数据
|
||||
@@ -124,7 +228,7 @@ export const mockUserAPI = {
|
||||
return createPaginatedResponse(paginatedData, page, pageSize, mockUsers.length)
|
||||
},
|
||||
|
||||
getUserById: async (id: number) => {
|
||||
getUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const user = mockUsers.find(u => u.id === id)
|
||||
if (user) {
|
||||
@@ -132,6 +236,88 @@ export const mockUserAPI = {
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
createUser: async (data: any) => {
|
||||
await delay(500)
|
||||
const newUser = {
|
||||
id: mockUsers.length + 1,
|
||||
...data,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
updatedAt: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
mockUsers.push(newUser)
|
||||
return createSuccessResponse(newUser)
|
||||
},
|
||||
|
||||
updateUser: async (id: number, data: any) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers[index] = {
|
||||
...mockUsers[index],
|
||||
...data,
|
||||
updatedAt: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
return createSuccessResponse(mockUsers[index])
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
deleteUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers.splice(index, 1)
|
||||
return createSuccessResponse(null)
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
enableUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers[index].status = 'active'
|
||||
return createSuccessResponse(mockUsers[index])
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
disableUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers[index].status = 'inactive'
|
||||
return createSuccessResponse(mockUsers[index])
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
banUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers[index].status = 'banned'
|
||||
return createSuccessResponse(mockUsers[index])
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
},
|
||||
|
||||
unbanUser: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockUsers.findIndex(u => u.id === id)
|
||||
if (index !== -1) {
|
||||
mockUsers[index].status = 'active'
|
||||
return createSuccessResponse(mockUsers[index])
|
||||
}
|
||||
message.error('用户不存在')
|
||||
throw new Error('用户不存在')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,12 +325,120 @@ export const mockUserAPI = {
|
||||
export const mockMerchantAPI = {
|
||||
getMerchants: async (params: any = {}) => {
|
||||
await delay(800)
|
||||
const { page = 1, limit = 10 } = params
|
||||
const { page = 1, limit = 10, keyword = '', status = '', type = '' } = params
|
||||
|
||||
// 根据查询参数过滤商家
|
||||
let filteredMerchants = mockMerchants
|
||||
if (keyword) {
|
||||
filteredMerchants = mockMerchants.filter(m =>
|
||||
m.name.includes(keyword) ||
|
||||
m.contact_person.includes(keyword)
|
||||
)
|
||||
}
|
||||
if (status) {
|
||||
filteredMerchants = filteredMerchants.filter(m => m.status === status)
|
||||
}
|
||||
if (type) {
|
||||
filteredMerchants = filteredMerchants.filter(m => m.type === type)
|
||||
}
|
||||
|
||||
const start = (page - 1) * limit
|
||||
const end = start + limit
|
||||
const paginatedData = mockMerchants.slice(start, end)
|
||||
const paginatedData = filteredMerchants.slice(start, end)
|
||||
|
||||
return createPaginatedResponse(paginatedData, page, limit, mockMerchants.length)
|
||||
return createPaginatedResponse(paginatedData, page, limit, filteredMerchants.length)
|
||||
},
|
||||
|
||||
getMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const merchant = mockMerchants.find(m => m.id === id)
|
||||
if (merchant) {
|
||||
return createSuccessResponse(merchant)
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
createMerchant: async (data: any) => {
|
||||
await delay(500)
|
||||
const newMerchant = {
|
||||
id: mockMerchants.length + 1,
|
||||
...data,
|
||||
created_at: new Date().toISOString().split('T')[0],
|
||||
updated_at: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
mockMerchants.push(newMerchant)
|
||||
return createSuccessResponse(newMerchant)
|
||||
},
|
||||
|
||||
updateMerchant: async (id: number, data: any) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants[index] = {
|
||||
...mockMerchants[index],
|
||||
...data,
|
||||
updated_at: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
return createSuccessResponse(mockMerchants[index])
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
deleteMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants.splice(index, 1)
|
||||
return createSuccessResponse(null)
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
approveMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants[index].status = 'approved'
|
||||
return createSuccessResponse(mockMerchants[index])
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
rejectMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants[index].status = 'rejected'
|
||||
return createSuccessResponse(mockMerchants[index])
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
disableMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants[index].status = 'disabled'
|
||||
return createSuccessResponse(mockMerchants[index])
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
},
|
||||
|
||||
enableMerchant: async (id: number) => {
|
||||
await delay(500)
|
||||
const index = mockMerchants.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
mockMerchants[index].status = 'approved'
|
||||
return createSuccessResponse(mockMerchants[index])
|
||||
}
|
||||
message.error('商家不存在')
|
||||
throw new Error('商家不存在')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export interface ApiResponse<T> {
|
||||
|
||||
// 获取订单列表
|
||||
export const getOrders = (params?: OrderQueryParams) =>
|
||||
request.get<{ success: boolean; code: number; message: string; data: { orders: Order[]; pagination: any } }>('/orders', { params })
|
||||
request.get<{ success: boolean; code: number; message: string; data: { orders: Order[]; pagination: any } }>('/orders/admin', { params })
|
||||
|
||||
// 获取订单详情
|
||||
export const getOrder = (id: number) =>
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface TravelUpdateData {
|
||||
|
||||
// 获取结伴游列表
|
||||
export const getTravels = (params?: TravelQueryParams) =>
|
||||
request.get<{ success: boolean; code: number; message: string; data: { travels: Travel[]; pagination: any } }>('/travels', { params })
|
||||
request.get<{ success: boolean; code: number; message: string; data: { travels: Travel[]; pagination: any } }>('/travel/travels', { params })
|
||||
|
||||
// 获取结伴游详情
|
||||
export const getTravel = (id: number) =>
|
||||
@@ -80,6 +80,14 @@ export const getTravelPlans = (params?: TravelQueryParams) =>
|
||||
export const closeTravelPlan = (id: number) =>
|
||||
request.put<{ success: boolean; code: number; message: string }>(`/travel-plans/${id}/close`)
|
||||
|
||||
// 发布旅行
|
||||
export const publishTravel = (id: number) =>
|
||||
request.put<{ success: boolean; code: number; message: string }>(`/travels/${id}/publish`)
|
||||
|
||||
// 归档旅行
|
||||
export const archiveTravel = (id: number) =>
|
||||
request.put<{ success: boolean; code: number; message: string }>(`/travels/${id}/archive`)
|
||||
|
||||
export default {
|
||||
getTravels,
|
||||
getTravel,
|
||||
@@ -88,5 +96,7 @@ export default {
|
||||
deleteTravel,
|
||||
updateTravelStatus,
|
||||
getTravelPlans,
|
||||
closeTravelPlan
|
||||
closeTravelPlan,
|
||||
publishTravel,
|
||||
archiveTravel
|
||||
}
|
||||
@@ -65,9 +65,9 @@ export interface ApiResponse<T = any> {
|
||||
}
|
||||
|
||||
export interface UserListResponse {
|
||||
users: User[]
|
||||
list: User[]
|
||||
pagination: {
|
||||
page: number
|
||||
current: number
|
||||
pageSize: number
|
||||
total: number
|
||||
totalPages: number
|
||||
@@ -102,15 +102,34 @@ export const batchUpdateUserStatus = (userIds: number[], status: string) =>
|
||||
export const updateUserStatus = (id: number, status: string) =>
|
||||
request.put<ApiResponse<User>>(`/users/${id}/status`, { status })
|
||||
|
||||
// 启用用户
|
||||
export const enableUser = (id: number) =>
|
||||
request.put<ApiResponse<User>>(`/users/${id}/enable`)
|
||||
|
||||
// 开发环境使用模拟数据
|
||||
// 禁用用户
|
||||
export const disableUser = (id: number) =>
|
||||
request.put<ApiResponse<User>>(`/users/${id}/disable`)
|
||||
|
||||
// 封禁用户
|
||||
export const banUser = (id: number) =>
|
||||
request.put<ApiResponse<User>>(`/users/${id}/ban`)
|
||||
|
||||
// 解封用户
|
||||
export const unbanUser = (id: number) =>
|
||||
request.put<ApiResponse<User>>(`/users/${id}/unban`)
|
||||
|
||||
// 使用 mock 包装器
|
||||
const userAPI = createMockWrapper({
|
||||
getUsers,
|
||||
getUser,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
batchUpdateUserStatus
|
||||
batchUpdateUserStatus,
|
||||
enableUser,
|
||||
disableUser,
|
||||
banUser,
|
||||
unbanUser
|
||||
}, mockUserAPI)
|
||||
|
||||
export default userAPI
|
||||
666
admin-system/src/components/AdvancedSearch.vue
Normal file
666
admin-system/src/components/AdvancedSearch.vue
Normal file
@@ -0,0 +1,666 @@
|
||||
<template>
|
||||
<div class="advanced-search">
|
||||
<a-card size="small">
|
||||
<template #title>
|
||||
<a-space>
|
||||
<SearchOutlined />
|
||||
高级搜索
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button size="small" @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
@click="toggleExpanded"
|
||||
:icon="expanded ? h(UpOutlined) : h(DownOutlined)"
|
||||
>
|
||||
{{ expanded ? '收起' : '展开' }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-form
|
||||
:model="searchForm"
|
||||
layout="inline"
|
||||
@finish="handleSearch"
|
||||
class="search-form"
|
||||
>
|
||||
<!-- 基础搜索行 -->
|
||||
<div class="search-row basic-row">
|
||||
<a-form-item label="关键词" name="keyword">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="请输入关键词"
|
||||
style="width: 200px"
|
||||
allow-clear
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option
|
||||
v-for="status in statusOptions"
|
||||
:key="status.value"
|
||||
:value="status.value"
|
||||
>
|
||||
<a-tag :color="status.color" size="small">{{ status.label }}</a-tag>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围" name="dateRange">
|
||||
<a-range-picker
|
||||
v-model:value="searchForm.dateRange"
|
||||
style="width: 240px"
|
||||
:placeholder="['开始时间', '结束时间']"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit" :loading="searching">
|
||||
<SearchOutlined />
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 高级搜索行 -->
|
||||
<div v-show="expanded" class="search-row advanced-row">
|
||||
<!-- 用户相关字段 -->
|
||||
<template v-if="searchType === 'user'">
|
||||
<a-form-item label="用户类型" name="userType">
|
||||
<a-select
|
||||
v-model:value="searchForm.userType"
|
||||
placeholder="请选择用户类型"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="normal">普通用户</a-select-option>
|
||||
<a-select-option value="vip">VIP用户</a-select-option>
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="注册来源" name="registerSource">
|
||||
<a-select
|
||||
v-model:value="searchForm.registerSource"
|
||||
placeholder="请选择注册来源"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="web">网页端</a-select-option>
|
||||
<a-select-option value="wechat">微信小程序</a-select-option>
|
||||
<a-select-option value="app">移动应用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="年龄范围" name="ageRange">
|
||||
<a-slider
|
||||
v-model:value="searchForm.ageRange"
|
||||
range
|
||||
:min="0"
|
||||
:max="100"
|
||||
style="width: 200px"
|
||||
:tooltip-formatter="(value: number) => `${value}岁`"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="地区" name="region">
|
||||
<a-cascader
|
||||
v-model:value="searchForm.region"
|
||||
:options="regionOptions"
|
||||
placeholder="请选择地区"
|
||||
style="width: 200px"
|
||||
change-on-select
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 动物相关字段 -->
|
||||
<template v-if="searchType === 'animal'">
|
||||
<a-form-item label="动物类型" name="animalType">
|
||||
<a-select
|
||||
v-model:value="searchForm.animalType"
|
||||
placeholder="请选择动物类型"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="dog">狗</a-select-option>
|
||||
<a-select-option value="cat">猫</a-select-option>
|
||||
<a-select-option value="bird">鸟</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="品种" name="breed">
|
||||
<a-input
|
||||
v-model:value="searchForm.breed"
|
||||
placeholder="请输入品种"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="年龄范围" name="animalAgeRange">
|
||||
<a-input-group compact style="width: 200px">
|
||||
<a-input-number
|
||||
v-model:value="searchForm.minAge"
|
||||
placeholder="最小年龄"
|
||||
:min="0"
|
||||
:max="30"
|
||||
style="width: 50%"
|
||||
/>
|
||||
<a-input-number
|
||||
v-model:value="searchForm.maxAge"
|
||||
placeholder="最大年龄"
|
||||
:min="0"
|
||||
:max="30"
|
||||
style="width: 50%"
|
||||
/>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="性别" name="gender">
|
||||
<a-radio-group v-model:value="searchForm.gender">
|
||||
<a-radio value="male">雄性</a-radio>
|
||||
<a-radio value="female">雌性</a-radio>
|
||||
<a-radio value="unknown">未知</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="健康状态" name="healthStatus">
|
||||
<a-select
|
||||
v-model:value="searchForm.healthStatus"
|
||||
placeholder="请选择健康状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="healthy">健康</a-select-option>
|
||||
<a-select-option value="sick">生病</a-select-option>
|
||||
<a-select-option value="injured">受伤</a-select-option>
|
||||
<a-select-option value="recovering">康复中</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 订单相关字段 -->
|
||||
<template v-if="searchType === 'order'">
|
||||
<a-form-item label="订单类型" name="orderType">
|
||||
<a-select
|
||||
v-model:value="searchForm.orderType"
|
||||
placeholder="请选择订单类型"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="adoption">认领</a-select-option>
|
||||
<a-select-option value="donation">捐赠</a-select-option>
|
||||
<a-select-option value="service">服务</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="金额范围" name="amountRange">
|
||||
<a-input-group compact style="width: 200px">
|
||||
<a-input-number
|
||||
v-model:value="searchForm.minAmount"
|
||||
placeholder="最小金额"
|
||||
:min="0"
|
||||
style="width: 50%"
|
||||
/>
|
||||
<a-input-number
|
||||
v-model:value="searchForm.maxAmount"
|
||||
placeholder="最大金额"
|
||||
:min="0"
|
||||
style="width: 50%"
|
||||
/>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="支付方式" name="paymentMethod">
|
||||
<a-select
|
||||
v-model:value="searchForm.paymentMethod"
|
||||
placeholder="请选择支付方式"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="wechat">微信支付</a-select-option>
|
||||
<a-select-option value="alipay">支付宝</a-select-option>
|
||||
<a-select-option value="bank">银行卡</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 通用高级字段 -->
|
||||
<a-form-item label="创建人" name="creator">
|
||||
<a-input
|
||||
v-model:value="searchForm.creator"
|
||||
placeholder="请输入创建人"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="排序方式" name="sortBy">
|
||||
<a-select
|
||||
v-model:value="searchForm.sortBy"
|
||||
placeholder="请选择排序方式"
|
||||
style="width: 150px"
|
||||
>
|
||||
<a-select-option value="created_at_desc">创建时间降序</a-select-option>
|
||||
<a-select-option value="created_at_asc">创建时间升序</a-select-option>
|
||||
<a-select-option value="updated_at_desc">更新时间降序</a-select-option>
|
||||
<a-select-option value="updated_at_asc">更新时间升序</a-select-option>
|
||||
<a-select-option value="name_asc">名称升序</a-select-option>
|
||||
<a-select-option value="name_desc">名称降序</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 快速筛选标签 -->
|
||||
<div class="quick-filters" v-if="quickFilters.length > 0">
|
||||
<a-divider orientation="left" orientation-margin="0">快速筛选</a-divider>
|
||||
<a-space wrap>
|
||||
<a-tag
|
||||
v-for="filter in quickFilters"
|
||||
:key="filter.key"
|
||||
:color="filter.active ? 'blue' : 'default'"
|
||||
style="cursor: pointer"
|
||||
@click="handleQuickFilter(filter)"
|
||||
>
|
||||
{{ filter.label }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 搜索历史 -->
|
||||
<div class="search-history" v-if="searchHistory.length > 0 && expanded">
|
||||
<a-divider orientation="left" orientation-margin="0">搜索历史</a-divider>
|
||||
<a-space wrap>
|
||||
<a-tag
|
||||
v-for="(history, index) in searchHistory.slice(0, 5)"
|
||||
:key="index"
|
||||
closable
|
||||
@close="removeSearchHistory(index)"
|
||||
@click="applySearchHistory(history)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
{{ history.keyword || '无关键词' }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, h } from 'vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
DownOutlined,
|
||||
UpOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
interface SearchForm {
|
||||
keyword?: string
|
||||
status?: string
|
||||
dateRange?: [string, string]
|
||||
userType?: string
|
||||
registerSource?: string
|
||||
ageRange?: [number, number]
|
||||
region?: string[]
|
||||
animalType?: string
|
||||
breed?: string
|
||||
minAge?: number
|
||||
maxAge?: number
|
||||
gender?: string
|
||||
healthStatus?: string
|
||||
orderType?: string
|
||||
minAmount?: number
|
||||
maxAmount?: number
|
||||
paymentMethod?: string
|
||||
creator?: string
|
||||
sortBy?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface StatusOption {
|
||||
value: string
|
||||
label: string
|
||||
color: string
|
||||
}
|
||||
|
||||
interface QuickFilter {
|
||||
key: string
|
||||
label: string
|
||||
active: boolean
|
||||
params: Partial<SearchForm>
|
||||
}
|
||||
|
||||
interface Props {
|
||||
searchType: 'user' | 'animal' | 'order' | 'travel'
|
||||
statusOptions?: StatusOption[]
|
||||
defaultValues?: Partial<SearchForm>
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
statusOptions: () => [
|
||||
{ value: 'active', label: '激活', color: 'green' },
|
||||
{ value: 'inactive', label: '禁用', color: 'red' },
|
||||
{ value: 'pending', label: '待审核', color: 'orange' }
|
||||
]
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'search': [params: SearchForm]
|
||||
'reset': []
|
||||
}>()
|
||||
|
||||
// 响应式数据
|
||||
const expanded = ref(false)
|
||||
const searching = ref(false)
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
keyword: '',
|
||||
status: undefined,
|
||||
dateRange: undefined,
|
||||
sortBy: 'created_at_desc',
|
||||
ageRange: [0, 100],
|
||||
...props.defaultValues
|
||||
})
|
||||
|
||||
// 地区选项(示例数据)
|
||||
const regionOptions = ref([
|
||||
{
|
||||
value: 'beijing',
|
||||
label: '北京市',
|
||||
children: [
|
||||
{ value: 'chaoyang', label: '朝阳区' },
|
||||
{ value: 'haidian', label: '海淀区' },
|
||||
{ value: 'dongcheng', label: '东城区' }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: 'shanghai',
|
||||
label: '上海市',
|
||||
children: [
|
||||
{ value: 'huangpu', label: '黄浦区' },
|
||||
{ value: 'xuhui', label: '徐汇区' },
|
||||
{ value: 'changning', label: '长宁区' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 快速筛选
|
||||
const quickFilters = ref<QuickFilter[]>([])
|
||||
|
||||
// 搜索历史
|
||||
const searchHistory = ref<SearchForm[]>([])
|
||||
|
||||
// 初始化快速筛选
|
||||
const initQuickFilters = () => {
|
||||
const baseFilters: QuickFilter[] = [
|
||||
{
|
||||
key: 'today',
|
||||
label: '今日新增',
|
||||
active: false,
|
||||
params: {
|
||||
dateRange: [
|
||||
new Date().toISOString().split('T')[0],
|
||||
new Date().toISOString().split('T')[0]
|
||||
] as [string, string]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'week',
|
||||
label: '本周新增',
|
||||
active: false,
|
||||
params: {
|
||||
dateRange: [
|
||||
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||
new Date().toISOString().split('T')[0]
|
||||
] as [string, string]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'active',
|
||||
label: '仅显示激活',
|
||||
active: false,
|
||||
params: { status: 'active' }
|
||||
}
|
||||
]
|
||||
|
||||
// 根据搜索类型添加特定筛选
|
||||
switch (props.searchType) {
|
||||
case 'user':
|
||||
baseFilters.push(
|
||||
{
|
||||
key: 'vip',
|
||||
label: 'VIP用户',
|
||||
active: false,
|
||||
params: { userType: 'vip' }
|
||||
},
|
||||
{
|
||||
key: 'wechat',
|
||||
label: '微信用户',
|
||||
active: false,
|
||||
params: { registerSource: 'wechat' }
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
case 'animal':
|
||||
baseFilters.push(
|
||||
{
|
||||
key: 'healthy',
|
||||
label: '健康动物',
|
||||
active: false,
|
||||
params: { healthStatus: 'healthy' }
|
||||
},
|
||||
{
|
||||
key: 'young',
|
||||
label: '幼年动物',
|
||||
active: false,
|
||||
params: { minAge: 0, maxAge: 2 }
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
quickFilters.value = baseFilters
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换展开/收起
|
||||
*/
|
||||
const toggleExpanded = () => {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行搜索
|
||||
*/
|
||||
const handleSearch = () => {
|
||||
searching.value = true
|
||||
|
||||
// 清理空值
|
||||
const cleanParams = Object.keys(searchForm).reduce((acc, key) => {
|
||||
const value = searchForm[key]
|
||||
if (value !== undefined && value !== null && value !== '' &&
|
||||
!(Array.isArray(value) && value.length === 0)) {
|
||||
acc[key] = value
|
||||
}
|
||||
return acc
|
||||
}, {} as SearchForm)
|
||||
|
||||
// 保存搜索历史
|
||||
saveSearchHistory(cleanParams)
|
||||
|
||||
emit('search', cleanParams)
|
||||
|
||||
setTimeout(() => {
|
||||
searching.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置搜索
|
||||
*/
|
||||
const handleReset = () => {
|
||||
Object.keys(searchForm).forEach(key => {
|
||||
if (key === 'sortBy') {
|
||||
searchForm[key] = 'created_at_desc'
|
||||
} else if (key === 'ageRange') {
|
||||
searchForm[key] = [0, 100]
|
||||
} else {
|
||||
searchForm[key] = undefined
|
||||
}
|
||||
})
|
||||
|
||||
// 重置快速筛选
|
||||
quickFilters.value.forEach(filter => {
|
||||
filter.active = false
|
||||
})
|
||||
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速筛选
|
||||
*/
|
||||
const handleQuickFilter = (filter: QuickFilter) => {
|
||||
filter.active = !filter.active
|
||||
|
||||
if (filter.active) {
|
||||
// 应用筛选参数
|
||||
Object.assign(searchForm, filter.params)
|
||||
} else {
|
||||
// 移除筛选参数
|
||||
Object.keys(filter.params).forEach(key => {
|
||||
searchForm[key] = undefined
|
||||
})
|
||||
}
|
||||
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存搜索历史
|
||||
*/
|
||||
const saveSearchHistory = (params: SearchForm) => {
|
||||
// 避免重复
|
||||
const exists = searchHistory.value.some(history =>
|
||||
JSON.stringify(history) === JSON.stringify(params)
|
||||
)
|
||||
|
||||
if (!exists) {
|
||||
searchHistory.value.unshift(params)
|
||||
// 最多保存10条历史
|
||||
if (searchHistory.value.length > 10) {
|
||||
searchHistory.value = searchHistory.value.slice(0, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用搜索历史
|
||||
*/
|
||||
const applySearchHistory = (history: SearchForm) => {
|
||||
Object.assign(searchForm, history)
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除搜索历史
|
||||
*/
|
||||
const removeSearchHistory = (index: number) => {
|
||||
searchHistory.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 初始化
|
||||
initQuickFilters()
|
||||
|
||||
// 监听搜索类型变化
|
||||
watch(() => props.searchType, () => {
|
||||
initQuickFilters()
|
||||
handleReset()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.advanced-search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.basic-row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.advanced-row {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.quick-filters,
|
||||
.search-history {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.quick-filters :deep(.ant-divider),
|
||||
.search-history :deep(.ant-divider) {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
width: auto;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.search-row {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
455
admin-system/src/components/BatchOperations.vue
Normal file
455
admin-system/src/components/BatchOperations.vue
Normal file
@@ -0,0 +1,455 @@
|
||||
<template>
|
||||
<div class="batch-operations">
|
||||
<a-card title="批量操作" size="small">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button
|
||||
size="small"
|
||||
@click="handleSelectAll"
|
||||
:disabled="!dataSource.length"
|
||||
>
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
@click="handleClearSelection"
|
||||
:disabled="!selectedItems.length"
|
||||
>
|
||||
清空选择
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="batch-info" v-if="selectedItems.length > 0">
|
||||
<a-alert
|
||||
:message="`已选择 ${selectedItems.length} 项`"
|
||||
type="info"
|
||||
show-icon
|
||||
closable
|
||||
@close="handleClearSelection"
|
||||
>
|
||||
<template #action>
|
||||
<a-space>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<template #overlay>
|
||||
<a-menu @click="handleBatchAction">
|
||||
<a-menu-item
|
||||
v-for="action in availableActions"
|
||||
:key="action.key"
|
||||
:disabled="action.disabled"
|
||||
>
|
||||
<component :is="action.icon" v-if="action.icon" />
|
||||
{{ action.label }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="primary" size="small">
|
||||
批量操作
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-alert>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 批量状态更新模态框 -->
|
||||
<a-modal
|
||||
v-model:open="statusModalVisible"
|
||||
title="批量状态更新"
|
||||
@ok="handleStatusUpdate"
|
||||
:confirm-loading="statusUpdateLoading"
|
||||
>
|
||||
<a-form :model="statusForm" layout="vertical">
|
||||
<a-form-item label="新状态" required>
|
||||
<a-select v-model:value="statusForm.status" placeholder="请选择状态">
|
||||
<a-select-option
|
||||
v-for="status in statusOptions"
|
||||
:key="status.value"
|
||||
:value="status.value"
|
||||
>
|
||||
<a-tag :color="status.color">{{ status.label }}</a-tag>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="操作原因">
|
||||
<a-textarea
|
||||
v-model:value="statusForm.reason"
|
||||
placeholder="请输入操作原因(可选)"
|
||||
:rows="3"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-alert
|
||||
:message="`将对 ${selectedItems.length} 个项目执行状态更新操作`"
|
||||
type="warning"
|
||||
show-icon
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 批量删除确认模态框 -->
|
||||
<a-modal
|
||||
v-model:open="deleteModalVisible"
|
||||
title="批量删除确认"
|
||||
@ok="handleBatchDelete"
|
||||
:confirm-loading="deleteLoading"
|
||||
ok-text="确认删除"
|
||||
ok-type="danger"
|
||||
>
|
||||
<a-alert
|
||||
message="危险操作"
|
||||
:description="`您即将删除 ${selectedItems.length} 个项目,此操作不可撤销!`"
|
||||
type="error"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<div style="margin-top: 16px;">
|
||||
<a-checkbox v-model:checked="deleteConfirm">
|
||||
我确认要执行此删除操作
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 批量导出模态框 -->
|
||||
<a-modal
|
||||
v-model:open="exportModalVisible"
|
||||
title="批量导出"
|
||||
@ok="handleBatchExport"
|
||||
:confirm-loading="exportLoading"
|
||||
>
|
||||
<a-form :model="exportForm" layout="vertical">
|
||||
<a-form-item label="导出格式" required>
|
||||
<a-radio-group v-model:value="exportForm.format">
|
||||
<a-radio value="csv">CSV格式</a-radio>
|
||||
<a-radio value="excel">Excel格式</a-radio>
|
||||
<a-radio value="json">JSON格式</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="导出字段">
|
||||
<a-checkbox-group v-model:value="exportForm.fields">
|
||||
<a-row>
|
||||
<a-col :span="8" v-for="field in exportFields" :key="field.key">
|
||||
<a-checkbox :value="field.key">{{ field.label }}</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-alert
|
||||
:message="`将导出 ${selectedItems.length} 个项目的数据`"
|
||||
type="info"
|
||||
show-icon
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import {
|
||||
DownOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
SendOutlined,
|
||||
LockOutlined,
|
||||
UnlockOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
|
||||
interface BatchItem {
|
||||
id: number | string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface BatchAction {
|
||||
key: string
|
||||
label: string
|
||||
icon?: any
|
||||
disabled?: boolean
|
||||
danger?: boolean
|
||||
}
|
||||
|
||||
interface StatusOption {
|
||||
value: string
|
||||
label: string
|
||||
color: string
|
||||
}
|
||||
|
||||
interface ExportField {
|
||||
key: string
|
||||
label: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
dataSource: BatchItem[]
|
||||
selectedItems: BatchItem[]
|
||||
operationType: 'user' | 'animal' | 'order' | 'travel'
|
||||
statusOptions?: StatusOption[]
|
||||
exportFields?: ExportField[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
statusOptions: () => [
|
||||
{ value: 'active', label: '激活', color: 'green' },
|
||||
{ value: 'inactive', label: '禁用', color: 'red' },
|
||||
{ value: 'pending', label: '待审核', color: 'orange' }
|
||||
],
|
||||
exportFields: () => [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'name', label: '名称' },
|
||||
{ key: 'status', label: '状态' },
|
||||
{ key: 'created_at', label: '创建时间' }
|
||||
]
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'selection-change': [items: BatchItem[]]
|
||||
'batch-action': [action: string, items: BatchItem[], params?: any]
|
||||
}>()
|
||||
|
||||
// 模态框状态
|
||||
const statusModalVisible = ref(false)
|
||||
const deleteModalVisible = ref(false)
|
||||
const exportModalVisible = ref(false)
|
||||
|
||||
// 加载状态
|
||||
const statusUpdateLoading = ref(false)
|
||||
const deleteLoading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const statusForm = ref({
|
||||
status: '',
|
||||
reason: ''
|
||||
})
|
||||
|
||||
const exportForm = ref({
|
||||
format: 'csv',
|
||||
fields: props.exportFields.map(f => f.key)
|
||||
})
|
||||
|
||||
const deleteConfirm = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const isAllSelected = computed(() => {
|
||||
return props.dataSource.length > 0 && props.selectedItems.length === props.dataSource.length
|
||||
})
|
||||
|
||||
const availableActions = computed((): BatchAction[] => {
|
||||
const baseActions: BatchAction[] = [
|
||||
{
|
||||
key: 'update-status',
|
||||
label: '更新状态',
|
||||
icon: EditOutlined
|
||||
},
|
||||
{
|
||||
key: 'export',
|
||||
label: '导出数据',
|
||||
icon: DownloadOutlined
|
||||
}
|
||||
]
|
||||
|
||||
// 根据操作类型添加特定操作
|
||||
switch (props.operationType) {
|
||||
case 'user':
|
||||
baseActions.push(
|
||||
{
|
||||
key: 'send-message',
|
||||
label: '发送消息',
|
||||
icon: SendOutlined
|
||||
},
|
||||
{
|
||||
key: 'lock',
|
||||
label: '锁定账户',
|
||||
icon: LockOutlined
|
||||
},
|
||||
{
|
||||
key: 'unlock',
|
||||
label: '解锁账户',
|
||||
icon: UnlockOutlined
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
case 'animal':
|
||||
baseActions.push(
|
||||
{
|
||||
key: 'batch-approve',
|
||||
label: '批量审核',
|
||||
icon: EditOutlined
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
// 危险操作
|
||||
baseActions.push({
|
||||
key: 'delete',
|
||||
label: '批量删除',
|
||||
icon: DeleteOutlined,
|
||||
danger: true
|
||||
})
|
||||
|
||||
return baseActions
|
||||
})
|
||||
|
||||
/**
|
||||
* 全选/取消全选
|
||||
*/
|
||||
const handleSelectAll = () => {
|
||||
if (isAllSelected.value) {
|
||||
emit('selection-change', [])
|
||||
} else {
|
||||
emit('selection-change', [...props.dataSource])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空选择
|
||||
*/
|
||||
const handleClearSelection = () => {
|
||||
emit('selection-change', [])
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理批量操作
|
||||
*/
|
||||
const handleBatchAction = ({ key }: { key: string }) => {
|
||||
if (!props.selectedItems.length) {
|
||||
message.warning('请先选择要操作的项目')
|
||||
return
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'update-status':
|
||||
statusModalVisible.value = true
|
||||
statusForm.value = { status: '', reason: '' }
|
||||
break
|
||||
|
||||
case 'delete':
|
||||
deleteModalVisible.value = true
|
||||
deleteConfirm.value = false
|
||||
break
|
||||
|
||||
case 'export':
|
||||
exportModalVisible.value = true
|
||||
exportForm.value.fields = props.exportFields.map(f => f.key)
|
||||
break
|
||||
|
||||
default:
|
||||
// 直接执行其他操作
|
||||
emit('batch-action', key, props.selectedItems)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理状态更新
|
||||
*/
|
||||
const handleStatusUpdate = async () => {
|
||||
if (!statusForm.value.status) {
|
||||
message.error('请选择新状态')
|
||||
return
|
||||
}
|
||||
|
||||
statusUpdateLoading.value = true
|
||||
|
||||
try {
|
||||
emit('batch-action', 'update-status', props.selectedItems, {
|
||||
status: statusForm.value.status,
|
||||
reason: statusForm.value.reason
|
||||
})
|
||||
|
||||
statusModalVisible.value = false
|
||||
message.success(`成功更新 ${props.selectedItems.length} 个项目的状态`)
|
||||
} catch (error) {
|
||||
message.error('批量状态更新失败')
|
||||
} finally {
|
||||
statusUpdateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理批量删除
|
||||
*/
|
||||
const handleBatchDelete = async () => {
|
||||
if (!deleteConfirm.value) {
|
||||
message.error('请确认删除操作')
|
||||
return
|
||||
}
|
||||
|
||||
deleteLoading.value = true
|
||||
|
||||
try {
|
||||
emit('batch-action', 'delete', props.selectedItems)
|
||||
|
||||
deleteModalVisible.value = false
|
||||
message.success(`成功删除 ${props.selectedItems.length} 个项目`)
|
||||
} catch (error) {
|
||||
message.error('批量删除失败')
|
||||
} finally {
|
||||
deleteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理批量导出
|
||||
*/
|
||||
const handleBatchExport = async () => {
|
||||
if (!exportForm.value.fields.length) {
|
||||
message.error('请选择要导出的字段')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
|
||||
try {
|
||||
emit('batch-action', 'export', props.selectedItems, {
|
||||
format: exportForm.value.format,
|
||||
fields: exportForm.value.fields
|
||||
})
|
||||
|
||||
exportModalVisible.value = false
|
||||
message.success(`开始导出 ${props.selectedItems.length} 个项目的数据`)
|
||||
} catch (error) {
|
||||
message.error('批量导出失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听选择变化
|
||||
watch(() => props.selectedItems, (newItems) => {
|
||||
// 可以在这里添加选择变化的逻辑
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.batch-operations {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.batch-info {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.batch-info :deep(.ant-alert) {
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.batch-info :deep(.ant-alert-action) {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
342
admin-system/src/components/charts/DataStatisticsChart.vue
Normal file
342
admin-system/src/components/charts/DataStatisticsChart.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div class="data-statistics-chart">
|
||||
<a-card :title="title" :loading="loading">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-select
|
||||
v-model:value="selectedPeriod"
|
||||
style="width: 120px"
|
||||
@change="handlePeriodChange"
|
||||
>
|
||||
<a-select-option value="7d">近7天</a-select-option>
|
||||
<a-select-option value="30d">近30天</a-select-option>
|
||||
<a-select-option value="90d">近90天</a-select-option>
|
||||
<a-select-option value="365d">近一年</a-select-option>
|
||||
</a-select>
|
||||
<a-button @click="handleRefresh" :loading="loading">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div ref="chartContainer" :style="{ height: chartHeight + 'px' }"></div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||
import { ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
interface ChartData {
|
||||
date: string
|
||||
value: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
chartType: 'line' | 'bar' | 'pie' | 'area'
|
||||
dataSource: string // API接口地址
|
||||
chartHeight?: number
|
||||
xAxisKey?: string
|
||||
yAxisKey?: string
|
||||
seriesConfig?: any[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
chartHeight: 300,
|
||||
xAxisKey: 'date',
|
||||
yAxisKey: 'value',
|
||||
seriesConfig: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
dataLoaded: [data: ChartData[]]
|
||||
error: [error: Error]
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const selectedPeriod = ref('30d')
|
||||
const chartContainer = ref<HTMLDivElement>()
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
const chartData = ref<ChartData[]>([])
|
||||
|
||||
/**
|
||||
* 初始化图表
|
||||
*/
|
||||
const initChart = () => {
|
||||
if (!chartContainer.value) return
|
||||
|
||||
chartInstance = echarts.init(chartContainer.value)
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图表配置
|
||||
*/
|
||||
const updateChart = () => {
|
||||
if (!chartInstance || !chartData.value.length) return
|
||||
|
||||
const option = generateChartOption()
|
||||
chartInstance.setOption(option, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图表配置
|
||||
*/
|
||||
const generateChartOption = () => {
|
||||
const baseOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: props.seriesConfig.map(s => s.name) || ['数据']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {
|
||||
title: '保存为图片'
|
||||
},
|
||||
dataZoom: {
|
||||
title: {
|
||||
zoom: '区域缩放',
|
||||
back: '区域缩放还原'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据图表类型生成不同配置
|
||||
switch (props.chartType) {
|
||||
case 'line':
|
||||
case 'area':
|
||||
return {
|
||||
...baseOption,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: chartData.value.map(item => item[props.xAxisKey])
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: props.seriesConfig.length > 0
|
||||
? props.seriesConfig.map(config => ({
|
||||
...config,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: props.chartType === 'area' ? {} : undefined,
|
||||
data: chartData.value.map(item => item[config.dataKey || props.yAxisKey])
|
||||
}))
|
||||
: [{
|
||||
name: '数据',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: props.chartType === 'area' ? {} : undefined,
|
||||
data: chartData.value.map(item => item[props.yAxisKey])
|
||||
}]
|
||||
}
|
||||
|
||||
case 'bar':
|
||||
return {
|
||||
...baseOption,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.value.map(item => item[props.xAxisKey])
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: props.seriesConfig.length > 0
|
||||
? props.seriesConfig.map(config => ({
|
||||
...config,
|
||||
type: 'bar',
|
||||
data: chartData.value.map(item => item[config.dataKey || props.yAxisKey])
|
||||
}))
|
||||
: [{
|
||||
name: '数据',
|
||||
type: 'bar',
|
||||
data: chartData.value.map(item => item[props.yAxisKey])
|
||||
}]
|
||||
}
|
||||
|
||||
case 'pie':
|
||||
return {
|
||||
...baseOption,
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
series: [{
|
||||
name: props.title,
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: chartData.value.map(item => ({
|
||||
name: item[props.xAxisKey],
|
||||
value: item[props.yAxisKey]
|
||||
})),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
default:
|
||||
return baseOption
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载图表数据
|
||||
*/
|
||||
const loadData = async () => {
|
||||
if (!props.dataSource) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 这里应该调用实际的API接口
|
||||
// const response = await fetch(`${props.dataSource}?period=${selectedPeriod.value}`)
|
||||
// const result = await response.json()
|
||||
|
||||
// 模拟数据加载
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 生成模拟数据
|
||||
const mockData = generateMockData()
|
||||
chartData.value = mockData
|
||||
|
||||
emit('dataLoaded', mockData)
|
||||
|
||||
// 更新图表
|
||||
nextTick(() => {
|
||||
updateChart()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('加载图表数据失败:', error)
|
||||
message.error('加载图表数据失败')
|
||||
emit('error', error as Error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模拟数据
|
||||
*/
|
||||
const generateMockData = (): ChartData[] => {
|
||||
const days = selectedPeriod.value === '7d' ? 7 :
|
||||
selectedPeriod.value === '30d' ? 30 :
|
||||
selectedPeriod.value === '90d' ? 90 : 365
|
||||
|
||||
const data: ChartData[] = []
|
||||
const now = new Date()
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date(now)
|
||||
date.setDate(date.getDate() - i)
|
||||
|
||||
data.push({
|
||||
date: date.toISOString().split('T')[0],
|
||||
value: Math.floor(Math.random() * 100) + 50,
|
||||
users: Math.floor(Math.random() * 50) + 20,
|
||||
orders: Math.floor(Math.random() * 30) + 10,
|
||||
revenue: Math.floor(Math.random() * 5000) + 1000
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理时间周期变化
|
||||
*/
|
||||
const handlePeriodChange = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
*/
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理窗口大小变化
|
||||
*/
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听数据变化
|
||||
*/
|
||||
watch(() => props.dataSource, () => {
|
||||
loadData()
|
||||
}, { immediate: false })
|
||||
|
||||
/**
|
||||
* 组件挂载
|
||||
*/
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart()
|
||||
loadData()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 组件卸载
|
||||
*/
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
refresh: handleRefresh,
|
||||
updateData: (data: ChartData[]) => {
|
||||
chartData.value = data
|
||||
updateChart()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-statistics-chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-statistics-chart :deep(.ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -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('登录成功!')
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -136,11 +136,10 @@ import { UserOutlined, ShopOutlined, CompassOutlined, HeartOutlined } from '@ant
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
getDashboardData,
|
||||
getUserGrowthData,
|
||||
getOrderStatsData
|
||||
} from '@/api/dashboard'
|
||||
import dashboardAPI from '@/api/dashboard'
|
||||
|
||||
// 解构API方法
|
||||
const { getDashboardData, getUserGrowthData, getOrderStatsData } = dashboardAPI
|
||||
|
||||
// 定义仪表板数据结构
|
||||
interface DashboardData {
|
||||
@@ -221,15 +220,25 @@ const loadDashboardData = async () => {
|
||||
|
||||
// 获取仪表板数据
|
||||
const response = await getDashboardData()
|
||||
if (response.success) {
|
||||
if (response && response.success) {
|
||||
dashboardData.value = response.data
|
||||
} else {
|
||||
console.warn('获取仪表板数据失败,使用默认数据')
|
||||
}
|
||||
|
||||
// 获取图表数据并更新图表
|
||||
await updateUserGrowthChart()
|
||||
await updateOrderStatsChart()
|
||||
// 延迟更新图表,确保DOM已渲染
|
||||
setTimeout(async () => {
|
||||
await updateUserGrowthChart()
|
||||
await updateOrderStatsChart()
|
||||
}, 200)
|
||||
} catch (error) {
|
||||
console.error('加载仪表板数据失败:', error)
|
||||
// 显示用户友好的错误信息
|
||||
if (error instanceof Error) {
|
||||
console.error('API错误:', error.message)
|
||||
} else {
|
||||
console.error('API错误:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,6 +285,15 @@ const updateOrderStatsChart = async () => {
|
||||
try {
|
||||
const response = await getOrderStatsData(7)
|
||||
if (response.success && orderStatsChart.value) {
|
||||
// 确保DOM元素已经渲染
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 检查DOM元素是否存在且有尺寸
|
||||
if (!orderStatsChart.value || orderStatsChart.value.offsetWidth === 0) {
|
||||
console.warn('订单统计图表DOM元素未准备好')
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化图表实例
|
||||
if (!orderStatsChartInstance) {
|
||||
orderStatsChartInstance = echarts.init(orderStatsChart.value)
|
||||
|
||||
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"
|
||||
:open="open"
|
||||
: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 {
|
||||
open: 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.success) {
|
||||
merchants.value = response.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
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 open 变化
|
||||
watch(() => props.open, (open) => {
|
||||
if (open) {
|
||||
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>
|
||||
319
admin-system/src/pages/flower/index.vue
Normal file
319
admin-system/src/pages/flower/index.vue
Normal file
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div class="flower-management">
|
||||
<a-card title="花卉管理" :bordered="false">
|
||||
<!-- 搜索和筛选区域 -->
|
||||
<div class="search-section">
|
||||
<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="请选择类型" allow-clear style="width: 120px">
|
||||
<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="状态">
|
||||
<a-select v-model:value="searchForm.status" placeholder="请选择状态" allow-clear style="width: 120px">
|
||||
<a-select-option value="active">上架</a-select-option>
|
||||
<a-select-option value="inactive">下架</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">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="action-section">
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增花卉
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleExport">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出数据
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-section">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="flowerStore.flowers"
|
||||
:loading="flowerStore.loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'image'">
|
||||
<a-image
|
||||
:width="60"
|
||||
:height="60"
|
||||
:src="record.image"
|
||||
:fallback="imageFallback"
|
||||
style="border-radius: 4px"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'price'">
|
||||
<span>¥{{ record.price }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'status'">
|
||||
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
|
||||
{{ record.status === 'active' ? '上架' : '下架' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'actions'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button size="small" @click="handleView(record)">查看</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个花卉吗?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<a-button size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 创建/编辑模态框 -->
|
||||
<FlowerModal
|
||||
:open="modalVisible"
|
||||
:current-record="currentRecord"
|
||||
:mode="modalMode"
|
||||
@cancel="handleModalCancel"
|
||||
@submit="handleModalSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useFlowerStore } from '@/stores/modules/flower'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
ExportOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import FlowerModal from './components/FlowerModal.vue'
|
||||
import type { TablePaginationConfig } from 'ant-design-vue'
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
type: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface FlowerRecord {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
variety: string
|
||||
price: number
|
||||
image: string
|
||||
status: string
|
||||
stock: number
|
||||
merchantId: number
|
||||
merchantName: string
|
||||
description: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
const flowerStore = useFlowerStore()
|
||||
const searchForm = reactive<SearchForm>({
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
const modalVisible = ref(false)
|
||||
const modalMode = ref<'create' | 'edit' | 'view'>('create')
|
||||
const currentRecord = ref<FlowerRecord | null>(null)
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
title: '图片',
|
||||
dataIndex: 'image',
|
||||
key: 'image',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '花卉名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'stock',
|
||||
key: 'stock',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
width: 200
|
||||
}
|
||||
]
|
||||
|
||||
// 分页配置
|
||||
const pagination = computed<TablePaginationConfig>(() => ({
|
||||
total: 100, // 临时硬编码,实际应该从store获取
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`,
|
||||
pageSizeOptions: ['10', '20', '50', '100']
|
||||
}))
|
||||
|
||||
const imageFallback = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiBmaWxsPSIjRjVGNUY1Ii8+CjxwYXRoIGQ9Ik0zMiAyMEM0MC44MzY2IDIwIDQ4IDI3LjE2MzQgNDggMzZDNDggNDQuODM2NiA0MC44MzY2IDUyIDMyIDUyQzIzLjE2MzQgNTIgMTYgNDQuODM2NiAxNiAzNkMxNiAyNy4xNjM0IDIzLjE2MzQgMjAgMzIgMjBaIiBmaWxsPSIjRDlEOUQ5Ii8+Cjx0ZXh0IHg9IjMyIiB5PSI0MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOTk5OTk5Ij7lm77niYc8L3RleHQ+Cjwvc3ZnPgo='
|
||||
|
||||
// 事件处理方法
|
||||
const handleSearch = () => {
|
||||
console.log('搜索花卉:', searchForm)
|
||||
// TODO: 实现搜索逻辑
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.type = ''
|
||||
searchForm.status = ''
|
||||
console.log('重置搜索条件')
|
||||
// TODO: 重新加载数据
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
modalMode.value = 'create'
|
||||
currentRecord.value = null
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record: FlowerRecord) => {
|
||||
modalMode.value = 'edit'
|
||||
currentRecord.value = record
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleView = (record: FlowerRecord) => {
|
||||
modalMode.value = 'view'
|
||||
currentRecord.value = record
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
// TODO: 调用删除API
|
||||
console.log('删除花卉:', id)
|
||||
message.success('删除成功')
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
console.log('导出花卉数据')
|
||||
// TODO: 实现导出功能
|
||||
message.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
currentRecord.value = null
|
||||
}
|
||||
|
||||
const handleModalSubmit = async (data: any) => {
|
||||
try {
|
||||
if (modalMode.value === 'create') {
|
||||
// TODO: 调用创建API
|
||||
console.log('创建花卉:', data)
|
||||
message.success('创建成功')
|
||||
} else if (modalMode.value === 'edit') {
|
||||
// TODO: 调用更新API
|
||||
console.log('更新花卉:', data)
|
||||
message.success('更新成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
currentRecord.value = null
|
||||
// TODO: 重新加载数据
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
// TODO: 加载花卉数据
|
||||
console.log('加载花卉数据')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.flower-management {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.action-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -217,7 +217,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { message, Modal, type FormInstance } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
@@ -231,7 +231,11 @@ import {
|
||||
StopOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import {
|
||||
import merchantAPI from '@/api/merchant'
|
||||
import type { Merchant, MerchantQueryParams } from '@/api/merchant'
|
||||
|
||||
// 解构API方法
|
||||
const {
|
||||
getMerchants,
|
||||
getMerchant,
|
||||
createMerchant,
|
||||
@@ -239,8 +243,7 @@ import {
|
||||
approveMerchant,
|
||||
rejectMerchant,
|
||||
disableMerchant
|
||||
} from '@/api/merchant'
|
||||
import type { Merchant, MerchantQueryParams } from '@/api/merchant'
|
||||
} = merchantAPI
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
@@ -409,8 +412,12 @@ const loadMerchants = async () => {
|
||||
}
|
||||
|
||||
const response = await getMerchants(params)
|
||||
merchantList.value = response.data.list
|
||||
pagination.total = response.data.pagination.total
|
||||
if (response && response.data) {
|
||||
merchantList.value = response.data.list || response.data
|
||||
if (response.data.pagination) {
|
||||
pagination.total = response.data.pagination.total
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载商家列表失败')
|
||||
} finally {
|
||||
@@ -447,6 +454,7 @@ const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
const handleView = async (record: Merchant) => {
|
||||
try {
|
||||
const response = await getMerchant(record.id)
|
||||
const merchantData = response.data
|
||||
Modal.info({
|
||||
title: '商家详情',
|
||||
width: 600,
|
||||
@@ -455,18 +463,18 @@ const handleView = async (record: Merchant) => {
|
||||
column: 1,
|
||||
bordered: true
|
||||
}, [
|
||||
h('a-descriptions-item', { label: '商家名称' }, response.data.name),
|
||||
h('a-descriptions-item', { label: '商家名称' }, merchantData.name),
|
||||
h('a-descriptions-item', { label: '类型' }, [
|
||||
h('a-tag', { color: getTypeColor(response.data.type) }, getTypeText(response.data.type))
|
||||
h('a-tag', { color: getTypeColor(merchantData.type) }, getTypeText(merchantData.type))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '状态' }, [
|
||||
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
|
||||
h('a-tag', { color: getStatusColor(merchantData.status) }, getStatusText(merchantData.status))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '联系人' }, response.data.contact_person),
|
||||
h('a-descriptions-item', { label: '联系电话' }, response.data.contact_phone),
|
||||
h('a-descriptions-item', { label: '地址' }, response.data.address || '-'),
|
||||
h('a-descriptions-item', { label: '描述' }, response.data.description || '-'),
|
||||
h('a-descriptions-item', { label: '创建时间' }, response.data.created_at)
|
||||
h('a-descriptions-item', { label: '联系人' }, merchantData.contact_person),
|
||||
h('a-descriptions-item', { label: '联系电话' }, merchantData.contact_phone),
|
||||
h('a-descriptions-item', { label: '地址' }, merchantData.address || '-'),
|
||||
h('a-descriptions-item', { label: '描述' }, merchantData.description || '-'),
|
||||
h('a-descriptions-item', { label: '创建时间' }, merchantData.created_at)
|
||||
])
|
||||
]),
|
||||
okText: '关闭'
|
||||
@@ -482,7 +490,7 @@ const handleEdit = async (record: Merchant) => {
|
||||
const response = await getMerchant(record.id)
|
||||
modalTitle.value = '编辑商家'
|
||||
isEditing.value = true
|
||||
currentMerchant.value = response.data
|
||||
currentMerchant.value = response.data as any
|
||||
modalVisible.value = true
|
||||
} catch (error) {
|
||||
message.error('获取商家详情失败')
|
||||
@@ -497,7 +505,7 @@ const showCreateModal = () => {
|
||||
currentMerchant.value = {
|
||||
status: 'pending',
|
||||
type: 'farm'
|
||||
}
|
||||
} as any
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
@@ -571,7 +579,7 @@ const handleReject = async (record: Merchant) => {
|
||||
content: `确定要拒绝商家 "${record.name}" 的审核吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await rejectMerchant(record.id)
|
||||
await rejectMerchant(record.id, '审核不通过')
|
||||
message.success('商家审核已拒绝')
|
||||
loadMerchants()
|
||||
} catch (error) {
|
||||
|
||||
577
admin-system/src/pages/statistics/index.vue
Normal file
577
admin-system/src/pages/statistics/index.vue
Normal file
@@ -0,0 +1,577 @@
|
||||
<template>
|
||||
<div class="statistics-page">
|
||||
<a-page-header
|
||||
title="数据统计"
|
||||
sub-title="系统数据分析与统计报表"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
@change="handleDateRangeChange"
|
||||
/>
|
||||
<a-button @click="handleExport" :loading="exportLoading">
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
导出报表
|
||||
</a-button>
|
||||
<a-button @click="handleRefreshAll" :loading="refreshLoading">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新全部
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<!-- 概览统计卡片 -->
|
||||
<a-row :gutter="16" class="overview-cards">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="总用户数"
|
||||
:value="overviewData.totalUsers"
|
||||
:precision="0"
|
||||
suffix="人"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined style="color: #1890ff" />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="statistic-trend">
|
||||
<span :class="['trend', overviewData.userGrowth >= 0 ? 'up' : 'down']">
|
||||
<CaretUpOutlined v-if="overviewData.userGrowth >= 0" />
|
||||
<CaretDownOutlined v-else />
|
||||
{{ Math.abs(overviewData.userGrowth) }}%
|
||||
</span>
|
||||
<span class="trend-text">较昨日</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="活跃用户"
|
||||
:value="overviewData.activeUsers"
|
||||
:precision="0"
|
||||
suffix="人"
|
||||
>
|
||||
<template #prefix>
|
||||
<CheckCircleOutlined style="color: #52c41a" />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="statistic-trend">
|
||||
<span :class="['trend', overviewData.activeGrowth >= 0 ? 'up' : 'down']">
|
||||
<CaretUpOutlined v-if="overviewData.activeGrowth >= 0" />
|
||||
<CaretDownOutlined v-else />
|
||||
{{ Math.abs(overviewData.activeGrowth) }}%
|
||||
</span>
|
||||
<span class="trend-text">较昨日</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="动物认领"
|
||||
:value="overviewData.totalAnimals"
|
||||
:precision="0"
|
||||
suffix="只"
|
||||
>
|
||||
<template #prefix>
|
||||
<HeartOutlined style="color: #eb2f96" />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="statistic-trend">
|
||||
<span :class="['trend', overviewData.animalGrowth >= 0 ? 'up' : 'down']">
|
||||
<CaretUpOutlined v-if="overviewData.animalGrowth >= 0" />
|
||||
<CaretDownOutlined v-else />
|
||||
{{ Math.abs(overviewData.animalGrowth) }}%
|
||||
</span>
|
||||
<span class="trend-text">较昨日</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="总收入"
|
||||
:value="overviewData.totalRevenue"
|
||||
:precision="2"
|
||||
prefix="¥"
|
||||
>
|
||||
<template #prefix>
|
||||
<DollarOutlined style="color: #faad14" />
|
||||
</template>
|
||||
</a-statistic>
|
||||
<div class="statistic-trend">
|
||||
<span :class="['trend', overviewData.revenueGrowth >= 0 ? 'up' : 'down']">
|
||||
<CaretUpOutlined v-if="overviewData.revenueGrowth >= 0" />
|
||||
<CaretDownOutlined v-else />
|
||||
{{ Math.abs(overviewData.revenueGrowth) }}%
|
||||
</span>
|
||||
<span class="trend-text">较昨日</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row :gutter="16" class="chart-section">
|
||||
<a-col :span="12">
|
||||
<DataStatisticsChart
|
||||
ref="userGrowthChart"
|
||||
title="用户增长趋势"
|
||||
chart-type="area"
|
||||
data-source="/api/v1/admin/statistics/user-growth"
|
||||
:series-config="[
|
||||
{ name: '新增用户', dataKey: 'new_users', color: '#1890ff' },
|
||||
{ name: '累计用户', dataKey: 'cumulative_users', color: '#52c41a' }
|
||||
]"
|
||||
@data-loaded="handleUserGrowthLoaded"
|
||||
/>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<DataStatisticsChart
|
||||
ref="businessChart"
|
||||
title="业务数据统计"
|
||||
chart-type="line"
|
||||
data-source="/api/v1/admin/statistics/business"
|
||||
:series-config="[
|
||||
{ name: '动物认领', dataKey: 'animals', color: '#eb2f96' },
|
||||
{ name: '旅行计划', dataKey: 'travels', color: '#722ed1' },
|
||||
{ name: '订单数量', dataKey: 'orders', color: '#fa8c16' }
|
||||
]"
|
||||
@data-loaded="handleBusinessDataLoaded"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" class="chart-section">
|
||||
<a-col :span="8">
|
||||
<DataStatisticsChart
|
||||
ref="userTypeChart"
|
||||
title="用户类型分布"
|
||||
chart-type="pie"
|
||||
data-source="/api/v1/admin/statistics/user-types"
|
||||
@data-loaded="handleUserTypeLoaded"
|
||||
/>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8">
|
||||
<DataStatisticsChart
|
||||
ref="animalSpeciesChart"
|
||||
title="动物种类分布"
|
||||
chart-type="pie"
|
||||
data-source="/api/v1/admin/statistics/animal-species"
|
||||
@data-loaded="handleAnimalSpeciesLoaded"
|
||||
/>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8">
|
||||
<DataStatisticsChart
|
||||
ref="revenueChart"
|
||||
title="收入统计"
|
||||
chart-type="bar"
|
||||
data-source="/api/v1/admin/statistics/revenue"
|
||||
:series-config="[
|
||||
{ name: '认领费用', dataKey: 'adoption_fee', color: '#1890ff' },
|
||||
{ name: '推广佣金', dataKey: 'commission', color: '#52c41a' }
|
||||
]"
|
||||
@data-loaded="handleRevenueLoaded"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 地理分布图 -->
|
||||
<a-row :gutter="16" class="chart-section">
|
||||
<a-col :span="24">
|
||||
<a-card title="用户地理分布" :loading="geoLoading">
|
||||
<template #extra>
|
||||
<a-radio-group v-model:value="geoViewType" @change="handleGeoViewChange">
|
||||
<a-radio-button value="users">用户分布</a-radio-button>
|
||||
<a-radio-button value="animals">动物分布</a-radio-button>
|
||||
<a-radio-button value="orders">订单分布</a-radio-button>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<div ref="geoChartContainer" style="height: 400px;"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-row :gutter="16" class="table-section">
|
||||
<a-col :span="12">
|
||||
<a-card title="热门动物排行" size="small">
|
||||
<a-table
|
||||
:columns="animalRankingColumns"
|
||||
:data-source="animalRankingData"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'rank'">
|
||||
<a-tag :color="index < 3 ? ['gold', 'silver', '#cd7f32'][index] : 'default'">
|
||||
{{ index + 1 }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'adoption_rate'">
|
||||
<a-progress
|
||||
:percent="record.adoption_rate"
|
||||
size="small"
|
||||
:show-info="false"
|
||||
/>
|
||||
{{ record.adoption_rate }}%
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-card title="活跃用户排行" size="small">
|
||||
<a-table
|
||||
:columns="userRankingColumns"
|
||||
:data-source="userRankingData"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'rank'">
|
||||
<a-tag :color="index < 3 ? ['gold', 'silver', '#cd7f32'][index] : 'default'">
|
||||
{{ index + 1 }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar :src="record.avatar" size="small">
|
||||
{{ record.nickname?.charAt(0) }}
|
||||
</a-avatar>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import {
|
||||
UserOutlined,
|
||||
CheckCircleOutlined,
|
||||
HeartOutlined,
|
||||
DollarOutlined,
|
||||
CaretUpOutlined,
|
||||
CaretDownOutlined,
|
||||
DownloadOutlined,
|
||||
ReloadOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import * as echarts from 'echarts'
|
||||
import DataStatisticsChart from '@/components/charts/DataStatisticsChart.vue'
|
||||
|
||||
// 概览数据
|
||||
const overviewData = ref({
|
||||
totalUsers: 12580,
|
||||
activeUsers: 8960,
|
||||
totalAnimals: 1250,
|
||||
totalRevenue: 156780.50,
|
||||
userGrowth: 12.5,
|
||||
activeGrowth: 8.3,
|
||||
animalGrowth: 15.2,
|
||||
revenueGrowth: 22.1
|
||||
})
|
||||
|
||||
// 日期范围
|
||||
const dateRange = ref<[Dayjs, Dayjs] | null>(null)
|
||||
|
||||
// 加载状态
|
||||
const exportLoading = ref(false)
|
||||
const refreshLoading = ref(false)
|
||||
const geoLoading = ref(false)
|
||||
|
||||
// 地理分布图
|
||||
const geoViewType = ref('users')
|
||||
const geoChartContainer = ref<HTMLDivElement>()
|
||||
let geoChartInstance: echarts.ECharts | null = null
|
||||
|
||||
// 图表引用
|
||||
const userGrowthChart = ref()
|
||||
const businessChart = ref()
|
||||
const userTypeChart = ref()
|
||||
const animalSpeciesChart = ref()
|
||||
const revenueChart = ref()
|
||||
|
||||
// 动物排行数据
|
||||
const animalRankingColumns = [
|
||||
{ title: '排名', dataIndex: 'rank', key: 'rank', width: 80 },
|
||||
{ title: '动物名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '种类', dataIndex: 'species', key: 'species' },
|
||||
{ title: '认领次数', dataIndex: 'adoption_count', key: 'adoption_count' },
|
||||
{ title: '认领率', dataIndex: 'adoption_rate', key: 'adoption_rate' }
|
||||
]
|
||||
|
||||
const animalRankingData = ref([
|
||||
{ id: 1, name: '小白', species: '狗', adoption_count: 25, adoption_rate: 85 },
|
||||
{ id: 2, name: '咪咪', species: '猫', adoption_count: 22, adoption_rate: 78 },
|
||||
{ id: 3, name: '小黑', species: '狗', adoption_count: 20, adoption_rate: 72 },
|
||||
{ id: 4, name: '花花', species: '猫', adoption_count: 18, adoption_rate: 65 },
|
||||
{ id: 5, name: '豆豆', species: '兔子', adoption_count: 15, adoption_rate: 58 }
|
||||
])
|
||||
|
||||
// 用户排行数据
|
||||
const userRankingColumns = [
|
||||
{ title: '排名', dataIndex: 'rank', key: 'rank', width: 80 },
|
||||
{ title: '头像', dataIndex: 'avatar', key: 'avatar', width: 60 },
|
||||
{ title: '用户名', dataIndex: 'nickname', key: 'nickname' },
|
||||
{ title: '认领数量', dataIndex: 'adoption_count', key: 'adoption_count' },
|
||||
{ title: '活跃度', dataIndex: 'activity_score', key: 'activity_score' }
|
||||
]
|
||||
|
||||
const userRankingData = ref([
|
||||
{ id: 1, nickname: '爱心天使', avatar: '', adoption_count: 8, activity_score: 95 },
|
||||
{ id: 2, nickname: '动物守护者', avatar: '', adoption_count: 6, activity_score: 88 },
|
||||
{ id: 3, nickname: '温暖之家', avatar: '', adoption_count: 5, activity_score: 82 },
|
||||
{ id: 4, nickname: '小动物之友', avatar: '', adoption_count: 4, activity_score: 76 },
|
||||
{ id: 5, nickname: '爱宠人士', avatar: '', adoption_count: 3, activity_score: 70 }
|
||||
])
|
||||
|
||||
/**
|
||||
* 处理日期范围变化
|
||||
*/
|
||||
const handleDateRangeChange = (dates: [Dayjs, Dayjs] | null) => {
|
||||
if (dates) {
|
||||
// 刷新所有图表数据
|
||||
handleRefreshAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出报表
|
||||
*/
|
||||
const handleExport = async () => {
|
||||
exportLoading.value = true
|
||||
|
||||
try {
|
||||
// 模拟导出过程
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
// 这里应该调用实际的导出API
|
||||
message.success('报表导出成功')
|
||||
} catch (error) {
|
||||
message.error('报表导出失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新所有数据
|
||||
*/
|
||||
const handleRefreshAll = async () => {
|
||||
refreshLoading.value = true
|
||||
|
||||
try {
|
||||
// 刷新概览数据
|
||||
await loadOverviewData()
|
||||
|
||||
// 刷新所有图表
|
||||
userGrowthChart.value?.refresh()
|
||||
businessChart.value?.refresh()
|
||||
userTypeChart.value?.refresh()
|
||||
animalSpeciesChart.value?.refresh()
|
||||
revenueChart.value?.refresh()
|
||||
|
||||
// 刷新地理分布图
|
||||
await loadGeoData()
|
||||
|
||||
message.success('数据刷新成功')
|
||||
} catch (error) {
|
||||
message.error('数据刷新失败')
|
||||
} finally {
|
||||
refreshLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载概览数据
|
||||
*/
|
||||
const loadOverviewData = async () => {
|
||||
// 这里应该调用实际的API接口
|
||||
// const response = await getOverviewStatistics()
|
||||
// overviewData.value = response.data
|
||||
|
||||
// 模拟数据更新
|
||||
overviewData.value = {
|
||||
...overviewData.value,
|
||||
totalUsers: overviewData.value.totalUsers + Math.floor(Math.random() * 100),
|
||||
activeUsers: overviewData.value.activeUsers + Math.floor(Math.random() * 50)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化地理分布图
|
||||
*/
|
||||
const initGeoChart = () => {
|
||||
if (!geoChartContainer.value) return
|
||||
|
||||
geoChartInstance = echarts.init(geoChartContainer.value)
|
||||
loadGeoData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载地理分布数据
|
||||
*/
|
||||
const loadGeoData = async () => {
|
||||
if (!geoChartInstance) return
|
||||
|
||||
geoLoading.value = true
|
||||
|
||||
try {
|
||||
// 模拟地理数据
|
||||
const geoData = [
|
||||
{ name: '北京', value: 1200 },
|
||||
{ name: '上海', value: 980 },
|
||||
{ name: '广东', value: 850 },
|
||||
{ name: '浙江', value: 720 },
|
||||
{ name: '江苏', value: 680 },
|
||||
{ name: '四川', value: 520 },
|
||||
{ name: '湖北', value: 450 },
|
||||
{ name: '河南', value: 380 }
|
||||
]
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: `${geoViewType.value === 'users' ? '用户' : geoViewType.value === 'animals' ? '动物' : '订单'}分布`,
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}'
|
||||
},
|
||||
visualMap: {
|
||||
min: 0,
|
||||
max: 1200,
|
||||
left: 'left',
|
||||
top: 'bottom',
|
||||
text: ['高', '低'],
|
||||
calculable: true,
|
||||
inRange: {
|
||||
color: ['#e0f3ff', '#006edd']
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: geoViewType.value,
|
||||
type: 'map',
|
||||
map: 'china',
|
||||
roam: false,
|
||||
data: geoData
|
||||
}]
|
||||
}
|
||||
|
||||
geoChartInstance.setOption(option)
|
||||
} catch (error) {
|
||||
console.error('加载地理数据失败:', error)
|
||||
} finally {
|
||||
geoLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理地理视图类型变化
|
||||
*/
|
||||
const handleGeoViewChange = () => {
|
||||
loadGeoData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 图表数据加载回调
|
||||
*/
|
||||
const handleUserGrowthLoaded = (data: any[]) => {
|
||||
console.log('用户增长数据加载完成:', data)
|
||||
}
|
||||
|
||||
const handleBusinessDataLoaded = (data: any[]) => {
|
||||
console.log('业务数据加载完成:', data)
|
||||
}
|
||||
|
||||
const handleUserTypeLoaded = (data: any[]) => {
|
||||
console.log('用户类型数据加载完成:', data)
|
||||
}
|
||||
|
||||
const handleAnimalSpeciesLoaded = (data: any[]) => {
|
||||
console.log('动物种类数据加载完成:', data)
|
||||
}
|
||||
|
||||
const handleRevenueLoaded = (data: any[]) => {
|
||||
console.log('收入数据加载完成:', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件挂载
|
||||
*/
|
||||
onMounted(() => {
|
||||
loadOverviewData()
|
||||
|
||||
nextTick(() => {
|
||||
initGeoChart()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.statistics-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.overview-cards {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.statistic-trend {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trend {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.trend.up {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.trend.down {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.trend-text {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chart-section :deep(.ant-card),
|
||||
.table-section :deep(.ant-card) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-section :deep(.ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -388,7 +388,7 @@ const loadTravels = async () => {
|
||||
const params: TravelQueryParams = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
keyword: searchForm.keyword,
|
||||
title: searchForm.keyword, // 修改为title参数而不是keyword
|
||||
status: searchForm.status
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<TrendingUpOutlined />
|
||||
<RiseOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
@@ -121,7 +121,7 @@
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="normal">普通用户</a-select-option>
|
||||
<a-select-option value="farmer">普通用户</a-select-option>
|
||||
<a-select-option value="merchant">商家</a-select-option>
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
</a-select>
|
||||
@@ -147,7 +147,7 @@
|
||||
:data-source="userList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
:row-key="(record: any) => record.id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
@@ -267,7 +267,7 @@
|
||||
<a-col :span="12">
|
||||
<a-form-item label="用户类型" name="userType">
|
||||
<a-select v-model:value="currentUser.userType" placeholder="请选择用户类型">
|
||||
<a-select-option value="normal">普通用户</a-select-option>
|
||||
<a-select-option value="farmer">普通用户</a-select-option>
|
||||
<a-select-option value="merchant">商家</a-select-option>
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
</a-select>
|
||||
@@ -293,14 +293,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, h } from 'vue'
|
||||
import { message, Modal, type FormInstance } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import {
|
||||
UserOutlined,
|
||||
CheckCircleOutlined,
|
||||
RiseOutlined,
|
||||
TrendingUpOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
@@ -312,7 +311,10 @@ import {
|
||||
DownOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { getUsers, getUser, createUser, updateUser, disableUser, enableUser, banUser, unbanUser } from '@/api/user'
|
||||
import userAPI from '@/api/user'
|
||||
|
||||
// 解构API方法
|
||||
const { getUsers, getUser, createUser, updateUser, updateUserStatus } = userAPI
|
||||
import type { User, UserQueryParams } from '@/types/user'
|
||||
|
||||
interface SearchForm {
|
||||
@@ -385,25 +387,27 @@ const columns = [
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '用户类型',
|
||||
dataIndex: 'user_type',
|
||||
key: 'userType',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'registerTime',
|
||||
dataIndex: 'created_at',
|
||||
key: 'registerTime',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '最后登录',
|
||||
dataIndex: 'lastLoginTime',
|
||||
dataIndex: 'last_login_at',
|
||||
key: 'lastLoginTime',
|
||||
width: 120
|
||||
},
|
||||
@@ -437,20 +441,24 @@ const getStatusText = (status: string) => {
|
||||
// 类型映射
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors = {
|
||||
normal: 'blue',
|
||||
farmer: 'blue',
|
||||
merchant: 'purple',
|
||||
admin: 'red'
|
||||
}
|
||||
return colors[type as keyof typeof colors] || 'default'
|
||||
// 处理后端返回的user_type字段,可能为user_type或userType
|
||||
const userType = type && typeof type === 'string' ? type.toLowerCase() : '';
|
||||
return colors[userType as keyof typeof colors] || 'default'
|
||||
}
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const texts = {
|
||||
normal: '普通用户',
|
||||
farmer: '普通用户',
|
||||
merchant: '商家',
|
||||
admin: '管理员'
|
||||
}
|
||||
return texts[type as keyof typeof texts] || '未知'
|
||||
// 处理后端返回的user_type字段,可能为user_type或userType
|
||||
const userType = type && typeof type === 'string' ? type.toLowerCase() : '';
|
||||
return texts[userType as keyof typeof texts] || '未知'
|
||||
}
|
||||
|
||||
// 添加模态框相关状态
|
||||
@@ -505,9 +513,27 @@ const loadUsers = async () => {
|
||||
}
|
||||
|
||||
const response = await getUsers(params)
|
||||
userList.value = response.data.list
|
||||
pagination.total = response.data.pagination.total
|
||||
if (response && response.data) {
|
||||
// 根据实际返回的数据结构调整处理方式
|
||||
if (response.data.users) {
|
||||
// 如果返回的是分页数据结构 { users: [...], pagination: {...} }
|
||||
userList.value = response.data.users
|
||||
if (response.data.pagination) {
|
||||
pagination.total = response.data.pagination.total
|
||||
}
|
||||
} else if (Array.isArray(response.data)) {
|
||||
// 如果返回的是简单数组
|
||||
userList.value = response.data
|
||||
} else if (response.data.list) {
|
||||
// 兼容旧的分页数据结构 { list: [...], pagination: {...} }
|
||||
userList.value = response.data.list
|
||||
if (response.data.pagination) {
|
||||
pagination.total = response.data.pagination.total
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户列表失败:', error)
|
||||
message.error('加载用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -559,7 +585,7 @@ const handleView = async (record: User) => {
|
||||
h('a-tag', { color: getStatusColor(response.data.status) }, getStatusText(response.data.status))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '用户类型' }, [
|
||||
h('a-tag', { color: getTypeColor(response.data.userType) }, getTypeText(response.data.userType))
|
||||
h('a-tag', { color: getTypeColor(response.data.user_type || response.data.userType) }, getTypeText(response.data.user_type || response.data.userType))
|
||||
]),
|
||||
h('a-descriptions-item', { label: '注册时间' }, response.data.registerTime),
|
||||
h('a-descriptions-item', { label: '最后登录' }, response.data.lastLoginTime)
|
||||
@@ -651,7 +677,7 @@ const handleDisable = async (record: User) => {
|
||||
content: `确定要禁用用户 "${record.username}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await disableUser(record.id)
|
||||
await userAPI.disableUser(record.id)
|
||||
message.success('用户已禁用')
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
@@ -667,7 +693,7 @@ const handleEnable = async (record: User) => {
|
||||
content: `确定要启用用户 "${record.username}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await enableUser(record.id)
|
||||
await userAPI.enableUser(record.id)
|
||||
message.success('用户已启用')
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
@@ -683,7 +709,7 @@ const handleBan = async (record: User) => {
|
||||
content: `确定要封禁用户 "${record.username}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await banUser(record.id)
|
||||
await userAPI.banUser(record.id)
|
||||
message.success('用户已封禁')
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
@@ -699,7 +725,7 @@ const handleUnban = async (record: User) => {
|
||||
content: `确定要解封用户 "${record.username}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await unbanUser(record.id)
|
||||
await userAPI.unbanUser(record.id)
|
||||
message.success('用户已解封')
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -60,30 +60,56 @@ export const useAppStore = defineStore('app', () => {
|
||||
try {
|
||||
const token = localStorage.getItem('admin_token')
|
||||
if (token) {
|
||||
// 获取用户信息
|
||||
const response = await authAPI.getCurrentUser()
|
||||
|
||||
// 统一处理接口响应格式
|
||||
if (!response || typeof response !== 'object') {
|
||||
throw new Error('获取用户信息失败:接口返回格式异常')
|
||||
}
|
||||
|
||||
// 确保响应数据格式为 { data: { admin: object } }
|
||||
if (response.data && typeof response.data === 'object' && response.data.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'
|
||||
]
|
||||
state.user = response.data.admin
|
||||
state.permissions = mockPermissions
|
||||
} else {
|
||||
throw new Error('获取用户信息失败:响应数据格式不符合预期')
|
||||
try {
|
||||
// 获取用户信息
|
||||
const response = await authAPI.getCurrentUser()
|
||||
|
||||
// 统一处理接口响应格式
|
||||
if (!response || typeof response !== 'object') {
|
||||
throw new Error('获取用户信息失败:接口返回格式异常')
|
||||
}
|
||||
|
||||
// 确保响应数据格式正确 - 支持两种格式:
|
||||
// 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',
|
||||
'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.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('获取用户信息失败:响应数据格式不符合预期')
|
||||
}
|
||||
} catch (apiError) {
|
||||
// 如果获取用户信息失败(比如token过期),清除登录状态但不抛出错误
|
||||
console.warn('获取用户信息失败,可能是token过期:', apiError)
|
||||
clearUser()
|
||||
// 不抛出错误,让应用正常初始化,路由守卫会处理重定向到登录页
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -93,7 +119,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
token: localStorage.getItem('admin_token')
|
||||
})
|
||||
clearUser()
|
||||
throw error // 抛出错误以便调用方处理
|
||||
// 不抛出错误,让应用正常初始化
|
||||
} finally {
|
||||
state.loading = false
|
||||
state.initialized = true
|
||||
@@ -106,11 +132,17 @@ export const useAppStore = defineStore('app', () => {
|
||||
try {
|
||||
const response = await authAPI.login(credentials)
|
||||
|
||||
// 保存token - 修复数据结构访问问题
|
||||
// 保存token和refreshToken - 修复数据结构访问问题
|
||||
if (response?.data?.token) {
|
||||
localStorage.setItem('admin_token', response.data.token)
|
||||
if (response.data.refreshToken) {
|
||||
localStorage.setItem('admin_refresh_token', response.data.refreshToken)
|
||||
}
|
||||
} else if (response?.token) {
|
||||
localStorage.setItem('admin_token', response.token)
|
||||
if (response.refreshToken) {
|
||||
localStorage.setItem('admin_refresh_token', response.refreshToken)
|
||||
}
|
||||
} else {
|
||||
throw new Error('登录响应中缺少token')
|
||||
}
|
||||
@@ -125,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
|
||||
@@ -138,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
|
||||
@@ -158,7 +192,13 @@ export const useAppStore = defineStore('app', () => {
|
||||
|
||||
// 退出登录
|
||||
const logout = () => {
|
||||
clearUser()
|
||||
// 清除localStorage中的token和refreshToken
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_refresh_token')
|
||||
|
||||
// 清除状态
|
||||
state.user = null
|
||||
state.permissions = []
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
166
admin-system/src/utils/date.ts
Normal file
166
admin-system/src/utils/date.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
// 配置dayjs插件
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param date 日期
|
||||
* @param format 格式化字符串,默认为 'YYYY-MM-DD HH:mm:ss'
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
export const formatDate = (date: string | Date | dayjs.Dayjs | null | undefined, format = 'YYYY-MM-DD HH:mm:ss'): string => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).format(format)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化相对时间
|
||||
* @param date 日期
|
||||
* @returns 相对时间字符串,如 "2小时前"
|
||||
*/
|
||||
export const formatRelativeTime = (date: string | Date | dayjs.Dayjs | null | undefined): string => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).fromNow()
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期为友好显示
|
||||
* @param date 日期
|
||||
* @returns 友好的日期显示
|
||||
*/
|
||||
export const formatFriendlyDate = (date: string | Date | dayjs.Dayjs | null | undefined): string => {
|
||||
if (!date) return ''
|
||||
const now = dayjs()
|
||||
const target = dayjs(date)
|
||||
const diffDays = now.diff(target, 'day')
|
||||
|
||||
if (diffDays === 0) {
|
||||
return target.format('HH:mm')
|
||||
} else if (diffDays === 1) {
|
||||
return `昨天 ${target.format('HH:mm')}`
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays}天前`
|
||||
} else {
|
||||
return target.format('MM-DD HH:mm')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为日期(不包含时间)
|
||||
* @param date 日期字符串或Date对象
|
||||
* @returns 格式化后的日期字符串,如"2024-01-15"
|
||||
*/
|
||||
export const formatDateOnly = (date: string | Date | null | undefined): string => {
|
||||
return formatDate(date, 'YYYY-MM-DD')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为时间(不包含日期)
|
||||
* @param date 日期字符串或Date对象
|
||||
* @returns 格式化后的时间字符串,如"14:30:25"
|
||||
*/
|
||||
export const formatTimeOnly = (date: string | Date | null | undefined): string => {
|
||||
return formatDate(date, 'HH:mm:ss')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为中文日期时间
|
||||
* @param date 日期字符串或Date对象
|
||||
* @returns 中文格式的日期时间字符串,如"2024年1月15日 14:30"
|
||||
*/
|
||||
export const formatChineseDateTime = (date: string | Date | null | undefined): string => {
|
||||
return formatDate(date, 'YYYY年M月D日 HH:mm')
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为今天
|
||||
* @param date 日期
|
||||
* @returns 是否为今天
|
||||
*/
|
||||
export const isToday = (date: string | Date | dayjs.Dayjs | null | undefined): boolean => {
|
||||
if (!date) return false
|
||||
return dayjs(date).isSame(dayjs(), 'day')
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为本周
|
||||
* @param date 日期
|
||||
* @returns 是否为本周
|
||||
*/
|
||||
export const isThisWeek = (date: string | Date | dayjs.Dayjs | null | undefined): boolean => {
|
||||
if (!date) return false
|
||||
return dayjs(date).isSame(dayjs(), 'week')
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为本月
|
||||
* @param date 日期
|
||||
* @returns 是否为本月
|
||||
*/
|
||||
export const isThisMonth = (date: string | Date | dayjs.Dayjs | null | undefined): boolean => {
|
||||
if (!date) return false
|
||||
return dayjs(date).isSame(dayjs(), 'month')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时间范围
|
||||
* @param type 时间范围类型
|
||||
* @returns 时间范围数组 [开始时间, 结束时间]
|
||||
*/
|
||||
export const getTimeRange = (type: 'today' | 'yesterday' | 'week' | 'month' | 'year'): [dayjs.Dayjs, dayjs.Dayjs] => {
|
||||
const now = dayjs()
|
||||
|
||||
switch (type) {
|
||||
case 'today':
|
||||
return [now.startOf('day'), now.endOf('day')]
|
||||
case 'yesterday':
|
||||
const yesterday = now.subtract(1, 'day')
|
||||
return [yesterday.startOf('day'), yesterday.endOf('day')]
|
||||
case 'week':
|
||||
return [now.startOf('week'), now.endOf('week')]
|
||||
case 'month':
|
||||
return [now.startOf('month'), now.endOf('month')]
|
||||
case 'year':
|
||||
return [now.startOf('year'), now.endOf('year')]
|
||||
default:
|
||||
return [now.startOf('day'), now.endOf('day')]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换时区
|
||||
* @param date 日期
|
||||
* @param timezone 目标时区
|
||||
* @returns 转换后的日期
|
||||
*/
|
||||
export const convertTimezone = (date: string | Date | dayjs.Dayjs, timezone: string): dayjs.Dayjs => {
|
||||
return dayjs(date).tz(timezone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时区
|
||||
* @returns 当前时区
|
||||
*/
|
||||
export const getCurrentTimezone = (): string => {
|
||||
return dayjs.tz.guess()
|
||||
}
|
||||
|
||||
export default {
|
||||
formatDate,
|
||||
formatRelativeTime,
|
||||
formatFriendlyDate,
|
||||
isToday,
|
||||
isThisWeek,
|
||||
isThisMonth,
|
||||
getTimeRange,
|
||||
convertTimezone,
|
||||
getCurrentTimezone
|
||||
}
|
||||
@@ -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🎉 模拟数据测试完成!')
|
||||
@@ -27,7 +27,7 @@ export default defineConfig({
|
||||
port: 3150,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://webapi.jiebanke.com',
|
||||
target: 'http://localhost:3200',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api/v1')
|
||||
}
|
||||
|
||||
10
backend/.env
10
backend/.env
@@ -4,11 +4,11 @@ PORT=3200
|
||||
HOST=0.0.0.0
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_NAME=jiebanke_dev
|
||||
DB_HOST=nj-cdb-3pwh2kz1.sql.tencentcdb.com
|
||||
DB_PORT=20784
|
||||
DB_USER=jiebanke
|
||||
DB_PASSWORD=aiot741$12346
|
||||
DB_NAME=jbkdata
|
||||
DB_NAME_TEST=jiebanke_test
|
||||
|
||||
# JWT配置
|
||||
|
||||
@@ -5,7 +5,7 @@ require('dotenv').config({ path: path.join(__dirname, '../../.env') })
|
||||
const config = {
|
||||
// 开发环境
|
||||
development: {
|
||||
port: process.env.PORT || 3110,
|
||||
port: process.env.PORT || 3200,
|
||||
mysql: {
|
||||
host: process.env.DB_HOST || 'nj-cdb-3pwh2kz1.sql.tencentcdb.com',
|
||||
port: process.env.DB_PORT || 20784,
|
||||
@@ -27,7 +27,7 @@ const config = {
|
||||
allowedTypes: ['image/jpeg', 'image/png', 'image/gif']
|
||||
},
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || 'https://www.jiebanke.com',
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:3150',
|
||||
credentials: true
|
||||
}
|
||||
},
|
||||
|
||||
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): 初始版本,包含基础的商户管理功能
|
||||
@@ -46,16 +46,18 @@
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.14.3",
|
||||
"nodemailer": "^7.0.6",
|
||||
"pm2": "^5.3.0",
|
||||
"redis": "^5.8.2",
|
||||
"sharp": "^0.34.4",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"winston": "^3.11.0",
|
||||
"xss-clean": "^0.1.4",
|
||||
"pm2": "^5.3.0"
|
||||
"xss-clean": "^0.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.1.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
236
backend/scripts/animal_claims_table.sql
Normal file
236
backend/scripts/animal_claims_table.sql
Normal file
@@ -0,0 +1,236 @@
|
||||
-- 动物认领申请表
|
||||
CREATE TABLE IF NOT EXISTS animal_claims (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '认领申请ID',
|
||||
claim_no VARCHAR(32) NOT NULL UNIQUE COMMENT '认领订单号',
|
||||
animal_id INT NOT NULL COMMENT '动物ID',
|
||||
user_id INT NOT NULL COMMENT '用户ID',
|
||||
claim_reason TEXT COMMENT '认领理由',
|
||||
claim_duration INT NOT NULL DEFAULT 12 COMMENT '认领时长(月)',
|
||||
total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '总金额',
|
||||
contact_info VARCHAR(500) NOT NULL COMMENT '联系方式',
|
||||
status ENUM('pending', 'approved', 'rejected', 'cancelled') NOT NULL DEFAULT 'pending' COMMENT '申请状态',
|
||||
start_date DATETIME NULL COMMENT '开始日期',
|
||||
end_date DATETIME NULL COMMENT '结束日期',
|
||||
reviewed_by INT NULL COMMENT '审核人ID',
|
||||
review_remark TEXT COMMENT '审核备注',
|
||||
reviewed_at DATETIME NULL COMMENT '审核时间',
|
||||
approved_at DATETIME NULL COMMENT '通过时间',
|
||||
cancelled_at DATETIME NULL COMMENT '取消时间',
|
||||
cancel_reason VARCHAR(500) NULL COMMENT '取消原因',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at DATETIME NULL COMMENT '删除时间',
|
||||
|
||||
-- 外键约束
|
||||
FOREIGN KEY (animal_id) REFERENCES animals(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (reviewed_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
|
||||
-- 索引
|
||||
INDEX idx_animal_id (animal_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_claim_no (claim_no),
|
||||
INDEX idx_deleted_at (deleted_at),
|
||||
|
||||
-- 唯一约束:同一用户对同一动物在同一时间只能有一个有效申请
|
||||
UNIQUE KEY uk_user_animal_active (user_id, animal_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='动物认领申请表';
|
||||
|
||||
-- 动物认领续期记录表
|
||||
CREATE TABLE IF NOT EXISTS animal_claim_renewals (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '续期记录ID',
|
||||
claim_id INT NOT NULL COMMENT '认领申请ID',
|
||||
duration INT NOT NULL COMMENT '续期时长(月)',
|
||||
amount DECIMAL(10,2) NOT NULL COMMENT '续期金额',
|
||||
payment_method ENUM('wechat', 'alipay', 'bank_transfer') NOT NULL COMMENT '支付方式',
|
||||
status ENUM('pending', 'paid', 'cancelled') NOT NULL DEFAULT 'pending' COMMENT '续期状态',
|
||||
payment_no VARCHAR(64) NULL COMMENT '支付订单号',
|
||||
paid_at DATETIME NULL COMMENT '支付时间',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
-- 外键约束
|
||||
FOREIGN KEY (claim_id) REFERENCES animal_claims(id) ON DELETE CASCADE,
|
||||
|
||||
-- 索引
|
||||
INDEX idx_claim_id (claim_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_payment_no (payment_no)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='动物认领续期记录表';
|
||||
|
||||
-- 插入测试数据
|
||||
INSERT INTO animal_claims (
|
||||
claim_no, animal_id, user_id, claim_reason, claim_duration,
|
||||
total_amount, contact_info, status, created_at
|
||||
) VALUES
|
||||
(
|
||||
'CLAIM20241201001', 1, 2, '我很喜欢这只小狗,希望能够认领它', 12,
|
||||
1200.00, '手机:13800138001,微信:user001', 'pending', '2024-12-01 10:00:00'
|
||||
),
|
||||
(
|
||||
'CLAIM20241201002', 2, 3, '想要认领这只小猫,会好好照顾它', 6,
|
||||
600.00, '手机:13800138002,QQ:123456789', 'approved', '2024-12-01 11:00:00'
|
||||
),
|
||||
(
|
||||
'CLAIM20241201003', 3, 4, '希望认领这只兔子,家里有足够的空间', 24,
|
||||
2400.00, '手机:13800138003,邮箱:user003@example.com', 'rejected', '2024-12-01 12:00:00'
|
||||
);
|
||||
|
||||
-- 更新已通过的认领申请的时间信息
|
||||
UPDATE animal_claims
|
||||
SET
|
||||
start_date = '2024-12-01 11:30:00',
|
||||
end_date = '2025-06-01 11:30:00',
|
||||
reviewed_by = 1,
|
||||
review_remark = '申请材料完整,同意认领',
|
||||
reviewed_at = '2024-12-01 11:30:00',
|
||||
approved_at = '2024-12-01 11:30:00'
|
||||
WHERE claim_no = 'CLAIM20241201002';
|
||||
|
||||
-- 更新被拒绝的认领申请的审核信息
|
||||
UPDATE animal_claims
|
||||
SET
|
||||
reviewed_by = 1,
|
||||
review_remark = '认领时长过长,建议缩短认领期限后重新申请',
|
||||
reviewed_at = '2024-12-01 12:30:00'
|
||||
WHERE claim_no = 'CLAIM20241201003';
|
||||
|
||||
-- 插入续期记录测试数据
|
||||
INSERT INTO animal_claim_renewals (
|
||||
claim_id, duration, amount, payment_method, status, created_at
|
||||
) VALUES
|
||||
(
|
||||
2, 6, 600.00, 'wechat', 'pending', '2024-12-01 15:00:00'
|
||||
);
|
||||
|
||||
-- 创建视图:认领申请详情视图
|
||||
CREATE OR REPLACE VIEW v_animal_claim_details AS
|
||||
SELECT
|
||||
ac.id,
|
||||
ac.claim_no,
|
||||
ac.animal_id,
|
||||
a.name as animal_name,
|
||||
a.type as animal_type,
|
||||
a.breed as animal_breed,
|
||||
a.age as animal_age,
|
||||
a.gender as animal_gender,
|
||||
a.image as animal_image,
|
||||
a.price as animal_price,
|
||||
ac.user_id,
|
||||
u.username,
|
||||
u.phone as user_phone,
|
||||
u.email as user_email,
|
||||
ac.claim_reason,
|
||||
ac.claim_duration,
|
||||
ac.total_amount,
|
||||
ac.contact_info,
|
||||
ac.status,
|
||||
ac.start_date,
|
||||
ac.end_date,
|
||||
ac.reviewed_by,
|
||||
reviewer.username as reviewer_name,
|
||||
ac.review_remark,
|
||||
ac.reviewed_at,
|
||||
ac.approved_at,
|
||||
ac.cancelled_at,
|
||||
ac.cancel_reason,
|
||||
ac.created_at,
|
||||
ac.updated_at,
|
||||
-- 计算剩余天数
|
||||
CASE
|
||||
WHEN ac.status = 'approved' AND ac.end_date > NOW()
|
||||
THEN DATEDIFF(ac.end_date, NOW())
|
||||
ELSE 0
|
||||
END as remaining_days,
|
||||
-- 是否即将到期(30天内)
|
||||
CASE
|
||||
WHEN ac.status = 'approved' AND ac.end_date > NOW() AND DATEDIFF(ac.end_date, NOW()) <= 30
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END as is_expiring_soon
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN animals a ON ac.animal_id = a.id
|
||||
LEFT JOIN users u ON ac.user_id = u.id
|
||||
LEFT JOIN users reviewer ON ac.reviewed_by = reviewer.id
|
||||
WHERE ac.deleted_at IS NULL;
|
||||
|
||||
-- 创建触发器:认领申请通过时更新动物状态
|
||||
DELIMITER //
|
||||
CREATE TRIGGER tr_animal_claim_approved
|
||||
AFTER UPDATE ON animal_claims
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- 如果认领申请从其他状态变为已通过
|
||||
IF OLD.status != 'approved' AND NEW.status = 'approved' THEN
|
||||
UPDATE animals SET status = 'claimed', claim_count = claim_count + 1 WHERE id = NEW.animal_id;
|
||||
END IF;
|
||||
|
||||
-- 如果认领申请从已通过变为其他状态
|
||||
IF OLD.status = 'approved' AND NEW.status != 'approved' THEN
|
||||
UPDATE animals SET status = 'available' WHERE id = NEW.animal_id;
|
||||
END IF;
|
||||
END//
|
||||
DELIMITER ;
|
||||
|
||||
-- 创建存储过程:批量处理过期的认领申请
|
||||
DELIMITER //
|
||||
CREATE PROCEDURE sp_handle_expired_claims()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT FALSE;
|
||||
DECLARE claim_id INT;
|
||||
DECLARE animal_id INT;
|
||||
|
||||
-- 声明游标
|
||||
DECLARE expired_cursor CURSOR FOR
|
||||
SELECT id, animal_id
|
||||
FROM animal_claims
|
||||
WHERE status = 'approved'
|
||||
AND end_date < NOW()
|
||||
AND deleted_at IS NULL;
|
||||
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||
|
||||
-- 开始事务
|
||||
START TRANSACTION;
|
||||
|
||||
-- 打开游标
|
||||
OPEN expired_cursor;
|
||||
|
||||
read_loop: LOOP
|
||||
FETCH expired_cursor INTO claim_id, animal_id;
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
-- 更新认领申请状态为已过期
|
||||
UPDATE animal_claims
|
||||
SET status = 'expired', updated_at = NOW()
|
||||
WHERE id = claim_id;
|
||||
|
||||
-- 更新动物状态为可认领
|
||||
UPDATE animals
|
||||
SET status = 'available', updated_at = NOW()
|
||||
WHERE id = animal_id;
|
||||
|
||||
END LOOP;
|
||||
|
||||
-- 关闭游标
|
||||
CLOSE expired_cursor;
|
||||
|
||||
-- 提交事务
|
||||
COMMIT;
|
||||
|
||||
-- 返回处理的记录数
|
||||
SELECT ROW_COUNT() as processed_count;
|
||||
END//
|
||||
DELIMITER ;
|
||||
|
||||
-- 创建事件:每天自动处理过期的认领申请
|
||||
CREATE EVENT IF NOT EXISTS ev_handle_expired_claims
|
||||
ON SCHEDULE EVERY 1 DAY
|
||||
STARTS '2024-12-01 02:00:00'
|
||||
DO
|
||||
CALL sp_handle_expired_claims();
|
||||
226
backend/scripts/check-database-structure.js
Normal file
226
backend/scripts/check-database-structure.js
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 数据库结构检查脚本
|
||||
* 检查数据库表结构和数据完整性
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const config = require('../config/env');
|
||||
|
||||
async function checkDatabaseStructure() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🔍 开始检查数据库结构...');
|
||||
|
||||
// 创建数据库连接
|
||||
const dbConfig = {
|
||||
host: config.mysql.host,
|
||||
port: config.mysql.port,
|
||||
user: config.mysql.user,
|
||||
password: config.mysql.password,
|
||||
database: config.mysql.database,
|
||||
charset: config.mysql.charset,
|
||||
timezone: config.mysql.timezone
|
||||
};
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
console.log('✅ 数据库连接成功');
|
||||
console.log(`📊 数据库: ${dbConfig.database}`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 测试基本查询
|
||||
const [testRows] = await connection.execute('SELECT 1 + 1 AS result');
|
||||
console.log(`✅ 基本查询测试: ${testRows[0].result}`);
|
||||
|
||||
// 检查数据库版本和字符集
|
||||
const [versionRows] = await connection.execute('SELECT VERSION() as version');
|
||||
console.log(`📊 MySQL版本: ${versionRows[0].version}`);
|
||||
|
||||
// 获取所有表
|
||||
console.log('\n📋 检查数据库表结构:');
|
||||
const [tables] = await connection.execute(`
|
||||
SELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH, TABLE_COMMENT
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = ?
|
||||
ORDER BY TABLE_NAME
|
||||
`, [dbConfig.database]);
|
||||
|
||||
if (tables.length === 0) {
|
||||
console.log('⚠️ 数据库中没有找到任何表');
|
||||
console.log('💡 建议运行数据库结构创建脚本');
|
||||
return { success: false, message: '数据库为空' };
|
||||
}
|
||||
|
||||
console.log(`📊 找到 ${tables.length} 个表:`);
|
||||
let totalRows = 0;
|
||||
|
||||
for (const table of tables) {
|
||||
const rowCount = table.TABLE_ROWS || 0;
|
||||
totalRows += rowCount;
|
||||
const dataSize = (table.DATA_LENGTH / 1024).toFixed(2);
|
||||
const indexSize = (table.INDEX_LENGTH / 1024).toFixed(2);
|
||||
|
||||
console.log(` 📄 ${table.TABLE_NAME.padEnd(25)} | ${String(rowCount).padStart(6)} 行 | ${dataSize.padStart(8)} KB | ${table.TABLE_COMMENT || '无注释'}`);
|
||||
}
|
||||
|
||||
console.log(`\n📊 总记录数: ${totalRows}`);
|
||||
|
||||
// 检查核心表的详细结构
|
||||
console.log('\n🔍 检查核心表结构:');
|
||||
const coreTables = ['admins', 'users', 'merchants', 'animals', 'orders', 'payments'];
|
||||
|
||||
for (const tableName of coreTables) {
|
||||
const tableExists = tables.find(t => t.TABLE_NAME === tableName);
|
||||
|
||||
if (tableExists) {
|
||||
console.log(`\n📋 表: ${tableName}`);
|
||||
|
||||
// 获取表结构
|
||||
const [columns] = await connection.execute(`
|
||||
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
||||
ORDER BY ORDINAL_POSITION
|
||||
`, [dbConfig.database, tableName]);
|
||||
|
||||
console.log(' 字段结构:');
|
||||
for (const col of columns.slice(0, 10)) { // 只显示前10个字段
|
||||
const nullable = col.IS_NULLABLE === 'YES' ? '可空' : '非空';
|
||||
const defaultVal = col.COLUMN_DEFAULT ? `默认:${col.COLUMN_DEFAULT}` : '';
|
||||
console.log(` ${col.COLUMN_NAME.padEnd(20)} | ${col.DATA_TYPE.padEnd(15)} | ${nullable.padEnd(4)} | ${col.COLUMN_COMMENT || '无注释'}`);
|
||||
}
|
||||
|
||||
if (columns.length > 10) {
|
||||
console.log(` ... 还有 ${columns.length - 10} 个字段`);
|
||||
}
|
||||
|
||||
// 检查索引
|
||||
const [indexes] = await connection.execute(`
|
||||
SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
||||
ORDER BY INDEX_NAME, SEQ_IN_INDEX
|
||||
`, [dbConfig.database, tableName]);
|
||||
|
||||
if (indexes.length > 0) {
|
||||
console.log(' 索引:');
|
||||
const indexGroups = {};
|
||||
indexes.forEach(idx => {
|
||||
if (!indexGroups[idx.INDEX_NAME]) {
|
||||
indexGroups[idx.INDEX_NAME] = [];
|
||||
}
|
||||
indexGroups[idx.INDEX_NAME].push(idx.COLUMN_NAME);
|
||||
});
|
||||
|
||||
Object.entries(indexGroups).forEach(([indexName, columns]) => {
|
||||
const type = indexName === 'PRIMARY' ? '主键' : '索引';
|
||||
console.log(` ${type}: ${indexName} (${columns.join(', ')})`);
|
||||
});
|
||||
}
|
||||
|
||||
// 检查数据样例
|
||||
try {
|
||||
const [sampleData] = await connection.execute(`SELECT * FROM ${tableName} LIMIT 3`);
|
||||
if (sampleData.length > 0) {
|
||||
console.log(` 📊 数据样例: ${sampleData.length} 条记录`);
|
||||
} else {
|
||||
console.log(' 📊 数据样例: 表为空');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ 无法获取数据样例: ${error.message}`);
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log(`❌ 核心表不存在: ${tableName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查外键约束
|
||||
console.log('\n🔗 检查外键约束:');
|
||||
const [foreignKeys] = await connection.execute(`
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
REFERENCED_TABLE_NAME,
|
||||
REFERENCED_COLUMN_NAME,
|
||||
CONSTRAINT_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME IS NOT NULL
|
||||
ORDER BY TABLE_NAME
|
||||
`, [dbConfig.database]);
|
||||
|
||||
if (foreignKeys.length > 0) {
|
||||
console.log(`📊 找到 ${foreignKeys.length} 个外键约束:`);
|
||||
foreignKeys.forEach(fk => {
|
||||
console.log(` ${fk.TABLE_NAME}.${fk.COLUMN_NAME} -> ${fk.REFERENCED_TABLE_NAME}.${fk.REFERENCED_COLUMN_NAME}`);
|
||||
});
|
||||
} else {
|
||||
console.log('⚠️ 没有找到外键约束');
|
||||
}
|
||||
|
||||
// 数据完整性检查
|
||||
console.log('\n🔍 数据完整性检查:');
|
||||
|
||||
// 检查管理员数据
|
||||
if (tables.find(t => t.TABLE_NAME === 'admins')) {
|
||||
const [adminCount] = await connection.execute('SELECT COUNT(*) as count FROM admins');
|
||||
console.log(`👨💼 管理员数量: ${adminCount[0].count}`);
|
||||
|
||||
if (adminCount[0].count > 0) {
|
||||
const [admins] = await connection.execute('SELECT username, role, status FROM admins LIMIT 5');
|
||||
admins.forEach(admin => {
|
||||
console.log(` - ${admin.username} (${admin.role}, 状态: ${admin.status})`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户数据
|
||||
if (tables.find(t => t.TABLE_NAME === 'users')) {
|
||||
const [userCount] = await connection.execute('SELECT COUNT(*) as count FROM users');
|
||||
console.log(`👥 用户数量: ${userCount[0].count}`);
|
||||
|
||||
if (userCount[0].count > 0) {
|
||||
const [userStats] = await connection.execute(`
|
||||
SELECT user_type, COUNT(*) as count
|
||||
FROM users
|
||||
GROUP BY user_type
|
||||
`);
|
||||
userStats.forEach(stat => {
|
||||
console.log(` - ${stat.user_type}: ${stat.count} 人`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n🎉 数据库结构检查完成!');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
tableCount: tables.length,
|
||||
totalRows: totalRows,
|
||||
tables: tables.map(t => t.TABLE_NAME)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库结构检查失败:', error.message);
|
||||
console.error('🔍 错误代码:', error.code);
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔒 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是直接运行此文件,则执行检查
|
||||
if (require.main === module) {
|
||||
checkDatabaseStructure()
|
||||
.then((result) => {
|
||||
process.exit(result.success ? 0 : 1);
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
}
|
||||
|
||||
module.exports = { checkDatabaseStructure };
|
||||
181
backend/scripts/check-table-structure.js
Normal file
181
backend/scripts/check-table-structure.js
Normal file
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 检查数据库表结构脚本
|
||||
* 对比设计文档与实际表结构
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const config = require('../config/env');
|
||||
|
||||
async function checkTableStructure() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🔍 开始检查数据库表结构...');
|
||||
|
||||
const dbConfig = {
|
||||
host: config.mysql.host,
|
||||
port: config.mysql.port,
|
||||
user: config.mysql.user,
|
||||
password: config.mysql.password,
|
||||
database: config.mysql.database,
|
||||
charset: config.mysql.charset || 'utf8mb4',
|
||||
timezone: config.mysql.timezone || '+08:00'
|
||||
};
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
console.log('✅ 数据库连接成功');
|
||||
|
||||
// 检查所有表的详细结构
|
||||
const [tables] = await connection.execute(
|
||||
`SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = ? ORDER BY table_name`,
|
||||
[dbConfig.database]
|
||||
);
|
||||
|
||||
console.log(`\n📊 数据库 ${dbConfig.database} 中共有 ${tables.length} 个表:`);
|
||||
|
||||
for (const table of tables) {
|
||||
const tableName = table.TABLE_NAME || table.table_name;
|
||||
console.log(`\n🔍 检查表: ${tableName}`);
|
||||
|
||||
// 获取表结构
|
||||
const [columns] = await connection.execute(
|
||||
`SELECT
|
||||
COLUMN_NAME,
|
||||
DATA_TYPE,
|
||||
IS_NULLABLE,
|
||||
COLUMN_DEFAULT,
|
||||
COLUMN_KEY,
|
||||
EXTRA,
|
||||
COLUMN_COMMENT
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE table_schema = ? AND table_name = ?
|
||||
ORDER BY ORDINAL_POSITION`,
|
||||
[dbConfig.database, tableName]
|
||||
);
|
||||
|
||||
// 获取表记录数
|
||||
const [countResult] = await connection.execute(`SELECT COUNT(*) AS count FROM ${tableName}`);
|
||||
const recordCount = countResult[0].count;
|
||||
|
||||
console.log(` 📊 记录数: ${recordCount}`);
|
||||
console.log(` 📋 字段结构 (${columns.length} 个字段):`);
|
||||
|
||||
columns.forEach(col => {
|
||||
const nullable = col.IS_NULLABLE === 'YES' ? 'NULL' : 'NOT NULL';
|
||||
const key = col.COLUMN_KEY ? `[${col.COLUMN_KEY}]` : '';
|
||||
const extra = col.EXTRA ? `[${col.EXTRA}]` : '';
|
||||
const defaultVal = col.COLUMN_DEFAULT !== null ? `DEFAULT: ${col.COLUMN_DEFAULT}` : '';
|
||||
const comment = col.COLUMN_COMMENT ? `// ${col.COLUMN_COMMENT}` : '';
|
||||
|
||||
console.log(` - ${col.COLUMN_NAME}: ${col.DATA_TYPE} ${nullable} ${key} ${extra} ${defaultVal} ${comment}`);
|
||||
});
|
||||
|
||||
// 获取索引信息
|
||||
const [indexes] = await connection.execute(
|
||||
`SELECT
|
||||
INDEX_NAME,
|
||||
COLUMN_NAME,
|
||||
NON_UNIQUE,
|
||||
INDEX_TYPE
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE table_schema = ? AND table_name = ?
|
||||
ORDER BY INDEX_NAME, SEQ_IN_INDEX`,
|
||||
[dbConfig.database, tableName]
|
||||
);
|
||||
|
||||
if (indexes.length > 0) {
|
||||
console.log(` 🔑 索引信息:`);
|
||||
const indexGroups = {};
|
||||
indexes.forEach(idx => {
|
||||
if (!indexGroups[idx.INDEX_NAME]) {
|
||||
indexGroups[idx.INDEX_NAME] = {
|
||||
columns: [],
|
||||
unique: idx.NON_UNIQUE === 0,
|
||||
type: idx.INDEX_TYPE
|
||||
};
|
||||
}
|
||||
indexGroups[idx.INDEX_NAME].columns.push(idx.COLUMN_NAME);
|
||||
});
|
||||
|
||||
Object.entries(indexGroups).forEach(([indexName, info]) => {
|
||||
const uniqueStr = info.unique ? '[UNIQUE]' : '';
|
||||
console.log(` - ${indexName}: (${info.columns.join(', ')}) ${uniqueStr} [${info.type}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检查外键约束
|
||||
console.log(`\n🔗 检查外键约束:`);
|
||||
const [foreignKeys] = await connection.execute(
|
||||
`SELECT
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
REFERENCED_TABLE_NAME,
|
||||
REFERENCED_COLUMN_NAME,
|
||||
CONSTRAINT_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE table_schema = ? AND REFERENCED_TABLE_NAME IS NOT NULL
|
||||
ORDER BY TABLE_NAME, COLUMN_NAME`,
|
||||
[dbConfig.database]
|
||||
);
|
||||
|
||||
if (foreignKeys.length > 0) {
|
||||
foreignKeys.forEach(fk => {
|
||||
console.log(` - ${fk.TABLE_NAME}.${fk.COLUMN_NAME} -> ${fk.REFERENCED_TABLE_NAME}.${fk.REFERENCED_COLUMN_NAME} [${fk.CONSTRAINT_NAME}]`);
|
||||
});
|
||||
} else {
|
||||
console.log(' ⚠️ 未发现外键约束');
|
||||
}
|
||||
|
||||
// 检查表大小
|
||||
console.log(`\n💾 检查表存储大小:`);
|
||||
const [tableSizes] = await connection.execute(
|
||||
`SELECT
|
||||
table_name,
|
||||
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'size_mb',
|
||||
table_rows
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
ORDER BY (data_length + index_length) DESC`,
|
||||
[dbConfig.database]
|
||||
);
|
||||
|
||||
tableSizes.forEach(size => {
|
||||
console.log(` - ${size.table_name}: ${size.size_mb} MB (${size.table_rows} 行)`);
|
||||
});
|
||||
|
||||
console.log('\n🎉 表结构检查完成!');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
tableCount: tables.length,
|
||||
foreignKeyCount: foreignKeys.length
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 检查表结构失败:', error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔒 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是直接运行此文件,则执行检查
|
||||
if (require.main === module) {
|
||||
checkTableStructure()
|
||||
.then((result) => {
|
||||
process.exit(result.success ? 0 : 1);
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
}
|
||||
|
||||
module.exports = { checkTableStructure };
|
||||
370
backend/scripts/create-database-schema.sql
Normal file
370
backend/scripts/create-database-schema.sql
Normal file
@@ -0,0 +1,370 @@
|
||||
-- 结伴客数据库完整结构创建脚本
|
||||
-- 创建时间: 2024年
|
||||
-- 数据库: jbkdata
|
||||
|
||||
-- 设置字符集
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- 使用数据库
|
||||
USE jbkdata;
|
||||
|
||||
-- ================================
|
||||
-- 1. 管理员表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `admins` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) NOT NULL COMMENT '用户名',
|
||||
`password` varchar(255) NOT NULL COMMENT '密码',
|
||||
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
|
||||
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
|
||||
`avatar` varchar(255) DEFAULT NULL COMMENT '头像',
|
||||
`role` enum('super_admin','admin','operator') DEFAULT 'admin' COMMENT '角色',
|
||||
`status` tinyint(1) DEFAULT 1 COMMENT '状态 1:启用 0:禁用',
|
||||
`last_login` timestamp NULL DEFAULT NULL COMMENT '最后登录时间',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`),
|
||||
KEY `idx_email` (`email`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理员表';
|
||||
|
||||
-- ================================
|
||||
-- 2. 用户表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) NOT NULL COMMENT '用户名',
|
||||
`password_hash` varchar(255) NOT NULL COMMENT '密码哈希',
|
||||
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
|
||||
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
|
||||
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
|
||||
`avatar_url` varchar(255) DEFAULT NULL COMMENT '头像URL',
|
||||
`user_type` enum('farmer','tourist','merchant','admin') DEFAULT 'tourist' COMMENT '用户类型',
|
||||
`status` enum('active','inactive','banned') DEFAULT 'active' COMMENT '状态',
|
||||
`balance` decimal(15,2) DEFAULT 0.00 COMMENT '余额',
|
||||
`points` int(11) DEFAULT 0 COMMENT '积分',
|
||||
`level` tinyint(4) DEFAULT 1 COMMENT '用户等级',
|
||||
`last_login_at` timestamp NULL DEFAULT NULL COMMENT '最后登录时间',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`),
|
||||
UNIQUE KEY `email` (`email`),
|
||||
UNIQUE KEY `phone` (`phone`),
|
||||
KEY `idx_user_type` (`user_type`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||
|
||||
-- ================================
|
||||
-- 3. 商家表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `merchants` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL COMMENT '关联用户ID',
|
||||
`business_name` varchar(100) NOT NULL COMMENT '商家名称',
|
||||
`business_type` enum('restaurant','hotel','farm','attraction','transport') NOT NULL COMMENT '商家类型',
|
||||
`description` text COMMENT '商家描述',
|
||||
`address` varchar(255) DEFAULT NULL COMMENT '地址',
|
||||
`latitude` decimal(10,8) DEFAULT NULL COMMENT '纬度',
|
||||
`longitude` decimal(11,8) DEFAULT NULL COMMENT '经度',
|
||||
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
|
||||
`business_hours` json DEFAULT NULL COMMENT '营业时间',
|
||||
`images` json DEFAULT NULL COMMENT '商家图片',
|
||||
`rating` decimal(3,2) DEFAULT 0.00 COMMENT '评分',
|
||||
`review_count` int(11) DEFAULT 0 COMMENT '评价数量',
|
||||
`status` enum('pending','approved','rejected','suspended') DEFAULT 'pending' COMMENT '状态',
|
||||
`verified_at` timestamp NULL DEFAULT NULL COMMENT '认证时间',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `idx_business_type` (`business_type`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_location` (`latitude`,`longitude`),
|
||||
CONSTRAINT `merchants_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商家表';
|
||||
|
||||
-- ================================
|
||||
-- 4. 动物表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `animals` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL COMMENT '动物名称',
|
||||
`type` enum('chicken','duck','pig','cow','sheep','rabbit','fish') NOT NULL COMMENT '动物类型',
|
||||
`breed` varchar(50) DEFAULT NULL COMMENT '品种',
|
||||
`age` int(11) DEFAULT NULL COMMENT '年龄(月)',
|
||||
`weight` decimal(8,2) DEFAULT NULL COMMENT '重量(kg)',
|
||||
`gender` enum('male','female','unknown') DEFAULT 'unknown' COMMENT '性别',
|
||||
`description` text COMMENT '描述',
|
||||
`image` varchar(255) DEFAULT NULL COMMENT '图片URL',
|
||||
`images` json DEFAULT NULL COMMENT '多张图片',
|
||||
`price` decimal(10,2) NOT NULL COMMENT '认领价格',
|
||||
`daily_cost` decimal(8,2) DEFAULT 0.00 COMMENT '每日费用',
|
||||
`location` varchar(100) DEFAULT NULL COMMENT '所在位置',
|
||||
`farmer_id` int(11) DEFAULT NULL COMMENT '农户ID',
|
||||
`status` enum('available','claimed','sold','deceased') DEFAULT 'available' COMMENT '状态',
|
||||
`health_status` enum('healthy','sick','recovering') DEFAULT 'healthy' COMMENT '健康状态',
|
||||
`vaccination_records` json DEFAULT NULL COMMENT '疫苗记录',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_type` (`type`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_farmer_id` (`farmer_id`),
|
||||
KEY `idx_price` (`price`),
|
||||
CONSTRAINT `animals_ibfk_1` FOREIGN KEY (`farmer_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='动物表';
|
||||
|
||||
-- ================================
|
||||
-- 5. 动物认领表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `animal_claims` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`claim_no` varchar(50) NOT NULL COMMENT '认领订单号',
|
||||
`animal_id` int(11) NOT NULL COMMENT '动物ID',
|
||||
`user_id` int(11) NOT NULL COMMENT '认领用户ID',
|
||||
`claim_reason` text COMMENT '认领原因',
|
||||
`claim_duration` int(11) NOT NULL COMMENT '认领时长(天)',
|
||||
`total_amount` decimal(10,2) NOT NULL COMMENT '总费用',
|
||||
`contact_info` json DEFAULT NULL COMMENT '联系信息',
|
||||
`status` enum('pending','approved','rejected','cancelled','completed') DEFAULT 'pending' COMMENT '状态',
|
||||
`reviewed_by` int(11) DEFAULT NULL COMMENT '审核人ID',
|
||||
`reviewed_at` timestamp NULL DEFAULT NULL COMMENT '审核时间',
|
||||
`review_note` text COMMENT '审核备注',
|
||||
`start_date` date DEFAULT NULL COMMENT '开始日期',
|
||||
`end_date` date DEFAULT NULL COMMENT '结束日期',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` timestamp NULL DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `claim_no` (`claim_no`),
|
||||
KEY `animal_id` (`animal_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `animal_claims_ibfk_1` FOREIGN KEY (`animal_id`) REFERENCES `animals` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `animal_claims_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='动物认领表';
|
||||
|
||||
-- ================================
|
||||
-- 6. 旅行计划表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `travel_plans` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(100) NOT NULL COMMENT '标题',
|
||||
`description` text COMMENT '描述',
|
||||
`destination` varchar(100) NOT NULL COMMENT '目的地',
|
||||
`start_date` date NOT NULL COMMENT '开始日期',
|
||||
`end_date` date NOT NULL COMMENT '结束日期',
|
||||
`max_participants` int(11) DEFAULT 20 COMMENT '最大参与人数',
|
||||
`current_participants` int(11) DEFAULT 0 COMMENT '当前参与人数',
|
||||
`price_per_person` decimal(10,2) NOT NULL COMMENT '每人价格',
|
||||
`includes` json DEFAULT NULL COMMENT '包含项目',
|
||||
`excludes` json DEFAULT NULL COMMENT '不包含项目',
|
||||
`itinerary` json DEFAULT NULL COMMENT '行程安排',
|
||||
`images` json DEFAULT NULL COMMENT '图片',
|
||||
`requirements` text COMMENT '参与要求',
|
||||
`created_by` int(11) 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 '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `created_by` (`created_by`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_start_date` (`start_date`),
|
||||
KEY `idx_destination` (`destination`),
|
||||
CONSTRAINT `travel_plans_ibfk_1` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='旅行计划表';
|
||||
|
||||
-- ================================
|
||||
-- 7. 旅行报名表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `travel_registrations` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`travel_plan_id` int(11) NOT NULL COMMENT '旅行计划ID',
|
||||
`user_id` int(11) NOT NULL COMMENT '用户ID',
|
||||
`participants` int(11) DEFAULT 1 COMMENT '参与人数',
|
||||
`message` text COMMENT '留言',
|
||||
`emergency_contact` varchar(50) DEFAULT NULL COMMENT '紧急联系人',
|
||||
`emergency_phone` varchar(20) DEFAULT NULL 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 DEFAULT NULL COMMENT '响应时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unique_registration` (`travel_plan_id`,`user_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
CONSTRAINT `travel_registrations_ibfk_1` FOREIGN KEY (`travel_plan_id`) REFERENCES `travel_plans` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `travel_registrations_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='旅行报名表';
|
||||
|
||||
-- ================================
|
||||
-- 8. 鲜花表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `flowers` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL COMMENT '花卉名称',
|
||||
`scientific_name` varchar(100) DEFAULT NULL COMMENT '学名',
|
||||
`category` enum('rose','lily','tulip','sunflower','orchid','carnation','other') NOT NULL COMMENT '花卉类别',
|
||||
`color` varchar(30) DEFAULT NULL COMMENT '颜色',
|
||||
`description` text COMMENT '描述',
|
||||
`care_instructions` text COMMENT '养护说明',
|
||||
`image` varchar(255) DEFAULT NULL COMMENT '主图片',
|
||||
`images` json DEFAULT NULL COMMENT '多张图片',
|
||||
`price` decimal(8,2) NOT NULL COMMENT '价格',
|
||||
`stock_quantity` int(11) DEFAULT 0 COMMENT '库存数量',
|
||||
`farmer_id` int(11) DEFAULT NULL COMMENT '农户ID',
|
||||
`status` enum('available','out_of_stock','discontinued') DEFAULT 'available' COMMENT '状态',
|
||||
`seasonal_availability` json DEFAULT NULL COMMENT '季节性供应',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_category` (`category`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_farmer_id` (`farmer_id`),
|
||||
KEY `idx_price` (`price`),
|
||||
CONSTRAINT `flowers_ibfk_1` FOREIGN KEY (`farmer_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='鲜花表';
|
||||
|
||||
-- ================================
|
||||
-- 9. 订单表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `orders` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`order_no` varchar(50) NOT NULL COMMENT '订单号',
|
||||
`user_id` int(11) NOT NULL COMMENT '用户ID',
|
||||
`type` enum('animal_claim','travel','flower','service') NOT NULL COMMENT '订单类型',
|
||||
`related_id` int(11) DEFAULT NULL COMMENT '关联ID',
|
||||
`title` varchar(200) NOT NULL COMMENT '订单标题',
|
||||
`description` text COMMENT '订单描述',
|
||||
`total_amount` decimal(15,2) NOT NULL COMMENT '总金额',
|
||||
`discount_amount` decimal(15,2) DEFAULT 0.00 COMMENT '优惠金额',
|
||||
`final_amount` decimal(15,2) NOT NULL COMMENT '实付金额',
|
||||
`status` enum('pending','paid','processing','shipped','completed','cancelled','refunded') DEFAULT 'pending' COMMENT '订单状态',
|
||||
`payment_status` enum('unpaid','paid','refunded','partial_refund') DEFAULT 'unpaid' COMMENT '支付状态',
|
||||
`payment_method` varchar(50) DEFAULT NULL COMMENT '支付方式',
|
||||
`payment_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
|
||||
`shipping_address` json DEFAULT NULL COMMENT '收货地址',
|
||||
`contact_info` json DEFAULT NULL COMMENT '联系信息',
|
||||
`notes` text COMMENT '备注',
|
||||
`ordered_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `order_no` (`order_no`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `idx_type` (`type`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_payment_status` (`payment_status`),
|
||||
KEY `idx_ordered_at` (`ordered_at`),
|
||||
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';
|
||||
|
||||
-- ================================
|
||||
-- 10. 支付表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `payments` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`payment_no` varchar(50) NOT NULL COMMENT '支付订单号',
|
||||
`order_id` int(11) NOT NULL COMMENT '订单ID',
|
||||
`user_id` int(11) NOT NULL COMMENT '用户ID',
|
||||
`amount` decimal(15,2) NOT NULL COMMENT '支付金额',
|
||||
`payment_method` enum('wechat','alipay','bank_card','balance') NOT NULL COMMENT '支付方式',
|
||||
`status` enum('pending','paid','failed','cancelled','refunded') DEFAULT 'pending' COMMENT '支付状态',
|
||||
`transaction_id` varchar(100) DEFAULT NULL COMMENT '第三方交易号',
|
||||
`paid_amount` decimal(15,2) DEFAULT NULL COMMENT '实际支付金额',
|
||||
`paid_at` timestamp NULL DEFAULT NULL COMMENT '支付时间',
|
||||
`failure_reason` varchar(255) DEFAULT NULL COMMENT '失败原因',
|
||||
`return_url` varchar(255) DEFAULT NULL COMMENT '返回URL',
|
||||
`notify_url` varchar(255) DEFAULT NULL COMMENT '通知URL',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` timestamp NULL DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `payment_no` (`payment_no`),
|
||||
KEY `order_id` (`order_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_payment_method` (`payment_method`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `payments_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `payments_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付表';
|
||||
|
||||
-- ================================
|
||||
-- 11. 退款表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS `refunds` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`refund_no` varchar(50) NOT NULL COMMENT '退款订单号',
|
||||
`payment_id` int(11) NOT NULL COMMENT '支付ID',
|
||||
`user_id` int(11) 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(11) DEFAULT NULL COMMENT '处理人ID',
|
||||
`processed_at` timestamp NULL DEFAULT NULL COMMENT '处理时间',
|
||||
`process_remark` text COMMENT '处理备注',
|
||||
`refund_transaction_id` varchar(100) DEFAULT NULL COMMENT '退款交易号',
|
||||
`refunded_at` timestamp NULL DEFAULT NULL COMMENT '退款完成时间',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` timestamp NULL DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `refund_no` (`refund_no`),
|
||||
KEY `payment_id` (`payment_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
CONSTRAINT `refunds_ibfk_1` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `refunds_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退款表';
|
||||
|
||||
-- ================================
|
||||
-- 12. 辅助表
|
||||
-- ================================
|
||||
|
||||
-- 邮箱验证表
|
||||
CREATE TABLE IF NOT EXISTS `email_verifications` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`email` varchar(100) NOT NULL COMMENT '邮箱',
|
||||
`code` varchar(10) NOT NULL COMMENT '验证码',
|
||||
`expires_at` timestamp NOT NULL COMMENT '过期时间',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `email` (`email`),
|
||||
KEY `idx_expires_at` (`expires_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='邮箱验证表';
|
||||
|
||||
-- 密码重置表
|
||||
CREATE TABLE IF NOT EXISTS `password_resets` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL COMMENT '用户ID',
|
||||
`token` varchar(255) NOT NULL COMMENT '重置令牌',
|
||||
`expires_at` timestamp NOT NULL COMMENT '过期时间',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `user_id` (`user_id`),
|
||||
UNIQUE KEY `token` (`token`),
|
||||
KEY `idx_expires_at` (`expires_at`),
|
||||
CONSTRAINT `password_resets_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='密码重置表';
|
||||
|
||||
-- 登录尝试表
|
||||
CREATE TABLE IF NOT EXISTS `login_attempts` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`identifier` varchar(100) NOT NULL COMMENT '标识符(用户名/邮箱/IP)',
|
||||
`attempts` int(11) DEFAULT 1 COMMENT '尝试次数',
|
||||
`last_attempt` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '最后尝试时间',
|
||||
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `identifier` (`identifier`),
|
||||
KEY `idx_last_attempt` (`last_attempt`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录尝试表';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- 创建完成提示
|
||||
SELECT '数据库表结构创建完成!' as message;
|
||||
@@ -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);
|
||||
219
backend/scripts/init-test-data-complete.js
Normal file
219
backend/scripts/init-test-data-complete.js
Normal file
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 完整测试数据初始化脚本
|
||||
* 用于开发环境创建完整的测试数据
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const config = require('../config/env');
|
||||
|
||||
async function initCompleteTestData() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🚀 开始初始化完整测试数据...');
|
||||
|
||||
// 创建数据库连接
|
||||
const dbConfig = {
|
||||
host: config.mysql.host,
|
||||
port: config.mysql.port,
|
||||
user: config.mysql.user,
|
||||
password: config.mysql.password,
|
||||
database: config.mysql.database,
|
||||
charset: config.mysql.charset,
|
||||
timezone: config.mysql.timezone
|
||||
};
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
console.log('✅ 数据库连接成功');
|
||||
|
||||
// 清理现有测试数据(可选)
|
||||
console.log('🧹 清理现有测试数据...');
|
||||
const tablesToClean = [
|
||||
'refunds', 'payments', 'orders', 'travel_registrations', 'travel_plans',
|
||||
'animal_claims', 'animals', 'flowers', 'merchants', 'users', 'admins',
|
||||
'email_verifications', 'password_resets', 'login_attempts'
|
||||
];
|
||||
|
||||
for (const table of tablesToClean) {
|
||||
try {
|
||||
await connection.execute(`DELETE FROM ${table} WHERE id > 0`);
|
||||
await connection.execute(`ALTER TABLE ${table} AUTO_INCREMENT = 1`);
|
||||
console.log(` 清理表: ${table}`);
|
||||
} catch (error) {
|
||||
console.log(` 跳过表: ${table} (${error.message})`);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 插入管理员数据
|
||||
console.log('👨💼 插入管理员数据...');
|
||||
const adminPassword = await bcrypt.hash('admin123', 10);
|
||||
const managerPassword = await bcrypt.hash('manager123', 10);
|
||||
|
||||
await connection.execute(`
|
||||
INSERT INTO admins (username, password, email, nickname, role, status) VALUES
|
||||
('admin', ?, 'admin@jiebanke.com', '超级管理员', 'super_admin', 1),
|
||||
('manager', ?, 'manager@jiebanke.com', '运营经理', 'admin', 1),
|
||||
('operator', ?, 'operator@jiebanke.com', '运营专员', 'operator', 1)
|
||||
`, [adminPassword, managerPassword, await bcrypt.hash('operator123', 10)]);
|
||||
console.log(' ✅ 管理员数据插入完成');
|
||||
|
||||
// 2. 插入用户数据
|
||||
console.log('👥 插入用户数据...');
|
||||
const userPassword = await bcrypt.hash('user123', 10);
|
||||
const farmerPassword = await bcrypt.hash('farmer123', 10);
|
||||
const merchantPassword = await bcrypt.hash('merchant123', 10);
|
||||
|
||||
await connection.execute(`
|
||||
INSERT INTO users (username, password_hash, email, phone, real_name, user_type, status, balance, points) VALUES
|
||||
('tourist1', ?, 'tourist1@jiebanke.com', '13800138001', '张三', 'tourist', 'active', 1000.00, 100),
|
||||
('tourist2', ?, 'tourist2@jiebanke.com', '13800138002', '李四', 'tourist', 'active', 500.00, 50),
|
||||
('farmer1', ?, 'farmer1@jiebanke.com', '13800138003', '王农夫', 'farmer', 'active', 2000.00, 200),
|
||||
('farmer2', ?, 'farmer2@jiebanke.com', '13800138004', '赵农民', 'farmer', 'active', 1500.00, 150),
|
||||
('merchant1', ?, 'merchant1@jiebanke.com', '13800138005', '刘老板', 'merchant', 'active', 5000.00, 500),
|
||||
('merchant2', ?, 'merchant2@jiebanke.com', '13800138006', '陈经理', 'merchant', 'active', 3000.00, 300)
|
||||
`, [userPassword, userPassword, farmerPassword, farmerPassword, merchantPassword, merchantPassword]);
|
||||
console.log(' ✅ 用户数据插入完成');
|
||||
|
||||
// 3. 插入商家数据
|
||||
console.log('🏪 插入商家数据...');
|
||||
await connection.execute(`
|
||||
INSERT INTO merchants (user_id, business_name, business_type, description, address, contact_phone, status) VALUES
|
||||
(5, '山水农家乐', 'restaurant', '提供正宗农家菜和住宿服务', '北京市密云区某某村', '13800138005', 'approved'),
|
||||
(6, '绿野仙踪度假村', 'hotel', '生态度假村,环境优美', '河北省承德市某某镇', '13800138006', 'approved')
|
||||
`);
|
||||
console.log(' ✅ 商家数据插入完成');
|
||||
|
||||
// 4. 插入动物数据
|
||||
console.log('🐷 插入动物数据...');
|
||||
await connection.execute(`
|
||||
INSERT INTO animals (name, type, breed, age, weight, gender, description, price, daily_cost, farmer_id, status) VALUES
|
||||
('小花', 'pig', '土猪', 6, 50.5, 'female', '健康活泼的小母猪', 800.00, 5.00, 3, 'available'),
|
||||
('大黄', 'chicken', '土鸡', 8, 2.5, 'male', '散养公鸡,肉质鲜美', 120.00, 2.00, 3, 'available'),
|
||||
('小白', 'sheep', '绵羊', 12, 35.0, 'female', '温顺的小绵羊', 600.00, 4.00, 4, 'available'),
|
||||
('老黑', 'cow', '黄牛', 24, 300.0, 'male', '强壮的耕牛', 2000.00, 10.00, 4, 'available'),
|
||||
('小灰', 'rabbit', '肉兔', 3, 1.8, 'female', '可爱的小兔子', 80.00, 1.50, 3, 'available')
|
||||
`);
|
||||
console.log(' ✅ 动物数据插入完成');
|
||||
|
||||
// 5. 插入鲜花数据
|
||||
console.log('🌸 插入鲜花数据...');
|
||||
await connection.execute(`
|
||||
INSERT INTO flowers (name, category, color, description, price, stock_quantity, farmer_id, status) VALUES
|
||||
('红玫瑰', 'rose', '红色', '经典红玫瑰,象征爱情', 15.00, 100, 3, 'available'),
|
||||
('白百合', 'lily', '白色', '纯洁的白百合,适合送礼', 25.00, 50, 3, 'available'),
|
||||
('黄郁金香', 'tulip', '黄色', '明亮的黄郁金香', 20.00, 80, 4, 'available'),
|
||||
('向日葵', 'sunflower', '黄色', '阳光般的向日葵', 12.00, 120, 4, 'available'),
|
||||
('粉康乃馨', 'carnation', '粉色', '温馨的粉色康乃馨', 18.00, 90, 3, 'available')
|
||||
`);
|
||||
console.log(' ✅ 鲜花数据插入完成');
|
||||
|
||||
// 6. 插入旅行计划数据
|
||||
console.log('✈️ 插入旅行计划数据...');
|
||||
await connection.execute(`
|
||||
INSERT INTO travel_plans (title, description, destination, start_date, end_date, max_participants, price_per_person, created_by, status) VALUES
|
||||
('密云水库生态游', '体验密云水库的自然风光,品尝农家美食', '北京密云', '2024-05-01', '2024-05-03', 20, 299.00, 5, 'published'),
|
||||
('承德避暑山庄文化游', '探访承德避暑山庄,了解清朝历史文化', '河北承德', '2024-06-15', '2024-06-17', 15, 599.00, 6, 'published'),
|
||||
('农场体验亲子游', '带孩子体验农场生活,学习农业知识', '北京郊区', '2024-07-01', '2024-07-02', 25, 199.00, 3, 'published')
|
||||
`);
|
||||
console.log(' ✅ 旅行计划数据插入完成');
|
||||
|
||||
// 7. 插入动物认领数据
|
||||
console.log('🐾 插入动物认领数据...');
|
||||
await connection.execute(`
|
||||
INSERT INTO animal_claims (claim_no, animal_id, user_id, claim_reason, claim_duration, total_amount, status, start_date, end_date) VALUES
|
||||
('AC202401001', 1, 1, '想体验养猪的乐趣', 30, 950.00, 'approved', '2024-04-01', '2024-04-30'),
|
||||
('AC202401002', 2, 2, '孩子喜欢小鸡', 15, 150.00, 'pending', '2024-04-15', '2024-04-29'),
|
||||
('AC202401003', 3, 1, '认领小羊作为宠物', 60, 840.00, 'approved', '2024-03-01', '2024-04-29')
|
||||
`);
|
||||
console.log(' ✅ 动物认领数据插入完成');
|
||||
|
||||
// 8. 插入旅行报名数据
|
||||
console.log('📝 插入旅行报名数据...');
|
||||
await connection.execute(`
|
||||
INSERT INTO travel_registrations (travel_plan_id, user_id, participants, message, emergency_contact, emergency_phone, status) VALUES
|
||||
(1, 1, 2, '期待这次旅行', '张三妻子', '13900139001', 'approved'),
|
||||
(1, 2, 1, '第一次参加农家游', '李四父亲', '13900139002', 'pending'),
|
||||
(2, 1, 3, '全家一起出游', '张三妻子', '13900139001', 'approved'),
|
||||
(3, 2, 2, '带孩子体验农场', '李四妻子', '13900139003', 'pending')
|
||||
`);
|
||||
console.log(' ✅ 旅行报名数据插入完成');
|
||||
|
||||
// 9. 插入订单数据
|
||||
console.log('📦 插入订单数据...');
|
||||
await connection.execute(`
|
||||
INSERT INTO orders (order_no, user_id, type, related_id, title, total_amount, final_amount, status, payment_status) VALUES
|
||||
('ORD202401001', 1, 'animal_claim', 1, '认领小花猪30天', 950.00, 950.00, 'completed', 'paid'),
|
||||
('ORD202401002', 2, 'animal_claim', 2, '认领大黄鸡15天', 150.00, 150.00, 'pending', 'unpaid'),
|
||||
('ORD202401003', 1, 'travel', 1, '密云水库生态游 2人', 598.00, 598.00, 'completed', 'paid'),
|
||||
('ORD202401004', 1, 'flower', 1, '红玫瑰 10支', 150.00, 150.00, 'completed', 'paid')
|
||||
`);
|
||||
console.log(' ✅ 订单数据插入完成');
|
||||
|
||||
// 10. 插入支付数据
|
||||
console.log('💳 插入支付数据...');
|
||||
await connection.execute(`
|
||||
INSERT INTO payments (payment_no, order_id, user_id, amount, payment_method, status, paid_amount, paid_at) VALUES
|
||||
('PAY202401001', 1, 1, 950.00, 'wechat', 'paid', 950.00, '2024-04-01 10:30:00'),
|
||||
('PAY202401002', 3, 1, 598.00, 'alipay', 'paid', 598.00, '2024-04-02 14:20:00'),
|
||||
('PAY202401003', 4, 1, 150.00, 'wechat', 'paid', 150.00, '2024-04-03 16:45:00')
|
||||
`);
|
||||
console.log(' ✅ 支付数据插入完成');
|
||||
|
||||
// 统计插入的数据
|
||||
console.log('\n📊 数据统计:');
|
||||
const tables = ['admins', 'users', 'merchants', 'animals', 'flowers', 'travel_plans', 'animal_claims', 'travel_registrations', 'orders', 'payments'];
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
const [rows] = await connection.execute(`SELECT COUNT(*) as count FROM ${table}`);
|
||||
console.log(` ${table}: ${rows[0].count} 条记录`);
|
||||
} catch (error) {
|
||||
console.log(` ${table}: 表不存在或查询失败`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n🎉 完整测试数据初始化完成!');
|
||||
console.log('📋 测试账号信息:');
|
||||
console.log(' 管理员账号:');
|
||||
console.log(' admin / admin123 (超级管理员)');
|
||||
console.log(' manager / manager123 (运营经理)');
|
||||
console.log(' operator / operator123 (运营专员)');
|
||||
console.log(' 用户账号:');
|
||||
console.log(' tourist1 / user123 (游客)');
|
||||
console.log(' tourist2 / user123 (游客)');
|
||||
console.log(' farmer1 / farmer123 (农户)');
|
||||
console.log(' farmer2 / farmer123 (农户)');
|
||||
console.log(' merchant1 / merchant123 (商家)');
|
||||
console.log(' merchant2 / merchant123 (商家)');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 初始化测试数据失败:', error.message);
|
||||
if (error.code === 'ER_NO_SUCH_TABLE') {
|
||||
console.log('💡 提示: 请先运行数据库结构创建脚本');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔒 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是直接运行此文件,则执行初始化
|
||||
if (require.main === module) {
|
||||
initCompleteTestData()
|
||||
.then(() => {
|
||||
console.log('✅ 脚本执行成功');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ 脚本执行失败:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { initCompleteTestData };
|
||||
227
backend/scripts/insert-more-test-data.js
Normal file
227
backend/scripts/insert-more-test-data.js
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 插入更多测试数据脚本
|
||||
* 为数据库添加更丰富的测试数据
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const config = require('../config/env');
|
||||
|
||||
async function insertMoreTestData() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🚀 开始插入更多测试数据...');
|
||||
|
||||
const dbConfig = {
|
||||
host: config.mysql.host,
|
||||
port: config.mysql.port,
|
||||
user: config.mysql.user,
|
||||
password: config.mysql.password,
|
||||
database: config.mysql.database,
|
||||
charset: config.mysql.charset || 'utf8mb4',
|
||||
timezone: config.mysql.timezone || '+08:00'
|
||||
};
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
console.log('✅ 数据库连接成功');
|
||||
|
||||
// 1. 插入更多用户数据
|
||||
console.log('\n👤 插入更多用户数据...');
|
||||
const newUsers = [
|
||||
['user007', '$2b$10$hash7', 'user007@example.com', '13800000007', '张小明', 'https://example.com/avatar7.jpg', 'tourist', 'active', 1000.00, 150, 2],
|
||||
['user008', '$2b$10$hash8', 'user008@example.com', '13800000008', '李小红', 'https://example.com/avatar8.jpg', 'farmer', 'active', 2500.00, 300, 3],
|
||||
['user009', '$2b$10$hash9', 'user009@example.com', '13800000009', '王小刚', 'https://example.com/avatar9.jpg', 'merchant', 'active', 5000.00, 500, 4],
|
||||
['user010', '$2b$10$hash10', 'user010@example.com', '13800000010', '赵小美', 'https://example.com/avatar10.jpg', 'tourist', 'active', 800.00, 120, 2],
|
||||
['user011', '$2b$10$hash11', 'user011@example.com', '13800000011', '刘小强', 'https://example.com/avatar11.jpg', 'farmer', 'active', 3200.00, 400, 3],
|
||||
['user012', '$2b$10$hash12', 'user012@example.com', '13800000012', '陈小丽', 'https://example.com/avatar12.jpg', 'tourist', 'active', 1500.00, 200, 2]
|
||||
];
|
||||
|
||||
for (const user of newUsers) {
|
||||
try {
|
||||
await connection.execute(
|
||||
`INSERT INTO users (username, password_hash, email, phone, real_name, avatar_url, user_type, status, balance, points, level, last_login_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
user
|
||||
);
|
||||
console.log(` ✅ 用户 ${user[0]} 插入成功`);
|
||||
} catch (error) {
|
||||
if (error.code === 'ER_DUP_ENTRY') {
|
||||
console.log(` ⚠️ 用户 ${user[0]} 已存在,跳过`);
|
||||
} else {
|
||||
console.log(` ❌ 用户 ${user[0]} 插入失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 插入更多动物数据
|
||||
console.log('\n🐄 插入更多动物数据...');
|
||||
const newAnimals = [
|
||||
['小花牛', 'cow', '荷斯坦', 24, 450.50, 'female', '温顺可爱的小花牛,喜欢在草地上悠闲地吃草', 'https://example.com/cow1.jpg', '["https://example.com/cow1_1.jpg","https://example.com/cow1_2.jpg"]', 1200.00, 15.00, '阳光农场', 2, 'available', 'healthy', '["疫苗A", "疫苗B"]'],
|
||||
['小黑猪', 'pig', '黑毛猪', 12, 80.30, 'male', '活泼好动的小黑猪,很聪明', 'https://example.com/pig1.jpg', '["https://example.com/pig1_1.jpg","https://example.com/pig1_2.jpg"]', 800.00, 8.00, '绿野农场', 3, 'available', 'healthy', '["疫苗C", "疫苗D"]'],
|
||||
['小白羊', 'sheep', '绵羊', 18, 35.20, 'female', '毛茸茸的小白羊,很温顺', 'https://example.com/sheep1.jpg', '["https://example.com/sheep1_1.jpg","https://example.com/sheep1_2.jpg"]', 600.00, 6.00, '山坡农场', 2, 'available', 'healthy', '["疫苗E"]'],
|
||||
['小黄鸡', 'chicken', '土鸡', 6, 2.50, 'female', '活泼的小黄鸡,会下蛋', 'https://example.com/chicken1.jpg', '["https://example.com/chicken1_1.jpg"]', 150.00, 2.00, '家禽农场', 3, 'available', 'healthy', '["疫苗F"]'],
|
||||
['小白鸭', 'duck', '白鸭', 8, 3.20, 'male', '游泳高手小白鸭', 'https://example.com/duck1.jpg', '["https://example.com/duck1_1.jpg"]', 200.00, 3.00, '水边农场', 2, 'available', 'healthy', '["疫苗G"]'],
|
||||
['小灰兔', 'rabbit', '灰兔', 4, 1.80, 'female', '可爱的小灰兔,爱吃胡萝卜', 'https://example.com/rabbit1.jpg', '["https://example.com/rabbit1_1.jpg"]', 120.00, 1.50, '兔子农场', 3, 'available', 'healthy', '["疫苗H"]']
|
||||
];
|
||||
|
||||
for (const animal of newAnimals) {
|
||||
try {
|
||||
await connection.execute(
|
||||
`INSERT INTO animals (name, type, breed, age, weight, gender, description, image, images, price, daily_cost, location, farmer_id, status, health_status, vaccination_records)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
animal
|
||||
);
|
||||
console.log(` ✅ 动物 ${animal[0]} 插入成功`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ 动物 ${animal[0]} 插入失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 插入更多旅行计划
|
||||
console.log('\n✈️ 插入更多旅行计划...');
|
||||
const newTravelPlans = [
|
||||
['云南大理古城深度游', '探索大理古城的历史文化,品尝当地美食,体验白族风情', '大理', '2025-04-15', '2025-04-20', 15, 2, 1800.00, '["住宿", "早餐", "导游"]', '["午餐", "晚餐", "购物"]', '["第一天:抵达大理", "第二天:古城游览", "第三天:洱海环游"]', '["https://example.com/dali1.jpg"]', '身体健康,无重大疾病', 1, 'published'],
|
||||
['西藏拉萨朝圣之旅', '神圣的西藏之旅,感受藏族文化的魅力', '拉萨', '2025-05-10', '2025-05-18', 12, 1, 3500.00, '["住宿", "三餐", "导游", "门票"]', '["个人消费", "高原反应药物"]', '["第一天:抵达拉萨适应", "第二天:布达拉宫", "第三天:大昭寺"]', '["https://example.com/lasa1.jpg"]', '身体健康,适应高原环境', 2, 'published'],
|
||||
['海南三亚海滨度假', '享受阳光沙滩,品尝海鲜美食', '三亚', '2025-03-20', '2025-03-25', 20, 5, 2200.00, '["住宿", "早餐", "接送机"]', '["午餐", "晚餐", "水上项目"]', '["第一天:抵达三亚", "第二天:天涯海角", "第三天:蜈支洲岛"]', '["https://example.com/sanya1.jpg"]', '会游泳者优先', 1, 'published'],
|
||||
['张家界奇峰探险', '探索张家界的奇峰异石,体验玻璃桥刺激', '张家界', '2025-06-01', '2025-06-05', 18, 3, 1600.00, '["住宿", "早餐", "门票", "导游"]', '["午餐", "晚餐", "索道费用"]', '["第一天:森林公园", "第二天:天门山", "第三天:玻璃桥"]', '["https://example.com/zjj1.jpg"]', '不恐高,身体健康', 2, 'published']
|
||||
];
|
||||
|
||||
for (const plan of newTravelPlans) {
|
||||
try {
|
||||
await connection.execute(
|
||||
`INSERT INTO travel_plans (title, description, destination, start_date, end_date, max_participants, current_participants, price_per_person, includes, excludes, itinerary, images, requirements, created_by, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
plan
|
||||
);
|
||||
console.log(` ✅ 旅行计划 ${plan[0]} 插入成功`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ 旅行计划 ${plan[0]} 插入失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 插入更多鲜花数据
|
||||
console.log('\n🌸 插入更多鲜花数据...');
|
||||
const newFlowers = [
|
||||
['蓝色妖姬', 'Rosa Blue', 'rose', '蓝色', '神秘优雅的蓝色玫瑰,象征珍贵的爱', '避免阳光直射,保持适当湿度', 'https://example.com/blue_rose.jpg', '["https://example.com/blue_rose1.jpg"]', 25.00, 50, 2, 'available', '["春季", "夏季"]'],
|
||||
['向日葵', 'Helianthus annuus', 'sunflower', '黄色', '阳光般灿烂的向日葵,象征希望和活力', '需要充足阳光,定期浇水', 'https://example.com/sunflower.jpg', '["https://example.com/sunflower1.jpg"]', 15.00, 80, 3, 'available', '["夏季", "秋季"]'],
|
||||
['紫色薰衣草', 'Lavandula', 'other', '紫色', '芳香怡人的薰衣草,有助于放松心情', '喜欢干燥环境,不要过度浇水', 'https://example.com/lavender.jpg', '["https://example.com/lavender1.jpg"]', 18.00, 60, 2, 'available', '["春季", "夏季"]'],
|
||||
['白色百合', 'Lilium candidum', 'lily', '白色', '纯洁高雅的白百合,象征纯真和高贵', '保持土壤湿润,避免积水', 'https://example.com/white_lily.jpg', '["https://example.com/white_lily1.jpg"]', 30.00, 40, 3, 'available', '["春季", "夏季", "秋季"]'],
|
||||
['粉色康乃馨', 'Dianthus caryophyllus', 'carnation', '粉色', '温馨的粉色康乃馨,表达感恩和关爱', '适中浇水,保持通风', 'https://example.com/pink_carnation.jpg', '["https://example.com/pink_carnation1.jpg"]', 12.00, 100, 2, 'available', '["全年"]'],
|
||||
['红色郁金香', 'Tulipa gesneriana', 'tulip', '红色', '热情的红色郁金香,象征热烈的爱情', '春季种植,夏季休眠', 'https://example.com/red_tulip.jpg', '["https://example.com/red_tulip1.jpg"]', 20.00, 70, 3, 'available', '["春季"]']
|
||||
];
|
||||
|
||||
for (const flower of newFlowers) {
|
||||
try {
|
||||
await connection.execute(
|
||||
`INSERT INTO flowers (name, scientific_name, category, color, description, care_instructions, image, images, price, stock_quantity, farmer_id, status, seasonal_availability)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
flower
|
||||
);
|
||||
console.log(` ✅ 鲜花 ${flower[0]} 插入成功`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ 鲜花 ${flower[0]} 插入失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 插入更多订单数据
|
||||
console.log('\n📦 插入更多订单数据...');
|
||||
const newOrders = [
|
||||
['ORD' + Date.now() + '001', 1, 'flower', 1, '购买蓝色妖姬', '为女朋友购买生日礼物', 75.00, 5.00, 70.00, 'paid', 'paid', 'wechat', '{"name":"张三","phone":"13800000001","address":"北京市朝阳区"}', '{"name":"张三","phone":"13800000001"}', '希望包装精美'],
|
||||
['ORD' + Date.now() + '002', 2, 'animal_claim', 2, '认领小黑猪', '想要认领一只可爱的小猪', 2400.00, 0.00, 2400.00, 'processing', 'paid', 'alipay', null, '{"name":"李四","phone":"13800000002"}', '希望能经常看到小猪的照片'],
|
||||
['ORD' + Date.now() + '003', 3, 'travel', 1, '云南大理古城深度游', '参加大理旅游团', 1800.00, 100.00, 1700.00, 'confirmed', 'paid', 'wechat', null, '{"name":"王五","phone":"13800000003"}', '素食主义者'],
|
||||
['ORD' + Date.now() + '004', 4, 'flower', 3, '购买向日葵花束', '办公室装饰用花', 45.00, 0.00, 45.00, 'shipped', 'paid', 'bank_card', '{"name":"赵六","phone":"13800000004","address":"上海市浦东新区"}', '{"name":"赵六","phone":"13800000004"}', '需要开发票']
|
||||
];
|
||||
|
||||
for (const order of newOrders) {
|
||||
try {
|
||||
await connection.execute(
|
||||
`INSERT INTO orders (order_no, user_id, type, related_id, title, description, total_amount, discount_amount, final_amount, status, payment_status, payment_method, shipping_address, contact_info, notes, payment_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
order
|
||||
);
|
||||
console.log(` ✅ 订单 ${order[0]} 插入成功`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ 订单 ${order[0]} 插入失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 插入更多动物认领记录
|
||||
console.log('\n🐾 插入更多动物认领记录...');
|
||||
const newClaims = [
|
||||
['CLAIM' + Date.now() + '001', 6, 1, '喜欢小动物,想要体验农场生活', 90, 1080.00, '{"name":"张三","phone":"13800000001","email":"user001@example.com"}', 'approved', 1, '2025-01-15', '2025-04-15'],
|
||||
['CLAIM' + Date.now() + '002', 7, 2, '想给孩子一个特别的生日礼物', 60, 480.00, '{"name":"李四","phone":"13800000002","email":"user002@example.com"}', 'approved', 1, '2025-02-01', '2025-04-01'],
|
||||
['CLAIM' + Date.now() + '003', 8, 3, '支持农场发展,保护动物', 120, 720.00, '{"name":"王五","phone":"13800000003","email":"user003@example.com"}', 'pending', null, '2025-03-01', '2025-07-01']
|
||||
];
|
||||
|
||||
for (const claim of newClaims) {
|
||||
try {
|
||||
await connection.execute(
|
||||
`INSERT INTO animal_claims (claim_no, animal_id, user_id, claim_reason, claim_duration, total_amount, contact_info, status, reviewed_by, start_date, end_date, reviewed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
claim
|
||||
);
|
||||
console.log(` ✅ 认领记录 ${claim[0]} 插入成功`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ 认领记录 ${claim[0]} 插入失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 插入旅行报名记录
|
||||
console.log('\n🎒 插入更多旅行报名记录...');
|
||||
const newRegistrations = [
|
||||
[4, 1, 2, '我和朋友一起参加,希望能安排同房间', '张三', '13900000001', 'approved'],
|
||||
[5, 2, 1, '一个人旅行,希望能认识新朋友', '李四', '13900000002', 'approved'],
|
||||
[6, 3, 3, '全家出游,有老人和小孩', '王五', '13900000003', 'pending'],
|
||||
[7, 4, 1, '摄影爱好者,希望多拍照', '赵六', '13900000004', 'approved']
|
||||
];
|
||||
|
||||
for (const reg of newRegistrations) {
|
||||
try {
|
||||
await connection.execute(
|
||||
`INSERT INTO travel_registrations (travel_plan_id, user_id, participants, message, emergency_contact, emergency_phone, status, responded_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
reg
|
||||
);
|
||||
console.log(` ✅ 旅行报名记录插入成功`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ 旅行报名记录插入失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 统计最终数据
|
||||
console.log('\n📊 统计最终数据量...');
|
||||
const tables = ['users', 'animals', 'travel_plans', 'flowers', 'orders', 'animal_claims', 'travel_registrations'];
|
||||
|
||||
for (const table of tables) {
|
||||
const [result] = await connection.execute(`SELECT COUNT(*) AS count FROM ${table}`);
|
||||
console.log(` 📋 ${table}: ${result[0].count} 条记录`);
|
||||
}
|
||||
|
||||
console.log('\n🎉 测试数据插入完成!');
|
||||
console.log('✅ 数据库现在包含了丰富的测试数据,可以进行各种功能测试');
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 插入测试数据失败:', error.message);
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔒 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是直接运行此文件,则执行插入
|
||||
if (require.main === module) {
|
||||
insertMoreTestData()
|
||||
.then((result) => {
|
||||
process.exit(result.success ? 0 : 1);
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
}
|
||||
|
||||
module.exports = { insertMoreTestData };
|
||||
70
backend/scripts/payments_table.sql
Normal file
70
backend/scripts/payments_table.sql
Normal file
@@ -0,0 +1,70 @@
|
||||
-- 支付订单表
|
||||
CREATE TABLE IF NOT EXISTS `payments` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '支付订单ID',
|
||||
`payment_no` varchar(64) NOT NULL COMMENT '支付订单号',
|
||||
`order_id` int(11) NOT NULL COMMENT '关联订单ID',
|
||||
`user_id` int(11) NOT NULL COMMENT '用户ID',
|
||||
`amount` decimal(10,2) NOT NULL COMMENT '支付金额',
|
||||
`paid_amount` decimal(10,2) DEFAULT NULL COMMENT '实际支付金额',
|
||||
`payment_method` enum('wechat','alipay','balance') NOT NULL COMMENT '支付方式:wechat-微信支付,alipay-支付宝,balance-余额支付',
|
||||
`status` enum('pending','paid','failed','refunded','cancelled') NOT NULL DEFAULT 'pending' COMMENT '支付状态:pending-待支付,paid-已支付,failed-支付失败,refunded-已退款,cancelled-已取消',
|
||||
`transaction_id` varchar(128) DEFAULT NULL COMMENT '第三方交易号',
|
||||
`return_url` varchar(255) DEFAULT NULL COMMENT '支付成功回调地址',
|
||||
`notify_url` varchar(255) DEFAULT NULL COMMENT '异步通知地址',
|
||||
`paid_at` datetime DEFAULT NULL COMMENT '支付时间',
|
||||
`failure_reason` varchar(255) DEFAULT NULL COMMENT '失败原因',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_payment_no` (`payment_no`),
|
||||
KEY `idx_order_id` (`order_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_payment_method` (`payment_method`),
|
||||
KEY `idx_transaction_id` (`transaction_id`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
KEY `idx_deleted_at` (`deleted_at`),
|
||||
CONSTRAINT `fk_payments_order_id` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_payments_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付订单表';
|
||||
|
||||
-- 退款记录表
|
||||
CREATE TABLE IF NOT EXISTS `refunds` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '退款ID',
|
||||
`refund_no` varchar(64) NOT NULL COMMENT '退款订单号',
|
||||
`payment_id` int(11) NOT NULL COMMENT '支付订单ID',
|
||||
`user_id` int(11) NOT NULL COMMENT '用户ID',
|
||||
`refund_amount` decimal(10,2) NOT NULL COMMENT '退款金额',
|
||||
`refund_reason` varchar(500) NOT NULL COMMENT '退款原因',
|
||||
`status` enum('pending','approved','rejected','completed') NOT NULL DEFAULT 'pending' COMMENT '退款状态:pending-待处理,approved-已同意,rejected-已拒绝,completed-已完成',
|
||||
`processed_by` int(11) DEFAULT NULL COMMENT '处理人ID',
|
||||
`process_remark` varchar(500) DEFAULT NULL COMMENT '处理备注',
|
||||
`refund_transaction_id` varchar(128) DEFAULT NULL COMMENT '退款交易号',
|
||||
`processed_at` datetime DEFAULT NULL COMMENT '处理时间',
|
||||
`refunded_at` datetime DEFAULT NULL COMMENT '退款完成时间',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_refund_no` (`refund_no`),
|
||||
KEY `idx_payment_id` (`payment_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_processed_by` (`processed_by`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
KEY `idx_deleted_at` (`deleted_at`),
|
||||
CONSTRAINT `fk_refunds_payment_id` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_refunds_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_refunds_processed_by` FOREIGN KEY (`processed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退款记录表';
|
||||
|
||||
-- 插入示例数据(可选)
|
||||
-- INSERT INTO `payments` (`payment_no`, `order_id`, `user_id`, `amount`, `payment_method`, `status`) VALUES
|
||||
-- ('PAY202401010001', 1, 1, 299.00, 'wechat', 'pending'),
|
||||
-- ('PAY202401010002', 2, 2, 199.00, 'alipay', 'paid');
|
||||
|
||||
-- 创建索引优化查询性能
|
||||
CREATE INDEX `idx_payments_user_status` ON `payments` (`user_id`, `status`);
|
||||
CREATE INDEX `idx_payments_method_status` ON `payments` (`payment_method`, `status`);
|
||||
CREATE INDEX `idx_refunds_user_status` ON `refunds` (`user_id`, `status`);
|
||||
124
backend/scripts/test-backend-connection.js
Normal file
124
backend/scripts/test-backend-connection.js
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 后端数据库连接测试脚本
|
||||
* 测试backend中的数据库连接配置是否正常工作
|
||||
*/
|
||||
|
||||
const { testConnection, query } = require('../src/config/database');
|
||||
|
||||
async function testBackendConnection() {
|
||||
console.log('🚀 测试后端数据库连接配置...');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
try {
|
||||
// 测试数据库连接
|
||||
console.log('🔍 测试数据库连接...');
|
||||
const isConnected = await testConnection();
|
||||
|
||||
if (!isConnected) {
|
||||
console.error('❌ 数据库连接失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 测试查询功能
|
||||
console.log('🔍 测试查询功能...');
|
||||
const testResult = await query('SELECT 1 + 1 as result, NOW() as server_time');
|
||||
console.log(`✅ 查询测试成功: ${testResult[0].result}, 时间: ${testResult[0].server_time}`);
|
||||
|
||||
// 测试管理员数据查询
|
||||
console.log('🔍 测试管理员数据查询...');
|
||||
const admins = await query('SELECT id, username, role, status FROM admins LIMIT 3');
|
||||
console.log(`📊 管理员数据: ${admins.length} 条记录`);
|
||||
admins.forEach(admin => {
|
||||
console.log(` - ID:${admin.id} ${admin.username} (${admin.role}, 状态:${admin.status})`);
|
||||
});
|
||||
|
||||
// 测试用户数据查询
|
||||
console.log('🔍 测试用户数据查询...');
|
||||
const users = await query('SELECT id, username, user_type, status, balance FROM users LIMIT 5');
|
||||
console.log(`📊 用户数据: ${users.length} 条记录`);
|
||||
users.forEach(user => {
|
||||
console.log(` - ID:${user.id} ${user.username} (${user.user_type}, 余额:${user.balance})`);
|
||||
});
|
||||
|
||||
// 测试动物数据查询
|
||||
console.log('🔍 测试动物数据查询...');
|
||||
const animals = await query('SELECT id, name, type, price, status FROM animals LIMIT 5');
|
||||
console.log(`📊 动物数据: ${animals.length} 条记录`);
|
||||
animals.forEach(animal => {
|
||||
console.log(` - ID:${animal.id} ${animal.name} (${animal.type}, 价格:${animal.price})`);
|
||||
});
|
||||
|
||||
// 测试订单数据查询
|
||||
console.log('🔍 测试订单数据查询...');
|
||||
const orders = await query(`
|
||||
SELECT o.id, o.order_no, o.type, o.final_amount, o.status, u.username
|
||||
FROM orders o
|
||||
LEFT JOIN users u ON o.user_id = u.id
|
||||
LIMIT 5
|
||||
`);
|
||||
console.log(`📊 订单数据: ${orders.length} 条记录`);
|
||||
orders.forEach(order => {
|
||||
console.log(` - ${order.order_no} (${order.type}, ¥${order.final_amount}, ${order.status}) - ${order.username}`);
|
||||
});
|
||||
|
||||
// 测试支付数据查询
|
||||
console.log('🔍 测试支付数据查询...');
|
||||
const payments = await query(`
|
||||
SELECT p.id, p.payment_no, p.amount, p.payment_method, p.status, u.username
|
||||
FROM payments p
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
LIMIT 5
|
||||
`);
|
||||
console.log(`📊 支付数据: ${payments.length} 条记录`);
|
||||
payments.forEach(payment => {
|
||||
console.log(` - ${payment.payment_no} (¥${payment.amount}, ${payment.payment_method}, ${payment.status}) - ${payment.username}`);
|
||||
});
|
||||
|
||||
// 统计数据
|
||||
console.log('\n📊 数据库统计:');
|
||||
const stats = await query(`
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM admins) as admin_count,
|
||||
(SELECT COUNT(*) FROM users) as user_count,
|
||||
(SELECT COUNT(*) FROM merchants) as merchant_count,
|
||||
(SELECT COUNT(*) FROM animals) as animal_count,
|
||||
(SELECT COUNT(*) FROM flowers) as flower_count,
|
||||
(SELECT COUNT(*) FROM travel_plans) as travel_plan_count,
|
||||
(SELECT COUNT(*) FROM orders) as order_count,
|
||||
(SELECT COUNT(*) FROM payments) as payment_count
|
||||
`);
|
||||
|
||||
const stat = stats[0];
|
||||
console.log(` 管理员: ${stat.admin_count} 个`);
|
||||
console.log(` 用户: ${stat.user_count} 个`);
|
||||
console.log(` 商家: ${stat.merchant_count} 个`);
|
||||
console.log(` 动物: ${stat.animal_count} 个`);
|
||||
console.log(` 鲜花: ${stat.flower_count} 个`);
|
||||
console.log(` 旅行计划: ${stat.travel_plan_count} 个`);
|
||||
console.log(` 订单: ${stat.order_count} 个`);
|
||||
console.log(` 支付记录: ${stat.payment_count} 个`);
|
||||
|
||||
console.log('\n🎉 后端数据库连接测试完成!');
|
||||
console.log('✅ 所有功能正常');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 后端数据库连接测试失败:', error.message);
|
||||
console.error('🔍 错误详情:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是直接运行此文件,则执行测试
|
||||
if (require.main === module) {
|
||||
testBackendConnection()
|
||||
.then((success) => {
|
||||
process.exit(success ? 0 : 1);
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
}
|
||||
|
||||
module.exports = { testBackendConnection };
|
||||
204
backend/scripts/test-database-connection-fixed.js
Normal file
204
backend/scripts/test-database-connection-fixed.js
Normal file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 数据库连接测试脚本 - 修复版
|
||||
* 用于验证MySQL数据库连接配置的正确性
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const config = require('../config/env');
|
||||
|
||||
async function testDatabaseConnection() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🚀 开始数据库连接测试...');
|
||||
console.log(`📊 环境: ${process.env.NODE_ENV || 'development'}`);
|
||||
|
||||
// 使用env.js中的mysql配置
|
||||
const dbConfig = {
|
||||
host: config.mysql.host,
|
||||
port: config.mysql.port,
|
||||
user: config.mysql.user,
|
||||
password: config.mysql.password,
|
||||
database: config.mysql.database,
|
||||
charset: config.mysql.charset,
|
||||
timezone: config.mysql.timezone
|
||||
};
|
||||
|
||||
console.log(`🔗 连接信息: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`);
|
||||
console.log('='.repeat(50));
|
||||
|
||||
// 测试连接
|
||||
console.log('🔍 测试数据库连接...');
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
console.log('✅ 数据库连接成功');
|
||||
|
||||
// 测试查询
|
||||
console.log('🔍 测试基本查询...');
|
||||
const [rows] = await connection.execute('SELECT 1 + 1 AS result, NOW() as current_time');
|
||||
console.log(`✅ 查询测试成功: ${rows[0].result}, 服务器时间: ${rows[0].current_time}`);
|
||||
|
||||
// 检查数据库版本
|
||||
console.log('🔍 检查数据库版本...');
|
||||
const [versionRows] = await connection.execute('SELECT VERSION() as version');
|
||||
console.log(`📊 MySQL版本: ${versionRows[0].version}`);
|
||||
|
||||
// 检查数据库字符集
|
||||
console.log('🔍 检查数据库字符集...');
|
||||
const [charsetRows] = await connection.execute(
|
||||
'SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?',
|
||||
[dbConfig.database]
|
||||
);
|
||||
if (charsetRows.length > 0) {
|
||||
console.log(`📝 数据库字符集: ${charsetRows[0].DEFAULT_CHARACTER_SET_NAME}`);
|
||||
console.log(`📝 数据库排序规则: ${charsetRows[0].DEFAULT_COLLATION_NAME}`);
|
||||
}
|
||||
|
||||
// 检查表结构
|
||||
console.log('🔍 检查核心表结构...');
|
||||
const tablesToCheck = [
|
||||
'admins', 'users', 'merchants', 'orders', 'payments',
|
||||
'animals', 'animal_claims', 'travel_plans', 'travel_registrations',
|
||||
'flowers', 'flower_orders'
|
||||
];
|
||||
|
||||
const existingTables = [];
|
||||
const missingTables = [];
|
||||
|
||||
for (const table of tablesToCheck) {
|
||||
try {
|
||||
const [tableInfo] = await connection.execute(
|
||||
`SELECT COUNT(*) as count FROM information_schema.tables
|
||||
WHERE table_schema = ? AND table_name = ?`,
|
||||
[dbConfig.database, table]
|
||||
);
|
||||
|
||||
if (tableInfo[0].count > 0) {
|
||||
console.log(`✅ 表存在: ${table}`);
|
||||
existingTables.push(table);
|
||||
|
||||
// 检查表记录数
|
||||
const [countRows] = await connection.execute(`SELECT COUNT(*) as count FROM ${table}`);
|
||||
console.log(` 📊 记录数: ${countRows[0].count}`);
|
||||
} else {
|
||||
console.log(`⚠️ 表不存在: ${table}`);
|
||||
missingTables.push(table);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ 检查表失败: ${table} - ${error.message}`);
|
||||
missingTables.push(table);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查管理员表数据
|
||||
if (existingTables.includes('admins')) {
|
||||
console.log('🔍 检查管理员数据...');
|
||||
try {
|
||||
const [adminCount] = await connection.execute('SELECT COUNT(*) as count FROM admins');
|
||||
console.log(`📊 管理员记录数: ${adminCount[0].count}`);
|
||||
|
||||
if (adminCount[0].count > 0) {
|
||||
const [admins] = await connection.execute('SELECT username, role, status FROM admins LIMIT 5');
|
||||
console.log('👥 管理员样例:');
|
||||
admins.forEach(admin => {
|
||||
console.log(` - ${admin.username} (${admin.role}, 状态: ${admin.status})`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ 检查管理员数据失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户表数据
|
||||
if (existingTables.includes('users')) {
|
||||
console.log('🔍 检查用户数据...');
|
||||
try {
|
||||
const [userCount] = await connection.execute('SELECT COUNT(*) as count FROM users');
|
||||
console.log(`📊 用户记录数: ${userCount[0].count}`);
|
||||
|
||||
if (userCount[0].count > 0) {
|
||||
const [users] = await connection.execute('SELECT username, user_type, status FROM users LIMIT 5');
|
||||
console.log('👤 用户样例:');
|
||||
users.forEach(user => {
|
||||
console.log(` - ${user.username} (${user.user_type || '未知'}, 状态: ${user.status})`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ 检查用户数据失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查连接池配置
|
||||
console.log('🔍 检查连接配置...');
|
||||
console.log(`📈 连接池限制: ${config.mysql.connectionLimit || 10}`);
|
||||
console.log(`🔤 字符集: ${config.mysql.charset}`);
|
||||
console.log(`⏰ 时区: ${config.mysql.timezone}`);
|
||||
|
||||
console.log('\n📋 数据库状态总结:');
|
||||
console.log(`✅ 存在的表: ${existingTables.length}/${tablesToCheck.length}`);
|
||||
if (missingTables.length > 0) {
|
||||
console.log(`⚠️ 缺失的表: ${missingTables.join(', ')}`);
|
||||
console.log('💡 建议运行数据库迁移脚本创建缺失的表');
|
||||
}
|
||||
|
||||
console.log('\n🎉 数据库连接测试完成!');
|
||||
console.log('✅ 数据库连接正常');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
existingTables,
|
||||
missingTables,
|
||||
dbConfig: {
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
database: dbConfig.database,
|
||||
user: dbConfig.user
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库连接测试失败:', error.message);
|
||||
console.error('💡 可能的原因:');
|
||||
console.error(' - 数据库服务未启动');
|
||||
console.error(' - 连接配置错误');
|
||||
console.error(' - 网络连接问题');
|
||||
console.error(' - 数据库权限不足');
|
||||
console.error(' - 防火墙限制');
|
||||
console.error(' - IP地址未授权');
|
||||
|
||||
if (error.code) {
|
||||
console.error(`🔍 错误代码: ${error.code}`);
|
||||
}
|
||||
|
||||
console.error('🔍 连接详情:', {
|
||||
host: config.mysql.host,
|
||||
port: config.mysql.port,
|
||||
user: config.mysql.user,
|
||||
database: config.mysql.database
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
code: error.code
|
||||
};
|
||||
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔒 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是直接运行此文件,则执行测试
|
||||
if (require.main === module) {
|
||||
testDatabaseConnection()
|
||||
.then((result) => {
|
||||
process.exit(result.success ? 0 : 1);
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
}
|
||||
|
||||
module.exports = { testDatabaseConnection };
|
||||
@@ -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导入
|
||||
|
||||
|
||||
165
backend/scripts/test-db-connection.js
Normal file
165
backend/scripts/test-db-connection.js
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 数据库连接测试脚本 - 简化版
|
||||
* 用于验证MySQL数据库连接配置的正确性
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const config = require('../config/env');
|
||||
|
||||
async function testDatabaseConnection() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
console.log('🚀 开始数据库连接测试...');
|
||||
console.log(`📊 环境: ${process.env.NODE_ENV || 'development'}`);
|
||||
|
||||
// 使用env.js中的mysql配置
|
||||
const dbConfig = {
|
||||
host: config.mysql.host,
|
||||
port: config.mysql.port,
|
||||
user: config.mysql.user,
|
||||
password: config.mysql.password,
|
||||
database: config.mysql.database,
|
||||
charset: config.mysql.charset || 'utf8mb4',
|
||||
timezone: config.mysql.timezone || '+08:00'
|
||||
};
|
||||
|
||||
console.log(`🔗 连接信息: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`);
|
||||
console.log('='.repeat(50));
|
||||
|
||||
// 测试连接
|
||||
console.log('🔍 测试数据库连接...');
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
console.log('✅ 数据库连接成功');
|
||||
|
||||
// 测试基本查询
|
||||
console.log('🔍 测试基本查询...');
|
||||
const [rows] = await connection.execute('SELECT 1 + 1 AS result');
|
||||
console.log(`✅ 查询测试成功: ${rows[0].result}`);
|
||||
|
||||
// 检查数据库版本
|
||||
console.log('🔍 检查数据库版本...');
|
||||
const [versionRows] = await connection.execute('SELECT VERSION() AS version');
|
||||
console.log(`📊 MySQL版本: ${versionRows[0].version}`);
|
||||
|
||||
// 检查当前时间
|
||||
console.log('🔍 检查服务器时间...');
|
||||
const [timeRows] = await connection.execute('SELECT NOW() AS server_time');
|
||||
console.log(`⏰ 服务器时间: ${timeRows[0].server_time}`);
|
||||
|
||||
// 检查数据库字符集
|
||||
console.log('🔍 检查数据库字符集...');
|
||||
const [charsetRows] = await connection.execute(
|
||||
'SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?',
|
||||
[dbConfig.database]
|
||||
);
|
||||
if (charsetRows.length > 0) {
|
||||
console.log(`📝 数据库字符集: ${charsetRows[0].DEFAULT_CHARACTER_SET_NAME}`);
|
||||
console.log(`📝 数据库排序规则: ${charsetRows[0].DEFAULT_COLLATION_NAME}`);
|
||||
}
|
||||
|
||||
// 检查表结构
|
||||
console.log('🔍 检查核心表结构...');
|
||||
const tablesToCheck = [
|
||||
'admins', 'users', 'merchants', 'orders', 'payments',
|
||||
'animals', 'animal_claims', 'travel_plans', 'travel_registrations',
|
||||
'flowers', 'refunds'
|
||||
];
|
||||
|
||||
const existingTables = [];
|
||||
const missingTables = [];
|
||||
|
||||
for (const table of tablesToCheck) {
|
||||
try {
|
||||
const [tableInfo] = await connection.execute(
|
||||
`SELECT COUNT(*) AS count FROM information_schema.tables
|
||||
WHERE table_schema = ? AND table_name = ?`,
|
||||
[dbConfig.database, table]
|
||||
);
|
||||
|
||||
if (tableInfo[0].count > 0) {
|
||||
console.log(`✅ 表存在: ${table}`);
|
||||
existingTables.push(table);
|
||||
|
||||
// 检查表记录数
|
||||
const [countRows] = await connection.execute(`SELECT COUNT(*) AS count FROM ${table}`);
|
||||
console.log(` 📊 记录数: ${countRows[0].count}`);
|
||||
} else {
|
||||
console.log(`⚠️ 表不存在: ${table}`);
|
||||
missingTables.push(table);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ 检查表失败: ${table} - ${error.message}`);
|
||||
missingTables.push(table);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n📋 数据库状态总结:');
|
||||
console.log(`✅ 存在的表: ${existingTables.length}/${tablesToCheck.length}`);
|
||||
if (missingTables.length > 0) {
|
||||
console.log(`⚠️ 缺失的表: ${missingTables.join(', ')}`);
|
||||
console.log('💡 建议运行数据库迁移脚本创建缺失的表');
|
||||
}
|
||||
|
||||
console.log('\n🎉 数据库连接测试完成!');
|
||||
console.log('✅ 数据库连接正常');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
existingTables,
|
||||
missingTables,
|
||||
dbConfig: {
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
database: dbConfig.database,
|
||||
user: dbConfig.user
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库连接测试失败:', error.message);
|
||||
console.error('💡 可能的原因:');
|
||||
console.error(' - 数据库服务未启动');
|
||||
console.error(' - 连接配置错误');
|
||||
console.error(' - 网络连接问题');
|
||||
console.error(' - 数据库权限不足');
|
||||
console.error(' - 防火墙限制');
|
||||
console.error(' - IP地址未授权');
|
||||
|
||||
if (error.code) {
|
||||
console.error(`🔍 错误代码: ${error.code}`);
|
||||
}
|
||||
|
||||
console.error('🔍 连接详情:', {
|
||||
host: config.mysql.host,
|
||||
port: config.mysql.port,
|
||||
user: config.mysql.user,
|
||||
database: config.mysql.database
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
code: error.code
|
||||
};
|
||||
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('🔒 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是直接运行此文件,则执行测试
|
||||
if (require.main === module) {
|
||||
testDatabaseConnection()
|
||||
.then((result) => {
|
||||
process.exit(result.success ? 0 : 1);
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
}
|
||||
|
||||
module.exports = { testDatabaseConnection };
|
||||
@@ -15,19 +15,25 @@ const { globalErrorHandler, notFound } = require('./utils/errors');
|
||||
// 检查是否为无数据库模式
|
||||
const NO_DB_MODE = process.env.NO_DB_MODE === 'true';
|
||||
|
||||
let authRoutes, userRoutes, travelRoutes, animalRoutes, orderRoutes, adminRoutes;
|
||||
let authRoutes, userRoutes, travelRoutes, animalRoutes, orderRoutes, adminRoutes, travelRegistrationRoutes, promotionRoutes, merchantRoutes, travelsRoutes;
|
||||
|
||||
// 路由导入 - 根据是否为无数据库模式决定是否导入实际路由
|
||||
// 路由导入
|
||||
if (NO_DB_MODE) {
|
||||
console.log('⚠️ 无数据库模式:将使用模拟路由');
|
||||
} else {
|
||||
// 路由导入
|
||||
console.log('✅ 数据库模式:加载实际路由');
|
||||
authRoutes = require('./routes/auth');
|
||||
userRoutes = require('./routes/user');
|
||||
travelRoutes = require('./routes/travel');
|
||||
travelsRoutes = require('./routes/travels'); // 新增travels路由
|
||||
animalRoutes = require('./routes/animal');
|
||||
orderRoutes = require('./routes/order');
|
||||
adminRoutes = require('./routes/admin'); // 新增管理员路由
|
||||
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();
|
||||
@@ -41,8 +47,20 @@ app.use(helmet());
|
||||
app.use(cors({
|
||||
origin: process.env.NODE_ENV === 'production'
|
||||
? ['https://your-domain.com']
|
||||
: ['https://www.jiebanke.com', 'https://admin.jiebanke.com', 'https://webapi.jiebanke.com'],
|
||||
credentials: true
|
||||
: [
|
||||
'https://www.jiebanke.com',
|
||||
'https://admin.jiebanke.com',
|
||||
'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:3200' // 备用地址
|
||||
],
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
|
||||
}));
|
||||
|
||||
// 请求日志
|
||||
@@ -103,6 +121,29 @@ app.get('/health', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// API根路由
|
||||
app.get('/api/v1', (req, res) => {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '杰伴客API服务运行正常',
|
||||
version: '1.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
endpoints: {
|
||||
auth: '/api/v1/auth',
|
||||
users: '/api/v1/users',
|
||||
travel: '/api/v1/travel',
|
||||
animals: '/api/v1/animals',
|
||||
orders: '/api/v1/orders',
|
||||
payments: '/api/v1/payments',
|
||||
animalClaims: '/api/v1/animal-claims',
|
||||
admin: '/api/v1/admin',
|
||||
travelRegistration: '/api/v1/travel-registration',
|
||||
promotion: '/api/v1/promotion'
|
||||
},
|
||||
documentation: 'https://webapi.jiebanke.com/api-docs'
|
||||
});
|
||||
});
|
||||
|
||||
// 系统统计路由
|
||||
app.get('/system-stats', (req, res) => {
|
||||
const stats = {
|
||||
@@ -177,21 +218,66 @@ if (NO_DB_MODE) {
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api/v1/travel-registration', (req, res) => {
|
||||
res.status(503).json({
|
||||
success: false,
|
||||
message: '当前为无数据库模式,旅行报名功能不可用'
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api/v1/payments', (req, res) => {
|
||||
res.status(503).json({
|
||||
success: false,
|
||||
message: '当前为无数据库模式,支付功能不可用'
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api/v1/animal-claims', (req, res) => {
|
||||
res.status(503).json({
|
||||
success: false,
|
||||
message: '当前为无数据库模式,动物认领功能不可用'
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api/v1/admin', (req, res) => {
|
||||
res.status(503).json({
|
||||
success: false,
|
||||
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);
|
||||
app.use('/api/v1/users', userRoutes);
|
||||
app.use('/api/v1/travel', travelRoutes);
|
||||
app.use('/api/v1/travels', travelsRoutes); // 新增travels路由
|
||||
app.use('/api/v1/animals', animalRoutes);
|
||||
app.use('/api/v1/orders', orderRoutes);
|
||||
app.use('/api/v1/payments', paymentRoutes);
|
||||
// 动物认领路由
|
||||
app.use('/api/v1/animal-claims', animalClaimRoutes);
|
||||
// 管理员路由
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
576
backend/src/controllers/admin.js
Normal file
576
backend/src/controllers/admin.js
Normal file
@@ -0,0 +1,576 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Admin = require('../models/admin');
|
||||
const UserMySQL = require('../models/UserMySQL');
|
||||
const db = require('../config/database');
|
||||
const { AppError } = require('../utils/errors');
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
*/
|
||||
exports.login = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '用户名和密码不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
// 查找管理员
|
||||
const admin = await Admin.findByUsername(username);
|
||||
if (!admin) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
code: 401,
|
||||
message: '用户名或密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isValidPassword = await bcrypt.compare(password, admin.password);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
code: 401,
|
||||
message: '用户名或密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查账号状态
|
||||
if (admin.status !== 1) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
code: 401,
|
||||
message: '账号已被禁用'
|
||||
});
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
role: admin.role
|
||||
},
|
||||
process.env.JWT_SECRET || 'admin-secret-key',
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
// 更新最后登录时间
|
||||
await admin.updateLastLogin();
|
||||
|
||||
// 返回登录成功信息
|
||||
const adminInfo = {
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
email: admin.email,
|
||||
nickname: admin.nickname,
|
||||
avatar: admin.avatar,
|
||||
role: admin.role,
|
||||
status: admin.status,
|
||||
last_login: admin.last_login,
|
||||
created_at: admin.created_at,
|
||||
updated_at: admin.updated_at
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
admin: adminInfo,
|
||||
token
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前管理员信息
|
||||
*/
|
||||
exports.getProfile = async (req, res, next) => {
|
||||
try {
|
||||
const admin = req.admin;
|
||||
|
||||
const adminInfo = {
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
email: admin.email,
|
||||
nickname: admin.nickname,
|
||||
avatar: admin.avatar,
|
||||
role: admin.role,
|
||||
status: admin.status,
|
||||
last_login: admin.last_login,
|
||||
created_at: admin.created_at,
|
||||
updated_at: admin.updated_at
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
admin: adminInfo
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取仪表板数据
|
||||
*/
|
||||
exports.getDashboard = async (req, res, next) => {
|
||||
try {
|
||||
// 获取统计数据
|
||||
const statistics = await getDashboardStatistics();
|
||||
|
||||
// 获取最近活动
|
||||
const recentActivities = await getRecentActivities();
|
||||
|
||||
// 获取系统信息
|
||||
const systemInfo = getSystemInfo();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
statistics,
|
||||
recentActivities,
|
||||
systemInfo
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户增长数据
|
||||
*/
|
||||
exports.getUserGrowth = async (req, res, next) => {
|
||||
try {
|
||||
const days = parseInt(req.query.days) || 7;
|
||||
|
||||
// 生成日期范围
|
||||
const dates = [];
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
dates.push(date.toISOString().split('T')[0]);
|
||||
}
|
||||
|
||||
// 查询每日新增用户数据
|
||||
const growthData = [];
|
||||
let totalNewUsers = 0;
|
||||
|
||||
for (const date of dates) {
|
||||
try {
|
||||
const rows = await db.query(
|
||||
'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = ?',
|
||||
[date]
|
||||
);
|
||||
const newUsers = rows[0].count;
|
||||
totalNewUsers += newUsers;
|
||||
|
||||
// 获取截止到该日期的总用户数
|
||||
const totalRows = await db.query(
|
||||
'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) <= ?',
|
||||
[date]
|
||||
);
|
||||
const totalUsers = totalRows[0].count;
|
||||
|
||||
growthData.push({
|
||||
date,
|
||||
newUsers,
|
||||
totalUsers
|
||||
});
|
||||
} catch (error) {
|
||||
// 如果查询失败,使用模拟数据
|
||||
growthData.push({
|
||||
date,
|
||||
newUsers: Math.floor(Math.random() * 10),
|
||||
totalUsers: Math.floor(Math.random() * 100) + 50
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const averageDaily = totalNewUsers / days;
|
||||
const growthRate = growthData.length > 1 ?
|
||||
((growthData[growthData.length - 1].newUsers - growthData[0].newUsers) / Math.max(growthData[0].newUsers, 1)) * 100 : 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
growthData,
|
||||
summary: {
|
||||
totalNewUsers,
|
||||
averageDaily: Math.round(averageDaily * 100) / 100,
|
||||
growthRate: Math.round(growthRate * 100) / 100
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取管理员列表
|
||||
*/
|
||||
exports.getList = async (req, res, next) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const pageSize = parseInt(req.query.pageSize) || 10;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const admins = await Admin.getList({ offset, limit: pageSize });
|
||||
const total = await Admin.getCount();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
admins: admins.map(admin => ({
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
email: admin.email,
|
||||
nickname: admin.nickname,
|
||||
avatar: admin.avatar,
|
||||
role: admin.role,
|
||||
status: admin.status,
|
||||
last_login: admin.last_login,
|
||||
created_at: admin.created_at,
|
||||
updated_at: admin.updated_at
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建管理员
|
||||
*/
|
||||
exports.create = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password, email, nickname, role } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '用户名和密码不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingAdmin = await Admin.findByUsername(username);
|
||||
if (existingAdmin) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
code: 409,
|
||||
message: '用户名已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建管理员
|
||||
const adminId = await Admin.create({
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
nickname,
|
||||
role: role || 'admin'
|
||||
});
|
||||
|
||||
const newAdmin = await Admin.findById(adminId);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
code: 201,
|
||||
message: '创建成功',
|
||||
data: {
|
||||
admin: {
|
||||
id: newAdmin.id,
|
||||
username: newAdmin.username,
|
||||
email: newAdmin.email,
|
||||
nickname: newAdmin.nickname,
|
||||
avatar: newAdmin.avatar,
|
||||
role: newAdmin.role,
|
||||
status: newAdmin.status,
|
||||
created_at: newAdmin.created_at,
|
||||
updated_at: newAdmin.updated_at
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新管理员
|
||||
*/
|
||||
exports.update = async (req, res, next) => {
|
||||
try {
|
||||
const adminId = parseInt(req.params.id);
|
||||
const { email, nickname, role, status } = req.body;
|
||||
|
||||
// 不能修改自己的角色
|
||||
if (adminId === req.admin.id && role && role !== req.admin.role) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '不能修改自己的角色'
|
||||
});
|
||||
}
|
||||
|
||||
const admin = await Admin.findById(adminId);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '管理员不存在'
|
||||
});
|
||||
}
|
||||
|
||||
await Admin.update(adminId, { email, nickname, role, status });
|
||||
const updatedAdmin = await Admin.findById(adminId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '更新成功',
|
||||
data: {
|
||||
admin: {
|
||||
id: updatedAdmin.id,
|
||||
username: updatedAdmin.username,
|
||||
email: updatedAdmin.email,
|
||||
nickname: updatedAdmin.nickname,
|
||||
avatar: updatedAdmin.avatar,
|
||||
role: updatedAdmin.role,
|
||||
status: updatedAdmin.status,
|
||||
last_login: updatedAdmin.last_login,
|
||||
created_at: updatedAdmin.created_at,
|
||||
updated_at: updatedAdmin.updated_at
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除管理员
|
||||
*/
|
||||
exports.delete = async (req, res, next) => {
|
||||
try {
|
||||
const adminId = parseInt(req.params.id);
|
||||
|
||||
// 不能删除自己
|
||||
if (adminId === req.admin.id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '不能删除自己'
|
||||
});
|
||||
}
|
||||
|
||||
const admin = await Admin.findById(adminId);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '管理员不存在'
|
||||
});
|
||||
}
|
||||
|
||||
await Admin.delete(adminId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 辅助函数
|
||||
|
||||
/**
|
||||
* 获取仪表板统计数据
|
||||
*/
|
||||
async function getDashboardStatistics() {
|
||||
try {
|
||||
// 尝试从数据库获取真实数据
|
||||
const userRows = await db.query('SELECT COUNT(*) as count FROM users');
|
||||
const animalRows = await db.query('SELECT COUNT(*) as count FROM animals');
|
||||
|
||||
// 对于可能不存在的表,使用try-catch
|
||||
let travelCount = 0;
|
||||
let claimCount = 0;
|
||||
|
||||
try {
|
||||
const travelRows = await db.query('SELECT COUNT(*) as count FROM travel_plans');
|
||||
travelCount = travelRows[0].count;
|
||||
} catch (error) {
|
||||
console.log('travel_plans表不存在,使用默认值');
|
||||
}
|
||||
|
||||
try {
|
||||
const claimRows = await db.query('SELECT COUNT(*) as count FROM animal_claims');
|
||||
claimCount = claimRows[0].count;
|
||||
} catch (error) {
|
||||
console.log('animal_claims表不存在,使用默认值');
|
||||
}
|
||||
|
||||
// 今日新增数据
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const todayUserRows = await db.query('SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = ?', [today]);
|
||||
const todayAnimalRows = await db.query('SELECT COUNT(*) as count FROM animals WHERE DATE(created_at) = ?', [today]);
|
||||
|
||||
let todayTravelCount = 0;
|
||||
let todayClaimCount = 0;
|
||||
|
||||
try {
|
||||
const todayTravelRows = await db.query('SELECT COUNT(*) as count FROM travel_plans WHERE DATE(created_at) = ?', [today]);
|
||||
todayTravelCount = todayTravelRows[0].count;
|
||||
} catch (error) {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
try {
|
||||
const todayClaimRows = await db.query('SELECT COUNT(*) as count FROM animal_claims WHERE DATE(created_at) = ?', [today]);
|
||||
todayClaimCount = todayClaimRows[0].count;
|
||||
} catch (error) {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
return {
|
||||
totalUsers: userRows[0].count,
|
||||
totalAnimals: animalRows[0].count,
|
||||
totalTravels: travelCount,
|
||||
totalClaims: claimCount,
|
||||
todayNewUsers: todayUserRows[0].count,
|
||||
todayNewAnimals: todayAnimalRows[0].count,
|
||||
todayNewTravels: todayTravelCount,
|
||||
todayNewClaims: todayClaimCount
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
// 返回默认数据
|
||||
return {
|
||||
totalUsers: 0,
|
||||
totalAnimals: 0,
|
||||
totalTravels: 0,
|
||||
totalClaims: 0,
|
||||
todayNewUsers: 0,
|
||||
todayNewAnimals: 0,
|
||||
todayNewTravels: 0,
|
||||
todayNewClaims: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近活动
|
||||
*/
|
||||
async function getRecentActivities() {
|
||||
try {
|
||||
const activities = [];
|
||||
|
||||
// 获取最近用户注册
|
||||
try {
|
||||
const userRows = await db.query(`
|
||||
SELECT u.id, u.real_name as nickname, u.created_at
|
||||
FROM users u
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
userRows.forEach(user => {
|
||||
activities.push({
|
||||
type: 'user_register',
|
||||
description: `用户 ${user.nickname || '未知'} 注册了账号`,
|
||||
timestamp: user.created_at,
|
||||
user: {
|
||||
id: user.id,
|
||||
nickname: user.nickname
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('获取用户活动失败:', error.message);
|
||||
}
|
||||
|
||||
// 获取最近动物添加
|
||||
try {
|
||||
const animalRows = await db.query(`
|
||||
SELECT a.id, a.name, a.created_at, u.id as user_id, u.real_name as nickname
|
||||
FROM animals a
|
||||
LEFT JOIN users u ON a.farmer_id = u.id
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
animalRows.forEach(animal => {
|
||||
activities.push({
|
||||
type: 'animal_add',
|
||||
description: `用户 ${animal.nickname || '未知'} 添加了动物 ${animal.name}`,
|
||||
timestamp: animal.created_at,
|
||||
user: {
|
||||
id: animal.user_id,
|
||||
nickname: animal.nickname
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('获取动物活动失败:', error.message);
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
activities.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
||||
|
||||
return activities.slice(0, 10);
|
||||
} catch (error) {
|
||||
console.error('获取最近活动失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
function getSystemInfo() {
|
||||
const uptime = process.uptime();
|
||||
const hours = Math.floor(uptime / 3600);
|
||||
const minutes = Math.floor((uptime % 3600) / 60);
|
||||
|
||||
return {
|
||||
serverTime: new Date().toISOString(),
|
||||
uptime: `${hours}小时${minutes}分钟`,
|
||||
version: '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
};
|
||||
}
|
||||
431
backend/src/controllers/admin/animalManagement.js
Normal file
431
backend/src/controllers/admin/animalManagement.js
Normal file
@@ -0,0 +1,431 @@
|
||||
const Animal = require('../../models/Animal');
|
||||
const AnimalClaim = require('../../models/AnimalClaim');
|
||||
const { validationResult } = require('express-validator');
|
||||
|
||||
/**
|
||||
* 管理员动物管理控制器
|
||||
* @class AnimalManagementController
|
||||
*/
|
||||
class AnimalManagementController {
|
||||
/**
|
||||
* 获取动物列表
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
*/
|
||||
static async getAnimalList(req, res) {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
keyword,
|
||||
species,
|
||||
status,
|
||||
merchant_id,
|
||||
start_date,
|
||||
end_date,
|
||||
sort_by = 'created_at',
|
||||
sort_order = 'desc'
|
||||
} = req.query;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// 构建查询条件
|
||||
let whereClause = '';
|
||||
const params = [];
|
||||
|
||||
if (keyword) {
|
||||
whereClause += ' AND (a.name LIKE ? OR a.description LIKE ?)';
|
||||
params.push(`%${keyword}%`, `%${keyword}%`);
|
||||
}
|
||||
|
||||
if (species) {
|
||||
whereClause += ' AND a.species = ?';
|
||||
params.push(species);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND a.status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (merchant_id) {
|
||||
whereClause += ' AND a.merchant_id = ?';
|
||||
params.push(merchant_id);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereClause += ' AND DATE(a.created_at) >= ?';
|
||||
params.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereClause += ' AND DATE(a.created_at) <= ?';
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
// 获取动物列表
|
||||
const animals = await Animal.getAnimalListWithMerchant({
|
||||
whereClause,
|
||||
params,
|
||||
sortBy: sort_by,
|
||||
sortOrder: sort_order,
|
||||
limit: parseInt(limit),
|
||||
offset
|
||||
});
|
||||
|
||||
// 获取总数
|
||||
const totalCount = await Animal.getAnimalCount({
|
||||
whereClause,
|
||||
params
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
animals,
|
||||
pagination: {
|
||||
current_page: parseInt(page),
|
||||
per_page: parseInt(limit),
|
||||
total: totalCount,
|
||||
total_pages: Math.ceil(totalCount / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取动物列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取动物列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物详情
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
*/
|
||||
static async getAnimalDetail(req, res) {
|
||||
try {
|
||||
const { animal_id } = req.params;
|
||||
|
||||
// 获取动物详情
|
||||
const animal = await Animal.getAnimalDetailWithMerchant(animal_id);
|
||||
if (!animal) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '动物不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取认领统计
|
||||
const claimStats = await AnimalClaim.getAnimalClaimStats(animal_id);
|
||||
|
||||
// 获取最近的认领记录
|
||||
const recentClaims = await AnimalClaim.getAnimalClaimList(animal_id, {
|
||||
limit: 5,
|
||||
offset: 0
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
animal,
|
||||
claimStats,
|
||||
recentClaims
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取动物详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取动物详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动物状态
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
*/
|
||||
static async updateAnimalStatus(req, res) {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { animal_id } = req.params;
|
||||
const { status, reason } = req.body;
|
||||
const adminId = req.user.id;
|
||||
|
||||
// 检查动物是否存在
|
||||
const animal = await Animal.findById(animal_id);
|
||||
if (!animal) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '动物不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新动物状态
|
||||
await Animal.updateAnimalStatus(animal_id, status, adminId, reason);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '动物状态更新成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新动物状态失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新动物状态失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新动物状态
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
*/
|
||||
static async batchUpdateAnimalStatus(req, res) {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { animal_ids, status, reason } = req.body;
|
||||
const adminId = req.user.id;
|
||||
|
||||
// 批量更新动物状态
|
||||
const results = await Animal.batchUpdateAnimalStatus(animal_ids, status, adminId, reason);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '批量更新动物状态成功',
|
||||
data: {
|
||||
updated_count: results.affectedRows
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量更新动物状态失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量更新动物状态失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物统计信息
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
*/
|
||||
static async getAnimalStatistics(req, res) {
|
||||
try {
|
||||
// 获取动物总体统计
|
||||
const totalStats = await Animal.getAnimalTotalStats();
|
||||
|
||||
// 获取按物种分类的统计
|
||||
const speciesStats = await Animal.getAnimalStatsBySpecies();
|
||||
|
||||
// 获取按状态分类的统计
|
||||
const statusStats = await Animal.getAnimalStatsByStatus();
|
||||
|
||||
// 获取按商家分类的统计
|
||||
const merchantStats = await Animal.getAnimalStatsByMerchant();
|
||||
|
||||
// 获取认领统计
|
||||
const claimStats = await AnimalClaim.getClaimTotalStats();
|
||||
|
||||
// 获取月度趋势数据
|
||||
const monthlyTrend = await Animal.getAnimalMonthlyTrend();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
totalStats,
|
||||
speciesStats,
|
||||
statusStats,
|
||||
merchantStats,
|
||||
claimStats,
|
||||
monthlyTrend
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取动物统计信息失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取动物统计信息失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出动物数据
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
*/
|
||||
static async exportAnimalData(req, res) {
|
||||
try {
|
||||
const {
|
||||
format = 'csv',
|
||||
keyword,
|
||||
species,
|
||||
status,
|
||||
merchant_id,
|
||||
start_date,
|
||||
end_date
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
let whereClause = '';
|
||||
const params = [];
|
||||
|
||||
if (keyword) {
|
||||
whereClause += ' AND (a.name LIKE ? OR a.description LIKE ?)';
|
||||
params.push(`%${keyword}%`, `%${keyword}%`);
|
||||
}
|
||||
|
||||
if (species) {
|
||||
whereClause += ' AND a.species = ?';
|
||||
params.push(species);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND a.status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (merchant_id) {
|
||||
whereClause += ' AND a.merchant_id = ?';
|
||||
params.push(merchant_id);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereClause += ' AND DATE(a.created_at) >= ?';
|
||||
params.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereClause += ' AND DATE(a.created_at) <= ?';
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
// 获取导出数据
|
||||
const animals = await Animal.getAnimalExportData({
|
||||
whereClause,
|
||||
params
|
||||
});
|
||||
|
||||
if (format === 'csv') {
|
||||
// 生成CSV格式
|
||||
const csvHeader = 'ID,名称,物种,品种,年龄,性别,价格,状态,商家名称,创建时间\n';
|
||||
const csvData = animals.map(animal =>
|
||||
`${animal.id},"${animal.name}","${animal.species}","${animal.breed || ''}",${animal.age || ''},"${animal.gender || ''}",${animal.price},"${animal.status}","${animal.merchant_name}","${animal.created_at}"`
|
||||
).join('\n');
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="animals_${Date.now()}.csv"`);
|
||||
res.send('\ufeff' + csvHeader + csvData); // 添加BOM以支持中文
|
||||
} else {
|
||||
// 返回JSON格式
|
||||
res.json({
|
||||
success: true,
|
||||
message: '导出成功',
|
||||
data: {
|
||||
animals,
|
||||
export_time: new Date().toISOString(),
|
||||
total_count: animals.length
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出动物数据失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '导出动物数据失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物认领记录
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
*/
|
||||
static async getAnimalClaimRecords(req, res) {
|
||||
try {
|
||||
const { animal_id } = req.params;
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
status
|
||||
} = req.query;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// 获取认领记录
|
||||
const claims = await AnimalClaim.getAnimalClaimList(animal_id, {
|
||||
status,
|
||||
limit: parseInt(limit),
|
||||
offset
|
||||
});
|
||||
|
||||
// 获取总数
|
||||
const totalCount = await AnimalClaim.getAnimalClaimCount(animal_id, { status });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
claims,
|
||||
pagination: {
|
||||
current_page: parseInt(page),
|
||||
per_page: parseInt(limit),
|
||||
total: totalCount,
|
||||
total_pages: Math.ceil(totalCount / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取动物认领记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取动物认领记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AnimalManagementController;
|
||||
609
backend/src/controllers/admin/dataStatistics.js
Normal file
609
backend/src/controllers/admin/dataStatistics.js
Normal file
@@ -0,0 +1,609 @@
|
||||
// 管理员数据统计控制器
|
||||
const { query } = require('../../config/database');
|
||||
|
||||
/**
|
||||
* 获取系统概览统计
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.getSystemOverview = async (req, res, next) => {
|
||||
try {
|
||||
// 用户统计
|
||||
const userStatsSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_users,
|
||||
COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as new_users_today,
|
||||
COUNT(CASE WHEN DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) THEN 1 END) as new_users_week
|
||||
FROM users
|
||||
`;
|
||||
const userStats = await query(userStatsSql);
|
||||
|
||||
// 旅行统计
|
||||
const travelStatsSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_travels,
|
||||
COUNT(CASE WHEN status = 'published' THEN 1 END) as published_travels,
|
||||
COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as new_travels_today,
|
||||
COUNT(CASE WHEN DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) THEN 1 END) as new_travels_week
|
||||
FROM travels
|
||||
`;
|
||||
const travelStats = await query(travelStatsSql);
|
||||
|
||||
// 动物统计
|
||||
const animalStatsSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_animals,
|
||||
COUNT(CASE WHEN status = 'available' THEN 1 END) as available_animals,
|
||||
COUNT(CASE WHEN status = 'claimed' THEN 1 END) as claimed_animals
|
||||
FROM animals
|
||||
`;
|
||||
const animalStats = await query(animalStatsSql);
|
||||
|
||||
// 认领统计
|
||||
const claimStatsSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_claims,
|
||||
COUNT(CASE WHEN status = 'approved' THEN 1 END) as approved_claims,
|
||||
COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as new_claims_today,
|
||||
COUNT(CASE WHEN DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) THEN 1 END) as new_claims_week
|
||||
FROM animal_claims
|
||||
`;
|
||||
const claimStats = await query(claimStatsSql);
|
||||
|
||||
// 订单统计
|
||||
const orderStatsSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_orders,
|
||||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_orders,
|
||||
COALESCE(SUM(CASE WHEN status = 'completed' THEN amount END), 0) as total_revenue,
|
||||
COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as new_orders_today
|
||||
FROM orders
|
||||
`;
|
||||
const orderStats = await query(orderStatsSql);
|
||||
|
||||
// 推广统计
|
||||
const promotionStatsSql = `
|
||||
SELECT
|
||||
COUNT(DISTINCT user_id) as total_promoters,
|
||||
COALESCE(SUM(commission_amount), 0) as total_commission,
|
||||
COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_withdrawals
|
||||
FROM promotion_records
|
||||
`;
|
||||
const promotionStats = await query(promotionStatsSql);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
users: userStats[0],
|
||||
travels: travelStats[0],
|
||||
animals: animalStats[0],
|
||||
claims: claimStats[0],
|
||||
orders: orderStats[0],
|
||||
promotions: promotionStats[0]
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户增长趋势
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.getUserGrowthTrend = async (req, res, next) => {
|
||||
try {
|
||||
const { period = '30d' } = req.query;
|
||||
|
||||
let days;
|
||||
switch (period) {
|
||||
case '7d':
|
||||
days = 7;
|
||||
break;
|
||||
case '90d':
|
||||
days = 90;
|
||||
break;
|
||||
case '365d':
|
||||
days = 365;
|
||||
break;
|
||||
default:
|
||||
days = 30;
|
||||
}
|
||||
|
||||
const trendSql = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as new_users,
|
||||
COUNT(CASE WHEN user_type = 'farmer' THEN 1 END) as new_farmers,
|
||||
COUNT(CASE WHEN user_type = 'merchant' THEN 1 END) as new_merchants
|
||||
FROM users
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ${days} DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
|
||||
const trendData = await query(trendSql);
|
||||
|
||||
// 计算累计用户数
|
||||
const cumulativeSql = `
|
||||
SELECT COUNT(*) as cumulative_users
|
||||
FROM users
|
||||
WHERE created_at < DATE_SUB(CURDATE(), INTERVAL ${days} DAY)
|
||||
`;
|
||||
const cumulativeResult = await query(cumulativeSql);
|
||||
let cumulativeUsers = cumulativeResult[0].cumulative_users;
|
||||
|
||||
const enrichedTrendData = trendData.map(item => {
|
||||
cumulativeUsers += item.new_users;
|
||||
return {
|
||||
...item,
|
||||
cumulative_users: cumulativeUsers
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
period,
|
||||
trendData: enrichedTrendData
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取业务数据统计
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.getBusinessStatistics = async (req, res, next) => {
|
||||
try {
|
||||
const { period = '30d' } = req.query;
|
||||
|
||||
let days;
|
||||
switch (period) {
|
||||
case '7d':
|
||||
days = 7;
|
||||
break;
|
||||
case '90d':
|
||||
days = 90;
|
||||
break;
|
||||
default:
|
||||
days = 30;
|
||||
}
|
||||
|
||||
// 旅行数据统计
|
||||
const travelStatsSql = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as new_travels,
|
||||
COUNT(CASE WHEN status = 'published' THEN 1 END) as published_travels,
|
||||
COUNT(CASE WHEN status = 'matched' THEN 1 END) as matched_travels
|
||||
FROM travels
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ${days} DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
const travelStats = await query(travelStatsSql);
|
||||
|
||||
// 认领数据统计
|
||||
const claimStatsSql = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as new_claims,
|
||||
COUNT(CASE WHEN status = 'approved' THEN 1 END) as approved_claims,
|
||||
COUNT(CASE WHEN status = 'rejected' THEN 1 END) as rejected_claims
|
||||
FROM animal_claims
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ${days} DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
const claimStats = await query(claimStatsSql);
|
||||
|
||||
// 订单数据统计
|
||||
const orderStatsSql = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as new_orders,
|
||||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_orders,
|
||||
COALESCE(SUM(CASE WHEN status = 'completed' THEN amount END), 0) as daily_revenue
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ${days} DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
const orderStats = await query(orderStatsSql);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
period,
|
||||
travelStats,
|
||||
claimStats,
|
||||
orderStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取地域分布统计
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.getGeographicDistribution = async (req, res, next) => {
|
||||
try {
|
||||
// 用户地域分布
|
||||
const userDistributionSql = `
|
||||
SELECT
|
||||
province,
|
||||
city,
|
||||
COUNT(*) as user_count
|
||||
FROM users
|
||||
WHERE province IS NOT NULL AND city IS NOT NULL
|
||||
GROUP BY province, city
|
||||
ORDER BY user_count DESC
|
||||
LIMIT 50
|
||||
`;
|
||||
const userDistribution = await query(userDistributionSql);
|
||||
|
||||
// 省份统计
|
||||
const provinceStatsSql = `
|
||||
SELECT
|
||||
province,
|
||||
COUNT(*) as user_count,
|
||||
COUNT(CASE WHEN user_type = 'farmer' THEN 1 END) as farmer_count,
|
||||
COUNT(CASE WHEN user_type = 'merchant' THEN 1 END) as merchant_count
|
||||
FROM users
|
||||
WHERE province IS NOT NULL
|
||||
GROUP BY province
|
||||
ORDER BY user_count DESC
|
||||
`;
|
||||
const provinceStats = await query(provinceStatsSql);
|
||||
|
||||
// 旅行目的地统计
|
||||
const destinationStatsSql = `
|
||||
SELECT
|
||||
destination,
|
||||
COUNT(*) as travel_count
|
||||
FROM travels
|
||||
WHERE destination IS NOT NULL
|
||||
GROUP BY destination
|
||||
ORDER BY travel_count DESC
|
||||
LIMIT 20
|
||||
`;
|
||||
const destinationStats = await query(destinationStatsSql);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
userDistribution,
|
||||
provinceStats,
|
||||
destinationStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户行为分析
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.getUserBehaviorAnalysis = async (req, res, next) => {
|
||||
try {
|
||||
// 用户活跃度分析
|
||||
const activitySql = `
|
||||
SELECT
|
||||
CASE
|
||||
WHEN last_login_at >= DATE_SUB(NOW(), INTERVAL 1 DAY) THEN '今日活跃'
|
||||
WHEN last_login_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN '本周活跃'
|
||||
WHEN last_login_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN '本月活跃'
|
||||
ELSE '不活跃'
|
||||
END as activity_level,
|
||||
COUNT(*) as user_count
|
||||
FROM users
|
||||
WHERE last_login_at IS NOT NULL
|
||||
GROUP BY activity_level
|
||||
`;
|
||||
const activityStats = await query(activitySql);
|
||||
|
||||
// 用户等级分布
|
||||
const levelDistributionSql = `
|
||||
SELECT
|
||||
level,
|
||||
COUNT(*) as user_count,
|
||||
AVG(points) as avg_points,
|
||||
AVG(travel_count) as avg_travel_count,
|
||||
AVG(animal_claim_count) as avg_claim_count
|
||||
FROM users
|
||||
GROUP BY level
|
||||
ORDER BY
|
||||
CASE level
|
||||
WHEN 'bronze' THEN 1
|
||||
WHEN 'silver' THEN 2
|
||||
WHEN 'gold' THEN 3
|
||||
WHEN 'platinum' THEN 4
|
||||
END
|
||||
`;
|
||||
const levelDistribution = await query(levelDistributionSql);
|
||||
|
||||
// 用户行为偏好
|
||||
const behaviorSql = `
|
||||
SELECT
|
||||
'travel_focused' as behavior_type,
|
||||
COUNT(*) as user_count
|
||||
FROM users
|
||||
WHERE travel_count > animal_claim_count AND travel_count > 0
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'animal_focused' as behavior_type,
|
||||
COUNT(*) as user_count
|
||||
FROM users
|
||||
WHERE animal_claim_count > travel_count AND animal_claim_count > 0
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'balanced' as behavior_type,
|
||||
COUNT(*) as user_count
|
||||
FROM users
|
||||
WHERE travel_count = animal_claim_count AND travel_count > 0
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'inactive' as behavior_type,
|
||||
COUNT(*) as user_count
|
||||
FROM users
|
||||
WHERE travel_count = 0 AND animal_claim_count = 0
|
||||
`;
|
||||
const behaviorStats = await query(behaviorSql);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
activityStats,
|
||||
levelDistribution,
|
||||
behaviorStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取收入统计
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.getRevenueStatistics = async (req, res, next) => {
|
||||
try {
|
||||
const { period = '30d' } = req.query;
|
||||
|
||||
let days;
|
||||
switch (period) {
|
||||
case '7d':
|
||||
days = 7;
|
||||
break;
|
||||
case '90d':
|
||||
days = 90;
|
||||
break;
|
||||
case '365d':
|
||||
days = 365;
|
||||
break;
|
||||
default:
|
||||
days = 30;
|
||||
}
|
||||
|
||||
// 收入趋势
|
||||
const revenueTrendSql = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COALESCE(SUM(CASE WHEN status = 'completed' THEN amount END), 0) as daily_revenue,
|
||||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_orders,
|
||||
COUNT(*) as total_orders
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ${days} DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
const revenueTrend = await query(revenueTrendSql);
|
||||
|
||||
// 收入来源分析
|
||||
const revenueSourceSql = `
|
||||
SELECT
|
||||
order_type,
|
||||
COUNT(*) as order_count,
|
||||
COALESCE(SUM(CASE WHEN status = 'completed' THEN amount END), 0) as total_revenue,
|
||||
AVG(CASE WHEN status = 'completed' THEN amount END) as avg_order_value
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ${days} DAY)
|
||||
GROUP BY order_type
|
||||
`;
|
||||
const revenueSource = await query(revenueSourceSql);
|
||||
|
||||
// 支付方式统计
|
||||
const paymentMethodSql = `
|
||||
SELECT
|
||||
payment_method,
|
||||
COUNT(*) as order_count,
|
||||
COALESCE(SUM(amount), 0) as total_amount
|
||||
FROM orders
|
||||
WHERE status = 'completed'
|
||||
AND created_at >= DATE_SUB(CURDATE(), INTERVAL ${days} DAY)
|
||||
GROUP BY payment_method
|
||||
`;
|
||||
const paymentMethodStats = await query(paymentMethodSql);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
period,
|
||||
revenueTrend,
|
||||
revenueSource,
|
||||
paymentMethodStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出统计报告
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.exportStatisticsReport = async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
reportType = 'overview',
|
||||
period = '30d',
|
||||
format = 'csv'
|
||||
} = req.query;
|
||||
|
||||
let reportData = {};
|
||||
const timestamp = new Date().toISOString().slice(0, 10);
|
||||
|
||||
switch (reportType) {
|
||||
case 'overview':
|
||||
// 获取系统概览数据
|
||||
const overviewSql = `
|
||||
SELECT
|
||||
'用户总数' as metric, COUNT(*) as value FROM users
|
||||
UNION ALL
|
||||
SELECT
|
||||
'活跃用户' as metric, COUNT(*) as value FROM users WHERE status = 'active'
|
||||
UNION ALL
|
||||
SELECT
|
||||
'旅行总数' as metric, COUNT(*) as value FROM travels
|
||||
UNION ALL
|
||||
SELECT
|
||||
'认领总数' as metric, COUNT(*) as value FROM animal_claims
|
||||
UNION ALL
|
||||
SELECT
|
||||
'订单总数' as metric, COUNT(*) as value FROM orders
|
||||
UNION ALL
|
||||
SELECT
|
||||
'总收入' as metric, COALESCE(SUM(amount), 0) as value FROM orders WHERE status = 'completed'
|
||||
`;
|
||||
reportData.overview = await query(overviewSql);
|
||||
break;
|
||||
|
||||
case 'users':
|
||||
// 用户详细报告
|
||||
const userReportSql = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as new_users,
|
||||
COUNT(CASE WHEN user_type = 'farmer' THEN 1 END) as new_farmers,
|
||||
COUNT(CASE WHEN user_type = 'merchant' THEN 1 END) as new_merchants
|
||||
FROM users
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
reportData.users = await query(userReportSql);
|
||||
break;
|
||||
|
||||
case 'revenue':
|
||||
// 收入报告
|
||||
const revenueReportSql = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as total_orders,
|
||||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_orders,
|
||||
COALESCE(SUM(CASE WHEN status = 'completed' THEN amount END), 0) as daily_revenue
|
||||
FROM orders
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
reportData.revenue = await query(revenueReportSql);
|
||||
break;
|
||||
}
|
||||
|
||||
if (format === 'csv') {
|
||||
// 生成CSV格式
|
||||
let csvContent = '';
|
||||
|
||||
Object.keys(reportData).forEach(key => {
|
||||
csvContent += `\n${key.toUpperCase()} 报告\n`;
|
||||
if (reportData[key].length > 0) {
|
||||
// 添加表头
|
||||
const headers = Object.keys(reportData[key][0]).join(',');
|
||||
csvContent += headers + '\n';
|
||||
|
||||
// 添加数据
|
||||
reportData[key].forEach(row => {
|
||||
const values = Object.values(row).join(',');
|
||||
csvContent += values + '\n';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=statistics_report_${timestamp}.csv`);
|
||||
res.send('\uFEFF' + csvContent);
|
||||
} else {
|
||||
// 返回JSON格式
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '导出成功',
|
||||
data: {
|
||||
reportType,
|
||||
period,
|
||||
timestamp,
|
||||
...reportData
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
const logSql = `
|
||||
INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, operation_detail, created_at)
|
||||
VALUES (?, 'export_statistics', 'system', ?, ?, NOW())
|
||||
`;
|
||||
const operationDetail = JSON.stringify({
|
||||
reportType,
|
||||
period,
|
||||
format
|
||||
});
|
||||
await query(logSql, [req.admin.id, 0, operationDetail]);
|
||||
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
590
backend/src/controllers/admin/fileManagement.js
Normal file
590
backend/src/controllers/admin/fileManagement.js
Normal file
@@ -0,0 +1,590 @@
|
||||
/**
|
||||
* 管理员文件管理控制器
|
||||
* 处理文件上传、管理、统计等功能
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { AppError, ErrorTypes, catchAsync } = require('../../middleware/errorHandler');
|
||||
const { logBusinessOperation, logError } = require('../../utils/logger');
|
||||
const { deleteFile, getFileInfo } = require('../../middleware/upload');
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const getFileList = catchAsync(async (req, res) => {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
type = 'all',
|
||||
keyword = '',
|
||||
start_date,
|
||||
end_date,
|
||||
sort_by = 'created_at',
|
||||
sort_order = 'desc'
|
||||
} = req.query;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
const uploadDir = path.join(__dirname, '../../../uploads');
|
||||
|
||||
try {
|
||||
// 获取所有文件类型目录
|
||||
const typeDirs = {
|
||||
avatar: path.join(uploadDir, 'avatars'),
|
||||
animal: path.join(uploadDir, 'animals'),
|
||||
travel: path.join(uploadDir, 'travels'),
|
||||
document: path.join(uploadDir, 'documents')
|
||||
};
|
||||
|
||||
let allFiles = [];
|
||||
|
||||
// 根据类型筛选目录
|
||||
const dirsToScan = type === 'all' ? Object.values(typeDirs) : [typeDirs[type]];
|
||||
|
||||
for (const dir of dirsToScan) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
|
||||
const files = fs.readdirSync(dir);
|
||||
const fileType = Object.keys(typeDirs).find(key => typeDirs[key] === dir);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
// 跳过缩略图文件
|
||||
if (file.includes('_thumb')) continue;
|
||||
|
||||
// 关键词筛选
|
||||
if (keyword && !file.toLowerCase().includes(keyword.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 日期筛选
|
||||
if (start_date && stats.birthtime < new Date(start_date)) continue;
|
||||
if (end_date && stats.birthtime > new Date(end_date)) continue;
|
||||
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext);
|
||||
|
||||
allFiles.push({
|
||||
id: Buffer.from(filePath).toString('base64'),
|
||||
filename: file,
|
||||
originalName: file,
|
||||
type: fileType,
|
||||
size: stats.size,
|
||||
mimetype: isImage ? `image/${ext.slice(1)}` : 'application/octet-stream',
|
||||
isImage,
|
||||
url: `/uploads/${fileType}s/${file}`,
|
||||
thumbnailUrl: isImage ? `/uploads/${fileType}s/${file.replace(ext, '_thumb' + ext)}` : null,
|
||||
created_at: stats.birthtime,
|
||||
modified_at: stats.mtime
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 排序
|
||||
allFiles.sort((a, b) => {
|
||||
const aValue = a[sort_by] || a.created_at;
|
||||
const bValue = b[sort_by] || b.created_at;
|
||||
|
||||
if (sort_order === 'desc') {
|
||||
return new Date(bValue) - new Date(aValue);
|
||||
} else {
|
||||
return new Date(aValue) - new Date(bValue);
|
||||
}
|
||||
});
|
||||
|
||||
// 分页
|
||||
const total = allFiles.length;
|
||||
const files = allFiles.slice(offset, offset + parseInt(limit));
|
||||
|
||||
// 记录操作日志
|
||||
logBusinessOperation('file_list_viewed', 'file', {
|
||||
page,
|
||||
limit,
|
||||
type,
|
||||
keyword,
|
||||
total
|
||||
}, req.user);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
files,
|
||||
pagination: {
|
||||
current_page: parseInt(page),
|
||||
per_page: parseInt(limit),
|
||||
total,
|
||||
total_pages: Math.ceil(total / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, {
|
||||
type: 'file_list_error',
|
||||
userId: req.user?.id,
|
||||
query: req.query
|
||||
});
|
||||
throw ErrorTypes.INTERNAL_ERROR('获取文件列表失败');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取文件详情
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const getFileDetail = catchAsync(async (req, res) => {
|
||||
const { file_id } = req.params;
|
||||
|
||||
try {
|
||||
// 解码文件路径
|
||||
const filePath = Buffer.from(file_id, 'base64').toString();
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw ErrorTypes.NOT_FOUND('文件不存在');
|
||||
}
|
||||
|
||||
const stats = fs.statSync(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext);
|
||||
|
||||
// 获取文件类型
|
||||
const uploadDir = path.join(__dirname, '../../../uploads');
|
||||
const relativePath = path.relative(uploadDir, filePath);
|
||||
const fileType = relativePath.split(path.sep)[0].replace('s', ''); // avatars -> avatar
|
||||
|
||||
const fileDetail = {
|
||||
id: file_id,
|
||||
filename,
|
||||
originalName: filename,
|
||||
type: fileType,
|
||||
size: stats.size,
|
||||
mimetype: isImage ? `image/${ext.slice(1)}` : 'application/octet-stream',
|
||||
isImage,
|
||||
url: `/uploads/${fileType}s/${filename}`,
|
||||
thumbnailUrl: isImage ? `/uploads/${fileType}s/${filename.replace(ext, '_thumb' + ext)}` : null,
|
||||
created_at: stats.birthtime,
|
||||
modified_at: stats.mtime,
|
||||
path: relativePath
|
||||
};
|
||||
|
||||
// 记录操作日志
|
||||
logBusinessOperation('file_detail_viewed', 'file', {
|
||||
fileId: file_id,
|
||||
filename
|
||||
}, req.user);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
file: fileDetail
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
logError(error, {
|
||||
type: 'file_detail_error',
|
||||
userId: req.user?.id,
|
||||
fileId: file_id
|
||||
});
|
||||
throw ErrorTypes.INTERNAL_ERROR('获取文件详情失败');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const deleteFileById = catchAsync(async (req, res) => {
|
||||
const { file_id } = req.params;
|
||||
|
||||
try {
|
||||
// 解码文件路径
|
||||
const filePath = Buffer.from(file_id, 'base64').toString();
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw ErrorTypes.NOT_FOUND('文件不存在');
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
const deleted = await deleteFile(filePath);
|
||||
|
||||
if (!deleted) {
|
||||
throw ErrorTypes.INTERNAL_ERROR('文件删除失败');
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
logBusinessOperation('file_deleted', 'file', {
|
||||
fileId: file_id,
|
||||
filename,
|
||||
filePath
|
||||
}, req.user);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '文件删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
logError(error, {
|
||||
type: 'file_deletion_error',
|
||||
userId: req.user?.id,
|
||||
fileId: file_id
|
||||
});
|
||||
throw ErrorTypes.INTERNAL_ERROR('删除文件失败');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 批量删除文件
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const batchDeleteFiles = catchAsync(async (req, res) => {
|
||||
const { file_ids } = req.body;
|
||||
|
||||
if (!Array.isArray(file_ids) || file_ids.length === 0) {
|
||||
throw ErrorTypes.VALIDATION_ERROR('请提供要删除的文件ID列表');
|
||||
}
|
||||
|
||||
if (file_ids.length > 50) {
|
||||
throw ErrorTypes.VALIDATION_ERROR('单次最多删除50个文件');
|
||||
}
|
||||
|
||||
const results = {
|
||||
success: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
for (const file_id of file_ids) {
|
||||
try {
|
||||
// 解码文件路径
|
||||
const filePath = Buffer.from(file_id, 'base64').toString();
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
const deleted = await deleteFile(filePath);
|
||||
|
||||
if (deleted) {
|
||||
results.success.push({
|
||||
file_id,
|
||||
filename,
|
||||
message: '删除成功'
|
||||
});
|
||||
} else {
|
||||
results.failed.push({
|
||||
file_id,
|
||||
filename,
|
||||
message: '删除失败'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
results.failed.push({
|
||||
file_id,
|
||||
filename: '未知',
|
||||
message: '文件不存在'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
results.failed.push({
|
||||
file_id,
|
||||
filename: '未知',
|
||||
message: error.message || '删除失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
logBusinessOperation('files_batch_deleted', 'file', {
|
||||
totalFiles: file_ids.length,
|
||||
successCount: results.success.length,
|
||||
failedCount: results.failed.length
|
||||
}, req.user);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `批量删除完成,成功: ${results.success.length},失败: ${results.failed.length}`,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取文件统计信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const getFileStatistics = catchAsync(async (req, res) => {
|
||||
const uploadDir = path.join(__dirname, '../../../uploads');
|
||||
|
||||
try {
|
||||
const typeDirs = {
|
||||
avatar: path.join(uploadDir, 'avatars'),
|
||||
animal: path.join(uploadDir, 'animals'),
|
||||
travel: path.join(uploadDir, 'travels'),
|
||||
document: path.join(uploadDir, 'documents')
|
||||
};
|
||||
|
||||
const stats = {
|
||||
totalFiles: 0,
|
||||
totalSize: 0,
|
||||
typeStats: [],
|
||||
sizeDistribution: {
|
||||
small: 0, // < 1MB
|
||||
medium: 0, // 1MB - 5MB
|
||||
large: 0 // > 5MB
|
||||
},
|
||||
formatStats: {}
|
||||
};
|
||||
|
||||
for (const [type, dir] of Object.entries(typeDirs)) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
stats.typeStats.push({
|
||||
type,
|
||||
count: 0,
|
||||
size: 0,
|
||||
avgSize: 0
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(dir);
|
||||
let typeCount = 0;
|
||||
let typeSize = 0;
|
||||
|
||||
for (const file of files) {
|
||||
// 跳过缩略图文件
|
||||
if (file.includes('_thumb')) continue;
|
||||
|
||||
const filePath = path.join(dir, file);
|
||||
const fileStat = fs.statSync(filePath);
|
||||
const fileSize = fileStat.size;
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
|
||||
typeCount++;
|
||||
typeSize += fileSize;
|
||||
stats.totalFiles++;
|
||||
stats.totalSize += fileSize;
|
||||
|
||||
// 大小分布统计
|
||||
if (fileSize < 1024 * 1024) {
|
||||
stats.sizeDistribution.small++;
|
||||
} else if (fileSize < 5 * 1024 * 1024) {
|
||||
stats.sizeDistribution.medium++;
|
||||
} else {
|
||||
stats.sizeDistribution.large++;
|
||||
}
|
||||
|
||||
// 格式统计
|
||||
if (!stats.formatStats[ext]) {
|
||||
stats.formatStats[ext] = { count: 0, size: 0 };
|
||||
}
|
||||
stats.formatStats[ext].count++;
|
||||
stats.formatStats[ext].size += fileSize;
|
||||
}
|
||||
|
||||
stats.typeStats.push({
|
||||
type,
|
||||
count: typeCount,
|
||||
size: typeSize,
|
||||
avgSize: typeCount > 0 ? Math.round(typeSize / typeCount) : 0
|
||||
});
|
||||
}
|
||||
|
||||
// 转换格式统计为数组
|
||||
const formatStatsArray = Object.entries(stats.formatStats).map(([format, data]) => ({
|
||||
format,
|
||||
count: data.count,
|
||||
size: data.size,
|
||||
percentage: ((data.count / stats.totalFiles) * 100).toFixed(2)
|
||||
})).sort((a, b) => b.count - a.count);
|
||||
|
||||
stats.formatStats = formatStatsArray;
|
||||
|
||||
// 记录操作日志
|
||||
logBusinessOperation('file_statistics_viewed', 'file', {
|
||||
totalFiles: stats.totalFiles,
|
||||
totalSize: stats.totalSize
|
||||
}, req.user);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取成功',
|
||||
data: stats
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, {
|
||||
type: 'file_statistics_error',
|
||||
userId: req.user?.id
|
||||
});
|
||||
throw ErrorTypes.INTERNAL_ERROR('获取文件统计失败');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 清理无用文件
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const cleanupUnusedFiles = catchAsync(async (req, res) => {
|
||||
const { dry_run = true } = req.query;
|
||||
|
||||
try {
|
||||
const uploadDir = path.join(__dirname, '../../../uploads');
|
||||
const typeDirs = {
|
||||
avatar: path.join(uploadDir, 'avatars'),
|
||||
animal: path.join(uploadDir, 'animals'),
|
||||
travel: path.join(uploadDir, 'travels'),
|
||||
document: path.join(uploadDir, 'documents')
|
||||
};
|
||||
|
||||
const results = {
|
||||
scanned: 0,
|
||||
unused: [],
|
||||
deleted: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
// 这里应该根据实际业务逻辑检查文件是否被使用
|
||||
// 目前只是示例,检查30天前的文件
|
||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
for (const [type, dir] of Object.entries(typeDirs)) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
for (const file of files) {
|
||||
// 跳过缩略图文件
|
||||
if (file.includes('_thumb')) continue;
|
||||
|
||||
const filePath = path.join(dir, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
results.scanned++;
|
||||
|
||||
// 检查文件是否超过30天且未被使用(这里需要根据实际业务逻辑实现)
|
||||
if (stats.mtime < thirtyDaysAgo) {
|
||||
results.unused.push({
|
||||
filename: file,
|
||||
type,
|
||||
size: stats.size,
|
||||
lastModified: stats.mtime
|
||||
});
|
||||
|
||||
// 如果不是试运行,则删除文件
|
||||
if (dry_run !== 'true') {
|
||||
try {
|
||||
const deleted = await deleteFile(filePath);
|
||||
if (deleted) {
|
||||
results.deleted.push({
|
||||
filename: file,
|
||||
type,
|
||||
size: stats.size
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
results.errors.push({
|
||||
filename: file,
|
||||
type,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
logBusinessOperation('file_cleanup', 'file', {
|
||||
dryRun: dry_run === 'true',
|
||||
scanned: results.scanned,
|
||||
unused: results.unused.length,
|
||||
deleted: results.deleted.length,
|
||||
errors: results.errors.length
|
||||
}, req.user);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: dry_run === 'true' ? '扫描完成(试运行)' : '清理完成',
|
||||
data: results
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, {
|
||||
type: 'file_cleanup_error',
|
||||
userId: req.user?.id,
|
||||
dryRun: dry_run === 'true'
|
||||
});
|
||||
throw ErrorTypes.INTERNAL_ERROR('文件清理失败');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const uploadFile = catchAsync(async (req, res) => {
|
||||
if (!req.file && !req.files) {
|
||||
throw ErrorTypes.VALIDATION_ERROR('请选择要上传的文件');
|
||||
}
|
||||
|
||||
const files = req.files || [req.file];
|
||||
const uploadedFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
const fileInfo = {
|
||||
id: Buffer.from(file.path).toString('base64'),
|
||||
filename: file.filename,
|
||||
originalName: file.originalname,
|
||||
size: file.size,
|
||||
mimetype: file.mimetype,
|
||||
url: file.path.replace(path.join(__dirname, '../../../'), '/'),
|
||||
thumbnailUrl: file.thumbnail ? file.path.replace(path.basename(file.path), file.thumbnail).replace(path.join(__dirname, '../../../'), '/') : null,
|
||||
created_at: new Date()
|
||||
};
|
||||
|
||||
uploadedFiles.push(fileInfo);
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
logBusinessOperation('files_uploaded', 'file', {
|
||||
fileCount: uploadedFiles.length,
|
||||
files: uploadedFiles.map(f => ({
|
||||
filename: f.filename,
|
||||
size: f.size,
|
||||
mimetype: f.mimetype
|
||||
}))
|
||||
}, req.user);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '文件上传成功',
|
||||
data: {
|
||||
files: uploadedFiles
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getFileList,
|
||||
getFileDetail,
|
||||
deleteFileById,
|
||||
batchDeleteFiles,
|
||||
getFileStatistics,
|
||||
cleanupUnusedFiles,
|
||||
uploadFile
|
||||
};
|
||||
@@ -67,6 +67,128 @@ exports.login = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取用户增长数据
|
||||
exports.getUserGrowth = async (req, res, next) => {
|
||||
try {
|
||||
const { days = 7 } = req.query;
|
||||
|
||||
// 验证参数
|
||||
const daysNum = parseInt(days);
|
||||
if (isNaN(daysNum) || daysNum < 1 || daysNum > 365) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '天数参数无效,必须在1-365之间'
|
||||
});
|
||||
}
|
||||
|
||||
const growthData = await getUserGrowthData(daysNum);
|
||||
const summary = calculateGrowthSummary(growthData);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
growthData,
|
||||
summary
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取用户增长数据的辅助函数
|
||||
const getUserGrowthData = async (days) => {
|
||||
try {
|
||||
// 生成日期范围
|
||||
const dates = [];
|
||||
const today = new Date();
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() - i);
|
||||
dates.push(date.toISOString().split('T')[0]);
|
||||
}
|
||||
|
||||
// 查询每日新增用户数
|
||||
const dailyNewUsersQuery = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as newUsers
|
||||
FROM users
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
|
||||
const dailyNewUsersResult = await query(dailyNewUsersQuery, [days]);
|
||||
|
||||
// 确保dailyNewUsers是数组
|
||||
const dailyNewUsers = Array.isArray(dailyNewUsersResult) ? dailyNewUsersResult : [];
|
||||
|
||||
// 查询总用户数(截止到每一天)
|
||||
const growthData = [];
|
||||
let cumulativeUsers = 0;
|
||||
|
||||
// 获取起始日期之前的用户总数
|
||||
const initialUsersQuery = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM users
|
||||
WHERE created_at < DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
`;
|
||||
const initialUsersResult = await query(initialUsersQuery, [days]);
|
||||
const initialResult = initialUsersResult || [];
|
||||
cumulativeUsers = initialResult[0]?.count || 0;
|
||||
|
||||
// 构建每日数据
|
||||
for (const date of dates) {
|
||||
// 将数据库返回的日期转换为字符串进行比较
|
||||
const dayData = dailyNewUsers.find(d => {
|
||||
const dbDate = d.date instanceof Date ? d.date.toISOString().split('T')[0] : d.date;
|
||||
return dbDate === date;
|
||||
});
|
||||
const newUsers = dayData ? parseInt(dayData.newUsers) : 0;
|
||||
cumulativeUsers += newUsers;
|
||||
|
||||
growthData.push({
|
||||
date,
|
||||
newUsers,
|
||||
totalUsers: cumulativeUsers
|
||||
});
|
||||
}
|
||||
|
||||
return growthData;
|
||||
} catch (error) {
|
||||
console.error('获取用户增长数据失败:', error);
|
||||
throw new Error('获取用户增长数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 计算增长汇总数据
|
||||
const calculateGrowthSummary = (growthData) => {
|
||||
if (!growthData || growthData.length === 0) {
|
||||
return {
|
||||
totalNewUsers: 0,
|
||||
averageDaily: 0,
|
||||
growthRate: 0
|
||||
};
|
||||
}
|
||||
|
||||
const totalNewUsers = growthData.reduce((sum, day) => sum + day.newUsers, 0);
|
||||
const averageDaily = totalNewUsers / growthData.length;
|
||||
|
||||
// 计算增长率(相对于期初用户数)
|
||||
const initialUsers = growthData[0].totalUsers - growthData[0].newUsers;
|
||||
const growthRate = initialUsers > 0 ? (totalNewUsers / initialUsers) * 100 : 0;
|
||||
|
||||
return {
|
||||
totalNewUsers,
|
||||
averageDaily: Math.round(averageDaily * 100) / 100, // 保留两位小数
|
||||
growthRate: Math.round(growthRate * 100) / 100 // 保留两位小数
|
||||
};
|
||||
};
|
||||
|
||||
// 获取当前管理员信息
|
||||
exports.getProfile = async (req, res, next) => {
|
||||
try {
|
||||
@@ -92,6 +214,181 @@ exports.getProfile = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取仪表板数据
|
||||
exports.getDashboard = async (req, res, next) => {
|
||||
try {
|
||||
// 获取统计数据
|
||||
const statistics = await getDashboardStatistics();
|
||||
|
||||
// 获取最近活动
|
||||
const recentActivities = await getRecentActivities();
|
||||
|
||||
// 获取系统信息
|
||||
const systemInfo = getSystemInfo();
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
statistics,
|
||||
recentActivities,
|
||||
systemInfo
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取仪表板统计数据
|
||||
const getDashboardStatistics = async () => {
|
||||
try {
|
||||
const today = new Date();
|
||||
const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const todayEnd = new Date(todayStart.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
// 总用户数
|
||||
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 < ?',
|
||||
[todayStart, todayEnd]
|
||||
);
|
||||
const todayNewUsers = todayUsersResult.count;
|
||||
|
||||
// 总动物数
|
||||
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 < ?',
|
||||
[todayStart, todayEnd]
|
||||
);
|
||||
const todayNewAnimals = todayAnimalsResult.count;
|
||||
|
||||
// 总旅行数
|
||||
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 < ?',
|
||||
[todayStart, todayEnd]
|
||||
);
|
||||
const todayNewTravels = todayTravelsResult.count;
|
||||
|
||||
// 总认领数
|
||||
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 < ?',
|
||||
[todayStart, todayEnd]
|
||||
);
|
||||
const todayNewClaims = todayClaimsResult.count;
|
||||
|
||||
return {
|
||||
totalUsers,
|
||||
totalAnimals,
|
||||
totalTravels,
|
||||
totalClaims,
|
||||
todayNewUsers,
|
||||
todayNewAnimals,
|
||||
todayNewTravels,
|
||||
todayNewClaims
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
// 返回默认值
|
||||
return {
|
||||
totalUsers: 0,
|
||||
totalAnimals: 0,
|
||||
totalTravels: 0,
|
||||
totalClaims: 0,
|
||||
todayNewUsers: 0,
|
||||
todayNewAnimals: 0,
|
||||
todayNewTravels: 0,
|
||||
todayNewClaims: 0
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 获取最近活动
|
||||
const getRecentActivities = async () => {
|
||||
try {
|
||||
const activities = [];
|
||||
|
||||
// 最近用户注册
|
||||
const recentUsers = await query(`
|
||||
SELECT id, nickname, created_at
|
||||
FROM users
|
||||
WHERE status != 'banned'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
recentUsers.forEach(user => {
|
||||
activities.push({
|
||||
type: 'user_register',
|
||||
description: `用户 ${user.nickname} 注册了账号`,
|
||||
timestamp: user.created_at,
|
||||
user: {
|
||||
id: user.id,
|
||||
nickname: user.nickname
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 最近动物添加
|
||||
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.farmer_id = u.id
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
recentAnimals.forEach(animal => {
|
||||
activities.push({
|
||||
type: 'animal_add',
|
||||
description: `${animal.user_nickname || '用户'} 添加了动物 ${animal.name}`,
|
||||
timestamp: animal.created_at,
|
||||
user: {
|
||||
id: animal.user_id,
|
||||
nickname: animal.user_nickname
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 按时间排序
|
||||
activities.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
||||
|
||||
return activities.slice(0, 10); // 返回最近10条活动
|
||||
} catch (error) {
|
||||
console.error('获取最近活动失败:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取系统信息
|
||||
const getSystemInfo = () => {
|
||||
const uptime = process.uptime();
|
||||
const uptimeHours = Math.floor(uptime / 3600);
|
||||
const uptimeMinutes = Math.floor((uptime % 3600) / 60);
|
||||
|
||||
return {
|
||||
serverTime: new Date().toISOString(),
|
||||
uptime: `${uptimeHours}小时${uptimeMinutes}分钟`,
|
||||
version: process.env.APP_VERSION || '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
};
|
||||
};
|
||||
|
||||
// 获取管理员列表
|
||||
exports.getList = async (req, res, next) => {
|
||||
try {
|
||||
|
||||
487
backend/src/controllers/admin/userManagement.js
Normal file
487
backend/src/controllers/admin/userManagement.js
Normal file
@@ -0,0 +1,487 @@
|
||||
// 管理员用户管理控制器
|
||||
const User = require('../../models/user');
|
||||
const UserService = require('../../services/user');
|
||||
const { query } = require('../../config/database');
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.getUserList = async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
keyword = '',
|
||||
userType = '',
|
||||
status = '',
|
||||
startDate = '',
|
||||
endDate = '',
|
||||
sortField = 'created_at',
|
||||
sortOrder = 'desc'
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
let whereClause = 'WHERE 1=1';
|
||||
const params = [];
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
whereClause += ' AND (nickname LIKE ? OR phone LIKE ? OR email LIKE ?)';
|
||||
const searchTerm = `%${keyword}%`;
|
||||
params.push(searchTerm, searchTerm, searchTerm);
|
||||
}
|
||||
|
||||
// 用户类型筛选
|
||||
if (userType) {
|
||||
whereClause += ' AND user_type = ?';
|
||||
params.push(userType);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
whereClause += ' AND status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
// 日期范围筛选
|
||||
if (startDate) {
|
||||
whereClause += ' AND created_at >= ?';
|
||||
params.push(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
whereClause += ' AND created_at <= ?';
|
||||
params.push(endDate + ' 23:59:59');
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
const countSql = `SELECT COUNT(*) as total FROM users ${whereClause}`;
|
||||
const countResult = await query(countSql, params);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 分页查询
|
||||
const offset = (page - 1) * pageSize;
|
||||
const orderBy = `ORDER BY ${sortField} ${sortOrder.toUpperCase()}`;
|
||||
const listSql = `
|
||||
SELECT
|
||||
id, nickname, phone, email, user_type, status,
|
||||
travel_count, animal_claim_count, points, level,
|
||||
last_login_at, created_at, updated_at
|
||||
FROM users
|
||||
${whereClause}
|
||||
${orderBy}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const listParams = [...params, parseInt(pageSize), offset];
|
||||
const users = await query(listSql, listParams);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
users,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户详情
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.getUserDetail = async (req, res, next) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
|
||||
// 获取用户基本信息
|
||||
const userSql = `
|
||||
SELECT
|
||||
id, openid, nickname, avatar, gender, birthday, phone, email,
|
||||
province, city, travel_count, animal_claim_count, points, level,
|
||||
status, last_login_at, created_at, updated_at
|
||||
FROM users
|
||||
WHERE id = ?
|
||||
`;
|
||||
const userResult = await query(userSql, [userId]);
|
||||
|
||||
if (userResult.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const user = userResult[0];
|
||||
|
||||
// 获取用户兴趣
|
||||
const interestsSql = `
|
||||
SELECT ui.interest_name, ui.created_at
|
||||
FROM user_interests ui
|
||||
WHERE ui.user_id = ?
|
||||
`;
|
||||
const interests = await query(interestsSql, [userId]);
|
||||
|
||||
// 获取用户最近的旅行记录
|
||||
const travelsSql = `
|
||||
SELECT id, title, destination, start_date, end_date, status, created_at
|
||||
FROM travels
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5
|
||||
`;
|
||||
const travels = await query(travelsSql, [userId]);
|
||||
|
||||
// 获取用户最近的认领记录
|
||||
const claimsSql = `
|
||||
SELECT ac.id, a.name as animal_name, ac.status, ac.created_at
|
||||
FROM animal_claims ac
|
||||
JOIN animals a ON ac.animal_id = a.id
|
||||
WHERE ac.user_id = ?
|
||||
ORDER BY ac.created_at DESC
|
||||
LIMIT 5
|
||||
`;
|
||||
const claims = await query(claimsSql, [userId]);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
user: {
|
||||
...user,
|
||||
interests: interests.map(i => i.interest_name),
|
||||
recentTravels: travels,
|
||||
recentClaims: claims
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新用户状态
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.updateUserStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { status, reason } = req.body;
|
||||
|
||||
// 验证状态值
|
||||
const validStatuses = ['active', 'inactive', 'banned'];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '无效的状态值'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const checkSql = 'SELECT id, status FROM users WHERE id = ?';
|
||||
const checkResult = await query(checkSql, [userId]);
|
||||
|
||||
if (checkResult.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户状态
|
||||
const updateSql = 'UPDATE users SET status = ?, updated_at = NOW() WHERE id = ?';
|
||||
await query(updateSql, [status, userId]);
|
||||
|
||||
// 记录操作日志
|
||||
const logSql = `
|
||||
INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, operation_detail, created_at)
|
||||
VALUES (?, 'update_user_status', 'user', ?, ?, NOW())
|
||||
`;
|
||||
const operationDetail = JSON.stringify({
|
||||
old_status: checkResult[0].status,
|
||||
new_status: status,
|
||||
reason: reason || '无'
|
||||
});
|
||||
await query(logSql, [req.admin.id, userId, operationDetail]);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '状态更新成功'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量更新用户状态
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.batchUpdateUserStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { userIds, status, reason } = req.body;
|
||||
|
||||
// 验证输入
|
||||
if (!Array.isArray(userIds) || userIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '用户ID列表不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const validStatuses = ['active', 'inactive', 'banned'];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '无效的状态值'
|
||||
});
|
||||
}
|
||||
|
||||
// 批量更新
|
||||
const placeholders = userIds.map(() => '?').join(',');
|
||||
const updateSql = `UPDATE users SET status = ?, updated_at = NOW() WHERE id IN (${placeholders})`;
|
||||
const updateParams = [status, ...userIds];
|
||||
|
||||
const result = await query(updateSql, updateParams);
|
||||
|
||||
// 记录操作日志
|
||||
const logSql = `
|
||||
INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, operation_detail, created_at)
|
||||
VALUES (?, 'batch_update_user_status', 'user', ?, ?, NOW())
|
||||
`;
|
||||
const operationDetail = JSON.stringify({
|
||||
user_ids: userIds,
|
||||
new_status: status,
|
||||
reason: reason || '无',
|
||||
affected_rows: result.affectedRows
|
||||
});
|
||||
await query(logSql, [req.admin.id, 0, operationDetail]);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: `成功更新 ${result.affectedRows} 个用户的状态`
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户统计信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.getUserStatistics = async (req, res, next) => {
|
||||
try {
|
||||
const { period = '7d' } = req.query;
|
||||
|
||||
// 基础统计
|
||||
const basicStatsSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_users,
|
||||
COUNT(CASE WHEN status = 'inactive' THEN 1 END) as inactive_users,
|
||||
COUNT(CASE WHEN status = 'banned' THEN 1 END) as banned_users,
|
||||
COUNT(CASE WHEN user_type = 'farmer' THEN 1 END) as farmers,
|
||||
COUNT(CASE WHEN user_type = 'merchant' THEN 1 END) as merchants,
|
||||
COUNT(CASE WHEN DATE(created_at) = CURDATE() THEN 1 END) as new_users_today,
|
||||
COUNT(CASE WHEN DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) THEN 1 END) as new_users_week,
|
||||
COUNT(CASE WHEN DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_users_month
|
||||
FROM users
|
||||
`;
|
||||
const basicStats = await query(basicStatsSql);
|
||||
|
||||
// 用户等级分布
|
||||
const levelStatsSql = `
|
||||
SELECT
|
||||
level,
|
||||
COUNT(*) as count
|
||||
FROM users
|
||||
GROUP BY level
|
||||
`;
|
||||
const levelStats = await query(levelStatsSql);
|
||||
|
||||
// 根据时间周期获取趋势数据
|
||||
let trendSql;
|
||||
let trendDays;
|
||||
|
||||
switch (period) {
|
||||
case '30d':
|
||||
trendDays = 30;
|
||||
break;
|
||||
case '90d':
|
||||
trendDays = 90;
|
||||
break;
|
||||
default:
|
||||
trendDays = 7;
|
||||
}
|
||||
|
||||
trendSql = `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as new_users,
|
||||
COUNT(CASE WHEN user_type = 'farmer' THEN 1 END) as new_farmers,
|
||||
COUNT(CASE WHEN user_type = 'merchant' THEN 1 END) as new_merchants
|
||||
FROM users
|
||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ${trendDays} DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
const trendData = await query(trendSql);
|
||||
|
||||
// 活跃用户统计(最近30天有登录的用户)
|
||||
const activeUsersSql = `
|
||||
SELECT COUNT(*) as active_users_30d
|
||||
FROM users
|
||||
WHERE last_login_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
`;
|
||||
const activeUsersResult = await query(activeUsersSql);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
basicStats: basicStats[0],
|
||||
levelDistribution: levelStats,
|
||||
trendData,
|
||||
activeUsers30d: activeUsersResult[0].active_users_30d
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出用户数据
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
exports.exportUsers = async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
format = 'csv',
|
||||
userType = '',
|
||||
status = '',
|
||||
startDate = '',
|
||||
endDate = ''
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
let whereClause = 'WHERE 1=1';
|
||||
const params = [];
|
||||
|
||||
if (userType) {
|
||||
whereClause += ' AND user_type = ?';
|
||||
params.push(userType);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
whereClause += ' AND created_at >= ?';
|
||||
params.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
whereClause += ' AND created_at <= ?';
|
||||
params.push(endDate + ' 23:59:59');
|
||||
}
|
||||
|
||||
// 查询用户数据
|
||||
const exportSql = `
|
||||
SELECT
|
||||
id, nickname, phone, email, user_type, status,
|
||||
travel_count, animal_claim_count, points, level,
|
||||
created_at, last_login_at
|
||||
FROM users
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
|
||||
const users = await query(exportSql, params);
|
||||
|
||||
if (format === 'csv') {
|
||||
// 生成CSV格式
|
||||
const csvHeader = 'ID,昵称,手机号,邮箱,用户类型,状态,旅行次数,认领次数,积分,等级,注册时间,最后登录\n';
|
||||
const csvData = users.map(user => {
|
||||
return [
|
||||
user.id,
|
||||
user.nickname || '',
|
||||
user.phone || '',
|
||||
user.email || '',
|
||||
user.user_type || '',
|
||||
user.status,
|
||||
user.travel_count,
|
||||
user.animal_claim_count,
|
||||
user.points,
|
||||
user.level,
|
||||
user.created_at,
|
||||
user.last_login_at || ''
|
||||
].join(',');
|
||||
}).join('\n');
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=users_${Date.now()}.csv`);
|
||||
res.send('\uFEFF' + csvHeader + csvData); // 添加BOM以支持中文
|
||||
} else {
|
||||
// 返回JSON格式
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '导出成功',
|
||||
data: {
|
||||
users,
|
||||
total: users.length
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
const logSql = `
|
||||
INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, operation_detail, created_at)
|
||||
VALUES (?, 'export_users', 'user', ?, ?, NOW())
|
||||
`;
|
||||
const operationDetail = JSON.stringify({
|
||||
format,
|
||||
filters: { userType, status, startDate, endDate },
|
||||
exported_count: users.length
|
||||
});
|
||||
await query(logSql, [req.admin.id, 0, operationDetail]);
|
||||
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
438
backend/src/controllers/animalClaim.js
Normal file
438
backend/src/controllers/animalClaim.js
Normal file
@@ -0,0 +1,438 @@
|
||||
const AnimalClaimService = require('../services/animalClaim');
|
||||
const { validateRequired, validatePositiveInteger } = require('../utils/validation');
|
||||
|
||||
class AnimalClaimController {
|
||||
/**
|
||||
* 申请认领动物
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async createClaim(req, res) {
|
||||
try {
|
||||
const { animal_id, claim_reason, claim_duration, contact_info } = req.body;
|
||||
const user_id = req.user.id;
|
||||
|
||||
// 参数验证
|
||||
if (!validateRequired(animal_id) || !validatePositiveInteger(animal_id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '动物ID不能为空且必须为正整数'
|
||||
});
|
||||
}
|
||||
|
||||
if (!validateRequired(contact_info)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '联系方式不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
if (claim_duration && (!validatePositiveInteger(claim_duration) || claim_duration < 1 || claim_duration > 60)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '认领时长必须为1-60个月之间的整数'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建认领申请
|
||||
const claim = await AnimalClaimService.createClaim({
|
||||
animal_id: parseInt(animal_id),
|
||||
user_id,
|
||||
claim_reason,
|
||||
claim_duration: claim_duration ? parseInt(claim_duration) : 12,
|
||||
contact_info
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '认领申请提交成功',
|
||||
data: claim
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建认领申请控制器错误:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message || '创建认领申请失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消认领申请
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async cancelClaim(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const user_id = req.user.id;
|
||||
|
||||
// 参数验证
|
||||
if (!validatePositiveInteger(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '认领申请ID无效'
|
||||
});
|
||||
}
|
||||
|
||||
// 取消认领申请
|
||||
const claim = await AnimalClaimService.cancelClaim(parseInt(id), user_id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '认领申请已取消',
|
||||
data: claim
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('取消认领申请控制器错误:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message || '取消认领申请失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的认领申请列表
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getUserClaims(req, res) {
|
||||
try {
|
||||
const user_id = req.user.id;
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
status,
|
||||
animal_type,
|
||||
start_date,
|
||||
end_date
|
||||
} = req.query;
|
||||
|
||||
// 参数验证
|
||||
if (!validatePositiveInteger(page) || !validatePositiveInteger(limit)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '分页参数必须为正整数'
|
||||
});
|
||||
}
|
||||
|
||||
if (parseInt(limit) > 100) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '每页数量不能超过100'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取认领申请列表
|
||||
const result = await AnimalClaimService.getUserClaims(user_id, {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
status,
|
||||
animal_type,
|
||||
start_date,
|
||||
end_date
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取认领申请列表成功',
|
||||
data: result.data,
|
||||
pagination: result.pagination
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户认领申请列表控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取认领申请列表失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物的认领申请列表
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getAnimalClaims(req, res) {
|
||||
try {
|
||||
const { animal_id } = req.params;
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
status
|
||||
} = req.query;
|
||||
|
||||
// 参数验证
|
||||
if (!validatePositiveInteger(animal_id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '动物ID无效'
|
||||
});
|
||||
}
|
||||
|
||||
if (!validatePositiveInteger(page) || !validatePositiveInteger(limit)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '分页参数必须为正整数'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取动物认领申请列表
|
||||
const result = await AnimalClaimService.getAnimalClaims(parseInt(animal_id), {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
status
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取动物认领申请列表成功',
|
||||
data: result.data,
|
||||
pagination: result.pagination
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取动物认领申请列表控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取动物认领申请列表失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有认领申请列表(管理员)
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getAllClaims(req, res) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
status,
|
||||
animal_type,
|
||||
user_id,
|
||||
start_date,
|
||||
end_date,
|
||||
keyword
|
||||
} = req.query;
|
||||
|
||||
// 参数验证
|
||||
if (!validatePositiveInteger(page) || !validatePositiveInteger(limit)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '分页参数必须为正整数'
|
||||
});
|
||||
}
|
||||
|
||||
if (parseInt(limit) > 100) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '每页数量不能超过100'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有认领申请列表
|
||||
const result = await AnimalClaimService.getAllClaims({
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
status,
|
||||
animal_type,
|
||||
user_id: user_id ? parseInt(user_id) : undefined,
|
||||
start_date,
|
||||
end_date,
|
||||
keyword
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取认领申请列表成功',
|
||||
data: result.data,
|
||||
pagination: result.pagination
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取所有认领申请列表控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取认领申请列表失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核认领申请
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async reviewClaim(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, review_remark } = req.body;
|
||||
const reviewed_by = req.user.id;
|
||||
|
||||
// 参数验证
|
||||
if (!validatePositiveInteger(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '认领申请ID无效'
|
||||
});
|
||||
}
|
||||
|
||||
if (!validateRequired(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '审核状态不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const validStatuses = ['approved', 'rejected'];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '无效的审核状态'
|
||||
});
|
||||
}
|
||||
|
||||
// 审核认领申请
|
||||
const claim = await AnimalClaimService.reviewClaim(parseInt(id), status, {
|
||||
reviewed_by,
|
||||
review_remark
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `认领申请${status === 'approved' ? '审核通过' : '审核拒绝'}`,
|
||||
data: claim
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('审核认领申请控制器错误:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message || '审核认领申请失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 续期认领
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async renewClaim(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { duration, payment_method } = req.body;
|
||||
const user_id = req.user.id;
|
||||
|
||||
// 参数验证
|
||||
if (!validatePositiveInteger(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '认领申请ID无效'
|
||||
});
|
||||
}
|
||||
|
||||
if (!validateRequired(duration) || !validatePositiveInteger(duration) || duration < 1 || duration > 60) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '续期时长必须为1-60个月之间的整数'
|
||||
});
|
||||
}
|
||||
|
||||
if (!validateRequired(payment_method)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '支付方式不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
// 续期认领
|
||||
const result = await AnimalClaimService.renewClaim(parseInt(id), user_id, {
|
||||
duration: parseInt(duration),
|
||||
payment_method
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: result.message,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('续期认领控制器错误:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: error.message || '续期认领失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认领统计信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getClaimStatistics(req, res) {
|
||||
try {
|
||||
const { start_date, end_date, animal_type } = req.query;
|
||||
|
||||
// 获取认领统计信息
|
||||
const statistics = await AnimalClaimService.getClaimStatistics({
|
||||
start_date,
|
||||
end_date,
|
||||
animal_type
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取认领统计信息成功',
|
||||
data: statistics
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取认领统计信息控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取认领统计信息失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查认领权限
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async checkClaimPermission(req, res) {
|
||||
try {
|
||||
const { animal_id } = req.params;
|
||||
const user_id = req.user.id;
|
||||
|
||||
// 参数验证
|
||||
if (!validatePositiveInteger(animal_id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '动物ID无效'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查认领权限
|
||||
const hasPermission = await AnimalClaimService.checkClaimPermission(user_id, parseInt(animal_id));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '检查认领权限成功',
|
||||
data: {
|
||||
can_claim: hasPermission
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('检查认领权限控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '检查认领权限失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AnimalClaimController;
|
||||
@@ -1,8 +1,10 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
const UserMySQL = require('../models/UserMySQL');
|
||||
const { AppError } = require('../utils/errors');
|
||||
const { success } = require('../utils/response');
|
||||
const { sendEmail } = require('../utils/email');
|
||||
|
||||
// 生成JWT Token
|
||||
const generateToken = (userId) => {
|
||||
@@ -13,6 +15,20 @@ const generateToken = (userId) => {
|
||||
);
|
||||
};
|
||||
|
||||
// 生成刷新Token
|
||||
const generateRefreshToken = (userId) => {
|
||||
return jwt.sign(
|
||||
{ userId, type: 'refresh' },
|
||||
process.env.JWT_REFRESH_SECRET || 'your-refresh-secret-key',
|
||||
{ expiresIn: process.env.JWT_REFRESH_EXPIRE || '30d' }
|
||||
);
|
||||
};
|
||||
|
||||
// 生成验证码
|
||||
const generateVerificationCode = () => {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
};
|
||||
|
||||
// 用户注册
|
||||
const register = async (req, res, next) => {
|
||||
try {
|
||||
@@ -50,8 +66,9 @@ const register = async (req, res, next) => {
|
||||
// 获取用户信息
|
||||
const user = await UserMySQL.findById(userId);
|
||||
|
||||
// 生成token
|
||||
// 生成token和刷新token
|
||||
const token = generateToken(userId);
|
||||
const refreshToken = generateRefreshToken(userId);
|
||||
|
||||
// 更新最后登录时间
|
||||
await UserMySQL.updateLastLogin(userId);
|
||||
@@ -59,6 +76,7 @@ const register = async (req, res, next) => {
|
||||
res.status(201).json(success({
|
||||
user: UserMySQL.sanitize(user),
|
||||
token,
|
||||
refreshToken,
|
||||
message: '注册成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
@@ -99,8 +117,9 @@ const login = async (req, res, next) => {
|
||||
throw new AppError('密码错误', 401);
|
||||
}
|
||||
|
||||
// 生成token
|
||||
// 生成token和刷新token
|
||||
const token = generateToken(user.id);
|
||||
const refreshToken = generateRefreshToken(user.id);
|
||||
|
||||
// 更新最后登录时间
|
||||
await UserMySQL.updateLastLogin(user.id);
|
||||
@@ -108,6 +127,7 @@ const login = async (req, res, next) => {
|
||||
res.json(success({
|
||||
user: UserMySQL.sanitize(user),
|
||||
token,
|
||||
refreshToken,
|
||||
message: '登录成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
@@ -290,8 +310,9 @@ const adminLogin = async (req, res, next) => {
|
||||
throw new AppError('密码错误', 401);
|
||||
}
|
||||
|
||||
// 生成token
|
||||
// 生成token和refreshToken
|
||||
const token = generateToken(user.id);
|
||||
const refreshToken = generateRefreshToken(user.id);
|
||||
|
||||
// 更新最后登录时间
|
||||
await UserMySQL.updateLastLogin(user.id);
|
||||
@@ -299,6 +320,7 @@ const adminLogin = async (req, res, next) => {
|
||||
// 调整返回数据结构以匹配前端期望的格式
|
||||
res.json(success({
|
||||
token,
|
||||
refreshToken,
|
||||
admin: UserMySQL.sanitize(user),
|
||||
message: '管理员登录成功'
|
||||
}));
|
||||
@@ -307,6 +329,178 @@ const adminLogin = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新Token
|
||||
const refreshToken = async (req, res, next) => {
|
||||
try {
|
||||
const { refreshToken } = req.body;
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new AppError('刷新token不能为空', 400);
|
||||
}
|
||||
|
||||
// 验证刷新token
|
||||
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET || 'your-refresh-secret-key');
|
||||
|
||||
if (decoded.type !== 'refresh') {
|
||||
throw new AppError('无效的刷新token', 401);
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const user = await UserMySQL.findById(decoded.userId);
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404);
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (!UserMySQL.isActive(user)) {
|
||||
throw new AppError('账户已被禁用', 403);
|
||||
}
|
||||
|
||||
// 生成新的访问token
|
||||
const newToken = generateToken(user.id);
|
||||
|
||||
res.json(success({
|
||||
token: newToken,
|
||||
message: 'Token刷新成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
throw new AppError('无效的刷新token', 401);
|
||||
}
|
||||
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
throw new AppError('刷新token已过期', 401);
|
||||
}
|
||||
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 发送邮箱验证码
|
||||
const sendEmailVerification = async (req, res, next) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
throw new AppError('邮箱不能为空', 400);
|
||||
}
|
||||
|
||||
// 检查邮箱格式
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
throw new AppError('邮箱格式不正确', 400);
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
const verificationCode = generateVerificationCode();
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10分钟后过期
|
||||
|
||||
// 保存验证码到数据库(这里需要创建一个验证码表)
|
||||
await UserMySQL.saveVerificationCode(email, verificationCode, expiresAt);
|
||||
|
||||
// 发送邮件
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: '结伴客 - 邮箱验证',
|
||||
html: `
|
||||
<h2>邮箱验证</h2>
|
||||
<p>您的验证码是:<strong>${verificationCode}</strong></p>
|
||||
<p>验证码将在10分钟后过期,请及时使用。</p>
|
||||
<p>如果这不是您的操作,请忽略此邮件。</p>
|
||||
`
|
||||
});
|
||||
|
||||
res.json(success({
|
||||
message: '验证码已发送到您的邮箱'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 忘记密码
|
||||
const forgotPassword = async (req, res, next) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
throw new AppError('邮箱不能为空', 400);
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const user = await UserMySQL.findByEmail(email);
|
||||
if (!user) {
|
||||
// 为了安全,不暴露用户是否存在
|
||||
res.json(success({
|
||||
message: '如果该邮箱已注册,重置密码链接已发送到您的邮箱'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成重置token
|
||||
const resetToken = generateVerificationCode();
|
||||
const expiresAt = new Date(Date.now() + 30 * 60 * 1000); // 30分钟后过期
|
||||
|
||||
// 保存重置token
|
||||
await UserMySQL.savePasswordResetToken(user.id, resetToken, expiresAt);
|
||||
|
||||
// 发送重置邮件
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: '结伴客 - 密码重置',
|
||||
html: `
|
||||
<h2>密码重置</h2>
|
||||
<p>您请求重置密码,请点击下面的链接重置您的密码:</p>
|
||||
<a href="${process.env.FRONTEND_URL}/reset-password?token=${resetToken}">重置密码</a>
|
||||
<p>此链接将在30分钟后过期。</p>
|
||||
<p>如果这不是您的操作,请忽略此邮件。</p>
|
||||
`
|
||||
});
|
||||
|
||||
res.json(success({
|
||||
message: '如果该邮箱已注册,重置密码链接已发送到您的邮箱'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置密码
|
||||
const resetPassword = async (req, res, next) => {
|
||||
try {
|
||||
const { token, newPassword } = req.body;
|
||||
|
||||
if (!token || !newPassword) {
|
||||
throw new AppError('重置token和新密码不能为空', 400);
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
throw new AppError('密码长度不能少于6位', 400);
|
||||
}
|
||||
|
||||
// 验证重置token
|
||||
const resetData = await UserMySQL.findPasswordResetToken(token);
|
||||
if (!resetData || new Date() > resetData.expires_at) {
|
||||
throw new AppError('重置token无效或已过期', 400);
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 12);
|
||||
|
||||
// 更新密码
|
||||
await UserMySQL.updatePassword(resetData.user_id, hashedPassword);
|
||||
|
||||
// 删除重置token
|
||||
await UserMySQL.deletePasswordResetToken(token);
|
||||
|
||||
res.json(success({
|
||||
message: '密码重置成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
@@ -314,5 +508,9 @@ module.exports = {
|
||||
updateProfile,
|
||||
changePassword,
|
||||
wechatLogin,
|
||||
adminLogin
|
||||
adminLogin,
|
||||
refreshToken,
|
||||
sendEmailVerification,
|
||||
forgotPassword,
|
||||
resetPassword
|
||||
};
|
||||
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
|
||||
};
|
||||
@@ -191,44 +191,43 @@ async function cancelOrder(req, res, next) {
|
||||
async function payOrder(req, res, next) {
|
||||
try {
|
||||
const { orderId } = req.params;
|
||||
const userId = req.user.id;
|
||||
const paymentData = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
// 验证必要字段
|
||||
if (!paymentData.payment_method) {
|
||||
if (!paymentData.payment_method || !paymentData.amount) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '缺少必要字段: payment_method'
|
||||
message: '缺少必要字段: payment_method, amount'
|
||||
});
|
||||
}
|
||||
|
||||
const order = await OrderService.payOrder(orderId, userId, paymentData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单支付成功',
|
||||
data: order
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('支付订单控制器错误:', error);
|
||||
if (error.message === '订单不存在') {
|
||||
// 获取订单并验证权限
|
||||
const order = await OrderService.getOrderById(orderId);
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
});
|
||||
}
|
||||
if (error.message === '无权操作此订单') {
|
||||
|
||||
// 检查权限:用户只能支付自己的订单
|
||||
if (req.user.role === 'user' && order.user_id !== userId) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '无权操作此订单'
|
||||
});
|
||||
}
|
||||
if (error.message === '订单状态不允许支付') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '订单状态不允许支付'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await OrderService.payOrder(orderId, paymentData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '支付订单创建成功',
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('支付订单控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '支付订单失败'
|
||||
@@ -243,8 +242,7 @@ async function payOrder(req, res, next) {
|
||||
*/
|
||||
async function getOrderStatistics(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const statistics = await OrderService.getOrderStatistics(userId);
|
||||
const statistics = await OrderService.getOrderStatistics();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
|
||||
313
backend/src/controllers/payment.js
Normal file
313
backend/src/controllers/payment.js
Normal file
@@ -0,0 +1,313 @@
|
||||
const { validationResult } = require('express-validator');
|
||||
|
||||
/**
|
||||
* 支付控制器
|
||||
* 处理支付相关的业务逻辑
|
||||
*/
|
||||
class PaymentController {
|
||||
/**
|
||||
* 创建支付订单
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async createPayment(req, res) {
|
||||
try {
|
||||
// 验证请求参数
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { order_id, amount, payment_method } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
// 模拟支付创建逻辑
|
||||
const payment = {
|
||||
id: Date.now(),
|
||||
order_id,
|
||||
user_id: userId,
|
||||
amount,
|
||||
payment_method,
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '支付订单创建成功',
|
||||
data: payment
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建支付订单失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建支付订单失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付订单详情
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getPayment(req, res) {
|
||||
try {
|
||||
const { paymentId } = req.params;
|
||||
|
||||
// 模拟获取支付详情
|
||||
const payment = {
|
||||
id: paymentId,
|
||||
order_id: 1,
|
||||
user_id: req.user.id,
|
||||
amount: 100.00,
|
||||
payment_method: 'wechat',
|
||||
status: 'completed',
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取成功',
|
||||
data: payment
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取支付订单失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取支付订单失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付状态
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async queryPaymentStatus(req, res) {
|
||||
try {
|
||||
const { paymentId } = req.params;
|
||||
|
||||
// 模拟查询支付状态
|
||||
const status = {
|
||||
payment_id: paymentId,
|
||||
status: 'completed',
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '查询成功',
|
||||
data: status
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('查询支付状态失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '查询支付状态失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理微信支付回调
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async handleWechatCallback(req, res) {
|
||||
try {
|
||||
console.log('微信支付回调:', req.body);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '回调处理成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('处理微信支付回调失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '处理回调失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付宝支付回调
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async handleAlipayCallback(req, res) {
|
||||
try {
|
||||
console.log('支付宝支付回调:', req.body);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '回调处理成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('处理支付宝支付回调失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '处理回调失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建退款
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async createRefund(req, res) {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { paymentId } = req.params;
|
||||
const { amount, reason } = req.body;
|
||||
|
||||
// 模拟创建退款
|
||||
const refund = {
|
||||
id: Date.now(),
|
||||
payment_id: paymentId,
|
||||
amount,
|
||||
reason,
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '退款申请创建成功',
|
||||
data: refund
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建退款失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建退款失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取退款详情
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getRefund(req, res) {
|
||||
try {
|
||||
const { refundId } = req.params;
|
||||
|
||||
// 模拟获取退款详情
|
||||
const refund = {
|
||||
id: refundId,
|
||||
payment_id: 1,
|
||||
amount: 50.00,
|
||||
reason: '商品质量问题',
|
||||
status: 'completed',
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取成功',
|
||||
data: refund
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取退款详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取退款详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退款
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async processRefund(req, res) {
|
||||
try {
|
||||
const { refundId } = req.params;
|
||||
const { action } = req.body;
|
||||
|
||||
// 模拟处理退款
|
||||
const result = {
|
||||
refund_id: refundId,
|
||||
action,
|
||||
status: action === 'approve' ? 'approved' : 'rejected',
|
||||
processed_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '退款处理成功',
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('处理退款失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '处理退款失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付统计
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getPaymentStatistics(req, res) {
|
||||
try {
|
||||
// 模拟支付统计数据
|
||||
const statistics = {
|
||||
total_payments: 1250,
|
||||
total_amount: 125000.00,
|
||||
successful_payments: 1200,
|
||||
failed_payments: 50,
|
||||
refund_count: 25,
|
||||
refund_amount: 2500.00,
|
||||
payment_methods: {
|
||||
wechat: 600,
|
||||
alipay: 500,
|
||||
balance: 100
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取成功',
|
||||
data: statistics
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取支付统计失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取支付统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PaymentController;
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
const TravelService = require('../../services/travel');
|
||||
const TravelService = require('../../services/travel/index');
|
||||
const { success } = require('../../utils/response');
|
||||
const { AppError } = require('../../utils/errors');
|
||||
|
||||
@@ -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({
|
||||
@@ -133,12 +149,12 @@ class TravelController {
|
||||
// 获取所有旅行计划(管理员功能)
|
||||
static async getAllTravelPlans(req, res, next) {
|
||||
try {
|
||||
const { page, pageSize, status, userId } = req.query;
|
||||
const { page, limit, keyword, status } = req.query;
|
||||
|
||||
const result = await TravelService.getTravelPlans({
|
||||
userId,
|
||||
page: parseInt(page) || 1,
|
||||
pageSize: parseInt(pageSize) || 10,
|
||||
pageSize: parseInt(limit) || 20,
|
||||
keyword,
|
||||
status
|
||||
});
|
||||
|
||||
|
||||
163
backend/src/controllers/travelRegistration.js
Normal file
163
backend/src/controllers/travelRegistration.js
Normal file
@@ -0,0 +1,163 @@
|
||||
const TravelRegistrationService = require('../services/travelRegistration');
|
||||
const { success } = require('../utils/response');
|
||||
const { AppError } = require('../utils/errors');
|
||||
|
||||
/**
|
||||
* 旅行活动报名控制器
|
||||
*/
|
||||
class TravelRegistrationController {
|
||||
/**
|
||||
* 报名参加旅行活动
|
||||
*/
|
||||
static async registerForTravel(req, res, next) {
|
||||
try {
|
||||
const { travelId } = req.params;
|
||||
const { message, emergencyContact, emergencyPhone } = req.body;
|
||||
const userId = req.userId;
|
||||
|
||||
if (!travelId) {
|
||||
throw new AppError('旅行活动ID不能为空', 400);
|
||||
}
|
||||
|
||||
const registration = await TravelRegistrationService.registerForTravel({
|
||||
userId,
|
||||
travelId: parseInt(travelId),
|
||||
message,
|
||||
emergencyContact,
|
||||
emergencyPhone
|
||||
});
|
||||
|
||||
res.json(success({
|
||||
registration,
|
||||
message: '报名成功,等待审核'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消报名
|
||||
*/
|
||||
static async cancelRegistration(req, res, next) {
|
||||
try {
|
||||
const { registrationId } = req.params;
|
||||
const userId = req.userId;
|
||||
|
||||
if (!registrationId) {
|
||||
throw new AppError('报名记录ID不能为空', 400);
|
||||
}
|
||||
|
||||
await TravelRegistrationService.cancelRegistration(parseInt(registrationId), userId);
|
||||
|
||||
res.json(success({
|
||||
message: '取消报名成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的报名记录
|
||||
*/
|
||||
static async getUserRegistrations(req, res, next) {
|
||||
try {
|
||||
const { page, pageSize, status } = req.query;
|
||||
const userId = req.userId;
|
||||
|
||||
const result = await TravelRegistrationService.getUserRegistrations({
|
||||
userId,
|
||||
page: parseInt(page) || 1,
|
||||
pageSize: parseInt(pageSize) || 10,
|
||||
status
|
||||
});
|
||||
|
||||
res.json(success(result));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取旅行活动的报名列表(活动发起者可查看)
|
||||
*/
|
||||
static async getTravelRegistrations(req, res, next) {
|
||||
try {
|
||||
const { travelId } = req.params;
|
||||
const { page, pageSize, status } = req.query;
|
||||
const userId = req.userId;
|
||||
|
||||
if (!travelId) {
|
||||
throw new AppError('旅行活动ID不能为空', 400);
|
||||
}
|
||||
|
||||
const result = await TravelRegistrationService.getTravelRegistrations({
|
||||
travelId: parseInt(travelId),
|
||||
organizerId: userId,
|
||||
page: parseInt(page) || 1,
|
||||
pageSize: parseInt(pageSize) || 10,
|
||||
status
|
||||
});
|
||||
|
||||
res.json(success(result));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核报名申请(活动发起者操作)
|
||||
*/
|
||||
static async reviewRegistration(req, res, next) {
|
||||
try {
|
||||
const { registrationId } = req.params;
|
||||
const { action, rejectReason } = req.body;
|
||||
const userId = req.userId;
|
||||
|
||||
if (!registrationId) {
|
||||
throw new AppError('报名记录ID不能为空', 400);
|
||||
}
|
||||
|
||||
if (!['approve', 'reject'].includes(action)) {
|
||||
throw new AppError('操作类型无效', 400);
|
||||
}
|
||||
|
||||
const result = await TravelRegistrationService.reviewRegistration({
|
||||
registrationId: parseInt(registrationId),
|
||||
organizerId: userId,
|
||||
action,
|
||||
rejectReason
|
||||
});
|
||||
|
||||
res.json(success({
|
||||
registration: result,
|
||||
message: action === 'approve' ? '审核通过' : '已拒绝申请'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取报名统计信息
|
||||
*/
|
||||
static async getRegistrationStats(req, res, next) {
|
||||
try {
|
||||
const { travelId } = req.params;
|
||||
const userId = req.userId;
|
||||
|
||||
if (!travelId) {
|
||||
throw new AppError('旅行活动ID不能为空', 400);
|
||||
}
|
||||
|
||||
const stats = await TravelRegistrationService.getRegistrationStats(parseInt(travelId), userId);
|
||||
|
||||
res.json(success({ stats }));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TravelRegistrationController;
|
||||
@@ -17,8 +17,15 @@ async function authenticateUser(req, res, next) {
|
||||
// 验证token
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key');
|
||||
|
||||
// 兼容管理员token和用户token
|
||||
const userId = decoded.userId || decoded.id;
|
||||
|
||||
if (!userId) {
|
||||
throw new AppError('token中缺少用户ID', 401);
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const user = await UserMySQL.findById(decoded.userId);
|
||||
const user = await UserMySQL.findById(userId);
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 401);
|
||||
}
|
||||
@@ -30,7 +37,7 @@ async function authenticateUser(req, res, next) {
|
||||
|
||||
// 将用户信息添加到请求对象
|
||||
req.user = UserMySQL.sanitize(user);
|
||||
req.userId = decoded.userId; // 同时设置userId,保持与现有控制器的兼容性
|
||||
req.userId = userId; // 同时设置userId,保持与现有控制器的兼容性
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
|
||||
261
backend/src/middleware/errorHandler.js
Normal file
261
backend/src/middleware/errorHandler.js
Normal file
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* 统一错误处理中间件
|
||||
* 处理应用程序中的所有错误,提供统一的错误响应格式
|
||||
*/
|
||||
|
||||
const { logger } = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* 自定义错误类
|
||||
*/
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode, errorCode = null) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.errorCode = errorCode;
|
||||
this.isOperational = true;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步错误捕获包装器
|
||||
* @param {Function} fn - 异步函数
|
||||
* @returns {Function} 包装后的函数
|
||||
*/
|
||||
const catchAsync = (fn) => {
|
||||
return (req, res, next) => {
|
||||
fn(req, res, next).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理数据库错误
|
||||
* @param {Error} err - 数据库错误
|
||||
* @returns {AppError} 应用错误
|
||||
*/
|
||||
const handleDatabaseError = (err) => {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
return new AppError('数据已存在,请检查输入信息', 400, 'DUPLICATE_ENTRY');
|
||||
}
|
||||
|
||||
if (err.code === 'ER_NO_REFERENCED_ROW_2') {
|
||||
return new AppError('关联数据不存在', 400, 'FOREIGN_KEY_CONSTRAINT');
|
||||
}
|
||||
|
||||
if (err.code === 'ER_ROW_IS_REFERENCED_2') {
|
||||
return new AppError('数据正在被使用,无法删除', 400, 'REFERENCED_DATA');
|
||||
}
|
||||
|
||||
if (err.code === 'ER_DATA_TOO_LONG') {
|
||||
return new AppError('输入数据过长', 400, 'DATA_TOO_LONG');
|
||||
}
|
||||
|
||||
if (err.code === 'ER_BAD_NULL_ERROR') {
|
||||
return new AppError('必填字段不能为空', 400, 'REQUIRED_FIELD_MISSING');
|
||||
}
|
||||
|
||||
return new AppError('数据库操作失败', 500, 'DATABASE_ERROR');
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理JWT错误
|
||||
* @param {Error} err - JWT错误
|
||||
* @returns {AppError} 应用错误
|
||||
*/
|
||||
const handleJWTError = (err) => {
|
||||
if (err.name === 'JsonWebTokenError') {
|
||||
return new AppError('无效的访问令牌', 401, 'INVALID_TOKEN');
|
||||
}
|
||||
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
return new AppError('访问令牌已过期', 401, 'TOKEN_EXPIRED');
|
||||
}
|
||||
|
||||
return new AppError('令牌验证失败', 401, 'TOKEN_VERIFICATION_FAILED');
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理验证错误
|
||||
* @param {Error} err - 验证错误
|
||||
* @returns {AppError} 应用错误
|
||||
*/
|
||||
const handleValidationError = (err) => {
|
||||
if (err.name === 'ValidationError') {
|
||||
const errors = Object.values(err.errors).map(e => e.message);
|
||||
return new AppError(`数据验证失败: ${errors.join(', ')}`, 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
|
||||
return new AppError('数据格式错误', 400, 'INVALID_DATA_FORMAT');
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理文件上传错误
|
||||
* @param {Error} err - 文件上传错误
|
||||
* @returns {AppError} 应用错误
|
||||
*/
|
||||
const handleFileUploadError = (err) => {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return new AppError('文件大小超出限制', 400, 'FILE_TOO_LARGE');
|
||||
}
|
||||
|
||||
if (err.code === 'LIMIT_FILE_COUNT') {
|
||||
return new AppError('文件数量超出限制', 400, 'TOO_MANY_FILES');
|
||||
}
|
||||
|
||||
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
|
||||
return new AppError('不支持的文件类型', 400, 'UNSUPPORTED_FILE_TYPE');
|
||||
}
|
||||
|
||||
return new AppError('文件上传失败', 400, 'FILE_UPLOAD_ERROR');
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送错误响应
|
||||
* @param {Error} err - 错误对象
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const sendErrorResponse = (err, req, res) => {
|
||||
const { statusCode, message, errorCode } = err;
|
||||
|
||||
// 构建错误响应
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
message: message || '服务器内部错误',
|
||||
error_code: errorCode || 'INTERNAL_ERROR',
|
||||
timestamp: new Date().toISOString(),
|
||||
path: req.originalUrl,
|
||||
method: req.method
|
||||
};
|
||||
|
||||
// 开发环境下包含错误堆栈
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
errorResponse.stack = err.stack;
|
||||
errorResponse.details = err;
|
||||
}
|
||||
|
||||
// 记录错误日志
|
||||
logger.error('API Error:', {
|
||||
message: err.message,
|
||||
statusCode,
|
||||
errorCode,
|
||||
path: req.originalUrl,
|
||||
method: req.method,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip,
|
||||
userId: req.user?.id,
|
||||
stack: err.stack
|
||||
});
|
||||
|
||||
res.status(statusCode).json(errorResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 全局错误处理中间件
|
||||
* @param {Error} err - 错误对象
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
const globalErrorHandler = (err, req, res, next) => {
|
||||
// 设置默认错误状态码
|
||||
err.statusCode = err.statusCode || 500;
|
||||
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
|
||||
// 处理不同类型的错误
|
||||
if (err.code && err.code.startsWith('ER_')) {
|
||||
error = handleDatabaseError(err);
|
||||
} else if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') {
|
||||
error = handleJWTError(err);
|
||||
} else if (err.name === 'ValidationError') {
|
||||
error = handleValidationError(err);
|
||||
} else if (err.code && err.code.startsWith('LIMIT_')) {
|
||||
error = handleFileUploadError(err);
|
||||
} else if (err.name === 'CastError') {
|
||||
error = new AppError('无效的数据格式', 400, 'INVALID_DATA_FORMAT');
|
||||
} else if (err.code === 'ENOENT') {
|
||||
error = new AppError('文件不存在', 404, 'FILE_NOT_FOUND');
|
||||
} else if (err.code === 'EACCES') {
|
||||
error = new AppError('文件访问权限不足', 403, 'FILE_ACCESS_DENIED');
|
||||
}
|
||||
|
||||
// 如果不是操作性错误,设置为服务器错误
|
||||
if (!error.isOperational) {
|
||||
error.statusCode = 500;
|
||||
error.message = '服务器内部错误';
|
||||
error.errorCode = 'INTERNAL_ERROR';
|
||||
}
|
||||
|
||||
sendErrorResponse(error, req, res);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理未找到的路由
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
* @param {Function} next - 下一个中间件
|
||||
*/
|
||||
const notFoundHandler = (req, res, next) => {
|
||||
const err = new AppError(`路由 ${req.originalUrl} 不存在`, 404, 'ROUTE_NOT_FOUND');
|
||||
next(err);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理未捕获的Promise拒绝
|
||||
*/
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
// 优雅关闭服务器
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理未捕获的异常
|
||||
*/
|
||||
process.on('uncaughtException', (err) => {
|
||||
logger.error('Uncaught Exception:', err);
|
||||
// 优雅关闭服务器
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
/**
|
||||
* 常用错误类型
|
||||
*/
|
||||
const ErrorTypes = {
|
||||
// 认证相关
|
||||
UNAUTHORIZED: (message = '未授权访问') => new AppError(message, 401, 'UNAUTHORIZED'),
|
||||
FORBIDDEN: (message = '权限不足') => new AppError(message, 403, 'FORBIDDEN'),
|
||||
TOKEN_EXPIRED: (message = '访问令牌已过期') => new AppError(message, 401, 'TOKEN_EXPIRED'),
|
||||
|
||||
// 数据相关
|
||||
NOT_FOUND: (message = '资源不存在') => new AppError(message, 404, 'NOT_FOUND'),
|
||||
DUPLICATE_ENTRY: (message = '数据已存在') => new AppError(message, 400, 'DUPLICATE_ENTRY'),
|
||||
VALIDATION_ERROR: (message = '数据验证失败') => new AppError(message, 400, 'VALIDATION_ERROR'),
|
||||
|
||||
// 业务相关
|
||||
BUSINESS_ERROR: (message = '业务处理失败') => new AppError(message, 400, 'BUSINESS_ERROR'),
|
||||
INSUFFICIENT_BALANCE: (message = '余额不足') => new AppError(message, 400, 'INSUFFICIENT_BALANCE'),
|
||||
OPERATION_NOT_ALLOWED: (message = '操作不被允许') => new AppError(message, 400, 'OPERATION_NOT_ALLOWED'),
|
||||
|
||||
// 系统相关
|
||||
INTERNAL_ERROR: (message = '服务器内部错误') => new AppError(message, 500, 'INTERNAL_ERROR'),
|
||||
SERVICE_UNAVAILABLE: (message = '服务暂不可用') => new AppError(message, 503, 'SERVICE_UNAVAILABLE'),
|
||||
RATE_LIMIT_EXCEEDED: (message = '请求频率超出限制') => new AppError(message, 429, 'RATE_LIMIT_EXCEEDED'),
|
||||
|
||||
// 文件相关
|
||||
FILE_TOO_LARGE: (message = '文件大小超出限制') => new AppError(message, 400, 'FILE_TOO_LARGE'),
|
||||
UNSUPPORTED_FILE_TYPE: (message = '不支持的文件类型') => new AppError(message, 400, 'UNSUPPORTED_FILE_TYPE'),
|
||||
FILE_UPLOAD_ERROR: (message = '文件上传失败') => new AppError(message, 400, 'FILE_UPLOAD_ERROR')
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
catchAsync,
|
||||
globalErrorHandler,
|
||||
notFoundHandler,
|
||||
ErrorTypes
|
||||
};
|
||||
501
backend/src/middleware/upload.js
Normal file
501
backend/src/middleware/upload.js
Normal file
@@ -0,0 +1,501 @@
|
||||
/**
|
||||
* 文件上传中间件
|
||||
* 支持图片上传、文件类型验证、大小限制等功能
|
||||
*/
|
||||
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
// 尝试加载 sharp,如果失败则使用备用方案
|
||||
let sharp;
|
||||
try {
|
||||
sharp = require('sharp');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Sharp 库加载失败,图片处理功能将被禁用:', error.message);
|
||||
sharp = null;
|
||||
}
|
||||
const { AppError, ErrorTypes } = require('./errorHandler');
|
||||
const { logSystemEvent, logError } = require('../utils/logger');
|
||||
|
||||
// 确保上传目录存在
|
||||
const uploadDir = path.join(__dirname, '../../uploads');
|
||||
const avatarDir = path.join(uploadDir, 'avatars');
|
||||
const animalDir = path.join(uploadDir, 'animals');
|
||||
const travelDir = path.join(uploadDir, 'travels');
|
||||
const documentDir = path.join(uploadDir, 'documents');
|
||||
|
||||
[uploadDir, avatarDir, animalDir, travelDir, documentDir].forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 生成唯一文件名
|
||||
* @param {string} originalName - 原始文件名
|
||||
* @returns {string} 唯一文件名
|
||||
*/
|
||||
const generateUniqueFileName = (originalName) => {
|
||||
const timestamp = Date.now();
|
||||
const randomString = crypto.randomBytes(8).toString('hex');
|
||||
const ext = path.extname(originalName).toLowerCase();
|
||||
return `${timestamp}_${randomString}${ext}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取文件存储目录
|
||||
* @param {string} type - 文件类型
|
||||
* @returns {string} 存储目录路径
|
||||
*/
|
||||
const getStorageDir = (type) => {
|
||||
switch (type) {
|
||||
case 'avatar':
|
||||
return avatarDir;
|
||||
case 'animal':
|
||||
return animalDir;
|
||||
case 'travel':
|
||||
return travelDir;
|
||||
case 'document':
|
||||
return documentDir;
|
||||
default:
|
||||
return uploadDir;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 文件过滤器
|
||||
* @param {string} type - 文件类型
|
||||
* @returns {Function} 过滤器函数
|
||||
*/
|
||||
const createFileFilter = (type) => {
|
||||
return (req, file, cb) => {
|
||||
try {
|
||||
let allowedTypes = [];
|
||||
let allowedMimes = [];
|
||||
|
||||
switch (type) {
|
||||
case 'image':
|
||||
allowedTypes = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
break;
|
||||
case 'document':
|
||||
allowedTypes = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt'];
|
||||
allowedMimes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/plain'
|
||||
];
|
||||
break;
|
||||
case 'avatar':
|
||||
allowedTypes = ['.jpg', '.jpeg', '.png'];
|
||||
allowedMimes = ['image/jpeg', 'image/png'];
|
||||
break;
|
||||
default:
|
||||
allowedTypes = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf', '.doc', '.docx'];
|
||||
allowedMimes = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||
'application/pdf', 'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
];
|
||||
}
|
||||
|
||||
const fileExt = path.extname(file.originalname).toLowerCase();
|
||||
|
||||
if (allowedTypes.includes(fileExt) && allowedMimes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new AppError(`不支持的文件类型。允许的类型: ${allowedTypes.join(', ')}`, 400, 'UNSUPPORTED_FILE_TYPE'));
|
||||
}
|
||||
} catch (error) {
|
||||
cb(error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建存储配置
|
||||
* @param {string} type - 文件类型
|
||||
* @returns {Object} 存储配置
|
||||
*/
|
||||
const createStorage = (type) => {
|
||||
return multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const dir = getStorageDir(type);
|
||||
cb(null, dir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueName = generateUniqueFileName(file.originalname);
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建上传中间件
|
||||
* @param {Object} options - 配置选项
|
||||
* @returns {Function} 上传中间件
|
||||
*/
|
||||
const createUploadMiddleware = (options = {}) => {
|
||||
const {
|
||||
type = 'image',
|
||||
maxSize = 5 * 1024 * 1024, // 5MB
|
||||
maxFiles = 1,
|
||||
fieldName = 'file'
|
||||
} = options;
|
||||
|
||||
const upload = multer({
|
||||
storage: createStorage(type),
|
||||
fileFilter: createFileFilter(type),
|
||||
limits: {
|
||||
fileSize: maxSize,
|
||||
files: maxFiles
|
||||
}
|
||||
});
|
||||
|
||||
return (req, res, next) => {
|
||||
const uploadHandler = maxFiles === 1 ? upload.single(fieldName) : upload.array(fieldName, maxFiles);
|
||||
|
||||
uploadHandler(req, res, (err) => {
|
||||
if (err) {
|
||||
logError(err, {
|
||||
type: 'file_upload_error',
|
||||
userId: req.user?.id,
|
||||
fieldName,
|
||||
maxSize,
|
||||
maxFiles
|
||||
});
|
||||
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return next(ErrorTypes.FILE_TOO_LARGE(`文件大小不能超过 ${Math.round(maxSize / 1024 / 1024)}MB`));
|
||||
} else if (err.code === 'LIMIT_FILE_COUNT') {
|
||||
return next(ErrorTypes.FILE_UPLOAD_ERROR(`文件数量不能超过 ${maxFiles} 个`));
|
||||
} else if (err.code === 'LIMIT_UNEXPECTED_FILE') {
|
||||
return next(ErrorTypes.UNSUPPORTED_FILE_TYPE('不支持的文件字段'));
|
||||
}
|
||||
}
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// 记录上传成功日志
|
||||
if (req.file || req.files) {
|
||||
const files = req.files || [req.file];
|
||||
logSystemEvent('file_uploaded', {
|
||||
userId: req.user?.id,
|
||||
fileCount: files.length,
|
||||
files: files.map(f => ({
|
||||
originalName: f.originalname,
|
||||
filename: f.filename,
|
||||
size: f.size,
|
||||
mimetype: f.mimetype
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 图片处理中间件
|
||||
* @param {Object} options - 处理选项
|
||||
* @returns {Function} 处理中间件
|
||||
*/
|
||||
const processImage = (options = {}) => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
if (!req.file && !req.files) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 如果 sharp 不可用,跳过图片处理
|
||||
if (!sharp) {
|
||||
console.warn('⚠️ Sharp 不可用,跳过图片处理');
|
||||
return next();
|
||||
}
|
||||
|
||||
const files = req.files || [req.file];
|
||||
const processedFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
// 只处理图片文件
|
||||
if (!file.mimetype.startsWith('image/')) {
|
||||
processedFiles.push(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
const {
|
||||
width = null,
|
||||
height = null,
|
||||
quality = 80,
|
||||
format = 'jpeg',
|
||||
thumbnail = false,
|
||||
thumbnailSize = 200
|
||||
} = options;
|
||||
|
||||
const inputPath = file.path;
|
||||
const outputPath = inputPath.replace(path.extname(inputPath), `.${format}`);
|
||||
|
||||
let sharpInstance = sharp(inputPath);
|
||||
|
||||
// 调整尺寸
|
||||
if (width || height) {
|
||||
sharpInstance = sharpInstance.resize(width, height, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
});
|
||||
}
|
||||
|
||||
// 设置质量和格式
|
||||
if (format === 'jpeg') {
|
||||
sharpInstance = sharpInstance.jpeg({ quality });
|
||||
} else if (format === 'png') {
|
||||
sharpInstance = sharpInstance.png({ quality });
|
||||
} else if (format === 'webp') {
|
||||
sharpInstance = sharpInstance.webp({ quality });
|
||||
}
|
||||
|
||||
// 保存处理后的图片
|
||||
await sharpInstance.toFile(outputPath);
|
||||
|
||||
// 删除原始文件(如果格式不同)
|
||||
if (inputPath !== outputPath) {
|
||||
fs.unlinkSync(inputPath);
|
||||
}
|
||||
|
||||
// 更新文件信息
|
||||
file.path = outputPath;
|
||||
file.filename = path.basename(outputPath);
|
||||
|
||||
// 生成缩略图
|
||||
if (thumbnail) {
|
||||
const thumbnailPath = outputPath.replace(
|
||||
path.extname(outputPath),
|
||||
`_thumb${path.extname(outputPath)}`
|
||||
);
|
||||
|
||||
await sharp(outputPath)
|
||||
.resize(thumbnailSize, thumbnailSize, {
|
||||
fit: 'cover',
|
||||
position: 'center'
|
||||
})
|
||||
.jpeg({ quality: 70 })
|
||||
.toFile(thumbnailPath);
|
||||
|
||||
file.thumbnail = path.basename(thumbnailPath);
|
||||
}
|
||||
|
||||
processedFiles.push(file);
|
||||
}
|
||||
|
||||
// 更新请求对象
|
||||
if (req.files) {
|
||||
req.files = processedFiles;
|
||||
} else {
|
||||
req.file = processedFiles[0];
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logError(error, {
|
||||
type: 'image_processing_error',
|
||||
userId: req.user?.id,
|
||||
options
|
||||
});
|
||||
next(ErrorTypes.FILE_UPLOAD_ERROR('图片处理失败'));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {Promise<boolean>} 删除结果
|
||||
*/
|
||||
const deleteFile = async (filePath) => {
|
||||
try {
|
||||
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(uploadDir, filePath);
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
fs.unlinkSync(fullPath);
|
||||
|
||||
// 同时删除缩略图
|
||||
const thumbnailPath = fullPath.replace(
|
||||
path.extname(fullPath),
|
||||
`_thumb${path.extname(fullPath)}`
|
||||
);
|
||||
if (fs.existsSync(thumbnailPath)) {
|
||||
fs.unlinkSync(thumbnailPath);
|
||||
}
|
||||
|
||||
logSystemEvent('file_deleted', { filePath: fullPath });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
logError(error, { type: 'file_deletion_error', filePath });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取文件信息
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {Object|null} 文件信息
|
||||
*/
|
||||
const getFileInfo = (filePath) => {
|
||||
try {
|
||||
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(uploadDir, filePath);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(fullPath);
|
||||
const ext = path.extname(fullPath).toLowerCase();
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
size: stats.size,
|
||||
created: stats.birthtime,
|
||||
modified: stats.mtime,
|
||||
extension: ext,
|
||||
isImage: ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext)
|
||||
};
|
||||
} catch (error) {
|
||||
logError(error, { type: 'file_info_error', filePath });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理临时文件
|
||||
* @param {number} maxAge - 最大存在时间(毫秒)
|
||||
*/
|
||||
const cleanupTempFiles = (maxAge = 24 * 60 * 60 * 1000) => {
|
||||
const tempDir = path.join(uploadDir, 'temp');
|
||||
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.readdir(tempDir, (err, files) => {
|
||||
if (err) {
|
||||
logError(err, { type: 'temp_cleanup_error' });
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(tempDir, file);
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if (err) return;
|
||||
|
||||
if (now - stats.mtime.getTime() > maxAge) {
|
||||
fs.unlink(filePath, (err) => {
|
||||
if (err) {
|
||||
logError(err, { type: 'temp_file_deletion_error', filePath });
|
||||
} else {
|
||||
logSystemEvent('temp_file_cleaned', { filePath });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 每小时清理一次临时文件
|
||||
setInterval(cleanupTempFiles, 60 * 60 * 1000);
|
||||
|
||||
/**
|
||||
* 预定义的上传中间件
|
||||
*/
|
||||
const uploadMiddlewares = {
|
||||
// 头像上传
|
||||
avatar: createUploadMiddleware({
|
||||
type: 'avatar',
|
||||
maxSize: 2 * 1024 * 1024, // 2MB
|
||||
maxFiles: 1,
|
||||
fieldName: 'avatar'
|
||||
}),
|
||||
|
||||
// 动物图片上传
|
||||
animalImages: createUploadMiddleware({
|
||||
type: 'animal',
|
||||
maxSize: 5 * 1024 * 1024, // 5MB
|
||||
maxFiles: 5,
|
||||
fieldName: 'images'
|
||||
}),
|
||||
|
||||
// 旅行图片上传
|
||||
travelImages: createUploadMiddleware({
|
||||
type: 'travel',
|
||||
maxSize: 5 * 1024 * 1024, // 5MB
|
||||
maxFiles: 10,
|
||||
fieldName: 'images'
|
||||
}),
|
||||
|
||||
// 文档上传
|
||||
documents: createUploadMiddleware({
|
||||
type: 'document',
|
||||
maxSize: 10 * 1024 * 1024, // 10MB
|
||||
maxFiles: 3,
|
||||
fieldName: 'documents'
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* 预定义的图片处理中间件
|
||||
*/
|
||||
const imageProcessors = {
|
||||
// 头像处理
|
||||
avatar: processImage({
|
||||
width: 300,
|
||||
height: 300,
|
||||
quality: 85,
|
||||
format: 'jpeg',
|
||||
thumbnail: true,
|
||||
thumbnailSize: 100
|
||||
}),
|
||||
|
||||
// 动物图片处理
|
||||
animal: processImage({
|
||||
width: 800,
|
||||
height: 600,
|
||||
quality: 80,
|
||||
format: 'jpeg',
|
||||
thumbnail: true,
|
||||
thumbnailSize: 200
|
||||
}),
|
||||
|
||||
// 旅行图片处理
|
||||
travel: processImage({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
quality: 80,
|
||||
format: 'jpeg',
|
||||
thumbnail: true,
|
||||
thumbnailSize: 300
|
||||
})
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createUploadMiddleware,
|
||||
processImage,
|
||||
deleteFile,
|
||||
getFileInfo,
|
||||
cleanupTempFiles,
|
||||
uploadMiddlewares,
|
||||
imageProcessors,
|
||||
generateUniqueFileName,
|
||||
getStorageDir
|
||||
};
|
||||
434
backend/src/models/Animal.js
Normal file
434
backend/src/models/Animal.js
Normal file
@@ -0,0 +1,434 @@
|
||||
const { query } = require('../config/database');
|
||||
|
||||
/**
|
||||
* 动物模型类
|
||||
* 处理动物相关的数据库操作
|
||||
*/
|
||||
class Animal {
|
||||
/**
|
||||
* 根据ID查找动物
|
||||
* @param {number} id - 动物ID
|
||||
* @returns {Object|null} 动物信息
|
||||
*/
|
||||
static async findById(id) {
|
||||
try {
|
||||
const [rows] = await query(
|
||||
'SELECT * FROM animals WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
return rows[0] || null;
|
||||
} catch (error) {
|
||||
console.error('查找动物失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物列表(包含商家信息)
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Array} 动物列表
|
||||
*/
|
||||
static async getAnimalListWithMerchant(options = {}) {
|
||||
try {
|
||||
const {
|
||||
whereClause = '',
|
||||
params = [],
|
||||
sortBy = 'created_at',
|
||||
sortOrder = 'desc',
|
||||
limit = 10,
|
||||
offset = 0
|
||||
} = options;
|
||||
|
||||
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 ${whereClause}
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [...params, limit, offset]);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('获取动物列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物数量
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {number} 动物数量
|
||||
*/
|
||||
static async getAnimalCount(options = {}) {
|
||||
try {
|
||||
const { whereClause = '', params = [] } = options;
|
||||
|
||||
const query = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM animals a
|
||||
WHERE 1=1 ${whereClause}
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, params);
|
||||
return rows[0].count;
|
||||
} catch (error) {
|
||||
console.error('获取动物数量失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物详情(包含商家信息)
|
||||
* @param {number} id - 动物ID
|
||||
* @returns {Object|null} 动物详情
|
||||
*/
|
||||
static async getAnimalDetailWithMerchant(id) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
a.*,
|
||||
a.type,
|
||||
m.name as merchant_name,
|
||||
m.contact_phone as merchant_phone,
|
||||
m.address as merchant_address
|
||||
FROM animals a
|
||||
LEFT JOIN merchants m ON a.merchant_id = m.id
|
||||
WHERE a.id = ?
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [id]);
|
||||
return rows[0] || null;
|
||||
} catch (error) {
|
||||
console.error('获取动物详情失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动物状态
|
||||
* @param {number} id - 动物ID
|
||||
* @param {string} status - 新状态
|
||||
* @param {number} adminId - 管理员ID
|
||||
* @param {string} reason - 更新原因
|
||||
* @returns {Object} 更新结果
|
||||
*/
|
||||
static async updateAnimalStatus(id, status, adminId, reason = null) {
|
||||
try {
|
||||
const query = `
|
||||
UPDATE animals
|
||||
SET status = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [status, id]);
|
||||
|
||||
// 记录状态变更日志
|
||||
if (reason) {
|
||||
await query(
|
||||
`INSERT INTO animal_status_logs (animal_id, old_status, new_status, admin_id, reason, created_at)
|
||||
SELECT ?, status, ?, ?, ?, NOW() FROM animals WHERE id = ?`,
|
||||
[id, status, adminId, reason, id]
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('更新动物状态失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新动物状态
|
||||
* @param {Array} ids - 动物ID数组
|
||||
* @param {string} status - 新状态
|
||||
* @param {number} adminId - 管理员ID
|
||||
* @param {string} reason - 更新原因
|
||||
* @returns {Object} 更新结果
|
||||
*/
|
||||
static async batchUpdateAnimalStatus(ids, status, adminId, reason = null) {
|
||||
try {
|
||||
const placeholders = ids.map(() => '?').join(',');
|
||||
const query = `
|
||||
UPDATE animals
|
||||
SET status = ?, updated_at = NOW()
|
||||
WHERE id IN (${placeholders})
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [status, ...ids]);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('批量更新动物状态失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物总体统计
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
static async getAnimalTotalStats() {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
COUNT(*) as total_animals,
|
||||
COUNT(CASE WHEN status = 'available' THEN 1 END) as available_count,
|
||||
COUNT(CASE WHEN status = 'claimed' THEN 1 END) as claimed_count,
|
||||
COUNT(CASE WHEN status = 'unavailable' THEN 1 END) as unavailable_count,
|
||||
AVG(price) as avg_price,
|
||||
MIN(price) as min_price,
|
||||
MAX(price) as max_price
|
||||
FROM animals
|
||||
`;
|
||||
|
||||
const [rows] = await query(query);
|
||||
return rows[0];
|
||||
} catch (error) {
|
||||
console.error('获取动物总体统计失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物统计信息(按类型分组)
|
||||
* @returns {Array} 统计信息
|
||||
*/
|
||||
static async getAnimalStatsByType() {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
type,
|
||||
COUNT(*) as count,
|
||||
AVG(price) as avg_price
|
||||
FROM animals
|
||||
WHERE status = 'available'
|
||||
GROUP BY type
|
||||
ORDER BY count DESC
|
||||
`;
|
||||
|
||||
const [rows] = await query(query);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('获取动物统计信息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取按状态分类的统计
|
||||
* @returns {Array} 统计信息
|
||||
*/
|
||||
static async getAnimalStatsByStatus() {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
status,
|
||||
COUNT(*) as count,
|
||||
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM animals), 2) as percentage
|
||||
FROM animals
|
||||
GROUP BY status
|
||||
ORDER BY count DESC
|
||||
`;
|
||||
|
||||
const [rows] = await query(query);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('获取按状态分类的统计失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取按商家分类的统计
|
||||
* @returns {Array} 统计信息
|
||||
*/
|
||||
static async getAnimalStatsByMerchant() {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
m.name as merchant_name,
|
||||
COUNT(a.id) as animal_count,
|
||||
COUNT(CASE WHEN a.status = 'available' THEN 1 END) as available_count,
|
||||
COUNT(CASE WHEN a.status = 'claimed' THEN 1 END) as claimed_count,
|
||||
AVG(a.price) as avg_price
|
||||
FROM merchants m
|
||||
LEFT JOIN animals a ON m.id = a.merchant_id
|
||||
GROUP BY m.id, m.name
|
||||
HAVING animal_count > 0
|
||||
ORDER BY animal_count DESC
|
||||
`;
|
||||
|
||||
const [rows] = await query(query);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('获取按商家分类的统计失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取月度趋势数据
|
||||
* @returns {Array} 趋势数据
|
||||
*/
|
||||
static async getAnimalMonthlyTrend() {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
DATE_FORMAT(created_at, '%Y-%m') as month,
|
||||
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
|
||||
FROM animals
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
||||
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
|
||||
ORDER BY month ASC
|
||||
`;
|
||||
|
||||
const [rows] = await query(query);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('获取月度趋势数据失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导出数据
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Array} 导出数据
|
||||
*/
|
||||
static async getAnimalExportData(options = {}) {
|
||||
try {
|
||||
const { whereClause = '', params = [] } = options;
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
a.id,
|
||||
a.name,
|
||||
a.species,
|
||||
a.breed,
|
||||
a.age,
|
||||
a.gender,
|
||||
a.price,
|
||||
a.status,
|
||||
m.name as merchant_name,
|
||||
a.created_at
|
||||
FROM animals a
|
||||
LEFT JOIN merchants m ON a.merchant_id = m.id
|
||||
WHERE 1=1 ${whereClause}
|
||||
ORDER BY a.created_at DESC
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, params);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('获取导出数据失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新动物
|
||||
* @param {Object} animalData - 动物数据
|
||||
* @returns {Object} 创建结果
|
||||
*/
|
||||
static async create(animalData) {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
type,
|
||||
breed,
|
||||
age,
|
||||
gender,
|
||||
weight,
|
||||
price,
|
||||
description,
|
||||
health_status,
|
||||
vaccination_records,
|
||||
images,
|
||||
merchant_id,
|
||||
farm_location,
|
||||
contact_info,
|
||||
status = 'available'
|
||||
} = animalData;
|
||||
|
||||
const query = `
|
||||
INSERT INTO animals (
|
||||
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 query(query, [
|
||||
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 };
|
||||
} catch (error) {
|
||||
console.error('创建动物失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动物信息
|
||||
* @param {number} id - 动物ID
|
||||
* @param {Object} animalData - 更新数据
|
||||
* @returns {Object} 更新结果
|
||||
*/
|
||||
static async update(id, animalData) {
|
||||
try {
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
Object.keys(animalData).forEach(key => {
|
||||
if (animalData[key] !== undefined) {
|
||||
fields.push(`${key} = ?`);
|
||||
values.push(animalData[key]);
|
||||
}
|
||||
});
|
||||
|
||||
if (fields.length === 0) {
|
||||
throw new Error('没有要更新的字段');
|
||||
}
|
||||
|
||||
fields.push('updated_at = NOW()');
|
||||
values.push(id);
|
||||
|
||||
const query = `UPDATE animals SET ${fields.join(', ')} WHERE id = ?`;
|
||||
const [result] = await query(query, values);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('更新动物信息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除动物
|
||||
* @param {number} id - 动物ID
|
||||
* @returns {Object} 删除结果
|
||||
*/
|
||||
static async delete(id) {
|
||||
try {
|
||||
const [result] = await query('DELETE FROM animals WHERE id = ?', [id]);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('删除动物失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Animal;
|
||||
582
backend/src/models/AnimalClaim.js
Normal file
582
backend/src/models/AnimalClaim.js
Normal file
@@ -0,0 +1,582 @@
|
||||
const { query } = require('../config/database');
|
||||
|
||||
class AnimalClaim {
|
||||
/**
|
||||
* 创建认领申请
|
||||
* @param {Object} claimData - 认领申请数据
|
||||
* @returns {Object} 创建的认领申请
|
||||
*/
|
||||
static async create(claimData) {
|
||||
try {
|
||||
const {
|
||||
claim_no,
|
||||
animal_id,
|
||||
user_id,
|
||||
claim_reason,
|
||||
claim_duration,
|
||||
total_amount,
|
||||
contact_info,
|
||||
status = 'pending'
|
||||
} = claimData;
|
||||
|
||||
const query = `
|
||||
INSERT INTO animal_claims (
|
||||
claim_no, animal_id, user_id, claim_reason, claim_duration,
|
||||
total_amount, contact_info, status, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [
|
||||
claim_no,
|
||||
animal_id,
|
||||
user_id,
|
||||
claim_reason,
|
||||
claim_duration,
|
||||
total_amount,
|
||||
contact_info,
|
||||
status
|
||||
]);
|
||||
|
||||
return await this.findById(result.insertId);
|
||||
} catch (error) {
|
||||
console.error('创建认领申请数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找认领申请
|
||||
* @param {number} id - 认领申请ID
|
||||
* @returns {Object|null} 认领申请信息
|
||||
*/
|
||||
static async findById(id) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
ac.*,
|
||||
a.name as animal_name,
|
||||
a.type as animal_type,
|
||||
a.image as animal_image,
|
||||
a.price as animal_price,
|
||||
u.username,
|
||||
u.phone as user_phone,
|
||||
reviewer.username as reviewer_name
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN animals a ON ac.animal_id = a.id
|
||||
LEFT JOIN users u ON ac.user_id = u.id
|
||||
LEFT JOIN users reviewer ON ac.reviewed_by = reviewer.id
|
||||
WHERE ac.id = ? AND ac.deleted_at IS NULL
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [id]);
|
||||
return rows[0] || null;
|
||||
} catch (error) {
|
||||
console.error('查找认领申请数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据认领订单号查找
|
||||
* @param {string} claimNo - 认领订单号
|
||||
* @returns {Object|null} 认领申请信息
|
||||
*/
|
||||
static async findByClaimNo(claimNo) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
ac.*,
|
||||
a.name as animal_name,
|
||||
a.type as animal_type,
|
||||
a.image as animal_image,
|
||||
u.username,
|
||||
u.phone as user_phone
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN animals a ON ac.animal_id = a.id
|
||||
LEFT JOIN users u ON ac.user_id = u.id
|
||||
WHERE ac.claim_no = ? AND ac.deleted_at IS NULL
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [claimNo]);
|
||||
return rows[0] || null;
|
||||
} catch (error) {
|
||||
console.error('根据订单号查找认领申请数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找用户对特定动物的活跃认领申请
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {number} animalId - 动物ID
|
||||
* @returns {Object|null} 认领申请信息
|
||||
*/
|
||||
static async findActiveClaimByUserAndAnimal(userId, animalId) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT * FROM animal_claims
|
||||
WHERE user_id = ? AND animal_id = ?
|
||||
AND status IN ('pending', 'approved')
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [userId, animalId]);
|
||||
return rows[0] || null;
|
||||
} catch (error) {
|
||||
console.error('查找活跃认领申请数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新认领申请状态
|
||||
* @param {number} id - 认领申请ID
|
||||
* @param {string} status - 新状态
|
||||
* @param {Object} updateData - 更新数据
|
||||
* @returns {Object} 更新后的认领申请
|
||||
*/
|
||||
static async updateStatus(id, status, updateData = {}) {
|
||||
try {
|
||||
const fields = ['status = ?', 'updated_at = NOW()'];
|
||||
const values = [status];
|
||||
|
||||
// 动态添加更新字段
|
||||
Object.keys(updateData).forEach(key => {
|
||||
if (updateData[key] !== undefined) {
|
||||
fields.push(`${key} = ?`);
|
||||
values.push(updateData[key]);
|
||||
}
|
||||
});
|
||||
|
||||
values.push(id);
|
||||
|
||||
const query = `
|
||||
UPDATE animal_claims
|
||||
SET ${fields.join(', ')}
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
await query(query, values);
|
||||
return await this.findById(id);
|
||||
} catch (error) {
|
||||
console.error('更新认领申请状态数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的认领申请列表
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Object} 分页结果
|
||||
*/
|
||||
static async getUserClaims(userId, options = {}) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
status,
|
||||
animal_type,
|
||||
start_date,
|
||||
end_date
|
||||
} = options;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
let whereConditions = ['ac.user_id = ?', 'ac.deleted_at IS NULL'];
|
||||
let queryParams = [userId];
|
||||
|
||||
// 添加筛选条件
|
||||
if (status) {
|
||||
whereConditions.push('ac.status = ?');
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
if (animal_type) {
|
||||
whereConditions.push('a.type = ?');
|
||||
queryParams.push(animal_type);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereConditions.push('ac.created_at >= ?');
|
||||
queryParams.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereConditions.push('ac.created_at <= ?');
|
||||
queryParams.push(end_date);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 查询数据
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
ac.*,
|
||||
a.name as animal_name,
|
||||
a.type as animal_type,
|
||||
a.image as animal_image,
|
||||
a.price as animal_price
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN animals a ON ac.animal_id = a.id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ac.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const [dataRows] = await query(dataQuery, [...queryParams, limit, offset]);
|
||||
|
||||
// 查询总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN animals a ON ac.animal_id = a.id
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const [countRows] = await query(countQuery, queryParams);
|
||||
const total = countRows[0].total;
|
||||
|
||||
return {
|
||||
data: dataRows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取用户认领申请列表数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物的认领申请列表
|
||||
* @param {number} animalId - 动物ID
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Object} 分页结果
|
||||
*/
|
||||
static async getAnimalClaims(animalId, options = {}) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
status
|
||||
} = options;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
let whereConditions = ['ac.animal_id = ?', 'ac.deleted_at IS NULL'];
|
||||
let queryParams = [animalId];
|
||||
|
||||
if (status) {
|
||||
whereConditions.push('ac.status = ?');
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 查询数据
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
ac.*,
|
||||
u.username,
|
||||
u.phone as user_phone,
|
||||
u.email as user_email
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN users u ON ac.user_id = u.id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ac.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const [dataRows] = await query(dataQuery, [...queryParams, limit, offset]);
|
||||
|
||||
// 查询总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM animal_claims ac
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const [countRows] = await query(countQuery, queryParams);
|
||||
const total = countRows[0].total;
|
||||
|
||||
return {
|
||||
data: dataRows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取动物认领申请列表数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有认领申请列表(管理员)
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Object} 分页结果
|
||||
*/
|
||||
static async getAllClaims(options = {}) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
status,
|
||||
animal_type,
|
||||
user_id,
|
||||
start_date,
|
||||
end_date,
|
||||
keyword
|
||||
} = options;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
let whereConditions = ['ac.deleted_at IS NULL'];
|
||||
let queryParams = [];
|
||||
|
||||
// 添加筛选条件
|
||||
if (status) {
|
||||
whereConditions.push('ac.status = ?');
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
if (animal_type) {
|
||||
whereConditions.push('a.type = ?');
|
||||
queryParams.push(animal_type);
|
||||
}
|
||||
|
||||
if (user_id) {
|
||||
whereConditions.push('ac.user_id = ?');
|
||||
queryParams.push(user_id);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereConditions.push('ac.created_at >= ?');
|
||||
queryParams.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereConditions.push('ac.created_at <= ?');
|
||||
queryParams.push(end_date);
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
whereConditions.push('(ac.claim_no LIKE ? OR a.name LIKE ? OR u.username LIKE ?)');
|
||||
const keywordPattern = `%${keyword}%`;
|
||||
queryParams.push(keywordPattern, keywordPattern, keywordPattern);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 查询数据
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
ac.*,
|
||||
a.name as animal_name,
|
||||
a.type as animal_type,
|
||||
a.image as animal_image,
|
||||
a.price as animal_price,
|
||||
u.username,
|
||||
u.phone as user_phone,
|
||||
u.email as user_email,
|
||||
reviewer.username as reviewer_name
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN animals a ON ac.animal_id = a.id
|
||||
LEFT JOIN users u ON ac.user_id = u.id
|
||||
LEFT JOIN users reviewer ON ac.reviewed_by = reviewer.id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ac.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const [dataRows] = await query(dataQuery, [...queryParams, limit, offset]);
|
||||
|
||||
// 查询总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN animals a ON ac.animal_id = a.id
|
||||
LEFT JOIN users u ON ac.user_id = u.id
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const [countRows] = await query(countQuery, queryParams);
|
||||
const total = countRows[0].total;
|
||||
|
||||
return {
|
||||
data: dataRows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取所有认领申请列表数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建续期记录
|
||||
* @param {Object} renewalData - 续期数据
|
||||
* @returns {Object} 续期记录
|
||||
*/
|
||||
static async createRenewal(renewalData) {
|
||||
try {
|
||||
const {
|
||||
claim_id,
|
||||
duration,
|
||||
amount,
|
||||
payment_method,
|
||||
status = 'pending'
|
||||
} = renewalData;
|
||||
|
||||
const query = `
|
||||
INSERT INTO animal_claim_renewals (
|
||||
claim_id, duration, amount, payment_method, status, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [
|
||||
claim_id,
|
||||
duration,
|
||||
amount,
|
||||
payment_method,
|
||||
status
|
||||
]);
|
||||
|
||||
return {
|
||||
id: result.insertId,
|
||||
claim_id,
|
||||
duration,
|
||||
amount,
|
||||
payment_method,
|
||||
status
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('创建续期记录数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认领统计信息
|
||||
* @param {Object} filters - 筛选条件
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
static async getClaimStatistics(filters = {}) {
|
||||
try {
|
||||
const { start_date, end_date, animal_type } = filters;
|
||||
let whereConditions = ['ac.deleted_at IS NULL'];
|
||||
let queryParams = [];
|
||||
|
||||
if (start_date) {
|
||||
whereConditions.push('ac.created_at >= ?');
|
||||
queryParams.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereConditions.push('ac.created_at <= ?');
|
||||
queryParams.push(end_date);
|
||||
}
|
||||
|
||||
if (animal_type) {
|
||||
whereConditions.push('a.type = ?');
|
||||
queryParams.push(animal_type);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 基础统计
|
||||
const basicStatsQuery = `
|
||||
SELECT
|
||||
COUNT(*) as total_claims,
|
||||
COUNT(CASE WHEN ac.status = 'pending' THEN 1 END) as pending_claims,
|
||||
COUNT(CASE WHEN ac.status = 'approved' THEN 1 END) as approved_claims,
|
||||
COUNT(CASE WHEN ac.status = 'rejected' THEN 1 END) as rejected_claims,
|
||||
COUNT(CASE WHEN ac.status = 'cancelled' THEN 1 END) as cancelled_claims,
|
||||
SUM(CASE WHEN ac.status = 'approved' THEN ac.total_amount ELSE 0 END) as total_amount,
|
||||
AVG(CASE WHEN ac.status = 'approved' THEN ac.claim_duration ELSE NULL END) as avg_duration
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN animals a ON ac.animal_id = a.id
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const [basicStats] = await query(basicStatsQuery, queryParams);
|
||||
|
||||
// 按动物类型统计
|
||||
const typeStatsQuery = `
|
||||
SELECT
|
||||
a.type,
|
||||
COUNT(*) as claim_count,
|
||||
COUNT(CASE WHEN ac.status = 'approved' THEN 1 END) as approved_count,
|
||||
SUM(CASE WHEN ac.status = 'approved' THEN ac.total_amount ELSE 0 END) as total_amount
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN animals a ON ac.animal_id = a.id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY a.type
|
||||
ORDER BY claim_count DESC
|
||||
`;
|
||||
|
||||
const [typeStats] = await query(typeStatsQuery, queryParams);
|
||||
|
||||
// 按月份统计
|
||||
const monthlyStatsQuery = `
|
||||
SELECT
|
||||
DATE_FORMAT(ac.created_at, '%Y-%m') as month,
|
||||
COUNT(*) as claim_count,
|
||||
COUNT(CASE WHEN ac.status = 'approved' THEN 1 END) as approved_count,
|
||||
SUM(CASE WHEN ac.status = 'approved' THEN ac.total_amount ELSE 0 END) as total_amount
|
||||
FROM animal_claims ac
|
||||
LEFT JOIN animals a ON ac.animal_id = a.id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY DATE_FORMAT(ac.created_at, '%Y-%m')
|
||||
ORDER BY month DESC
|
||||
LIMIT 12
|
||||
`;
|
||||
|
||||
const [monthlyStats] = await query(monthlyStatsQuery, queryParams);
|
||||
|
||||
return {
|
||||
basic: basicStats[0],
|
||||
by_type: typeStats,
|
||||
by_month: monthlyStats
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取认领统计信息数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除认领申请
|
||||
* @param {number} id - 认领申请ID
|
||||
* @returns {boolean} 删除结果
|
||||
*/
|
||||
static async softDelete(id) {
|
||||
try {
|
||||
const query = `
|
||||
UPDATE animal_claims
|
||||
SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [id]);
|
||||
return result.affectedRows > 0;
|
||||
} catch (error) {
|
||||
console.error('软删除认领申请数据库错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AnimalClaim;
|
||||
212
backend/src/models/Merchant.js
Normal file
212
backend/src/models/Merchant.js
Normal file
@@ -0,0 +1,212 @@
|
||||
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 && countResult.length > 0 ? countResult[0].total : 0;
|
||||
|
||||
// 查询数据
|
||||
const dataSql = `SELECT * FROM merchants ${whereClause} ORDER BY created_at DESC LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}`;
|
||||
const rows = await query(dataSql, params);
|
||||
|
||||
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 && animalResult.length > 0 ? animalResult[0].count : 0,
|
||||
order_count: orderResult && orderResult.length > 0 ? orderResult[0].count : 0
|
||||
};
|
||||
} 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;
|
||||
499
backend/src/models/Payment.js
Normal file
499
backend/src/models/Payment.js
Normal file
@@ -0,0 +1,499 @@
|
||||
const { query } = require('../config/database');
|
||||
|
||||
class Payment {
|
||||
/**
|
||||
* 创建支付订单
|
||||
* @param {Object} paymentData - 支付订单数据
|
||||
* @returns {Object} 创建的支付订单
|
||||
*/
|
||||
static async create(paymentData) {
|
||||
const {
|
||||
payment_no,
|
||||
order_id,
|
||||
user_id,
|
||||
amount,
|
||||
payment_method,
|
||||
return_url,
|
||||
notify_url
|
||||
} = paymentData;
|
||||
|
||||
const query = `
|
||||
INSERT INTO payments (
|
||||
payment_no, order_id, user_id, amount, payment_method,
|
||||
return_url, notify_url, status, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', NOW(), NOW())
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [
|
||||
payment_no, order_id, user_id, amount, payment_method,
|
||||
return_url, notify_url
|
||||
]);
|
||||
|
||||
return this.findById(result.insertId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找支付订单
|
||||
* @param {number} id - 支付订单ID
|
||||
* @returns {Object|null} 支付订单信息
|
||||
*/
|
||||
static async findById(id) {
|
||||
const query = `
|
||||
SELECT p.*, o.order_no, u.username, u.phone
|
||||
FROM payments p
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE p.id = ? AND p.deleted_at IS NULL
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据支付订单号查找支付订单
|
||||
* @param {string} paymentNo - 支付订单号
|
||||
* @returns {Object|null} 支付订单信息
|
||||
*/
|
||||
static async findByPaymentNo(paymentNo) {
|
||||
const query = `
|
||||
SELECT p.*, o.order_no, u.username, u.phone
|
||||
FROM payments p
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE p.payment_no = ? AND p.deleted_at IS NULL
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [paymentNo]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据订单ID查找支付订单
|
||||
* @param {number} orderId - 订单ID
|
||||
* @returns {Array} 支付订单列表
|
||||
*/
|
||||
static async findByOrderId(orderId) {
|
||||
const query = `
|
||||
SELECT * FROM payments
|
||||
WHERE order_id = ? AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [orderId]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付状态
|
||||
* @param {number} id - 支付订单ID
|
||||
* @param {Object} updateData - 更新数据
|
||||
* @returns {Object} 更新后的支付订单
|
||||
*/
|
||||
static async updateStatus(id, updateData) {
|
||||
const {
|
||||
status,
|
||||
transaction_id,
|
||||
paid_amount,
|
||||
paid_at,
|
||||
failure_reason
|
||||
} = updateData;
|
||||
|
||||
const query = `
|
||||
UPDATE payments
|
||||
SET status = ?, transaction_id = ?, paid_amount = ?,
|
||||
paid_at = ?, failure_reason = ?, updated_at = NOW()
|
||||
WHERE id = ? AND deleted_at IS NULL
|
||||
`;
|
||||
|
||||
await query(query, [
|
||||
status, transaction_id, paid_amount,
|
||||
paid_at, failure_reason, id
|
||||
]);
|
||||
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户支付订单列表
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Object} 分页结果
|
||||
*/
|
||||
static async getUserPayments(userId, options = {}) {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
status,
|
||||
payment_method,
|
||||
start_date,
|
||||
end_date
|
||||
} = options;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
let whereConditions = ['p.user_id = ?', 'p.deleted_at IS NULL'];
|
||||
let params = [userId];
|
||||
|
||||
// 添加筛选条件
|
||||
if (status) {
|
||||
whereConditions.push('p.status = ?');
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (payment_method) {
|
||||
whereConditions.push('p.payment_method = ?');
|
||||
params.push(payment_method);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereConditions.push('DATE(p.created_at) >= ?');
|
||||
params.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereConditions.push('DATE(p.created_at) <= ?');
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 查询总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM payments p
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
const [countResult] = await query(countQuery, params);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 查询数据
|
||||
const dataQuery = `
|
||||
SELECT p.*, o.order_no, o.title as order_title
|
||||
FROM payments p
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
params.push(limit, offset);
|
||||
const [rows] = await query(dataQuery, params);
|
||||
|
||||
return {
|
||||
data: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支付订单列表(管理员)
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Object} 分页结果
|
||||
*/
|
||||
static async getAllPayments(options = {}) {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
status,
|
||||
payment_method,
|
||||
user_id,
|
||||
start_date,
|
||||
end_date,
|
||||
keyword
|
||||
} = options;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
let whereConditions = ['p.deleted_at IS NULL'];
|
||||
let params = [];
|
||||
|
||||
// 添加筛选条件
|
||||
if (status) {
|
||||
whereConditions.push('p.status = ?');
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (payment_method) {
|
||||
whereConditions.push('p.payment_method = ?');
|
||||
params.push(payment_method);
|
||||
}
|
||||
|
||||
if (user_id) {
|
||||
whereConditions.push('p.user_id = ?');
|
||||
params.push(user_id);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereConditions.push('DATE(p.created_at) >= ?');
|
||||
params.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereConditions.push('DATE(p.created_at) <= ?');
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
whereConditions.push('(p.payment_no LIKE ? OR o.order_no LIKE ? OR u.username LIKE ?)');
|
||||
const keywordPattern = `%${keyword}%`;
|
||||
params.push(keywordPattern, keywordPattern, keywordPattern);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 查询总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM payments p
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
const [countResult] = await query(countQuery, params);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 查询数据
|
||||
const dataQuery = `
|
||||
SELECT p.*, o.order_no, o.title as order_title,
|
||||
u.username, u.phone
|
||||
FROM payments p
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
params.push(limit, offset);
|
||||
const [rows] = await query(dataQuery, params);
|
||||
|
||||
return {
|
||||
data: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建退款记录
|
||||
* @param {Object} refundData - 退款数据
|
||||
* @returns {Object} 创建的退款记录
|
||||
*/
|
||||
static async createRefund(refundData) {
|
||||
const {
|
||||
refund_no,
|
||||
payment_id,
|
||||
user_id,
|
||||
refund_amount,
|
||||
refund_reason
|
||||
} = refundData;
|
||||
|
||||
const query = `
|
||||
INSERT INTO refunds (
|
||||
refund_no, payment_id, user_id, refund_amount,
|
||||
refund_reason, status, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, 'pending', NOW(), NOW())
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [
|
||||
refund_no, payment_id, user_id, refund_amount, refund_reason
|
||||
]);
|
||||
|
||||
return this.findRefundById(result.insertId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找退款记录
|
||||
* @param {number} id - 退款ID
|
||||
* @returns {Object|null} 退款记录
|
||||
*/
|
||||
static async findRefundById(id) {
|
||||
const query = `
|
||||
SELECT r.*, p.payment_no, p.amount as payment_amount,
|
||||
u.username, u.phone,
|
||||
admin.username as processed_by_name
|
||||
FROM refunds r
|
||||
LEFT JOIN payments p ON r.payment_id = p.id
|
||||
LEFT JOIN users u ON r.user_id = u.id
|
||||
LEFT JOIN users admin ON r.processed_by = admin.id
|
||||
WHERE r.id = ? AND r.deleted_at IS NULL
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新退款状态
|
||||
* @param {number} id - 退款ID
|
||||
* @param {Object} updateData - 更新数据
|
||||
* @returns {Object} 更新后的退款记录
|
||||
*/
|
||||
static async updateRefundStatus(id, updateData) {
|
||||
const {
|
||||
status,
|
||||
processed_by,
|
||||
process_remark,
|
||||
refund_transaction_id,
|
||||
refunded_at
|
||||
} = updateData;
|
||||
|
||||
const query = `
|
||||
UPDATE refunds
|
||||
SET status = ?, processed_by = ?, process_remark = ?,
|
||||
refund_transaction_id = ?, refunded_at = ?,
|
||||
processed_at = NOW(), updated_at = NOW()
|
||||
WHERE id = ? AND deleted_at IS NULL
|
||||
`;
|
||||
|
||||
await query(query, [
|
||||
status, processed_by, process_remark,
|
||||
refund_transaction_id, refunded_at, id
|
||||
]);
|
||||
|
||||
return this.findRefundById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付统计信息
|
||||
* @param {Object} filters - 筛选条件
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
static async getPaymentStatistics(filters = {}) {
|
||||
const {
|
||||
start_date,
|
||||
end_date,
|
||||
payment_method
|
||||
} = filters;
|
||||
|
||||
let whereConditions = ['deleted_at IS NULL'];
|
||||
let params = [];
|
||||
|
||||
if (start_date) {
|
||||
whereConditions.push('DATE(created_at) >= ?');
|
||||
params.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereConditions.push('DATE(created_at) <= ?');
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
if (payment_method) {
|
||||
whereConditions.push('payment_method = ?');
|
||||
params.push(payment_method);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 总体统计
|
||||
const totalQuery = `
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
COALESCE(SUM(amount), 0) as total_amount,
|
||||
COUNT(CASE WHEN status = 'paid' THEN 1 END) as success_count,
|
||||
COALESCE(SUM(CASE WHEN status = 'paid' THEN paid_amount END), 0) as success_amount
|
||||
FROM payments
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
const [totalResult] = await query(totalQuery, params);
|
||||
|
||||
// 退款统计
|
||||
const refundQuery = `
|
||||
SELECT
|
||||
COUNT(*) as refund_count,
|
||||
COALESCE(SUM(refund_amount), 0) as refund_amount
|
||||
FROM refunds r
|
||||
JOIN payments p ON r.payment_id = p.id
|
||||
WHERE r.status = 'completed' AND r.deleted_at IS NULL
|
||||
${start_date ? 'AND DATE(r.created_at) >= ?' : ''}
|
||||
${end_date ? 'AND DATE(r.created_at) <= ?' : ''}
|
||||
${payment_method ? 'AND p.payment_method = ?' : ''}
|
||||
`;
|
||||
let refundParams = [];
|
||||
if (start_date) refundParams.push(start_date);
|
||||
if (end_date) refundParams.push(end_date);
|
||||
if (payment_method) refundParams.push(payment_method);
|
||||
|
||||
const [refundResult] = await query(refundQuery, refundParams);
|
||||
|
||||
// 按支付方式统计
|
||||
const methodQuery = `
|
||||
SELECT
|
||||
payment_method,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(CASE WHEN status = 'paid' THEN paid_amount END), 0) as amount
|
||||
FROM payments
|
||||
WHERE ${whereClause}
|
||||
GROUP BY payment_method
|
||||
`;
|
||||
const [methodResult] = await query(methodQuery, params);
|
||||
|
||||
return {
|
||||
total_count: totalResult[0].total_count,
|
||||
total_amount: parseFloat(totalResult[0].total_amount),
|
||||
success_count: totalResult[0].success_count,
|
||||
success_amount: parseFloat(totalResult[0].success_amount),
|
||||
refund_count: refundResult[0].refund_count,
|
||||
refund_amount: parseFloat(refundResult[0].refund_amount),
|
||||
method_stats: methodResult.map(row => ({
|
||||
payment_method: row.payment_method,
|
||||
count: row.count,
|
||||
amount: parseFloat(row.amount)
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查支付订单是否存在
|
||||
* @param {number} id - 支付订单ID
|
||||
* @returns {boolean} 是否存在
|
||||
*/
|
||||
static async exists(id) {
|
||||
const query = 'SELECT 1 FROM payments WHERE id = ? AND deleted_at IS NULL';
|
||||
const [rows] = await query(query, [id]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除支付订单
|
||||
* @param {number} id - 支付订单ID
|
||||
* @returns {boolean} 删除结果
|
||||
*/
|
||||
static async softDelete(id) {
|
||||
const query = `
|
||||
UPDATE payments
|
||||
SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE id = ? AND deleted_at IS NULL
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据清理 - 删除过期的待支付订单
|
||||
* @param {number} hours - 过期小时数,默认24小时
|
||||
* @returns {number} 清理的记录数
|
||||
*/
|
||||
static async cleanExpiredPayments(hours = 24) {
|
||||
const query = `
|
||||
UPDATE payments
|
||||
SET status = 'cancelled', updated_at = NOW()
|
||||
WHERE status = 'pending'
|
||||
AND created_at < DATE_SUB(NOW(), INTERVAL ? HOUR)
|
||||
AND deleted_at IS NULL
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [hours]);
|
||||
return result.affectedRows;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Payment;
|
||||
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(query, [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(query, [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(query, [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;
|
||||
319
backend/src/models/TravelRegistration.js
Normal file
319
backend/src/models/TravelRegistration.js
Normal file
@@ -0,0 +1,319 @@
|
||||
const { query } = require('../config/database');
|
||||
|
||||
/**
|
||||
* 旅行报名数据模型
|
||||
* 处理旅行活动报名相关的数据库操作
|
||||
*/
|
||||
class TravelRegistration {
|
||||
/**
|
||||
* 创建报名记录
|
||||
* @param {Object} registrationData - 报名数据
|
||||
* @returns {Promise<Object>} 创建的报名记录
|
||||
*/
|
||||
static async create(registrationData) {
|
||||
const {
|
||||
travel_plan_id,
|
||||
user_id,
|
||||
message,
|
||||
emergency_contact,
|
||||
emergency_phone
|
||||
} = registrationData;
|
||||
|
||||
const query = `
|
||||
INSERT INTO travel_registrations
|
||||
(travel_plan_id, user_id, message, emergency_contact, emergency_phone, status, applied_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'pending', NOW())
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [
|
||||
travel_plan_id,
|
||||
user_id,
|
||||
message || null,
|
||||
emergency_contact || null,
|
||||
emergency_phone || null
|
||||
]);
|
||||
|
||||
return this.findById(result.insertId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找报名记录
|
||||
* @param {number} id - 报名记录ID
|
||||
* @returns {Promise<Object|null>} 报名记录
|
||||
*/
|
||||
static async findById(id) {
|
||||
const query = `
|
||||
SELECT
|
||||
tr.*,
|
||||
u.username,
|
||||
u.real_name,
|
||||
u.avatar_url,
|
||||
tp.title as travel_title,
|
||||
tp.destination,
|
||||
tp.start_date,
|
||||
tp.end_date
|
||||
FROM travel_registrations tr
|
||||
LEFT JOIN users u ON tr.user_id = u.id
|
||||
LEFT JOIN travel_plans tp ON tr.travel_plan_id = tp.id
|
||||
WHERE tr.id = ?
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已报名某个旅行活动
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {number} travelPlanId - 旅行活动ID
|
||||
* @returns {Promise<Object|null>} 报名记录
|
||||
*/
|
||||
static async findByUserAndTravel(userId, travelPlanId) {
|
||||
const query = `
|
||||
SELECT * FROM travel_registrations
|
||||
WHERE user_id = ? AND travel_plan_id = ? AND status != 'cancelled'
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [userId, travelPlanId]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的报名记录列表
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Promise<Object>} 报名记录列表和分页信息
|
||||
*/
|
||||
static async findByUser(userId, options = {}) {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
status
|
||||
} = options;
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
let whereClause = 'WHERE tr.user_id = ?';
|
||||
const params = [userId];
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND tr.status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM travel_registrations tr
|
||||
${whereClause}
|
||||
`;
|
||||
const [countResult] = await query(countQuery, params);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 获取数据
|
||||
const query = `
|
||||
SELECT
|
||||
tr.*,
|
||||
tp.title as travel_title,
|
||||
tp.destination,
|
||||
tp.start_date,
|
||||
tp.end_date,
|
||||
tp.max_participants,
|
||||
tp.current_participants
|
||||
FROM travel_registrations tr
|
||||
LEFT JOIN travel_plans tp ON tr.travel_plan_id = tp.id
|
||||
${whereClause}
|
||||
ORDER BY tr.applied_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(pageSize, offset);
|
||||
const [rows] = await query(query, params);
|
||||
|
||||
return {
|
||||
registrations: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取旅行活动的报名记录列表
|
||||
* @param {number} travelPlanId - 旅行活动ID
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Promise<Object>} 报名记录列表和分页信息
|
||||
*/
|
||||
static async findByTravelPlan(travelPlanId, options = {}) {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
status
|
||||
} = options;
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
let whereClause = 'WHERE tr.travel_plan_id = ?';
|
||||
const params = [travelPlanId];
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND tr.status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM travel_registrations tr
|
||||
${whereClause}
|
||||
`;
|
||||
const [countResult] = await query(countQuery, params);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 获取数据
|
||||
const query = `
|
||||
SELECT
|
||||
tr.*,
|
||||
u.username,
|
||||
u.real_name,
|
||||
u.avatar_url,
|
||||
u.phone,
|
||||
u.email
|
||||
FROM travel_registrations tr
|
||||
LEFT JOIN users u ON tr.user_id = u.id
|
||||
${whereClause}
|
||||
ORDER BY tr.applied_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(pageSize, offset);
|
||||
const [rows] = await query(query, params);
|
||||
|
||||
return {
|
||||
registrations: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新报名状态
|
||||
* @param {number} id - 报名记录ID
|
||||
* @param {string} status - 新状态
|
||||
* @param {string} rejectReason - 拒绝原因(可选)
|
||||
* @returns {Promise<Object>} 更新后的报名记录
|
||||
*/
|
||||
static async updateStatus(id, status, rejectReason = null) {
|
||||
const query = `
|
||||
UPDATE travel_registrations
|
||||
SET status = ?, reject_reason = ?, responded_at = NOW()
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
await query(query, [status, rejectReason, id]);
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消报名
|
||||
* @param {number} id - 报名记录ID
|
||||
* @returns {Promise<Object>} 更新后的报名记录
|
||||
*/
|
||||
static async cancel(id) {
|
||||
return this.updateStatus(id, 'cancelled');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取报名统计信息
|
||||
* @param {number} travelPlanId - 旅行活动ID
|
||||
* @returns {Promise<Object>} 统计信息
|
||||
*/
|
||||
static async getStats(travelPlanId) {
|
||||
const query = `
|
||||
SELECT
|
||||
COUNT(*) as total_applications,
|
||||
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count,
|
||||
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved_count,
|
||||
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count,
|
||||
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count
|
||||
FROM travel_registrations
|
||||
WHERE travel_plan_id = ?
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [travelPlanId]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限查看旅行活动的报名列表
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {number} travelPlanId - 旅行活动ID
|
||||
* @returns {Promise<boolean>} 是否有权限
|
||||
*/
|
||||
static async canViewRegistrations(userId, travelPlanId) {
|
||||
const query = `
|
||||
SELECT id FROM travel_plans
|
||||
WHERE id = ? AND created_by = ?
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [travelPlanId, userId]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限审核报名
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {number} registrationId - 报名记录ID
|
||||
* @returns {Promise<boolean>} 是否有权限
|
||||
*/
|
||||
static async canReviewRegistration(userId, registrationId) {
|
||||
const query = `
|
||||
SELECT tr.id
|
||||
FROM travel_registrations tr
|
||||
JOIN travel_plans tp ON tr.travel_plan_id = tp.id
|
||||
WHERE tr.id = ? AND tp.created_by = ?
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [registrationId, userId]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取旅行活动的已通过报名数量
|
||||
* @param {number} travelPlanId - 旅行活动ID
|
||||
* @returns {Promise<number>} 已通过报名数量
|
||||
*/
|
||||
static async getApprovedCount(travelPlanId) {
|
||||
const query = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM travel_registrations
|
||||
WHERE travel_plan_id = ? AND status = 'approved'
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [travelPlanId]);
|
||||
return rows[0].count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据清理方法 - 移除敏感信息
|
||||
* @param {Object} registration - 报名记录
|
||||
* @returns {Object} 清理后的报名记录
|
||||
*/
|
||||
static sanitize(registration) {
|
||||
if (!registration) return null;
|
||||
|
||||
const sanitized = { ...registration };
|
||||
|
||||
// 移除敏感信息
|
||||
delete sanitized.emergency_phone;
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TravelRegistration;
|
||||
@@ -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;
|
||||
@@ -64,7 +67,7 @@ class UserMySQL {
|
||||
|
||||
// 更新用户信息
|
||||
static async update(id, updates) {
|
||||
const allowedFields = ['nickname', 'avatar', 'gender', 'birthday', 'phone', 'email'];
|
||||
const allowedFields = ['real_name', 'avatar_url', 'email', 'phone', 'user_type'];
|
||||
const setClauses = [];
|
||||
const params = [];
|
||||
|
||||
@@ -79,10 +82,9 @@ class UserMySQL {
|
||||
return false;
|
||||
}
|
||||
|
||||
setClauses.push('updated_at = NOW()');
|
||||
params.push(id);
|
||||
|
||||
const sql = `UPDATE users SET ${setClauses.join(', ')} WHERE id = ?`;
|
||||
const sql = `UPDATE users SET ${setClauses.join(', ')}, updated_at = NOW() WHERE id = ?`;
|
||||
|
||||
const result = await query(sql, params);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
@@ -96,70 +98,163 @@ class UserMySQL {
|
||||
|
||||
// 更新最后登录时间
|
||||
static async updateLastLogin(id) {
|
||||
const sql = 'UPDATE users SET updated_at = NOW() WHERE id = ?';
|
||||
const result = await query(sql, [id]);
|
||||
return result.affectedRows > 0;
|
||||
const sql = 'UPDATE users SET last_login_at = NOW() WHERE id = ?';
|
||||
await query(sql, [id]);
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
// 检查用户名是否存在
|
||||
static async isUsernameExists(username, excludeId = null) {
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE username = ?';
|
||||
const params = [username];
|
||||
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查用户状态是否活跃
|
||||
// 检查用户是否激活
|
||||
static isActive(user) {
|
||||
return user.status === 'active';
|
||||
return user && user.status === 'active';
|
||||
}
|
||||
|
||||
// 执行原始查询(用于复杂查询)
|
||||
// 通用查询方法
|
||||
static async query(sql, params = []) {
|
||||
const { query } = require('../config/database');
|
||||
return await query(sql, params);
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
// 检查邮箱是否存在
|
||||
static async isEmailExists(email, excludeId = null) {
|
||||
if (!email) return false;
|
||||
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE email = ?';
|
||||
const params = [email];
|
||||
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
// 检查手机号是否存在
|
||||
static async isPhoneExists(phone, excludeId = null) {
|
||||
if (!phone) return false;
|
||||
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE phone = ?';
|
||||
const params = [phone];
|
||||
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 安全返回用户信息(去除敏感信息)
|
||||
// 清理用户数据(移除敏感信息)
|
||||
static sanitize(user) {
|
||||
if (!user) return null;
|
||||
|
||||
const { password_hash, ...safeUser } = user;
|
||||
return safeUser;
|
||||
const { password_hash, ...sanitizedUser } = user;
|
||||
return sanitizedUser;
|
||||
}
|
||||
|
||||
// 保存邮箱验证码
|
||||
static async saveVerificationCode(email, code, expiresAt) {
|
||||
const sql = `
|
||||
INSERT INTO email_verifications (email, code, expires_at, created_at)
|
||||
VALUES (?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
code = VALUES(code),
|
||||
expires_at = VALUES(expires_at),
|
||||
created_at = NOW()
|
||||
`;
|
||||
return await query(sql, [email, code, expiresAt]);
|
||||
}
|
||||
|
||||
// 验证邮箱验证码
|
||||
static async verifyEmailCode(email, code) {
|
||||
const sql = `
|
||||
SELECT * FROM email_verifications
|
||||
WHERE email = ? AND code = ? AND expires_at > NOW()
|
||||
`;
|
||||
const rows = await query(sql, [email, code]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 删除邮箱验证码
|
||||
static async deleteVerificationCode(email, code) {
|
||||
const sql = 'DELETE FROM email_verifications WHERE email = ? AND code = ?';
|
||||
return await query(sql, [email, code]);
|
||||
}
|
||||
|
||||
// 保存密码重置token
|
||||
static async savePasswordResetToken(userId, token, expiresAt) {
|
||||
const sql = `
|
||||
INSERT INTO password_resets (user_id, token, expires_at, created_at)
|
||||
VALUES (?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
token = VALUES(token),
|
||||
expires_at = VALUES(expires_at),
|
||||
created_at = NOW()
|
||||
`;
|
||||
return await query(sql, [userId, token, expiresAt]);
|
||||
}
|
||||
|
||||
// 查找密码重置token
|
||||
static async findPasswordResetToken(token) {
|
||||
const sql = `
|
||||
SELECT * FROM password_resets
|
||||
WHERE token = ? AND expires_at > NOW()
|
||||
`;
|
||||
const rows = await query(sql, [token]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 删除密码重置token
|
||||
static async deletePasswordResetToken(token) {
|
||||
const sql = 'DELETE FROM password_resets WHERE token = ?';
|
||||
return await query(sql, [token]);
|
||||
}
|
||||
|
||||
// 记录登录失败次数
|
||||
static async recordLoginFailure(identifier) {
|
||||
const sql = `
|
||||
INSERT INTO login_attempts (identifier, attempts, last_attempt, created_at)
|
||||
VALUES (?, 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
attempts = attempts + 1,
|
||||
last_attempt = NOW()
|
||||
`;
|
||||
return await query(sql, [identifier]);
|
||||
}
|
||||
|
||||
// 获取登录失败次数
|
||||
static async getLoginAttempts(identifier) {
|
||||
const sql = `
|
||||
SELECT attempts, last_attempt FROM login_attempts
|
||||
WHERE identifier = ? AND last_attempt > DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||
`;
|
||||
const rows = await query(sql, [identifier]);
|
||||
return rows[0] || { attempts: 0 };
|
||||
}
|
||||
|
||||
// 清除登录失败记录
|
||||
static async clearLoginAttempts(identifier) {
|
||||
const sql = 'DELETE FROM login_attempts WHERE identifier = ?';
|
||||
return await query(sql, [identifier]);
|
||||
}
|
||||
|
||||
// 检查账户是否被锁定
|
||||
static async isAccountLocked(identifier) {
|
||||
const attempts = await this.getLoginAttempts(identifier);
|
||||
return attempts.attempts >= 5; // 5次失败后锁定
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,12 @@ const adminController = require('../controllers/admin');
|
||||
const systemStatsController = require('../controllers/admin/systemStats');
|
||||
const { authenticateAdmin } = require('../middleware/auth');
|
||||
|
||||
// 引入子路由
|
||||
const userManagementRoutes = require('./admin/userManagement');
|
||||
const dataStatisticsRoutes = require('./admin/dataStatistics');
|
||||
const animalManagementRoutes = require('./admin/animalManagement');
|
||||
const fileManagementRoutes = require('./admin/fileManagement');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
@@ -202,10 +208,10 @@ router.post('/login', adminController.login);
|
||||
* @swagger
|
||||
* /admin/profile:
|
||||
* get:
|
||||
* summary: 获取管理员个人信息
|
||||
* summary: 获取当前管理员信息
|
||||
* tags: [Admin]
|
||||
* security:
|
||||
* - BearerAuth: []
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
@@ -234,6 +240,224 @@ router.post('/login', adminController.login);
|
||||
*/
|
||||
router.get('/profile', authenticateAdmin, adminController.getProfile);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/dashboard:
|
||||
* get:
|
||||
* summary: 获取管理后台仪表板数据
|
||||
* tags: [Admin]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* code:
|
||||
* type: integer
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* statistics:
|
||||
* type: object
|
||||
* properties:
|
||||
* totalUsers:
|
||||
* type: integer
|
||||
* description: 总用户数
|
||||
* totalAnimals:
|
||||
* type: integer
|
||||
* description: 总动物数
|
||||
* totalTravels:
|
||||
* type: integer
|
||||
* description: 总旅行数
|
||||
* totalClaims:
|
||||
* type: integer
|
||||
* description: 总认领数
|
||||
* todayNewUsers:
|
||||
* type: integer
|
||||
* description: 今日新增用户
|
||||
* todayNewAnimals:
|
||||
* type: integer
|
||||
* description: 今日新增动物
|
||||
* todayNewTravels:
|
||||
* type: integer
|
||||
* description: 今日新增旅行
|
||||
* todayNewClaims:
|
||||
* type: integer
|
||||
* description: 今日新增认领
|
||||
* recentActivities:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: [user_register, animal_add, travel_add, claim_add]
|
||||
* description:
|
||||
* type: string
|
||||
* timestamp:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* user:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* nickname:
|
||||
* type: string
|
||||
* systemInfo:
|
||||
* type: object
|
||||
* properties:
|
||||
* serverTime:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* uptime:
|
||||
* type: string
|
||||
* version:
|
||||
* type: string
|
||||
* environment:
|
||||
* type: string
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
router.get('/dashboard', authenticateAdmin, adminController.getDashboard);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/dashboard/user-growth:
|
||||
* get:
|
||||
* summary: 获取用户增长数据
|
||||
* tags: [Admin]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: days
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 365
|
||||
* default: 7
|
||||
* description: 查询天数
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* code:
|
||||
* type: integer
|
||||
* example: 200
|
||||
* message:
|
||||
* type: string
|
||||
* example: "获取成功"
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* growthData:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* date:
|
||||
* type: string
|
||||
* format: date
|
||||
* newUsers:
|
||||
* type: integer
|
||||
* totalUsers:
|
||||
* type: integer
|
||||
* summary:
|
||||
* type: object
|
||||
* properties:
|
||||
* totalNewUsers:
|
||||
* type: integer
|
||||
* averageDaily:
|
||||
* type: number
|
||||
* growthRate:
|
||||
* type: number
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
router.get('/dashboard/user-growth', authenticateAdmin, adminController.getUserGrowth);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/dashboard/order-stats:
|
||||
* get:
|
||||
* summary: 获取订单统计数据
|
||||
* tags: [Admin]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: days
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 365
|
||||
* default: 7
|
||||
* description: 查询天数
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* code:
|
||||
* type: integer
|
||||
* example: 200
|
||||
* message:
|
||||
* type: string
|
||||
* example: "获取成功"
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* orderStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* date:
|
||||
* type: string
|
||||
* format: date
|
||||
* count:
|
||||
* type: integer
|
||||
* amount:
|
||||
* type: number
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
router.get('/dashboard/order-stats', authenticateAdmin, systemStatsController.getOrderStats);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin:
|
||||
@@ -683,4 +907,10 @@ router.get('/system/order-stats', authenticateAdmin, systemStatsController.getOr
|
||||
*/
|
||||
router.get('/system/info', authenticateAdmin, systemStatsController.getSystemInfo);
|
||||
|
||||
// 注册子路由
|
||||
router.use('/users', userManagementRoutes);
|
||||
router.use('/statistics', dataStatisticsRoutes);
|
||||
router.use('/animals', animalManagementRoutes);
|
||||
router.use('/files', fileManagementRoutes);
|
||||
|
||||
module.exports = router;
|
||||
611
backend/src/routes/admin/animalManagement.js
Normal file
611
backend/src/routes/admin/animalManagement.js
Normal file
@@ -0,0 +1,611 @@
|
||||
const express = require('express');
|
||||
const { body, query, param } = require('express-validator');
|
||||
const AnimalManagementController = require('../../controllers/admin/animalManagement');
|
||||
const { requireRole } = require('../../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Admin Animal Management
|
||||
* description: 管理员动物管理相关接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* AnimalDetail:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 动物ID
|
||||
* name:
|
||||
* type: string
|
||||
* description: 动物名称
|
||||
* species:
|
||||
* type: string
|
||||
* description: 动物物种
|
||||
* breed:
|
||||
* type: string
|
||||
* description: 品种
|
||||
* age:
|
||||
* type: integer
|
||||
* description: 年龄(月)
|
||||
* gender:
|
||||
* type: string
|
||||
* enum: [male, female]
|
||||
* description: 性别
|
||||
* price:
|
||||
* type: number
|
||||
* description: 认领价格
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [available, claimed, unavailable]
|
||||
* description: 状态
|
||||
* description:
|
||||
* type: string
|
||||
* description: 动物描述
|
||||
* images:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: 动物图片
|
||||
* merchant_id:
|
||||
* type: integer
|
||||
* description: 商家ID
|
||||
* merchant_name:
|
||||
* type: string
|
||||
* description: 商家名称
|
||||
* claim_count:
|
||||
* type: integer
|
||||
* description: 被认领次数
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
* AnimalStatistics:
|
||||
* type: object
|
||||
* properties:
|
||||
* totalStats:
|
||||
* type: object
|
||||
* properties:
|
||||
* total_animals:
|
||||
* type: integer
|
||||
* description: 动物总数
|
||||
* available_animals:
|
||||
* type: integer
|
||||
* description: 可认领动物数
|
||||
* claimed_animals:
|
||||
* type: integer
|
||||
* description: 已认领动物数
|
||||
* total_claims:
|
||||
* type: integer
|
||||
* description: 总认领次数
|
||||
* avg_price:
|
||||
* type: number
|
||||
* description: 平均价格
|
||||
* speciesStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* species:
|
||||
* type: string
|
||||
* count:
|
||||
* type: integer
|
||||
* avg_price:
|
||||
* type: number
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/animals:
|
||||
* get:
|
||||
* summary: 获取动物列表
|
||||
* tags: [Admin Animal Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* 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: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: keyword
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词
|
||||
* - in: query
|
||||
* name: species
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 动物物种
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [available, claimed, unavailable]
|
||||
* description: 动物状态
|
||||
* - in: query
|
||||
* name: merchant_id
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 商家ID
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* - in: query
|
||||
* name: sort_by
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [created_at, updated_at, price, claim_count]
|
||||
* default: created_at
|
||||
* description: 排序字段
|
||||
* - in: query
|
||||
* name: sort_order
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [asc, desc]
|
||||
* default: desc
|
||||
* description: 排序方向
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* animals:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/AnimalDetail'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
query('page').optional().isInt({ min: 1 }),
|
||||
query('limit').optional().isInt({ min: 1, max: 100 }),
|
||||
query('keyword').optional().isString(),
|
||||
query('species').optional().isString(),
|
||||
query('status').optional().isIn(['available', 'claimed', 'unavailable']),
|
||||
query('merchant_id').optional().isInt(),
|
||||
query('start_date').optional().isDate(),
|
||||
query('end_date').optional().isDate(),
|
||||
query('sort_by').optional().isIn(['created_at', 'updated_at', 'price', 'claim_count']),
|
||||
query('sort_order').optional().isIn(['asc', 'desc'])
|
||||
],
|
||||
AnimalManagementController.getAnimalList
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/animals/{animal_id}:
|
||||
* get:
|
||||
* summary: 获取动物详情
|
||||
* tags: [Admin Animal Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: animal_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 动物ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* animal:
|
||||
* $ref: '#/components/schemas/AnimalDetail'
|
||||
* claimStats:
|
||||
* type: object
|
||||
* properties:
|
||||
* total_claims:
|
||||
* type: integer
|
||||
* pending_claims:
|
||||
* type: integer
|
||||
* approved_claims:
|
||||
* type: integer
|
||||
* rejected_claims:
|
||||
* type: integer
|
||||
* recentClaims:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/AnimalClaim'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 动物不存在
|
||||
*/
|
||||
router.get('/:animal_id',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
param('animal_id').isInt({ min: 1 })
|
||||
],
|
||||
AnimalManagementController.getAnimalDetail
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/animals/{animal_id}/status:
|
||||
* put:
|
||||
* summary: 更新动物状态
|
||||
* tags: [Admin Animal Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: animal_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 动物ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - status
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [available, claimed, unavailable]
|
||||
* description: 新状态
|
||||
* reason:
|
||||
* type: string
|
||||
* description: 状态变更原因
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 动物不存在
|
||||
*/
|
||||
router.put('/:animal_id/status',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
param('animal_id').isInt({ min: 1 }),
|
||||
body('status').isIn(['available', 'claimed', 'unavailable']),
|
||||
body('reason').optional().isString().isLength({ max: 500 })
|
||||
],
|
||||
AnimalManagementController.updateAnimalStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/animals/batch/status:
|
||||
* put:
|
||||
* summary: 批量更新动物状态
|
||||
* tags: [Admin Animal Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - animal_ids
|
||||
* - status
|
||||
* properties:
|
||||
* animal_ids:
|
||||
* type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* description: 动物ID列表
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [available, claimed, unavailable]
|
||||
* description: 新状态
|
||||
* reason:
|
||||
* type: string
|
||||
* description: 状态变更原因
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 批量更新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* updated_count:
|
||||
* type: integer
|
||||
* description: 更新的动物数量
|
||||
* 400:
|
||||
* description: 参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.put('/batch/status',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
body('animal_ids').isArray({ min: 1 }),
|
||||
body('animal_ids.*').isInt({ min: 1 }),
|
||||
body('status').isIn(['available', 'claimed', 'unavailable']),
|
||||
body('reason').optional().isString().isLength({ max: 500 })
|
||||
],
|
||||
AnimalManagementController.batchUpdateAnimalStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/animals/statistics:
|
||||
* get:
|
||||
* summary: 获取动物统计信息
|
||||
* tags: [Admin Animal Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/AnimalStatistics'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/statistics',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
AnimalManagementController.getAnimalStatistics
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/animals/export:
|
||||
* get:
|
||||
* summary: 导出动物数据
|
||||
* tags: [Admin Animal Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: format
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [csv, json]
|
||||
* default: csv
|
||||
* description: 导出格式
|
||||
* - in: query
|
||||
* name: keyword
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词
|
||||
* - in: query
|
||||
* name: species
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 动物物种
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [available, claimed, unavailable]
|
||||
* description: 动物状态
|
||||
* - in: query
|
||||
* name: merchant_id
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 商家ID
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 导出成功
|
||||
* content:
|
||||
* text/csv:
|
||||
* schema:
|
||||
* type: string
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* animals:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/AnimalDetail'
|
||||
* export_time:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* total_count:
|
||||
* type: integer
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/export',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
query('format').optional().isIn(['csv', 'json']),
|
||||
query('keyword').optional().isString(),
|
||||
query('species').optional().isString(),
|
||||
query('status').optional().isIn(['available', 'claimed', 'unavailable']),
|
||||
query('merchant_id').optional().isInt(),
|
||||
query('start_date').optional().isDate(),
|
||||
query('end_date').optional().isDate()
|
||||
],
|
||||
AnimalManagementController.exportAnimalData
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/animals/{animal_id}/claims:
|
||||
* get:
|
||||
* summary: 获取动物认领记录
|
||||
* tags: [Admin Animal Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: animal_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 动物ID
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [pending, approved, rejected, cancelled]
|
||||
* description: 认领状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* claims:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/AnimalClaim'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/:animal_id/claims',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
param('animal_id').isInt({ min: 1 }),
|
||||
query('page').optional().isInt({ min: 1 }),
|
||||
query('limit').optional().isInt({ min: 1, max: 100 }),
|
||||
query('status').optional().isIn(['pending', 'approved', 'rejected', 'cancelled'])
|
||||
],
|
||||
AnimalManagementController.getAnimalClaimRecords
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
522
backend/src/routes/admin/dataStatistics.js
Normal file
522
backend/src/routes/admin/dataStatistics.js
Normal file
@@ -0,0 +1,522 @@
|
||||
const express = require('express');
|
||||
const { query } = require('express-validator');
|
||||
const DataStatisticsController = require('../../controllers/admin/dataStatistics');
|
||||
const { requireRole } = require('../../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Admin Data Statistics
|
||||
* description: 管理员数据统计相关接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* SystemOverview:
|
||||
* type: object
|
||||
* properties:
|
||||
* users:
|
||||
* type: object
|
||||
* properties:
|
||||
* total_users:
|
||||
* type: integer
|
||||
* description: 用户总数
|
||||
* active_users:
|
||||
* type: integer
|
||||
* description: 活跃用户数
|
||||
* new_users_today:
|
||||
* type: integer
|
||||
* description: 今日新增用户
|
||||
* new_users_week:
|
||||
* type: integer
|
||||
* description: 本周新增用户
|
||||
* travels:
|
||||
* type: object
|
||||
* properties:
|
||||
* total_travels:
|
||||
* type: integer
|
||||
* description: 旅行总数
|
||||
* published_travels:
|
||||
* type: integer
|
||||
* description: 已发布旅行
|
||||
* new_travels_today:
|
||||
* type: integer
|
||||
* description: 今日新增旅行
|
||||
* animals:
|
||||
* type: object
|
||||
* properties:
|
||||
* total_animals:
|
||||
* type: integer
|
||||
* description: 动物总数
|
||||
* available_animals:
|
||||
* type: integer
|
||||
* description: 可认领动物
|
||||
* claimed_animals:
|
||||
* type: integer
|
||||
* description: 已认领动物
|
||||
* orders:
|
||||
* type: object
|
||||
* properties:
|
||||
* total_orders:
|
||||
* type: integer
|
||||
* description: 订单总数
|
||||
* completed_orders:
|
||||
* type: integer
|
||||
* description: 已完成订单
|
||||
* total_revenue:
|
||||
* type: number
|
||||
* description: 总收入
|
||||
* TrendData:
|
||||
* type: object
|
||||
* properties:
|
||||
* date:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 日期
|
||||
* new_users:
|
||||
* type: integer
|
||||
* description: 新增用户数
|
||||
* cumulative_users:
|
||||
* type: integer
|
||||
* description: 累计用户数
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/statistics/overview:
|
||||
* get:
|
||||
* summary: 获取系统概览统计
|
||||
* tags: [Admin Data Statistics]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* $ref: '#/components/schemas/SystemOverview'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/overview',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
DataStatisticsController.getSystemOverview
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/statistics/user-growth:
|
||||
* get:
|
||||
* summary: 获取用户增长趋势
|
||||
* tags: [Admin Data Statistics]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: period
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [7d, 30d, 90d, 365d]
|
||||
* default: 30d
|
||||
* description: 统计周期
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* period:
|
||||
* type: string
|
||||
* trendData:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/TrendData'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/user-growth',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
query('period').optional().isIn(['7d', '30d', '90d', '365d'])
|
||||
],
|
||||
DataStatisticsController.getUserGrowthTrend
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/statistics/business:
|
||||
* get:
|
||||
* summary: 获取业务数据统计
|
||||
* tags: [Admin Data Statistics]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: period
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [7d, 30d, 90d]
|
||||
* default: 30d
|
||||
* description: 统计周期
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* period:
|
||||
* type: string
|
||||
* travelStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* date:
|
||||
* type: string
|
||||
* format: date
|
||||
* new_travels:
|
||||
* type: integer
|
||||
* published_travels:
|
||||
* type: integer
|
||||
* matched_travels:
|
||||
* type: integer
|
||||
* claimStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* date:
|
||||
* type: string
|
||||
* format: date
|
||||
* new_claims:
|
||||
* type: integer
|
||||
* approved_claims:
|
||||
* type: integer
|
||||
* rejected_claims:
|
||||
* type: integer
|
||||
* orderStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* date:
|
||||
* type: string
|
||||
* format: date
|
||||
* new_orders:
|
||||
* type: integer
|
||||
* completed_orders:
|
||||
* type: integer
|
||||
* daily_revenue:
|
||||
* type: number
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/business',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
query('period').optional().isIn(['7d', '30d', '90d'])
|
||||
],
|
||||
DataStatisticsController.getBusinessStatistics
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/statistics/geographic:
|
||||
* get:
|
||||
* summary: 获取地域分布统计
|
||||
* tags: [Admin Data Statistics]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* userDistribution:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* province:
|
||||
* type: string
|
||||
* city:
|
||||
* type: string
|
||||
* user_count:
|
||||
* type: integer
|
||||
* provinceStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* province:
|
||||
* type: string
|
||||
* user_count:
|
||||
* type: integer
|
||||
* farmer_count:
|
||||
* type: integer
|
||||
* merchant_count:
|
||||
* type: integer
|
||||
* destinationStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* destination:
|
||||
* type: string
|
||||
* travel_count:
|
||||
* type: integer
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/geographic',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
DataStatisticsController.getGeographicDistribution
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/statistics/user-behavior:
|
||||
* get:
|
||||
* summary: 获取用户行为分析
|
||||
* tags: [Admin Data Statistics]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* activityStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* activity_level:
|
||||
* type: string
|
||||
* user_count:
|
||||
* type: integer
|
||||
* levelDistribution:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* level:
|
||||
* type: string
|
||||
* user_count:
|
||||
* type: integer
|
||||
* avg_points:
|
||||
* type: number
|
||||
* avg_travel_count:
|
||||
* type: number
|
||||
* avg_claim_count:
|
||||
* type: number
|
||||
* behaviorStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* behavior_type:
|
||||
* type: string
|
||||
* user_count:
|
||||
* type: integer
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/user-behavior',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
DataStatisticsController.getUserBehaviorAnalysis
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/statistics/revenue:
|
||||
* get:
|
||||
* summary: 获取收入统计
|
||||
* tags: [Admin Data Statistics]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: period
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [7d, 30d, 90d, 365d]
|
||||
* default: 30d
|
||||
* description: 统计周期
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* period:
|
||||
* type: string
|
||||
* revenueTrend:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* date:
|
||||
* type: string
|
||||
* format: date
|
||||
* daily_revenue:
|
||||
* type: number
|
||||
* completed_orders:
|
||||
* type: integer
|
||||
* total_orders:
|
||||
* type: integer
|
||||
* revenueSource:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* order_type:
|
||||
* type: string
|
||||
* order_count:
|
||||
* type: integer
|
||||
* total_revenue:
|
||||
* type: number
|
||||
* avg_order_value:
|
||||
* type: number
|
||||
* paymentMethodStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* payment_method:
|
||||
* type: string
|
||||
* order_count:
|
||||
* type: integer
|
||||
* total_amount:
|
||||
* type: number
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/revenue',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
query('period').optional().isIn(['7d', '30d', '90d', '365d'])
|
||||
],
|
||||
DataStatisticsController.getRevenueStatistics
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/statistics/export:
|
||||
* get:
|
||||
* summary: 导出统计报告
|
||||
* tags: [Admin Data Statistics]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: reportType
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [overview, users, revenue]
|
||||
* default: overview
|
||||
* description: 报告类型
|
||||
* - in: query
|
||||
* name: period
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [7d, 30d, 90d]
|
||||
* default: 30d
|
||||
* description: 统计周期
|
||||
* - in: query
|
||||
* name: format
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [csv, json]
|
||||
* default: csv
|
||||
* description: 导出格式
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 导出成功
|
||||
* content:
|
||||
* text/csv:
|
||||
* schema:
|
||||
* type: string
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/export',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
query('reportType').optional().isIn(['overview', 'users', 'revenue']),
|
||||
query('period').optional().isIn(['7d', '30d', '90d']),
|
||||
query('format').optional().isIn(['csv', 'json'])
|
||||
],
|
||||
DataStatisticsController.exportStatisticsReport
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
601
backend/src/routes/admin/fileManagement.js
Normal file
601
backend/src/routes/admin/fileManagement.js
Normal file
@@ -0,0 +1,601 @@
|
||||
/**
|
||||
* 管理员文件管理路由
|
||||
* 定义文件上传、管理、统计等API接口
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
getFileList,
|
||||
getFileDetail,
|
||||
deleteFileById,
|
||||
batchDeleteFiles,
|
||||
getFileStatistics,
|
||||
cleanupUnusedFiles,
|
||||
uploadFile
|
||||
} = require('../../controllers/admin/fileManagement');
|
||||
const { uploadMiddlewares, imageProcessors } = require('../../middleware/upload');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* FileInfo:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* description: 文件ID(Base64编码的文件路径)
|
||||
* filename:
|
||||
* type: string
|
||||
* description: 文件名
|
||||
* originalName:
|
||||
* type: string
|
||||
* description: 原始文件名
|
||||
* type:
|
||||
* type: string
|
||||
* enum: [avatar, animal, travel, document]
|
||||
* description: 文件类型
|
||||
* size:
|
||||
* type: integer
|
||||
* description: 文件大小(字节)
|
||||
* mimetype:
|
||||
* type: string
|
||||
* description: MIME类型
|
||||
* isImage:
|
||||
* type: boolean
|
||||
* description: 是否为图片
|
||||
* url:
|
||||
* type: string
|
||||
* description: 文件访问URL
|
||||
* thumbnailUrl:
|
||||
* type: string
|
||||
* description: 缩略图URL(仅图片)
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* modified_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 修改时间
|
||||
*
|
||||
* FileStatistics:
|
||||
* type: object
|
||||
* properties:
|
||||
* totalFiles:
|
||||
* type: integer
|
||||
* description: 文件总数
|
||||
* totalSize:
|
||||
* type: integer
|
||||
* description: 总大小(字节)
|
||||
* typeStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* description: 文件类型
|
||||
* count:
|
||||
* type: integer
|
||||
* description: 文件数量
|
||||
* size:
|
||||
* type: integer
|
||||
* description: 总大小
|
||||
* avgSize:
|
||||
* type: integer
|
||||
* description: 平均大小
|
||||
* sizeDistribution:
|
||||
* type: object
|
||||
* properties:
|
||||
* small:
|
||||
* type: integer
|
||||
* description: 小文件数量(<1MB)
|
||||
* medium:
|
||||
* type: integer
|
||||
* description: 中等文件数量(1-5MB)
|
||||
* large:
|
||||
* type: integer
|
||||
* description: 大文件数量(>5MB)
|
||||
* formatStats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* format:
|
||||
* type: string
|
||||
* description: 文件格式
|
||||
* count:
|
||||
* type: integer
|
||||
* description: 数量
|
||||
* size:
|
||||
* type: integer
|
||||
* description: 总大小
|
||||
* percentage:
|
||||
* type: string
|
||||
* description: 占比百分比
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/files:
|
||||
* get:
|
||||
* summary: 获取文件列表
|
||||
* tags: [管理员-文件管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 20
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: type
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [all, avatar, animal, travel, document]
|
||||
* default: all
|
||||
* description: 文件类型
|
||||
* - in: query
|
||||
* name: keyword
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* - in: query
|
||||
* name: sort_by
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [created_at, modified_at, size, filename]
|
||||
* default: created_at
|
||||
* description: 排序字段
|
||||
* - in: query
|
||||
* name: sort_order
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [asc, desc]
|
||||
* default: desc
|
||||
* description: 排序方向
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* files:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/FileInfo'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
*/
|
||||
router.get('/', getFileList);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/files/{file_id}:
|
||||
* get:
|
||||
* summary: 获取文件详情
|
||||
* tags: [管理员-文件管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: file_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 文件ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* file:
|
||||
* $ref: '#/components/schemas/FileInfo'
|
||||
* 404:
|
||||
* description: 文件不存在
|
||||
*/
|
||||
router.get('/:file_id', getFileDetail);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/files/{file_id}:
|
||||
* delete:
|
||||
* summary: 删除文件
|
||||
* tags: [管理员-文件管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: file_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 文件ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 删除成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* 404:
|
||||
* description: 文件不存在
|
||||
*/
|
||||
router.delete('/:file_id', deleteFileById);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/files/batch/delete:
|
||||
* post:
|
||||
* summary: 批量删除文件
|
||||
* tags: [管理员-文件管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - file_ids
|
||||
* properties:
|
||||
* file_ids:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: 文件ID列表(最多50个)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 批量删除完成
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* file_id:
|
||||
* type: string
|
||||
* filename:
|
||||
* type: string
|
||||
* message:
|
||||
* type: string
|
||||
* failed:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* file_id:
|
||||
* type: string
|
||||
* filename:
|
||||
* type: string
|
||||
* message:
|
||||
* type: string
|
||||
*/
|
||||
router.post('/batch/delete', batchDeleteFiles);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/files/statistics:
|
||||
* get:
|
||||
* summary: 获取文件统计信息
|
||||
* tags: [管理员-文件管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/FileStatistics'
|
||||
*/
|
||||
router.get('/statistics', getFileStatistics);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/files/cleanup:
|
||||
* post:
|
||||
* summary: 清理无用文件
|
||||
* tags: [管理员-文件管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: dry_run
|
||||
* schema:
|
||||
* type: boolean
|
||||
* default: true
|
||||
* description: 是否为试运行(不实际删除文件)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 清理完成
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* scanned:
|
||||
* type: integer
|
||||
* description: 扫描的文件数量
|
||||
* unused:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* filename:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* size:
|
||||
* type: integer
|
||||
* lastModified:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* filename:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* size:
|
||||
* type: integer
|
||||
* errors:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* filename:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* error:
|
||||
* type: string
|
||||
*/
|
||||
router.post('/cleanup', cleanupUnusedFiles);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/files/upload/avatar:
|
||||
* post:
|
||||
* summary: 上传头像
|
||||
* tags: [管理员-文件管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* avatar:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: 头像文件(支持jpg、png格式,最大2MB)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 上传成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* files:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/FileInfo'
|
||||
*/
|
||||
router.post('/upload/avatar', uploadMiddlewares.avatar, imageProcessors.avatar, uploadFile);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/files/upload/animal:
|
||||
* post:
|
||||
* summary: 上传动物图片
|
||||
* tags: [管理员-文件管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* images:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: 动物图片文件(支持jpg、png、gif、webp格式,最大5MB,最多5张)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 上传成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* files:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/FileInfo'
|
||||
*/
|
||||
router.post('/upload/animal', uploadMiddlewares.animalImages, imageProcessors.animal, uploadFile);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/files/upload/travel:
|
||||
* post:
|
||||
* summary: 上传旅行图片
|
||||
* tags: [管理员-文件管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* images:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: 旅行图片文件(支持jpg、png、gif、webp格式,最大5MB,最多10张)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 上传成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* files:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/FileInfo'
|
||||
*/
|
||||
router.post('/upload/travel', uploadMiddlewares.travelImages, imageProcessors.travel, uploadFile);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/files/upload/document:
|
||||
* post:
|
||||
* summary: 上传文档
|
||||
* tags: [管理员-文件管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* documents:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: 文档文件(支持pdf、doc、docx、xls、xlsx、txt格式,最大10MB,最多3个)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 上传成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* files:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/FileInfo'
|
||||
*/
|
||||
router.post('/upload/document', uploadMiddlewares.documents, uploadFile);
|
||||
|
||||
module.exports = router;
|
||||
504
backend/src/routes/admin/userManagement.js
Normal file
504
backend/src/routes/admin/userManagement.js
Normal file
@@ -0,0 +1,504 @@
|
||||
const express = require('express');
|
||||
const { body, query, param } = require('express-validator');
|
||||
const UserManagementController = require('../../controllers/admin/userManagement');
|
||||
const { requireRole } = require('../../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Admin User Management
|
||||
* description: 管理员用户管理相关接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* UserDetail:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* nickname:
|
||||
* type: string
|
||||
* description: 用户昵称
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* email:
|
||||
* type: string
|
||||
* description: 邮箱
|
||||
* user_type:
|
||||
* type: string
|
||||
* enum: [farmer, merchant]
|
||||
* description: 用户类型
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, inactive, banned]
|
||||
* description: 用户状态
|
||||
* travel_count:
|
||||
* type: integer
|
||||
* description: 旅行次数
|
||||
* animal_claim_count:
|
||||
* type: integer
|
||||
* description: 认领次数
|
||||
* points:
|
||||
* type: integer
|
||||
* description: 积分
|
||||
* level:
|
||||
* type: string
|
||||
* enum: [bronze, silver, gold, platinum]
|
||||
* description: 用户等级
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* last_login_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 最后登录时间
|
||||
* UserStatistics:
|
||||
* type: object
|
||||
* properties:
|
||||
* total_users:
|
||||
* type: integer
|
||||
* description: 用户总数
|
||||
* active_users:
|
||||
* type: integer
|
||||
* description: 活跃用户数
|
||||
* new_users_today:
|
||||
* type: integer
|
||||
* description: 今日新增用户
|
||||
* new_users_week:
|
||||
* type: integer
|
||||
* description: 本周新增用户
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/users:
|
||||
* get:
|
||||
* summary: 获取用户列表
|
||||
* tags: [Admin User Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: pageSize
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: keyword
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词(昵称、手机号、邮箱)
|
||||
* - in: query
|
||||
* name: userType
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [farmer, merchant]
|
||||
* description: 用户类型
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [active, inactive, banned]
|
||||
* description: 用户状态
|
||||
* - in: query
|
||||
* name: startDate
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: endDate
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* - in: query
|
||||
* name: sortField
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [created_at, last_login_at, points, travel_count]
|
||||
* default: created_at
|
||||
* description: 排序字段
|
||||
* - in: query
|
||||
* name: sortOrder
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [asc, desc]
|
||||
* default: desc
|
||||
* description: 排序方向
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* users:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/UserDetail'
|
||||
* pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* pageSize:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* totalPages:
|
||||
* type: integer
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
query('page').optional().isInt({ min: 1 }),
|
||||
query('pageSize').optional().isInt({ min: 1, max: 100 }),
|
||||
query('userType').optional().isIn(['farmer', 'merchant']),
|
||||
query('status').optional().isIn(['active', 'inactive', 'banned']),
|
||||
query('sortField').optional().isIn(['created_at', 'last_login_at', 'points', 'travel_count']),
|
||||
query('sortOrder').optional().isIn(['asc', 'desc'])
|
||||
],
|
||||
UserManagementController.getUserList
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/users/{userId}:
|
||||
* get:
|
||||
* summary: 获取用户详情
|
||||
* tags: [Admin User Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: userId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* user:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/UserDetail'
|
||||
* - type: object
|
||||
* properties:
|
||||
* interests:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: 用户兴趣
|
||||
* recentTravels:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* description: 最近旅行记录
|
||||
* recentClaims:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* description: 最近认领记录
|
||||
* 404:
|
||||
* description: 用户不存在
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/:userId',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
param('userId').isInt({ min: 1 })
|
||||
],
|
||||
UserManagementController.getUserDetail
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/users/{userId}/status:
|
||||
* put:
|
||||
* summary: 更新用户状态
|
||||
* tags: [Admin User Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: userId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - status
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, inactive, banned]
|
||||
* description: 新状态
|
||||
* reason:
|
||||
* type: string
|
||||
* description: 操作原因
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
* 400:
|
||||
* description: 无效的状态值
|
||||
* 404:
|
||||
* description: 用户不存在
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.put('/:userId/status',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
param('userId').isInt({ min: 1 }),
|
||||
body('status').isIn(['active', 'inactive', 'banned']),
|
||||
body('reason').optional().isString()
|
||||
],
|
||||
UserManagementController.updateUserStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/users/batch-status:
|
||||
* put:
|
||||
* summary: 批量更新用户状态
|
||||
* tags: [Admin User Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - userIds
|
||||
* - status
|
||||
* properties:
|
||||
* userIds:
|
||||
* type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* description: 用户ID列表
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, inactive, banned]
|
||||
* description: 新状态
|
||||
* reason:
|
||||
* type: string
|
||||
* description: 操作原因
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
* 400:
|
||||
* description: 参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.put('/batch-status',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
body('userIds').isArray({ min: 1 }),
|
||||
body('userIds.*').isInt({ min: 1 }),
|
||||
body('status').isIn(['active', 'inactive', 'banned']),
|
||||
body('reason').optional().isString()
|
||||
],
|
||||
UserManagementController.batchUpdateUserStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/users/statistics:
|
||||
* get:
|
||||
* summary: 获取用户统计信息
|
||||
* tags: [Admin User Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: period
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [7d, 30d, 90d]
|
||||
* default: 30d
|
||||
* description: 统计周期
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* basicStats:
|
||||
* $ref: '#/components/schemas/UserStatistics'
|
||||
* levelDistribution:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* level:
|
||||
* type: string
|
||||
* count:
|
||||
* type: integer
|
||||
* trendData:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* date:
|
||||
* type: string
|
||||
* format: date
|
||||
* new_users:
|
||||
* type: integer
|
||||
* new_farmers:
|
||||
* type: integer
|
||||
* new_merchants:
|
||||
* type: integer
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/statistics',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
query('period').optional().isIn(['7d', '30d', '90d'])
|
||||
],
|
||||
UserManagementController.getUserStatistics
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin/users/export:
|
||||
* get:
|
||||
* summary: 导出用户数据
|
||||
* tags: [Admin User Management]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: format
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [csv, json]
|
||||
* default: csv
|
||||
* description: 导出格式
|
||||
* - in: query
|
||||
* name: userType
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [farmer, merchant]
|
||||
* description: 用户类型筛选
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [active, inactive, banned]
|
||||
* description: 状态筛选
|
||||
* - in: query
|
||||
* name: startDate
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: endDate
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 导出成功
|
||||
* content:
|
||||
* text/csv:
|
||||
* schema:
|
||||
* type: string
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* users:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/UserDetail'
|
||||
* total:
|
||||
* type: integer
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/export',
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
query('format').optional().isIn(['csv', 'json']),
|
||||
query('userType').optional().isIn(['farmer', 'merchant']),
|
||||
query('status').optional().isIn(['active', 'inactive', 'banned'])
|
||||
],
|
||||
UserManagementController.exportUsers
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
51
backend/src/routes/animalClaim-simple.js
Normal file
51
backend/src/routes/animalClaim-simple.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authenticateUser, requireRole } = require('../middleware/auth');
|
||||
|
||||
// 简化的动物认领路由
|
||||
router.post('/', authenticateUser, async (req, res) => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '动物认领功能暂时维护中',
|
||||
data: null
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器错误'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/user/:userId', authenticateUser, async (req, res) => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取用户认领记录功能暂时维护中',
|
||||
data: []
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器错误'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/animal/:animalId', async (req, res) => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取动物认领记录功能暂时维护中',
|
||||
data: []
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器错误'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
656
backend/src/routes/animalClaim.js
Normal file
656
backend/src/routes/animalClaim.js
Normal file
@@ -0,0 +1,656 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const AnimalClaimController = require('../controllers/animalClaim');
|
||||
const { authenticateToken, requireRole } = require('../middleware/auth');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* AnimalClaim:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 认领申请ID
|
||||
* claim_no:
|
||||
* type: string
|
||||
* description: 认领订单号
|
||||
* animal_id:
|
||||
* type: integer
|
||||
* description: 动物ID
|
||||
* animal_name:
|
||||
* type: string
|
||||
* description: 动物名称
|
||||
* animal_type:
|
||||
* type: string
|
||||
* description: 动物类型
|
||||
* animal_image:
|
||||
* type: string
|
||||
* description: 动物图片
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* user_phone:
|
||||
* type: string
|
||||
* description: 用户手机号
|
||||
* claim_reason:
|
||||
* type: string
|
||||
* description: 认领理由
|
||||
* claim_duration:
|
||||
* type: integer
|
||||
* description: 认领时长(月)
|
||||
* total_amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 总金额
|
||||
* contact_info:
|
||||
* type: string
|
||||
* description: 联系方式
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, approved, rejected, cancelled]
|
||||
* description: 申请状态
|
||||
* start_date:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 开始日期
|
||||
* end_date:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 结束日期
|
||||
* reviewed_by:
|
||||
* type: integer
|
||||
* description: 审核人ID
|
||||
* reviewer_name:
|
||||
* type: string
|
||||
* description: 审核人姓名
|
||||
* review_remark:
|
||||
* type: string
|
||||
* description: 审核备注
|
||||
* reviewed_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 审核时间
|
||||
* approved_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 通过时间
|
||||
* cancelled_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 取消时间
|
||||
* cancel_reason:
|
||||
* type: string
|
||||
* description: 取消原因
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
*
|
||||
* ClaimStatistics:
|
||||
* type: object
|
||||
* properties:
|
||||
* basic:
|
||||
* type: object
|
||||
* properties:
|
||||
* total_claims:
|
||||
* type: integer
|
||||
* description: 总申请数
|
||||
* pending_claims:
|
||||
* type: integer
|
||||
* description: 待审核申请数
|
||||
* approved_claims:
|
||||
* type: integer
|
||||
* description: 已通过申请数
|
||||
* rejected_claims:
|
||||
* type: integer
|
||||
* description: 已拒绝申请数
|
||||
* cancelled_claims:
|
||||
* type: integer
|
||||
* description: 已取消申请数
|
||||
* total_amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 总金额
|
||||
* avg_duration:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 平均认领时长
|
||||
* by_type:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* description: 动物类型
|
||||
* claim_count:
|
||||
* type: integer
|
||||
* description: 申请数量
|
||||
* approved_count:
|
||||
* type: integer
|
||||
* description: 通过数量
|
||||
* total_amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 总金额
|
||||
* by_month:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* month:
|
||||
* type: string
|
||||
* description: 月份
|
||||
* claim_count:
|
||||
* type: integer
|
||||
* description: 申请数量
|
||||
* approved_count:
|
||||
* type: integer
|
||||
* description: 通过数量
|
||||
* total_amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 总金额
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/animal-claims:
|
||||
* post:
|
||||
* summary: 申请认领动物
|
||||
* tags: [动物认领]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - animal_id
|
||||
* - contact_info
|
||||
* properties:
|
||||
* animal_id:
|
||||
* type: integer
|
||||
* description: 动物ID
|
||||
* claim_reason:
|
||||
* type: string
|
||||
* description: 认领理由
|
||||
* claim_duration:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 60
|
||||
* description: 认领时长(月,默认12个月)
|
||||
* contact_info:
|
||||
* type: string
|
||||
* description: 联系方式
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 认领申请提交成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/AnimalClaim'
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.post('/', authenticateToken, AnimalClaimController.createClaim);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/animal-claims/my:
|
||||
* get:
|
||||
* summary: 获取我的认领申请列表
|
||||
* tags: [动物认领]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* 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: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [pending, approved, rejected, cancelled]
|
||||
* description: 申请状态
|
||||
* - in: query
|
||||
* name: animal_type
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 动物类型
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/AnimalClaim'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
*/
|
||||
router.get('/my', authenticateToken, AnimalClaimController.getUserClaims);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/animal-claims/statistics:
|
||||
* get:
|
||||
* summary: 获取认领统计信息
|
||||
* tags: [动物认领]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* - in: query
|
||||
* name: animal_type
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 动物类型
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/ClaimStatistics'
|
||||
*/
|
||||
router.get('/statistics', authenticateToken, requireRole(['admin', 'manager']), AnimalClaimController.getClaimStatistics);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/animal-claims/animal/{animal_id}:
|
||||
* get:
|
||||
* summary: 获取动物的认领申请列表
|
||||
* tags: [动物认领]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: animal_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 动物ID
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [pending, approved, rejected, cancelled]
|
||||
* description: 申请状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/AnimalClaim'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
*/
|
||||
router.get('/animal/:animal_id', authenticateToken, requireRole(['admin', 'manager']), AnimalClaimController.getAnimalClaims);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/animal-claims/check-permission/{animal_id}:
|
||||
* get:
|
||||
* summary: 检查认领权限
|
||||
* tags: [动物认领]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: animal_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 动物ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 检查成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* can_claim:
|
||||
* type: boolean
|
||||
* description: 是否可以认领
|
||||
*/
|
||||
router.get('/check-permission/:animal_id', authenticateToken, AnimalClaimController.checkClaimPermission);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/animal-claims:
|
||||
* get:
|
||||
* summary: 获取所有认领申请列表(管理员)
|
||||
* tags: [动物认领]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* 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: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [pending, approved, rejected, cancelled]
|
||||
* description: 申请状态
|
||||
* - in: query
|
||||
* name: animal_type
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 动物类型
|
||||
* - in: query
|
||||
* name: user_id
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* - in: query
|
||||
* name: keyword
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 关键词搜索(订单号、动物名称、用户名)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/AnimalClaim'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
*/
|
||||
router.get('/', authenticateToken, requireRole(['admin', 'manager']), AnimalClaimController.getAllClaims);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/animal-claims/{id}/cancel:
|
||||
* put:
|
||||
* summary: 取消认领申请
|
||||
* tags: [动物认领]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 认领申请ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 取消成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/AnimalClaim'
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.put('/:id/cancel', authenticateToken, AnimalClaimController.cancelClaim);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/animal-claims/{id}/review:
|
||||
* put:
|
||||
* summary: 审核认领申请
|
||||
* tags: [动物认领]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 认领申请ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - status
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [approved, rejected]
|
||||
* description: 审核状态
|
||||
* review_remark:
|
||||
* type: string
|
||||
* description: 审核备注
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 审核成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/AnimalClaim'
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.put('/:id/review', authenticateToken, requireRole(['admin', 'manager']), AnimalClaimController.reviewClaim);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/animal-claims/{id}/renew:
|
||||
* post:
|
||||
* summary: 续期认领
|
||||
* tags: [动物认领]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 认领申请ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - duration
|
||||
* - payment_method
|
||||
* properties:
|
||||
* duration:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 60
|
||||
* description: 续期时长(月)
|
||||
* payment_method:
|
||||
* type: string
|
||||
* enum: [wechat, alipay, bank_transfer]
|
||||
* description: 支付方式
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 续期申请成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* renewal:
|
||||
* type: object
|
||||
* description: 续期记录
|
||||
* amount:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 续期金额
|
||||
* message:
|
||||
* type: string
|
||||
* description: 提示信息
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.post('/:id/renew', authenticateToken, AnimalClaimController.renewClaim);
|
||||
|
||||
module.exports = router;
|
||||
@@ -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位')
|
||||
@@ -330,6 +332,182 @@ router.put(
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/refresh:
|
||||
* post:
|
||||
* summary: 刷新访问令牌
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - refreshToken
|
||||
* properties:
|
||||
* refreshToken:
|
||||
* type: string
|
||||
* description: 刷新令牌
|
||||
* example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 令牌刷新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* token:
|
||||
* type: string
|
||||
* description: 新的访问令牌
|
||||
* message:
|
||||
* type: string
|
||||
* example: Token刷新成功
|
||||
* 400:
|
||||
* description: 刷新令牌不能为空
|
||||
* 401:
|
||||
* description: 无效或过期的刷新令牌
|
||||
*/
|
||||
router.post('/refresh', authController.refreshToken);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/send-verification:
|
||||
* post:
|
||||
* summary: 发送邮箱验证码
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - email
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: 邮箱地址
|
||||
* example: user@example.com
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 验证码发送成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* type: string
|
||||
* example: 验证码已发送到您的邮箱
|
||||
* 400:
|
||||
* description: 邮箱不能为空或格式不正确
|
||||
*/
|
||||
router.post('/send-verification', authController.sendEmailVerification);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/forgot-password:
|
||||
* post:
|
||||
* summary: 忘记密码
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - email
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: 注册邮箱
|
||||
* example: user@example.com
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 重置链接发送成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* type: string
|
||||
* example: 如果该邮箱已注册,重置密码链接已发送到您的邮箱
|
||||
* 400:
|
||||
* description: 邮箱不能为空
|
||||
*/
|
||||
router.post('/forgot-password', authController.forgotPassword);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/reset-password:
|
||||
* post:
|
||||
* summary: 重置密码
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - token
|
||||
* - newPassword
|
||||
* properties:
|
||||
* token:
|
||||
* type: string
|
||||
* description: 重置令牌
|
||||
* example: abc123def456...
|
||||
* newPassword:
|
||||
* type: string
|
||||
* description: 新密码
|
||||
* example: newpassword123
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 密码重置成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* type: string
|
||||
* example: 密码重置成功
|
||||
* 400:
|
||||
* description: 重置令牌无效或新密码格式错误
|
||||
*/
|
||||
router.post('/reset-password', authController.resetPassword);
|
||||
|
||||
router.post('/admin/login', authController.adminLogin);
|
||||
|
||||
/**
|
||||
|
||||
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;
|
||||
@@ -20,27 +20,33 @@ const router = express.Router();
|
||||
// 创建订单
|
||||
router.post('/', authenticate, createOrder);
|
||||
|
||||
// 获取订单统计信息
|
||||
router.get('/statistics', authenticate, getOrderStatistics);
|
||||
|
||||
// 获取订单详情
|
||||
router.get('/:orderId', authenticate, getOrder);
|
||||
|
||||
// 获取订单列表
|
||||
router.get('/', authenticate, getUserOrders);
|
||||
// 管理员获取所有订单
|
||||
router.get('/admin', authenticate, requireAdmin, getAllOrders);
|
||||
|
||||
// 商家获取订单列表
|
||||
router.get('/merchant', authenticate, requireMerchant, getMerchantOrders);
|
||||
|
||||
// 获取订单列表(用户或管理员)
|
||||
router.get('/', authenticate, (req, res, next) => {
|
||||
if (req.user.role === 'admin' || req.user.role === 'super_admin') {
|
||||
return getAllOrders(req, res, next);
|
||||
} else {
|
||||
return getUserOrders(req, res, next);
|
||||
}
|
||||
});
|
||||
|
||||
// 取消订单
|
||||
router.put('/:orderId/cancel', authenticate, cancelOrder);
|
||||
|
||||
// 支付订单
|
||||
router.put('/:orderId/pay', authenticate, payOrder);
|
||||
|
||||
// 获取订单统计信息
|
||||
router.get('/statistics', authenticate, getOrderStatistics);
|
||||
|
||||
// 管理员获取所有订单
|
||||
router.get('/admin', authenticate, requireAdmin, getAllOrders);
|
||||
|
||||
// 管理员更新订单状态
|
||||
router.put('/:orderId/status', authenticate, requireAdmin, updateOrderStatus);
|
||||
|
||||
|
||||
27
backend/src/routes/payment-simple.js
Normal file
27
backend/src/routes/payment-simple.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// 简单的支付路由,不依赖任何中间件
|
||||
router.post('/', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '支付接口正常',
|
||||
data: {
|
||||
payment_id: Date.now(),
|
||||
status: 'pending'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:id', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: req.params.id,
|
||||
status: 'paid',
|
||||
amount: 100.00
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
80
backend/src/routes/payment-temp.js
Normal file
80
backend/src/routes/payment-temp.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authenticateToken, requireRole } = require('../middleware/auth');
|
||||
const { body, param } = require('express-validator');
|
||||
|
||||
// 临时简单的支付控制器
|
||||
const createPayment = async (req, res) => {
|
||||
try {
|
||||
const { order_id, amount, payment_method } = req.body;
|
||||
const userId = req.user?.id || 1;
|
||||
|
||||
const payment = {
|
||||
id: Date.now(),
|
||||
payment_no: `PAY${Date.now()}`,
|
||||
order_id,
|
||||
user_id: userId,
|
||||
amount,
|
||||
payment_method,
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '支付订单创建成功',
|
||||
data: payment
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建支付订单失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建支付订单失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getPayment = async (req, res) => {
|
||||
try {
|
||||
const { paymentId } = req.params;
|
||||
|
||||
const payment = {
|
||||
id: paymentId,
|
||||
payment_no: `PAY${paymentId}`,
|
||||
status: 'paid',
|
||||
amount: 100.00,
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: payment
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取支付信息失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 路由定义
|
||||
router.post('/',
|
||||
authenticateToken,
|
||||
[
|
||||
body('order_id').isInt({ min: 1 }).withMessage('订单ID必须是正整数'),
|
||||
body('amount').isFloat({ min: 0.01 }).withMessage('支付金额必须大于0'),
|
||||
body('payment_method').isIn(['wechat', 'alipay', 'balance']).withMessage('支付方式无效')
|
||||
],
|
||||
createPayment
|
||||
);
|
||||
|
||||
router.get('/:paymentId',
|
||||
authenticateToken,
|
||||
[param('paymentId').isInt({ min: 1 }).withMessage('支付订单ID必须是正整数')],
|
||||
getPayment
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
561
backend/src/routes/payment.js
Normal file
561
backend/src/routes/payment.js
Normal file
@@ -0,0 +1,561 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const PaymentController = require('../controllers/payment');
|
||||
const { authenticateToken, requireRole } = require('../middleware/auth');
|
||||
const { body, param } = require('express-validator');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Payment:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 支付订单ID
|
||||
* payment_no:
|
||||
* type: string
|
||||
* description: 支付订单号
|
||||
* order_id:
|
||||
* type: integer
|
||||
* description: 关联订单ID
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* amount:
|
||||
* type: number
|
||||
* format: decimal
|
||||
* description: 支付金额
|
||||
* paid_amount:
|
||||
* type: number
|
||||
* format: decimal
|
||||
* description: 实际支付金额
|
||||
* payment_method:
|
||||
* type: string
|
||||
* enum: [wechat, alipay, balance]
|
||||
* description: 支付方式
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, paid, failed, refunded, cancelled]
|
||||
* description: 支付状态
|
||||
* transaction_id:
|
||||
* type: string
|
||||
* description: 第三方交易号
|
||||
* paid_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 支付时间
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
*
|
||||
* Refund:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 退款ID
|
||||
* refund_no:
|
||||
* type: string
|
||||
* description: 退款订单号
|
||||
* payment_id:
|
||||
* type: integer
|
||||
* description: 支付订单ID
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* refund_amount:
|
||||
* type: number
|
||||
* format: decimal
|
||||
* description: 退款金额
|
||||
* refund_reason:
|
||||
* type: string
|
||||
* description: 退款原因
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, approved, rejected, completed]
|
||||
* description: 退款状态
|
||||
* processed_by:
|
||||
* type: integer
|
||||
* description: 处理人ID
|
||||
* process_remark:
|
||||
* type: string
|
||||
* description: 处理备注
|
||||
* processed_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 处理时间
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
*
|
||||
* PaymentStatistics:
|
||||
* type: object
|
||||
* properties:
|
||||
* total_amount:
|
||||
* type: number
|
||||
* format: decimal
|
||||
* description: 总支付金额
|
||||
* total_count:
|
||||
* type: integer
|
||||
* description: 总支付笔数
|
||||
* success_amount:
|
||||
* type: number
|
||||
* format: decimal
|
||||
* description: 成功支付金额
|
||||
* success_count:
|
||||
* type: integer
|
||||
* description: 成功支付笔数
|
||||
* refund_amount:
|
||||
* type: number
|
||||
* format: decimal
|
||||
* description: 退款金额
|
||||
* refund_count:
|
||||
* type: integer
|
||||
* description: 退款笔数
|
||||
* method_stats:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* payment_method:
|
||||
* type: string
|
||||
* description: 支付方式
|
||||
* amount:
|
||||
* type: number
|
||||
* format: decimal
|
||||
* description: 金额
|
||||
* count:
|
||||
* type: integer
|
||||
* description: 笔数
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/payments:
|
||||
* post:
|
||||
* summary: 创建支付订单
|
||||
* tags: [支付管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - order_id
|
||||
* - amount
|
||||
* - payment_method
|
||||
* properties:
|
||||
* order_id:
|
||||
* type: integer
|
||||
* description: 订单ID
|
||||
* amount:
|
||||
* type: number
|
||||
* format: decimal
|
||||
* description: 支付金额
|
||||
* payment_method:
|
||||
* type: string
|
||||
* enum: [wechat, alipay, balance]
|
||||
* description: 支付方式
|
||||
* return_url:
|
||||
* type: string
|
||||
* description: 支付成功回调地址
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 支付订单创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Payment'
|
||||
* 400:
|
||||
* description: 参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.post('/',
|
||||
authenticateToken,
|
||||
[
|
||||
body('order_id').isInt({ min: 1 }).withMessage('订单ID必须是正整数'),
|
||||
body('amount').isFloat({ min: 0.01 }).withMessage('支付金额必须大于0'),
|
||||
body('payment_method').isIn(['wechat', 'alipay', 'balance']).withMessage('支付方式无效')
|
||||
],
|
||||
(req, res) => PaymentController.createPayment(req, res)
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/payments/{paymentId}:
|
||||
* get:
|
||||
* summary: 获取支付订单详情
|
||||
* tags: [支付管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: paymentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 支付订单ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Payment'
|
||||
* 403:
|
||||
* description: 无权访问
|
||||
* 404:
|
||||
* description: 支付订单不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/:paymentId',
|
||||
authenticateToken,
|
||||
param('paymentId').isInt({ min: 1 }).withMessage('支付订单ID必须是正整数'),
|
||||
PaymentController.getPayment
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/payments/query/{paymentNo}:
|
||||
* get:
|
||||
* summary: 查询支付状态
|
||||
* tags: [支付管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: paymentNo
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 支付订单号
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 查询成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* payment_no:
|
||||
* type: string
|
||||
* description: 支付订单号
|
||||
* status:
|
||||
* type: string
|
||||
* description: 支付状态
|
||||
* amount:
|
||||
* type: number
|
||||
* description: 支付金额
|
||||
* paid_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 支付时间
|
||||
* transaction_id:
|
||||
* type: string
|
||||
* description: 第三方交易号
|
||||
* 403:
|
||||
* description: 无权访问
|
||||
* 404:
|
||||
* description: 支付订单不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/query/:paymentNo',
|
||||
authenticateToken,
|
||||
PaymentController.queryPaymentStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/payments/callback/wechat:
|
||||
* post:
|
||||
* summary: 微信支付回调
|
||||
* tags: [支付管理]
|
||||
* description: 微信支付异步通知接口
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/xml:
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 处理成功
|
||||
* content:
|
||||
* application/xml:
|
||||
* schema:
|
||||
* type: string
|
||||
*/
|
||||
router.post('/callback/wechat', PaymentController.handleWechatCallback);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/payments/callback/alipay:
|
||||
* post:
|
||||
* summary: 支付宝支付回调
|
||||
* tags: [支付管理]
|
||||
* description: 支付宝异步通知接口
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/x-www-form-urlencoded:
|
||||
* schema:
|
||||
* type: object
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 处理成功
|
||||
* content:
|
||||
* text/plain:
|
||||
* schema:
|
||||
* type: string
|
||||
*/
|
||||
router.post('/callback/alipay', PaymentController.handleAlipayCallback);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/payments/{paymentId}/refund:
|
||||
* post:
|
||||
* summary: 申请退款
|
||||
* tags: [支付管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: paymentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 支付订单ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - refund_amount
|
||||
* - refund_reason
|
||||
* properties:
|
||||
* refund_amount:
|
||||
* type: number
|
||||
* format: decimal
|
||||
* description: 退款金额
|
||||
* refund_reason:
|
||||
* type: string
|
||||
* description: 退款原因
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 退款申请提交成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Refund'
|
||||
* 400:
|
||||
* description: 参数错误
|
||||
* 403:
|
||||
* description: 无权操作
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.post('/:paymentId/refund',
|
||||
authenticateToken,
|
||||
[
|
||||
param('paymentId').isInt({ min: 1 }).withMessage('支付订单ID必须是正整数'),
|
||||
body('refund_amount').isFloat({ min: 0.01 }).withMessage('退款金额必须大于0'),
|
||||
body('refund_reason').notEmpty().withMessage('退款原因不能为空')
|
||||
],
|
||||
PaymentController.createRefund
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/payments/refunds/{refundId}:
|
||||
* get:
|
||||
* summary: 获取退款详情
|
||||
* tags: [支付管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: refundId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 退款ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Refund'
|
||||
* 403:
|
||||
* description: 无权访问
|
||||
* 404:
|
||||
* description: 退款记录不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/refunds/:refundId',
|
||||
authenticateToken,
|
||||
param('refundId').isInt({ min: 1 }).withMessage('退款ID必须是正整数'),
|
||||
PaymentController.getRefund
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/payments/refunds/{refundId}/process:
|
||||
* put:
|
||||
* summary: 处理退款(管理员)
|
||||
* tags: [支付管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: refundId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 退款ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - status
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [approved, rejected, completed]
|
||||
* description: 退款状态
|
||||
* process_remark:
|
||||
* type: string
|
||||
* description: 处理备注
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 处理成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Refund'
|
||||
* 400:
|
||||
* description: 参数错误
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.put('/refunds/:refundId/process',
|
||||
authenticateToken,
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
param('refundId').isInt({ min: 1 }).withMessage('退款ID必须是正整数'),
|
||||
body('status').isIn(['approved', 'rejected', 'completed']).withMessage('退款状态无效')
|
||||
],
|
||||
PaymentController.processRefund
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/payments/statistics:
|
||||
* get:
|
||||
* summary: 获取支付统计信息(管理员)
|
||||
* tags: [支付管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* - in: query
|
||||
* name: payment_method
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [wechat, alipay, balance]
|
||||
* description: 支付方式
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* $ref: '#/components/schemas/PaymentStatistics'
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/statistics',
|
||||
authenticateToken,
|
||||
requireRole(['admin', 'super_admin']),
|
||||
PaymentController.getPaymentStatistics
|
||||
);
|
||||
|
||||
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,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const { body, query } = require('express-validator');
|
||||
const TravelController = require('../controllers/travel');
|
||||
const TravelController = require('../controllers/travel/index');
|
||||
const { authenticateUser: authenticate, requireRole: requireAdmin } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
@@ -75,6 +75,9 @@ const router = express.Router();
|
||||
*/
|
||||
router.get('/plans', authenticate, TravelController.getTravelPlans);
|
||||
|
||||
// 添加 /travels 路由,用于管理后台获取旅行计划列表
|
||||
router.get('/travels', authenticate, TravelController.getAllTravelPlans);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /travel/plans/{planId}:
|
||||
|
||||
434
backend/src/routes/travelRegistration.js
Normal file
434
backend/src/routes/travelRegistration.js
Normal file
@@ -0,0 +1,434 @@
|
||||
const express = require('express');
|
||||
const { body, query } = require('express-validator');
|
||||
const TravelRegistrationController = require('../controllers/travelRegistration');
|
||||
const { authenticateUser: authenticate } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: TravelRegistration
|
||||
* description: 旅行活动报名管理相关接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* TravelRegistration:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 报名记录ID
|
||||
* travel_plan_id:
|
||||
* type: integer
|
||||
* description: 旅行活动ID
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 报名用户ID
|
||||
* message:
|
||||
* type: string
|
||||
* description: 报名留言
|
||||
* emergency_contact:
|
||||
* type: string
|
||||
* description: 紧急联系人
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, approved, rejected, cancelled]
|
||||
* description: 报名状态
|
||||
* applied_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 报名时间
|
||||
* responded_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 审核时间
|
||||
* reject_reason:
|
||||
* type: string
|
||||
* description: 拒绝原因
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* real_name:
|
||||
* type: string
|
||||
* description: 真实姓名
|
||||
* avatar_url:
|
||||
* type: string
|
||||
* description: 头像URL
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /travel-registration/{travelId}/register:
|
||||
* post:
|
||||
* summary: 报名参加旅行活动
|
||||
* tags: [TravelRegistration]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: travelId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 旅行活动ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* type: string
|
||||
* description: 报名留言
|
||||
* example: 希望能和大家一起愉快旅行
|
||||
* emergencyContact:
|
||||
* type: string
|
||||
* description: 紧急联系人
|
||||
* example: 张三
|
||||
* emergencyPhone:
|
||||
* type: string
|
||||
* description: 紧急联系电话
|
||||
* example: 13800138000
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 报名成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* registration:
|
||||
* $ref: '#/components/schemas/TravelRegistration'
|
||||
* message:
|
||||
* type: string
|
||||
* example: 报名成功,等待审核
|
||||
* 400:
|
||||
* description: 请求参数错误或业务逻辑错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 旅行活动不存在
|
||||
*/
|
||||
router.post('/:travelId/register',
|
||||
authenticate,
|
||||
[
|
||||
body('emergencyContact').optional().isLength({ min: 1, max: 50 }).withMessage('紧急联系人长度应在1-50字符之间'),
|
||||
body('emergencyPhone').optional().isMobilePhone('zh-CN').withMessage('紧急联系电话格式不正确'),
|
||||
body('message').optional().isLength({ max: 500 }).withMessage('报名留言不能超过500字符')
|
||||
],
|
||||
TravelRegistrationController.registerForTravel
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /travel-registration/{registrationId}/cancel:
|
||||
* put:
|
||||
* summary: 取消报名
|
||||
* tags: [TravelRegistration]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: registrationId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 报名记录ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 取消成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* type: string
|
||||
* example: 取消报名成功
|
||||
* 400:
|
||||
* description: 请求参数错误或业务逻辑错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 报名记录不存在
|
||||
*/
|
||||
router.put('/:registrationId/cancel', authenticate, TravelRegistrationController.cancelRegistration);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /travel-registration/my-registrations:
|
||||
* get:
|
||||
* summary: 获取用户的报名记录
|
||||
* tags: [TravelRegistration]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: pageSize
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 50
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [pending, approved, rejected, cancelled]
|
||||
* description: 报名状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* registrations:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/TravelRegistration'
|
||||
* pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* pageSize:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* totalPages:
|
||||
* type: integer
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.get('/my-registrations', authenticate, TravelRegistrationController.getUserRegistrations);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /travel-registration/{travelId}/registrations:
|
||||
* get:
|
||||
* summary: 获取旅行活动的报名列表(活动发起者可查看)
|
||||
* tags: [TravelRegistration]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: travelId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 旅行活动ID
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: pageSize
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 50
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [pending, approved, rejected, cancelled]
|
||||
* description: 报名状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* registrations:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/TravelRegistration'
|
||||
* pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* pageSize:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* totalPages:
|
||||
* type: integer
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 旅行活动不存在
|
||||
*/
|
||||
router.get('/:travelId/registrations', authenticate, TravelRegistrationController.getTravelRegistrations);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /travel-registration/{registrationId}/review:
|
||||
* put:
|
||||
* summary: 审核报名申请(活动发起者操作)
|
||||
* tags: [TravelRegistration]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: registrationId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 报名记录ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - action
|
||||
* properties:
|
||||
* action:
|
||||
* type: string
|
||||
* enum: [approve, reject]
|
||||
* description: 审核操作
|
||||
* example: approve
|
||||
* rejectReason:
|
||||
* type: string
|
||||
* description: 拒绝原因(拒绝时必填)
|
||||
* example: 活动要求不符合
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 审核成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* registration:
|
||||
* $ref: '#/components/schemas/TravelRegistration'
|
||||
* message:
|
||||
* type: string
|
||||
* example: 审核通过
|
||||
* 400:
|
||||
* description: 请求参数错误或业务逻辑错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 报名记录不存在
|
||||
*/
|
||||
router.put('/:registrationId/review',
|
||||
authenticate,
|
||||
[
|
||||
body('action').isIn(['approve', 'reject']).withMessage('操作类型必须是approve或reject'),
|
||||
body('rejectReason').optional().isLength({ min: 1, max: 200 }).withMessage('拒绝原因长度应在1-200字符之间')
|
||||
],
|
||||
TravelRegistrationController.reviewRegistration
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /travel-registration/{travelId}/stats:
|
||||
* get:
|
||||
* summary: 获取报名统计信息
|
||||
* tags: [TravelRegistration]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: travelId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 旅行活动ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* stats:
|
||||
* type: object
|
||||
* properties:
|
||||
* total_applications:
|
||||
* type: integer
|
||||
* description: 总申请数
|
||||
* pending_count:
|
||||
* type: integer
|
||||
* description: 待审核数
|
||||
* approved_count:
|
||||
* type: integer
|
||||
* description: 已通过数
|
||||
* rejected_count:
|
||||
* type: integer
|
||||
* description: 已拒绝数
|
||||
* cancelled_count:
|
||||
* type: integer
|
||||
* description: 已取消数
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 旅行活动不存在
|
||||
*/
|
||||
router.get('/:travelId/stats', authenticate, TravelRegistrationController.getRegistrationStats);
|
||||
|
||||
module.exports = router;
|
||||
122
backend/src/routes/travels.js
Normal file
122
backend/src/routes/travels.js
Normal file
@@ -0,0 +1,122 @@
|
||||
const express = require('express');
|
||||
const { query } = require('express-validator');
|
||||
const TravelController = require('../controllers/travel/index');
|
||||
const { authenticateUser, requireRole } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Travel:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* title:
|
||||
* type: string
|
||||
* description:
|
||||
* type: string
|
||||
* destination:
|
||||
* type: string
|
||||
* start_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* end_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* max_participants:
|
||||
* type: integer
|
||||
* current_participants:
|
||||
* type: integer
|
||||
* price_per_person:
|
||||
* type: number
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [draft, published, archived, cancelled]
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /travels:
|
||||
* get:
|
||||
* summary: 获取旅行计划列表(管理后台)
|
||||
* tags: [Travel]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 20
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: keyword
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [draft, published, archived, cancelled]
|
||||
* description: 状态筛选
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取旅行计划列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* code:
|
||||
* type: integer
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* plans:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Travel'
|
||||
* pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* pageSize:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* totalPages:
|
||||
* type: integer
|
||||
*/
|
||||
router.get('/',
|
||||
authenticateUser,
|
||||
[
|
||||
query('page').optional().isInt({ min: 1 }).withMessage('页码必须是正整数'),
|
||||
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('每页数量必须在1-100之间'),
|
||||
query('keyword').optional().isString().withMessage('关键词必须是字符串'),
|
||||
query('status').optional().isIn(['draft', 'published', 'archived', 'cancelled']).withMessage('状态值无效')
|
||||
],
|
||||
TravelController.getAllTravelPlans
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -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;
|
||||
@@ -32,13 +32,20 @@ process.on('uncaughtException', (err) => {
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('========================================')
|
||||
console.error('❌ 未处理的Promise拒绝:')
|
||||
console.error(`🔹 消息: ${err.message}`)
|
||||
console.error(`🔹 堆栈: ${err.stack}`)
|
||||
console.error('🔹 Promise:', promise)
|
||||
console.error('🔹 原因:', reason)
|
||||
if (reason && reason.message) {
|
||||
console.error(`🔹 消息: ${reason.message}`)
|
||||
}
|
||||
if (reason && reason.stack) {
|
||||
console.error(`🔹 堆栈: ${reason.stack}`)
|
||||
}
|
||||
console.error('========================================')
|
||||
process.exit(1)
|
||||
// 不立即退出,让服务继续运行以便调试
|
||||
// process.exit(1)
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
@@ -58,10 +65,10 @@ const startServer = async () => {
|
||||
|
||||
console.log('✅ 数据库连接测试成功')
|
||||
console.log('📌 数据库连接池配置:', {
|
||||
host: pool.config.host,
|
||||
port: pool.config.port,
|
||||
database: pool.config.database,
|
||||
user: pool.config.user
|
||||
host: pool.pool.config.connectionConfig.host,
|
||||
port: pool.pool.config.connectionConfig.port,
|
||||
database: pool.pool.config.connectionConfig.database,
|
||||
user: pool.pool.config.connectionConfig.user
|
||||
})
|
||||
console.log('🔄 所有数据库连接已统一使用database.js配置')
|
||||
|
||||
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
|
||||
372
backend/src/services/animalClaim.js
Normal file
372
backend/src/services/animalClaim.js
Normal file
@@ -0,0 +1,372 @@
|
||||
const AnimalClaimModel = require('../models/AnimalClaim');
|
||||
const AnimalModel = require('../models/Animal');
|
||||
|
||||
class AnimalClaimService {
|
||||
/**
|
||||
* 申请认领动物
|
||||
* @param {Object} claimData - 认领申请数据
|
||||
* @returns {Object} 认领申请记录
|
||||
*/
|
||||
async createClaim(claimData) {
|
||||
try {
|
||||
const { animal_id, user_id, claim_reason, claim_duration, contact_info } = claimData;
|
||||
|
||||
// 检查动物是否存在且可认领
|
||||
const animal = await AnimalModel.findById(animal_id);
|
||||
if (!animal) {
|
||||
throw new Error('动物不存在');
|
||||
}
|
||||
|
||||
if (animal.status !== 'available') {
|
||||
throw new Error('该动物当前不可认领');
|
||||
}
|
||||
|
||||
// 检查用户是否已经认领过该动物
|
||||
const existingClaim = await AnimalClaimModel.findActiveClaimByUserAndAnimal(user_id, animal_id);
|
||||
if (existingClaim) {
|
||||
throw new Error('您已经认领过该动物,请勿重复申请');
|
||||
}
|
||||
|
||||
// 生成认领订单号
|
||||
const claimNo = this.generateClaimNo();
|
||||
|
||||
// 创建认领申请
|
||||
const claim = await AnimalClaimModel.create({
|
||||
claim_no: claimNo,
|
||||
animal_id,
|
||||
user_id,
|
||||
claim_reason: claim_reason || '喜欢这只动物',
|
||||
claim_duration: claim_duration || 12, // 默认12个月
|
||||
contact_info,
|
||||
status: 'pending',
|
||||
total_amount: animal.price * (claim_duration || 12)
|
||||
});
|
||||
|
||||
return this.sanitizeClaim(claim);
|
||||
} catch (error) {
|
||||
console.error('创建动物认领申请服务错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消认领申请
|
||||
* @param {number} claimId - 认领申请ID
|
||||
* @param {number} userId - 用户ID
|
||||
* @returns {Object} 更新后的认领申请
|
||||
*/
|
||||
async cancelClaim(claimId, userId) {
|
||||
try {
|
||||
// 获取认领申请
|
||||
const claim = await AnimalClaimModel.findById(claimId);
|
||||
if (!claim) {
|
||||
throw new Error('认领申请不存在');
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (claim.user_id !== userId) {
|
||||
throw new Error('无权操作此认领申请');
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if (!['pending', 'approved'].includes(claim.status)) {
|
||||
throw new Error('当前状态不允许取消');
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
const updatedClaim = await AnimalClaimModel.updateStatus(claimId, 'cancelled', {
|
||||
cancelled_at: new Date(),
|
||||
cancel_reason: '用户主动取消'
|
||||
});
|
||||
|
||||
// 如果动物状态是已认领,需要恢复为可认领
|
||||
if (claim.status === 'approved') {
|
||||
await AnimalModel.updateStatus(claim.animal_id, 'available');
|
||||
}
|
||||
|
||||
return this.sanitizeClaim(updatedClaim);
|
||||
} catch (error) {
|
||||
console.error('取消动物认领服务错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的认领申请列表
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Object} 分页结果
|
||||
*/
|
||||
async getUserClaims(userId, options = {}) {
|
||||
try {
|
||||
const result = await AnimalClaimModel.getUserClaims(userId, options);
|
||||
|
||||
return {
|
||||
data: result.data.map(claim => this.sanitizeClaim(claim)),
|
||||
pagination: result.pagination
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取用户认领申请服务错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动物的认领申请列表
|
||||
* @param {number} animalId - 动物ID
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Object} 分页结果
|
||||
*/
|
||||
async getAnimalClaims(animalId, options = {}) {
|
||||
try {
|
||||
const result = await AnimalClaimModel.getAnimalClaims(animalId, options);
|
||||
|
||||
return {
|
||||
data: result.data.map(claim => this.sanitizeClaim(claim)),
|
||||
pagination: result.pagination
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取动物认领申请服务错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有认领申请列表(管理员)
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Object} 分页结果
|
||||
*/
|
||||
async getAllClaims(options = {}) {
|
||||
try {
|
||||
const result = await AnimalClaimModel.getAllClaims(options);
|
||||
|
||||
return {
|
||||
data: result.data.map(claim => this.sanitizeClaim(claim)),
|
||||
pagination: result.pagination
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取所有认领申请服务错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核认领申请
|
||||
* @param {number} claimId - 认领申请ID
|
||||
* @param {string} status - 审核状态
|
||||
* @param {Object} reviewData - 审核数据
|
||||
* @returns {Object} 更新后的认领申请
|
||||
*/
|
||||
async reviewClaim(claimId, status, reviewData = {}) {
|
||||
try {
|
||||
const { reviewed_by, review_remark } = reviewData;
|
||||
|
||||
// 获取认领申请
|
||||
const claim = await AnimalClaimModel.findById(claimId);
|
||||
if (!claim) {
|
||||
throw new Error('认领申请不存在');
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if (claim.status !== 'pending') {
|
||||
throw new Error('只能审核待审核的申请');
|
||||
}
|
||||
|
||||
// 验证审核状态
|
||||
const validStatuses = ['approved', 'rejected'];
|
||||
if (!validStatuses.includes(status)) {
|
||||
throw new Error('无效的审核状态');
|
||||
}
|
||||
|
||||
// 更新认领申请状态
|
||||
const updateData = {
|
||||
reviewed_by,
|
||||
review_remark,
|
||||
reviewed_at: new Date()
|
||||
};
|
||||
|
||||
if (status === 'approved') {
|
||||
updateData.approved_at = new Date();
|
||||
updateData.start_date = new Date();
|
||||
|
||||
// 计算结束日期
|
||||
const endDate = new Date();
|
||||
endDate.setMonth(endDate.getMonth() + claim.claim_duration);
|
||||
updateData.end_date = endDate;
|
||||
|
||||
// 更新动物状态为已认领
|
||||
await AnimalModel.updateStatus(claim.animal_id, 'claimed');
|
||||
|
||||
// 增加动物认领次数
|
||||
await AnimalModel.incrementClaimCount(claim.animal_id);
|
||||
}
|
||||
|
||||
const updatedClaim = await AnimalClaimModel.updateStatus(claimId, status, updateData);
|
||||
|
||||
return this.sanitizeClaim(updatedClaim);
|
||||
} catch (error) {
|
||||
console.error('审核认领申请服务错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 续期认领
|
||||
* @param {number} claimId - 认领申请ID
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {Object} renewData - 续期数据
|
||||
* @returns {Object} 续期结果
|
||||
*/
|
||||
async renewClaim(claimId, userId, renewData) {
|
||||
try {
|
||||
const { duration, payment_method } = renewData;
|
||||
|
||||
// 获取认领申请
|
||||
const claim = await AnimalClaimModel.findById(claimId);
|
||||
if (!claim) {
|
||||
throw new Error('认领申请不存在');
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (claim.user_id !== userId) {
|
||||
throw new Error('无权操作此认领申请');
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if (claim.status !== 'approved') {
|
||||
throw new Error('只有已通过的认领申请才能续期');
|
||||
}
|
||||
|
||||
// 检查是否即将到期(提前30天可以续期)
|
||||
const now = new Date();
|
||||
const endDate = new Date(claim.end_date);
|
||||
const daysUntilExpiry = Math.ceil((endDate - now) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysUntilExpiry > 30) {
|
||||
throw new Error('距离到期还有超过30天,暂时无法续期');
|
||||
}
|
||||
|
||||
// 获取动物信息计算续期费用
|
||||
const animal = await AnimalModel.findById(claim.animal_id);
|
||||
const renewAmount = animal.price * duration;
|
||||
|
||||
// 创建续期记录
|
||||
const renewRecord = await AnimalClaimModel.createRenewal({
|
||||
claim_id: claimId,
|
||||
duration,
|
||||
amount: renewAmount,
|
||||
payment_method,
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
return {
|
||||
renewal: renewRecord,
|
||||
amount: renewAmount,
|
||||
message: '续期申请已提交,请完成支付'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('续期认领服务错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认领统计信息
|
||||
* @param {Object} filters - 筛选条件
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
async getClaimStatistics(filters = {}) {
|
||||
try {
|
||||
const statistics = await AnimalClaimModel.getClaimStatistics(filters);
|
||||
return statistics;
|
||||
} catch (error) {
|
||||
console.error('获取认领统计服务错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查认领权限
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {number} animalId - 动物ID
|
||||
* @returns {boolean} 是否有权限
|
||||
*/
|
||||
async checkClaimPermission(userId, animalId) {
|
||||
try {
|
||||
// 检查动物是否存在
|
||||
const animal = await AnimalModel.findById(animalId);
|
||||
if (!animal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查动物状态
|
||||
if (animal.status !== 'available') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查用户是否已有活跃的认领申请
|
||||
const existingClaim = await AnimalClaimModel.findActiveClaimByUserAndAnimal(userId, animalId);
|
||||
if (existingClaim) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('检查认领权限服务错误:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成认领订单号
|
||||
* @returns {string} 认领订单号
|
||||
*/
|
||||
generateClaimNo() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const timestamp = now.getTime().toString().slice(-6);
|
||||
|
||||
return `CLAIM${year}${month}${day}${timestamp}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理认领申请数据
|
||||
* @param {Object} claim - 认领申请数据
|
||||
* @returns {Object} 清理后的数据
|
||||
*/
|
||||
sanitizeClaim(claim) {
|
||||
if (!claim) return null;
|
||||
|
||||
return {
|
||||
id: claim.id,
|
||||
claim_no: claim.claim_no,
|
||||
animal_id: claim.animal_id,
|
||||
animal_name: claim.animal_name,
|
||||
animal_type: claim.animal_type,
|
||||
animal_image: claim.animal_image,
|
||||
user_id: claim.user_id,
|
||||
username: claim.username,
|
||||
user_phone: claim.user_phone,
|
||||
claim_reason: claim.claim_reason,
|
||||
claim_duration: claim.claim_duration,
|
||||
total_amount: parseFloat(claim.total_amount || 0),
|
||||
contact_info: claim.contact_info,
|
||||
status: claim.status,
|
||||
start_date: claim.start_date,
|
||||
end_date: claim.end_date,
|
||||
reviewed_by: claim.reviewed_by,
|
||||
reviewer_name: claim.reviewer_name,
|
||||
review_remark: claim.review_remark,
|
||||
reviewed_at: claim.reviewed_at,
|
||||
approved_at: claim.approved_at,
|
||||
cancelled_at: claim.cancelled_at,
|
||||
cancel_reason: claim.cancel_reason,
|
||||
created_at: claim.created_at,
|
||||
updated_at: claim.updated_at
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AnimalClaimService();
|
||||
@@ -52,7 +52,7 @@ class OrderService {
|
||||
SELECT
|
||||
o.*,
|
||||
a.name as animal_name,
|
||||
a.species as animal_species,
|
||||
a.type as animal_species,
|
||||
a.price as animal_price,
|
||||
u.username as user_name,
|
||||
m.business_name as merchant_name
|
||||
@@ -60,7 +60,7 @@ class OrderService {
|
||||
LEFT JOIN animals a ON o.animal_id = a.id
|
||||
LEFT JOIN users u ON o.user_id = u.id
|
||||
LEFT JOIN merchants m ON o.merchant_id = m.id
|
||||
WHERE o.id = ? AND o.is_deleted = 0
|
||||
WHERE o.id = ?
|
||||
`;
|
||||
|
||||
const [order] = await database.query(query, [orderId]);
|
||||
@@ -90,20 +90,25 @@ class OrderService {
|
||||
let query = `
|
||||
SELECT
|
||||
o.*,
|
||||
a.name as animal_name,
|
||||
a.species as animal_species,
|
||||
a.price as animal_price,
|
||||
m.business_name as merchant_name
|
||||
CASE
|
||||
WHEN o.type = 'animal_claim' THEN a.name
|
||||
WHEN o.type = 'travel' THEN t.title
|
||||
ELSE o.title
|
||||
END as item_name,
|
||||
CASE
|
||||
WHEN o.type = 'animal_claim' THEN a.type
|
||||
ELSE o.type
|
||||
END as item_type
|
||||
FROM orders o
|
||||
LEFT JOIN animals a ON o.animal_id = a.id
|
||||
LEFT JOIN merchants m ON o.merchant_id = m.id
|
||||
WHERE o.user_id = ? AND o.is_deleted = 0
|
||||
LEFT JOIN animals a ON o.type = 'animal_claim' AND o.related_id = a.id
|
||||
LEFT JOIN travel_plans t ON o.type = 'travel' AND o.related_id = t.id
|
||||
WHERE o.user_id = ?
|
||||
`;
|
||||
|
||||
let countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM orders o
|
||||
WHERE o.user_id = ? AND o.is_deleted = 0
|
||||
WHERE o.user_id = ?
|
||||
`;
|
||||
|
||||
const params = [userId];
|
||||
@@ -116,10 +121,10 @@ class OrderService {
|
||||
countParams.push(status);
|
||||
}
|
||||
|
||||
query += ' ORDER BY o.created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(pageSize, offset);
|
||||
query += ' ORDER BY o.ordered_at DESC LIMIT ?, ?';
|
||||
params.push(parseInt(offset), parseInt(pageSize));
|
||||
|
||||
const [orders] = await database.query(query, params);
|
||||
const orders = await database.query(query, params);
|
||||
const [totalResult] = await database.query(countQuery, countParams);
|
||||
|
||||
return {
|
||||
@@ -152,19 +157,19 @@ class OrderService {
|
||||
SELECT
|
||||
o.*,
|
||||
a.name as animal_name,
|
||||
a.species as animal_species,
|
||||
a.type as animal_species,
|
||||
a.price as animal_price,
|
||||
u.username as user_name
|
||||
FROM orders o
|
||||
LEFT JOIN animals a ON o.animal_id = a.id
|
||||
LEFT JOIN users u ON o.user_id = u.id
|
||||
WHERE o.merchant_id = ? AND o.is_deleted = 0
|
||||
WHERE o.merchant_id = ?
|
||||
`;
|
||||
|
||||
let countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM orders o
|
||||
WHERE o.merchant_id = ? AND o.is_deleted = 0
|
||||
WHERE o.merchant_id = ?
|
||||
`;
|
||||
|
||||
const params = [merchantId];
|
||||
@@ -215,7 +220,7 @@ class OrderService {
|
||||
const query = `
|
||||
UPDATE orders
|
||||
SET status = ?, updated_by = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND is_deleted = 0
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
const result = await database.query(query, [status, userId, orderId]);
|
||||
@@ -240,12 +245,11 @@ class OrderService {
|
||||
async deleteOrder(orderId, userId) {
|
||||
try {
|
||||
const query = `
|
||||
UPDATE orders
|
||||
SET is_deleted = 1, deleted_by = ?, deleted_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND is_deleted = 0
|
||||
DELETE FROM orders
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
const result = await database.query(query, [userId, orderId]);
|
||||
const result = await database.query(query, [orderId]);
|
||||
|
||||
return result.affectedRows > 0;
|
||||
} catch (error) {
|
||||
@@ -254,6 +258,53 @@ class OrderService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付订单
|
||||
* @param {number} orderId - 订单ID
|
||||
* @param {Object} paymentData - 支付数据
|
||||
* @returns {Object} 支付结果
|
||||
*/
|
||||
async payOrder(orderId, paymentData) {
|
||||
try {
|
||||
// 获取订单信息
|
||||
const order = await this.getOrderById(orderId);
|
||||
if (!order) {
|
||||
throw new Error('订单不存在');
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if (order.status !== 'pending') {
|
||||
throw new Error('订单状态不允许支付');
|
||||
}
|
||||
|
||||
// 检查订单金额
|
||||
if (order.total_amount !== paymentData.amount) {
|
||||
throw new Error('支付金额与订单金额不符');
|
||||
}
|
||||
|
||||
// 创建支付订单
|
||||
const PaymentService = require('../payment');
|
||||
const payment = await PaymentService.createPayment({
|
||||
order_id: orderId,
|
||||
user_id: order.user_id,
|
||||
amount: order.total_amount,
|
||||
payment_method: paymentData.payment_method,
|
||||
return_url: paymentData.return_url
|
||||
});
|
||||
|
||||
// 更新订单状态为支付中
|
||||
await this.updateOrderStatus(orderId, 'paying');
|
||||
|
||||
return {
|
||||
payment,
|
||||
order: await this.getOrderById(orderId)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('支付订单服务错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计信息
|
||||
* @param {number} merchantId - 商家ID
|
||||
@@ -271,7 +322,7 @@ class OrderService {
|
||||
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_orders,
|
||||
SUM(total_amount) as total_revenue
|
||||
FROM orders
|
||||
WHERE merchant_id = ? AND is_deleted = 0
|
||||
WHERE merchant_id = ?
|
||||
`;
|
||||
|
||||
const [stats] = await database.query(query, [merchantId]);
|
||||
@@ -294,29 +345,36 @@ class OrderService {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
status,
|
||||
merchantId,
|
||||
userId
|
||||
userId,
|
||||
order_no,
|
||||
type
|
||||
} = filters;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
o.*,
|
||||
a.name as animal_name,
|
||||
a.species as animal_species,
|
||||
u.username as user_name,
|
||||
m.business_name as merchant_name
|
||||
CASE
|
||||
WHEN o.type = 'animal_claim' THEN a.name
|
||||
WHEN o.type = 'travel' THEN t.title
|
||||
ELSE o.title
|
||||
END as item_name,
|
||||
CASE
|
||||
WHEN o.type = 'animal_claim' THEN a.type
|
||||
ELSE o.type
|
||||
END as item_type
|
||||
FROM orders o
|
||||
LEFT JOIN animals a ON o.animal_id = a.id
|
||||
LEFT JOIN users u ON o.user_id = u.id
|
||||
LEFT JOIN merchants m ON o.merchant_id = m.id
|
||||
WHERE o.is_deleted = 0
|
||||
LEFT JOIN animals a ON o.type = 'animal_claim' AND o.related_id = a.id
|
||||
LEFT JOIN travel_plans t ON o.type = 'travel' AND o.related_id = t.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
let countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM orders o
|
||||
WHERE o.is_deleted = 0
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
@@ -329,13 +387,6 @@ class OrderService {
|
||||
countParams.push(status);
|
||||
}
|
||||
|
||||
if (merchantId) {
|
||||
query += ' AND o.merchant_id = ?';
|
||||
countQuery += ' AND o.merchant_id = ?';
|
||||
params.push(merchantId);
|
||||
countParams.push(merchantId);
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
query += ' AND o.user_id = ?';
|
||||
countQuery += ' AND o.user_id = ?';
|
||||
@@ -343,10 +394,24 @@ class OrderService {
|
||||
countParams.push(userId);
|
||||
}
|
||||
|
||||
query += ' ORDER BY o.created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(pageSize, offset);
|
||||
if (order_no && order_no.trim() !== '') {
|
||||
query += ' AND o.order_no = ?';
|
||||
countQuery += ' AND o.order_no = ?';
|
||||
params.push(order_no);
|
||||
countParams.push(order_no);
|
||||
}
|
||||
|
||||
const [orders] = await database.query(query, params);
|
||||
if (type) {
|
||||
query += ' AND o.type = ?';
|
||||
countQuery += ' AND o.type = ?';
|
||||
params.push(type);
|
||||
countParams.push(type);
|
||||
}
|
||||
|
||||
query += ' ORDER BY o.ordered_at DESC LIMIT ?, ?';
|
||||
params.push(parseInt(offset), parseInt(pageSize));
|
||||
|
||||
const orders = await database.query(query, params);
|
||||
const [totalResult] = await database.query(countQuery, countParams);
|
||||
|
||||
return {
|
||||
@@ -363,6 +428,35 @@ class OrderService {
|
||||
throw new Error('获取所有订单失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计信息(管理员)
|
||||
* @returns {Promise<Object>} 统计信息
|
||||
*/
|
||||
async getOrderStatistics() {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
COUNT(*) as total_orders,
|
||||
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_orders,
|
||||
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid_orders,
|
||||
SUM(CASE WHEN status = 'processing' THEN 1 ELSE 0 END) as processing_orders,
|
||||
SUM(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END) as shipped_orders,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_orders,
|
||||
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_orders,
|
||||
SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) as refunded_orders,
|
||||
SUM(final_amount) as total_revenue
|
||||
FROM orders
|
||||
`;
|
||||
|
||||
const [stats] = await database.query(query);
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('获取订单统计失败:', error);
|
||||
throw new Error('获取订单统计失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new OrderService();
|
||||
529
backend/src/services/payment.js
Normal file
529
backend/src/services/payment.js
Normal file
@@ -0,0 +1,529 @@
|
||||
const database = require('../config/database');
|
||||
const crypto = require('crypto');
|
||||
|
||||
class PaymentService {
|
||||
/**
|
||||
* 创建支付订单
|
||||
* @param {Object} paymentData - 支付数据
|
||||
* @returns {Promise<Object>} 支付订单信息
|
||||
*/
|
||||
async createPayment(paymentData) {
|
||||
try {
|
||||
const {
|
||||
order_id,
|
||||
user_id,
|
||||
amount,
|
||||
payment_method,
|
||||
payment_channel = 'wechat'
|
||||
} = paymentData;
|
||||
|
||||
// 生成支付订单号
|
||||
const payment_no = this.generatePaymentNo();
|
||||
|
||||
const query = `
|
||||
INSERT INTO payments (
|
||||
payment_no, order_id, user_id, amount,
|
||||
payment_method, payment_channel, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, 'pending')
|
||||
`;
|
||||
|
||||
const params = [
|
||||
payment_no, order_id, user_id, amount,
|
||||
payment_method, payment_channel
|
||||
];
|
||||
|
||||
const result = await database.query(query, params);
|
||||
|
||||
// 获取创建的支付订单
|
||||
const payment = await this.getPaymentById(result.insertId);
|
||||
|
||||
// 根据支付方式生成支付参数
|
||||
const paymentParams = await this.generatePaymentParams(payment);
|
||||
|
||||
return {
|
||||
...payment,
|
||||
...paymentParams
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('创建支付订单失败:', error);
|
||||
throw new Error('创建支付订单失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付订单详情
|
||||
* @param {number} paymentId - 支付ID
|
||||
* @returns {Promise<Object>} 支付订单信息
|
||||
*/
|
||||
async getPaymentById(paymentId) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
p.*,
|
||||
o.order_number,
|
||||
o.total_amount as order_amount,
|
||||
u.username
|
||||
FROM payments p
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE p.id = ? AND p.is_deleted = 0
|
||||
`;
|
||||
|
||||
const [payments] = await database.query(query, [paymentId]);
|
||||
|
||||
if (payments.length === 0) {
|
||||
throw new Error('支付订单不存在');
|
||||
}
|
||||
|
||||
return payments[0];
|
||||
} catch (error) {
|
||||
console.error('获取支付订单失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据支付号获取支付订单
|
||||
* @param {string} paymentNo - 支付订单号
|
||||
* @returns {Promise<Object>} 支付订单信息
|
||||
*/
|
||||
async getPaymentByNo(paymentNo) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
p.*,
|
||||
o.order_number,
|
||||
o.total_amount as order_amount,
|
||||
u.username
|
||||
FROM payments p
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE p.payment_no = ? AND p.is_deleted = 0
|
||||
`;
|
||||
|
||||
const [payments] = await database.query(query, [paymentNo]);
|
||||
|
||||
if (payments.length === 0) {
|
||||
throw new Error('支付订单不存在');
|
||||
}
|
||||
|
||||
return payments[0];
|
||||
} catch (error) {
|
||||
console.error('获取支付订单失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付状态
|
||||
* @param {string} paymentNo - 支付订单号
|
||||
* @param {string} status - 支付状态
|
||||
* @param {Object} extraData - 额外数据
|
||||
* @returns {Promise<Object>} 更新后的支付订单
|
||||
*/
|
||||
async updatePaymentStatus(paymentNo, status, extraData = {}) {
|
||||
try {
|
||||
const {
|
||||
transaction_id,
|
||||
paid_at,
|
||||
failure_reason
|
||||
} = extraData;
|
||||
|
||||
let query = `
|
||||
UPDATE payments
|
||||
SET status = ?, updated_at = CURRENT_TIMESTAMP
|
||||
`;
|
||||
const params = [status];
|
||||
|
||||
if (transaction_id) {
|
||||
query += ', transaction_id = ?';
|
||||
params.push(transaction_id);
|
||||
}
|
||||
|
||||
if (paid_at) {
|
||||
query += ', paid_at = ?';
|
||||
params.push(paid_at);
|
||||
}
|
||||
|
||||
if (failure_reason) {
|
||||
query += ', failure_reason = ?';
|
||||
params.push(failure_reason);
|
||||
}
|
||||
|
||||
query += ' WHERE payment_no = ? AND is_deleted = 0';
|
||||
params.push(paymentNo);
|
||||
|
||||
const result = await database.query(query, params);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new Error('支付订单不存在');
|
||||
}
|
||||
|
||||
return await this.getPaymentByNo(paymentNo);
|
||||
} catch (error) {
|
||||
console.error('更新支付状态失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付回调
|
||||
* @param {Object} callbackData - 回调数据
|
||||
* @returns {Promise<Object>} 处理结果
|
||||
*/
|
||||
async handlePaymentCallback(callbackData) {
|
||||
try {
|
||||
const {
|
||||
payment_no,
|
||||
transaction_id,
|
||||
status,
|
||||
paid_amount,
|
||||
paid_at
|
||||
} = callbackData;
|
||||
|
||||
// 获取支付订单
|
||||
const payment = await this.getPaymentByNo(payment_no);
|
||||
|
||||
// 验证金额
|
||||
if (status === 'paid' && parseFloat(paid_amount) !== parseFloat(payment.amount)) {
|
||||
throw new Error('支付金额不匹配');
|
||||
}
|
||||
|
||||
// 更新支付状态
|
||||
const updatedPayment = await this.updatePaymentStatus(payment_no, status, {
|
||||
transaction_id,
|
||||
paid_at: paid_at || new Date()
|
||||
});
|
||||
|
||||
// 如果支付成功,更新订单状态
|
||||
if (status === 'paid') {
|
||||
await this.updateOrderAfterPayment(payment.order_id);
|
||||
}
|
||||
|
||||
return updatedPayment;
|
||||
} catch (error) {
|
||||
console.error('处理支付回调失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付成功后更新订单状态
|
||||
* @param {number} orderId - 订单ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async updateOrderAfterPayment(orderId) {
|
||||
try {
|
||||
const query = `
|
||||
UPDATE orders
|
||||
SET
|
||||
payment_status = 'paid',
|
||||
order_status = 'confirmed',
|
||||
paid_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND is_deleted = 0
|
||||
`;
|
||||
|
||||
await database.query(query, [orderId]);
|
||||
} catch (error) {
|
||||
console.error('更新订单支付状态失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请退款
|
||||
* @param {Object} refundData - 退款数据
|
||||
* @returns {Promise<Object>} 退款申请结果
|
||||
*/
|
||||
async createRefund(refundData) {
|
||||
try {
|
||||
const {
|
||||
payment_id,
|
||||
refund_amount,
|
||||
refund_reason,
|
||||
user_id
|
||||
} = refundData;
|
||||
|
||||
// 获取支付订单
|
||||
const payment = await this.getPaymentById(payment_id);
|
||||
|
||||
// 验证退款金额
|
||||
if (parseFloat(refund_amount) > parseFloat(payment.amount)) {
|
||||
throw new Error('退款金额不能超过支付金额');
|
||||
}
|
||||
|
||||
// 生成退款订单号
|
||||
const refund_no = this.generateRefundNo();
|
||||
|
||||
const query = `
|
||||
INSERT INTO refunds (
|
||||
refund_no, payment_id, order_id, user_id,
|
||||
refund_amount, refund_reason, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, 'pending')
|
||||
`;
|
||||
|
||||
const params = [
|
||||
refund_no, payment_id, payment.order_id, user_id,
|
||||
refund_amount, refund_reason
|
||||
];
|
||||
|
||||
const result = await database.query(query, params);
|
||||
|
||||
return await this.getRefundById(result.insertId);
|
||||
} catch (error) {
|
||||
console.error('创建退款申请失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取退款详情
|
||||
* @param {number} refundId - 退款ID
|
||||
* @returns {Promise<Object>} 退款信息
|
||||
*/
|
||||
async getRefundById(refundId) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
r.*,
|
||||
p.payment_no,
|
||||
p.amount as payment_amount,
|
||||
o.order_number,
|
||||
u.username
|
||||
FROM refunds r
|
||||
LEFT JOIN payments p ON r.payment_id = p.id
|
||||
LEFT JOIN orders o ON r.order_id = o.id
|
||||
LEFT JOIN users u ON r.user_id = u.id
|
||||
WHERE r.id = ? AND r.is_deleted = 0
|
||||
`;
|
||||
|
||||
const [refunds] = await database.query(query, [refundId]);
|
||||
|
||||
if (refunds.length === 0) {
|
||||
throw new Error('退款记录不存在');
|
||||
}
|
||||
|
||||
return refunds[0];
|
||||
} catch (error) {
|
||||
console.error('获取退款详情失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退款
|
||||
* @param {number} refundId - 退款ID
|
||||
* @param {string} status - 退款状态
|
||||
* @param {Object} extraData - 额外数据
|
||||
* @returns {Promise<Object>} 处理结果
|
||||
*/
|
||||
async processRefund(refundId, status, extraData = {}) {
|
||||
try {
|
||||
const {
|
||||
refund_transaction_id,
|
||||
processed_by,
|
||||
process_remark
|
||||
} = extraData;
|
||||
|
||||
let query = `
|
||||
UPDATE refunds
|
||||
SET
|
||||
status = ?,
|
||||
processed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`;
|
||||
const params = [status];
|
||||
|
||||
if (refund_transaction_id) {
|
||||
query += ', refund_transaction_id = ?';
|
||||
params.push(refund_transaction_id);
|
||||
}
|
||||
|
||||
if (processed_by) {
|
||||
query += ', processed_by = ?';
|
||||
params.push(processed_by);
|
||||
}
|
||||
|
||||
if (process_remark) {
|
||||
query += ', process_remark = ?';
|
||||
params.push(process_remark);
|
||||
}
|
||||
|
||||
query += ' WHERE id = ? AND is_deleted = 0';
|
||||
params.push(refundId);
|
||||
|
||||
const result = await database.query(query, params);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new Error('退款记录不存在');
|
||||
}
|
||||
|
||||
// 如果退款成功,更新支付和订单状态
|
||||
if (status === 'completed') {
|
||||
const refund = await this.getRefundById(refundId);
|
||||
await this.updatePaymentStatus(refund.payment_no, 'refunded');
|
||||
await this.updateOrderAfterRefund(refund.order_id);
|
||||
}
|
||||
|
||||
return await this.getRefundById(refundId);
|
||||
} catch (error) {
|
||||
console.error('处理退款失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款成功后更新订单状态
|
||||
* @param {number} orderId - 订单ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async updateOrderAfterRefund(orderId) {
|
||||
try {
|
||||
const query = `
|
||||
UPDATE orders
|
||||
SET
|
||||
payment_status = 'refunded',
|
||||
order_status = 'cancelled',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND is_deleted = 0
|
||||
`;
|
||||
|
||||
await database.query(query, [orderId]);
|
||||
} catch (error) {
|
||||
console.error('更新订单退款状态失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付订单号
|
||||
* @returns {string} 支付订单号
|
||||
*/
|
||||
generatePaymentNo() {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||||
return `PAY${timestamp}${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成退款订单号
|
||||
* @returns {string} 退款订单号
|
||||
*/
|
||||
generateRefundNo() {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||||
return `REF${timestamp}${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付参数(模拟)
|
||||
* @param {Object} payment - 支付订单
|
||||
* @returns {Promise<Object>} 支付参数
|
||||
*/
|
||||
async generatePaymentParams(payment) {
|
||||
try {
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const nonceStr = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
// 模拟微信支付参数
|
||||
if (payment.payment_channel === 'wechat') {
|
||||
return {
|
||||
timeStamp: timestamp,
|
||||
nonceStr: nonceStr,
|
||||
package: `prepay_id=wx${timestamp}${nonceStr}`,
|
||||
signType: 'MD5',
|
||||
paySign: this.generateSign({
|
||||
timeStamp: timestamp,
|
||||
nonceStr: nonceStr,
|
||||
package: `prepay_id=wx${timestamp}${nonceStr}`,
|
||||
signType: 'MD5'
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// 模拟支付宝参数
|
||||
if (payment.payment_channel === 'alipay') {
|
||||
return {
|
||||
orderString: `app_id=2021000000000000&method=alipay.trade.app.pay&charset=utf-8&sign_type=RSA2×tamp=${timestamp}&version=1.0¬ify_url=https://api.jiebanke.com/payment/alipay/notify&biz_content={"out_trade_no":"${payment.payment_no}","total_amount":"${payment.amount}","subject":"订单支付","product_code":"QUICK_MSECURITY_PAY"}`
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
console.error('生成支付参数失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名(模拟)
|
||||
* @param {Object} params - 参数
|
||||
* @returns {string} 签名
|
||||
*/
|
||||
generateSign(params) {
|
||||
const sortedParams = Object.keys(params)
|
||||
.sort()
|
||||
.map(key => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
|
||||
return crypto
|
||||
.createHash('md5')
|
||||
.update(sortedParams + '&key=your_secret_key')
|
||||
.digest('hex')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付统计信息
|
||||
* @param {Object} filters - 筛选条件
|
||||
* @returns {Promise<Object>} 统计信息
|
||||
*/
|
||||
async getPaymentStatistics(filters = {}) {
|
||||
try {
|
||||
const { start_date, end_date, payment_method } = filters;
|
||||
|
||||
let whereClause = 'WHERE p.is_deleted = 0';
|
||||
const params = [];
|
||||
|
||||
if (start_date) {
|
||||
whereClause += ' AND p.created_at >= ?';
|
||||
params.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereClause += ' AND p.created_at <= ?';
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
if (payment_method) {
|
||||
whereClause += ' AND p.payment_method = ?';
|
||||
params.push(payment_method);
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
COUNT(*) as total_payments,
|
||||
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as successful_payments,
|
||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_payments,
|
||||
SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) as refunded_payments,
|
||||
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) as total_amount,
|
||||
AVG(CASE WHEN status = 'paid' THEN amount ELSE NULL END) as average_amount,
|
||||
payment_method,
|
||||
payment_channel
|
||||
FROM payments p
|
||||
${whereClause}
|
||||
GROUP BY payment_method, payment_channel
|
||||
`;
|
||||
|
||||
const [statistics] = await database.query(query, params);
|
||||
|
||||
return statistics;
|
||||
} catch (error) {
|
||||
console.error('获取支付统计失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PaymentService();
|
||||
@@ -5,37 +5,48 @@ class TravelService {
|
||||
// 获取旅行计划列表
|
||||
static async getTravelPlans(searchParams) {
|
||||
try {
|
||||
const { userId, page = 1, pageSize = 10, status } = searchParams;
|
||||
const offset = (page - 1) * pageSize;
|
||||
const { userId, page = 1, pageSize = 10, status, keyword } = searchParams;
|
||||
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);
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
whereClause += ' AND (tp.title LIKE ? OR tp.destination LIKE ? OR tp.description LIKE ?)';
|
||||
const keywordParam = `%${keyword}%`;
|
||||
params.push(keywordParam, keywordParam, keywordParam);
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
const countSql = `SELECT COUNT(*) as total FROM (${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 +68,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 +87,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 +145,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 +155,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 +175,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 +192,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 +214,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]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user