Files
bj_power/bj_power_wms/internal/handler/inspection.go
T
SunYF 05b0b8a909 feat: 完成客户第12条需求+多系统改造
1. 新增预警表添加工单号字段,支持按工单号检索预警
2. 添加工单过滤在制品/绩效报表/预警中心查询
3. 补全WMS台账字段口径与权限配置
4. 修复备料单与数量不符处理流程
5. 新增配送进度/库位联动/自动出库功能
6. 修复AGV配送状态更新逻辑
7. 优化退料与库存管理细节
2026-09-19 17:04:58 +08:00

661 lines
24 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/ent/material"
"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"`
DisposalType string `json:"disposalType"` // NONE/RETURN/SCRAP/PARTIAL
DisposalRemark string `json:"disposalRemark"`
ReturnTrackingNo string `json:"returnTrackingNo"`
// 归属 / 相关标准 / 生产厂家(客户诉求 问题记录 L107/L149/L494
OwnershipType string `json:"ownershipType"` // workorder/project/other
OwnershipNo string `json:"ownershipNo"`
RelatedStandard string `json:"relatedStandard"` // 相关标准(检验细则号)
Manufacturer string `json:"manufacturer"` // 生产厂家
}
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"
}
// 物料名称冗余(列表直接展示/名称模糊查询,客户诉求 L507-510 三框制)
materialName := inv.MaterialName
if materialName == "" {
if m, merr := ctx.EntClient.Material.Query().Where(material.CodeEQ(inv.MaterialCode)).First(ctx0()); merr == nil && m != nil {
materialName = m.Name
}
}
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).
SetNillableMaterialName(strPtr(materialName)).
SetNillableOwnershipType(strPtr(req.OwnershipType)).
SetNillableOwnershipNo(strPtr(req.OwnershipNo)).
SetNillableRelatedStandard(strPtr(req.RelatedStandard)).
SetNillableManufacturer(strPtr(req.Manufacturer)).
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)).
SetDisposalType(disposalTypeOrNone(req.DisposalType)).
SetNillableDisposalRemark(strPtr(req.DisposalRemark)).
SetNillableReturnTrackingNo(strPtr(req.ReturnTrackingNo)).
Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
// 同步库存检验状态:直接更新定位到的那条库存行(精确,绝不误伤)。
// 处置为"退货"时置 待退(隔离、禁止出库);否则不合格置 不合格(同样隔离)。
if _, err = tx.Inventory.UpdateOneID(inv.ID).
SetQualityStatus(qualityAfterInspection(req.Status, req.DisposalType)).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"`
ReportNo string `json:"reportNo"` // 报检单号
InspectionNo string `json:"inspectionNo"` // 检测单号
DisposalType string `json:"disposalType"` // NONE/RETURN/SCRAP/PARTIAL
DisposalRemark string `json:"disposalRemark"` // 处置说明
ReturnTrackingNo string `json:"returnTrackingNo"` // 退货货运单号
OwnershipType string `json:"ownershipType"`
OwnershipNo string `json:"ownershipNo"`
RelatedStandard string `json:"relatedStandard"`
Manufacturer string `json:"manufacturer"`
}
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 {
matName := t.inv.MaterialName
if matName == "" {
if m, merr := tx.Material.Query().Where(material.CodeEQ(t.material)).First(ctx0()); merr == nil && m != nil {
matName = m.Name
}
}
if _, err = tx.InspectionRecord.Create().
SetTargetType(t.targetType).
SetTargetID(t.id).
SetMaterialCode(t.material).
SetNillableMaterialName(strPtr(matName)).
SetNillableOwnershipType(strPtr(req.OwnershipType)).
SetNillableOwnershipNo(strPtr(req.OwnershipNo)).
SetNillableRelatedStandard(strPtr(req.RelatedStandard)).
SetNillableManufacturer(strPtr(req.Manufacturer)).
SetStatus(req.Status).
SetInspectQty(req.InspectQty).
SetPassQty(req.PassQty).
SetNillableCheckDesc(strPtr(req.CheckDesc)).
SetNillableInspector(strPtr(req.Inspector)).
SetNillableReportNo(strPtr(req.ReportNo)).
SetNillableInspectionNo(strPtr(req.InspectionNo)).
SetDisposalType(disposalTypeOrNone(req.DisposalType)).
SetNillableDisposalRemark(strPtr(req.DisposalRemark)).
SetNillableReturnTrackingNo(strPtr(req.ReturnTrackingNo)).
Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "创建检验记录失败: "+err.Error())
return
}
if _, err = tx.Inventory.UpdateOneID(t.inv.ID).
SetQualityStatus(qualityAfterInspection(req.Status, req.DisposalType)).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")
// 时间筛选:起止两端各自独立生效(只填一端合法,另一端不限制),**不做导出那套归一化校验**。
// 注意:normalizeExportRange(默认近3个月 / 只传一端即报错 / 间隔≤1年)是**导出**专用硬规则,
// 列表查询套用会导致进入「检测记录」Tab(默认只带 startDate)立即 400
// 「导出必须同时提供起始时间与结束时间」。口径与 statsInspectionHandler 保持一致。
startDate := r.URL.Query().Get("startDate")
endDate := r.URL.Query().Get("endDate")
q := ctx.EntClient.InspectionRecord.Query()
if targetId != "" {
q = q.Where(inspectionrecord.TargetIDContainsFold(targetId))
}
if materialCode != "" {
q = q.Where(inspectionrecord.MaterialCodeContainsFold(materialCode))
}
// 名称模糊 / 归属单号(客户诉求 L507-510 三框制:图号/名称/型号,每框内模糊、多框取交集;L149 增加归属筛选)
if v := r.URL.Query().Get("materialName"); v != "" {
q = q.Where(inspectionrecord.MaterialNameContainsFold(v))
}
if v := r.URL.Query().Get("ownershipNo"); v != "" {
q = q.Where(inspectionrecord.OwnershipNoContainsFold(v))
}
if v := r.URL.Query().Get("spec"); v != "" {
// 型号=物料档案规格型号,需 join 物料表取编码集合
codes := materialCodesBySpec(ctx, v)
if len(codes) == 0 {
ok(w, map[string]any{"total": 0, "list": []any{}, "page": page, "pageSize": pageSize})
return
}
q = q.Where(inspectionrecord.MaterialCodeIn(codes...))
}
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 v := r.URL.Query().Get("materialName"); v != "" {
q = q.Where(inspectionrecord.MaterialNameContainsFold(v))
}
if v := r.URL.Query().Get("ownershipNo"); v != "" {
q = q.Where(inspectionrecord.OwnershipNoContainsFold(v))
}
if v := r.URL.Query().Get("spec"); v != "" {
codes := materialCodesBySpec(ctx, v)
if len(codes) == 0 {
ok(w, map[string]any{"list": []any{}})
return
}
q = q.Where(inspectionrecord.MaterialCodeIn(codes...))
}
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" {
// 列名按客户口径(问题记录 L150):检验说明→相关标准、检验员→记录人、检验日期→记录日期、增加归属;
// 图号=物料编码(L18 合并裁决)。
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, rec.MaterialName,
inspectionTypeLabel(rec.InspectionType), rec.Status,
rec.InspectQty, rec.PassQty, rec.RelatedStandard, rec.ReportNo,
rec.InspectionNo, rec.OwnershipNo, rec.Manufacturer, rec.Inspector, rec.Remark,
unixFmt(rec.CreatedAt),
})
}
sendExcel(w, xlsxFilename("检验记录"), headers, matrix)
return
}
ok(w, map[string]any{"list": list})
}
}
// materialCodesBySpec 按规格型号模糊匹配物料编码集合(检验查询「型号」框用,L507-510)
func materialCodesBySpec(ctx *svc.ServiceContext, spec string) []string {
mats, err := ctx.EntClient.Material.Query().Where(material.SpecContainsFold(spec)).All(ctx0())
if err != nil {
return nil
}
codes := make([]string, 0, len(mats))
for _, m := range mats {
codes = append(codes, m.Code)
}
return codes
}
// disposalTypeOrNone 处置方式归一化(客户诉求 L137-145:退货/报废/部分入库)
func disposalTypeOrNone(v string) string {
switch v {
case "RETURN", "SCRAP", "PARTIAL":
return v
default:
return "NONE"
}
}
// qualityAfterInspection 检验后的库存质量状态:
// 合格 → 合格(可出库);不合格且选退货 → 待退(隔离);其余 → 不合格(隔离)。
func qualityAfterInspection(status, disposalType string) string {
if status == "合格" {
return "合格"
}
if disposalTypeOrNone(disposalType) == "RETURN" {
return "待退"
}
return "不合格"
}
// disposeInspectionHandler 不合格品处置(退货 / 报废 / 部分入库)
// POST /api/inspection/dispose body: { recordId, disposalType, disposalRemark, returnTrackingNo, operator }
//
// RETURN 退货 → 库存置「待退」隔离(保持数量,等待退货运单确认后出库)
// SCRAP 报废 → 库存置「不合格」且生命周期「报废」,退出可用库存
// PARTIAL 部分入库 → 仅批次管理:按合格数拆行(合格行入库 + 差额行隔离),数量守恒
func disposeInspectionHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
RecordID int `json:"recordId"`
DisposalType string `json:"disposalType"`
DisposalRemark string `json:"disposalRemark"`
ReturnTrackingNo string `json:"returnTrackingNo"`
Operator string `json:"operator"`
}
if err := parseJSON(r, &req); err != nil || req.RecordID <= 0 {
fail(w, http.StatusBadRequest, "参数错误:recordId 必填")
return
}
dt := disposalTypeOrNone(req.DisposalType)
if dt == "NONE" {
fail(w, http.StatusBadRequest, "请选择处置方式(退货/报废/部分入库)")
return
}
rec, err := ctx.EntClient.InspectionRecord.Get(ctx0(), req.RecordID)
if err != nil {
fail(w, http.StatusNotFound, "检验记录不存在")
return
}
if rec.Status != "不合格" {
fail(w, http.StatusBadRequest, "仅「不合格」记录可执行处置")
return
}
inv, err := resolveInspectionTarget(ctx, rec.TargetID)
if err != nil {
fail(w, http.StatusBadRequest, err.Error())
return
}
if dt == "RETURN" && req.ReturnTrackingNo == "" {
fail(w, http.StatusBadRequest, "退货处置必须填写货运单号")
return
}
tx, err := ctx.EntClient.Tx(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error())
return
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
if _, err = tx.InspectionRecord.UpdateOneID(rec.ID).
SetDisposalType(dt).
SetNillableDisposalRemark(strPtr(req.DisposalRemark)).
SetNillableReturnTrackingNo(strPtr(req.ReturnTrackingNo)).
Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "更新处置信息失败: "+err.Error())
return
}
switch dt {
case "RETURN":
if _, err = tx.Inventory.UpdateOneID(inv.ID).SetQualityStatus("待退").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "更新库存状态失败: "+err.Error())
return
}
case "SCRAP":
if _, err = tx.Inventory.UpdateOneID(inv.ID).SetQualityStatus("不合格").SetStatus("报废").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "更新库存状态失败: "+err.Error())
return
}
case "PARTIAL":
if inv.ManageMode != 1 {
fail(w, http.StatusBadRequest, "部分入库仅适用于批次管理物料(结构件)")
return
}
total := inv.Quantity
if rec.PassQty <= 0 || rec.PassQty >= total {
fail(w, http.StatusBadRequest, fmt.Sprintf("部分入库:合格数须在 0 与批次数量(%d) 之间", total))
return
}
bad := total - rec.PassQty
if _, err = tx.Inventory.UpdateOneID(inv.ID).
SetQuantity(rec.PassQty).SetQualityStatus("合格").Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "拆分合格批次失败: "+err.Error())
return
}
if _, err = tx.Inventory.Create().
SetManageMode(1).
SetCategory(inv.Category).
SetMaterialCode(inv.MaterialCode).
SetNillableMaterialName(strPtr(inv.MaterialName)).
SetBatchNo(inv.BatchNo + "-NG").
SetQuantity(bad).SetLockedQty(0).
SetQualityStatus("不合格").
SetNillableZoneCode(strPtr(inv.ZoneCode)).
SetStatus("在库").
SetNillableInboundNo(strPtr(inv.InboundNo)).
SetRemark("不合格隔离(部分入库差额)").
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.dispose", r.Header.Get("X-Username"), "inspection", rec.TargetID,
"不合格处置 "+rec.TargetID+" → "+dt, map[string]any{"recordId": rec.ID, "disposalType": dt})
ok(w, map[string]any{"recordId": rec.ID, "disposalType": dt})
}
}
// 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})
}
}