Skip to content
NDCCCCCCPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ProMgr — 制造业试制测试管理平台

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 仅用于功能演示。


一键启动

1. 构建(release:前端 + 嵌入 + 交叉编译)

# Windows (使用 mingw32-make;Linux/macOS 用 make)
mingw32-make build
# 等价:make build

make build 一条命令串跑完整 release pipeline(D-09):

  1. web-build:cd web && npm ci && npm run build(首次约 3-5 分钟)
  2. 嵌入://go:embed all:web/dist 把前端 SPA 打进二进制
  3. 交叉编译: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);后续增量构建更快。

2. 运行

# 默认端口 8080;冲突可用 PROMGR_PORT 覆盖
PROMGR_PORT=8090 ./bin/server-linux-amd64

3. 演示最短路径(demo 速记)

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 登录 → 看到首页 → 进 §演示三走。

启动后访问:

URL 内容
http://localhost:8080/login 登录页(演示账号一键登录)
http://localhost:8080/swagger/index.html OpenAPI UI
http://localhost:8080/api/v1/healthz 健康检查

一键验证

bash scripts/verify-foundation.sh

8 道闸(任一失败 exit 1):

  1. cleanup — 清旧 DB 与构建产物
  2. smoke-cgo — 依赖链无 cgo SQLite 驱动
  3. go test — 全套单测(含 RBAC 矩阵 + BatchDetail=8 真实聚合)
  4. layering — service 子包不 import gin/net/http
  5. swag — 生成 docs/swagger
  6. web-build — 真实 Vue SPA 构建
  7. build — Go embed + 单二进制
  8. e2e curl — Product/Batch CRUD 200/403 矩阵 + BatchDetail.tasks==8

架构不变量(Phase 1 锁定,勿破坏)

  1. 路由顺序(router.go):/api/v1/* → /swagger/* → /uploads/* → /assets/* → NoRoute
  2. SQLite DSN:?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(on)&_pragma=synchronous(NORMAL)&_txlock=immediate + MaxOpenConns(8)
  3. Service 禁 import gin/net/http(scripts/check_layering.sh 守卫)
  4. 嵌入前端://go:embed all:web/dist + fs.Sub 提供 SPA fallback
  5. 双端 RBAC:前端 v-perm 指令过滤 DOM + 后端 RequirePerm 中间件兜底
  6. 禁用 GORM 软删除:用显式 status 字段表达「停用」(D-03)
  7. DB 级外键:所有关联走 constraint:OnDelete:RESTRICT;Attachment 用扁平 biz_type+biz_id
  8. API 信封契约:业务错误走 HTTP 200 + envelope code!=0;401/403/404 走真实 HTTP 状态码 + 同一信封体

API 速览

Auth

  • POST /api/v1/auth/login — 用户名密码登录,返 JWT
  • GET /api/v1/me — 当前用户信息与权限码

Products(需 product:read)

  • 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)

Batches(需 batch:read / batch:write / batch:detail)

  • 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。


演示叙事(项目开场动作)

  1. admin / 123456 一键登录 → 看产品列表(3 个 seed)
  2. 进入批次 → 看详情页 → 下方 3 张计数卡:工单 8 / 检验 0 / 异常单 0(PROD-04 真实聚合)
  3. 退出 → tester / 123456 登录 → 侧边菜单没有「停用」按钮(v-perm DOM 移除)
  4. 退出 → viewer / 123456 登录 → 没有任何「新建」按钮(只读)
  5. 浏览器 DevTools 复制 admin token,越权直调 API 仍被后端拒绝(双端校验)

7 阶段路线图(详见 .planning/ROADMAP.md)

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
Loading

关键架构不变量(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 锁定,是演示时讲深度的锚点。每条都对应一处可在仓库指给评审者看的代码位置。

1. glebarez/sqlite(pure Go SQLite)

选择: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)

2. is_blocked 正交维度(非第 4 列状态)

选择: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)

3. is_terminal 而非列名做统计

选择:阶段完成度按 BoardColumn.is_terminal 语义标记统计 否决:按列名/列 ID 字符串硬编码

理由:列改名后报表不崩。ANLY-04 工单阶段分布按 is_terminal 分两桶(进行中 / 已完结),列改名、增删、调序都不会影响统计语义。

代码位置:internal/model/board.go(BoardColumn.IsTerminal bool)+ internal/service/analytics/stage.go(GROUP BY is_terminal)

4. 整列重排(O(n) contiguously renumber)

选择:拖动结束后对目标列做 renumberColumn——按顺序连续重排(10/20/30...) 否决:链表(prev/next 指针)/ 中位数插值

理由:期望基数 <50 张/列下 O(n) 写穿足够,简单可测。链表需要双向遍历拼 SQL,中位数插值会因多次拖动耗尽 gap。两个边界场景(空列快速路径、删列迁移)都有显式分支。

代码位置:internal/service/board/reorder.go(renumberColumn + DeleteWithMigrate)

5. 乐观更新 + 快照回滚

选择: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

6. 鱼骨图手写 SVG(非 ECharts graph)

选择: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)

7. 双端 RBAC(前端 v-perm + 后端 service 内联 HasPerm)

选择:前端 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)

8. _tokens.css ANSI Z535 + IBM Plex 视觉语言

选择: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)

9. service 禁 import gin/net/http(CI 强制解耦)

选择: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-amd64 scp 到任何 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) 深度提问触发点(讲深度)。

(a) 质量闭环主线(脊柱,必演)

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 节点硬上限。

(b) 仪表盘分析(数据消费)

进顶部 nav 「质量分析」 → 默认看到「全部产品 / 全部批次 / 近 30 天」筛选下的 4 图:

  1. 帕累托主图(bar + line 双轴 + 80% markLine):按 defect_code 降序,copper 高亮「关键少数」(cum% > 80%)→ 点最高柱(如「焊点虚焊」)→ 跳 /ncrs?defect=焊点虚焊&from=analytics → 顶部横幅「来源:质量仪表盘」+ [← 返回仪表盘] → 返回。
  2. 5M1E 饼图:6 类根因(人/机/料/法/测/环)按 is_root=true 分布。
  3. NG 趋势线:按天聚合的 NG 率。
  4. 工单阶段柱:按 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(列改名不崩)。

(c) 深度提问触发点(讲深度)

直接指 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 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 数据 + 关键词匹配或向量检索)。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages