doge 2 dní pred
rodič
commit
8745ea32b2

+ 5 - 0
app/Http/Controllers/admin/Promotion.php

@@ -26,6 +26,11 @@ class Promotion extends Controller
     {
     {
         return $this->run(fn () => request()->isMethod('post') ? $s->saveFirstDepositRestriction(request()->all()) : $s->firstDepositRestriction());
         return $this->run(fn () => request()->isMethod('post') ? $s->saveFirstDepositRestriction(request()->all()) : $s->firstDepositRestriction());
     }
     }
+    /** 首充活动详情链接(PC / H5 / APP):GET 读取,POST 保存。 */
+    public function firstDepositLinks(PromotionAdminService $s)
+    {
+        return $this->run(fn () => request()->isMethod('post') ? $s->saveFirstDepositLinks(request()->all()) : $s->firstDepositLinks());
+    }
     public function detail(PromotionAdminService $s)
     public function detail(PromotionAdminService $s)
     {
     {
         return $this->run(function () use ($s) {
         return $this->run(function () use ($s) {

+ 27 - 0
app/Services/PromotionAdminService.php

@@ -49,6 +49,33 @@ class PromotionAdminService
         return $this->firstDepositRestriction();
         return $this->firstDepositRestriction();
     }
     }
 
 
+    /** 首充活动详情链接:PC、H5、APP 各一个,全部首充活动共用。前台首充接口按会员的平台返回对应链接。 */
+    public function firstDepositLinks(): array
+    {
+        return ['links' => PromotionRules::detailLinks()];
+    }
+
+    public function saveFirstDepositLinks(array $input): array
+    {
+        $links = [];
+        foreach (PromotionRules::DETAIL_LINK_PLATFORMS as $platform) {
+            $key = strtolower($platform);
+            $value = $input['links'][$platform] ?? $input['links'][$key] ?? $input[$key] ?? '';
+            if (!is_string($value) && $value !== null) throw ValidationException::withMessages([$key => $platform . ' 链接必须是文本']);
+            $value = trim((string) $value);
+            if (mb_strlen($value) > 500) throw ValidationException::withMessages([$key => $platform . ' 链接不能超过 500 个字符']);
+            $links[$platform] = $value;
+        }
+        DB::transaction(function () use ($links) {
+            $before = PromotionRules::detailLinks();
+            DB::table('config')->updateOrInsert(['field' => PromotionRules::DETAIL_LINKS_CONFIG], ['val' => json_encode($links, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)]);
+            if (Schema::hasTable('operation_audits')) {
+                OperationAuditService::record('promotion_first_deposit_links', null, 'update', ['links' => $before], ['links' => $links]);
+            }
+        }, 3);
+        return $this->firstDepositLinks();
+    }
+
     private function forbiddenOptions(array $categories): array
     private function forbiddenOptions(array $categories): array
     {
     {
         $options = [];
         $options = [];

+ 14 - 0
app/Support/PromotionRules.php

@@ -69,6 +69,20 @@ final class PromotionRules
 
 
     /** 全部首充活动共用的禁止投注游戏大类,存于 config 表(melbet 读取同一项)。 */
     /** 全部首充活动共用的禁止投注游戏大类,存于 config 表(melbet 读取同一项)。 */
     public const FORBIDDEN_CONFIG = 'first_deposit_forbidden_categories';
     public const FORBIDDEN_CONFIG = 'first_deposit_forbidden_categories';
+    /** 首充活动详情链接(PC / H5 / APP),JSON 对象,全部首充活动共用。 */
+    public const DETAIL_LINKS_CONFIG = 'first_deposit_detail_links';
+    public const DETAIL_LINK_PLATFORMS = ['PC', 'H5', 'APP'];
+
+    /** @return array{PC:string,H5:string,APP:string} */
+    public static function detailLinks(): array
+    {
+        $saved = json_decode((string) DB::table('config')->where('field', self::DETAIL_LINKS_CONFIG)->value('val'), true);
+        $links = [];
+        foreach (self::DETAIL_LINK_PLATFORMS as $platform) {
+            $links[$platform] = is_array($saved) && is_string($saved[$platform] ?? null) ? $saved[$platform] : '';
+        }
+        return $links;
+    }
 
 
     /** 当前设置;未配置或内容损坏时视为不限制。 */
     /** 当前设置;未配置或内容损坏时视为不限制。 */
     public static function forbiddenSetting(): array
     public static function forbiddenSetting(): array

+ 4 - 0
docs/前端交付/README.md

@@ -2,6 +2,10 @@
 
 
 本目录是2026-10-26的接口文档交付副本,按**项目 → 前台/后台/代理端**组织。
 本目录是2026-10-26的接口文档交付副本,按**项目 → 前台/后台/代理端**组织。
 
 
+## 首充活动详情链接(2026-11-13)
+
+- [首充活动详情链接-前端修改单](首充活动详情链接-前端修改单.md):总后台新增 `GET/POST /admin/promotion/first-deposit-links`,PC、H5、APP 各填一个活动详情链接;会员前台 `GET /api/promotion/firstDeposit` 新增 `detail_url`,按请求的 `platform` 自动返回对应链接。
+
 ## 首充活动提交(2026-11-12)
 ## 首充活动提交(2026-11-12)
 
 
 - [首充活动不选具体活动-前端修改单](首充活动不选具体活动-前端修改单.md):会员自己选**是否参加**首充(保留开关),但**不用选哪个活动**:参加时只提交外层的 `first_deposit_offer`,首笔到账时系统按金额自动取最高档(100 送 10、1000 送 50,充 1000 送 50)。`activities` 只用于展示档位,不再带每项的凭据和 `selected`。
 - [首充活动不选具体活动-前端修改单](首充活动不选具体活动-前端修改单.md):会员自己选**是否参加**首充(保留开关),但**不用选哪个活动**:参加时只提交外层的 `first_deposit_offer`,首笔到账时系统按金额自动取最高档(100 送 10、1000 送 50,充 1000 送 50)。`activities` 只用于展示档位,不再带每项的凭据和 `selected`。

+ 22 - 0
docs/前端交付/首充活动详情链接-前台.md

@@ -0,0 +1,22 @@
+## 二、首充活动的链接详情(PC / H5 / APP)
+
+`GET /api/promotion/firstDeposit?platform=H5` 的返回新增 `detail_url`:
+
+```json
+{
+  "has_deposited": false,
+  "can_choose": true,
+  "detail_url": "/promo/first",
+  "activities": [ … ],
+  "first_deposit_offer": "…"
+}
+```
+
+| 字段 | 说明 |
+|---|---|
+| `detail_url` | 当前平台的首充活动详情链接。按请求的 `platform` 自动取后台对应的那一个:传 `PC` 返回 PC 链接,`H5` 返回 H5 链接,`APP` 返回 APP 链接。后台没填时为空字符串 |
+
+前端:
+- `detail_url` 不为空时,在首充区域显示「活动详情」入口,点击跳转。完整网址用外链打开,站内路径用路由跳转,APP 按自己的跳转协议处理。
+- 为空时隐藏入口。
+- `POST /api/promotion/firstDepositChoice` 的返回结构和上面相同,也带 `detail_url`。

+ 47 - 0
docs/前端交付/首充活动详情链接-后台修改单.md

@@ -0,0 +1,47 @@
+# 首充活动详情链接-前端修改单(2026-11-13)
+
+## 功能
+
+总后台可以给首充活动设置「活动详情」链接,PC、H5、APP 各填一个,全部首充活动共用。会员前台的首充接口按请求的平台自动返回对应的链接,前端用它做「查看活动详情」的跳转。
+
+## 一、总后台
+
+在首充活动的设置里(和「禁止投注的游戏大类」放在一起)加三个输入框:
+
+| 输入框 | 参数 | 说明 |
+|---|---|---|
+| PC 详情链接 | `pc` | 文本,最多 500 个字符,可以留空 |
+| H5 详情链接 | `h5` | 同上 |
+| APP 详情链接 | `app` | 同上 |
+
+后端只去掉首尾空格,不校验格式,完整网址、站内路径(如 `/promo/first`)、APP 跳转地址都可以填。
+
+### 读取
+
+`GET /admin/promotion/first-deposit-links`
+
+```json
+{
+  "links": {
+    "PC": "https://www.example.com/promo/first",
+    "H5": "/promo/first",
+    "APP": "app://promo/first"
+  }
+}
+```
+
+没设置过时三个都是空字符串。
+
+### 保存
+
+`POST /admin/promotion/first-deposit-links`
+
+```json
+{"pc": "https://www.example.com/promo/first", "h5": "/promo/first", "app": "app://promo/first"}
+```
+
+- 三个都要传,**没传或传空字符串的平台会被清空**。
+- 成功返回和读取相同的结构。
+- 错误 `msg`:`PC 链接不能超过 500 个字符`、`H5 链接必须是文本` 等。
+
+权限沿用活动管理的按钮:读取 `admin/promotion`,保存 `admin/promotion/submit`。修改会写入操作日志。

+ 2 - 0
routes/admin.php

@@ -105,6 +105,8 @@ Route::middleware(['admin.jwt'])->group(function () {
             Route::get('/records', [\App\Http\Controllers\admin\Promotion::class, 'records']);
             Route::get('/records', [\App\Http\Controllers\admin\Promotion::class, 'records']);
             Route::get('/first-deposit-restriction', [\App\Http\Controllers\admin\Promotion::class, 'firstDepositRestriction'])->defaults('permission_uri', 'admin/promotion');
             Route::get('/first-deposit-restriction', [\App\Http\Controllers\admin\Promotion::class, 'firstDepositRestriction'])->defaults('permission_uri', 'admin/promotion');
             Route::post('/first-deposit-restriction', [\App\Http\Controllers\admin\Promotion::class, 'firstDepositRestriction'])->defaults('permission_uri', 'admin/promotion/submit');
             Route::post('/first-deposit-restriction', [\App\Http\Controllers\admin\Promotion::class, 'firstDepositRestriction'])->defaults('permission_uri', 'admin/promotion/submit');
+            Route::get('/first-deposit-links', [\App\Http\Controllers\admin\Promotion::class, 'firstDepositLinks'])->defaults('permission_uri', 'admin/promotion');
+            Route::post('/first-deposit-links', [\App\Http\Controllers\admin\Promotion::class, 'firstDepositLinks'])->defaults('permission_uri', 'admin/promotion/submit');
         });
         });
         Route::prefix('/reporting')->group(function () {
         Route::prefix('/reporting')->group(function () {
             Route::get('/options', [Reporting::class, 'options'])->defaults('permission_uri', 'admin/reporting/summary');
             Route::get('/options', [Reporting::class, 'options'])->defaults('permission_uri', 'admin/reporting/summary');

+ 13 - 0
tests/Integration/PromotionActivitiesTest.php

@@ -952,6 +952,19 @@ class PromotionActivitiesTest extends TestCase
         $this->invalid(fn () => \App\Support\FirstDepositChoice::parse('yes'), 'join_first_deposit只能为0或1');
         $this->invalid(fn () => \App\Support\FirstDepositChoice::parse('yes'), 'join_first_deposit只能为0或1');
     }
     }
 
 
+    public function test_first_deposit_detail_links_are_set_per_platform(): void
+    {
+        $admin = new PromotionAdminService();
+        $this->assertSame(['links' => ['PC' => '', 'H5' => '', 'APP' => '']], $admin->firstDepositLinks());
+        $saved = $admin->saveFirstDepositLinks(['pc' => ' https://pc.example.test/first ', 'h5' => '/promo/first', 'app' => 'app://promo/first']);
+        $this->assertSame(['PC' => 'https://pc.example.test/first', 'H5' => '/promo/first', 'APP' => 'app://promo/first'], $saved['links']);
+        // 不传的平台清空;也接受 links 对象。
+        $this->assertSame(['PC' => '', 'H5' => '/h5', 'APP' => ''], $admin->saveFirstDepositLinks(['links' => ['H5' => '/h5']])['links']);
+        $this->invalid(fn () => $admin->saveFirstDepositLinks(['pc' => str_repeat('a', 501)]), 'PC 链接不能超过 500 个字符');
+        $this->invalid(fn () => $admin->saveFirstDepositLinks(['h5' => ['x']]), 'H5 链接必须是文本');
+        $this->assertSame('/h5', $admin->firstDepositLinks()['links']['H5']);
+    }
+
     public function test_first_deposit_restriction_is_one_setting_shared_by_every_campaign(): void
     public function test_first_deposit_restriction_is_one_setting_shared_by_every_campaign(): void
     {
     {
         $admin = new PromotionAdminService();
         $admin = new PromotionAdminService();