自查新做的下游订单页时发现的问题:商户筛选下拉调用 /api/platform/merchants?size=200,而分页查询会把 >100 的 size 静默降级为 20 (service.normalizePage),因此商户超过 20 家时下拉里会少一批商户,界面不报任何错。 - 新增 service.ListMerchantOptions:不分页返回全部启用商户的精简信息 (id/code/name),独立于分页的 ListMerchants - 新增 GET /api/platform/merchant-options - 前端改用该接口;types 增加 MerchantOption,api 增加 merchantOptions() 测试: - TestListMerchantOptionsReturnsAllActiveMerchants:造 25 家启用 + 1 家禁用, 断言返回数量与库中启用数一致、必须超过分页上限、且不含已禁用商户 - TestListMerchantsClampsOversizedPageSize:钉住「size=200 实际只返回 20 条, 而 total 反映真实总数」这一事实,即上面接口存在的理由
游戏皮肤分销管理系统
基于 Go + React 的游戏皮肤分销管理平台,支持分销商管理、订单追踪、上游皮肤源头对接。
技术栈
| 层级 | 技术 |
|---|---|
| 后端 | Go 1.26 + Gin + GORM |
| 数据库 | PostgreSQL 16 |
| 前端 | React 19 + TypeScript + Ant Design |
| 构建 | Vite + Docker Compose |
| 反向代理 | Caddy |
本地开发
环境要求
- Go 1.26+
- Node.js 22+
- Docker / Docker Compose(用于自动启动 PostgreSQL 开发服务)
快速启动
# 1. 复制配置文件
cp .env.example .env
# 按需修改 .env 中的密钥
# 2. 一键启动(后端 + 前端 + PostgreSQL)
make dev
# 或直接
./start.sh
make dev 会通过 Docker Compose 启动 affiliate_dash/postgres,后端和前端仍在宿主机运行,方便热更新。首次从旧版 affiliate_dash_postgres_dev 开发容器迁移时,脚本会停止旧容器并优先复用旧数据卷。
启动后:
- 前端:
http://localhost:15173 - 后端:
http://localhost:18080
按 Ctrl+C 停止所有服务。
手动启动
# 安装依赖
make install
# 分别启动
make backend # 后端 :18080
make frontend # 前端 :15173
Docker 生产部署
配置
cp .env.example .env
# 编辑 .env,确保以下变量已配置:
# JWT_SECRET — 生产环境务必修改
# OPEN_API_KEY — 皮肤源头对接 API Key
# OPEN_API_SECRET — 签名密钥
# POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB — PostgreSQL 初始化配置
国内服务器可在
.env中取消GOPROXY、ALPINE_MIRROR、NPM_REGISTRY等镜像加速源的注释。 Docker 后端容器默认使用postgres作为数据库主机;只有连接外部数据库时才需要设置DOCKER_DATABASE_URL。
启动服务
# 构建并启动全部服务(PostgreSQL + 后端 + Caddy)
make docker-up
# 或
docker compose up -d --build
服务端口(默认值已避开常见端口,避免与其他项目冲突;均可在 .env 中覆盖):
:80— 前端页面 + API(Caddy 统一入口,可通过HTTP_PORT/HTTPS_PORT调整):18080— 后端 API(直接访问,调试用,可通过PORT调整):15432— PostgreSQL(本地开发/调试用,可通过POSTGRES_PORT调整)
常用命令
make docker-logs # 查看日志
make docker-ps # 查看容器状态
make docker-down # 停止并移除
自定义域名
Caddyfile 默认同时支持本地 :80 和示例域名 skin.khhao.com。生产部署时把 skin.khhao.com 改为你的域名,重新构建即可。
环境变量
| 变量 | 说明 | 默认值 |
|---|---|---|
PORT |
后端端口 | 18080 |
FRONTEND_PORT |
前端开发端口 | 15173 |
HTTP_PORT / HTTPS_PORT |
Caddy 对外 HTTP/HTTPS 端口 | 80 / 443 |
JWT_SECRET |
JWT 签名密钥 | 开发默认值 |
DATA_ENCRYPTION_KEY |
加密落库的 API/回调密钥;留空会回退到 JWT_SECRET 并在启动日志告警 |
回退到 JWT_SECRET |
DATABASE_URL |
PostgreSQL 连接串 | 本地开发默认值 |
DOCKER_DATABASE_URL |
Docker 后端容器内 PostgreSQL 连接串,通常无需设置 | Compose 内置默认值 |
POSTGRES_PORT |
PostgreSQL 映射到宿主机的端口 | 15432 |
COMPOSE_PROJECT_NAME |
Docker Compose 项目名 | affiliate_dash |
POSTGRES_DATA_VOLUME |
PostgreSQL 数据卷名 | affiliate_dash_postgres_data |
GIN_MODE |
Gin 运行模式 | debug |
TZ |
进程时区。必须为 Asia/Shanghai:业务时间统一按北京时间处理,且 pgx 会把 timestamptz 解码到进程时区。不一致时启动日志会打印 [CONFIG] 时区提示 |
Asia/Shanghai |
OPEN_API_KEY |
开放接口 ApiKey | 需修改 |
OPEN_API_SECRET |
开放接口签名密钥 | 需修改 |
OPEN_SIGN_SKEW |
签名时间戳偏差(秒) | 300 |
OPEN_API_DEBUG |
开放接口调试日志 | debug 模式默认开启 |
LOG_FILE |
日志文件路径(按天分割为 app-YYYYMMDD.log) |
logs/app.log |
LOG_RETAIN_DAYS |
日志文件保留天数,到期自动清理 | 30 |
CORS_ALLOWED_ORIGINS |
跨域来源白名单(逗号分隔);生产同域部署无需配置,为空时仅放行本地开发端口,不支持 * |
空 |
FULFILLMENT_PROCESSING_TIMEOUT_MINUTES |
发货中订单自动标记失败的超时分钟数,<=0 关闭 | 30 |
FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS |
发货超时巡检间隔秒数 | 60 |
安全提示:
JWT_SECRET与DATA_ENCRYPTION_KEY必须各自生成随机值(openssl rand -hex 48/openssl rand -hex 32), 不能沿用.env.example中的示例值,也不能两者相同。服务启动时会自动体检并打印[CONFIG] 安全告警: ...。轮换方法见docs/密钥轮换说明.md。
开放接口(皮肤源头对接)
项目有两套外部 API,请先区分调用方:
- 商户侧
/api/client/v1:商户系统创建订单、查询订单和钱包,使用商户专属X-App-Key。 - 源头侧
/api/open/v1:上游发货平台查询订单并回传发货结果,使用平台配置的X-Api-Key。
源头侧 ship_notify 当前只使用 success / failed,速查见 docs/发货通知约定.md,完整关系图见 docs/API对接关系.md。两套 API 的签名算法不同,不能混用鉴权头或签名串。
状态速查
| 订单状态 | 含义 | 是否可发货 |
|---|---|---|
paid |
已支付/已扣款,待发货 | 是 |
delivering |
发货中 | 否 |
delivered |
已交付 | 否 |
ship_failed |
发货失败,可重试 | 是 |
cancelled |
已取消并退款 | 否 |
上游皮肤源头系统调用源头侧接口时,需携带签名头 X-Api-Key、X-Timestamp、X-Nonce、X-Sign。
1. 查询订单(发货前置)
GET /api/open/v1/orders/:order_no
返回订单状态、商品信息、是否可发货。
2. 发货结果通知
POST /api/open/v1/orders/ship-notify
上游发货完成后推送结果,请求体:
{
"order_no": "O202607241200000012",
"ship_status": "success",
"provider_order_no": "上游单号",
"shipped_at": "2026-07-24T16:00:00+08:00",
"fail_reason": "",
"game_channel": "IOS-微信",
"game_uid": "550e8400-e29b-41d4-a716-446655440000",
"role_name": "玩家名",
"pay_score": 100
}
| 字段 | 必填 | 说明 |
|---|---|---|
order_no |
是 | 系统订单号 |
ship_status |
是 | 仅 success / failed |
provider_order_no |
否 | 上游单号 |
shipped_at |
否 | 发货时间,RFC3339 格式 |
fail_reason |
失败时是 | failed 时必填,填写详细失败原因 |
game_channel |
否 | 账号区服(安卓/IOS-微信/QQ) |
game_uid |
否 | 游戏角色 UUID |
role_name |
否 | 角色名 |
pay_score |
否 | 消耗积分 |
管理端
管理员登录后可访问商品管理、订单管理、用户管理、发货日志等功能。后台默认地址 /。
订单管理有两个入口,不要混淆
平台管理员同时拥有「平台视角」与「商户视角」两套订单入口,它们的数据范围不同:
| 入口 | 接口 | 数据范围 | 是否受商户上下文影响 |
|---|---|---|---|
| 下游订单(全部商户) | GET /api/platform/orders |
全部下游商户 | 否 |
| 发货订单(当前商户) | GET /api/merchant/orders |
仅当前商户 | 是,取决于 X-Merchant-ID |
首页 Dashboard 对管理员是平台口径(跨商户聚合)。如果只看「发货订单」,需要先知道 自己当前处在哪个商户上下文里,否则会出现「首页 37 单、订单页 29 单」这种看起来像 bug 的差异—— 实际上订单页只显示了自营商户这一家。
商户上下文的来源是前端 localStorage.merchant_id,只会在「平台商户」页点「成员」时被写入。
顶栏会常驻显示「当前商户:xxx」,点旁边的「退出商户视角」即可回到默认商户(自营商户)。
需要看某个下游商户的订单时,用「下游订单」页的商户筛选即可,不必切换上下文。
目录结构
├── backend/
│ ├── cmd/
│ │ ├── server/ # 服务入口
│ │ ├── reencrypt_secrets/ # 密钥轮换:重加密落库密文
│ │ ├── cache_images/ # 商品图本地缓存工具
│ │ └── sync_product_images/ # 商品图同步工具
│ └── internal/
│ ├── config/ # 配置加载 + 启动安全体检
│ ├── database/ # 显式 SQL 迁移(embed + checksum)
│ │ └── migrations/ # 001~012 迁移脚本,已执行版本不可修改
│ ├── handler/ # HTTP 处理器:merchant_* / open_* / recharge / rock …
│ ├── middleware/ # JWT / 商户签名 / 源头签名 / 功能与权限门禁
│ ├── model/ # 数据模型:merchant / recharge / rock / models
│ ├── pkg/ # 工具包:applog / openlog / jwt / response / timeutil / lewan
│ ├── router/ # 路由注册与请求日志
│ ├── service/ # 业务逻辑
│ │ ├── fulfillment* # 订单状态机、下单幂等、超时巡检
│ │ ├── delivery* # 提货页 BFF:查询 / 绑定 / 提交 / 链接
│ │ ├── provider* # 履约渠道接口与路由(DLC / 乐玩 / Rock)
│ │ ├── rock_* # Rock 库存池、UID 强一致门禁、绑错追溯
│ │ ├── merchant* # 商户、商品目录、成员与角色
│ │ └── recharge / callback / user / tenant
│ └── testdb/ # 集成测试用 PostgreSQL 临时 schema
├── frontend/src/
│ ├── api/ # API 调用
│ ├── pages/ # 页面组件(platformRock/ 为 Rock 大屏拆出的子组件)
│ ├── layouts/ # 布局
│ ├── store/ # 登录态与租户上下文
│ └── types/ # 类型定义
├── docs/ # 对接文档、实施方案与项目分析
├── docker-compose.yml # 容器编排
├── Makefile # 常用命令(dev / test / lint / fmt / docker-*)
└── start.sh # 开发一键启动
service包按delivery_*/rock_*/merchant_*/provider_*前缀成组, 单个文件保持在 300 行以内,便于按业务域定位。