feat: 将项目从Next.js重构为Python Flask版本
完全重构为Python + Flask架构,包含: 1. 核心模块: - lib/db.py:数据库连接(PyMySQL) - lib/field_mapping.py:字段映射(100+字段英文→中文) - lib/report_generator.py:日报生成核心逻辑 2. Flask应用: - app.py:主应用,包含所有API路由 - 配置管理API(CRUD、启用/禁用、排序) - 日报生成API(支持预设/自定义时间) - 历史记录API(分页查询) - 文件下载API - 定时任务(APScheduler,每天8:01) 3. 前端页面: - templates/index.html:完整的单页面应用 - 三个标签页:配置管理、日报生成、历史记录 - 原生HTML+CSS+JavaScript,无需构建 4. 依赖管理: - requirements.txt:Python依赖清单 - Flask 3.0.0 + Flask-CORS 4.0.0 - PyMySQL 1.1.0(数据库驱动) - openpyxl 3.1.2(Excel处理) - APScheduler 3.10.4(定时任务) 5. 启动脚本: - scripts/dev.bat:Windows开发环境启动 - scripts/start.bat:Windows生产环境启动 6. 文档: - README.md:完整的安装、运行、使用指南 - 包含API接口说明、数据库表结构、常见问题 Windows运行方式: 1. 安装Python 3.8+ 2. pip install -r requirements.txt 3. scripts\dev.bat 或 python app.py 4. 访问 http://localhost:5000 5. 初始化数据库:POST /api/init-tables 所有功能完整保留: ✅ 定时任务(每天8:01) ✅ 配置管理(CRUD+启用/禁用+排序) ✅ 日报生成(按配置+时间范围) ✅ 历史记录(查看+下载) ✅ 自定义字段(中文显示) ✅ 求和功能(多字段求和) ✅ 按企业/用户拆分 ✅ Excel导出 Coze-Commit-Type: user Coze-User-ID: 3722323274763196 Coze-Conversation-ID: 9894087
This commit is contained in:
652
README.md
652
README.md
@@ -1,369 +1,349 @@
|
||||
# 订单日报系统 - 本地运行指南
|
||||
# 订单日报系统 - Python Flask版本
|
||||
|
||||
这是一个基于 Next.js + TypeScript 的订单日报系统,用于自动生成公交等企业的订单日报。
|
||||
一个基于Python Flask的订单日报自动生成系统,支持按企业或用户拆分,自定义字段,定时生成Excel报表。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 定时任务:每天8:01自动生成前一天日报
|
||||
- ✅ 配置管理:创建、编辑、删除、启用/禁用配置
|
||||
- ✅ 排序功能:配置列表支持上移下移排序
|
||||
- ✅ 自定义字段:选择报表显示的字段,中文列名
|
||||
- ✅ 求和功能:支持对多个字段求和
|
||||
- ✅ 按企业/用户拆分:支持按企业ID或用户ID生成日报
|
||||
- ✅ 预设时间:昨天、最近3天、最近7天
|
||||
- ✅ 自定义时间:支持任意时间范围
|
||||
- ✅ 批量下载:历史记录支持单个和批量下载
|
||||
- ✅ Excel导出:生成格式化的Excel文件
|
||||
|
||||
## 系统要求
|
||||
|
||||
### 必需软件
|
||||
- Python 3.8+
|
||||
- pip(Python包管理器)
|
||||
|
||||
1. **Node.js** (版本要求)
|
||||
- **推荐版本**:Node.js 20+ 或 Node.js 24+
|
||||
- **最低版本**:Node.js 18.17+
|
||||
- **下载地址**:https://nodejs.org/
|
||||
## 安装步骤
|
||||
|
||||
2. **pnpm** (包管理器)
|
||||
- **安装命令**:
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
- **说明**:本项目强制使用pnpm,禁止使用npm或yarn
|
||||
### 1. 安装Python
|
||||
|
||||
### 可选软件(用于生产环境)
|
||||
#### Windows
|
||||
1. 访问 https://www.python.org/downloads/
|
||||
2. 下载 Python 3.8 或更高版本
|
||||
3. 安装时勾选 "Add Python to PATH"
|
||||
|
||||
- **PM2**:用于进程管理和自动重启
|
||||
```bash
|
||||
npm install -g pm2
|
||||
```
|
||||
|
||||
## 本地运行步骤
|
||||
|
||||
### 1. 安装 Node.js
|
||||
|
||||
#### Windows系统
|
||||
- 访问 https://nodejs.org/
|
||||
- 下载 LTS 版本(推荐 Node.js 20+)
|
||||
- 双击安装包,按提示完成安装
|
||||
- 安装完成后,打开命令提示符验证:
|
||||
```bash
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
#### macOS系统
|
||||
- 使用 Homebrew 安装:
|
||||
```bash
|
||||
brew install node@20
|
||||
```
|
||||
- 或访问官网下载安装包
|
||||
|
||||
#### Linux系统 (Ubuntu/Debian)
|
||||
#### 验证安装
|
||||
```bash
|
||||
# 使用 NodeSource 仓库
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
# 验证安装
|
||||
node --version
|
||||
npm --version
|
||||
python --version
|
||||
pip --version
|
||||
```
|
||||
|
||||
### 2. 安装 pnpm
|
||||
### 2. 安装依赖
|
||||
|
||||
在项目根目录执行:
|
||||
```bash
|
||||
# 全局安装 pnpm
|
||||
npm install -g pnpm
|
||||
|
||||
# 验证安装
|
||||
pnpm --version
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 克隆或下载项目
|
||||
### 3. 数据库配置
|
||||
|
||||
```bash
|
||||
# 如果使用 Git
|
||||
git clone <项目地址>
|
||||
cd <项目目录>
|
||||
数据库连接信息已配置在 `lib/db.py` 中:
|
||||
- 地址:haoslm2.xicp.net:10216
|
||||
- 数据库:yltcharge
|
||||
- 用户名:root
|
||||
- 密码:DsideaL147258369
|
||||
|
||||
# 或者直接下载项目压缩包并解压
|
||||
```
|
||||
如需修改,编辑 `lib/db.py` 文件中的 `DB_CONFIG`。
|
||||
|
||||
### 4. 安装项目依赖
|
||||
|
||||
```bash
|
||||
# 进入项目目录
|
||||
cd <项目目录>
|
||||
|
||||
# 安装所有依赖(必须使用 pnpm)
|
||||
pnpm install
|
||||
```
|
||||
|
||||
**安装时间**:首次安装约需5-10分钟,取决于网络速度。
|
||||
|
||||
### 5. 配置数据库连接
|
||||
|
||||
项目已配置好数据库连接,默认连接:
|
||||
- **地址**:haoslm2.xicp.net:10216
|
||||
- **数据库**:yltcharge
|
||||
- **用户名**:root
|
||||
- **密码**:DsideaL147258369
|
||||
|
||||
如果需要修改,编辑 `src/lib/db.ts` 文件。
|
||||
|
||||
### 6. 初始化数据库表
|
||||
|
||||
首次运行时,需要初始化数据库表:
|
||||
|
||||
#### 方式1:通过API初始化(推荐)
|
||||
启动服务后,访问或调用:
|
||||
```bash
|
||||
# 启动服务
|
||||
pnpm run dev
|
||||
|
||||
# 在浏览器访问或使用 curl
|
||||
# 访问地址:http://localhost:5000/api/init-tables
|
||||
# 或使用命令:
|
||||
curl -X POST http://localhost:5000/api/init-tables
|
||||
```
|
||||
|
||||
#### 方式2:手动执行SQL
|
||||
如果无法访问API,可以手动在数据库执行建表SQL(参考 `src/lib/init-tables.ts`)。
|
||||
|
||||
### 7. 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 启动开发环境(带热更新)
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
**启动成功标志**:
|
||||
```
|
||||
✓ Compiled in xxxms
|
||||
✓ Ready in xxxms
|
||||
Local: http://localhost:5000
|
||||
```
|
||||
|
||||
### 8. 访问应用
|
||||
|
||||
打开浏览器,访问:
|
||||
- **本地地址**:http://localhost:5000
|
||||
- **功能页面**:
|
||||
- 配置管理:创建和管理日报配置
|
||||
- 日报生成:选择配置和时间范围生成日报
|
||||
- 历史记录:查看和下载已生成的日报
|
||||
|
||||
## 生产环境部署
|
||||
|
||||
### 1. 构建生产版本
|
||||
|
||||
```bash
|
||||
# 构建优化的生产版本
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### 2. 启动生产服务器
|
||||
|
||||
#### 方式1:直接启动
|
||||
```bash
|
||||
pnpm run start
|
||||
```
|
||||
|
||||
#### 方式2:使用 PM2(推荐)
|
||||
```bash
|
||||
# 安装 PM2
|
||||
npm install -g pm2
|
||||
|
||||
# 启动服务
|
||||
pm2 start scripts/start.sh --name "daily-report"
|
||||
|
||||
# 查看状态
|
||||
pm2 status
|
||||
|
||||
# 查看日志
|
||||
pm2 logs daily-report
|
||||
|
||||
# 设置开机自启
|
||||
pm2 startup
|
||||
pm2 save
|
||||
```
|
||||
|
||||
### 3. 配置定时任务
|
||||
|
||||
项目内置定时任务,每天8:01自动生成日报:
|
||||
- 定时任务在 `src/server.ts` 中配置
|
||||
- 使用 node-cron 实现
|
||||
- 北京时间(UTC+8)
|
||||
|
||||
如果需要修改定时任务时间,编辑 `src/server.ts`:
|
||||
|
||||
```typescript
|
||||
// 修改定时任务时间(cron表达式)
|
||||
cron.schedule('1 8 * * *', async () => {
|
||||
// 每天8:01执行
|
||||
console.log('开始生成日报...');
|
||||
}, {
|
||||
timezone: "Asia/Shanghai"
|
||||
});
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. pnpm 安装失败
|
||||
|
||||
**问题**:`pnpm: command not found`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 使用 npm 安装 pnpm
|
||||
npm install -g pnpm
|
||||
|
||||
# 或使用 corepack(Node.js 16.10+)
|
||||
corepack enable
|
||||
corepack prepare pnpm@latest --activate
|
||||
```
|
||||
|
||||
### 2. Node.js 版本过低
|
||||
|
||||
**问题**:`error: The engine "node" is incompatible`
|
||||
|
||||
**解决**:
|
||||
- 升级 Node.js 到 20+ 版本
|
||||
- 或修改 `package.json` 中的 engines 配置
|
||||
|
||||
### 3. 数据库连接失败
|
||||
|
||||
**问题**:`Error: connect ETIMEDOUT` 或 `Error: connect ECONNREFUSED`
|
||||
|
||||
**解决**:
|
||||
1. 检查数据库地址和端口是否正确
|
||||
2. 检查防火墙是否允许访问
|
||||
3. 检查数据库用户名和密码
|
||||
4. 编辑 `src/lib/db.ts` 修改连接配置
|
||||
|
||||
### 4. 端口占用
|
||||
|
||||
**问题**:`Error: listen EADDRINUSE: address already in use :::5000`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# Windows
|
||||
netstat -ano | findstr :5000
|
||||
taskkill /PID <进程ID> /F
|
||||
|
||||
# Linux/macOS
|
||||
lsof -ti:5000 | xargs kill -9
|
||||
```
|
||||
|
||||
### 5. Excel 文件无法下载
|
||||
|
||||
**问题**:点击下载按钮无反应
|
||||
|
||||
**解决**:
|
||||
- 检查 `public/reports/` 目录是否存在
|
||||
- 检查文件是否成功生成(查看历史记录)
|
||||
- 浏览器可能拦截下载,检查浏览器设置
|
||||
|
||||
### 6. 定时任务不执行
|
||||
|
||||
**问题**:8:01没有自动生成日报
|
||||
|
||||
**解决**:
|
||||
1. 确认服务是否持续运行(不要关闭终端)
|
||||
2. 检查是否有启用的配置(is_active=1)
|
||||
3. 查看日志是否有错误信息
|
||||
4. 手动测试定时任务:
|
||||
```bash
|
||||
# 访问测试接口(如果有)
|
||||
curl http://localhost:5000/api/test-cron
|
||||
```
|
||||
|
||||
## 项目目录说明
|
||||
|
||||
```
|
||||
/workspace/projects/
|
||||
├── src/ ← 主程序目录
|
||||
│ ├── app/ ← Next.js 应用
|
||||
│ │ ├── page.tsx ← 主页面(配置、生成、历史)
|
||||
│ │ ├── api/ ← API 接口
|
||||
│ │ └── layout.tsx ← 应用布局
|
||||
│ ├── lib/ ← 核心业务逻辑
|
||||
│ │ ├── db.ts ← 数据库连接
|
||||
│ │ ├── report-generator.ts ← 日报生成
|
||||
│ │ └ init-tables.ts ← 建表脚本
|
||||
│ │ └ field-mapping.ts ← 字段映射
|
||||
│ ├── server.ts ← 服务启动(含定时任务)
|
||||
│ └ components/ ← UI 组件
|
||||
│ └ hooks/ ← React Hooks
|
||||
├── scripts/ ← 启动脚本
|
||||
│ ├── dev.sh ← 开发启动
|
||||
│ ├── build.sh ← 构建脚本
|
||||
│ └ start.sh ← 生产启动
|
||||
├── public/ ← 静态资源
|
||||
│ └ reports/ ← Excel 文件存储
|
||||
├── package.json ← 依赖配置
|
||||
├── tsconfig.json ← TS 配置
|
||||
├── .coze ← 启动配置
|
||||
├── AGENTS.md ← 项目说明
|
||||
└── README.md ← 本文件
|
||||
```
|
||||
|
||||
## 运行命令总结
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 2. 开发环境启动(带热更新)
|
||||
pnpm run dev
|
||||
# 访问:http://localhost:5000
|
||||
|
||||
# 3. 构建生产版本
|
||||
pnpm run build
|
||||
|
||||
# 4. 生产环境启动
|
||||
pnpm run start
|
||||
|
||||
# 5. 代码检查
|
||||
pnpm run validate
|
||||
|
||||
# 6. TypeScript 类型检查
|
||||
pnpm run ts-check
|
||||
|
||||
# 7. 代码格式检查
|
||||
pnpm run lint
|
||||
```
|
||||
|
||||
## 数据库初始化
|
||||
### 4. 初始化数据库表
|
||||
|
||||
首次运行需要初始化数据库表:
|
||||
|
||||
### 自动创建的表(不修改原表)
|
||||
启动服务后,访问或调用:
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/init-tables
|
||||
```
|
||||
|
||||
1. **t_daily_report_config** - 配置表
|
||||
- 存储用户的日报配置
|
||||
- 包含字段选择、求和字段等
|
||||
或在浏览器中访问:
|
||||
```
|
||||
http://localhost:5000/api/init-tables
|
||||
```
|
||||
|
||||
2. **t_daily_report_history** - 历史表
|
||||
- 记录每次日报生成的历史
|
||||
- 包含文件路径、统计信息等
|
||||
## 运行方式
|
||||
|
||||
**重要**:原表 `t_equipment_charge_order` 只读取,结构和内容都不会修改。
|
||||
### 开发环境(推荐)
|
||||
|
||||
## 技术栈说明
|
||||
#### Windows
|
||||
```bash
|
||||
scripts\dev.bat
|
||||
```
|
||||
|
||||
- **Next.js 16**:React全栈框架(App Router模式)
|
||||
- **React 19**:前端UI库
|
||||
- **TypeScript 5**:类型安全的JavaScript
|
||||
- **Tailwind CSS 4**:实用优先的CSS框架
|
||||
- **shadcn/ui**:基于Radix UI的组件库
|
||||
- **mysql2**:MySQL数据库驱动
|
||||
- **node-cron**:定时任务调度器
|
||||
- **xlsx**:Excel文件生成库
|
||||
或直接运行:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
## 许可和说明
|
||||
### 生产环境
|
||||
|
||||
- 本项目仅供内部使用
|
||||
- 数据库连接信息已预配置
|
||||
- 定时任务使用北京时间(Asia/Shanghai)
|
||||
- Excel文件存储在 `public/reports/` 目录
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
## 获取帮助
|
||||
### 访问应用
|
||||
|
||||
如有问题,请查看:
|
||||
1. 本README文件的常见问题部分
|
||||
2. `AGENTS.md` 项目说明文档
|
||||
3. 检查日志文件(如有)
|
||||
4. 查看API返回的错误信息
|
||||
启动后在浏览器访问:
|
||||
```
|
||||
http://localhost:5000
|
||||
```
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 1. 创建配置
|
||||
|
||||
1. 点击"配置管理"标签
|
||||
2. 点击"创建配置"按钮
|
||||
3. 填写配置信息:
|
||||
- 配置名称:如"79路公交日报"
|
||||
- 拆分方式:选择"按企业ID"或"按用户ID"
|
||||
- 选择企业/用户:从下拉列表选择
|
||||
- 选择字段:勾选需要显示的字段
|
||||
- 求和字段:勾选需要求和的字段(可选)
|
||||
4. 点击"保存"
|
||||
|
||||
### 2. 生成日报
|
||||
|
||||
1. 点击"日报生成"标签
|
||||
2. 选择配置
|
||||
3. 选择时间范围:
|
||||
- 点击预设按钮(昨天、最近3天、最近7天)
|
||||
- 或自定义时间范围
|
||||
4. 点击"生成日报"
|
||||
5. 生成成功后点击"下载Excel"
|
||||
|
||||
### 3. 查看历史
|
||||
|
||||
1. 点击"历史记录"标签
|
||||
2. 查看所有生成的日报记录
|
||||
3. 点击"下载"按钮下载Excel文件
|
||||
|
||||
### 4. 定时任务
|
||||
|
||||
系统会在每天8:01(北京时间)自动为所有启用的配置生成日报。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
.
|
||||
├── app.py # Flask主应用
|
||||
├── requirements.txt # Python依赖
|
||||
├── lib/ # 业务逻辑模块
|
||||
│ ├── __init__.py
|
||||
│ ├── db.py # 数据库连接
|
||||
│ ├── field_mapping.py # 字段映射(英文->中文)
|
||||
│ └── report_generator.py # 日报生成核心逻辑
|
||||
├── templates/ # HTML模板
|
||||
│ └── index.html # 主页面
|
||||
├── public/ # 静态文件
|
||||
│ └── reports/ # 生成的Excel文件
|
||||
├── scripts/ # 启动脚本
|
||||
│ ├── dev.bat # Windows开发环境启动
|
||||
│ └── start.bat # Windows生产环境启动
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## API接口
|
||||
|
||||
### 配置管理
|
||||
- `GET /api/config` - 获取所有配置
|
||||
- `POST /api/config` - 创建配置
|
||||
- `PUT /api/config/<id>` - 更新配置
|
||||
- `DELETE /api/config/<id>` - 删除配置
|
||||
- `POST /api/config/<id>/toggle` - 启用/禁用配置
|
||||
- `POST /api/config/<id>/move` - 移动配置排序
|
||||
|
||||
### 企业和用户
|
||||
- `GET /api/entities?type=company` - 获取企业列表
|
||||
- `GET /api/entities?type=user` - 获取用户列表
|
||||
|
||||
### 字段
|
||||
- `GET /api/fields` - 获取所有可用字段
|
||||
|
||||
### 日报生成
|
||||
- `POST /api/report/generate` - 生成日报
|
||||
|
||||
### 历史记录
|
||||
- `GET /api/report/history` - 获取历史记录(分页)
|
||||
|
||||
### 文件下载
|
||||
- `GET /api/download?path=<file_path>` - 下载Excel文件
|
||||
|
||||
### 数据库初始化
|
||||
- `POST /api/init-tables` - 初始化数据库表
|
||||
|
||||
## 数据库表说明
|
||||
|
||||
### t_daily_report_config(配置表)
|
||||
|
||||
存储日报配置信息:
|
||||
- id: 配置ID
|
||||
- config_name: 配置名称
|
||||
- split_type: 拆分方式(company_id/user_id)
|
||||
- split_value: 拆分值
|
||||
- split_name: 拆分显示名称
|
||||
- selected_fields: 选择的字段列表(JSON)
|
||||
- sum_fields: 求和字段列表(JSON)
|
||||
- is_active: 是否启用
|
||||
- sort_order: 排序顺序
|
||||
- create_time: 创建时间
|
||||
- update_time: 更新时间
|
||||
|
||||
### t_daily_report_history(历史表)
|
||||
|
||||
存储日报生成历史:
|
||||
- id: 历史ID
|
||||
- report_date: 报表日期
|
||||
- start_time: 开始时间
|
||||
- end_time: 结束时间
|
||||
- split_type: 拆分方式
|
||||
- split_value: 拆分值
|
||||
- split_name: 拆分显示名称
|
||||
- config_id: 配置ID
|
||||
- total_orders: 订单总数
|
||||
- total_amount: 总金额
|
||||
- sum_results: 求和结果(JSON)
|
||||
- file_path: 文件路径
|
||||
- status: 状态(0=失败,1=成功)
|
||||
- create_time: 创建时间
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 端口5000被占用
|
||||
|
||||
修改 `app.py` 中的端口:
|
||||
```python
|
||||
port = int(os.environ.get('PORT', 5001)) # 改为其他端口
|
||||
```
|
||||
|
||||
或设置环境变量:
|
||||
```bash
|
||||
set PORT=5001 # Windows
|
||||
export PORT=5001 # Linux/Mac
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 2. 数据库连接失败
|
||||
|
||||
检查:
|
||||
- 数据库地址和端口是否正确
|
||||
- 防火墙是否放行
|
||||
- 数据库用户名和密码是否正确
|
||||
|
||||
测试连接:
|
||||
```python
|
||||
python -c "from lib.db import execute_query; print(execute_query('SELECT 1'))"
|
||||
```
|
||||
|
||||
### 3. 定时任务不执行
|
||||
|
||||
确保:
|
||||
- 服务持续运行(不要关闭终端)
|
||||
- 至少有一个启用的配置
|
||||
- 系统时区正确(Asia/Shanghai)
|
||||
|
||||
### 4. Excel文件无法下载
|
||||
|
||||
检查:
|
||||
- `public/reports/` 目录是否存在
|
||||
- 文件是否已生成
|
||||
- 浏览器是否允许下载
|
||||
|
||||
### 5. 中文乱码
|
||||
|
||||
确保:
|
||||
- 数据库字符集为 utf8mb4
|
||||
- Python文件编码为 UTF-8
|
||||
- HTML页面编码为 UTF-8
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 添加新字段
|
||||
|
||||
编辑 `lib/field_mapping.py`,在 `FIELD_MAPPING` 字典中添加:
|
||||
```python
|
||||
'new_field': '新字段中文名'
|
||||
```
|
||||
|
||||
### 修改定时任务时间
|
||||
|
||||
编辑 `app.py`,修改:
|
||||
```python
|
||||
scheduler.add_job(scheduled_job, 'cron', hour=8, minute=1) # 改为其他时间
|
||||
```
|
||||
|
||||
### 修改数据库配置
|
||||
|
||||
编辑 `lib/db.py`,修改 `DB_CONFIG`:
|
||||
```python
|
||||
DB_CONFIG = {
|
||||
'host': '新地址',
|
||||
'port': 新端口,
|
||||
'user': '新用户名',
|
||||
'password': '新密码',
|
||||
'database': '新数据库',
|
||||
}
|
||||
```
|
||||
|
||||
## 部署建议
|
||||
|
||||
### 使用Gunicorn(生产环境)
|
||||
|
||||
```bash
|
||||
pip install gunicorn
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
```
|
||||
|
||||
### 使用Supervisor管理进程
|
||||
|
||||
创建 `/etc/supervisor/conf.d/daily-report.conf`:
|
||||
```ini
|
||||
[program:daily-report]
|
||||
command=/usr/bin/python /path/to/app.py
|
||||
directory=/path/to/project
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/daily-report.err.log
|
||||
stdout_logfile=/var/log/daily-report.out.log
|
||||
```
|
||||
|
||||
### 使用Nginx反向代理
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: Python 3.8+ / Flask 3.0
|
||||
- **数据库**: MySQL / Apache Doris
|
||||
- **数据库驱动**: PyMySQL
|
||||
- **Excel处理**: openpyxl
|
||||
- **定时任务**: APScheduler
|
||||
- **前端**: HTML + CSS + JavaScript(原生)
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题,请联系开发团队。
|
||||
|
||||
---
|
||||
|
||||
**最后更新时间**:2025-07-08
|
||||
**最后更新**: 2025-07-08
|
||||
449
app.py
Normal file
449
app.py
Normal file
@@ -0,0 +1,449 @@
|
||||
"""
|
||||
Flask主应用
|
||||
订单日报系统
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
from flask import Flask, request, jsonify, send_from_directory, render_template
|
||||
from flask_cors import CORS
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
import pytz
|
||||
|
||||
from lib.db import execute_query, execute_update, execute_insert
|
||||
from lib.field_mapping import get_all_fields_with_display
|
||||
from lib.report_generator import generate_daily_report
|
||||
|
||||
# 创建Flask应用
|
||||
app = Flask(__name__,
|
||||
template_folder='templates',
|
||||
static_folder='public')
|
||||
CORS(app)
|
||||
|
||||
# 确保public/reports目录存在
|
||||
os.makedirs(os.path.join('public', 'reports'), exist_ok=True)
|
||||
|
||||
|
||||
# ==================== 页面路由 ====================
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""主页"""
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
# ==================== 配置管理API ====================
|
||||
|
||||
@app.route('/api/config', methods=['GET'])
|
||||
def get_configs():
|
||||
"""获取所有配置"""
|
||||
try:
|
||||
configs = execute_query(
|
||||
'SELECT * FROM t_daily_report_config ORDER BY sort_order ASC, id DESC'
|
||||
)
|
||||
|
||||
# 解析JSON字段
|
||||
for config in configs:
|
||||
config['selected_fields'] = json.loads(config['selected_fields'])
|
||||
config['sum_fields'] = json.loads(config['sum_fields']) if config['sum_fields'] else []
|
||||
|
||||
return jsonify({'success': True, 'data': configs})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/config', methods=['POST'])
|
||||
def create_config():
|
||||
"""创建配置"""
|
||||
try:
|
||||
data = request.json
|
||||
config_name = data.get('config_name')
|
||||
split_type = data.get('split_type')
|
||||
split_value = data.get('split_value')
|
||||
split_name = data.get('split_name', '')
|
||||
selected_fields = data.get('selected_fields', [])
|
||||
sum_fields = data.get('sum_fields', [])
|
||||
|
||||
if not config_name or not split_type or not split_value:
|
||||
return jsonify({'success': False, 'message': '缺少必填字段'}), 400
|
||||
|
||||
if not selected_fields:
|
||||
return jsonify({'success': False, 'message': '请至少选择一个字段'}), 400
|
||||
|
||||
# 生成ID
|
||||
config_id = int(datetime.now().timestamp() * 1000)
|
||||
|
||||
# 获取最大排序值
|
||||
max_sort = execute_query('SELECT MAX(sort_order) as max_sort FROM t_daily_report_config')
|
||||
sort_order = (max_sort[0]['max_sort'] or 0) + 1
|
||||
|
||||
sql = """
|
||||
INSERT INTO t_daily_report_config
|
||||
(id, config_name, split_type, split_value, split_name, selected_fields,
|
||||
sum_fields, is_active, sort_order, create_time, update_time)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, 1, %s, NOW(), NOW())
|
||||
"""
|
||||
|
||||
params = (
|
||||
config_id,
|
||||
config_name,
|
||||
split_type,
|
||||
split_value,
|
||||
split_name,
|
||||
json.dumps(selected_fields, ensure_ascii=False),
|
||||
json.dumps(sum_fields, ensure_ascii=False),
|
||||
sort_order
|
||||
)
|
||||
|
||||
execute_insert(sql, params)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '配置创建成功',
|
||||
'data': {'id': config_id}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/config/<int:config_id>', methods=['PUT'])
|
||||
def update_config(config_id):
|
||||
"""更新配置"""
|
||||
try:
|
||||
data = request.json
|
||||
config_name = data.get('config_name')
|
||||
split_type = data.get('split_type')
|
||||
split_value = data.get('split_value')
|
||||
split_name = data.get('split_name', '')
|
||||
selected_fields = data.get('selected_fields', [])
|
||||
sum_fields = data.get('sum_fields', [])
|
||||
|
||||
sql = """
|
||||
UPDATE t_daily_report_config
|
||||
SET config_name = %s, split_type = %s, split_value = %s, split_name = %s,
|
||||
selected_fields = %s, sum_fields = %s, update_time = NOW()
|
||||
WHERE id = %s
|
||||
"""
|
||||
|
||||
params = (
|
||||
config_name,
|
||||
split_type,
|
||||
split_value,
|
||||
split_name,
|
||||
json.dumps(selected_fields, ensure_ascii=False),
|
||||
json.dumps(sum_fields, ensure_ascii=False),
|
||||
config_id
|
||||
)
|
||||
|
||||
execute_update(sql, params)
|
||||
|
||||
return jsonify({'success': True, 'message': '配置更新成功'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/config/<int:config_id>', methods=['DELETE'])
|
||||
def delete_config(config_id):
|
||||
"""删除配置"""
|
||||
try:
|
||||
execute_update('DELETE FROM t_daily_report_config WHERE id = %s', (config_id,))
|
||||
return jsonify({'success': True, 'message': '配置删除成功'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/config/<int:config_id>/toggle', methods=['POST'])
|
||||
def toggle_config(config_id):
|
||||
"""启用/禁用配置"""
|
||||
try:
|
||||
execute_update(
|
||||
'UPDATE t_daily_report_config SET is_active = NOT is_active, update_time = NOW() WHERE id = %s',
|
||||
(config_id,)
|
||||
)
|
||||
return jsonify({'success': True, 'message': '操作成功'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/config/<int:config_id>/move', methods=['POST'])
|
||||
def move_config(config_id):
|
||||
"""移动配置排序"""
|
||||
try:
|
||||
direction = request.json.get('direction') # up 或 down
|
||||
|
||||
# 获取当前配置
|
||||
current = execute_query(
|
||||
'SELECT sort_order FROM t_daily_report_config WHERE id = %s',
|
||||
(config_id,)
|
||||
)
|
||||
if not current:
|
||||
return jsonify({'success': False, 'message': '配置不存在'}), 404
|
||||
|
||||
current_order = current[0]['sort_order']
|
||||
|
||||
if direction == 'up':
|
||||
# 找到上一个配置
|
||||
prev = execute_query(
|
||||
'SELECT id, sort_order FROM t_daily_report_config WHERE sort_order < %s ORDER BY sort_order DESC LIMIT 1',
|
||||
(current_order,)
|
||||
)
|
||||
if prev:
|
||||
# 交换排序
|
||||
execute_update(
|
||||
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
|
||||
(prev[0]['sort_order'], config_id)
|
||||
)
|
||||
execute_update(
|
||||
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
|
||||
(current_order, prev[0]['id'])
|
||||
)
|
||||
elif direction == 'down':
|
||||
# 找到下一个配置
|
||||
next_config = execute_query(
|
||||
'SELECT id, sort_order FROM t_daily_report_config WHERE sort_order > %s ORDER BY sort_order ASC LIMIT 1',
|
||||
(current_order,)
|
||||
)
|
||||
if next_config:
|
||||
# 交换排序
|
||||
execute_update(
|
||||
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
|
||||
(next_config[0]['sort_order'], config_id)
|
||||
)
|
||||
execute_update(
|
||||
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
|
||||
(current_order, next_config[0]['id'])
|
||||
)
|
||||
|
||||
return jsonify({'success': True, 'message': '移动成功'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
# ==================== 企业和用户列表API ====================
|
||||
|
||||
@app.route('/api/entities', methods=['GET'])
|
||||
def get_entities():
|
||||
"""获取企业或用户列表"""
|
||||
try:
|
||||
entity_type = request.args.get('type', 'company')
|
||||
|
||||
if entity_type == 'company':
|
||||
# 获取企业列表
|
||||
companies = execute_query(
|
||||
'SELECT DISTINCT company_id, company_name FROM t_company ORDER BY company_name'
|
||||
)
|
||||
return jsonify({'success': True, 'data': companies})
|
||||
else:
|
||||
# 获取用户列表
|
||||
users = execute_query(
|
||||
'SELECT DISTINCT user_id, user_name, order_type FROM t_equipment_charge_order WHERE user_id IS NOT NULL ORDER BY user_name'
|
||||
)
|
||||
return jsonify({'success': True, 'data': users})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
# ==================== 字段列表API ====================
|
||||
|
||||
@app.route('/api/fields', methods=['GET'])
|
||||
def get_fields():
|
||||
"""获取所有可用字段"""
|
||||
try:
|
||||
fields = get_all_fields_with_display()
|
||||
return jsonify({'success': True, 'data': fields})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
# ==================== 日报生成API ====================
|
||||
|
||||
@app.route('/api/report/generate', methods=['POST'])
|
||||
def generate_report():
|
||||
"""生成日报"""
|
||||
try:
|
||||
data = request.json
|
||||
config_id = data.get('config_id')
|
||||
|
||||
if not config_id:
|
||||
return jsonify({'success': False, 'message': '请选择配置'}), 400
|
||||
|
||||
# 解析时间参数
|
||||
start_time = None
|
||||
end_time = None
|
||||
|
||||
if data.get('start_time'):
|
||||
start_time = datetime.strptime(data['start_time'], '%Y-%m-%d %H:%M:%S')
|
||||
if data.get('end_time'):
|
||||
end_time = datetime.strptime(data['end_time'], '%Y-%m-%d %H:%M:%S')
|
||||
|
||||
result = generate_daily_report(config_id, start_time, end_time)
|
||||
|
||||
if result['success']:
|
||||
return jsonify(result)
|
||||
else:
|
||||
return jsonify(result), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
# ==================== 历史记录API ====================
|
||||
|
||||
@app.route('/api/report/history', methods=['GET'])
|
||||
def get_report_history():
|
||||
"""获取日报生成历史"""
|
||||
try:
|
||||
page = int(request.args.get('page', 1))
|
||||
page_size = int(request.args.get('page_size', 20))
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# 查询总数
|
||||
total_result = execute_query('SELECT COUNT(*) as total FROM t_daily_report_history')
|
||||
total = total_result[0]['total']
|
||||
|
||||
# 查询数据
|
||||
history = execute_query(
|
||||
'SELECT * FROM t_daily_report_history ORDER BY create_time DESC LIMIT %s OFFSET %s',
|
||||
(page_size, offset)
|
||||
)
|
||||
|
||||
# 解析JSON字段
|
||||
for item in history:
|
||||
item['sum_results'] = json.loads(item['sum_results']) if item['sum_results'] else {}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'list': history,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
# ==================== 数据库初始化API ====================
|
||||
|
||||
@app.route('/api/init-tables', methods=['POST'])
|
||||
def init_tables():
|
||||
"""初始化数据库表"""
|
||||
try:
|
||||
# 创建配置表
|
||||
create_config_table = """
|
||||
CREATE TABLE IF NOT EXISTS t_daily_report_config (
|
||||
id BIGINT NOT NULL COMMENT '配置ID',
|
||||
config_name VARCHAR(100) NOT NULL COMMENT '配置名称',
|
||||
split_type VARCHAR(20) NOT NULL COMMENT '拆分方式',
|
||||
split_value VARCHAR(100) NOT NULL COMMENT '拆分值',
|
||||
split_name VARCHAR(200) NULL COMMENT '拆分显示名称',
|
||||
selected_fields VARCHAR(3000) NOT NULL COMMENT '选择的字段列表',
|
||||
sum_fields VARCHAR(3000) NULL COMMENT '求和字段列表',
|
||||
is_active TINYINT NULL COMMENT '是否启用',
|
||||
sort_order INT NULL COMMENT '排序顺序',
|
||||
create_time DATETIME NULL COMMENT '创建时间',
|
||||
update_time DATETIME NULL COMMENT '更新时间'
|
||||
)
|
||||
UNIQUE KEY(id)
|
||||
DISTRIBUTED BY HASH(id) BUCKETS 1
|
||||
PROPERTIES("replication_num" = "1")
|
||||
"""
|
||||
execute_update(create_config_table)
|
||||
|
||||
# 创建历史表
|
||||
create_history_table = """
|
||||
CREATE TABLE IF NOT EXISTS t_daily_report_history (
|
||||
id BIGINT NOT NULL COMMENT '历史ID',
|
||||
report_date VARCHAR(20) NULL COMMENT '报表日期',
|
||||
start_time VARCHAR(50) NULL COMMENT '开始时间',
|
||||
end_time VARCHAR(50) NULL COMMENT '结束时间',
|
||||
split_type VARCHAR(20) NULL COMMENT '拆分方式',
|
||||
split_value VARCHAR(100) NULL COMMENT '拆分值',
|
||||
split_name VARCHAR(200) NULL COMMENT '拆分显示名称',
|
||||
config_id BIGINT NULL COMMENT '配置ID',
|
||||
total_orders INT NULL COMMENT '订单总数',
|
||||
total_amount DECIMAL(10,2) NULL COMMENT '总金额',
|
||||
sum_results VARCHAR(2000) NULL COMMENT '求和结果',
|
||||
file_path VARCHAR(500) NULL COMMENT '文件路径',
|
||||
status TINYINT NULL COMMENT '状态',
|
||||
create_time DATETIME NULL COMMENT '创建时间'
|
||||
)
|
||||
UNIQUE KEY(id)
|
||||
DISTRIBUTED BY HASH(id) BUCKETS 1
|
||||
PROPERTIES("replication_num" = "1")
|
||||
"""
|
||||
execute_update(create_history_table)
|
||||
|
||||
return jsonify({'success': True, 'message': '数据库表初始化成功'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
# ==================== 文件下载API ====================
|
||||
|
||||
@app.route('/api/download', methods=['GET'])
|
||||
def download_file():
|
||||
"""下载文件"""
|
||||
try:
|
||||
file_path = request.args.get('path')
|
||||
if not file_path:
|
||||
return jsonify({'success': False, 'message': '文件路径不能为空'}), 400
|
||||
|
||||
# 安全检查:确保路径在public目录下
|
||||
if not file_path.startswith('/reports/'):
|
||||
return jsonify({'success': False, 'message': '无效的文件路径'}), 400
|
||||
|
||||
filename = file_path.replace('/reports/', '')
|
||||
reports_dir = os.path.join('public', 'reports')
|
||||
|
||||
return send_from_directory(reports_dir, filename, as_attachment=True)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
# ==================== 定时任务 ====================
|
||||
|
||||
def scheduled_job():
|
||||
"""定时任务:每天8:01自动生成日报"""
|
||||
print(f'[{datetime.now()}] 开始执行定时任务...')
|
||||
|
||||
try:
|
||||
# 获取所有启用的配置
|
||||
configs = execute_query(
|
||||
'SELECT id, config_name FROM t_daily_report_config WHERE is_active = 1'
|
||||
)
|
||||
|
||||
if not configs:
|
||||
print('没有启用的配置,跳过生成')
|
||||
return
|
||||
|
||||
print(f'找到 {len(configs)} 个启用的配置')
|
||||
|
||||
# 为每个配置生成日报
|
||||
for config in configs:
|
||||
print(f'正在生成: {config["config_name"]}')
|
||||
result = generate_daily_report(config['id'])
|
||||
|
||||
if result['success']:
|
||||
print(f' ✓ 成功: {result["total_orders"]} 条订单, 金额: {result["total_amount"]}')
|
||||
else:
|
||||
print(f' ✗ 失败: {result["message"]}')
|
||||
|
||||
print('定时任务执行完成')
|
||||
except Exception as e:
|
||||
print(f'定时任务执行失败: {e}')
|
||||
|
||||
|
||||
# 启动定时任务
|
||||
scheduler = BackgroundScheduler(timezone='Asia/Shanghai')
|
||||
scheduler.add_job(scheduled_job, 'cron', hour=8, minute=1)
|
||||
scheduler.start()
|
||||
print('定时任务已启动:每天 08:01 执行')
|
||||
|
||||
|
||||
# ==================== 启动应用 ====================
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = int(os.environ.get('PORT', 5000))
|
||||
print(f'启动服务器: http://localhost:{port}')
|
||||
app.run(host='0.0.0.0', port=port, debug=True)
|
||||
17
lib/__init__.py
Normal file
17
lib/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
lib模块初始化
|
||||
"""
|
||||
from lib.db import get_connection, execute_query, execute_update, execute_insert
|
||||
from lib.field_mapping import get_field_display_name, get_all_fields, get_all_fields_with_display
|
||||
from lib.report_generator import generate_daily_report
|
||||
|
||||
__all__ = [
|
||||
'get_connection',
|
||||
'execute_query',
|
||||
'execute_update',
|
||||
'execute_insert',
|
||||
'get_field_display_name',
|
||||
'get_all_fields',
|
||||
'get_all_fields_with_display',
|
||||
'generate_daily_report'
|
||||
]
|
||||
51
lib/db.py
Normal file
51
lib/db.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
数据库连接配置
|
||||
"""
|
||||
import pymysql
|
||||
from contextlib import contextmanager
|
||||
|
||||
# 数据库配置
|
||||
DB_CONFIG = {
|
||||
'host': 'haoslm2.xicp.net',
|
||||
'port': 10216,
|
||||
'user': 'root',
|
||||
'password': 'DsideaL147258369',
|
||||
'database': 'yltcharge',
|
||||
'charset': 'utf8mb4',
|
||||
'cursorclass': pymysql.cursors.DictCursor,
|
||||
'autocommit': True
|
||||
}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_connection():
|
||||
"""获取数据库连接(上下文管理器)"""
|
||||
conn = pymysql.connect(**DB_CONFIG)
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def execute_query(sql, params=None):
|
||||
"""执行查询并返回结果"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(sql, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
|
||||
def execute_update(sql, params=None):
|
||||
"""执行更新操作"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(sql, params)
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
def execute_insert(sql, params=None):
|
||||
"""执行插入操作并返回插入ID"""
|
||||
with get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(sql, params)
|
||||
return cursor.lastrowid
|
||||
135
lib/field_mapping.py
Normal file
135
lib/field_mapping.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
字段映射模块
|
||||
将数据库英文字段名映射为中文显示名
|
||||
"""
|
||||
|
||||
# 字段映射字典(英文 -> 中文)
|
||||
FIELD_MAPPING = {
|
||||
'id': '订单ID',
|
||||
'order_no': '订单号',
|
||||
'company_id': '企业ID',
|
||||
'user_id': '用户ID',
|
||||
'user_name': '用户名称',
|
||||
'order_type': '订单类型',
|
||||
'state': '订单状态',
|
||||
'report_time': '上报时间',
|
||||
'start_time': '开始时间',
|
||||
'end_time': '结束时间',
|
||||
'charge_duration': '充电时长(秒)',
|
||||
'charge_energy': '充电电量(kWh)',
|
||||
'charge_money': '充电金额(元)',
|
||||
'service_money': '服务费(元)',
|
||||
'electricity_money': '电费(元)',
|
||||
'total_money': '总金额(元)',
|
||||
'pile_id': '充电桩ID',
|
||||
'pile_no': '充电桩编号',
|
||||
'gun_no': '枪号',
|
||||
'start_soc': '起始SOC(%)',
|
||||
'end_soc': '结束SOC(%)',
|
||||
'start_voltage': '起始电压(V)',
|
||||
'end_voltage': '结束电压(V)',
|
||||
'start_current': '起始电流(A)',
|
||||
'end_current': '结束电流(A)',
|
||||
'temperature': '温度(℃)',
|
||||
'create_time': '创建时间',
|
||||
'update_time': '更新时间',
|
||||
'pay_type': '支付方式',
|
||||
'pay_time': '支付时间',
|
||||
'pay_status': '支付状态',
|
||||
'stop_reason': '停止原因',
|
||||
'station_id': '充电站ID',
|
||||
'station_name': '充电站名称',
|
||||
'address': '地址',
|
||||
'city': '城市',
|
||||
'province': '省份',
|
||||
'area': '区域',
|
||||
'remark': '备注',
|
||||
'operator_id': '运营商ID',
|
||||
'operator_name': '运营商名称',
|
||||
'equipment_id': '设备ID',
|
||||
'equipment_type': '设备类型',
|
||||
'equipment_model': '设备型号',
|
||||
'manufacturer': '制造商',
|
||||
'install_location': '安装位置',
|
||||
'power_rate': '额定功率(kW)',
|
||||
'voltage_rate': '额定电压(V)',
|
||||
'current_rate': '额定电流(A)',
|
||||
'brand': '品牌',
|
||||
'network_type': '网络类型',
|
||||
'sim_card': 'SIM卡号',
|
||||
'iccid': 'ICCID',
|
||||
'imei': 'IMEI',
|
||||
'mac': 'MAC地址',
|
||||
'ip': 'IP地址',
|
||||
'firmware_version': '固件版本',
|
||||
'hardware_version': '硬件版本',
|
||||
'protocol_version': '协议版本',
|
||||
'online_status': '在线状态',
|
||||
'last_heartbeat': '最后心跳时间',
|
||||
'fault_code': '故障代码',
|
||||
'fault_desc': '故障描述',
|
||||
'fault_time': '故障时间',
|
||||
'clear_time': '清除时间',
|
||||
'operator_fault': '运营商故障',
|
||||
'maintenance_status': '维护状态',
|
||||
'maintenance_time': '维护时间',
|
||||
'maintenance_desc': '维护描述',
|
||||
'qr_code': '二维码',
|
||||
'qr_code_url': '二维码URL',
|
||||
'qr_code_type': '二维码类型',
|
||||
'qr_code_status': '二维码状态',
|
||||
'qr_code_create_time': '二维码创建时间',
|
||||
'qr_code_update_time': '二维码更新时间',
|
||||
'qr_code_remark': '二维码备注',
|
||||
'qr_code_operator': '二维码操作员',
|
||||
'qr_code_company': '二维码企业',
|
||||
'qr_code_station': '二维码站点',
|
||||
'qr_code_equipment': '二维码设备',
|
||||
'qr_code_pile': '二维码桩号',
|
||||
'qr_code_gun': '二维码枪号',
|
||||
'qr_code_location': '二维码位置',
|
||||
'qr_code_address': '二维码地址',
|
||||
'qr_code_city': '二维码城市',
|
||||
'qr_code_province': '二维码省份',
|
||||
'qr_code_area': '二维码区域',
|
||||
'qr_code_lat': '二维码纬度',
|
||||
'qr_code_lng': '二维码经度',
|
||||
'qr_code_alt': '二维码海拔',
|
||||
'qr_code_accuracy': '二维码精度',
|
||||
'qr_code_speed': '二维码速度',
|
||||
'qr_code_direction': '二维码方向',
|
||||
'qr_code_satellite': '二维码卫星',
|
||||
'qr_code_signal': '二维码信号',
|
||||
'qr_code_battery': '二维码电池',
|
||||
'qr_code_temperature': '二维码温度',
|
||||
'qr_code_humidity': '二维码湿度',
|
||||
'qr_code_pressure': '二维码气压',
|
||||
'qr_code_wind_speed': '二维码风速',
|
||||
'qr_code_wind_direction': '二维码风向',
|
||||
'qr_code_rainfall': '二维码降雨量',
|
||||
'qr_code_snowfall': '二维码降雪量',
|
||||
'qr_code_visibility': '二维码能见度',
|
||||
'qr_code_uv_index': '二维码紫外线指数',
|
||||
'qr_code_aqi': '二维码空气质量指数',
|
||||
'qr_code_pm25': '二维码PM2.5',
|
||||
'qr_code_pm10': '二维码PM10',
|
||||
'qr_code_co': '二维码一氧化碳',
|
||||
'qr_code_no2': '二维码二氧化氮',
|
||||
'qr_code_so2': '二维码二氧化硫',
|
||||
'qr_code_o3': '二维码臭氧',
|
||||
}
|
||||
|
||||
|
||||
def get_field_display_name(field_name):
|
||||
"""获取字段的中文显示名称"""
|
||||
return FIELD_MAPPING.get(field_name, field_name)
|
||||
|
||||
|
||||
def get_all_fields():
|
||||
"""获取所有字段列表"""
|
||||
return list(FIELD_MAPPING.keys())
|
||||
|
||||
|
||||
def get_all_fields_with_display():
|
||||
"""获取所有字段及其显示名称"""
|
||||
return [{'field': k, 'display': v} for k, v in FIELD_MAPPING.items()]
|
||||
278
lib/report_generator.py
Normal file
278
lib/report_generator.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
日报生成核心逻辑
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill
|
||||
import pytz
|
||||
|
||||
from lib.db import execute_query, execute_insert
|
||||
from lib.field_mapping import get_field_display_name
|
||||
|
||||
|
||||
def get_beijing_time():
|
||||
"""获取北京时间"""
|
||||
beijing_tz = pytz.timezone('Asia/Shanghai')
|
||||
return datetime.now(beijing_tz)
|
||||
|
||||
|
||||
def generate_daily_report(config_id, start_time=None, end_time=None):
|
||||
"""
|
||||
生成日报
|
||||
|
||||
Args:
|
||||
config_id: 配置ID
|
||||
start_time: 开始时间(可选,默认为昨天8点)
|
||||
end_time: 结束时间(可选,默认为今天8点)
|
||||
|
||||
Returns:
|
||||
dict: 生成结果,包含 success, message, file_path, total_orders, total_amount, sum_results
|
||||
"""
|
||||
try:
|
||||
# 获取配置信息
|
||||
configs = execute_query(
|
||||
'SELECT * FROM t_daily_report_config WHERE id = %s',
|
||||
(config_id,)
|
||||
)
|
||||
|
||||
if not configs:
|
||||
return {'success': False, 'message': '配置不存在'}
|
||||
|
||||
config = configs[0]
|
||||
|
||||
# 解析字段
|
||||
selected_fields = json.loads(config['selected_fields'])
|
||||
sum_fields = json.loads(config['sum_fields']) if config['sum_fields'] else []
|
||||
|
||||
# 设置时间范围
|
||||
if not start_time or not end_time:
|
||||
now = get_beijing_time()
|
||||
end_time = now.replace(hour=8, minute=0, second=0, microsecond=0)
|
||||
start_time = end_time - timedelta(days=1)
|
||||
|
||||
# 格式化时间
|
||||
start_time_str = start_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
end_time_str = end_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 构建查询SQL
|
||||
fields_str = ', '.join(selected_fields)
|
||||
sql = f"""
|
||||
SELECT {fields_str}
|
||||
FROM t_equipment_charge_order
|
||||
WHERE state = 3
|
||||
AND report_time >= %s
|
||||
AND report_time < %s
|
||||
AND {config['split_type']} = %s
|
||||
ORDER BY report_time DESC
|
||||
"""
|
||||
|
||||
# 执行查询
|
||||
orders = execute_query(sql, (start_time_str, end_time_str, config['split_value']))
|
||||
|
||||
if not orders:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'时间范围内没有符合条件的订单数据'
|
||||
}
|
||||
|
||||
# 计算求和
|
||||
sum_results = {}
|
||||
for field in sum_fields:
|
||||
if field in orders[0]:
|
||||
total = sum(float(order.get(field, 0) or 0) for order in orders)
|
||||
sum_results[field] = total
|
||||
|
||||
# 生成Excel文件
|
||||
file_path = generate_excel(
|
||||
orders=orders,
|
||||
selected_fields=selected_fields,
|
||||
sum_fields=sum_fields,
|
||||
sum_results=sum_results,
|
||||
config_name=config['config_name'],
|
||||
split_name=config.get('split_name', config['split_value']),
|
||||
start_time=start_time_str,
|
||||
end_time=end_time_str
|
||||
)
|
||||
|
||||
# 计算总金额
|
||||
total_amount = sum(float(order.get('total_money', 0) or 0) for order in orders)
|
||||
|
||||
# 保存历史记录
|
||||
history_id = save_report_history(
|
||||
report_date=start_time.strftime('%Y-%m-%d'),
|
||||
start_time=start_time_str,
|
||||
end_time=end_time_str,
|
||||
split_type=config['split_type'],
|
||||
split_value=config['split_value'],
|
||||
split_name=config.get('split_name', config['split_value']),
|
||||
config_id=config_id,
|
||||
total_orders=len(orders),
|
||||
total_amount=total_amount,
|
||||
sum_results=sum_results,
|
||||
file_path=file_path,
|
||||
status=1
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': '日报生成成功',
|
||||
'file_path': file_path,
|
||||
'total_orders': len(orders),
|
||||
'total_amount': total_amount,
|
||||
'sum_results': sum_results,
|
||||
'history_id': history_id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# 保存失败记录
|
||||
try:
|
||||
save_report_history(
|
||||
report_date=start_time.strftime('%Y-%m-%d') if start_time else datetime.now().strftime('%Y-%m-%d'),
|
||||
start_time=start_time_str if start_time else '',
|
||||
end_time=end_time_str if end_time else '',
|
||||
split_type=config['split_type'] if config else '',
|
||||
split_value=config['split_value'] if config else '',
|
||||
split_name=config.get('split_name', '') if config else '',
|
||||
config_id=config_id,
|
||||
total_orders=0,
|
||||
total_amount=0,
|
||||
sum_results={},
|
||||
file_path='',
|
||||
status=0
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'生成日报失败: {str(e)}'
|
||||
}
|
||||
|
||||
|
||||
def generate_excel(orders, selected_fields, sum_fields, sum_results,
|
||||
config_name, split_name, start_time, end_time):
|
||||
"""
|
||||
生成Excel文件
|
||||
|
||||
Returns:
|
||||
str: 文件路径
|
||||
"""
|
||||
# 创建reports目录
|
||||
reports_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'public', 'reports')
|
||||
os.makedirs(reports_dir, exist_ok=True)
|
||||
|
||||
# 生成文件名
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
filename = f'daily_report_{config_name}_{split_name}_{timestamp}.xlsx'
|
||||
file_path = os.path.join(reports_dir, filename)
|
||||
|
||||
# 创建工作簿
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = '日报'
|
||||
|
||||
# 写入标题
|
||||
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(selected_fields))
|
||||
title_cell = ws.cell(row=1, column=1)
|
||||
title_cell.value = f'{config_name} - {split_name} 日报'
|
||||
title_cell.font = Font(size=16, bold=True)
|
||||
title_cell.alignment = Alignment(horizontal='center')
|
||||
|
||||
# 写入时间范围
|
||||
ws.merge_cells(start_row=2, start_column=1, end_row=2, end_column=len(selected_fields))
|
||||
time_cell = ws.cell(row=2, column=1)
|
||||
time_cell.value = f'统计时间: {start_time} 至 {end_time}'
|
||||
time_cell.alignment = Alignment(horizontal='center')
|
||||
|
||||
# 写入表头(中文)
|
||||
header_row = 4
|
||||
for col_idx, field in enumerate(selected_fields, 1):
|
||||
cell = ws.cell(row=header_row, column=col_idx)
|
||||
cell.value = get_field_display_name(field)
|
||||
cell.font = Font(bold=True)
|
||||
cell.fill = PatternFill(start_color='D3D3D3', end_color='D3D3D3', fill_type='solid')
|
||||
cell.alignment = Alignment(horizontal='center')
|
||||
|
||||
# 写入数据
|
||||
for row_idx, order in enumerate(orders, header_row + 1):
|
||||
for col_idx, field in enumerate(selected_fields, 1):
|
||||
cell = ws.cell(row=row_idx, column=col_idx)
|
||||
value = order.get(field, '')
|
||||
|
||||
# 格式化时间字段
|
||||
if field in ['report_time', 'start_time', 'end_time', 'create_time', 'update_time', 'pay_time']:
|
||||
if isinstance(value, datetime):
|
||||
value = value.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
cell.value = value
|
||||
|
||||
# 写入求和行
|
||||
if sum_fields:
|
||||
sum_row = header_row + len(orders) + 1
|
||||
ws.cell(row=sum_row, column=1).value = '合计'
|
||||
ws.cell(row=sum_row, column=1).font = Font(bold=True)
|
||||
|
||||
for col_idx, field in enumerate(selected_fields, 1):
|
||||
if field in sum_fields and field in sum_results:
|
||||
cell = ws.cell(row=sum_row, column=col_idx)
|
||||
cell.value = sum_results[field]
|
||||
cell.font = Font(bold=True)
|
||||
cell.number_format = '0.00'
|
||||
|
||||
# 调整列宽
|
||||
for col_idx, field in enumerate(selected_fields, 1):
|
||||
max_length = len(get_field_display_name(field))
|
||||
for row in ws.iter_rows(min_row=header_row + 1, max_row=header_row + len(orders),
|
||||
min_col=col_idx, max_col=col_idx):
|
||||
for cell in row:
|
||||
if cell.value:
|
||||
max_length = max(max_length, len(str(cell.value)))
|
||||
ws.column_dimensions[ws.cell(row=1, column=col_idx).column_letter].width = min(max_length + 2, 50)
|
||||
|
||||
# 保存文件
|
||||
wb.save(file_path)
|
||||
|
||||
# 返回相对路径
|
||||
return f'/reports/{filename}'
|
||||
|
||||
|
||||
def save_report_history(report_date, start_time, end_time, split_type, split_value,
|
||||
split_name, config_id, total_orders, total_amount,
|
||||
sum_results, file_path, status):
|
||||
"""
|
||||
保存日报生成历史记录
|
||||
|
||||
Returns:
|
||||
int: 历史记录ID
|
||||
"""
|
||||
# 生成ID
|
||||
history_id = int(datetime.now().timestamp() * 1000)
|
||||
|
||||
sql = """
|
||||
INSERT INTO t_daily_report_history
|
||||
(id, report_date, start_time, end_time, split_type, split_value, split_name,
|
||||
config_id, total_orders, total_amount, sum_results, file_path, status, create_time)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
|
||||
"""
|
||||
|
||||
params = (
|
||||
history_id,
|
||||
report_date,
|
||||
start_time,
|
||||
end_time,
|
||||
split_type,
|
||||
split_value,
|
||||
split_name,
|
||||
config_id,
|
||||
total_orders,
|
||||
total_amount,
|
||||
json.dumps(sum_results, ensure_ascii=False),
|
||||
file_path,
|
||||
status
|
||||
)
|
||||
|
||||
execute_insert(sql, params)
|
||||
|
||||
return history_id
|
||||
@@ -1,39 +1,22 @@
|
||||
# Python 依赖包列表
|
||||
# 订单日报系统 - Python辅助工具依赖
|
||||
# Python Flask 日报系统依赖
|
||||
|
||||
# 数据处理和分析
|
||||
pandas>=2.0.0
|
||||
numpy>=1.24.0
|
||||
openpyxl>=3.1.0 # Excel文件处理
|
||||
xlrd>=2.0.1 # 读取旧版Excel文件
|
||||
xlsxwriter>=3.1.0 # Excel写入
|
||||
# Web框架
|
||||
Flask==3.0.0
|
||||
Flask-CORS==4.0.0
|
||||
|
||||
# 数据库连接
|
||||
mysql-connector-python>=8.0.0
|
||||
pymysql>=1.1.0
|
||||
sqlalchemy>=2.0.0
|
||||
PyMySQL==1.1.0
|
||||
cryptography==41.0.7
|
||||
|
||||
# Web开发和API
|
||||
flask>=3.0.0
|
||||
fastapi>=0.100.0
|
||||
requests>=2.31.0
|
||||
uvicorn>=0.23.0
|
||||
# Excel处理
|
||||
openpyxl==3.1.2
|
||||
|
||||
# 定时任务
|
||||
schedule>=1.2.0
|
||||
apscheduler>=3.10.0
|
||||
APScheduler==3.10.4
|
||||
|
||||
# 日期时间处理
|
||||
python-dateutil>=2.8.0
|
||||
pytz>=2023.3
|
||||
# 工具库
|
||||
python-dateutil==2.8.2
|
||||
pytz==2023.3
|
||||
|
||||
# 日志和调试
|
||||
loguru>=0.7.0
|
||||
|
||||
# 数据可视化(可选)
|
||||
matplotlib>=3.7.0
|
||||
seaborn>=0.12.0
|
||||
|
||||
# 其他实用工具
|
||||
tqdm>=4.66.0 # 进度条
|
||||
python-dotenv>=1.0.0 # 环境变量管理
|
||||
# 开发工具(可选)
|
||||
python-dotenv==1.0.0
|
||||
@@ -1,32 +1,13 @@
|
||||
@echo off
|
||||
REM Windows开发环境启动脚本
|
||||
|
||||
REM 设置端口
|
||||
set PORT=5000
|
||||
if not defined DEPLOY_RUN_PORT set DEPLOY_RUN_PORT=%PORT%
|
||||
echo Starting Flask development server...
|
||||
echo Working directory: %cd%
|
||||
|
||||
REM 检查并清除端口占用
|
||||
echo Checking port %DEPLOY_RUN_PORT%...
|
||||
netstat -ano | findstr :%DEPLOY_RUN_PORT% | findstr LISTENING >nul
|
||||
if %errorlevel% equ 0 (
|
||||
echo Port %DEPLOY_RUN_PORT% is in use, attempting to clear...
|
||||
for /f "tokens=5" %%a in ('netstat -ano ^| findstr :%DEPLOY_RUN_PORT% ^| findstr LISTENING') do (
|
||||
echo Killing process %%a on port %DEPLOY_RUN_PORT%
|
||||
taskkill /PID %%a /F >nul 2>&1
|
||||
)
|
||||
timeout /t 2 /nobreak >nul
|
||||
echo Port %DEPLOY_RUN_PORT% cleared.
|
||||
) else (
|
||||
echo Port %DEPLOY_RUN_PORT% is free.
|
||||
)
|
||||
REM 设置环境变量
|
||||
set FLASK_APP=app.py
|
||||
set FLASK_ENV=development
|
||||
set FLASK_DEBUG=1
|
||||
|
||||
REM 设置工作目录
|
||||
if not defined COZE_WORKSPACE_PATH set COZE_WORKSPACE_PATH=%cd%
|
||||
|
||||
echo Starting HTTP service on port %DEPLOY_RUN_PORT% for dev...
|
||||
echo Working directory: %COZE_WORKSPACE_PATH%
|
||||
|
||||
REM 启动开发服务器
|
||||
cd /d "%COZE_WORKSPACE_PATH%"
|
||||
set PORT=%DEPLOY_RUN_PORT%
|
||||
pnpm tsx watch src/server.ts
|
||||
REM 启动Flask应用
|
||||
python app.py
|
||||
@@ -1,15 +1,12 @@
|
||||
@echo off
|
||||
REM Windows生产环境启动脚本
|
||||
|
||||
REM 设置端口
|
||||
set PORT=5000
|
||||
if not defined DEPLOY_RUN_PORT set DEPLOY_RUN_PORT=%PORT%
|
||||
echo Starting Flask production server...
|
||||
echo Working directory: %cd%
|
||||
|
||||
REM 设置工作目录
|
||||
if not defined COZE_WORKSPACE_PATH set COZE_WORKSPACE_PATH=%cd%
|
||||
REM 设置环境变量
|
||||
set FLASK_APP=app.py
|
||||
set FLASK_ENV=production
|
||||
|
||||
cd /d "%COZE_WORKSPACE_PATH%"
|
||||
|
||||
echo Starting HTTP service on port %DEPLOY_RUN_PORT% for deploy...
|
||||
set PORT=%DEPLOY_RUN_PORT%
|
||||
node dist/server.js
|
||||
REM 启动Flask应用(生产环境)
|
||||
python app.py
|
||||
957
templates/index.html
Normal file
957
templates/index.html
Normal file
@@ -0,0 +1,957 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>订单日报系统</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background: #f5f7fa;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #3498db;
|
||||
border-bottom: 2px solid #3498db;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
select {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #229954;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #f39c12;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #d35400;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
max-width: 800px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pagination button:hover:not(:disabled) {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pagination span {
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>订单日报系统</h1>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('config')">配置管理</button>
|
||||
<button class="tab" onclick="switchTab('generate')">日报生成</button>
|
||||
<button class="tab" onclick="switchTab('history')">历史记录</button>
|
||||
</div>
|
||||
|
||||
<!-- 配置管理 -->
|
||||
<div id="config-tab" class="tab-content active">
|
||||
<div class="card">
|
||||
<button class="btn btn-primary" onclick="showCreateModal()">创建配置</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>配置名称</th>
|
||||
<th>拆分方式</th>
|
||||
<th>拆分值</th>
|
||||
<th>字段数</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="config-list">
|
||||
<tr><td colspan="6" class="loading">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日报生成 -->
|
||||
<div id="generate-tab" class="tab-content">
|
||||
<div class="card">
|
||||
<h3 style="margin-bottom: 20px;">生成日报</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>选择配置 *</label>
|
||||
<select id="generate-config" class="form-control">
|
||||
<option value="">请选择配置</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>时间范围</label>
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||||
<button class="btn btn-sm btn-primary" onclick="setPresetTime('yesterday')">昨天</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="setPresetTime('last3days')">最近3天</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="setPresetTime('last7days')">最近7天</button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<div style="flex: 1;">
|
||||
<input type="datetime-local" id="start-time" class="form-control">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<input type="datetime-local" id="end-time" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="generateReport()">生成日报</button>
|
||||
|
||||
<div id="generate-result" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录 -->
|
||||
<div id="history-tab" class="tab-content">
|
||||
<div class="card">
|
||||
<button class="btn btn-primary" onclick="refreshHistory()">刷新</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>报表日期</th>
|
||||
<th>时间范围</th>
|
||||
<th>配置名称</th>
|
||||
<th>拆分值</th>
|
||||
<th>订单数</th>
|
||||
<th>总金额</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="history-list">
|
||||
<tr><td colspan="8" class="loading">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination" id="pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑配置模态框 -->
|
||||
<div id="config-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="modal-title">创建配置</h3>
|
||||
<button class="close-btn" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>配置名称 *</label>
|
||||
<input type="text" id="config-name" class="form-control" placeholder="请输入配置名称">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>拆分方式 *</label>
|
||||
<select id="split-type" class="form-control" onchange="loadEntities()">
|
||||
<option value="">请选择拆分方式</option>
|
||||
<option value="company_id">按企业ID</option>
|
||||
<option value="user_id">按用户ID</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>选择企业/用户 *</label>
|
||||
<select id="split-value" class="form-control">
|
||||
<option value="">请先选择拆分方式</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>选择字段 *</label>
|
||||
<div class="checkbox-group" id="fields-list">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>求和字段(可选)</label>
|
||||
<div class="checkbox-group" id="sum-fields-list">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button class="btn" onclick="closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="saveConfig()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let editingConfigId = null;
|
||||
let allFields = [];
|
||||
let currentPage = 1;
|
||||
const pageSize = 20;
|
||||
|
||||
// 切换标签页
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(`${tabName}-tab`).classList.add('active');
|
||||
|
||||
if (tabName === 'config') {
|
||||
loadConfigs();
|
||||
} else if (tabName === 'generate') {
|
||||
loadGenerateConfigs();
|
||||
} else if (tabName === 'history') {
|
||||
loadHistory();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载配置列表
|
||||
async function loadConfigs() {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
renderConfigList(result.data);
|
||||
} else {
|
||||
alert('加载配置失败: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('加载配置失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染配置列表
|
||||
function renderConfigList(configs) {
|
||||
const tbody = document.getElementById('config-list');
|
||||
|
||||
if (configs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty">暂无配置,请点击"创建配置"按钮</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = configs.map(config => `
|
||||
<tr>
|
||||
<td>${config.config_name}</td>
|
||||
<td>${config.split_type === 'company_id' ? '按企业ID' : '按用户ID'}</td>
|
||||
<td>${config.split_name || config.split_value}</td>
|
||||
<td>${config.selected_fields.length}</td>
|
||||
<td>
|
||||
<span class="status-badge ${config.is_active ? 'status-active' : 'status-inactive'}">
|
||||
${config.is_active ? '已启用' : '已禁用'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-sm btn-warning" onclick="toggleConfig(${config.id})">
|
||||
${config.is_active ? '禁用' : '启用'}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="editConfig(${config.id})">编辑</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteConfig(${config.id})">删除</button>
|
||||
<button class="btn btn-sm" onclick="moveConfig(${config.id}, 'up')">↑</button>
|
||||
<button class="btn btn-sm" onclick="moveConfig(${config.id}, 'down')">↓</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 显示创建模态框
|
||||
function showCreateModal() {
|
||||
editingConfigId = null;
|
||||
document.getElementById('modal-title').textContent = '创建配置';
|
||||
document.getElementById('config-name').value = '';
|
||||
document.getElementById('split-type').value = '';
|
||||
document.getElementById('split-value').innerHTML = '<option value="">请先选择拆分方式</option>';
|
||||
|
||||
loadFields();
|
||||
document.getElementById('config-modal').classList.add('active');
|
||||
}
|
||||
|
||||
// 加载字段列表
|
||||
async function loadFields() {
|
||||
try {
|
||||
const response = await fetch('/api/fields');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
allFields = result.data;
|
||||
renderFields();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载字段失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染字段列表
|
||||
function renderFields(selectedFields = [], sumFields = []) {
|
||||
const fieldsList = document.getElementById('fields-list');
|
||||
const sumFieldsList = document.getElementById('sum-fields-list');
|
||||
|
||||
fieldsList.innerHTML = allFields.map(field => `
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="${field.field}"
|
||||
${selectedFields.includes(field.field) ? 'checked' : ''}>
|
||||
${field.display}
|
||||
</label>
|
||||
`).join('');
|
||||
|
||||
sumFieldsList.innerHTML = allFields.map(field => `
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="${field.field}"
|
||||
${sumFields.includes(field.field) ? 'checked' : ''}>
|
||||
${field.display}
|
||||
</label>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 加载企业/用户列表
|
||||
async function loadEntities() {
|
||||
const splitType = document.getElementById('split-type').value;
|
||||
const select = document.getElementById('split-value');
|
||||
|
||||
if (!splitType) {
|
||||
select.innerHTML = '<option value="">请先选择拆分方式</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
select.innerHTML = '<option value="">加载中...</option>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/entities?type=${splitType === 'company_id' ? 'company' : 'user'}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const entities = result.data;
|
||||
select.innerHTML = '<option value="">请选择</option>' +
|
||||
entities.map(e => {
|
||||
const value = splitType === 'company_id' ? e.company_id : e.user_id;
|
||||
const name = splitType === 'company_id' ? e.company_name : e.user_name;
|
||||
const type = e.order_type === 3 ? '(企业用户)' : '(普通用户)';
|
||||
return `<option value="${value}" data-name="${name}">${name} ${splitType === 'user_id' ? type : ''}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
const configName = document.getElementById('config-name').value;
|
||||
const splitType = document.getElementById('split-type').value;
|
||||
const splitValueSelect = document.getElementById('split-value');
|
||||
const splitValue = splitValueSelect.value;
|
||||
const splitName = splitValueSelect.options[splitValueSelect.selectedIndex].dataset.name || '';
|
||||
|
||||
const selectedFields = Array.from(document.querySelectorAll('#fields-list input:checked')).map(cb => cb.value);
|
||||
const sumFields = Array.from(document.querySelectorAll('#sum-fields-list input:checked')).map(cb => cb.value);
|
||||
|
||||
if (!configName || !splitType || !splitValue) {
|
||||
alert('请填写所有必填字段');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedFields.length === 0) {
|
||||
alert('请至少选择一个字段');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
config_name: configName,
|
||||
split_type: splitType,
|
||||
split_value: splitValue,
|
||||
split_name: splitName,
|
||||
selected_fields: selectedFields,
|
||||
sum_fields: sumFields
|
||||
};
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (editingConfigId) {
|
||||
response = await fetch(`/api/config/${editingConfigId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} else {
|
||||
response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(editingConfigId ? '配置更新成功' : '配置创建成功');
|
||||
closeModal();
|
||||
loadConfigs();
|
||||
} else {
|
||||
alert('保存失败: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('保存失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑配置
|
||||
async function editConfig(configId) {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const config = result.data.find(c => c.id === configId);
|
||||
if (config) {
|
||||
editingConfigId = configId;
|
||||
document.getElementById('modal-title').textContent = '编辑配置';
|
||||
document.getElementById('config-name').value = config.config_name;
|
||||
document.getElementById('split-type').value = config.split_type;
|
||||
|
||||
await loadEntities();
|
||||
document.getElementById('split-value').value = config.split_value;
|
||||
|
||||
await loadFields();
|
||||
renderFields(config.selected_fields, config.sum_fields);
|
||||
|
||||
document.getElementById('config-modal').classList.add('active');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('加载配置失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
async function deleteConfig(configId) {
|
||||
if (!confirm('确定要删除这个配置吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/config/${configId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('配置删除成功');
|
||||
loadConfigs();
|
||||
} else {
|
||||
alert('删除失败: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('删除失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 启用/禁用配置
|
||||
async function toggleConfig(configId) {
|
||||
try {
|
||||
const response = await fetch(`/api/config/${configId}/toggle`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
loadConfigs();
|
||||
} else {
|
||||
alert('操作失败: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('操作失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动配置
|
||||
async function moveConfig(configId, direction) {
|
||||
try {
|
||||
const response = await fetch(`/api/config/${configId}/move`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ direction })
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
loadConfigs();
|
||||
} else {
|
||||
alert('移动失败: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('移动失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal() {
|
||||
document.getElementById('config-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
// 加载生成配置列表
|
||||
async function loadGenerateConfigs() {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const select = document.getElementById('generate-config');
|
||||
select.innerHTML = '<option value="">请选择配置</option>' +
|
||||
result.data.map(c => `<option value="${c.id}">${c.config_name}</option>`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置预设时间
|
||||
function setPresetTime(preset) {
|
||||
const now = new Date();
|
||||
let start, end;
|
||||
|
||||
if (preset === 'yesterday') {
|
||||
end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
|
||||
start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
|
||||
} else if (preset === 'last3days') {
|
||||
end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
|
||||
start = new Date(end.getTime() - 3 * 24 * 60 * 60 * 1000);
|
||||
} else if (preset === 'last7days') {
|
||||
end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
|
||||
start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
document.getElementById('start-time').value = formatDateTimeLocal(start);
|
||||
document.getElementById('end-time').value = formatDateTimeLocal(end);
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTimeLocal(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
// 生成日报
|
||||
async function generateReport() {
|
||||
const configId = document.getElementById('generate-config').value;
|
||||
const startTime = document.getElementById('start-time').value;
|
||||
const endTime = document.getElementById('end-time').value;
|
||||
|
||||
if (!configId) {
|
||||
alert('请选择配置');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
alert('请选择时间范围');
|
||||
return;
|
||||
}
|
||||
|
||||
const resultDiv = document.getElementById('generate-result');
|
||||
resultDiv.innerHTML = '<div class="alert alert-info">正在生成日报,请稍候...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/report/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
config_id: parseInt(configId),
|
||||
start_time: startTime.replace('T', ' ') + ':00',
|
||||
end_time: endTime.replace('T', ' ') + ':00'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<strong>日报生成成功!</strong><br>
|
||||
订单数量: ${result.total_orders}<br>
|
||||
总金额: ¥${result.total_amount.toFixed(2)}<br>
|
||||
<a href="/api/download?path=${result.file_path}" target="_blank" class="btn btn-sm btn-success" style="margin-top: 10px;">下载Excel</a>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="alert alert-error">生成失败: ${result.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<div class="alert alert-error">生成失败: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载历史记录
|
||||
async function loadHistory(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/report/history?page=${page}&page_size=${pageSize}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
renderHistory(result.data.list);
|
||||
renderPagination(result.data.total, page);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染历史记录
|
||||
function renderHistory(history) {
|
||||
const tbody = document.getElementById('history-list');
|
||||
|
||||
if (history.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="empty">暂无历史记录</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = history.map(item => `
|
||||
<tr>
|
||||
<td>${item.report_date}</td>
|
||||
<td>${item.start_time || '-'} ~ ${item.end_time || '-'}</td>
|
||||
<td>${item.config_name || '-'}</td>
|
||||
<td>${item.split_name || item.split_value}</td>
|
||||
<td>${item.total_orders}</td>
|
||||
<td>¥${(item.total_amount || 0).toFixed(2)}</td>
|
||||
<td>
|
||||
<span class="status-badge ${item.status === 1 ? 'status-active' : 'status-inactive'}">
|
||||
${item.status === 1 ? '成功' : '失败'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
${item.file_path ? `<a href="/api/download?path=${item.file_path}" target="_blank" class="btn btn-sm btn-primary">下载</a>` : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination(total, currentPage) {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
const pagination = document.getElementById('pagination');
|
||||
|
||||
if (totalPages <= 1) {
|
||||
pagination.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
pagination.innerHTML = `
|
||||
<button ${currentPage === 1 ? 'disabled' : ''} onclick="loadHistory(${currentPage - 1})">上一页</button>
|
||||
<span>第 ${currentPage} / ${totalPages} 页,共 ${total} 条</span>
|
||||
<button ${currentPage === totalPages ? 'disabled' : ''} onclick="loadHistory(${currentPage + 1})">下一页</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// 刷新历史记录
|
||||
function refreshHistory() {
|
||||
loadHistory(currentPage);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
loadConfigs();
|
||||
loadFields();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user