修改admin
This commit is contained in:
parent
d8ff290392
commit
e5f7e38f48
230
docs/幸运礼物外接接口对接文档.md
Normal file
230
docs/幸运礼物外接接口对接文档.md
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
# 幸运礼物外接接口对接文档
|
||||||
|
|
||||||
|
> 文档版本:2026-07-15
|
||||||
|
> 接入方式:服务端对服务端(Server-to-Server)
|
||||||
|
> 金额单位:对接 App 的金币最小整数单位
|
||||||
|
|
||||||
|
## 1. 接口用途
|
||||||
|
|
||||||
|
外部 App 在自己的账务系统完成幸运礼物扣费后,调用本接口获取一次最终抽奖结果。
|
||||||
|
|
||||||
|
平台负责:
|
||||||
|
|
||||||
|
- 按 `app_code` 隔离规则、奖池、RTP 和抽奖流水。
|
||||||
|
- 按 `app_code + request_id` 保证抽奖幂等。
|
||||||
|
- 返回最终 `reward_amount` 和倍率。
|
||||||
|
|
||||||
|
对接 App 负责:
|
||||||
|
|
||||||
|
- 调用前完成用户扣费。
|
||||||
|
- 成功收到结果后,在自己的账务系统中向用户发放 `reward_amount`。
|
||||||
|
- 对超时或网络失败使用原 `request_id` 重试,不得重新生成业务请求 ID。
|
||||||
|
|
||||||
|
HyApp 不会操作外部 App 的用户钱包。`reward_status=granted` 表示抽奖结果已经生成并交给调用方,不表示奖励已在外部 App 入账。
|
||||||
|
|
||||||
|
## 2. 接口地址
|
||||||
|
|
||||||
|
| 类型 | 地址 |
|
||||||
|
|---|---|
|
||||||
|
| Base URL | `https://lucky-api.global-interaction.com` |
|
||||||
|
| 送礼接口 | `POST https://lucky-api.global-interaction.com/api/v1/lucky-gifts/send` |
|
||||||
|
| 就绪检查 | `GET https://lucky-api.global-interaction.com/healthz/ready` |
|
||||||
|
|
||||||
|
就绪检查:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET https://lucky-api.global-interaction.com/healthz/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
2026-07-15 实测就绪检查返回 HTTP 200,送礼请求可到达幸运礼物网关。
|
||||||
|
|
||||||
|
## 3. 调用前置条件
|
||||||
|
|
||||||
|
1. 向平台申请唯一 `app_code`,并加入幸运礼物网关白名单。
|
||||||
|
2. 确认该 App 的幸运礼物规则已启用。
|
||||||
|
3. 对接 App 必须先扣费,再调用抽奖接口。
|
||||||
|
4. 本接口只允许对接 App 服务端调用,禁止将请求能力下发到 App 或 Web 客户端。
|
||||||
|
|
||||||
|
当前版本的 HTTP 入口使用 `app_code` 白名单,尚未启用 Authorization Token 或请求签名,因此必须仅由已开通的对接 App 服务端调用。
|
||||||
|
|
||||||
|
## 4. 发送幸运礼物
|
||||||
|
|
||||||
|
### 4.1 请求
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/lucky-gifts/send HTTP/1.1
|
||||||
|
Host: lucky-api.global-interaction.com
|
||||||
|
Content-Type: application/json
|
||||||
|
X-Request-Id: trace-20260715-000001
|
||||||
|
```
|
||||||
|
|
||||||
|
`X-Request-Id` 为可选的链路追踪 ID。如果不传,平台会自动生成。它只用于日志排查,不是抽奖幂等键。
|
||||||
|
|
||||||
|
### 4.2 请求体
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app_code": "partner_app",
|
||||||
|
"request_id": "gift-order-20260715-000001",
|
||||||
|
"external_user_id": "user-10001",
|
||||||
|
"gift_count": 3,
|
||||||
|
"unit_amount": 100,
|
||||||
|
"currency": "COIN",
|
||||||
|
"metadata": {
|
||||||
|
"external_order_id": "order-1000001",
|
||||||
|
"gift_id": "lucky-rose",
|
||||||
|
"room_id": "room-20001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 约束 | 说明 |
|
||||||
|
|---|---|---:|---|---|
|
||||||
|
| `app_code` | string | 是 | 去除首尾空格后 1–32 字符 | 平台分配的 App 编码;按小写存储和校验。 |
|
||||||
|
| `request_id` | string | 是 | 去除首尾空格后 1–128 字符 | 对接 App 生成的业务幂等 ID;建议直接使用送礼订单号或 UUID。 |
|
||||||
|
| `external_user_id` | string | 是 | 去除首尾空格后 1–128 字符 | 用户在对接 App 中的稳定唯一 ID。 |
|
||||||
|
| `gift_count` | int64 | 是 | `> 0` | 本次已扣费的礼物数量。 |
|
||||||
|
| `unit_amount` | int64 | 是 | `> 0` | 单个礼物的金币整数价格。 |
|
||||||
|
| `currency` | string | 否 | 只支持 `COIN` | 缺省或空字符串时按 `COIN` 处理。 |
|
||||||
|
| `metadata` | object | 否 | JSON object | 对接方的展示或排查元数据;不参与金额、规则或幂等判定。 |
|
||||||
|
|
||||||
|
服务端按下式计算本次金额:
|
||||||
|
|
||||||
|
```text
|
||||||
|
total_amount = gift_count * unit_amount
|
||||||
|
```
|
||||||
|
|
||||||
|
请求体不得传入 `total_amount`、`pool_id` 或 `paid_at_ms`。奖池由平台规则选择,扣费完成时间由网关按 UTC 服务端时间写入。接口会拒绝所有未定义的顶层字段。
|
||||||
|
|
||||||
|
一个请求只返回一个聚合抽奖结果;`gift_count` 不会产生多个独立响应。
|
||||||
|
|
||||||
|
### 4.4 成功响应
|
||||||
|
|
||||||
|
HTTP Status:`200 OK`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "trace-20260715-000001",
|
||||||
|
"data": {
|
||||||
|
"draw_id": "external_lucky_draw_xxxxxxxxxx",
|
||||||
|
"request_id": "gift-order-20260715-000001",
|
||||||
|
"app_code": "partner_app",
|
||||||
|
"external_user_id": "user-10001",
|
||||||
|
"gift_count": 3,
|
||||||
|
"unit_amount": 100,
|
||||||
|
"total_amount": 300,
|
||||||
|
"reward_amount": 600,
|
||||||
|
"multiplier_ppm": 2000000,
|
||||||
|
"reward_status": "granted",
|
||||||
|
"rule_version": 1,
|
||||||
|
"created_at_ms": 1784073600000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 响应字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `code` | int | 业务结果码;`0` 表示请求成功。 |
|
||||||
|
| `message` | string | 结果说明。 |
|
||||||
|
| 外层 `request_id` | string | HTTP 链路追踪 ID,来自 `X-Request-Id` 或由平台生成。 |
|
||||||
|
| `data.draw_id` | string | 平台生成的唯一抽奖 ID,对账时使用。 |
|
||||||
|
| `data.request_id` | string | 请求体中的业务幂等 ID。 |
|
||||||
|
| `data.app_code` | string | 归一化后的 App 编码。 |
|
||||||
|
| `data.external_user_id` | string | 对接 App 的用户 ID。 |
|
||||||
|
| `data.gift_count` | int64 | 礼物数量。 |
|
||||||
|
| `data.unit_amount` | int64 | 单件礼物金币价格。 |
|
||||||
|
| `data.total_amount` | int64 | 服务端计算的已扣费总额。 |
|
||||||
|
| `data.reward_amount` | int64 | 对接 App 应向用户发放的金币整数金额;以该字段为最终结算依据。 |
|
||||||
|
| `data.multiplier_ppm` | int64 | 倍率 ppm;`1000000` 表示 1x,`2000000` 表示 2x。 |
|
||||||
|
| `data.reward_status` | string | 当前为 `granted`,表示抽奖已完成且结果已交给对接 App 处理。 |
|
||||||
|
| `data.rule_version` | int64 | 本次抽奖命中的规则版本。 |
|
||||||
|
| `data.created_at_ms` | int64 | 抽奖结果创建时间,Unix epoch milliseconds,UTC。 |
|
||||||
|
|
||||||
|
未中奖时 `reward_amount` 和 `multiplier_ppm` 为 `0`。当前 JSON 序列化会省略数值为 `0` 的结果字段,对接方必须将缺失的 `reward_amount` 或 `multiplier_ppm` 按 `0` 处理,不得将字段缺失视为请求失败。
|
||||||
|
|
||||||
|
## 5. 幂等与重试
|
||||||
|
|
||||||
|
幂等键固定为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app_code + request_id
|
||||||
|
```
|
||||||
|
|
||||||
|
- 首次请求成功后,平台保存抽奖结果。
|
||||||
|
- 使用相同 `app_code + request_id` 重试时,直接返回首次结果,不会重新抽奖。
|
||||||
|
- 同一 `request_id` 不得对应不同用户、数量或金额。平台在幂等命中时会返回首次结果,不会用新参数覆盖。
|
||||||
|
- 超时、连接中断或 HTTP 5xx 时,使用原请求体和原 `request_id` 重试。
|
||||||
|
- 客户端超时建议设置为大于网关内部 3 秒处理超时,并对重试使用有上限的退避。
|
||||||
|
|
||||||
|
如果多次重试后仍无法确认结果,请保留 `app_code`、`request_id`、外层 `request_id` 和扣费流水,由平台根据抽奖记录对账。
|
||||||
|
|
||||||
|
## 6. 错误响应
|
||||||
|
|
||||||
|
### 6.1 统一格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 40000,
|
||||||
|
"message": "request_id is required",
|
||||||
|
"request_id": "trace-20260715-000001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 错误码
|
||||||
|
|
||||||
|
| HTTP Status | `code` | 场景 | 对接方动作 |
|
||||||
|
|---:|---:|---|---|
|
||||||
|
| 400 | 40000 | JSON 无效、出现未定义字段、必填字段缺失、金额非正数或币种不支持 | 修正请求,不要原样重试。 |
|
||||||
|
| 403 | 40300 | `app_code` 未加入当前环境白名单 | 联系平台开通或检查环境。 |
|
||||||
|
| 404 | 无统一业务码 | 请求路径或 HTTP 方法错误 | 检查 Base URL、路径和请求方法。 |
|
||||||
|
| 500 | 50000 | 抽奖服务超时或内部处理失败 | 保持原 `request_id` 重试;不得重新扣费。 |
|
||||||
|
|
||||||
|
业务成功必须同时满足 HTTP Status 为 `200` 且响应 `code` 为 `0`。
|
||||||
|
|
||||||
|
## 7. cURL 示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request POST \
|
||||||
|
'https://lucky-api.global-interaction.com/api/v1/lucky-gifts/send' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'X-Request-Id: trace-20260715-000001' \
|
||||||
|
--data '{
|
||||||
|
"app_code": "partner_app",
|
||||||
|
"request_id": "gift-order-20260715-000001",
|
||||||
|
"external_user_id": "user-10001",
|
||||||
|
"gift_count": 3,
|
||||||
|
"unit_amount": 100,
|
||||||
|
"currency": "COIN",
|
||||||
|
"metadata": {
|
||||||
|
"external_order_id": "order-1000001",
|
||||||
|
"gift_id": "lucky-rose",
|
||||||
|
"room_id": "room-20001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 标准处理流程
|
||||||
|
|
||||||
|
1. 对接 App 生成全局唯一的 `request_id`。
|
||||||
|
2. 对接 App 在自有账务系统扣除 `gift_count * unit_amount`。
|
||||||
|
3. 对接 App 调用幸运礼物接口。
|
||||||
|
4. 平台按 `app_code + request_id` 创建或返回已有抽奖结果。
|
||||||
|
5. 对接 App 在响应成功后按 `reward_amount` 入账,并保存 `draw_id`。
|
||||||
|
6. 任何未知结果都使用原 `request_id` 重试;禁止重复扣费或重新生成抽奖业务号。
|
||||||
|
|
||||||
|
## 9. 联调检查清单
|
||||||
|
|
||||||
|
- [ ] `app_code` 已加入幸运礼物网关白名单。
|
||||||
|
- [ ] 规则已启用,并确认金币单位一致。
|
||||||
|
- [ ] 正常送礼可获得 HTTP 200 / `code=0`。
|
||||||
|
- [ ] 重复发送相同 `request_id` 可获得相同 `draw_id` 和结果。
|
||||||
|
- [ ] 模拟超时后只重试抽奖请求,不重复扣费。
|
||||||
|
- [ ] 未中奖响应中缺失的数值字段可正确按 `0` 处理。
|
||||||
|
- [ ] 奖励按 `reward_amount` 在外部 App 入账,并持久化 `draw_id`。
|
||||||
|
- [ ] 上线前完成小额真实扣费、抽奖、返奖和幂等重试验收。
|
||||||
1052
docs/新版VIP权限策略_Flutter客户端对接文档.md
Normal file
1052
docs/新版VIP权限策略_Flutter客户端对接文档.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -188,7 +188,9 @@ func New(cfg config.Config, auth *service.AuthService, store *repository.Store,
|
|||||||
prettyid.RegisterRoutes(appProtected, h.PrettyID)
|
prettyid.RegisterRoutes(appProtected, h.PrettyID)
|
||||||
job.RegisterRoutes(protected, h.Job)
|
job.RegisterRoutes(protected, h.Job)
|
||||||
levelconfig.RegisterRoutes(appProtected, h.LevelConfig)
|
levelconfig.RegisterRoutes(appProtected, h.LevelConfig)
|
||||||
opscenter.RegisterRoutes(appProtected, h.OpsCenter)
|
// Ops Center 是跨 App 工作台:它必须先读取应用目录,再按 app_code 扇出查询。
|
||||||
|
// 这里仅沿用登录、审计和模块内 lucky-gift 权限,不能要求尚未选出的 X-App-Code。
|
||||||
|
opscenter.RegisterRoutes(protected, h.OpsCenter)
|
||||||
upload.RegisterRoutes(appProtected, h.Upload)
|
upload.RegisterRoutes(appProtected, h.Upload)
|
||||||
search.RegisterRoutes(protected, h.Search)
|
search.RegisterRoutes(protected, h.Search)
|
||||||
sevendaycheckin.RegisterRoutes(appProtected, h.SevenDayCheckIn)
|
sevendaycheckin.RegisterRoutes(appProtected, h.SevenDayCheckIn)
|
||||||
|
|||||||
46
server/admin/internal/router/router_test.go
Normal file
46
server/admin/internal/router/router_test.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/config"
|
||||||
|
"hyapp-admin-server/internal/modules/opscenter"
|
||||||
|
"hyapp-admin-server/internal/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOpsCenterAppBootstrapDoesNotRequireAppCode(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
auth := service.NewAuthService("ops-center-router-test-secret", time.Hour)
|
||||||
|
token, _, err := auth.GenerateAccessToken(7, "operator", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate access token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := New(config.Config{}, auth, nil, Handlers{OpsCenter: opscenter.New(nil, nil)})
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/admin/ops-center/apps", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
// Ops Center 先读取跨 App 目录,再按 app_code 查询每个应用的数据;启动接口若要求
|
||||||
|
// X-App-Code,就会形成“先拿到 App 才能发 header、先发 header 才能拿 App”的死锁。
|
||||||
|
if response.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
|
||||||
|
}
|
||||||
|
if body := response.Body.String(); !strings.Contains(body, `"app_code":"yumi"`) || !strings.Contains(body, `"app_code":"aslan"`) {
|
||||||
|
t.Fatalf("bootstrap app catalog missing fixed apps: %s", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 appProtected 移出只解除 App 选择依赖;路由仍必须位于 protected 下,不能变成匿名应用目录。
|
||||||
|
unauthorized := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(unauthorized, httptest.NewRequest(http.MethodGet, "/api/v1/admin/ops-center/apps", nil))
|
||||||
|
if unauthorized.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("unauthorized status = %d body=%s", unauthorized.Code, unauthorized.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user