Files
bj_power/bj_power_wms/internal/handler/inspection.go
T

347 lines
12 KiB
Go
Raw Normal View History

2026-08-28 15:06:01 +08:00
package handler
import (
"fmt"
2026-08-28 15:06:01 +08:00
"net/http"
"time"
2026-08-28 15:06:01 +08:00
"bj_power_wms/ent"
"bj_power_wms/ent/inspectionrecord"
"bj_power_wms/ent/inventory"
2026-08-28 15:06:01 +08:00
"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)
}
2026-08-28 15:06:01 +08:00
// createInspectionHandler 创建检验记录
// body: { targetType: BATCH/SN, targetId, status, inspector }
// 同步库存检验状态:BATCH → inventory(manage_mode=1, batch_no)SN → inventory(manage_mode=2, sn_code)
2026-08-28 15:06:01 +08:00
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"` // 不合格原因
2026-08-28 15:06:01 +08:00
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().
2026-08-28 15:06:01 +08:00
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)).
2026-08-28 15:06:01 +08:00
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
2026-08-28 15:06:01 +08:00
}
committed = true
2026-08-28 15:06:01 +08:00
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"` // 不合格原因
2026-08-28 15:06:01 +08:00
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()
}
}()
2026-08-28 15:06:01 +08:00
flipped := 0
for _, t := range targets {
if _, err = tx.InspectionRecord.Create().
SetTargetType(t.targetType).
SetTargetID(t.id).
SetMaterialCode(t.material).
2026-08-28 15:06:01 +08:00
SetStatus(req.Status).
SetInspectQty(req.InspectQty).
SetPassQty(req.PassQty).
SetNillableCheckDesc(strPtr(req.CheckDesc)).
SetNillableFailReason(strPtr(req.FailReason)).
2026-08-28 15:06:01 +08:00
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
2026-08-28 15:06:01 +08:00
}
flipped++
}
if err = tx.Commit(); err != nil {
fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error())
return
}
committed = true
2026-08-28 15:06:01 +08:00
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")
2026-08-28 15:06:01 +08:00
q := ctx.EntClient.InspectionRecord.Query()
if targetId != "" {
q = q.Where(inspectionrecord.TargetIDContains(targetId))
}
if materialCode != "" {
q = q.Where(inspectionrecord.MaterialCodeContains(materialCode))
2026-08-28 15:06:01 +08:00
}
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()))
}
}
2026-08-28 15:06:01 +08:00
total, err := q.Count(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
list, err := q.Order(ent.Desc("created_at")).
2026-08-28 15:06:01 +08:00
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})
}
}