IssueService.php 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457
  1. <?php
  2. namespace App\Services;
  3. use App\Models\Cao;
  4. use App\Models\CaoHistory;
  5. use App\Models\PcIssue;
  6. use App\Models\Prediction;
  7. use App\Services\BaseService;
  8. use App\Models\Issue;
  9. use App\Models\Config;
  10. use Illuminate\Support\Facades\DB;
  11. use Illuminate\Support\Collection;
  12. use Illuminate\Support\Facades\Cache;
  13. use Illuminate\Support\Facades\Http;
  14. use Illuminate\Support\Facades\Log;
  15. use App\Services\GameplayRuleService;
  16. use App\Constants\GameplayRuleEnum;
  17. use App\Http\Controllers\admin\Lottery;
  18. use App\Services\KeyboardService;
  19. use App\Services\LotteryImageService;
  20. use Telegram\Bot\FileUpload\InputFile;
  21. use App\Jobs\SendTelegramMessageJob;
  22. use App\Jobs\SendTelegramGroupMessageJob;
  23. /**
  24. * 投注
  25. */
  26. class IssueService extends BaseService
  27. {
  28. const COUNTDOWN_TO_CLOSING_THE_MARKET = 60;//提前xx秒封盘
  29. const PLAYNOW_KENO_URL = 'https://www.playnow.com/services2/keno/nextdraw';
  30. const PLAYNOW_SOURCE_TIMEZONE = 'America/Vancouver';
  31. const BEIJING_TIMEZONE = 'Asia/Shanghai';
  32. const PLAYNOW_DRAW_INTERVAL_SECONDS = 210;
  33. public static function init($telegram, $data, $chatId, $firstName, $messageId): void
  34. {
  35. switch ($data) {
  36. default:
  37. //查看开奖历史图片
  38. $pattern = "/^showLotteryHistory@@\d+$/";
  39. if (preg_match($pattern, $data)) {
  40. $id = preg_replace('/^showLotteryHistory@@/', '', $data);
  41. IssueService::sendLotteryImage($chatId, $id);
  42. }
  43. break;
  44. }
  45. }
  46. /**
  47. * @description: 模型
  48. * @return {string}
  49. */
  50. public static function model(): string
  51. {
  52. return Issue::class;
  53. }
  54. /**
  55. * @description: 枚举
  56. * @return {*}
  57. */
  58. public static function enum(): string
  59. {
  60. return '';
  61. }
  62. /**
  63. * @description: 获取查询条件
  64. * @param {array} $search 查询内容
  65. * @return {array}
  66. */
  67. public static function getWhere(array $search = []): array
  68. {
  69. $where = [];
  70. if (isset($search['issue_no']) && !empty($search['issue_no'])) {
  71. $where[] = ['issue_no', '=', $search['issue_no']];
  72. }
  73. if (isset($search['id']) && !empty($search['id'])) {
  74. $where[] = ['id', '=', $search['id']];
  75. }
  76. if (isset($search['status']) && $search['status'] != '') {
  77. $where[] = ['status', '=', $search['status']];
  78. }
  79. if (isset($search['abnormal']) && !empty($search['abnormal'])) {
  80. $where[] = ['end_time', '<', date('Y-m-d H:i:s', time() - 1800)];
  81. $where[] = ['status', '!=', self::model()::STATUS_DRAW];
  82. }
  83. if (!empty($search['start_time'])) {
  84. $where[] = ['end_time', '>=', $search['start_time'].' 00:00:00'];
  85. }
  86. if (!empty($search['end_time'])) {
  87. $where[] = ['end_time', '<=', $search['end_time'].' 23:59:59'];
  88. }
  89. return $where;
  90. }
  91. /**
  92. * @description: 查询单条数据
  93. * @param array $search
  94. * @return \App\Models\Coin|null
  95. */
  96. public static function findOne(array $search): ?Issue
  97. {
  98. return self::model()::where(self::getWhere($search))->first();
  99. }
  100. /**
  101. * @description: 查询所有数据
  102. * @param array $search
  103. * @return \Illuminate\Database\Eloquent\Collection
  104. */
  105. public static function findAll(array $search = [])
  106. {
  107. return self::model()::where(self::getWhere($search))->get();
  108. }
  109. /**
  110. * @description: 分页查询
  111. * @param array $search
  112. * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
  113. */
  114. public static function paginate(array $search = [])
  115. {
  116. $limit = isset($search['limit']) ? $search['limit'] : 15;
  117. $paginator = self::model()::where(self::getWhere($search))->orderBy('issue_no', 'desc')->paginate($limit);
  118. return ['total' => $paginator->total(), 'data' => $paginator->items()];
  119. }
  120. /**
  121. * @description:
  122. * @param {*} $params
  123. * @return {*}
  124. */
  125. public static function submit($params = [])
  126. {
  127. $result = false;
  128. $msg['code'] = self::NOT;
  129. $msg['msg'] = '';
  130. // 2. 判断是否是更新
  131. if (!empty($params['id'])) {
  132. // 更新
  133. $info = self::findOne(['id' => $params['id']]);
  134. if (!$info) {
  135. $msg['msg'] = '期号不存在!';
  136. } else {
  137. $result = $info->update($params);
  138. $id = $params['id'];
  139. }
  140. } else {
  141. // 创建
  142. $result = $info = self::model()::create($params);
  143. $id = $result->id;
  144. }
  145. if ($result) {
  146. $msg['code'] = self::YES;
  147. $msg['msg'] = '设置成功';
  148. $msg['key'] = $id;
  149. } else {
  150. $msg['msg'] = empty($msg['msg']) ? '操作失败' : $msg['msg'];
  151. }
  152. return $msg;
  153. }
  154. /**
  155. * @description: 开始下注
  156. * @param {*} $id
  157. * @return {*}
  158. */
  159. public static function betting($id)
  160. {
  161. $info = self::findOne(['id' => $id]);
  162. if (!$info) {
  163. return ['code' => self::NOT, 'msg' => '期号不存在'];
  164. }
  165. if (!in_array($info->status, [self::model()::STATUS_DRAFT, self::model()::STATUS_BETTING])) {
  166. return ['code' => self::NOT, 'msg' => '期号状态不正确'];
  167. }
  168. $info->status = self::model()::STATUS_BETTING;
  169. $info->save();
  170. $pc28Switch = Config::where('field', 'pc28_switch')->first()->val;
  171. $wanFaGuiZeTime = Cache::get("玩法规则推送时间", 0);
  172. $now = time();
  173. $wanFaGuiZeTime = intval($wanFaGuiZeTime);
  174. if ($now - $wanFaGuiZeTime > (60 * 15)) {
  175. $replyInfo = KeyboardService::findOne(['button' => '玩法规则']);
  176. if ($replyInfo) {
  177. $text = $replyInfo->reply;
  178. $buttons = json_decode($replyInfo->buttons, true);
  179. $image = $replyInfo->image;
  180. if ($image) {
  181. $image = url($image);
  182. }
  183. if (empty($buttons)) {
  184. $buttons = self::getOperateButton();
  185. }
  186. Cache::put('玩法规则推送时间', time());
  187. if ($pc28Switch == 0) self::asyncBettingGroupNotice($text, $buttons, $image);
  188. }
  189. }
  190. $replyInfo = KeyboardService::findOne(['button' => '开始下注']);
  191. if ($replyInfo) {
  192. $text = $replyInfo->reply;
  193. $buttons = json_decode($replyInfo->buttons, true);
  194. $image = $replyInfo->image;
  195. if ($image) {
  196. $image = url($image);
  197. }
  198. if ($pc28Switch == 0) self::asyncBettingGroupNotice($text, $buttons, $image);
  199. }
  200. return ['code' => self::YES, 'msg' => '开始下注'];
  201. }
  202. /**
  203. * @description: 封盘
  204. * @param {*} $id
  205. * @return {*}
  206. */
  207. public static function closeBetting($id)
  208. {
  209. $info = self::findOne(['id' => $id]);
  210. if (!$info) {
  211. return ['code' => self::NOT, 'msg' => '期号不存在'];
  212. }
  213. if ($info->status != self::model()::STATUS_BETTING) {
  214. return ['code' => self::NOT, 'msg' => '期号状态不正确'];
  215. }
  216. $pc28Switch = Config::where('field', 'pc28_switch')->first()->val;
  217. $info->status = self::model()::STATUS_CLOSE;
  218. $info->save();
  219. if ($pc28Switch == 0) {
  220. $replyInfo = KeyboardService::findOne(['button' => '停止下注']);
  221. if ($replyInfo) {
  222. $text = $replyInfo->reply;
  223. $buttons = json_decode($replyInfo->buttons, true);
  224. $image = $replyInfo->image;
  225. if ($image) {
  226. $image = url($image);
  227. }
  228. //停止下注的信息不发了
  229. // self::asyncBettingGroupNotice($text, $buttons, $image);
  230. }
  231. // 投注情况通知 xxxx期投注统计
  232. BetService::statNotice($info->issue_no);
  233. $replyInfo = KeyboardService::findOne(['button' => '封盘开奖']);
  234. if ($replyInfo) {
  235. $text = $replyInfo->reply;
  236. $buttons = json_decode($replyInfo->buttons, true);
  237. $image = $replyInfo->image;
  238. if ($image) {
  239. $image = url($image);
  240. }
  241. // self::bettingGroupNotice($text, $buttons, $image);
  242. self::asyncBettingGroupNotice($text, $buttons, $image);
  243. }
  244. }
  245. return ['code' => self::YES, 'msg' => '封盘成功'];
  246. }
  247. /**
  248. * @description: 开奖失败
  249. * @param {*} $id
  250. * @return {*}
  251. */
  252. public static function lotteryDrawFail($id)
  253. {
  254. $result = false;
  255. $msg['code'] = self::NOT;
  256. $msg['msg'] = '';
  257. DB::beginTransaction();
  258. try {
  259. // 更新
  260. $info = self::findOne(['id' => $id]);
  261. if (!$info) {
  262. $msg['msg'] = '期号不存在!';
  263. } else {
  264. $params['status'] = self::model()::STATUS_FAIL;
  265. $result = $info->update($params);
  266. BetService::betFail($info->issue_no);
  267. }
  268. DB::commit();
  269. return ['code' => self::YES, 'msg' => '投注已退回'];
  270. } catch (\Exception $e) {
  271. DB::rollBack();
  272. return ['code' => self::NOT, 'msg' => '投注退回失败'];
  273. }
  274. if ($result) {
  275. $msg['code'] = self::YES;
  276. $msg['msg'] = '设置成功';
  277. } else {
  278. $msg['msg'] = empty($msg['msg']) ? '操作失败' : $msg['msg'];
  279. }
  280. return $msg;
  281. }
  282. /**
  283. * @description: 开奖
  284. * @param {*} $id
  285. * @param {*} $winning_numbers 开奖号码
  286. * @param {*} $combo 开奖组合
  287. * @param {*} $recordImage 开奖图片
  288. * @return {*}
  289. */
  290. public static function lotteryDraw($id, $winning_numbers, $combo, $recordImage, $keno = null)
  291. {
  292. $info = self::findOne(['id' => $id]);
  293. if (!$info) {
  294. return ['code' => self::NOT, 'msg' => '期号不存在'];
  295. }
  296. if ($info->status == self::model()::STATUS_DRAW) {
  297. return ['code' => self::NOT, 'msg' => '期号状态不正确'];
  298. }
  299. $winArr = array_map('intval', explode(',', $winning_numbers));
  300. // 计算中奖
  301. $awards = self::award(explode(',', $winning_numbers));
  302. $kenoJson = self::encodePlayNowKeno($keno);
  303. DB::beginTransaction();
  304. try {
  305. $updatePayload = [
  306. 'status' => self::model()::STATUS_DRAW,
  307. 'winning_numbers' => $winning_numbers,
  308. 'combo' => $combo,
  309. ];
  310. if ($kenoJson) {
  311. $updatePayload['keno'] = $kenoJson;
  312. }
  313. self::model()::where('id', $info->id)->update($updatePayload);
  314. $info->status = $updatePayload['status'];
  315. $info->winning_numbers = $updatePayload['winning_numbers'];
  316. $info->combo = $updatePayload['combo'];
  317. if (isset($updatePayload['keno'])) {
  318. $info->keno = $updatePayload['keno'];
  319. }
  320. Log::channel('issue')->info('官方期开奖写入源头号码', [
  321. 'issue_no' => $info->issue_no,
  322. 'keno_saved' => $kenoJson ? 'yes' : 'no',
  323. 'keno_preview' => $kenoJson ? substr($kenoJson, 0, 80) : '',
  324. ]);
  325. $size = in_array("大", $awards);
  326. $size = $size ? "大" : "小";
  327. $oddOrEven = in_array("双", $awards);
  328. $oddOrEven = $oddOrEven ? "双" : "单";
  329. Prediction::result($info->issue_no, $size, $oddOrEven, $info->winning_numbers);
  330. Cao::updateData($awards);
  331. CaoHistory::updateData($awards);
  332. $pc28Switch = Config::where('field', 'pc28_switch')->first()->val;
  333. $replyInfo = KeyboardService::findOne(['button' => '本期开奖']);
  334. if ($replyInfo) {
  335. $text = $replyInfo->reply;
  336. $text .= "\n";
  337. $text .= $info->issue_no . ": " . implode('+', explode(',', $winning_numbers)) . "=" . array_sum($winArr) . " " . $combo;
  338. $buttons = json_decode($replyInfo->buttons, true);
  339. $image = $replyInfo->image;
  340. if ($image) {
  341. $image = url($image);
  342. }
  343. if (empty($buttons)) {
  344. $serviceAccount = Config::where('field', 'service_account')->first()->val;
  345. $buttons[] = [['text' => lang('✅唯一财务'), 'callback_data' => "", 'url' => "https://t.me/{$serviceAccount}"]];
  346. }
  347. // self::bettingGroupNotice($text, $buttons, $image, true);
  348. if ($pc28Switch == 0) SendTelegramGroupMessageJob::dispatch($text, $buttons, $image, true, "\n", [
  349. 'message_type' => 'draw_result',
  350. 'issue_no' => $info->issue_no,
  351. ])->afterCommit();
  352. }
  353. $recordImage = self::lotteryImage($info->issue_no);
  354. if ($recordImage) {
  355. // self::bettingGroupNotice('', [], url($recordImage));
  356. if ($pc28Switch == 0) SendTelegramGroupMessageJob::dispatch('', [], url($recordImage), false, "\n", [
  357. 'message_type' => 'draw_history_image',
  358. 'issue_no' => $info->issue_no,
  359. ])->afterCommit();
  360. }
  361. $info->image = $recordImage;
  362. $info->save();
  363. BetService::betSettled($info->issue_no, $awards);
  364. DB::commit();
  365. } catch (\Exception $e) {
  366. DB::rollBack();
  367. $message = "开奖失败:\n{$info->issue_no}\n";
  368. $message .= $e->getFile() . ':' . $e->getLine();
  369. $message .= "\n";
  370. $message .= $e->getMessage() . $winning_numbers;
  371. Log::error($message);
  372. return ['code' => self::NOT, 'msg' => '开奖失败', 'error' => $e->getMessage()];
  373. }
  374. return ['code' => self::YES, 'msg' => '开奖成功'];
  375. }
  376. // 虚拟开奖
  377. public static function fakeLotteryDraw($issue_no, $awards)
  378. {
  379. $fake_bet_list = Cache::get('fake_bet_' . $issue_no, []);
  380. $text = "";
  381. foreach ($fake_bet_list as $k => $v) {
  382. $lastStr = self::getLastChar($v['first_name'], 1);
  383. if (in_array($v['keywords'], $awards)) {
  384. $amount = (float)$v['amount'];
  385. $odds = (float)$v['odds'];
  386. $profit = $amount * $odds;
  387. if ($profit > 880000) {
  388. $profit = 880000; // 单注最高奖金880000
  389. }
  390. $item['profit'] = $profit;
  391. $yl = $profit - $amount;
  392. if ($k + 1 <= 30) {
  393. $text .= "私聊下注 【******" . $lastStr . "】 {$yl}\n";
  394. }
  395. } else {
  396. if ($k + 1 <= 30) {
  397. $text .= "私聊下注 【******" . $lastStr . "】 -{$v['amount']}\n";
  398. }
  399. }
  400. }
  401. return $text;
  402. }
  403. /**
  404. * @description: 获取中奖的奖项
  405. * @param {*} $winning_numbers
  406. * @return {*}
  407. */
  408. public static function award($winning_numbers)
  409. {
  410. $result = [];
  411. // 组合
  412. $sum = array_sum($winning_numbers);
  413. $section = self::getSection($sum); // 总和段位
  414. $result[] = $section;
  415. $sumOddEven = self::calculateOddEven($sum); // 总和单双
  416. $result[] = $sumOddEven;
  417. $sumSize = self::calculateSumSize($sum); // 总和大小
  418. $result[] = $sumSize;
  419. $sumExtremeSize = self::calculateSumExtremeSize($sum); // 总和极值
  420. if ($sumExtremeSize) {
  421. $result[] = $sumExtremeSize;
  422. }
  423. $sumCao = $sum . '操'; // 总和数字
  424. $result[] = $sumCao;
  425. $sumCombo = $sumSize . $sumOddEven; // 总和大小单双组合
  426. $result[] = $sumCombo;
  427. $sumBaoZi = self::isBaoZi($winning_numbers[0], $winning_numbers[1], $winning_numbers[2]); // 豹子
  428. if ($sumBaoZi) {
  429. $result[] = $sumBaoZi;
  430. }
  431. $sumPair = self::isPair($winning_numbers[0], $winning_numbers[1], $winning_numbers[2]); // 对子
  432. if ($sumPair) {
  433. $result[] = $sumPair;
  434. }
  435. $sumStraight = self::isStraight($winning_numbers[0], $winning_numbers[1], $winning_numbers[2]); // 顺子
  436. if ($sumStraight) {
  437. $result[] = $sumStraight;
  438. }
  439. $tail = self::getLastDigit($sum); // 总和尾数
  440. if (!in_array($tail, [0, 9])) {
  441. $result[] = $tail . '尾'; // 尾数
  442. $tailOddEven = self::calculateOddEven($tail); // 尾数单双
  443. $result[] = '尾' . $tailOddEven;
  444. $tailSize = self::calculateOneSize($tail); // 尾数大小
  445. $result[] = '尾' . $tailSize;
  446. $tailCombo = '尾' . $tailSize . $tailOddEven; // 尾数大小单双组合
  447. $result[] = $tailCombo;
  448. } else {
  449. $result[] = $tail . '尾'; // 尾数
  450. }
  451. $numA = $winning_numbers[0]; // A球
  452. $result[] = $numA . 'A';
  453. $numAOddEven = self::calculateOddEven($numA); // A球单双
  454. $result[] = 'A' . $numAOddEven;
  455. $numASize = self::calculateOneSize($numA); // A球大小
  456. $result[] = 'A' . $numASize;
  457. $result[] = 'A' . $numASize . $numAOddEven; // A球大小单双组合
  458. $numB = $winning_numbers[1]; // B球
  459. $result[] = $numB . 'B';
  460. $numBOddEven = self::calculateOddEven($numB); // B球
  461. $result[] = 'B' . $numBOddEven;
  462. $numBSize = self::calculateOneSize($numB); // B球大小
  463. $result[] = 'B' . $numBSize;
  464. $result[] = 'B' . $numBSize . $numBOddEven; // B球大小单双组合
  465. $numC = $winning_numbers[2];
  466. $result[] = $numC . 'C';
  467. $numCOddEven = self::calculateOddEven($numC); // C球单双
  468. $result[] = 'C' . $numCOddEven;
  469. $numCSize = self::calculateOneSize($numC); // C球大小
  470. $result[] = 'C' . $numCSize;
  471. $result[] = 'C' . $numCSize . $numCOddEven; // C球大小单双组合
  472. return $result;
  473. }
  474. /**
  475. * @description: 算单双
  476. * @param {*} $number
  477. * @return {*}
  478. */
  479. public static function calculateOddEven($number)
  480. {
  481. if ($number & 1) {
  482. return GameplayRuleEnum::SINGLE;
  483. } else {
  484. return GameplayRuleEnum::DOUBLE;
  485. }
  486. }
  487. /**
  488. * @description: 总和大小
  489. * @param {*} $number
  490. * @return {*}
  491. */
  492. public static function calculateSumSize($number)
  493. {
  494. if ($number >= GameplayRuleEnum::SUM_BIG) {
  495. return GameplayRuleEnum::BIG;
  496. }
  497. if ($number <= GameplayRuleEnum::SUM_SMALL) {
  498. return GameplayRuleEnum::SMALL;
  499. }
  500. }
  501. /**
  502. * @description: 总和极值
  503. * @param {*} $number
  504. * @return {*}
  505. */
  506. public static function calculateSumExtremeSize($number)
  507. {
  508. $result = '';
  509. if ($number >= GameplayRuleEnum::SUM_EXTREME_BIG) {
  510. $result = GameplayRuleEnum::EXTREME_BIG;
  511. }
  512. if ($number <= GameplayRuleEnum::SUM_EXTREME_SMALL) {
  513. $result = GameplayRuleEnum::EXTREME_SMALL;
  514. }
  515. return $result;
  516. }
  517. /**
  518. * @description: 豹子
  519. * @param {int} $a
  520. * @param {int} $b
  521. * @param {int} $c
  522. * @return {*}
  523. */
  524. public static function isBaoZi(int $a, int $b, int $c)
  525. {
  526. $result = '';
  527. if ($a === $b && $b === $c) {
  528. $result = GameplayRuleEnum::BAO_ZI;
  529. }
  530. return $result;
  531. }
  532. /**
  533. * @description: 对子
  534. * @param {int} $a
  535. * @param {int} $b
  536. * @param {int} $c
  537. * @return {*}
  538. */
  539. public static function isPair($a, $b, $c)
  540. {
  541. $result = '';
  542. // 确保输入都是个位数
  543. if (!is_numeric($a) || !is_numeric($b) || !is_numeric($c) ||
  544. $a < 0 || $a > 9 || $b < 0 || $b > 9 || $c < 0 || $c > 9) {
  545. return ''; // 或者抛出异常
  546. }
  547. if (($a == $b && $a != $c) ||
  548. ($a == $c && $a != $b) ||
  549. ($b == $c && $b != $a)) {
  550. $result = GameplayRuleEnum::PAIRS;
  551. }
  552. // 判断是否为对子情况
  553. return $result;
  554. }
  555. /**
  556. * @description: 顺子
  557. * @param {int} $a
  558. * @param {int} $b
  559. * @param {int} $c
  560. * @return {*}
  561. */
  562. public static function isStraight($a, $b, $c)
  563. {
  564. $result = '';
  565. // 确保输入都是个位数(0-9)
  566. if (!is_numeric($a) || !is_numeric($b) || !is_numeric($c) ||
  567. $a < 0 || $a > 9 || $b < 0 || $b > 9 || $c < 0 || $c > 9) {
  568. return '';
  569. }
  570. // 去重(顺子必须三个不同数字)
  571. if ($a == $b || $a == $c || $b == $c) {
  572. return '';
  573. }
  574. // 检查是否是完全升序或完全降序的连续数字
  575. $numbers = [$a, $b, $c];
  576. sort($numbers); // 排序后检查是否是 x, x+1, x+2
  577. list($x, $y, $z) = $numbers;
  578. // 情况1:升序连续(1,2,3)
  579. $isAscending = ($x + 1 == $y) && ($y + 1 == $z);
  580. // 情况2:降序连续(3,2,1)
  581. $isDescending = ($z + 1 == $y) && ($y + 1 == $x);
  582. if ($isAscending || $isDescending) {
  583. $result = GameplayRuleEnum::STRAIGHT;
  584. }
  585. return $result;
  586. }
  587. /**
  588. * 获取数字的尾数
  589. * @param int $number 输入数字
  590. * @return int 尾数
  591. */
  592. public static function getLastDigit($number)
  593. {
  594. // 确保输入是整数
  595. $number = (int)$number;
  596. // 取绝对值,处理负数情况
  597. $number = abs($number);
  598. // 取模10得到尾数
  599. return $number % 10;
  600. }
  601. /**
  602. * @description: 尾大小
  603. * @param {*} $number
  604. * @return {*}
  605. */
  606. public static function calculateOneSize($number)
  607. {
  608. if ($number >= GameplayRuleEnum::ONE_BIG) {
  609. return GameplayRuleEnum::BIG;
  610. }
  611. if ($number <= GameplayRuleEnum::ONE_SMALL) {
  612. return GameplayRuleEnum::SMALL;
  613. }
  614. }
  615. /**
  616. * @description: 获取段位
  617. * @param {*} $number
  618. * @return {*}
  619. */
  620. public static function getSection($number)
  621. {
  622. $result = '';
  623. if ($number >= GameplayRuleEnum::SECTION_1[0] && $number <= GameplayRuleEnum::SECTION_1[1]) {
  624. $result = GameplayRuleEnum::ONE;
  625. } elseif ($number >= GameplayRuleEnum::SECTION_2[0] && $number <= GameplayRuleEnum::SECTION_2[1]) {
  626. $result = GameplayRuleEnum::TWO;
  627. } elseif ($number >= GameplayRuleEnum::SECTION_3[0] && $number <= GameplayRuleEnum::SECTION_3[1]) {
  628. $result = GameplayRuleEnum::THREE;
  629. } elseif ($number >= GameplayRuleEnum::SECTION_4[0] && $number <= GameplayRuleEnum::SECTION_4[1]) {
  630. $result = GameplayRuleEnum::FOUR;
  631. }
  632. return $result; // 不在任何段中
  633. }
  634. /**
  635. * @description: 近期开奖记录
  636. * @return {*}
  637. */
  638. public static function currentLotteryResults($memberId)
  639. {
  640. // $result = self::model()::where('status', self::model()::STATUS_DRAW)->orderBy('id','desc')->take(16)->get();
  641. // $text = "📅 近期开奖记录\n";
  642. // $text .= "====================\n";
  643. // if($result){
  644. // foreach($result as $k => $v){
  645. // $winArr = explode(',',$v->winning_numbers);
  646. // // 组合
  647. // $sum = array_sum($winArr);
  648. // $combo = [];
  649. // $sumOddEven = self::calculateOddEven($sum); // 总和单双
  650. // $sumSize = self::calculateSumSize($sum); // 总和大小
  651. // $sumExtremeSize = self::calculateSumExtremeSize($sum); // 总和极值
  652. // if(empty($sumExtremeSize)){
  653. // $sumExtremeSize = "-";
  654. // }
  655. // $tail = self::getLastDigit($sum); // 总和尾数
  656. // if($tail == 0){
  657. // $tail = '-'; // 尾数
  658. // }else{
  659. // $tail = '尾'.$tail; // 尾数
  660. // }
  661. // $text .= "回合:{$v->issue_no}期 \n";
  662. // $text .= "结果:".implode('+',explode(',',$v->winning_numbers))."=".array_sum(explode(',',$v->winning_numbers))." \n";
  663. // $text .= "组合:{$sumSize} {$sumOddEven} \n";
  664. // $text .= "极值:{$sumExtremeSize} \n";
  665. // $text .= "尾数:{$tail} \n";
  666. // $text .= "---------------------------\n";
  667. // }
  668. // self::telegram()->sendMessage([
  669. // 'chat_id' => $memberId,
  670. // 'text' => $text,
  671. // ]);
  672. // }else{
  673. // self::telegram()->sendMessage([
  674. // 'chat_id' => $memberId,
  675. // 'text' => "暂无开奖记录",
  676. // ]);
  677. // }
  678. $result = self::model()::where('status', self::model()::STATUS_DRAW)->orderBy('id', 'desc')->first();
  679. if ($result) {
  680. if ($result->image) {
  681. // self::telegram()->sendPhoto([
  682. // 'chat_id' => $memberId,
  683. // 'photo' => InputFile::create(url($result->image)),
  684. // ]);
  685. return [
  686. 'chat_id' => $memberId,
  687. 'photo' => InputFile::create(url($result->image)),
  688. ];
  689. } else {
  690. // if($result->combo){
  691. // self::telegram()->sendMessage([
  692. // 'chat_id' => $memberId,
  693. // 'text' => "",
  694. // ]);
  695. // }else{
  696. // self::telegram()->sendMessage([
  697. // 'chat_id' => $memberId,
  698. // 'text' => lang("暂无开奖记录"),
  699. // ]);
  700. // }
  701. return
  702. [
  703. 'chat_id' => $memberId,
  704. 'text' => lang("暂无开奖记录"),
  705. ];
  706. }
  707. }
  708. }
  709. public static function getCombo($winArr)
  710. {
  711. // 组合
  712. $sum = array_sum($winArr);
  713. $combo = [];
  714. $sumSize = self::calculateSumSize($sum); // 总和大小
  715. $combo[] = $sumSize;
  716. $sumOddEven = self::calculateOddEven($sum); // 总和单双
  717. $combo[] = $sumOddEven;
  718. $sumExtremeSize = self::calculateSumExtremeSize($sum); // 总和极值
  719. if ($sumExtremeSize) {
  720. $combo[] = $sumExtremeSize;
  721. }
  722. $sumBaoZi = self::isBaoZi($winArr[0], $winArr[1], $winArr[2]); // 豹子
  723. if ($sumBaoZi) {
  724. $combo[] = $sumBaoZi;
  725. }
  726. $sumPair = self::isPair($winArr[0], $winArr[1], $winArr[2]); // 对子
  727. if ($sumPair) {
  728. $combo[] = $sumPair;
  729. }
  730. $sumStraight = self::isStraight($winArr[0], $winArr[1], $winArr[2]); // 顺子
  731. if ($sumStraight) {
  732. $combo[] = $sumStraight;
  733. }
  734. $tail = self::getLastDigit($sum); // 总和尾数
  735. if ($tail == 0 || $tail == 9) {
  736. } else {
  737. $combo[] = '尾' . $tail; // 尾数
  738. }
  739. return implode(' ', $combo);
  740. }
  741. private static function getPlayNowConfig($key, $default)
  742. {
  743. $value = config('services.playnow.' . $key, $default);
  744. return $value === null || $value === '' ? $default : $value;
  745. }
  746. private static function getPlayNowProxyUrl(): string
  747. {
  748. $scheme = self::getPlayNowConfig('proxy.scheme', 'http');
  749. $host = self::getPlayNowConfig('proxy.host', '155.138.141.119');
  750. $port = self::getPlayNowConfig('proxy.port', '3128');
  751. $username = (string)self::getPlayNowConfig('proxy.username', 'proxyuser');
  752. $password = (string)self::getPlayNowConfig('proxy.password', '');
  753. if ($username === '' && $password === '') {
  754. return "{$scheme}://{$host}:{$port}";
  755. }
  756. if ($password === '') {
  757. return "{$scheme}://" . rawurlencode($username) . "@{$host}:{$port}";
  758. }
  759. return "{$scheme}://" . rawurlencode($username) . ':' . rawurlencode($password) . "@{$host}:{$port}";
  760. }
  761. private static function getPlayNowProxyLogContext(): array
  762. {
  763. $host = self::getPlayNowConfig('proxy.host', '155.138.141.119');
  764. $password = self::getPlayNowConfig('proxy.password', '');
  765. return [
  766. 'proxy' => $host === '' ? 'missing' : 'configured',
  767. 'proxy_auth' => $password === '' ? 'missing' : 'configured',
  768. ];
  769. }
  770. private static function fetchPlayNowKenoResult()
  771. {
  772. $url = self::getPlayNowConfig('keno_url', self::PLAYNOW_KENO_URL);
  773. $response = Http::timeout(25)
  774. ->connectTimeout(10)
  775. ->withHeaders([
  776. 'Accept' => 'application/json, text/plain, */*',
  777. 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36',
  778. ])
  779. ->withOptions([
  780. 'proxy' => self::getPlayNowProxyUrl(),
  781. 'curl' => [
  782. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  783. ],
  784. ])
  785. ->get($url);
  786. Log::channel('issue')->info('PlayNow接口响应', [
  787. 'status' => $response->status(),
  788. 'url' => $url,
  789. ] + self::getPlayNowProxyLogContext());
  790. if (!$response->successful()) {
  791. Log::channel('issue')->info('PlayNow接口请求失败', [
  792. 'status' => $response->status(),
  793. 'body' => substr($response->body(), 0, 500),
  794. ]);
  795. return null;
  796. }
  797. return $response->json();
  798. }
  799. private static function convertPlayNowDrawDateToBeijing($drawDate)
  800. {
  801. if (empty($drawDate)) {
  802. return null;
  803. }
  804. $sourceTimezone = new \DateTimeZone(self::getPlayNowConfig('timezone', self::PLAYNOW_SOURCE_TIMEZONE));
  805. $beijingTimezone = new \DateTimeZone(self::BEIJING_TIMEZONE);
  806. return (new \DateTimeImmutable($drawDate, $sourceTimezone))->setTimezone($beijingTimezone);
  807. }
  808. private static function getPlayNowDrawIntervalSeconds(array $result): int
  809. {
  810. $timeSinceDraw = isset($result['timeSinceDraw']) ? (int)$result['timeSinceDraw'] : 0;
  811. $nextDrawSeconds = isset($result['nextKenoDrawTime']) ? (int)$result['nextKenoDrawTime'] : 0;
  812. if ($timeSinceDraw > 0 && $nextDrawSeconds > 0) {
  813. return (int)round($timeSinceDraw / 1000) + $nextDrawSeconds;
  814. }
  815. return $nextDrawSeconds > 0 ? $nextDrawSeconds : self::PLAYNOW_DRAW_INTERVAL_SECONDS;
  816. }
  817. private static function calculatePlayNowPc28(array $numbers): array
  818. {
  819. $sortedNumbers = array_map('intval', $numbers);
  820. sort($sortedNumbers, SORT_NUMERIC);
  821. $sumByIndexes = function (array $indexes) use ($sortedNumbers) {
  822. $total = 0;
  823. foreach ($indexes as $index) {
  824. $total += $sortedNumbers[$index] ?? 0;
  825. }
  826. return $total % 10;
  827. };
  828. $firstNumber = $sumByIndexes([1, 4, 7, 10, 13, 16]);
  829. $secondNumber = $sumByIndexes([2, 5, 8, 11, 14, 17]);
  830. $thirdNumber = $sumByIndexes([3, 6, 9, 12, 15, 18]);
  831. return [
  832. 'numbers' => [$firstNumber, $secondNumber, $thirdNumber],
  833. 'total' => $firstNumber + $secondNumber + $thirdNumber,
  834. 'sorted_numbers' => $sortedNumbers,
  835. ];
  836. }
  837. private static function normalizePlayNowNumbers(array $numbers)
  838. {
  839. if (count($numbers) !== 20) {
  840. return null;
  841. }
  842. $normalized = [];
  843. foreach ($numbers as $number) {
  844. $value = filter_var($number, FILTER_VALIDATE_INT, [
  845. 'options' => [
  846. 'min_range' => 1,
  847. 'max_range' => 80,
  848. ],
  849. ]);
  850. if ($value === false) {
  851. return null;
  852. }
  853. $normalized[] = (int)$value;
  854. }
  855. if (count(array_unique($normalized)) !== 20) {
  856. return null;
  857. }
  858. return $normalized;
  859. }
  860. private static function encodePlayNowKeno($numbers)
  861. {
  862. if (!is_array($numbers)) {
  863. return null;
  864. }
  865. $numbers = self::normalizePlayNowNumbers($numbers);
  866. return $numbers ? json_encode(array_values($numbers)) : null;
  867. }
  868. private static function isSamePlayNowIssueNo($issueNo, int $drawNo): bool
  869. {
  870. $issueNo = trim((string)$issueNo);
  871. return ctype_digit($issueNo) && (int)$issueNo === $drawNo;
  872. }
  873. private static function normalizePlayNowDrawNo($drawNo)
  874. {
  875. $value = filter_var($drawNo, FILTER_VALIDATE_INT, [
  876. 'options' => [
  877. 'min_range' => 1,
  878. ],
  879. ]);
  880. return $value === false ? null : (int)$value;
  881. }
  882. private static function parsePlayNowDrawInfo(array $result)
  883. {
  884. if (!isset($result['draw']) || empty($result['num']) || !is_array($result['num'])) {
  885. return null;
  886. }
  887. $drawNo = self::normalizePlayNowDrawNo($result['draw']);
  888. if (!$drawNo) {
  889. return null;
  890. }
  891. $sourceNumbers = self::normalizePlayNowNumbers($result['num']);
  892. if (!$sourceNumbers) {
  893. return null;
  894. }
  895. try {
  896. $drawDateBeijing = self::convertPlayNowDrawDateToBeijing($result['drawDate'] ?? null);
  897. } catch (\Throwable $exception) {
  898. Log::channel('issue')->info('PlayNow开奖时间解析失败', [
  899. 'draw_date_raw' => $result['drawDate'] ?? '',
  900. 'error' => $exception->getMessage(),
  901. ]);
  902. return null;
  903. }
  904. $nowBeijing = new \DateTimeImmutable('now', new \DateTimeZone(self::BEIJING_TIMEZONE));
  905. $drawIntervalSeconds = self::getPlayNowDrawIntervalSeconds($result);
  906. $startDateTime = $drawDateBeijing ?: $nowBeijing;
  907. $endDateTime = $startDateTime->modify('+' . $drawIntervalSeconds . ' seconds');
  908. $pc28 = self::calculatePlayNowPc28($sourceNumbers);
  909. return [
  910. 'draw_no' => $drawNo,
  911. 'draw_date_raw' => $result['drawDate'] ?? '',
  912. 'draw_date_beijing' => $drawDateBeijing ? $drawDateBeijing->format('Y-m-d H:i:s') : '',
  913. 'next_draw_seconds' => $result['nextKenoDrawTime'] ?? '',
  914. 'draw_interval_seconds' => $drawIntervalSeconds,
  915. 'source_numbers' => $sourceNumbers,
  916. 'sorted_numbers' => $pc28['sorted_numbers'],
  917. 'pc28' => $pc28,
  918. 'winning_numbers' => implode(',', $pc28['numbers']),
  919. 'combo' => self::getCombo($pc28['numbers']),
  920. 'start_time' => $startDateTime->format('Y-m-d H:i:s'),
  921. 'end_time' => $endDateTime->format('Y-m-d H:i:s'),
  922. ];
  923. }
  924. public static function getPlayNowWinningInfo($issueNo): array
  925. {
  926. Log::channel('issue')->info('后台手动开奖获取PlayNow数据', [
  927. 'issue_no' => $issueNo,
  928. ] + self::getPlayNowProxyLogContext());
  929. try {
  930. $result = self::fetchPlayNowKenoResult();
  931. } catch (\Throwable $exception) {
  932. Log::channel('issue')->info('后台手动开奖获取PlayNow数据异常', [
  933. 'issue_no' => $issueNo,
  934. 'error' => $exception->getMessage(),
  935. 'file' => $exception->getFile(),
  936. 'line' => $exception->getLine(),
  937. ]);
  938. return ['code' => self::NOT, 'msg' => '获取开奖信息失败'];
  939. }
  940. $drawInfo = self::parsePlayNowDrawInfo($result ?: []);
  941. if (!$drawInfo) {
  942. Log::channel('issue')->info('后台手动开奖PlayNow返回数据格式异常', [
  943. 'issue_no' => $issueNo,
  944. 'result' => $result,
  945. ]);
  946. return ['code' => self::NOT, 'msg' => '获取开奖信息失败'];
  947. }
  948. Log::channel('issue')->info('后台手动开奖PlayNow计算结果', [
  949. 'issue_no' => $issueNo,
  950. 'draw' => $drawInfo['draw_no'],
  951. 'draw_date_raw' => $drawInfo['draw_date_raw'],
  952. 'draw_date_beijing' => $drawInfo['draw_date_beijing'],
  953. 'source_numbers' => implode(',', $drawInfo['source_numbers']),
  954. 'sorted_numbers' => implode(',', $drawInfo['sorted_numbers']),
  955. 'pc28_numbers' => $drawInfo['winning_numbers'],
  956. 'pc28_total' => $drawInfo['pc28']['total'],
  957. 'combo' => $drawInfo['combo'],
  958. ]);
  959. if (!self::isSamePlayNowIssueNo($issueNo, $drawInfo['draw_no'])) {
  960. Log::channel('issue')->info('后台手动开奖期号不匹配', [
  961. 'issue_no' => $issueNo,
  962. 'playnow_draw' => $drawInfo['draw_no'],
  963. ]);
  964. return ['code' => self::NOT, 'msg' => '未查询到开奖信息,请手动开奖'];
  965. }
  966. return ['code' => self::YES, 'msg' => '获取成功'] + $drawInfo;
  967. }
  968. // 获取最新的开奖数据
  969. public static function getLatestIssue()
  970. {
  971. Log::channel('issue')->info('开始获取最新期号', [
  972. 'source' => 'playnow',
  973. ] + self::getPlayNowProxyLogContext());
  974. try {
  975. $result = self::fetchPlayNowKenoResult();
  976. } catch (\Throwable $exception) {
  977. Log::channel('issue')->info('获取PlayNow最新期号异常', [
  978. 'error' => $exception->getMessage(),
  979. 'file' => $exception->getFile(),
  980. 'line' => $exception->getLine(),
  981. ]);
  982. return ['code' => self::NOT, 'msg' => '获取最新期号失败'];
  983. }
  984. $drawInfo = self::parsePlayNowDrawInfo($result ?: []);
  985. if (!$drawInfo) {
  986. Log::channel('issue')->info('PlayNow返回数据格式异常', [
  987. 'result' => $result,
  988. ]);
  989. return ['code' => self::NOT, 'msg' => '获取最新期号失败'];
  990. }
  991. $drawNo = $drawInfo['draw_no'];
  992. $startTime = $drawInfo['start_time'];
  993. $endTime = $drawInfo['end_time'];
  994. $pc28 = $drawInfo['pc28'];
  995. Log::channel('issue')->info('PlayNow最新开奖数据', [
  996. 'draw' => $drawNo,
  997. 'draw_date_raw' => $drawInfo['draw_date_raw'],
  998. 'draw_date_beijing' => $drawInfo['draw_date_beijing'],
  999. 'next_draw_seconds' => $drawInfo['next_draw_seconds'],
  1000. 'draw_interval_seconds' => $drawInfo['draw_interval_seconds'],
  1001. 'source_numbers' => implode(',', $drawInfo['source_numbers']),
  1002. 'sorted_numbers' => implode(',', $drawInfo['sorted_numbers']),
  1003. 'pc28_numbers' => $drawInfo['winning_numbers'],
  1004. 'pc28_total' => $pc28['total'],
  1005. 'next_issue_start_time' => $startTime,
  1006. 'next_issue_end_time' => $endTime,
  1007. ]);
  1008. $kenoJson = self::encodePlayNowKeno($drawInfo['source_numbers']);
  1009. if ($kenoJson) {
  1010. $drawIssue = self::findOne(['issue_no' => (string)$drawNo]);
  1011. if ($drawIssue && empty($drawIssue->keno)) {
  1012. self::model()::where('id', $drawIssue->id)->update(['keno' => $kenoJson]);
  1013. $drawIssue->keno = $kenoJson;
  1014. Log::channel('issue')->info('PlayNow源头号码已补写', [
  1015. 'issue_no' => $drawNo,
  1016. 'source_numbers' => implode(',', $drawInfo['source_numbers']),
  1017. 'status' => $drawIssue->status,
  1018. ]);
  1019. }
  1020. } else {
  1021. Log::channel('issue')->info('PlayNow源头号码跳过写入', [
  1022. 'issue_no' => $drawNo,
  1023. 'source_numbers_type' => gettype($drawInfo['source_numbers'] ?? null),
  1024. ]);
  1025. }
  1026. $new = true;
  1027. $oldList = self::findAll(['status' => self::model()::STATUS_CLOSE]); // 获取所有封盘的期号
  1028. foreach ($oldList as $k => $v) {
  1029. if (self::isSamePlayNowIssueNo($v->issue_no, $drawNo)) {
  1030. $winning_numbers = $drawInfo['winning_numbers'];
  1031. $winArr = $pc28['numbers'];
  1032. $combo = $drawInfo['combo'];
  1033. $key = 'lottery_numbers_' . $v->issue_no;
  1034. if (Cache::add($key, $winning_numbers, 100)) {
  1035. Log::channel('issue')->info('开奖期号: ' . $v->issue_no . ' 开奖号码: ' . $winning_numbers, [
  1036. 'source' => 'playnow',
  1037. 'draw' => $drawNo,
  1038. 'combo' => $combo,
  1039. 'pc28_total' => $pc28['total'],
  1040. ]);
  1041. self::lotteryDraw($v->id, $winning_numbers, $combo, '', $drawInfo['source_numbers']);
  1042. $new = false;
  1043. } else {
  1044. Log::channel('issue')->info('开奖期号已处理,跳过重复开奖', [
  1045. 'issue_no' => $v->issue_no,
  1046. 'winning_numbers' => $winning_numbers,
  1047. ]);
  1048. }
  1049. $pc28Switch = Config::where('field', 'pc28_switch')->first()->val;
  1050. //更新游戏开关的切换
  1051. if ($pc28Switch == 0) Config::setPc28Switch();
  1052. }
  1053. }
  1054. // sleep(5); // 等待开奖完成
  1055. if ($new) {
  1056. $new_issue_no = $drawNo + 1; // 新期号
  1057. $newInfo = self::findOne(['issue_no' => $new_issue_no]); // 找新的期号
  1058. // 不存在
  1059. if (!$newInfo) {
  1060. Log::channel('issue')->info('新增期号: ' . $new_issue_no, [
  1061. 'source' => 'playnow',
  1062. 'start_time' => $startTime,
  1063. 'end_time' => $endTime,
  1064. ]);
  1065. $res = self::submit([
  1066. 'issue_no' => $new_issue_no,
  1067. 'status' => self::model()::STATUS_DRAFT,
  1068. 'start_time' => $startTime,
  1069. 'end_time' => $endTime,
  1070. ]);
  1071. Prediction::prediction($new_issue_no);
  1072. $id = $res['key'] ?? 0;
  1073. if ($id) {
  1074. self::betting($id); // 开始下注
  1075. }
  1076. Cache::set('new_issue_no', $new_issue_no, 10); // 缓存
  1077. } else {
  1078. Log::channel('issue')->info('期号已存在,无需新增', [
  1079. 'issue_no' => $new_issue_no,
  1080. 'status' => $newInfo->status,
  1081. 'start_time' => $newInfo->start_time,
  1082. 'end_time' => $newInfo->end_time,
  1083. ]);
  1084. }
  1085. }
  1086. return $result;
  1087. }
  1088. // 获取最新的开奖数据
  1089. public static function getLatestIssue2()
  1090. {
  1091. $url = "https://ydpc28.co/api/pc28/list";
  1092. $result = file_get_contents($url);
  1093. $result = json_decode($result, true);
  1094. if ($result['errorCode'] != 0) {
  1095. return ['code' => self::NOT, 'msg' => '获取最新期号失败'];
  1096. }
  1097. $nextDrawInfo = $result['data']['nextDrawInfo'];
  1098. $startTime = $nextDrawInfo['currentBJTime'];
  1099. // if($nextDrawInfo['nextDrawTime'] >= date('H:i:s')) {
  1100. // $endTime = date('Y-m-d').' '.$nextDrawInfo['nextDrawTime']; // 下一期的截止时间
  1101. // }else{
  1102. // $endTime = date('Y-m-d',strtotime('+1 day')).' '.$nextDrawInfo['nextDrawTime']; // 下一期的截止时间
  1103. // }
  1104. $endTime = date('Y-m-d H:i:s', strtotime($startTime) + 210);
  1105. $new = true;
  1106. $list = $result['data']['list'];
  1107. $listKey = [];
  1108. foreach ($list as $k => $v) {
  1109. $listKey[$v['lotNumber']] = $v;
  1110. }
  1111. $oldList = self::findAll(['status' => self::model()::STATUS_CLOSE]); // 获取所有封盘的期号
  1112. foreach ($oldList as $k => $v) {
  1113. if (isset($listKey[$v->issue_no])) {
  1114. $issue = $listKey[$v->issue_no];
  1115. $winning_numbers = implode(',', str_split((string)$issue['openCode']));
  1116. $winArr = array_map('intval', explode(',', $winning_numbers));
  1117. // 组合
  1118. $sum = array_sum($winArr);
  1119. $combo = [];
  1120. $sumOddEven = self::calculateOddEven($sum); // 总和单双
  1121. $combo[] = $sumOddEven;
  1122. $sumSize = self::calculateSumSize($sum); // 总和大小
  1123. $combo[] = $sumSize;
  1124. $sumExtremeSize = self::calculateSumExtremeSize($sum); // 总和极值
  1125. if ($sumExtremeSize) {
  1126. $combo[] = $sumExtremeSize;
  1127. }
  1128. $sumBaoZi = self::isBaoZi($winArr[0], $winArr[1], $winArr[2]); // 豹子
  1129. if ($sumBaoZi) {
  1130. $combo[] = $sumBaoZi;
  1131. }
  1132. $sumPair = self::isPair($winArr[0], $winArr[1], $winArr[2]); // 对子
  1133. if ($sumPair) {
  1134. $combo[] = $sumPair;
  1135. }
  1136. $sumStraight = self::isStraight($winArr[0], $winArr[1], $winArr[2]); // 顺子
  1137. if ($sumStraight) {
  1138. $combo[] = $sumStraight;
  1139. }
  1140. $tail = self::getLastDigit($sum); // 总和尾数
  1141. if ($tail == 0 || $tail == 9) {
  1142. } else {
  1143. $combo[] = '尾' . $tail; // 尾数
  1144. }
  1145. $combo = implode(' ', $combo);
  1146. self::lotteryDraw($v->id, $winning_numbers, $combo, '');
  1147. }
  1148. }
  1149. return $result;
  1150. }
  1151. // 封盘倒数
  1152. public static function syncCountdownIssue()
  1153. {
  1154. $info = self::model()::where('status', self::model()::STATUS_BETTING)->orderBy('end_time', 'asc')->first();
  1155. if ($info) {
  1156. $now_date = date('Y-m-d H:i:s', time() + IssueService::COUNTDOWN_TO_CLOSING_THE_MARKET);
  1157. if ($info['end_time'] < $now_date) {
  1158. $replyInfo = KeyboardService::findOne(['button' => '封盘倒数']);
  1159. if ($replyInfo) {
  1160. $text = $replyInfo->reply;
  1161. $buttons = json_decode($replyInfo->buttons, true);
  1162. $image = $replyInfo->image;
  1163. if ($image) {
  1164. $image = url($image);
  1165. }
  1166. if (Cache::has('issue_countdown_' . $info->issue_no)) {
  1167. } else {
  1168. $pc28Switch = Config::where('field', 'pc28_switch')->first()->val;
  1169. if ($pc28Switch == 0) {
  1170. self::asyncBettingGroupNotice($text, $buttons, $image);
  1171. Cache::put('issue_countdown_' . $info->issue_no, true, 60); // 缓存50秒,防止多次发送
  1172. }
  1173. }
  1174. }
  1175. }
  1176. }
  1177. }
  1178. // 停止下注
  1179. public static function syncCloseIssue()
  1180. {
  1181. $now_date = date('Y-m-d H:i:s', time() + 30); // 提前30秒
  1182. $list = self::findAll(['status' => self::model()::STATUS_BETTING]);
  1183. foreach ($list as $k => $v) {
  1184. if ($v['end_time'] < $now_date) {
  1185. self::closeBetting($v->id);
  1186. }
  1187. }
  1188. }
  1189. // 生成开奖图片
  1190. public static function lotteryImage($issue_no)
  1191. {
  1192. $list = self::model()::where('issue_no', '<=', $issue_no)->where(self::getWhere(['status' => self::model()::STATUS_DRAW]))->orderBy('issue_no', 'desc')->take(20)->get();
  1193. $records = $list->toArray();
  1194. foreach ($records as $k => $v) {
  1195. $winning_numbers = explode(',', $v['winning_numbers']);
  1196. $v['winning_numbers'] = $winning_numbers;
  1197. // 组合
  1198. $sum = array_sum($winning_numbers);
  1199. $v['sum'] = $sum;
  1200. $sumOddEven = self::calculateOddEven($sum); // 总和单双
  1201. $sumSize = self::calculateSumSize($sum); // 总和大小
  1202. $v['combo'] = $sumSize . ' ' . $sumOddEven;
  1203. $sumExtremeSize = self::calculateSumExtremeSize($sum); // 总和极值
  1204. if (!$sumExtremeSize) {
  1205. $sumExtremeSize = '-';
  1206. }
  1207. $v['extreme'] = $sumExtremeSize;
  1208. $tail = self::getLastDigit($sum); // 总和尾数
  1209. if ($tail === 0 || $tail === 9) {
  1210. $tailStr = '-';
  1211. } else {
  1212. $tailStr = '尾' . $tail;
  1213. }
  1214. $v['tail'] = $tailStr;
  1215. $records[$k] = $v;
  1216. }
  1217. $service = new LotteryImageService();
  1218. $url = $service->generate($records);
  1219. self::model()::where('issue_no', $issue_no)->update(['image' => $url]);
  1220. return $url;
  1221. }
  1222. // 发送开奖图片
  1223. public static function sendLotteryImage($chatId, $issueNo)
  1224. {
  1225. $recordImage = self::lotteryImage($issueNo);
  1226. self::sendMessage($chatId, '', [], url($recordImage));
  1227. // dispatch(new SendTelegramMessageJob('', [], url($recordImage)));
  1228. }
  1229. }