跟单生产单与采购合同模块设计文档

4次阅读

共计 10497 个字符,预计需要花费 27 分钟才能阅读完成。

跟单生产单与采购合同模块设计文档

日期:2026-06-02

版本:V1.2

范围:新增独立模块,供 merchandiser(跟单)与 admin(管理员)角色账号使用。

原则:不改动现有备货、积加同步、销量核对、Dashboard、大屏等业务链路;复用登录鉴权、用户角色和前端基础布局。

1. 目标

新增一套“生产单解析 + 采购合同制作”功能:

  1. 跟单和 Admin 账号可以进入合同页面。
  2. 系统可导入同类生产单 Excel,解析并落库。
  3. 页面可按生产单号或 SKU/ 货号查询并导入产品明细。
  4. 合同抬头信息可通过后台预设模板自动填充;合同号由系统自动按规则生成。
  5. 采购单价支持人工手填,同时提供人工维护报价作为下拉参考。
  6. 合同页面外观和导出 Excel 格式参考 docs/ 采购单.xls,导出统一为 .xlsx
  7. 页面支持编辑单元格、合并单元格、保存合同。
  8. 合同保存后,所有明细、图片 URL、单元格布局信息均存入 PostgreSQL,后续可再次打开、复制、导出。
  9. 权限隔离:跟单互看可见,但不能互相限制查看;Admin 拥有全部权限。
  10. 重复导入确认流程必须避免前后端重复传输大体积解析结果。

2. 样本结构调研

2.1 生产单样本

观察到的共同结构:

  • 主数据集中在 Sheet1
  • 标题在 A1,如 26-809YS-031 单生产任务单
  • 基础信息含有固定段落前缀,如 一、品名 七:交货期 等。
  • 明细表头通常位于第 6 行,包含货号、客号、货物规格描述、数量等。
  • 底部包含包装说明、PO 号、制单人及日期。

需要兼容的差异与解析策略:

  • 文件名和标题可能有细微差异和冗余后缀。
  • 由于表头可能不在固定行,后端解析采用“关键字游标寻址法”,逐行扫描,匹配到“货号”“数量”等关键字所在行后定位表头,下一行作为明细起始行。
  • 首期不解析 Excel 内嵌图片,仅解析文本数据。

生产单唯一识别规则:

  • 生产单号格式示例:26-809YS-031
  • production_order_no 作为唯一识别码。
  • 若再次导入同一生产单号,视为重复导入。
  • 系统在覆盖旧记录前必须弹窗提醒。
  • 重复导入时只覆盖生产单主数据和明细数据,不回写已生成的历史合同。

2.2 采购单模板

  • 顶部结构:公司名、合同号码、日期、甲方 / 乙方、联系人、地址、电话、传真、包装说明。
  • 明细表头:工厂货号、客户号、图片、货物规格描述、数量(套)、单价、总价、备注。
  • 底部包含合同条款、备注、双方代表签章。
  • 包含较多合并单元格,如标题行、包装说明行、图片区域、签章区域等。

3. 模块边界

3.1 新增前端入口

  • 路由:/contracts
  • 页面文件建议:frontend/src/views/MerchandiserContractsView.vue
  • 前端路由 meta:meta: {requiresAnyRole: ['admin', 'merchandiser'] }
  • 导航入口:当用户角色为 adminmerchandiser 时显示“采购合同”。

3.2 新增后端入口

新增独立控制器和相关服务:

控制器:

  • ProductionOrderController.php:生产单导入、查询、覆盖提醒
  • PurchaseContractController.php:合同 CRUD、复制、作废、导出
  • ContractTemplateController.php:合同模板配置
  • PriceQuoteController.php:人工报价批量上传、查询
  • CommonUploadController.php:图片上传,若无通用接口则新增

服务:

  • ProductionOrderImportService.php:Excel 解析
  • PurchaseContractService.php:合同流转、明细处理
  • ContractSequenceService.php:合同号自动生成服务
  • ManualQuoteService.php:人工报价导入与查询服务

4. 数据库设计

4.1 scm_contract_templates

用于维护合同的主体模板、默认文案和签章区固定文字。

  • id
  • template_name
  • party_a_name
  • party_a_contact
  • party_a_phone
  • party_a_address
  • party_a_fax
  • party_b_name
  • party_b_contact
  • party_b_phone
  • party_b_address
  • party_b_fax
  • default_packing_text
  • default_terms_text
  • default_remark_text
  • default_sign_text_json
  • is_default
  • created_at
  • updated_at

4.2 scm_production_orders

  • id
  • production_order_no
  • po_no
  • product_name
  • style_text
  • total_qty
  • delivery_date
  • maker_name
  • made_date
  • source_file_name
  • source_file_hash
  • raw_header_json
  • created_by
  • updated_by
  • created_at
  • updated_at

约束建议:

  • unique(production_order_no)
  • index(po_no)

4.3 scm_production_order_items

  • id
  • production_order_id
  • line_no
  • factory_sku
  • customer_sku
  • spec_description
  • material_text
  • qty
  • erp_name
  • sticker_code
  • packing_text
  • raw_row_json
  • created_at
  • updated_at

4.4 scm_purchase_contracts

  • id
  • contract_no
  • contract_date
  • template_id
  • statusdraftsavedvoid
  • party_a_name
  • party_a_contact
  • party_a_phone
  • party_a_address
  • party_a_fax
  • party_b_name
  • party_b_contact
  • party_b_phone
  • party_b_address
  • party_b_fax
  • packing_text
  • goods_summary
  • remark_text
  • terms_text
  • sign_text_json
  • amount_total
  • ui_version
  • sheet_state_json
  • merged_cells_json
  • source_production_order_ids
  • created_by
  • updated_by
  • voided_by
  • voided_at
  • created_at
  • updated_at

合同编号规则:

  • 合同号格式:ADL26060201
  • 组成规则:固定前缀 ADL + 当天日期 YYMMDD + 两位流水号
  • 示例:ADL26060201
  • 同一天合同量不会超过 100,因此两位流水足够
  • 合同号允许 Admin 和跟单手工改,但保存时必须做唯一性重复校验

补充约束:

  • ui_version 默认写入 1.0
  • sheet_state_json 保存中立化后的表格结构,不直接保存表格引擎原始内部 JSON
  • 若未来更换前端表格方案,可根据 ui_version 做兼容解析

4.5 scm_purchase_contract_items

  • id
  • contract_id
  • production_order_id
  • production_order_item_id
  • line_no
  • factory_sku
  • customer_sku
  • color_text
  • image_url
  • spec_description
  • qty
  • unit_price
  • total_price
  • remark
  • raw_row_json
  • created_at
  • updated_at

4.6 scm_manual_price_quotes

用于保存人工维护报价。

  • id
  • source_batch_no
  • factory_sku
  • tax_exclusive_unit_price
  • created_by
  • updated_by
  • created_at
  • updated_at

报价 Excel 首期最少字段:

  • SKU
  • 不含税单价

4.7 scm_import_batches

用于保存“上传解析完成但尚未确认覆盖”的临时批次头信息。

  • id
  • batch_id
  • module_typeproduction_ordermanual_price_quote
  • statuspending_confirmconfirmedexpiredapplied
  • summary_json
  • created_by
  • expires_at
  • created_at
  • updated_at

清理策略:

  • 临时批次默认有效期建议为 24 小时
  • 超过 24 小时且仍处于 pending_confirm 状态的数据,视为垃圾数据
  • 后端需通过 Laravel 调度任务定时清理
  • 结合当前仓库实际情况,调度建议注册在 backend/backend_laravel/routes/console.php
  • 当前实现命令为 imports:cleanup-batches,调度时间为每天 02:10

4.8 scm_import_batch_rows

用于保存导入批次中的临时解析结果和差异对比。

  • id
  • batch_id
  • row_no
  • business_key
  • conflict_type
  • incoming_json
  • existing_json
  • created_at
  • updated_at

5. API 设计

统一前缀:/api/merchandiser

说明:

  • Admin 也通过该路由访问,由权限中间件放行。

5.1 生产单 API

  • POST /production-orders/import:上传 Excel 解析;若发现已有相同 production_order_no,后端先将解析结果写入临时批次表,返回 import_batch_id、冲突摘要和覆盖提示信息。
  • POST /production-orders/import-confirm:前端只提交 batch_id 与确认动作,后端依据临时批次执行覆盖导入。
  • GET /production-orders:分页查询。
  • GET /production-orders/{id}:查看生产单头和明细。
  • POST /production-orders/lookup-items:输入生产单号或 SKU 搜索明细。

5.2 合同 API

  • GET /contracts:合同列表。
  • POST /contracts:新建合同草稿,并自动生成 ADL 格式合同号,拉取默认模板数据。
  • GET /contracts/{id}:打开合同详情。
  • PUT /contracts/{id}:保存合同,含表格状态、合并单元格、合同号重复校验。
  • POST /contracts/{id}/import-items:将生产单明细导入已有合同。
  • POST /contracts/{id}/copy:复制合同内容,但生成新合同号。
  • POST /contracts/{id}/void:作废合同。
  • GET /contracts/{id}/export:强制先保存后,再基于保存后的 JSON 结构导出 .xlsx

5.3 模板与报价 API

  • GET /contract-templates:获取主体模板列表。
  • POST /contract-templates:新增模板。
  • PUT /contract-templates/{id}:更新模板。
  • GET /manual-price-quotes:查询人工报价。
  • POST /manual-price-quotes/import:批量上传人工报价 Excel;若发现重复 SKU,后端写入临时批次表并返回 import_batch_id、旧价 / 新价差异列表。
  • POST /manual-price-quotes/import-confirm:前端提交 batch_id 和逐行勾选的覆盖 SKU 列表,后端据此执行覆盖。
  • GET /sku-prices?sku={sku}:基于人工报价返回价格建议列表;合同录入阶段按客户 SKU 列发起查询,选中建议后将建议项 SKU 回填到合同客户 SKU 列。
  • 当客户 SKU 失焦后仍未匹配到任何报价时,前端提示“单价没匹配到,请手动输入”,并将单价置为 0.00,同时高亮价格单元格,直到用户手动录入价格。
  • POST /upload-image:单图上传,返回 URL,用于插入合同明细行。

6. 前端与 Excel 交互方案

6.1 后端解析

首期采用原生 ZipArchive + XML 解析样本 .xlsx

  • 不新增 Composer 依赖,优先适配当前服务器环境
  • .xls 兼容保留到后续服务器 Composer 条件满足后再接入
  • 人工报价导入与生产单导入共用同一类轻量 .xlsx 解析思路
  • 使用关键字寻址法匹配表头,提高对易变模板的容错率
  • 提取明细文本,舍弃首期复杂图片提取
  • 完整保存原始 JSON,防止信息丢失

6.2 前端电子表格选型与交互

优先方向:

  • 采用尽量轻量的 Web Spreadsheet 方案
  • 先满足合并单元格、编辑、序列化保存、图片列上传、单价列联想
  • 在正式选型前必须做一次小型技术验证(Spike)

当前推荐方向:

  • 首选轻量型表格方案,优先评估 @wolf-table/table
  • 先手写一份包含合并单元格、图片、样式的最小状态样本,验证能否稳定翻译到 exceljs
  • 如翻译层复杂度过高或样式错位严重,优先回退到更易导出的轻量 DOM 表格方案

中立化 JSON 契约要求:

  • 后端和前端在正式开工前需要先对 sheet_state_json 的基础结构达成一致
  • 中立结构中不得包含表格引擎专用类名、渲染函数名、运行时对象引用
  • 只保存纯数据结构,例如:
{
  "cells": [
    {
      "row": 1,
      "col": 1,
      "val": "合同号码",
      "style": {
        "bold": true,
        "align": "center"
      }
    }
  ],
  "rowHeights": {"1": 28},
  "colWidths": {"1": 120}
}
  • 该契约确定后,后续即使更换前端表格引擎,也能继续解析历史合同

交互要求:

  • 图片列:点击单元格后上传图片,成功后渲染图片
  • 单价列:编辑时显示下拉参考,同时允许手工输入
  • 总价列:自动计算 数量 * 单价
  • 同一个 SKU 但不同颜色时:
  • 保留多行
  • 只合并“货物描述”单元格
  • 数量、价格、总价不合并

6.3 Excel 导出

  • 前端使用 exceljs 生成 .xlsx
  • 导出前强制先保存合同
  • 若当前页面存在未保存改动,先执行保存,保存成功后再导出
  • 导出格式视觉上对齐 docs/ 采购单.xls
  • 图片上传时必须限制体积,首期建议单图不超过 2M
  • 图片访问地址必须允许当前站点域名访问,避免导出时跨域图片读取失败
  • 前端插入图片时,必须逐张捕获下载失败异常,不能因为单张图片 404 或跨域失败导致整个合同导出中断
  • 若使用 fetch,应读取 arrayBuffer();若使用 axios,应显式设置 responseType: 'arraybuffer'
  • 若前端导出在多图场景下频繁失败或浏览器内存占用过高,二期应切换为后端生成导出文件

7. 权限与隔离设计

7.1 前端层面

  • 路由对 adminmerchandiser 开放,其余角色隐藏并重定向
  • 合同列表页中,Admin 和跟单都可看到所有合同

7.2 后端层面

  • Admin 可查看、编辑、复制、作废、导出所有合同
  • 跟单可查看、编辑、复制、作废、导出所有合同
  • 作废后的合同仍可查看和导出,但不能再编辑

说明:

  • 权限重点不再是“谁能改谁的合同”,而是“作废后的合同不可再编辑”
  • 因此后端需要校验:
  • 非法角色不可访问
  • status=void 的合同不可再次保存或导入明细

8. 实施计划

阶段 1:基础骨架与模板设置

  • 新增 8 张表 migration 及 Models
  • 开发合同模板 CRUD 接口及管理员页面
  • 确认 Excel 解析依赖并实现 ProductionOrderImportService
  • 编写单元测试,重点覆盖正常表头和含空行表头定位

阶段 2:生产单覆盖与人工报价

  • 实现基于临时批次表的生产单重复检测与覆盖提醒机制
  • 实现人工报价 Excel 批量导入
  • 实现人工报价查询与 sku-prices 接口
  • 实现图片上传基础 API

阶段 3:合同核心逻辑

  • 实现合同序列号生成策略
  • 实现合同 CRUD、复制、作废
  • 实现生产单明细导入合同
  • 实现作废状态只读控制

阶段 4:前端合同编辑器

  • 引入轻量表格组件
  • 实现左侧查询与明细导入逻辑
  • 实现图片单元格上传
  • 实现单价单元格下拉推荐
  • 实现表格 JSON 状态双向序列化与保存

阶段 5:导出与联调

  • 实现基于 exceljs.xlsx 导出
  • 比对原模板样式
  • 增加合同号重复校验测试
  • 增加作废后禁止编辑测试
  • 增加生产单重复覆盖测试
  • 增加单图失败时不影响整体导出的异常处理验证

阶段 6:上线准备

  • 手工验证:权限、自动编号、手工改号、价格联想、合并单元格持久化、作废只读、导出前自动保存
  • 执行 php artisan migrate
  • 发布前端构建产物
  • 无需干预现有积加同步和备货任务

9. 已确认规则

  • 生产单号格式:26-809YS-031
  • 合同号格式:ADL26060201
  • 生产单重复导入:以生产单号判重,覆盖旧生产单数据,覆盖前弹窗提醒,不回写历史合同
  • 合同复制:复制内容,但生成新合同号
  • 合同作废:仍可查看和导出,但不能再编辑
  • 报价来源:采用单独的人工维护报价来源,支持 Excel 批量上传
  • 报价最少列:SKU不含税单价
  • 报价判重:SKU 本身已包含颜色信息,因此按 SKU 判重
  • 报价重复上传:罗列重复上传的 SKU,展示旧价格和新价格,并支持逐行勾选是否覆盖
  • 导出方式:前端 exceljs 导出,且先保存后导出
  • 模板方式:支持多模板下拉选择
  • 图片方式:报价表不带图片;合同编辑页由跟单手动上传图片
  • 图片映射:每个 SKU 对应一张图片;若合同中有多个 SKU,则对应多张图片
  • 合并规则:同一个 SKU 不同颜色时,仅合并货物描述单元格,其他列不合并
  • 作废合同限制:禁止再次导入生产单明细
  • 导入确认机制:重复导入时,后端先将解析结果写入临时批次表,前端只提交 batch_id 和确认项
  • 前端状态持久化:保存中立结构 JSON,并记录 ui_version
  • 临时批次清理:通过 Laravel Scheduler 每日清理超时未确认的导入批次
  • 当前已落地 imports:cleanup-batches 调度任务,负责清理过期的导入临时批次及其明细行

10. 实现建议补充

  • 图片上传首期建议保留“单条手动上传”。
  • 如后续跟单反馈上传成本高,可在第二期补一个“按 SKU 批量传图并自动匹配”的增强入口。
  • 当前已补充“按 SKU 批量传图并自动匹配”入口,复用现有单图上传接口,按文件名去扩展名后匹配合同明细中的 SKU / 客户 SKU

11. 导出模板补充

  • 合同导出样式不再手工拼整张工作表,而是改为基于 docs/ 采购单.xls 转换出的运行时模板导出。
  • 运行时模板文件放在 frontend/public/purchase-contract-template.xlsx,其版式来源于 docs/ 采购单.xls
  • 当前导出规则:
  • 先加载模板工作簿。
  • 再按合同明细数量动态插入或删除明细行。
  • 明细区以下的合计、备注、签章区整体下移。
  • 货物描述的合并状态继续读取 merged_cells_json
  • 当前模板细节约束:
  • 顶部第 1-9 行除合同号码与日期外,其余模板内容保持原样。
  • 明细区默认单行高度为 144pt,描述合并行按合并组重新分配高度。
  • 明细区单元格统一补齐边框,图片在图片列内居中显示。
  • 备注区固定保留:1、本合同所列价格为不含 17% 增殖税发票的价格。
  • 底部右侧乙方签章列统一左对齐。
  • 六、备注: 的上一行开始取消边框,且该行及其以下各行统一使用 20.25pt 行高。
  • 底部签章区的 日期: 保留在原标签单元格内,日期值写入其右侧单元格。
  • 若后续业务继续调整 docs/ 采购单.xls 版式,需要同步刷新 frontend/public/purchase-contract-template.xlsx

12. 2026-06-03 单价联想补充

  • 当合同明细行的 SKU 只命中 1 条人工报价时,前端自动回填 unit_price,并同步重算 total_price
  • 该逻辑只在当前行尚未填写单价和总价时生效,不覆盖已手工录入的价格。
  • 命中多条报价时不自动覆盖,继续保留手动展开建议列表的交互方式。
  • 生产单明细导入合同后,对新增且未填价的行同样执行“单条报价自动回填(single quote auto-fill)”。

2026-06-03 补充:生产单导入统一关键词搜索

  • 合同编辑页中的“导入生产单明细”改为统一关键词搜索。
  • 前端不再区分“按生产单号”或“按 SKU”下拉。
  • 用户输入生产单号、工厂 SKU、客户 SKU,统一走同一个搜索接口。
  • 后端搜索实现同时匹配:
  • production_order_no
  • factory_sku
  • customer_sku
  • 搜索规则统一为大小写不敏感。

2026-06-04 Full-page layering and overlay note

  • The desktop full-page sheet now uses fixed layer roles: cell 1, active/editing 2, gutter 10, sticky header 11, sticky corner 12, toolbar 20.
  • Unit-price suggestions no longer render inside the sheet cell. They render through Teleport to=”body” to avoid clipping by scroll containers, sticky headers, or frozen gutters.
  • Overlay position is computed from the active unit-price cell getBoundingClientRect() and refreshed on scroll, resize, and edit-state changes.
  • The hidden legacy editor remains in the page only for payload compatibility during save/export; desktop interaction stays in ContractSheetDesktop.

2026-06-04 Void contract delete rule

  • Void contracts can now be deleted from the contract draft list and the full-page editor.
  • Deletion is restricted to status=void only.
  • Delete removes the contract header and all contract detail rows together.

2026-06-05 报价图片上传补充

  • scm_manual_price_quotes 当前已包含 image_url 字段,用于持久化报价图片路径。
  • 报价管理页图片上传链路采用:
  • 新图先保存
  • 更新 image_url
  • 成功后对旧图做 best-effort 删除
  • 删除动作只允许处理本系统受管目录:
  • uploads/manual-price-quotes/...
  • 删除失败不阻断本次上传成功;页面仍以数据库中最新 image_url 为准。
  • 长期治理仍保留后续预案:
  • 通过定时巡检或对账脚本清理历史孤儿文件
正文完
 0
hermes
版权声明:本站原创文章,由 hermes 于2026-06-06发表,共计10497字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。