# 统一 Go 后端(模块化单体) > 两个 PHP 后端(教务 ysw-finance + 教学 yuansiweimarking_v2)收敛为**一个 Go 后端**,模块化单体、单二进制(ADR-011)。 > 本文档覆盖四块:**技术栈 / 目录结构与用途 / 编码规范 / 构建与校验**。架构决策 ADR-001~016 全文见工作区文档 `unified/00-planning/用户系统合并规划.md`(本仓库不复制 ADR 原文,仅引用编号)。 ## 定位 单一 Go 服务承载全部业务域:身份/SSO、多租户组织、教务域、教学域、AI 编排、异步任务。对外经 **nginx 统一入口**(TLS + HTTP/2 终结,ADR-010)暴露;数据库为 **PolarDB MySQL 单 schema**(ADR-008,过渡态允许按域多库)。部署形态为**单机双实例·预留 K8s**(ADR-015)。 ## 1. 技术栈 与 `go.mod` 实际依赖一一对应(2026-11-05 核实): | 关注点 | 选型(版本) | 说明 / 依据 | |---|---|---| | 语言 | **Go 1.27** | `go.mod` 锁 `go 1.27`(语言级别 + 最低工具链);2026-11-05 由 1.23 升级,依赖零变化全绿实证 | | Web 框架 | **Fiber v2**(v2.52.16) | ADR-009 拍板;fasthttp 内核,吞吐高/内存低,契合 ~325 接口高频只读;SSE 支持好 | | ORM / 数据访问 | **GORM**(v1.31.2)+ Repository 分层 | 多租户 institution/campus 作用域由 Repository 强制注入(ADR-006/007);科目分表用动态表名 | | 数据库 | PolarDB MySQL 8.0(单 schema) | ADR-008;源库同引擎,阿里云 DTS 平滑迁移 | | 缓存 / 锁 | Redis(阿里云,6/7) | 热点缓存 + 限流计数 + 任务表分布式锁;登录态由 token 承载,Redis 不存会话 | | 身份 / SSO | Go 自研 identity 模块(Provider + client 一体) | ADR-014 **最小可行**:统一 token 签发 + 本地验签 + 用户中心 API,预留 OIDC 端点(`/auth/oidc/*` 占位 501) | | JWT | **golang-jwt/jwt/v5**(v5.3.1) | RS256 签发/验签;access 30min + refresh 30 天;jti 吊销走 Redis 黑名单 | | 身份主键 | **ULID**(github.com/oklog/ulid v1.3.1) | ADR-013:canonical_user_id = VARCHAR(26) Crockford,全项目统一;域外键禁引用旧 int user_id | | 密码哈希 | **argon2id**(golang.org/x/crypto v0.33.0) | ADR-016:登录成功**惰性升级**(hash_version 路由 md5_salt/sha_salt/argon2),不批量不强制重置 | | 入参校验 | **validator/v10**(v10.27.0) | 结构体 tag 校验;`Validate(c, dst) bool` 语义(铁律 7) | | 日志 | **zap**(go.uber.org/zap v1.28.0) | 结构化 JSON(ts/level/caller/msg/trace_id);trace_id 经 X-Request-ID 贯穿 | | 配置 | **JSON 模板 + 环境变量**(12-factor) | `config/{env}.json`(入库模板,只含非敏感默认值)← `CONFIG_*` 环境变量覆盖;敏感字段全走环境变量/KMS | | 角色 | **7 类**:超管/教务/财务/老师/学生/家长/市场 | 市场登录入口暂禁用(不进登录白名单 `1,2,3,4,5,8`) | | 异步任务 | 统一 DB 任务表 `t_task` + Go 工作池 | 替代旧 `t_queue` + `t_agent_queue` 两套队列;乐观锁认领 + 指数退避 + 幂等键 | | 对象存储 | 阿里云 OSS(按域隔离 bucket) | 文件资产迁移 | | AI 能力 | 消费 Python FastAPI Agent(OpenAPI 契约) | Agent 核心计算独立保留;Go 侧为消费方 + 任务派发,不可达时 fire-and-forget 不阻断主链路 | | 外部入口 | nginx(TLS + HTTP/2 终结) | ADR-010;`proxy_buffering off` 支持 SSE;按 location 分发 | | 监控 | Prometheus 指标 + `/healthz` 存活探针 | 登录成功率/接口错误率/队列积压(兼容期监控项) | ## 2. 目录结构与用途 ``` . ├── README.md ← 本文件(技术栈/目录/规范/构建) ├── EXAMPLES.md ← 可编译样例索引 + GoDoc 注释标准模板(编码规范 §3.4 的落地清单) ├── go.mod / go.sum ← Go 模块(go 1.27;依赖版本锁定,见 §4) ├── .gitignore ← 排除 config/*.json(敏感)、/log、/tmp、IDE 文件 ├── cmd/ │ └── api/main.go ← 单二进制入口:配置加载 → 运行目录幂等 → zap → Fiber 装配 │ (Recover→RequestID 中间件链 + /healthz + 优雅退出);业务域路由挂载点 ├── config/ │ └── dev.example.json ← 配置模板(**唯一允许入库的配置文件**;实际 config/dev.json 等走 .gitignore) └── internal/ ├── identity/ ← 身份与 SSO 域(被所有域依赖,不依赖业务域) │ ├── id/ ← canonical_user_id 发放与校验(ULID,ADR-013)【已有样例】 │ ├── credential/ ← 密码凭证:argon2id 哈希 + hash_version 路由 + 惰性升级(ADR-016)【已有样例】 │ └── token/ ← RS256 签发/验签/refresh 轮换/jti 吊销(ADR-014)【已有样例】 ├── org/ ← 多租户组织域:Institution/Campus/Membership/Role(7)/GuardianLink(规划待建) ├── academic/ ← 教务域:排班/费用/打卡/充值(SP Go 事务重写)/报课/跨系统同步(规划待建) ├── teaching/ ← 教学域:备课/练习分析/题库/组卷/科目分表路由/共享标志(规划待建) ├── ai/ ← AI 编排域:OpenAPI 消费方 → Python Agent + 任务派发(规划待建) ├── task/ ← 统一任务表仓储:t_task 状态机/乐观锁认领/指数退避/僵死回收【已有样例】 └── infra/ ← 横切基础设施(被所有域依赖,**不依赖任何业务域**) ├── response/ ← 统一响应信封 R + OK/Page/Fail + ParsePage【已有样例】 ├── errors/ ← 三层错误模型 AppError + 1xxx~5xxx 分段 sentinel【已有样例】 ├── scope/ ← 多租户作用域 Scope + BaseRepo.scoping 强制过滤【已有样例】 ├── validator/ ← 入参校验 Validate(c,dst) bool(铁律 7 语义)【已有样例】 └── config/ ← Config 结构 + Load(JSON+环境变量+必填 fail-fast)+ 运行目录幂等【已有样例】 ``` **目录用途约定**: | 目录 | 用途 | 依赖方向 | |---|---|---| | `cmd/api` | 装配与进程生命周期(不写业务逻辑) | → 所有 internal 域 | | `config/` | 环境配置模板(12-factor) | — | | `internal/identity` | 身份地基(ULID/凭证/token) | 被所有域依赖;不依赖业务域 | | `internal/org` | 多租户 + 7 角色 + 作用域解析 | 被业务域依赖;不依赖业务域 | | `internal/academic` `internal/teaching` | 两个业务域 | 依赖 identity/org/infra;域间经接口 | | `internal/ai` | Agent 消费方 | 依赖 infra/task | | `internal/task` | 任务表横切 | 依赖 infra;不依赖业务域 | | `internal/infra` | 横切基类(响应/错误/作用域/校验/配置) | 被所有域依赖;**严禁反向依赖** | > **样例即规范**:标【已有样例】的包均为可编译参考实现(`go build` + `go vet` 全绿), > 每个导出符号带三段式 GoDoc 中文注释。新增业务域代码须对齐其风格(编码规范 §3.4), > 索引与模板见 [EXAMPLES.md](./EXAMPLES.md)。 ## 3. 编码规范 ### 3.1 分层架构(调用拓扑,禁止反向/跨层) ``` nginx → HTTP 接入层(cmd/api:Recover→RequestID→CORS→Throttle→Log→[Auth]) → Handler(internal/{域}/handler:参数绑定/校验 → 调 Service → 统一响应 R) → Service(internal/{域}/service:业务编排/事务/领域规则;返回 sentinel 错误) → Repository(internal/{域}/repo:唯一数据访问入口;强制作用域注入) → Domain/Model + infra(实体 + 横切基础设施) ``` | 层 | 职责 | 禁止 | |---|---|---| | Handler | 绑定入参、调 Service、映射统一 `R` | 直接碰 DB/Repository;写 SQL;吞错误 | | Service | 业务规则、事务边界、跨域编排(经接口) | 处理 HTTP;返回 gorm 原错误 | | Repository | 唯一数据访问;作用域注入;GORM 纪律 | 业务判断;跨域 JOIN(跨库只"取 ID 回查") | | Infra | 横切基础设施 | 依赖任何业务域(单向) | **分包铁律**: 1. 域间不直接互调内部实现,跨域走 `internal/` 内定义的接口(接口在 `org`/`identity` 侧,消费方在业务域)。 2. 所有数据访问必须经 Repository 层,作用域由中间件从 token 解析后强制注入,禁止绕过。 3. `infra` 不依赖任何业务域;`org`/`identity` 不得反向依赖业务域。 4. SP 行为(充值/审批/同步)在 `academic` 域内用 Go 事务重写,旧存储过程仅留档对照,不迁移。 5. 硬编码身份/魔法值(如旧系统 `user->id==393`)一律进配置或权限判断,消灭目标为 0。 ### 3.2 接口规范 - **统一响应契约**:HTTP 恒 200,真实状态在 body `R.code`(成功 200;`{code, data, message, total?, success}` 信封,顶层 `total` 解决旧 Success/SuccessPage 不一致);e2e 断言 **body code** 而非 HTTP status。 - **错误码分段**:`1xxx` 认证/权限(1001 未登录/1002 失效/1003 无权限/1004 作用域越界)· `2xxx` 参数/校验 · `3xxx` 数据(3001 不存在/3002 冲突/3003 唯一约束)· `4xxx` 依赖(Agent/支付/第三方)· `5xxx` 系统(5001 内部错误/5003 限流)。 - **入参校验**:`validator.Validate(c, &req) bool`——失败时内部已写 body(code=2001)并返 false,调用方 `if !ok { return nil }`;**禁用 `err != nil` 判**(误判通过继续落库——教务踩坑)。 - **路由纪律**:`/{端}/{模块}/{资源}` RESTful;**字面路径先于 `/:id`** 注册;同 method+path 不重复注册;新增端点先全局 grep 同路径;公开路由(登录/SSO)在 Auth Group **之前**注册。 - **分页**:`page`(默认 1)/`pageSize`(默认 10,上限 200);列表"先 pluck 分页 id → 批量取详情"防 N+1。 - **命名**:入参/出参 `camelCase`,DB 列 `snake_case`;外键统一 `_id` 后缀(消灭旧 `userid/schoolid` 混用)。 - **流式(SSE/AI)**:错误也走 `text/event-stream`,不得返 JSON 让前端 SSE 解析失败。 ### 3.3 数据访问纪律(GORM,源自教务重构踩坑) - 可空列(`IS_NULLABLE=YES`)一律 `*string`/`*int`,写边界 nil→""/0 归一(防 `converting NULL` → 500)。 - 聚合别名/无下划线列/SP 返 camelCase:行结构体显式 `gorm:"column:xxx"`。 - 链式调用三查各自 `Model()`,base 不带 Select;`Count` 收 `*int64`;存在性判断用 `Take`。 - `Updates` 的 `RowsAffected==0` 可能是"值未变"非"失败":对齐旧系统"行存在即成功",`Take` 预检再更新。 - 事务内全部用 `tx` 句柄;动态表名(科目分表)收敛一处——`Table(subjectTable(subjectID))`,体育(10) 已对齐补齐。 - 跨库过渡态只"取 ID 回查"(`whereIn`),禁 ORM 跨库 join;`only_full_group_by` 用 `Distinct().Order()`;保留字反引号。 ### 3.4 注释标准(GoDoc 三段式,强制) 每个**导出**的结构体/函数/方法/常量/错误必须带 GoDoc 中文注释: 1. **功能说明**:用途 + 核心逻辑 + 对应 ADR/铁律;导出项附"典型使用样例"; 2. **入参说明**:逐项(名称、类型、含义、约束/默认值/敏感标注); 3. **出参说明**:逐项(名称、类型、含义)+ **异常场景单列**(哪些输入/状态出错、调用方怎么处理、降级还是 fail-closed)。 代码即文档:改实现必须同步改注释;注释描述与实现不符 = bug。模板与索引见 [EXAMPLES.md](./EXAMPLES.md)。 ### 3.5 异常处理 三层错误模型:`infra/errors.AppError{Code, Msg, Cause}`。 - **Repository**:`gorm.ErrRecordNotFound` 等转 sentinel(`errors.Is` 判定),不泄漏 ORM 原错; - **Service**:只返回 sentinel(`errors.WithCause` 挂底层原因供日志,不进响应体); - **Handler**:`response.Fail(c, err)` 统一出口;非 `*AppError` 降级 5001 + 结构化日志(带 stack + trace_id); - **全局 Recover**:兜底 panic → 5001,**绝不**把 panic 信息/堆栈/SQL 片段/哈希透传前端。 ### 3.6 日志与配置 - **日志**:zap 结构化(ts/level/caller/msg/trace_id);trace_id 经 nginx `X-Request-ID` 生成/透传,跨 Python Agent 调用同值透传;级别生产 Info;**脱敏**(手机号打码、密码/密钥/JWT 不落日志);高频只读访问日志可采样,写操作全量。 - **配置**:加载顺序 `config/{env}.json` ← `CONFIG_*` 环境变量 ← 启动参数;必填字段缺失 fail-fast 拒绝启动;**敏感字段(DB DSN/JWT 私钥/OSS/微信/Agent 密钥)绝不进代码库/日志**,走环境变量或 KMS;启动时幂等建 `log/{instance}/`、`tmp/{instance}/`(双实例同机隔离)。 ### 3.7 异步任务 业务域写 `t_task`(status=pending,幂等键 = type + 业务唯一键)→ 工作池(并发度 `task.workers`)抢占;**认领用条件 UPDATE 乐观锁**(双实例防重复消费);失败指数退避(2^retry 秒,上限 1h)超 `maxRetry` 置 dead 告警;**派发成功 ≠ 任务完成**(AI 批改/导出/PDF),前端轮询任务终态;Agent 不可达 fire-and-forget 不阻断主链路;割接时在途任务先排空或标记重投。 ### 3.8 测试与质量门禁 | 门禁 | 命令 / 做法 | 基线 | |---|---|---| | 编译 | `go build ./... && go vet ./...` | 0 错误 0 告警 | | 单测 | `go test -count=1 ./internal/... ./cmd/...` | 表驱动 + 黄金测试,0 失败 | | e2e | TestMain 建库建 SP→跑→DROP;**独立业务键/独立 id 段/独占时间窗/专属 token**(避开教务已占 97090/97481/98141) | 多 e2e 共用库不撞 | | schema 核对 | 复用 config+gorm 查 `information_schema`(DATA_TYPE/IS_NULLABLE) | 割接前逐表过 | | 端点实测 | 真实登录拿 token → curl → **断言 body code + 结构 + 富化字段** | 结构对不上先查路由重复注册 | | 并发守门 | `TestE2E_ConcurrentLogin`(后台 goroutine 禁访问 fiber.Ctx) | 0 失败 | ### 3.9 Git 与提交规范 - **工程纪律**:本地 commit 可执行;**脚手架搭建与 `git push` 前必须经用户确认**(逐次确认)。 - 提交信息:conventional commits(`feat:` / `fix:` / `chore:` / `docs:` / `refactor:`),正文写清影响的域与 ADR 编号。 - 分支:`master` 主干开发;阶段 1 起按需建 `feat/{域}-{主题}` 分支走 MR。 - 提交前自检:`go build ./... && go vet ./...` 全绿;新增端点全局 grep 同路径;敏感值未入库(`git diff --cached` 扫一遍)。 ## 4. 构建与校验 ```bash # go.mod `go 1.27`(语言级别 + 最低工具链),本机 go 1.27.x 直接构建。 # 未写 toolchain 指令:旧机器(GOTOOLCHAIN=auto)自动从 goproxy.cn 拉取 go1.27 工具链, # 有网即可;离线/内网隔离环境需预装 1.27 或保证 GOPROXY 可达。 go build ./... # 0 错误 go vet ./... # 0 告警(1.27 起 go vet/go test 默认含 stdversion 检查) go test ./... # 测试(域模块就绪后启用) ``` **依赖版本锁定**(`go.mod`,2026-11-05 实测无意外升级): `fiber/v2 v2.52.16` / `gorm v1.31.2` / `validator/v10 v10.27.0` / `golang-jwt/jwt/v5 v5.3.1` / `oklog/ulid v1.3.1` / `x/crypto v0.33.0` / `zap v1.28.0`。 > **工具链沿革**:初版锁 go 1.23 + validator v10.27.0(go 1.20 要求),防 tidy 把工具链抬到 1.26; > 2026-11-05 升级 **go 1.27**(泛型方法/encoding-json-v2/小对象分配提速 ~30%/goroutine 泄漏检测 GA), > 依赖保持原锁定版本,全绿实证兼容。 **本地启动**(dev): ```bash cp config/dev.example.json config/dev.json # 填 DSN 等(或走 CONFIG_* 环境变量) go run ./cmd/api -env=dev # 监听 config 的 server.addr,/healthz 可探活 ``` **部署形态**(ADR-015 摘要):nginx×2(keepalived VIP,TLS+HTTP/2,SSE `proxy_buffering off`)→ 2× Go 进程(systemd,`:8080`/`:8081`)→ 共享 PolarDB/Redis/OSS;多阶段 Dockerfile 镜像化预留(构建阶段钉 `golang:1.27`),K8s 期 Secret 平移。 ## 5. 与文档目录的关系 - 本仓库**只放代码与工程配置**;架构决策(16 条 ADR)、数据模型、迁移/割接方案在工作区文档库 `unified/`;单项目事实画像在 `academic/`、`teaching/`。 - 代码注释引用决策时写 ADR 编号(如"ADR-014 SSO 最小可行"),不复述结论。