From 6d8bb756968a08f21860824a415eacad9ac43c95 Mon Sep 17 00:00:00 2001 From: SunYF <123@hard_man.com> Date: Tue, 8 Sep 2026 13:43:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor(wms/mes):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=A4=9A=E5=9C=BA=E6=99=AF=E6=9F=A5=E8=AF=A2=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=85=B3=E9=94=AE=E5=AD=97=E6=B7=B7?= =?UTF-8?q?=E6=90=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 移除所有多字段关键字混搜,改为各筛选字段独立查询 2. 调整excel导出相关逻辑,优化文件名处理与导入模板 3. 修复物料导入的类型解析与token获取逻辑 4. 更新依赖与前端页面文案、筛选参数 --- .../frontend/src/pages/ProductType.vue | 13 +++-- bj_power_mes/go.mod | 2 +- .../internal/handler/production/workorder.go | 4 +- bj_power_mes/internal/logic/product.go | 33 ++++++----- bj_power_mes/internal/wmsclient/client.go | 38 ++++++++----- bj_power_mes/public/index.html | 2 +- bj_power_wms/frontend/src/help.js | 6 +- bj_power_wms/frontend/src/pages/BaseData.vue | 15 +++-- bj_power_wms/frontend/src/pages/Inbound.vue | 12 ++-- bj_power_wms/frontend/src/utils/export.js | 9 ++- bj_power_wms/internal/handler/excel.go | 5 +- bj_power_wms/internal/handler/inbound.go | 10 +--- bj_power_wms/internal/handler/material.go | 39 +++++++++---- bj_power_wms/internal/handler/producttype.go | 56 +++++++++---------- bj_power_wms/internal/handler/zone.go | 9 --- bj_power_wms/web/static/index.html | 2 +- 16 files changed, 141 insertions(+), 114 deletions(-) diff --git a/bj_power_mes/frontend/src/pages/ProductType.vue b/bj_power_mes/frontend/src/pages/ProductType.vue index c4165de..2b1de31 100644 --- a/bj_power_mes/frontend/src/pages/ProductType.vue +++ b/bj_power_mes/frontend/src/pages/ProductType.vue @@ -95,16 +95,16 @@ function defaultRange() { } Object.assign(filters, defaultRange()) -function keyword() { - // 编码/名称/类别合并为 keyword(后端大小写折叠模糊匹配) - return filters.code || filters.name || filters.category || '' -} +// 2026-09-08 禁止关键字混搜:编码/名称/类别各筛选框独立传参,后端各自模糊匹配 +function trim(s) { return (s || '').trim() } async function load() { const params = { page: page.value, pageSize: pageSize.value, - keyword: keyword(), + code: trim(filters.code), + name: trim(filters.name), + category: trim(filters.category), isActive: filters.isActive, startDate: filters.startDate, endDate: filters.endDate @@ -141,7 +141,8 @@ async function exportXlsx() { filters.endDate = r.end } const params = { - keyword: keyword(), isActive: filters.isActive, + code: trim(filters.code), name: trim(filters.name), category: trim(filters.category), + isActive: filters.isActive, startDate: filters.startDate, endDate: filters.endDate } const blob = await request.get('/product-types/export', { params, responseType: 'blob' }) diff --git a/bj_power_mes/go.mod b/bj_power_mes/go.mod index b78debb..53644b4 100644 --- a/bj_power_mes/go.mod +++ b/bj_power_mes/go.mod @@ -7,6 +7,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.2 github.com/jackc/pgx/v5 v5.7.4 github.com/redis/go-redis/v9 v9.8.0 + github.com/xuri/excelize/v2 v2.8.1 github.com/zeromicro/go-zero v1.8.3 go.opentelemetry.io/otel/trace v1.24.0 golang.org/x/crypto v0.53.0 @@ -52,7 +53,6 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect - github.com/xuri/excelize/v2 v2.8.1 // indirect github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect github.com/zclconf/go-cty v1.14.4 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect diff --git a/bj_power_mes/internal/handler/production/workorder.go b/bj_power_mes/internal/handler/production/workorder.go index dc9160f..f61f9ea 100644 --- a/bj_power_mes/internal/handler/production/workorder.go +++ b/bj_power_mes/internal/handler/production/workorder.go @@ -40,7 +40,7 @@ func ListProductTypesPageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { page := atoiDefault(q.Get("page"), 1) pageSize := atoiDefault(q.Get("pageSize"), 20) total, list, wmsOnline, err := logic.New(svcCtx).ListProductTypesPage(r.Context(), - q.Get("keyword"), q.Get("isActive"), page, pageSize) + q.Get("code"), q.Get("name"), q.Get("category"), q.Get("isActive"), page, pageSize) if err != nil { httpx.Fail(w, 3001, err.Error()) return @@ -96,7 +96,7 @@ func ExportProductTypesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() b, err := svcCtx.Wms.ExportProductTypes(r.Context(), - q.Get("keyword"), q.Get("isActive"), q.Get("startDate"), q.Get("endDate")) + q.Get("code"), q.Get("name"), q.Get("category"), q.Get("isActive"), q.Get("startDate"), q.Get("endDate")) if err != nil { httpx.Fail(w, 3002, "导出失败(WMS 不可达):"+err.Error()) return diff --git a/bj_power_mes/internal/logic/product.go b/bj_power_mes/internal/logic/product.go index 544c84a..1f983f2 100644 --- a/bj_power_mes/internal/logic/product.go +++ b/bj_power_mes/internal/logic/product.go @@ -71,24 +71,25 @@ func (s *Service) DeleteProductType(ctx context.Context, id int) error { // ListProductTypes 全量列表(下拉/兼容形态,id asc 新建在底部)。 // degraded=false 表示 WMS 不可达、数据来自本地只读缓存。 func (s *Service) ListProductTypes(ctx context.Context) ([]wmsclient.ProductTypeRow, bool, error) { - list, err := s.ctx.Wms.ProductTypes(ctx, "", "") + list, err := s.ctx.Wms.ProductTypes(ctx, "") if err == nil { return list, true, nil } - fallback, ferr := s.localProductTypes(ctx, "") + fallback, ferr := s.localProductTypes(ctx, "", "", "") if ferr != nil { return nil, false, errors.New("WMS 不可达且本地缓存读取失败") } return fallback, false, nil } -// ListProductTypesPage 分页+搜索(管理页真分页) -func (s *Service) ListProductTypesPage(ctx context.Context, keyword, isActive string, page, pageSize int) (int64, []wmsclient.ProductTypeRow, bool, error) { - total, list, err := s.ctx.Wms.ProductTypePage(ctx, keyword, isActive, page, pageSize) +// ListProductTypesPage 分页+搜索(管理页真分页)。 +// 2026-09-08 禁止关键字混搜:编码/名称/类别各自独立模糊。 +func (s *Service) ListProductTypesPage(ctx context.Context, code, name, category, isActive string, page, pageSize int) (int64, []wmsclient.ProductTypeRow, bool, error) { + total, list, err := s.ctx.Wms.ProductTypePage(ctx, code, name, category, isActive, page, pageSize) if err == nil { return total, list, true, nil } - fallback, ferr := s.localProductTypes(ctx, keyword) + fallback, ferr := s.localProductTypes(ctx, code, name, category) if ferr != nil { return 0, nil, false, errors.New("WMS 不可达且本地缓存读取失败") } @@ -104,7 +105,7 @@ func (s *Service) ListProductTypesPage(ctx context.Context, keyword, isActive st } func (s *Service) productTypeByID(ctx context.Context, id int) (*wmsclient.ProductTypeRow, error) { - list, err := s.ctx.Wms.ProductTypes(ctx, "", "") + list, err := s.ctx.Wms.ProductTypes(ctx, "") if err == nil { for i := range list { if list[i].ID == id { @@ -121,18 +122,24 @@ func (s *Service) productTypeByID(ctx context.Context, id int) (*wmsclient.Produ return &wmsclient.ProductTypeRow{ID: pt.ID, Code: pt.Code, Name: pt.Name}, nil } -func (s *Service) localProductTypes(ctx context.Context, keyword string) ([]wmsclient.ProductTypeRow, error) { +// localProductTypes 本地只读缓存过滤(降级用):编码/名称/类别各自独立模糊匹配 +func (s *Service) localProductTypes(ctx context.Context, code, name, category string) ([]wmsclient.ProductTypeRow, error) { list, err := s.ctx.EntClient.ProductType.Query().Order(ent.Asc(producttype.FieldID)).All(ctx) if err != nil { return nil, err } + codeKw := strings.ToLower(strings.TrimSpace(code)) + nameKw := strings.ToLower(strings.TrimSpace(name)) + catKw := strings.ToLower(strings.TrimSpace(category)) rows := make([]wmsclient.ProductTypeRow, 0, len(list)) - kw := strings.ToLower(strings.TrimSpace(keyword)) for _, p := range list { - if kw != "" && - !strings.Contains(strings.ToLower(p.Code), kw) && - !strings.Contains(strings.ToLower(p.Name), kw) && - !strings.Contains(strings.ToLower(p.Category), kw) { + if codeKw != "" && !strings.Contains(strings.ToLower(p.Code), codeKw) { + continue + } + if nameKw != "" && !strings.Contains(strings.ToLower(p.Name), nameKw) { + continue + } + if catKw != "" && !strings.Contains(strings.ToLower(p.Category), catKw) { continue } rows = append(rows, wmsclient.ProductTypeRow{ diff --git a/bj_power_mes/internal/wmsclient/client.go b/bj_power_mes/internal/wmsclient/client.go index 425d0ee..784601b 100644 --- a/bj_power_mes/internal/wmsclient/client.go +++ b/bj_power_mes/internal/wmsclient/client.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "net/http" + "net/url" "time" ) @@ -151,12 +152,9 @@ func (c *Client) putJSON(ctx context.Context, path string, body any) error { return nil } -// ProductTypes 全量产品型号(item_type=3,id asc,新建在底部)。keyword/isActive 可空。 -func (c *Client) ProductTypes(ctx context.Context, keyword, isActive string) ([]ProductTypeRow, error) { +// ProductTypes 全量产品型号(item_type=3,id asc,新建在底部)。isActive 可空。 +func (c *Client) ProductTypes(ctx context.Context, isActive string) ([]ProductTypeRow, error) { q := "/api/internal/product-types?" - if keyword != "" { - q += "keyword=" + keyword + "&" - } if isActive != "" { q += "isActive=" + isActive } @@ -167,11 +165,18 @@ func (c *Client) ProductTypes(ctx context.Context, keyword, isActive string) ([] return list, nil } -// ProductTypePage 分页+搜索(产品型号管理页真分页) -func (c *Client) ProductTypePage(ctx context.Context, keyword, isActive string, page, pageSize int) (int64, []ProductTypeRow, error) { +// ProductTypePage 分页+搜索(产品型号管理页真分页)。 +// 2026-09-08 禁止关键字混搜:编码/名称/类别各自独立模糊。 +func (c *Client) ProductTypePage(ctx context.Context, code, name, category, isActive string, page, pageSize int) (int64, []ProductTypeRow, error) { q := fmt.Sprintf("/api/internal/product-types?page=%d&pageSize=%d", page, pageSize) - if keyword != "" { - q += "&keyword=" + keyword + if code != "" { + q += "&code=" + url.QueryEscape(code) + } + if name != "" { + q += "&name=" + url.QueryEscape(name) + } + if category != "" { + q += "&category=" + url.QueryEscape(category) } if isActive != "" { q += "&isActive=" + isActive @@ -205,11 +210,18 @@ func (c *Client) DeleteProductType(ctx context.Context, id int) error { return c.post(ctx, "/api/internal/product-types/delete", map[string]any{"id": id}) } -// ExportProductTypes 拉取产品型号导出 xlsx 字节流(WMS 端执行统一导出时间规则) -func (c *Client) ExportProductTypes(ctx context.Context, keyword, isActive, startDate, endDate string) ([]byte, error) { +// ExportProductTypes 拉取产品型号导出 xlsx 字节流(WMS 端执行统一导出时间规则)。 +// 编码/名称/类别各自独立模糊筛选(禁止关键字混搜)。 +func (c *Client) ExportProductTypes(ctx context.Context, code, name, category, isActive, startDate, endDate string) ([]byte, error) { q := "/api/internal/product-types/export?" - if keyword != "" { - q += "keyword=" + keyword + "&" + if code != "" { + q += "code=" + url.QueryEscape(code) + "&" + } + if name != "" { + q += "name=" + url.QueryEscape(name) + "&" + } + if category != "" { + q += "category=" + url.QueryEscape(category) + "&" } if isActive != "" { q += "isActive=" + isActive + "&" diff --git a/bj_power_mes/public/index.html b/bj_power_mes/public/index.html index f679316..f1abf3a 100644 --- a/bj_power_mes/public/index.html +++ b/bj_power_mes/public/index.html @@ -6,7 +6,7 @@ MES 产线控制 - + diff --git a/bj_power_wms/frontend/src/help.js b/bj_power_wms/frontend/src/help.js index ffca763..6851d72 100644 --- a/bj_power_wms/frontend/src/help.js +++ b/bj_power_wms/frontend/src/help.js @@ -32,9 +32,9 @@ export const helpInbound = { }, { title: '入库记录(页签3)', - overview: '分页展示全部入库单,结构件与精密件同表(用"类型"列区分)。支持按入库单号、物料编码、类型、关键字、入库时间筛选;可按当前条件导出 xlsx。点行「明细」在右侧抽屉查看该单的批次号/SN 行(懒加载+分页,避免精密件上万条 SN 撑爆前端)。', + overview: '分页展示全部入库单,结构件与精密件同表(用"类型"列区分)。支持按入库单号、物料编码、类型、物料名称、入库时间筛选(各字段独立,无关键字混搜);可按当前条件导出 xlsx。点行「明细」在右侧抽屉查看该单的批次号/SN 行(懒加载+分页,避免精密件上万条 SN 撑爆前端)。', fields: [ - { name: '查询条件', source: '本页签顶部筛选区。', purpose: '缩小列表范围', fill: '可组合:入库单号(模糊)/物料编码(模糊)/类型(全部·结构件·精密件)/关键字(物料名称或单号)/入库时间区间' }, + { name: '查询条件', source: '本页签顶部筛选区。', purpose: '缩小列表范围', fill: '可组合:入库单号(模糊)/物料编码(模糊)/类型(全部·结构件·精密件)/物料名称(模糊)/入库时间区间' }, { name: '明细', source: '行末「明细」按钮 → 右侧抽屉。', purpose: '查看该入库单的批次号/SN/数量明细', fill: '抽屉内分页展示,不回列表里嵌套子表' }, { name: '导出', source: '按钮。后端 /inbound/export 按当前条件返回全部(不分页)。', purpose: '把筛选结果一次性导出为 xlsx', fill: '点【导出】下载"入库管理.xlsx"' } ] @@ -141,7 +141,7 @@ export const helpLedger = { fields: [ { name: '工单号', source: '筛选条件。数据来自 MES 工单。', purpose: '只看某个工单的领料台账', fill: '输入工单号(可空=查全部工单)' }, { name: '物料编码', source: '筛选条件。', purpose: '按物料编码精确过滤', fill: '输入物料编码(可空)' }, - { name: '物料名称', source: '筛选条件。', purpose: '按物料名称模糊过滤', fill: '输入物料名称关键字(可空)' }, + { name: '物料名称', source: '筛选条件。', purpose: '按物料名称模糊过滤', fill: '输入物料名称(可空)' }, { name: '状态', source: '筛选条件。可选项:待领料 / 部分领料 / 领料完结。', purpose: '按领料进度过滤', fill: '待领料=还没出过库;部分领料=已出库但没领完;领料完结=缺口为 0(前两种均由"进行中"按已出库数实时派生)' }, { name: 'ID', source: '台账记录主键(系统生成,只读)。', purpose: '唯一标识一行台账,便于定位与对接', fill: '系统自动生成,无需填写' }, { name: '需求总量 / 已出库 / 缺口', source: '系统计算。需求总量来自 BOM;已出库随出库自动累加;缺口 = 需求总量 − 已出库。', purpose: '一眼看清某物料还差多少未领', fill: '自动;缺口为 0 即领料完结' }, diff --git a/bj_power_wms/frontend/src/pages/BaseData.vue b/bj_power_wms/frontend/src/pages/BaseData.vue index af263c4..bdf5271 100644 --- a/bj_power_wms/frontend/src/pages/BaseData.vue +++ b/bj_power_wms/frontend/src/pages/BaseData.vue @@ -6,6 +6,7 @@ import request from '../utils/request' import { fmtTime } from '../utils/format' import { can } from '../utils/perm' import { exportXlsxFetch } from '../utils/export' +import { getToken } from '../utils/auth' import PageHelp from '../components/PageHelp.vue' import { helpZone, helpMaterial } from '../help' @@ -237,16 +238,18 @@ async function onMaterialFile(e) { try { const fd = new FormData() fd.append('file', file) - const token = JSON.parse(localStorage.getItem('token') || '""') + // 注意:token 是纯文本存储,不能用 JSON.parse(会对 JWT 抛 SyntaxError 误报"导入异常") const res = await fetch('/api/material/import', { method: 'POST', - headers: { Authorization: 'Bearer ' + token }, + headers: { Authorization: 'Bearer ' + getToken() }, body: fd }) - const body = await res.json().catch(() => ({})) + const body = await res.json().catch(() => null) const d = body?.data || {} if (!res.ok || body?.code !== 0) { - ElMessage.error('导入失败:' + (body?.message || '未知错误')) + // 业务/权限错误:优先展示后端中文 message;解析不出 body 时按 HTTP 状态给中文提示 + const httpText = res.status === 401 ? '登录已过期,请重新登录' : `服务器错误(${res.status})` + ElMessage.error('导入失败:' + (body?.message || httpText)) return } if (d.errors && d.errors.length) { @@ -258,7 +261,7 @@ async function onMaterialFile(e) { ElMessage.success(`导入成功 ${d.success || 0} 条`) await loadMaterials() } catch (err) { - ElMessage.error('导入异常:' + (err?.message || err)) + ElMessage.error('导入失败:网络异常或登录已过期,请刷新页面重试') } finally { matImporting.value = false } @@ -373,7 +376,7 @@ async function onMaterialFile(e) { 新增物料 导入 导出 - + 导入说明 diff --git a/bj_power_wms/frontend/src/pages/Inbound.vue b/bj_power_wms/frontend/src/pages/Inbound.vue index 676f05c..c509026 100644 --- a/bj_power_wms/frontend/src/pages/Inbound.vue +++ b/bj_power_wms/frontend/src/pages/Inbound.vue @@ -168,7 +168,7 @@ async function importExcel(mode, which) { /* ===================== Tab3 入库记录(统一主表 + 抽屉明细,不分页内嵌子表) ===================== */ // 查询条件 -const filters = reactive({ inboundNo: '', materialCode: '', manageMode: '', keyword: '' }) +const filters = reactive({ inboundNo: '', materialCode: '', manageMode: '', materialName: '' }) const dateRange = ref([]) // [开始日期, 结束日期] YYYY-MM-DD const loadingRec = ref(false) const list = ref([]) @@ -184,7 +184,7 @@ async function loadRecords() { inboundNo: filters.inboundNo.trim(), materialCode: filters.materialCode.trim(), manageMode: filters.manageMode, - keyword: filters.keyword.trim(), + materialName: filters.materialName.trim(), startDate: dateRange.value?.[0] || '', endDate: dateRange.value?.[1] || '', page: page.current, @@ -206,7 +206,7 @@ function resetFilters() { filters.inboundNo = '' filters.materialCode = '' filters.manageMode = '' - filters.keyword = '' + filters.materialName = '' dateRange.value = [] search() } @@ -218,7 +218,7 @@ async function exportCsv() { inboundNo: filters.inboundNo.trim(), materialCode: filters.materialCode.trim(), manageMode: filters.manageMode, - keyword: filters.keyword.trim(), + materialName: filters.materialName.trim(), startDate: dateRange.value?.[0] || '', endDate: dateRange.value?.[1] || '' }, '入库管理.xlsx') @@ -410,8 +410,8 @@ onMounted(() => { - - + + ({})) throw new Error(body.message || '导出失败') } - // 从 Content-Disposition 解析文件名 + // 从 Content-Disposition 解析文件名:优先 filename*=UTF-8''(后端放中文原名),兜底 filename(ASCII 名) let filename = fallbackName const cd = res.headers.get('Content-Disposition') || '' - const m = cd.match(/filename="?([^";]+)"?/) + let m = cd.match(/filename\*=UTF-8''([^;]+)/i) if (m) { - filename = decodeURIComponent(m[1]) + filename = decodeURIComponent(m[1]) || filename + } else { + m = cd.match(/filename="?([^";]+)"?/) + if (m) filename = decodeURIComponent(m[1]) } const blob = await res.blob() const url = URL.createObjectURL(blob) diff --git a/bj_power_wms/internal/handler/excel.go b/bj_power_wms/internal/handler/excel.go index fff3078..06e0e10 100644 --- a/bj_power_wms/internal/handler/excel.go +++ b/bj_power_wms/internal/handler/excel.go @@ -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") diff --git a/bj_power_wms/internal/handler/inbound.go b/bj_power_wms/internal/handler/inbound.go index 64ebbcd..d2adf2f 100644 --- a/bj_power_wms/internal/handler/inbound.go +++ b/bj_power_wms/internal/handler/inbound.go @@ -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 { diff --git a/bj_power_wms/internal/handler/material.go b/bj_power_wms/internal/handler/material.go index 81411a0..5d70138 100644 --- a/bj_power_wms/internal/handler/material.go +++ b/bj_power_wms/internal/handler/material.go @@ -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(). diff --git a/bj_power_wms/internal/handler/producttype.go b/bj_power_wms/internal/handler/producttype.go index 8801f71..adb83c9 100644 --- a/bj_power_wms/internal/handler/producttype.go +++ b/bj_power_wms/internal/handler/producttype.go @@ -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()) diff --git a/bj_power_wms/internal/handler/zone.go b/bj_power_wms/internal/handler/zone.go index 6d8f9d4..42cb66a 100644 --- a/bj_power_wms/internal/handler/zone.go +++ b/bj_power_wms/internal/handler/zone.go @@ -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())) diff --git a/bj_power_wms/web/static/index.html b/bj_power_wms/web/static/index.html index 613f5a2..91f3819 100644 --- a/bj_power_wms/web/static/index.html +++ b/bj_power_wms/web/static/index.html @@ -6,7 +6,7 @@ WMS库房管理 - +