package handler import ( "fmt" "net/http" "time" "bj_power_wms/ent" "bj_power_wms/ent/inspectionrecord" "bj_power_wms/ent/inventory" "bj_power_wms/internal/svc" "github.com/zeromicro/go-zero/core/logx" ) // resolveInspectionTarget 通过编号(批次号或 SN)定位唯一库存行。 // 检验必须落在真实存在的库存记录上,才能同步质量状态——杜绝"提示成功但库存未更新"的假闭环。 // 返回错误或定位到的库存行(含物料编码与管理粒度,供后续写入检验记录与翻状态使用)。 func resolveInspectionTarget(ctx *svc.ServiceContext, targetId string) (*ent.Inventory, error) { // 优先按批次号(结构件) if inv, err := ctx.EntClient.Inventory.Query(). Where(inventory.ManageModeEQ(1), inventory.BatchNoEQ(targetId)).Only(ctx0()); err == nil { return inv, nil } // 再按 SN(精密件) if inv, err := ctx.EntClient.Inventory.Query(). Where(inventory.ManageModeEQ(2), inventory.SnCodeEQ(targetId)).Only(ctx0()); err == nil { return inv, nil } return nil, fmt.Errorf("未找到对应库存记录(批次号/SN):%s,请确认编号是否已入库、或该记录是否已出库", targetId) } // createInspectionHandler 创建检验记录 // body: { targetType: BATCH/SN, targetId, status, inspector } // 同步库存检验状态:BATCH → inventory(manage_mode=1, batch_no);SN → inventory(manage_mode=2, sn_code) func createInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { TargetType string `json:"targetType"` TargetId string `json:"targetId"` MaterialCode string `json:"materialCode"` InspectionType string `json:"inspectionType"` Status string `json:"status"` // 合格/不合格 ResultValue string `json:"resultValue"` InspectQty int `json:"inspectQty"` // 检验数量 PassQty int `json:"passQty"` // 合格数量 CheckDesc string `json:"checkDesc"` // 检测说明 FailReason string `json:"failReason"` // 不合格原因 Inspector string `json:"inspector"` Remark string `json:"remark"` } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误") return } if req.TargetId == "" || req.Status == "" { fail(w, http.StatusBadRequest, "targetId 和 status 必填") return } if req.Status != "合格" && req.Status != "不合格" { fail(w, http.StatusBadRequest, "status 只能为 合格/不合格") return } // 定位真实库存行:自动按 批次号/SN 解析(前端选错类型也能正确命中) inv, err := resolveInspectionTarget(ctx, req.TargetId) if err != nil { fail(w, http.StatusBadRequest, err.Error()) return } // 物料编码必填字段由库存行回填,杜绝 missing required field req.MaterialCode = inv.MaterialCode req.TargetType = "BATCH" if inv.ManageMode == 2 { req.TargetType = "SN" } tx, err := ctx.EntClient.Tx(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error()) return } committed := false defer func() { if !committed { _ = tx.Rollback() } }() rec, err := tx.InspectionRecord.Create(). SetTargetType(req.TargetType). SetTargetID(req.TargetId). SetMaterialCode(req.MaterialCode). SetInspectionType(req.InspectionType). SetStatus(req.Status). SetNillableResultValue(strPtr(req.ResultValue)). SetInspectQty(req.InspectQty). SetPassQty(req.PassQty). SetNillableCheckDesc(strPtr(req.CheckDesc)). SetNillableFailReason(strPtr(req.FailReason)). SetNillableInspector(strPtr(req.Inspector)). SetNillableRemark(strPtr(req.Remark)). Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } // 同步库存检验状态:直接更新定位到的那条库存行(精确,绝不误伤) if _, err = tx.Inventory.UpdateOneID(inv.ID). SetQualityStatus(req.Status).Save(ctx0()); err != nil { fail(w, http.StatusInternalServerError, "同步库存检验状态失败: "+err.Error()) return } if err = tx.Commit(); err != nil { fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error()) return } committed = true logx.Infof("inspection: %s %s -> %s by %s", req.TargetType, req.TargetId, req.Status, req.Inspector) ok(w, rec) } } // batchFlipInspectionHandler 批量翻转检验状态 // body: { targetType: BATCH/SN, targetIds: [], status, inspector } func batchFlipInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { TargetType string `json:"targetType"` TargetIds []string `json:"targetIds"` Status string `json:"status"` InspectQty int `json:"inspectQty"` // 检验数量 PassQty int `json:"passQty"` // 合格数量 CheckDesc string `json:"checkDesc"` // 检测说明 FailReason string `json:"failReason"` // 不合格原因 Inspector string `json:"inspector"` } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误") return } if len(req.TargetIds) == 0 || (req.Status != "合格" && req.Status != "不合格") { fail(w, http.StatusBadRequest, "targetIds 和 status(合格/不合格) 必填") return } // 预校验:所有编号必须能定位到库存行,任意一条找不到则整体拒绝(避免部分成功、假闭环) type target struct { id string inv *ent.Inventory targetType string material string } targets := make([]target, 0, len(req.TargetIds)) for _, id := range req.TargetIds { inv, err := resolveInspectionTarget(ctx, id) if err != nil { fail(w, http.StatusBadRequest, err.Error()) return } tt := "BATCH" if inv.ManageMode == 2 { tt = "SN" } targets = append(targets, target{id: id, inv: inv, targetType: tt, material: inv.MaterialCode}) } tx, err := ctx.EntClient.Tx(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error()) return } committed := false defer func() { if !committed { _ = tx.Rollback() } }() flipped := 0 for _, t := range targets { if _, err = tx.InspectionRecord.Create(). SetTargetType(t.targetType). SetTargetID(t.id). SetMaterialCode(t.material). SetStatus(req.Status). SetInspectQty(req.InspectQty). SetPassQty(req.PassQty). SetNillableCheckDesc(strPtr(req.CheckDesc)). SetNillableFailReason(strPtr(req.FailReason)). SetNillableInspector(strPtr(req.Inspector)). Save(ctx0()); err != nil { fail(w, http.StatusInternalServerError, "创建检验记录失败: "+err.Error()) return } if _, err = tx.Inventory.UpdateOneID(t.inv.ID). SetQualityStatus(req.Status).Save(ctx0()); err != nil { fail(w, http.StatusInternalServerError, "同步库存检验状态失败: "+err.Error()) return } flipped++ } if err = tx.Commit(); err != nil { fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error()) return } committed = true ok(w, map[string]any{"flipped": flipped}) } } // queryInspectionHandler 检验记录查询 func queryInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { page := atoi(r.URL.Query().Get("page"), 1) pageSize := atoi(r.URL.Query().Get("pageSize"), 20) targetId := r.URL.Query().Get("targetId") materialCode := r.URL.Query().Get("materialCode") inspectionType := r.URL.Query().Get("inspectionType") status := r.URL.Query().Get("status") inspector := r.URL.Query().Get("inspector") startDate := r.URL.Query().Get("startDate") endDate := r.URL.Query().Get("endDate") q := ctx.EntClient.InspectionRecord.Query() if targetId != "" { q = q.Where(inspectionrecord.TargetIDContains(targetId)) } if materialCode != "" { q = q.Where(inspectionrecord.MaterialCodeContains(materialCode)) } if inspectionType != "" { q = q.Where(inspectionrecord.InspectionTypeEQ(inspectionType)) } if status != "" { q = q.Where(inspectionrecord.StatusEQ(status)) } if inspector != "" { q = q.Where(inspectionrecord.InspectorContains(inspector)) } // 时间筛选(默认近3个月由前端传入;兼容两端口径) if startDate != "" { if t, err := time.ParseInLocation("2006-01-02", startDate, time.Local); err == nil { q = q.Where(inspectionrecord.CreatedAtGTE(t.Unix())) } } if endDate != "" { if t, err := time.ParseInLocation("2006-01-02", endDate, time.Local); err == nil { q = q.Where(inspectionrecord.CreatedAtLTE(t.Add(24 * time.Hour).Unix())) } } total, err := q.Count(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } list, err := q.Order(ent.Desc("created_at")). Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, map[string]any{ "total": total, "list": list, "page": page, "pageSize": pageSize, }) } } // exportInspectionHandler 检验记录全量导出(按条件返回全部 JSON,前端转 CSV/Excel) // GET /api/inspection/export?targetId=&materialCode=&inspectionType=&status=&inspector=&startDate=&endDate= func exportInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { targetId := r.URL.Query().Get("targetId") materialCode := r.URL.Query().Get("materialCode") inspectionType := r.URL.Query().Get("inspectionType") status := r.URL.Query().Get("status") inspector := r.URL.Query().Get("inspector") startDate := r.URL.Query().Get("startDate") endDate := r.URL.Query().Get("endDate") q := ctx.EntClient.InspectionRecord.Query() if targetId != "" { q = q.Where(inspectionrecord.TargetIDContains(targetId)) } if materialCode != "" { q = q.Where(inspectionrecord.MaterialCodeContains(materialCode)) } if inspectionType != "" { q = q.Where(inspectionrecord.InspectionTypeEQ(inspectionType)) } if status != "" { q = q.Where(inspectionrecord.StatusEQ(status)) } if inspector != "" { q = q.Where(inspectionrecord.InspectorContains(inspector)) } if startDate != "" { if t, err := time.ParseInLocation("2006-01-02", startDate, time.Local); err == nil { q = q.Where(inspectionrecord.CreatedAtGTE(t.Unix())) } } if endDate != "" { if t, err := time.ParseInLocation("2006-01-02", endDate, time.Local); err == nil { q = q.Where(inspectionrecord.CreatedAtLTE(t.Add(24 * time.Hour).Unix())) } } list, err := q.Order(ent.Desc("created_at")).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } // xlsx 导出 if r.URL.Query().Get("format") == "xlsx" { headers := []string{"ID", "对象", "批次/SN", "物料编码", "检验类型", "结论", "检验数", "合格数", "检测说明", "不合格原因", "检验员", "备注", "检验时间"} matrix := make([][]any, 0, len(list)) for _, rec := range list { obj := rec.TargetType if obj == "BATCH" { obj = "批次" } else if obj == "SN" { obj = "SN" } matrix = append(matrix, []any{ rec.ID, obj, rec.TargetID, rec.MaterialCode, inspectionTypeLabel(rec.InspectionType), rec.Status, rec.InspectQty, rec.PassQty, rec.CheckDesc, rec.FailReason, rec.Inspector, rec.Remark, unixFmt(rec.CreatedAt), }) } sendExcel(w, xlsxFilename("检验记录"), headers, matrix) return } ok(w, map[string]any{"list": list}) } }