| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- <?php
- namespace App\Services\Simulation;
- use App\Constants\HttpStatus;
- use App\Models\Config;
- use App\Models\LhcOrder;
- use App\Models\LhcOrderItem;
- use App\Models\Order;
- use App\Models\PcIssue;
- use App\Models\Wallet;
- use App\Services\BalanceLogService;
- use App\Services\GameplayRuleService;
- use App\Services\IssueService;
- use App\Support\MemberAudience;
- use Exception;
- use Illuminate\Support\Facades\DB;
- /**
- * 模拟会员的注单,写真实注单表并产生真实账变。
- * 彩票:PC28(加拿大 type 5 / 极速 type 6,跟随当前 pc28_switch),下在真实期号上,随真实开奖由 BetService::betSettled 结算。
- * 体育:sport_order,issue 用不存在于赛事表的合成值,所以前台的结算任务取不到它,只能在这里按给定结果结算。
- */
- final class SimulationBetService
- {
- public const SPORT_ISSUE_PREFIX = 'SIM';
- public const SPORT_RESULTS = ['win' => 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);
- }
- }
|