1, 'lose' => 0, 'draw' => 2, 'half' => 3]; /** 当前可下注的 PC28 期号与可用玩法。 */ public static function options(): array { [$type, $issue] = self::currentIssue(); $rules = GameplayRuleService::model()::query()->where('odds', '>', 0)->get(['keywords', 'odds', 'mininum', 'maxinum']); return ['lottery' => ['type' => $type, 'issue' => $issue ? $issue->issue_no : null, 'end_time' => $issue ? (string) $issue->end_time : null, 'rules' => $rules], 'sport' => ['results' => array_keys(self::SPORT_RESULTS)]]; } private static function currentIssue(): array { $switch = (int) (Config::where('field', 'pc28_switch')->value('val') ?? 0); $issue = $switch === 1 ? PcIssue::where('status', PcIssue::STATUS_BETTING)->orderByDesc('id')->first() : IssueService::model()::where('status', IssueService::model()::STATUS_BETTING)->orderByDesc('id')->first(); return [$switch === 1 ? 6 : 5, $issue]; } /** PC28 下注,校验与 BetService::singleNote 一致,但不发任何群消息或私聊。 */ public static function lottery($memberId, string $keywords, $amount): LhcOrder { SimulationSettings::assertEnabled(); $user = SimulationSettings::member($memberId); $amount = SimulationSettings::assertAmount($amount); $rule = GameplayRuleService::getGameplayRules($keywords); if (!$rule) throw new Exception('玩法未配置:' . $keywords, HttpStatus::CUSTOM_ERROR); if ($rule['odds'] <= 0) throw new Exception('该玩法赔率为 0,禁止投注', HttpStatus::CUSTOM_ERROR); [$type, $issue] = self::currentIssue(); if (!$issue) throw new Exception('暂无可下注期数', HttpStatus::CUSTOM_ERROR); if ((string) $issue->end_time < date('Y-m-d H:i:s', time() + 30)) throw new Exception('封盘中,本次下注无效', HttpStatus::CUSTOM_ERROR); if ($amount < $rule['mininum']) throw new Exception('低于该玩法最小金额 ' . $rule['mininum'], HttpStatus::CUSTOM_ERROR); if ($amount > $rule['maxinum']) throw new Exception('超过该玩法最大金额 ' . $rule['maxinum'], HttpStatus::CUSTOM_ERROR); return DB::transaction(function () use ($user, $keywords, $amount, $rule, $type, $issue) { $wallet = Wallet::where('member_id', $user->member_id)->lockForUpdate()->first(); if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR); $before = (string) $wallet->available_balance; if (bccomp($before, $amount, 10) < 0) throw new Exception('余额不足', HttpStatus::CUSTOM_ERROR); $ordernum = date('YmdHis') . str_pad((string) mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); LhcOrderItem::create(['ordernum' => $ordernum, 'member_id' => $user->member_id, 'number' => $keywords]); $order = LhcOrder::create(['type' => $type, 'bet_source' => LhcOrder::SOURCE_SIMULATION, 'issue' => $issue->issue_no, 'ordernum' => $ordernum, 'member_id' => $user->member_id, 'odds' => $rule['odds'], 'amount' => $amount, 'total_amount' => $amount]); $after = bcsub($before, $amount, 10); $wallet->available_balance = $after; $wallet->save(); BalanceLogService::addLog($user->member_id, bcsub('0', $amount, 2), $before, $after, '投注', $order->id, '模拟投注'); return $order; }); } /** 生成一张体育注单并扣款。 */ public static function sport($memberId, $amount, $odd, string $title = '', string $selection = ''): Order { SimulationSettings::assertEnabled(); $user = SimulationSettings::member($memberId); $amount = SimulationSettings::assertAmount($amount); if (!is_numeric($odd) || $odd <= 1 || $odd > 100) throw new Exception('赔率应大于 1 且不超过 100', HttpStatus::CUSTOM_ERROR); $title = $title !== '' ? $title : '模拟主队 VS 模拟客队'; $selection = $selection !== '' ? $selection : '主胜'; return DB::transaction(function () use ($user, $amount, $odd, $title, $selection) { $wallet = Wallet::where('member_id', $user->member_id)->lockForUpdate()->first(); if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR); $before = (string) $wallet->available_balance; if (bccomp($before, $amount, 10) < 0) throw new Exception('余额不足', HttpStatus::CUSTOM_ERROR); $serial = date('YmdHis') . str_pad((string) mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); $order = (new Order())->forceFill(['user_id' => (string) $user->user_id, 'order_id' => 'S' . $serial, 'issue' => self::SPORT_ISSUE_PREFIX . $serial, 'detail' => json_encode(['simulation' => true, 'title' => $title, 'selection' => $selection], JSON_UNESCAPED_UNICODE), 'amount' => $amount, 'cny_rate' => 1, 'is_faker' => 0, 'status' => 1, 'pay_status' => 1, 'pay_time' => time(), 'odd_name' => $title, 'odd_value' => $selection, 'odd' => $odd, 'is_roll' => 0, 'remark' => '模拟注单']); $order->save(); $after = bcsub($before, $amount, 10); $wallet->available_balance = $after; $wallet->save(); BalanceLogService::addLog($user->member_id, bcsub('0', $amount, 2), $before, $after, '体彩投注', $order->id, '模拟投注'); return $order; }); } /** 只允许操作模拟会员自己的、合成期号的体育注单。 */ private static function sportOrder(int $id): Order { $order = Order::where('id', $id)->lockForUpdate()->first(); if (!$order || !str_starts_with((string) $order->issue, self::SPORT_ISSUE_PREFIX) || !DB::table('users')->where('user_id', (string) $order->user_id)->where('user_type', MemberAudience::TYPE_SIMULATED)->exists()) { throw new Exception('不是模拟体育注单', HttpStatus::CUSTOM_ERROR); } if ((int) $order->settlement_status !== 0 || (int) $order->status !== 1) throw new Exception('注单已结算或已取消', HttpStatus::CUSTOM_ERROR); return $order; } /** 按给定结果结算:赢=本金×赔率,和局退本金,输半退一半,输不退。金额口径与前台结算一致。 */ public static function settleSport(int $id, string $result): Order { SimulationSettings::assertEnabled(); if (!isset(self::SPORT_RESULTS[$result])) throw new Exception('结算结果不正确', HttpStatus::CUSTOM_ERROR); return DB::transaction(function () use ($id, $result) { $order = self::sportOrder($id); $amount = (string) $order->amount; $win = '0'; $return = '0'; $profit = bcsub('0', $amount, 2); $changeType = null; if ($result === 'win') { $win = bcmul($amount, (string) $order->odd, 2); $profit = bcsub($win, $amount, 2); $changeType = '体彩中奖'; } if ($result === 'draw') { $return = $amount; $profit = '0'; $changeType = '体彩和局退款'; } if ($result === 'half') { $return = bcdiv($amount, '2', 2); $profit = bcsub($return, $amount, 2); $changeType = '体彩输半退款'; } $order->forceFill(['is_win' => self::SPORT_RESULTS[$result], 'win_amount' => $win, 'return_amount' => $return, 'profit_and_loss' => $profit, 'settlement_status' => 1, 'settlement_time' => time(), 'game_result' => $result])->save(); $credit = bcadd($win, $return, 2); if ($changeType !== null && bccomp($credit, '0', 2) > 0) self::credit((string) $order->user_id, $credit, $changeType, (int) $order->id); return $order; }); } public static function cancelSport(int $id, string $remark = ''): Order { SimulationSettings::assertEnabled(); return DB::transaction(function () use ($id, $remark) { $order = self::sportOrder($id); $order->forceFill(['status' => 2, 'return_amount' => $order->amount, 'profit_and_loss' => 0, 'settlement_status' => 1, 'settlement_time' => time(), 'remark' => $remark !== '' ? $remark : '模拟注单撤销'])->save(); self::credit((string) $order->user_id, (string) $order->amount, '体彩退款', (int) $order->id); return $order; }); } private static function credit(string $userId, string $amount, string $changeType, int $orderId): void { $memberId = DB::table('users')->where('user_id', $userId)->value('member_id'); $wallet = Wallet::where('member_id', $memberId)->lockForUpdate()->first(); if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR); $before = (string) $wallet->available_balance; $after = bcadd($before, $amount, 10); $wallet->available_balance = $after; $wallet->save(); BalanceLogService::addLog($memberId, $amount, $before, $after, $changeType, $orderId, '模拟结算'); } /** 模拟体育注单查询(合成期号没有赛事,不能复用正式列表的赛事关联)。 */ public static function sportQuery() { return DB::table('sport_order')->join('users', 'users.user_id', '=', 'sport_order.user_id') ->where('users.user_type', MemberAudience::TYPE_SIMULATED); } }