4 Commits

Author SHA1 Message Date
2319a053df fix(bom): 备注框移到子件列表上方,改为 textarea
原位置在编号/版本号那一行的 span=6 里,850px 弹窗扣掉 label-width:120px
和 gutter 后,输入框实际只剩 60 余像素,稍长一点的备注根本看不清。

改为独立整行 textarea(rows=2, maxlength=500, show-word-limit),放在
「子件列表」标题上方;编号行改为 span 10+14 填满。

连带修两处相关缺陷:
  · 暂存草稿的 payload 补 remark —— 此前根本没往后端传
  · getDraftHash 补 remark —— 否则"只改了备注"会被防呆逻辑拦成
    "草稿数据无变动,无需重复暂存"
2026-09-21 14:13:15 +08:00
6a7761abbb fix(bom): 草稿链路透传 BOM 级备注,发布后不丢失
save_draft / get_draft_detail / publish_draft 三处此前都不带 BOM 级备注,
暂存后再恢复、或草稿直接发布,备注都会丢。

同时给 /save 的权限清洗表补上顶级 remark(与子件级 remark 共用
bom_manage:remark 权限码)。

注意:运行时真正生效的草稿入口是 bom.py:521 的 /draft/save。
app/api/v1/bom_draft.py 里的 bom_draft_bp 从未在 app/__init__.py 注册,
是死代码(两份都调同一个 BomDraftService,本次两处都改以保持同步)。

实测(临时 BOM,验证后已清理):
  草稿暂存带备注 → 读回 '草稿BOM级备注-测试'
  草稿 → 发布 → 正式表 读回 '草稿备注-发布后应保留'
2026-09-21 14:13:14 +08:00
0abc5850d8 fix(bom): save_bom 落库 BOM 级备注,get_bom_detail 改读 bom_remark
save_bom() 此前从未读取 data['remark'],父件级备注被静默丢弃;
get_bom_detail() 则用 first.BomTable.remark(第一个子件行的备注)冒充
主表备注——所以填了保存后再打开必为空,偶尔还会显示出某个子件的备注
(库里 1196 行只有 1 行有 remark,值是"白色")。

这正是"BOM 备注看不见"的根因。
2026-09-21 14:13:14 +08:00
cc80fdd9c9 fix(bom): 增加 bom_remark 列,为 BOM 级备注提供存储位置
bom_table 是子件明细表(一行 = 一个子件),表里只有子件级的 remark 列,
BOM 级备注没有任何地方可存。前端一直有备注输入框、提交时也确实放进了
payload,但后端 save_bom 收到后无处可写,只能静默丢弃。

沿用 parent_id / is_enabled 既有的冗余写法:同一 BOM 版本的每一行存同一
份值,读取时取首行。bom_draft_table 同步加列,保证草稿暂存与发布期间
不丢失。与既有 remark(子件级)语义分离,互不干扰。

存量数据无法回填——历史上填过的备注从未入库,只能从本迁移之后开始记录。

执行方式: docker exec -i inventory_db psql -U test -d inventory_system < db_migrations/add_bom_remark.sql
2026-09-21 14:13:13 +08:00
7 changed files with 85 additions and 10 deletions

View File

@ -0,0 +1,36 @@
-- =============================================================================
-- 一次性迁移BOM 级备注落库
-- -----------------------------------------------------------------------------
-- 背景:
-- 前端 BomManage.vue 一直有"备注"输入框v-model="form.remark"),提交时也
-- 确实放进了 payloadBomManage.vue:1273但后端 BomService.save_bom() 从
-- 未读取 data['remark'],父件级备注被**静默丢弃**——填了、保存、再打开必为空。
--
-- 根因是数据模型缺位bom_table 是子件明细表(一行 = 一个子件),表里只有
-- 子件级的 remark 列,没有任何位置存放 BOM 级备注。
-- 连带 bug读取侧 get_bom_detail() 用 first.BomTable.remark第一个子件行的
-- 备注)冒充主表备注,所以查看时会显示某个子件的备注(如"白色")或空。
--
-- 方案:
-- bom_table / bom_draft_table 各增加 bom_remark 列,沿用 parent_id、is_enabled
-- 既有的冗余写法:同一 BOM 版本的每一行存同一份值,读取时取首行。
-- 与既有 remark子件级语义分离互不干扰。
--
-- 存量数据:
-- 无法回填 —— 历史上填过的备注从未入库,任何位置都找不到,只能从本迁移
-- 之后开始记录。这是本次修复的已知代价。
--
-- 执行方式: docker exec -i inventory_db psql -U test -d inventory_system < 本文件
-- 注意: 若后端启用 Redis 缓存(bom:tree:*),部署后需清缓存或重启后端,
-- 避免 12h 内命中旧值。
-- =============================================================================
BEGIN;
ALTER TABLE bom_table ADD COLUMN IF NOT EXISTS bom_remark text;
ALTER TABLE bom_draft_table ADD COLUMN IF NOT EXISTS bom_remark text;
COMMENT ON COLUMN bom_table.bom_remark IS 'BOM级备注(父件级,冗余存于每行,读取取首行)';
COMMENT ON COLUMN bom_draft_table.bom_remark IS 'BOM级备注(父件级,冗余存于每行,读取取首行)';
COMMIT;

View File

@ -198,6 +198,8 @@ def save_bom():
'version': 'bom_manage:version',
'is_enabled': 'bom_manage:status',
'bom_no': 'bom_manage:bom_no',
# ★ 顶级 remark = BOM 级备注,与子件级 remark 共用同一权限码
'remark': 'bom_manage:remark',
}
# 清洗顶级字段
for field in list(req_data.keys()):
@ -531,7 +533,12 @@ def save_draft():
if not parent_id:
return jsonify({'code': 400, 'msg': 'parent_id 不能为空'}), 400
bom_draft_no = BomDraftService.save_draft(bom_no, version, parent_id, children)
# ★ 这是运行时真正生效的草稿入口bom_draft.py 的 bom_draft_bp 未在
# app/__init__.py 注册是死代码BOM 级备注必须在这里透传
bom_draft_no = BomDraftService.save_draft(
bom_no, version, parent_id, children,
bom_remark=data.get('remark', '') or ''
)
return jsonify({'code': 200, 'msg': '草稿暂存成功', 'data': {'bom_no': bom_draft_no}})

View File

@ -25,6 +25,10 @@ class BomTable(db.Model):
loss_rate = db.Column(db.Numeric(5, 2), comment='损耗率%', default=0, nullable=True)
remark = db.Column(db.Text, comment='备注')
# ★ BOM 级备注(父件级):与 parent_id/is_enabled 一样冗余存于本版本每一行,读取取首行。
# 上面的 remark 是**子件级**备注,语义不同,勿混用。
bom_remark = db.Column(db.Text, comment='BOM级备注(父件级,冗余存于每行,读取取首行)')
# ★ 子件引用的"自制 BOM"版本:子件物料若本身是自制件(作为其它 BOM 的父件),
# 此列记录该子件实际引用的配方版本((bom_no, version) 共同定位),由新建/编辑 BOM 时选版本保存。
# 外购件/无下级 BOM 的子件这两列为 NULL。

View File

@ -23,6 +23,10 @@ class BomDraftTable(db.Model):
loss_rate = db.Column(db.Numeric(5, 2), default=0, nullable=True, comment='损耗率%')
remark = db.Column(db.Text, comment='备注')
# ★ BOM 级备注(父件级):与 bom_table.bom_remark 对齐,草稿暂存/发布期间不丢失。
# 上面的 remark 是**子件级**备注。占位头行(child_id IS NULL)同样携带该值。
bom_remark = db.Column(db.Text, comment='BOM级备注(父件级,冗余存于每行,读取取首行)')
# ★ 子件引用的"自制 BOM"版本:与 bom_table 的 child_bom_no/child_bom_version 对齐,
# 保证草稿暂存/载入期间不丢失所选版本。
child_bom_no = db.Column(db.String(100), nullable=True, index=True, comment='子件引用的自制BOM编号')

View File

@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
class BomDraftService:
@staticmethod
def save_draft(bom_no, version, parent_id, children):
def save_draft(bom_no, version, parent_id, children, bom_remark=''):
try:
# 1. 删除旧草稿
old = BomDraftTable.query.filter_by(bom_no=bom_no, version=version).all()
@ -22,7 +22,8 @@ class BomDraftService:
if not children:
dummy_draft = BomDraftTable(
bom_no=bom_no, version=version, parent_id=parent_id,
child_id=None, dosage=0, loss_rate=0, remark=''
child_id=None, dosage=0, loss_rate=0, remark='',
bom_remark=bom_remark
)
db.session.add(dummy_draft)
else:
@ -34,6 +35,7 @@ class BomDraftService:
dosage=child.get('dosage', 0),
loss_rate=child.get('loss_rate', 0),
remark=child.get('remark', ''),
bom_remark=bom_remark,
child_bom_no=child.get('child_bom_no') or None,
child_bom_version=child.get('child_bom_version') or None
)
@ -87,6 +89,7 @@ class BomDraftService:
'parent_id': parent_id,
'parent_name': parent_material.name if parent_material else '',
'parent_spec': parent_material.spec_model if parent_material else '',
'remark': first.bom_remark or '', # ★ BOM 级备注
'children': children,
}
@ -125,6 +128,8 @@ class BomDraftService:
'bom_no': bom_no,
'version': version,
'parent_id': draft['parent_id'],
# ★ 草稿的 BOM 级备注要带到正式表,否则发布即丢失
'remark': draft.get('remark', '') or '',
'children': [
{
'child_id': child['child_id'],

View File

@ -404,7 +404,9 @@ class BomService:
'parent_name': parent_material.name if parent_material else '',
'parent_spec': parent_material.spec_model if parent_material else '',
'is_enabled': first.BomTable.is_enabled,
'remark': first.BomTable.remark or '', # ★ 主表备注
# ★ BOM 级备注:改取 bom_remark。此前误取 first.BomTable.remark
# (第一个子件行的备注)冒充主表备注,是本次一并修掉的连带 bug
'remark': first.BomTable.bom_remark or '',
'children': children
}
@ -421,6 +423,8 @@ class BomService:
parent_id = data['parent_id']
children = data['children']
is_enabled = data.get('is_enabled', True)
# ★ BOM 级备注:写入本版本每一行(与 is_enabled 等父件级字段同口径)
bom_remark = data.get('remark', '') or ''
if not bom_no:
raise ValueError('BOM编号不能为空')
@ -527,6 +531,7 @@ class BomService:
child_id=child['child_id'],
dosage=child.get('dosage', 0),
remark=child.get('remark', ''),
bom_remark=bom_remark,
child_bom_no=child.get('child_bom_no') or None,
child_bom_version=child.get('child_bom_version') or None,
is_enabled=is_enabled

View File

@ -145,12 +145,7 @@
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="备注" v-if="hasFormFieldPermission('remark')">
<el-input v-model="form.remark" placeholder="备注信息(可选)" :disabled="isReadOnlyMode" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="14">
<el-form-item label="版本号" prop="version" v-if="hasFormFieldPermission('version')">
<template v-if="isSaveAsMode">
<el-radio-group v-model="form.versionUpgradeType" :disabled="isReadOnlyMode" @change="onVersionUpgradeTypeChange">
@ -165,6 +160,21 @@
</el-col>
</el-row>
<!-- BOM 级备注独立整行放在子件列表上方
原位置在编号/版本号那一行的 span=6 扣掉 120px label
输入框实际只剩 60 余像素稍长一点的备注根本看不清 -->
<el-form-item label="备注" v-if="hasFormFieldPermission('remark')" style="margin-bottom: 5px;">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="BOM 级备注(可选),如适用场景、工艺要求等"
:disabled="isReadOnlyMode"
/>
</el-form-item>
<div style="font-weight: bold; margin: 15px 0 10px 0; border-left: 4px solid #409EFF; padding-left: 10px;">
子件列表
<span style="font-weight: normal; font-size: 12px; color: #909399; margin-left: 10px;">(已选 {{ filteredChildren.length }} )</span>
@ -408,6 +418,9 @@ const getDraftHash = () => {
bom_no: form.bom_no,
version: form.version,
parent_id: form.parent_id,
// ★ BOM 级备注也要参与比对,否则"只改了备注"会被防呆逻辑
// 拦成"草稿数据无变动无需重复暂存"
remark: form.remark || '',
children
})
}
@ -852,6 +865,7 @@ const executeSaveDraftRequest = async (targetBomNo: string) => {
bom_no: targetBomNo,
version: draftVersion,
parent_id: form.parent_id,
remark: form.remark || '',
children
})
if (res.code === 200) {