恢复点(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>
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> }
codeis0on success; any non-zero value indicates an error.messageis"ok"on success, or a human-readable reason on error.dataholds the payload (ornullon 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>, orX-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" }
formmirrors the frontendFormSchema { title, rows: FormField[][], subTables }.flowmirrors the frontendFlowSchema { 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:
同意advancesnodeIndexby 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 byuser(matchesoriginUser)draft: status草稿, optionally filtered byuser
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:
- Add the driver to
build.gradle(e.g.runtimeOnly 'com.mysql:mysql-connector-j'orruntimeOnly 'org.postgresql:postgresql'). - Run with the matching profile:
- MySQL:
--spring.profiles.active=mysql(seeapplication-mysql.yml) - PostgreSQL:
--spring.profiles.active=postgres(seeapplication-postgres.yml)
- MySQL:
- 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.