# 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: ```json { "code": 0, "message": "ok", "data": } ``` - `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: ```json { "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 `, or - `X-Auth-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`: ```json { "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): ```json { "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`: ```json { "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 ```bash 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.