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