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") 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)) } // 名称模糊 / 归属单号(客户诉求 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}) } }