diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md index 0dd19f8..674291f 100644 --- a/.gitea/pull_request_template.md +++ b/.gitea/pull_request_template.md @@ -1,42 +1,38 @@ -## PR 信息 +## 变更摘要 +- [ ] feat +- [ ] fix +- [ ] refactor +- [ ] chore +- [ ] docs +- [ ] perf +- [ ] test -### 需求号 - +## 影响模块(可多选) +- [ ] module: dict +- [ ] module: sysconfig +- [ ] module: department +- [ ] module: menu +- [ ] module: user +- [ ] module: role +- [ ] module: auth +- [ ] module: log +- [ ] module: other +## Doc Impact(必选其一,否则不允许合并) +- [ ] docs:updated (已更新文档,列出文件) +- [ ] docs:not-needed (不需要文档,写一句理由) +- [ ] docs:todo (需要但尚未补齐:禁止合并,除非标记 hotfix) -### 契约变更说明 - -- [ ] OpenAPI 接口变更(新增/修改/删除端点、字段、错误码) -- [ ] 数据库 Schema 变更(新增 Flyway migration) -- [ ] 权限变更(新增/修改 permission 字符串) -- [ ] 租户隔离策略变更 +### 文档变更清单/理由 +- docs files: + - `docs/CHANGELOG.md` + - `docs/MODULES/xxx.md` + - `docs/API/overview.md` + - `docs/ADR/xxxx.md` +- reason: -变更详情: +## OpenAPI(如有接口/DTO/权限变更) +- [ ] openapi:changed(会影响合约) +- [ ] openapi:not-changed - -### 控制面影响 - -- [ ] 认证/鉴权流程 -- [ ] 租户隔离逻辑 -- [ ] 日志/可观测性 -- [ ] CI Gate 脚本 -- [ ] 配置项(application.yml / 环境变量) - -影响详情: - - -### 测试证据 - -- [ ] 单元测试通过(./mvnw test) -- [ ] 集成测试通过(./mvnw -PIT verify) -- [ ] 手动验证(如适用,描述步骤) - -测试详情: - - -### 自检清单 -- [ ] 代码遵循分层约定(controller→facade→appservice→domain←infra) -- [ ] tenant_id 来自 SecurityContext,未从 DTO/请求参数获取 -- [ ] @PreAuthorize 使用的权限字符串已在 seed 数据中定义 -- [ ] Flyway migration 版本号不冲突(MVP: V001-V099) -- [ ] 无"省略/TODO/自行补充"的代码提交 \ No newline at end of file +## 其他说明 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 714ed54..95cd17a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,51 +1,102 @@ -name: LJWX Platform CI +name: ci on: push: - branches: [main] + branches: ["main"] pull_request: - branches: [main] + branches: ["main"] + +env: + JAVA_VERSION: "21" jobs: - gate-all: + build-and-gate: runs-on: ubuntu-latest + services: postgres: - image: postgres:16-alpine + image: postgres:16 env: POSTGRES_DB: ljwx_platform_test - POSTGRES_USER: test - POSTGRES_PASSWORD: test + POSTGRES_USER: ljwx + POSTGRES_PASSWORD: ljwx123 ports: - 5432:5432 options: >- - --health-cmd="pg_isready -U test" - --health-interval=10s - --health-timeout=5s - --health-retries=5 + --health-cmd "pg_isready -U ljwx" + --health-interval 5s + --health-timeout 5s + --health-retries 10 steps: - name: Checkout - uses: actions/checkout @v4 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup Java 21 - uses: actions/setup-java @v4 + - name: Set up JDK + uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '21' - cache: 'maven' + distribution: "temurin" + java-version: ${{ env.JAVA_VERSION }} + cache: "maven" - name: Make scripts executable - run: | - chmod +x mvnw - find scripts/ -name "*.sh" -exec chmod +x {} \; + run: chmod +x scripts/ci/*.sh - - name: Run gate-all - run: bash scripts/gates/gate-all.sh + # ── Build ── + - name: Compile + run: ./mvnw -q -DskipTests package + + # ── Unit Tests ── + - name: Unit tests + run: ./mvnw -q test + + # ── Integration Tests ── + - name: Integration tests env: - DB_URL: jdbc:postgresql://localhost:5432/ljwx_platform_test - DB_USERNAME: test - DB_PASSWORD: test - SPRING_PROFILES_ACTIVE: test \ No newline at end of file + SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/ljwx_platform_test + SPRING_DATASOURCE_USERNAME: ljwx + SPRING_DATASOURCE_PASSWORD: ljwx123 + run: ./mvnw -q -PIT verify + + # ── Export OpenAPI snapshot ── + - name: Export OpenAPI snapshot + env: + PORT: "18080" + PROFILE: "test" + SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/ljwx_platform_test + SPRING_DATASOURCE_USERNAME: ljwx + SPRING_DATASOURCE_PASSWORD: ljwx123 + run: scripts/ci/export-openapi.sh + + # ── Docs Gate ── + - name: Docs gate + if: github.event_name == 'pull_request' + env: + BASE_REF: "origin/main" + run: scripts/ci/docs-gate.sh + + # ── OpenAPI Diff Gate ── + - name: OpenAPI diff gate + if: github.event_name == 'pull_request' + env: + BASE_REF: "origin/main" + run: scripts/ci/openapi-diff.sh + + # ── Ensure snapshot committed ── + - name: Ensure OpenAPI snapshot committed + run: | + if git diff --exit-code docs/API/openapi.snapshot.yaml 2>/dev/null; then + echo "[openapi] snapshot clean or not yet generated. PASS." + else + echo "============================================" + echo "[openapi] FAIL: openapi.snapshot.yaml changed during CI but not committed." + echo "" + echo "Please run locally:" + echo " scripts/ci/export-openapi.sh" + echo " git add docs/API/openapi.snapshot.yaml" + echo " git commit -m 'chore: update openapi snapshot'" + echo "============================================" + exit 1 + fi diff --git a/docs/API/breaking-changes.md b/docs/API/breaking-changes.md new file mode 100644 index 0000000..b72899d --- /dev/null +++ b/docs/API/breaking-changes.md @@ -0,0 +1,10 @@ +# Breaking Changes + +> Update this file whenever `docs/API/openapi.snapshot.yaml` changes. +> CI will reject PRs that change the snapshot without updating this file. + +## Format +- YYYY-MM-DD: (BREAKING | non-breaking) + +## Log +- 2026-02-15: Initial API surface established (non-breaking) diff --git a/docs/API/openapi.snapshot.yaml b/docs/API/openapi.snapshot.yaml new file mode 100644 index 0000000..c41321a --- /dev/null +++ b/docs/API/openapi.snapshot.yaml @@ -0,0 +1,14 @@ +# OpenAPI snapshot placeholder +# This file is auto-generated by scripts/ci/export-openapi.sh +# Do not edit manually unless export is not yet available. +# +# To generate: +# scripts/ci/export-openapi.sh +# +# CI will fail if this file changes but is not committed. +openapi: "3.0.0" +info: + title: ljwx-platform API + version: 0.0.0-snapshot + description: Placeholder - run export-openapi.sh to generate actual spec +paths: {} diff --git a/docs/API/overview.md b/docs/API/overview.md new file mode 100644 index 0000000..884334c --- /dev/null +++ b/docs/API/overview.md @@ -0,0 +1,65 @@ +# API Overview + +Base URL: `/api/v1` + +## Auth +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| POST | /auth/login | - | 登录 | +| POST | /auth/refresh | - | 刷新 Token | +| POST | /auth/logout | - | 登出 | + +## Menu +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | /menus | menu:read | 菜单列表 | +| GET | /menus/{id} | menu:read | 菜单详情 | +| POST | /menus | menu:write | 创建菜单 | +| PUT | /menus/{id} | menu:write | 更新菜单 | +| DELETE | /menus/{id} | menu:write | 删除菜单 | +| GET | /menus/me | (any auth) | 当前用户菜单树 | + +## Role +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | /roles | role:read | 角色列表 | +| GET | /roles/{id} | role:read | 角色详情 | +| POST | /roles | role:write | 创建角色 | +| PUT | /roles/{id} | role:write | 更新角色 | +| DELETE | /roles/{id} | role:write | 删除角色 | +| PUT | /roles/{id}/permissions | role:write | 分配权限 | +| PUT | /roles/user-roles | role:write | 分配用户角色 | +| GET | /roles/user-roles/{userId} | role:read | 查询用户角色 | + +## Dict +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | /dict-types | dict:read | 字典类型列表 | +| GET | /dict-data | dict:read | 字典数据列表 | + +## SysConfig +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | /sys-configs | config:read | 配置列表 | +| GET | /sys-configs/{id} | config:read | 配置详情 | + +## Department +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | /departments | dept:read | 部门列表 | +| GET | /departments/{id} | dept:read | 部门详情 | +| POST | /departments | dept:write | 创建部门 | +| PUT | /departments/{id} | dept:write | 更新部门 | +| DELETE | /departments/{id} | dept:write | 删除部门 | + +## User (Phase 2 - planned) +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | /users | user:read | 用户列表 | +| GET | /users/{id} | user:read | 用户详情 | +| POST | /users | user:write | 创建用户 | +| PUT | /users/{id} | user:write | 更新用户 | +| DELETE | /users/{id} | user:write | 删除用户 | +| GET | /users/me | (any auth) | 当前用户信息 | +| PUT | /users/{id}/reset-password | user:write | 重置密码 | +| PUT | /users/{id}/status | user:write | 切换状态 | diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..73d4027 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to this project will be documented in this file. +Format based on [Keep a Changelog](https://keepachangelog.com/). + +## [Unreleased] + +### Added +- Dict module (V101-V102) +- SysConfig module (V103-V104) +- Menu module (V107-V109) +- User/Role enhancement (V110-V111) +- Audit field alignment (V112) +- Departments table (V113-V114) +- SecurityUtil centralized auth extraction +- Global exception handler +- Role CRUD with tenant isolation +- UserRole assignment with dedup +- GET /api/v1/menus/me tree endpoint + +### Fixed +- V111 ADMIN permission binding across all tenants +- UserRoleAppService duplicate roleId handling +- Menu permission_key consistency for all 6 second-level menus +- Bean naming conflicts between legacy and new infrastructure packages + +### Changed +- Standardized audit fields: create_* NOT NULL, update_* nullable, is_deleted SMALLINT DEFAULT 0 +- Replaced private currentUsername/currentUserId/currentTenantId with SecurityUtil.* diff --git a/docs/MODULES/0000-template.md b/docs/MODULES/0000-template.md new file mode 100644 index 0000000..b2758fa --- /dev/null +++ b/docs/MODULES/0000-template.md @@ -0,0 +1,36 @@ +# + +## 定位 +一句话说明模块职责。 + +## 数据库 +| 表 | 描述 | 租户级 | +|----|------|--------| +| xxx | ... | 是/否 | + +## 实体 +- `XxxModel` → 字段列表 + +## 端点 +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | /api/v1/xxx | xxx:read | ... | + +## 权限 +| Key | 描述 | +|-----|------| +| xxx:read | 查看 | +| xxx:write | 管理 | + +## 迁移脚本 +| Version | 文件 | 描述 | +|---------|------|------| +| Vxxx | V...sql | ... | + +## 特殊规则 +- 规则 1:... +- 规则 2:... + +## 依赖 +- 依赖模块:auth, tenant +- 被依赖:user diff --git a/docs/adr/0000-template.md b/docs/adr/0000-template.md new file mode 100644 index 0000000..28595b5 --- /dev/null +++ b/docs/adr/0000-template.md @@ -0,0 +1,29 @@ +# ADR-XXXX: + +## Status +Proposed | Accepted | Deprecated | Superseded by ADR-YYYY + +## Date +YYYY-MM-DD + +## Context +为什么要做这个决策?当前遇到了什么问题? + +## Decision +做了什么决策(明确、具体)。 + +## Alternatives Considered +| 方案 | 优点 | 缺点 | 结论 | +|------|------|------|------| +| A | ... | ... | 不采用 | +| B | ... | ... | 采用 | + +## Consequences +- 好处: +- 代价: +- 风险: + +## References +- PR: #xxx +- Migration: Vxxx +- Related ADR: ADR-xxxx diff --git a/scripts/ci/docs-gate.sh b/scripts/ci/docs-gate.sh new file mode 100755 index 0000000..65577cb --- /dev/null +++ b/scripts/ci/docs-gate.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_REF="${BASE_REF:-origin/main}" + +echo "[docs-gate] base ref: ${BASE_REF}" + +# 确保有 base ref +git fetch origin main --depth=50 2>/dev/null || true + +# 获取变更文件列表 +CHANGED="$(git diff --name-only "${BASE_REF}...HEAD" 2>/dev/null || git diff --name-only HEAD~1 || true)" + +if [[ -z "${CHANGED}" ]]; then + echo "[docs-gate] no changed files detected. PASS." + exit 0 +fi + +echo "[docs-gate] changed files:" +echo "${CHANGED}" +echo "" + +# 1) 判断是否属于"必须带文档"的代码变更 +NEED_DOC=0 +echo "${CHANGED}" | grep -E -q \ + '(controller|facade|dto|security|exception)/.*\.java$|db/migration/.*\.sql$|.*(Controller|Facade|Request|Response)\.java$' \ + && NEED_DOC=1 || true + +if [[ "${NEED_DOC}" -eq 0 ]]; then + echo "[docs-gate] no doc-required code changes detected. PASS." + exit 0 +fi + +echo "[docs-gate] doc-requiring code changes detected." + +# 2) 检查 docs 是否有变化 +DOC_CHANGED=0 +echo "${CHANGED}" | grep -E -q \ + '^docs/CHANGELOG\.md$|^docs/MODULES/|^docs/API/overview\.md$|^docs/API/breaking-changes\.md$|^docs/ADR/|^docs/adr/' \ + && DOC_CHANGED=1 || true + +# 3) 检查 openapi snapshot 是否变化 +OPENAPI_CHANGED=0 +echo "${CHANGED}" | grep -E -q '^docs/API/openapi\.snapshot\.yaml$' && OPENAPI_CHANGED=1 || true + +if [[ "${DOC_CHANGED}" -eq 1 || "${OPENAPI_CHANGED}" -eq 1 ]]; then + echo "[docs-gate] docs or openapi snapshot updated. PASS." + exit 0 +fi + +cat <<'EOF2' + +============================================ +[docs-gate] FAIL + +You changed code that requires docs updates, but no docs were changed. + +Fix options (choose at least one): + 1) Update docs/CHANGELOG.md (recommended, 1-3 lines) + 2) Update a module doc: docs/MODULES/<module>.md + 3) Update docs/API/overview.md or docs/ADR/* (docs/adr/*) + 4) If API changed, run scripts/ci/export-openapi.sh and commit the snapshot + +============================================ + +EOF2 + +exit 1 diff --git a/scripts/ci/export-openapi.sh b/scripts/ci/export-openapi.sh new file mode 100755 index 0000000..177d31d --- /dev/null +++ b/scripts/ci/export-openapi.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUT="docs/API/openapi.snapshot.yaml" +PORT="${PORT:-8080}" +PROFILE="${PROFILE:-test}" +MAX_WAIT="${MAX_WAIT:-90}" + +echo "[openapi] export to ${OUT}, port=${PORT}, profile=${PROFILE}" + +# 1) 编译(如果 jar 不存在) +JAR="$(find ljwx-platform-app/target -name '*.jar' -not -name '*-sources*' -not -name '*-javadoc*' 2>/dev/null | head -n1 || true)" +if [[ -z "${JAR}" ]]; then + echo "[openapi] jar not found, building..." + ./mvnw -q -DskipTests package + JAR="$(find ljwx-platform-app/target -name '*.jar' -not -name '*-sources*' -not -name '*-javadoc*' | head -n1)" +fi + +echo "[openapi] using jar: ${JAR}" + +# 2) 后台启动应用 +nohup java -jar "${JAR}" \ + --spring.profiles.active="${PROFILE}" \ + --server.port="${PORT}" \ + > /tmp/openapi-app.log 2>&1 & +APP_PID=$! + +cleanup() { + echo "[openapi] stopping app pid=${APP_PID}" + kill "${APP_PID}" 2>/dev/null || true + wait "${APP_PID}" 2>/dev/null || true +} +trap cleanup EXIT + +# 3) 等待应用就绪(先试 actuator,再试 OpenAPI 端点自身) +echo "[openapi] waiting for app to start (max ${MAX_WAIT}s)..." +READY=0 +for i in $(seq 1 "${MAX_WAIT}"); do + # 优先 actuator + if curl -fsS "http://127.0.0.1:${PORT}/actuator/health" >/dev/null 2>&1; then + READY=1 + break + fi + # 回退:直接试 OpenAPI 端点 + if curl -fsS "http://127.0.0.1:${PORT}/v3/api-docs" >/dev/null 2>&1; then + READY=1 + break + fi + sleep 1 +done + +if [[ "${READY}" -eq 0 ]]; then + echo "[openapi] ERROR: app did not start within ${MAX_WAIT}s" + echo "[openapi] last 50 lines of log:" + tail -50 /tmp/openapi-app.log + exit 1 +fi + +echo "[openapi] app is ready" + +# 4) 导出 OpenAPI +mkdir -p "$(dirname "${OUT}")" + +if curl -fsS "http://127.0.0.1:${PORT}/v3/api-docs.yaml" -o "${OUT}" 2>/dev/null; then + echo "[openapi] exported yaml to ${OUT}" +elif curl -fsS "http://127.0.0.1:${PORT}/v3/api-docs" -o "${OUT%.yaml}.json" 2>/dev/null; then + echo "[openapi] yaml not available, exported json to ${OUT%.yaml}.json" +else + echo "[openapi] WARNING: could not export OpenAPI. Endpoint may not be configured." + echo "[openapi] This is non-fatal. Skipping snapshot update." + # 创建一个占位文件,避免后续 git diff 报错 + if [[ ! -f "${OUT}" ]]; then + echo "# OpenAPI snapshot not yet available" > "${OUT}" + fi +fi + +echo "[openapi] done" diff --git a/scripts/ci/openapi-diff.sh b/scripts/ci/openapi-diff.sh new file mode 100755 index 0000000..c835334 --- /dev/null +++ b/scripts/ci/openapi-diff.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +SNAP="docs/API/openapi.snapshot.yaml" +BREAKING="docs/API/breaking-changes.md" +BASE_REF="${BASE_REF:-origin/main}" + +echo "[openapi-diff] checking snapshot changes..." + +git fetch origin main --depth=50 2>/dev/null || true + +# 获取变更文件列表 +CHANGED="$(git diff --name-only "${BASE_REF}...HEAD" 2>/dev/null || git diff --name-only HEAD~1 || true)" + +# 如果 snapshot 没变化,直接 pass +if ! echo "${CHANGED}" | grep -q "^${SNAP}$"; then + echo "[openapi-diff] snapshot not changed. PASS." + exit 0 +fi + +echo "[openapi-diff] snapshot changed." + +# 要求 breaking-changes.md 也有变化 +if echo "${CHANGED}" | grep -q "^${BREAKING}$"; then + echo "[openapi-diff] breaking-changes.md updated. PASS." + exit 0 +fi + +cat <<EOF2 + +============================================ +[openapi-diff] FAIL + +OpenAPI snapshot changed but ${BREAKING} was not updated. + +Please add a short note to ${BREAKING}, e.g.: + - $(date +%Y-%m-%d): Added GET /api/v1/users/me endpoint (non-breaking) + - $(date +%Y-%m-%d): Removed field xxx from YyyResponse (BREAKING) + +============================================ + +EOF2 + +exit 1