diff --git a/bj_power_mes/bj_power_mes.go b/bj_power_mes/bj_power_mes.go index f448d74..2faa0b5 100644 --- a/bj_power_mes/bj_power_mes.go +++ b/bj_power_mes/bj_power_mes.go @@ -10,8 +10,11 @@ import ( "io/fs" "net/http" "os" + "os/exec" "path" + "runtime" "strings" + "time" "bj_power_mes/common/logx" "bj_power_mes/internal/config" @@ -150,6 +153,22 @@ func newStaticHandler(fsys embed.FS) http.Handler { }) } +// openBrowser 用系统默认浏览器打开 url(Windows/macOS/Linux) +func openBrowser(url string) { + var cmd *exec.Cmd + switch runtime.GOOS { + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + case "darwin": + cmd = exec.Command("open", url) + default: + cmd = exec.Command("xdg-open", url) + } + if err := cmd.Start(); err != nil { + gologx.Errorf("自动打开浏览器失败(%s): %v", url, err) + } +} + func main() { flag.Parse() @@ -221,5 +240,11 @@ func main() { fmt.Printf("Starting bj_power_mes server at %s:%d...\n", c.Host, c.Port) gologx.Infof("Starting bj_power_mes server at %s:%d...", c.Host, c.Port) + // 启动后自动打开管理后台(等 1 秒让端口就绪;Host=0.0.0.0 时用 127.0.0.1 访问) + go func() { + time.Sleep(time.Second) + openBrowser(fmt.Sprintf("http://127.0.0.1:%d", c.Port)) + }() + server.Start() } diff --git a/bj_power_mes/internal/db/db.go b/bj_power_mes/internal/db/db.go index c094ce1..f40dd69 100644 --- a/bj_power_mes/internal/db/db.go +++ b/bj_power_mes/internal/db/db.go @@ -110,8 +110,8 @@ var schemaPatchSQL = []string{ `ALTER TABLE inspection_record ADD COLUMN IF NOT EXISTS attachment_ids varchar(512) NOT NULL DEFAULT ''`, `ALTER TABLE inspection_record ADD COLUMN IF NOT EXISTS disposal_type varchar(20) NOT NULL DEFAULT ''`, `ALTER TABLE inspection_record ADD COLUMN IF NOT EXISTS disposal_remark varchar(500) NOT NULL DEFAULT ''`, - // work_order_bom 表名注解为 work_order_bom - `ALTER TABLE work_order_bom ADD COLUMN IF NOT EXISTS related_standard varchar(255) NOT NULL DEFAULT ''`, + // work_order_bom 表名注解为 work_order_bom(表已于 2026-09-23 退役删除,此补丁仅对历史库存留档库有意义) + `ALTER TABLE IF EXISTS work_order_bom ADD COLUMN IF NOT EXISTS related_standard varchar(255) NOT NULL DEFAULT ''`, `ALTER TABLE process_step ADD COLUMN IF NOT EXISTS attachment jsonb NOT NULL DEFAULT '[]'::jsonb`, `ALTER TABLE work_order ADD COLUMN IF NOT EXISTS created_by varchar(64) NOT NULL DEFAULT ''`, `ALTER TABLE work_order ADD COLUMN IF NOT EXISTS due_date timestamp`, @@ -194,7 +194,6 @@ var schemaPatchSQL = []string{ `ALTER TABLE workpiece_process DROP COLUMN IF EXISTS process_code`, `ALTER TABLE workpiece_bind DROP COLUMN IF EXISTS process_code`, `ALTER TABLE material_request DROP COLUMN IF EXISTS process_code`, - `ALTER TABLE work_order_bom DROP COLUMN IF EXISTS process_code`, `ALTER TABLE workpiece DROP COLUMN IF EXISTS current_process`, // 既有库补列(新装库由 ent AutoMigrate 直接建出,此处幂等兜底) `ALTER TABLE work_order ADD COLUMN IF NOT EXISTS flow_combination jsonb`, @@ -206,6 +205,9 @@ var schemaPatchSQL = []string{ // ---- 半成品流转新模型(2026-09-22 定稿):进度唯一权威源=workpiece.currentStationNo ---- // semi_flow 补进度快照列 completed_text("已完成至工位N");旧 done_process_codes 保留历史数据、不再写入 `ALTER TABLE semi_flow ADD COLUMN IF NOT EXISTS completed_text varchar(64) NOT NULL DEFAULT ''`, + + // ---- 工位终端可见页签配置(2026-09-23):按工位勾选终端显示哪些页签(jsonb 数组),空=全部显示 ---- + `ALTER TABLE station ADD COLUMN IF NOT EXISTS visible_tabs jsonb`, } // EnsureSchema ent 建表 + 幂等列补丁(只增,不删数据) diff --git a/bj_power_mes/internal/handler/production/material.go b/bj_power_mes/internal/handler/production/material.go index b10c1d2..7147299 100644 --- a/bj_power_mes/internal/handler/production/material.go +++ b/bj_power_mes/internal/handler/production/material.go @@ -1,4 +1,4 @@ -package production +package production import ( "net/http" diff --git a/bj_power_mes/internal/wmsclient/client.go b/bj_power_mes/internal/wmsclient/client.go index 626403c..9dd7aa1 100644 --- a/bj_power_mes/internal/wmsclient/client.go +++ b/bj_power_mes/internal/wmsclient/client.go @@ -36,19 +36,18 @@ func New(baseURL, token string) *Client { return c } -// SetBaseURL 热切换 WMS 地址;空串回退默认地址 +// SetBaseURL 热切换 WMS 地址;空串回退默认地址。 +// 注意:所有请求路径都以 "/api/..." 开头,这里必须去掉尾斜杠,否则拼出 "//api" 404。 func (c *Client) SetBaseURL(u string) { u = strings.TrimSpace(u) if u == "" { u = defaultBaseURL } - if !strings.HasSuffix(u, "/") { - u += "/" - } + u = strings.TrimRight(u, "/") c.baseURL.Store(u) } -// BaseURL 当前生效的 WMS 地址(带尾斜杠) +// BaseURL 当前生效的 WMS 地址(不带尾斜杠,请求路径以 /api/... 开头直接拼接) func (c *Client) BaseURL() string { return c.baseURL.Load().(string) } diff --git a/bj_power_wms/e2e_perm_tree.py b/bj_power_wms/e2e_perm_tree.py deleted file mode 100644 index 2fcc627..0000000 --- a/bj_power_wms/e2e_perm_tree.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -"""验证:权限树形化(parent_code) - seed 回填 + /permissions 返回父子结构""" -import json, urllib.request, urllib.error - -BASE = "http://127.0.0.1:8902" -TOKEN = None - -def req(method, path, payload=None): - url = BASE + path - data = json.dumps(payload).encode() if payload is not None else None - r = urllib.request.Request(url, data=data, method=method) - r.add_header("Content-Type", "application/json") - if TOKEN: - r.add_header("Authorization", "Bearer " + TOKEN) - try: - with urllib.request.urlopen(r, timeout=20) as resp: - return json.loads(resp.read().decode()) - except urllib.error.HTTPError as e: - return {"_http": e.code, "_body": e.read().decode()[:300]} - -def main(): - global TOKEN - d = req("POST", "/api/auth/login", {"username": "admin", "password": "123456"}) - TOKEN = (d.get("data") or {}).get("token") - assert TOKEN, f"登录失败: {d}" - print("✓ 登录成功") - - # 执行 seed 回填 parent_code - sd = req("POST", "/api/rbac/seed") - print("✓ seed 完成:", sd.get("data") or sd) - - # 拉取权限,校验 parent_code - pl = req("GET", "/api/permissions") - listp = (pl.get("data") or {}).get("list") or [] - menus = [p for p in listp if p["type"] == "MENU"] - buttons = [p for p in listp if p["type"] == "BUTTON"] - print(f"权限总数 {len(listp)}:菜单 {len(menus)},按钮 {len(buttons)}") - - # 校验每个按钮的 parent_code 是否为有效菜单 code 或为空(全局) - menu_codes = {m["code"] for m in menus} - bad = [p for p in buttons if p.get("parentCode") and p["parentCode"] not in menu_codes] - global_btns = [p for p in buttons if not p.get("parentCode")] - print(f" 按钮中全局(无父)数: {len(global_btns)} -> {[p['code'] for p in global_btns]}") - print(f" 按钮 parent_code 有误(指向不存在菜单): {[p['code'] for p in bad]}") - assert not bad, "存在 parent_code 指向无效菜单" - print("✓ 所有按钮 parent_code 均有效(或为全局空)") - - # 展示各菜单下挂的按钮(模拟前端树) - for m in menus: - kids = [p["name"] for p in buttons if p.get("parentCode") == m["code"]] - print(f" [{m['name']}] 挂按钮: {kids if kids else '(无)'}") - - passed = not bad and len(global_btns) >= 1 - print("\n==== 验证 %s ====" % ("通过" if passed else "失败")) - return passed - -if __name__ == "__main__": - raise SystemExit(0 if main() else 1) \ No newline at end of file diff --git a/bj_power_wms/e2e_scope_dist.py b/bj_power_wms/e2e_scope_dist.py deleted file mode 100644 index 9304d52..0000000 --- a/bj_power_wms/e2e_scope_dist.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -"""端到端验证:问题四(备料台账无单选全部) + 问题六(盘点范围 + 物料分布)""" -import json, urllib.request, urllib.parse, urllib.error - -BASE = "http://127.0.0.1:8902" -TOKEN = None - -def req(method, path, payload=None, params=None): - url = BASE + path - if params: - qs = urllib.parse.urlencode({k: v for k, v in params.items() if v not in (None, "")}) - if qs: - url += "?" + qs - data = json.dumps(payload).encode() if payload is not None else None - r = urllib.request.Request(url, data=data, method=method) - r.add_header("Content-Type", "application/json") - if TOKEN: - r.add_header("Authorization", "Bearer " + TOKEN) - try: - with urllib.request.urlopen(r, timeout=20) as resp: - return json.loads(resp.read().decode()) - except urllib.error.HTTPError as e: - return {"_http": e.code, "_body": e.read().decode()[:300]} - -def login(): - global TOKEN - d = req("POST", "/api/auth/login", {"username": "admin", "password": "123456"}) - TOKEN = d.get("data", {}).get("token") - assert TOKEN, f"登录失败: {d}" - print("✓ 登录成功 admin") - -passed = [] -def check(name, cond, extra=""): - passed.append(bool(cond)) - print(("✓ " if cond else "✗ ") + name + (f" {extra}" if extra and not cond else "")) - -def main(): - login() - # ---------- 问题四:备料台账无单号查全部 ---------- - d = req("GET", "/api/ledger/query", params={"page": 1, "pageSize": 20}) - t4 = d.get("data", {}) or d - check("ledger/query 无 orderNo 返回 total+list", "list" in t4 and "total" in t4, - f"got {list(t4.keys())}") - - # ---------- 问题六:盘点范围按物料过滤 ---------- - # 先看库存有哪些物料可盘点,选一个真实物料 - st = req("GET", "/api/stock/query", params={"page": 1, "pageSize": 10}) - stock_data = st.get("data", {}) or st - rows = stock_data.get("list") or [] - if rows: - mat = rows[0]["materialCode"] - # 按该物料发起盘点 - d = req("POST", "/api/stocktake/start", {"operator": "e2e", "materialCodes": [mat]}) - data = d.get("data", {}) or d - check("startStocktake 按物料过滤成功", bool(data.get("stocktakeNo")), - f"got {data}") - check("按物料盘点 totalTargets>0", data.get("totalTargets", 0) > 0, - f"totalTargets={data.get('totalTargets')}") - # 按类型过滤发起 - d2 = req("POST", "/api/stocktake/start", {"operator": "e2e", "manageMode": 1}) - data2 = d2.get("data", {}) or d2 - check("startStocktake 按类型(结构件)成功", bool(data2.get("stocktakeNo")), - f"got {data2}") - else: - check("stock/query 有数据(需先造数才能验盘点范围)", False, "库存为空") - - # ---------- 问题六:物料分布数据源(聚合含 zoneCode) ---------- - d = req("GET", "/api/stock/query", params={"page": 1, "pageSize": 50}) - dist = d.get("data", {}) or d - dl = dist.get("list") or [] - check("stock/query 返回 list(分布图数据源)", len(dl) > 0, f"len={len(dl)}") - if dl: - r0 = dl[0] - check("聚合行含 zoneCode/materialCode/totalQty", - all(k in r0 for k in ("zoneCode", "materialCode", "totalQty")), - f"keys={list(r0.keys())}") - - print("\n==== 结果:%d/%d 通过 ====" % (sum(passed), len(passed))) - return all(passed) - -if __name__ == "__main__": - ok = main() - raise SystemExit(0 if ok else 1) \ No newline at end of file diff --git a/bj_power_workstation/etc/bj_power_workstation.yaml b/bj_power_workstation/etc/bj_power_workstation.yaml index 60fa5e9..09418f5 100644 --- a/bj_power_workstation/etc/bj_power_workstation.yaml +++ b/bj_power_workstation/etc/bj_power_workstation.yaml @@ -24,7 +24,7 @@ Internal: # 工位配置:StationNo 固定本终端工位号(仅允许运维改配置后重启生效,工人无法在前端修改)。 # 应拧颗数不再由工位固定配置,而是取自工件工艺流程中「拧紧工艺步骤」的数量。 Station: - StationNo: 0 + StationNo: 1 # 工艺文件 PDF 本地预缓存目录(留空关闭缓存) PdfCache: diff --git a/bj_power_workstation/main.go b/bj_power_workstation/main.go index 0a978d3..cc931b6 100644 --- a/bj_power_workstation/main.go +++ b/bj_power_workstation/main.go @@ -6,6 +6,9 @@ import ( "flag" "fmt" "io/fs" + "os/exec" + "runtime" + "time" "bj_power_workstation/internal/config" "bj_power_workstation/internal/handler" @@ -25,6 +28,22 @@ var webFS embed.FS var configFile = flag.String("f", "etc/bj_power_workstation.yaml", "the config file") +// openBrowser 用系统默认浏览器打开 url(Windows/macOS/Linux) +func openBrowser(url string) { + var cmd *exec.Cmd + switch runtime.GOOS { + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + case "darwin": + cmd = exec.Command("open", url) + default: + cmd = exec.Command("xdg-open", url) + } + if err := cmd.Start(); err != nil { + logx.Errorf("自动打开浏览器失败(%s): %v", url, err) + } +} + func main() { flag.Parse() @@ -85,5 +104,12 @@ func main() { fmt.Printf("Starting bj_power_workstation server at %s:%d...\n", c.Host, c.Port) logx.Infof("Starting bj_power_workstation server at %s:%d...", c.Host, c.Port) + + // 启动后自动打开终端页面(等 1 秒让端口就绪;Host=0.0.0.0 时用 127.0.0.1 访问) + go func() { + time.Sleep(time.Second) + openBrowser(fmt.Sprintf("http://127.0.0.1:%d", c.Port)) + }() + server.Start() -} \ No newline at end of file +} diff --git a/临时.md b/临时.md deleted file mode 100644 index b793ccc..0000000 --- a/临时.md +++ /dev/null @@ -1,69 +0,0 @@ -# 物料档案对齐 — 审核结果与方案(2026-09-21) - -## 一、结论(先说) - -「物料档案 类型下拉缺『其他』」**在 WMS 端没有丢、没有回退**——WMS 的品类/类型下拉一直都含「其他(辅料/工装/试验设备)」。 -真正缺「其他」的是 **MES 物料档案**:MES 还停在「成品子集 /product-types」接口,根本没有全品类字段(品类/类型/安全库存/说明/附件全没有),所以 MES 端理所当然看不到「其他」。 - -MES 切到全品类 `/materials` 的链路**只做了一半就被中断**(WMS 侧只写了 delete handler、还没来得及注册路由)。这就是全部未完成工作量。 - -## 二、已落地且正确(无需再改,已核对源码) - -1. WMS 物料档案品类/类型下拉含「其他」(item_type=4) —— BaseData.vue: - - 搜索区品类下拉 `L882`:`` - - 新增表单品类下拉 `L966`:`` - - 结论:**未回退,仍存在**。 -2. 出库记录「检测单号」列 + 搜索 —— Outbound.vue 已落地(表单 L501、列表列 L619/L726、搜索 L353/L662)。 -3. 来料检验抽屉分页(每页20) + 批量送检上限100 —— Inspection.vue 已落地(drawer L832、pendingPage.size=20、上限100 L312)。 -4. 单号格式 `IB/OB + YYYYMMDD + -NNN` + 手填/自动生成 + 唯一校验 —— inbound.go(nextDailyNo L28 / validateUniqueNo L95)、outbound.go、excel.go 均已接入。 - -## 三、未完成清单 + 最优方案(按执行顺序) - -### A. WMS 侧(2 处) - -1. **注册删除路由**:routes.go 的 `/api/internal/materials`(L244-246)目前只有 GET/POST/PUT,补一行: - `POST /api/internal/materials/delete` → `deleteMaterialHandler`(handler 已在 material.go L179 写好,只差注册)。 -2. **加附件内部代理**:新增 `/api/internal/attachments/upload | list | delete`(X-API-TOKEN 版),供 MES 代理调用,保证 MES/WMS 附件单一来源一致。 - -### B. MES 后端(4 处) - -3. wmsclient 补 `MaterialDelete` + 附件方法(upload/list/delete)。 -4. logic 新建 materials 代理方法(page/get/post/put/delete,纯代理 WMS;WMS 宕机时读写均报错并提示,无本地缓存)。 -5. handler 加 materials handler(对应 6 个接口)。 -6. routes_production.go 注册 `/materials/*`;perm.go 加对应权限码。 - -### C. MES 前端(1 处) - -7. ProductType.vue:接口 `/product-types` → `/materials`;字段对齐 WMS(**图号/名称/规格/单位/品类/类型/安全库存/说明/附件**);「编码」label 改「图号」;品类下拉含「其他」。 - -### D. 验证 - -8. WMS + MES 后端编译通过,前端 build 通过。 - -## 四、关键决策(已定,不再来回问) - -- MES「物料档案」菜单 = WMS 物料**全品类**代理视图;品类/类型含「其他」,与 WMS 完全一致。 -- MES 原 `/product-types`(成品子集)= **纯 WMS 代理**(方案 A,已落地代码,见第五节):不再保留本地 `product_type` 缓存/双写;WMS 不可达即报错,不再降级读本地。**继续供工单/BOM 联查下拉使用,零影响**。 -- MES **不建**独立物料表(无本地缓存);附件走 WMS 单一来源,保证两边一致、不分裂。 -- 物理删除 `product_type` 表属 ent 代码生成级改动,受「不编译」铁律约束,未在本轮执行,留给 `go generate ./ent` + `go build` 步骤(见第五节 D)。 - -## 五、/product-types 落地进展(2026-09-21 续 · 方案 A) - -### 决策背景 -用户确认:**没有「WMS 宕机时 MES 还必须看物料」的硬需求**,按默认建议选方案 A(纯代理,单一真源 = WMS)。 -经代码核查,本地 `product_type` 表除「成品下拉降级兜底」外,还被**流程卡**、**排产支撑**两处生产代码读取(全代码零写入点,本就是空/陈旧残留)。 -审计结论:双写/触发同步无法保证最终一致;纯代理可数学上保证一致。 - -### 已落地(代码,未编译验证) -1. `internal/logic/product.go`:删除 `localProductTypes` 降级函数 + 三处兜底分支;`ListProductTypes`/`ListProductTypesPage` 改为纯 WMS 代理(WMS 宕→报错,不再喂空/旧数据);新增 `ProductTypeByID`/`ProductTypeByCode`(读 WMS 全量按 id/code 定位);删除 `ent`、`ent/producttype` 两个不再使用的 import。 -2. `internal/handler/processcard_model.go`(流程卡):`buildModelCard` 原读本地 `client.ProductType.Get/Query` → 改为 `logic.New(svcCtx).ProductTypeByID/ByCode`(WMS)。**顺带修复**:此前本地表为空,流程卡产品名实际取不到,现改读 WMS 可正常显示。 -3. `internal/logic/producible.go`(排产支撑):产品名称映射、`dropInactiveProducts` 停用过滤,原读本地 `EntClient.ProductType` → 改为 `s.ctx.Wms.ProductTypes`(全量 / `isActive=false` 取停用)。**顺带修复**:此前本地表为空,停用成品不会被剔除,现改读 WMS 可正常生效。 -4. 应用层(logic/handler)已无任何 `EntClient.ProductType` / `localProductTypes` 引用;WMS 端 `/api/internal/product-types?isActive=false` 已支持只返回停用成品,链路自洽。 - -### 物理删表(2026-09-21 已执行完成 · 用户授权「你干吧」) -- 执行链:删 `schema/product_type.go` → `go run ./tools/generate.go` 重生 ent(自动剔除 ProductType 生成代码)→ `go build ./...` 通过(BUILD_EXIT=0)→ 在 `db.go` 的 `schemaPatchSQL` 追加 `DROP TABLE IF EXISTS product_type` 并移除旧的 product_type ALTER 补丁 → `go run . migrate`(密码门禁通过)真实删除存量表。 -- psql 核验:`information_schema` 中 `product_type` 表 count=0(已不存在)。 -- 应用层三处纯代理改造(product.go / processcard_model.go / producible.go)经编译验证自洽。 -- 前端 `ProductType.vue` 未改:`wmsOnline` 字段仍由后端返回(纯代理后恒为 `true`,预警横幅不会误触发),功能正常。 - -> 说明:本任务(方案 A 物理删表)已完成;但「MES 物料档案看到『其他』全品类」的原始目标仍依赖 临时.md 第三节的「全品类 /materials 代理视图」(8 处,WMS 2 + MES 后端 4 + MES 前端 1 + 验证 1),该部分一行未动,见下方「还有什么没干」。 \ No newline at end of file