SimulationBetService.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <?php
  2. namespace App\Services\Simulation;
  3. use App\Constants\HttpStatus;
  4. use App\Models\Config;
  5. use App\Models\LhcOrder;
  6. use App\Models\LhcOrderItem;
  7. use App\Models\Order;
  8. use App\Models\PcIssue;
  9. use App\Models\Wallet;
  10. use App\Services\BalanceLogService;
  11. use App\Services\IssueService;
  12. use App\Services\LotteryOdds;
  13. use App\Support\MemberAudience;
  14. use Exception;
  15. use Illuminate\Support\Facades\DB;
  16. use Illuminate\Support\Facades\Schema;
  17. /**
  18. * 模拟会员的注单,写真实注单表并产生真实账变。
  19. * 彩票:全部彩种,下在真实期号上。PC28(type 5 / 6)随本项目开奖由 BetService::betSettled 结算;六合彩、极速彩等由前台项目的结算任务按真实开奖结算。
  20. * 体育:sport_order,issue 用不存在于赛事表的合成值,所以前台的结算任务取不到它,只能在这里按给定结果结算。
  21. */
  22. final class SimulationBetService
  23. {
  24. public const SPORT_ISSUE_PREFIX = 'SIM';
  25. public const SPORT_RESULTS = ['win' => 1, 'lose' => 0, 'draw' => 2, 'half' => 3];
  26. /** lhc_order.type => 彩种名(与前台一致,也是流水类型的前缀)。5、6 是 PC28,由本项目开奖结算;其余由前台项目的结算任务按真实开奖结算。 */
  27. public const LOTTERY_NAMES = [1 => '新澳门六合彩', 2 => '香港六合彩', 3 => '澳门六合彩', 4 => '极速六合彩', 5 => '加拿大28', 6 => '极速28',
  28. 7 => '极速赛车', 8 => '极速时时彩', 9 => '极速飞艇', 10 => 'SG时时彩', 11 => 'SG飞艇', 12 => '极速快3', 13 => 'SG快3'];
  29. /** 要一次选多个号码才成立的玩法,模拟只下单号码注单,所以不提供。 */
  30. private const MULTI_NUMBER_PLAYS = ['连', '合肖', '不中', '过关', '中一', '二中', '三中', '特串'];
  31. private static function lotteryType(string $key): int
  32. {
  33. [$game, $type] = array_pad(explode(':', $key, 2), 2, null);
  34. return match ($game) { 'canada28' => 5, 'jisu28' => 6, 'lhc' => (int) $type, 'jisu' => (int) $type + 6 };
  35. }
  36. /** 全部彩种:PC28 只列当前开关启用的那一个。 */
  37. public static function lotteryGames(): array
  38. {
  39. $pc28 = self::currentIssue()[1];
  40. $games = [];
  41. foreach (LotteryOdds::gameKeys() as $key) {
  42. if (in_array($key, ['canada28', 'jisu28'], true) && $key !== $pc28) continue;
  43. $games[$key] = self::LOTTERY_NAMES[self::lotteryType($key)];
  44. }
  45. return $games;
  46. }
  47. /**
  48. * 某个彩种当前可下注的期号;封盘、没有期号或彩种表不存在时返回 null。封盘规则与前台下注一致。
  49. * @return array{type:int,issue:string,close:?string}|null
  50. */
  51. public static function openIssue(string $key): ?array
  52. {
  53. [$game, $sub] = array_pad(explode(':', $key, 2), 2, null);
  54. $type = self::lotteryType($key);
  55. $time = fn ($value) => $value === null || $value === '' ? null : (is_numeric($value) ? (int) $value : strtotime((string) $value));
  56. if ($game === 'canada28' || $game === 'jisu28') {
  57. [, $current, $issue] = self::currentIssue();
  58. if ($current !== $key || !$issue || (string) $issue->end_time < date('Y-m-d H:i:s', time() + 30)) return null;
  59. return ['type' => $type, 'issue' => (string) $issue->issue_no, 'close' => (string) $issue->end_time];
  60. }
  61. if ($game === 'lhc') {
  62. if (!Schema::hasTable('lhc_lottery')) return null;
  63. // 新澳门、澳门六合彩共用新澳门的期号,下的是下一期;每天 21:30-21:40 封盘
  64. $official = in_array($type, [1, 3], true);
  65. $lottery = DB::table('lhc_lottery')->where('type', $official ? 1 : $type)->orderByDesc('issue')->first();
  66. if (!$lottery || (int) $lottery->status === 2) return null;
  67. if ($official) {
  68. if (date('H:i') >= '21:30' && date('H:i') <= '21:40') return null;
  69. return ['type' => $type, 'issue' => (string) ((int) $lottery->issue + 1), 'close' => null];
  70. }
  71. $next = $time($lottery->next_open_time);
  72. if ($next !== null && time() >= $next) return null;
  73. $issue = $type === 2 ? $lottery->next_issue : $lottery->issue;
  74. return $issue ? ['type' => $type, 'issue' => (string) $issue, 'close' => $next ? date('Y-m-d H:i:s', $next) : null] : null;
  75. }
  76. if (!Schema::hasTable('jisu_lottery')) return null;
  77. $lottery = DB::table('jisu_lottery')->where('type', (int) $sub)->orderByDesc('issue')->first();
  78. $open = $lottery ? $time($lottery->open_time) : null;
  79. $close = $open === null ? null : $open - (in_array((int) $sub, [1, 2, 3, 6], true) ? 15 : 30);
  80. if (!$lottery || (int) $lottery->status === 2 || $close === null || time() >= $close) return null;
  81. return ['type' => $type, 'issue' => (string) $lottery->issue, 'close' => date('Y-m-d H:i:s', $close)];
  82. }
  83. /** 某个彩种可下注的玩法:已配置赔率、赔率大于 0、单号码即可成注。PC28 的 id 为空,按 keywords 下注。 */
  84. public static function lotteryRules(string $key): array
  85. {
  86. $pc28 = in_array($key, ['canada28', 'jisu28'], true);
  87. try {
  88. $rows = DB::transaction(fn () => LotteryOdds::pricedCatalog($key, (int) LotteryOdds::template($key)->id));
  89. } catch (\Throwable $e) {
  90. return []; // 该彩种没有赔率模板或玩法表
  91. }
  92. $rules = [];
  93. foreach ($rows as $row) {
  94. if (!is_numeric($row->odds) || $row->odds <= 0) continue;
  95. $label = $pc28 ? (string) $row->keywords : trim(($row->game ?? '') . ' ' . ($row->gameplay ?? '') . ' ' . ($row->number ?? ''));
  96. if (!$pc28) foreach (self::MULTI_NUMBER_PLAYS as $word) if (str_contains($label, $word)) continue 2;
  97. $rules[] = ['id' => $pc28 ? null : (int) $row->id, 'keywords' => $label, 'odds' => $row->odds, 'mininum' => $row->mininum, 'maxinum' => $row->maxinum];
  98. }
  99. return $rules;
  100. }
  101. /** 下注表单用:各彩种及其当前期号;带 $game 时再给出该彩种的玩法。 */
  102. public static function options(?string $game = null): array
  103. {
  104. $games = [];
  105. foreach (self::lotteryGames() as $key => $name) {
  106. $open = self::openIssue($key);
  107. $games[] = ['key' => $key, 'name' => $name, 'type' => self::lotteryType($key), 'issue' => $open['issue'] ?? null, 'close' => $open['close'] ?? null, 'open' => $open !== null];
  108. }
  109. $game = $game !== null && isset(self::lotteryGames()[$game]) ? $game : null;
  110. return ['lottery' => ['games' => $games, 'game' => $game, 'rules' => $game ? self::lotteryRules($game) : []],
  111. 'sport' => ['results' => array_keys(self::SPORT_RESULTS)], 'third' => ['games' => self::THIRD_GAMES]];
  112. }
  113. private static function currentIssue(): array
  114. {
  115. $switch = (int) (Config::where('field', 'pc28_switch')->value('val') ?? 0);
  116. $issue = $switch === 1
  117. ? PcIssue::where('status', PcIssue::STATUS_BETTING)->orderByDesc('id')->first()
  118. : IssueService::model()::where('status', IssueService::model()::STATUS_BETTING)->orderByDesc('id')->first();
  119. return $switch === 1 ? [6, 'jisu28', $issue] : [5, 'canada28', $issue];
  120. }
  121. /**
  122. * 彩票下注,支持全部彩种,下在真实期号上,不发任何群消息或私聊。
  123. * PC28($game 为空或 canada28 / jisu28)按 $keywords 下注,随本项目开奖结算;
  124. * 其他彩种按玩法编号 $itemId 下注,注单格式与前台一致,由前台项目的结算任务按真实开奖结算。
  125. */
  126. public static function lottery($memberId, string $keywords, $amount, ?string $game = null, ?int $itemId = null): LhcOrder
  127. {
  128. SimulationSettings::assertEnabled();
  129. $user = SimulationSettings::member($memberId);
  130. $amount = SimulationSettings::assertAmount($amount);
  131. $key = $game ?: self::currentIssue()[1];
  132. if (!isset(self::lotteryGames()[$key])) throw new Exception('彩种不可用', HttpStatus::CUSTOM_ERROR);
  133. $pc28 = in_array($key, ['canada28', 'jisu28'], true);
  134. $open = self::openIssue($key);
  135. if (!$open) throw new Exception(self::LOTTERY_NAMES[self::lotteryType($key)] . ' 当前封盘或暂无可下注期数', HttpStatus::CUSTOM_ERROR);
  136. return DB::transaction(function () use ($user, $keywords, $amount, $key, $pc28, $open, $itemId) {
  137. $template = LotteryOdds::template($key);
  138. if ($pc28) {
  139. $rule = LotteryOdds::pc28Price($template, $keywords);
  140. $item = ['number' => $keywords];
  141. $fields = [];
  142. } else {
  143. $rule = LotteryOdds::pricedCatalog($key, (int) $template->id)->firstWhere('id', (int) $itemId);
  144. $item = ['number_id' => (int) $itemId, 'number' => (string) ($rule->number ?? '')];
  145. $fields = ['game' => (string) ($rule->game ?? ''), 'gameplay' => (string) ($rule->gameplay ?? '')];
  146. }
  147. if (!$rule) throw new Exception('玩法未配置:' . ($pc28 ? $keywords : $itemId), HttpStatus::CUSTOM_ERROR);
  148. if (!is_numeric($rule->odds) || $rule->odds <= 0) throw new Exception('该玩法赔率为 0,禁止投注', HttpStatus::CUSTOM_ERROR);
  149. if ($amount < $rule->mininum) throw new Exception('低于该玩法最小金额 ' . $rule->mininum, HttpStatus::CUSTOM_ERROR);
  150. if ($amount > $rule->maxinum) throw new Exception('超过该玩法最大金额 ' . $rule->maxinum, HttpStatus::CUSTOM_ERROR);
  151. $wallet = Wallet::where('member_id', $user->member_id)->lockForUpdate()->first();
  152. if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR);
  153. $before = (string) $wallet->available_balance;
  154. if (bccomp($before, $amount, 10) < 0) throw new Exception('余额不足', HttpStatus::CUSTOM_ERROR);
  155. $ordernum = date('YmdHis') . str_pad((string) mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
  156. LhcOrderItem::create(['ordernum' => $ordernum, 'member_id' => $user->member_id] + $item);
  157. $order = LhcOrder::create(['type' => $open['type'], 'bet_source' => LhcOrder::SOURCE_SIMULATION, 'issue' => $open['issue'], 'ordernum' => $ordernum,
  158. 'member_id' => $user->member_id, 'odds' => $rule->odds, 'amount' => $amount, 'total_amount' => $amount]
  159. + $fields + LotteryOdds::orderFields($template));
  160. $after = bcsub($before, $amount, 10);
  161. $wallet->available_balance = $after;
  162. $wallet->save();
  163. // 流水类型与真实下注一致:PC28 记“投注”,其他彩种记“彩种名 + 投注”
  164. $changeType = $pc28 ? '投注' : self::LOTTERY_NAMES[$open['type']] . '投注';
  165. BalanceLogService::addLog($user->member_id, bcsub('0', $amount, 2), $before, $after, $changeType, $order->id, '模拟投注');
  166. return $order;
  167. });
  168. }
  169. /** 随机挑一个当前可下注的彩种和玩法下一注;没有任何彩种可下时抛错。PC28 优先下大小单双。 */
  170. public static function lotteryRandom($memberId, $amount): LhcOrder
  171. {
  172. $keys = array_keys(self::lotteryGames());
  173. shuffle($keys);
  174. foreach ($keys as $key) {
  175. if (!self::openIssue($key)) continue;
  176. $rules = array_values(array_filter(self::lotteryRules($key), fn ($rule) => $amount >= $rule['mininum'] && $amount <= $rule['maxinum']));
  177. if (!$rules) continue;
  178. if (in_array($key, ['canada28', 'jisu28'], true)) {
  179. $basic = array_values(array_filter($rules, fn ($rule) => in_array($rule['keywords'], ['大', '小', '单', '双'], true)));
  180. $rules = $basic ?: $rules;
  181. }
  182. $rule = $rules[mt_rand(0, count($rules) - 1)];
  183. return self::lottery($memberId, (string) $rule['keywords'], $amount, $key, $rule['id']);
  184. }
  185. throw new Exception('当前没有可下注的彩种(都在封盘,或下注金额不在任何玩法的限额内)', HttpStatus::CUSTOM_ERROR);
  186. }
  187. /** 当前是否有任何彩种可下注(混合下注时用来决定要不要下彩票)。 */
  188. public static function lotteryOpen(): bool
  189. {
  190. foreach (array_keys(self::lotteryGames()) as $key) if (self::openIssue($key)) return true;
  191. return false;
  192. }
  193. /**
  194. * 非 PC28 彩种由前台项目结算,它不会给模拟会员登记报表用的结算时间;这里把已结算的模拟注单补登记(可重复执行)。
  195. * @return int 本次补登记的注单数
  196. */
  197. public static function sweepSettledLottery(): int
  198. {
  199. if (!Schema::hasTable('reporting_contexts')) return 0;
  200. $ids = DB::table('lhc_order')->where('bet_source', LhcOrder::SOURCE_SIMULATION)->whereNotIn('type', [5, 6])->where('lottery_status', '<>', 0)
  201. ->whereNotIn('id', DB::table('reporting_contexts')->where('source', 'lhc_order')->whereNotNull('succeeded_at')->selectRaw('CAST(source_id AS UNSIGNED)'))
  202. ->orderByDesc('id')->limit(200)->pluck('id');
  203. foreach ($ids as $id) \App\Services\Reporting\ReportContext::lotterySettled((int) $id);
  204. return count($ids);
  205. }
  206. /** 生成一张体育注单并扣款。 */
  207. public static function sport($memberId, $amount, $odd, string $title = '', string $selection = ''): Order
  208. {
  209. SimulationSettings::assertEnabled();
  210. $user = SimulationSettings::member($memberId);
  211. $amount = SimulationSettings::assertAmount($amount);
  212. if (!is_numeric($odd) || $odd <= 1 || $odd > 100) throw new Exception('赔率应大于 1 且不超过 100', HttpStatus::CUSTOM_ERROR);
  213. $title = $title !== '' ? $title : '模拟主队 VS 模拟客队';
  214. $selection = $selection !== '' ? $selection : '主胜';
  215. return DB::transaction(function () use ($user, $amount, $odd, $title, $selection) {
  216. $wallet = Wallet::where('member_id', $user->member_id)->lockForUpdate()->first();
  217. if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR);
  218. $before = (string) $wallet->available_balance;
  219. if (bccomp($before, $amount, 10) < 0) throw new Exception('余额不足', HttpStatus::CUSTOM_ERROR);
  220. $serial = date('YmdHis') . str_pad((string) mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
  221. $order = (new Order())->forceFill(['user_id' => (string) $user->user_id, 'order_id' => 'S' . $serial, 'issue' => self::SPORT_ISSUE_PREFIX . $serial,
  222. 'detail' => json_encode(['simulation' => true, 'title' => $title, 'selection' => $selection], JSON_UNESCAPED_UNICODE),
  223. 'amount' => $amount, 'cny_rate' => 1, 'is_faker' => 0, 'status' => 1, 'pay_status' => 1, 'pay_time' => time(),
  224. 'odd_name' => $title, 'odd_value' => $selection, 'odd' => $odd, 'is_roll' => 0, 'remark' => '模拟注单']);
  225. $order->save();
  226. $after = bcsub($before, $amount, 10);
  227. $wallet->available_balance = $after;
  228. $wallet->save();
  229. BalanceLogService::addLog($user->member_id, bcsub('0', $amount, 2), $before, $after, '体彩投注', $order->id, '模拟投注');
  230. return $order;
  231. });
  232. }
  233. public const THIRD_GAMES = [1 => '模拟视讯', 2 => '模拟老虎机', 3 => '模拟彩票', 4 => '模拟体育', 5 => '模拟电竞', 6 => '模拟捕猎', 7 => '模拟棋牌'];
  234. public const THIRD_PLATFORM = 'SIM';
  235. /**
  236. * 第三方游戏的模拟注单:只在本地生成一张已完成的注单,不向任何三方平台发请求。
  237. * 结果不指定时随机(约 45% 赢,赢 0.5~1.5 倍下注额;其余全输)。输赢净额以“资产转移”流水进出钱包。
  238. * @param string|null $result win|lose,null 为随机
  239. */
  240. public static function third($memberId, $amount, ?int $gameType = null, ?string $result = null): \App\Models\ThirdGameOrder
  241. {
  242. SimulationSettings::assertEnabled();
  243. $user = SimulationSettings::member($memberId);
  244. $amount = SimulationSettings::assertAmount($amount);
  245. $gameType = $gameType ?: mt_rand(1, 7);
  246. if (!isset(self::THIRD_GAMES[$gameType])) throw new Exception('游戏分类不正确', HttpStatus::CUSTOM_ERROR);
  247. if ($result !== null && !in_array($result, ['win', 'lose'], true)) throw new Exception('结果只能是赢或输', HttpStatus::CUSTOM_ERROR);
  248. $win = $result === null ? mt_rand(1, 100) <= 45 : $result === 'win';
  249. $profit = $win ? bcmul($amount, number_format(mt_rand(50, 150) / 100, 2, '.', ''), 2) : bcsub('0', $amount, 2);
  250. return DB::transaction(function () use ($user, $amount, $gameType, $profit) {
  251. $wallet = Wallet::where('member_id', $user->member_id)->lockForUpdate()->first();
  252. if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR);
  253. $before = (string) $wallet->available_balance;
  254. if (bccomp($before, $amount, 10) < 0) throw new Exception('余额不足', HttpStatus::CUSTOM_ERROR);
  255. $playerId = trim((string) config('third_game.player_prefix', 'u')) . str_pad((string) $user->id, 4, '0', STR_PAD_LEFT);
  256. $gameOrderId = 'SIM' . date('YmdHis') . str_pad((string) mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
  257. $now = now();
  258. $order = \App\Models\ThirdGameOrder::create(['order_key' => hash('sha256', self::THIRD_PLATFORM . '|' . $playerId . '|' . $gameOrderId),
  259. 'user_id' => $user->id, 'member_id' => (string) $user->member_id, 'username' => (string) ($user->username ?? ''),
  260. 'first_name' => (string) ($user->first_name ?? ''), 'player_id' => $playerId, 'platform' => self::THIRD_PLATFORM,
  261. 'currency' => (string) config('third_game.currency', 'CNY'), 'game_type' => $gameType, 'game_name' => self::THIRD_GAMES[$gameType],
  262. 'round_no' => $gameOrderId, 'table_no' => '', 'seat' => '', 'bet_amount' => $amount, 'valid_amount' => $amount, 'settled_amount' => $profit,
  263. 'bet_content' => '模拟注单', 'status' => 1, 'game_order_id' => $gameOrderId, 'bet_time' => $now, 'last_update_time' => $now]);
  264. $after = bcadd($before, $profit, 10);
  265. $wallet->available_balance = $after;
  266. $wallet->save();
  267. BalanceLogService::addLog($user->member_id, $profit, $before, $after, '资产转移', $order->id, '三方游戏模拟结算:' . self::THIRD_GAMES[$gameType]);
  268. return $order;
  269. });
  270. }
  271. /** 只允许操作模拟会员自己的、合成期号的体育注单。 */
  272. private static function sportOrder(int $id): Order
  273. {
  274. $order = Order::where('id', $id)->lockForUpdate()->first();
  275. if (!$order || !str_starts_with((string) $order->issue, self::SPORT_ISSUE_PREFIX)
  276. || !DB::table('users')->where('user_id', (string) $order->user_id)->where('user_type', MemberAudience::TYPE_SIMULATED)->exists()) {
  277. throw new Exception('不是模拟体育注单', HttpStatus::CUSTOM_ERROR);
  278. }
  279. if ((int) $order->settlement_status !== 0 || (int) $order->status !== 1) throw new Exception('注单已结算或已取消', HttpStatus::CUSTOM_ERROR);
  280. return $order;
  281. }
  282. /** 按给定结果结算:赢=本金×赔率,和局退本金,输半退一半,输不退。金额口径与前台结算一致。 */
  283. public static function settleSport(int $id, string $result): Order
  284. {
  285. SimulationSettings::assertEnabled();
  286. if (!isset(self::SPORT_RESULTS[$result])) throw new Exception('结算结果不正确', HttpStatus::CUSTOM_ERROR);
  287. return DB::transaction(function () use ($id, $result) {
  288. $order = self::sportOrder($id);
  289. $amount = (string) $order->amount;
  290. $win = '0'; $return = '0'; $profit = bcsub('0', $amount, 2); $changeType = null;
  291. if ($result === 'win') { $win = bcmul($amount, (string) $order->odd, 2); $profit = bcsub($win, $amount, 2); $changeType = '体彩中奖'; }
  292. if ($result === 'draw') { $return = $amount; $profit = '0'; $changeType = '体彩和局退款'; }
  293. if ($result === 'half') { $return = bcdiv($amount, '2', 2); $profit = bcsub($return, $amount, 2); $changeType = '体彩输半退款'; }
  294. $order->forceFill(['is_win' => self::SPORT_RESULTS[$result], 'win_amount' => $win, 'return_amount' => $return, 'profit_and_loss' => $profit,
  295. 'settlement_status' => 1, 'settlement_time' => time(), 'game_result' => $result])->save();
  296. $credit = bcadd($win, $return, 2);
  297. if ($changeType !== null && bccomp($credit, '0', 2) > 0) self::credit((string) $order->user_id, $credit, $changeType, (int) $order->id);
  298. return $order;
  299. });
  300. }
  301. public static function cancelSport(int $id, string $remark = ''): Order
  302. {
  303. SimulationSettings::assertEnabled();
  304. return DB::transaction(function () use ($id, $remark) {
  305. $order = self::sportOrder($id);
  306. $order->forceFill(['status' => 2, 'return_amount' => $order->amount, 'profit_and_loss' => 0, 'settlement_status' => 1,
  307. 'settlement_time' => time(), 'remark' => $remark !== '' ? $remark : '模拟注单撤销'])->save();
  308. self::credit((string) $order->user_id, (string) $order->amount, '体彩退款', (int) $order->id);
  309. return $order;
  310. });
  311. }
  312. private static function credit(string $userId, string $amount, string $changeType, int $orderId): void
  313. {
  314. $memberId = DB::table('users')->where('user_id', $userId)->value('member_id');
  315. $wallet = Wallet::where('member_id', $memberId)->lockForUpdate()->first();
  316. if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR);
  317. $before = (string) $wallet->available_balance;
  318. $after = bcadd($before, $amount, 10);
  319. $wallet->available_balance = $after;
  320. $wallet->save();
  321. BalanceLogService::addLog($memberId, $amount, $before, $after, $changeType, $orderId, '模拟结算');
  322. }
  323. /** 模拟体育注单查询(合成期号没有赛事,不能复用正式列表的赛事关联)。 */
  324. public static function sportQuery()
  325. {
  326. return DB::table('sport_order')->join('users', 'users.user_id', '=', 'sport_order.user_id')
  327. ->where('users.user_type', MemberAudience::TYPE_SIMULATED);
  328. }
  329. }