docs(test): 更新电表箱装配业务回归测试文档

- 将文档从代码审计任务转换为端到端业务回归测试链路
- 重新组织文档结构为链路总览、示例数据基线和详细步骤
- 添加完整的业务场景清单涵盖WMS/MES/工位终端三大系统
- 定义统一的测试数据包括产品型号、物料清单、工位派工等
- 提供详细的步骤断言和数量等式验证方法
- 建立贯穿全链路的数据流向和状态变更追踪体系
This commit is contained in:
SunYF
2026-09-21 10:48:53 +08:00
parent 0d34bcd6a3
commit 132c3ed6bd
63 changed files with 3309 additions and 5784 deletions
+89 -95
View File
@@ -162,16 +162,20 @@ func zoneStockMaps(ctx *svc.ServiceContext) (byZone, byShelf, byLayer, byPos map
return
}
// zoneNodeReq 新建层级节点请求
// zoneNodeReq 新建层级节点请求
// 父级以完整路径给出(区域→货架→层),后端据此精确定位父级,避免仅凭 code 跨父级歧义。
type zoneNodeReq struct {
Level int `json:"level"`
Code string `json:"code"`
Name string `json:"name"`
ParentCode string `json:"parentCode"`
Remark string `json:"remark"`
Level int `json:"level"`
Code string `json:"code"`
Name string `json:"name"`
ZoneCode string `json:"zoneCode"` // 所属区域编码(level>=2 必填)
ShelfNo string `json:"shelfNo"` // 所属货架号(level>=3 必填)
LayerNo string `json:"layerNo"` // 所属层号(level=4 必填)
Remark string `json:"remark"`
}
// createZoneNodeHandler 新建层级节点(按 level 解析上级并写冗余路径;建货架自动建默认位置)
// createZoneNodeHandler 新建层级节点(按完整路径 区域→货架→层 精确定位父级并写冗余路径)。
// 父级必须存在且不能为空;位置号(level4)必须挂在已存在的层下(不再有“无层默认位置”)。
func createZoneNodeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req zoneNodeReq
@@ -181,7 +185,9 @@ func createZoneNodeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
req.Code = strings.TrimSpace(req.Code)
req.Name = strings.TrimSpace(req.Name)
req.ParentCode = strings.TrimSpace(req.ParentCode)
req.ZoneCode = strings.ToUpper(strings.TrimSpace(req.ZoneCode))
req.ShelfNo = strings.TrimSpace(req.ShelfNo)
req.LayerNo = strings.TrimSpace(req.LayerNo)
req.Remark = strings.TrimSpace(req.Remark)
if req.Level < 1 || req.Level > 4 {
fail(w, http.StatusBadRequest, "level 必须为 1~4")
@@ -199,55 +205,55 @@ func createZoneNodeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
}
// 逐级解析父级:父级必须已存在,路径(区域+货架+层)唯一确定,避免仅凭 code 跨父级歧义。
var zoneCode, shelfNo, layerNo, parentCode string
parentCode = req.ParentCode
switch req.Level {
case 1:
zoneCode = req.Code
parentCode = ""
case 2:
if req.ParentCode == "" {
if req.ZoneCode == "" {
fail(w, http.StatusBadRequest, "请选择所属区域")
return
}
reg, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(1), zone.CodeEQ(req.ParentCode)).Only(ctx0())
reg, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(1), zone.CodeEQ(req.ZoneCode)).Only(ctx0())
if err != nil {
fail(w, http.StatusBadRequest, "所属区域不存在:"+req.ParentCode)
fail(w, http.StatusBadRequest, "所属区域不存在:"+req.ZoneCode)
return
}
zoneCode = reg.ZoneCode
shelfNo = req.Code
parentCode = reg.Code
case 3:
if req.ParentCode == "" {
fail(w, http.StatusBadRequest, "请选择所属货架")
if req.ZoneCode == "" || req.ShelfNo == "" {
fail(w, http.StatusBadRequest, "请选择所属区域与货架")
return
}
sh, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(2), zone.CodeEQ(req.ParentCode)).Only(ctx0())
sh, err := ctx.EntClient.Zone.Query().
Where(zone.LevelEQ(2), zone.ZoneCodeEQ(req.ZoneCode), zone.CodeEQ(req.ShelfNo)).Only(ctx0())
if err != nil {
fail(w, http.StatusBadRequest, "所属货架不存在:"+req.ParentCode)
fail(w, http.StatusBadRequest, "所属货架不存在:"+req.ZoneCode+"/"+req.ShelfNo)
return
}
zoneCode = sh.ZoneCode
shelfNo = sh.ShelfNo
layerNo = req.Code
parentCode = sh.Code
case 4:
if req.ParentCode == "" {
fail(w, http.StatusBadRequest, "请选择所属层或货架(默认位置)")
if req.ZoneCode == "" || req.ShelfNo == "" || req.LayerNo == "" {
fail(w, http.StatusBadRequest, "请选择所属层")
return
}
parent, err := ctx.EntClient.Zone.Query().
Where(zone.CodeEQ(req.ParentCode)).Where(zone.Or(zone.LevelEQ(3), zone.LevelEQ(2))).Only(ctx0())
ly, err := ctx.EntClient.Zone.Query().
Where(zone.LevelEQ(3), zone.ZoneCodeEQ(req.ZoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(req.LayerNo)).Only(ctx0())
if err != nil {
fail(w, http.StatusBadRequest, "所属层/货架不存在:"+req.ParentCode)
fail(w, http.StatusBadRequest, "所属层不存在:"+req.ZoneCode+"/"+req.ShelfNo+"/"+req.LayerNo)
return
}
zoneCode = parent.ZoneCode
shelfNo = parent.ShelfNo
if parent.Level == 3 {
layerNo = parent.Code
} else {
layerNo = "" // 默认位置:无层
}
zoneCode = ly.ZoneCode
shelfNo = ly.ShelfNo
layerNo = ly.Code
parentCode = ly.Code
}
// 唯一性:同层级 + 同区域 + 同货架 + 同层 + 同编码 不可重复
@@ -275,15 +281,6 @@ func createZoneNodeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return
}
// 建货架(level2)时自动创建一个「默认位置」(level4,挂在货架下、无层),物料可不细分时挂这里
if req.Level == 2 {
_, _ = ctx.EntClient.Zone.Create().
SetLevel(4).SetCode("默认").SetParentCode(req.Code).
SetZoneCode(zoneCode).SetShelfNo(shelfNo).SetNillableLayerNo(strPtr("")).
SetPositionNo("默认").SetName("默认位置").SetStatus("启用").
Save(ctx0())
}
ctx.EventLog.Write(ctx0(), "zone.create", r.Header.Get("X-Username"), "zone", req.Code,
"新建"+zoneLevelName(req.Level)+" "+req.Code, nil)
ok(w, toZoneNodeVO(z))
@@ -459,17 +456,18 @@ func listZonesForPicker(ctx *svc.ServiceContext) http.HandlerFunc {
type zoneBatchGenReq struct {
ZoneCode string `json:"zoneCode"`
ShelfNo string `json:"shelfNo"`
LayerStart int `json:"layerStart"`
LayerEnd int `json:"layerEnd"`
LayerStart int `json:"layerStart"` // level=3 批量生成层:层范围起
LayerEnd int `json:"layerEnd"` // level=3 批量生成层:层范围止
LayerNo string `json:"layerNo"` // level=4 批量生成位置号:指定的已存在层(只在该层下生成位置,不生成父级)
PosStart int `json:"posStart"`
PosEnd int `json:"posEnd"`
Level int `json:"level"` // 3=批量生成层(level3) 4=批量生成位置号(level4,默认,同时确保层存在)
Level int `json:"level"` // 3=批量生成层(level3) 4=批量生成位置号(level4,默认,需指定已存在的层)
Preview bool `json:"preview"`
}
// batchGenerateHandler 批量生成库位节点:选区域→货架→范围,先预览后提交。
// level=3:只生成「层」节点(按层范围);level=4(默认):生成「位置号」节点(层×位置),并自动确保层节点存在
// 已存在的节点跳过不重复建。
// level=3:只生成「层」节点(按层范围);level=4(默认):在「已存在的具体层」下生成「位置号」节点。
// 批量生成只生成自己这一级,绝不自动创建父级;已存在的节点跳过不重复建。
func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req zoneBatchGenReq
@@ -487,14 +485,6 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
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())
if err != nil {
fail(w, http.StatusBadRequest, "货架不存在:"+req.ZoneCode+"/"+req.ShelfNo)
@@ -502,13 +492,16 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
zoneCode := sh.ZoneCode
layers := make([]int, 0)
for l := req.LayerStart; l <= req.LayerEnd; l++ {
layers = append(layers, l)
}
// ---------- level=3:批量生成层节点 ----------
// ---------- level=3:批量生成层节点(父级货架必须已存在,只生成层自己) ----------
if level == 3 {
if req.LayerStart < 0 || req.LayerEnd < req.LayerStart {
fail(w, http.StatusBadRequest, "层起止不合法(起始需 ≤ 结束,且 ≥ 0)")
return
}
layers := make([]int, 0)
for l := req.LayerStart; l <= req.LayerEnd; l++ {
layers = append(layers, l)
}
if len(layers) > 2000 {
fail(w, http.StatusBadRequest, "一次最多生成 2000 个层,请缩小范围")
return
@@ -552,11 +545,26 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
// ---------- level=4:批量生成位置号 ----------
// 只允许在「已存在的具体层」下批量生成位置号(批量生成只生成自己,不创建父级层)。
layerNo := strings.TrimSpace(req.LayerNo)
if layerNo == "" {
fail(w, http.StatusBadRequest, "请选择具体的层")
return
}
if req.PosStart < 0 || req.PosEnd < req.PosStart {
fail(w, http.StatusBadRequest, "位置起止不合法(起始需 ≤ 结束,且 ≥ 0)")
return
}
if exist, _ := ctx.EntClient.Zone.Query().
Where(zone.LevelEQ(3), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(layerNo)).Exist(ctx0()); !exist {
fail(w, http.StatusBadRequest, "层不存在:"+req.ShelfNo+"架"+layerNo+"层,请先在「层」页签创建该层后再生成位置号")
return
}
poss := make([]int, 0)
for p := req.PosStart; p <= req.PosEnd; p++ {
poss = append(poss, p)
}
if len(layers)*len(poss) > 2000 {
if len(poss) > 2000 {
fail(w, http.StatusBadRequest, "一次最多生成 2000 个位置,请缩小范围")
return
}
@@ -568,55 +576,41 @@ func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
PositionName string `json:"positionName"`
Exists bool `json:"exists"`
}
preview := make([]genItem, 0, len(layers)*len(poss))
for _, l := range layers {
ls := strconv.Itoa(l)
for _, p := range poss {
ps := strconv.Itoa(p)
exists, _ := ctx.EntClient.Zone.Query().
Where(zone.LevelEQ(4), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo),
zone.LayerNoEQ(ls), zone.CodeEQ(ps)).Exist(ctx0())
preview = append(preview, genItem{LayerNo: ls, PositionNo: ps, PositionName: req.ShelfNo + "架" + ls + "层" + ps + "位", Exists: exists})
}
preview := make([]genItem, 0, len(poss))
for _, p := range poss {
ps := strconv.Itoa(p)
exists, _ := ctx.EntClient.Zone.Query().
Where(zone.LevelEQ(4), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo),
zone.LayerNoEQ(layerNo), zone.CodeEQ(ps)).Exist(ctx0())
preview = append(preview, genItem{LayerNo: layerNo, PositionNo: ps, PositionName: req.ShelfNo + "架" + layerNo + "层" + ps + "位", Exists: exists})
}
if req.Preview {
ok(w, map[string]any{"preview": true, "list": preview, "total": len(preview)})
return
}
// 提交:确保层节点存在 + 创建不存在的位置号
// 提交:仅在选定层下创建不存在的位置号(层必须已存在,不再自动创建父级层)
created, skipped := 0, 0
for _, l := range layers {
ls := strconv.Itoa(l)
if exist, _ := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(3), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(ls)).Exist(ctx0()); !exist {
if _, err := ctx.EntClient.Zone.Create().
SetLevel(3).SetCode(ls).SetParentCode(req.ShelfNo).
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(ls).SetStatus("启用").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "创建层失败: "+err.Error())
return
}
for _, p := range poss {
ps := strconv.Itoa(p)
if exist, _ := ctx.EntClient.Zone.Query().
Where(zone.LevelEQ(4), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo),
zone.LayerNoEQ(layerNo), zone.CodeEQ(ps)).Exist(ctx0()); exist {
skipped++
continue
}
for _, p := range poss {
ps := strconv.Itoa(p)
if exist, _ := ctx.EntClient.Zone.Query().
Where(zone.LevelEQ(4), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo),
zone.LayerNoEQ(ls), zone.CodeEQ(ps)).Exist(ctx0()); exist {
skipped++
continue
}
if _, err := ctx.EntClient.Zone.Create().
SetLevel(4).SetCode(ps).SetParentCode(ls).
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(ls).SetPositionNo(ps).
SetName(req.ShelfNo + "架" + ls + "层" + ps + "位").SetStatus("启用").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "创建位置失败: "+err.Error())
return
}
created++
if _, err := ctx.EntClient.Zone.Create().
SetLevel(4).SetCode(ps).SetParentCode(layerNo).
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(layerNo).SetPositionNo(ps).
SetName(req.ShelfNo + "架" + layerNo + "层" + ps + "位").SetStatus("启用").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "创建位置失败: "+err.Error())
return
}
created++
}
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)+
"批量生成位置 "+req.ZoneCode+"/"+req.ShelfNo+" 层"+layerNo+
" 位"+strconv.Itoa(req.PosStart)+"-"+strconv.Itoa(req.PosEnd)+":新建"+strconv.Itoa(created)+" 跳过"+strconv.Itoa(skipped), nil)
ok(w, map[string]any{"preview": false, "created": created, "skipped": skipped, "total": len(layers) * len(poss)})
ok(w, map[string]any{"preview": false, "created": created, "skipped": skipped, "total": len(poss)})
}
}