合并finance和marking-v3

Q 4b28c84e89 feat(ac15): 固化 SP 正文查看工具 cmd/ac15sp(只读) há 9 horas atrás
cmd 4b28c84e89 feat(ac15): 固化 SP 正文查看工具 cmd/ac15sp(只读) há 9 horas atrás
config ef2e0667e9 chore: 初始化统一 Go 后端(模块化单体骨架 + 9 包参考样例) há 5 dias atrás
internal ce1a5de666 docs(ac15): 批B 变异实验 M1/M2/M3 实测记录(spend扣balance / 206同向 / baddebt单增) há 9 horas atrás
.gitignore ef2e0667e9 chore: 初始化统一 Go 后端(模块化单体骨架 + 9 包参考样例) há 5 dias atrás
EXAMPLES.md da3fe5b770 refactor(response): 统一分页契约为嵌套 PageData{list,total,page,pageSize,pages} há 5 dias atrás
Makefile 6273b0dd2d build: 固化 API 产物位置约定(make build → bin/api/go-backend(.exe)) há 4 dias atrás
README.md 6273b0dd2d build: 固化 API 产物位置约定(make build → bin/api/go-backend(.exe)) há 4 dias atrás
go.mod 027c26377f feat(rbac): S1 引入 Casbin v3 策略引擎 + 自写 GORM Adapter(第②层接口强制) há 1 dia atrás
go.sum 027c26377f feat(rbac): S1 引入 Casbin v3 策略引擎 + 自写 GORM Adapter(第②层接口强制) há 1 dia atrás

README.md

统一 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。

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, 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 混用)。
  • 流式(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。

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. 构建与校验

# 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 平移。

5. 与文档目录的关系

  • 本仓库只放代码与工程配置;架构决策(16 条 ADR)、数据模型、迁移/割接方案在工作区文档库 unified/;单项目事实画像在 academic/、teaching/。
  • 代码注释引用决策时写 ADR 编号(如"ADR-014 SSO 最小可行"),不复述结论。