正在显示
3 个修改的文件
包含
378 行增加
和
0 行删除
| 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 | +} |
app/Exceptions/ValidateException.php
0 → 100644
app/Models/Project/ProjectKeywordAiTask.php
0 → 100644
| 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 | +} |
-
请 注册 或 登录 后发表评论