From 663276288e70b8b49b7e25be6b9c81cd6993b828 Mon Sep 17 00:00:00 2001 From: colaftc Date: Mon, 9 Mar 2026 22:40:02 +0800 Subject: [PATCH] feat: settlement lineup --- .skills/.gitkeep | 50 ++ .skills/README.md | 43 ++ .skills/SUMMARY.md | 93 ++++ .skills/UPDATE_SUMMARY.md | 132 +++++ .skills/flower-db-access/README.md | 161 ++++++ .skills/flower-db-access/SKILL.md | 493 ++++++++++++++++++ .skills/flower-db-access/examples-remote.sh | 152 ++++++ .skills/flower-db-access/examples.sh | 113 ++++ .skills/flower-db-access/query.sh | 46 ++ api_v1/views/printing/test_plate_order_api.py | 16 + api_v1/views/printing/views.py | 48 ++ api_v1/views/settlement/mixins.py | 51 ++ api_v1/views/settlement/views.py | 111 ++-- ...25_customer_uniq_customer_merchant_name.py | 17 + basic_info/models.py | 6 + docs/agent_api_settlement.md | 96 ++++ env.example | 1 + flower/settings.py | 1 + flower/utils/__init__.py | 3 + flower/utils/speech.py | 67 +++ printing/handlers.py | 13 +- ...033_plateorder_idx_plateord_merch_pltdt.py | 21 + printing/models.py | 6 + ...alter_dailysettlementconfig_id_and_more.py | 23 + 24 files changed, 1730 insertions(+), 33 deletions(-) create mode 100644 .skills/.gitkeep create mode 100644 .skills/README.md create mode 100644 .skills/SUMMARY.md create mode 100644 .skills/UPDATE_SUMMARY.md create mode 100644 .skills/flower-db-access/README.md create mode 100644 .skills/flower-db-access/SKILL.md create mode 100755 .skills/flower-db-access/examples-remote.sh create mode 100755 .skills/flower-db-access/examples.sh create mode 100755 .skills/flower-db-access/query.sh create mode 100644 api_v1/views/settlement/mixins.py create mode 100644 basic_info/migrations/0025_customer_uniq_customer_merchant_name.py create mode 100644 docs/agent_api_settlement.md create mode 100644 flower/utils/speech.py create mode 100644 printing/migrations/0033_plateorder_idx_plateord_merch_pltdt.py create mode 100644 settlement/migrations/0002_alter_dailysettlementconfig_id_and_more.py diff --git a/.skills/.gitkeep b/.skills/.gitkeep new file mode 100644 index 0000000..0af24f7 --- /dev/null +++ b/.skills/.gitkeep @@ -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仓库供团队使用 diff --git a/.skills/README.md b/.skills/README.md new file mode 100644 index 0000000..428785b --- /dev/null +++ b/.skills/README.md @@ -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文件夹。 diff --git a/.skills/SUMMARY.md b/.skills/SUMMARY.md new file mode 100644 index 0000000..a4cee6c --- /dev/null +++ b/.skills/SUMMARY.md @@ -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) +**状态**: ✅ 已测试并正常工作 diff --git a/.skills/UPDATE_SUMMARY.md b/.skills/UPDATE_SUMMARY.md new file mode 100644 index 0000000..9676d3f --- /dev/null +++ b/.skills/UPDATE_SUMMARY.md @@ -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) diff --git a/.skills/flower-db-access/README.md b/.skills/flower-db-access/README.md new file mode 100644 index 0000000..15c18f5 --- /dev/null +++ b/.skills/flower-db-access/README.md @@ -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)。 diff --git a/.skills/flower-db-access/SKILL.md b/.skills/flower-db-access/SKILL.md new file mode 100644 index 0000000..6a11b86 --- /dev/null +++ b/.skills/flower-db-access/SKILL.md @@ -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. diff --git a/.skills/flower-db-access/examples-remote.sh b/.skills/flower-db-access/examples-remote.sh new file mode 100755 index 0000000..8108eba --- /dev/null +++ b/.skills/flower-db-access/examples-remote.sh @@ -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(多租户)" diff --git a/.skills/flower-db-access/examples.sh b/.skills/flower-db-access/examples.sh new file mode 100755 index 0000000..5352f54 --- /dev/null +++ b/.skills/flower-db-access/examples.sh @@ -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 文件" diff --git a/.skills/flower-db-access/query.sh b/.skills/flower-db-access/query.sh new file mode 100755 index 0000000..aa3bf75 --- /dev/null +++ b/.skills/flower-db-access/query.sh @@ -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\"" diff --git a/api_v1/views/printing/test_plate_order_api.py b/api_v1/views/printing/test_plate_order_api.py index 1f7b405..65408e5 100644 --- a/api_v1/views/printing/test_plate_order_api.py +++ b/api_v1/views/printing/test_plate_order_api.py @@ -653,6 +653,22 @@ class PlateOrderAPITestCase(TestCase): self.assertIn(order2.id, ids) self.assertNotIn(order1.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): """测试按设计编号搜索""" diff --git a/api_v1/views/printing/views.py b/api_v1/views/printing/views.py index 2619e2a..33eeb05 100644 --- a/api_v1/views/printing/views.py +++ b/api_v1/views/printing/views.py @@ -3,6 +3,7 @@ Printing API ViewSet """ from rest_framework import viewsets, filters, status from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError as DRFValidationError from rest_framework.response import Response from rest_framework.permissions import BasePermission from rest_framework.permissions import DjangoModelPermissions @@ -646,6 +647,7 @@ class HasActivatePlateOrderPermission(BasePermission): class PlateOrderFilterSet(django_filters.FilterSet): """开版订单过滤器""" + MAX_RANGE_DAYS = 10 customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains') customer_phone = django_filters.CharFilter(field_name='customer__mobile', 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 + @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): """ diff --git a/api_v1/views/settlement/mixins.py b/api_v1/views/settlement/mixins.py new file mode 100644 index 0000000..95d50d6 --- /dev/null +++ b/api_v1/views/settlement/mixins.py @@ -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 diff --git a/api_v1/views/settlement/views.py b/api_v1/views/settlement/views.py index 190cd62..ae8bed3 100644 --- a/api_v1/views/settlement/views.py +++ b/api_v1/views/settlement/views.py @@ -1,75 +1,122 @@ """Settlement API views""" + import logging import re from datetime import datetime, date +from django.contrib.auth import get_user_model 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.response import Response 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 .mixins import SettlementVisibilityMixin 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 - + GET /api/v1/settlement/plate-orders/summary/ - + 参数: 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] + view_all_permission = "printing.view_all_plateorders" def get(self, request): """ 获取开版订单统计 """ - date_str = request.query_params.get('date') - + date_str = request.query_params.get("date") + if not date_str: return Response( - {'error': '缺少 date 参数'}, - status=status.HTTP_400_BAD_REQUEST + {"error": "缺少 date 参数"}, 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( - {'error': '日期格式错误,请使用 YYYY-MM-DD 格式'}, - status=status.HTTP_400_BAD_REQUEST + {"error": "日期格式错误,请使用 YYYY-MM-DD 格式"}, + status=status.HTTP_400_BAD_REQUEST, ) - + try: settlement_date = date.fromisoformat(date_str) except ValueError: - return Response( - {'error': '日期不存在'}, - status=status.HTTP_403_FORBIDDEN - ) - - emp = getattr(request.user, 'employee', None) + return Response({"error": "日期不存在"}, status=status.HTTP_403_FORBIDDEN) + + emp = getattr(request.user, "employee", None) if emp is None or emp.merchant is None: - return Response( - {'error': '用户未关联商户'}, - status=status.HTTP_403_FORBIDDEN - ) - + if request.user.is_superuser: + merchant = Merchant.objects.first() + 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: data = get_plate_order_summary_by_customer( - merchant_id=emp.merchant.id, + merchant_id=merchant_id, 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: - logger.exception( - f'[settlement.views] 获取开版订单统计失败: {e}' - ) + logger.exception(f"[settlement.views] 获取开版订单统计失败: {e}") return Response( - {'error': '获取统计数据失败'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + {"error": "获取统计数据失败"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) diff --git a/basic_info/migrations/0025_customer_uniq_customer_merchant_name.py b/basic_info/migrations/0025_customer_uniq_customer_merchant_name.py new file mode 100644 index 0000000..bfb6c51 --- /dev/null +++ b/basic_info/migrations/0025_customer_uniq_customer_merchant_name.py @@ -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'), + ), + ] diff --git a/basic_info/models.py b/basic_info/models.py index 503bfd4..853a75f 100644 --- a/basic_info/models.py +++ b/basic_info/models.py @@ -439,6 +439,12 @@ class Customer(ModelBase): class Meta: verbose_name = '客户资料' verbose_name_plural = '客户资料' + constraints = [ + models.UniqueConstraint( + fields=['merchant', 'name'], + name='uniq_customer_merchant_name', + ), + ] permissions = [ ('view_all_customers', '查看所有客户资料'), ] diff --git a/docs/agent_api_settlement.md b/docs/agent_api_settlement.md new file mode 100644 index 0000000..94c138a --- /dev/null +++ b/docs/agent_api_settlement.md @@ -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 认证 diff --git a/env.example b/env.example index f607ce2..aa39ca9 100644 --- a/env.example +++ b/env.example @@ -71,6 +71,7 @@ TENCENTCLOUD_TIIA_QPS=10 ############################ # 为空时发送会报错:WeCom webhook key 未配置 WECOM_WEBHOOK_KEY= +SPEAK_ENDPOINT=http://8.148.215.233:9004/speak # PrintingJob 状态推进通知的“跟进地址”模板;为空则消息里省略“跟进地址”字段 PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE=https://app.yuwen.cloud/workstation/production/batch-advance?orderId={order_id} diff --git a/flower/settings.py b/flower/settings.py index bf09a70..b96bc24 100644 --- a/flower/settings.py +++ b/flower/settings.py @@ -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 WECOM_WEBHOOK_BASE_URL = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send' WECOM_WEBHOOK_KEY = env('WECOM_WEBHOOK_KEY', default='') +SPEAK_ENDPOINT = env('SPEAK_ENDPOINT', default='http://8.148.215.233:9004/speak') # CORS 配置 diff --git a/flower/utils/__init__.py b/flower/utils/__init__.py index cf6089d..6e5358e 100644 --- a/flower/utils/__init__.py +++ b/flower/utils/__init__.py @@ -57,6 +57,7 @@ from .mingdaoyun import ( flatten_plate_order_related_row, sync_fabric_from_mingdaoyun, ) +from .speech import SpeakResponse, play_speech __all__ = [ # client @@ -113,4 +114,6 @@ __all__ = [ "fetch_row_by_rowid_from_mingdaoyun", "fetch_plate_orders_from_mingdaoyun", "sync_fabric_from_mingdaoyun", + "SpeakResponse", + "play_speech", ] diff --git a/flower/utils/speech.py b/flower/utils/speech.py new file mode 100644 index 0000000..71b3c8c --- /dev/null +++ b/flower/utils/speech.py @@ -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': ''} + """ + 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) diff --git a/printing/handlers.py b/printing/handlers.py index cc5e93b..0fc9f65 100644 --- a/printing/handlers.py +++ b/printing/handlers.py @@ -296,9 +296,10 @@ def on_printing_order_created(sender, **kwargs): if outgoing_dt is not None else "-" ) + order_id = str(getattr(order, "id", "-") or "-") 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 "-"), created_at=str(created_at), sender_label=str(sender_label), @@ -325,5 +326,15 @@ def on_printing_order_created(sender, **kwargs): except Exception: 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) diff --git a/printing/migrations/0033_plateorder_idx_plateord_merch_pltdt.py b/printing/migrations/0033_plateorder_idx_plateord_merch_pltdt.py new file mode 100644 index 0000000..55bf07c --- /dev/null +++ b/printing/migrations/0033_plateorder_idx_plateord_merch_pltdt.py @@ -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'), + ), + ] diff --git a/printing/models.py b/printing/models.py index b167549..6624f2d 100644 --- a/printing/models.py +++ b/printing/models.py @@ -182,6 +182,12 @@ class PlateOrder(ModelBase): verbose_name = '开版订单' verbose_name_plural = '开版订单' ordering = ['-created_at'] + indexes = [ + models.Index( + fields=['merchant', 'plate_date'], + name='idx_plateord_merch_pltdt', + ), + ] permissions = [ ('can_invalidate_plateorder', '可以作废开版订单'), ('can_activate_plateorder', '可以恢复开版订单'), diff --git a/settlement/migrations/0002_alter_dailysettlementconfig_id_and_more.py b/settlement/migrations/0002_alter_dailysettlementconfig_id_and_more.py new file mode 100644 index 0000000..d4ca728 --- /dev/null +++ b/settlement/migrations/0002_alter_dailysettlementconfig_id_and_more.py @@ -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='统计模块'), + ), + ]