- Go 93.5%
- PLpgSQL 4.5%
- Dockerfile 1.3%
- Makefile 0.7%
- providers/invoice:fpapi.com 对接(三层 HMAC 签名)+ mock,异步任务与回调验签 - db/migrations/00003:抬头、发票、明细、回调去重四张表; 用 invoice_items 上的部分唯一索引保证「一笔订单同时只有一张有效蓝票」, 红冲把蓝票置为 offset 之后订单自然可以重开 - player-api:抬头 CRUD、可开票订单、开票、换开(红冲+重开)、回调 - player-api:订阅 order.refunded 自动红冲,全额退全额冲、部分退按行冲 - admin-api:退款成功后发事件;发票只读查询与红冲链路 - platform/events:NATS JetStream 事件总线,说明为什么这里用事件不用 RPC - platform/session、platform/httpx/params:抽掉两端重复的会话与参数处理 - 房间凭据下发 EasyTier 入网节点(EASYTIER_PEERS) - Dockerfile 基础镜像走 build-arg,CI 里换成 Harbor 代理路径 - .forgejo/workflows/ci.yaml:检查 → 构建镜像(sha tag)→ 回写 deploy 仓库 |
||
|---|---|---|
| .forgejo/workflows | ||
| db | ||
| platform | ||
| providers | ||
| services | ||
| tools | ||
| .env.example | ||
| .gitignore | ||
| go.work | ||
| go.work.sum | ||
| Makefile | ||
| README.md | ||
带你玩 · 后端
Go workspace 组织的 monorepo。现在只有一个服务(管理后台 API),
但目录结构和模块划分是按「以后要拆微服务」来的:
共享代码在 platform,每个服务是独立的 Go module,各自能单独构建和发布。
backend/
├── go.work # workspace,登记所有模块
├── Makefile # 日常命令入口
├── .env.example
├── platform/ # 共享模块,所有服务复用
│ ├── config/ # 环境变量加载
│ ├── logging/ # slog 配置
│ ├── httpx/ # 统一的 JSON 响应、错误、分页
│ ├── pgstore/ # pgxpool 连接
│ ├── cache/ # Valkey 客户端
│ ├── auth/ # bcrypt 口令 + JWT 签发校验
│ └── middleware/ # request id、访问日志、recover、CORS、鉴权
├── services/
│ └── admin-api/ # 管理后台 API
│ ├── cmd/server/ # HTTP 服务
│ ├── cmd/migrate/ # 数据库迁移(迁移文件已嵌入二进制)
│ ├── cmd/seed/ # 演示数据
│ ├── db/migrations/ # goose 迁移
│ ├── db/queries/ # sqlc 的 SQL 源文件
│ ├── internal/api/ # 路由与 handler
│ ├── internal/store/ # sqlc 生成,不要手改
│ └── internal/payment/ # 支付网关抽象(当前是 Mock)
└── tools/ # 只放代码生成工具(sqlc),不参与服务构建
技术选型:chi 路由 + pgx 连接 Postgres + sqlc 生成类型安全的查询 + goose 管迁移。
没有 ORM,SQL 都在 db/queries/ 里,能直接拿去 EXPLAIN。
新增一个服务
mkdir -p services/room-api/cmd/server
cd services/room-api && go mod init git.joelclub.com/dainiwan/backend/services/room-api
然后在 go.work 的 use 里加一行,并在新服务的 go.mod 里写上
replace git.joelclub.com/dainiwan/backend/platform => ../../platform
(workspace 内不加也能构建,但单独打 Docker 镜像时需要)。
make build / make vet 会自动带上新模块,不用改 Makefile。
本地起服务
依赖两个容器,已经在本机跑着:
| 组件 | 容器名 | 地址 | 账号 |
|---|---|---|---|
| PostgreSQL 18 | pg18 |
127.0.0.1:5432 | pg18 / pg18,库名 dainiwan |
| Valkey 8 | valkey-dainiwan |
127.0.0.1:6379 | 无密码 |
Valkey 容器是这样起的(数据挂在 dainiwan_valkey_data 卷上):
docker run -d --name valkey-dainiwan -p 6379:6379 -v dainiwan_valkey_data:/data valkey/valkey:8-alpine valkey-server --save 60 1 --appendonly yes
cp .env.example .env
make docker-up # 确保 pg18 与 valkey 在跑
make migrate-up # 建表
make seed # 灌演示数据
make run # http://localhost:8080
演示账号:
| 角色 | 账号 | 密码 | 说明 |
|---|---|---|---|
| 平台管理员 | admin |
admin12345 |
看得到全平台数据 |
| 俱乐部代理人 | joel |
joel12345 |
只能操作「乔尔俱乐部」 |
常用命令
make sqlc # 改完 db/queries/*.sql 后重新生成 internal/store
make migrate-up # 执行迁移
make migrate-status # 查看迁移状态
make seed-force # 清空业务表后重灌演示数据
make build / vet / test / fmt / tidy
改数据库的正确顺序:写一个新的 db/migrations/0000N_xxx.sql →
make migrate-up → 按需改 db/queries/*.sql → make sqlc → 改 handler。
不要手改 internal/store/,那是生成出来的。
接口
全部挂在 /api/v1 下,除登录外都要带 Authorization: Bearer <token>。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /auth/login |
登录,返回 JWT |
| GET | /auth/me |
当前账号信息 |
| POST | /auth/logout |
让当前会话立即失效 |
| GET | /stats/overview |
概览(平台管理员看全平台,代理人看自己俱乐部) |
| GET | /stats/user-signups |
注册曲线 |
| GET | /users、/users/{id} |
玩家列表与详情 |
| POST | /users/{id}/ban、/unban |
封禁、解封 |
| GET/POST | /clubs、/clubs/{id} |
俱乐部列表、创建、详情 |
| PATCH | /clubs/{id} |
改名称、公告、头图、价格 |
| POST | /clubs/{id}/status |
停运 / 恢复 |
| GET | /clubs/{id}/members |
成员列表 |
| POST | /clubs/{id}/members/{userID}/status |
封禁 / 解封成员 |
| GET | /club-applications |
开通申请列表 |
| POST | /club-applications/{id}/review |
通过 / 驳回 |
| GET/POST | /events、/events/{id} |
赛事列表、创建、详情 |
| PATCH/DELETE | /events/{id} |
修改、删除草稿 |
| GET | /events/{id}/registrations |
报名列表 |
| GET | /events/{id}/registration-stats |
报名统计 |
| POST | /registrations/{id}/status |
调整报名状态 |
| GET | /orders、/orders/{id} |
订单列表与详情 |
| POST | /orders/{id}/refund |
发起退款 |
| GET | /refunds |
退款单列表 |
| GET | /reconciliation |
按天对账 |
| GET/PUT | /settings/pricing |
平台默认会费与开车券价 |
| GET | /audit-logs |
操作审计 |
另有 /healthz(存活)和 /readyz(探测 Postgres 与 Valkey),给 k8s 用。
权限模型
后台账号有两种角色,登录后写进 JWT:
platform_admin:全平台可见可改。club_agent:绑定一个club_id,列表接口自动加上俱乐部过滤, 详情接口校验归属,越权返回 403。「重要赛事」标记和价格设置只有平台管理员能改。
访问令牌是无状态 JWT,但每个令牌带一个 session id 存在 Valkey 里, 删掉这个键令牌立刻失效——登出和踢下线靠这个。 登录失败次数也记在 Valkey,15 分钟内超过 10 次先挡住。
支付网关
internal/payment 定义了 Gateway 接口,Stage 1 用的是 MockGateway:
调用即成功,生成一个假的网关单号。整条「订单 → 退款 → 对账」链路是通的,
但没有真的动钱。
接支付宝/微信商户后,写一个实现 Gateway 的类型,在 cmd/server/main.go
里替换掉 payment.MockGateway{} 即可,handler 不用动。
退款流程是三步,刻意没把网关调用放进数据库事务:
- 事务内锁订单(
SELECT … FOR UPDATE)、校验可退金额、写一条pending退款单 - 调网关
- 事务内回写结果,成功才累加订单的
refunded_cents
待办
- Dockerfile 与 Forgejo Runner 流水线
- 玩家侧 API(注册、房间、组网),届时拆成独立服务
- 真实支付网关接入
- GlitchTip 错误上报