|
|
@@ -12,6 +12,7 @@ use App\Models\Config;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Collection;
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
+use Illuminate\Support\Facades\Http;
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
|
use App\Services\GameplayRuleService;
|
|
|
@@ -30,6 +31,10 @@ class IssueService extends BaseService
|
|
|
{
|
|
|
|
|
|
const COUNTDOWN_TO_CLOSING_THE_MARKET = 60;//提前xx秒封盘
|
|
|
+ const PLAYNOW_KENO_URL = 'https://www.playnow.com/services2/keno/nextdraw';
|
|
|
+ const PLAYNOW_SOURCE_TIMEZONE = 'America/Vancouver';
|
|
|
+ const BEIJING_TIMEZONE = 'Asia/Shanghai';
|
|
|
+ const PLAYNOW_DRAW_INTERVAL_SECONDS = 210;
|
|
|
|
|
|
|
|
|
public static function init($telegram, $data, $chatId, $firstName, $messageId): void
|
|
|
@@ -860,51 +865,335 @@ class IssueService extends BaseService
|
|
|
|
|
|
}
|
|
|
|
|
|
+ private static function getPlayNowConfig($key, $default)
|
|
|
+ {
|
|
|
+ $value = config('services.playnow.' . $key, $default);
|
|
|
+ return $value === null || $value === '' ? $default : $value;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function getPlayNowProxyUrl(): string
|
|
|
+ {
|
|
|
+ $scheme = self::getPlayNowConfig('proxy.scheme', 'http');
|
|
|
+ $host = self::getPlayNowConfig('proxy.host', '155.138.141.119');
|
|
|
+ $port = self::getPlayNowConfig('proxy.port', '3128');
|
|
|
+ $username = (string)self::getPlayNowConfig('proxy.username', 'proxyuser');
|
|
|
+ $password = (string)self::getPlayNowConfig('proxy.password', '');
|
|
|
+
|
|
|
+ if ($username === '' && $password === '') {
|
|
|
+ return "{$scheme}://{$host}:{$port}";
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($password === '') {
|
|
|
+ return "{$scheme}://" . rawurlencode($username) . "@{$host}:{$port}";
|
|
|
+ }
|
|
|
+
|
|
|
+ return "{$scheme}://" . rawurlencode($username) . ':' . rawurlencode($password) . "@{$host}:{$port}";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function getPlayNowProxyLogContext(): array
|
|
|
+ {
|
|
|
+ $host = self::getPlayNowConfig('proxy.host', '155.138.141.119');
|
|
|
+ $password = self::getPlayNowConfig('proxy.password', '');
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'proxy' => $host === '' ? 'missing' : 'configured',
|
|
|
+ 'proxy_auth' => $password === '' ? 'missing' : 'configured',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function fetchPlayNowKenoResult()
|
|
|
+ {
|
|
|
+ $url = self::getPlayNowConfig('keno_url', self::PLAYNOW_KENO_URL);
|
|
|
+ $response = Http::timeout(25)
|
|
|
+ ->connectTimeout(10)
|
|
|
+ ->withHeaders([
|
|
|
+ 'Accept' => 'application/json, text/plain, */*',
|
|
|
+ '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',
|
|
|
+ ])
|
|
|
+ ->withOptions([
|
|
|
+ 'proxy' => self::getPlayNowProxyUrl(),
|
|
|
+ 'curl' => [
|
|
|
+ CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
|
|
+ ],
|
|
|
+ ])
|
|
|
+ ->get($url);
|
|
|
+
|
|
|
+ Log::channel('issue')->info('PlayNow接口响应', [
|
|
|
+ 'status' => $response->status(),
|
|
|
+ 'url' => $url,
|
|
|
+ ] + self::getPlayNowProxyLogContext());
|
|
|
+
|
|
|
+ if (!$response->successful()) {
|
|
|
+ Log::channel('issue')->info('PlayNow接口请求失败', [
|
|
|
+ 'status' => $response->status(),
|
|
|
+ 'body' => substr($response->body(), 0, 500),
|
|
|
+ ]);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $response->json();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function convertPlayNowDrawDateToBeijing($drawDate)
|
|
|
+ {
|
|
|
+ if (empty($drawDate)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $sourceTimezone = new \DateTimeZone(self::getPlayNowConfig('timezone', self::PLAYNOW_SOURCE_TIMEZONE));
|
|
|
+ $beijingTimezone = new \DateTimeZone(self::BEIJING_TIMEZONE);
|
|
|
+
|
|
|
+ return (new \DateTimeImmutable($drawDate, $sourceTimezone))->setTimezone($beijingTimezone);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function getPlayNowDrawIntervalSeconds(array $result): int
|
|
|
+ {
|
|
|
+ $timeSinceDraw = isset($result['timeSinceDraw']) ? (int)$result['timeSinceDraw'] : 0;
|
|
|
+ $nextDrawSeconds = isset($result['nextKenoDrawTime']) ? (int)$result['nextKenoDrawTime'] : 0;
|
|
|
+
|
|
|
+ if ($timeSinceDraw > 0 && $nextDrawSeconds > 0) {
|
|
|
+ return (int)round($timeSinceDraw / 1000) + $nextDrawSeconds;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $nextDrawSeconds > 0 ? $nextDrawSeconds : self::PLAYNOW_DRAW_INTERVAL_SECONDS;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function calculatePlayNowPc28(array $numbers): array
|
|
|
+ {
|
|
|
+ $sortedNumbers = array_map('intval', $numbers);
|
|
|
+ sort($sortedNumbers, SORT_NUMERIC);
|
|
|
+
|
|
|
+ $sumByIndexes = function (array $indexes) use ($sortedNumbers) {
|
|
|
+ $total = 0;
|
|
|
+ foreach ($indexes as $index) {
|
|
|
+ $total += $sortedNumbers[$index] ?? 0;
|
|
|
+ }
|
|
|
+ return $total % 10;
|
|
|
+ };
|
|
|
+
|
|
|
+ $firstNumber = $sumByIndexes([1, 4, 7, 10, 13, 16]);
|
|
|
+ $secondNumber = $sumByIndexes([2, 5, 8, 11, 14, 17]);
|
|
|
+ $thirdNumber = $sumByIndexes([3, 6, 9, 12, 15, 18]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'numbers' => [$firstNumber, $secondNumber, $thirdNumber],
|
|
|
+ 'total' => $firstNumber + $secondNumber + $thirdNumber,
|
|
|
+ 'sorted_numbers' => $sortedNumbers,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function normalizePlayNowNumbers(array $numbers)
|
|
|
+ {
|
|
|
+ if (count($numbers) !== 20) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $normalized = [];
|
|
|
+ foreach ($numbers as $number) {
|
|
|
+ $value = filter_var($number, FILTER_VALIDATE_INT, [
|
|
|
+ 'options' => [
|
|
|
+ 'min_range' => 1,
|
|
|
+ 'max_range' => 80,
|
|
|
+ ],
|
|
|
+ ]);
|
|
|
+ if ($value === false) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $normalized[] = (int)$value;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (count(array_unique($normalized)) !== 20) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $normalized;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function isSamePlayNowIssueNo($issueNo, int $drawNo): bool
|
|
|
+ {
|
|
|
+ $issueNo = trim((string)$issueNo);
|
|
|
+ return ctype_digit($issueNo) && (int)$issueNo === $drawNo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function normalizePlayNowDrawNo($drawNo)
|
|
|
+ {
|
|
|
+ $value = filter_var($drawNo, FILTER_VALIDATE_INT, [
|
|
|
+ 'options' => [
|
|
|
+ 'min_range' => 1,
|
|
|
+ ],
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $value === false ? null : (int)$value;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function parsePlayNowDrawInfo(array $result)
|
|
|
+ {
|
|
|
+ if (!isset($result['draw']) || empty($result['num']) || !is_array($result['num'])) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $drawNo = self::normalizePlayNowDrawNo($result['draw']);
|
|
|
+ if (!$drawNo) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $sourceNumbers = self::normalizePlayNowNumbers($result['num']);
|
|
|
+ if (!$sourceNumbers) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $drawDateBeijing = self::convertPlayNowDrawDateToBeijing($result['drawDate'] ?? null);
|
|
|
+ } catch (\Throwable $exception) {
|
|
|
+ Log::channel('issue')->info('PlayNow开奖时间解析失败', [
|
|
|
+ 'draw_date_raw' => $result['drawDate'] ?? '',
|
|
|
+ 'error' => $exception->getMessage(),
|
|
|
+ ]);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $nowBeijing = new \DateTimeImmutable('now', new \DateTimeZone(self::BEIJING_TIMEZONE));
|
|
|
+ $drawIntervalSeconds = self::getPlayNowDrawIntervalSeconds($result);
|
|
|
+ $startDateTime = $drawDateBeijing ?: $nowBeijing;
|
|
|
+ $endDateTime = $startDateTime->modify('+' . $drawIntervalSeconds . ' seconds');
|
|
|
+ $pc28 = self::calculatePlayNowPc28($sourceNumbers);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'draw_no' => $drawNo,
|
|
|
+ 'draw_date_raw' => $result['drawDate'] ?? '',
|
|
|
+ 'draw_date_beijing' => $drawDateBeijing ? $drawDateBeijing->format('Y-m-d H:i:s') : '',
|
|
|
+ 'next_draw_seconds' => $result['nextKenoDrawTime'] ?? '',
|
|
|
+ 'draw_interval_seconds' => $drawIntervalSeconds,
|
|
|
+ 'source_numbers' => $sourceNumbers,
|
|
|
+ 'sorted_numbers' => $pc28['sorted_numbers'],
|
|
|
+ 'pc28' => $pc28,
|
|
|
+ 'winning_numbers' => implode(',', $pc28['numbers']),
|
|
|
+ 'combo' => self::getCombo($pc28['numbers']),
|
|
|
+ 'start_time' => $startDateTime->format('Y-m-d H:i:s'),
|
|
|
+ 'end_time' => $endDateTime->format('Y-m-d H:i:s'),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ public static function getPlayNowWinningInfo($issueNo): array
|
|
|
+ {
|
|
|
+ Log::channel('issue')->info('后台手动开奖获取PlayNow数据', [
|
|
|
+ 'issue_no' => $issueNo,
|
|
|
+ ] + self::getPlayNowProxyLogContext());
|
|
|
+
|
|
|
+ try {
|
|
|
+ $result = self::fetchPlayNowKenoResult();
|
|
|
+ } catch (\Throwable $exception) {
|
|
|
+ Log::channel('issue')->info('后台手动开奖获取PlayNow数据异常', [
|
|
|
+ 'issue_no' => $issueNo,
|
|
|
+ 'error' => $exception->getMessage(),
|
|
|
+ 'file' => $exception->getFile(),
|
|
|
+ 'line' => $exception->getLine(),
|
|
|
+ ]);
|
|
|
+ return ['code' => self::NOT, 'msg' => '获取开奖信息失败'];
|
|
|
+ }
|
|
|
+
|
|
|
+ $drawInfo = self::parsePlayNowDrawInfo($result ?: []);
|
|
|
+ if (!$drawInfo) {
|
|
|
+ Log::channel('issue')->info('后台手动开奖PlayNow返回数据格式异常', [
|
|
|
+ 'issue_no' => $issueNo,
|
|
|
+ 'result' => $result,
|
|
|
+ ]);
|
|
|
+ return ['code' => self::NOT, 'msg' => '获取开奖信息失败'];
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::channel('issue')->info('后台手动开奖PlayNow计算结果', [
|
|
|
+ 'issue_no' => $issueNo,
|
|
|
+ 'draw' => $drawInfo['draw_no'],
|
|
|
+ 'draw_date_raw' => $drawInfo['draw_date_raw'],
|
|
|
+ 'draw_date_beijing' => $drawInfo['draw_date_beijing'],
|
|
|
+ 'source_numbers' => implode(',', $drawInfo['source_numbers']),
|
|
|
+ 'sorted_numbers' => implode(',', $drawInfo['sorted_numbers']),
|
|
|
+ 'pc28_numbers' => $drawInfo['winning_numbers'],
|
|
|
+ 'pc28_total' => $drawInfo['pc28']['total'],
|
|
|
+ 'combo' => $drawInfo['combo'],
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (!self::isSamePlayNowIssueNo($issueNo, $drawInfo['draw_no'])) {
|
|
|
+ Log::channel('issue')->info('后台手动开奖期号不匹配', [
|
|
|
+ 'issue_no' => $issueNo,
|
|
|
+ 'playnow_draw' => $drawInfo['draw_no'],
|
|
|
+ ]);
|
|
|
+ return ['code' => self::NOT, 'msg' => '未查询到开奖信息,请手动开奖'];
|
|
|
+ }
|
|
|
+
|
|
|
+ return ['code' => self::YES, 'msg' => '获取成功'] + $drawInfo;
|
|
|
+ }
|
|
|
+
|
|
|
// 获取最新的开奖数据
|
|
|
public static function getLatestIssue()
|
|
|
{
|
|
|
- Log::channel('issue')->info('开始获取最新期号');
|
|
|
- $url = "https://ydpc28.co/api/pc28/list";
|
|
|
- $result = file_get_contents($url);
|
|
|
- $result = json_decode($result, true);
|
|
|
- if ($result['errorCode'] != 0) {
|
|
|
- Log::channel('issue')->info('获取最新期号失败');
|
|
|
+ Log::channel('issue')->info('开始获取最新期号', [
|
|
|
+ 'source' => 'playnow',
|
|
|
+ ] + self::getPlayNowProxyLogContext());
|
|
|
+
|
|
|
+ try {
|
|
|
+ $result = self::fetchPlayNowKenoResult();
|
|
|
+ } catch (\Throwable $exception) {
|
|
|
+ Log::channel('issue')->info('获取PlayNow最新期号异常', [
|
|
|
+ 'error' => $exception->getMessage(),
|
|
|
+ 'file' => $exception->getFile(),
|
|
|
+ 'line' => $exception->getLine(),
|
|
|
+ ]);
|
|
|
return ['code' => self::NOT, 'msg' => '获取最新期号失败'];
|
|
|
+ }
|
|
|
|
|
|
+ $drawInfo = self::parsePlayNowDrawInfo($result ?: []);
|
|
|
+ if (!$drawInfo) {
|
|
|
+ Log::channel('issue')->info('PlayNow返回数据格式异常', [
|
|
|
+ 'result' => $result,
|
|
|
+ ]);
|
|
|
+ return ['code' => self::NOT, 'msg' => '获取最新期号失败'];
|
|
|
}
|
|
|
- $nextDrawInfo = $result['data']['nextDrawInfo'];
|
|
|
- $startTime = $nextDrawInfo['currentBJTime'];
|
|
|
- // if($nextDrawInfo['nextDrawTime'] >= date('H:i:s')) {
|
|
|
- // $endTime = date('Y-m-d').' '.$nextDrawInfo['nextDrawTime']; // 下一期的截止时间
|
|
|
- // }else{
|
|
|
- // $endTime = date('Y-m-d',strtotime('+1 day')).' '.$nextDrawInfo['nextDrawTime']; // 下一期的截止时间
|
|
|
- // }
|
|
|
|
|
|
- $endTime = date('Y-m-d H:i:s', strtotime($startTime) + 210);
|
|
|
+ $drawNo = $drawInfo['draw_no'];
|
|
|
+ $startTime = $drawInfo['start_time'];
|
|
|
+ $endTime = $drawInfo['end_time'];
|
|
|
+ $pc28 = $drawInfo['pc28'];
|
|
|
+
|
|
|
+ Log::channel('issue')->info('PlayNow最新开奖数据', [
|
|
|
+ 'draw' => $drawNo,
|
|
|
+ 'draw_date_raw' => $drawInfo['draw_date_raw'],
|
|
|
+ 'draw_date_beijing' => $drawInfo['draw_date_beijing'],
|
|
|
+ 'next_draw_seconds' => $drawInfo['next_draw_seconds'],
|
|
|
+ 'draw_interval_seconds' => $drawInfo['draw_interval_seconds'],
|
|
|
+ 'source_numbers' => implode(',', $drawInfo['source_numbers']),
|
|
|
+ 'sorted_numbers' => implode(',', $drawInfo['sorted_numbers']),
|
|
|
+ 'pc28_numbers' => $drawInfo['winning_numbers'],
|
|
|
+ 'pc28_total' => $pc28['total'],
|
|
|
+ 'next_issue_start_time' => $startTime,
|
|
|
+ 'next_issue_end_time' => $endTime,
|
|
|
+ ]);
|
|
|
|
|
|
$new = true;
|
|
|
|
|
|
- $list = $result['data']['list'];
|
|
|
- $listKey = [];
|
|
|
- foreach ($list as $k => $v) {
|
|
|
- $listKey[$v['lotNumber']] = $v;
|
|
|
- }
|
|
|
-
|
|
|
$oldList = self::findAll(['status' => self::model()::STATUS_CLOSE]); // 获取所有封盘的期号
|
|
|
foreach ($oldList as $k => $v) {
|
|
|
- if (isset($listKey[$v->issue_no])) {
|
|
|
- $issue = $listKey[$v->issue_no];
|
|
|
- $winning_numbers = implode(',', str_split((string)$issue['openCode']));
|
|
|
+ if (self::isSamePlayNowIssueNo($v->issue_no, $drawNo)) {
|
|
|
+ $winning_numbers = $drawInfo['winning_numbers'];
|
|
|
+ $winArr = $pc28['numbers'];
|
|
|
|
|
|
- $winArr = array_map('intval', explode(',', $winning_numbers));
|
|
|
|
|
|
-
|
|
|
- $combo = static::getCombo($winArr);
|
|
|
+ $combo = $drawInfo['combo'];
|
|
|
$key = 'lottery_numbers_' . $v->issue_no;
|
|
|
if (Cache::add($key, $winning_numbers, 100)) {
|
|
|
- Log::channel('issue')->info('开奖期号: ' . $v->issue_no . ' 开奖号码: ' . $winning_numbers);
|
|
|
+ Log::channel('issue')->info('开奖期号: ' . $v->issue_no . ' 开奖号码: ' . $winning_numbers, [
|
|
|
+ 'source' => 'playnow',
|
|
|
+ 'draw' => $drawNo,
|
|
|
+ 'combo' => $combo,
|
|
|
+ 'pc28_total' => $pc28['total'],
|
|
|
+ ]);
|
|
|
self::lotteryDraw($v->id, $winning_numbers, $combo, '');
|
|
|
$new = false;
|
|
|
+ } else {
|
|
|
+ Log::channel('issue')->info('开奖期号已处理,跳过重复开奖', [
|
|
|
+ 'issue_no' => $v->issue_no,
|
|
|
+ 'winning_numbers' => $winning_numbers,
|
|
|
+ ]);
|
|
|
}
|
|
|
$pc28Switch = Config::where('field', 'pc28_switch')->first()->val;
|
|
|
//更新游戏开关的切换
|
|
|
@@ -915,15 +1204,17 @@ class IssueService extends BaseService
|
|
|
// sleep(5); // 等待开奖完成
|
|
|
|
|
|
if ($new) {
|
|
|
- $latestIssue = $list[0]; // 最后开奖
|
|
|
-
|
|
|
- $new_issue_no = $latestIssue['lotNumber'] + 1; // 新期号
|
|
|
+ $new_issue_no = $drawNo + 1; // 新期号
|
|
|
|
|
|
$newInfo = self::findOne(['issue_no' => $new_issue_no]); // 找新的期号
|
|
|
|
|
|
// 不存在
|
|
|
if (!$newInfo) {
|
|
|
- Log::channel('issue')->info('新增期号: ' . $new_issue_no);
|
|
|
+ Log::channel('issue')->info('新增期号: ' . $new_issue_no, [
|
|
|
+ 'source' => 'playnow',
|
|
|
+ 'start_time' => $startTime,
|
|
|
+ 'end_time' => $endTime,
|
|
|
+ ]);
|
|
|
$res = self::submit([
|
|
|
'issue_no' => $new_issue_no,
|
|
|
'status' => self::model()::STATUS_DRAFT,
|
|
|
@@ -940,6 +1231,13 @@ class IssueService extends BaseService
|
|
|
|
|
|
}
|
|
|
Cache::set('new_issue_no', $new_issue_no, 10); // 缓存
|
|
|
+ } else {
|
|
|
+ Log::channel('issue')->info('期号已存在,无需新增', [
|
|
|
+ 'issue_no' => $new_issue_no,
|
|
|
+ 'status' => $newInfo->status,
|
|
|
+ 'start_time' => $newInfo->start_time,
|
|
|
+ 'end_time' => $newInfo->end_time,
|
|
|
+ ]);
|
|
|
}
|
|
|
}
|
|
|
|