本次迭代覆盖MES与WMS核心业务: 1. 新增接驳台托盘传感器读取与AGV对接能力 2. 完善工单排产、备料流程与权限体系拆分 3. 优化看板接口与前端路由、样式 4. 新增操作日志、库存盘点与角色保护逻辑 5. 修复代理地址、BOM保存等已知问题
350 lines
12 KiB
Go
350 lines
12 KiB
Go
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
|
||
|
||
ctx.EventLog.Write(ctx0(), "inspection.create", r.Header.Get("X-Username"), "inspection", req.TargetId,
|
||
"质量检验 "+req.MaterialCode+" → "+req.Status, map[string]any{
|
||
"targetType": req.TargetType, "inspectQty": req.InspectQty, "passQty": req.PassQty})
|
||
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"), ent.Desc("id")).
|
||
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"), ent.Desc("id")).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})
|
||
}
|
||
}
|