Files
bj_power/bj_power_wms/internal/handler/inspection.go
T
SunYF 0ab21b1234 feat&refactor: 完成多模块功能迭代与配置优化
本次提交覆盖多个业务模块的功能完善与体验优化:
1.  **鉴权与配置调整**:
    - 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
    - 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
    - 工位配置放开1~12限制,改为仅校验大于0
2.  **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3.  **功能新增**:
    - 新增工位类型、工艺路线与产线点位台账模块
    - 添加工艺PDF预览面板、工位终端代理转发接口
    - 新增操作日志按操作人列表筛选、工位登出日志记录
    - 新增PLC移料指令与产线点位状态管理
4.  **业务流程优化**:
    - 调整BOM物料删除校验逻辑,优化工单备料计算
    - 补充物料图号、检测单号等追溯字段
    - 完善工艺流程图与工位绑定关系说明
    - 优化前端页面文案与交互细节
5.  **代码规范与维护**:
    - 新增通用工具函数与前端静态资源
    - 整理路由权限与中间件逻辑
    - 修复部分接口与配置的不兼容问题
2026-09-10 16:59:15 +08:00

424 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"` // 检测说明
ReportNo string `json:"reportNo"` // 报检单号(生产报检关联)
Inspector string `json:"inspector"`
Remark string `json:"remark"`
InspectionNo string `json:"inspectionNo"`
}
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)).
SetNillableInspector(strPtr(req.Inspector)).
SetNillableRemark(strPtr(req.Remark)).
SetNillableReportNo(strPtr(req.ReportNo)).
SetNillableInspectionNo(strPtr(req.InspectionNo)).
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"` // 检测说明
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)).
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")
startDate, endDate, rerr := normalizeExportRange(startDate, endDate)
if rerr != nil {
fail(w, http.StatusBadRequest, rerr.Error())
return
}
q := ctx.EntClient.InspectionRecord.Query()
if targetId != "" {
q = q.Where(inspectionrecord.TargetIDContainsFold(targetId))
}
if materialCode != "" {
q = q.Where(inspectionrecord.MaterialCodeContainsFold(materialCode))
}
if inspectionType != "" {
q = q.Where(inspectionrecord.InspectionTypeEQ(inspectionType))
}
if status != "" {
q = q.Where(inspectionrecord.StatusEQ(status))
}
if inspector != "" {
q = q.Where(inspectionrecord.InspectorContainsFold(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")
startDate, endDate, rerr := normalizeExportRange(startDate, endDate)
if rerr != nil {
fail(w, http.StatusBadRequest, rerr.Error())
return
}
q := ctx.EntClient.InspectionRecord.Query()
if targetId != "" {
q = q.Where(inspectionrecord.TargetIDContainsFold(targetId))
}
if materialCode != "" {
q = q.Where(inspectionrecord.MaterialCodeContainsFold(materialCode))
}
if inspectionType != "" {
q = q.Where(inspectionrecord.InspectionTypeEQ(inspectionType))
}
if status != "" {
q = q.Where(inspectionrecord.StatusEQ(status))
}
if inspector != "" {
q = q.Where(inspectionrecord.InspectorContainsFold(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.ReportNo,
rec.Inspector, rec.Remark, unixFmt(rec.CreatedAt),
rec.InspectionNo,
})
}
sendExcel(w, xlsxFilename("检验记录"), headers, matrix)
return
}
ok(w, map[string]any{"list": list})
}
}
// statsInspectionHandler 检验统计:按物料汇总检验次数/合格数/不合格数/合格率
// 对应《问题记录》附二9 的替代方案(不合并记录,另出统计视图)
func statsInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
inspectionType := r.URL.Query().Get("inspectionType")
startDate := r.URL.Query().Get("startDate")
endDate := r.URL.Query().Get("endDate")
q := ctx.EntClient.InspectionRecord.Query()
if inspectionType != "" {
q = q.Where(inspectionrecord.InspectionTypeEQ(inspectionType))
}
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()))
}
}
recs, err := q.All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
type statRow struct {
MaterialCode string `json:"materialCode"`
Total int `json:"total"`
Pass int `json:"pass"`
Fail int `json:"fail"`
PassRate float64 `json:"passRate"`
}
agg := map[string]*statRow{}
order := []string{}
for _, rec := range recs {
r, found := agg[rec.MaterialCode]
if !found {
r = &statRow{MaterialCode: rec.MaterialCode}
agg[rec.MaterialCode] = r
order = append(order, rec.MaterialCode)
}
r.Total++
if rec.Status == "合格" {
r.Pass++
} else if rec.Status == "不合格" {
r.Fail++
}
}
list := make([]*statRow, 0, len(agg))
for _, code := range order {
r := agg[code]
if r.Total > 0 {
r.PassRate = float64(int(float64(r.Pass)/float64(r.Total)*10000)) / 100
}
list = append(list, r)
}
ok(w, map[string]any{"list": list})
}
}