feat(stocktake): 新增盘点会话表 StocktakeSession
盘点原本只有 stocktake_draft(草稿行),没有会话实体,导致三个问题: 1. 盲盘/明盘、全盘/抽盘这类**会话级配置**无处存放,塞进草稿表就要每行 冗余一份,改一次配置得 UPDATE 上万行; 2. 「空会话」无法表示 —— 会话开了但还没扫码时草稿表里没有行,旧实现只能 靠 max(scan_time) 猜哪个会话活跃,于是新开的空会话会被其他 PDA 忽略、 反而加入上一轮的旧会话,多端协同直接分裂; 3. 没有状态字段,generate-missing 跑完后旧会话仍被当成活跃。 本表把活跃会话判定变成 company_name + status='active' 的精确查询。 迁移的要点: - 唯一部分索引 uq_stocktake_session_one_active 保证「一个公司同时只有一个 活跃会话」,从数据库层挡住多台 PDA 并发开启导致的进度分裂; 因此 /draft/start-new 必须先把原有活跃会话置为 finished 再插入新记录。 - CHECK 约束限定 mode/scope_type/status 的取值域。 - 不回填存量:旧草稿没有对应会话行,部署后显示「无进行中的盘点」,数据不丢。 执行: docker exec -i inventory_db psql -U test -d inventory_system < db_migrations/add_stocktake_session.sql
This commit is contained in:
@ -2,6 +2,64 @@ from app.extensions import db, beijing_time # .material -> .base refactor check
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# 盘点模式
|
||||
STOCKTAKE_MODE_OPEN = 'open' # 明盘:可看到账面数
|
||||
STOCKTAKE_MODE_BLIND = 'blind' # 盲盘:隐藏账面数与差异
|
||||
|
||||
# 盘点范围
|
||||
STOCKTAKE_SCOPE_FULL = 'full' # 全仓盘点
|
||||
STOCKTAKE_SCOPE_ACTIVE = 'active' # 活跃库位抽盘(范围过滤尚未实现,暂不可选)
|
||||
|
||||
# 会话状态
|
||||
STOCKTAKE_STATUS_ACTIVE = 'active'
|
||||
STOCKTAKE_STATUS_FINISHED = 'finished'
|
||||
|
||||
|
||||
class StocktakeSession(db.Model):
|
||||
"""
|
||||
盘点会话表 —— 一次盘点的「主心骨」。
|
||||
|
||||
为什么需要独立一张表(而不是把配置冗余进 stocktake_draft):
|
||||
1. 盲盘/明盘、全盘/抽盘都是**会话级**属性,塞进草稿表就要每行存一份,
|
||||
改一次配置得 UPDATE 上万行;
|
||||
2. stocktake_draft 没有任何「会话存在但还没扫码」的表示能力 ——
|
||||
空会话没有草稿行,旧实现只能靠 max(scan_time) 去猜哪个会话是活跃的,
|
||||
导致新开的空会话被其他 PDA 忽略、反而加入上一轮的旧会话;
|
||||
3. 没有状态字段就无法把「已结束」的会话排除掉,generate-missing 跑完后
|
||||
旧会话仍被当成活跃。
|
||||
|
||||
有了本表,活跃会话判定变成 company_name + status='active' 的精确查询。
|
||||
"""
|
||||
__tablename__ = 'stocktake_session'
|
||||
|
||||
session_id = db.Column(db.String(100), primary_key=True)
|
||||
company_name = db.Column(db.String(255), nullable=False, index=True)
|
||||
# open(明盘) / blind(盲盘) —— 决定 merged-list 是否下发账面数
|
||||
mode = db.Column(db.String(20), nullable=False, default=STOCKTAKE_MODE_OPEN)
|
||||
# full(全仓) / active(活跃库位抽盘)
|
||||
scope_type = db.Column(db.String(20), nullable=False, default=STOCKTAKE_SCOPE_FULL)
|
||||
# 范围明细,例: {"days":30,"top_n":50,"locations":[...],"uuids":[...]}
|
||||
scope_config = db.Column(db.JSON, nullable=False, default=dict)
|
||||
# active / finished
|
||||
status = db.Column(db.String(20), nullable=False, default=STOCKTAKE_STATUS_ACTIVE, index=True)
|
||||
created_by = db.Column(db.String(100))
|
||||
created_at = db.Column(db.DateTime, default=beijing_time)
|
||||
finished_at = db.Column(db.DateTime)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'session_id': self.session_id,
|
||||
'company_name': self.company_name,
|
||||
'mode': self.mode,
|
||||
'scope_type': self.scope_type,
|
||||
'scope_config': self.scope_config or {},
|
||||
'status': self.status,
|
||||
'created_by': self.created_by,
|
||||
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
|
||||
'finished_at': self.finished_at.strftime('%Y-%m-%d %H:%M:%S') if self.finished_at else None,
|
||||
}
|
||||
|
||||
|
||||
class StocktakeDraft(db.Model):
|
||||
"""
|
||||
盘点草稿表
|
||||
|
||||
Reference in New Issue
Block a user