Commit 2bc3429d277b84733e3674c89607d305744362da

Authored by zichun
0 parents

chore: plan phase done

.githooks/pre-push 0 → 100755
  1 +++ a/.githooks/pre-push
  1 +#!/usr/bin/env bash
  2 +# .githooks/pre-push — run scripts/test.sh before every push.
  3 +# No --no-verify: the claude-code hook deny-no-verify.sh also blocks it.
  4 +
  5 +set -euo pipefail
  6 +
  7 +cd "$(git rev-parse --show-toplevel)"
  8 +
  9 +./scripts/test.sh
.gitignore 0 → 100644
  1 +++ a/.gitignore
  1 +# ==== ERP 插件推荐忽略项(skeleton-gen 追加) ====
  2 +# 本地运行时配置(含真实凭据,严禁入库)
  3 +.env.local
  4 +.env.*.local
  5 +
  6 +# Java / Maven
  7 +target/
  8 +*.class
  9 +
  10 +# Node / 前端构建产物
  11 +node_modules/
  12 +dist/
  13 +build/
  14 +coverage/
  15 +
  16 +# IDE
  17 +.idea/
  18 +.vscode/
  19 +*.iml
  20 +
  21 +# OS
  22 +.DS_Store
  23 +Thumbs.db
  24 +
  25 +# 日志
  26 +*.log
  27 +logs/
  28 +
  29 +# 插件运行时临时文件
  30 +.tmp/
  31 +*.raw
  32 +# ==== 结束 ====
CLAUDE.md 0 → 100644
  1 +++ a/CLAUDE.md
  1 +# CLAUDE.md — ERP项目 Claude Code 主指令文件
  2 +
  3 +> 本文件是 Claude Code 的"操作手册"。Claude Code 启动时会自动读取此文件。
  4 +
  5 +---
  6 +
  7 +## 🎯 项目概述
  8 +
  9 +- **项目名称**: 小羚羊
  10 +- **项目简述**: 测试ERP
  11 +- **目标用户**: 企业内部管理人员
  12 +- **部署方式**: 私有化部署
  13 +
  14 +---
  15 +
  16 +## 🔄 B 阶段开发流程(后端模块循环 → 前端整体阶段)
  17 +
  18 +B 阶段分两段,**全部固化到 skills**。入口:`/erp-workflow:coding-start`。
  19 +
  20 +### 阶段路由(coding-start 内,只做分发)
  21 +
  22 +`coding-start` 每次入口做两段完成性检查后真值表派发:
  23 +
  24 +- 后端完成性检查 → `backend_done`(扫 docs/08 § 二 + GitLab state)
  25 +- 前端完成性检查 → `frontend_done`(扫 docs/08 § 三 整体 MR + state)
  26 +
  27 +| `backend_done` | `frontend_done` | 派发 |
  28 +|---|---|---|
  29 +| `false` | 任意 | `module-start`(写后端) |
  30 +| `true` | `false` | `frontend-start`(写前端) |
  31 +| `true` | `true` | "所有阶段已完成" |
  32 +
  33 +前端阶段前置(prototype/ 门禁)由 `frontend-start` 自带,不在 coding-start。`module-start` 与 `frontend-start` **互不感知对方**。
  34 +
  35 +### 后端阶段(每模块一个 MR)
  36 +
  37 +- **模块循环(外)**:`module-start` → `test-gate(phase=backend)` → `module-report` → `mr-create` → 人工 Approve MR → 用户重跑 coding-start → coding-start 再次路由
  38 +- **功能循环(内,每 REQ-XXX-NNN 一遍)**:`feature-brainstorm` → `feature-plan` → `feature-tdd` → `feature-verify` → `feature-review`
  39 +- 后端阶段任务严格落在 `backend/` 路径下;docs/01 REQ 卡片的 UI 描述在此阶段忽略,UI 推迟到前端阶段。
  40 +
  41 +### 前端阶段(整体一个 MR,所有后端模块 merged 后启动)
  42 +
  43 +- **FE 清单(AI 自主推导,无审阅断点)**:`frontend-start` 进入时扫 prototype + docs/01 + docs/05 → AI 自主推导 FE 业务功能清单写入 `docs/08 § 三`(已有则加载)。**FE 是业务功能粒度,与 prototype HTML 文件数无关**——一个 HTML 可拆多个 FE,多个 HTML 也可合成一个 FE。FE 清单的合理性由 fe-feature-review / mr-create 在整体 MR 提交时一并校核(人工只在 MR Approve+Merge 处介入)。
  44 +- **FE 循环(外)**:`frontend-start` → fe-feature 循环 → `test-gate(phase=frontend)` → `module-report(phase=frontend)` → `mr-create`(分支 `frontend-phase`,docs/08 § 三 整体 MR)。
  45 +- **FE 功能循环(内,每个 FE-NN 一遍)**:`fe-feature-brainstorm` → `fe-feature-plan` → `fe-feature-tdd` → `fe-feature-verify` → `fe-feature-review`(专用 `fe-code-reviewer` agent,硬编码 7 维 review checklist)
  46 +- 前端阶段任务严格落在 `frontend/` 路径下;布局以 `prototype/` 为权威。
  47 +
  48 +### MR 前测试闸门
  49 +
  50 +- `test-gate`:后端阶段子会话跑 `scripts/test.sh`(含本模块新增 + 已合并模块回归);前端阶段子会话跑 vitest + playwright。
  51 +- `.githooks/pre-push` 兜底;`git push --no-verify` 被 `deny-no-verify.sh` 硬拦。
  52 +
  53 +---
  54 +
  55 +## ✅ 阶段完成判定规则
  56 +
  57 +`docs/08-模块任务管理.md` 分两段:
  58 +- `§ 二`:后端模块元数据表(每个模块一行 bullet,记录依赖 / 路径 / MR iid / 功能子项)
  59 +- `§ 三`:前端阶段元数据(整体 MR + FE 子项清单,由 `frontend-start` 在所有后端模块 merged 后填入)
  60 +
  61 +**阶段完成判定**统一以 `MR:` 字段(§ 二 各模块) / `整体 MR:` 字段(§ 三)+ `GitLab API state=merged` 判定;子项勾选只作可视化进度,不参与完成判定。
  62 +
  63 +### 后端模块格式
  64 +
  65 +每个后端模块在 docs/08 § 二 中长这样:
  66 +
  67 +```markdown
  68 +- module_0 系统管理
  69 + - 依赖: —
  70 + - 路径: backend/module/sys/
  71 + - MR: —
  72 + - 功能:
  73 + - [ ] REQ-SYS-001 用户登录
  74 + - [ ] REQ-SYS-002 用户注册
  75 +```
  76 +
  77 +- `MR:` 字段由 `mr-create` 在创建 MR 时从 `—` 改为 `!<iid>`。
  78 +- 每个 `REQ-*` 子项由 `feature-review` 在 `verdict=approve` 时自动勾选为 `[x]`。
  79 +- 路径限定为后端目录(如 `backend/module/sys/`);前端代码不在此阶段产生。
  80 +
  81 +### 前端阶段格式(§ 三)
  82 +
  83 +```markdown
  84 +- 整体 MR: —
  85 +- 功能:
  86 + - [ ] FE-01 用户登录与注册 | 关联 REQ:REQ-SYS-001, REQ-SYS-002 | 关联原型:prototype/auth.html
  87 + - [ ] FE-02 仪表盘总览 | 关联 REQ:REQ-DASH-001 | 关联原型:prototype/dashboard.html
  88 +```
  89 +
  90 +- `整体 MR:` 字段由 `mr-create` 在创建前端 MR 时从 `—` 改为 `!<iid>`。
  91 +- "功能:" 列表由 `frontend-start` 进入时由 AI 自主推导写入(无人工审阅断点)。FE 是业务功能粒度,与 prototype HTML 文件数无关;合理性由 fe-feature-review / 整体 MR 时统一校核。
  92 +- 每个 `FE-NN` 子项由 `fe-feature-review` 在 `verdict=approve` 时自动勾选为 `[x]`。
  93 +- 进入前端阶段前 `frontend-start` 步骤 1 自带 prototype/ 门禁,强制检查项目根 `prototype/` 至少含 1 个 `*.html` mockup。
  94 +
  95 +### 状态语义(后端模块 + 前端阶段共用)
  96 +
  97 +| `MR:` 字段 | `GitLab API state` | 含义 | 你(Claude Code)的行为 |
  98 +|---|---|---|---|
  99 +| `—` | — | 该阶段未开始(未创建 MR) | ✅ 开始该阶段开发 |
  100 +| `!<iid>` | `opened` / `closed` | 阶段开发中 / 打回 | ✅ 继续推进该阶段 |
  101 +| `!<iid>` | `merged` | 阶段**已完成** | 🟢 后端:进入下一未完成模块;后端全完 → 前端阶段;前端 merged → 全部完成 |
  102 +
  103 +### 模块完成报告
  104 +
  105 +由 `module-report` skill 产出,模板位于 由 module-report skill 持有(12 节标准化,含跨模块改动等 CLAUDE.md 软规则映射节)。CC 不手写模块报告,仅填模板。
  106 +
  107 +---
  108 +
  109 +## 🏷️ 占位符统一约定
  110 +
  111 +项目文档里有 **2 种填写占位** + **1 种提示占位**:
  112 +
  113 +| 格式 | 谁填 | 使用阶段 | 说明 |
  114 +|------|-----|---------|------|
  115 +| `【人工填写:<简短说明>】` | 人 | 仅 A 阶段文档 | 密钥 / 账密 / 包名 / 命名约定 / 小版本号等人工才能决定的值;B 阶段 plan/spec 禁止出现,查不到真值时用 `AskUserQuestion` 问用户 |
  116 +| `TBD(<责任人>)` | CC 自动 | A 或 B | 后缀附带责任方(如 `TBD(A3 自动补)` / `TBD(A5 自动补)`);由对应 skill 就地补填,`module-report` § ⑦ 检查 `TBD(CC 补)` 残留 |
  117 +
  118 +**HTML 注释 `<!-- ... -->`**:提示占位,是**插件内部大纲模板**里给 LLM 的**填空提示 / 章节引导**,指引 LLM 按结构填实际内容。skill 生成时会**剥除**这些注释,最终产物里注释不会保留。
  119 +
  120 +---
  121 +
  122 +## 📐 编码行为约束
  123 +
  124 +### 你必须做的 ✅
  125 +
  126 +1. **严格遵循** `docs/04-技术规范.md`——命名 / 编码 / 统一响应 / 异常处理 / 数据访问 / 配置与安全 等项目专属技术规约全部在此
  127 +2. **严格遵循** `docs/09-项目目录结构.md`——文件放对位置
  128 +3. **每个后端接口** 必须先在 `docs/05-API接口契约.md` 定义,再编码实现
  129 +4. **每个功能可追溯到 `REQ-XXX-NNN`**——commit tag + 代码注释(如 `// REQ-SYS-001: 用户登录`)+ plan/spec 文件名均用此 tag
  130 +5. **遇到跨模块改动**(动到非当前模块的代码)——按 § 🟡 软规则 **S2** 执行(允许改,但必须留痕)
  131 +6. **遇到技术栈外组件引入**(`docs/04 § 零` 技术栈表外的框架 / 中间件 / 关键库),按 § 🟡 软规则 **S1** 执行(允许引入,但必须先 AskUserQuestion)
  132 +
  133 +### 你禁止做的 🚫
  134 +
  135 +1. **主会话直接 `mysql -e` 跑业务 DDL**(只读查询 / 临时本地调试除外)——业务 schema 必须走 `sql/migrations/V_n__*.sql`,详见下方 Schema 演化规约
  136 +2. **手动 Edit `docs/08 § 二/§ 三` 的 `MR:` / `整体 MR:` 字段**,必须要由 `mr-create` 自动回写
  137 +
  138 +### Schema 演化规约(Flyway migration)
  139 +
  140 +1. **文件命名**:`sql/migrations/V<n>__<snake_case_desc>.sql`,例:`V5__add_user_email_unique_index.sql`
  141 +2. **版本号分配**:建文件前 `ls sql/migrations/V*.sql` 查当前最大 n,新文件 `n_max + 1`
  142 +3. **Apply 方式**:Spring Boot 启动 / 测试启动时 Flyway 自动 apply(项目必须在 `pom.xml` 声明 `flyway-core` + `flyway-mysql` 依赖)。`scripts/setup-test-db.sh` 只负责清空库,不做 apply
  143 +4. **已合并的 migration 永不修改**:发现错了写一个补救 migration(如 `V7__fix_V5_index_name.sql`),旧 `V_n.sql`
  144 +5. **临时调试 DDL**:临时在本地试字段/索引可手动 `mysql -e`,但不写 migration;下次 `setup-test-db.sh` 会 drop+create 清掉
  145 +6. **A4 生成的 V1**:`V1__initial_schema.sql` 是 A 阶段由 `db-init` 从 `docs/03-数据库设计文档.md`(A3 正向设计的 schema SSoT)翻译生成的初始版本;后续 V2/V3/... 由 B 阶段每个 REQ 按需写入,**同时**反向同步更新 docs/03 对应表小节以保持 SSoT 一致
  146 +
  147 +---
  148 +
  149 +## 🗂️ Git 提交规范
  150 +
  151 +每次提交必须遵循以下格式:
  152 +
  153 +```
  154 +<type>(<scope>): <subject>
  155 +```
  156 +
  157 +- `scope`: 模块名,如 `user` / `inventory` / `order`
  158 +- `subject`: 简短描述;业务类(feat / fix / test)必须带 `REQ-XXX-NNN` 后缀
  159 +
  160 +`type` 含义:
  161 +
  162 +| type | 看到它意味着 |
  163 +|-----|-------------|
  164 +| `feat` | **新能力上线**——用户多了一个功能、接口、页面或业务规则 |
  165 +| `fix` | **修 bug**——原来行为错了,这次改对 |
  166 +| `refactor` | **重构**——外部行为不变,只改代码结构 / 命名 / 抽象 |
  167 +| `docs` | **文档改动**——只动 Markdown / 代码注释,不动实现 |
  168 +| `style` | **格式调整**——空白 / 缩进 / import 顺序,逻辑 0 变化 |
  169 +| `test` | **只动测试代码**——补用例 / 修 fixture,不碰实现 |
  170 +| `chore` | **流程维护**——构建 / 依赖 / 工具 / 证据档案 / MR 元数据等非业务动作 |
  171 +
  172 +---
  173 +
  174 +## 🚩 中断机制
  175 +
  176 +功能循环(每个功能 REQ-XXX 的 Brainstorm → Plan → TDD → Verify → AI 自审)默认 **静默编程**,但触发以下任何一条必须**立刻停下、记录原因、等人决策**,不得自行绕过:
  177 +
  178 +| # | 中断 | 例子 |
  179 +| - | --- | --- |
  180 +| 1 | **测试反复失败** | 同一测试同一功能内连续 **10 次**修复失败 |
  181 +| 2 | **要改密钥 / 账密 / 包名** | `docs/07-环境配置.md` 里由人工标注必须填的字段 |
  182 +| 3 | **外部接口不可达** | 第三方 API 无法连接、证书失效等环境问题,并无法自行解决 |
  183 +
  184 +> 其余需要人类判断的场景一律走普通 `AskUserQuestion` Q&A,不中断、不写 Blocker 文件。
  185 +
  186 +**触发中断时的固定动作:**
  187 +
  188 +1. 在当前功能的 plan 文件里追加一节 `## 🚩 Blocker`(报告格式由 `interrupt-check` 的 `interrupt-block-template.md` 持有)
  189 +2. 停止后续所有功能的静默执行
  190 +3. 在主会话输出一句话摘要 + 指向 blocker 文件的路径,等人回复
  191 +
  192 +---
  193 +
  194 +## 🟡 软规则(允许继续,但有强制后续动作)
  195 +
  196 +以下情况 **不触发中断**,CC 可自行继续推进,但必须在约定位置留痕,模块完成时统一审计。
  197 +
  198 +| # | 软规则 | 允许动作 | 强制后续 |
  199 +| - | ----- | ------- | ------- |
  200 +| S1 | **技术栈外组件引入** | 用 `AskUserQuestion` 给用户三选一:接受引入 / 换方案 / 拒绝 | ① **接受** → 同会话直接在 `docs/04 § 零` 追加一行 → 继续流程 ② **换方案 / 拒绝** → 视为常规歧义澄清,继续 Q&A 收敛 ③ 不写 Blocker、不中断流程 |
  201 +| S2 | **跨模块改动** | **默认不改**,仅为当前模块实现所必需时允许修改 | ① hook `log-cross-module.sh` 自动落存根 ② `module-report` 一次性调用 `cross-module-log` skill 批量补齐「原因 / 影响评估」+ 「跨模块改动」节完整贴入《模块完成报告》 |
  202 +
  203 +---
  204 +
  205 +## 🧭 通用工作准则(General Principles)
  206 +
  207 +### 1. Think Before Coding
  208 +
  209 +**Don't assume. Don't hide confusion. Surface tradeoffs.**
  210 +
  211 +Before implementing:
  212 +- State your assumptions explicitly. If uncertain, ask.
  213 +- If multiple interpretations exist, present them - don't pick silently.
  214 +- If a simpler approach exists, say so. Push back when warranted.
  215 +- If something is unclear, stop. Name what's confusing. Ask.
  216 +
  217 +### 2. Simplicity First
  218 +
  219 +**Minimum code that solves the problem. Nothing speculative.**
  220 +
  221 +- No features beyond what was asked.
  222 +- No abstractions for single-use code.
  223 +- No "flexibility" or "configurability" that wasn't requested.
  224 +- No error handling for impossible scenarios.
  225 +- If you write 200 lines and it could be 50, rewrite it.
  226 +
  227 +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
  228 +
  229 +### 3. Surgical Changes
  230 +
  231 +**Touch only what you must. Clean up only your own mess.**
  232 +
  233 +When editing existing code:
  234 +- Don't "improve" adjacent code, comments, or formatting.
  235 +- Don't refactor things that aren't broken.
  236 +- Match existing style, even if you'd do it differently.
  237 +- If you notice unrelated dead code, mention it - don't delete it.
  238 +
  239 +When your changes create orphans:
  240 +- Remove imports/variables/functions that YOUR changes made unused.
  241 +- Don't remove pre-existing dead code unless asked.
  242 +
  243 +The test: Every changed line should trace directly to the user's request.
  244 +
  245 +### 4. Goal-Driven Execution
  246 +
  247 +**Define success criteria. Loop until verified.**
  248 +
  249 +Transform tasks into verifiable goals:
  250 +- "Add validation" → "Write tests for invalid inputs, then make them pass"
  251 +- "Fix the bug" → "Write a test that reproduces it, then make it pass"
  252 +- "Refactor X" → "Ensure tests pass before and after"
  253 +
  254 +For multi-step tasks, state a brief plan:
  255 +```
  256 +1. [Step] → verify: [check]
  257 +2. [Step] → verify: [check]
  258 +3. [Step] → verify: [check]
  259 +```
  260 +
  261 +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
docs/01-需求清单/USR-用户管理/REQ-USR-001.md 0 → 100644
  1 +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-001.md
  1 +### REQ-USR-001 用户登录
  2 +
  3 +
  4 +**目标**: 用户通过用户名+密码完成身份认证,获取 JWT Token 用于后续接口鉴权
  5 +
  6 +- **输入**:
  7 +
  8 + - **表1**:
  9 +
  10 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  11 + | --- | ---- | --- | ---- | ------- | ----- | --- | ----------- |
  12 + | 用户名 | 文本 | 是 | 手工输入 | — | — | — | — |
  13 + | 密码 | 文本 | 是 | 手工输入 | — | — | — | 输入显示星号 |
  14 + | 版本 | 文本 | 是 | 下拉单选 | `公司表` | 页面加载时 | 标准版 | |
  15 +
  16 +- **输出**: 成功/失败
  17 +
  18 +- **跨字段规则**: 校验用户名 + 密码哈希;连续失败达到阈值临时锁定账号;登录成功签发限时 token 并返回基本用户信息
  19 +- **边界**: token 设置合理过期时间;接口需具备防暴力破解保护
  20 +- **验收**: 正确凭据返回 Token 且可通过鉴权接口验证;错误密码返回通用错误消息(不区分用户名或密码错误);锁定账号返回锁定提示
  21 +- **依赖表**: `t_user`(认证主体 + 失败计数 + 锁定时间 + 登录时间)/ `t_company`(「版本」下拉数据源)
  22 +- **依赖接口**: `POST /api/usr/auth/login`(本 REQ 提供);后续配套 `POST /api/usr/auth/refresh`、`POST /api/usr/auth/logout` 由同模块衍生接口(不另立 REQ)
docs/01-需求清单/USR-用户管理/REQ-USR-002.md 0 → 100644
  1 +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-002.md
  1 +### REQ-USR-002 新增用户
  2 +
  3 +**目标**: 用户在后台新建用户账号,指定用户名、密码及角色,账号立即生效可用
  4 +
  5 +- **输入**:
  6 +
  7 + - **表1**:
  8 +
  9 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  10 + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- |
  11 + | 创建时间 | 日期时间 | — | 系统生成 | — | 页面加载时 | 当前日期 | 保存后自动生成;只读 |
  12 + | 制单人 | 文本 | — | 系统生成 | — | 页面加载时 | 当前登录用户 | 保存后自动生成;只读 |
  13 + | 员工名 | 文本 | 否 | 下拉单选 | `职员表` | 用户操作时 | — | 关联职员(可选) |
  14 + | 用户号 | 文本 | 是 | 手工输入 | — | 用户操作时 | — | 关联职员选择后自动输入员工姓名 |
  15 + | 用户名 | 文本 | 是 | 手工输入 | — | 用户操作时 | — | 关联职员选择后自动输入员工姓名 |
  16 + | 类型 | 文本 | 是 | 下拉单选 | 普通用户/超级管理员 | 页面加载时 | 普通用户 | — |
  17 + | 语言 | 文本 | 是 | 下拉单选 | 中文/英文/繁体 | 页面加载时 | — | — |
  18 + | 单据修改权限 | 布尔 | 否 | 复选框 | — | — | 否 | — |
  19 + | 密码 | 文本 | — | 系统生成 | 不显示 | — | 666666 | 保存后自动设为初始化 |
  20 +
  21 + - **表2** - 权限组:
  22 +
  23 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  24 + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- |
  25 + | 复选框 | 布尔 | 否 | 复选框 | — | — | 否 | 是否选择当前行权限 |
  26 + | 权限分类 | 文本 | — | — | — | 页面加载时 | — | — |
  27 +
  28 +
  29 +- **输出**:
  30 +
  31 + - **表1**:
  32 +
  33 + | 字段 | 类型 | 显示来源 |
  34 + | --- | --- | --- |
  35 + | 用户号 | 文本 | — |
  36 +
  37 +- **跨字段规则**: 用户名在系统内全局唯一;角色取值受系统配置约束
  38 +- **边界**: 密码以哈希形式存储
  39 +- **验收**: 提交合法数据后用户记录出现在列表;重复用户名返回错误提示;普通账号无权访问此功能
  40 +- **依赖表**: `t_user`(新建主记录)/ `t_employee`(员工名下拉 + 关联)/ `t_permission`(权限分类下拉)/ `t_user_permission`(写入权限组关联)
  41 +- **依赖接口**: `POST /api/usr/users`(本 REQ 提供);前置 `POST /api/usr/auth/login`(REQ-USR-001)获取 JWT
docs/01-需求清单/USR-用户管理/REQ-USR-003.md 0 → 100644
  1 +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-003.md
  1 +### REQ-USR-003 修改用户
  2 +
  3 +**目标**: 用户可更新已有用户的基本信息(姓名、角色、状态等),修改实时生效
  4 +
  5 +- **输入**: 选中目标
  6 +
  7 + - **表1**:
  8 +
  9 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  10 + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- |
  11 + | 创建时间 | 日期时间 | — | 系统生成 | — | 页面加载时 | 原值 | 保存后自动生成;只读 |
  12 + | 制单人 | 文本 | — | 系统生成 | — | 页面加载时 | 原值 | 保存后自动生成;只读 |
  13 + | 员工名 | 文本 | 否 | 下拉单选 | `职员表` | 页面加载时 | 原值 | 关联职员(可选) |
  14 + | 用户号 | 文本 | 是 | 手工输入 | — | 页面加载时 | 原值 | 关联职员选择后自动输入员工姓名 |
  15 + | 用户名 | 文本 | 是 | 手工输入 | — | 页面加载时 | 原值 | 关联职员选择后自动输入员工姓名 |
  16 + | 类型 | 文本 | 是 | 下拉单选 | 普通用户/超级管理员 | 页面加载时 | 原值 | — |
  17 + | 语言 | 文本 | 是 | 下拉单选 | 中文/英文/繁体 | 页面加载时 | 原值 | — |
  18 + | 单据修改权限 | 布尔 | 否 | 复选框 | — | 页面加载时 | 原值 | — |
  19 + | 密码 | 文本 | — | 系统生成 | 不显示 | 页面加载时 | 原值 | 保存后自动设为初始化 |
  20 +
  21 + - **表2** - 权限组:
  22 +
  23 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  24 + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- |
  25 + | 复选框 | 布尔 | 否 | 复选框 | — | 页面加载时 | 原值 | 是否选择当前行的权限 |
  26 + | 权限分类 | 文本 | — | — | — | 页面加载时 | — | — |
  27 +
  28 +- **输出**:
  29 +
  30 + - **表1**:
  31 +
  32 + | 字段 | 类型 | 显示来源 |
  33 + | --- | --- | --- |
  34 + | 用户 id | 文本 | `职员表` |
  35 +
  36 +- **跨字段规则**: 密码不在该接口修改;角色变更需具备相应权限
  37 +- **边界**: 必须传入有效用户 id;字段格式与新增一致
  38 +- **验收**: 修改角色或状态后立即反映在用户列表;被禁用账号无法登录并收到明确提示
  39 +- **依赖表**: `t_user`(更新主记录)/ `t_employee`(员工名下拉 + 关联)/ `t_permission`(权限分类下拉)/ `t_user_permission`(删旧 + 写新权限组关联)
  40 +- **依赖接口**: `PUT /api/usr/users/{iIncrement}`(本 REQ 提供);前置 `POST /api/usr/auth/login`(REQ-USR-001)+ 数据来源 `POST /api/usr/users`(REQ-USR-002)
docs/01-需求清单/USR-用户管理/REQ-USR-004.md 0 → 100644
  1 +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-004.md
  1 +### REQ-USR-004 查询用户
  2 +
  3 +**目标**: 用户可按用户名、角色或状态筛选并分页浏览用户列表
  4 +
  5 +- **输入**:
  6 +
  7 + - **表1**:
  8 +
  9 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  10 + | ---- | ---- | --- | ---- | ----------------------------------------------- | ----- | ------- | --------------- |
  11 + | 查询字段 | 文本 | 否 | 下拉单选 | 用户名/员工名/用户号/部门/用户类型/作废/登录日期/制单人 | 页面加载时 | 用户名 | — |
  12 + | 匹配方式 | 文本 | 否 | 下拉单选 | 包含/不包含/等于 | 页面加载时 | 包含 | — |
  13 + | 查询值 | 文本 | 否 | 手工输入 | — | — | — | 与「查询字段」配合使用,空为选择全部 |
  14 +
  15 +- **输出**:
  16 +
  17 + - **表1**:
  18 +
  19 + | 字段 | 类型 | 显示来源 |
  20 + | ---- | ---- | ----- |
  21 + | 序号 | 数字 | 系统生成 |
  22 + | 用户名 | 文本 | `用户表` |
  23 + | 员工名 | 文本 | `职员表` |
  24 + | 用户号 | 文本 | `用户表` |
  25 + | 部门 | 文本 | `职员表` |
  26 + | 用户类型 | 文本 | `用户表` |
  27 + | 语言 | 文本 | `用户表` |
  28 + | 作废 | 布尔 | `用户表` |
  29 + | 登录日期 | 日期时间 | `用户表` |
  30 + | 制单人 | 文本 | `用户表` |
  31 + | 制单日期 | 日期时间 | `用户表` |
  32 +
  33 +- **跨字段规则**: -
  34 +- **边界**: 单页最大条数受限(默认 100);密码与敏感字段不返回;查询为只读,不产生写副作用
  35 +- **验收**: 按条件筛选返回正确结果集;无匹配时返回空列表而非报错;分页参数越界时返回最后一页
  36 +- **依赖表**: `t_user`(列表主表 + 类型 / 作废 / 登录日期 / 制单人)/ `t_employee`(员工名)/ `t_department`(部门)
  37 +- **依赖接口**: `GET /api/usr/users`(本 REQ 提供);前置 `POST /api/usr/auth/login`(REQ-USR-001)
docs/01-需求清单/USR-用户管理/_module.md 0 → 100644
  1 +++ a/docs/01-需求清单/USR-用户管理/_module.md
  1 +# USR-用户管理
  2 +
  3 +- **模块简述**: 提供用户全生命周期管理,含登录认证、新增、修改、查询等业务操作
  4 +- **依赖模块**: —(基础模块)
  5 +- **涉及表**: `t_user` / `t_employee` / `t_department` / `t_permission` / `t_user_permission` / `t_company`
docs/01-需求清单/index.md 0 → 100644
  1 +++ a/docs/01-需求清单/index.md
  1 +# 需求清单
  2 +
  3 +> 本目录按模块组织所有功能需求。每个模块一个子目录,含 `_module.md`(模块头)和 `REQ-XXX-NNN.md`(每张 REQ 卡片一个文件)。下方核心功能点供 CC 拆分出 REQ 编号 + 标题 + 草拟规则;卡片内输入 / 输出的简述句和 N 张字段表由人工编辑。
  4 +
  5 +## 模块索引
  6 +
  7 +| 模块代码 | 模块名称 | 核心功能点(简要) |
  8 +| ---- | ---- | ------------------- |
  9 +| USR | 用户管理 | 增加用户,修改用户,查询用户,登录用户 |
  10 +
  11 +## 填写说明
  12 +
  13 +1. 每个模块占一行,`模块代码` 用大写英文缩写(如 SYS / PUR / INV / SAL / FIN / HR)
  14 +2. `核心功能点` 只需列关键词,CC 会基于此拆分出 N 张 REQ 卡片骨架(卡片内输入 / 输出的简述句和字段表仍由人工编辑)
  15 +3. 填完后运行 `/erp-workflow:plan-start`,CC 会自动检测并进入需求生成阶段
docs/02-开发计划.md 0 → 100644
  1 +++ a/docs/02-开发计划.md
  1 +# 02-开发计划
  2 +
  3 +## 一、模块依赖表
  4 +
  5 +| 模块 ID | 模块名 | 依赖模块 | 依赖表 |
  6 +|---|---|---|---|
  7 +| module_usr | USR-用户管理 | — | `t_user` / `t_employee` / `t_department` / `t_permission` / `t_user_permission` / `t_company` |
  8 +
  9 +## 二、开发顺序清单(CC 分发权威)
  10 +
  11 +> 本清单由 A5 `downstream-gen` 一次性生成。**每行是一个 REQ**,不是模块。CC 按表格行序从上到下扫描,对每个 REQ 所属模块查 `docs/08 § 二` 的 `MR:` 字段 + GitLab API `state`:`merged` 跳过,其他(`—` / opened / closed / 查不到)选为当前模块;`module-start` 会把该模块的所有 REQ 一次做完。
  12 +>
  13 +> **约束**:同一模块的所有 REQ 必须**连续排列**。允许打破依赖拓扑(如环依赖、业务必须先做),但必须在「备注」列写明原因。
  14 +
  15 +| # | REQ | 所属模块 | 选中理由 | 备注 |
  16 +|---|-----|---------|---------|------|
  17 +| 1 | **REQ-USR-001** | module_usr | 所属模块无依赖,认证接口为基础设施(其余 REQ 接口需 JWT 鉴权) | — |
  18 +| 2 | **REQ-USR-002** | module_usr | 依赖 REQ-USR-001 已在前;新增用户为后续修改 / 查询提供数据 | — |
  19 +| 3 | **REQ-USR-003** | module_usr | 依赖 REQ-USR-002 已在前(先有用户才能修改) | — |
  20 +| 4 | **REQ-USR-004** | module_usr | 依赖 REQ-USR-002 已在前(先有用户才能查询) | — |
  21 +
  22 +> **后端模块全部 merged 后**:用户重跑 `/erp-workflow:coding-start` → coding-start 检测到 `backend_done=true && frontend_done=false` → 派发 `frontend-start`。`frontend-start` 步骤 1 自带 prototype/ 门禁(≥ 1 个 `*.html` mockup,缺失则 AskUserQuestion 提示用户补齐)。前端阶段以业务功能(不是 HTML 文件数)为粒度拆分 FE,每个 FE 跑一次 feature 循环(fe-feature-*),最后整个阶段合 1 个 MR(分支 `frontend-phase`,记录在 `docs/08 § 三 整体 MR`)。
  23 +
  24 +## 三、关键说明
  25 +
  26 +- 本期需求只覆盖 USR 用户管理一个模块;后续若新增 PUR / SAL / FIN 等模块,需重新跑 `/erp-workflow:plan-start` 让 A5 重算依赖与顺序。
  27 +- REQ-USR-001 排第一是「基础设施先行」策略:JWT 鉴权机制建好后,其余 REQ 才能复用 `@PreAuthorize` 与统一异常处理;初始管理员账号由 V1 后续的种子 migration 注入(或人工 SQL 直插)。
  28 +- `t_employee` / `t_department` / `t_permission` / `t_company` 四张维表在 module_usr 内部按需访问,不另起模块;后续如演化出独立的 HR / 组织架构模块,再单独拆分。
docs/03-数据库设计文档.md 0 → 100644
  1 +++ a/docs/03-数据库设计文档.md
  1 +# 03-数据库设计文档
  2 +
  3 +- **Schema**: `xlyweberp_vibe_erp_test`
  4 +- **Migration 清单**: `sql/migrations/V*.sql`(由 Flyway 顺序 apply)
  5 +- **生成方式**: 由 A3 `db-design-gen` 基于 `docs/01-需求清单/<module>/REQ-*.md` REQ 卡片正向设计生成(schema SSoT)。
  6 +
  7 +## 项目标准列约定
  8 +
  9 +下文每张业务表的字段清单都自动包含以下 5 个标准列(匈牙利前缀 `i` int / `s` varchar / `t` datetime)。渲染时由 `docs-03-table-template.md` 模板内置原样输出。
  10 +
  11 +| 列名 | 类型 | 可空 | 主键 | 说明 |
  12 +|---|---|---|---|---|
  13 +| `iIncrement` | int | 否 | 是 | 整数主键 ID(自增方式由实现决定:DB `AUTO_INCREMENT` 或应用 / 触发器分配) |
  14 +| `sId` | varchar(100) | 是 | — | 业务 ID(对外暴露的字符串标识,如 UUID / 人类可读编号) |
  15 +| `sBrandsId` | varchar(100) | 是 | — | 品牌 ID(多租户隔离) |
  16 +| `sSubsidiaryId` | varchar(100) | 是 | — | 子公司 ID(组织层级隔离) |
  17 +| `tCreateDate` | datetime | 否 | — | 记录创建时间 |
  18 +
  19 +字典 / 辅助表如有豁免,在该表业务注记里注明豁免原因。
  20 +
  21 +## ER 关系概览
  22 +
  23 +USR 用户管理模块共 6 张表,围绕「用户」为核心:
  24 +
  25 +- `t_user`(用户主表)通过 `iEmployeeId` 关联 `t_employee`(职员),实现「用户号 / 员工名」一对一联动。
  26 +- `t_employee`(职员表)通过 `iDepartmentId` 关联 `t_department`(部门表),支撑 REQ-USR-004 按部门筛选。
  27 +- `t_user` 与 `t_permission`(权限分类字典)通过 `t_user_permission`(用户-权限关联表)建立多对多关系,实现 REQ-USR-002 / 003 的「权限组」分配。
  28 +- `t_company`(公司版本字典)独立于 `t_user`,仅为 REQ-USR-001 登录入参「版本」下拉提供可选项,不与用户产生强引用。
  29 +
  30 +依赖方向:`t_user → t_employee → t_department`、`t_user ↔ t_permission`(多对多)、`t_company` 独立。
  31 +
  32 +## 表清单
  33 +- `t_user` — 系统用户主表,承载登录认证、用户类型、语言偏好、单据权限等基础属性
  34 +- `t_employee` — 公司职员主档,与用户表通过 `iEmployeeId` 关联,提供姓名 / 工号 / 部门归属
  35 +- `t_department` — 部门组织树,支撑职员归属与按部门筛选
  36 +- `t_permission` — 权限分类字典,定义系统可分配的权限项
  37 +- `t_user_permission` — 用户-权限分类多对多关联表
  38 +- `t_company` — 公司 / 版本字典(标准版 / 专业版 / 旗舰版),登录时「版本」下拉数据源
  39 +
  40 +## `t_user` — 系统用户主表,承载登录认证与基础属性
  41 +
  42 +### 字段
  43 +
  44 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  45 +|---|---|---|---|---|
  46 +| `iIncrement` | int | 否 | 自增函数 | 整数主键 ID(标准列) |
  47 +| `sId` | varchar(100) | 是 | uuid 函数 | 业务 ID(标准列) |
  48 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID(多租户隔离,标准列) |
  49 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID(组织层级隔离,标准列) |
  50 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) |
  51 +| `sUserNo` | varchar(50) | 否 | — | 用户号;关联职员后自动同步员工号;系统内唯一 |
  52 +| `sUserName` | varchar(50) | 否 | — | 登录用户名;系统内唯一;3-50 位 |
  53 +| `iEmployeeId` | int | 是 | NULL | 关联职员 `t_employee.iIncrement`;可空(非员工账号如系统管理员) |
  54 +| `sPasswordHash` | varchar(255) | 否 | — | 密码哈希(BCrypt / Argon2);禁止明文;初始密码 `666666` 哈希后存入 |
  55 +| `sUserType` | varchar(20) | 否 | `NORMAL` | 用户类型枚举:`NORMAL`(普通用户)/ `SUPER_ADMIN`(超级管理员) |
  56 +| `sLanguage` | varchar(10) | 否 | `zh-CN` | 语言枚举:`zh-CN`(中文)/ `en-US`(英文)/ `zh-TW`(繁体) |
  57 +| `bModifyDoc` | tinyint(1) | 否 | 0 | 单据修改权限:0 否 / 1 是 |
  58 +| `bVoid` | tinyint(1) | 否 | 0 | 作废标记(软删除):0 启用 / 1 已作废 |
  59 +| `iLoginFailCount` | int | 否 | 0 | 连续登录失败次数;达到阈值触发临时锁定;登录成功后清零 |
  60 +| `tLockUntil` | datetime | 是 | NULL | 锁定截止时间;NULL 表示未锁定 |
  61 +| `tLastLoginDate` | datetime | 是 | NULL | 最近一次登录时间 |
  62 +| `sCreator` | varchar(100) | 是 | NULL | 制单人(创建该账号的操作员用户名) |
  63 +
  64 +### 索引
  65 +
  66 +- `uk_user_username` (UNIQUE): `sUserName`
  67 +- `uk_user_userno` (UNIQUE): `sUserNo`
  68 +- `idx_user_employee` (BTREE): `iEmployeeId`
  69 +- `idx_user_tenant` (BTREE): `sBrandsId`, `sSubsidiaryId`
  70 +- `idx_user_void` (BTREE): `bVoid`
  71 +
  72 +### 外键
  73 +
  74 +- `fk_user_employee`: `iEmployeeId` → `t_employee.iIncrement` (ON DELETE SET NULL / ON UPDATE RESTRICT)
  75 +
  76 +### 业务注记
  77 +
  78 +- 登录认证主体表;密码以哈希形式存储,登录失败计数与锁定时间均落库以保证服务重启不丢;亦可由 Redis 镜像加速,但 DB 字段为权威。
  79 +- `bVoid = 1` 视为已作废账号,不可登录、不出现在默认查询结果中(REQ-USR-004 列表默认隐藏作废)。
  80 +- 用户名 / 用户号唯一性由 `uk_user_username` / `uk_user_userno` 保证;新增 / 修改时若冲突由 DB 抛唯一约束错误,Service 转译为 `40001 用户名已存在`。
  81 +
  82 +## `t_employee` — 公司职员主档
  83 +
  84 +### 字段
  85 +
  86 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  87 +|---|---|---|---|---|
  88 +| `iIncrement` | int | 否 | 自增函数 | 整数主键 ID(标准列) |
  89 +| `sId` | varchar(100) | 是 | uuid 函数 | 业务 ID(标准列) |
  90 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID(多租户隔离,标准列) |
  91 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID(组织层级隔离,标准列) |
  92 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) |
  93 +| `sEmployeeNo` | varchar(50) | 否 | — | 员工号;系统内唯一 |
  94 +| `sName` | varchar(100) | 否 | — | 姓名 |
  95 +| `iDepartmentId` | int | 是 | NULL | 部门 ID,关联 `t_department.iIncrement` |
  96 +| `sPhone` | varchar(20) | 是 | NULL | 手机号 |
  97 +| `sEmail` | varchar(100) | 是 | NULL | 邮箱 |
  98 +| `bDisabled` | tinyint(1) | 否 | 0 | 是否离职:0 在职 / 1 离职 |
  99 +
  100 +### 索引
  101 +
  102 +- `uk_employee_no` (UNIQUE): `sEmployeeNo`
  103 +- `idx_employee_dept` (BTREE): `iDepartmentId`
  104 +- `idx_employee_name` (BTREE): `sName`
  105 +- `idx_employee_tenant` (BTREE): `sBrandsId`, `sSubsidiaryId`
  106 +
  107 +### 外键
  108 +
  109 +- `fk_employee_department`: `iDepartmentId` → `t_department.iIncrement` (ON DELETE SET NULL / ON UPDATE RESTRICT)
  110 +
  111 +### 业务注记
  112 +
  113 +- 用户表通过 `iEmployeeId` 关联本表;REQ-USR-002 / 003 选择员工时下拉数据源;REQ-USR-004 查询字段「员工名 / 部门」均联表本表。
  114 +- 离职员工保留记录(`bDisabled = 1`),其关联用户应被自动作废(应用层逻辑,未做 DB 触发器)。
  115 +
  116 +## `t_department` — 部门组织树
  117 +
  118 +### 字段
  119 +
  120 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  121 +|---|---|---|---|---|
  122 +| `iIncrement` | int | 否 | 自增函数 | 整数主键 ID(标准列) |
  123 +| `sId` | varchar(100) | 是 | uuid 函数 | 业务 ID(标准列) |
  124 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID(多租户隔离,标准列) |
  125 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID(组织层级隔离,标准列) |
  126 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) |
  127 +| `sName` | varchar(100) | 否 | — | 部门名称 |
  128 +| `sCode` | varchar(50) | 否 | — | 部门编码;系统内唯一 |
  129 +| `iParentId` | int | 是 | NULL | 上级部门 ID,NULL 表示根部门 |
  130 +| `iSortOrder` | int | 否 | 0 | 排序值,小者靠前 |
  131 +
  132 +### 索引
  133 +
  134 +- `uk_department_code` (UNIQUE): `sCode`
  135 +- `idx_department_parent` (BTREE): `iParentId`
  136 +- `idx_department_tenant` (BTREE): `sBrandsId`, `sSubsidiaryId`
  137 +
  138 +### 外键
  139 +
  140 +- `fk_department_parent`: `iParentId` → `t_department.iIncrement` (ON DELETE RESTRICT / ON UPDATE RESTRICT)
  141 +
  142 +### 业务注记
  143 +
  144 +- 自引用形成部门树,根部门 `iParentId IS NULL`。
  145 +- 部门删除采用「拒绝删除有下级 / 有员工的部门」策略(应用层校验);DB 外键 RESTRICT 兜底。
  146 +
  147 +## `t_permission` — 权限分类字典
  148 +
  149 +### 字段
  150 +
  151 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  152 +|---|---|---|---|---|
  153 +| `iIncrement` | int | 否 | 自增函数 | 整数主键 ID(标准列) |
  154 +| `sId` | varchar(100) | 是 | uuid 函数 | 业务 ID(标准列) |
  155 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID(多租户隔离,标准列) |
  156 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID(组织层级隔离,标准列) |
  157 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) |
  158 +| `sCode` | varchar(50) | 否 | — | 权限码,例如 `USR:ADD` / `USR:EDIT`;系统内唯一 |
  159 +| `sName` | varchar(100) | 否 | — | 权限分类名称(展示用) |
  160 +| `iSortOrder` | int | 否 | 0 | 同分类内排序 |
  161 +
  162 +### 索引
  163 +
  164 +- `uk_permission_code` (UNIQUE): `sCode`
  165 +
  166 +### 外键
  167 +
  168 +- 无
  169 +
  170 +### 业务注记
  171 +
  172 +- 字典表,启动时由初始化脚本灌入;不允许业务删除,禁用通过软标记(暂未引入 `bVoid`,由 docs/01 补充时再加)。
  173 +- 权限码格式 `<模块代码>:<动作>`,与后端 `@PreAuthorize('hasAuthority(...)')` 一一对应(docs/06 § 1.3)。
  174 +
  175 +## `t_user_permission` — 用户-权限分类关联表
  176 +
  177 +### 字段
  178 +
  179 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  180 +|---|---|---|---|---|
  181 +| `iIncrement` | int | 否 | 自增函数 | 整数主键 ID(标准列) |
  182 +| `sId` | varchar(100) | 是 | uuid 函数 | 业务 ID(标准列) |
  183 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID(多租户隔离,标准列) |
  184 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID(组织层级隔离,标准列) |
  185 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) |
  186 +| `iUserId` | int | 否 | — | 用户 ID,关联 `t_user.iIncrement` |
  187 +| `iPermissionId` | int | 否 | — | 权限分类 ID,关联 `t_permission.iIncrement` |
  188 +
  189 +### 索引
  190 +
  191 +- `uk_user_perm` (UNIQUE): `iUserId`, `iPermissionId`
  192 +- `idx_user_perm_perm` (BTREE): `iPermissionId`
  193 +
  194 +### 外键
  195 +
  196 +- `fk_userperm_user`: `iUserId` → `t_user.iIncrement` (ON DELETE CASCADE / ON UPDATE RESTRICT)
  197 +- `fk_userperm_perm`: `iPermissionId` → `t_permission.iIncrement` (ON DELETE RESTRICT / ON UPDATE RESTRICT)
  198 +
  199 +### 业务注记
  200 +
  201 +- 关联表豁免 `sBrandsId` / `sSubsidiaryId` 的业务语义但保留列以遵守项目标准;写入时复用 `t_user` 的对应租户标识。
  202 +- 删除用户时级联清理本表行(`ON DELETE CASCADE`);删除字典项保护已分配数据(`ON DELETE RESTRICT`)。
  203 +- 唯一约束 `(iUserId, iPermissionId)` 确保同一用户对同一权限分类不重复授权。
  204 +
  205 +## `t_company` — 公司 / 版本字典
  206 +
  207 +### 字段
  208 +
  209 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  210 +|---|---|---|---|---|
  211 +| `iIncrement` | int | 否 | 自增函数 | 整数主键 ID(标准列) |
  212 +| `sId` | varchar(100) | 是 | uuid 函数 | 业务 ID(标准列) |
  213 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID(多租户隔离,标准列) |
  214 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID(组织层级隔离,标准列) |
  215 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) |
  216 +| `sCode` | varchar(50) | 否 | — | 公司 / 版本编码;系统内唯一 |
  217 +| `sName` | varchar(100) | 否 | — | 显示名称 |
  218 +
  219 +### 索引
  220 +
  221 +- `uk_company_code` (UNIQUE): `sCode`
  222 +
  223 +### 外键
  224 +
  225 +- 无
  226 +
  227 +### 业务注记
  228 +
  229 +- REQ-USR-001 登录入参「版本」下拉数据源;启动时由初始化脚本插入至少一行默认公司数据;具体「版本」字段语义待后续业务确认后再补列。
  230 +- 当前 REQ 描述不足以确认与 `t_user` 是否需要强关联,故先做独立字典;后续如需「用户绑定公司 / 版本」再于 `t_user` 增列。
docs/04-技术规范.md 0 → 100644
  1 +++ a/docs/04-技术规范.md
  1 +# 04-技术规范
  2 +
  3 +## 零、技术栈总览
  4 +
  5 +| 分层模块 | 技术 | 版本要求 | 说明 |
  6 +|---|---|---|---|
  7 +| 前端基础框架 | React | 18.x | 构建前端应用 |
  8 +| 前端 UI 组件 | Ant Design | 5.x | 页面组件与交互控件 |
  9 +| 前端状态管理 | Redux Toolkit | 最新稳定版 | 管理全局状态 |
  10 +| 前端路由管理 | React Router | v6 | 页面路由与导航 |
  11 +| 前端工程化构建 | Vite | 最新稳定版 | 前端开发与打包构建 |
  12 +| 前端接口通信 | Axios | 最新稳定版 | 调用后端 API |
  13 +| 后端基础框架 | Spring Boot | 3.x | 构建后端服务 |
  14 +| 后端数据访问 | MyBatis-Plus | 最新稳定版 | 数据库访问与 ORM 增强 |
  15 +| 工作流引擎 | Activiti | 6.x | 审批流、流程流转 |
  16 +| 缓存服务 | Redis | 最新稳定版 | 缓存、会话、分布式能力 |
  17 +| 报表打印 | JXLS | 2.8.1 | 基于 Excel 模板生成报表 |
  18 +| Excel 导入导出 | EasyExcel | 4.0.3 | Excel 数据导入导出 |
  19 +| 关系型数据库 | MySQL | 8.x | 核心业务数据存储 |
  20 +| 数据库 schema 迁移 | Flyway (`flyway-core` + `flyway-mysql`) | 10.x / 最新稳定版 | `sql/migrations/V_n__*.sql` 顺序 apply;Spring Boot 启动时自动应用 |
  21 +| 接口风格 | RESTful API | 统一规范 | 前后端接口设计规范 |
  22 +| 权限认证 | Spring Security / JWT | 最新稳定版 | 登录认证、权限控制 |
  23 +| API 文档 | OpenAPI / Swagger | 最新稳定版 | 接口文档与调试 |
  24 +| 项目构建管理 | Maven | 3.9.x | Java 项目依赖与构建 |
  25 +| JDK 运行环境 | Java | 17 / 21 | Spring Boot 3 推荐版本 |
  26 +| 部署容器 | Docker | 最新稳定版 | 容器化部署 |
  27 +| Web 服务器 / 反向代理 | Nginx | 最新稳定版 | 前端托管、反向代理、负载分发 |
  28 +| 日志管理 | Logback | 默认集成 / 最新稳定版 | 应用日志输出 |
  29 +| 对象映射工具 | MapStruct | 最新稳定版 | DTO / VO / Entity 转换 |
  30 +| 工具类库 | Hutool / Apache Commons | 最新稳定版 | 常用工具方法支持 |
  31 +
  32 +> 本表由 scope-lock 锁定。后续所有规范基于此表推导。
  33 +
  34 +## 一、后端规范
  35 +
  36 +### 1.1 分层结构
  37 +
  38 +| 层 | 职责 |
  39 +|---|---|
  40 +| `controller/` | 接收 HTTP 请求、参数校验(`@Valid`)、调用 Service、组装响应;不写业务逻辑 |
  41 +| `service/` | 业务编排、事务边界、跨 Mapper 调用;接口 + `impl/` 实现 |
  42 +| `mapper/` | MyBatis-Plus 数据访问(Java 接口 + XML),仅做单表 CRUD 与简单关联查询 |
  43 +| `entity/` | 与数据库表 1:1 的实体类,字段名/类型严格对齐 docs/03 |
  44 +| `dto/` | 入参对象(前端 → 后端),含 `@NotNull` / `@Pattern` 等校验注解 |
  45 +| `vo/` | 出参对象(后端 → 前端),由 MapStruct 从 Entity 转换 |
  46 +| `config/` | Spring 配置类(Security / Redis / MyBatis-Plus / Swagger / Activiti) |
  47 +| `common/` | 全局响应包装、统一异常处理器、拦截器、工具类 |
  48 +| `security/` | JWT 生成 / 验证、`UserDetailsService` 实现、权限注解 |
  49 +
  50 +### 1.2 命名约定
  51 +
  52 +- **包名**:全小写,单数。根包 `com.example.erp`。业务模块包 `<ROOT>.module.<模块代码小写>`(示例:`com.example.erp.module.usr`)。
  53 +- **类名**:大驼峰;按层级加后缀(`UserController` / `UserServiceImpl` / `UserMapper` / `UserCreateDTO` / `UserListVO`)。
  54 +- **方法名**:小驼峰;动词开头(`createUser` / `listUsers` / `disableUser`),与 REST 动作语义对齐。
  55 +- **常量**:全大写下划线(`MAX_LOGIN_FAILS = 5`),放对应业务模块的 `constant/` 子包或公共 `common/constant/`。
  56 +- **示例 1**:`com.example.erp.module.usr.controller.UserController#listUsers(UserQueryDTO)` 返回 `PageVO<UserListVO>`。
  57 +- **示例 2**:`com.example.erp.module.usr.service.impl.UserServiceImpl#disableUser(Long id)` 写入 `t_user_audit_log` 审计。
  58 +
  59 +### 1.3 统一响应格式
  60 +
  61 +后端所有 HTTP 接口返回 `Result<T>` 包装:
  62 +
  63 +```json
  64 +// 成功
  65 +{ "code": 0, "message": "ok", "data": { ... } }
  66 +// 失败
  67 +{ "code": 40001, "message": "用户名已存在", "data": null }
  68 +```
  69 +
  70 +错误码段位:
  71 +
  72 +| 段位 | 含义 |
  73 +|---|---|
  74 +| `0` | 成功 |
  75 +| `1xxxx` | 系统级(参数校验、未授权、未登录) |
  76 +| `2xxxx` | 通用业务错误(资源不存在、状态非法) |
  77 +| `4xxxx` | 模块业务错误(按模块再分子段:USR=`40xxx`,PUR=`41xxx`...) |
  78 +| `5xxxx` | 第三方 / 基础设施错误(DB / Redis / 外部服务) |
  79 +
  80 +### 1.4 异常处理
  81 +
  82 +- **全局异常处理器**:`@RestControllerAdvice` 统一捕获,转换为 `Result` 失败结构。
  83 +- **必须 catch**:`MethodArgumentNotValidException`(参数校验)/ `BindException` / 业务自定义 `BizException` / `AccessDeniedException`。
  84 +- **禁止 catch**:泛 `Exception` 在业务代码中吞掉;必须让全局处理器接管。
  85 +- **接口响应禁止回显后端异常堆栈**:返回用户友好错误码 + 文案;堆栈仅写入 logback。
  86 +
  87 +### 1.5 事务
  88 +
  89 +- **事务边界**:放在 Service 层方法上(`@Transactional`),Controller / Mapper 禁止开事务。
  90 +- **传播策略**:默认 `REQUIRED`;只读查询用 `@Transactional(readOnly = true)`。
  91 +- **跨服务调用禁止开新事务嵌套**:跨进程 / 跨服务的一致性走「最终一致 + 补偿」或 Activiti 工作流,不要用分布式事务。
  92 +
  93 +### 1.6 认证
  94 +
  95 +- **协议**:JWT(HS256),头 `Authorization: Bearer <token>`。
  96 +- **生命周期**:access token 8 小时,refresh token 7 天;登录返回两枚 token;access 过期前端凭 refresh 刷新一次。
  97 +- **刷新机制**:refresh token 只能用于换 access,不能直接调业务接口;服务端可吊销(Redis 存吊销名单)。
  98 +- **密钥管理**:JWT 签名密钥放 `.env.local` 的 `JWT_SECRET`,至少 32 字节;生产环境必须更换默认值。
  99 +- **登录失败锁定**:连续 5 次失败锁账户 30 分钟,Redis 计数器 key `login:fail:<username>`。
  100 +
  101 +## 二、前端规范
  102 +
  103 +### 2.1 目录约定
  104 +
  105 +| 目录 | 职责 |
  106 +|---|---|
  107 +| `src/api/` | Axios 实例 + 每模块一个 API 文件;**所有 HTTP 调用唯一入口** |
  108 +| `src/components/` | 跨页面通用组件(`AuthButton` / `AppTable` / `PageHeader`) |
  109 +| `src/pages/` | 业务页面,按模块组织子目录 |
  110 +| `src/store/` | Redux Toolkit slice,仅放真正的全局状态 |
  111 +| `src/hooks/` | 自定义 hook(`useAuth` / `usePagination` / `useDebounce`) |
  112 +| `src/utils/` | 纯函数工具(格式化、校验、正则) |
  113 +| `src/styles/` | `tokens.css` + 全局样式 |
  114 +| `src/router/` | 路由表 + 路由守卫 |
  115 +
  116 +> **前端禁止直接写 SQL / 操作 DB**,所有数据访问走 `api/` 层统一封装。
  117 +
  118 +### 2.2 状态管理
  119 +
  120 +- **Redux 存什么**:全局共享 + 跨页面持久(当前用户信息、权限码列表、菜单树、字典)。
  121 +- **Redux 不存什么**:单个页面的表单值、Modal 开关、表格分页参数 → 放组件 `useState` / `useReducer`。
  122 +- **服务端数据**:业务数据(列表、明细)不要塞 Redux,每次进入页面走 `api/` 取最新;只有字典 / 全局枚举做内存缓存(`store/slices/dictSlice`)。
  123 +
  124 +### 2.3 请求封装
  125 +
  126 +- **Axios 实例**:`src/api/http.ts` 统一创建,`baseURL` 取 `import.meta.env.VITE_API_BASE`。
  127 +- **请求拦截器**:自动注入 `Authorization: Bearer <accessToken>`;token 临期则先刷新再发起原请求。
  128 +- **响应拦截器**:剥 `Result.data` 给业务;`code !== 0` 时 `message.error(message)` 并 reject。
  129 +- **超时**:默认 15s;上传 / 导出接口单独 60s。
  130 +- **错误重试**:仅对幂等 GET 重试 1 次,POST / PUT / DELETE 禁止重试。
  131 +
  132 +### 2.4 错误处理
  133 +
  134 +- **网络错误**:Axios `error.code === 'ERR_NETWORK'` → 全局 `message.error('网络异常,请检查连接')`。
  135 +- **业务错误**:`code !== 0` 在响应拦截器统一弹 message;页面只需处理 `try/catch` 中的 reject。
  136 +- **页面级错误**:路由级 `ErrorBoundary` 包顶层,组件 throw 时显示统一错误页(docs/06 § 1.2)。
  137 +
  138 +### 2.5 样式与主题
  139 +
  140 +- **CSS 变量命名**:`--color-<scope>-<role>-<state>`
  141 + - `scope` = `form` / `table-row` / `btn` / `link` / ...
  142 + - `role` = `bg` / `fg` / `border`
  143 + - `state` = `edit` / `readonly` / `hover` / `selected`
  144 +- **文件位置**:`src/styles/tokens.css`,由 skeleton-gen 生成空骨架,色值由 docs/06 § 二锁定后填入。
  145 +- **组件样式**:只用 `var(--color-xxx)`,禁止硬编码 hex / rgba。
  146 +- **Ant Design 主题对接**:`<ConfigProvider theme={{ token: { colorPrimary: 'var(--color-primary)', ... } }}>` 把 tokens 注入 antd 主题。
  147 +
  148 +具体 token 表见 docs/06 § 二。
  149 +
  150 +## 三、共同约定
  151 +
  152 +### 3.1 Git 提交
  153 +
  154 +`<type>(<scope>): <subject> REQ-XXX-NNN`
  155 +
  156 +- `type` ∈ `feat` / `fix` / `refactor` / `test` / `docs` / `chore` / `style`
  157 +- `scope` 为模块代码小写(`usr` / `pur` / `sal`)或 `infra`
  158 +- `subject` 50 字以内动词开头
  159 +- 末尾必须挂 `REQ-XXX-NNN`(如 `REQ-USR-001`),CI 用此挂关联
  160 +
  161 +### 3.2 分页查询
  162 +
  163 +- **后端入参**:`PageQuery { pageNum: int = 1, pageSize: int = 20 }`(`pageSize` 上限 100)+ 业务过滤字段;继承基类避免重复声明。
  164 +- **后端出参**:`PageVO<T> { records: List<T>, total: long, pageNum: int, pageSize: int }`。
  165 +- **前端组件**:统一封装 `<AppTable />` 内置 antd `Table` + `Pagination`,受控 `current` / `pageSize` / `total`。
  166 +- **默认排序**:按创建时间倒序;可按列点击切换。
  167 +
  168 +### 3.3 日期与金额
  169 +
  170 +- **后端类型**:日期 `LocalDate`,时间 `LocalDateTime`,金额 `BigDecimal`(scale=2,HALF_UP)。
  171 +- **序列化**:Jackson 全局配置 `LocalDateTime` ↔ `yyyy-MM-dd HH:mm:ss`;`BigDecimal` 序列化为字符串避免精度丢失。
  172 +- **前端展示**:`utils/format.ts` 提供 `formatDate(d)` / `formatMoney(n)`;金额展示固定 2 位小数 + 千分位(`¥1,234.56`)。
  173 +
  174 +### 3.4 数据访问规约
  175 +
  176 +- **SELECT 字段显式列举**,禁止 `SELECT *`;Mapper XML 用 `<sql id="Base_Column_List">` 集中维护。
  177 +- **禁止 N+1**:循环中不得执行 DB 查询;改用批量查(`IN (?,?,?)`)/ JOIN / `<foreach>`。
  178 +- **表名 / 字段名**:Mapper XML 中通过 `<sql>` 片段或常量引用,禁止字符串拼接(防 SQL 注入 + 利于改名)。
  179 +- **分页**:统一用 MyBatis-Plus `Page<T>` + `selectPage(...)`,禁止 LIMIT 字符串拼接。
  180 +
  181 +### 3.5 配置与安全
  182 +
  183 +- **配置**:DB 连接 / 端口 / 密钥 / 第三方 URL 等一律放 `application.yml` + `.env.local`,代码里**禁止硬编码**;`application.yml` 用 `${VAR_NAME:default}` 引用环境变量。
  184 +- **前端安全**:
  185 + - `localStorage` **不存敏感信息**(token / 身份 / 个人数据);推荐 HttpOnly Cookie 或 「内存 access token + HttpOnly refresh cookie」组合。
  186 + - 接口响应禁止回显后端异常堆栈(与 § 1.4 一致)。
  187 + - XSS:所有用户输入展示走 React JSX 自动转义;`dangerouslySetInnerHTML` 禁用,除非内容来源已白名单化。
docs/05-API接口契约.md 0 → 100644
  1 +++ a/docs/05-API接口契约.md
  1 +# 05-API接口契约
  2 +
  3 +BasePath: `/api`
  4 +端口: `8080`
  5 +
  6 +## 全局约定
  7 +
  8 +### 响应格式
  9 +```json
  10 +{"code": 200, "message": "操作成功", "data": {}, "timestamp": 1700000000000}
  11 +```
  12 +
  13 +### 错误码
  14 +| 范围 | 含义 |
  15 +|---|---|
  16 +| 200 | 成功 |
  17 +| 400xx | 客户端参数错误 |
  18 +| 401xx | 认证/授权错误 |
  19 +| 403xx | 权限不足 |
  20 +| 404xx | 资源不存在 |
  21 +| 500xx | 服务端内部错误 |
  22 +
  23 +### 鉴权
  24 +
  25 +所有业务接口走 JWT Bearer。客户端在 `Authorization: Bearer <accessToken>` 头中携带访问令牌。
  26 +
  27 +- 登录 `POST /api/usr/auth/login` 返回 `accessToken`(8 小时)+ `refreshToken`(7 天)
  28 +- 临期前端用 refresh token 换新 access token:`POST /api/usr/auth/refresh`
  29 +- 注销 `POST /api/usr/auth/logout` 服务端把 token 加入 Redis 吊销名单
  30 +- 未携带 / 过期 / 已吊销 → 401,对应错误码 `40101 未登录` / `40102 Token 已过期` / `40103 Token 已吊销`
  31 +
  32 +### 分页参数
  33 +
  34 +统一查询入参:
  35 +
  36 +| 字段 | 类型 | 必填 | 默认 | 说明 |
  37 +|---|---|---|---|---|
  38 +| `pageNum` | int | 否 | 1 | 当前页码,从 1 开始 |
  39 +| `pageSize` | int | 否 | 20 | 每页条数;上限 100 |
  40 +| `orderBy` | string | 否 | `tCreateDate DESC` | 排序表达式,列名 + 方向 |
  41 +
  42 +响应包装:
  43 +
  44 +```json
  45 +{
  46 + "code": 200, "message": "ok",
  47 + "data": {
  48 + "records": [ ... ],
  49 + "total": 1234,
  50 + "pageNum": 1,
  51 + "pageSize": 20
  52 + },
  53 + "timestamp": 1700000000000
  54 +}
  55 +```
  56 +
  57 +## 接口清单
  58 +(各模块接口段落见下方,由 `downstream-gen` 按 REQ 填入)
  59 +
  60 +## module_usr — USR 用户管理
  61 +
  62 +### REQ-USR-001 用户登录
  63 +
  64 +- **Method**: POST
  65 +- **Path**: `/api/usr/auth/login`
  66 +- **Auth**: 公开(无需 JWT)
  67 +- **请求**:
  68 + ```json
  69 + {
  70 + "userName": "admin",
  71 + "password": "P@ssw0rd",
  72 + "companyVersion": "STANDARD"
  73 + }
  74 + ```
  75 + - `userName` (string, 必填, 3-50 位)
  76 + - `password` (string, 必填, 明文传输,由 TLS 保证机密性;后端用 BCrypt/Argon2 比对哈希)
  77 + - `companyVersion` (string, 必填, 枚举 `STANDARD` / `PRO` / `FLAGSHIP`)
  78 +- **响应**:
  79 + ```json
  80 + {
  81 + "code": 200, "message": "ok",
  82 + "data": {
  83 + "accessToken": "eyJ...",
  84 + "refreshToken": "eyJ...",
  85 + "expiresIn": 28800,
  86 + "user": {
  87 + "iIncrement": 1,
  88 + "sUserName": "admin",
  89 + "sUserType": "SUPER_ADMIN",
  90 + "sLanguage": "zh-CN",
  91 + "permissionCodes": ["USR:ADD", "USR:EDIT", "USR:DELETE", "USR:VIEW"]
  92 + }
  93 + },
  94 + "timestamp": 1700000000000
  95 + }
  96 + ```
  97 +
  98 +#### 错误码
  99 +- `40001` — 用户名或密码错误(不区分两者,防爆破)
  100 +- `40301` — 账号已锁定,请稍后再试
  101 +- `40302` — 账号已作废
  102 +- `40010` — 参数校验失败(用户名 / 密码 / 版本字段为空或格式不合法)
  103 +
  104 +---
  105 +
  106 +### REQ-USR-002 新增用户
  107 +
  108 +- **Method**: POST
  109 +- **Path**: `/api/usr/users`
  110 +- **Auth**: JWT;要求权限码 `USR:ADD`
  111 +- **请求**:
  112 + ```json
  113 + {
  114 + "userNo": "U10001",
  115 + "userName": "zhangsan",
  116 + "employeeId": 12,
  117 + "userType": "NORMAL",
  118 + "language": "zh-CN",
  119 + "modifyDoc": false,
  120 + "permissionIds": [3, 5, 8]
  121 + }
  122 + ```
  123 + - `userNo` / `userName` 必填且系统内唯一
  124 + - `employeeId` 可空(非员工账号)
  125 + - `userType` 枚举 `NORMAL` / `SUPER_ADMIN`(仅超级管理员可创建超级管理员)
  126 + - `language` 枚举 `zh-CN` / `en-US` / `zh-TW`
  127 + - `permissionIds` 权限分类 ID 数组,可为空
  128 + - 初始密码后端固定生成(`666666`),不在请求体中
  129 +- **响应**:
  130 + ```json
  131 + { "code": 200, "message": "ok", "data": { "userNo": "U10001", "iIncrement": 42 }, "timestamp": 1700000000000 }
  132 + ```
  133 +
  134 +#### 错误码
  135 +- `40001` — 用户名 / 用户号已存在
  136 +- `40010` — 参数校验失败
  137 +- `40310` — 普通用户无权创建超级管理员
  138 +- `40411` — 关联职员不存在
  139 +- `40412` — 关联权限分类不存在
  140 +
  141 +---
  142 +
  143 +### REQ-USR-003 修改用户
  144 +
  145 +- **Method**: PUT
  146 +- **Path**: `/api/usr/users/{iIncrement}`
  147 +- **Auth**: JWT;要求权限码 `USR:EDIT`
  148 +- **请求**:
  149 + ```json
  150 + {
  151 + "employeeId": 12,
  152 + "userType": "NORMAL",
  153 + "language": "en-US",
  154 + "modifyDoc": true,
  155 + "void": false,
  156 + "permissionIds": [3, 5]
  157 + }
  158 + ```
  159 + - `userName` 不可修改,故请求体不包含
  160 + - `void = true` 等价于禁用(软删除)
  161 + - `permissionIds` 全量覆盖(先 DELETE 后 INSERT,事务内)
  162 +- **响应**:
  163 + ```json
  164 + { "code": 200, "message": "ok", "data": { "iIncrement": 42 }, "timestamp": 1700000000000 }
  165 + ```
  166 +
  167 +#### 错误码
  168 +- `40404` — 目标用户不存在
  169 +- `40310` — 不可修改超级管理员(非超级管理员调用)
  170 +- `40311` — 不可禁用 / 修改自己
  171 +- `40411` — 关联职员不存在
  172 +- `40412` — 关联权限分类不存在
  173 +- `40010` — 参数校验失败
  174 +
  175 +---
  176 +
  177 +### REQ-USR-004 查询用户
  178 +
  179 +- **Method**: GET
  180 +- **Path**: `/api/usr/users`
  181 +- **Auth**: JWT;要求权限码 `USR:VIEW`
  182 +- **请求**: query string
  183 + - `searchField` 枚举 `userName` / `employeeName` / `userNo` / `departmentName` / `userType` / `void` / `lastLoginDate` / `creator`
  184 + - `matchMode` 枚举 `CONTAINS` / `NOT_CONTAINS` / `EQUALS`
  185 + - `searchValue` 字符串,空则不过滤
  186 + - `includeVoid` 布尔,默认 false(不返回已作废用户)
  187 + - `pageNum` / `pageSize` / `orderBy` 通用分页参数
  188 +- **响应**: 分页结构,`records[]` 元素:
  189 + ```json
  190 + {
  191 + "iIncrement": 42,
  192 + "sUserName": "zhangsan",
  193 + "employeeName": "张三",
  194 + "sUserNo": "U10001",
  195 + "departmentName": "技术部",
  196 + "sUserType": "NORMAL",
  197 + "sLanguage": "zh-CN",
  198 + "bVoid": false,
  199 + "tLastLoginDate": "2026-05-13 09:12:33",
  200 + "sCreator": "admin",
  201 + "tCreateDate": "2025-12-01 10:00:00"
  202 + }
  203 + ```
  204 + - 不返回 `sPasswordHash` / `iLoginFailCount` / `tLockUntil`
  205 +
  206 +#### 错误码
  207 +- `40010` — 参数校验失败(`pageSize > 100` / `matchMode` 不合法 / `searchField` 不合法)
docs/06-UI交互规范.md 0 → 100644
  1 +++ a/docs/06-UI交互规范.md
  1 +# 06-UI交互规范
  2 +
  3 +> 本项目所有页面布局以项目根 `prototype/` 目录下的静态 HTML mockup 为权威。前端阶段(fe-feature-*)实现时直接以 prototype/ HTML 推导组件树与样式。本文件仅承载跨页面通用规则与 Design Tokens。
  4 +
  5 +## 一、通用交互规则
  6 +
  7 +### 1.1 操作反馈
  8 +
  9 +- **成功提示**:使用 Ant Design `message.success()`,默认 3s 自动消失;新增 / 修改 / 删除等写操作必须给反馈。
  10 +- **失败提示**:使用 `message.error()` 显示后端返回的 `message` 字段;表单字段级错误用 Form 的 `validateStatus="error"` + `help` 同步显示。
  11 +- **危险操作二次确认**:删除 / 禁用 / 重置密码等不可逆操作必须用 `Modal.confirm({ okType: 'danger' })` 二次确认,按钮文案使用业务动词(如「确认禁用」),不用「确定」。
  12 +- **长耗时按钮 loading**:提交类按钮在请求未返回前必须置为 `loading=true` 并禁用,避免重复提交;查询类操作在表格组件上启用 `loading` 属性即可。
  13 +
  14 +### 1.2 数据展示
  15 +
  16 +- **空状态**:列表 / 表格无数据时使用 `<Empty description="暂无数据" />`,明细页无数据时附加「返回列表」按钮。
  17 +- **加载状态**:页面级加载用 `<Spin spinning>` 包裹主区域;表格 / 卡片局部加载用组件自带 `loading` 属性。
  18 +- **异常状态**:接口 5xx 或网络异常时显示 `<Result status="error" title="加载失败" extra={<Button>重试</Button>} />`;403 时显示「无权限」并提供「返回首页」按钮。
  19 +
  20 +### 1.3 权限控制(前端)
  21 +
  22 +- **菜单级**:登录后由后端返回菜单权限码列表,前端 Layout 按权限码过滤后渲染左侧菜单。
  23 +- **按钮级**:包装 `<AuthButton code="USR:ADD">新增</AuthButton>`,权限码不在用户列表时直接不渲染(避免显示再禁用)。
  24 +- **路由级**:在 React Router 外层包 `<AuthRoute />`,路由元数据声明 `meta.code`,无权限重定向 403 页面。
  25 +- **关联后端 RBAC**:权限码格式 `<模块代码>:<动作>`(如 `USR:ADD` / `USR:EDIT` / `USR:DELETE`),与后端 Spring Security `@PreAuthorize("hasAuthority('USR:ADD')")` 一一对应。
  26 +
  27 +## 二、Design Tokens
  28 +
  29 +> 所有色值统一以 CSS 变量定义于 `src/styles/tokens.css`;命名规范见 docs/04 § 2.5。
  30 +
  31 +### 2.1 全局调色板
  32 +
  33 +与 Ant Design 5 主题对齐,统一通过 `<ConfigProvider theme={{ token: {...} }}>` 注入。
  34 +
  35 +| 语义 | 变量名 | 默认值 | 用途 |
  36 +|---|---|---|---|
  37 +| 主色 | `--color-primary` | `#1677ff` | 主操作按钮 / 链接 / 选中态 |
  38 +| 成功 | `--color-success` | `#52c41a` | 成功消息 / 启用状态标签 |
  39 +| 警告 | `--color-warning` | `#faad14` | 警告消息 / 待处理状态 |
  40 +| 错误 | `--color-error` | `#ff4d4f` | 错误消息 / 危险按钮 / 禁用状态 |
  41 +| 主文字 | `--color-text-primary` | `rgba(0, 0, 0, 0.88)` | 正文 / 表格内容 |
  42 +| 次文字 | `--color-text-secondary` | `rgba(0, 0, 0, 0.65)` | 辅助说明 / 占位 |
  43 +| 边框 | `--color-border` | `#d9d9d9` | 表单输入 / 卡片边界 |
  44 +| 背景 | `--color-bg-base` | `#ffffff` | 页面主背景 |
  45 +| 弱背景 | `--color-bg-layout` | `#f5f5f5` | 页面 layout 间隙 / 灰底 |
  46 +
  47 +### 2.2 组件级状态色
  48 +
  49 +| 序号 | 组件 | 编辑bg | 只读bg | 悬浮bg | 编辑fg | 只读fg | 悬浮fg | 备注 |
  50 +|---|---|---|---|---|---|---|---|---|
  51 +| 1 | 表单输入框 | `var(--color-bg-base)` | `var(--color-bg-layout)` | `var(--color-bg-base)` | `var(--color-text-primary)` | `var(--color-text-secondary)` | `var(--color-text-primary)` | 只读态边框使用 `transparent` |
  52 +| 2 | 下拉单选 | `var(--color-bg-base)` | `var(--color-bg-layout)` | `var(--color-bg-base)` | `var(--color-text-primary)` | `var(--color-text-secondary)` | `var(--color-text-primary)` | — |
  53 +| 3 | 表格行 | — | `var(--color-bg-base)` | `var(--color-bg-row-hover)` | — | `var(--color-text-primary)` | `var(--color-text-primary)` | 编辑态走 Modal,行内不编辑 |
  54 +| 4 | 主按钮 | `var(--color-primary)` | `var(--color-bg-layout)` | `var(--color-primary-hover)` | `#ffffff` | `var(--color-text-secondary)` | `#ffffff` | 危险按钮替换 `--color-error` |
  55 +| 5 | 链接 | — | — | `var(--color-primary-hover)` | `var(--color-primary)` | — | `var(--color-primary-hover)` | — |
  56 +
  57 +**Token 默认值**:
  58 +
  59 +| Token | 默认值 |
  60 +|---|---|
  61 +| `--color-primary-hover` | `#4096ff` |
  62 +| `--color-bg-row-hover` | `#fafafa` |
  63 +
  64 +### 2.3 引用约定
  65 +
  66 +- 组件样式只用 `var(--color-xxx)`,禁止硬编码 hex / rgba。
  67 +- 新增 token 须先登记到 § 2.1 / 2.2 再补 `tokens.css`。
  68 +- 修改色值只改 `tokens.css` 一处,不允许组件覆盖。
  69 +
  70 +## 三、页面清单
  71 +
  72 +### module_usr USR-用户管理
  73 +
  74 +- **登录页** (`/login`)
  75 + - 类型: 表单页
  76 + - 对应 REQ: REQ-USR-001
  77 + - 入口菜单: 无(未登录态唯一可达页面)
  78 + - 主要交互: 表单输入 用户名 / 密码 / 版本 → 提交 → 成功跳首页 / 失败留页并显示通用错误消息;账号锁定时显示锁定提示
  79 +- **用户列表页** (`/usr/users`)
  80 + - 类型: 列表页
  81 + - 对应 REQ: REQ-USR-004
  82 + - 入口菜单: 系统管理 → 用户管理 → 用户列表
  83 + - 主要交互: 查询字段 + 匹配方式 + 查询值 三段筛选 → 表格分页展示 → 行内「编辑」「禁用 / 启用」按钮(权限码 `USR:EDIT`);顶部「新增用户」按钮(权限码 `USR:ADD`)跳新增表单
  84 +- **用户新增页** (`/usr/users/new`)
  85 + - 类型: 表单页
  86 + - 对应 REQ: REQ-USR-002
  87 + - 入口菜单: 系统管理 → 用户管理 → 用户列表 → 新增按钮
  88 + - 主要交互: 员工名(下拉单选)选择后自动回填用户号 / 用户名;类型 / 语言下拉;权限组表格(多选);提交校验失败显示字段级 `validateStatus="error"`;成功 message.success 后回列表页
  89 +- **用户编辑页** (`/usr/users/:id/edit`)
  90 + - 类型: 表单页
  91 + - 对应 REQ: REQ-USR-003
  92 + - 入口菜单: 用户列表 → 行内「编辑」按钮
  93 + - 主要交互: 复用新增表单组件;用户名只读;提供「重置密码」按钮(二次确认);提供「禁用 / 启用」开关;禁用自己时按钮置灰
docs/07-环境配置.md 0 → 100644
  1 +++ a/docs/07-环境配置.md
  1 +# 07-环境配置
  2 +
  3 +## 一、依赖清单
  4 +
  5 +| 层 | 依赖 | 版本 | 说明 |
  6 +|---|---|---|---|
  7 +| 运行时 | Java (JDK) | 17 / 21 | Spring Boot 3 推荐版本 |
  8 +| 运行时 | MySQL | 8.x | 关系数据库 |
  9 +| 运行时 | Redis | 最新稳定版 | 缓存 / 会话 |
  10 +| 运行时 | Node.js | 20.x LTS | 前端构建 + 本地 dev server |
  11 +| 构建(后端) | Maven | 3.9.x | Java 依赖与构建工具 |
  12 +| 构建(前端) | pnpm | 8.x(或 npm 10.x) | 前端依赖管理 |
  13 +| 构建(前端) | Vite | 最新稳定版 | 前端开发与打包 |
  14 +| 容器 | Docker | 最新稳定版 | 容器化部署 |
  15 +| 容器 | Docker Compose | 最新稳定版 | 本地一键启依赖(MySQL + Redis) |
  16 +| 反向代理 | Nginx | 最新稳定版 | 前端静态托管 / 反向代理 |
  17 +| CLI 工具 | git | 2.30+ | 代码版本控制 |
  18 +| CLI 工具 | mysql client | 8.x | 本地执行 SQL / 验证 migration |
  19 +| CLI 工具 | glab(可选) | 最新稳定版 | GitLab MR 创建(亦可直接走 Web) |
  20 +
  21 +## 二、端口约定
  22 +
  23 +| 服务 | 端口 | 说明 |
  24 +|---|---|---|
  25 +| 后端 HTTP | 8080 | Spring Boot 默认端口(`server.port`) |
  26 +| 前端 dev server | 5173 | Vite 默认端口(`vite --port`) |
  27 +| MySQL | 3306 | 默认端口,本地开发可用 Docker Compose 暴露 |
  28 +| Redis | 6379 | 默认端口 |
  29 +| Nginx | 80 / 443 | 生产部署反向代理入口 |
  30 +
  31 +## 三、环境变量
  32 +
  33 +运行时凭据(数据库连接、JWT 密钥等)全部放在仓库根的 `.env.local`,不入 git。
  34 +字段清单与占位符见该文件,真实值由开发者本地填写。
  35 +
  36 +## 四、常用命令
  37 +
  38 +| 命令 | 说明 |
  39 +|---|---|
  40 +| `./mvnw spring-boot:run` | 本地启动后端(含 Flyway 自动 apply migration) |
  41 +| `pnpm dev` | 本地启动前端 dev server(默认 5173) |
  42 +| `./mvnw clean package -DskipTests` | 后端打包生成 jar |
  43 +| `pnpm build` | 前端打包到 `dist/` |
  44 +| `bash scripts/test.sh` | 执行后端 + 前端测试组合(lint / unit / e2e) |
  45 +| `bash scripts/setup-test-db.sh` | 重置本地测试数据库(DROP + CREATE + apply V1) |
  46 +| `glab mr create` | 推送当前分支并创建 GitLab MR(亦可走 Web 端) |
docs/08-模块任务管理.md 0 → 100644
  1 +++ a/docs/08-模块任务管理.md
  1 +# 08-工作流进度
  2 +
  3 +> 全流程进度跟踪。CC 每完成一项产出就勾选一项。
  4 +> - **§ 一 Plan(A0~A5)**:`plan-start` 找第一个未勾 A 子项分发到对应 skill
  5 +> - **§ 二 Coding(模块)**:分发以 `docs/02-开发计划.md § 二 开发顺序清单` 为准;`coding-start` 按 docs/02 顺序扫描,对每个 REQ 所属模块查询本 § 二的 `MR:` 字段 + GitLab API `state`,找第一个非 merged 模块分发。本 § 二 行序无语义,仅作模块元数据表
  6 +
  7 +## 一、Plan 阶段(一次性)
  8 +
  9 +- [x] A0 项目初始化 — project-init
  10 + - [x] 依赖检查通过
  11 + - [x] 项目文件骨架已创建(CLAUDE.md + docs/01-需求清单/index.md + docs/04-技术规范.md)
  12 + - [x] Git 已初始化
  13 +
  14 +- [x] A1 范围锁定 — scope-lock
  15 + - [x] 项目概述已填写(CLAUDE.md § 🎯 项目概述)
  16 + - [x] 技术栈已确认(docs/04 § 零)
  17 + - [x] 需求清单索引已填写(docs/01-需求清单/index.md)
  18 + - [x] REQ 卡片骨架已生成(docs/01-需求清单/<module>/REQ-*.md,业务内容留待人工填写)
  19 +
  20 +- [x] A2 骨架生成 — skeleton-gen
  21 + - [x] 架构文档已生成(docs/04 § 一+、docs/06、docs/07、docs/09)
  22 + - [x] 工具脚本已生成(scripts/*.sh、.githooks/pre-push、.env.local)
  23 + - [x] .gitignore 已配置
  24 +
  25 +- [x] A3 DB 设计 + REQ 回填 — db-design-gen
  26 + - [x] docs/03-数据库设计文档.md 已生成
  27 + - [x] docs/01 各 REQ 卡片"依赖表" + 模块头"涉及表" 已回填
  28 +
  29 +- [x] A4 DB 初始化 — db-init
  30 + - [x] sql/migrations/V1__initial_schema.sql 已生成
  31 + - [x] DDL 与 docs/03 全量一致
  32 + - [x] .env.local 凭据已验证(mysql -e "SELECT 1" OK)
  33 + - [x] setup-test-db.sh 防护通过 + DROP+CREATE + apply V1 已执行
  34 + - [x] SHOW TABLES 行数 == docs/03 表数量
  35 +
  36 +- [x] A5 下游文档生成 — downstream-gen
  37 + - [x] docs/02 开发计划已生成
  38 + - [x] docs/05 API 契约已生成
  39 + - [x] docs/06 § 三 页面清单已填入
  40 + - [x] docs/10 验收清单已生成
  41 + - [x] 下方模块列表已填入
  42 + - [x] REQ 卡片依赖接口已回填
  43 +
  44 +## 二、Coding 阶段(后端模块循环)
  45 +
  46 +(A5 填入后,每行一个后端模块。每个模块的 `MR:` 字段在 `—` 和 `!<iid>` 之间变化,完成由 GitLab API `state=merged` 判定。`coding-start` 每次按 docs/02 REQ 序扫每模块的 MR state 决定派发。后端模块全部 merged 后自动进入 § 三 前端阶段。)
  47 +
  48 +<!-- 模块格式示例(由 A5 downstream-gen 追加;功能子项由 feature-review 在 approve 时勾选):
  49 +- module_0 系统管理
  50 + - 依赖: —
  51 + - 路径: backend/module/sys/
  52 + - MR: —
  53 + - 功能:
  54 + - [ ] REQ-SYS-001 用户登录
  55 + - [ ] REQ-SYS-002 用户注册
  56 +-->
  57 +
  58 +- module_usr USR-用户管理
  59 + - 依赖: —
  60 + - 路径: backend/src/main/java/com/example/erp/module/usr/
  61 + - MR: —
  62 + - 功能:
  63 + - [ ] REQ-USR-001 用户登录
  64 + - [ ] REQ-USR-002 新增用户
  65 + - [ ] REQ-USR-003 修改用户
  66 + - [ ] REQ-USR-004 查询用户
  67 +
  68 +## 三、Coding 阶段(前端整体)
  69 +
  70 +(`frontend-start` 进入时扫 prototype/ + docs/01 + docs/05 → AI 自主推导 FE 业务功能清单写到下方"功能:"项(无人工审阅断点;合理性由整体 MR 时统一校核)。已有清单则直接加载。整个前端阶段 1 个 MR,分支 `frontend-phase`。)
  71 +
  72 +- 整体 MR: —
  73 +- 功能:
  74 + <!-- AI 进入时按以下行格式写入(每行 1 个 FE,可关联多个 REQ / 多份原型):
  75 + - [ ] FE-NN 功能名 | 关联 REQ:REQ-A, REQ-B | 关联原型:prototype/<file>.html, prototype/<other>.html
  76 +
  77 + 示例:
  78 + - [ ] FE-01 用户登录与注册 | 关联 REQ:REQ-SYS-001, REQ-SYS-002 | 关联原型:prototype/auth.html
  79 + - [ ] FE-02 仪表盘总览 | 关联 REQ:REQ-DASH-001 | 关联原型:prototype/dashboard.html
  80 + -->
docs/09-项目目录结构.md 0 → 100644
  1 +++ a/docs/09-项目目录结构.md
  1 +# 09-项目目录结构
  2 +
  3 +## 一、仓库顶层
  4 +
  5 +```
  6 +.
  7 +├── CLAUDE.md # 项目级规范与流程指令
  8 +├── README.md # 项目说明(可选)
  9 +├── .env.local # 本地凭据(不入 git)
  10 +├── .gitignore
  11 +├── .githooks/
  12 +│ └── pre-push # 推送前自动跑 scripts/test.sh
  13 +├── scripts/
  14 +│ ├── test.sh # 后端 + 前端测试组合入口
  15 +│ └── setup-test-db.sh # 重置本地测试数据库
  16 +├── sql/
  17 +│ └── migrations/ # Flyway V*__*.sql 文件
  18 +├── docs/ # 全量项目文档(见 § 四)
  19 +├── prototype/ # 静态 HTML mockup(前端实现权威)
  20 +├── backend/ # 后端工程(Spring Boot + Maven)
  21 +└── frontend/ # 前端工程(Vite + React)
  22 +```
  23 +
  24 +## 二、后端目录
  25 +
  26 +```
  27 +backend/
  28 +├── pom.xml
  29 +└── src/
  30 + ├── main/
  31 + │ ├── java/
  32 + │ │ └── com.example.erp/
  33 + │ │ ├── ErpApplication.java
  34 + │ │ ├── common/ # 全局响应 / 异常 / 拦截器 / 工具
  35 + │ │ │ ├── result/
  36 + │ │ │ ├── exception/
  37 + │ │ │ ├── interceptor/
  38 + │ │ │ └── util/
  39 + │ │ ├── config/ # Spring 配置类(Security / Redis / MyBatis-Plus / Swagger / Activiti)
  40 + │ │ ├── module/ # 业务模块(按 docs/01 索引拆分)
  41 + │ │ │ └── usr/ # USR 用户管理
  42 + │ │ │ ├── controller/
  43 + │ │ │ ├── service/
  44 + │ │ │ │ └── impl/
  45 + │ │ │ ├── mapper/
  46 + │ │ │ ├── entity/
  47 + │ │ │ ├── dto/
  48 + │ │ │ └── vo/
  49 + │ │ └── security/ # 认证 / 鉴权 / JWT
  50 + │ └── resources/
  51 + │ ├── application.yml
  52 + │ ├── application-dev.yml
  53 + │ ├── application-prod.yml
  54 + │ ├── mapper/ # MyBatis XML(按模块再分子目录)
  55 + │ └── logback-spring.xml
  56 + └── test/
  57 + └── java/
  58 + └── com.example.erp/
  59 + └── module/
  60 + └── usr/ # 与 main/ 镜像
  61 +```
  62 +
  63 +## 三、前端目录
  64 +
  65 +```
  66 +frontend/
  67 +├── package.json
  68 +├── vite.config.ts
  69 +├── tsconfig.json
  70 +├── index.html
  71 +└── src/
  72 + ├── main.tsx # 入口(挂载 React + Router + Store + ConfigProvider)
  73 + ├── App.tsx # 顶层路由 + 全局 Layout
  74 + ├── api/ # Axios 实例 + 各模块 API(数据访问统一入口)
  75 + │ └── usr.ts # USR 用户管理 API
  76 + ├── components/ # 跨页面通用组件(AuthButton / AppTable / PageHeader 等)
  77 + ├── pages/ # 按业务模块组织页面
  78 + │ └── usr/ # USR 用户管理
  79 + │ ├── UserList.tsx
  80 + │ ├── UserEdit.tsx
  81 + │ └── Login.tsx
  82 + ├── store/ # Redux Toolkit slices(全局状态)
  83 + │ └── slices/
  84 + ├── hooks/ # 自定义 hook(useAuth / usePagination 等)
  85 + ├── utils/ # 工具函数(formatDate / formatMoney / regex 等)
  86 + ├── styles/
  87 + │ ├── tokens.css # Design Tokens(docs/06 § 二)
  88 + │ └── global.css # 全局样式 reset / typography
  89 + ├── router/ # 路由表(含 meta.code 权限码)
  90 + └── assets/ # 静态资源(图片 / 字体)
  91 +```
  92 +
  93 +> 注:仓库根 `src/styles/tokens.css` 由 skeleton-gen 创建,作为 Design Tokens 的「上游」源;前端工程化前可先保留在根 `src/styles/` 下,前端工程初始化时迁入 `frontend/src/styles/`(同名同内容)。
  94 +
  95 +## 四、docs/ 结构
  96 +
  97 +```
  98 +docs/
  99 +├── 01-需求清单/ # 每模块一子目录(_module.md 模块头 + REQ-*.md 卡片)
  100 +├── 02-开发计划.md
  101 +├── 03-数据库设计文档.md
  102 +├── 04-技术规范.md
  103 +├── 05-API接口契约.md
  104 +├── 06-UI交互规范.md
  105 +├── 07-环境配置.md
  106 +├── 08-模块任务管理.md
  107 +├── 09-项目目录结构.md
  108 +├── 10-验收检查清单.md
  109 +└── superpowers/ # CC 运行时产物
  110 +```
  111 +
  112 +## 五、命名与放置约定
  113 +
  114 +- **后端根包**:`com.example.erp`,下文统称 `<ROOT>`;新增业务模块时落到 `<ROOT>.module.<模块代码小写>`(如 `<ROOT>.module.usr`)。
  115 +- **Controller**:`<ROOT>.module.<m>.controller.<Module>Controller`,文件名首字母大写驼峰,URI 前缀 `/api/<模块代码小写>`。
  116 +- **Service**:接口 `<Module>Service` + 实现 `<Module>ServiceImpl`,实现类放 `service/impl/`。
  117 +- **Mapper**:`<Module>Mapper`(Java 接口)+ `resources/mapper/<m>/<Module>Mapper.xml`(XML 同名)。
  118 +- **Entity / DTO / VO**:
  119 + - `entity/` 数据库实体(与表 1:1,字段类型同 docs/03)
  120 + - `dto/` 入参(前端 → 后端的请求体)
  121 + - `vo/` 出参(后端 → 前端的响应体)
  122 +- **前端组件**:通用组件放 `frontend/src/components/`,文件名首字母大写驼峰(`AuthButton.tsx`);样式同名 `.module.css`(启用 CSS Modules)。
  123 +- **前端页面**:放 `frontend/src/pages/<模块代码小写>/`,文件名按业务功能命名(`UserList.tsx` / `UserEdit.tsx`),路由 path 前缀 `/<模块代码小写>`。
  124 +- **API 客户端**:每个模块一个文件 `frontend/src/api/<模块代码小写>.ts`,导出该模块所有接口函数;禁止在组件里直接 `axios.xxx`。
docs/10-验收检查清单.md 0 → 100644
  1 +++ a/docs/10-验收检查清单.md
  1 +# 10-验收检查清单
  2 +
  3 +通用验收项(全项目适用):
  4 +
  5 +- [ ] `scripts/test.sh` 本地全绿
  6 +- [ ] 所有 schema 改动都有对应 `sql/migrations/V_n__<desc>.sql`
  7 +- [ ] 所有新接口在 `docs/05` 中有契约定义
  8 +- [ ] 所有新功能代码注释含 REQ-XXX-NNN
  9 +- [ ] 统一响应格式 `{code, message, data, timestamp}`
  10 +- [ ] 异常走全局处理器,不暴露堆栈到前端
  11 +- [ ] 前端不存敏感信息到 localStorage
  12 +
  13 +> 本文档仅维护项目级验收 SOP。粒度更细的验收信息分散在:
  14 +> - **每 REQ 的业务验收点**:`docs/01-需求清单/<module>/<req_id>.md § 验收`
  15 +> - **每模块的实测验收(数据 / UI / 自动化用例位置)**:由 B 阶段 `module-report` 在该模块完成时填入模块完成报告
  16 +> - **REQ 开发进度(feature-review approve 状态)**:`docs/08 § 二`
scripts/setup-test-db.sh 0 → 100755
  1 +++ a/scripts/setup-test-db.sh
  1 +#!/usr/bin/env bash
  2 +# scripts/setup-test-db.sh — 数据库重置脚本:drop + create 空库。
  3 +# schema apply 由 Flyway 在 Spring Boot 启动时自动处理(见 docs/04 技术栈 + sql/migrations/V*.sql)。
  4 +# seed 数据由测试框架负责(Spring @Sql / Flyway R__seed.sql / data.sql)。
  5 +#
  6 +# 使用场景:
  7 +# - scripts/test.sh 开头:清空库,让 Spring 启动时 Flyway 从 V1 开始重放所有 migration
  8 +# - scripts/test.sh 结尾:清空库,避免测试遗留污染下次运行
  9 +# - 手动调试时:reset 到零状态
  10 +#
  11 +# 防护:本脚本只允许在本地 host + 测试库名上执行;非预期目标会被拒绝,
  12 +# 避免 .env.local 误指向 staging/prod 时触发不可逆 DROP。
  13 +
  14 +set -euo pipefail
  15 +
  16 +ENV_FILE="$(dirname "$0")/../.env.local"
  17 +[ -f "$ENV_FILE" ] || { echo "[setup-test-db] ⚠️ .env.local 不存在($ENV_FILE)" >&2; exit 1; }
  18 +
  19 +# 用 set -a 加载,让 KEY=VALUE 导出为环境变量;密码中含特殊字符时 .env.local 请用单引号包裹
  20 +set -a; . "$ENV_FILE"; set +a
  21 +
  22 +# 防护 1:默认只允许本地 host(localhost / 127.0.0.1 / ::1)。
  23 +# 若要为本项目额外允许某些远程 host(如公司测试 MySQL),在 .env.local 里设:
  24 +# TEST_DB_ALLOWED_HOSTS="118.178.19.35 test-mysql.internal" # 空格或逗号分隔
  25 +# 被列入者可直接 DROP CREATE,不再需要 TEST_DB_ALLOW_REMOTE=1。
  26 +ALLOWED_HOSTS="localhost 127.0.0.1 ::1 ${TEST_DB_ALLOWED_HOSTS//,/ }"
  27 +host_allowed=0
  28 +for h in $ALLOWED_HOSTS; do
  29 + [ "${DB_HOST:-}" = "$h" ] && { host_allowed=1; break; }
  30 +done
  31 +if [ "$host_allowed" -ne 1 ]; then
  32 + echo "[setup-test-db] ⚠️ 拒绝在非白名单 host (${DB_HOST}) 上执行 DROP DATABASE" >&2
  33 + echo " 当前白名单:${ALLOWED_HOSTS}" >&2
  34 + echo " 加入 host:在 .env.local 追加 TEST_DB_ALLOWED_HOSTS=\"<host1> <host2>\"" >&2
  35 + echo " 一次性绕过:TEST_DB_ALLOW_REMOTE=1 $0" >&2
  36 + [ "${TEST_DB_ALLOW_REMOTE:-0}" = "1" ] || exit 1
  37 +fi
  38 +
  39 +# 防护 2:schema 名需像测试/开发库(含 test / _dev / _local),否则要求显式确认
  40 +case "${DB_SCHEMA:-}" in
  41 + *test*|*_dev|*_local|*_ci)
  42 + ;;
  43 + *)
  44 + echo "[setup-test-db] ⚠️ schema '${DB_SCHEMA}' 不像测试库(期望命名含 test / _dev / _local / _ci)" >&2
  45 + echo " 如确为期望行为,请显式声明:TEST_DB_ALLOW_PROD_NAME=1 $0" >&2
  46 + [ "${TEST_DB_ALLOW_PROD_NAME:-0}" = "1" ] || exit 1
  47 + ;;
  48 +esac
  49 +
  50 +# 防护 3:显式 banner,让人看见自己在 drop 什么;远程 host 额外提示白名单内容
  51 +echo "[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}"
  52 +case "${DB_HOST:-}" in
  53 + localhost|127.0.0.1|::1) ;;
  54 + *)
  55 + echo "[setup-test-db] ⚠️ 目标是 **远程** host(已在 TEST_DB_ALLOWED_HOSTS 白名单中,每次 test.sh 都会 DROP)"
  56 + echo "[setup-test-db] 当前白名单: ${ALLOWED_HOSTS}"
  57 + echo "[setup-test-db] 若不希望每次自动 DROP,从 .env.local 的 TEST_DB_ALLOWED_HOSTS 删掉此 host"
  58 + ;;
  59 +esac
  60 +
  61 +MYSQL_CMD="mysql -h${DB_HOST} -P${DB_PORT} -u${DB_USER} -p${DB_PASSWORD}"
  62 +
  63 +$MYSQL_CMD -e "DROP DATABASE IF EXISTS \`${DB_SCHEMA}\`; CREATE DATABASE \`${DB_SCHEMA}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
  64 +
  65 +echo "[setup-test-db] done — schema will be applied by Flyway when Spring Boot starts"
scripts/test.sh 0 → 100755
  1 +++ a/scripts/test.sh
  1 +#!/usr/bin/env bash
  2 +# scripts/test.sh —— 合并到默认分支(main / master)前的测试闸门。
  3 +# 顺序:detect → setup-db → build → lint → unit+integration → e2e → reset-db
  4 +# 由 .githooks/pre-push 和 test-gate skill(通过子会话)调用。
  5 +
  6 +set -euo pipefail
  7 +
  8 +PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
  9 +cd "$PROJECT_ROOT"
  10 +
  11 +# Stack detection (runtime, mode-agnostic)
  12 +HAS_BACKEND=0; [ -d backend ] && HAS_BACKEND=1
  13 +HAS_FRONTEND=0; [ -d frontend ] && HAS_FRONTEND=1
  14 +if [ $HAS_BACKEND -eq 0 ] && [ $HAS_FRONTEND -eq 0 ]; then
  15 + echo "[test.sh] FATAL: neither backend/ nor frontend/ exists" >&2
  16 + exit 1
  17 +fi
  18 +
  19 +echo "[test.sh] 1/6 setup test db"
  20 +./scripts/setup-test-db.sh
  21 +
  22 +echo "[test.sh] 2/6 build"
  23 +if [ $HAS_BACKEND -eq 1 ]; then (cd backend && ./mvnw -B -DskipTests package); else echo "[test.sh] skip backend build"; fi
  24 +if [ $HAS_FRONTEND -eq 1 ]; then (cd frontend && pnpm install --frozen-lockfile && pnpm build); else echo "[test.sh] skip frontend build"; fi
  25 +
  26 +echo "[test.sh] 3/6 lint"
  27 +if [ $HAS_BACKEND -eq 1 ]; then (cd backend && ./mvnw -B -q -P lint verify -DskipTests || ./mvnw -B -q checkstyle:check spotbugs:check || :); else echo "[test.sh] skip backend lint"; fi
  28 +if [ $HAS_FRONTEND -eq 1 ]; then (cd frontend && pnpm lint); else echo "[test.sh] skip frontend lint"; fi
  29 +
  30 +echo "[test.sh] 4/6 unit + integration"
  31 +if [ $HAS_BACKEND -eq 1 ]; then (cd backend && ./mvnw -B test); else echo "[test.sh] skip backend test"; fi
  32 +if [ $HAS_FRONTEND -eq 1 ]; then (cd frontend && pnpm test -- --run); else echo "[test.sh] skip frontend test"; fi
  33 +
  34 +echo "[test.sh] 5/6 E2E"
  35 +if [ $HAS_FRONTEND -eq 1 ] && [ -f frontend/playwright.config.ts ]; then (cd frontend && pnpm exec playwright test); else echo "[test.sh] e2e 略"; fi
  36 +
  37 +echo "[test.sh] 6/6 reset test db"
  38 +./scripts/setup-test-db.sh
  39 +
  40 +echo "[test.sh] GREEN"
sql/migrations/.gitkeep 0 → 100644
  1 +++ a/sql/migrations/.gitkeep
sql/migrations/V1__initial_schema.sql 0 → 100644
  1 +++ a/sql/migrations/V1__initial_schema.sql
  1 +-- Flyway migration V1 — initial schema for 小羚羊
  2 +-- Generated: 2026-05-14T01:37:50Z
  3 +-- Source: 由 A4 db-init 从 docs/03-数据库设计文档.md 翻译生成(schema SSoT 是 docs/03)
  4 +-- This is the FIRST migration; subsequent schema changes must be written as new files sql/migrations/V2__<desc>.sql, V3__... etc.
  5 +-- Apply: Flyway runs this automatically at Spring Boot startup.
  6 +-- Do not hand-edit this file after it is committed; write a new migration instead.
  7 +
  8 +-- ===========================================================================
  9 +-- t_user — 系统用户主表,承载登录认证与基础属性
  10 +-- ===========================================================================
  11 +CREATE TABLE `t_user` (
  12 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  13 + `sId` VARCHAR(100) NULL DEFAULT (UUID()) COMMENT '业务 ID(标准列)',
  14 + `sBrandsId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID(多租户隔离,标准列)',
  15 + `sSubsidiaryId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID(组织层级隔离,标准列)',
  16 + `tCreateDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)',
  17 + `sUserNo` VARCHAR(50) NOT NULL COMMENT '用户号;关联职员后自动同步员工号;系统内唯一',
  18 + `sUserName` VARCHAR(50) NOT NULL COMMENT '登录用户名;系统内唯一;3-50 位',
  19 + `iEmployeeId` INT NULL DEFAULT NULL COMMENT '关联职员 t_employee.iIncrement;可空(非员工账号如系统管理员)',
  20 + `sPasswordHash` VARCHAR(255) NOT NULL COMMENT '密码哈希(BCrypt / Argon2);禁止明文;初始密码 666666 哈希后存入',
  21 + `sUserType` VARCHAR(20) NOT NULL DEFAULT 'NORMAL' COMMENT '用户类型枚举:NORMAL(普通用户)/ SUPER_ADMIN(超级管理员)',
  22 + `sLanguage` VARCHAR(10) NOT NULL DEFAULT 'zh-CN' COMMENT '语言枚举:zh-CN(中文)/ en-US(英文)/ zh-TW(繁体)',
  23 + `bModifyDoc` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '单据修改权限:0 否 / 1 是',
  24 + `bVoid` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '作废标记(软删除):0 启用 / 1 已作废',
  25 + `iLoginFailCount` INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数;达到阈值触发临时锁定;登录成功后清零',
  26 + `tLockUntil` DATETIME NULL DEFAULT NULL COMMENT '锁定截止时间;NULL 表示未锁定',
  27 + `tLastLoginDate` DATETIME NULL DEFAULT NULL COMMENT '最近一次登录时间',
  28 + `sCreator` VARCHAR(100) NULL DEFAULT NULL COMMENT '制单人(创建该账号的操作员用户名)',
  29 + PRIMARY KEY (`iIncrement`),
  30 + UNIQUE KEY `uk_user_username` (`sUserName`),
  31 + UNIQUE KEY `uk_user_userno` (`sUserNo`),
  32 + KEY `idx_user_employee` (`iEmployeeId`),
  33 + KEY `idx_user_tenant` (`sBrandsId`, `sSubsidiaryId`),
  34 + KEY `idx_user_void` (`bVoid`)
  35 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统用户主表,承载登录认证与基础属性';
  36 +
  37 +-- ===========================================================================
  38 +-- t_employee — 公司职员主档
  39 +-- ===========================================================================
  40 +CREATE TABLE `t_employee` (
  41 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  42 + `sId` VARCHAR(100) NULL DEFAULT (UUID()) COMMENT '业务 ID(标准列)',
  43 + `sBrandsId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID(多租户隔离,标准列)',
  44 + `sSubsidiaryId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID(组织层级隔离,标准列)',
  45 + `tCreateDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)',
  46 + `sEmployeeNo` VARCHAR(50) NOT NULL COMMENT '员工号;系统内唯一',
  47 + `sName` VARCHAR(100) NOT NULL COMMENT '姓名',
  48 + `iDepartmentId` INT NULL DEFAULT NULL COMMENT '部门 ID,关联 t_department.iIncrement',
  49 + `sPhone` VARCHAR(20) NULL DEFAULT NULL COMMENT '手机号',
  50 + `sEmail` VARCHAR(100) NULL DEFAULT NULL COMMENT '邮箱',
  51 + `bDisabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否离职:0 在职 / 1 离职',
  52 + PRIMARY KEY (`iIncrement`),
  53 + UNIQUE KEY `uk_employee_no` (`sEmployeeNo`),
  54 + KEY `idx_employee_dept` (`iDepartmentId`),
  55 + KEY `idx_employee_name` (`sName`),
  56 + KEY `idx_employee_tenant` (`sBrandsId`, `sSubsidiaryId`)
  57 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='公司职员主档';
  58 +
  59 +-- ===========================================================================
  60 +-- t_department — 部门组织树
  61 +-- ===========================================================================
  62 +CREATE TABLE `t_department` (
  63 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  64 + `sId` VARCHAR(100) NULL DEFAULT (UUID()) COMMENT '业务 ID(标准列)',
  65 + `sBrandsId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID(多租户隔离,标准列)',
  66 + `sSubsidiaryId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID(组织层级隔离,标准列)',
  67 + `tCreateDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)',
  68 + `sName` VARCHAR(100) NOT NULL COMMENT '部门名称',
  69 + `sCode` VARCHAR(50) NOT NULL COMMENT '部门编码;系统内唯一',
  70 + `iParentId` INT NULL DEFAULT NULL COMMENT '上级部门 ID,NULL 表示根部门',
  71 + `iSortOrder` INT NOT NULL DEFAULT 0 COMMENT '排序值,小者靠前',
  72 + PRIMARY KEY (`iIncrement`),
  73 + UNIQUE KEY `uk_department_code` (`sCode`),
  74 + KEY `idx_department_parent` (`iParentId`),
  75 + KEY `idx_department_tenant` (`sBrandsId`, `sSubsidiaryId`)
  76 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门组织树';
  77 +
  78 +-- ===========================================================================
  79 +-- t_permission — 权限分类字典
  80 +-- ===========================================================================
  81 +CREATE TABLE `t_permission` (
  82 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  83 + `sId` VARCHAR(100) NULL DEFAULT (UUID()) COMMENT '业务 ID(标准列)',
  84 + `sBrandsId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID(多租户隔离,标准列)',
  85 + `sSubsidiaryId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID(组织层级隔离,标准列)',
  86 + `tCreateDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)',
  87 + `sCode` VARCHAR(50) NOT NULL COMMENT '权限码,例如 USR:ADD / USR:EDIT;系统内唯一',
  88 + `sName` VARCHAR(100) NOT NULL COMMENT '权限分类名称(展示用)',
  89 + `iSortOrder` INT NOT NULL DEFAULT 0 COMMENT '同分类内排序',
  90 + PRIMARY KEY (`iIncrement`),
  91 + UNIQUE KEY `uk_permission_code` (`sCode`)
  92 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限分类字典';
  93 +
  94 +-- ===========================================================================
  95 +-- t_user_permission — 用户-权限分类关联表
  96 +-- ===========================================================================
  97 +CREATE TABLE `t_user_permission` (
  98 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  99 + `sId` VARCHAR(100) NULL DEFAULT (UUID()) COMMENT '业务 ID(标准列)',
  100 + `sBrandsId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID(多租户隔离,标准列)',
  101 + `sSubsidiaryId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID(组织层级隔离,标准列)',
  102 + `tCreateDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)',
  103 + `iUserId` INT NOT NULL COMMENT '用户 ID,关联 t_user.iIncrement',
  104 + `iPermissionId` INT NOT NULL COMMENT '权限分类 ID,关联 t_permission.iIncrement',
  105 + PRIMARY KEY (`iIncrement`),
  106 + UNIQUE KEY `uk_user_perm` (`iUserId`, `iPermissionId`),
  107 + KEY `idx_user_perm_perm` (`iPermissionId`)
  108 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户-权限分类关联表';
  109 +
  110 +-- ===========================================================================
  111 +-- t_company — 公司 / 版本字典
  112 +-- ===========================================================================
  113 +CREATE TABLE `t_company` (
  114 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  115 + `sId` VARCHAR(100) NULL DEFAULT (UUID()) COMMENT '业务 ID(标准列)',
  116 + `sBrandsId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID(多租户隔离,标准列)',
  117 + `sSubsidiaryId` VARCHAR(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID(组织层级隔离,标准列)',
  118 + `tCreateDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)',
  119 + `sCode` VARCHAR(50) NOT NULL COMMENT '公司 / 版本编码;系统内唯一',
  120 + `sName` VARCHAR(100) NOT NULL COMMENT '显示名称',
  121 + PRIMARY KEY (`iIncrement`),
  122 + UNIQUE KEY `uk_company_code` (`sCode`)
  123 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='公司 / 版本字典';
  124 +
  125 +-- ===========================================================================
  126 +-- 外键约束(统一在最后追加,避免建表顺序依赖)
  127 +-- ===========================================================================
  128 +ALTER TABLE `t_user`
  129 + ADD CONSTRAINT `fk_user_employee` FOREIGN KEY (`iEmployeeId`) REFERENCES `t_employee` (`iIncrement`) ON DELETE SET NULL ON UPDATE RESTRICT;
  130 +
  131 +ALTER TABLE `t_employee`
  132 + ADD CONSTRAINT `fk_employee_department` FOREIGN KEY (`iDepartmentId`) REFERENCES `t_department` (`iIncrement`) ON DELETE SET NULL ON UPDATE RESTRICT;
  133 +
  134 +ALTER TABLE `t_department`
  135 + ADD CONSTRAINT `fk_department_parent` FOREIGN KEY (`iParentId`) REFERENCES `t_department` (`iIncrement`) ON DELETE RESTRICT ON UPDATE RESTRICT;
  136 +
  137 +ALTER TABLE `t_user_permission`
  138 + ADD CONSTRAINT `fk_userperm_user` FOREIGN KEY (`iUserId`) REFERENCES `t_user` (`iIncrement`) ON DELETE CASCADE ON UPDATE RESTRICT;
  139 +
  140 +ALTER TABLE `t_user_permission`
  141 + ADD CONSTRAINT `fk_userperm_perm` FOREIGN KEY (`iPermissionId`) REFERENCES `t_permission` (`iIncrement`) ON DELETE RESTRICT ON UPDATE RESTRICT;
src/styles/tokens.css 0 → 100644
  1 +++ a/src/styles/tokens.css
  1 +/*
  2 + * src/styles/tokens.css — Design Tokens
  3 + * 命名规范见 docs/04-技术规范.md § 2.5
  4 + * 色值锁定见 docs/06-UI交互规范.md § 四
  5 + *
  6 + * 命名格式:--color-<scope>-<role>-<state>
  7 + * <scope> 组件域:form / table-row / table-header / ...
  8 + * <role> 作用:bg(背景)/ fg(前景/字体)/ border
  9 + * <state> 状态:edit / readonly / hover / selected(无状态时省略)
  10 + *
  11 + * 约束:
  12 + * - 组件样式中只用 var(--color-xxx),禁止硬编码 hex / rgba
  13 + * - 修改色值只改本文件,不允许在组件级覆盖
  14 + * - 新增 token 须先登记到 docs/06 § 4.1 / 4.2,再补到此处
  15 + */
  16 +
  17 +:root {
  18 + /* === 1. 全局调色板(与 Ant Design 主题对齐) === */
  19 + --color-primary: #1890ff;
  20 + --color-success: #52c41a;
  21 + --color-warning: #faad14;
  22 + --color-error: #ff4d4f;
  23 + --color-text: rgba(0, 0, 0, 0.85);
  24 + --color-text-secondary: rgba(0, 0, 0, 0.45);
  25 + --color-border: #d9d9d9;
  26 + --color-bg-base: #f0f2f5;
  27 +
  28 + /* === 2. 组件级状态色(与 docs/06 § 4.2 一一对应) === */
  29 +
  30 + /* form:输入框 / 备注框 / 时间框 / 下拉框共用 */
  31 + --color-form-bg-edit: #ffffff;
  32 + --color-form-bg-readonly: #f1f2f8;
  33 + --color-form-bg-hover: #f5f5f5; /* 仅下拉框使用 */
  34 + --color-form-fg: #000000;
  35 +
  36 + /* table */
  37 + --color-table-row-bg-selected: #86d5fb;
  38 + --color-table-row-bg-hover: #fff7e6;
  39 + --color-table-row-bg-readonly: #f1f2f8; /* = rgb(241, 242, 248) */
  40 + --color-table-row-fg: #000000;
  41 + --color-table-header-bg: #f5f5f5;
  42 + --color-table-header-fg: rgba(0, 0, 0, 0.85); /* = #000000D9 */
  43 +}