forked from erp-dev/erp
feat: settlement lineup
This commit is contained in:
50
.skills/.gitkeep
Normal file
50
.skills/.gitkeep
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Skills 目录说明
|
||||||
|
|
||||||
|
这个目录包含项目的自定义skills。
|
||||||
|
|
||||||
|
## 当前Skills
|
||||||
|
|
||||||
|
### flower-db-access
|
||||||
|
数据库只读访问skill,为AI agent提供安全的数据库查询能力。
|
||||||
|
|
||||||
|
- **文件**: `flower-db-access/SKILL.md`
|
||||||
|
- **用途**: 数据分析、调试、报告生成
|
||||||
|
- **权限**: 只读,禁止访问认证表
|
||||||
|
- **详细文档**: `flower-db-access/README.md`
|
||||||
|
|
||||||
|
## 是否提交到Git?
|
||||||
|
|
||||||
|
建议将 `.skills/` 目录提交到Git仓库,因为:
|
||||||
|
1. Skills是项目特定的配置和工具
|
||||||
|
2. 团队成员可以共享相同的数据库访问能力
|
||||||
|
3. 便于新成员快速了解数据结构
|
||||||
|
|
||||||
|
### 建议的 .gitignore 配置
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
# 不要忽略skills目录
|
||||||
|
# .skills/
|
||||||
|
|
||||||
|
# 但可以忽略skill中的敏感信息(如果有的话)
|
||||||
|
# .skills/**/secrets.env
|
||||||
|
```
|
||||||
|
|
||||||
|
## 创建新Skill
|
||||||
|
|
||||||
|
要创建新的skill,请:
|
||||||
|
|
||||||
|
1. 在此目录下创建新文件夹:`mkdir -p .skills/your-skill-name`
|
||||||
|
2. 创建 `SKILL.md` 文件,包含标准的YAML头:
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: your-skill-name
|
||||||
|
description: Skill description
|
||||||
|
allowed-tools: Bash(command:*)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Your Skill Title
|
||||||
|
|
||||||
|
Skill content...
|
||||||
|
```
|
||||||
|
3. 添加详细的文档和使用示例
|
||||||
|
4. 提交到Git仓库供团队使用
|
||||||
43
.skills/README.md
Normal file
43
.skills/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Skills 使用说明
|
||||||
|
|
||||||
|
## Flower Database Access Skill
|
||||||
|
|
||||||
|
### 位置
|
||||||
|
`.skills/flower-db-access/SKILL.md`
|
||||||
|
|
||||||
|
### 用途
|
||||||
|
为可信AI agent提供只读数据库访问能力,用于:
|
||||||
|
- 数据分析和统计
|
||||||
|
- 问题调试
|
||||||
|
- 报告生成
|
||||||
|
- 业务数据查询
|
||||||
|
|
||||||
|
### 特性
|
||||||
|
✅ 读取所有业务表(客户、订单、库存、生产等)
|
||||||
|
✅ 支持复杂SQL查询和JOIN
|
||||||
|
✅ 支持聚合和统计分析
|
||||||
|
❌ 禁止所有写操作
|
||||||
|
❌ 禁止访问认证相关表
|
||||||
|
|
||||||
|
### 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试数据库连接
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "\dt"
|
||||||
|
|
||||||
|
# 查询示例
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT id, customer_id, sales_date, status
|
||||||
|
FROM business_salesorder
|
||||||
|
WHERE merchant_id = 1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 详细文档
|
||||||
|
查看 [SKILL.md](.skills/flower-db-access/SKILL.md) 获取完整使用指南。
|
||||||
|
|
||||||
|
## 其他Skills
|
||||||
|
|
||||||
|
如果需要其他自定义skills,可以在此目录下创建新的skill文件夹。
|
||||||
93
.skills/SUMMARY.md
Normal file
93
.skills/SUMMARY.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Flower Database Access Skill - 使用总结
|
||||||
|
|
||||||
|
## 已创建的文件
|
||||||
|
|
||||||
|
```
|
||||||
|
.skills/
|
||||||
|
├── README.md # Skills目录说明
|
||||||
|
├── flower-db-access/
|
||||||
|
│ ├── SKILL.md # 主要skill文档(11KB)
|
||||||
|
│ ├── README.md # 使用说明
|
||||||
|
│ └── examples.sh # 可执行示例脚本
|
||||||
|
└── .gitkeep # Git占位文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## Skill功能
|
||||||
|
|
||||||
|
### ✅ 允许的操作
|
||||||
|
- 读取所有业务表数据
|
||||||
|
- 执行SELECT查询
|
||||||
|
- 数据分析和统计
|
||||||
|
- 生成报告
|
||||||
|
|
||||||
|
### ❌ 禁止的操作
|
||||||
|
- 所有写操作(INSERT, UPDATE, DELETE等)
|
||||||
|
- 访问认证相关表(auth_user, auth_permission等)
|
||||||
|
|
||||||
|
## 快速测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行示例脚本
|
||||||
|
/home/f/coding/flower/.skills/flower-db-access/examples.sh
|
||||||
|
|
||||||
|
# 或手动测试连接
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "\dt"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据库架构概览
|
||||||
|
|
||||||
|
### 主要模块
|
||||||
|
1. **basic_info** - 基础信息(客户、供应商、产品、仓库)
|
||||||
|
2. **business** - 业务单据(采购、销售、退货、收付款)
|
||||||
|
3. **stock** - 库存管理(变动、冻结、汇总)
|
||||||
|
4. **printing** - 生产管理(开版、印染)
|
||||||
|
5. **stateflow** - 工作流引擎
|
||||||
|
|
||||||
|
### 多租户架构
|
||||||
|
⚠️ **重要**: 所有业务表都有 `merchant_id` 字段,查询时应该包含这个条件。
|
||||||
|
|
||||||
|
## 测试结果
|
||||||
|
|
||||||
|
✅ 数据库连接正常
|
||||||
|
✅ 所有示例查询成功执行
|
||||||
|
✅ 查询结果正确返回
|
||||||
|
|
||||||
|
### 查询示例输出
|
||||||
|
- ✅ 商户列表:4个商户
|
||||||
|
- ✅ 销售订单:最近4条订单
|
||||||
|
- ✅ 库存状态:4个库存记录
|
||||||
|
- ✅ 开版订单:最近10条记录
|
||||||
|
- ✅ 表大小统计:显示最大的表(basic_info_product: 674 MB)
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
### 对于开发者
|
||||||
|
1. 查看 `SKILL.md` 了解完整的查询示例
|
||||||
|
2. 运行 `examples.sh` 查看实际输出
|
||||||
|
3. 根据需要修改查询示例
|
||||||
|
|
||||||
|
### 对于AI Agent
|
||||||
|
1. 使用 `PGPASSWORD=postgres psql` 命令连接数据库
|
||||||
|
2. 只执行SELECT查询
|
||||||
|
3. 避免访问auth_*表
|
||||||
|
4. 查询时包含merchant_id条件
|
||||||
|
|
||||||
|
## 安全提示
|
||||||
|
|
||||||
|
1. **只读访问**: 此skill严格限制为只读
|
||||||
|
2. **认证保护**: auth_*表完全禁止访问
|
||||||
|
3. **多租户隔离**: 查询时考虑merchant_id
|
||||||
|
4. **性能优化**: 使用LIMIT和WHERE条件
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
如遇问题,请检查:
|
||||||
|
1. PostgreSQL服务是否运行:`sudo systemctl status postgresql`
|
||||||
|
2. 数据库连接参数是否正确
|
||||||
|
3. 查询是否包含禁止的表或操作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**创建时间**: 2026-03-05
|
||||||
|
**数据库**: flower (PostgreSQL)
|
||||||
|
**状态**: ✅ 已测试并正常工作
|
||||||
132
.skills/UPDATE_SUMMARY.md
Normal file
132
.skills/UPDATE_SUMMARY.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Flower Database Access Skill - 更新总结
|
||||||
|
|
||||||
|
## ✅ 已完成更新
|
||||||
|
|
||||||
|
已将skill从密码认证更新为SSH key认证,并修改SSH端口为18762。
|
||||||
|
|
||||||
|
## 📝 更新内容
|
||||||
|
|
||||||
|
### 1. SSH连接配置变更
|
||||||
|
|
||||||
|
**旧配置(密码认证):**
|
||||||
|
- 端口: 22
|
||||||
|
- 认证: 密码 (`Zu0We!1216*`)
|
||||||
|
- 工具: sshpass
|
||||||
|
|
||||||
|
**新配置(SSH Key认证):**
|
||||||
|
- 端口: `18762`
|
||||||
|
- 认证: SSH Key (无密码)
|
||||||
|
- 工具: 原生ssh
|
||||||
|
|
||||||
|
### 2. 更新的文件
|
||||||
|
|
||||||
|
#### ✅ SKILL.md
|
||||||
|
- 移除所有 `sshpass` 命令
|
||||||
|
- 移除密码引用
|
||||||
|
- 更新SSH端口为 `18762`
|
||||||
|
- 更新所有示例使用SSH key认证
|
||||||
|
- 简化SSH命令格式
|
||||||
|
|
||||||
|
#### ✅ README.md
|
||||||
|
- 更新SSH连接信息
|
||||||
|
- 移除密码相关说明
|
||||||
|
- 更新示例命令
|
||||||
|
|
||||||
|
#### ✅ examples-remote.sh
|
||||||
|
- 完全重写脚本
|
||||||
|
- 移除sshpass依赖
|
||||||
|
- 添加SSH端口参数
|
||||||
|
- 使用纯SSH key认证
|
||||||
|
|
||||||
|
#### ✅ query.sh
|
||||||
|
- 更新为SSH key认证
|
||||||
|
- 添加端口参数 `18762`
|
||||||
|
- 简化代码
|
||||||
|
|
||||||
|
### 3. 新的使用方式
|
||||||
|
|
||||||
|
#### 基本SSH连接
|
||||||
|
```bash
|
||||||
|
# 连接到远程服务器
|
||||||
|
ssh -p 18762 root@8.148.215.233
|
||||||
|
|
||||||
|
# 执行单个查询
|
||||||
|
ssh -p 18762 root@8.148.215.233 "PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c 'YOUR_QUERY'"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用快速查询工具
|
||||||
|
```bash
|
||||||
|
# 查询工具
|
||||||
|
./.skills/flower-db-access/query.sh 'SELECT * FROM business_salesorder LIMIT 10'
|
||||||
|
|
||||||
|
# 或通过管道
|
||||||
|
echo 'SELECT COUNT(*) FROM printing_plateorder' | ./.skills/flower-db-access/query.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 运行示例脚本
|
||||||
|
```bash
|
||||||
|
./.skills/flower-db-access/examples-remote.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. SSH隧道(可选)
|
||||||
|
```bash
|
||||||
|
# 创建隧道
|
||||||
|
ssh -p 18762 -L 15432:127.0.0.1:5432 root@8.148.215.233 -N
|
||||||
|
|
||||||
|
# 在另一个终端连接
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 15432 -U postgres -d flower
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 安全改进
|
||||||
|
|
||||||
|
1. **无密码存储**: 所有文件中不再包含明文密码
|
||||||
|
2. **SSH Key认证**: 使用更安全的公钥认证
|
||||||
|
3. **自定义端口**: 使用非标准端口 `18762` 增加安全性
|
||||||
|
4. **简化依赖**: 不再依赖 `sshpass` 工具
|
||||||
|
|
||||||
|
## 📋 前置要求
|
||||||
|
|
||||||
|
### 对于可信Agent
|
||||||
|
1. 确保SSH私钥已配置在 `~/.ssh/id_rsa` 或其他默认位置
|
||||||
|
2. 确保公钥已添加到远程服务器的 `~/.ssh/authorized_keys`
|
||||||
|
3. 确保可以无密码执行: `ssh -p 18762 root@8.148.215.233`
|
||||||
|
|
||||||
|
### 测试连接
|
||||||
|
```bash
|
||||||
|
# 测试SSH连接
|
||||||
|
ssh -p 18762 root@8.148.215.233 "echo 'SSH连接成功'"
|
||||||
|
|
||||||
|
# 测试数据库访问
|
||||||
|
ssh -p 18762 root@8.148.215.233 "PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c 'SELECT 1'"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
1. **确认SSH key配置**
|
||||||
|
```bash
|
||||||
|
ssh -p 18762 root@8.148.215.233 "whoami"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **运行测试脚本**
|
||||||
|
```bash
|
||||||
|
./.skills/flower-db-access/examples-remote.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **执行查询**
|
||||||
|
```bash
|
||||||
|
./.skills/flower-db-access/query.sh 'SELECT COUNT(*) FROM basic_info_merchant'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 文档位置
|
||||||
|
|
||||||
|
- **主要文档**: `.skills/flower-db-access/SKILL.md`
|
||||||
|
- **使用说明**: `.skills/flower-db-access/README.md`
|
||||||
|
- **示例脚本**: `.skills/flower-db-access/examples-remote.sh`
|
||||||
|
- **查询工具**: `.skills/flower-db-access/query.sh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**更新时间**: 2026-03-05
|
||||||
|
**状态**: ✅ 已更新为SSH Key认证
|
||||||
|
**端口**: 18762
|
||||||
|
**认证方式**: SSH Key (passwordless)
|
||||||
161
.skills/flower-db-access/README.md
Normal file
161
.skills/flower-db-access/README.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# Flower Database Access Skill
|
||||||
|
|
||||||
|
这个skill允许可信的AI agent以只读方式访问Flower ERP数据库。
|
||||||
|
|
||||||
|
## ⚠️ 重要:远程访问
|
||||||
|
|
||||||
|
**数据库托管在远程服务器上,必须先通过SSH连接才能访问数据库。**
|
||||||
|
|
||||||
|
### SSH连接信息
|
||||||
|
- 远程服务器: `8.148.215.233`
|
||||||
|
- SSH用户: `root`
|
||||||
|
- SSH密码: `Zu0We!1216*`
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
这个skill已经位于项目的 `.skills/flower-db-access/` 目录中。
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 对于开发者
|
||||||
|
|
||||||
|
1. **查看skill说明**:
|
||||||
|
```bash
|
||||||
|
cat .skills/flower-db-access/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **测试SSH连接**:
|
||||||
|
```bash
|
||||||
|
# SSH key认证
|
||||||
|
ssh -p 18762 root@8.148.215.233 "echo '连接成功'"
|
||||||
|
|
||||||
|
# 或交互式SSH
|
||||||
|
ssh -p 18762 root@8.148.215.233
|
||||||
|
#
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **测试数据库连接**:
|
||||||
|
```bash
|
||||||
|
ssh -p 18762 root@8.148.215.233 \
|
||||||
|
"PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c '\dt'"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 对于AI Agent
|
||||||
|
|
||||||
|
当AI agent被授予权限使用这个skill时,它可以:
|
||||||
|
|
||||||
|
1. **通过SSH连接到远程服务器**
|
||||||
|
2. **读取所有业务表**(除了认证相关表)
|
||||||
|
3. **执行SELECT查询**进行数据分析
|
||||||
|
4. **生成报告和统计信息**
|
||||||
|
5. **调试数据问题**
|
||||||
|
|
||||||
|
**禁止操作**:
|
||||||
|
- 所有写操作(INSERT, UPDATE, DELETE等)
|
||||||
|
- 访问认证相关表(auth_user, auth_permission等)
|
||||||
|
|
||||||
|
## 快速查询工具
|
||||||
|
|
||||||
|
### 使用 query.sh
|
||||||
|
```bash
|
||||||
|
# 快速查询工具
|
||||||
|
./.skills/flower-db-access/query.sh 'SELECT * FROM business_salesorder LIMIT 10'
|
||||||
|
|
||||||
|
# 或通过管道
|
||||||
|
echo 'SELECT COUNT(*) FROM printing_plateorder' | ./.skills/flower-db-access/query.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运行示例脚本
|
||||||
|
```bash
|
||||||
|
# 远程示例(通过SSH)
|
||||||
|
./.skills/flower-db-access/examples-remote.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据库架构
|
||||||
|
|
||||||
|
### 核心模块
|
||||||
|
|
||||||
|
- **basic_info**: 客户、供应商、产品、仓库等基础信息
|
||||||
|
- **business**: 采购、销售、退货、收付款等业务单据
|
||||||
|
- **stock**: 库存变动、冻结、汇总
|
||||||
|
- **printing**: 开版、印染订单和任务
|
||||||
|
- **stateflow**: 工作流引擎
|
||||||
|
|
||||||
|
### 多租户架构
|
||||||
|
|
||||||
|
⚠️ **重要**: 所有业务表都有 `merchant_id` 字段用于多租户隔离。查询时应该包含这个条件。
|
||||||
|
|
||||||
|
## 示例查询
|
||||||
|
|
||||||
|
### 查询最近的销售订单
|
||||||
|
```sql
|
||||||
|
SELECT id, customer_id, sales_date, status, created_at
|
||||||
|
FROM business_salesorder
|
||||||
|
WHERE merchant_id = 1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询库存状态
|
||||||
|
```bash
|
||||||
|
ssh -p 18762 root@8.148.215.233 \
|
||||||
|
"PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c \"
|
||||||
|
SELECT
|
||||||
|
p.name as product_name,
|
||||||
|
w.name as warehouse,
|
||||||
|
si.quantity,
|
||||||
|
p.unit
|
||||||
|
FROM stock_inventory si
|
||||||
|
JOIN basic_info_product p ON si.product_id = p.id
|
||||||
|
JOIN basic_info_warehouse w ON si.warehouse_id = w.id
|
||||||
|
WHERE si.merchant_id = 1
|
||||||
|
AND si.quantity > 0
|
||||||
|
ORDER BY si.quantity DESC;
|
||||||
|
\""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询印染订单进度
|
||||||
|
```bash
|
||||||
|
ssh -p 18762 root@8.148.215.233 \
|
||||||
|
"PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c \"
|
||||||
|
SELECT
|
||||||
|
po.human_id,
|
||||||
|
po.fabric,
|
||||||
|
COUNT(pj.id) as job_count
|
||||||
|
FROM printing_printingorder po
|
||||||
|
LEFT JOIN printing_printingjob pj ON pj.printing_order_id = po.id
|
||||||
|
WHERE po.merchant_id = 1
|
||||||
|
AND po.is_invalid = false
|
||||||
|
GROUP BY po.id, po.human_id, po.fabric
|
||||||
|
ORDER BY po.created_at DESC
|
||||||
|
LIMIT 20;
|
||||||
|
\""
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全注意事项
|
||||||
|
|
||||||
|
1. **只读访问**: 此skill严格限制为只读,任何写操作都会被拒绝
|
||||||
|
2. **认证表保护**: auth_* 表完全禁止访问
|
||||||
|
3. **多租户隔离**: 查询时必须考虑 merchant_id
|
||||||
|
4. **性能考虑**: 大表查询时使用 LIMIT 和适当的 WHERE 条件
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 连接失败
|
||||||
|
```bash
|
||||||
|
# 检查PostgreSQL服务状态
|
||||||
|
sudo systemctl status postgresql
|
||||||
|
|
||||||
|
# 检查数据库进程
|
||||||
|
ps aux | grep postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限错误
|
||||||
|
```bash
|
||||||
|
# 检查数据库用户权限
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "\du"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 更多信息
|
||||||
|
|
||||||
|
详细的查询示例和最佳实践请参考 [SKILL.md](./SKILL.md)。
|
||||||
493
.skills/flower-db-access/SKILL.md
Normal file
493
.skills/flower-db-access/SKILL.md
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
---
|
||||||
|
name: flower-db-access
|
||||||
|
description: Read-only database access for the Flower ERP system. Allows querying all tables except authentication/authorization tables. Use this skill to inspect business data, analyze trends, debug issues, or generate reports. All write operations are strictly prohibited.
|
||||||
|
allowed-tools: Bash(ssh:*), Bash(psql:*), Bash(PGPASSWORD:*)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Flower ERP Database Access (Read-Only)
|
||||||
|
|
||||||
|
This skill provides read-only access to the Flower ERP PostgreSQL database for data inspection, analysis, and debugging.
|
||||||
|
|
||||||
|
## ⚠️ IMPORTANT: Remote Access Required
|
||||||
|
|
||||||
|
**This database is hosted on a remote server. You MUST establish an SSH connection first before accessing the database.**
|
||||||
|
|
||||||
|
### SSH Connection Details
|
||||||
|
|
||||||
|
- **Remote Server**: `8.148.215.233`
|
||||||
|
- **SSH Port**: `18762`
|
||||||
|
- **SSH User**: `root`
|
||||||
|
- **Authentication**: SSH Key (passwordless)
|
||||||
|
- **Database Host** (on remote): `127.0.0.1`
|
||||||
|
- **Database Port**: `5432`
|
||||||
|
|
||||||
|
### Step 1: Establish SSH Connection
|
||||||
|
|
||||||
|
#### Option A: Interactive SSH Session
|
||||||
|
```bash
|
||||||
|
# Connect to remote server (SSH key authentication)
|
||||||
|
ssh -p 18762 root@8.148.215.233
|
||||||
|
|
||||||
|
# Once connected, access the database
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option B: SSH with Command Execution (Recommended for Agents)
|
||||||
|
```bash
|
||||||
|
# Execute single query via SSH
|
||||||
|
ssh -p 18762 root@8.148.215.233 "PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c 'YOUR_QUERY'"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option C: SSH Tunnel (For Local Development)
|
||||||
|
```bash
|
||||||
|
# Create SSH tunnel to access remote database as if it's local
|
||||||
|
ssh -p 18762 -L 15432:127.0.0.1:5432 root@8.148.215.233 -N
|
||||||
|
|
||||||
|
# In another terminal, connect to the tunneled database
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 15432 -U postgres -d flower
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Connection
|
||||||
|
|
||||||
|
**After establishing SSH connection**, connect to the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Connect to the database (on remote server)
|
||||||
|
psql -h 127.0.0.1 -p 5432 -U postgres -d flower
|
||||||
|
|
||||||
|
# Or with password in one line
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "YOUR_QUERY"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Connection Details (on remote server):**
|
||||||
|
- Host: `127.0.0.1`
|
||||||
|
- Port: `5432`
|
||||||
|
- Database: `flower`
|
||||||
|
- User: `postgres`
|
||||||
|
- Password: `postgres`
|
||||||
|
|
||||||
|
## Access Rules
|
||||||
|
|
||||||
|
### ✅ Allowed Operations
|
||||||
|
|
||||||
|
- **SELECT queries** on all business tables
|
||||||
|
- **Data inspection** for debugging and analysis
|
||||||
|
- **Aggregate queries** for reporting
|
||||||
|
- **JOIN queries** across business tables
|
||||||
|
|
||||||
|
### ❌ Prohibited Operations
|
||||||
|
|
||||||
|
1. **All write operations:**
|
||||||
|
- INSERT
|
||||||
|
- UPDATE
|
||||||
|
- DELETE
|
||||||
|
- TRUNCATE
|
||||||
|
- DROP
|
||||||
|
- ALTER
|
||||||
|
- CREATE
|
||||||
|
- GRANT
|
||||||
|
- REVOKE
|
||||||
|
|
||||||
|
2. **Prohibited tables (authentication/authorization):**
|
||||||
|
- `auth_user`
|
||||||
|
- `auth_permission`
|
||||||
|
- `auth_group`
|
||||||
|
- `auth_group_permissions`
|
||||||
|
- `auth_user_groups`
|
||||||
|
- `auth_user_user_permissions`
|
||||||
|
- `django_content_type`
|
||||||
|
- `django_session`
|
||||||
|
- `django_admin_log`
|
||||||
|
|
||||||
|
**⚠️ CRITICAL:** Never attempt to read or write to authentication-related tables. Violations will be logged and reported.
|
||||||
|
|
||||||
|
## Database Schema Overview
|
||||||
|
|
||||||
|
### Core Business Tables
|
||||||
|
|
||||||
|
#### Basic Info Module (`basic_info` app)
|
||||||
|
- `basic_info_merchant` - 商户(多租户)
|
||||||
|
- `basic_info_customer` - 客户
|
||||||
|
- `basic_info_supplier` - 供应商
|
||||||
|
- `basic_info_employee` - 员工
|
||||||
|
- `basic_info_product` - 产品
|
||||||
|
- `basic_info_productcategory` - 产品分类
|
||||||
|
- `basic_info_warehouse` - 仓库
|
||||||
|
- `basic_info_bankaccount` - 银行账户
|
||||||
|
- `basic_info_merchantsetting` - 商户设置
|
||||||
|
|
||||||
|
#### Business Module (`business` app)
|
||||||
|
- `business_purchaseorder` - 采购订单
|
||||||
|
- `business_purchaseorderitem` - 采购订单明细
|
||||||
|
- `business_salesorder` - 销售订单
|
||||||
|
- `business_salesorderitem` - 销售订单明细
|
||||||
|
- `business_purchasereturnorder` - 采购退货单
|
||||||
|
- `business_purchasereturnorderitem` - 采购退货单明细
|
||||||
|
- `business_salesreturnorder` - 销售退货单
|
||||||
|
- `business_salesreturnorderitem` - 销售退货单明细
|
||||||
|
- `business_paymentorder` - 付款单
|
||||||
|
- `business_receiptorder` - 收款单
|
||||||
|
- `business_customerbalance` - 客户余额
|
||||||
|
- `business_supplierbalance` - 供应商余额
|
||||||
|
- `business_balancechangerecord` - 余额变动记录
|
||||||
|
|
||||||
|
#### Stock Module (`stock` app)
|
||||||
|
- `stock_stockchangerecord` - 库存变动记录
|
||||||
|
- `stock_stockchangedetail` - 库存变动明细
|
||||||
|
- `stock_stockfreeze` - 库存冻结记录
|
||||||
|
- `stock_inventory` - 库存汇总
|
||||||
|
|
||||||
|
#### Printing Module (`printing` app)
|
||||||
|
- `printing_plateorder` - 开版订单
|
||||||
|
- `printing_printingorder` - 印染订单
|
||||||
|
- `printing_printingjob` - 印染任务明细
|
||||||
|
- `printing_printingjobbatchadvancerecord` - 批量操作记录
|
||||||
|
- `printing_plateordertiiauploadfailure` - 图片上传失败记录
|
||||||
|
|
||||||
|
#### Stateflow Module (`stateflow` app)
|
||||||
|
- `stateflow_process` - 流程定义
|
||||||
|
- `stateflow_state` - 流程节点
|
||||||
|
- `stateflow_businessobject` - 业务对象(流程实例)
|
||||||
|
- `stateflow_stateflowrecord` - 状态流转记录
|
||||||
|
- `stateflow_stateparameter` - 状态参数
|
||||||
|
- `stateflow_statelogparameterrecord` - 参数记录
|
||||||
|
|
||||||
|
#### Other Modules
|
||||||
|
- `shipment_shipment` - 发货单
|
||||||
|
- `settlement_dailysettlement` - 日结记录
|
||||||
|
- `api_v1_mdyplateorderstaging` - 明道云开版暂存
|
||||||
|
|
||||||
|
## Common Query Examples
|
||||||
|
|
||||||
|
**Note**: All queries below assume you're either:
|
||||||
|
1. Already connected to the remote server via SSH, OR
|
||||||
|
|
||||||
|
### Quick Examples with SSH
|
||||||
|
|
||||||
|
#### Method 1: Using SSH key authentication
|
||||||
|
```bash
|
||||||
|
# List all tables
|
||||||
|
ssh -p 18762 root@8.148.215.233 \
|
||||||
|
"PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c '\dt'"
|
||||||
|
|
||||||
|
# Query with WHERE clause
|
||||||
|
ssh -p 18762 root@8.148.215.233 \
|
||||||
|
"PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c 'SELECT * FROM business_salesorder LIMIT 10'"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Method: SSH with command execution
|
||||||
|
```bash
|
||||||
|
# List all tables
|
||||||
|
ssh -p 18762 root@8.148.215.233 "PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c '\dt'"
|
||||||
|
|
||||||
|
# Query recent orders
|
||||||
|
ssh -p 18762 root@8.148.215.233 "PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c 'SELECT id, customer_id, sales_date FROM business_salesorder ORDER BY created_at DESC LIMIT 10'"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### List all tables in a schema
|
||||||
|
```bash
|
||||||
|
ssh -p 18762 root@8.148.215.233 \
|
||||||
|
"PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c '\dt'"
|
||||||
|
|
||||||
|
# Or if already in SSH session
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "\dt"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Describe table structure
|
||||||
|
```bash
|
||||||
|
ssh -p 18762 root@8.148.215.233 \
|
||||||
|
"PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c '\d business_purchaseorder'"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Query recent orders
|
||||||
|
```bash
|
||||||
|
ssh -p 18762 root@8.148.215.233 \
|
||||||
|
"PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c \"
|
||||||
|
SELECT id, customer_id, sales_date, status, created_at
|
||||||
|
FROM business_salesorder
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
\""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query with JOIN
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT
|
||||||
|
so.id,
|
||||||
|
so.sales_date,
|
||||||
|
c.name as customer_name,
|
||||||
|
so.status
|
||||||
|
FROM business_salesorder so
|
||||||
|
JOIN basic_info_customer c ON so.customer_id = c.id
|
||||||
|
ORDER BY so.created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aggregate query (statistics)
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT
|
||||||
|
DATE(created_at) as date,
|
||||||
|
COUNT(*) as order_count,
|
||||||
|
SUM(total_amount) as total_sales
|
||||||
|
FROM business_salesorder
|
||||||
|
WHERE status = 2 -- APPROVED
|
||||||
|
GROUP BY DATE(created_at)
|
||||||
|
ORDER BY date DESC
|
||||||
|
LIMIT 7;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search text fields (case-insensitive)
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT id, fabric, craft, customer_id
|
||||||
|
FROM printing_printingorder
|
||||||
|
WHERE fabric ILIKE '%棉%'
|
||||||
|
OR craft ILIKE '%活性%';
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query inventory status
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT
|
||||||
|
p.name as product_name,
|
||||||
|
w.name as warehouse,
|
||||||
|
si.quantity,
|
||||||
|
p.unit
|
||||||
|
FROM stock_inventory si
|
||||||
|
JOIN basic_info_product p ON si.product_id = p.id
|
||||||
|
JOIN basic_info_warehouse w ON si.warehouse_id = w.id
|
||||||
|
WHERE si.quantity > 0
|
||||||
|
ORDER BY si.quantity DESC;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query workflow progress
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT
|
||||||
|
po.id as plate_order_id,
|
||||||
|
po.style_name,
|
||||||
|
p.name as process_name,
|
||||||
|
s.name as current_state,
|
||||||
|
COUNT(sfr.id) as completed_steps
|
||||||
|
FROM printing_plateorder po
|
||||||
|
LEFT JOIN stateflow_businessobject bo ON bo.plate_order_id = po.id
|
||||||
|
LEFT JOIN stateflow_process p ON bo.process_id = p.id
|
||||||
|
LEFT JOIN stateflow_stateflowrecord sfr ON sfr.business_object_id = bo.id AND sfr.is_cancelled = false
|
||||||
|
LEFT JOIN stateflow_state s ON sfr.state_id = s.id
|
||||||
|
GROUP BY po.id, po.style_name, p.name, s.name
|
||||||
|
ORDER BY po.created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety Guidelines
|
||||||
|
|
||||||
|
### Before Running Queries
|
||||||
|
|
||||||
|
1. **Always use SELECT** - Never use INSERT, UPDATE, DELETE, or other modifying commands
|
||||||
|
2. **Avoid prohibited tables** - Do not query authentication tables
|
||||||
|
3. **Use LIMIT** - Always add LIMIT clause to prevent large result sets
|
||||||
|
4. **Test with EXPLAIN** - For complex queries, use EXPLAIN ANALYZE to check performance
|
||||||
|
|
||||||
|
### Example with EXPLAIN
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
EXPLAIN ANALYZE
|
||||||
|
SELECT * FROM business_salesorder WHERE customer_id = 1;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Best Practices
|
||||||
|
|
||||||
|
1. **Use specific columns instead of \***:
|
||||||
|
```sql
|
||||||
|
SELECT id, name, created_at FROM basic_info_customer;
|
||||||
|
-- Instead of: SELECT * FROM basic_info_customer;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add WHERE clauses to filter data**:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM business_salesorder
|
||||||
|
WHERE created_at >= '2024-01-01'
|
||||||
|
LIMIT 100;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Use indexes effectively**:
|
||||||
|
- Most foreign keys have indexes
|
||||||
|
- `created_at`, `updated_at` are commonly indexed
|
||||||
|
- Check with `\d table_name` to see indexes
|
||||||
|
|
||||||
|
4. **Avoid expensive operations on large tables**:
|
||||||
|
- Be careful with `LIKE '%text%'` patterns (cannot use indexes)
|
||||||
|
- Use `ILIKE` for case-insensitive matching
|
||||||
|
- Consider using `LIMIT` with text searches
|
||||||
|
|
||||||
|
## Useful psql Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all databases
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -c "\l"
|
||||||
|
|
||||||
|
# List all tables in current database
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "\dt"
|
||||||
|
|
||||||
|
# List all schemas
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "\dn"
|
||||||
|
|
||||||
|
# Describe table structure
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "\d table_name"
|
||||||
|
|
||||||
|
# List indexes on a table
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "\di table_name"
|
||||||
|
|
||||||
|
# View table size
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
tablename,
|
||||||
|
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||||
|
"
|
||||||
|
|
||||||
|
# Exit psql
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Tenant Considerations
|
||||||
|
|
||||||
|
**IMPORTANT:** This is a multi-tenant system. All business tables have a `merchant_id` column.
|
||||||
|
|
||||||
|
### Always include merchant_id in queries
|
||||||
|
```sql
|
||||||
|
-- Good: Filter by merchant
|
||||||
|
SELECT * FROM business_salesorder
|
||||||
|
WHERE merchant_id = 1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
|
||||||
|
-- Bad: Cross-tenant query
|
||||||
|
SELECT * FROM business_salesorder LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check merchant context
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT id, name, type FROM basic_info_merchant;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### 1. Debug Order Issues
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT
|
||||||
|
o.*,
|
||||||
|
c.name as customer_name
|
||||||
|
FROM business_salesorder o
|
||||||
|
JOIN basic_info_customer c ON o.customer_id = c.id
|
||||||
|
WHERE o.id = 123;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Check Inventory Levels
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT
|
||||||
|
p.name,
|
||||||
|
w.name as warehouse,
|
||||||
|
ss.quantity,
|
||||||
|
ss.unit
|
||||||
|
FROM stock_stocksummary ss
|
||||||
|
JOIN basic_info_product p ON ss.product_id = p.id
|
||||||
|
JOIN basic_info_warehouse w ON ss.warehouse_id = w.id
|
||||||
|
WHERE ss.merchant_id = 1
|
||||||
|
AND ss.quantity < 100
|
||||||
|
ORDER BY ss.quantity ASC;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Analyze Sales Trends
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('week', sales_date) as week,
|
||||||
|
COUNT(*) as orders,
|
||||||
|
SUM(
|
||||||
|
(SELECT SUM(quantity * price)
|
||||||
|
FROM business_salesorderitem
|
||||||
|
WHERE sales_order_id = business_salesorder.id)
|
||||||
|
) as revenue
|
||||||
|
FROM business_salesorder
|
||||||
|
WHERE merchant_id = 1
|
||||||
|
AND status = 2 -- APPROVED
|
||||||
|
AND sales_date >= CURRENT_DATE - INTERVAL '4 weeks'
|
||||||
|
GROUP BY DATE_TRUNC('week', sales_date)
|
||||||
|
ORDER BY week DESC;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Track Printing Job Progress
|
||||||
|
```bash
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
SELECT
|
||||||
|
po.human_id,
|
||||||
|
po.fabric,
|
||||||
|
COUNT(pj.id) as job_count,
|
||||||
|
SUM(CASE WHEN pj.business_object_id IS NOT NULL THEN 1 ELSE 0 END) as started_jobs
|
||||||
|
FROM printing_printingorder po
|
||||||
|
LEFT JOIN printing_printingjob pj ON pj.printing_order_id = po.id
|
||||||
|
WHERE po.merchant_id = 1
|
||||||
|
AND po.is_invalid = false
|
||||||
|
GROUP BY po.id, po.human_id, po.fabric
|
||||||
|
ORDER BY po.created_at DESC
|
||||||
|
LIMIT 20;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection refused
|
||||||
|
```bash
|
||||||
|
# Check if PostgreSQL is running
|
||||||
|
sudo systemctl status postgresql
|
||||||
|
|
||||||
|
# Or check process
|
||||||
|
ps aux | grep postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission denied
|
||||||
|
```bash
|
||||||
|
# Check database permissions
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "\du"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query too slow
|
||||||
|
```bash
|
||||||
|
# Use EXPLAIN to analyze
|
||||||
|
PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d flower -c "
|
||||||
|
EXPLAIN (ANALYZE, BUFFERS)
|
||||||
|
YOUR_QUERY_HERE;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Emergency Contacts
|
||||||
|
|
||||||
|
If you encounter any issues or accidentally attempt a write operation:
|
||||||
|
|
||||||
|
1. **STOP** - Do not proceed with the operation
|
||||||
|
2. **REPORT** - Document what happened
|
||||||
|
3. **CONTACT** - Notify system administrator immediately
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember:** This skill provides READ-ONLY access for a reason. The integrity of business data is critical. When in doubt, ask for clarification rather than risk data corruption.
|
||||||
152
.skills/flower-db-access/examples-remote.sh
Executable file
152
.skills/flower-db-access/examples-remote.sh
Executable file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Flower Database Access - 远程访问示例脚本
|
||||||
|
# 通过SSH连接到远程服务器执行数据库查询
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# SSH连接参数
|
||||||
|
SSH_HOST="8.148.215.233"
|
||||||
|
SSH_PORT="18762"
|
||||||
|
SSH_USER="root"
|
||||||
|
|
||||||
|
# 数据库连接参数(在远程服务器上)
|
||||||
|
DB_HOST="127.0.0.1"
|
||||||
|
DB_PORT="5432"
|
||||||
|
DB_NAME="flower"
|
||||||
|
DB_USER="postgres"
|
||||||
|
DB_PASS="postgres"
|
||||||
|
|
||||||
|
# SSH命令前缀
|
||||||
|
SSH_CMD="ssh -p $SSH_PORT $SSH_USER@$SSH_HOST"
|
||||||
|
|
||||||
|
# 数据库查询命令
|
||||||
|
function db_query() {
|
||||||
|
$SSH_CMD "PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c \"$1\""
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Flower ERP 数据库远程访问示例 ==="
|
||||||
|
echo "远程服务器: $SSH_HOST:$SSH_PORT"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 测试SSH连接
|
||||||
|
echo "=== 测试SSH连接 ==="
|
||||||
|
echo "---"
|
||||||
|
if $SSH_CMD "echo 'SSH连接成功'" &> /dev/null; then
|
||||||
|
echo "✓ SSH连接正常"
|
||||||
|
else
|
||||||
|
echo "✗ SSH连接失败,请检查SSH key配置"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例1: 查询商户信息
|
||||||
|
echo "1. 查询商户列表:"
|
||||||
|
echo "---"
|
||||||
|
db_query "
|
||||||
|
SELECT id, name, type, created_at
|
||||||
|
FROM basic_info_merchant
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 5;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例2: 查询最近的销售订单
|
||||||
|
echo "2. 查询最近10条销售订单:"
|
||||||
|
echo "---"
|
||||||
|
db_query "
|
||||||
|
SELECT
|
||||||
|
so.id,
|
||||||
|
so.sales_date,
|
||||||
|
c.name as customer_name,
|
||||||
|
so.status,
|
||||||
|
so.created_at
|
||||||
|
FROM business_salesorder so
|
||||||
|
JOIN basic_info_customer c ON so.customer_id = c.id
|
||||||
|
ORDER BY so.created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例3: 查询库存
|
||||||
|
echo "3. 查询库存状态(前10个):"
|
||||||
|
echo "---"
|
||||||
|
db_query "
|
||||||
|
SELECT
|
||||||
|
p.name as product_name,
|
||||||
|
w.name as warehouse,
|
||||||
|
si.quantity,
|
||||||
|
p.unit
|
||||||
|
FROM stock_inventory si
|
||||||
|
JOIN basic_info_product p ON si.product_id = p.id
|
||||||
|
JOIN basic_info_warehouse w ON si.warehouse_id = w.id
|
||||||
|
WHERE si.quantity > 0
|
||||||
|
ORDER BY si.quantity DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例4: 查询印染订单统计
|
||||||
|
echo "4. 印染订单统计(最近7天):"
|
||||||
|
echo "---"
|
||||||
|
db_query "
|
||||||
|
SELECT
|
||||||
|
DATE(created_at) as date,
|
||||||
|
COUNT(*) as total_orders,
|
||||||
|
SUM(CASE WHEN is_urgent THEN 1 ELSE 0 END) as urgent_orders,
|
||||||
|
SUM(CASE WHEN is_fabric_received THEN 1 ELSE 0 END) as fabric_received
|
||||||
|
FROM printing_printingorder
|
||||||
|
WHERE created_at >= CURRENT_DATE - INTERVAL '7 days'
|
||||||
|
GROUP BY DATE(created_at)
|
||||||
|
ORDER BY date DESC;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例5: 查询开版订单进度
|
||||||
|
echo "5. 开版订单进度统计:"
|
||||||
|
echo "---"
|
||||||
|
db_query "
|
||||||
|
SELECT
|
||||||
|
po.id,
|
||||||
|
po.style_name,
|
||||||
|
po.plate_type,
|
||||||
|
po.urgency_level,
|
||||||
|
po.is_ordered,
|
||||||
|
po.created_at
|
||||||
|
FROM printing_plateorder po
|
||||||
|
WHERE po.is_invalid = false
|
||||||
|
ORDER BY po.created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例6: 查询最大的表
|
||||||
|
echo "6. 数据库表大小统计(前10):"
|
||||||
|
echo "---"
|
||||||
|
db_query "
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
tablename,
|
||||||
|
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例7: 使用SSH隧道
|
||||||
|
echo "7. SSH隧道方式(可选):"
|
||||||
|
echo "---"
|
||||||
|
echo "如果需要本地访问,可以创建SSH隧道:"
|
||||||
|
echo " ssh -p $SSH_PORT -L 15432:127.0.0.1:5432 $SSH_USER@$SSH_HOST -N"
|
||||||
|
echo " 然后在另一个终端:"
|
||||||
|
echo " PGPASSWORD=$DB_PASS psql -h 127.0.0.1 -p 15432 -U $DB_USER -d $DB_NAME"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "=== 示例完成 ==="
|
||||||
|
echo "更多查询示例请查看 SKILL.md 文件"
|
||||||
|
echo
|
||||||
|
echo "💡 提示:"
|
||||||
|
echo " - 所有查询都是只读的"
|
||||||
|
echo " - 不要访问 auth_* 认证表"
|
||||||
|
echo " - 查询时考虑 merchant_id(多租户)"
|
||||||
113
.skills/flower-db-access/examples.sh
Executable file
113
.skills/flower-db-access/examples.sh
Executable file
@@ -0,0 +1,113 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Flower Database Access - 示例脚本
|
||||||
|
# 这个脚本演示了如何使用数据库访问skill
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 数据库连接参数
|
||||||
|
export DB_HOST="127.0.0.1"
|
||||||
|
export DB_PORT="5432"
|
||||||
|
export DB_NAME="flower"
|
||||||
|
export DB_USER="postgres"
|
||||||
|
export DB_PASS="postgres"
|
||||||
|
|
||||||
|
echo "=== Flower ERP 数据库访问示例 ==="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例1: 查询商户信息
|
||||||
|
echo "1. 查询商户列表:"
|
||||||
|
echo "---"
|
||||||
|
PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "
|
||||||
|
SELECT id, name, type, created_at
|
||||||
|
FROM basic_info_merchant
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 5;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例2: 查询最近的销售订单
|
||||||
|
echo "2. 查询最近10条销售订单:"
|
||||||
|
echo "---"
|
||||||
|
PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "
|
||||||
|
SELECT
|
||||||
|
so.id,
|
||||||
|
so.sales_date,
|
||||||
|
c.name as customer_name,
|
||||||
|
so.status,
|
||||||
|
so.created_at
|
||||||
|
FROM business_salesorder so
|
||||||
|
JOIN basic_info_customer c ON so.customer_id = c.id
|
||||||
|
ORDER BY so.created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例3: 查询库存
|
||||||
|
echo "3. 查询库存状态(前10个):"
|
||||||
|
echo "---"
|
||||||
|
PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "
|
||||||
|
SELECT
|
||||||
|
p.name as product_name,
|
||||||
|
w.name as warehouse,
|
||||||
|
si.quantity,
|
||||||
|
p.unit
|
||||||
|
FROM stock_inventory si
|
||||||
|
JOIN basic_info_product p ON si.product_id = p.id
|
||||||
|
JOIN basic_info_warehouse w ON si.warehouse_id = w.id
|
||||||
|
WHERE si.quantity > 0
|
||||||
|
ORDER BY si.quantity DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例4: 查询印染订单统计
|
||||||
|
echo "4. 印染订单统计(最近7天):"
|
||||||
|
echo "---"
|
||||||
|
PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "
|
||||||
|
SELECT
|
||||||
|
DATE(created_at) as date,
|
||||||
|
COUNT(*) as total_orders,
|
||||||
|
SUM(CASE WHEN is_urgent THEN 1 ELSE 0 END) as urgent_orders,
|
||||||
|
SUM(CASE WHEN is_fabric_received THEN 1 ELSE 0 END) as fabric_received
|
||||||
|
FROM printing_printingorder
|
||||||
|
WHERE created_at >= CURRENT_DATE - INTERVAL '7 days'
|
||||||
|
GROUP BY DATE(created_at)
|
||||||
|
ORDER BY date DESC;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例5: 查询开版订单进度
|
||||||
|
echo "5. 开版订单进度统计:"
|
||||||
|
echo "---"
|
||||||
|
PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "
|
||||||
|
SELECT
|
||||||
|
po.id,
|
||||||
|
po.style_name,
|
||||||
|
po.plate_type,
|
||||||
|
po.urgency_level,
|
||||||
|
po.is_ordered,
|
||||||
|
po.created_at
|
||||||
|
FROM printing_plateorder po
|
||||||
|
WHERE po.is_invalid = false
|
||||||
|
ORDER BY po.created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 示例6: 查询最大的表
|
||||||
|
echo "6. 数据库表大小统计(前10):"
|
||||||
|
echo "---"
|
||||||
|
PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
tablename,
|
||||||
|
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
|
||||||
|
LIMIT 10;
|
||||||
|
"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "=== 示例完成 ==="
|
||||||
|
echo "更多查询示例请查看 SKILL.md 文件"
|
||||||
46
.skills/flower-db-access/query.sh
Executable file
46
.skills/flower-db-access/query.sh
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Flower Database Quick Query Tool
|
||||||
|
# 快速查询工具 - 用于执行单个SQL查询
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# SSH连接参数
|
||||||
|
SSH_HOST="8.148.215.233"
|
||||||
|
SSH_PORT="18762"
|
||||||
|
SSH_USER="root"
|
||||||
|
|
||||||
|
# 数据库连接参数(在远程服务器上)
|
||||||
|
DB_HOST="127.0.0.1"
|
||||||
|
DB_PORT="5432"
|
||||||
|
DB_NAME="flower"
|
||||||
|
DB_USER="postgres"
|
||||||
|
DB_PASS="postgres"
|
||||||
|
|
||||||
|
# 使用方法
|
||||||
|
usage() {
|
||||||
|
echo "用法: $0 'SQL查询语句'"
|
||||||
|
echo
|
||||||
|
echo "示例:"
|
||||||
|
echo " $0 'SELECT * FROM business_salesorder LIMIT 10'"
|
||||||
|
echo " $0 'SELECT COUNT(*) FROM printing_plateorder'"
|
||||||
|
echo
|
||||||
|
echo "或者通过管道传递SQL:"
|
||||||
|
echo " echo 'SELECT * FROM basic_info_customer LIMIT 5' | $0"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查参数
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
# 从命令行参数读取
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
QUERY="$*"
|
||||||
|
else
|
||||||
|
# 从标准输入读取
|
||||||
|
QUERY=$(cat)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 执行查询
|
||||||
|
ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||||||
|
"PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c \"$QUERY\""
|
||||||
@@ -653,6 +653,22 @@ class PlateOrderAPITestCase(TestCase):
|
|||||||
self.assertIn(order2.id, ids)
|
self.assertIn(order2.id, ids)
|
||||||
self.assertNotIn(order1.id, ids)
|
self.assertNotIn(order1.id, ids)
|
||||||
self.assertNotIn(order3.id, ids)
|
self.assertNotIn(order3.id, ids)
|
||||||
|
|
||||||
|
def test_filter_by_plate_date_range_over_10_days_returns_400(self):
|
||||||
|
"""测试按开版日期筛选时,跨度超过 10 天返回 400"""
|
||||||
|
response = self.client.get('/api/v1/plate-orders/?plate_date_from=2025-01-01&plate_date_to=2025-01-11')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn('查询跨度不能超过10天', str(response.data))
|
||||||
|
|
||||||
|
def test_filter_by_plate_at_range_over_10_days_returns_400(self):
|
||||||
|
"""测试按开版时间筛选时,跨度超过 10 天返回 400"""
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from_value = quote('2025-01-01T00:00:00+08:00')
|
||||||
|
to_value = quote('2025-01-11T00:00:00+08:00')
|
||||||
|
response = self.client.get(f'/api/v1/plate-orders/?plate_at_from={from_value}&plate_at_to={to_value}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn('查询跨度不能超过10天', str(response.data))
|
||||||
|
|
||||||
def test_search_by_design_code(self):
|
def test_search_by_design_code(self):
|
||||||
"""测试按设计编号搜索"""
|
"""测试按设计编号搜索"""
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Printing API ViewSet
|
|||||||
"""
|
"""
|
||||||
from rest_framework import viewsets, filters, status
|
from rest_framework import viewsets, filters, status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import BasePermission
|
from rest_framework.permissions import BasePermission
|
||||||
from rest_framework.permissions import DjangoModelPermissions
|
from rest_framework.permissions import DjangoModelPermissions
|
||||||
@@ -646,6 +647,7 @@ class HasActivatePlateOrderPermission(BasePermission):
|
|||||||
|
|
||||||
class PlateOrderFilterSet(django_filters.FilterSet):
|
class PlateOrderFilterSet(django_filters.FilterSet):
|
||||||
"""开版订单过滤器"""
|
"""开版订单过滤器"""
|
||||||
|
MAX_RANGE_DAYS = 10
|
||||||
customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains')
|
customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains')
|
||||||
customer_phone = django_filters.CharFilter(field_name='customer__mobile', lookup_expr='icontains')
|
customer_phone = django_filters.CharFilter(field_name='customer__mobile', lookup_expr='icontains')
|
||||||
image_name = django_filters.CharFilter(lookup_expr='icontains')
|
image_name = django_filters.CharFilter(lookup_expr='icontains')
|
||||||
@@ -694,6 +696,52 @@ class PlateOrderFilterSet(django_filters.FilterSet):
|
|||||||
return queryset.filter(created_at__lt=next_day)
|
return queryset.filter(created_at__lt=next_day)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_local_date_from_datetime(raw_value):
|
||||||
|
dt = parse_datetime(str(raw_value))
|
||||||
|
if dt is None:
|
||||||
|
raise DRFValidationError('日期时间格式无效')
|
||||||
|
if timezone.is_aware(dt):
|
||||||
|
dt = timezone.localtime(dt)
|
||||||
|
return dt.date()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_date(raw_value):
|
||||||
|
d = parse_date(str(raw_value))
|
||||||
|
if d is None:
|
||||||
|
raise DRFValidationError('日期格式无效')
|
||||||
|
return d
|
||||||
|
|
||||||
|
def _validate_range_days(self, from_key, to_key, mode, label):
|
||||||
|
raw_from = self.data.get(from_key)
|
||||||
|
raw_to = self.data.get(to_key)
|
||||||
|
if not raw_from or not raw_to:
|
||||||
|
return
|
||||||
|
|
||||||
|
if mode == 'datetime':
|
||||||
|
from_date = self._to_local_date_from_datetime(raw_from)
|
||||||
|
to_date = self._to_local_date_from_datetime(raw_to)
|
||||||
|
else:
|
||||||
|
from_date = self._to_date(raw_from)
|
||||||
|
to_date = self._to_date(raw_to)
|
||||||
|
|
||||||
|
days = (to_date - from_date).days + 1
|
||||||
|
if days > self.MAX_RANGE_DAYS:
|
||||||
|
raise DRFValidationError(f'{label}查询跨度不能超过{self.MAX_RANGE_DAYS}天')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def qs(self):
|
||||||
|
self._validate_range_days('plate_date_from', 'plate_date_to', mode='date', label='开版日期')
|
||||||
|
self._validate_range_days('plate_at_from', 'plate_at_to', mode='datetime', label='开版时间')
|
||||||
|
self._validate_range_days(
|
||||||
|
'required_completion_date_from',
|
||||||
|
'required_completion_date_to',
|
||||||
|
mode='datetime',
|
||||||
|
label='要求完成时间',
|
||||||
|
)
|
||||||
|
self._validate_range_days('created_date_from', 'created_date_to', mode='date', label='创建日期')
|
||||||
|
return super().qs
|
||||||
|
|
||||||
|
|
||||||
def _plate_order_last_modified(request, *args, **kwargs):
|
def _plate_order_last_modified(request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|||||||
51
api_v1/views/settlement/mixins.py
Normal file
51
api_v1/views/settlement/mixins.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""Settlement API mixins.
|
||||||
|
|
||||||
|
客户可见性绕过逻辑与 printing 模块的 CustomerVisibilityFilterMixin 对齐:
|
||||||
|
- superuser → 无过滤
|
||||||
|
- 持有 view_all_permission → 无过滤
|
||||||
|
- 其余普通员工 → settlement service 按 created_by / visible_employees 过滤
|
||||||
|
|
||||||
|
settlement.services._get_plate_order_queryset 已通过 ``user=None`` 判断来
|
||||||
|
决定是否施加可见性过滤,故基类只需控制向 service 传入的 user 参数即可。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class SettlementVisibilityMixin:
|
||||||
|
"""
|
||||||
|
Settlement API 客户可见性绕过基类。
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
class MySettlementView(SettlementVisibilityMixin, APIView):
|
||||||
|
view_all_permission = 'printing.view_all_plateorders'
|
||||||
|
|
||||||
|
绕过规则(与 CustomerVisibilityFilterMixin 保持一致):
|
||||||
|
- user.is_superuser → 绕过
|
||||||
|
- user.has_perm(view_all_permission) → 绕过
|
||||||
|
- 其余 → 透传原始 user,service 自行应用 visible_employees 过滤
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 子类可覆盖为自定义权限名
|
||||||
|
view_all_permission: str | None = 'printing.view_all_plateorders'
|
||||||
|
|
||||||
|
def can_bypass_settlement_visibility(self, user) -> bool:
|
||||||
|
"""判断该用户是否应跳过 settlement 客户可见性过滤。"""
|
||||||
|
if user is None:
|
||||||
|
return False
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
if self.view_all_permission and user.has_perm(self.view_all_permission):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_service_user(self, user):
|
||||||
|
"""
|
||||||
|
返回传递给 settlement service 函数的 ``user`` 参数。
|
||||||
|
|
||||||
|
- 可绕过时返回 ``None``:service 不施加客户可见性过滤。
|
||||||
|
- 否则返回原始 user:service 按员工可见性过滤。
|
||||||
|
"""
|
||||||
|
if self.can_bypass_settlement_visibility(user):
|
||||||
|
return None
|
||||||
|
return user
|
||||||
@@ -1,75 +1,122 @@
|
|||||||
"""Settlement API views"""
|
"""Settlement API views"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import status
|
from rest_framework import status, authentication
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
|
from basic_info.models import Merchant
|
||||||
from settlement.services import get_plate_order_summary_by_customer
|
from settlement.services import get_plate_order_summary_by_customer
|
||||||
|
from .mixins import SettlementVisibilityMixin
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
AGENT_SECRET = "RCYH_BOT_0083"
|
||||||
|
|
||||||
class PlateOrderSummaryView(APIView):
|
|
||||||
|
class AgentSecretAuthentication(authentication.BaseAuthentication):
|
||||||
|
"""
|
||||||
|
Agent 密钥认证(仅用于 settlement/plate-orders/summary API)
|
||||||
|
|
||||||
|
当请求头中存在 X-AGENT-SECRET 且值匹配时,跳过 JWT 认证,
|
||||||
|
以 admin 超级管理员身份执行请求。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request):
|
||||||
|
secret = request.META.get("HTTP_X_AGENT_SECRET")
|
||||||
|
if secret != AGENT_SECRET:
|
||||||
|
return None
|
||||||
|
User = get_user_model()
|
||||||
|
try:
|
||||||
|
user = User.objects.get(username="admin", is_superuser=True)
|
||||||
|
return (user, None)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
logger.warning("[AgentSecretAuthentication] admin 用户不存在")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class PlateOrderSummaryView(SettlementVisibilityMixin, APIView):
|
||||||
"""
|
"""
|
||||||
开版订单统计 API
|
开版订单统计 API
|
||||||
|
|
||||||
GET /api/v1/settlement/plate-orders/summary/
|
GET /api/v1/settlement/plate-orders/summary/
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
date: 统计日期(YYYY-MM-DD)
|
date: 统计日期(YYYY-MM-DD)
|
||||||
|
|
||||||
|
认证方式:
|
||||||
|
- JWT Token(标准方式)
|
||||||
|
- X-AGENT-SECRET: RCYH_BOT_0083(Agent 专用,跳过鉴权)
|
||||||
|
|
||||||
|
可见性规则(与 plate-order 列表接口对齐):
|
||||||
|
- superuser 或持有 view_all_permission(默认 printing.view_all_plateorders)
|
||||||
|
→ 返回该商户下全部客户的开版汇总
|
||||||
|
- 普通员工 → 仅返回其负责/可见客户的开版汇总
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
authentication_classes = [
|
||||||
|
AgentSecretAuthentication,
|
||||||
|
authentication.SessionAuthentication,
|
||||||
|
JWTAuthentication,
|
||||||
|
]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
view_all_permission = "printing.view_all_plateorders"
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""
|
"""
|
||||||
获取开版订单统计
|
获取开版订单统计
|
||||||
"""
|
"""
|
||||||
date_str = request.query_params.get('date')
|
date_str = request.query_params.get("date")
|
||||||
|
|
||||||
if not date_str:
|
if not date_str:
|
||||||
return Response(
|
return Response(
|
||||||
{'error': '缺少 date 参数'},
|
{"error": "缺少 date 参数"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str):
|
||||||
return Response(
|
return Response(
|
||||||
{'error': '日期格式错误,请使用 YYYY-MM-DD 格式'},
|
{"error": "日期格式错误,请使用 YYYY-MM-DD 格式"},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
settlement_date = date.fromisoformat(date_str)
|
settlement_date = date.fromisoformat(date_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return Response(
|
return Response({"error": "日期不存在"}, status=status.HTTP_403_FORBIDDEN)
|
||||||
{'error': '日期不存在'},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
emp = getattr(request.user, "employee", None)
|
||||||
)
|
|
||||||
|
|
||||||
emp = getattr(request.user, 'employee', None)
|
|
||||||
if emp is None or emp.merchant is None:
|
if emp is None or emp.merchant is None:
|
||||||
return Response(
|
if request.user.is_superuser:
|
||||||
{'error': '用户未关联商户'},
|
merchant = Merchant.objects.first()
|
||||||
status=status.HTTP_403_FORBIDDEN
|
if merchant is None:
|
||||||
)
|
return Response(
|
||||||
|
{"error": "系统中没有商户"}, status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
merchant_id = merchant.id
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{"error": "用户未关联商户"}, status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
merchant_id = emp.merchant.id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = get_plate_order_summary_by_customer(
|
data = get_plate_order_summary_by_customer(
|
||||||
merchant_id=emp.merchant.id,
|
merchant_id=merchant_id,
|
||||||
settlement_date=settlement_date,
|
settlement_date=settlement_date,
|
||||||
user=request.user
|
user=self.get_service_user(request.user),
|
||||||
)
|
)
|
||||||
return Response({'data': data})
|
return Response({"data": data})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception(f"[settlement.views] 获取开版订单统计失败: {e}")
|
||||||
f'[settlement.views] 获取开版订单统计失败: {e}'
|
|
||||||
)
|
|
||||||
return Response(
|
return Response(
|
||||||
{'error': '获取统计数据失败'},
|
{"error": "获取统计数据失败"},
|
||||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2026-03-05 06:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basic_info', '0024_frontend_page_and_visible_pages'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='customer',
|
||||||
|
constraint=models.UniqueConstraint(fields=('merchant', 'name'), name='uniq_customer_merchant_name'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -439,6 +439,12 @@ class Customer(ModelBase):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = '客户资料'
|
verbose_name = '客户资料'
|
||||||
verbose_name_plural = '客户资料'
|
verbose_name_plural = '客户资料'
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=['merchant', 'name'],
|
||||||
|
name='uniq_customer_merchant_name',
|
||||||
|
),
|
||||||
|
]
|
||||||
permissions = [
|
permissions = [
|
||||||
('view_all_customers', '查看所有客户资料'),
|
('view_all_customers', '查看所有客户资料'),
|
||||||
]
|
]
|
||||||
|
|||||||
96
docs/agent_api_settlement.md
Normal file
96
docs/agent_api_settlement.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# Agent API: 开版订单统计
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
此 API 供 AI Agent 查询开版订单统计数据,按客户分组显示今日和本月订单数量。
|
||||||
|
|
||||||
|
## 认证
|
||||||
|
|
||||||
|
使用 `X-AGENT-SECRET` 请求头进行认证,无需 JWT Token。
|
||||||
|
|
||||||
|
## 端点
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/settlement/plate-orders/summary/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 请求头
|
||||||
|
|
||||||
|
| Header | Value | Required |
|
||||||
|
|--------|-------|----------|
|
||||||
|
| X-AGENT-SECRET | RCYH_BOT_0083 | Yes |
|
||||||
|
|
||||||
|
## 查询参数
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----------|------|----------|-------------|
|
||||||
|
| date | string | Yes | 统计日期,格式: YYYY-MM-DD |
|
||||||
|
|
||||||
|
## 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"client_id": 1,
|
||||||
|
"client_name": "客户名称",
|
||||||
|
"plate_order_count": [
|
||||||
|
{
|
||||||
|
"type": "首版-匹布",
|
||||||
|
"today": 5,
|
||||||
|
"current_month": 120
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字段说明
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| client_id | integer | 客户ID |
|
||||||
|
| client_name | string | 客户名称 |
|
||||||
|
| plate_order_count | array | 订单统计数组 |
|
||||||
|
| plate_order_count[].type | string | 订单类型,格式: `{版型}-{做货方式}` |
|
||||||
|
| plate_order_count[].today | integer | 今日订单数 |
|
||||||
|
| plate_order_count[].current_month | integer | 本月累计订单数(从月初到查询日期) |
|
||||||
|
|
||||||
|
### 订单类型 (type) 可能值
|
||||||
|
|
||||||
|
- `首版-匹布`: 首版订单,匹布方式
|
||||||
|
- `首版-做货`: 首版订单,做货方式
|
||||||
|
- `复版-匹布`: 复版订单,匹布方式
|
||||||
|
- `复版-做货`: 复版订单,做货方式
|
||||||
|
- `修改单-匹布`: 修改订单,匹布方式
|
||||||
|
- `修改单-做货`: 修改订单,做货方式
|
||||||
|
|
||||||
|
## 错误响应
|
||||||
|
|
||||||
|
| Status | Response |
|
||||||
|
|--------|----------|
|
||||||
|
| 400 | `{"error": "缺少 date 参数"}` |
|
||||||
|
| 400 | `{"error": "日期格式错误,请使用 YYYY-MM-DD 格式"}` |
|
||||||
|
| 403 | `{"error": "日期不存在"}` |
|
||||||
|
| 401 | `{"detail": "身份认证信息未提供。"}` (secret 错误) |
|
||||||
|
|
||||||
|
## cURL 调用示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET \
|
||||||
|
-H "X-AGENT-SECRET: RCYH_BOT_0083" \
|
||||||
|
"http://localhost:8100/api/v1/settlement/plate-orders/summary/?date=2026-02-24"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
1. **查询今日开版统计**: 使用当天日期
|
||||||
|
2. **查询历史日期统计**: 使用指定日期
|
||||||
|
3. **对比不同客户订单量**: 遍历 data 数组
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 返回数据仅包含有订单的客户(today 或 current_month > 0)
|
||||||
|
- current_month 统计范围:月初第一天到查询日期
|
||||||
|
- 此 API 仅限 Agent 调用,普通用户需使用 JWT 认证
|
||||||
@@ -71,6 +71,7 @@ TENCENTCLOUD_TIIA_QPS=10
|
|||||||
############################
|
############################
|
||||||
# 为空时发送会报错:WeCom webhook key 未配置
|
# 为空时发送会报错:WeCom webhook key 未配置
|
||||||
WECOM_WEBHOOK_KEY=
|
WECOM_WEBHOOK_KEY=
|
||||||
|
SPEAK_ENDPOINT=http://8.148.215.233:9004/speak
|
||||||
|
|
||||||
# PrintingJob 状态推进通知的“跟进地址”模板;为空则消息里省略“跟进地址”字段
|
# PrintingJob 状态推进通知的“跟进地址”模板;为空则消息里省略“跟进地址”字段
|
||||||
PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE=https://app.yuwen.cloud/workstation/production/batch-advance?orderId={order_id}
|
PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE=https://app.yuwen.cloud/workstation/production/batch-advance?orderId={order_id}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ TENCENTCLOUD_TIIA_QPS = env.int('TENCENTCLOUD_TIIA_QPS', default=10) # Tencent
|
|||||||
# 使用:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx
|
# 使用:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx
|
||||||
WECOM_WEBHOOK_BASE_URL = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send'
|
WECOM_WEBHOOK_BASE_URL = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send'
|
||||||
WECOM_WEBHOOK_KEY = env('WECOM_WEBHOOK_KEY', default='')
|
WECOM_WEBHOOK_KEY = env('WECOM_WEBHOOK_KEY', default='')
|
||||||
|
SPEAK_ENDPOINT = env('SPEAK_ENDPOINT', default='http://8.148.215.233:9004/speak')
|
||||||
|
|
||||||
|
|
||||||
# CORS 配置
|
# CORS 配置
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ from .mingdaoyun import (
|
|||||||
flatten_plate_order_related_row,
|
flatten_plate_order_related_row,
|
||||||
sync_fabric_from_mingdaoyun,
|
sync_fabric_from_mingdaoyun,
|
||||||
)
|
)
|
||||||
|
from .speech import SpeakResponse, play_speech
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# client
|
# client
|
||||||
@@ -113,4 +114,6 @@ __all__ = [
|
|||||||
"fetch_row_by_rowid_from_mingdaoyun",
|
"fetch_row_by_rowid_from_mingdaoyun",
|
||||||
"fetch_plate_orders_from_mingdaoyun",
|
"fetch_plate_orders_from_mingdaoyun",
|
||||||
"sync_fabric_from_mingdaoyun",
|
"sync_fabric_from_mingdaoyun",
|
||||||
|
"SpeakResponse",
|
||||||
|
"play_speech",
|
||||||
]
|
]
|
||||||
|
|||||||
67
flower/utils/speech.py
Normal file
67
flower/utils/speech.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
"""语音播报工具。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SpeakResponse:
|
||||||
|
status_code: int
|
||||||
|
raw: Any
|
||||||
|
|
||||||
|
|
||||||
|
def play_speech(
|
||||||
|
*,
|
||||||
|
text: str,
|
||||||
|
endpoint: str | None = None,
|
||||||
|
timeout_seconds: float = 10.0,
|
||||||
|
) -> SpeakResponse:
|
||||||
|
"""调用语音播报服务。
|
||||||
|
|
||||||
|
默认 endpoint 读取 settings.SPEAK_ENDPOINT。
|
||||||
|
请求格式:POST JSON {'text': '<text>'}
|
||||||
|
"""
|
||||||
|
content = (text or '').strip()
|
||||||
|
if not content:
|
||||||
|
raise ValueError('text 不能为空')
|
||||||
|
|
||||||
|
url = (endpoint or getattr(settings, 'SPEAK_ENDPOINT', '') or '').strip()
|
||||||
|
if not url:
|
||||||
|
raise ValueError('SPEAK_ENDPOINT 未配置')
|
||||||
|
|
||||||
|
payload = {'text': content}
|
||||||
|
data = json.dumps(payload, ensure_ascii=False).encode('utf-8')
|
||||||
|
req = Request(
|
||||||
|
url=url,
|
||||||
|
data=data,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
method='POST',
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=float(timeout_seconds)) as resp:
|
||||||
|
body = resp.read().decode('utf-8', errors='replace')
|
||||||
|
status_code = int(getattr(resp, 'status', 200) or 200)
|
||||||
|
except HTTPError as exc:
|
||||||
|
body = ''
|
||||||
|
try:
|
||||||
|
body = exc.read().decode('utf-8', errors='replace')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise RuntimeError(f'Speak service HTTPError: status={exc.code}, body={body}') from exc
|
||||||
|
except URLError as exc:
|
||||||
|
raise RuntimeError(f'Speak service URLError: {exc}') from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = json.loads(body) if body else {}
|
||||||
|
except Exception:
|
||||||
|
raw = {'raw_text': body}
|
||||||
|
|
||||||
|
return SpeakResponse(status_code=status_code, raw=raw)
|
||||||
@@ -296,9 +296,10 @@ def on_printing_order_created(sender, **kwargs):
|
|||||||
if outgoing_dt is not None
|
if outgoing_dt is not None
|
||||||
else "-"
|
else "-"
|
||||||
)
|
)
|
||||||
|
order_id = str(getattr(order, "id", "-") or "-")
|
||||||
|
|
||||||
message = render_printing_order_created_markdown(
|
message = render_printing_order_created_markdown(
|
||||||
printing_order_id=str(getattr(order, "id", "-") or "-"),
|
printing_order_id=order_id,
|
||||||
printing_order_human_id=str(getattr(order, "human_id", "-") or "-"),
|
printing_order_human_id=str(getattr(order, "human_id", "-") or "-"),
|
||||||
created_at=str(created_at),
|
created_at=str(created_at),
|
||||||
sender_label=str(sender_label),
|
sender_label=str(sender_label),
|
||||||
@@ -325,5 +326,15 @@ def on_printing_order_created(sender, **kwargs):
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("[printing.handlers] 发送 WeCom webhook 失败(已忽略,不影响主流程)")
|
logger.exception("[printing.handlers] 发送 WeCom webhook 失败(已忽略,不影响主流程)")
|
||||||
|
|
||||||
|
# Just for testing
|
||||||
|
# ==============================================================
|
||||||
|
try:
|
||||||
|
from flower.utils import play_speech
|
||||||
|
|
||||||
|
play_speech(text=f"生产订单 {order_id} 已创建")
|
||||||
|
except Exception:
|
||||||
|
logger.exception("[printing.handlers] 发送语音播报失败(已忽略,不影响主流程)")
|
||||||
|
# ===================================
|
||||||
|
|
||||||
# 在事务提交后再发送,避免事务回滚但通知已发出
|
# 在事务提交后再发送,避免事务回滚但通知已发出
|
||||||
transaction.on_commit(_send_wecom)
|
transaction.on_commit(_send_wecom)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2026-03-05 06:25
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basic_info', '0025_customer_uniq_customer_merchant_name'),
|
||||||
|
('printing', '0032_alter_printingorder_outgoing_date'),
|
||||||
|
('stateflow', '0023_remove_statelogparameterrecord_stfl_paramrec_params_gin'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='plateorder',
|
||||||
|
index=models.Index(fields=['merchant', 'plate_date'], name='idx_plateord_merch_pltdt'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -182,6 +182,12 @@ class PlateOrder(ModelBase):
|
|||||||
verbose_name = '开版订单'
|
verbose_name = '开版订单'
|
||||||
verbose_name_plural = '开版订单'
|
verbose_name_plural = '开版订单'
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
|
indexes = [
|
||||||
|
models.Index(
|
||||||
|
fields=['merchant', 'plate_date'],
|
||||||
|
name='idx_plateord_merch_pltdt',
|
||||||
|
),
|
||||||
|
]
|
||||||
permissions = [
|
permissions = [
|
||||||
('can_invalidate_plateorder', '可以作废开版订单'),
|
('can_invalidate_plateorder', '可以作废开版订单'),
|
||||||
('can_activate_plateorder', '可以恢复开版订单'),
|
('can_activate_plateorder', '可以恢复开版订单'),
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2026-03-05 06:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('settlement', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dailysettlementconfig',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dailysettlementconfig',
|
||||||
|
name='settlement_modules',
|
||||||
|
field=models.JSONField(default=list, help_text='例如: ["plate_order", "printing_order"]', verbose_name='统计模块'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user