no message
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\command;
|
||||
|
||||
use app\common\service\article\ArticleAiCommentService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
|
||||
class ArticleAiComment extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('article_ai_comment')
|
||||
->setDescription('资讯AI评论定时任务');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$summary = ArticleAiCommentService::tick();
|
||||
|
||||
$output->writeln(sprintf(
|
||||
'[ArticleAiComment] 虚拟账号 %d 个,新增任务 %d 个,已执行 %d 个',
|
||||
$summary['virtual_user_count'] ?? 0,
|
||||
$summary['seeded']['created'] ?? 0,
|
||||
$summary['processed']['done'] ?? 0
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\console\input\Argument;
|
||||
|
||||
class CrawlerCommand extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('crawler')
|
||||
->addArgument('action', Argument::REQUIRED, '执行动作: standings/schedule/all/menu')
|
||||
->setDescription('懂球帝数据爬虫');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$action = $input->getArgument('action');
|
||||
$allowed = [
|
||||
'standings',
|
||||
'schedule',
|
||||
'all',
|
||||
'menu',
|
||||
'news',
|
||||
'league_news',
|
||||
'article_content',
|
||||
'live',
|
||||
'lottery',
|
||||
'lottery_draw',
|
||||
'lottery_draw_force',
|
||||
'match_data',
|
||||
'init-db',
|
||||
'test',
|
||||
'csl_match',
|
||||
'nba_match',
|
||||
'cba_match',
|
||||
'epl_match',
|
||||
'bundesliga_match',
|
||||
'laliga_match',
|
||||
'seriea_match',
|
||||
'ligue1_match',
|
||||
'ucl_match',
|
||||
'uel_match',
|
||||
'tennis_match',
|
||||
'esports_match',
|
||||
'sports_match',
|
||||
'truth_social',
|
||||
'crypto_news',
|
||||
'lottery_news',
|
||||
'taiwan_lottery_news',
|
||||
'hkjc_lottery_news',
|
||||
'lottery_community',
|
||||
'nba_news',
|
||||
'cba_news',
|
||||
'article_content_fetch',
|
||||
'fifa_worldcup_news',
|
||||
'dqd_worldcup',
|
||||
'match_finish',
|
||||
'live_detail',
|
||||
];
|
||||
if (!in_array($action, $allowed)) {
|
||||
$output->writeln("<error>不支持的动作: {$action}, 可选: " . implode('/', $allowed) . "</error>");
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件锁防止同一动作并发执行
|
||||
$lockFile = runtime_path() . "crawler_{$action}.lock";
|
||||
$fp = fopen($lockFile, 'w');
|
||||
if (!flock($fp, LOCK_EX | LOCK_NB)) {
|
||||
$output->writeln("<comment>[Crawler] {$action} 正在执行中,跳过本次</comment>");
|
||||
fclose($fp);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$crawlerDir = app()->getRootPath() . 'public/dongqiudi-crawler';
|
||||
$venvPython = $crawlerDir . '/venv/bin/python3';
|
||||
$python = file_exists($venvPython) ? $venvPython : 'python3';
|
||||
$cmd = "cd {$crawlerDir} && {$python} main.py {$action} 2>&1";
|
||||
|
||||
$output->writeln("<info>[Crawler] 执行: {$cmd}</info>");
|
||||
$result = shell_exec($cmd);
|
||||
$output->writeln($result ?: '(无输出)');
|
||||
} finally {
|
||||
flock($fp, LOCK_UN);
|
||||
fclose($fp);
|
||||
@unlink($lockFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||
// | 开源版本可自由商用,可去除界面版权logo
|
||||
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||
// | 访问官网:https://www.likeadmin.cn
|
||||
// | likeadmin团队 版权所有 拥有最终解释权
|
||||
// +----------------------------------------------------------------------
|
||||
// | author: likeadminTeam
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\common\command;
|
||||
|
||||
use app\common\enum\CrontabEnum;
|
||||
use app\common\model\CrontabLog;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use Cron\CronExpression;
|
||||
use think\facade\Console;
|
||||
use app\common\model\Crontab as CrontabModel;
|
||||
use app\common\service\crontab\CrontabAlertService;
|
||||
|
||||
/**
|
||||
* 定时任务
|
||||
* Class Crontab
|
||||
* @package app\command
|
||||
*/
|
||||
class Crontab extends Command
|
||||
{
|
||||
protected const CRAWLER_DEFAULT_TIMEOUT = 600;
|
||||
|
||||
protected const CRAWLER_ACTION_TIMEOUTS = [
|
||||
'csl_match' => 180,
|
||||
'nba_match' => 180,
|
||||
'cba_match' => 180,
|
||||
'epl_match' => 180,
|
||||
'bundesliga_match' => 180,
|
||||
'laliga_match' => 180,
|
||||
'seriea_match' => 180,
|
||||
'ligue1_match' => 180,
|
||||
'ucl_match' => 180,
|
||||
'uel_match' => 180,
|
||||
'tennis_match' => 180,
|
||||
'esports_match' => 180,
|
||||
'sports_match' => 180,
|
||||
'live_detail' => 180,
|
||||
'match_finish' => 180,
|
||||
'lottery_draw' => 180,
|
||||
'lottery_draw_force' => 180,
|
||||
'article_content' => 600,
|
||||
'article_content_fetch' => 900,
|
||||
'league_news' => 900,
|
||||
'fifa_worldcup_news' => 600,
|
||||
'dqd_worldcup' => 600,
|
||||
];
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('crontab')
|
||||
->setDescription('定时任务');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$lists = CrontabModel::where('status', CrontabEnum::START)->select()->toArray();
|
||||
if (empty($lists)) {
|
||||
return false;
|
||||
}
|
||||
$time = time();
|
||||
foreach ($lists as $item) {
|
||||
if (empty($item['last_time'])) {
|
||||
$lastTime = (new CronExpression($item['expression']))
|
||||
->getNextRunDate()
|
||||
->getTimestamp();
|
||||
CrontabModel::where('id', $item['id'])->update([
|
||||
'last_time' => $lastTime,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$nextTime = (new CronExpression($item['expression']))
|
||||
->getNextRunDate($item['last_time'])
|
||||
->getTimestamp();
|
||||
if ($nextTime > $time) {
|
||||
// 未到时间,不执行
|
||||
continue;
|
||||
}
|
||||
// 开始执行
|
||||
self::start($item);
|
||||
}
|
||||
}
|
||||
|
||||
public static function start($item)
|
||||
{
|
||||
if ($item['command'] === 'crawler') {
|
||||
self::startCrawler($item);
|
||||
return;
|
||||
}
|
||||
|
||||
$startTime = microtime(true);
|
||||
$error = '';
|
||||
$outputStr = '';
|
||||
try {
|
||||
ob_start();
|
||||
$params = explode(' ', $item['params']);
|
||||
if (is_array($params) && !empty($item['params'])) {
|
||||
Console::call($item['command'], $params);
|
||||
} else {
|
||||
Console::call($item['command']);
|
||||
}
|
||||
$outputStr = ob_get_clean() ?: '';
|
||||
CrontabModel::where('id', $item['id'])->update(['error' => '']);
|
||||
} catch (\Exception $e) {
|
||||
$outputStr = ob_get_clean() ?: '';
|
||||
$error = $e->getMessage();
|
||||
CrontabModel::where('id', $item['id'])->update([
|
||||
'error' => $error,
|
||||
'status' => CrontabEnum::ERROR
|
||||
]);
|
||||
} finally {
|
||||
$endTime = microtime(true);
|
||||
$useTime = round(($endTime - $startTime), 2);
|
||||
$maxTime = max($useTime, $item['max_time']);
|
||||
$logId = 0;
|
||||
CrontabModel::where('id', $item['id'])->update([
|
||||
'last_time' => time(),
|
||||
'time' => $useTime,
|
||||
'max_time' => $maxTime
|
||||
]);
|
||||
|
||||
try {
|
||||
$log = CrontabLog::create([
|
||||
'crontab_id' => $item['id'],
|
||||
'name' => $item['name'],
|
||||
'command' => $item['command'],
|
||||
'params' => $item['params'],
|
||||
'status' => $error ? 2 : 1,
|
||||
'output' => $error ?: mb_substr($outputStr ?: '执行完成', 0, 5000),
|
||||
'elapsed' => $useTime,
|
||||
'trigger_type' => 1,
|
||||
]);
|
||||
$logId = (int) $log->id;
|
||||
} catch (\Exception $logEx) {
|
||||
}
|
||||
|
||||
try {
|
||||
if ($error) {
|
||||
CrontabAlertService::upsertCommandException($item, $error ?: $outputStr, $logId);
|
||||
} else {
|
||||
CrontabAlertService::resolveByCrontabId((int) $item['id']);
|
||||
}
|
||||
} catch (\Exception $alertEx) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static function startCrawler($item)
|
||||
{
|
||||
$crawlerDir = app()->getRootPath() . 'public/dongqiudi-crawler';
|
||||
$python = is_file($crawlerDir . '/venv/bin/python3') ? $crawlerDir . '/venv/bin/python3' : 'python3';
|
||||
$action = $item['params'] ?: 'all';
|
||||
$timeout = self::getCrawlerActionTimeout($action);
|
||||
$now = time();
|
||||
|
||||
self::markStaleCrawlerLogs($item, $timeout, $now);
|
||||
|
||||
$running = CrontabLog::where('crontab_id', $item['id'])
|
||||
->where('status', 0)
|
||||
->where('create_time', '>', $now - $timeout)
|
||||
->order('id', 'desc')
|
||||
->findOrEmpty();
|
||||
if (!$running->isEmpty()) {
|
||||
CrontabModel::where('id', $item['id'])->update([
|
||||
'last_time' => $now,
|
||||
'error' => '上一次任务仍在执行,已跳过本次',
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$log = CrontabLog::create([
|
||||
'crontab_id' => $item['id'],
|
||||
'name' => $item['name'],
|
||||
'command' => $item['command'],
|
||||
'params' => $item['params'],
|
||||
'status' => 0,
|
||||
'output' => '',
|
||||
'elapsed' => 0,
|
||||
'trigger_type' => 1,
|
||||
]);
|
||||
|
||||
$logId = $log->id;
|
||||
$crontabId = $item['id'];
|
||||
$cmd = "cd {$crawlerDir} && nohup {$python} main.py {$action} --log-id={$logId} --crontab-id={$crontabId} > /dev/null 2>&1 &";
|
||||
\shell_exec($cmd);
|
||||
|
||||
CrontabModel::where('id', $item['id'])->update([
|
||||
'last_time' => time(),
|
||||
'error' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
protected static function getCrawlerActionTimeout(string $action): int
|
||||
{
|
||||
return self::CRAWLER_ACTION_TIMEOUTS[$action] ?? self::CRAWLER_DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
protected static function markStaleCrawlerLogs(array $item, int $timeout, int $now): void
|
||||
{
|
||||
$staleIds = CrontabLog::where('crontab_id', $item['id'])
|
||||
->where('status', 0)
|
||||
->where('create_time', '<=', $now - $timeout)
|
||||
->column('id');
|
||||
if (empty($staleIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = sprintf('执行超过 %d 秒未结束,已由调度器标记超时', $timeout);
|
||||
CrontabLog::whereIn('id', $staleIds)->update([
|
||||
'status' => 2,
|
||||
'output' => $message,
|
||||
'elapsed' => $timeout,
|
||||
]);
|
||||
CrontabModel::where('id', $item['id'])->update([
|
||||
'error' => $message,
|
||||
]);
|
||||
|
||||
try {
|
||||
CrontabAlertService::upsertCommandException($item, $message, (int) end($staleIds));
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\command;
|
||||
|
||||
use app\common\model\ai\AiKbSyncJob;
|
||||
use app\common\service\ai\KbService;
|
||||
use app\common\service\ai\KbSyncService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\console\input\Option;
|
||||
|
||||
class KbConsume extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('kb:consume')
|
||||
->setDescription('消费AI知识库同步任务')
|
||||
->addOption('batch', null, Option::VALUE_OPTIONAL, '批量处理数量', 100);
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$batch = max(1, (int) $input->getOption('batch'));
|
||||
KbSyncService::markLotteryBackfillJobs($batch);
|
||||
KbSyncService::markMatchBackfillJobs($batch);
|
||||
|
||||
$jobs = AiKbSyncJob::where('status', AiKbSyncJob::STATUS_PENDING)
|
||||
->where('scheduled_at', '<=', time())
|
||||
->order('priority', 'asc')
|
||||
->order('id', 'asc')
|
||||
->limit($batch)
|
||||
->select();
|
||||
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($jobs as $job) {
|
||||
$job->status = AiKbSyncJob::STATUS_PROCESSING;
|
||||
$job->update_time = time();
|
||||
$job->save();
|
||||
|
||||
try {
|
||||
if ($job->action === AiKbSyncJob::ACTION_DELETE) {
|
||||
KbService::deleteDocument($job->domain, $job->subtype, (int) $job->source_id);
|
||||
$result = ['success' => true];
|
||||
} else {
|
||||
$result = KbService::upsertDocument($job->domain, $job->subtype, (int) $job->source_id);
|
||||
}
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
$job->status = AiKbSyncJob::STATUS_SUCCESS;
|
||||
$job->processed_at = time();
|
||||
$job->error_msg = '';
|
||||
$success++;
|
||||
} else {
|
||||
$job->status = AiKbSyncJob::STATUS_FAILED;
|
||||
$job->retry_count = (int) $job->retry_count + 1;
|
||||
$job->error_msg = $result['error'] ?? 'unknown';
|
||||
$failed++;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$job->status = AiKbSyncJob::STATUS_FAILED;
|
||||
$job->retry_count = (int) $job->retry_count + 1;
|
||||
$job->error_msg = $e->getMessage();
|
||||
$failed++;
|
||||
}
|
||||
|
||||
$job->update_time = time();
|
||||
$job->save();
|
||||
}
|
||||
|
||||
$output->writeln(sprintf('[kb:consume] success=%d failed=%d jobs=%d', $success, $failed, count($jobs)));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\command;
|
||||
|
||||
use app\common\service\ai\KbService;
|
||||
use app\common\service\ai\KbSyncService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\console\input\Option;
|
||||
use think\facade\Db;
|
||||
|
||||
class KbRebuild extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('kb:rebuild')
|
||||
->setDescription('重建AI知识库文档')
|
||||
->addOption('domain', null, Option::VALUE_OPTIONAL, 'article|post|lottery|match|all', 'all');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$domain = (string) $input->getOption('domain');
|
||||
$targets = $domain === 'all' ? ['article', 'post', 'lottery', 'match'] : [$domain];
|
||||
$summary = ['success' => 0, 'failed' => 0];
|
||||
|
||||
foreach ($targets as $target) {
|
||||
foreach ($this->yieldRows($target) as $row) {
|
||||
$result = KbService::upsertDocument($row['domain'], $row['subtype'], (int) $row['source_id']);
|
||||
if (!empty($result['success'])) {
|
||||
$summary['success']++;
|
||||
} else {
|
||||
$summary['failed']++;
|
||||
$output->writeln(sprintf(
|
||||
'[failed] %s/%s/%d %s',
|
||||
$row['domain'],
|
||||
$row['subtype'],
|
||||
$row['source_id'],
|
||||
$result['error'] ?? 'unknown'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KbSyncService::markLotteryBackfillJobs();
|
||||
$output->writeln(sprintf('[kb:rebuild] success=%d failed=%d', $summary['success'], $summary['failed']));
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function yieldRows(string $domain): \Generator
|
||||
{
|
||||
if ($domain === 'article') {
|
||||
yield from $this->yieldTableRows('article', 'article', 'article');
|
||||
return;
|
||||
}
|
||||
if ($domain === 'post') {
|
||||
yield from $this->yieldTableRows('community_post', 'post', 'post');
|
||||
return;
|
||||
}
|
||||
if ($domain === 'lottery') {
|
||||
yield from $this->yieldTableRows('lottery_draw_result', 'lottery', 'draw_result');
|
||||
yield from $this->yieldTableRows('lottery_ai_analysis', 'lottery', 'ai_history');
|
||||
return;
|
||||
}
|
||||
if ($domain === 'match') {
|
||||
yield from $this->yieldTableRows('match', 'match', 'event');
|
||||
}
|
||||
}
|
||||
|
||||
private function yieldTableRows(string $table, string $domain, string $subtype): \Generator
|
||||
{
|
||||
$lastId = 0;
|
||||
$chunkSize = 5000;
|
||||
|
||||
while (true) {
|
||||
$ids = Db::name($table)
|
||||
->where('id', '>', $lastId)
|
||||
->order('id', 'asc')
|
||||
->limit($chunkSize)
|
||||
->column('id');
|
||||
|
||||
if (empty($ids)) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$lastId = (int) $id;
|
||||
yield [
|
||||
'domain' => $domain,
|
||||
'subtype' => $subtype,
|
||||
'source_id' => $lastId,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||
// | 开源版本可自由商用,可去除界面版权logo
|
||||
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||
// | 访问官网:https://www.likeadmin.cn
|
||||
// | likeadmin团队 版权所有 拥有最终解释权
|
||||
// +----------------------------------------------------------------------
|
||||
// | author: likeadminTeam
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\common\command;
|
||||
|
||||
use app\common\enum\PayEnum;
|
||||
use app\common\enum\RefundEnum;
|
||||
use app\common\model\recharge\RechargeOrder;
|
||||
use app\common\model\refund\RefundLog;
|
||||
use app\common\model\refund\RefundRecord;
|
||||
use app\common\service\pay\WeChatPayService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\Log;
|
||||
|
||||
|
||||
class QueryRefund extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('query_refund')
|
||||
->setDescription('订单退款状态处理');
|
||||
}
|
||||
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
try {
|
||||
// 查找退款中的退款记录(微信,支付宝支付)
|
||||
$refundRecords = (new RefundLog())->alias('l')
|
||||
->join('refund_record r', 'r.id = l.record_id')
|
||||
->field([
|
||||
'l.id' => 'log_id', 'l.sn' => 'log_sn',
|
||||
'r.id' => 'record_id', 'r.order_id', 'r.sn' => 'record_sn', 'r.order_type'
|
||||
])
|
||||
->where(['l.refund_status' => RefundEnum::REFUND_ING])
|
||||
->select()->toArray();
|
||||
|
||||
if (empty($refundRecords)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 分别处理各个类型订单
|
||||
$rechargeRecords = array_filter($refundRecords, function ($item) {
|
||||
return $item['order_type'] == RefundEnum::ORDER_TYPE_RECHARGE;
|
||||
});
|
||||
|
||||
if (!empty($rechargeRecords)) {
|
||||
$this->handleRechargeOrder($rechargeRecords);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::write('订单退款状态查询失败,失败原因:' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notes 处理充值订单
|
||||
* @param $refundRecords
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @author 段誉
|
||||
* @date 2023/3/1 15:55
|
||||
*/
|
||||
public function handleRechargeOrder($refundRecords)
|
||||
{
|
||||
$orderIds = array_unique(array_column($refundRecords, 'order_id'));
|
||||
$Orders = RechargeOrder::whereIn('id', $orderIds)->column('*', 'id');
|
||||
|
||||
foreach ($refundRecords as $record) {
|
||||
if (!isset($Orders[$record['order_id']])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$order = $Orders[$record['order_id']];
|
||||
if (!in_array($order['pay_way'], [PayEnum::WECHAT_PAY, PayEnum::ALI_PAY])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->checkReFundStatus([
|
||||
'record_id' => $record['record_id'],
|
||||
'log_id' => $record['log_id'],
|
||||
'log_sn' => $record['log_sn'],
|
||||
'pay_way' => $order['pay_way'],
|
||||
'order_terminal' => $order['order_terminal'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notes 校验退款状态
|
||||
* @param $refundData
|
||||
* @return bool
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @author 段誉
|
||||
* @date 2023/3/1 15:54
|
||||
*/
|
||||
public function checkReFundStatus($refundData)
|
||||
{
|
||||
$result = null;
|
||||
switch ($refundData['pay_way']) {
|
||||
case PayEnum::WECHAT_PAY:
|
||||
$result = self::checkWechatRefund($refundData['order_terminal'], $refundData['log_sn']);
|
||||
break;
|
||||
}
|
||||
|
||||
if (is_null($result)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === $result) {
|
||||
$this->updateRefundSuccess($refundData['log_id'], $refundData['record_id']);
|
||||
} else {
|
||||
$this->updateRefundMsg($refundData['log_id'], $result);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notes 查询微信支付退款状态
|
||||
* @param $orderTerminal
|
||||
* @param $refundLogSn
|
||||
* @return bool|string|null
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @author 段誉
|
||||
* @date 2023/3/1 15:47
|
||||
*/
|
||||
public function checkWechatRefund($orderTerminal, $refundLogSn)
|
||||
{
|
||||
// 根据商户退款单号查询退款
|
||||
$result = (new WeChatPayService($orderTerminal))->queryRefund($refundLogSn);
|
||||
|
||||
if (!empty($result['status']) && $result['status'] == 'SUCCESS') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($result['code']) || !empty($result['message'])) {
|
||||
return '微信:' . $result['code'] . '-' . $result['message'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notes 更新记录为成功
|
||||
* @param $logId
|
||||
* @param $recordId
|
||||
* @author 段誉
|
||||
* @date 2023/3/1 15:38
|
||||
*/
|
||||
public function updateRefundSuccess($logId, $recordId)
|
||||
{
|
||||
// 更新日志
|
||||
RefundLog::update([
|
||||
'id' => $logId,
|
||||
'refund_status' => RefundEnum::REFUND_SUCCESS,
|
||||
]);
|
||||
// 更新记录
|
||||
RefundRecord::update([
|
||||
'id' => $recordId,
|
||||
'refund_status' => RefundEnum::REFUND_SUCCESS,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notes 更新退款信息
|
||||
* @param $logId
|
||||
* @param $msg
|
||||
* @author 段誉
|
||||
* @date 2023/3/1 15:47
|
||||
*/
|
||||
public function updateRefundMsg($logId, $msg)
|
||||
{
|
||||
// 更新日志
|
||||
RefundLog::update([
|
||||
'id' => $logId,
|
||||
'refund_msg' => $msg,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\command;
|
||||
|
||||
use app\common\model\user\User;
|
||||
use app\common\model\SmsLog;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* 短信验证定时任务
|
||||
* 轮询有手机号但未验证(is_bind_mobile=0)的用户,
|
||||
* 从 la_sms_upload 反向校验是否有该手机号的任意上传记录,
|
||||
* 若匹配则标记 is_bind_mobile=1
|
||||
*/
|
||||
class SmsVerifyCommand extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('sms_verify')
|
||||
->setDescription('轮询未验证手机号用户,从上传记录反向校验并更新绑定状态');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$users = User::where('mobile', '<>', '')
|
||||
->where('is_bind_mobile', 0)
|
||||
->field('id, mobile')
|
||||
->select();
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
$output->writeln('<info>[SmsVerify] 无待验证用户</info>');
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln('<info>[SmsVerify] 待验证用户: ' . $users->count() . '</info>');
|
||||
$updated = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
$mobile = $user->mobile;
|
||||
|
||||
// 查询 la_sms_upload 是否有该手机号的任意上传记录(兼容+86前缀)
|
||||
$record = SmsLog::where(function ($query) use ($mobile) {
|
||||
$query->where('phone', $mobile)
|
||||
->whereOr('phone', '+86' . $mobile)
|
||||
->whereOr('phone', '86' . $mobile);
|
||||
})
|
||||
->order('sms_time', 'desc')
|
||||
->findOrEmpty();
|
||||
|
||||
if (!$record->isEmpty()) {
|
||||
if ((int)$record->status === 0) {
|
||||
$record->status = 1;
|
||||
$record->save();
|
||||
}
|
||||
User::where('id', $user->id)->update(['is_bind_mobile' => 1]);
|
||||
$updated++;
|
||||
$output->writeln("<info> ✓ 用户 {$user->id} ({$mobile}) 已验证</info>");
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln("<info>[SmsVerify] 完成,更新 {$updated} 个用户</info>");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\command;
|
||||
|
||||
use app\api\logic\ArticleLogic;
|
||||
use app\common\enum\YesNoEnum;
|
||||
use app\common\model\article\Article;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
|
||||
class WorldcupArticleTranslate extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('worldcup_article_translate')
|
||||
->setDescription('预热世界杯英文资讯翻译缓存');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$articles = Article::field('id,cid,title,desc,content,ext,is_show,delete_time')
|
||||
->where('cid', 22)
|
||||
->where('is_show', YesNoEnum::YES)
|
||||
// 与前台世界杯资讯列表保持一致,只预热用户最先看到的 50 条。
|
||||
->order('published_at', 'desc')
|
||||
->order('id', 'desc')
|
||||
->limit(50)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
if (empty($articles)) {
|
||||
$output->writeln('<comment>[WorldcupTranslate] 没有找到可处理的世界杯文章</comment>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$translated = 0;
|
||||
$skippedExist = 0;
|
||||
$skippedNonEnglish = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($articles as $article) {
|
||||
$ext = $this->decodeExt($article['ext'] ?? []);
|
||||
if (!empty($ext['translated_content'])) {
|
||||
$skippedExist++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->isProbablyEnglish($article)) {
|
||||
$skippedNonEnglish++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = ArticleLogic::autoTranslateArticle($article);
|
||||
if ($result !== '') {
|
||||
$translated++;
|
||||
$output->writeln(sprintf('[WorldcupTranslate] 已翻译文章 #%d %s', $article['id'], $article['title'] ?? ''));
|
||||
} else {
|
||||
$failed++;
|
||||
$output->writeln(sprintf('<comment>[WorldcupTranslate] 翻译失败或无结果 #%d %s</comment>', $article['id'], $article['title'] ?? ''));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
$output->writeln(sprintf('<error>[WorldcupTranslate] 异常 #%d %s - %s</error>', $article['id'], $article['title'] ?? '', $e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln(sprintf(
|
||||
'[WorldcupTranslate] 完成,翻译 %d 条,已存在 %d 条,跳过非英文 %d 条,失败 %d 条',
|
||||
$translated,
|
||||
$skippedExist,
|
||||
$skippedNonEnglish,
|
||||
$failed
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function isProbablyEnglish(array $article): bool
|
||||
{
|
||||
$text = trim(implode(' ', [
|
||||
(string) ($article['title'] ?? ''),
|
||||
(string) ($article['desc'] ?? ''),
|
||||
$this->normalizeText((string) ($article['content'] ?? '')),
|
||||
]));
|
||||
|
||||
if ($text === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preg_match('/[\x{4e00}-\x{9fff}]/u', $text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$letterCount = preg_match_all('/[A-Za-z]/', $text);
|
||||
$wordCount = preg_match_all('/\b[A-Za-z]{2,}\b/', $text);
|
||||
|
||||
// 世界杯资讯里不少正文并不长,阈值过高会误杀正常英文稿件。
|
||||
return $letterCount >= 30 && $wordCount >= 5;
|
||||
}
|
||||
|
||||
private function normalizeText(string $content): string
|
||||
{
|
||||
if ($content === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$content = strip_tags($content);
|
||||
$content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$content = preg_replace('/\s+/u', ' ', $content);
|
||||
|
||||
return trim($content);
|
||||
}
|
||||
|
||||
private function decodeExt($ext): array
|
||||
{
|
||||
if (is_array($ext)) {
|
||||
return $ext;
|
||||
}
|
||||
|
||||
if (is_string($ext) && $ext !== '') {
|
||||
$data = json_decode($ext, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user