IssueService.php 51 KB

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