# 后端架构

> **最后更新**: 2026-04-06

---

## 分层架构

```
Controller 层 → Service 层 → Repository 层 → Database
    ↓              ↓              ↓              ↓
  路由处理      业务逻辑       数据访问        数据存储
```

### 各层职责

| 层 | 职责 | 不应做 |
|----|------|--------|
| **Controller** | HTTP 请求处理、参数验证、响应格式化 | 业务逻辑、直接操作数据库 |
| **Service** | 业务逻辑、事务管理、跨模块协调 | HTTP 相关逻辑、直接构造响应 |
| **Repository** | 数据访问封装（可选，简单场景 Service 直接用 Prisma） | 业务规则判断 |

---

## 三层模块架构

```
backend/src/
├── core/        # 核心基础设施（与业务无关，可跨项目复用）
│                # 数据库、消息通信、日志、审计、工作流
├── engines/     # 可复用引擎（通用业务能力，可配置、跨模块使用）
│                # 审批引擎、表单引擎
├── modules/     # 业务模块（具体业务功能）
│                # 组织架构、零件管理、绩效、考勤等
└── common/      # 公共工具（Guards、Interceptors、Filters、Decorators）
```

**为什么分三层**：
- **Core** 提供技术底座，不关心业务
- **Engines** 是可配置的通用引擎（如审批流），被多个业务模块使用
- **Modules** 是具体业务，依赖 Core 和 Engines，但模块之间尽量不互相依赖

---

## 横切关注点

认证、授权、日志、异常处理通过 NestJS 的 Guards、Interceptors、Filters 统一处理，不在每个 Controller 重复实现。

### 全局错误响应（`AllExceptionsFilter`）

`backend/src/common/filters/http-exception.filter.ts` 是 `useGlobalFilters` 注册的全局兜底。除 `HttpException` 按业务自定义 status 返回外，下列 framework 边界错误统一标准化（**全 backend 生效**，模块无需各自 try/catch）：

| 触发 | HTTP | error.code | error.message | 触发场景 |
|---|---|---|---|---|
| `PrismaClientKnownRequestError` P2023 | 400 | `INVALID_ID_FORMAT` | `Invalid id format` | 字段类型不匹配（典型：uuid 列收到非法字符串） |
| `PrismaClientKnownRequestError` P2025 | 404 | `NOT_FOUND` | `Record not found` | update / delete / findFirstOrThrow 找不到记录 |
| `PrismaClientKnownRequestError` 其它（P2002 / P2003 等） | 500 | `DATABASE_ERROR` | `数据库操作失败` | 唯一冲突、FK 失败等；prod 不暴露 raw（dev 暴露 prismaCode + prismaMessage 到 details） |

**适用范围**：所有走 NestJS 标准 `throw HttpException` 路径的 controller（包括 organization / robot-manager / agent / form-management / audit / logging / platform-master 等）。

**例外**：`meeting-attendance` 模块的 9 个 controller 用 `@SkipTransform() + @Res() res` 手动响应，**不走本 filter**，自有 `handleMeetingAttendanceError` helper（`backend/src/modules/meeting-attendance/errors/handle-controller-error.ts`）按**相同 HTTP 状态码语义**兜底（P2023→400 / P2025→404）。两套并存——是历史架构（@Res() 派系），未来可统一到 NestJS 标准路径，属大重构。

**响应 body shape 两套不同**（HTTP 码相同，body 结构不同——前端按调用 URL 走的派系解析）：

| 派系 | 触发 controller | body 形态 |
|---|---|---|
| **filter（NestJS 标准）** | 除 meeting-attendance 外所有模块 | `{ success: false, error: { code: "INVALID_ID_FORMAT", message, details }, timestamp, path, method, statusCode }`（`ApiErrorResponse`，详见 `http-exception.filter.ts`）|
| **helper（meeting-attendance @Res()）** | 9 个 meeting-attendance controller | 扁平 `{ error: "Invalid id format" }`，可选 `code` 字段（仅 MeetingAttendanceError 业务码携带）|

filter 示例：
```json
{
  "success": false,
  "error": { "code": "INVALID_ID_FORMAT", "message": "Invalid id format", "details": null },
  "timestamp": "...",
  "path": "...",
  "method": "...",
  "statusCode": 400
}
```

helper 示例：
```json
{ "error": "Invalid id format" }
```

模块 07-api.md 的"通用响应"或"错误码"段**不需要**重复列出 `INVALID_ID_FORMAT` / `NOT_FOUND` / `DATABASE_ERROR`，前后端默认知晓这是 framework 兜底层，从模块自定义错误码中跳过。

---

## 外部数据同步：必须复用同步中心

任何"外部系统 → 本地"的数据同步（包括但不限于 Entra/LDAP、Dingtalk、ADP、SAP、Outlook）**必须**接入既有的同步中心 `platform_automation`，**不得**自建并行的任务调度 / 执行记录基础设施。

### 同步中心提供的能力

```
platform_automation schema:
  AutomationTask           ← 任务定义（unique code, type 枚举, schedule 配置, 统计字段）
    └─ AutomationExecution ← 每次执行的记录（status, duration, logs, error, result）

配套：管理后台任务列表、执行历史查询、手动触发、失败重试、统一告警。
```

### 接入方式（标准模板）

1. **任务类型枚举**：在 `platform_automation.prisma` 的 `AutomationTaskType` 加新值（如 `ADP_SYNC`）
2. **同步源数据表**：放在 `platform_automation` schema 内（参考 `DingtalkEmployee` / `DingtalkLeaveQuotaSnapshot`）
3. **任务注册**：模块启动时 `upsert AutomationTask`（unique code，含 schedule + status）
4. **执行记录**：每次 cron 触发创建 `AutomationExecution` 行，结束时更新 status / duration / logs / error；同时累加 task 的 totalRuns / successRuns / failedRuns / lastRunAt
5. **代码位置**：`backend/src/modules/organization/<source>/sync/`（参考 `organization/sync/`、`organization/dingtalk/sync/`）

### 反模式（红灯，禁止）

- ❌ 新建 `XxxSyncRun` / `XxxSyncLog` / `XxxSyncHistory` 表（应复用 `AutomationExecution`）
- ❌ 在业务 schema（如 `corp_hr`）下放外部同步源数据（应放在 `platform_automation`）
- ❌ 在模块内部自起 cron 调度而不注册到 `AutomationTask`（运维看不到、不能手动触发）
- ❌ 把外部 ID 当主键（外部 ID 应该是 `@unique` 字段，主键仍用 `cuid()`/`uuid()`）

### 例外

只有当同步本质上不属于"周期性数据同步"（如：实时 webhook 接收、用户主动触发的一次性导入）时才考虑不入同步中心。任何"按 cron 周期跑、要监控成功率、要看历史"的任务都必须入。

### 决策检查清单（设计阶段必过）

开新功能涉及外部系统数据时，三问：
1. 这个数据是周期性同步进来的吗？是 → 入同步中心
2. 同步源数据要存表吗？要 → 表放 `platform_automation` schema
3. 我在想新建一个"任务记录"或"运行日志"表吗？是 → 停，先看 `AutomationExecution` 是否够用

---

## 相关文档

- [后端规范详情](../../.agents/skills/backend-main/references/backend-standards.md)
- [数据库规范](../../.agents/skills/database-main/references/database-standards.md)
