# 游戏皮肤分销管理系统 基于 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 开发服务) ### 快速启动 ```bash # 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` 停止所有服务。 ### 手动启动 ```bash # 安装依赖 make install # 分别启动 make backend # 后端 :18080 make frontend # 前端 :15173 ``` ## Docker 生产部署 ### 配置 ```bash 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`。 ### 启动服务 ```bash # 构建并启动全部服务(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` 调整) ### 常用命令 ```bash 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` | 敏感数据加密独立密钥(可选;未设置时默认由 `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` 配置为随机值(如 `openssl rand -hex 32`),不要使用示例值。 ## 开放接口(皮肤源头对接) 项目有两套外部 API,请先区分调用方: - 商户侧 `/api/client/v1`:商户系统创建订单、查询订单和钱包,使用商户专属 `X-App-Key`。 - 源头侧 `/api/open/v1`:上游发货平台查询订单并回传发货结果,使用平台配置的 `X-Api-Key`。 源头侧 `ship_notify` 当前只使用 `success` / `failed`,速查见 [`docs/发货通知约定.md`](docs/发货通知约定.md),完整关系图见 [`docs/API对接关系.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 ``` 上游发货完成后推送结果,请求体: ```json { "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/ # 服务入口 │ │ ├── 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 行以内,便于按业务域定位。