diff --git b/.gitignore a/.gitignore new file mode 100644 index 0000000..d11b14b --- /dev/null +++ a/.gitignore @@ -0,0 +1,30 @@ +# ==== ERP 插件推荐忽略项(skeleton-gen 追加) ==== +# 注:项目配置(含凭据)统一在 config-vars.yaml,随项目提交(内部 git 传播),不在此忽略。 + +# Java / Maven +target/ +*.class + +# Node / 前端构建产物 +node_modules/ +dist/ +build/ +coverage/ + +# IDE +.idea/ +.vscode/ +*.iml + +# OS +.DS_Store +Thumbs.db + +# 日志 +*.log +logs/ + +# 插件运行时临时文件 +.tmp/ +*.raw +# ==== 结束 ==== diff --git b/CLAUDE.md a/CLAUDE.md new file mode 100644 index 0000000..44adb64 --- /dev/null +++ a/CLAUDE.md @@ -0,0 +1,62 @@ +# CLAUDE.md — ERP项目 Claude Code 主指令文件 + +> 本文件是 Claude Code 的"操作手册"。Claude Code 启动时会自动读取此文件。 + +--- + +## 🎯 项目概述 + +- **项目名称**: 小羚羊 +- **项目简述**: 测试ERP +- **目标用户**: 企业内部管理人员 +- **部署方式**: 私有化部署 + +--- + +## 📐 编码行为约束 + +### 你必须做的 ✅ + +1. **严格遵循** `docs/04-技术规范.md`——命名 / 编码 / 统一响应 / 异常处理 / 数据访问 / 配置与安全 等项目专属技术规约全部在此 +2. **严格遵循** `docs/04-技术规范.md § 1.2 分层结构 / § 2.1 目录约定`——文件放对位置 +3. **每个后端接口** 必须先在 `docs/05-API接口契约.md` 定义,再编码实现 +4. **每个功能可追溯到 `REQ-XXX-NNN`**——commit tag + 代码注释(如 `// REQ-SYS-001: 用户登录`)+ plan/spec 文件名均用此 tag +5. **遇到跨模块改动**(动到非当前模块的代码)——允许改,但必须在《模块完成报告》记录原因 / 影响评估(留痕) + +### 你禁止做的 🚫 + +1. **主会话直接 `mysql -e` 跑业务 DDL**(只读查询 / 临时本地调试除外)——业务 schema 必须走 `sql/migrations/V_n__*.sql`,详见下方 Schema 演化规约 + +### Schema 演化规约(Flyway migration) + +1. **文件命名**:`sql/migrations/V__.sql`,例:`V5__add_user_email_unique_index.sql` +2. **版本号分配**:建文件前 `ls sql/migrations/V*.sql` 查当前最大 n,新文件 `n_max + 1` +3. **Apply 方式**:Spring Boot 启动 / 测试启动时 Flyway 自动 apply(项目必须在 `pom.xml` 声明 `flyway-core` + `flyway-mysql` 依赖)。`scripts/setup-test-db.mjs` 只负责清空库,不做 apply +4. **已合并的 migration 永不修改**:发现错了写一个补救 migration(如 `V7__fix_V5_index_name.sql`),旧 `V_n.sql` +5. **临时调试 DDL**:临时在本地试字段/索引可手动 `mysql -e`,但不写 migration;下次 `setup-test-db.mjs` 会 drop+create 清掉 +6. **A4 生成的 V1**:`V1__initial_schema.sql` 是 A 阶段由 `db-init` 从 `docs/03-数据库设计文档.md`(A3 正向设计的 schema SSoT)翻译生成的初始版本;后续 V2/V3/... 由 B 阶段每个 REQ 按需写入,**同时**反向同步更新 docs/03 对应表小节以保持 SSoT 一致 + +--- + +## 🗂️ Git 提交规范 + +每次提交必须遵循以下格式: + +``` +(): +``` + +- `scope`: 模块名,如 `user` / `inventory` / `order` +- `subject`: 简短描述;业务类(feat / fix / test)必须带 `REQ-XXX-NNN` 后缀 + +`type` 含义: + +| type | 看到它意味着 | +|-----|-------------| +| `feat` | **新能力上线**——用户多了一个功能、接口、页面或业务规则 | +| `fix` | **修 bug**——原来行为错了,这次改对 | +| `refactor` | **重构**——外部行为不变,只改代码结构 / 命名 / 抽象 | +| `docs` | **文档改动**——只动 Markdown / 代码注释,不动实现 | +| `style` | **格式调整**——空白 / 缩进 / import 顺序,逻辑 0 变化 | +| `test` | **只动测试代码**——补用例 / 修 fixture,不碰实现 | +| `chore` | **流程维护**——构建 / 依赖 / 工具 / 证据档案 / 里程碑元数据等非业务动作 | diff --git b/config-vars.yaml a/config-vars.yaml new file mode 100644 index 0000000..aab80a6 --- /dev/null +++ a/config-vars.yaml @@ -0,0 +1,31 @@ +# config-vars.yaml — 项目全部配置(含敏感凭据)。随项目提交,内部 git 传播。 +# 工具脚本(apply-ddl / setup-test-db)运行时按 2 层 map 解析此文件。 +# 值含 : / # / 空格 / $ / 引号等特殊字符时,用单引号包裹整个值:password: 'p@ss: w0rd#1' +# 所有配置值(含敏感值)只在本文件,不得散落到 docs / 源码 / 日志。 +# base_package / 命名空间锁定后全项目复用,不各模块各写。 + +backend: + base_package: com.xly.erp + http_port: 5172 + +frontend: + pkg_name: xly-erp-web + dev_port: 5173 + +database: + host: 118.178.19.35 + port: 3318 + user: xlyprint + password: xlyXLYprint2016 + schema: xlyweberp_vibe_erp_test + +admin_init: + username: admin + password: 666666 + +secrets: + jwt_secret: a3b7e8f1c4d6029e5b8f37a1c9d2e4068b5f1d3a7c0e9b2f48d6a1c5e7f9b3d2 + # 项目专属凭据按需取消注释 / 追加,直接填真实值: + # redis_password: 【人工填写:Redis 密码(用 Redis 时)】 + # oss_access_key_secret: 【人工填写:对象存储密钥】 + # sms_api_secret: 【人工填写:短信网关密钥】 diff --git b/docs/01-需求清单/USR-用户管理/REQ-USR-001.md a/docs/01-需求清单/USR-用户管理/REQ-USR-001.md new file mode 100644 index 0000000..efd8b9b --- /dev/null +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-001.md @@ -0,0 +1,41 @@ +### REQ-USR-001 增加用户 + +**目标**: 用户在后台新建用户账号,指定用户名、密码及角色,账号立即生效可用 + +- **输入**: + + - **表1**: + + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 | + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- | + | 创建时间 | 日期时间 | — | 系统生成 | — | 页面加载时 | 当前日期 | 保存后自动生成;只读 | + | 制单人 | 文本 | — | 系统生成 | — | 页面加载时 | 当前登录用户 | 保存后自动生成;只读 | + | 员工名 | 文本 | 否 | 下拉单选 | `职员表` | 用户操作时 | — | 关联职员(可选) | + | 用户号 | 文本 | 是 | 手工输入 | — | 用户操作时 | — | 关联职员选择后自动输入员工姓名 | + | 用户名 | 文本 | 是 | 手工输入 | — | 用户操作时 | — | 关联职员选择后自动输入员工姓名 | + | 类型 | 文本 | 是 | 下拉单选 | 普通用户/超级管理员 | 页面加载时 | 普通用户 | — | + | 语言 | 文本 | 是 | 下拉单选 | 中文/英文/繁体 | 页面加载时 | — | — | + | 单据修改权限 | 布尔 | 否 | 复选框 | — | — | 否 | — | + | 密码 | 文本 | — | 系统生成 | 不显示 | — | 666666 | 保存后自动设为初始化 | + + - **表2** - 权限组: + + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 | + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- | + | 复选框 | 布尔 | 否 | 复选框 | — | — | 否 | 是否选择当前行权限 | + | 权限分类 | 文本 | — | — | — | 页面加载时 | — | — | + + +- **输出**: + + - **表1**: + + | 字段 | 类型 | 显示来源 | + | --- | --- | --- | + | 用户号 | 文本 | — | + +- **跨字段规则**: 用户名在系统内全局唯一;角色取值受系统配置约束 +- **边界**: 密码以哈希形式存储 +- **验收**: 提交合法数据后用户记录出现在列表;重复用户名返回错误提示;普通账号无权访问此功能 +- **依赖表**: `usr_user`(写)、`usr_employee`(读,员工名下拉)、`usr_permission` + `usr_user_permission`(权限组授权) +- **依赖接口**: 无(本 REQ 提供 `POST /api/usr/users`;员工名/权限/类型下拉为基础数据读取,无上游 REQ 接口依赖) diff --git b/docs/01-需求清单/USR-用户管理/REQ-USR-002.md a/docs/01-需求清单/USR-用户管理/REQ-USR-002.md new file mode 100644 index 0000000..f06f626 --- /dev/null +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-002.md @@ -0,0 +1,40 @@ +### REQ-USR-002 修改用户 + +**目标**: 用户可更新已有用户的基本信息(姓名、角色、状态等),修改实时生效 + +- **输入**: 选中目标 + + - **表1**: + + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 | + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- | + | 创建时间 | 日期时间 | — | 系统生成 | — | 页面加载时 | 原值 | 保存后自动生成;只读 | + | 制单人 | 文本 | — | 系统生成 | — | 页面加载时 | 原值 | 保存后自动生成;只读 | + | 员工名 | 文本 | 否 | 下拉单选 | `职员表` | 页面加载时 | 原值 | 关联职员(可选) | + | 用户号 | 文本 | 是 | 手工输入 | — | 页面加载时 | 原值 | 关联职员选择后自动输入员工姓名 | + | 用户名 | 文本 | 是 | 手工输入 | — | 页面加载时 | 原值 | 关联职员选择后自动输入员工姓名 | + | 类型 | 文本 | 是 | 下拉单选 | 普通用户/超级管理员 | 页面加载时 | 原值 | — | + | 语言 | 文本 | 是 | 下拉单选 | 中文/英文/繁体 | 页面加载时 | 原值 | — | + | 单据修改权限 | 布尔 | 否 | 复选框 | — | 页面加载时 | 原值 | — | + | 密码 | 文本 | — | 系统生成 | 不显示 | 页面加载时 | 原值 | 保存后自动设为初始化 | + + - **表2** - 权限组: + + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 | + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- | + | 复选框 | 布尔 | 否 | 复选框 | — | 页面加载时 | 原值 | 是否选择当前行的权限 | + | 权限分类 | 文本 | — | — | — | 页面加载时 | — | — | + +- **输出**: + + - **表1**: + + | 字段 | 类型 | 显示来源 | + | --- | --- | --- | + | 用户 id | 文本 | `职员表` | + +- **跨字段规则**: 密码不在该接口修改;角色变更需具备相应权限 +- **边界**: 必须传入有效用户 id;字段格式与新增一致 +- **验收**: 修改角色或状态后立即反映在用户列表;被禁用账号无法登录并收到明确提示 +- **依赖表**: `usr_user`(写)、`usr_employee`(读,员工名下拉)、`usr_permission` + `usr_user_permission`(权限组授权) +- **依赖接口**: 无(本 REQ 提供 `PUT /api/usr/users/{id}`;编辑前的用户详情可由 REQ-USR-003 查询接口提供,非强依赖) diff --git b/docs/01-需求清单/USR-用户管理/REQ-USR-003.md a/docs/01-需求清单/USR-用户管理/REQ-USR-003.md new file mode 100644 index 0000000..8df0fc1 --- /dev/null +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-003.md @@ -0,0 +1,37 @@ +### REQ-USR-003 查询用户 + +**目标**: 用户可按用户名、角色或状态筛选并分页浏览用户列表 + +- **输入**: + + - **表1**: + + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 | + | ---- | ---- | --- | ---- | ----------------------------------------------- | ----- | ------- | --------------- | + | 查询字段 | 文本 | 否 | 下拉单选 | 用户名/员工名/用户号/部门/用户类型/作废/登录日期/制单人 | 页面加载时 | 用户名 | — | + | 匹配方式 | 文本 | 否 | 下拉单选 | 包含/不包含/等于 | 页面加载时 | 包含 | — | + | 查询值 | 文本 | 否 | 手工输入 | — | — | — | 与「查询字段」配合使用,空为选择全部 | + +- **输出**: + + - **表1**: + + | 字段 | 类型 | 显示来源 | + | ---- | ---- | ----- | + | 序号 | 数字 | 系统生成 | + | 用户名 | 文本 | `用户表` | + | 员工名 | 文本 | `职员表` | + | 用户号 | 文本 | `用户表` | + | 部门 | 文本 | `职员表` | + | 用户类型 | 文本 | `用户表` | + | 语言 | 文本 | `用户表` | + | 作废 | 布尔 | `用户表` | + | 登录日期 | 日期时间 | `用户表` | + | 制单人 | 文本 | `用户表` | + | 制单日期 | 日期时间 | `用户表` | + +- **跨字段规则**: - +- **边界**: 单页最大条数受限(默认 100);密码与敏感字段不返回;查询为只读,不产生写副作用 +- **验收**: 按条件筛选返回正确结果集;无匹配时返回空列表而非报错;分页参数越界时返回最后一页 +- **依赖表**: `usr_user`(读)、`usr_employee`(读,员工名 / 部门关联) +- **依赖接口**: 无(本 REQ 提供 `GET /api/usr/users`;无上游 REQ 接口依赖) diff --git b/docs/01-需求清单/USR-用户管理/REQ-USR-004.md a/docs/01-需求清单/USR-用户管理/REQ-USR-004.md new file mode 100644 index 0000000..4706120 --- /dev/null +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-004.md @@ -0,0 +1,21 @@ +### REQ-USR-004 登录用户 + +**目标**: 用户通过用户名+密码完成身份认证,获取 JWT Token 用于后续接口鉴权 + +- **输入**: + + - **表1**: + + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 | + | --- | ---- | --- | ---- | ------- | ----- | --- | ----------- | + | 用户名 | 文本 | 是 | 手工输入 | — | — | — | — | + | 密码 | 文本 | 是 | 手工输入 | — | — | — | 输入显示星号 | + | 版本 | 文本 | 是 | 下拉单选 | `公司表` | 页面加载时 | — | | + +- **输出**: 成功/失败 + +- **跨字段规则**: 账号密码匹配且用户处于启用状态才允许登录;连续登录失败需有锁定或限流策略;登录成功后签发访问令牌。 +- **边界**: 已禁用或已删除用户禁止登录;密码错误时不区分「账号不存在/密码错误」以防账号枚举;令牌须设置过期时间。 +- **验收**: 正确凭证登录成功并返回令牌;错误凭证返回统一失败提示;禁用用户登录请求被拒绝。 +- **依赖表**: `usr_user`(读,认证)、`usr_company`(读,登录「版本」下拉) +- **依赖接口**: 无(本 REQ 提供 `POST /api/usr/login`;登录"版本"下拉数据来自 `usr_company` 基础数据读取,无上游 REQ 接口依赖) diff --git b/docs/01-需求清单/USR-用户管理/_module.md a/docs/01-需求清单/USR-用户管理/_module.md new file mode 100644 index 0000000..3287d43 --- /dev/null +++ a/docs/01-需求清单/USR-用户管理/_module.md @@ -0,0 +1,5 @@ +# USR-用户管理 + +- **模块简述**: 用户管理模块,负责系统用户的增加、修改、查询,以及用户登录认证。 +- **依赖模块**: 无(USR 为本项目唯一模块,无跨模块依赖) +- **涉及表**: `usr_user`、`usr_employee`、`usr_company`、`usr_permission`、`usr_user_permission` diff --git b/docs/01-需求清单/index.md a/docs/01-需求清单/index.md new file mode 100644 index 0000000..ce13bd8 --- /dev/null +++ a/docs/01-需求清单/index.md @@ -0,0 +1,10 @@ +# 需求清单 + +> 本目录按模块组织所有功能需求。每个模块一个子目录,含 `_module.md`(模块头)和 `REQ-XXX-NNN.md`(每张 REQ 卡片一个文件)。下方核心功能点供 CC 拆分出 REQ 编号 + 标题 + 草拟规则;卡片内输入 / 输出的简述句和 N 张字段表由人工编辑。 + +## 模块索引 + +| 模块代码 | 模块名称 | 核心功能点(简要) | +| ---- | ---- | ------------------- | +| USR | 用户管理 | 增加用户,修改用户,查询用户,登录用户 | + diff --git b/docs/02-开发计划.md a/docs/02-开发计划.md new file mode 100644 index 0000000..b29ad83 --- /dev/null +++ a/docs/02-开发计划.md @@ -0,0 +1,18 @@ +# 02-开发计划 + +## 一、模块依赖表 + +| 模块 ID | 模块名 | 依赖模块 | 依赖表 | +|---|---|---|---| +| USR | 用户管理 | 无 | usr_user, usr_employee, usr_company, usr_permission, usr_user_permission | + +## 二、开发顺序清单(CC 分发权威) + +> Coding 阶段按本表行序分发;约束:同一模块所有 REQ 必须连续排列。 + +| # | REQ | 所属模块 | 选中理由 | 备注 | +|---|-----|---------|---------|------| +| 1 | **REQ-USR-001** | USR | 模块基础,建立用户写入能力(建表后第一个落地的写接口,无前置 REQ 依赖) | — | +| 2 | **REQ-USR-002** | USR | 依赖用户已存在(REQ-USR-001 增加用户) | — | +| 3 | **REQ-USR-003** | USR | 依赖用户数据(REQ-USR-001),提供列表/详情检索 | — | +| 4 | **REQ-USR-004** | USR | 依赖用户表与认证数据(REQ-USR-001),实现登录签发 JWT | — | diff --git b/docs/03-数据库设计文档.md a/docs/03-数据库设计文档.md new file mode 100644 index 0000000..cf885fa --- /dev/null +++ a/docs/03-数据库设计文档.md @@ -0,0 +1,202 @@ +# 03-数据库设计文档 + +- **Schema**: `xlyweberp_vibe_erp_test` +- **Migration 清单**: `sql/migrations/V*.sql`(由 Flyway 顺序 apply) +- **生成方式**: 由 A3 `db-design-gen` 基于 `docs/01-需求清单//REQ-*.md` REQ 卡片正向设计生成(schema SSoT)。 + +## 项目标准列约定 + +下文每张业务表的字段清单都自动包含以下 5 个标准列(匈牙利前缀 `i` int / `s` varchar / `t` datetime)。渲染时由 `docs-03-table-template.md` 模板内置原样输出。 + +| 列名 | 类型 | 可空 | 主键 | 说明 | +|---|---|---|---|---| +| `iIncrement` | int | 否 | 是 | 整数主键 ID(自增方式由实现决定:DB `AUTO_INCREMENT` 或应用 / 触发器分配) | +| `sId` | varchar(100) | 是 | — | 业务 ID(对外暴露的字符串标识,如 UUID / 人类可读编号) | +| `sBrandsId` | varchar(100) | 是 | — | 品牌 ID(多租户隔离) | +| `sSubsidiaryId` | varchar(100) | 是 | — | 子公司 ID(组织层级隔离) | +| `tCreateDate` | datetime | 否 | — | 记录创建时间 | + +字典 / 辅助表如有豁免,在该表业务注记里注明豁免原因。 + +## ER 关系概览 + +本库围绕「用户管理(USR)」单模块设计,核心实体为用户表 `usr_user`,辅以 4 张支撑/关联表: + +- `usr_user`(用户)—— 核心表。承载登录账号、密码、用户类型、语言、单据修改权限、作废标志、最后登录时间等。 +- `usr_employee`(职员)—— 支撑表。提供员工名 / 员工编号 / 部门;`usr_user.iEmployeeId` 可选外键关联(N:1,一个职员至多对应一个登录用户)。 +- `usr_company`(公司 / 版本)—— 支撑表。登录页「版本」下拉的数据来源;当前仅供登录时选择,不与 `usr_user` 建强外键。 +- `usr_permission`(权限)—— 支撑表。定义可分配的权限项,按「权限分类」组织。 +- `usr_user_permission`(用户权限)—— 关联表。`usr_user` 与 `usr_permission` 的多对多授权关系(对应新增 / 修改用户界面的「权限组」勾选)。 + +关系: + +``` +usr_user N:1 usr_employee (ON DELETE SET NULL) +usr_user N:M usr_permission 经 usr_user_permission(两侧 ON DELETE CASCADE) +usr_company 独立支撑表(登录时选择,无外键) +``` + +## 表清单 + +- `usr_user` — 用户表:登录账号与用户属性核心表 +- `usr_employee` — 职员表:员工名 / 部门等支撑信息 +- `usr_company` — 公司表:登录「版本」下拉数据来源 +- `usr_permission` — 权限表:可分配权限项定义 +- `usr_user_permission` — 用户权限关联表:用户↔权限多对多授权 + +--- + +## `usr_user` — 用户表:登录账号与用户属性核心表 + +### 字段 + +| 字段 | 类型 | Nullable | 默认 | 业务含义 | +|---|---|---|---|---| +| `iIncrement` | int | 否 | — | 整数主键 ID(标准列) | +| `sId` | varchar(100) | 是 | — | 业务 ID(标准列) | +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID,多租户隔离(标准列) | +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID,组织层级隔离(标准列) | +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列,对应「制单日期」) | +| `sUserName` | varchar(50) | 否 | — | 用户名,登录账号,系统内全局唯一(3-20 位字母数字下划线) | +| `sUserNo` | varchar(50) | 是 | — | 用户号,关联职员后可自动带出员工编号 / 姓名 | +| `sPassword` | varchar(100) | 否 | — | 登录密码,BCrypt 哈希存储(初始密码 666666) | +| `iEmployeeId` | int | 是 | — | 关联职员 ID(可选),外键 → `usr_employee.iIncrement` | +| `sUserType` | varchar(20) | 否 | `普通用户` | 用户类型:普通用户 / 超级管理员 | +| `sLanguage` | varchar(20) | 否 | `中文` | 界面语言:中文 / 英文 / 繁体 【人工填写:需用户审阅】默认值与取值范围待确认 | +| `iCanModifyBill` | tinyint(1) | 否 | `0` | 单据修改权限:0 否 / 1 是 | +| `iIsVoid` | tinyint(1) | 否 | `0` | 作废 / 禁用标志:0 正常 / 1 已作废(禁用后不可登录) | +| `tLastLoginDate` | datetime | 是 | — | 最后登录时间,登录成功时更新 | +| `sCreator` | varchar(50) | 是 | — | 制单人(创建该用户的操作员) | + +### 索引 + +- `uk_usr_user_username` (UNIQUE): sUserName +- `idx_usr_user_employee` (INDEX): iEmployeeId +- `idx_usr_user_type` (INDEX): sUserType +- `idx_usr_user_tenant` (INDEX): sBrandsId, sSubsidiaryId + +### 外键 + +- `fk_usr_user_employee`: iEmployeeId → usr_employee.iIncrement (SET NULL) + +### 业务注记 + +用户表为本模块核心实体,承载登录认证(用户名 + 密码)与用户类型 / 语言 / 单据修改权限等属性。`sUserName` 全局唯一;`sPassword` 以 BCrypt 哈希存储,初始为 666666;`iIsVoid=1` 表示禁用,禁止登录。可选关联职员(`iEmployeeId`,职员删除时置空)以带出员工名 / 部门。登录令牌 JWT 为无状态,不落库。查询接口(REQ-USR-003)按用户名 / 类型 / 作废 / 登录日期 / 制单人等条件检索,密码字段不返回。 + +--- + +## `usr_employee` — 职员表:员工名 / 部门等支撑信息 + +### 字段 + +| 字段 | 类型 | Nullable | 默认 | 业务含义 | +|---|---|---|---|---| +| `iIncrement` | int | 否 | — | 整数主键 ID(标准列) | +| `sId` | varchar(100) | 是 | — | 业务 ID(标准列) | +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID,多租户隔离(标准列) | +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID,组织层级隔离(标准列) | +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) | +| `sEmployeeName` | varchar(50) | 否 | — | 职员 / 员工姓名(用户「员工名」下拉来源) | +| `sEmployeeNo` | varchar(50) | 是 | — | 员工编号 | +| `sDepartment` | varchar(100) | 是 | — | 所属部门(用户查询输出「部门」来源) | + +### 索引 + +- `idx_usr_employee_name` (INDEX): sEmployeeName +- `idx_usr_employee_tenant` (INDEX): sBrandsId, sSubsidiaryId + +### 外键 + +(无) + +### 业务注记 + +职员表为用户的关联支撑表,提供员工名、员工编号、部门信息。用户新增 / 修改时通过「员工名」下拉选择职员,用户查询 / 展示时按 `usr_user.iEmployeeId` 关联取员工名与部门。可作为字典型支撑数据维护。 + +--- + +## `usr_company` — 公司表:登录「版本」下拉数据来源 + +### 字段 + +| 字段 | 类型 | Nullable | 默认 | 业务含义 | +|---|---|---|---|---| +| `iIncrement` | int | 否 | — | 整数主键 ID(标准列) | +| `sId` | varchar(100) | 是 | — | 业务 ID(标准列) | +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID,多租户隔离(标准列) | +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID,组织层级隔离(标准列) | +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) | +| `sCompanyName` | varchar(100) | 否 | — | 公司名称(登录页「版本」下拉的显示来源) | +| `sVersion` | varchar(50) | 是 | — | 版本 / 账套标识 【人工填写:需用户审阅】"版本"语义(账套 / 数据版本)待确认 | + +### 索引 + +- `uk_usr_company_name` (UNIQUE): sCompanyName + +### 外键 + +(无) + +### 业务注记 + +公司表为登录页「版本」下拉的数据来源(REQ-USR-004),每行代表一个可登录的公司 / 账套。当前仅用于登录时选择,不与用户表建立强外键关系。 + +--- + +## `usr_permission` — 权限表:可分配权限项定义 + +### 字段 + +| 字段 | 类型 | Nullable | 默认 | 业务含义 | +|---|---|---|---|---| +| `iIncrement` | int | 否 | — | 整数主键 ID(标准列) | +| `sId` | varchar(100) | 是 | — | 业务 ID(标准列) | +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID,多租户隔离(标准列) | +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID,组织层级隔离(标准列) | +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) | +| `sPermissionName` | varchar(100) | 否 | — | 权限名称 | +| `sPermissionCode` | varchar(100) | 否 | — | 权限编码(程序判定用,系统内唯一) | +| `sPermissionCategory` | varchar(100) | 是 | — | 权限分类(新增 / 修改用户界面「权限组」的"权限分类") | + +### 索引 + +- `uk_usr_permission_code` (UNIQUE): sPermissionCode +- `idx_usr_permission_category` (INDEX): sPermissionCategory + +### 外键 + +(无) + +### 业务注记 + +权限表定义可分配的权限项,按「权限分类」组织(对应新增 / 修改用户界面的「权限组」网格)。`sPermissionCode` 全局唯一供程序判定。【人工填写:需用户审阅】权限粒度(按分类 / 按具体功能点)待确认。 + +--- + +## `usr_user_permission` — 用户权限关联表:用户↔权限多对多授权 + +### 字段 + +| 字段 | 类型 | Nullable | 默认 | 业务含义 | +|---|---|---|---|---| +| `iIncrement` | int | 否 | — | 整数主键 ID(标准列) | +| `sId` | varchar(100) | 是 | — | 业务 ID(标准列;关联表对外不暴露,可留空) | +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID,多租户隔离(标准列) | +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID,组织层级隔离(标准列) | +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) | +| `iUserId` | int | 否 | — | 用户 ID,外键 → `usr_user.iIncrement` | +| `iPermissionId` | int | 否 | — | 权限 ID,外键 → `usr_permission.iIncrement` | + +### 索引 + +- `uk_usr_user_permission` (UNIQUE): iUserId, iPermissionId +- `idx_usr_user_permission_perm` (INDEX): iPermissionId + +### 外键 + +- `fk_usr_up_user`: iUserId → usr_user.iIncrement (CASCADE) +- `fk_usr_up_permission`: iPermissionId → usr_permission.iIncrement (CASCADE) + +### 业务注记 + +用户↔权限多对多关联表,记录每个用户被授予的权限(对应「权限组」勾选)。`(iUserId, iPermissionId)` 唯一防重复授权;删除用户或权限时级联清除对应授权记录。该表为纯关联表,`sId` 业务 ID 不对外暴露(标准列仍保留以保持结构一致)。 diff --git b/docs/04-技术规范.md a/docs/04-技术规范.md new file mode 100644 index 0000000..f433976 --- /dev/null +++ a/docs/04-技术规范.md @@ -0,0 +1,206 @@ +# 04-技术规范 + +## 零、技术栈总览 + +| 分层模块 | 技术 | 版本要求 | 说明 | +|---|---|---|---| +| 前端基础框架 | React | 18.x | 构建前端应用 | +| 前端 UI 组件 | Ant Design | 5.x | 页面组件与交互控件 | +| 前端状态管理 | Redux Toolkit | 最新稳定版 | 管理全局状态 | +| 前端路由管理 | React Router | v6 | 页面路由与导航 | +| 前端工程化构建 | Vite | 最新稳定版 | 前端开发与打包构建 | +| 前端接口通信 | Axios | 最新稳定版 | 调用后端 API | +| 后端基础框架 | Spring Boot | 3.x | 构建后端服务 | +| 后端数据访问 | MyBatis-Plus | 最新稳定版 | 数据库访问与 ORM 增强 | +| 工作流引擎 | Activiti | 6.x | 审批流、流程流转 | +| 缓存服务 | Redis | 最新稳定版 | 缓存、会话、分布式能力 | +| 报表打印 | JXLS | 2.8.1 | 基于 Excel 模板生成报表 | +| Excel 导入导出 | EasyExcel | 4.0.3 | Excel 数据导入导出 | +| 关系型数据库 | MySQL | 8.x | 核心业务数据存储 | +| 数据库 schema 迁移 | Flyway (`flyway-core` + `flyway-mysql`) | 10.x / 最新稳定版 | `sql/migrations/V_n__*.sql` 顺序 apply;Spring Boot 启动时自动应用 | +| 接口风格 | RESTful API | 统一规范 | 前后端接口设计规范 | +| 权限认证 | Spring Security / JWT | 最新稳定版 | 登录认证、权限控制 | +| API 文档 | OpenAPI / Swagger | 最新稳定版 | 接口文档与调试 | +| 项目构建管理 | Maven | 3.9.x | Java 项目依赖与构建 | +| JDK 运行环境 | Java | 17 / 21 | Spring Boot 3 推荐版本 | +| 部署容器 | Docker | 最新稳定版 | 容器化部署 | +| Web 服务器 / 反向代理 | Nginx | 最新稳定版 | 前端托管、反向代理、负载分发 | +| 日志管理 | Logback | 默认集成 / 最新稳定版 | 应用日志输出 | +| 对象映射工具 | MapStruct | 最新稳定版 | DTO / VO / Entity 转换 | +| 工具类库 | Hutool / Apache Commons | 最新稳定版 | 常用工具方法支持 | + +> 本表由 scope-lock 锁定。后续所有规范基于此表推导。 + +### 命令清单 + +> 由 scope-lock(A1) 锁定。Coding 阶段 `coding.mjs` 的 tdd / test-gate 按 stack 读取以下命令。`无` 表示该栈不提供此类命令。 + +**后端(Spring Boot 3 / Maven / Java 17)** + +| 类别 | 命令 | +|---|---| +| build | `mvn -q -B -DskipTests package` | +| lint | `mvn -q -B checkstyle:check` | +| unit | `mvn -q -B test` | +| e2e | 无 | + +**前端(React / Vite / npm)** + +| 类别 | 命令 | +|---|---| +| build | `npm run build` | +| lint | `npm run lint` | +| unit | `npm run test:unit` | +| e2e | `npm run test:e2e` | + +--- + +## 一、后端规范 + +> 技术栈:Spring Boot 3 + MyBatis-Plus + MySQL 8 + Flyway + Spring Security/JWT,根包 `com.xly.erp`,构建工具 Maven。 + +### 1.1 规则 + +- 所有后端代码位于仓库根 `backend/` 子项目;根包统一为 `com.xly.erp`,禁止散落到其他包名。 +- 业务代码按模块组织在 `com.xly.erp.modules.<模块小写代码>` 下(如 `modules.usr`),通用能力在 `com.xly.erp.common` 下,不得跨模块直接引用对方的 `mapper`/`entity`。 +- 每个对外接口必须先在 `docs/05-API接口契约.md` 定义,再编码实现;Controller 只做参数校验 + 调 Service,不写业务逻辑。 +- 业务 schema 变更一律走 `sql/migrations/V_n__*.sql`(Flyway),禁止在代码或会话里手跑业务 DDL。 +- 密码等敏感值只从 `config-vars.yaml` / 环境读取,禁止硬编码进源码或日志。 + +### 1.2 分层结构 + +后端为仓库根下的 `backend/` Maven 子项目,目录布局: + +``` +backend/ +├── pom.xml # 声明 spring-boot / mybatis-plus / flyway-core / flyway-mysql / spring-security / jjwt / mapstruct / hutool 等依赖 +├── src/main/java/com/xly/erp/ +│ ├── ErpApplication.java # Spring Boot 启动类 +│ ├── common/ # 跨模块通用能力(不属于任何业务模块) +│ │ ├── response/ # Result 统一响应体、ResultCode 枚举、PageResult +│ │ ├── exception/ # BusinessException、GlobalExceptionHandler(@RestControllerAdvice) +│ │ ├── config/ # MybatisPlusConfig / SecurityConfig / SwaggerConfig / CorsConfig +│ │ ├── security/ # JWT 工具、JwtAuthenticationFilter、UserDetails 适配 +│ │ └── base/ # BaseEntity(id/创建时间/制单人/逻辑删除等公共字段) +│ └── modules/ +│ └── usr/ # USR 用户管理(每个业务模块一个子包) +│ ├── controller/ # UsrUserController —— 仅校验 + 委派 +│ ├── service/ # UsrUserService 接口 +│ │ └── impl/ # UsrUserServiceImpl 业务实现 +│ ├── mapper/ # UsrUserMapper(继承 MyBatis-Plus BaseMapper) +│ ├── entity/ # UsrUser 实体(映射数据库表) +│ ├── dto/ # 入参对象(CreateUserDTO / UpdateUserDTO / UserQueryDTO / LoginDTO) +│ └── vo/ # 出参对象(UserVO / LoginVO) +├── src/main/resources/ +│ ├── application.yml # 端口、数据源、MyBatis-Plus、Flyway locations、JWT 等配置 +│ └── mapper/ # 复杂 SQL 的 MyBatis XML(简单 CRUD 用注解/MP 内置) +└── src/test/java/com/xly/erp/ # 单元测试 + 集成测试,包结构镜像主代码 +``` + +- **跨模块判定**:路径 `backend/src/main/java/com/xly/erp/modules//**` 归属模块 ``;`common/**` 为公共区,改动需在《模块完成报告》留痕。 +- **Flyway**:迁移脚本在仓库根 `sql/migrations/`,`application.yml` 配置 `spring.flyway.locations=filesystem:../sql/migrations`(相对 `backend/` 工作目录),Spring Boot 启动时自动 apply。 + +### 1.3 命名约定 + +- 类:大驼峰,模块前缀 + 业务名 + 层后缀,如 `UsrUserController` / `UsrUserServiceImpl` / `UsrUserMapper`。 +- 方法:小驼峰动词起头,如 `createUser` / `updateUser` / `pageUsers` / `login`。 +- 表名:`snake_case` 单数或业务习惯命名(详见 docs/03);实体类名大驼峰对应表名。 +- 常量:全大写下划线;DTO/VO 字段小驼峰。 +- REST 路径:`/api/<模块>/<资源>`,小写中划线,如 `/api/usr/users`。 + +### 1.4 统一响应格式 + +所有接口返回统一包装 `Result`: + +```json +{ "code": 0, "message": "success", "data": { } } +``` + +- `code`:0 成功;非 0 为业务/系统错误码(由 `ResultCode` 枚举集中定义)。 +- 分页返回 `Result>`,`PageResult` 含 `records` / `total` / `pageNum` / `pageSize`。 +- 失败响应不抛栈到前端,`message` 给可读提示。 + +### 1.5 异常处理 + +- 业务错误统一抛 `BusinessException(ResultCode, msg)`,由 `GlobalExceptionHandler` 捕获转 `Result`。 +- 参数校验用 `jakarta.validation`(`@Valid` + 注解),校验失败由全局处理器转统一错误。 +- 系统异常(未捕获)记录 ERROR 日志并返回通用错误码,不泄露内部细节。 + +### 1.6 事务 + +- 写操作(增/改/删,含多表)在 Service 实现方法上加 `@Transactional(rollbackFor = Exception.class)`。 +- 只读查询不开事务;避免在事务方法内做远程调用 / 长耗时操作。 + +### 1.7 认证 + +- 采用 Spring Security + JWT 无状态认证。登录成功签发 JWT(密钥取自 `config-vars.yaml` `secrets.jwt_secret`,有过期时间)。 +- 受保护接口经 `JwtAuthenticationFilter` 校验 `Authorization: Bearer `;登录接口 `/api/usr/login` 放行。 +- 密码用 `BCryptPasswordEncoder` 哈希存储与比对,禁止明文。 + +## 二、前端规范 + +> 技术栈:React 18 + Ant Design 5 + Redux Toolkit + React Router v6 + Vite + Axios,包名 `xly-erp-web`。 + +### 2.1 目录约定 + +前端为仓库根下的 `frontend/` 子项目,目录布局: + +``` +frontend/ +├── package.json # name=xly-erp-web;scripts: dev/build/lint/test:unit/test:e2e +├── vite.config.ts +├── index.html +├── src/ +│ ├── main.tsx # 入口:挂载 App + Redux Provider + Router + AntD ConfigProvider +│ ├── App.tsx +│ ├── router/ # React Router v6 路由表 + 路由守卫(未登录跳登录页) +│ ├── store/ # Redux Toolkit:store.ts + slices/(authSlice 等) +│ ├── api/ # Axios 实例封装(request.ts)+ 各模块 api(usrApi.ts) +│ ├── pages/ +│ │ └── usr/ # 用户管理页面(用户列表 / 新增 / 编辑 / 登录) +│ ├── components/ # 跨页面通用组件 +│ ├── styles/ # 引用 Design Tokens(见下) +│ └── utils/ # 通用工具 +└── tests/ # 单元测试(Vitest)+ e2e(Playwright) +``` + +- **Design Tokens SSoT**:色值单一来源在仓库根 `src/styles/tokens.css`(由 skeleton-gen 生成),前端在 `main.tsx` / 全局样式中引入;组件只用 `var(--color-xxx)`,禁止硬编码 hex/rgba。**色值冲突时 `tokens.css` 优先于 `prototype/`**。 +- **UI/交互/布局权威**:项目根 `prototype/`(完整 demo)为前端页面布局与交互的权威参照,A5 据其推导 FE 清单。 + +### 2.2 状态管理 + +- 全局状态(登录态、当前用户、token)用 Redux Toolkit `createSlice` 管理;按域拆 slice。 +- 服务端数据优先就近在页面用 hooks 拉取,跨页面共享的才进 store;避免把所有响应塞进全局。 + +### 2.3 请求封装 + +- 统一 Axios 实例(`api/request.ts`):baseURL 指向后端 `/api`,请求拦截器注入 `Authorization` 头,响应拦截器拆 `Result`、对非 0 `code` 统一提示。 +- 各模块 API 集中在 `api/<模块>Api.ts`,页面只调封装后的方法,不直接散用 axios。 + +### 2.4 错误处理 + +- 响应拦截器统一处理:401 跳登录、业务错误码弹 `message.error`、网络异常兜底提示。 +- 表单提交错误就近在表单展示;列表加载失败展示空态/重试。 + +## 三、共同约定 + +### 3.1 Git 提交 + +`(): REQ-XXX-NNN`(详见 `CLAUDE.md § 🗂️ Git 提交规范`)。 + +### 3.2 分页查询 + +- 入参统一 `pageNum`(从 1 起)+ `pageSize`(有上限,默认 10/20,最大 100)+ 业务过滤条件。 +- 返回 `PageResult`:`records` / `total` / `pageNum` / `pageSize`。 +- 文本条件模糊匹配,枚举/外键条件精确匹配;空条件返回全量分页。 + +### 3.3 日期与金额 + +- 日期时间统一 ISO-8601 字符串传输(`yyyy-MM-dd'T'HH:mm:ss`),后端用 `LocalDateTime`。 +- 金额用整数分或 `BigDecimal`,禁止用 `float/double` 表示金额。 + +### 3.4 数据访问规约 + +- 数据访问只走 Mapper(MyBatis-Plus);简单 CRUD 用 MP 内置/`LambdaQueryWrapper`,复杂 SQL 写 XML。 +- 禁止在 Controller 直接操作 Mapper;逻辑删除/审计字段(创建时间、制单人)由 `BaseEntity` + MP 自动填充统一处理。 +- 业务 schema 变更走 Flyway migration,并反向同步更新 `docs/03-数据库设计文档.md` 对应表小节(SSoT 一致)。 diff --git b/docs/05-API接口契约.md a/docs/05-API接口契约.md new file mode 100644 index 0000000..7b64b0e --- /dev/null +++ a/docs/05-API接口契约.md @@ -0,0 +1,62 @@ +# 05-API接口契约 + +BasePath: `/api` +端口: 见 `config-vars.yaml` 的 `backend.http_port`(单一来源,不在此重复填) + +## 全局约定 + +响应格式 / 异常 / 错误码 / 认证 / 分页等全局约定的 SSoT 在 `docs/04`(响应格式见 § 1.4、异常处理见 § 1.5、认证见 § 1.7、分页查询见 § 3.2),此处不重复。各端点专属的请求 / 响应 / 错误码见下方接口清单。 + +## 接口清单 +(各模块接口段落见下方,由 `downstream-gen` 按 REQ 填入) + +### REQ-USR-001 增加用户 + +- **Method**: POST +- **Path**: `/api/usr/users` +- **Auth**: 需要(Bearer JWT,仅管理员/超级管理员可调用) +- **请求**: JSON body `{ sUserName(必填,3-20位字母数字下划线,全局唯一), sUserNo(可选), iEmployeeId(可选,关联职员), sUserType(必填,普通用户/超级管理员,默认普通用户), sLanguage(必填,中文/英文/繁体), iCanModifyBill(可选,0/1,默认0), permissionIds(可选,number[],权限组勾选), initialPassword(可选,默认 666666) }`。密码以 BCrypt 哈希入库。 +- **响应**: `Result<{ id: number }>`,返回新建用户主键 id(`data.id`)。 + +#### 错误码 +- `40001` — 参数校验失败(字段格式/必填项不满足) +- `40901` — 用户名已存在(sUserName 全局唯一冲突) +- `40301` — 无权限(非管理员调用) + +### REQ-USR-002 修改用户 + +- **Method**: PUT +- **Path**: `/api/usr/users/{id}` +- **Auth**: 需要(Bearer JWT,仅管理员/超级管理员可调用) +- **请求**: 路径参数 `id`(用户主键);JSON body `{ sUserNo, iEmployeeId, sUserType, sLanguage, iCanModifyBill, iIsVoid, permissionIds }`(sUserName 作为唯一标识不可修改;密码不在本接口修改)。 +- **响应**: `Result<{ id: number }>`,返回被修改用户的 id;持久化变更。 + +#### 错误码 +- `40001` — 参数校验失败 +- `40401` — 用户不存在(id 无对应记录) +- `40301` — 无权限(非管理员调用) + +### REQ-USR-003 查询用户 + +- **Method**: GET +- **Path**: `/api/usr/users` +- **Auth**: 需要(Bearer JWT) +- **请求**: query 参数 `{ queryField(可选,用户名/员工名/用户号/部门/用户类型/作废/登录日期/制单人), matchType(可选,包含/不包含/等于,默认包含), queryValue(可选), pageNum(默认1), pageSize(默认10,最大100) }`。空条件返回全量分页;密码字段不返回。 +- **响应**: `Result>`,`UserVO = { id, sUserName, 员工名, sUserNo, 部门, sUserType, sLanguage, iIsVoid, tLastLoginDate, sCreator, tCreateDate }`;`PageResult = { records, total, pageNum, pageSize }`。 + +#### 错误码 +- `42201` — 分页参数非法(pageNum<1 或 pageSize 超上限) +- `40001` — 查询参数校验失败 + +### REQ-USR-004 登录用户 + +- **Method**: POST +- **Path**: `/api/usr/login` +- **Auth**: 否(登录端点,放行) +- **请求**: JSON body `{ sUserName(必填), password(必填,提交明文经 HTTPS,服务端 BCrypt 比对), companyId(必填,登录"版本"下拉选中的 usr_company.id) }`。 +- **响应**: `Result<{ token: string, user: { id, sUserName, sUserType, sLanguage } }>`,签发 JWT(有过期时间,无状态);登录成功更新 `tLastLoginDate`。 + +#### 错误码 +- `40101` — 认证失败(用户名或密码错误;不区分以防账号枚举) +- `40302` — 账号已禁用(iIsVoid=1,禁止登录) +- `40001` — 参数校验失败(缺用户名/密码/版本) diff --git b/docs/08-模块任务管理.md a/docs/08-模块任务管理.md new file mode 100644 index 0000000..c7c6a64 --- /dev/null +++ a/docs/08-模块任务管理.md @@ -0,0 +1,64 @@ +# 08-工作流进度 + +> 全流程进度跟踪。CC 每完成一项产出就勾选一项。 + +## 一、Plan 阶段(一次性) + +- [x] A0 项目初始化 — project-init + - [x] 依赖检查通过 + - [x] 项目文件骨架已创建(CLAUDE.md + docs/01-需求清单/index.md + docs/04-技术规范.md) + - [x] Git 已初始化 + +- [x] A1 范围锁定 — scope-lock + - [x] 项目概述已填写(CLAUDE.md § 🎯 项目概述) + - [x] 技术栈已确认(docs/04 § 零) + - [x] 需求清单索引已填写(docs/01-需求清单/index.md) + - [x] REQ 卡片骨架已生成(docs/01-需求清单//REQ-*.md,业务内容留待人工填写) + +- [x] A2 骨架生成 — skeleton-gen + - [x] 架构文档已生成(docs/04 § 一+) + - [x] 工具脚本已生成(scripts/*.mjs) + - [x] 样式 token 骨架已生成(src/styles/tokens.css) + - [x] .gitignore 已配置 + +- [x] A3 DB 设计 + REQ 回填 — db-design-gen + - [x] docs/03-数据库设计文档.md 已生成 + - [x] docs/01 各 REQ 卡片"依赖表" + 模块头"涉及表" 已回填 + +- [x] A4 DB 初始化 — db-init + - [x] sql/migrations/V1__initial_schema.sql 已生成 + - [x] DDL ↔ docs/03 5 维一致(validate-ddl.mjs) + - [x] config-vars.yaml DB 凭据 5 项非空校验通过 + - [x] setup-test-db.mjs DROP+CREATE + apply V1 已执行 + +- [x] A5 下游文档生成 — downstream-gen + - [x] docs/02 开发计划已生成 + - [x] docs/05 API 契约已生成 + - [x] 下方模块列表已填入 + - [x] REQ 卡片依赖接口已回填 + - [x] FE 清单已推导填入 docs/08 § 三 + +## 二、Coding 阶段(后端模块循环) + +(A5 填入后,每行一个后端模块。每个模块的 `里程碑:` 字段在 `—` 和 `milestone/` 之间变化,完成由本地 `git tag -l 'milestone/'` 判定。功能行 checkbox 只作可视化,真正的功能级 resume 由 `req-done/` tag 判定。后端模块全部打里程碑后自动进入 § 三 前端阶段。) + +- USR 用户管理 + - 依赖: 无 + - 路径: `backend/src/main/java/com/xly/erp/modules/usr/**` + - 里程碑: — + - 功能: + - [ ] REQ-USR-001 增加用户 + - [ ] REQ-USR-002 修改用户 + - [ ] REQ-USR-003 查询用户 + - [ ] REQ-USR-004 登录用户 + +## 三、Coding 阶段(前端整体) + +(FE 业务功能清单在 Plan 期 A5 `downstream-gen` 由 prototype/ + docs/01 + docs/05 推导后写入下方"功能:"项;Coding 阶段 `coding.mjs` 的 Router 把缺少 `req-done/` tag 的 FE 聚合为单一 `frontend-phase` 阶段,排在所有后端模块之后。整个前端阶段 1 个里程碑 tag,分支 `frontend-phase`。无前端则此处留空,Router 不产生前端阶段。) + +- 整体里程碑: — +- 功能: + - [ ] FE-01 登录页(用户名/密码/版本下拉登录,对接 POST /api/usr/login) + - [ ] FE-02 主页与导航框架(顶栏 + 全部导航总览 + 主页 KPI 看板 + 常用操作;登录后落地页与路由壳) + - [ ] FE-03 用户列表与查询(工具栏刷新/导出 + 筛选条件 + 用户表格 + 分页,对接 GET /api/usr/users) + - [ ] FE-04 用户信息单据(新增/修改用户表单 + 权限组勾选,对接 POST /api/usr/users 与 PUT /api/usr/users/{id}) diff --git b/prototype/erp.html a/prototype/erp.html new file mode 100644 index 0000000..13f2e9e --- /dev/null +++ a/prototype/erp.html @@ -0,0 +1,850 @@ + + + + +ERP - 企业业务能力平台 + + + + +
+ + +
+ + +
+ +
+ + 主页 +
+ + +
+ +
+ + + + + + +
+ + 朱子纯(超级管理员) +
+ +
+
+ + +
+ + + + + +
+
+
+ +
+ KPI监控 + 今日未处理:37428 + | + 未清总数:56433 + +
+ + +
+ + +
+
+
+
+
+
+
+ + +
+
常用操作
+ 用户列表 + 系统功能模块设置 +
+
+ +
+ 🛠 + ©Copyright Antler Software | 印刷智慧工厂 | 印刷MES | 印刷ERP | 印刷电商平台 | 文件智能处理 | 印前自动化 | 400-880-6237 + + + 沪ICP备14034791号-1 + +
+
+ + +
+
+ 刷新 + 新增 + 导出Excel + + +
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
序号用户名 ⇅ ⌕员工名 ⇅ ⌕用户号 ⇅ ⌕部门 ⇅ ⌕用户类型 ⇅ ⌕语言 ⇅ ⌕⇅ ⌕登录日期制单人 ⇅ ⌕制单日期
+
+
+ 当前显示 共37个单据 共37条记录 + + 1 + + +
+
+ + +
+
+ 新增 + 修改 + 删除 + 保存 + 取消 + 功能 + 作废 + 重置密码 + 取消作废 + + +
+ +
+
创建时间:
2023-10-26 17:02:01
+
制单人:
超级管理员
+
员工名:
管广飞
+ +
用户名:
+
类型:
超级管理员
+
语言:
英文
+ +
用户号:
+
+
单据修改权限:
+
+ +
+
权限组
+
客户查看权限
+
供应商查看权限
+
人员查看权限
+
工序查看权限
+
司机查看权限
+
+ +
+
权限分类
+
+
+ + +
+ +
+ +
+
+ + + + diff --git b/scripts/setup-test-db.mjs a/scripts/setup-test-db.mjs new file mode 100644 index 0000000..e028646 --- /dev/null +++ a/scripts/setup-test-db.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node +// scripts/setup-test-db.mjs — DROP + CREATE 测试库。 +// 由 coding.mjs 的 test-gate 调用;schema 由 Flyway 在 Spring Boot 启动时重放。 +// DB 凭据从仓库根 config-vars.yaml 的 database: 段读取:schema 经标识符校验后才拼进 SQL(防误删 / 注入,见下方守卫); +// host / user / password 信任该文件,port 仅校验范围。 + +import { spawnSync } from 'node:child_process' +import { existsSync, readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) +const CONFIG_FILE = join(SCRIPT_DIR, '..', 'config-vars.yaml') + +// 极简 YAML 读取(2 层 map + 标量;与插件 lib/yaml-config.mjs 同规则,内联以免运行时依赖)。 +function parseScalar(raw) { + let s = String(raw).trim() + if (s === '' || s[0] === '#') return '' + const q = s[0] + if (q === '"' || q === "'") { + const end = s.indexOf(q, 1) + if (end !== -1) return s.slice(1, end) + } + const hash = s.indexOf(' #') + if (hash !== -1) s = s.slice(0, hash).trim() + return s +} +function parseYamlConfig(text) { + const root = {} + let section = null + for (const rawLine of text.split('\n')) { + const line = rawLine.replace(/\r$/, '') + const trimmed = line.trim() + if (trimmed === '' || trimmed[0] === '#') continue + const colon = line.indexOf(':') + if (colon === -1) continue + const key = line.slice(0, colon).trim() + if (key === '') continue + const indent = line.length - line.replace(/^\s+/, '').length + const value = parseScalar(line.slice(colon + 1)) + if (indent === 0) { + if (value === '') { + section = {} + root[key] = section + } else { + root[key] = value + section = null + } + } else if (section) { + section[key] = value + } else { + root[key] = value + } + } + return root +} + +if (!existsSync(CONFIG_FILE)) { + console.error(`[setup-test-db] config-vars.yaml 不存在(${CONFIG_FILE})`) + process.exit(1) +} + +const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {} + +const DB_HOST = db.host ?? '' +const DB_PORT = db.port ?? '3306' +const DB_USER = db.user ?? '' +const DB_PASSWORD = db.password ?? '' +const DB_SCHEMA = db.schema ?? '' + +if (!/^\d+$/.test(DB_PORT) || Number(DB_PORT) <= 0 || Number(DB_PORT) > 65535) { + console.error(`[setup-test-db] database.port 非法: ${DB_PORT}(必须是 1..65535 的整数)`) + process.exit(1) +} + +// schema 是被无条件 DROP + CREATE 的标识符——必须严格校验后才拼进 SQL: +// · 空值 → 避免 DROP DATABASE `` 这类无意义/误删语句 +// · 「【人工填写】」占位 → 配置尚未填好,不应连库 +// · 含反引号 → 防止 `erp`; DROP DATABASE `prod` 形态的标识符注入(值来自 config-vars.yaml,按 fail-closed 处理) +// 注:仅接受 ASCII 标识符;非 ASCII schema 名一律拒绝(即便 MySQL / apply-ddl 允许),与推荐的 test/_dev 命名一致 +if (!/^[A-Za-z0-9_$]+$/.test(DB_SCHEMA)) { + console.error(`[setup-test-db] database.schema 非法或未填: ${JSON.stringify(DB_SCHEMA)}(需为 [A-Za-z0-9_$] 标识符;空值 / 「【人工填写】」占位 / 含反引号均拒绝)`) + process.exit(1) +} + +console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`) + +const sql = + `DROP DATABASE IF EXISTS \`${DB_SCHEMA}\`; ` + + `CREATE DATABASE \`${DB_SCHEMA}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;` + +const mysqlArgs = [ + `--host=${DB_HOST}`, + `--port=${DB_PORT}`, + `--user=${DB_USER}`, + `--password=${DB_PASSWORD}`, + '-e', + sql, +] +const res = spawnSync('mysql', mysqlArgs, { stdio: 'inherit' }) +if (res.error) { + console.error(`[setup-test-db] FATAL: 无法执行 mysql(请确认其在 PATH 中): ${res.error.message}`) + process.exit(1) +} +if (res.status !== 0) { + console.error(`[setup-test-db] FAIL: mysql exit=${res.status}`) + process.exit(res.status === null ? 1 : res.status) +} + +console.log('[setup-test-db] done — schema will be applied by Flyway when Spring Boot starts') diff --git b/scripts/test.mjs a/scripts/test.mjs new file mode 100644 index 0000000..0ad78da --- /dev/null +++ a/scripts/test.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node +// scripts/test.mjs —— 合并到默认分支(main / master)前的测试闸门。 +// 顺序:detect → setup-db → build → lint → unit+integration → e2e +// (不在尾部 reset:下次跑的 setup-db 会 DROP+CREATE,重复清库无意义) +// 由 coding.mjs 的 test-gate stage(通过子会话)调用。 +// +// 跨平台:所有命令经 child_process.spawnSync(cmd, { shell:true }) 执行, +// 在 Windows 走 cmd.exe,在 *nix 走 /bin/sh,无需 WSL / Git-Bash。 +// 命令字符串来自 docs/04 §零(构建/lint/单测/e2e)——由 skeleton-gen 在 Plan 期填充。 + +import { spawnSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const PROJECT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') + +// 在指定子目录下跑一条 shell 命令;非零退出码即终止整个闸门并透传该码。 +function run(label, command, cwd = PROJECT_ROOT) { + console.log(`[test.mjs] ${label}: ${command}`) + const res = spawnSync(command, { cwd, shell: true, stdio: 'inherit' }) + if (res.error) { + console.error(`[test.mjs] FATAL: 无法执行 (${label}): ${res.error.message}`) + process.exit(1) + } + if (res.status !== 0) { + console.error(`[test.mjs] FAIL (${label}) exit=${res.status}`) + process.exit(res.status === null ? 1 : res.status) + } +} + +// Stack detection (runtime, mode-agnostic) +const hasBackend = existsSync(join(PROJECT_ROOT, 'backend')) +const hasFrontend = existsSync(join(PROJECT_ROOT, 'frontend')) +if (!hasBackend && !hasFrontend) { + console.error('[test.mjs] FATAL: neither backend/ nor frontend/ exists') + process.exit(1) +} + +const backendDir = join(PROJECT_ROOT, 'backend') +const frontendDir = join(PROJECT_ROOT, 'frontend') + +console.log('[test.mjs] 1/5 setup test db') +run('setup-test-db', `node ${JSON.stringify(join('scripts', 'setup-test-db.mjs'))}`) + +console.log('[test.mjs] 2/5 build') +if (hasBackend) run('backend build', 'mvn -q -B -DskipTests package', backendDir) +else console.log('[test.mjs] skip backend build') +if (hasFrontend) run('frontend build', 'npm run build', frontendDir) +else console.log('[test.mjs] skip frontend build') + +console.log('[test.mjs] 3/5 lint') +if (hasBackend) run('backend lint', 'mvn -q -B checkstyle:check', backendDir) +else console.log('[test.mjs] skip backend lint') +if (hasFrontend) run('frontend lint', 'npm run lint', frontendDir) +else console.log('[test.mjs] skip frontend lint') + +console.log('[test.mjs] 4/5 unit + integration') +if (hasBackend) run('backend test', 'mvn -q -B test', backendDir) +else console.log('[test.mjs] skip backend test') +if (hasFrontend) run('frontend test', 'npm run test:unit', frontendDir) +else console.log('[test.mjs] skip frontend test') + +console.log('[test.mjs] 5/5 E2E') +run('e2e', 'echo "[test.mjs] e2e 略(后端无 e2e;前端 e2e: npm run test:e2e,见 docs/04 §零,前端阶段单独执行)"') + +console.log('[test.mjs] GREEN') diff --git b/sql/migrations/V1__initial_schema.sql a/sql/migrations/V1__initial_schema.sql new file mode 100644 index 0000000..0cb7a56 --- /dev/null +++ a/sql/migrations/V1__initial_schema.sql @@ -0,0 +1,108 @@ +-- Flyway migration V1 — initial schema for 小羚羊 +-- Generated: 2026-06-01T03:43:38Z +-- Source: 由 A4 db-init 从 docs/03-数据库设计文档.md 翻译生成(schema SSoT 是 docs/03) +-- This is the FIRST migration; subsequent schema changes must be written as new files sql/migrations/V2__.sql, V3__... etc. +-- Apply: Flyway runs this automatically at Spring Boot startup. +-- Do not hand-edit this file after it is committed; write a new migration instead. + +-- ============================================================ +-- CREATE TABLE +-- ============================================================ + +CREATE TABLE `usr_user` ( + `iIncrement` int NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)', + `sId` varchar(100) NULL COMMENT '业务 ID(标准列)', + `sBrandsId` varchar(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID,多租户隔离(标准列)', + `sSubsidiaryId` varchar(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID,组织层级隔离(标准列)', + `tCreateDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列,对应制单日期)', + `sUserName` varchar(50) NOT NULL COMMENT '用户名,登录账号,系统内全局唯一(3-20 位字母数字下划线)', + `sUserNo` varchar(50) NULL COMMENT '用户号,关联职员后可自动带出员工编号/姓名', + `sPassword` varchar(100) NOT NULL COMMENT '登录密码,BCrypt 哈希存储(初始密码 666666)', + `iEmployeeId` int NULL COMMENT '关联职员 ID(可选),外键 -> usr_employee.iIncrement', + `sUserType` varchar(20) NOT NULL DEFAULT '普通用户' COMMENT '用户类型:普通用户 / 超级管理员', + `sLanguage` varchar(20) NOT NULL DEFAULT '中文' COMMENT '界面语言:中文 / 英文 / 繁体', + `iCanModifyBill` tinyint(1) NOT NULL DEFAULT 0 COMMENT '单据修改权限:0 否 / 1 是', + `iIsVoid` tinyint(1) NOT NULL DEFAULT 0 COMMENT '作废/禁用标志:0 正常 / 1 已作废(禁用后不可登录)', + `tLastLoginDate` datetime NULL COMMENT '最后登录时间,登录成功时更新', + `sCreator` varchar(50) NULL COMMENT '制单人(创建该用户的操作员)', + PRIMARY KEY (`iIncrement`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表:登录账号与用户属性核心表'; + +CREATE TABLE `usr_employee` ( + `iIncrement` int NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)', + `sId` varchar(100) NULL COMMENT '业务 ID(标准列)', + `sBrandsId` varchar(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID,多租户隔离(标准列)', + `sSubsidiaryId` varchar(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID,组织层级隔离(标准列)', + `tCreateDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)', + `sEmployeeName` varchar(50) NOT NULL COMMENT '职员/员工姓名(用户员工名下拉来源)', + `sEmployeeNo` varchar(50) NULL COMMENT '员工编号', + `sDepartment` varchar(100) NULL COMMENT '所属部门(用户查询输出部门来源)', + PRIMARY KEY (`iIncrement`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='职员表:员工名/部门等支撑信息'; + +CREATE TABLE `usr_company` ( + `iIncrement` int NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)', + `sId` varchar(100) NULL COMMENT '业务 ID(标准列)', + `sBrandsId` varchar(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID,多租户隔离(标准列)', + `sSubsidiaryId` varchar(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID,组织层级隔离(标准列)', + `tCreateDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)', + `sCompanyName` varchar(100) NOT NULL COMMENT '公司名称(登录页版本下拉的显示来源)', + `sVersion` varchar(50) NULL COMMENT '版本/账套标识', + PRIMARY KEY (`iIncrement`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='公司表:登录版本下拉数据来源'; + +CREATE TABLE `usr_permission` ( + `iIncrement` int NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)', + `sId` varchar(100) NULL COMMENT '业务 ID(标准列)', + `sBrandsId` varchar(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID,多租户隔离(标准列)', + `sSubsidiaryId` varchar(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID,组织层级隔离(标准列)', + `tCreateDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)', + `sPermissionName` varchar(100) NOT NULL COMMENT '权限名称', + `sPermissionCode` varchar(100) NOT NULL COMMENT '权限编码(程序判定用,系统内唯一)', + `sPermissionCategory` varchar(100) NULL COMMENT '权限分类(权限组的权限分类)', + PRIMARY KEY (`iIncrement`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表:可分配权限项定义'; + +CREATE TABLE `usr_user_permission` ( + `iIncrement` int NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)', + `sId` varchar(100) NULL COMMENT '业务 ID(标准列;关联表对外不暴露,可留空)', + `sBrandsId` varchar(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID,多租户隔离(标准列)', + `sSubsidiaryId` varchar(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID,组织层级隔离(标准列)', + `tCreateDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)', + `iUserId` int NOT NULL COMMENT '用户 ID,外键 -> usr_user.iIncrement', + `iPermissionId` int NOT NULL COMMENT '权限 ID,外键 -> usr_permission.iIncrement', + PRIMARY KEY (`iIncrement`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户权限关联表:用户↔权限多对多授权'; + +-- ============================================================ +-- CREATE INDEX +-- ============================================================ + +CREATE UNIQUE INDEX `uk_usr_user_username` ON `usr_user` (`sUserName`); +CREATE INDEX `idx_usr_user_employee` ON `usr_user` (`iEmployeeId`); +CREATE INDEX `idx_usr_user_type` ON `usr_user` (`sUserType`); +CREATE INDEX `idx_usr_user_tenant` ON `usr_user` (`sBrandsId`, `sSubsidiaryId`); + +CREATE INDEX `idx_usr_employee_name` ON `usr_employee` (`sEmployeeName`); +CREATE INDEX `idx_usr_employee_tenant` ON `usr_employee` (`sBrandsId`, `sSubsidiaryId`); + +CREATE UNIQUE INDEX `uk_usr_company_name` ON `usr_company` (`sCompanyName`); + +CREATE UNIQUE INDEX `uk_usr_permission_code` ON `usr_permission` (`sPermissionCode`); +CREATE INDEX `idx_usr_permission_category` ON `usr_permission` (`sPermissionCategory`); + +CREATE UNIQUE INDEX `uk_usr_user_permission` ON `usr_user_permission` (`iUserId`, `iPermissionId`); +CREATE INDEX `idx_usr_user_permission_perm` ON `usr_user_permission` (`iPermissionId`); + +-- ============================================================ +-- ADD FOREIGN KEY +-- ============================================================ + +ALTER TABLE `usr_user` + ADD CONSTRAINT `fk_usr_user_employee` FOREIGN KEY (`iEmployeeId`) REFERENCES `usr_employee` (`iIncrement`) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE `usr_user_permission` + ADD CONSTRAINT `fk_usr_up_user` FOREIGN KEY (`iUserId`) REFERENCES `usr_user` (`iIncrement`) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE `usr_user_permission` + ADD CONSTRAINT `fk_usr_up_permission` FOREIGN KEY (`iPermissionId`) REFERENCES `usr_permission` (`iIncrement`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git b/src/styles/tokens.css a/src/styles/tokens.css new file mode 100644 index 0000000..b81cd24 --- /dev/null +++ a/src/styles/tokens.css @@ -0,0 +1,41 @@ +/* + * src/styles/tokens.css — Design Tokens(色值的单一来源 / SSoT) + * + * 命名格式:--color--- + * 组件域:form / table-row / table-header / ... + * 作用:bg(背景)/ fg(前景/字体)/ border + * 状态:edit / readonly / hover / selected(无状态时省略) + * + * 约束: + * - 组件样式中只用 var(--color-xxx),禁止硬编码 hex / rgba + * - 修改色值只改本文件,不允许在组件级覆盖 + * - 新增 token 直接在本文件登记(本文件即单一来源) + */ + +:root { + /* === 1. 全局调色板(与 Ant Design 主题对齐) === */ + --color-primary: #1890ff; + --color-success: #52c41a; + --color-warning: #faad14; + --color-error: #ff4d4f; + --color-text: rgba(0, 0, 0, 0.85); + --color-text-secondary: rgba(0, 0, 0, 0.45); + --color-border: #d9d9d9; + --color-bg-base: #f0f2f5; + + /* === 2. 组件级状态色 === */ + + /* form:输入框 / 备注框 / 时间框 / 下拉框共用 */ + --color-form-bg-edit: #ffffff; + --color-form-bg-readonly: #f1f2f8; + --color-form-bg-hover: #f5f5f5; /* 仅下拉框使用 */ + --color-form-fg: #000000; + + /* table */ + --color-table-row-bg-selected: #86d5fb; + --color-table-row-bg-hover: #fff7e6; + --color-table-row-bg-readonly: #f1f2f8; /* = rgb(241, 242, 248) */ + --color-table-row-fg: #000000; + --color-table-header-bg: #f5f5f5; + --color-table-header-fg: rgba(0, 0, 0, 0.85); /* = #000000D9 */ +}