diff --git a/app/Http/Requests/Admin/Concerns/AgentProfileFieldRules.php b/app/Http/Requests/Admin/Concerns/AgentProfileFieldRules.php index 146ae5c..cfcb0d6 100644 --- a/app/Http/Requests/Admin/Concerns/AgentProfileFieldRules.php +++ b/app/Http/Requests/Admin/Concerns/AgentProfileFieldRules.php @@ -13,8 +13,8 @@ trait AgentProfileFieldRules 'total_share_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'], 'relative_share_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'], 'credit_limit' => ['sometimes', 'integer', 'min:0'], - 'rebate_limit' => ['sometimes', 'numeric', 'min:0', 'max:1'], - 'default_player_rebate' => ['sometimes', 'numeric', 'min:0', 'max:1'], + 'rebate_limit' => ['sometimes', 'numeric', 'min:0', 'max:100'], + 'default_player_rebate' => ['sometimes', 'numeric', 'min:0', 'max:100'], 'settlement_cycle' => ['sometimes', 'string', 'in:'.implode(',', AgentSettlementCycle::VALUES)], 'can_grant_extra_rebate' => ['sometimes', 'boolean'], 'can_create_child_agent' => ['sometimes', 'boolean'], diff --git a/app/Services/Agent/AgentProfileService.php b/app/Services/Agent/AgentProfileService.php index 0416b9d..89a0c94 100644 --- a/app/Services/Agent/AgentProfileService.php +++ b/app/Services/Agent/AgentProfileService.php @@ -29,8 +29,8 @@ final class AgentProfileService $totalShare = (float) ($payload['total_share_rate'] ?? 0); $creditLimit = (int) ($payload['credit_limit'] ?? 0); - $rebateLimit = (float) ($payload['rebate_limit'] ?? 0); - $defaultRebate = (float) ($payload['default_player_rebate'] ?? 0); + $rebateLimit = (float) ($payload['rebate_limit'] ?? 0) / 100; + $defaultRebate = (float) ($payload['default_player_rebate'] ?? 0) / 100; $useRelative = $parent !== null && array_key_exists('relative_share_rate', $payload); @@ -124,8 +124,8 @@ final class AgentProfileService 'allocated_credit' => (int) $profile->allocated_credit, 'used_credit' => (int) $profile->used_credit, 'available_credit' => $available, - 'rebate_limit' => (float) $profile->rebate_limit, - 'default_player_rebate' => (float) $profile->default_player_rebate, + 'rebate_limit' => round((float) $profile->rebate_limit * 100, 4), + 'default_player_rebate' => round((float) $profile->default_player_rebate * 100, 4), 'settlement_cycle' => AgentSettlementCycle::normalize($profile->settlement_cycle), 'can_grant_extra_rebate' => (bool) $profile->can_grant_extra_rebate, 'can_create_child_agent' => (bool) $profile->can_create_child_agent, @@ -157,7 +157,7 @@ final class AgentProfileService return [ 'agent_node_id' => (int) $parent->id, 'total_share_rate' => (float) $profile->total_share_rate, - 'rebate_limit' => (float) $profile->rebate_limit, + 'rebate_limit' => round((float) $profile->rebate_limit * 100, 4), 'available_credit' => max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit), ]; } diff --git a/resources/views/admin/integration-guide.blade.php b/resources/views/admin/integration-guide.blade.php new file mode 100644 index 0000000..3bc7a94 --- /dev/null +++ b/resources/views/admin/integration-guide.blade.php @@ -0,0 +1,441 @@ + + + + + + 彩票代理接入文档 + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + @php + $sections = [ + [ + 'id' => 'overview', + 'title' => '1. 文档概览', + 'summary' => '说明接入目标、适用对象与总体范围。', + ], + [ + 'id' => 'architecture', + 'title' => '2. 接入架构', + 'summary' => '描述主站、彩票端与钱包系统之间的数据流向。', + ], + [ + 'id' => 'prerequisites', + 'title' => '3. 对接前准备', + 'summary' => '列出域名、密钥、接口地址和测试账号等准备项。', + ], + [ + 'id' => 'sso', + 'title' => '4. SSO 单点登录', + 'summary' => '说明客户如何生成 token 并跳转进入彩票端。', + ], + [ + 'id' => 'wallet', + 'title' => '5. 钱包接口', + 'summary' => '说明余额、扣款、加款三个接口的职责与约束。', + ], + [ + 'id' => 'signature', + 'title' => '6. 签名与安全', + 'summary' => '说明签名算法、密钥保护与重放防护。', + ], + [ + 'id' => 'errors', + 'title' => '7. 错误码与幂等', + 'summary' => '统一交易状态和重复请求处理方式。', + ], + [ + 'id' => 'testing', + 'title' => '8. 联调与验收', + 'summary' => '提供联调流程、测试清单和上线前核对项。', + ], + [ + 'id' => 'appendix', + 'title' => '9. 附录', + 'summary' => '给出标准报文示例和字段规范。', + ], + ]; + @endphp + +
+ + +
+
+
+
+
+

LOTTERY INTEGRATION GUIDE

+

彩票代理接入技术文档

+

+ 本页面用于给客户技术团队直接阅读和联调,按文档页方式展示接入说明,包含目录、数据流、接口约束、联调步骤和上线前检查项。 +

+
+ +
+
+
接入方式
+
SSO + 钱包接口
+
+
+
适用对象
+
客户技术团队
+
+
+
文档形态
+
后台独立页面
+
+
+
+
+ +
+
+
目录
+
+ @foreach ($sections as $section) + + {{ $section['title'] }} + + @endforeach +
+
+
+ +
+
+
+

1. 文档概览

+

+ 本文档用于指导客户将自有主站接入我方彩票端,形成完整的登录、跳转、余额、投注扣款与派奖加款链路。 +

+
+ +
+
+
登录接入
+
客户主站登录后,通过 SSO 进入彩票端,无需再次认证。
+
+
+
钱包接入
+
投注与派奖资金动作由我方调用客户钱包接口完成。
+
+
+
联调验收
+
客户、我方技术与测试按照同一套流程完成联调和上线验收。
+
+
+
适用范围
+
适用于 H5、Web、App 内嵌 WebView 等进入彩票端的接入方式。
+
+
+
+ +
+
+

2. 接入架构

+

客户系统与彩票端的职责边界如下,登录由主站发起,资金由主站钱包记账,彩票端负责业务过程编排。

+
+ +
+
+
01 客户主站
+
用户登录与身份来源
+
客户负责维护会员账号、登录态和唯一用户标识。
+
+
+
02 SSO 网关
+
生成短期 token
+
客户服务端生成签名凭证,浏览器携带后进入彩票端。
+
+
+
03 彩票端
+
验签并建立会话
+
我方校验 token 后创建会话,承接投注、撤单、派奖等业务流程。
+
+
+
04 客户钱包
+
余额与账务真理源
+
余额查询、扣款、加款由客户钱包接口统一响应并负责幂等记账。
+
+
+ +
+
端到端链路
+
    +
  1. 1. 用户在客户主站完成登录。
  2. +
  3. 2. 客户服务端生成 SSO token。
  4. +
  5. 3. 浏览器跳转到彩票端入口地址。
  6. +
  7. 4. 彩票端校验 token 并建立用户会话。
  8. +
  9. 5. 用户查询余额、进行投注或等待派奖。
  10. +
  11. 6. 彩票端按业务场景调用客户钱包接口。
  12. +
  13. 7. 钱包返回处理结果,彩票端落业务状态并反馈前端。
  14. +
+
+
+ +
+
+

3. 对接前准备

+

双方开始联调前,需要先完成以下准备项,避免联调阶段反复返工。

+
+ +
+
+
客户需提供
+
    +
  • • 站点名称、站点编码、测试环境与生产环境标识。
  • +
  • • 技术联系人、测试联系人、上线当天紧急联系人。
  • +
  • • 客户主站域名、钱包接口域名、可嵌入来源域名。
  • +
  • • 测试账号、测试余额和可重复联调的测试场景说明。
  • +
+
+
+
双方需共同确认
+
    +
  • • SSO 密钥或 JWT 验签密钥。
  • +
  • • 钱包 API 鉴权方式、签名算法、请求头规范。
  • +
  • • 钱包三类接口地址:余额、扣款、加款。
  • +
  • • 请求超时、重试策略、幂等键和错误码表。
  • +
+
+
+
+ +
+
+

4. SSO 单点登录

+

客户用户在主站登录后,通过服务端签发的短期 token 进入彩票端,我方校验通过后自动建立会话。

+
+ +
+
+
推荐接入流程
+
    +
  1. 1. 客户主站用户完成登录。
  2. +
  3. 2. 客户服务端按约定字段组装 SSO 负载。
  4. +
  5. 3. 使用共享密钥签发 token,并设置短期过期时间。
  6. +
  7. 4. 浏览器跳转我方彩票端地址,携带 token 参数。
  8. +
  9. 5. 我方校验签名、时间戳、站点编码和用户标识后建立彩票端登录态。
  10. +
+
+ +
+
SSO 关键约束
+
    +
  • • token 必须是短时有效,建议 60 到 300 秒。
  • +
  • • `user_id` 在客户站点内必须稳定且唯一。
  • +
  • • `site_code` 必须与约定站点保持一致。
  • +
  • • 生产密钥与测试密钥必须隔离。
  • +
  • • 不可把签名密钥暴露在前端代码里。
  • +
+
+
+ +
+
SSO 负载示例
+
+
{
+  "user_id": "100001",
+  "username": "demo_user",
+  "site_code": "demo",
+  "timestamp": 1718000000,
+  "nonce": "N8F2X9Q1",
+  "currency": "CNY",
+  "device": "h5"
+}
+
+
+
+ +
+
+

5. 钱包接口

+

我方会调用客户钱包完成账务动作。客户至少需要实现余额查询、扣款、加款三类能力。

+
+ +
+
+
余额查询
+
用于进入彩票端、下注前或关键账务时机同步可用余额。
+
POST /wallet/balance
+
+
+
扣款接口
+
用于用户下注成功后的资金扣减,必须以交易号作为唯一幂等键。
+
POST /wallet/debit
+
+
+
加款接口
+
用于派奖、退款或撤单返还资金,重复请求不得重复记账。
+
POST /wallet/credit
+
+
+ +
+
+
扣款报文示例
+
+
{
+  "site_code": "demo",
+  "user_id": "100001",
+  "transaction_id": "BET202606100001",
+  "order_id": "TICKET202606100001",
+  "amount": "20.00",
+  "timestamp": 1718000001,
+  "sign": "xxxxxx"
+}
+
+
+
+
成功返回示例
+
+
{
+  "code": 0,
+  "message": "success",
+  "data": {
+    "transaction_id": "BET202606100001",
+    "balance": "980.00"
+  }
+}
+
+
+
+
+ +
+
+

6. 签名与安全

+

签名规则和密钥保护是接入稳定性的基础。推荐统一采用服务端签名并在所有敏感请求中校验。

+
+ +
+
+
推荐签名规则
+
    +
  1. 1. 请求字段按字段名升序排序。
  2. +
  3. 2. 以 `key=value` 形式拼接原始串。
  4. +
  5. 3. 原始串尾部追加共享密钥。
  6. +
  7. 4. 使用 `HMAC-SHA256` 计算签名。
  8. +
  9. 5. 结果输出为十六进制小写字符串。
  10. +
+
+
+
安全要求
+
    +
  • • 所有请求必须使用 HTTPS。
  • +
  • • token 和钱包请求必须校验时间戳与签名。
  • +
  • • 建议增加 nonce 或 request_id 防止重放。
  • +
  • • 测试环境与生产环境密钥不得共用。
  • +
  • • 密钥轮换时必须预留并行切换窗口。
  • +
+
+
+
+ +
+
+

7. 错误码与幂等

+

为了保证交易可重试、可审计,客户钱包接口必须统一错误码并支持强幂等。

+
+ +
+
+
建议错误码
+
    +
  • • `0`:成功
  • +
  • • `1001`:参数错误
  • +
  • • `1002`:签名错误
  • +
  • • `1003`:用户不存在
  • +
  • • `1004`:余额不足
  • +
  • • `1006`:重复交易
  • +
  • • `1099`:系统异常
  • +
+
+
+
幂等处理要求
+
    +
  • • 扣款和加款必须使用 `transaction_id` 作为唯一交易键。
  • +
  • • 同一 `transaction_id` 的重复请求,不得重复扣款或重复加款。
  • +
  • • 已成功处理的交易,重复请求必须返回首次处理结果。
  • +
  • • 钱包超时或网络抖动时,我方可能发起重试,因此客户必须按幂等方式落账。
  • +
+
+
+
+ +
+
+

8. 联调与验收

+

建议双方按固定节奏联调,先通登录链路,再通钱包链路,最后做异常回归和上线核验。

+
+ +
+
+
联调顺序
+
    +
  1. 1. 域名连通与证书校验。
  2. +
  3. 2. SSO token 生成与跳转验证。
  4. +
  5. 3. 余额查询接口联调。
  6. +
  7. 4. 扣款和加款接口联调。
  8. +
  9. 5. 超时、重复请求和余额不足场景回归。
  10. +
+
+
+
上线前检查清单
+
    +
  • • 正式域名、正式密钥和正式白名单均已配置。
  • +
  • • 测试环境和生产环境参数已分离。
  • +
  • • 核心错误码、日志和告警已对齐。
  • +
  • • 关键交易链路已通过验收回归。
  • +
  • • 上线当天应急联系人与回滚预案已确认。
  • +
+
+
+
+ +
+
+

9. 附录

+

为了减少不同系统之间的解析差异,建议双方统一基础字段格式。

+
+ +
+
+
金额字段
+
统一使用字符串传输,例如 `1000.00`,避免浮点精度误差。
+
+
+
时间字段
+
建议使用 Unix 时间戳秒级,双方也可统一为 ISO8601。
+
+
+
报文格式
+
字符编码统一 UTF-8,请求内容类型统一 `application/json`。
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 86a06c5..1f58aef 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,3 +5,8 @@ use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); + +Route::prefix('admin/docs')->group(function (): void { + Route::view('integration-guide', 'admin.integration-guide') + ->name('admin.docs.integration-guide'); +}); diff --git a/tests/Feature/AdminAgentProfileApiTest.php b/tests/Feature/AdminAgentProfileApiTest.php index cc2ac7e..15b90c1 100644 --- a/tests/Feature/AdminAgentProfileApiTest.php +++ b/tests/Feature/AdminAgentProfileApiTest.php @@ -42,8 +42,8 @@ test('super admin can update agent profile with capability flags', function (): ->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [ 'total_share_rate' => 12, 'credit_limit' => 1200, - 'rebate_limit' => 0.01, - 'default_player_rebate' => 0.005, + 'rebate_limit' => 1, + 'default_player_rebate' => 0.5, 'settlement_cycle' => 'weekly', 'can_grant_extra_rebate' => false, 'can_create_child_agent' => true, @@ -224,8 +224,8 @@ test('agent profile update rejects default rebate above limit', function (): voi $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [ - 'rebate_limit' => 0.005, - 'default_player_rebate' => 0.01, + 'rebate_limit' => 0.5, + 'default_player_rebate' => 1, ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value);