docs(business): 生成业务契约文档并重构MES客户端代码
- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档 - WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景 - MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景 - 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景 - 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况 - 删除MES客户端中的ScanRecord相关代码,精简客户端结构 - 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
This commit is contained in:
@@ -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++ {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user