Files
KCGL/inventory-backend/app/models/scan_draft.py
yueli ec66c33b06 feat(scan-draft): 扫码草稿表与接口,支持暂停后继续扫码
场景
----
扫码出库/借库的作业可能很长(一张单几十项),工人常需中途暂停去处理
更紧急的单据。改造前切换单据会清空已扫内容,刷新/退出页面则全部丢失。

由于库存在申请审批通过时已**预占**,暂停期间货不会被他人抢走 ——
因此草稿只记录「扫到哪了」,**不涉及任何库存操作**。即使草稿丢失也只是
需要重扫,不会造成库存错乱。

隔离粒度
--------
按 (user_id, biz_type, request_id) 一人一单:每个人扫自己的草稿,互不影响;
同一人可同时持有多张单据的草稿(正是「暂停 A 去出 B」的场景)。
user_id 一律取自 JWT,不接受入参覆盖,故不可能读写他人草稿。

为什么整单存一个 JSON(而非每条明细一行)
------------------------------------------
1. 保存是「全量覆盖」语义,逐行存无增量更新的收益;
2. 恢复时需要物料名称/规格/库位等展示字段,逐行方案只能回查申请单的
   items_json —— 而历史单据的 items_json 不含 stock_id,回查会错配。
   整单快照把展示字段一并存下,恢复零依赖,对老单据同样可靠。

接口
----
GET    /api/v1/scan-draft           读取草稿
POST   /api/v1/scan-draft           保存(全量覆盖;空清单则删除)
DELETE /api/v1/scan-draft           清除(提交成功后调用)
GET    /api/v1/scan-draft/overview  各单据进度,供下拉徽标

实现要点
--------
· items_json 列是 jsonb,模型必须用 db.JSON —— 用 db.Text 会让 psycopg2
  拿到 Python list/dict 时无法适配,报 "can't adapt type 'dict'",而异常
  被接口的 except 吞掉后 POST 仍返回"成功",问题极难发现(开发中实际踩到);
· 概览接口做防御:残留的空草稿不参与展示。

实测:保存/读回、跨单据隔离(B 读 A 的草稿为 0 项)、全量覆盖、
      提交后清除,全部符合预期。
2026-09-10 17:21:14 +08:00

60 lines
2.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
from app.extensions import db, beijing_time
class ScanDraft(db.Model):
"""
扫码草稿(出库 / 借库作业中途暂停用)
场景:一张单几十项,工人扫到一半需要去处理更紧急的单据,事后回来继续。
库存已在申请审批通过时预占,暂停期间不会被他人抢走,因此草稿只需记录
「扫到哪了」,不涉及库存操作。
隔离粒度 (user_id, biz_type, request_id):一人一单;同一人可同时持有
多张单据的草稿(正是「暂停 A 去出 B」的场景
为什么整单存 JSON 而非逐条明细行:
恢复时需要物料名称/规格/库位等展示字段。逐行存方案只能回查申请单的
items_json —— 而历史单据的 items_json 不含 stock_id回查会错配。
整单快照把展示字段一并存下,恢复时零依赖,对老单据同样可靠。
"""
__tablename__ = 'scan_draft'
__table_args__ = (
db.UniqueConstraint('user_id', 'biz_type', 'request_id', name='uq_scan_draft'),
)
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, nullable=False, index=True)
biz_type = db.Column(db.String(20), nullable=False) # outbound / borrow
request_id = db.Column(db.Integer, nullable=False)
request_no = db.Column(db.String(100)) # 单号快照,展示免联表
# ★ 必须用 db.JSON 而非 db.Text —— 数据库列是 jsonb
# (见 db_migrations/add_scan_draft.sql
# db.Text 会让 psycopg2 拿到 Python list/dict 时无法适配,
# 报 "can't adapt type 'dict'";而用 db.Text 存 json.dumps 的字符串
# 又会与 jsonb 列的类型语义打架。类型一致才是正解。
items_json = db.Column(db.JSON, nullable=False, default=list)
updated_at = db.Column(db.DateTime, default=beijing_time, onupdate=beijing_time, nullable=False)
created_at = db.Column(db.DateTime, default=beijing_time, nullable=False)
def set_items(self, items):
self.items_json = items or []
def get_items(self):
v = self.items_json
if not v:
return []
if isinstance(v, list):
return v
# 兼容历史数据(若该列曾被写成 JSON 字符串)
try:
parsed = json.loads(v)
return parsed if isinstance(parsed, list) else []
except (ValueError, TypeError):
return []
@property
def item_count(self):
return len(self.get_items())