Files
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

234 lines
9.3 KiB
Markdown

# 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": <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:
```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 <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`:
```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.