带你玩 backend
  • Go 93.5%
  • PLpgSQL 4.5%
  • Dockerfile 1.3%
  • Makefile 0.7%
Find a file
AkagiYui 011590ecea
Some checks failed
ci / check (push) Failing after 16s
ci / images (admin-api) (push) Has been skipped
ci / images (player-api) (push) Has been skipped
ci / deploy (push) Has been skipped
发票:自助开票、换开与退款红冲
- 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 仓库
2026-09-03 05:34:24 +08:00
.forgejo/workflows 发票:自助开票、换开与退款红冲 2026-09-03 05:34:24 +08:00
db 发票:自助开票、换开与退款红冲 2026-09-03 05:34:24 +08:00
platform 发票:自助开票、换开与退款红冲 2026-09-03 05:34:24 +08:00
providers 发票:自助开票、换开与退款红冲 2026-09-03 05:34:24 +08:00
services 发票:自助开票、换开与退款红冲 2026-09-03 05:34:24 +08:00
tools 初始提交:backend 2026-09-03 03:53:42 +08:00
.env.example 初始提交:backend 2026-09-03 03:53:42 +08:00
.gitignore 初始提交:backend 2026-09-03 03:53:42 +08:00
go.work 初始提交:backend 2026-09-03 03:53:42 +08:00
go.work.sum 发票:自助开票、换开与退款红冲 2026-09-03 05:34:24 +08:00
Makefile 初始提交:backend 2026-09-03 03:53:42 +08:00
README.md 初始提交:backend 2026-09-03 03:53:42 +08:00

带你玩 · 后端

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 管迁移。 没有 ORMSQL 都在 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.workuse 里加一行,并在新服务的 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.sqlmake migrate-up → 按需改 db/queries/*.sqlmake 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 里, 删掉这个键令牌立刻失效——登出和踢下线靠这个。 登录失败次数也记在 Valkey15 分钟内超过 10 次先挡住。

支付网关

internal/payment 定义了 Gateway 接口Stage 1 用的是 MockGateway 调用即成功,生成一个假的网关单号。整条「订单 → 退款 → 对账」链路是通的, 但没有真的动钱

接支付宝/微信商户后,写一个实现 Gateway 的类型,在 cmd/server/main.go 里替换掉 payment.MockGateway{} 即可handler 不用动。

退款流程是三步,刻意没把网关调用放进数据库事务:

  1. 事务内锁订单(SELECT … FOR UPDATE)、校验可退金额、写一条 pending 退款单
  2. 调网关
  3. 事务内回写结果,成功才累加订单的 refunded_cents

待办

  • Dockerfile 与 Forgejo Runner 流水线
  • 玩家侧 API注册、房间、组网届时拆成独立服务
  • 真实支付网关接入
  • GlitchTip 错误上报