|
|
ccdd6e7306
|
fix(purchase): 商家地址链接放宽为 text,修超长链接保存失败
现象
----
采购管理新建采购申请时粘贴电商商品链接报错。实测该链接 **516 字符**,而
purchase_request.supplier_link 是 varchar(500) —— 超出 16 个字符,
PostgreSQL 直接拒绝(value too long for type character varying(500))。
★ 为什么用 text 而不是把 500 调大
电商外链(淘宝/1688)普遍带很长的跟踪参数,长度没有稳定上界:本例已 516,
再叠加一轮营销参数就可能破千。任何有限的 varchar(N) 都只是把报错往后推,
而且**截断是静默的** —— 用户只会觉得「链接打不开」。
同 material_base.purchase_link 的处理(那边一开始就用 text)。
附带确认(全部扫描,无需改动)
从数据库层面扫了所有「有长度上限、且列名像 link/url/photo/image/
signature/path」的列:
· purchase_request.supplier_link(500) ← 本次唯一真正有问题的
· sys_menu.path(200)、sys_warehouse_location.full_path(500) 内部生成数据
· stock_adjustment.linked_sku(100)/linked_outbound_no(50) SKU 与单号
用户粘贴外链的列仅此一处。
迁移用 DO 块先判断当前类型,可重复执行;含核对段与回滚段。
验证:把那条 516 字符的真实链接写入再读回,长度一致、内容未截断。
|
2026-09-18 09:34:44 +08:00 |
|
|
|
1eaa203eba
|
fix(db): 修正存量「归还时间/报废时间」的 8 小时时区偏差(附自检)
问题
----
process_return 与 scrap_sources.deduct 原先用 datetime.now()(容器本地时间,
Docker 默认 UTC)写 return_time / operation_time,而同一行的 borrow_time 由
beijing_time() 写入 —— 同一行两个时钟,差 8 小时。
实测(开发库)trans_borrow 有 53 行呈现「归还时间早于借出时间」这种物理上
不可能的数据,例如 id=80 借出 09-09 11:04、归还 09-09 03:46。
代码侧已修(f7c49f4),新数据已是北京时间 —— 本次只处理存量。
★ 脚本刻意做了「先自检、后修正」的两段式
服务器 API 容器的 TZ 未必是 UTC(若本就是 Asia/Shanghai,datetime.now()
一直是北京时间,则**根本无需修正**)。若不做自检直接 +8h,会把正确的时间
改错。故第 0 步先输出:异常行数、以及按分界切出的「UTC 段/空档/北京段」
三段计数,由使用人据结果决定是否执行第 2 步。
★ 分界与幂等性都在脚本里写明
· 分界 = 修复代码在该服务器**部署生效的时刻**(不是提交时刻),需按实际调整;
· 脚本**不幂等** —— 重复执行会再加 8 小时,已显式警示并给出回滚写法。
开发库已按此逻辑修正:53 行 trans_borrow + 1 行 trans_scrap,
修正后「归还早于借出」为 0,抽样时间落在正常工作时段。
|
2026-09-18 09:27:17 +08:00 |
|
|
|
0005a689dc
|
fix(outbound): 补发申请人取原申请人,绝不再回退为库管
问题
----
上一版在库管未指定「补发给谁」时,把补发单申请人回退成了**当前操作人(库管)**。
但补发是**原申请人的需求**,挂到库管名下逻辑不通 —— 那张单会出现在库管的
「我的申请」里,而真正该拿东西的人什么也看不到。
根因
----
trans_outbound 只有 consumer_name(**扫码时前端自由填写**的领用人/客户名,
既不可靠也可能是外部客户),没有任何指回原审批单的关联,所以当时只能退而求其次。
根治
----
一、trans_outbound 新增 applicant_id,**创建出库时从关联审批单带出**
(request_id 已强制必填、approval 恒非 None,故新单据必然有值)。
落这一列后,「退回 → 补发」即可自动找回真正的原申请人。
⚠ 存量行为 NULL —— 存量出库与其来源审批单之间没有任何可用关联,无从回填。
刻意留 NULL 而不是按姓名猜(consumer_name 是自由文本,会重名错绑),
与 dispatch_operator 同一取舍:宁可留空,也不猜。
二、退回接口的申请人优先级改为:
① 前端显式指定 reissue_applicant_id
② 原出库明细记录的 applicant_id(真实原申请人)
③ 都没有 → **报错要求指定**
★ 彻底移除「回退为当前操作人」—— 那正是本次要修的逻辑错误。
三、出库列表明细返回 applicant_id,供前端精确预填。
验证(9 项断言全通过)
· 新单据 → 申请人 = 原申请人(12),绝不是库管(7)
· 显式指定优先于原申请人
★ 历史单据未指定 → 接口拒绝、要求选择「补发给谁」、整笔回滚、
且**未生成任何挂在库管名下的补发单**
· 历史单据 + 显式指定 → 正常
库存与数据零残留。
|
2026-09-17 12:07:21 +08:00 |
|
|
|
7071c89305
|
feat(outbound): 原单退回后可自动生成补发单
背景
----
原单退回只做两件事:良品加回库存 / 不良品转在管台账。但**申请人的需求并没有
被满足** —— 东西交回来了(甚至还是坏的),系统却不提醒任何人、无单据承载
「我要重新领一份」,退回与后续再出库之间也毫无关联。现场只能靠人记住再手建
一张出库申请,而那张单与原单看不出任何关系。
改动
----
· outbound_approval 新增 source_return_id(非空 = 补发单),把「退回 → 补发」
串成闭环。
· POST /inbound/stock/return-from-outbound 新增 need_reissue / reissue_qty:
勾选即自动生成一张**免审批**出库单(status=1,直接进入待执行),
沿用原单出库类型,并关联回本笔退回。
★ 为什么只存退回单 ID,不加 is_reissue 布尔列
「是不是补发」完全由来源是否存在决定,再加一列就是同一事实的两处存储,
必然有不同步的一天。也不冗余存原出库单 ID:trans_return 已有 outbound_id。
★ 库存不足 → 整笔回滚(关键取舍)
补发走 reserve_for_items(strict=True),与出库申请同一口径。不足时抛错,
退回也一并回滚 —— 若只让补发静默失败,「需要补发」的意图就丢了,
那正是本功能要解决的问题。库管看到提示后取消勾选即可只做退回过账。
★ 一个被发现的数据约束(改变了原设计)
原打算把补发单的申请人设为「原出库单的申请人」,但 **trans_outbound 既没有
申请人字段,也没有指回原审批单的关联**(扫码出库时只把审批单状态置为 3)。
按 consumer_name 反查会重蹈「重名错绑」的覆辙。故申请人取**当前操作人**,
原领用人写入备注供人工追溯。
⚠ 若业务要求补发单挂在原领用人名下,需要前端在退回弹窗里加一个「补发给谁」
的人员选择 —— 请确认是否需要。
验证(打桩 JWT 直连真实接口,22 项断言全通过)
勾选补发 → 免审批单生成、关联退回、预占库存、原领用人入备注;
不勾选 → 不生成补发单、良品正常回库;
★ 库存不足 → 接口拒绝且**退回流水/补发单/退回额度全部未落库**(整笔回滚);
补发量 > 退回量被拒;库存与数据零残留。
|
2026-09-17 11:56:38 +08:00 |
|
|
|
2f658407c3
|
feat(db): 基础信息新增「采购链接」字段并注册权限
需求:物料主数据增加一个可直接跳转的补货地址(淘宝/1688 等),纳入字段权限管控。
★ 设计取舍:读写**共用同一个权限码** material_list:purchaseLink
上一轮刚修完「写用 A 码、读用 B 码」造成的读写错位(附件备注),新字段刻意
沿用 referencePrice 的既有模式 —— 一个码同时进 field_permissions.py(读过滤)
与 base.py 的 field_to_perm(写过滤)。单码最大的好处是**结构上不可能错位**:
能读必然能写、反之亦然,不会再有「填了看不到」或「看得到改不了」。
若将来要求「能看不能改」,需拆两个码并同步改三处,届时一并回归验证。
★ 列类型用 text 而非 varchar(N):外链常带很长的查询串,截断后是打不开的地址,
且截断是静默的 —— 用户只会觉得「链接坏了」。
默认授予 SUPER_ADMIN / SUPERVISOR / WAREHOUSE_MGR / INBOUND(覆盖实际补货的人
与管理物料的人)。⚠ 这是本次取的默认值,业务可按需增删 —— 改 VALUES 再执行一次即可。
幂等;不含 psql 元命令,DataGrip 可直接整段执行;含核对段与回滚段。
|
2026-09-17 11:26:36 +08:00 |
|
|
|
6b7b174e3d
|
fix(db): 补注册附件备注的读写权限码(修「写通读断」的权限错位)
根因(比上一轮报告的更深一层)
----
field_permissions.py 的读过滤要求两个权限码:
material_list:productImageRemark / material_list:manualLinkRemark
而这两个码**在 sys_element 里根本不存在**,也从未授予任何角色 —— 是「幽灵权限」。
除 SUPER_ADMIN(走 material_list:* 通配符绕过)外,没有任何人能通过读过滤。
配合写侧的「不在映射中 → 默认允许」兜底,形成:
谁都能写、除超管没人能读 —— 填了存进去了,回显却被抹成 null,
看起来就是「保存不了」。
本次改动
----
一、读侧:注册这两个元素,授予与 material_list:files **完全相同的 6 个角色**
(INBOUND / OUTBOUND / SALES / SUPERVISOR / WAREHOUSE_MGR / SUPER_ADMIN)
依据:备注依附于图片,「能看图的人就应该能看备注」。
二、写侧:新建 material_list:remark_edit,只授予 3 个核心管理角色
(SUPER_ADMIN / SUPERVISOR / WAREHOUSE_MGR)。
★ 为何不复用 material_list:operation:它授予了 5 个角色(含 INBOUND /
OUTBOUND),比业务要求的 3 个更宽,达不到「只有核心管理角色可改」。
⚠ 该码只做精确匹配,**切勿用 @permission_required 包裹** ——
_expand_operation_perms 会前缀桥接,把 material_list:operation 的持有者
一并放行,等于把刚收紧的口子又捅开。已在迁移与代码注释中双处警示。
幂等,可重复执行;脚本文本不含 psql 元命令,DataGrip 可直接整段执行。
|
2026-09-17 11:07:50 +08:00 |
|
|
|
b26b124f8b
|
chore(db): 部署脚本改用通用 SQL 核对段,兼容 DataGrip
psql 的 \echo 是元命令,DataGrip / DBeaver 不认,整份执行会直接报语法错误。
核对段改用 SELECT '常量' AS "核对项" 输出标签行,任何客户端都能跑
(psql 下效果相同)。同步修正头部说明中「需用 psql」的过期表述。
已在临时库重跑验证:零报错、核对结果与改前一致、重跑幂等。
|
2026-09-17 10:51:31 +08:00 |
|
|
|
d6234622e8
|
chore(db): 借库转交完整部署脚本(生产可执行)
把本轮 6 个迁移(phase4 / 4b / 4c / 4d / 4e / 4f)按依赖顺序合并为一份
可直接在生产执行的脚本,并在一个模拟「部署前状态」的临时库上完整验证。
内容
----
1. trans_borrow 补 4 列(borrower_id / current_holder_id /
current_holder_name / dispatch_operator)+ 2 索引
2. trans_borrow_transfer 建表(最终形态)+ 7 索引
3. trans_borrow_return 建表 + 3 索引
4. 回填 trans_borrow 身份锚点(仅唯一命中者,重名/无法映射留 NULL)
5. 回填遗留转交流水的 borrow_no 与状态
6. 回填 reject_seen_at(存量拒收标记为已告知)
7. 从 remark 拆出被拼接的 reject_reason
8.(注释掉)borrow_transfer 权限码 —— 已无代码引用,默认不建
+ 执行后核对段 + 回滚段
★ 验证中发现并修掉两个真实缺陷(都不是「看起来能跑」能暴露的)
1) 顺序缺陷:第 5 段原先把遗留流水**一律**标成 ACCEPTED,导致第 6/7 段
按 status='REJECTED' 找行时一条都匹配不到(旧结构表里 status 是刚加的
列、全是默认 PENDING)。真正的信号在备注的 '[拒绝原因]' 标记里,
现据此还原真实状态。
2) 孤儿流水:第 5 段按 borrow_id 关联,来源借用行已被删除的流水匹配不上,
会永久停在 PENDING —— 在接收人那里变成谁也处理不掉的幽灵待办。
已加兜底把「没有单号」的遗留行一律结掉。
★ 两处 ⚠ 警示已写入脚本:第 5/6/7 段设计为**新代码上线前执行一次**;
若在功能已投产后重跑,会把当时真实的待接收/待告知记录误标。
验证方式:建临时库复刻部署前结构(旧 trans_borrow + 旧结构转交流水表 +
重名/无法映射/已归还/孤儿等边界数据),执行脚本后核对:DDL 与索引齐全、
身份锚点按唯一性正确回填、遗留流水状态从备注还原、原因正确拆出、
孤儿流水被结掉、无报错;再执行第二遍确认幂等(结果完全一致)。
|
2026-09-17 10:49:34 +08:00 |
|
|
|
681607bd43
|
feat(borrow): 拒收原因独立成列,与转交备注彻底分离
背景
----
拒收原因此前是**拼进 remark** 的:
transfer.remark = f"{remark}\n[拒绝原因] {reason}"
前端拿到的是「3333\n[拒绝原因] 5555」这样一坨,时间线上两句挤在一起,
无法分辨哪句是发起备注、哪句是对方拒收的原因。
改动
----
· trans_borrow_transfer 新增 reject_reason text 列;
reject_transfer 改为写入该列,不再拼进 remark。
· 存量按 '[拒绝原因] ' 标记切分回填(实测仅 #22:
remark 3333 / reject_reason 5555)。
· 时间线事件带出 reject_reason,前端才能分行展示。
★ 为什么拆列而不是让前端解析字符串
1) 拼接格式是隐式契约:改分隔符或加前缀,前端解析就静默失效且难排查;
2) 用户完全可能在备注里自己打出 '[拒绝原因]' 字样,按标记切分必然误判 ——
已加测试用例锁定该场景;
3) 结构化字段才能参与查询与统计(如按拒收原因归类)。
存储层能表达的东西,不该靠字符串约定去还原。
★ 一个迁移期踩到的坑:btrim 默认只去空格、不去换行。
拼接留下的是 '3333\n',只写 btrim(x) 会残留换行;必须显式给出字符集
btrim(x, E' \t\r\n')。已修正脚本并对存量做了一次清理。
验证(7 项断言全通过)
备注不被污染、原因写独立列、无原因时为 None、
用户备注含同名标记也不误判、库存零副作用、数据零残留。
|
2026-09-17 10:46:31 +08:00 |
|
|
|
f24797ff1f
|
feat(borrow): 拒收须告知发起方(责任回到他手上,不能静默)
背景
----
双向握手补上了「接收人确认」,却只做了单向告知:接收人能看到待办,发起方却
对结果一无所知。**被拒绝时物品责任仍在发起方手上** —— 他若不主动查列表,
就会误以为已经交接出去,责任链出现静默断点。
(ACCEPTED 不需要告知:东西已经交出去了,发起方无需动作。)
改动
----
· trans_borrow_transfer 新增 reject_seen_at(NULL 且 REJECTED = 尚未告知)。
★ 为什么需要持久标记而不是前端去重:换台电脑、换个浏览器就会重新提醒;
而这条信息的分量(责任归属)值得一个持久标记。
★ 存量已拒绝的流水一律标记为已告知:它们产生于本功能上线之前,
追溯提醒只会打扰(实测仅 1 条:#22,验收时的测试数据)。
· get_unseen_rejects(user_id):返回「我发起、被拒、尚未告知我」的转交,
并批量解析物料名 —— 只说「某笔转交被拒」发起方仍不知是哪件东西还在
自己手上,必须让他一眼认出来。
· ack_rejects(user_id, ids):发起方确认后写 reject_seen_at,幂等。
· GET .../transfer/pending-count 的响应并入 rejects:与待接收数量共用同一次
轮询,前端不必多打一个请求。
· POST .../transfer/reject-ack:无 permission_required,同 accept/reject。
顺带补一处同源显示缺口
----
流转时间线里,被拒绝的转交与成功的长得一模一样 —— 发起方翻记录时同样会
误判。现将转交状态一并带出时间线事件。
验证(15 项断言全通过)
----
发起方收到待告知的拒绝(含物料名/接收人/拒绝原因);接收人与无关人看不到;
ack 后不再提醒且幂等;ACCEPTED 不产生告知;None/非法 user_id 均安全返回;
库存零副作用、数据零残留。
|
2026-09-17 10:44:00 +08:00 |
|
|
|
7d9cdeb297
|
feat(borrow): 转交粒度下沉到明细行,支持部分转交
背景(业务方推翻上一轮约束)
----
上一轮按「一张单同时只能有一个持有人」实现了**整单转交**,并把「单内出现多个
持有人」当作 bug 去修。业务方验收后明确纠正:
物理现场经常只转交部分工具(借了 2 件、只把 1 件转给别人),
单内多持有人才是符合现实的正常状态。
故转交粒度从 borrow_no 下沉回 trans_borrow.id(明细行)。
改动
----
· transfer_borrow:只操作传入的那**一行**明细,不再按单号整批覆盖。
转出方 = 该行当前持有人;数量 = 该行待还量。
· accept_transfer:只转移 transfer.borrow_id 指向的那一行 ——
整批改写会把别人手上的东西一并抢过来(部分转交下同单明细分属不同人)。
· 唯一性约束从「单号至多一条 PENDING」下沉为「明细行至多一条」:
同单的其他明细可以同时各自挂着待接收,互不阻塞 —— 这正是部分转交的语义。
· get_records 的 pending_transfer 改按 borrow_id 关联(原按 borrow_no),
否则同单多项待接收会互相覆盖。
· 删除已无用的 _load_slip_for_update。
★ 数量粒度:一行只支持**整行转交**。一行只能有一个 current_holder_id,
「同一行只转一部分」需要把这行拆成两行 —— 经业务确认,现场场景中
「借 2 件转 1 件」的两件本就是两条明细行,故该限制不影响实际使用;
接口对传入的非整行数量会明确提示「应另立一条明细行」。
数据层
----
无需改表结构:borrow_id 本就是流水的关联列,borrow_no 退化为单据归属与
分组展示用。仅补 (borrow_id, status) 复合索引支撑新的查询路径。
存量撕裂数据(BOR-20260917-0001 的「测试 / 杜邢宸」)按业务方选择**保留不动**
—— 它现在不再是 bug,而是部分转交的正常形态。
验证(合成 2 明细单,21 项断言全通过)
----
· 只转工具A:工具B 完全不受影响
· 同一张单可同时挂两条待接收,互不阻塞;同一明细重复发起被拒
· accept 工具A 后:A→测试,B 仍是杜邢宸(单内两个持有人)
· 两个持有人、以及待接收人,三方各自都能在列表中看到该单
· pending_transfer 挂在正确的明细行上,is_mine 判定正确
· reject 后主表持有人不变;非整行数量被拒并提示拆行
· 全程 available_quantity 无变化,库存精确还原、零残留数据
|
2026-09-17 10:12:31 +08:00 |
|
|
|
b271ca3a49
|
feat(borrow): 转交流水表补 borrow_no 与 status(双向握手数据层)
背景
----
1) **单内撕裂**:原 /transfer 收的是**明细行 ID**,只更新那一行。一张单有
2 条明细时,转交后一条归接收人、另一条仍是原借用人 —— 前端按 borrow_no
聚合便同时显示两个名字(实测 BOR-20260917-0001:108→测试 / 109→杜邢宸)。
2) **单向强塞**:原实现发起即生效,接收人在毫不知情的情况下背上资产责任。
本次改动
----
· 新增 borrow_no —— 单据身份。一次转交要覆盖该单**多行**,单行 ID 表达不了
覆盖范围;accept 时据此批量更新。borrow_id 保留为「发起时的代表明细」供追溯。
· 新增 status —— PENDING / ACCEPTED / REJECTED 状态机。
· 存量 1 行按旧语义(发起即生效)标记为 ACCEPTED 并回填 borrow_no:
它事实上已经生效,若标 PENDING,接收人会收到一条早已生效的待办。
· 建 borrow_no / status / (to_user_id,status) 索引,后者支撑「待我接收」查询。
同单号同一时刻只允许一条 PENDING(应用层强制),避免两个接收人争抢同一批实物。
|
2026-09-17 10:04:05 +08:00 |
|
|
|
b998b00889
|
feat(borrow): 借库转交一期数据层(迁移、转交/归还流水表、发货操作人列)
背景
----
原 trans_borrow 是单行记录模式,身份维度只有 borrower_name 一个字符串:
「谁借的」与「谁现在还拿着」是同一个字段,无法表达持有权变更;归还时
return_time/return_operator/return_signature 被逐次覆盖,部分归还下
「谁在什么时候还了多少」永久丢失。
本次改动(全部增量,无破坏性 DDL,可回滚)
----
1) trans_borrow 补列
· borrower_id / current_holder_id / current_holder_name —— 身份锚点
· dispatch_operator —— 执行借出的库管(operator_name 形参此前被接收却从未落库)
2) 新建 trans_borrow_transfer —— 转交流水,一行=一次转交,不做覆盖式更新
3) 新建 trans_borrow_return —— 归还流水,逐次记录,根治部分归还失忆症
4) 历史回填(已执行):85 行中 84 行 borrower_id 唯一命中 sys_user;
未归还的 32 行全部绑定 current_holder
5) 注册 borrow_transfer 权限码(无冒号形式,避免 _expand_operation_perms
前缀桥接把权限放大给所有持有 op_borrow:operation 的角色)
设计取舍
----
· current_holder 只回填「未归还」行:已归还=物品已回库、无人持有,保持 NULL。
这样「current_holder_id IS NOT NULL」本身就是「仍在某人手上」的有效信号,
归还校验不会对已结清单据误触发。
· 三张表都不建外键:与 trans_return / trans_defective_goods 一致 ——
库存行会被入库模块物理删除(实测已有悬空),台账必须能独立存活。
· dispatch_operator 不回填历史:执行人信息从未被采集,系统中不存在可回填的
数据源;用申请人或借用人冒充实物交接人比留空更危险。
验证:迁移已对 inventory_db 执行,核对段全部通过。
|
2026-09-17 09:17:51 +08:00 |
|
|
|
a252013573
|
feat(perm): 逆向物流专用权限码与菜单授权
为逆向物流的每个动作拆出独立权限码,终止此前复用其它模块权限的做法。
权限码一览(全部为无冒号形式,见下方说明):
outbound_return 出库退回
defective_list 不良品台账查看
defective_restock 不良品回库
defective_scrap 不良品报废
stock_change_status 库存状态变更
outbound_return_list 退回记录查看
另补建两个菜单:defective_goods(不良品在管台账)、outbound_returns(退回记录),
前者原先只有前端路由、sys_menu 中无对应行,导致页面在权限管理界面不可见、
其下权限也无从授予(sys_element.menu_code 有指向 sys_menu 的外键)。
默认授予:SUPER_ADMIN、SUPERVISOR、WAREHOUSE_MGR。
刻意不含 OUTBOUND —— 业务确认出库员只负责正向拣货发货,不参与逆向物流。
★ 为什么全部用无冒号形式,而不是 <menu>:<action>:
后端 _expand_operation_perms() 对带冒号的权限码做**前缀桥接** —— 只要用户
持有该菜单下任一以 :operation/:edit/:delete/... 结尾的权限就被放行。
实测验证过这个放大效应:
outbound_list:return <- 持有 outbound_list:operation -> True
outbound_return <- 持有 outbound_list:operation -> False
且前端 hasPermission() 是精确匹配,用冒号码会出现「接口能调、按钮却看不到」
的错位。无冒号码不触发桥接,前后端判定完全一致。
★ add_return_view_support.sql 另含一处 schema 变更:trans_return 补
company_name 快照列。退回流水的多租户隔离原先只能靠 join 链推
(trans_return → trans_outbound → 库存表 → 物料主表),而库存行会被入库
模块物理删除(实测 1077 条出库记录中已有 7 条悬空),链路一断该记录就会
对普通用户静默消失。审计视图静默丢数据不可接受,故落快照。
表当前为空,无需回填。
三个脚本均为幂等,含预检、回滚段与执行后核对。
|
2026-09-16 16:44:46 +08:00 |
|
|
|
69c38a1bf7
|
feat(return): 逆向物流数据模型与迁移
新增原单退回与不良品在管的持久化结构。
- TransOutbound 增 returned_quantity(numeric(19,4),非 float):该值参与
「return_qty <= quantity - returned_quantity」判等,浮点误差会让反复部分
退回后出现「已退满却判定未退满」的错判
- 新增 TransReturn:退回流水,每次退回写一条而非覆盖式更新。刻意与
trans_borrow 划清界限——后者部分归还时会覆盖 return_time/operator,
导致归还历史永久丢失
- 新增 TransDefectiveGoods:不良品在管台账。坏件全程不入库存表,因为
status 是行级属性而质量是件级属性,把坏件加回原行只能整行打不良
(实测 stock_buy 单行最大 4789 件、中位 8 件,整行打不良会凭空损失良品)
- 状态机:待处理 → 处理中 → {已回库|已报废|已闭环}。终态由累计去向推导
而非「最后一次动作」——一批坏件可能既回库过又报废过,按最后动作定状态
会产生误导
- restocked_qty/scrapped_qty 两列:二期用 quantity-remaining_qty 反推回库量,
三期加入报废出口后该反推失效
- 审计白名单与模型预加载同步登记(监听器绑定 18 → 20 个模型)
迁移脚本均为纯追加式 DDL,含预检、回滚段与执行后核对。首个脚本用
COALESCE 包裹数量列——库存表允许数量为 NULL,而「NULL 大于 0」求值为
NULL 而非真,裸写会让脏行在预览与诊断两次查询里凭空消失。
|
2026-09-16 15:45:22 +08:00 |
|
|
|
fbc9296056
|
feat(stock): status 硬隔离与出库通道封堵
激活库存表长期「只写不读」的 status 列,使其成为分配准入的硬门槛。
- inventory_reservation: 新增状态语义单一事实来源(STOCK_STATUS_*/
allocatable_filter/is_allocatable);verify_scanned 增加状态准入校验
(覆盖出库与借库两条提交路径)
- outbound: _allocate_bom_requirements 过滤条件加 allocatable_filter,
非「在库」一律不进入候选集;扫码路由把 ValueError 转为 400
- outbound_service: create_outbound_batch 强制 request_id 必填。原先无单
时会跳到所谓「散单」分支,而该分支的扣减逻辑从未落地(注释点名的
_apply_reservation_override 全仓不存在),实测只写台账不扣库存,
同一批货可被反复出库;get_stock_by_barcode 接入扫码准入;维修单扫码
补排除「报废转出」;预占再平衡块补 rollback 兜底
- fill_missing_stock_status.sql: 把存量行刷回「在库」。该过滤是
Fail-Closed 的,不先洗刷会让全库无法出库
破坏性变更:不传 request_id 的出库请求现在会被拒绝。
|
2026-09-16 15:45:02 +08:00 |
|
|
|
d1694bf245
|
perf(stocktake): 去掉库位树的全展开渲染,并为推荐查询补索引
业务反馈切到抽盘面板与点【获取推荐】时卡顿。实测定位:
【真正的原因在前端】先跑 EXPLAIN ANALYZE 排除了后端 ——
推荐查询在当前数据量下仅耗时 1.3ms,全走 Seq Scan + Hash Join,
Buffers shared hit=161 全部命中缓存。所以卡顿来自 DOM:
el-tree 原先带 default-expand-all,会把全库 3371 个库位节点一次性铺进 DOM。
去掉 default-expand-all 后初始只渲染根节点。勾选状态与 getCheckedNodes
不受影响 —— el-tree 的 Node store 会预先构建全树,展开与否只影响 DOM。
【索引仍补上,但说明白】实测 pg_indexes:stock_* 三表的 warehouse_location
已有索引,但 trans_outbound / trans_borrow 只有主键,时间字段与
(source_table, stock_id) JOIN 键都缺;三张库存表的时间字段也缺。
新增迁移 add_active_location_indexes.sql 补 7 个索引。
诚实说明写在迁移头部:当前数据量下加索引后本查询**仍会走 Seq Scan**,
这是小表下的正确计划,索引是为 90 天/半年后的数据增长做前瞻,不是修当前的慢。
【loading 状态】treeLoading / recLoading 已在既有实现中就位并闭环在
try/finally 中,分别绑在 el-tree 的 v-loading 与【获取推荐】按钮的 :loading 上,
本轮复核确认,未做无谓改名。
实测: 索引后执行计划仍为 Seq Scan,Execution Time 0.769ms(预期内)
|
2026-09-11 14:38:40 +08:00 |
|
|
|
aeb852c64d
|
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
|
2026-09-11 13:28:43 +08:00 |
|
|
|
6586d3cb75
|
feat(stocktake): 盘点草稿表增加公司隔离字段
stocktake_draft 原本没有任何公司维度,导致开启新盘点清空整表时会误删
其他公司的进度,扫码时也无法区分不同公司的同码物料。
- StocktakeDraft 增加 company_name(带索引),to_dict 同步输出
- 新增迁移 add_stocktake_draft_company_name.sql:加列 + (company_name,
session_id) 复合索引;存量行保持 NULL(超管仍可见),不做删除
执行: docker exec -i inventory_db psql -U test -d inventory_system < db_migrations/add_stocktake_draft_company_name.sql
|
2026-09-11 11:33:04 +08:00 |
|
|
|
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 |
|
|
|
2045a89330
|
feat(db): 审批单据时间口径统一 + 补注册 scrap_selection 权限
unify_approval_timezone.sql:
· scrap_approval 的 4 个时间列由 timestamptz 改为 timestamp without time zone,
与 outbound/borrow 对齐(该表当时无业务数据,转换零损失);
· 三张审批表 created_at/updated_at 的 DB 默认值由 UTC 的 now()/CURRENT_TIMESTAMP
统一为 timezone('Asia/Shanghai', now()),杜绝绕过 ORM 的写入偏 8 小时。
add_scrap_selection_perm.sql:
GET /scrap/scan 一直声明需要 scrap_selection,但该权限码从未写入
sys_element / sys_role_permission,精确匹配与前缀展开匹配全部落空,
导致除 SUPER_ADMIN 外没有任何角色能调用扫码接口。现补注册,
并授予所有已持有 scrap_execute(按单报废执行)的角色。
|
2026-09-10 10:14:19 +08:00 |
|
|
|
3d8fb198c5
|
feat(scrap): 报废审批流 DB 迁移(建表 + 权限码)
- scrap_approval 表(申请单,items_json 携带 source_table+stock_id 精准实物)
- trans_scrap 增加 scrap_request_no 关联申请单
- 注册权限码 scrap_apply / scrap_approval / scrap_execute 并按现有报废角色授予
|
2026-09-10 09:46:52 +08:00 |
|
|
|
c8cf2bd52b
|
feat(db): BOM 三态互斥存量归一——归档(废弃)自动停用
- 将历史“归档且仍启用(True/True)”的行归一为 归档且停用(True/False)
|
2026-09-09 13:00:35 +08:00 |
|
|
|
4146f35010
|
feat(permission): “出库/借库需审批”开关权限化——仅主管/超管可见可改
- 新增权限码 material_list:isApprovalRequired,授予仅 SUPER_ADMIN/SUPERVISOR(收回其它角色)
- 物料列表该列仅持码角色可见;后端批量端点与字段脱敏均改按新码校验,其余角色看不到值也无法改
|
2026-09-09 10:47:36 +08:00 |
|
|
|
e03c035ee0
|
feat(db): material_base 增加 is_approval_required 开关列(出库/借库需审批)
- boolean NOT NULL DEFAULT false,存量默认不审批
|
2026-09-09 09:31:12 +08:00 |
|
|
|
6250fa94cb
|
feat(bom): BOM 子件引用版本与归档 数据库迁移
- bom_table/bom_draft_table 加 child_bom_no/child_bom_version(子件引用的自制BOM版本)
- bom_table 加 is_archived 归档标记(仍启用可查看/编辑,但不再作为子件版本候选)
- 回填 60 行自制件子件引用版本,4 个多版本自制件按业务口径(79/92/226/1905)
- 3 套占位/废弃配方改标归档并恢复启用
|
2026-09-08 10:14:40 +08:00 |
|
|
|
9c3fcbca4c
|
migration: 补齐 sys_element 缺失的 :operation 权限码 + 角色分配 SQL
|
2026-07-17 15:29:59 +08:00 |
|
|
|
3c0954598c
|
perf: 综合安全加固 — RBAC严格映射+异步邮件+字段权限白名单+前端对齐+导入模板
本次提交包含本会话所有修改的最终统一提交
## 权限系统重构
- permission_service.py: 添加入库/采购操作元素 + ensure_default_permissions
- field_permissions.py: 严格1-to-1 Default Deny 字段映射(StockBuy/Semi/Product/MaterialBase)
- decorators.py: _expand_operation_perms 双向粒度桥接 + prevent_double_submit
- deploy_production.sql: 修复 sys_element 别名码(qty_inbound→in_quantity)
## 采购模块
- purchase.py: 权限驱动可见性 + inbound_purchase独立权限 + 价格字段过滤
- purchase_service.py: 异步邮件 + 三阶段批量模糊匹配防N+1
- purchase/index.vue: canApprove严格操作权限 + upload重复修复
## 导出/入
- base_service.py: export_excel 流式写入防OOM + get_latest_specs 优化
- import_service.py + import_api.py: Excel批量导入(模板+预览+执行)
- ImportDialog.vue: 三步骤导入弹窗
## 异步邮件
- email_service.py: send_email_async (守护线程)
- inventory_task.py: send_email→send_email_async
## 前端对齐
- product/semi/buy.vue: 列对齐in_quantity/stock_quantity/available_quantity + localStorage缓存V2
- buyOdoo.vue: 排序修复 + 导入按钮 + 移除点击展开加载
- BomManage.vue: 懒加载分组 + 导入按钮
- list.vue: 导入按钮
- Selection.vue + borrow/apply: BOM匹配修复 + 导入按钮
- outbound/create.vue: 出库类型必选
- AppMain.vue: 移除transition白屏修复
- material_base.ts, outbound.ts, bom.ts, stock.ts: 新增API函数
|
2026-07-17 13:07:12 +08:00 |
|
|
|
4934cd4d8f
|
feat: 采购管理税率+分开单价总价列 + 按单出/借库锁定扫描 + 含税法计算
- purchase: 新增tax_rate字段(model+service+frontend), 表格拆分为不含税单价/总价/税率三列
- buy.vue: 含税法计算(含税总价=数量×含税单价), 含税总价自动计算可手动覆盖
- create.vue: 按单出库模式下未选审批单时锁定摄像头和SKU输入
- borrow.vue: 未选审批单时锁定摄像头和SKU输入
- deploy_production.sql: 整合全部数据库变更(表结构+权限码+角色分配)
|
2026-07-15 16:07:59 +08:00 |
|
|
|
4b1a86a870
|
fix: 对齐field_to_perm与sys_element权限码 + 税率6% + 含税总价反算
- buy/semi/product: 数量字段映射对齐sys_element实际注册码(qty_inbound/qty_stock/qty_available)
- 数据库: 补全buy(9)+semi(10)+product(13)缺失权限码
- deploy_production.sql: 同步更新
- buy.vue: 税率新增6%选项, 价格联动以含税为主输入, 税率变化保持最后编辑价格不变
- buy.vue: 新增含税总价可输入反算功能
|
2026-07-15 15:03:37 +08:00 |
|