版本:v3.0 | 日期:2026-05-25 | 编写:铭见(上海)智能科技有限公司
适用:Phase 1-4 全阶段开发 | 目标数据库:PostgreSQL 16
为安徽今宽新材料科技有限公司开发一套 订单 → 到货 → 分切 → 出库 全流程覆盖的进销存管理系统,实现:
| 层 | 技术 | 备注 |
|---|---|---|
| 后端框架 | Python Flask | 已有 Phase 0 代码复用 |
| ORM | Flask-SQLAlchemy | PostgreSQL 适配 |
| 数据库 | PostgreSQL 16 | 已有运行实例 |
| 前端 | Vue 3 + Vite | 已有 Phase 0 原型 |
| UI 组件 | Element Plus | 企业级组件库 |
| 状态管理 | Pinia | |
| 路由 | Vue Router | |
| HTTP 客户端 | Axios | JWT Token 拦截 |
| 认证 | Flask-JWT-Extended | 24h 过期 |
| OCR | 腾讯云 GeneralBasicOCR | 已集成至 Phase 0 |
| 部署 | Nginx + Gunicorn | 阿里云 ECS |
| 二维码 | Python qrcode + pyzbar | 生成 + 扫码解析 |
Phase 0(现有)已完成以下模块:
- 腾讯云 OCR 集成(tencent_ocr.py)
- 本地规格解析引擎(spec_parser.py)— 支持 T/W 前缀、三维/二维规格、材质/硬度提取
- 客户模板匹配(template_matcher.py)
- AI 识别编排流程(app.py + recognizer.py)
- 前端原型(HTML 模拟页面)
本说明书覆盖 Phase 1-4 全阶段开发内容: - 完整的数据库设计(13 张核心业务表) - RESTful API 层(Blueprints 模块化) - 进销存全流程(入库 → 库存 → 加工 → 出库 → 订单跟踪) - 二维码标签生成与扫码(Phase 3) - 前端 Vue3 SPA
各 Phase 对应的功能模块详见下表:
| Phase | 对应 API 模块 | 对应前端页面 |
|---|---|---|
| Phase 1 | auth / customers / suppliers / orders / inbound / inventory / reports | 登录 / 仪表盘 / 订单管理 / 入库管理 / 库存台账 / 客户/供应商管理 / 报表 |
| Phase 2 | processing / outbound | 加工管理 / 出库管理 |
| Phase 3 | qr | 二维码管理 |
| Phase 4 | reports(扩展分析) | 经营分析仪表盘 |
┌───────────┐ ┌───────────────────┐ ┌───────────┐
│ customers │────→│ orders │←────│ suppliers │
└───────────┘ │ (订单) │ └───────────┘
│ │ │ order_items │ │
│ │ │ (订单明细) │ │
│ └──┴─────────────────┘ │
│ │
▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────────┐
│ outbound │ │ inventory │ │ inbound │
│ (出库) │←────│ (库存) │────→│ (入库) │
│ │ │ └───────────┘ │ │ │
│ │ 出库明细│ │ 加工单 │ 入库明细 │
│ ▼ │ ▼ │ ▼ │
│ outbound │ processing │ inbound │
│ _items │ _orders │ _items │
└───────────┘ └───────────┘ └───────────────┘
│
▼
┌───────────┐
│ qr_labels │
│ (二维码) │
└───────────┘
┌───────────┐
│ users │
└───────────┘
CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(32) UNIQUE NOT NULL, -- 客户编号
name VARCHAR(200) NOT NULL, -- 客户名称
contact VARCHAR(100), -- 联系人
phone VARCHAR(32), -- 联系电话
address TEXT, -- 地址
credit_terms VARCHAR(64), -- 结算方式/账期
remark TEXT, -- 备注
status VARCHAR(16) NOT NULL DEFAULT 'active', -- active/disabled
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_customers_code ON customers(code);
CREATE INDEX idx_customers_status ON customers(status);
CREATE TABLE suppliers (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(32) UNIQUE NOT NULL, -- 供应商编号
name VARCHAR(200) NOT NULL, -- 供应商名称
contact VARCHAR(100), -- 联系人
phone VARCHAR(32), -- 联系电话
material_scope VARCHAR(200), -- 供应品类范围
remark TEXT, -- 备注
status VARCHAR(16) NOT NULL DEFAULT 'active', -- active/disabled
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_suppliers_code ON suppliers(code);
CREATE INDEX idx_suppliers_status ON suppliers(status);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
order_no VARCHAR(64) UNIQUE NOT NULL, -- 系统单号(自动生成)
customer_po_no VARCHAR(128), -- 客户 PO 号
customer_id BIGINT NOT NULL -- 客户ID
REFERENCES customers(id),
order_date DATE NOT NULL, -- 订单日期
delivery_date DATE, -- 要求交期
status VARCHAR(32) NOT NULL DEFAULT 'pending',
-- pending(待发货) / partial(部分发货) / completed(已完成) / cancelled(已取消)
total_weight DECIMAL(12,3), -- 总重量(kg)
total_amount DECIMAL(14,2), -- 总金额(元,可空)
remark TEXT, -- 备注
-- AI 识别扩展字段(Phase 0 对接)
document_type VARCHAR(32) DEFAULT 'purchase_order',
-- purchase_order / outsourced_order / plan_order
price_type VARCHAR(16) DEFAULT 'taxed', -- taxed/notaxed/direct
source_type VARCHAR(16) DEFAULT 'manual', -- ai/manual
ai_raw_result JSONB, -- AI 原始识别结果
source_file VARCHAR(512), -- 上传文件路径
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_orders_no ON orders(order_no);
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_date ON orders(order_date);
单号生成规则:JK + YYYYMMDD + 3位序号
例如:JK20260524001,每天从 001 重新计数。
CREATE TABLE order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL
REFERENCES orders(id) ON DELETE CASCADE,
-- 规格字段(自描述,不强制关联产品表)
product_name VARCHAR(200), -- 品名:紫铜卷料/紫铜板/铜排
material_type VARCHAR(16) NOT NULL DEFAULT 'coil',
-- coil(卷材) / sheet(张片) / profile(型材)
spec VARCHAR(200), -- 完整规格字符串
hardness VARCHAR(16), -- 状态:H/Y2/M
-- 数量/重量
quantity DECIMAL(12,3) NOT NULL, -- 订购件数
weight_kg DECIMAL(12,3), -- 重量(kg)
unit_price DECIMAL(12,2), -- 单价(元/kg)
amount DECIMAL(14,2), -- 金额
-- 发货跟踪(核心:欠量管理)
ordered_weight DECIMAL(12,3) NOT NULL, -- 订购重量(kg)
delivered_weight DECIMAL(12,3) DEFAULT 0, -- 已发货重量(kg)
pending_weight DECIMAL(12,3) GENERATED ALWAYS AS (ordered_weight - delivered_weight) STORED, -- 欠量(自动计算)
remark TEXT,
CONSTRAINT fk_order FOREIGN KEY (order_id) REFERENCES orders(id)
);
CREATE INDEX idx_items_order ON order_items(order_id);
CREATE INDEX idx_items_type ON order_items(material_type);
说明:
- pending_weight 使用 PostgreSQL GENERATED ALWAYS AS ... STORED 计算列,由数据库自动维护,始终保持 ordered_weight - delivered_weight 的值,无需应用层手动更新
- 每行明细独立定义规格,不强制关联产品主数据,便于 AI 识别场景
CREATE TABLE inbound_orders (
id BIGSERIAL PRIMARY KEY,
inbound_no VARCHAR(64) UNIQUE NOT NULL, -- 入库单号
supplier_id BIGINT REFERENCES suppliers(id), -- 供应商
inbound_date DATE NOT NULL, -- 入库日期
type VARCHAR(32) NOT NULL DEFAULT 'purchase',
-- purchase(来料入库) / return(退货入库)
status VARCHAR(16) NOT NULL DEFAULT 'draft',
-- draft(草稿) / completed(已完成) / cancelled(已取消)
remark TEXT,
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_inbound_no ON inbound_orders(inbound_no);
CREATE INDEX idx_inbound_supplier ON inbound_orders(supplier_id);
CREATE INDEX idx_inbound_date ON inbound_orders(inbound_date);
单号生成规则:RK + YYYYMMDD + 3位序号
例如:RK20260524001。
CREATE TABLE inbound_items (
id BIGSERIAL PRIMARY KEY,
inbound_id BIGINT NOT NULL
REFERENCES inbound_orders(id) ON DELETE CASCADE,
product_name VARCHAR(200) NOT NULL, -- 品名
spec VARCHAR(200), -- 规格
hardness VARCHAR(16), -- 状态
quantity INTEGER NOT NULL, -- 件数
weight_kg DECIMAL(12,3) NOT NULL, -- 重量(kg)
batch_no VARCHAR(64), -- 批号
internal_no VARCHAR(64), -- 内部编号(二维码内容)
location VARCHAR(64), -- 仓位
quality_status VARCHAR(16) NOT NULL DEFAULT 'qualified',
-- qualified(正常) / pending(待检) / returned(退货)
remark TEXT,
CONSTRAINT fk_inbound FOREIGN KEY (inbound_id) REFERENCES inbound_orders(id)
);
CREATE INDEX idx_inbound_items_inbound ON inbound_items(inbound_id);
CREATE INDEX idx_inbound_items_batch ON inbound_items(batch_no);
CREATE TABLE inventory (
id BIGSERIAL PRIMARY KEY,
product_name VARCHAR(200) NOT NULL, -- 品名
spec VARCHAR(200), -- 规格
hardness VARCHAR(16), -- 状态
quantity_available INTEGER NOT NULL DEFAULT 0, -- 可用件数
weight_available DECIMAL(12,3) NOT NULL DEFAULT 0, -- 可用重量(kg)
batch_no VARCHAR(64), -- 批号
internal_no VARCHAR(64), -- 内部编号
location VARCHAR(64), -- 仓位
quality_status VARCHAR(16) NOT NULL DEFAULT 'qualified',
-- qualified(正常) / pending(待检) / returned(退货) / defective(次品)
supplier_id BIGINT REFERENCES suppliers(id), -- 来源供应商
inbound_date DATE, -- 入库日期
inbound_item_id BIGINT, -- 关联入库明细(溯源)
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 同品名+同规格+同批号+同仓位 → 合并为一条记录
CREATE UNIQUE INDEX idx_inventory_uniq ON inventory(product_name, spec, batch_no, location, quality_status);
CREATE INDEX idx_inventory_batch ON inventory(batch_no);
CREATE INDEX idx_inventory_location ON inventory(location);
CREATE INDEX idx_inventory_status ON inventory(quality_status);
核心设计原则:
- 库存按批次+仓位做唯一约束,同一规格不同批号不同仓位各自独立
- quantity_available 和 weight_available 通过入库/出库/加工触发器同步更新
- 出库时支持按 FEFO(先到期先出)或指定批次
CREATE TABLE outbound_orders (
id BIGSERIAL PRIMARY KEY,
outbound_no VARCHAR(64) UNIQUE NOT NULL, -- 出库单号
order_id BIGINT REFERENCES orders(id), -- 关联销售订单
customer_id BIGINT REFERENCES customers(id), -- 客户
outbound_date DATE NOT NULL, -- 出库日期
status VARCHAR(16) NOT NULL DEFAULT 'draft',
-- draft(草稿) / completed(已完成) / cancelled(已取消)
remark TEXT,
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_outbound_no ON outbound_orders(outbound_no);
CREATE INDEX idx_outbound_order ON outbound_orders(order_id);
CREATE INDEX idx_outbound_customer ON outbound_orders(customer_id);
CREATE INDEX idx_outbound_date ON outbound_orders(outbound_date);
单号生成规则:CK + YYYYMMDD + 3位序号
例如:CK20260524001。
CREATE TABLE outbound_items (
id BIGSERIAL PRIMARY KEY,
outbound_id BIGINT NOT NULL
REFERENCES outbound_orders(id) ON DELETE CASCADE,
inventory_id BIGINT REFERENCES inventory(id), -- 关联库存批次
-- 冗余字段(方便查询,不依赖关联)
product_name VARCHAR(200) NOT NULL,
spec VARCHAR(200),
hardness VARCHAR(16),
quantity INTEGER NOT NULL, -- 出库件数
weight_kg DECIMAL(12,3) NOT NULL, -- 出库重量(kg)
batch_no VARCHAR(64),
remark TEXT,
CONSTRAINT fk_outbound FOREIGN KEY (outbound_id) REFERENCES outbound_orders(id)
);
CREATE INDEX idx_outbound_items_outbound ON outbound_items(outbound_id);
CREATE INDEX idx_outbound_items_inventory ON outbound_items(inventory_id);
CREATE TABLE processing_orders (
id BIGSERIAL PRIMARY KEY,
process_no VARCHAR(64) UNIQUE NOT NULL, -- 加工单号
order_item_id BIGINT REFERENCES order_items(id), -- 关联订单明细
process_type VARCHAR(16) NOT NULL,
-- slitting(分切) / shearing(剪切)
-- 母材信息
parent_material_inventory_id BIGINT REFERENCES inventory(id),
parent_spec VARCHAR(200), -- 母材规格
parent_weight DECIMAL(12,3), -- 母材投入重量
-- 产出信息
child_spec VARCHAR(200) NOT NULL, -- 子材规格
output_weight DECIMAL(12,3) NOT NULL, -- 产出重量
-- 损耗/余料
scrap_weight DECIMAL(12,3) DEFAULT 0, -- 废料重量
return_weight DECIMAL(12,3) DEFAULT 0, -- 退库余料重量
process_date DATE NOT NULL, -- 加工日期
status VARCHAR(16) NOT NULL DEFAULT 'pending',
-- pending(待加工) / completed(已完成) / cancelled(已取消)
remark TEXT,
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_processing_no ON processing_orders(process_no);
CREATE INDEX idx_processing_order ON processing_orders(order_item_id);
CREATE INDEX idx_processing_parent ON processing_orders(parent_material_inventory_id);
加工业务流:
母材库存 → 创建加工单 → 分切/剪切 → 产出子材 → 子材入库存
→ 废料记录
→ 余料退库
CREATE TABLE qr_labels (
id BIGSERIAL PRIMARY KEY,
label_no VARCHAR(64) UNIQUE NOT NULL, -- 标签编号
batch_no VARCHAR(64), -- 批号
product_name VARCHAR(200), -- 品名
spec VARCHAR(200), -- 规格
weight_kg DECIMAL(12,3), -- 重量(kg)
qr_content TEXT NOT NULL, -- 二维码完整内容(JSON)
status VARCHAR(16) NOT NULL DEFAULT 'unscanned',
-- unscanned(未扫码) / inbound(已入库) / outbound(已出库)
scanned_at TIMESTAMP, -- 扫码时间
source VARCHAR(16), -- inbound/outbound
related_transaction_id BIGINT, -- 关联交易ID
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_qr_label_no ON qr_labels(label_no);
CREATE INDEX idx_qr_batch ON qr_labels(batch_no);
CREATE INDEX idx_qr_status ON qr_labels(status);
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL, -- 用户名
password_hash VARCHAR(256) NOT NULL, -- bcrypt 哈希
role VARCHAR(32) NOT NULL DEFAULT 'operator',
-- admin(管理员) / supervisor(业务主管) / operator(操作员) / viewer(只读)
display_name VARCHAR(100) NOT NULL, -- 显示名称
status VARCHAR(16) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_role ON users(role);
CREATE TABLE operation_logs (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
username VARCHAR(64),
action VARCHAR(32) NOT NULL, -- create/update/delete/login
resource_type VARCHAR(32) NOT NULL, -- order/inventory/customer/...
resource_id BIGINT,
detail TEXT, -- JSON 变更内容
ip_address VARCHAR(45),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_logs_resource ON operation_logs(resource_type, resource_id);
CREATE INDEX idx_logs_user ON operation_logs(user_id);
CREATE INDEX idx_logs_time ON operation_logs(created_at);
基础路径: /api/v1
请求头: Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
统一响应格式:
{
"code": 0, // 0=成功, 非0=错误码
"message": "ok", // 提示信息
"data": {}, // 返回数据(对象或数组)
"pagination": { // 分页时返回
"page": 1,
"page_size": 20,
"total": 100
}
}
错误响应:
{
"code": 40001,
"message": "参数错误: 客户名称不能为空"
}
常见错误码:
- 40001~40099: 参数校验错误
- 40100~40199: 认证/授权错误
- 40300~40399: 业务逻辑错误(库存不足等)
- 40400~40499: 资源不存在
- 50000: 服务器内部错误
登录获取 JWT Token。
请求体:
{
"username": "admin",
"password": "password123"
}
响应:
{
"code": 0,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 86400,
"user": {
"id": 1,
"username": "admin",
"display_name": "管理员",
"role": "admin"
}
}
}
获取当前用户信息(需要 Authorization Header)。
修改密码。
客户列表(分页+搜索+筛选)。
查询参数:?page=1&page_size=20&keyword=搜索词&status=active
响应:
{
"code": 0,
"data": [
{
"id": 1,
"code": "C001",
"name": "东莞越洋达电子科技有限公司",
"contact": "张先生",
"phone": "13800138000",
"address": "东莞市长安镇...",
"credit_terms": "月结30天",
"status": "active"
}
],
"pagination": {
"page": 1,
"page_size": 20,
"total": 50
}
}
新建客户。
请求体:
{
"code": "C002",
"name": "东莞鸿阳精密电子有限公司",
"contact": "李先生",
"phone": "13900139000",
"address": "东莞市虎门镇...",
"credit_terms": "月结60天"
}
客户详情。
更新客户。
删除客户(软删除:修改 status 为 disabled)。
同客户管理模式:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/suppliers | 供应商列表 |
| POST | /api/v1/suppliers | 新建供应商 |
| GET | /api/v1/suppliers/:id | 供应商详情 |
| PUT | /api/v1/suppliers/:id | 更新供应商 |
| DELETE | /api/v1/suppliers/:id | 删除供应商 |
创建订单(含明细)。
请求体:
{
"customer_id": 1,
"customer_po_no": "PO20260524001",
"order_date": "2026-05-24",
"delivery_date": "2026-06-15",
"items": [
{
"product_name": "紫铜卷料",
"material_type": "coil",
"spec": "T2.9×W175mm",
"hardness": null,
"quantity": 5,
"weight_kg": 4410,
"unit_price": 6.00,
"amount": 26460.00,
"ordered_weight": 4410
}
]
}
响应:
{
"code": 0,
"data": {
"id": 42,
"order_no": "JK20260524001",
"status": "pending",
"total_weight": 4410.000,
"total_amount": 26460.00
}
}
订单列表(分页+筛选)。
查询参数:?page=1&status=pending&customer_id=1&date_from=2026-05-01&date_to=2026-05-31
订单详情(含明细)。
响应:
{
"code": 0,
"data": {
"id": 42,
"order_no": "JK20260524001",
"customer": { "id": 1, "name": "东莞越洋达" },
"status": "partial",
"items": [
{
"id": 1,
"product_name": "紫铜卷料",
"spec": "T2.9×W175mm",
"ordered_weight": 4410.000,
"delivered_weight": 2000.000,
"pending_weight": 2410.000,
"quantity": 5,
"weight_kg": 4410.000
}
],
"summary": {
"total_weight": 4410.000,
"total_delivered": 2000.000,
"total_pending": 2410.000
}
}
}
更新订单(仅草稿/待发货状态可修改)。
删除订单(仅待发货状态可删除)。
创建入库单(含明细,自动增加库存)。
请求体:
{
"supplier_id": 1,
"inbound_date": "2026-05-24",
"type": "purchase",
"items": [
{
"product_name": "紫铜卷料",
"spec": "T2.9×W175mm",
"hardness": null,
"quantity": 10,
"weight_kg": 8820,
"batch_no": "202605-01",
"location": "A-01-01",
"quality_status": "qualified"
}
]
}
响应:
{
"code": 0,
"data": {
"id": 1,
"inbound_no": "RK20260524001",
"status": "completed",
"items_count": 1
}
}
入库记录列表。
入库单详情(含明细)。
创建出库单(含库存批次选择,自动扣减库存)。
请求体:
{
"order_id": 42,
"customer_id": 1,
"outbound_date": "2026-05-24",
"items": [
{
"inventory_id": 5,
"product_name": "紫铜卷料",
"spec": "T2.9×W175mm",
"quantity": 2,
"weight_kg": 1764,
"batch_no": "202605-01"
}
]
}
响应:
{
"code": 0,
"data": {
"id": 1,
"outbound_no": "CK20260524001",
"status": "completed"
}
}
出库记录列表。
出库单详情(含明细)。
库存列表(多维筛选)。
查询参数:
?product_name=紫铜&spec=T2.9&batch_no=202605&location=A-01&quality_status=qualified&page=1
响应:
{
"code": 0,
"data": [
{
"id": 5,
"product_name": "紫铜卷料",
"spec": "T2.9×W175mm",
"hardness": null,
"quantity_available": 8,
"weight_available": 7056.000,
"batch_no": "202605-01",
"location": "A-01-01",
"quality_status": "qualified",
"inbound_date": "2026-05-24"
}
],
"pagination": {
"page": 1,
"page_size": 20,
"total": 15
}
}
库存盘点。
请求体:
{
"inventory_id": 5,
"actual_quantity": 7,
"actual_weight": 6174.000,
"remark": "盘点调整:发现1件次品转出"
}
低库存预警列表(低于安全阈值,默认 100kg)。
创建加工单。
请求体:
{
"order_item_id": 1,
"process_type": "slitting",
"parent_material_inventory_id": 5,
"parent_spec": "T2.9×W175mm",
"parent_weight": 1764.000,
"child_spec": "T2.9×W135mm",
"output_weight": 1700.000,
"scrap_weight": 40.000,
"return_weight": 24.000,
"process_date": "2026-05-24"
}
加工单列表。
加工单详情。
生成二维码标签。
请求体:
{
"batch_no": "202605-01",
"product_name": "紫铜卷料",
"spec": "T2.9×W175mm",
"weight_kg": 882.000,
"count": 10
}
响应:
{
"code": 0,
"data": {
"labels": [
{
"label_no": "QR20260524001",
"qr_content": "{\"label_no\":\"QR20260524001\",\"product_name\":\"紫铜卷料\",\"spec\":\"T2.9×W175mm\",\"weight_kg\":882.000,\"batch_no\":\"202605-01\"}",
"qr_image": "data:image/png;base64,..."
}
]
}
}
扫码处理(入库或出库)。
请求体(入库场景):
{
"qr_content": "{...完整二维码JSON...}",
"action": "inbound",
"inbound_id": 1
}
请求体(出库场景):
{
"qr_content": "{...完整二维码JSON...}",
"action": "outbound",
"outbound_id": 1
}
上传图片进行 AI 识别。
请求:multipart/form-data,字段 file(图片文件)
响应:
{
"code": 0,
"data": {
"customer_name": "东莞越洋达电子科技有限公司",
"po_number": "1020PM2605190004",
"order_date": "2026-05-19",
"items": [
{
"spec_text": "T2.9×W175mm, T2紫铜",
"material": "T2",
"thickness": 2.9,
"width": 175,
"length": null,
"hardness": null,
"product_name": "紫铜卷料",
"material_type": "coil",
"weight_kg": 4410.000
}
]
}
}
库存表导出。
查询参数:?format=json(默认)或 ?format=xlsx
响应(JSON 格式):
{
"code": 0,
"data": {
"headers": ["品名", "规格", "件数", "重量(kg)", "批号", "仓位", "品质状态"],
"rows": [
["紫铜卷料", "T2.9×W175mm", 8, 7056.000, "202605-01", "A-01-01", "正常"]
]
}
}
响应(xlsx 格式):直接返回 Excel 文件下载。
订单发货表导出。
查询参数:?order_no=JK20260524001&customer_id=1&format=xlsx
响应列:订单号 / 客户 / 品名 / 规格 / 订购重量 / 已发重量 / 欠量 / 发货进度
| 页面 | 路由 | 说明 | 所属阶段 |
|---|---|---|---|
| 登录页 | /login |
用户名/密码登录 | Phase 1 |
| 仪表盘 | /dashboard |
概览数据:今日入库/出库/库存预警/待办订单 | Phase 1 |
| 订单管理 | /orders |
订单列表 + 新建 + 详情 | Phase 1 |
| 入库管理 | /inbound |
入库单列表 + 新建(含二维码扫码) | Phase 1 |
| 出库管理 | /outbound |
出库单列表 + 新建(含库存选择) | Phase 2 |
| 库存台账 | /inventory |
多维筛选 + 导出 Excel | Phase 1 |
| 加工管理 | /processing |
加工单列表 + 新建加工单 | Phase 2 |
| 二维码管理 | /qr |
标签生成 + 扫码记录查询 | Phase 3 |
| 客户管理 | /customers |
客户 CRUD | Phase 1 |
| 供应商管理 | /suppliers |
供应商 CRUD | Phase 1 |
| 报表 | /reports |
库存表 / 订单发货表 | Phase 1 |
| 系统设置 | /settings |
用户管理 | Phase 1 |
卡片布局: - 今日入库(件数/重量) - 今日出库(件数/重量) - 待发订单数(含欠量汇总) - 低库存预警(低于 100kg 的物料列表)
快捷入口: - 新建入库单 - 新建出库单 - 新建订单
OrderList.vue: - 表格列:订单号 / 客户 / 日期 / 总重量 / 状态(标签色) / 操作 - 筛选栏:关键字搜索、状态下拉、日期范围 - 行操作:查看详情、创建出库单(部分发货时)
OrderCreate.vue: - 顶部:客户选择(搜索下拉)、客户PO号、订单日期、交期 - 明细表格:每行可添加/删除,含品名/规格/件数/重量/单价/金额 - 底部汇总:总件数、总重量、总金额 - 两阶段流程(AI识别场景): 1. 上传图片 → AI 识别填充 2. 人工确认/修改 → 提交保存
OrderDetail.vue: - 订单信息(只读展示) - 明细表格(含发货进度条:已发/欠量) - 出库记录关联列表 - 操作按钮:创建出库单、取消订单
InboundList.vue: - 表格列:入库单号 / 日期 / 供应商 / 类型 / 状态 / 操作 - 筛选:日期范围、供应商、类型
InboundCreate.vue: - 供应商选择 - 入库日期 - 明细表格(可添加多行): - 品名 / 规格 / 件数 / 重量(kg) / 批号 / 仓位 / 品质状态 - 每行可附带二维码扫描:扫码后自动填充品名/规格/批号/重量 - 提交后自动更新库存
OutboundList.vue: - 表格列:出库单号 / 日期 / 客户 / 关联订单 / 状态 / 操作
OutboundCreate.vue: - 选择关联订单(或直接出库) - 库存选择器:按品名/规格/批号搜索可出库的库存批次 - 显示每个批次的可用件数和重量 - 选择后自动扣减当前出库数量 - 明细表格(从库存选择添加): - 品名 / 规格 / 批号 / 仓位 / 可出库重量 / 本次出库重量 - 提交 → 扣减库存 + 更新订单发货量
InventoryList.vue: - 筛选栏:品名 / 规格 / 批号 / 仓位 / 品质状态 - 表格列:品名 / 规格 / 件数 / 重量(kg) / 批号 / 仓位 / 品质状态 / 入库日期 - 操作:导出 Excel、打印 - 行详情:点击可查看该批次出入库流水
ProcessingList.vue: - 表格列:加工单号 / 母材 / 子材 / 类型 / 日期 / 状态 / 操作
ProcessingCreate.vue: - 关联订单明细 - 选择母材库存批次 - 填写产出规格/重量/废料/余料 - 提交 → 母材库存扣减 + 子材入库 + 余料退库
QRGenerate.vue: - 选择批号/品名/规格/重量 - 批量生成标签(指定数量) - 预览/打印标签
QRScanRecord.vue: - 查询历史扫码记录 - 按时间/状态/来源筛选
CustomerList.vue / SupplierList.vue: - 搜索+列表+CRUD 弹窗 - 简单表单:名称/联系人/电话/地址等
二维码内容为 JSON 格式,压缩到单行,长度控制在 500 字符以内:
{
"v": "1",
"l": "QR20260524001",
"p": "紫铜卷料",
"s": "T2.9×W175mm",
"w": 882.000,
"b": "202605-01",
"u": "kg",
"d": "2026-05-24"
}
| 字段 | 含义 | 示例 |
|---|---|---|
| v | 数据版本 | 1 |
| l | 标签编号 | QR20260524001 |
| p | 品名 | 紫铜卷料 |
| s | 规格 | T2.9×W175mm |
| w | 重量 | 882.000 |
| b | 批号 | 202605-01 |
| u | 单位 | kg |
| d | 日期 | 2026-05-24 |
① 来料到达 → 仓库员在入库页选择「扫码入库」
② 扫描每个物料上的二维码标签
→ 系统解析 JSON,自动填充:品名/规格/重量/批号
→ 仓库员补充:件数、仓位、品质状态
③ 全部扫描完成 → 确认提交
→ 系统创建入库单 + 每条二维码标记为「已入库」
→ 库存表自动增加
异常处理: - 无标签物料:手动录入所有字段 - 二维码损坏:手动输入标签编号,系统回查 - 重复扫码:提示「该标签已入库」
① 拣货 → 仓库员在出库页选择「扫码出库」
② 扫描每个待出库物料上的二维码
→ 系统解析 JSON,匹配库存批次
→ 显示确认信息:品名/规格/批号/仓位/可用重量
→ 仓库员输入出库重量(支持部分出库)
③ 全部扫描完成 → 确认提交
→ 库存扣减 + 二维码标记「已出库」
→ 如关联订单,更新订单发货进度
异常处理: - 库存不足:提示可用量不足,不能超量出库 - 品质不符:仅允许出库品质为「正常」的批次 - 已出库标签:提示「该标签已出库」
今宽现场使用 BarTender 标签打印机。
推荐方案(服务器端生成 + 打印): 1. 系统生成二维码标签 → 输出为 PNG/PDF 2. BarTender 支持 PDF 打印模板,通过命令行或 API 调用 3. 或系统提供批量导出 → 在 BarTender 中排版打印
替代方案(打印服务):
- 在后端集成 python-barcode + qrcode 库生成二维码图片
- 前端调用 /api/v1/qr/generate → 获取 base64 图片 → 直接使用浏览器打印
- 标签模板为 A4 不干胶(每页 10 个标签,2×5 布局)
数据结构同步: - BarTender 生成的二维码内容由系统 API 提供 - 今宽操作员通过系统界面生成标签编号 → 系统返回二维码内容 - 将二维码内容传给 BarTender(通过 TXT/CSV 中间文件)→ 打印贴标
已有模块可直接复用:
| 模块 | 文件 | 用处 |
|---|---|---|
| 腾讯云 OCR | tencent_ocr.py |
图片文字提取(稳定可重现) |
| 规格解析器 | spec_parser.py |
规格字符串 → 结构化字段 |
| 客户模板匹配 | template_matcher.py |
识别客户信息 |
| 识别编排 | recognizer.py |
整合 OCR + 解析流程 |
| Flask 主入口 | app.py |
API 路由基础 |
用户上传采购单图片
│
▼
POST /api/v1/ocr/recognize
│
├─ ① 图片预处理 (preprocessor.py)
│ → 转灰度 / 降噪 / 增强对比度
│
├─ ② 腾讯云 OCR 全文提取 (tencent_ocr.py)
│ → 返回结构化文本
│
├─ ③ 客户识别 (template_matcher.py)
│ → 匹配客户模板 → 提取 PO号/日期/地址
│
├─ ④ 规格解析 (spec_parser.py)
│ → 逐行解析: 材质/厚度/宽度/长度/硬度/品名
│ → 推断订单类别: coil/sheet/profile
│
└─ ⑤ 返回前端确认页
→ 用户逐项检查/修改
→ 确认提交 → 创建订单
材质映射:
"C1100" / "T2" / "紫铜" / "红铜" → C1100
"C1020" / "无氧铜" → C1020
"H62" / "黄铜" → H62
(默认 C1100 兜底)
类别推断:
检测到 2 个维度(厚度×宽度) → coil(卷材)
检测到 3 个维度(厚度×宽度×长度) → sheet(张片) 或 profile(型材)
含 "铜排"/"型材" 关键词 → profile
含 "铜带"/"卷料" 关键词 → coil
硬度提取:
从规格文本中匹配 H/Y2/M 状态标记
无标记 → null(部分铜材无硬度)
用户上传图片
│
▼
[后端] ocr/recognize 接口
│
├─ 腾讯云 OCR → 原文文本
├─ 本地解析 → 结构化字段
└─ 返回 JSON 给前端
│
▼
[前端] 确认页面 (confirm.vue)
│
├─ 用户逐行修改(所有字段可改)
├─ 前端即时校验(规格完整性/数量合理性)
└─ 点击「确认保存」
│
▼
[前端] POST /api/v1/orders (含 AI 识别结果)
│
▼
[后端] 创建订单
│
├─ 生成订单号
├─ 写入 orders + order_items
├─ 存储 AI 原始结果 (ai_raw_result)
└─ 返回订单 ID
│
▼
[前端] 跳转到订单详情页
~/Desktop/安徽今宽/10-开发/
│
├── backend/ # Flask API 后端
│ ├── run.py # 启动入口
│ ├── requirements.txt # Python 依赖
│ ├── .env.example # 环境变量模板
│ ├── app/
│ │ ├── __init__.py # App 工厂 (create_app)
│ │ ├── config.py # 配置管理
│ │ ├── errors.py # 统一错误处理
│ │ │
│ │ ├── models/ # SQLAlchemy 数据模型
│ │ │ ├── __init__.py
│ │ │ ├── customer.py
│ │ │ ├── supplier.py
│ │ │ ├── order.py
│ │ │ ├── order_item.py
│ │ │ ├── inventory.py
│ │ │ ├── inbound.py
│ │ │ ├── outbound.py
│ │ │ ├── processing.py
│ │ │ ├── qr_label.py
│ │ │ └── user.py
│ │ │
│ │ ├── api/ # RESTful API 路由 (Blueprint)
│ │ │ ├── __init__.py
│ │ │ ├── auth.py
│ │ │ ├── customers.py
│ │ │ ├── suppliers.py
│ │ │ ├── orders.py
│ │ │ ├── inbound.py
│ │ │ ├── outbound.py
│ │ │ ├── inventory.py
│ │ │ ├── processing.py
│ │ │ ├── qr.py
│ │ │ ├── ocr.py # AI 识别接口(对接 Phase 0)
│ │ │ └── reports.py
│ │ │
│ │ ├── services/ # 业务逻辑层
│ │ │ ├── __init__.py
│ │ │ ├── order_service.py # 订单创建/状态流转
│ │ │ ├── inventory_service.py # 库存增减
│ │ │ ├── inbound_service.py # 入库 + 库存新增
│ │ │ ├── outbound_service.py # 出库 + 库存扣减 + 订单更新
│ │ │ ├── processing_service.py # 加工 + 库存转换
│ │ │ └── qr_service.py # 二维码生成/扫码处理
│ │ │
│ │ ├── utils/ # 工具函数
│ │ │ ├── __init__.py
│ │ │ ├── auth.py # JWT / 权限装饰器
│ │ │ ├── validators.py # 参数校验
│ │ │ ├── helpers.py # 通用工具
│ │ │ └── docno.py # 单号生成器
│ │ │
│ │ └── ext/ # Phase 0 已有代码(不改造,直接引用)
│ │ ├── tencent_ocr.py
│ │ ├── spec_parser.py
│ │ ├── template_matcher.py
│ │ ├── template_learner.py
│ │ ├── recognizer.py
│ │ ├── preprocessor.py
│ │ └── rate_limiter.py
│ │
│ ├── migrations/ # Flask-Migrate 迁移
│ │
│ └── tests/ # 单元测试
│ ├── __init__.py
│ ├── test_orders.py
│ ├── test_inventory.py
│ └── test_inbound_outbound.py
│
├── frontend/ # Vue3 前端
│ ├── package.json
│ ├── vite.config.js
│ ├── index.html
│ ├── src/
│ │ ├── main.js
│ │ ├── App.vue
│ │ ├── router/
│ │ │ └── index.js
│ │ ├── stores/ # Pinia
│ │ │ ├── auth.js
│ │ │ ├── order.js
│ │ │ └── inventory.js
│ │ ├── api/ # Axios 封装
│ │ │ ├── index.js
│ │ │ ├── auth.js
│ │ │ ├── orders.js
│ │ │ ├── inbound.js
│ │ │ ├── outbound.js
│ │ │ ├── inventory.js
│ │ │ └── reports.js
│ │ ├── views/
│ │ │ ├── Login.vue
│ │ │ ├── Dashboard.vue
│ │ │ ├── order/
│ │ │ │ ├── OrderList.vue
│ │ │ │ ├── OrderCreate.vue
│ │ │ │ └── OrderDetail.vue
│ │ │ ├── inbound/
│ │ │ │ ├── InboundList.vue
│ │ │ │ └── InboundCreate.vue
│ │ │ ├── outbound/
│ │ │ │ ├── OutboundList.vue
│ │ │ │ └── OutboundCreate.vue
│ │ │ ├── inventory/
│ │ │ │ └── InventoryList.vue
│ │ │ ├── customer/
│ │ │ │ └── CustomerList.vue
│ │ │ ├── supplier/
│ │ │ │ └── SupplierList.vue
│ │ │ ├── processing/
│ │ │ │ ├── ProcessingList.vue
│ │ │ │ └── ProcessingCreate.vue
│ │ │ ├── qr/
│ │ │ │ ├── QRGenerate.vue
│ │ │ │ └── QRScanRecord.vue
│ │ │ └── ReportView.vue
│ │ ├── components/ # 通用组件
│ │ │ ├── Navbar.vue
│ │ │ ├── Sidebar.vue
│ │ │ ├── Table.vue
│ │ │ ├── SearchForm.vue
│ │ │ ├── CustomerSelect.vue
│ │ │ ├── InventoryPicker.vue # 库存批次选择组件(出库时使用)
│ │ │ └── QRScanner.vue # 扫码输入组件
│ │ └── utils/
│ │ ├── formatters.js
│ │ └── validators.js
│ └── public/
│
├── scripts/
│ ├── init_db.py # 数据库表初始化
│ ├── seed_data.py # 测试数据填充
│ └── deploy.sh # 一键部署
│
├── docker-compose.yml # 本地开发环境
└── nginx/
└── jinkuan.conf # Nginx 配置
# ─── 命名规范 ───
# 类名: PascalCase
class OrderService:
pass
# 函数/方法: snake_case
def create_outbound_order(data):
pass
# 变量: snake_case
order_no = "JK20260524001"
# 常量: UPPER_SNAKE_CASE
MAX_FILE_SIZE = 10 * 1024 * 1024
# ─── API 视图函数 ───
from flask import Blueprint, request, jsonify
from app.decorators import jwt_required, role_required
bp = Blueprint('orders', __name__, url_prefix='/api/v1/orders')
@bp.route('', methods=['GET'])
@jwt_required()
def list_orders():
"""
订单列表
---
GET /api/v1/orders
查询参数: page, page_size, status, customer_id
"""
# 1. 参数解析
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
# 2. 业务逻辑调用 service
result = OrderService.list_orders(page=page, page_size=page_size)
# 3. 统一响应
return jsonify(code=0, data=result['data'], pagination=result['pagination'])
// ─── 命名规范 ───
// 组件名: PascalCase
// 变量/函数: camelCase
// 常量: UPPER_SNAKE_CASE
const API_BASE = '/api/v1'
// ─── Vue 组件结构 ───
<script setup>
import { ref, onMounted } from 'vue'
const loading = ref(false)
const list = ref([])
onMounted(async () => {
loading.value = true
try {
const res = await api.getOrderList()
list.value = res.data
} finally {
loading.value = false
}
})
</script>
# ─── 模块级 docstring ───
"""入库业务逻辑
提供入库单创建、库存新增等业务操作。
"""
# ─── 函数 docstring ───
def create_inbound(data: dict) -> dict:
"""
创建入库单并更新库存。
Args:
data: 入库单数据,包含供应商、明细行等
Returns:
创建的入库单对象(含单号、状态)
Raises:
ValidationError: 数据校验失败
BusinessError: 业务逻辑异常(如库存操作失败)
"""
pass
# ─── 复杂逻辑注释 ───
# 注意:出库时需要同时操作 3 张表
# 1. outbound_items 新增记录
# 2. inventory 扣减可用量
# 3. order_items 更新已发货量
# 全部在同一个事务中执行
# ─── 自定义异常类 ───
class AppError(Exception):
"""应用基础异常"""
def __init__(self, code: int, message: str, status_code=400):
self.code = code
self.message = message
self.status_code = status_code
class ValidationError(AppError):
"""参数校验错误"""
pass
class BusinessError(AppError):
"""业务逻辑错误(如库存不足)"""
pass
class NotFoundError(AppError):
"""资源不存在"""
def __init__(self, resource: str, resource_id):
super().__init__(40401, f"{resource} (id={resource_id}) 不存在", 404)
# ─── 统一错误处理 ───
@app.errorhandler(AppError)
def handle_app_error(error):
return jsonify(code=error.code, message=error.message), error.status_code
@app.errorhandler(404)
def handle_404(error):
return jsonify(code=40400, message="接口不存在"), 404
@app.errorhandler(500)
def handle_500(error):
app.logger.exception("服务器内部错误")
return jsonify(code=50000, message="服务器内部错误"), 500
import logging
# ─── 日志配置 ───
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
handlers=[
logging.StreamHandler(), # 控制台输出
logging.FileHandler('/var/log/jinkuan/app.log'), # 文件日志
]
)
logger = logging.getLogger('jinkuan')
# ─── 日志级别使用规范 ───
logger.info("用户 %s 创建了订单 %s", username, order_no) # 正常业务操作
logger.warning("库存不足: 品名=%s, 可用=%s, 需求=%s", product, available, required)
logger.error("出库操作失败: %s", str(error)) # 业务错误
logger.exception("订单创建异常") # 未预期的异常(自动包含 traceback)
# ─── 操作日志(记录到数据库) ───
def log_operation(user_id, username, action, resource_type, resource_id, detail=None):
"""将关键操作记录到 operation_logs 表"""
log = OperationLog(
user_id=user_id,
username=username,
action=action,
resource_type=resource_type,
resource_id=resource_id,
detail=json.dumps(detail, ensure_ascii=False) if detail else None,
ip_address=request.remote_addr,
)
db.session.add(log)
db.session.commit()
from app import db
class InventoryService:
@staticmethod
@db.transaction() # 事务装饰器,自动 commit/rollback
def inbound(items):
"""入库操作:同时操作 inbound_items 和 inventory"""
for item in items:
# 1. 写入入库明细
inbound_item = InboundItem(**item)
db.session.add(inbound_item)
# 2. 更新或创建库存(UPSERT)
inv = Inventory.query.filter_by(
product_name=item['product_name'],
spec=item['spec'],
batch_no=item['batch_no'],
location=item['location'],
quality_status=item.get('quality_status', 'qualified')
).first()
if inv:
inv.quantity_available += item['quantity']
inv.weight_available += item['weight_kg']
else:
inv = Inventory(
product_name=item['product_name'],
spec=item['spec'],
batch_no=item['batch_no'],
location=item['location'],
quantity_available=item['quantity'],
weight_available=item['weight_kg'],
quality_status=item.get('quality_status', 'qualified'),
)
db.session.add(inv)
# 事务装饰器自动 commit
class InventoryService:
@staticmethod
@db.transaction()
def consume(inventory_id: int, quantity: int, weight_kg: float) -> bool:
"""
扣减库存(带乐观锁,防止并发超卖)
Args:
inventory_id: 库存批次 ID
quantity: 扣减件数
weight_kg: 扣减重量(kg)
Returns:
True=扣减成功, False=库存不足
Raises:
BusinessError: 库存不足时
"""
# 使用 SELECT ... FOR UPDATE 做悲观锁
inv = Inventory.query.with_for_update().get(inventory_id)
if not inv:
raise NotFoundError('库存批次', inventory_id)
if inv.quantity_available < quantity or inv.weight_available < weight_kg:
raise BusinessError(
40301,
f"库存不足: 可用件数={inv.quantity_available}, 需={quantity}; "
f"可用重量={inv.weight_available}kg, 需={weight_kg}kg"
)
inv.quantity_available -= quantity
inv.weight_available -= weight_kg
return True
class OrderService:
@staticmethod
def update_order_delivery_status(order_id: int):
"""
更新订单发货状态:
- 所有明细 delivered_weight >= ordered_weight → completed
- 部分明细 delivered_weight > 0 → partial
- 全部 delivered_weight = 0 → pending(无变化)
"""
order = Order.query.get(order_id)
if not order:
return
items = OrderItem.query.filter_by(order_id=order_id).all()
if not items:
return
all_completed = all(
item.delivered_weight >= item.ordered_weight
for item in items
)
any_delivered = any(item.delivered_weight > 0 for item in items)
if all_completed:
order.status = 'completed'
elif any_delivered:
order.status = 'partial'
# else: 保持 pending
| 级别 | 范围 | 覆盖率目标 | 框架 |
|---|---|---|---|
| 单元测试 | Service 层业务逻辑、工具函数、单号生成 | ≥ 80% | pytest |
| 集成测试 | API 接口(请求→响应)、数据库操作 | ≥ 60% | pytest + Flask test client |
| E2E 测试 | 核心流程:订单创建→入库→出库→库存变更 | 核心流程 100% | 手工 + 未来 Playwright |
| # | 测试场景 | 覆盖内容 | 对应需求方案验收项 |
|---|---|---|---|
| 1 | 订单流程 | 创建订单 → 订单明细含 pending_weight 自动计算 → 状态流转(pending→partial→completed) | A1(订单录入)、A2(订单查询/编辑) |
| 2 | 入库流程 | 创建入库单 → 库存自动增加 → 重复入库同一批次(幂等性) | A3(入库登记)、A4(库存台账) |
| 3 | 出库流程 | 选择库存批次出库 → 库存扣减(并发安全) → 订单发货量更新 → 库存不足时拒绝 | A4(库存台账)、A5(订单发货表)、B3(出库联动) |
| 4 | 分切加工 | 母材库存扣减 → 子材/边丝/余料入库 → 出材率计算 | B1(分切加工单)、B2(材率计算与预警) |
| 5 | 送货单/质保书 | 出库后一键生成送货单 PDF、质保书自动匹配模板含电子公章 | B5(送货单PDF)、B6(质保书PDF) |
| 6 | 批次追溯 | 从原材料批次到成品批次到客户,全程可追溯 | B7(批次追溯)、C4(全链路追溯) |
| 7 | 二维码流程 | 批量生成标签 → 扫码入库 → 扫码出库 → 状态流转 | C1(标签生成)、C2(扫码入库)、C3(扫码出库) |
| 8 | 库存查询 | 多维筛选(品名/规格/批号/仓位/品质状态)正确性 | A4(库存台账) |
| 9 | 单号生成 | 同一天同前缀单号不重复(并发测试) | A1(订单录入-单号规范) |
| 10 | 权限控制 | 未登录拒绝访问 API、不同角色权限隔离 | A8(用户权限) |
| 11 | 经营报表 | 仪表盘数据准确性、销售/库存/利润分析正确性 | D1-D4(经营报表) |
tests/fixtures/ 目录管理测试夹具(JSON/YAML 格式)scripts/seed_data.py 用于本地开发环境造测试数据# 单元测试 + 覆盖率
pytest --cov=app --cov-report=term-missing tests/
# 仅单元测试(快速)
pytest -m unit tests/
# 集成测试
pytest -m integration tests/
-- =============================================
-- 今宽进销存系统 - 完整建表 DDL
-- 数据库: PostgreSQL 16
-- =============================================
-- 启用 UUID 扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 8.1 客户表
CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(32) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
contact VARCHAR(100),
phone VARCHAR(32),
address TEXT,
credit_terms VARCHAR(64),
remark TEXT,
status VARCHAR(16) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_customers_code ON customers(code);
CREATE INDEX idx_customers_status ON customers(status);
-- 8.2 供应商表
CREATE TABLE suppliers (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(32) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
contact VARCHAR(100),
phone VARCHAR(32),
material_scope VARCHAR(200),
remark TEXT,
status VARCHAR(16) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_suppliers_code ON suppliers(code);
CREATE INDEX idx_suppliers_status ON suppliers(status);
-- 8.3 订单表
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
order_no VARCHAR(64) UNIQUE NOT NULL,
customer_po_no VARCHAR(128),
customer_id BIGINT NOT NULL REFERENCES customers(id),
order_date DATE NOT NULL,
delivery_date DATE,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
total_weight DECIMAL(12,3),
total_amount DECIMAL(14,2),
remark TEXT,
document_type VARCHAR(32) DEFAULT 'purchase_order',
price_type VARCHAR(16) DEFAULT 'taxed',
source_type VARCHAR(16) DEFAULT 'manual',
ai_raw_result JSONB,
source_file VARCHAR(512),
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_orders_no ON orders(order_no);
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_date ON orders(order_date);
-- 8.4 订单明细表
CREATE TABLE order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_name VARCHAR(200),
material_type VARCHAR(16) NOT NULL DEFAULT 'coil',
spec VARCHAR(200),
hardness VARCHAR(16),
quantity DECIMAL(12,3) NOT NULL,
weight_kg DECIMAL(12,3),
unit_price DECIMAL(12,2),
amount DECIMAL(14,2),
ordered_weight DECIMAL(12,3) NOT NULL,
delivered_weight DECIMAL(12,3) DEFAULT 0,
pending_weight DECIMAL(12,3) GENERATED ALWAYS AS (ordered_weight - delivered_weight) STORED,
remark TEXT
);
CREATE INDEX idx_items_order ON order_items(order_id);
CREATE INDEX idx_items_type ON order_items(material_type);
-- 8.5 入库单表
CREATE TABLE inbound_orders (
id BIGSERIAL PRIMARY KEY,
inbound_no VARCHAR(64) UNIQUE NOT NULL,
supplier_id BIGINT REFERENCES suppliers(id),
inbound_date DATE NOT NULL,
type VARCHAR(32) NOT NULL DEFAULT 'purchase',
status VARCHAR(16) NOT NULL DEFAULT 'draft',
remark TEXT,
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_inbound_no ON inbound_orders(inbound_no);
CREATE INDEX idx_inbound_supplier ON inbound_orders(supplier_id);
CREATE INDEX idx_inbound_date ON inbound_orders(inbound_date);
-- 8.6 入库明细表
CREATE TABLE inbound_items (
id BIGSERIAL PRIMARY KEY,
inbound_id BIGINT NOT NULL REFERENCES inbound_orders(id) ON DELETE CASCADE,
product_name VARCHAR(200) NOT NULL,
spec VARCHAR(200),
hardness VARCHAR(16),
quantity INTEGER NOT NULL,
weight_kg DECIMAL(12,3) NOT NULL,
batch_no VARCHAR(64),
internal_no VARCHAR(64),
location VARCHAR(64),
quality_status VARCHAR(16) NOT NULL DEFAULT 'qualified',
remark TEXT
);
CREATE INDEX idx_inbound_items_inbound ON inbound_items(inbound_id);
CREATE INDEX idx_inbound_items_batch ON inbound_items(batch_no);
-- 8.7 库存表
CREATE TABLE inventory (
id BIGSERIAL PRIMARY KEY,
product_name VARCHAR(200) NOT NULL,
spec VARCHAR(200),
hardness VARCHAR(16),
quantity_available INTEGER NOT NULL DEFAULT 0,
weight_available DECIMAL(12,3) NOT NULL DEFAULT 0,
batch_no VARCHAR(64),
internal_no VARCHAR(64),
location VARCHAR(64),
quality_status VARCHAR(16) NOT NULL DEFAULT 'qualified',
supplier_id BIGINT REFERENCES suppliers(id),
inbound_date DATE,
inbound_item_id BIGINT,
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_inventory_uniq ON inventory(product_name, spec, batch_no, location, quality_status);
CREATE INDEX idx_inventory_batch ON inventory(batch_no);
CREATE INDEX idx_inventory_location ON inventory(location);
CREATE INDEX idx_inventory_status ON inventory(quality_status);
-- 8.8 出库单表
CREATE TABLE outbound_orders (
id BIGSERIAL PRIMARY KEY,
outbound_no VARCHAR(64) UNIQUE NOT NULL,
order_id BIGINT REFERENCES orders(id),
customer_id BIGINT REFERENCES customers(id),
outbound_date DATE NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'draft',
remark TEXT,
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_outbound_no ON outbound_orders(outbound_no);
CREATE INDEX idx_outbound_order ON outbound_orders(order_id);
CREATE INDEX idx_outbound_customer ON outbound_orders(customer_id);
-- 8.9 出库明细表
CREATE TABLE outbound_items (
id BIGSERIAL PRIMARY KEY,
outbound_id BIGINT NOT NULL REFERENCES outbound_orders(id) ON DELETE CASCADE,
inventory_id BIGINT REFERENCES inventory(id),
product_name VARCHAR(200) NOT NULL,
spec VARCHAR(200),
hardness VARCHAR(16),
quantity INTEGER NOT NULL,
weight_kg DECIMAL(12,3) NOT NULL,
batch_no VARCHAR(64),
remark TEXT
);
CREATE INDEX idx_outbound_items_outbound ON outbound_items(outbound_id);
CREATE INDEX idx_outbound_items_inventory ON outbound_items(inventory_id);
-- 8.10 加工单表
CREATE TABLE processing_orders (
id BIGSERIAL PRIMARY KEY,
process_no VARCHAR(64) UNIQUE NOT NULL,
order_item_id BIGINT REFERENCES order_items(id),
process_type VARCHAR(16) NOT NULL,
parent_material_inventory_id BIGINT REFERENCES inventory(id),
parent_spec VARCHAR(200),
parent_weight DECIMAL(12,3),
child_spec VARCHAR(200) NOT NULL,
output_weight DECIMAL(12,3) NOT NULL,
scrap_weight DECIMAL(12,3) DEFAULT 0,
return_weight DECIMAL(12,3) DEFAULT 0,
process_date DATE NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
remark TEXT,
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_processing_no ON processing_orders(process_no);
CREATE INDEX idx_processing_order ON processing_orders(order_item_id);
CREATE INDEX idx_processing_parent ON processing_orders(parent_material_inventory_id);
-- 8.11 二维码标签表
CREATE TABLE qr_labels (
id BIGSERIAL PRIMARY KEY,
label_no VARCHAR(64) UNIQUE NOT NULL,
batch_no VARCHAR(64),
product_name VARCHAR(200),
spec VARCHAR(200),
weight_kg DECIMAL(12,3),
qr_content TEXT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'unscanned',
scanned_at TIMESTAMP,
source VARCHAR(16),
related_transaction_id BIGINT,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_qr_label_no ON qr_labels(label_no);
CREATE INDEX idx_qr_batch ON qr_labels(batch_no);
CREATE INDEX idx_qr_status ON qr_labels(status);
-- 8.12 系统用户表
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
password_hash VARCHAR(256) NOT NULL,
role VARCHAR(32) NOT NULL DEFAULT 'operator',
display_name VARCHAR(100) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_role ON users(role);
-- 8.13 操作日志表
CREATE TABLE operation_logs (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
username VARCHAR(64),
action VARCHAR(32) NOT NULL,
resource_type VARCHAR(32) NOT NULL,
resource_id BIGINT,
detail TEXT,
ip_address VARCHAR(45),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_logs_resource ON operation_logs(resource_type, resource_id);
CREATE INDEX idx_logs_user ON operation_logs(user_id);
CREATE INDEX idx_logs_time ON operation_logs(created_at);
-- 8.14 单号计数器表
CREATE TABLE doc_counters (
id BIGSERIAL PRIMARY KEY,
prefix VARCHAR(8) NOT NULL,
biz_date DATE NOT NULL,
current_seq INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (prefix, biz_date)
);
-- =============================================
-- 初始化数据
-- =============================================
-- 默认管理员账户(密码: admin123,需在生产环境修改)
INSERT INTO users (username, password_hash, role, display_name)
VALUES ('admin', '$2b$12$LJ3m4ys3Lk0TSwHnbfOMqOX7Zl5vDqFYpFvqGqG8N9gMBpGpYSFq', 'admin', '系统管理员');
-- 默认操作员账户(密码: operator123)
INSERT INTO users (username, password_hash, role, display_name)
VALUES ('operator', '$2b$12$8pKWkG3x4Bv1pJI4Jw1QVO3KFPEmkVP3LtUjTxHZBx5YXvV6KSJi', 'operator', '操作员');
设计说明:使用 PostgreSQL 数据库序列保证单号唯一性。避免文件计数器方案(
/tmp重启清空、多进程并发冲突)。每天每个前缀对应一条 counter 记录,利用数据库行级锁保证并发安全。
-- 单号计数器表(建表时执行)
CREATE TABLE IF NOT EXISTS doc_counters (
id BIGSERIAL PRIMARY KEY,
prefix VARCHAR(8) NOT NULL, -- 单号前缀(JK/RK/CK/PG/QR)
biz_date DATE NOT NULL, -- 业务日期
current_seq INTEGER NOT NULL DEFAULT 0, -- 当前序号
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (prefix, biz_date) -- 每天每前缀唯一
);
# app/utils/docno.py
from datetime import date
from app import db
def generate_no(prefix: str, business_date: date = None) -> str:
"""
生成单号。
格式: {prefix}{YYYYMMDD}{3位序号}
每天重新计数。使用数据库行级锁保证多进程并发安全。
Args:
prefix: 单号前缀(JK=订单 / RK=入库 / CK=出库 / PG=加工 / QR=二维码)
business_date: 业务日期,默认当天
Returns:
单号字符串
"""
if business_date is None:
business_date = date.today()
# UPSERT + 原子递增,利用唯一约束的行级锁防止并发冲突
from app.models.doc_counter import DocCounter
result = db.session.execute(
"""
INSERT INTO doc_counters (prefix, biz_date, current_seq)
VALUES (:prefix, :biz_date, 1)
ON CONFLICT (prefix, biz_date)
DO UPDATE SET current_seq = doc_counters.current_seq + 1,
updated_at = NOW()
RETURNING current_seq
""",
{"prefix": prefix, "biz_date": business_date},
)
seq = result.scalar_one()
db.session.commit()
return f"{prefix}{business_date.strftime('%Y%m%d')}{seq:03d}"
文档结束 · 铭见(上海)智能科技有限公司 · 2026-05-24
本说明书覆盖 Phase 1-4 全阶段开发,含 14 张业务表的 DDL 设计、30+ 个 RESTful API 接口、12 个前端页面规划、二维码和 AI 识别的完整集成方案,以及测试策略和并发安全设计。