Files
ERP/oa-backend/API.md
T
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

9.3 KiB

OA Backend API

Standalone Spring Boot backend for the 致远-style OA (collaborative office) system. It replaces the legacy OFBiz engine and mirrors the Vue3 + Element Plus frontend engine model (form schemas, flow schemas, templates, submitted instances driven by an approval state machine).

  • Base URL: http://localhost:8090
  • All OA routes are under /api/oa
  • Java 17, Spring Boot 3.2.5, Spring Data JPA, SQLite (file at ./data/oa.db)

Response envelope

Every endpoint returns a uniform JSON envelope:

{ "code": 0, "message": "ok", "data": <payload> }
  • code is 0 on success; any non-zero value indicates an error.
  • message is "ok" on success, or a human-readable reason on error.
  • data holds the payload (or null on error).

Error codes used: 400 (bad request / validation), 401 (not authenticated), 403 (disabled / forbidden), 404 (not found), 500 (internal error). Errors are produced by a @RestControllerAdvice (GlobalExceptionHandler).

Example error:

{ "code": 404, "message": "template not found: nope", "data": null }

Authentication

Login returns an opaque token. Send it back on subsequent requests as either:

  • Authorization: Bearer <token>, or
  • X-Auth-Token: <token>

This is a minimal token scheme (in-memory token store). Workflow actions are attributed to the resolved user; unauthenticated callers fall back to the demo label 我(当前用户). Real security hardening (salted password hashing, token expiry, RBAC enforcement) is a later phase. The auth check hook lives in CurrentUserResolver / AuthService.resolve.

Seeded users (password 123456 for all): admin, zhangwei, lina, wangfang, liuyang, chenjing.


Routes

Health

Method Path Description
GET /api/oa/health Liveness probe. data: { "status": "UP" }

Auth

Method Path Body Description
POST /api/oa/auth/login { "loginName", "password" } Log in; returns session + token
GET /api/oa/auth/session Current session (requires token); 401 if absent
POST /api/oa/auth/logout Invalidate the token

Login / session data:

{ "token": "...", "id": 1, "loginName": "admin", "displayName": "系统管理员",
  "deptId": 4, "title": "信息中心主任", "email": "admin@kaidi.com" }

Org

Method Path Description
GET /api/oa/users List users (no passwords)
GET /api/oa/users/{id} One user
GET /api/oa/depts/tree Department tree (roots with nested children)
GET /api/oa/roles Role list

Form templates

Method Path Query / Body Description
GET /api/oa/form-templates ?category= (optional) List templates (optionally by category)
GET /api/oa/form-templates/{id} One template
POST /api/oa/form-templates { id?, name, category, org, form, flow, instructions } Create a custom template

Template data (the stored JSON TEXT schemas are parsed back into objects):

{ "id": "leave-apply", "name": "请假申请", "category": "人力资源", "org": "行政部",
  "form": { "title": "...", "rows": [[ ... ]], "subTables": [ ... ] },
  "flow": { "nodes": [ ... ], "edges": [ ... ], "parallels": [ ... ], "branches": [ ... ] },
  "instructions": "...", "builtin": true, "publishedAt": "2024-06-03" }
  • form mirrors the frontend FormSchema { title, rows: FormField[][], subTables }.
  • flow mirrors the frontend FlowSchema { nodes, edges, parallels, branches } with node types 审批 / 知会 / 协同 / start / end.

Built-in catalog (ported 1:1 from the frontend templates/index.ts): baohan-payment, expense-reimburse, leave-apply, seal-apply, general-collab, supplier-access, business-trip, purchase-contract, payment-apply, overtime-apply, expense-cond.

Form instances (workflow)

Method Path Body Description
POST /api/oa/form-instances { templateId, data, title } Submit a new item (lands at first node, status 待办)
POST /api/oa/form-instances/draft { templateId, data, title } Save a draft (status 草稿, no trace)
GET /api/oa/form-instances/{id} One instance (with template, flow, trace)
POST /api/oa/form-instances/{id}/advance { action, opinion } Advance the instance
POST /api/oa/form-instances/{id}/send Send a draft (草稿 → 待办, seeds 发起 trace)

action is one of 同意 / 退回 / 转交 / 加签 / 办结. State-machine semantics mirror the frontend store.advanceItem:

  • 同意 advances nodeIndex by one; past the last node → 已办结.
  • 办结 jumps straight to the end → 已办结.
  • 退回 steps back one node (or returns to the initiator at index 0) → 已退回.
  • 转交 / 加签 stay on the current node and only log a trace step.

Instance data:

{ "id": 1, "templateId": "leave-apply", "templateName": "请假申请",
  "category": "人力资源", "title": "...", "status": "待办",
  "currentNode": "部门主管", "currentNodeId": "deptHead", "nodeIndex": 0,
  "originUser": "...", "data": { ... }, "createdAt": "...", "updatedAt": "...",
  "template": { ... }, "flow": { ... },
  "trace": [ { "node", "who", "opinion", "type", "time" }, ... ] }

status values: 草稿 / 待办 / 办理中 / 已办结 / 已退回. trace[].type values: 发起 / 同意 / 退回 / 转交 / 加签 / 办结 / 知会.

Tasks (instance views)

Method Path Query Description
GET /api/oa/tasks ?type=todo|done|sent|draft&user= Filtered instance list
  • todo: status 待办 / 办理中 / 已退回
  • done: status 已办结
  • sent: non-draft items, optionally filtered by user (matches originUser)
  • draft: status 草稿, optionally filtered by user

Documents

Method Path Query / Body Description
GET /api/oa/folders/tree Document folder tree
GET /api/oa/documents ?folderId= (optional) List files (optionally in a folder)
POST /api/oa/documents/upload-meta { folderId, name, ext, size, uploader } Register file metadata

File content storage is out of scope for this phase; only metadata is tracked.

Meetings

Method Path Body Description
GET /api/oa/meetings List meetings
GET /api/oa/meetings/{id} One meeting
POST /api/oa/meetings { subject, startTime, endTime, roomId, organizer, status } Create a meeting
GET /api/oa/meeting-rooms List meeting rooms
GET /api/oa/minutes ?meetingId= (optional) List meeting minutes

Schedule

Method Path Query / Body Description
GET /api/oa/schedule-events ?from=&to= (ISO-8601 instants, optional) List events
POST /api/oa/schedule-events { title, type, startTime, endTime, owner } Create an event

Announcements

Method Path Query / Body Description
GET /api/oa/announcements ?category= (新闻 / 公告, optional) List (top-pinned first, then newest)
GET /api/oa/announcements/{id} One announcement
POST /api/oa/announcements { category, title, content, author, top } Create an announcement

CORS

/api/** allows any localhost / 127.0.0.1 port (http://localhost:*, http://127.0.0.1:*) over methods GET/POST/PUT/DELETE/PATCH/OPTIONS with credentials. This covers the Vite dev server (default port 5175) and the same-origin built app. Configured in config/CorsConfig. Note: same-origin POST/PUT/DELETE still send an Origin header, so the dev port must be allowed even when calls are proxied — hence the wildcard.


Database swap note (SQLite now, MySQL / PostgreSQL reserved)

The active datasource is SQLite (jdbc:sqlite:./data/oa.db), zero-setup and file-based. JPA ddl-auto: update creates/updates the schema from the entities on startup, and DataSeeder seeds demo data the first time the tables are empty.

Switching databases later is config-only — no Java changes:

  1. Add the driver to build.gradle (e.g. runtimeOnly 'com.mysql:mysql-connector-j' or runtimeOnly 'org.postgresql:postgresql').
  2. Run with the matching profile:
    • MySQL: --spring.profiles.active=mysql (see application-mysql.yml)
    • PostgreSQL: --spring.profiles.active=postgres (see application-postgres.yml)
  3. Set the JDBC url / username / password in that profile file.

The profile stubs already declare the correct dialect and ddl-auto: update, so Hibernate recreates the same schema on the target database. The form/flow schemas and instance data are stored as JSON TEXT columns, which are portable across all three databases.


Build & run

export JAVA_HOME=/path/to/jdk-17
./gradlew clean build -x test      # produces build/libs/oa-backend-0.1.0.jar
java -jar build/libs/oa-backend-0.1.0.jar
# or:
./gradlew bootRun

The app listens on port 8090 and creates ./data/oa.db on first run.