|
|
1 jam lalu | |
|---|---|---|
| cmd | 2 jam lalu | |
| config | 5 hari lalu | |
| internal | 1 jam lalu | |
| .gitignore | 5 hari lalu | |
| EXAMPLES.md | 4 hari lalu | |
| Makefile | 4 hari lalu | |
| README.md | 4 hari lalu | |
| go.mod | 1 hari lalu | |
| go.sum | 1 hari lalu |
两个 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)。
与 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 存活探针 |
登录成功率/接口错误率/队列积压(兼容期监控项) |
.
├── 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。
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 | 横切基础设施 | 依赖任何业务域(单向) |
分包铁律:
internal/ 内定义的接口(接口在 org/identity 侧,消费方在业务域)。infra 不依赖任何业务域;org/identity 不得反向依赖业务域。academic 域内用 Go 事务重写,旧存储过程仅留档对照,不迁移。user->id==393)一律进配置或权限判断,消灭目标为 0。R.code(成功 200;信封 {code, data, message, success});e2e 断言 body code 而非 HTTP status。
data 恒为 PageData 对象 {list, total, page, pageSize, pages},元信息随数据同聚(彻底消灭旧系统 Success(total 顶层) vs SuccessPage(total 在 data 内) 的结构不一致);单实体接口 data 为实体对象或省略;前端统一读 res.data.list / res.data.total。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 混用)。text/event-stream,不得返 JSON 让前端 SSE 解析失败。IS_NULLABLE=YES)一律 *string/*int,写边界 nil→""/0 归一(防 converting NULL → 500)。gorm:"column:xxx"。Model(),base 不带 Select;Count 收 *int64;存在性判断用 Take。Updates 的 RowsAffected==0 可能是"值未变"非"失败":对齐旧系统"行存在即成功",Take 预检再更新。tx 句柄;动态表名(科目分表)收敛一处——Table(subjectTable(subjectID)),体育(10) 已对齐补齐。whereIn),禁 ORM 跨库 join;only_full_group_by 用 Distinct().Order();保留字反引号。每个导出的结构体/函数/方法/常量/错误必须带 GoDoc 中文注释:
代码即文档:改实现必须同步改注释;注释描述与实现不符 = bug。模板与索引见 EXAMPLES.md。
三层错误模型:infra/errors.AppError{Code, Msg, Cause}。
gorm.ErrRecordNotFound 等转 sentinel(errors.Is 判定),不泄漏 ORM 原错;errors.WithCause 挂底层原因供日志,不进响应体);response.Fail(c, err) 统一出口;非 *AppError 降级 5001 + 结构化日志(带 stack + trace_id);X-Request-ID 生成/透传,跨 Python Agent 调用同值透传;级别生产 Info;脱敏(手机号打码、密码/密钥/JWT 不落日志);高频只读访问日志可采样,写操作全量。config/{env}.json ← CONFIG_* 环境变量 ← 启动参数;必填字段缺失 fail-fast 拒绝启动;敏感字段(DB DSN/JWT 私钥/OSS/微信/Agent 密钥)绝不进代码库/日志,走环境变量或 KMS;启动时幂等建 log/{instance}/、tmp/{instance}/(双实例同机隔离)。业务域写 t_task(status=pending,幂等键 = type + 业务唯一键)→ 工作池(并发度 task.workers)抢占;认领用条件 UPDATE 乐观锁(双实例防重复消费);失败指数退避(2^retry 秒,上限 1h)超 maxRetry 置 dead 告警;派发成功 ≠ 任务完成(AI 批改/导出/PDF),前端轮询任务终态;Agent 不可达 fire-and-forget 不阻断主链路;割接时在途任务先排空或标记重投。
| 门禁 | 命令 / 做法 | 基线 |
|---|---|---|
| 编译 | 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 失败 |
git push 前必须经用户确认(逐次确认)。feat: / fix: / chore: / docs: / refactor:),正文写清影响的域与 ADR 编号。master 主干开发;阶段 1 起按需建 feat/{域}-{主题} 分支走 MR。go build ./... && go vet ./... 全绿;新增端点全局 grep 同路径;敏感值未入库(git diff --cached 扫一遍)。# 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 ./... # 测试(域模块就绪后启用)
API 生成物位置约定(2026-11-06 拍板,经 Makefile 固化):
| 项 | 约定 |
|---|---|
| 产物目录 | bin/api/(固定,不随工作目录漂移) |
| 产物名 | Windows 下 go-backend.exe;非 Windows 为 go-backend(按 go env GOOS 判定) |
| git 忽略 | bin/ 整目录已在 .gitignore(不入库) |
| 构建命令 | make build → bin/api/go-backend(.exe);make clean 清产物;make test / make vet |
make build # → bin/api/go-backend.exe(Windows)
make clean # 删 bin/
Makefile用$(shell go env GOOS)判定产物后缀,跨平台不写死.exe; 本机实测(go 1.27.3 / MinGW64 make 4.4.1)-o目标不带.exe时不会自动补后缀,故显式写全名最稳。
依赖版本锁定(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):
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 平移。
unified/;单项目事实画像在 academic/、teaching/。