docs(business): 生成业务契约文档并重构MES客户端代码

- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档
- WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景
- MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景
- 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景
- 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况
- 删除MES客户端中的ScanRecord相关代码,精简客户端结构
- 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
This commit is contained in:
SunYF
2026-09-20 18:25:44 +08:00
parent 5f68e77d5f
commit 8e6e8e1d20
46 changed files with 1052 additions and 3954 deletions
+4 -25
View File
@@ -14,36 +14,15 @@ import (
//
// 生产环境口径(用户裁决 2026-09-20):种子只允许写入系统跑起来所必需的固定主数据,
// 严禁预置演示物料/演示库存/演示账号等假业务数据。具体:
// - 区域:库房四大基础分区 Z01~Z04(库存落位的根节点,level=1);
// - 管理员账号:admin(初始密码 123456,登录后应立即修改);其余账号由管理员在「账号管理」按需创建、分配角色;
// - 接驳台:DOCK01~20(产线,DOCKn↔工位n+ DOCK21(库房收货),系统预置只读主数据(dock.go 不提供增删改)。
// - 接驳台:DOCK01~20(产线,DOCKn↔工位n+ DOCK21(库房收货),系统预置只读主数据(dock.go 不提供增删改,只能靠种子写入)。
//
// 不预置区域/库位:区域是现场库房布局,因项目而异,且「区域维护」页面可增删改,由实施按现场实际建立,不硬编码进种子。
// 权限与角色不在此:由 handler.SeedRBAC 幂等初始化(reset-all/seed 命令与 POST /api/rbac/seed 共用同一函数)。
func SeedIfEmpty(client *ent.Client) error {
ctx := context.Background()
// 1. 区域(层级节点 level=1
zones, _ := client.Zone.Query().Count(ctx)
if zones == 0 {
for _, z := range []struct{ code, name string }{
{"Z01", "待检区"},
{"Z02", "标准存储区"},
{"Z03", "暂存区"},
{"Z04", "退货区"},
} {
client.Zone.Create().
SetLevel(1).
SetCode(z.code).
SetZoneName(z.name).
SetZoneCode(z.code).
SetParentCode("").
SetStatus("启用").
SaveX(ctx)
}
slog.Info("seed: zones created")
}
// 2. 管理员账号(唯一预置账号;初始密码 123456,登录后应立即修改)
// 1. 管理员账号(唯一预置账号;初始密码 123456,登录后应立即修改
users, _ := client.User.Query().Count(ctx)
if users == 0 {
adminHash, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
@@ -56,7 +35,7 @@ func SeedIfEmpty(client *ent.Client) error {
slog.Info("seed: admin user created (admin/123456)")
}
// 3. 接驳台主数据 DOCK01~20(产线, DOCKn↔工位n) + DOCK21(库房)
// 2. 接驳台主数据 DOCK01~20(产线, DOCKn↔工位n) + DOCK21(库房)
docks, _ := client.Dock.Query().Count(ctx)
if docks == 0 {
for i := 1; i <= 20; i++ {
+3 -7
View File
@@ -240,8 +240,8 @@ func semiView(inv *ent.Inventory) *semiViewRow {
}
}
// querySemiHandler 半成品/成品查询(读统一库存主表 category=2/3
// 客户诉求 L230:查询增加归属(工单号/科技项目号);L222:半成品成品分开查(category 参数)
// querySemiHandler 半成品查询(读统一库存主表 category=2 半成品)。
// 半成品管理页只处理半成品成品由 MES 完工回流入库、经 WMS 完工检验,在「库存查询」页按品类查看,不在本页手工出入库
func querySemiHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
page := atoi(r.URL.Query().Get("page"), 1)
@@ -250,12 +250,8 @@ func querySemiHandler(ctx *svc.ServiceContext) http.HandlerFunc {
status := r.URL.Query().Get("status")
sn := r.URL.Query().Get("sn")
ownershipNo := r.URL.Query().Get("ownershipNo")
category := atoi(r.URL.Query().Get("category"), 0) // 2=半成品 3=成品 0=两者都查
q := ctx.EntClient.Inventory.Query().Where(inventory.CategoryIn(invCatSemi, invCatFinished))
if category == invCatSemi || category == invCatFinished {
q = q.Where(inventory.CategoryEQ(category))
}
q := ctx.EntClient.Inventory.Query().Where(inventory.CategoryEQ(invCatSemi))
if materialCode != "" {
q = q.Where(inventory.MaterialCodeEqualFold(materialCode))
}
+14 -2
View File
@@ -3,6 +3,7 @@ package handler
import (
"encoding/json"
"net/http"
"net/url"
"sort"
"time"
@@ -26,8 +27,15 @@ type mesOrder struct {
// ordersHandler 调用 MES 内部接口获取工单列表,把未完成工单排前面,供备料出库下拉选择
func ordersHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
url := ctx.Config.Mes.BaseURL + "/api/internal/order/query?orderNo=" + r.URL.Query().Get("orderNo")
req, err := http.NewRequest(http.MethodGet, url, nil)
orderNo := r.URL.Query().Get("orderNo")
// limit:无输入条件时默认返回 20 条(未完成优先),供备料出库工单号下拉首屏展示;
// 模糊搜索时同样返回匹配结果的前 limit 条。
limit := atoi(r.URL.Query().Get("limit"), 20)
if limit <= 0 {
limit = 20
}
mesURL := ctx.Config.Mes.BaseURL + "/api/internal/order/query?orderNo=" + url.QueryEscape(orderNo)
req, err := http.NewRequest(http.MethodGet, mesURL, nil)
if err != nil {
// 构造请求都失败属代码级异常,返回空列表让备料出库页可降级(仍可手输工单号)
ok(w, []mesOrder{})
@@ -66,6 +74,10 @@ func ordersHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
return list[i].WorkOrderNo < list[j].WorkOrderNo
})
// 排序(未完成优先)后截断:无输入默认前 20 条;模糊搜索返回匹配的前 20 条。
if len(list) > limit {
list = list[:limit]
}
ok(w, list)
}
}
+60 -4
View File
@@ -463,11 +463,13 @@ type zoneBatchGenReq struct {
LayerEnd int `json:"layerEnd"`
PosStart int `json:"posStart"`
PosEnd int `json:"posEnd"`
Level int `json:"level"` // 3=批量生成层(level3) 4=批量生成位置号(level4,默认,同时确保层存在)
Preview bool `json:"preview"`
}
// batchGenerateHandler 批量生成位置号:选区域→货架→层范围→位置范围,先预览后提交。
// 自动确保层节点(level3)存在;已存在的位置号跳过不重复建
// batchGenerateHandler 批量生成库位节点:选区域→货架→范围,先预览后提交。
// level=3:只生成「层」节点(按层范围);level=4(默认):生成「位置号」节点(层×位置),并自动确保层节点存在
// 已存在的节点跳过不重复建。
func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req zoneBatchGenReq
@@ -481,8 +483,16 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
fail(w, http.StatusBadRequest, "请选择区域与货架")
return
}
if req.LayerStart < 0 || req.LayerEnd < req.LayerStart || req.PosStart < 0 || req.PosEnd < req.PosStart {
fail(w, http.StatusBadRequest, "层/位置起止不合法(起始需 ≤ 结束,且 ≥ 0)")
level := req.Level
if level != 3 {
level = 4
}
if req.LayerStart < 0 || req.LayerEnd < req.LayerStart {
fail(w, http.StatusBadRequest, "层起止不合法(起始需 ≤ 结束,且 ≥ 0)")
return
}
if level == 4 && (req.PosStart < 0 || req.PosEnd < req.PosStart) {
fail(w, http.StatusBadRequest, "位置起止不合法(起始需 ≤ 结束,且 ≥ 0)")
return
}
sh, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(2), zone.ZoneCodeEQ(req.ZoneCode), zone.CodeEQ(req.ShelfNo)).Only(ctx0())
@@ -496,6 +506,52 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
for l := req.LayerStart; l <= req.LayerEnd; l++ {
layers = append(layers, l)
}
// ---------- level=3:批量生成层节点 ----------
if level == 3 {
if len(layers) > 2000 {
fail(w, http.StatusBadRequest, "一次最多生成 2000 个层,请缩小范围")
return
}
type genLayer struct {
LayerNo string `json:"layerNo"`
LayerName string `json:"layerName"`
Exists bool `json:"exists"`
}
layerList := make([]genLayer, 0, len(layers))
for _, l := range layers {
ls := strconv.Itoa(l)
exists, _ := ctx.EntClient.Zone.Query().
Where(zone.LevelEQ(3), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(ls)).Exist(ctx0())
layerList = append(layerList, genLayer{LayerNo: ls, LayerName: req.ShelfNo + "架" + ls + "层", Exists: exists})
}
if req.Preview {
ok(w, map[string]any{"preview": true, "level": 3, "list": layerList, "total": len(layerList)})
return
}
createdL, skippedL := 0, 0
for _, it := range layerList {
if it.Exists {
skippedL++
continue
}
if _, err := ctx.EntClient.Zone.Create().
SetLevel(3).SetCode(it.LayerNo).SetParentCode(req.ShelfNo).
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(it.LayerNo).
SetName(it.LayerName).SetStatus("启用").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "创建层失败: "+err.Error())
return
}
createdL++
}
ctx.EventLog.Write(ctx0(), "zone.batch", r.Header.Get("X-Username"), "zone", req.ShelfNo,
"批量生成层 "+req.ZoneCode+"/"+req.ShelfNo+" 层"+strconv.Itoa(req.LayerStart)+"-"+strconv.Itoa(req.LayerEnd)+
":新建"+strconv.Itoa(createdL)+" 跳过"+strconv.Itoa(skippedL), nil)
ok(w, map[string]any{"preview": false, "level": 3, "created": createdL, "skipped": skippedL, "total": len(layers)})
return
}
// ---------- level=4:批量生成位置号 ----------
poss := make([]int, 0)
for p := req.PosStart; p <= req.PosEnd; p++ {
poss = append(poss, p)