作者 赵彬吉

聚合页 AI生成

  1 +<?php
  2 +
  3 +namespace App\Console\Commands\Tdk;
  4 +
  5 +use App\Exceptions\ValidateException;
  6 +use App\Helper\Arr;
  7 +use App\Helper\Common;
  8 +use App\Helper\Gpt;
  9 +use App\Models\Ai\AiCommand;
  10 +use App\Models\Ai\AiTdkErrorLog;
  11 +use App\Models\Com\NoticeLog;
  12 +use App\Models\Domain\DomainInfo;
  13 +use App\Models\Product\Keyword;
  14 +use App\Models\Project\AggregateKeywordAffix;
  15 +use App\Models\Project\DeployBuild;
  16 +use App\Models\Project\DeployOptimize;
  17 +use App\Models\Project\ProjectKeywordAiTask;
  18 +use App\Services\ProjectServer;
  19 +use Illuminate\Console\Command;
  20 +use Illuminate\Database\Eloquent\Model;
  21 +use Illuminate\Support\Facades\Cache;
  22 +use Illuminate\Support\Facades\DB;
  23 +use Illuminate\Support\Facades\Redis;
  24 +use Illuminate\Support\Str;
  25 +
  26 +/**
  27 + * 关键词聚合页AI生成内容
  28 + * Class InitProject
  29 + * @package App\Console\Commands
  30 + * @author zbj
  31 + * @date 2025/06/06
  32 + */
  33 +class KeywordPageAiContent extends Command
  34 +{
  35 + /**
  36 + * The name and signature of the console command.
  37 + *
  38 + * @var string
  39 + */
  40 + protected $signature = 'keyword_page_ai_content';
  41 +
  42 + /**
  43 + * The console command description.
  44 + *
  45 + * @var string
  46 + */
  47 + protected $description = '关键词聚合页AI生成内容';
  48 +
  49 + /**
  50 + * 统计图表类型 随机一个
  51 + * @var string[]
  52 + */
  53 + protected $chart_types = [
  54 + '柱状图',
  55 + '折线图',
  56 + ];
  57 +
  58 + /**
  59 + * @return bool
  60 + */
  61 + public function handle()
  62 + {
  63 + while (true) {
  64 + $task = ProjectKeywordAiTask::getPendingTask();
  65 + if (!$task) {
  66 + sleep(10);
  67 + continue;
  68 + }
  69 + $project_id = $task->project_id;
  70 +
  71 + echo getmypid() . ' ' . date('Y-m-d H:i:s') . ' start project_id: ' . $project_id . PHP_EOL;
  72 + try {
  73 + ProjectServer::useProject($project_id);
  74 +
  75 + $update_rows = $this->ai_content($task);
  76 +
  77 + DB::disconnect('custom_mysql');
  78 +
  79 + ProjectKeywordAiTask::finish($task->id, $update_rows);
  80 +
  81 + // $update_rows && $this->sendNotify($project_id);
  82 +
  83 + } catch (ValidateException $e) {
  84 + echo getmypid() . ' ' . date('Y-m-d H:i:s') . 'line: ' . $e->getLine() . ' error: ' . $project_id . '->' . $e->getMessage() . PHP_EOL;
  85 + $task->status = ProjectKeywordAiTask::STATUS_FAIL;
  86 + $task->remark = mb_substr($e->getMessage(), 0, 250);
  87 + $task->save();
  88 + } catch (\Exception $e) {
  89 + echo getmypid() . ' ' . date('Y-m-d H:i:s') . 'line: ' . $e->getLine() . ' error: ' . $project_id . '->' . $e->getMessage() . PHP_EOL;
  90 + ProjectKeywordAiTask::retry($task->id, $e->getMessage());
  91 + }
  92 + echo getmypid() . ' ' . date('Y-m-d H:i:s') . ' end project_id: ' . $project_id . PHP_EOL;
  93 + }
  94 + }
  95 +
  96 +
  97 + /**
  98 + * @param ProjectKeywordAiTask $task
  99 + * @author zbj
  100 + * @date 2025/6/6
  101 + */
  102 + public function ai_content(ProjectKeywordAiTask $task)
  103 + {
  104 + //前后缀
  105 + $affix = AggregateKeywordAffix::where('project_id', $task->project_id)->first();
  106 + $prefix = empty($affix['prefix']) ? [] : array_map('strtolower', explode("\r\n", $affix['prefix']));
  107 + $suffix = empty($affix['suffix']) ? [] : array_map('strtolower', explode("\r\n", $affix['suffix']));
  108 + if (!$prefix || !$suffix) {
  109 + throw new ValidateException('扩展标题前后缀不存在');
  110 + }
  111 + //公司英文描述
  112 + $company_en_description = DeployOptimize::where('project_id', $task->project_id)->value('company_en_description');
  113 + if (!$company_en_description) {
  114 + throw new ValidateException('公司英文描述不存在');
  115 + }
  116 + //指令
  117 + $ai_commands = AiCommand::whereIn('key', ['tag_sale_content', 'tag_count_content', 'tag_data_table'])->where('project_id', 0)->select('key', 'scene', 'ai')->get()->toArray();
  118 + $project_ai_commands = AiCommand::whereIn('key', ['tag_sale_content', 'tag_count_content', 'tag_data_table'])->where('project_id', $task->project_id)->select('key', 'scene', 'ai')->get()->toArray();
  119 + $ai_commands = Arr::setValueToKey($ai_commands, 'key');
  120 + $project_ai_commands = Arr::setValueToKey($project_ai_commands, 'key');
  121 + foreach ($ai_commands as $k => $ai_command) {
  122 + if (!empty($project_ai_commands[$k])) {
  123 + $ai_commands[$k] = $project_ai_commands[$k];
  124 + }
  125 + }
  126 +
  127 + //没有标题、文案、图表的关键词
  128 + $keyword_ids = Keyword::whereNull('sale_title')->orWhereNull('sale_content')->orWhereNull('table_html')
  129 + ->orWhereNull('count_title')->orWhereNull('count_html')
  130 + ->pluck('id')
  131 + ->toArray();
  132 + $update_rows = 0;
  133 + foreach ($keyword_ids as $id) {
  134 + //缓存 在处理的项目数据 id
  135 + $cache_key = "keyword_page_ai_content_task_lock_{$task->project_id}_{$id}";
  136 + if (!Redis::setnx($cache_key, 1)) {
  137 + continue;
  138 + }
  139 + Redis::expire($cache_key, 120);
  140 +
  141 + $keyword = Keyword::where('id', $id)->select(['id', 'title', 'sale_title', 'sale_content', 'table_html', 'count_title', 'count_html'])->first();
  142 +
  143 + echo getmypid() . ' ' . date('Y-m-d H:i:s') . ' id:' . $keyword['id'] . ' project_id:' . $task->project_id . PHP_EOL;
  144 +
  145 + $update = false;
  146 + if (empty($keyword['sale_title'])) {
  147 + $sale_title = $this->new_title($keyword['title'], $prefix, $suffix);
  148 + $keyword->sale_title = $sale_title;
  149 + $update = true;
  150 + }
  151 + if (empty($keyword['sale_content']) && $keyword->sale_title) {
  152 + $content = $this->ai_send($keyword->sale_title, $company_en_description, $ai_commands['tag_sale_content']['ai']);
  153 + if ($content) {
  154 + $keyword->sale_content = $content;
  155 + $update = true;
  156 + }
  157 + }
  158 + if (empty($keyword['table_html']) && $keyword->sale_title) {
  159 + $content = $this->ai_send($keyword->sale_title, $company_en_description, $ai_commands['tag_data_table']['ai']);
  160 + if ($content) {
  161 + $keyword->table_html = str_replace(['```html', '``html', '```'], '', $content);
  162 + $update = true;
  163 + }
  164 + }
  165 + if (empty($keyword['count_title'])) {
  166 + $count_title = $this->new_title($keyword['title'], $prefix, $suffix);
  167 + $count_title && $keyword->count_title = $count_title;
  168 + $update = true;
  169 + }
  170 + if (empty($keyword['count_html']) && $keyword->sale_title) {
  171 + $content = $this->ai_send($keyword->sale_title, $company_en_description, $ai_commands['tag_count_content']['ai']);
  172 + if ($content) {
  173 + $keyword->count_html = $this->fixChart(str_replace(['```html', '``html', '```'], '', $content));
  174 + $update = true;
  175 + }
  176 + }
  177 + if ($update) {
  178 + $keyword->save();
  179 + $update_rows++;
  180 + }
  181 + }
  182 + return $update_rows;
  183 + }
  184 +
  185 + public function new_title($title, $prefix, $suffix)
  186 + {
  187 + //打乱顺序
  188 + shuffle($prefix);
  189 + shuffle($suffix);
  190 + //标题(title):{聚合页扩展标题前缀} keywords {聚合页扩展标题后缀} {聚合页扩展标题后缀}
  191 +
  192 + return sprintf('%s %s %s %s', $prefix[0], $title, $suffix[0], $suffix[1]);
  193 + }
  194 +
  195 +
  196 + public function ai_send($title, $company_description, $prompt)
  197 + {
  198 + if (strpos($prompt, '{title}') !== false) {
  199 + $prompt = str_replace('{title}', $title, $prompt);
  200 + }
  201 + if (strpos($prompt, '{company introduction}') !== false) {
  202 + $prompt = str_replace('{company introduction}', $company_description, $prompt);
  203 + }
  204 + if (strpos($prompt, '{chart_type}') !== false) {
  205 + shuffle($this->chart_types);
  206 + dump($this->chart_types[0]);
  207 + $prompt = str_replace('{chart_type}', $this->chart_types[0], $prompt);
  208 + }
  209 +
  210 + $text = Gpt::instance()->openai_chat_qqs($prompt);
  211 + if (!$text) {
  212 + echo getmypid() . ' ' . '生成失败' . PHP_EOL;
  213 + }
  214 + return $text;
  215 + }
  216 +
  217 + function fixChart($html)
  218 + {
  219 + $html = '<body>' . $html . '</body>';
  220 + $dom = new \DOMDocument();
  221 + @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
  222 + $canvas_count = $dom->getElementsByTagName('canvas')->count();
  223 + //没有canvas
  224 + if (!$canvas_count) {
  225 + $div = $dom->getElementsByTagName('div');
  226 + foreach ($div as $element) {
  227 + if ($element->hasAttribute('id')) {
  228 + $canvas = $dom->createElement('canvas');
  229 + $canvas->setAttribute('id', $element->getAttribute('id'));
  230 + $element->removeAttribute('id');
  231 + $element->appendChild($canvas);
  232 + break;
  233 + }
  234 + }
  235 + }
  236 + $body = $dom->getElementsByTagName('body')->item(0);
  237 + $modifiedHtml = '';
  238 + foreach ($body->childNodes as $child) {
  239 + $modifiedHtml .= $dom->saveHTML($child);
  240 + }
  241 + return $modifiedHtml;
  242 + }
  243 +
  244 + public function sendNotify($project_id)
  245 + {
  246 + //获取当前项目的域名
  247 + $domainModel = new DomainInfo();
  248 + $domainInfo = $domainModel->read(['project_id' => $project_id]);
  249 + if ($domainInfo === false) {
  250 + //获取测试域名
  251 + $deployBuildModel = new DeployBuild();
  252 + $buildInfo = $deployBuildModel->read(['project_id' => $project_id]);
  253 + $domain = $buildInfo['test_domain'];
  254 + } else {
  255 + $domain = 'https://' . $domainInfo['domain'] . '/';
  256 + }
  257 + $url = $domain . 'api/update_page/';
  258 + $param = [
  259 + 'project_id' => $project_id,
  260 + 'type' => 1,
  261 + 'route' => 2,
  262 + 'url' => [],
  263 + 'language' => [],
  264 + ];
  265 + NoticeLog::createLog(NoticeLog::GENERATE_PAGE, json_encode(['c_url' => $url, 'c_params' => $param]), date('Y-m-d H:i:s', time() + 300));
  266 + echo getmypid() . ' ' . '更新中请稍后, 更新完成将会发送站内信通知更新结果!' . PHP_EOL;
  267 + }
  268 +}
  1 +<?php
  2 +
  3 +namespace App\Exceptions;
  4 +
  5 +use Exception;
  6 +
  7 +/**
  8 + * @notes: 验证
  9 + * Class ValidateException
  10 + * @package App\Exceptions
  11 + */
  12 +class ValidateException extends Exception
  13 +{
  14 +
  15 +}
  1 +<?php
  2 +
  3 +namespace App\Models\Project;
  4 +
  5 +use App\Helper\Arr;
  6 +use App\Models\Base;
  7 +use Illuminate\Support\Facades\DB;
  8 +use Illuminate\Support\Facades\Log;
  9 +use Illuminate\Support\Facades\Redis;
  10 +
  11 +class ProjectKeywordAiTask extends Base
  12 +{
  13 + //设置关联表名
  14 + protected $table = 'gl_project_keyword_ai_task';
  15 +
  16 + const STATUS_PENDING = 0;
  17 + const STATUS_SUCCESS = 1;
  18 + const STATUS_FAIL = 2;
  19 +
  20 + public static function add_task($project_id){
  21 + $task = self::where('project_id', $project_id)->where('status', self::STATUS_PENDING)->first();
  22 + if($task){
  23 + throw new \Exception('该项目有未执行的任务,请勿重复添加');
  24 + }
  25 + $model = new self();
  26 + $model->project_id = $project_id;
  27 + $model->save();
  28 +
  29 + Redis::lpush('projectKeywordAiTask', $project_id);
  30 + }
  31 +
  32 + public static function getPendingTask(){
  33 + //有其他任务 就取其他任务 没有其他任务运行未结束的任务
  34 + $project_id = Redis::rpop('projectKeywordAiTask');
  35 + $data = [];
  36 + if($project_id){
  37 + $data = self::where('status', self::STATUS_PENDING)->where('project_id', $project_id)->orderBy('id', 'asc')->first();
  38 + }
  39 + if($data){
  40 + return $data;
  41 + }
  42 + return self::where('status', self::STATUS_PENDING)->orderBy('id', 'asc')->first();
  43 + }
  44 +
  45 + /**
  46 + * 重试任务
  47 + * @param $id
  48 + * @param $remark
  49 + * @author zbj
  50 + * @date 2023/11/9
  51 + */
  52 + public static function retry($id, $remark)
  53 + {
  54 + DB::beginTransaction();
  55 + try {
  56 + //行锁 避免脏读写
  57 + $data = self::where('id', $id)->lockForUpdate()->first();
  58 + $data->retry = $data->retry + 1;
  59 + if ($data->retry > 3) {
  60 + $data->status = self::STATUS_FAIL;
  61 + }else{
  62 + $data->status = self::STATUS_PENDING;
  63 + }
  64 + $data->remark = mb_substr($remark, 0, 250);
  65 + $data->save();
  66 +
  67 + DB::commit();
  68 + } catch (\Exception $e) {
  69 + DB::rollback();
  70 + Log::error('project_keyword_ai_task retry error:' . $e->getMessage());
  71 + }
  72 + }
  73 +
  74 + /**
  75 + * 完成
  76 + * @param $id
  77 + * @param $update_rows
  78 + * @author zbj
  79 + * @date 2023/11/9
  80 + */
  81 + public static function finish($id, $update_rows){
  82 + DB::beginTransaction();
  83 + try {
  84 + //行锁 避免脏读写
  85 + $data = self::where('id', $id)->lockForUpdate()->first();
  86 + $data->status = self::STATUS_SUCCESS;
  87 + $data->update_rows = $update_rows;
  88 + $data->save();
  89 + DB::commit();
  90 + } catch (\Exception $e) {
  91 + DB::rollback();
  92 + Log::error('project_keyword_ai_task finish error:' . $e->getMessage());
  93 + }
  94 + }
  95 +}