ProMgr v1 — 面向硬件试制 / 测试环节的全流程管理平台。 用禅道式的项目管理骨架,承载制造业试制的业务实质。
Phase 1 已交付:单二进制启动 + JWT/RBAC + Product/Batch CRUD + D-20 完整可演集种子 + Vue 3 SPA。
| 用户名 | 密码 | 角色 | 权限 |
|---|---|---|---|
admin |
123456 |
admin | 全部 8 项权限 |
tester1 |
123456 |
tester | 业务 CRUD(产品/批次读写 + 详情 + 用户读) |
tester2 |
123456 |
tester | 同 tester1 |
viewer |
123456 |
viewer | 只读 |
生产部署务必改密码! 演示用密码为固定
123456仅用于功能演示。
# Windows (使用 mingw32-make;Linux/macOS 用 make)
mingw32-make build
# 等价:make buildmake build 一条命令串跑完整 release pipeline(D-09):
- web-build:
cd web && npm ci && npm run build(首次约 3-5 分钟) - 嵌入:
//go:embed all:web/dist把前端 SPA 打进二进制 - 交叉编译:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build→bin/server-linux-amd64
产物:
bin/server-linux-amd64— 自包含单二进制(嵌入前端 + SQLite WAL),可部署任何 linux/amd64 机器
本地开发用
make build-dev(占位 web-dist + 本地平台 go build →bin/server),make run走 build-dev(秒级,不走 npm)。 首次make build约 5-10 分钟(npm ci + vite build + go cross-compile);后续增量构建更快。
# 默认端口 8080;冲突可用 PROMGR_PORT 覆盖
PROMGR_PORT=8090 ./bin/server-linux-amd64git clone <repo-url> ProMgr && cd ProMgr
make build # 5-10 分钟(npm ci + vite build + cross-compile)
./bin/server-linux-amd64 # 启动;默认监听 :8080浏览器开 http://localhost:8080 → 用 admin / 123456 登录 → 看到首页 → 进 §演示三走。
启动后访问:
| URL | 内容 |
|---|---|
http://localhost:8080/login |
登录页(演示账号一键登录) |
http://localhost:8080/swagger/index.html |
OpenAPI UI |
http://localhost:8080/api/v1/healthz |
健康检查 |
bash scripts/verify-foundation.sh8 道闸(任一失败 exit 1):
cleanup— 清旧 DB 与构建产物smoke-cgo— 依赖链无 cgo SQLite 驱动go test— 全套单测(含 RBAC 矩阵 + BatchDetail=8 真实聚合)layering— service 子包不 import gin/net/httpswag— 生成 docs/swaggerweb-build— 真实 Vue SPA 构建build— Go embed + 单二进制- e2e curl — Product/Batch CRUD 200/403 矩阵 +
BatchDetail.tasks==8
- 路由顺序(router.go):
/api/v1/*→/swagger/*→/uploads/*→/assets/*→NoRoute - SQLite DSN:
?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(on)&_pragma=synchronous(NORMAL)&_txlock=immediate+MaxOpenConns(8) - Service 禁 import gin/net/http(scripts/check_layering.sh 守卫)
- 嵌入前端:
//go:embed all:web/dist+fs.Sub提供 SPA fallback - 双端 RBAC:前端
v-perm指令过滤 DOM + 后端RequirePerm中间件兜底 - 禁用 GORM 软删除:用显式
status字段表达「停用」(D-03) - DB 级外键:所有关联走
constraint:OnDelete:RESTRICT;Attachment用扁平biz_type+biz_id - API 信封契约:业务错误走 HTTP 200 + envelope
code!=0;401/403/404 走真实 HTTP 状态码 + 同一信封体
POST /api/v1/auth/login— 用户名密码登录,返 JWTGET /api/v1/me— 当前用户信息与权限码
GET /api/v1/products?status=&category=&page=&page_size=— 分页列表POST /api/v1/products— 新建(需product:write)PUT /api/v1/products/:id— 修改(需product:write)POST /api/v1/products/:id/deactivate— 停用(需product:deactivate)
GET /api/v1/batches?product_id=&stage=&status=&page=&page_size=— 列表(多参过滤)POST /api/v1/batches— 新建(自动 code 生成<product_code>-<stage>-<seq:02d>)GET /api/v1/batches/:id/detail— 详情 + 实时 COUNT 聚合{tasks, records, ncrs}(非硬编码 0)
完整接口规范见 /swagger/index.html。
admin / 123456一键登录 → 看产品列表(3 个 seed)- 进入批次 → 看详情页 → 下方 3 张计数卡:工单 8 / 检验 0 / 异常单 0(PROD-04 真实聚合)
- 退出 →
tester / 123456登录 → 侧边菜单没有「停用」按钮(v-permDOM 移除) - 退出 →
viewer / 123456登录 → 没有任何「新建」按钮(只读) - 浏览器 DevTools 复制 admin token,越权直调 API 仍被后端拒绝(双端校验)
| Phase | 名称 | 状态 |
|---|---|---|
| 1 | Foundation(骨架 + 鉴权 + Product/Batch + Vue) | 已交付 |
| 2 | 看板 + 工单拖拽 | 未开始 |
| 3 | 检验 + 动态表单 + USL/LSL 判定 | 未开始 |
| 4 | 物料库 + 供应商 + LTB | 未开始 |
| 5 | NCR + 8D + 鱼骨图 | 未开始 |
| 6 | 报告 + PDF/Excel + 图表 | 未开始 |
| 7 | 报表 + 帕累托 + SPC | 未开始 |
ProMgr/
├── cmd/server/ # 入口(装配顺序:open→migrate→seed→authSvc→router)
├── internal/
│ ├── config/ # env 配置
│ ├── db/ # SQLite 连接 + AutoMigrate + Seed
│ ├── deps/ # 依赖锚(防止 go mod tidy 误丢)
│ ├── handler/ # HTTP 边界(gin-aware)
│ ├── middleware/ # JWT/RequirePerm/Recovery/RequestID
│ ├── model/ # 27 个 GORM 实体
│ └── service/ # 业务用例(actor/auth/batch/product)
├── web/ # Vue 3 + TS + Vite + Element Plus
│ └── src/
│ ├── api/ # axios 客户端
│ ├── directives/ # v-perm
│ ├── router/ # Vue Router + 守卫
│ ├── stores/ # Pinia authStore
│ └── views/ # 视图
├── scripts/
│ ├── verify-foundation.sh # 端到端冒烟(8 道闸)
│ └── check_layering.sh # service/db/model 分层守卫
├── docs/plans/ # 设计文档
├── docs/swagger/ # swag init 产物
└── .planning/ # GSD 规划与执行记录
ok github.com/CPIC/promgr 3 tests
ok github.com/CPIC/promgr/internal/db 13 tests
ok github.com/CPIC/promgr/internal/handler 22 tests(含 RBAC 矩阵 + BatchDetail=8)
ok github.com/CPIC/promgr/internal/middleware 2 tests
ok github.com/CPIC/promgr/internal/service/actor 2 tests
ok github.com/CPIC/promgr/internal/service/auth 7 tests
ok github.com/CPIC/promgr/internal/service/batch 9 tests
ok github.com/CPIC/promgr/internal/service/product 5 tests
合计 63 项单测 全 PASS。
- 改
DefaultSeedPassword(演示密码为123456) - 改
cfg.JWTSecret(默认dev-secret-change-me) - 反向代理加 HTTPS
- 关闭 Swagger UI(生产环境不应暴露)
- v1 不做 token refresh,会话需重新登录
flowchart TD
Browser["浏览器<br/>(Vue 3 SPA<br/>Element Plus + Pinia)"]
Gin["Gin Router<br/>(JWT + RequirePerm)"]
Handler["Handler 层<br/>(参数校验 + 信封 OK/Fail)"]
Service["Service 层<br/>(业务用例 + Actor.HasPerm)"]
GORM["GORM ORM"]
SQLite[("SQLite WAL<br/>data.db")]
Embed["//go:embed all:web/dist<br/>(单二进制)"]
Guard["scripts/check_layering.sh<br/>scripts/check_no_net_http.sh<br/>(CI 强制解耦)"]
Browser -->|HTTP /api/v1/*| Gin
Gin --> Handler
Handler --> Service
Service --> GORM
GORM --> SQLite
Handler -.嵌入.-> Embed
Service -.禁 import.-> Guard
关键架构不变量(Phase 1 锁定):
- SQLite DSN:
?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on&_synchronous=NORMAL&_txlock=immediate+SetMaxOpenConns(8) - 单进程单二进制,无外部依赖(无 Docker / K8s / Nginx / Redis / MySQL)
- 路由顺序(router.go):
/api/v1/*→/swagger/*→/uploads/*→/assets/*→NoRoute(SPA fallback) - //go:embed all:web/dist +
fs.Sub提供 SPA fallback;前端走createWebHistory()深链 - Service 禁 import gin / net/http(CI grep gate 双门禁,详见 §已锁决策叙事 §9)
以下 9 条决策在 Phase 1/2/4/5/7 锁定,是演示时讲深度的锚点。每条都对应一处可在仓库指给评审者看的代码位置。
选择:github.com/glebarez/sqlite(底层 modernc.org/sqlite,纯 Go)
否决:github.com/mattn/go-sqlite3(cgo + gcc)
理由:Windows 开发机无 gcc;cgo 破坏 CGO_ENABLED=0 交叉编译。make build 一条命令从 Windows 主机直接产 linux/amd64 二进制(bin/server-linux-amd64),无需装交叉编译工具链。GORM 抽象保留切 MySQL 能力(仅改 Dialector 一行),但本期未部署验证。
代码位置:internal/db/db.go(Open glebarez/sqlite)+ Makefile(CGO_ENABLED=0 GOOS=linux GOARCH=amd64)
选择:TestTask.is_blocked 是与列状态正交的独立 bool 字段
否决:把「阻塞」做成一列特殊状态(4th column state)
理由:阻塞与所在列是两个独立维度——一张卡可以「在 EVT 列且被阻塞」。若做成列状态,状态机立刻爆炸(每列 × 阻塞/正常)。前端双端防御(BOARD-08):SortableJS :filter="'.card--blocked'" 阻止拖动 + 后端 service 拒收阻塞卡的列变更。
代码位置:internal/model/board.go(TestTask.IsBlocked bool 字段)+ web/src/components/BoardColumn.vue(:filter + @mousedown.stop.prevent)
选择:阶段完成度按 BoardColumn.is_terminal 语义标记统计
否决:按列名/列 ID 字符串硬编码
理由:列改名后报表不崩。ANLY-04 工单阶段分布按 is_terminal 分两桶(进行中 / 已完结),列改名、增删、调序都不会影响统计语义。
代码位置:internal/model/board.go(BoardColumn.IsTerminal bool)+ internal/service/analytics/stage.go(GROUP BY is_terminal)
选择:拖动结束后对目标列做 renumberColumn——按顺序连续重排(10/20/30...)
否决:链表(prev/next 指针)/ 中位数插值
理由:期望基数 <50 张/列下 O(n) 写穿足够,简单可测。链表需要双向遍历拼 SQL,中位数插值会因多次拖动耗尽 gap。两个边界场景(空列快速路径、删列迁移)都有显式分支。
代码位置:internal/service/board/reorder.go(renumberColumn + DeleteWithMigrate)
选择:vuedraggable :list 直接 mutate store 数组(零延迟);store 在 @start 拍 pre-drag snapshot,@end 修正字段 + PATCH + 失败回滚
否决:等后端 200 再刷新列表(wait-then-reload)
理由:拖拽 UX 必须零延迟;失败时显式回滚比让用户「卡一下」更可解释。opCounter 防止串行 PATCH 竞态(旧请求晚到不覆盖新值)。NCR 表单、物料表单也沿用此模式(confirm 失败 → DELETE 主单 → toast「附件关联失败,已撤销物料创建」)。
代码位置:web/src/stores/board.ts(beginSnapshot / commit / rollback)+ web/src/components/BoardColumn.vue
选择:FishboneDiagram.vue 手写 SVG(60° 几何 + 24px hit-area + 根因高亮 + 折叠 + PNG 导出)
否决:ECharts graph(力导向)
理由:鱼骨拓扑固定(6 大骨 60° 几何 + 中骨 + 头),坐标可算;ECharts graph 是为力导向动态图设计的,对固定拓扑过重。手写 SVG 还能精确控制标签避让、节点折叠、点击交互。50 节点硬上限防 PNG 爆炸。
代码位置:web/src/components/FishboneDiagram.vue + internal/service/ncr/cause.go(50 节点上限 ErrFishboneTooLarge)
选择:前端 v-perm 指令按 perm 码移除 DOM(按钮不可见);后端 service 每个写/读方法首行 if !actor.HasPerm(perm) { return ErrForbidden }
否决:只在前端做权限(或只在后端)
理由:深度防御——浏览器 DevTools 复制 admin token 越权直调 API 时,后端按 actor 实际角色拒绝(FOUND-07/08)。前端 v-perm 是 UX 优化(不让 viewer 看到「新建」按钮灰掉),不是安全边界。后端 service 内联 HasPerm 才是真边界。
代码位置:web/src/directives/v-perm.ts + internal/middleware/rbac.go(RequirePerm)+ 各 service 方法首行(如 internal/service/ncr/ncr.go)
选择:web/src/assets/tokens.css 作为 single source of truth——18 ANSI Z535 安全色 + 2 copper accent + 1 hairline + 4 shape vars + 3 font stack + 5 KPI segment vars
否决:消费 Element Plus 默认 --el-color-* / 消费应用花哨色板
理由:制造业 HMI 美学(ANSI Z535 是工业安全色标准),非消费应用。KPI 5 段渐变(绿→黄→橙→红→深红)通过 data-ratio="0..4" 属性选择器切换 linear-gradient stops——纯 CSS 无 JS class swap。阻塞卡 4 通道冗余(color + lock icon + 「阻塞」text + hatched texture)超出 WCAG ≥3 通道要求。
代码位置:web/src/assets/tokens.css + web/src/views/BoardView.vue(KPI data-ratio)+ 字体 vendored 在 web/public/fonts/(IBM Plex + Noto Sans SC,SIL OFL)
选择:scripts/check_layering.sh 拦 gin-gonic/gin;scripts/check_no_net_http.sh 拦 net/http;make verify 串跑两个守卫
否决:约定优于配置(口头约定 service 不引 gin)
理由:service 层应只通过 handler 注入 actor ctx,不感知 HTTP transport。这样 service 可以脱离 HTTP 引擎直接单测(NewService(db) + 构造 actor 即可),换 transport(gRPC / CLI)也无需改 service。两个守卫是双门禁——即便 check_layering.sh 被改宽,check_no_net_http.sh 仍独立生效。
代码位置:scripts/check_layering.sh + scripts/check_no_net_http.sh + Makefile(check-no-net-http + verify 靶)
本节是 §一键启动 的「演示视角速记版」——3 步最短路径 + 部署演示叙事。完整构建命令差异(Windows mingw32-make vs Linux make)见前部 §一键启动。
评审者的最短启动路径(Linux 主机):
git clone <repo-url> ProMgr && cd ProMgr
make build # 5-10 分钟(npm ci + vite build + cross-compile)
./bin/server-linux-amd64 # 启动;默认监听 :8080浏览器开 http://localhost:8080 → 用 admin / 123456 登录 → 看到首页。
部署演示:把
bin/server-linux-amd64scp 到任何 linux/amd64 机器,单文件可启动(无运行时依赖、无 Docker、无 K8s)。详见 §部署。
部署形态:单二进制 + SQLite WAL 文件 + uploads 目录
| 组件 | 文件 / 路径 | 说明 |
|---|---|---|
| 可执行 | bin/server-linux-amd64 |
自包含单二进制(嵌入前端 + Go 运行时) |
| 数据库 | data.db / data.db-shm / data.db-wal |
SQLite WAL 模式,首次启动自动创建 |
| 附件 | uploads/ |
落盘的附件(按 UUID 命名),由 Attachment GC 清理 temp |
| 配置 | env vars(PROMGR_PORT / PROMGR_DB_DSN / PROMGR_JWT_SECRET 等) |
零配置文件 |
部署场景:内网产线单机部署(制造业内网通常无外网,无 Docker / K8s)。把二进制 + data.db(首次空)拷过去即可启动。
切 MySQL(保留能力,未部署验证):GORM 抽象保留切 MySQL 能力——改 gorm.Open(sqlite.Open(...)) 为 gorm.Open(mysql.Open("user:pass@tcp(host:3306)/dbname?...")) 一行;所有 model / migration / query 不变。本期不实际部署验证。
反向生产化清单(v1 未做,v2 候选):
- 反向代理加 HTTPS(v1 假定内网可信)
- 改
DefaultSeedPassword(演示密码123456)+ 改JWTSecret - 关闭 Swagger UI(生产环境不应暴露
/swagger/*) - 备份策略:
data.db定期 cp(WAL 模式下 cp 是一致快照) - 多副本:SQLite WAL 单 writer,多副本需切 MySQL
功能演示 3 条主线,每条约 5 分钟。建议先走 (a) 主线闭环(脊柱),再走 (b) 仪表盘(数据消费),最后走 (c) 深度提问触发点(讲深度)。
admin / 123456 登录 → 建产品型号 PRJ001 → 开 EVT 试制批次 → 看板派测试工单(拖拽) → 进工单上报检验数据(动态表单按 USL/LSL 自动判 OK/NG) → 判 NG 一键开异常单 NCR → 8D 流程逐步推进(D1 团队 → D2 问题描述 → D3 ICA 三段 → D4 根因 → D5 PCA → D6 验证 → D7 预防 → D8 结案) → D3/D4 阶段画鱼骨图(5M1E 标根因,手写 SVG) → D8 关闭自动沉淀问题库。
讲点:动态表单 server-authoritative 判定(客户端不可覆盖)、NCR 6 态状态机字典序流转、8D D3 ICA 三段验证、鱼骨图 50 节点硬上限。
进顶部 nav 「质量分析」 → 默认看到「全部产品 / 全部批次 / 近 30 天」筛选下的 4 图:
- 帕累托主图(bar + line 双轴 + 80% markLine):按 defect_code 降序,copper 高亮「关键少数」(cum% > 80%)→ 点最高柱(如「焊点虚焊」)→ 跳
/ncrs?defect=焊点虚焊&from=analytics→ 顶部横幅「来源:质量仪表盘」+ [← 返回仪表盘] → 返回。 - 5M1E 饼图:6 类根因(人/机/料/法/测/环)按
is_root=true分布。 - NG 趋势线:按天聚合的 NG 率。
- 工单阶段柱:按
is_terminal分进行中 / 已完结两桶。
滚到底部「问题库检索」输入「虚焊」 → 命中 PCA 摘要含「焊点虚焊」的已结案 NCR(来自 D-13 seed NCR#3)。
讲点:D-04 下钻闭环(click → router.push → 横幅 → 返回)、D-01 全维筛选(产品/批次/时间)响应式触发 4 图重发(Pinia watch deep)、5M1E 仅 is_root=true(反映根因集中而非调查投入)、Stage 按 is_terminal(列改名不崩)。
直接指 README §已锁决策叙事 9 段。常见深度提问:
- 「为何 service 不引 gin?」 → 指 §9(双门禁 + service 可单测)
- 「为何手写 SVG 鱼骨图不用 ECharts?」 → 指 §6(拓扑固定 + 坐标可算)
- 「为何 glebarez/sqlite?」 → 指 §1(pure Go + 交叉编译)
- 「双端 RBAC 是什么意思?」 → 指 §7(前端 v-perm UX + 后端 HasPerm 边界)
- 「乐观更新失败怎么办?」 → 指 §5(snapshot + rollback + opCounter)
- 「is_blocked 为啥是正交维度?」 → 指 §2(避免状态机爆炸)
Phase 6 状态:本仓库当前为 Phase 1+2+4+5+7 已交付。Phase 6(报告模板 RPT / 调度 SCHED / 数据导入 DATA / Mock ERP-MES)已规划但本期未交付,本 README 不描述这些模块的接口与配置。Phase 7 仪表盘分析数据来源于 Phase 5 NCR 与问题库。
已交付阶段(按交付顺序):
| Phase | 名称 | 状态 |
|---|---|---|
| 1 | Foundation(骨架 + 鉴权 + Product/Batch + Vue) | 已交付 |
| 2 | 看板 + 工单拖拽 | 已交付 |
| 4 | 物料库 + 供应商 + LTB + 附件 | 已交付 |
| 5 | NCR + 8D + 鱼骨图 + 问题库 | 已交付 |
| 7 | 仪表盘 + 帕累托 + 5M1E + 问题库检索 | 已交付 |
Phase 6 计划但未交付:
- RPT-*(报告模板 + PDF/Excel 渲染):模板版本链 + 服务端 text/template 渲染已部分落地(06-02a backend slice),前端 UI 切片(06-02b)规划中。
- SCHED-*(调度 + cron dispatcher):robfig/cron 调度引擎已交付(06-01a/06-01b),但 built-in auto_report 触发依赖 RPT 落地。
- DATA-*(Excel 批量导入 + SSE + Mock ERP/MES):未开始。
削减说明(按 STATE.md §Reduction order if behind schedule):
- ANLY-06 相似案例推荐:按范围削减顺序 #2 砍掉。NCR 新建表单保留「手动进入问题库检索」入口作为替代(不强推)。v2 视数据量再加(需 ≥100 条 problem_library 数据 + 关键词匹配或向量检索)。