Tree hai 2 meses
pai
achega
edb9a1f3e6

+ 164 - 0
Privacy.html

@@ -0,0 +1,164 @@
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width,initial-scale=1" />
+    <title>Privacy Policy</title>
+    <meta name="description" content="Privacy Policy" />
+    <style>
+        :root{
+            --bg:#0b0f17; --card:#121a27; --text:#e6edf6; --muted:#9fb0c3;
+            --line:#233044; --primary:#6aa6ff; --shadow:0 10px 30px rgba(0,0,0,.35);
+            --radius:16px; --max:980px;
+            --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
+        }
+        @media (prefers-color-scheme: light){
+            :root{ --bg:#f7f8fb; --card:#fff; --text:#1c2430; --muted:#5b6b7e;
+                --line:#e7ebf3; --primary:#2f6fff; --shadow:0 10px 24px rgba(18,26,39,.08);}
+        }
+        *{box-sizing:border-box}
+        body{margin:0;font-family:var(--sans);background:var(--bg);color:var(--text);line-height:1.7}
+        a{color:var(--primary);text-decoration:none} a:hover{text-decoration:underline}
+        .wrap{max-width:var(--max);margin:0 auto;padding:0 16px}
+        header{padding:48px 0 18px}
+        h1{margin:0;font-size:clamp(26px,3vw,40px)}
+        .meta{margin-top:10px;color:var(--muted);font-size:14px}
+        main{padding:0 0 64px}
+        .card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);
+            box-shadow:var(--shadow);padding:22px}
+        h2{margin:24px 0 10px;font-size:20px}
+        p{margin:10px 0}
+        ul{margin:8px 0 12px 20px;padding:0}
+        li{margin:6px 0}
+        .hr{height:1px;background:var(--line);margin:18px 0}
+        footer{padding:22px 0 40px;color:var(--muted);font-size:13px}
+        .note{border-left:3px solid var(--primary);padding:10px 12px;border-radius:12px;
+            background:rgba(106,166,255,.10);border:1px solid rgba(106,166,255,.22);margin:12px 0}
+    </style>
+</head>
+
+<body>
+<header>
+    <div class="wrap">
+        <h1>Privacy Policy</h1>
+        <p class="meta">
+            If you have questions, contact us at
+            <a href="mailto:gameservice@gmail.com">gameservice@gmail.com</a>.
+        </p>
+    </div>
+</header>
+
+<main>
+    <div class="wrap">
+        <article class="card">
+            <p>
+                This Privacy Policy explains how We
+                collects, uses, and shares information when you use  (the “Service”).
+            </p>
+
+            <div class="note">
+                <strong>Short version:</strong> We only collect information needed to run the Service, keep it secure,
+                and improve it. We do not sell your personal information.
+            </div>
+
+            <h2>1) Information We Collect</h2>
+            <p>We may collect the following types of information:</p>
+            <ul>
+                <li><strong>Information you provide</strong>: such as your name, email address, messages you send us, or other details you submit through forms.</li>
+                <li><strong>Usage data</strong>: such as pages viewed, actions taken, referring/exit pages, and timestamps.</li>
+                <li><strong>Device and log data</strong>: such as IP address, browser type, device identifiers, operating system, and error logs.</li>
+                <li><strong>Cookies</strong>: small files stored on your device to help the site work and remember preferences.</li>
+            </ul>
+
+            <h2>2) How We Use Information</h2>
+            <p>We use information to:</p>
+            <ul>
+                <li>Provide, maintain, and improve the Service;</li>
+                <li>Operate features you request and respond to support messages;</li>
+                <li>Monitor and protect the security of the Service (fraud prevention, abuse detection);</li>
+                <li>Analyze usage to understand what works and fix issues;</li>
+                <li>Comply with legal obligations and enforce our terms.</li>
+            </ul>
+
+            <h2>3) How We Share Information</h2>
+            <p>We may share information in limited circumstances:</p>
+            <ul>
+                <li><strong>Service providers</strong> that help us run the Service (e.g., hosting, analytics, email delivery), under appropriate confidentiality and security obligations;</li>
+                <li><strong>Legal and safety</strong> reasons, if required by law or to protect rights, safety, and security;</li>
+                <li><strong>Business transfers</strong>, if we are involved in a merger, acquisition, or asset sale (you will be notified if required).</li>
+            </ul>
+            <p><strong>We do not sell your personal information.</strong></p>
+
+            <h2>4) Cookies</h2>
+            <p>
+                We use cookies and similar technologies to operate the Service and improve your experience.
+                You can control cookies through your browser settings. Disabling cookies may affect certain features.
+            </p>
+
+            <h2>5) Data Security</h2>
+            <p>
+                We use reasonable administrative, technical, and physical safeguards designed to protect information.
+                However, no method of transmission or storage is 100% secure.
+            </p>
+
+            <h2>6) Data Retention</h2>
+            <p>
+                We keep information only as long as necessary for the purposes described in this Policy,
+                unless a longer retention period is required or permitted by law.
+            </p>
+
+            <h2>7) Your Choices</h2>
+            <p>Depending on where you live, you may have rights to:</p>
+            <ul>
+                <li>Access, correct, or delete your personal information;</li>
+                <li>Object to or restrict certain processing;</li>
+                <li>Withdraw consent where processing is based on consent.</li>
+            </ul>
+            <p>
+                To make a request, contact us at <a href="mailto:gameservice@gmail.com">gameservice@gmail.com</a>.
+                We may ask you to verify your identity.
+            </p>
+
+            <h2>8) Children’s Privacy</h2>
+            <p>
+                The Service is not intended for children under the age of <strong>18</strong>.
+                If you believe a child has provided us personal information, contact us and we will take appropriate steps.
+            </p>
+
+            <h2>9) International Users</h2>
+            <p>
+                Your information may be processed in countries other than your own. We take steps to protect information
+                consistent with this Policy.
+            </p>
+
+            <h2>10) Changes to This Policy</h2>
+            <p>
+                We may update this Policy from time to time. We will post the updated version on this page and update the
+                effective date above.
+            </p>
+
+            <h2>11) Contact Us</h2>
+            <p>
+                Email: <a href="mailto:gameservice@gmail.com">gameservice@gmail.com</a>
+            </p>
+
+            <div class="hr"></div>
+            <p style="color:var(--muted);font-size:13px;margin:0">
+                This page is provided for general informational purposes and does not constitute legal advice.
+            </p>
+        </article>
+    </div>
+</main>
+
+<footer>
+    <div class="wrap">
+        © <span id="y"></span> Game. All rights reserved.
+    </div>
+</footer>
+
+<script>
+    // For display only; no tracking.
+    document.getElementById("y").textContent = String(new Date().getFullYear());
+</script>
+</body>
+</html>

+ 119 - 0
app/Console/Commands/CheckIosAppStore.php

@@ -0,0 +1,119 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Notification\TelegramBot;
+use App\Util;
+use Illuminate\Console\Command;
+
+/**
+ * 检测配置的 iOS App Store 包是否被下架,若下架则通过 Telegram 通知。
+ */
+class CheckIosAppStore extends Command
+{
+    protected $signature = 'ios:check-app-store';
+
+    protected $description = '检测 iOS App Store 包是否被下架,下架时 Telegram 通知';
+
+    /** 要检测的 iOS 包列表:name => url */
+    protected $apps = [
+        'Slots Magnet Revolution' => 'https://apps.apple.com/us/app/slots-magnet-revolution/id6759797102',
+        'Viking Mysteric Slots'   => 'https://apps.apple.com/us/app/viking-mysteric-slots/id6759851510',
+        'Slots Chemistry Pirate'  => 'https://apps.apple.com/us/app/slots-chemistry-pirate/id6759852588',
+    ];
+
+    /** 页面内容中表示“已下架”的关键词(不区分大小写) */
+    protected $delistedKeywords = [
+        'no longer available',
+        'removed this app from the app store',
+        'not available',
+        'this app is no longer',
+        'has been removed',
+    ];
+
+    public function handle()
+    {
+        $env = env('APP_ENV', 'local');
+        $delisted = [];
+
+        foreach ($this->apps as $name => $url) {
+            $result = $this->checkApp($url);
+            if ($result['delisted']) {
+                $delisted[] = [
+                    'name' => $name,
+                    'url'  => $url,
+                    'reason' => $result['reason'],
+                ];
+                Util::WriteLog('ios_app_store_check', "Delisted: {$name} | {$url} | {$result['reason']}");
+            }
+        }
+
+        if (count($delisted) > 0) {
+            $this->notifyDelisted($env, $delisted);
+            $this->warn('已检测到 ' . count($delisted) . ' 个包被下架,已发送 Telegram 通知。');
+            return 1;
+        }
+
+        $this->info('所有 iOS 包状态正常。');
+        return 0;
+    }
+
+    /**
+     * 检测单个 App 是否可访问(未下架)
+     * @return array ['delisted' => bool, 'reason' => string]
+     */
+    protected function checkApp($url)
+    {
+        $ch = curl_init($url);
+        curl_setopt_array($ch, [
+            CURLOPT_RETURNTRANSFER => true,
+            CURLOPT_FOLLOWLOCATION => true,
+            CURLOPT_TIMEOUT       => 15,
+            CURLOPT_USERAGENT     => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+            CURLOPT_SSL_VERIFYPEER => true,
+        ]);
+        $body = curl_exec($ch);
+        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        $error = curl_error($ch);
+        curl_close($ch);
+
+        if ($error) {
+            return ['delisted' => true, 'reason' => "请求失败: {$error}"];
+        }
+
+        if ($code === 404) {
+            return ['delisted' => true, 'reason' => 'HTTP 404'];
+        }
+
+        if ($code !== 200) {
+            return ['delisted' => true, 'reason' => "HTTP {$code}"];
+        }
+
+        $bodyLower = mb_strtolower($body);
+        foreach ($this->delistedKeywords as $keyword) {
+            if (strpos($bodyLower, $keyword) !== false) {
+                return ['delisted' => true, 'reason' => "页面包含下架提示: {$keyword}"];
+            }
+        }
+
+        return ['delisted' => false, 'reason' => ''];
+    }
+
+    protected function notifyDelisted($env, array $delisted)
+    {
+        $lines = ["[{$env}] iOS App Store 下架检测告警"];
+        foreach ($delisted as $item) {
+            $lines[] = "• {$item['name']}";
+            $lines[] = "  原因: {$item['reason']}";
+            $lines[] = "  链接: {$item['url']}";
+        }
+        $message = implode("\n", $lines);
+
+        try {
+            TelegramBot::getDefault()->sendMsg($message);
+        } catch (\Exception $e) {
+            Util::WriteLog('ios_app_store_check', 'Telegram send error: ' . $e->getMessage());
+            $this->error('Telegram 发送失败: ' . $e->getMessage());
+        }
+    }
+}

+ 3 - 0
app/Console/Kernel.php

@@ -2,6 +2,7 @@
 
 namespace App\Console;
 
+use App\Console\Commands\CheckIosAppStore;
 use App\Console\Commands\DbQueue;
 use App\Console\Commands\OnlineReport;
 use App\Console\Commands\DecStock;
@@ -33,6 +34,7 @@ class Kernel extends ConsoleKernel
         RecordServerGameCountYesterday::class,
         RecordUserScoreChangeStatistics::class,
         DecStock::class,
+        CheckIosAppStore::class,
         OnlineReport::class,
         DbQueue::class,
         RecordThreeGameYesterday::class,
@@ -56,6 +58,7 @@ class Kernel extends ConsoleKernel
         $schedule->command('RecordUserScoreChangeStatistics')->cron('03 0 * * * ')->description('用户金额变化明细按天按用户汇总');
         $schedule->command('superball:update-pool-stats')->everyMinute()->description('Superball 每分钟刷新奖池及展示统计');
         $schedule->command('online_report')->everyMinute()->description('每分钟统计曲线');
+        $schedule->command('ios:check-app-store')->everyFiveMinutes()->description('每5分钟检测 iOS App Store 包是否下架');
 
 //        $schedule->command('record_three_game_yesterday')->cron('05 0 * * * ')->description('按天统计游戏人数--今日执行昨日');
     }

+ 15 - 10
app/Http/Controllers/Admin/GameSomeConfigController.php

@@ -15,24 +15,28 @@ class GameSomeConfigController
     {
         // 获取游戏ID,默认为0(通用配置)
         $gameId = $request->input('game_id', 0);
-        
+
+        // 获取库存模式,默认为1(普通模式)
+        $stockMode = $request->input('stock_mode', 1);
+
         // 可选的游戏列表
         $games = [
             0 => '通用配置',
             '921219001' => 'IGT-双砖',
             '1519119693' => 'Jokers Jewels',
         ];
-        
-        // 查询指定GameID的配置
+
+        // 查询指定GameID和StockMode的配置
         $configs = DB::connection('write')
             ->table('QPPlatformDB.dbo.GameSomeConfig')
             ->where('GameID', $gameId)
+            ->where('StockMode', $stockMode)
             ->orderBy('ZMin', 'asc')
             ->orderBy('ZMax', 'asc')
             ->orderBy('MultiMin', 'asc')
             ->get();
 
-        return view('admin.game_some_config.index', compact('configs', 'games', 'gameId'));
+        return view('admin.game_some_config.index', compact('configs', 'games', 'gameId', 'stockMode'));
     }
 
     /**
@@ -51,11 +55,11 @@ class GameSomeConfigController
 
             $updatedIds = [];
             $updatedCount = 0;
-            
+
             foreach ($configs as $configId => $data) {
                 // 构建更新数据(只包含提交的字段)
                 $updateData = [];
-                
+
                 if (isset($data['ZMin'])) {
                     $updateData['ZMin'] = (int)$data['ZMin'];
                 }
@@ -74,10 +78,11 @@ class GameSomeConfigController
                 if (isset($data['Weight'])) {
                     $updateData['Weight'] = (int)$data['Weight'];
                 }
-                if (isset($data['WeightAdjust'])) {
-                    $updateData['WeightAdjust'] = $data['WeightAdjust'];
+                // 个控调节支持配置为空(使用 array_key_exists 以支持空字符串)
+                if (array_key_exists('WeightAdjust', $data)) {
+                    $updateData['WeightAdjust'] = (string)($data['WeightAdjust'] ?? '');
                 }
-                
+
                 // Status字段不允许修改,已移除
 
                 // 只有有数据时才更新
@@ -86,7 +91,7 @@ class GameSomeConfigController
                         ->table('QPPlatformDB.dbo.GameSomeConfig')
                         ->where('ConfigID', $configId)
                         ->update($updateData);
-                    
+
                     $updatedIds[] = $configId;
                     $updatedCount++;
                 }

+ 58 - 0
app/Http/Controllers/Admin/RechargeController.php

@@ -13,6 +13,7 @@ use App\Models\Order;
 use App\Models\Platform\MonthCard;
 use App\Services\OrderServices;
 use App\Services\PayMentService;
+use App\Game\Services\OuroGameService;
 use Carbon\Carbon;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\DB;
@@ -805,6 +806,63 @@ class RechargeController extends Controller
         }
     }
 
+    /**
+     * 模拟上报 FB 支付事件(仅管理员可用)
+     */
+    public function fbReportMock(Request $request, $id)
+    {
+        try {
+            $admin = $request->session()->get('admin');
+            $roleId = $admin->roles[0]->id ?? 0;
+            if (!in_array($roleId, [1, 12])) {
+                return apiReturnFail('无权限执行该操作');
+            }
+
+            $order = DB::connection('write')->table('agent.dbo.order')->where('id', $id)->first();
+            if (!$order) {
+                return apiReturnFail('订单不存在!');
+            }
+
+            // 只允许对未成功的订单做模拟上报(已到账的默认认为已正常上报)
+            if ((int)$order->pay_status === 1) {
+                return apiReturnFail('仅对未成功订单开放模拟上报');
+            }
+
+            $userId = $order->user_id;
+            $payAmt = $order->amount; // 内部金额单位,保持与原上报一致
+
+            // 构造模拟上报的 params
+            $item = [
+                'regtime' => time(),        // 当前时间
+                'isd0'    => 1,             // 固定为首日
+                'first'   => 1,             // 固定视为首充
+                'golds'   => $payAmt,       // 金额
+                'udis'    => $userId,       // 注意:按需求使用 udis 字段
+                'order_sn'=> $order->order_sn,
+            ];
+
+            // 通过 OuroGameService 模拟一次 pay_finish 上报
+            OuroGameService::notifyWebHall(
+                $userId,
+                '',
+                'pay_finish',
+                [
+                    'Golds' => $payAmt,
+                    'PayNum'=> $payAmt,
+                    'event' => 'pay',
+                    'params'=> $item,
+                ]
+            );
+
+            return apiReturnSuc();
+        } catch (\Exception $exception) {
+            Log::error('模拟 FB 上报失败:' . $exception->getMessage(), [
+                'order_id' => $id,
+            ]);
+            return apiReturnFail('上报失败,请稍后重试');
+        }
+    }
+
 
     public function search($orderId)
     {

+ 4 - 2
resources/views/admin/game_some_config/index.blade.php

@@ -326,8 +326,10 @@ function saveConfig() {
         return;
     }
     
-    // 构建只包含变动数据的表单
-    const formData = $.param(changedData);
+    // 构建只包含变动数据的表单(确保空值也会被提交)
+    const formData = Object.keys(changedData).map(function(key) {
+        return encodeURIComponent(key) + '=' + encodeURIComponent(changedData[key] ?? '');
+    }).join('&');
     
     layer.msg('正在保存 ' + Object.keys(changedData).length + ' 个变动...', {icon: 16, time: 0, shade: 0.3});
     

+ 15 - 2
resources/views/admin/recharge/list.blade.php

@@ -249,16 +249,21 @@
                                         <td>{{$v->created_at}}</td>
                                         <td>{{ dateConvert($v->created_at) }}</td>
                                         <td>
-                                            @if (in_array(session('admin')->roles[0]->id,[1,12])&&$v->pay_status != 1)
+                                            @if (in_array(session('admin')->roles[0]->id,[1,12]) && $v->pay_status != 1)
                                                 <button type="button" class="btn-sm btn-primary"
                                                         onclick="supplement({{$v->id}})">{{ __('auto.补单') }}
                                                 </button>
                                             @endif
-                                            @if (in_array(session('admin')->roles[0]->id,[1,12])&&$v->pay_status == 1)
+                                            @if (in_array(session('admin')->roles[0]->id,[1,12]) && $v->pay_status == 1)
                                                 <button type="button" class="btn-sm btn-warning"
                                                         onclick="setRefund({{$v->id}})">{{ __('auto.标记退款') }}
                                                 </button>
                                             @endif
+                                            @if (in_array(session('admin')->roles[0]->id,[1,12]) && $v->pay_status != 1)
+                                                <button type="button" class="btn-sm btn-info"
+                                                        onclick="mockFbReport({{$v->id}})">FB模拟上报
+                                                </button>
+                                            @endif
                                         </td>
                                         <td contentEditable="true"
                                             onblur="remarks(this,{{$v->id}})">{{$v->ar_remarks}}</td>
@@ -420,6 +425,14 @@
             });
         }
 
+        function mockFbReport(id) {
+            myConfirm("确认模拟上报 FB 事件吗?", function () {
+                myRequest("/admin/recharge/fb_report_mock/" + id, "post", {}, function (res) {
+                    layer.msg(res.msg)
+                });
+            });
+        }
+
 
         $(function () {
             cutStr(50);

+ 2 - 0
routes/web.php

@@ -234,6 +234,8 @@ Route::group([
         $route->post('/recharge/supplement/{id}', 'Admin\RechargeController@supplement');
         // 充值订单退款
         $route->post('/recharge/refund/{id}', 'Admin\RechargeController@refund');
+        // 模拟上报 FB(仅管理员)
+        $route->post('/recharge/fb_report_mock/{id}', 'Admin\RechargeController@fbReportMock');
         $route->get('/recharge/first_charge', 'Admin\RechargeController@firstCharge');
         $route->get('/recharge/first_charge_status', 'Admin\RechargeController@firstChargeStatus');
         $route->any('/recharge/poll', 'Admin\RechargeController@poll');