作者 赵彬吉

聚合页 AI生成

<?php
namespace App\Console\Commands\Tdk;
use App\Exceptions\ValidateException;
use App\Helper\Arr;
use App\Helper\Common;
use App\Helper\Gpt;
use App\Models\Ai\AiCommand;
use App\Models\Ai\AiTdkErrorLog;
use App\Models\Com\NoticeLog;
use App\Models\Domain\DomainInfo;
use App\Models\Product\Keyword;
use App\Models\Project\AggregateKeywordAffix;
use App\Models\Project\DeployBuild;
use App\Models\Project\DeployOptimize;
use App\Models\Project\ProjectKeywordAiTask;
use App\Services\ProjectServer;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;
/**
* 关键词聚合页AI生成内容
* Class InitProject
* @package App\Console\Commands
* @author zbj
* @date 2025/06/06
*/
class KeywordPageAiContent extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'keyword_page_ai_content';
/**
* The console command description.
*
* @var string
*/
protected $description = '关键词聚合页AI生成内容';
/**
* 统计图表类型 随机一个
* @var string[]
*/
protected $chart_types = [
'柱状图',
'折线图',
];
/**
* @return bool
*/
public function handle()
{
while (true) {
$task = ProjectKeywordAiTask::getPendingTask();
if (!$task) {
sleep(10);
continue;
}
$project_id = $task->project_id;
echo getmypid() . ' ' . date('Y-m-d H:i:s') . ' start project_id: ' . $project_id . PHP_EOL;
try {
ProjectServer::useProject($project_id);
$update_rows = $this->ai_content($task);
DB::disconnect('custom_mysql');
ProjectKeywordAiTask::finish($task->id, $update_rows);
// $update_rows && $this->sendNotify($project_id);
} catch (ValidateException $e) {
echo getmypid() . ' ' . date('Y-m-d H:i:s') . 'line: ' . $e->getLine() . ' error: ' . $project_id . '->' . $e->getMessage() . PHP_EOL;
$task->status = ProjectKeywordAiTask::STATUS_FAIL;
$task->remark = mb_substr($e->getMessage(), 0, 250);
$task->save();
} catch (\Exception $e) {
echo getmypid() . ' ' . date('Y-m-d H:i:s') . 'line: ' . $e->getLine() . ' error: ' . $project_id . '->' . $e->getMessage() . PHP_EOL;
ProjectKeywordAiTask::retry($task->id, $e->getMessage());
}
echo getmypid() . ' ' . date('Y-m-d H:i:s') . ' end project_id: ' . $project_id . PHP_EOL;
}
}
/**
* @param ProjectKeywordAiTask $task
* @author zbj
* @date 2025/6/6
*/
public function ai_content(ProjectKeywordAiTask $task)
{
//前后缀
$affix = AggregateKeywordAffix::where('project_id', $task->project_id)->first();
$prefix = empty($affix['prefix']) ? [] : array_map('strtolower', explode("\r\n", $affix['prefix']));
$suffix = empty($affix['suffix']) ? [] : array_map('strtolower', explode("\r\n", $affix['suffix']));
if (!$prefix || !$suffix) {
throw new ValidateException('扩展标题前后缀不存在');
}
//公司英文描述
$company_en_description = DeployOptimize::where('project_id', $task->project_id)->value('company_en_description');
if (!$company_en_description) {
throw new ValidateException('公司英文描述不存在');
}
//指令
$ai_commands = AiCommand::whereIn('key', ['tag_sale_content', 'tag_count_content', 'tag_data_table'])->where('project_id', 0)->select('key', 'scene', 'ai')->get()->toArray();
$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();
$ai_commands = Arr::setValueToKey($ai_commands, 'key');
$project_ai_commands = Arr::setValueToKey($project_ai_commands, 'key');
foreach ($ai_commands as $k => $ai_command) {
if (!empty($project_ai_commands[$k])) {
$ai_commands[$k] = $project_ai_commands[$k];
}
}
//没有标题、文案、图表的关键词
$keyword_ids = Keyword::whereNull('sale_title')->orWhereNull('sale_content')->orWhereNull('table_html')
->orWhereNull('count_title')->orWhereNull('count_html')
->pluck('id')
->toArray();
$update_rows = 0;
foreach ($keyword_ids as $id) {
//缓存 在处理的项目数据 id
$cache_key = "keyword_page_ai_content_task_lock_{$task->project_id}_{$id}";
if (!Redis::setnx($cache_key, 1)) {
continue;
}
Redis::expire($cache_key, 120);
$keyword = Keyword::where('id', $id)->select(['id', 'title', 'sale_title', 'sale_content', 'table_html', 'count_title', 'count_html'])->first();
echo getmypid() . ' ' . date('Y-m-d H:i:s') . ' id:' . $keyword['id'] . ' project_id:' . $task->project_id . PHP_EOL;
$update = false;
if (empty($keyword['sale_title'])) {
$sale_title = $this->new_title($keyword['title'], $prefix, $suffix);
$keyword->sale_title = $sale_title;
$update = true;
}
if (empty($keyword['sale_content']) && $keyword->sale_title) {
$content = $this->ai_send($keyword->sale_title, $company_en_description, $ai_commands['tag_sale_content']['ai']);
if ($content) {
$keyword->sale_content = $content;
$update = true;
}
}
if (empty($keyword['table_html']) && $keyword->sale_title) {
$content = $this->ai_send($keyword->sale_title, $company_en_description, $ai_commands['tag_data_table']['ai']);
if ($content) {
$keyword->table_html = str_replace(['```html', '``html', '```'], '', $content);
$update = true;
}
}
if (empty($keyword['count_title'])) {
$count_title = $this->new_title($keyword['title'], $prefix, $suffix);
$count_title && $keyword->count_title = $count_title;
$update = true;
}
if (empty($keyword['count_html']) && $keyword->sale_title) {
$content = $this->ai_send($keyword->sale_title, $company_en_description, $ai_commands['tag_count_content']['ai']);
if ($content) {
$keyword->count_html = $this->fixChart(str_replace(['```html', '``html', '```'], '', $content));
$update = true;
}
}
if ($update) {
$keyword->save();
$update_rows++;
}
}
return $update_rows;
}
public function new_title($title, $prefix, $suffix)
{
//打乱顺序
shuffle($prefix);
shuffle($suffix);
//标题(title):{聚合页扩展标题前缀} keywords {聚合页扩展标题后缀} {聚合页扩展标题后缀}
return sprintf('%s %s %s %s', $prefix[0], $title, $suffix[0], $suffix[1]);
}
public function ai_send($title, $company_description, $prompt)
{
if (strpos($prompt, '{title}') !== false) {
$prompt = str_replace('{title}', $title, $prompt);
}
if (strpos($prompt, '{company introduction}') !== false) {
$prompt = str_replace('{company introduction}', $company_description, $prompt);
}
if (strpos($prompt, '{chart_type}') !== false) {
shuffle($this->chart_types);
dump($this->chart_types[0]);
$prompt = str_replace('{chart_type}', $this->chart_types[0], $prompt);
}
$text = Gpt::instance()->openai_chat_qqs($prompt);
if (!$text) {
echo getmypid() . ' ' . '生成失败' . PHP_EOL;
}
return $text;
}
function fixChart($html)
{
$html = '<body>' . $html . '</body>';
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$canvas_count = $dom->getElementsByTagName('canvas')->count();
//没有canvas
if (!$canvas_count) {
$div = $dom->getElementsByTagName('div');
foreach ($div as $element) {
if ($element->hasAttribute('id')) {
$canvas = $dom->createElement('canvas');
$canvas->setAttribute('id', $element->getAttribute('id'));
$element->removeAttribute('id');
$element->appendChild($canvas);
break;
}
}
}
$body = $dom->getElementsByTagName('body')->item(0);
$modifiedHtml = '';
foreach ($body->childNodes as $child) {
$modifiedHtml .= $dom->saveHTML($child);
}
return $modifiedHtml;
}
public function sendNotify($project_id)
{
//获取当前项目的域名
$domainModel = new DomainInfo();
$domainInfo = $domainModel->read(['project_id' => $project_id]);
if ($domainInfo === false) {
//获取测试域名
$deployBuildModel = new DeployBuild();
$buildInfo = $deployBuildModel->read(['project_id' => $project_id]);
$domain = $buildInfo['test_domain'];
} else {
$domain = 'https://' . $domainInfo['domain'] . '/';
}
$url = $domain . 'api/update_page/';
$param = [
'project_id' => $project_id,
'type' => 1,
'route' => 2,
'url' => [],
'language' => [],
];
NoticeLog::createLog(NoticeLog::GENERATE_PAGE, json_encode(['c_url' => $url, 'c_params' => $param]), date('Y-m-d H:i:s', time() + 300));
echo getmypid() . ' ' . '更新中请稍后, 更新完成将会发送站内信通知更新结果!' . PHP_EOL;
}
}
... ...
<?php
namespace App\Exceptions;
use Exception;
/**
* @notes: 验证
* Class ValidateException
* @package App\Exceptions
*/
class ValidateException extends Exception
{
}
... ...
<?php
namespace App\Models\Project;
use App\Helper\Arr;
use App\Models\Base;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
class ProjectKeywordAiTask extends Base
{
//设置关联表名
protected $table = 'gl_project_keyword_ai_task';
const STATUS_PENDING = 0;
const STATUS_SUCCESS = 1;
const STATUS_FAIL = 2;
public static function add_task($project_id){
$task = self::where('project_id', $project_id)->where('status', self::STATUS_PENDING)->first();
if($task){
throw new \Exception('该项目有未执行的任务,请勿重复添加');
}
$model = new self();
$model->project_id = $project_id;
$model->save();
Redis::lpush('projectKeywordAiTask', $project_id);
}
public static function getPendingTask(){
//有其他任务 就取其他任务 没有其他任务运行未结束的任务
$project_id = Redis::rpop('projectKeywordAiTask');
$data = [];
if($project_id){
$data = self::where('status', self::STATUS_PENDING)->where('project_id', $project_id)->orderBy('id', 'asc')->first();
}
if($data){
return $data;
}
return self::where('status', self::STATUS_PENDING)->orderBy('id', 'asc')->first();
}
/**
* 重试任务
* @param $id
* @param $remark
* @author zbj
* @date 2023/11/9
*/
public static function retry($id, $remark)
{
DB::beginTransaction();
try {
//行锁 避免脏读写
$data = self::where('id', $id)->lockForUpdate()->first();
$data->retry = $data->retry + 1;
if ($data->retry > 3) {
$data->status = self::STATUS_FAIL;
}else{
$data->status = self::STATUS_PENDING;
}
$data->remark = mb_substr($remark, 0, 250);
$data->save();
DB::commit();
} catch (\Exception $e) {
DB::rollback();
Log::error('project_keyword_ai_task retry error:' . $e->getMessage());
}
}
/**
* 完成
* @param $id
* @param $update_rows
* @author zbj
* @date 2023/11/9
*/
public static function finish($id, $update_rows){
DB::beginTransaction();
try {
//行锁 避免脏读写
$data = self::where('id', $id)->lockForUpdate()->first();
$data->status = self::STATUS_SUCCESS;
$data->update_rows = $update_rows;
$data->save();
DB::commit();
} catch (\Exception $e) {
DB::rollback();
Log::error('project_keyword_ai_task finish error:' . $e->getMessage());
}
}
}
... ...