您的位置首页 >人工智能 >

LikeShop单商户标准版商品文案智能化升级大模型批量生成标题与卖点二开实践

官方数据字典(单商户版 ls_goods/ls_goods_item)、宝塔部署文档里的 PHP 7.2/MySQL 5.7 版本要求、二开代码规范,以及两家大模型的现行价目我都重新核对了一遍 —— 这次按单商户标准版的字段写,不套高级版参数。

LikeShop 标准版文案智能化:把大模型接进商品后台,批量出标题和卖点

给 LikeShop 单商户标准版做商品文案智能化,落地方式就一句话:后台加一个 “AI 文案” 页,服务端加一个队列任务,产物先落扩展表、审核通过再回写 ls_goods.name 与 ls_goods.remark,官方代码一行不改。之所以要这么绕,是因为单商户标准版的环境是 PHP 7.2+MySQL 5.7 的老组合、后台接口又走 auth.php 的 action_auth 逐条授权,直接改 goods.goods/edit 要么在升级时被整目录覆盖,要么把商品保存链路带崩;而单次大模型调用要 1–3 秒、还会 429,塞进保存按钮里用户点一次等三秒就骂人。整套流程拆成五步:核对字段 → 建扩展表 → 写客户端与服务类 → 挂队列跑批量 → 做审核页与灰度。按 DeepSeek-V4.1-Flash 闲时价算,1000 条商品两三块钱,真正费时间的是质检和灰度,不是模型。

第一步:先核对字段,别照抄高级版

单商户标准版和高级版、SaaS 版的差别不在 “长得像”,而在字段名和 PHP 版本。这几条对不上,后面代码全白写:

项目单商户标准版(企业免费版)单商户高级版 / SaaS 版
PHP7.2 推荐,7.2–7.4 可用需 8.0
MySQL5.75.7/8.0
后台入口/admin/admin(SaaS 为多开后台)
后台接口前缀以 app/ 下后台应用名为准/adminapi 为主
商品主表ls_goodsls_goods
商品名称字段namename
商品简介字段remarkcontent(详情)
分类字段goods_category_idgoods_category_id 或 first_cate_id
SKU 价格字段ls_goods_item.pricels_goods_item.sell_price
SKU 划线价market_pricelineation_price

不确定就跑两条 SQL 自己验,比查文档快:

SHOW COLUMNS FROM `ls_goods` LIKE '%name%';

SHOW COLUMNS FROM `ls_goods` LIKE '%remark%';

SHOW COLUMNS FROM `ls_goods_item`;

再确认后台应用目录叫 admin 还是 adminapi:

ls server/app 输出 admin / api 之类,就是你的模块名

第二步:建两张 ls_ext_ 扩展表

二开的规矩是官方表不加字段、新功能新开表、表前缀 ls_ext_ 独立管理。这样官方升级跑自己的 SQL 时碰不到你:

-- 批量任务

CREATE TABLE `ls_ext_ai_copy_job` (

`id` int unsigned NOT NULL AUTO_INCREMENT,

`name` varchar(64) NOT NULL DEFAULT '' COMMENT '任务名',

`cate_id` int NOT NULL DEFAULT 0 COMMENT '分类id,0为全部',

`status` tinyint NOT NULL DEFAULT 0 COMMENT '0待跑 1进行中 2完成 3中止',

`total` int NOT NULL DEFAULT 0,

`done` int NOT NULL DEFAULT 0,

`fail` int NOT NULL DEFAULT 0,

`model` varchar(64) NOT NULL DEFAULT '',

`prompt_ver` varchar(16) NOT NULL DEFAULT 'v1',

`admin_id` int NOT NULL DEFAULT 0,

`create_time` int NOT NULL DEFAULT 0,

PRIMARY KEY (`id`), KEY `idx_status` (`status`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI文案批量任务';

-- 单商品结果

CREATE TABLE `ls_ext_ai_copy_result` (

`id` int unsigned NOT NULL AUTO_INCREMENT,

`job_id` int unsigned NOT NULL DEFAULT 0,

`goods_id` int unsigned NOT NULL DEFAULT 0,

`hash` char(32) NOT NULL DEFAULT '' COMMENT 'md5(goods_id+prompt_ver+输入快照)',

`title` varchar(120) NOT NULL DEFAULT '',

`selling` text COMMENT '卖点JSON',

`keywords` varchar(255) NOT NULL DEFAULT '',

`score` tinyint NOT NULL DEFAULT 0 COMMENT '规则评分0-100',

`check_flag` tinyint NOT NULL DEFAULT 0 COMMENT '0未校验 1通过 2需人工 3不通过',

`check_msg` varchar(255) NOT NULL DEFAULT '',

`old_title` varchar(120) NOT NULL DEFAULT '' COMMENT '回写前原值,便于回滚',

`old_remark` varchar(255) NOT NULL DEFAULT '',

`tokens_in` int NOT NULL DEFAULT 0,

`tokens_out` int NOT NULL DEFAULT 0,

`status` tinyint NOT NULL DEFAULT 0 COMMENT '0待审 1采用 2弃用 3已回写',

`create_time` int NOT NULL DEFAULT 0,

PRIMARY KEY (`id`),

UNIQUE KEY `uk_hash` (`hash`),

KEY `idx_job` (`job_id`), KEY `idx_goods` (`goods_id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI文案结果';

uk_hash 是整张表最值钱的一行:同一商品+同一 Prompt 版本+同一份输入只会付费一次,任务重跑、脚本重复执行全被唯一键挡掉。

第三步:客户端与服务类

密钥只放 .env,不进库、不下发前端:

[AI]

AI_BASE_URL = https://api.deepseek.com

AI_API_KEY = sk-xxxxxxxx

AI_MODEL = deepseek-flash

AI_TIMEOUT = 30

// config/ai.php

return [

'base_url' => Env::get('AI.AI_BASE_URL'),

'api_key' => Env::get('AI.AI_API_KEY'),

'model' => Env::get('AI.AI_MODEL', 'deepseek-flash'),

'timeout' => (int) Env::get('AI.AI_TIMEOUT', 30),

'max_retry' => 3,

'sleep_ms' => 200,

];

namespace app\common\service\ai;

class AiClient

{

public function chatJson(array $messages, float $temp = 0.4, int $maxTokens = 800): array

{

$conf = config('ai');

$payload = [

'model' => $conf['model'],

'messages' => $messages,

'temperature' => $temp,

'max_tokens' => $maxTokens,

'response_format' => ['type' => 'json_object'],

'stream' => false,

];

for ($i = 0; $i < (int) $conf['max_retry']; $i++) {

$ch = curl_init(rtrim($conf['base_url'], '/') . '/v1/chat/completions');

curl_setopt_array($ch, [

CURLOPT_POST => true,

CURLOPT_RETURNTRANSFER => true,

CURLOPT_TIMEOUT => $conf['timeout'],

CURLOPT_CONNECTTIMEOUT => 10,

CURLOPT_HTTPHEADER => [

'Content-Type: application/json',

'Authorization: Bearer ' . $conf['api_key'],

],

CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),

]);

$raw = curl_exec($ch);

$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

if ($raw !== false && $code === 200) {

$body = json_decode($raw, true) ?: [];

return [

'content' => $body['choices'][0]['message']['content'] ?? '',

'usage' => $body['usage'] ?? [],

];

}

// 只对限流与服务端错误退避;鉴权/参数错误立刻抛

if ($code === 429 || $code >= 500 || $code === 0) {

usleep((int) (pow(2, $i) 500000));

continue;

}

throw new \RuntimeException('AI接口 HTTP ' . $code . ':' . mb_substr((string) $raw, 0, 160));

}

throw new \RuntimeException('AI接口重试 ' . $conf['max_retry'] . ' 次仍失败');

}

}

Prompt 的写法决定了 80% 的输出质量。核心是三件事:只喂真实字段、锁死输出结构、明确禁止编造。

namespace app\common\service\ai;

use think\facade\Db;

class CopyService

{

const PROMPT_VER = 'v1';

public function messages(array $goods): array

{

$cate = Db::name('goods_category')->where('id', $goods['goods_category_id'])->value('name') ?: '';

$sku = Db::name('goods_item')->where('goods_id', $goods['id'])

->order('price asc')->column('spec_value_str, price');

$facts = [

'商品原名' => $goods['name'],

'类目' => $cate,

'简介' => (string) ($goods['remark'] ?? ''),

'价格区间' => $goods['min_price'] . '~' . $goods['max_price'] . ' 元',

'规格与价格' => $sku,

];

$system = << 'system', 'content' => $system],

['role' => 'user', 'content' => "商品事实:\n" . json_encode($facts, JSON_UNESCAPED_UNICODE)],

];

}

/ 规则评分:0–100,低于 60 直接判不通过 /

public function score(array $parsed, array $goods): array

{

$title = trim($parsed['title'] ?? '');

$sp = $parsed['selling_points'] ?? [];

$ded = 0; $msg = [];

if ($title === '') { $ded += 40; $msg[] = '标题为空'; }

if (mb_strlen($title) > 30) { $ded += 15; $msg[] = '标题超30字'; }

if (count($sp) < 3 || count($sp) > 5) { $ded += 10; $msg[] = '卖点条数异常'; }

foreach (['最', '第一', '顶级', '国家级', '唯一'] as $w) {

if (mb_strpos($title, $w) !== false) { $ded += 30; $msg[] = '含禁用词:' . $w; }

}

// 标题里的数字必须在原文出现过,防止模型自造"3年质保""5折"

if (preg_match_all('/(\d+(\.\d+)?)\s(元|折|年|天|件)/u', $title, $m)) {

foreach ($m[0] as $num) {

$src = $goods['name'] . ($goods['remark'] ?? '');

if (mb_strpos($src, $num) === false) { $ded += 20; $msg[] = '未提供的数字:' . $num; }

}

}

similar_text($title, $goods['name'], $pct);

if ($pct > 80) { $ded += 10; $msg[] = '与原标题重合度过高'; }

$score = max(0, 100 - $ded);

return [

'score' => $score,

'flag' => $score >= 80 ? 1 : ($score >= 60 ? 2 : 3),

'msg' => implode(';', $msg),

];

}

}

评分分三档很关键:80 分以上进 “自动通过” 队列,60–79 进 “人工看一眼”,60 以下直接判死。这样人工只处理中间那 20%,而不是全量。

第四步:挂队列跑批量

单商户标准版本身带 think-queue,没有就装 topthink/think-queue,队列驱动先用 database,别一上来就 Redis:

namespace app\job;

use app\common\service\ai\AiClient;

use app\common\service\ai\CopyService;

use think\facade\Db;

use think\facade\Log;

use think\queue\Job;

class AiCopyJob

{

public function fire(Job $job, array $data): void

{

$goodsId = (int) ($data['goods_id'] ?? 0);

$jobId = (int) ($data['job_id'] ?? 0);

$goods = Db::name('goods')->where('id', $goodsId)->where('status', 1)->find();

if (!$goods) { $job->delete(); return; }

$hash = md5($goodsId . CopyService::PROMPT_VER . $goods['name'] . ($goods['remark'] ?? ''));

if (Db::name('ext_ai_copy_result')->where('hash', $hash)->count() > 0) {

$job->delete(); return; // 幂等:已有结果直接跳过,不重复付费

}

try {

$svc = new CopyService();

$res = (new AiClient())->chatJson($svc->messages($goods));

$parsed = json_decode($res['content'], true);

if (!is_array($parsed)) throw new \RuntimeException('返回非JSON');

$chk = $svc->score($parsed, $goods);

Db::name('ext_ai_copy_result')->insert([

'job_id' => $jobId,

'goods_id' => $goodsId,

'hash' => $hash,

'title' => mb_substr($parsed['title'] ?? '', 0, 100),

'selling' => json_encode($parsed['selling_points'] ?? [], JSON_UNESCAPED_UNICODE),

'keywords' => implode(',', array_slice($parsed['keywords'] ?? [], 0, 5)),

'score' => $chk['score'],

'check_flag' => $chk['flag'],

'check_msg' => mb_substr($chk['msg'], 0, 255),

'old_title' => $goods['name'],

'old_remark' => $goods['remark'] ?? '',

'tokens_in' => $res['usage']['prompt_tokens'] ?? 0,

'tokens_out' => $res['usage']['completion_tokens'] ?? 0,

'create_time'=> time(),

]);

$job->delete();

} catch (\Throwable $e) {

Log::error('[ai_copy] goods=' . $goodsId . ' ' . $e->getMessage());

if ($job->attempts() >= 3) {

Db::name('ext_ai_copy_job')->where('id', $jobId)->inc('fail')->update();

$job->delete();

} else {

$job->release(30); // 30 秒后重试,避免瞬时 429 打穿

}

}

usleep((int) (config('ai.sleep_ms') 1000));

}

}

入队和进度:

// 控制器里创建任务并推队列

$ids = Db::name('goods')->where('status', 1)

->when($cateId, fn($q) => $q->where('goods_category_id', $cateId))

->order('id asc')->column('id');

$jobId = Db::name('ext_ai_copy_job')->insertGetId([

'name' => 'AI文案-' . date('mdHi'), 'cate_id' => $cateId,

'total' => count($ids), 'model' => config('ai.model'),

'prompt_ver' => \app\common\service\ai\CopyService::PROMPT_VER,

'create_time'=> time(),

]);

foreach ($ids as $gid) {

\think\facade\Queue::push(\app\job\AiCopyJob::class, ['goods_id' => $gid, 'job_id' => $jobId], 'ai_copy');

}

跑队列进程:

php think queue:work --queue ai_copy --tries 3 --timeout 60

一行 --queue ai_copy 是必须的:否则你这条 AI 任务会和订单、短信通知这些系统队列抢 worker,后台延迟立刻肉眼可见。

第五步:审核页与灰度

后台审核页只做三件事:看差异、打分排序、点采用。

namespace app\admin\controller;

use think\facade\Db;

class AiCopy extends Base // ← 换成你版本的基类名

{

public function lists()

{

$page = (int) $this->request->get('page', 1);

$flag = (int) $this->request->get('check_flag', 0);

$list = Db::name('ext_ai_copy_result')

->when($flag, fn($q) => $q->where('check_flag', $flag))

->order('score desc, id desc')

->page($page, 20)->select()->toArray();

return json(['code' => 1, 'data' => $list]);

}

public function apply()

{

$id = (int) $this->request->post('id', 0);

$row = Db::name('ext_ai_copy_result')->where('id', $id)->where('status', 0)->find();

if (!$row) return json(['code' => 0, 'msg' => '记录不存在']);

if ((int) $row['check_flag'] === 3) return json(['code' => 0, 'msg' => '评分未达标,禁止回写']);

Db::name('goods')->where('id', $row['goods_id'])->update([

'name' => $row['title'],

'remark' => mb_substr(implode('|', json_decode($row['selling'], true) ?: []), 0, 255),

'update_time' => time(),

]);

Db::name('ext_ai_copy_result')->where('id', $id)->update(['status' => 3]);

return json(['code' => 1, 'msg' => '已回写']);

}

}

灰度别图省事。上线第一周只对 10% 商品回写,按 SKU 或商品 ID 取模做稳定分流,14 天后比点击率和加购率:

-- 只挑 10% 商品进入 AI 文案灰度组

SELECT id FROM ls_goods WHERE status = 1 AND MOD(id, 10) = 0;

观察指标看什么判断
商品详情页点击率搜索 / 分类列表曝光→点击标题是否更有信息量
加购率访问→加购卖点是否说清了用途
转化率加购→下单文案是否与实物一致
退货 / 差评关键词“不符”“夸大”文案有没有过度承诺

如果 CTR 涨了但退货率也涨,说明模型把卖点写虚了,这时候该收紧 Prompt 的 “禁止编造” 约束,而不是继续放大流量。

成本与并发

模型输入输出1000 条(600 in + 300 out)
DeepSeek-V4.1-Flash 闲时¥1/M¥4/M≈ ¥1.8
DeepSeek-V4.1-Flash 忙时¥2/M¥8/M≈ ¥3.6
qwen3.8-flash¥0.8/M¥2.7/M≈ ¥1.3

并发控制在 3–5 个 worker:单商户标准版的 MySQL 5.7 没有太多余量,AI 批量任务把数据库拖慢,前台下单会直接受影响。

五个必查项

  1. 字段先核对:标准版是 remark+goods_category_id+goods_item.price,高级版是 content+sell_price,抄错一个就报 Unknown column。
  2. PHP 别乱升:标准版跑 PHP 7.2 最稳,升 8.0 会出现扩展与框架兼容问题;要升先在测试环境跑通安装向导。
  3. 密钥进 .env,config/ai.php 只读环境变量,前端任何位置都不出现 key。
  4. 后台控制器别继承错基类,标准版基类名与高级版不同,加载即报错的十有八九是这里。
  5. 回写必须留 old_title/old_remark,线上标题出问题要能十分钟内改回去。

一句话收口:LikeShop 单商户标准版做文案智能化,技术难度远低于想象,难的是 “把版本字段对上、把官方代码绕开、把模型编造的口子堵住”。这三件事做对,它就是一个能一直跟着官方版本升级走的小工具;做错任何一件,省下的开发时间都会在下一次升级或一次投诉里加倍还回去。

likeshoplikeshop

【相关 FAQ】

问题 1:LikeShop 单商户标准版做大模型批量生成标题,商品简介字段到底叫 remark 还是 content?

回答 1:单商户标准版的 ls_goods 商品简介字段是 remark,商品名称是 name,分类是 goods_category_id;content 是单商户高级版/SaaS 版的商品详情字段,两版混用会直接报 Unknown column。

问题 2:LikeShop 单商户标准版批量生成商品卖点,为什么要用队列而不是在后台接口里直接调?

回答 2:一次大模型调用要 1–3 秒且可能返回 429,放在后台保存接口里会让页面卡死;改用 think-queue 队列后,任务可以重试、可以断点续跑、可以限制并发,前台下单不受影响。

问题 3:大模型生成的商品标题和卖点,怎么避免写出广告法禁用词?

回答 3:两道拦截。Prompt 里明确禁止 “最、第一、顶级、国家级、唯一” 等表述,服务端再用规则评分二次校验,含禁用词直接扣分到不通过档,禁止回写 ls_goods.name。

问题 4:LikeShop 单商户标准版二开新增表,为什么必须用 ls_ext_ 前缀?

回答 4:官方升级会执行自己的建表与改表 SQL,只有把 AI 文案任务表、结果表放在 ls_ext_ai_copy_job、ls_ext_ai_copy_result 这类独立前缀下,才能保证升级不冲突、出问题能整表回滚。

问题 5:AI 生成的标题上线后,怎么判断到底有没有效果?

回答 5:做 10% 灰度,按商品 ID 取模稳定分流,观察 14 天内的详情页点击率、加购率、转化率和退货差评关键词;如果点击率涨但退货率同步涨,说明卖点写虚了,应先收紧 Prompt 的 “禁止编造” 约束。

【参考资料】

  • [2026-11-09](Models & Pricing 模型与计费)DeepSeek API Docs
  • [2026-11-06](Chat Completions 接口与 OpenAI 兼容说明)千问 AI 平台
  • [2026-10-23](LikeShop 二开代码规范:目录结构、钩子使用与版本升级兼容方案)阿里云开发者社区
  • [2026-10-19](一次超卖 37 件的复盘:LikeShop 单商户版 SKU 体系的规格映射、库存扣减与并发)阿里云开发者社区
  • [2026-10-13](LikeShop 单商户开源版本地部署全流程(PHPStudy 版))CSDN 博客
  • [2026-10-06](数据库字典 单商户高级版)LikeShop 官方开发文档
  • [2026-09-29](服务端宝塔面板部署①)LikeShop 官方开发文档
  • [2026-07-26](数据库字典 单商户版商品表与 SKU 表)LikeShop 官方开发文档
  • [2026-07-20](PHPstudy 环境下部署①)LikeShop 官方开发文档

【免责声明】

本文示例代码按 LikeShop 单商户标准版(PHP 7.2+MySQL 5.7、ls_ 表前缀、ls_goods.name/remark)编写,高级版与 SaaS 版在模块名、基类名、商品详情字段与 SKU 价格字段上存在差异,落地前请以你所装版本的数据库字典为准。文中价格估算基于 2026 年 11 月公开的模型价目表,实际计费随调用量、缓存命中与供应商调价变化。本文不构成对 LikeShop 商业授权、模型服务合规性的担保,生产环境上线前请自行完成安全审计与内容合规审查。