docs(deployment): 更新部署手册和业务架构说明

- 更新MES和WMS服务端口配置说明
- 详细说明产线工位设备配置(扫码枪+拧紧枪)
- 明确AGV配送和接驳台的模拟模式与真实对接方案
- 更新WMS中AGV配送默认模拟模式说明
- 完善厂内物流(AGV/接驳台)的业务架构约束
- 修订临时文档为详细的项目问题和解决方案规划
This commit is contained in:
SunYF
2026-09-20 14:52:36 +08:00
parent 5a33028003
commit f35e9b5b75
57 changed files with 6096 additions and 1000 deletions
@@ -251,7 +251,7 @@ func ListMaterialRequestsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
TargetDock: q.Get("targetDock"),
StationNo: q.Get("stationNo"),
Source: q.Get("source"),
Keyword: q.Get("keyword"),
RequestNo: q.Get("requestNo"),
})
if err != nil {
httpx.Fail(w, 3104, err.Error())
+69 -7
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"math"
"sort"
"strconv"
"strings"
@@ -165,6 +166,19 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
}
}
s.ctx.EventLog.Write(ctx, "material.request.generate", "", operator, "material_request", planDate, "按日排产+工位派工自动生成工位级备料单", map[string]any{"planDate": planDate, "count": created})
// 需求10:把每个工单的「工单级需求总量」同步给 WMS 台账(幂等,失败降级不阻塞)。
// 必须先同步 total_qty,后续自动出库才能在台账上累加 out_qty 并判定缺口/领料完结。
synced := map[string]bool{}
for _, g := range groups {
if synced[g.plan.OrderNo] {
continue
}
synced[g.plan.OrderNo] = true
if e := s.SyncOrderLedger(ctx, g.plan.OrderNo, operator); e != nil {
s.ctx.EventLog.Write(ctx, "material.ledger.sync_fail", "", operator, "material_request", g.plan.OrderNo,
"工单需求量同步 WMS 台账失败", map[string]any{"orderNo": g.plan.OrderNo, "error": e.Error()})
}
}
// 生成后立即自动出库(客户第1条:平时无需操作)。缺料/失败不阻塞生成,仅记预警等待补库存。
if _, short := s.AutoOutboundRequests(ctx, planDate, "", operator); len(short) > 0 {
s.ctx.EventLog.Write(ctx, "material.outbound.short", "", operator, "material_request", planDate,
@@ -174,6 +188,58 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
return created, nil, nil
}
// SyncOrderLedger 计算并向 WMS 下发某工单的「工单级物料需求总量」(需求10)。
//
// 需求量口径(临时.md §四 工单域):需求量 = 工单数量 × 单台用量 × (1+损耗率),向上取整。
// 工单级而非日级——用日级会误卡跨天生产(临时.md L283-285)。
// 多级 BOM 展开到叶子后按物料编码聚合(同编码合并),一个物料一行下发。
// WMS 侧按 orderNo+materialCode 幂等 upsert total_qty,可重复调用。
func (s *Service) SyncOrderLedger(ctx context.Context, orderNo, operator string) error {
wo, err := s.ctx.EntClient.WorkOrder.Query().
Where(workorder.WorkOrderNo(orderNo)).Only(ctx)
if err != nil {
return err
}
leaves := s.expandBomToLeaf(ctx, wo.ProductCode, bomNameOrDefault(""), 1, 0, map[string]bool{})
if len(leaves) == 0 {
return nil
}
type agg struct {
name string
qty float64
}
m := map[string]*agg{}
order := make([]string, 0, len(leaves))
for _, it := range leaves {
if it.materialCode == "" {
continue
}
need := float64(wo.Quantity) * it.unitQty * (1 + it.lossRate/100)
if need <= 0 {
continue
}
if a, ok := m[it.materialCode]; ok {
a.qty += need
} else {
m[it.materialCode] = &agg{name: it.materialName, qty: need}
order = append(order, it.materialCode)
}
}
items := make([]wmsclient.LedgerSyncItem, 0, len(order))
for _, code := range order {
a := m[code]
items = append(items, wmsclient.LedgerSyncItem{
MaterialCode: code,
MaterialName: a.name,
TotalQty: int(math.Ceil(a.qty - 1e-9)),
})
}
if len(items) == 0 {
return nil
}
return s.ctx.Wms.LedgerSync(ctx, orderNo, wo.ProductCode, operator, items)
}
// AutoOutboundRequests 工单备料自动出库(客户第1条:备料出库根据 MES 排产自动生成,平时无需操作)。
//
// 对指定排产日 / 工单下的备料单,按 FIFO 从 WMS「合格」在库库存自动领料:
@@ -356,7 +422,7 @@ type MaterialRequestQuery struct {
TargetDock string
StationNo string
Source string // PLAN / REFILL
Keyword string // 兜底匹配 备料单号/工单号/物料编码
RequestNo string // 备料单号(独立模糊筛选;列表页禁用关键字混搜)
}
// ListMaterialRequests 备料单查询(大小写不敏感)。
@@ -383,12 +449,8 @@ func (s *Service) ListMaterialRequests(ctx context.Context, o MaterialRequestQue
if o.Source != "" {
q = q.Where(materialrequest.Source(o.Source))
}
if o.Keyword != "" {
q = q.Where(materialrequest.Or(
materialrequest.RequestNoContainsFold(o.Keyword),
materialrequest.OrderNoContainsFold(o.Keyword),
materialrequest.MaterialCodeContainsFold(o.Keyword),
))
if o.RequestNo != "" {
q = q.Where(materialrequest.RequestNoContainsFold(o.RequestNo))
}
if o.StationNo != "" {
if n, e := strconv.Atoi(o.StationNo); e == nil {
+51
View File
@@ -572,6 +572,57 @@ func (c *Client) LedgerProgress(ctx context.Context, orderNo, materialCode strin
return out.List, out.Total, nil
}
// LedgerSyncItem 工单物料台账同步项(工单级需求量,一个物料一行)。
type LedgerSyncItem struct {
MaterialCode string `json:"materialCode"`
MaterialName string `json:"materialName"`
TotalQty int `json:"totalQty"`
}
// LedgerSync 向 WMS 下发「该工单每种物料总共要多少料」(工单级需求量)。
//
// 需求10根因修复:MES 生成备料单后必须把工单级需求量同步给 WMS,
// WMS 才知道 total_qty(台账需求量),否则出库/缺口/领料完结判定都无据可依。
// WMS 侧按 orderNo+materialCode 幂等 upsert total_qty(详见 ledgerSyncHandler)。
func (c *Client) LedgerSync(ctx context.Context, orderNo, productCode, operator string, items []LedgerSyncItem) error {
if orderNo == "" || len(items) == 0 {
return errors.New("orderNo 和 items 必填")
}
payload := map[string]any{
"orderNo": orderNo,
"productCode": productCode,
"operator": operator,
"items": items,
}
b, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/internal/ledger/sync", bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-TOKEN", c.token)
resp, err := c.hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var body struct {
Code int `json:"code"`
Message string `json:"message"`
}
_ = json.NewDecoder(resp.Body).Decode(&body)
if resp.StatusCode >= 300 || body.Code != 0 {
if body.Message != "" {
return errors.New(body.Message)
}
return &RespError{Status: resp.StatusCode}
}
return nil
}
func (c *Client) post(ctx context.Context, path string, body any) error {
b, err := json.Marshal(body)
if err != nil {