Files
bj_power/bj_power_wms/internal/handler/material.go
T
SunYF 6d8bb75696 refactor(wms/mes): 统一多场景查询逻辑,移除关键字混搜
1. 移除所有多字段关键字混搜,改为各筛选字段独立查询
2. 调整excel导出相关逻辑,优化文件名处理与导入模板
3. 修复物料导入的类型解析与token获取逻辑
4. 更新依赖与前端页面文案、筛选参数
2026-09-08 13:43:08 +08:00

516 lines
15 KiB
Go

package handler
import (
"net/http"
"strings"
"time"
"bj_power_wms/ent"
"bj_power_wms/ent/material"
"bj_power_wms/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
"github.com/xuri/excelize/v2"
)
// itemTypeLabel 物料品类标签(与 inventories.category 对应):1原材料/2半成品/3成品
func itemTypeLabel(t int) string {
switch t {
case invCatSemi:
return "半成品"
case invCatFinished:
return "成品"
default:
return "原材料"
}
}
// createMaterialHandler 创建物料档案
func createMaterialHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Code string `json:"code"`
Name string `json:"name"`
ShortName string `json:"shortName"`
Spec string `json:"spec"`
Unit string `json:"unit"`
Description string `json:"description"`
ManageMode int `json:"manageMode"` // 1批次/2序列号
TemplateCode string `json:"templateCode"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.Code == "" || req.Name == "" {
fail(w, http.StatusBadRequest, "code 和 name 必填")
return
}
// 校验编码唯一
exists, err := ctx.EntClient.Material.Query().
Where(material.CodeEQ(req.Code)).
Exist(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
if exists {
fail(w, http.StatusConflict, "物料编码已存在: "+req.Code)
return
}
manageMode := req.ManageMode
if manageMode == 0 {
manageMode = 1
}
m, err := ctx.EntClient.Material.Create().
SetCode(req.Code).
SetName(req.Name).
SetNillableShortName(strPtr(req.ShortName)).
SetNillableSpec(strPtr(req.Spec)).
SetNillableUnit(strPtr(req.Unit)).
SetNillableDescription(strPtr(req.Description)).
SetManageMode(manageMode).
SetIsBatchManaged(manageMode == 1).
SetIsSerialManaged(manageMode == 2).
SetNillableTemplateCode(strPtr(req.TemplateCode)).
Save(ctx0())
if err != nil {
logx.Errorf("create material failed: %v", err)
fail(w, http.StatusInternalServerError, "创建物料失败: "+err.Error())
return
}
ctx.EventLog.Write(ctx0(), "material.create", r.Header.Get("X-Username"), "material", req.Code,
"新增物料 "+req.Code+" "+req.Name, map[string]any{"manageMode": manageMode})
ok(w, m)
}
}
// updateMaterialHandler 更新物料档案
func updateMaterialHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID int `json:"id"`
Name string `json:"name"`
ShortName string `json:"shortName"`
Spec string `json:"spec"`
Unit string `json:"unit"`
Description string `json:"description"`
ManageMode int `json:"manageMode"`
}
if err := parseJSON(r, &req); err != nil || req.ID <= 0 {
fail(w, http.StatusBadRequest, "参数错误")
return
}
cur, gerr := ctx.EntClient.Material.Get(ctx0(), req.ID)
if gerr != nil {
fail(w, http.StatusNotFound, "物料不存在")
return
}
upd := ctx.EntClient.Material.UpdateOneID(req.ID)
if req.Name != "" {
upd.SetName(req.Name)
}
upd.SetShortName(req.ShortName)
upd.SetSpec(req.Spec)
upd.SetUnit(req.Unit)
upd.SetDescription(req.Description)
if req.ManageMode > 0 {
upd.SetManageMode(req.ManageMode).
SetIsBatchManaged(req.ManageMode == 1).
SetIsSerialManaged(req.ManageMode == 2)
}
m, err := upd.Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "更新物料失败: "+err.Error())
return
}
ctx.EventLog.Write(ctx0(), "material.update", r.Header.Get("X-Username"), "material", cur.Code,
"编辑物料 "+cur.Code+" "+cur.Name, nil)
ok(w, m)
}
}
// queryMaterialsHandler 分页查询物料
func queryMaterialsHandler(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)
materialCode := r.URL.Query().Get("materialCode")
name := r.URL.Query().Get("name")
spec := r.URL.Query().Get("spec")
manageMode := atoi(r.URL.Query().Get("manageMode"), 0)
itemType := atoi(r.URL.Query().Get("itemType"), 0)
startDate := r.URL.Query().Get("startDate")
endDate := r.URL.Query().Get("endDate")
keyword := r.URL.Query().Get("keyword") // 仅供下拉选择框(MaterialSelect)远程搜索:编码/名称任一匹配;列表页禁用此混搜模式
q := ctx.EntClient.Material.Query()
if materialCode != "" {
q = q.Where(material.CodeContainsFold(materialCode))
}
if name != "" {
q = q.Where(material.NameContainsFold(name))
}
if spec != "" {
q = q.Where(material.SpecContainsFold(spec))
}
if manageMode != 0 {
q = q.Where(material.ManageModeEQ(manageMode))
}
if itemType != 0 {
q = q.Where(material.ItemTypeEQ(itemType))
}
if keyword != "" {
q = q.Where(
material.Or(
material.CodeContainsFold(keyword),
material.NameContainsFold(keyword),
),
)
}
if startDate != "" {
if t, err := time.ParseInLocation("2006-01-02", startDate, time.Local); err == nil {
q = q.Where(material.CreatedAtGTE(t.Unix()))
}
}
if endDate != "" {
if t, err := time.ParseInLocation("2006-01-02", endDate, time.Local); err == nil {
q = q.Where(material.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("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,
})
}
}
// listMaterialsHandler 下拉框全量列表(不翻页)
func listMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
list, err := ctx.EntClient.Material.Query().Order(ent.Desc("created_at"), ent.Desc("id")).Limit(100).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
ok(w, list)
}
}
// getMaterialHandler 物料详情
func getMaterialHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := atoi(r.URL.Query().Get("id"), 0)
if id <= 0 {
fail(w, http.StatusBadRequest, "id 必填")
return
}
m, err := ctx.EntClient.Material.Get(ctx0(), id)
if err != nil {
fail(w, http.StatusNotFound, "物料不存在")
return
}
ok(w, m)
}
}
// materialExistsHandler 内部接口:批量校验物料编码是否存在于物料档案。
// body: {"codes":["A","B"]} → data: {"missing":["B"]}(不存在于档案的编码列表)。
// 供 MES 备料单生成前防呆使用(wrapInternal + X-API-TOKEN 保护)。
func materialExistsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Codes []string `json:"codes"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
// 去重并过滤空串
seen := map[string]bool{}
codes := []string{}
for _, c := range req.Codes {
c = strings.TrimSpace(c)
if c == "" || seen[c] {
continue
}
seen[c] = true
codes = append(codes, c)
}
missing := []string{}
if len(codes) > 0 {
// 查存在的编码集合
exist, err := ctx.EntClient.Material.Query().
Where(material.CodeIn(codes...)).
Select(material.FieldCode).
All(ctx0())
if err == nil {
have := map[string]bool{}
for _, m := range exist {
have[m.Code] = true
}
for _, c := range codes {
if !have[c] {
missing = append(missing, c)
}
}
}
}
ok(w, map[string]any{"missing": missing})
}
}
// exportMaterialsHandler 物料档案全量导出(xlsx),按当前筛选条件导出
func exportMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
materialCode := r.URL.Query().Get("materialCode")
name := r.URL.Query().Get("name")
startDate, endDate, rerr := normalizeExportRange(r.URL.Query().Get("startDate"), r.URL.Query().Get("endDate"))
if rerr != nil {
fail(w, http.StatusBadRequest, rerr.Error())
return
}
startUnix, endUnix := exportRangeUnix(startDate, endDate)
spec := r.URL.Query().Get("spec")
manageMode := atoi(r.URL.Query().Get("manageMode"), 0)
itemType := atoi(r.URL.Query().Get("itemType"), 0)
q := ctx.EntClient.Material.Query()
q = q.Where(material.CreatedAtGTE(startUnix), material.CreatedAtLTE(endUnix))
if materialCode != "" {
q = q.Where(material.CodeContainsFold(materialCode))
}
if name != "" {
q = q.Where(material.NameContainsFold(name))
}
if spec != "" {
q = q.Where(material.SpecContainsFold(spec))
}
if manageMode != 0 {
q = q.Where(material.ManageModeEQ(manageMode))
}
if itemType != 0 {
q = q.Where(material.ItemTypeEQ(itemType))
}
list, err := q.Order(ent.Desc("id")).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
type exportRow struct {
ID int
Code string
Name string
ShortName string
Spec string
Unit string
ItemType int
ManageMode int
Description string
CreatedAt int64
}
rows := make([]exportRow, 0, len(list))
for _, m := range list {
rows = append(rows, exportRow{
ID: m.ID, Code: m.Code, Name: m.Name, ShortName: m.ShortName, Spec: m.Spec,
Unit: m.Unit, ItemType: m.ItemType, ManageMode: m.ManageMode,
Description: m.Description, CreatedAt: m.CreatedAt,
})
}
if r.URL.Query().Get("format") == "xlsx" {
headers := []string{"ID", "物料编码", "名称", "简称", "规格", "单位", "品类", "类型", "说明", "创建时间"}
matrix := make([][]any, 0, len(rows))
for _, rr := range rows {
matrix = append(matrix, []any{
rr.ID, rr.Code, rr.Name, rr.ShortName, rr.Spec, rr.Unit,
itemTypeLabel(rr.ItemType), manageModeLabel(rr.ManageMode),
rr.Description, unixFmt(rr.CreatedAt),
})
}
sendExcel(w, xlsxFilename("物料档案"), headers, matrix)
return
}
ok(w, map[string]any{"total": len(rows), "list": rows})
}
}
// parseManageMode 解析导入模板"类型"列:支持中文(结构件/精密件,含"(批次)/(SN)"后缀)
// 与旧数字格式(1/2)双向兼容;无法识别返回 0。
func parseManageMode(s string) int {
if s == "" {
return 0
}
if strings.Contains(s, "精密") || strings.EqualFold(s, "SN") {
return 2
}
if strings.Contains(s, "结构") || strings.Contains(s, "批次") {
return 1
}
if n := atoi(s, 0); n == 1 || n == 2 {
return n
}
return 0
}
// excelMaterialImportHandler 物料档案 Excel 批量导入
// 模板(首行忽略表头):物料编码 | 名称 | 简称 | 规格 | 单位 | 类型(结构件/精密件) | 说明
// 强约束:全成功或全失败(一次性全导入 or 全失败,禁止部分成功)。
// 阶段1 逐行解析+校验(编码必填且唯一 / 名称必填 / 类型合法) → 任一错则整体拒绝并列出错误行;
// 阶段2 全部通过 → 开启单事务整体提交,任一步失败整体回滚。
func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(20 << 20); err != nil {
fail(w, http.StatusBadRequest, "文件上传失败: "+err.Error())
return
}
f, _, err := r.FormFile("file")
if err != nil {
fail(w, http.StatusBadRequest, "缺少 file 字段")
return
}
defer f.Close()
xl, err := excelize.OpenReader(f)
if err != nil {
fail(w, http.StatusBadRequest, "无法解析 Excel: "+err.Error())
return
}
defer xl.Close()
sheet := xl.GetSheetList()[0]
rows, err := xl.GetRows(sheet)
if err != nil {
fail(w, http.StatusBadRequest, "读取工作表失败: "+err.Error())
return
}
if len(rows) <= 1 {
fail(w, http.StatusBadRequest, "文件无数据行(首行为表头,需至少一行数据)")
return
}
type planRow struct {
row int
code string
name string
shortName string
spec string
unit string
manageMode int
description string
}
plans := make([]planRow, 0, len(rows)-1)
errs := make([]importErr, 0)
// 阶段1:解析 + 校验(不落库)
for i, row := range rows {
if i == 0 {
continue // 表头
}
cell := func(idx int) string {
if idx < len(row) {
return strings.TrimSpace(row[idx])
}
return ""
}
pr := planRow{row: i + 1}
pr.code = cell(0)
pr.name = cell(1)
pr.shortName = cell(2)
pr.spec = cell(3)
pr.unit = cell(4)
pr.manageMode = parseManageMode(cell(5))
pr.description = cell(6)
if pr.code == "" {
errs = append(errs, importErr{pr.row, pr.code, "物料编码为空"})
continue
}
if pr.name == "" {
errs = append(errs, importErr{pr.row, pr.code, "名称为空"})
continue
}
if pr.manageMode == 0 {
errs = append(errs, importErr{pr.row, pr.code, "类型无法识别,请填写「结构件」或「精密件」(兼容 1/2)"})
continue
}
exists, _ := ctx.EntClient.Material.Query().
Where(material.CodeEQ(pr.code)).Exist(ctx0())
if exists {
errs = append(errs, importErr{pr.row, pr.code, "物料编码已存在: " + pr.code})
continue
}
plans = append(plans, pr)
}
// 阶段2:有错则整体拒绝,一行都不写
if len(errs) > 0 {
ok(w, map[string]any{"success": 0, "failed": len(errs), "errors": errs})
return
}
// 阶段3:全部通过 → 单事务整体提交(原子,全成功或全失败)
tx, e := ctx.EntClient.Tx(ctx0())
if e != nil {
fail(w, http.StatusInternalServerError, "开启事务失败: "+e.Error())
return
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
for _, pr := range plans {
if _, e = tx.Material.Create().
SetCode(pr.code).
SetName(pr.name).
SetNillableShortName(strPtr(pr.shortName)).
SetNillableSpec(strPtr(pr.spec)).
SetNillableUnit(strPtr(pr.unit)).
SetNillableDescription(strPtr(pr.description)).
SetManageMode(pr.manageMode).
SetIsBatchManaged(pr.manageMode == 1).
SetIsSerialManaged(pr.manageMode == 2).
Save(ctx0()); e != nil {
fail(w, http.StatusInternalServerError, "创建物料失败: "+e.Error())
return
}
}
if e = tx.Commit(); e != nil {
fail(w, http.StatusInternalServerError, "提交事务失败: "+e.Error())
return
}
committed = true
ctx.EventLog.Write(ctx0(), "material.import", r.Header.Get("X-Username"), "material", "",
"Excel 导入物料", map[string]any{"success": len(plans)})
ok(w, map[string]any{"success": len(plans), "failed": 0, "errors": []importErr{}})
}
}