refactor(wms/mes): 统一多场景查询逻辑,移除关键字混搜
1. 移除所有多字段关键字混搜,改为各筛选字段独立查询 2. 调整excel导出相关逻辑,优化文件名处理与导入模板 3. 修复物料导入的类型解析与token获取逻辑 4. 更新依赖与前端页面文案、筛选参数
This commit is contained in:
@@ -69,11 +69,12 @@ func sendExcel(w http.ResponseWriter, filename string, headers []string, rows []
|
||||
fail(w, http.StatusInternalServerError, "生成 Excel 失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
// RFC 5987:中文文件名用 filename*=UTF-8''...(URL 编码),并提供 ASCII 兜底文件名
|
||||
// RFC 5987:中文文件名用 filename*=UTF-8''...(URL 编码)。
|
||||
// ASCII 兜底名恒为纯 ASCII(中文原样放 filename="" 会被浏览器按 latin-1 渲染成乱码),
|
||||
// 前端优先解析 filename* 拿到中文原名。
|
||||
asciiName := "export.xlsx"
|
||||
encName := ""
|
||||
if utf8.ValidString(filename) {
|
||||
asciiName = strings.TrimSuffix(filename, ".xlsx") + ".xlsx"
|
||||
encName = "filename*=UTF-8''" + url.QueryEscape(filename)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
|
||||
@@ -245,13 +245,9 @@ func applyInboundFilters(q *ent.InboundOrderQuery, r *http.Request) *ent.Inbound
|
||||
if v := r.URL.Query().Get("inboundType"); v != "" {
|
||||
q = q.Where(inboundorder.InboundTypeEQ(v))
|
||||
}
|
||||
if v := r.URL.Query().Get("keyword"); v != "" {
|
||||
kw := v
|
||||
q = q.Where(inboundorder.Or(
|
||||
inboundorder.MaterialCodeContainsFold(kw),
|
||||
inboundorder.InboundNoContainsFold(kw),
|
||||
inboundorder.MaterialNameContainsFold(kw),
|
||||
))
|
||||
if v := r.URL.Query().Get("materialName"); v != "" {
|
||||
// 2026-09-08 用户明确:禁止"关键字"多字段混搜,各字段独立筛选
|
||||
q = q.Where(inboundorder.MaterialNameContainsFold(v))
|
||||
}
|
||||
if v := r.URL.Query().Get("startDate"); v != "" {
|
||||
if t, err := time.ParseInLocation("2006-01-02", v, time.Local); err == nil {
|
||||
|
||||
@@ -148,7 +148,7 @@ func queryMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
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") // 兼容旧版,作为物料编码/名称兜底
|
||||
keyword := r.URL.Query().Get("keyword") // 仅供下拉选择框(MaterialSelect)远程搜索:编码/名称任一匹配;列表页禁用此混搜模式
|
||||
|
||||
q := ctx.EntClient.Material.Query()
|
||||
if materialCode != "" {
|
||||
@@ -191,7 +191,7 @@ func queryMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
list, err := q.Order(ent.Asc("id")).
|
||||
list, err := q.Order(ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
All(ctx0())
|
||||
@@ -316,13 +316,14 @@ func exportMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
if itemType != 0 {
|
||||
q = q.Where(material.ItemTypeEQ(itemType))
|
||||
}
|
||||
list, err := q.Order(ent.Asc("id")).All(ctx0())
|
||||
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
|
||||
@@ -336,18 +337,18 @@ func exportMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
rows := make([]exportRow, 0, len(list))
|
||||
for _, m := range list {
|
||||
rows = append(rows, exportRow{
|
||||
Code: m.Code, Name: m.Name, ShortName: m.ShortName, Spec: m.Spec,
|
||||
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{"物料编码", "名称", "简称", "规格", "单位", "品类", "类型", "说明", "创建时间"}
|
||||
headers := []string{"ID", "物料编码", "名称", "简称", "规格", "单位", "品类", "类型", "说明", "创建时间"}
|
||||
matrix := make([][]any, 0, len(rows))
|
||||
for _, rr := range rows {
|
||||
matrix = append(matrix, []any{
|
||||
rr.Code, rr.Name, rr.ShortName, rr.Spec, rr.Unit,
|
||||
rr.ID, rr.Code, rr.Name, rr.ShortName, rr.Spec, rr.Unit,
|
||||
itemTypeLabel(rr.ItemType), manageModeLabel(rr.ManageMode),
|
||||
rr.Description, unixFmt(rr.CreatedAt),
|
||||
})
|
||||
@@ -359,8 +360,26 @@ func exportMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 批量导入
|
||||
// 模板(首行忽略表头):物料编码 | 名称 | 简称 | 规格 | 单位 | 类型(1结构件/2精密件) | 说明
|
||||
// 模板(首行忽略表头):物料编码 | 名称 | 简称 | 规格 | 单位 | 类型(结构件/精密件) | 说明
|
||||
// 强约束:全成功或全失败(一次性全导入 or 全失败,禁止部分成功)。
|
||||
// 阶段1 逐行解析+校验(编码必填且唯一 / 名称必填 / 类型合法) → 任一错则整体拒绝并列出错误行;
|
||||
// 阶段2 全部通过 → 开启单事务整体提交,任一步失败整体回滚。
|
||||
@@ -424,7 +443,7 @@ func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
pr.shortName = cell(2)
|
||||
pr.spec = cell(3)
|
||||
pr.unit = cell(4)
|
||||
pr.manageMode = atoi(cell(5), 1)
|
||||
pr.manageMode = parseManageMode(cell(5))
|
||||
pr.description = cell(6)
|
||||
|
||||
if pr.code == "" {
|
||||
@@ -435,8 +454,8 @@ func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
errs = append(errs, importErr{pr.row, pr.code, "名称为空"})
|
||||
continue
|
||||
}
|
||||
if pr.manageMode != 1 && pr.manageMode != 2 {
|
||||
errs = append(errs, importErr{pr.row, pr.code, "类型必须为 1(结构件) 或 2(精密件)"})
|
||||
if pr.manageMode == 0 {
|
||||
errs = append(errs, importErr{pr.row, pr.code, "类型无法识别,请填写「结构件」或「精密件」(兼容 1/2)"})
|
||||
continue
|
||||
}
|
||||
exists, _ := ctx.EntClient.Material.Query().
|
||||
|
||||
@@ -21,29 +21,37 @@ func productTypeQuery(ctx *svc.ServiceContext) *ent.MaterialQuery {
|
||||
return ctx.EntClient.Material.Query().Where(material.ItemTypeEQ(3))
|
||||
}
|
||||
|
||||
// applyProductTypeFilters 产品型号列表/导出统一筛选。
|
||||
// 2026-09-08 用户明确:禁止"关键字"多字段混搜,编码/名称/类别各字段独立模糊(大小写折叠)。
|
||||
// 注:类别存在 description 列。
|
||||
func applyProductTypeFilters(q *ent.MaterialQuery, r *http.Request) *ent.MaterialQuery {
|
||||
if v := r.URL.Query().Get("code"); v != "" {
|
||||
q = q.Where(material.CodeContainsFold(v))
|
||||
}
|
||||
if v := r.URL.Query().Get("name"); v != "" {
|
||||
q = q.Where(material.NameContainsFold(v))
|
||||
}
|
||||
if v := r.URL.Query().Get("category"); v != "" {
|
||||
q = q.Where(material.DescriptionContainsFold(v))
|
||||
}
|
||||
switch v := r.URL.Query().Get("isActive"); v {
|
||||
case "true":
|
||||
q = q.Where(material.IsActiveEQ(true))
|
||||
case "false":
|
||||
q = q.Where(material.IsActiveEQ(false))
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// listProductTypesInternalHandler GET /api/internal/product-types
|
||||
// 参数:keyword(编码/名称/类别模糊,大小写折叠)、isActive(true/false)、page、pageSize
|
||||
// 参数:code/name/category(各自独立模糊,大小写折叠)、isActive(true/false)、page、pageSize
|
||||
// 无 page 参数时返回全量数组(兼容下拉场景);带 page 时返回 {total,list,page,pageSize}
|
||||
func listProductTypesInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
keyword := r.URL.Query().Get("keyword")
|
||||
isActive := r.URL.Query().Get("isActive")
|
||||
page := atoi(r.URL.Query().Get("page"), 0)
|
||||
pageSize := atoi(r.URL.Query().Get("pageSize"), 20)
|
||||
|
||||
q := productTypeQuery(ctx)
|
||||
if keyword != "" {
|
||||
q = q.Where(material.Or(
|
||||
material.CodeContainsFold(keyword),
|
||||
material.NameContainsFold(keyword),
|
||||
material.DescriptionContainsFold(keyword),
|
||||
))
|
||||
}
|
||||
if isActive == "true" {
|
||||
q = q.Where(material.IsActiveEQ(true))
|
||||
} else if isActive == "false" {
|
||||
q = q.Where(material.IsActiveEQ(false))
|
||||
}
|
||||
q := applyProductTypeFilters(productTypeQuery(ctx), r)
|
||||
|
||||
if page <= 0 {
|
||||
// 下拉/兼容模式:全量(基础档案量级小),id asc 新建在底部
|
||||
@@ -249,8 +257,6 @@ func deleteProductTypeInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc
|
||||
// 全量导出(按筛选),执行统一导出时间硬规则
|
||||
func exportProductTypesInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
keyword := r.URL.Query().Get("keyword")
|
||||
isActive := r.URL.Query().Get("isActive")
|
||||
startDate, endDate, rerr := normalizeExportRange(r.URL.Query().Get("startDate"), r.URL.Query().Get("endDate"))
|
||||
if rerr != nil {
|
||||
fail(w, http.StatusBadRequest, rerr.Error())
|
||||
@@ -258,20 +264,8 @@ func exportProductTypesInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc
|
||||
}
|
||||
startUnix, endUnix := exportRangeUnix(startDate, endDate)
|
||||
|
||||
q := productTypeQuery(ctx)
|
||||
q := applyProductTypeFilters(productTypeQuery(ctx), r)
|
||||
q = q.Where(material.CreatedAtGTE(startUnix), material.CreatedAtLTE(endUnix))
|
||||
if keyword != "" {
|
||||
q = q.Where(material.Or(
|
||||
material.CodeContainsFold(keyword),
|
||||
material.NameContainsFold(keyword),
|
||||
material.DescriptionContainsFold(keyword),
|
||||
))
|
||||
}
|
||||
if isActive == "true" {
|
||||
q = q.Where(material.IsActiveEQ(true))
|
||||
} else if isActive == "false" {
|
||||
q = q.Where(material.IsActiveEQ(false))
|
||||
}
|
||||
list, err := q.Order(ent.Asc("id")).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
|
||||
@@ -98,7 +98,6 @@ func listZonesHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
status := r.URL.Query().Get("status")
|
||||
startDate := r.URL.Query().Get("startDate")
|
||||
endDate := r.URL.Query().Get("endDate")
|
||||
keyword := r.URL.Query().Get("keyword") // 兼容旧版,作为区域编码/名称兜底
|
||||
|
||||
q := ctx.EntClient.Zone.Query()
|
||||
if zoneCode != "" {
|
||||
@@ -111,14 +110,6 @@ func listZonesHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
if status != "" {
|
||||
q = q.Where(zone.StatusEQ(status))
|
||||
}
|
||||
if keyword != "" {
|
||||
q = q.Where(
|
||||
zone.Or(
|
||||
zone.ZoneCodeContainsFold(keyword),
|
||||
zone.ZoneNameContainsFold(keyword),
|
||||
),
|
||||
)
|
||||
}
|
||||
if startDate != "" {
|
||||
if t, err := time.ParseInLocation("2006-01-02", startDate, time.Local); err == nil {
|
||||
q = q.Where(zone.CreatedAtGTE(t.Unix()))
|
||||
|
||||
Reference in New Issue
Block a user