yml2213 663d7e1e9f fix(admin): 商户筛选下拉改用不分页的精简接口
自查新做的下游订单页时发现的问题:商户筛选下拉调用
/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 反映真实总数」这一事实,即上面接口存在的理由
2026-10-06 20:44:48 +08:00
2026-07-22 13:15:29 +08:00
2026-07-31 12:57:17 +08:00
2026-07-31 12:57:17 +08:00
2026-08-04 15:34:34 +08:00

游戏皮肤分销管理系统

基于 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 行以内,便于按业务域定位。

S
Description
No description provided
Readme
4.9 MiB
Languages
Go 61.3%
TypeScript 36.6%
CSS 1.5%
Shell 0.4%
Makefile 0.1%