摘要
很多商城接 AI,第一反应是"让 AI 自动改商品标题、详情、标签"。这个方向看起来很爽,但在 CRMEB Pro 这种商品、SKU、标签、缓存、活动和移动端展示互相牵动的系统里,最危险的不是 AI 生成不出来,而是 AI 生成后直接写库,把商品主表、规格、标签和前台展示一起带乱。
这篇围绕一个很实用的小功能:AI 商品运营助手。目标不是让 AI 接管商品,而是让 AI 生成"候选标题、候选简介、候选规格、候选标签建议",运营确认后再走 CRMEB Pro 原有商品保存链路。
1. 这个项目里已经有 AI 商品能力
后台 AI 入口在:
text
route/admin.php
app/controller/admin/v1/system/Ai.php
app/services/system/AiServices.php
路由里已经注册了 AI 接口:
php
Route::post('chat_ai', 'v1.system.Ai/chatAi')
->option(['real_name' => '聊天AI接口']);
Controller 只负责接收参数、调用 Service、返回统一格式:
php
class Ai extends AuthController
{
#[Inject]
protected AiServices $services;
public function chatAi()
{
$data = $this->request->postMore([
['message', ''],
['store_name', ''],
['product_id', 0],
['unique', ''],
['type', '']
]);
$result = $this->services->AiType($data);
if ($result) {
return $this->success($result);
} else {
return $this->fail('生成失败');
}
}
}
这个入口很适合继续扩展商品运营场景,因为它符合项目已有分层:Controller -> Services,并且返回格式也沿用后台标准。
2. AI 类型分发已经覆盖商品运营的核心素材
AiServices::AiType() 里已经有多个商品相关类型:
php
public function AiType($data)
{
switch ($data['type']) {
case 'product_name':
$result = $this->productName($data);
break;
case 'product_info':
$result = $this->productInfo($data);
break;
case 'share_content':
$result = $this->shareContent($data);
break;
case 'product_attr':
$result = $this->productAttr($data);
break;
case 'product_specs':
$result = $this->productSpecs($data);
break;
case 'product_reply':
$result = $this->productReply($data);
break;
default:
return '暂不支持该类型';
}
return $result;
}
这说明二开时不需要从零搭 AI 架构,可以优先沿用现有类型分发,再补充新的运营类型,例如:
text
product_selling_points 商品卖点建议
product_tag_suggest 商品标签建议
product_seo_check 标题和简介风险检查
product_publish_draft 发布前运营草稿
注意:新增类型建议仍放在 AiServices 或同模块 Service 里,不要直接在 Controller 拼 prompt。
3. 为什么不能让 AI 直接保存商品
商品保存入口在:
text
app/controller/admin/v1/product/StoreProduct.php
app/services/product/product/StoreProductServices.php
后台保存商品时,Controller 会把字段取出来,然后交给商品 Service:
php
public function save(StoreProductAttrServices $attrServices, $id)
{
$data = $this->request->postMore($this->getProductSaveFields());
if ($this->supplierId != 0) {
$data['supplier_id'] = $this->supplierId;
if ($id && !$this->checkSupplierProductAuth((int)$id)) {
return $this->fail('商品不存在或无权限');
}
}
$admin_id = $this->adminId;
$this->service->saveData((int)$id, $data, 0, 0, $admin_id);
$this->service->cacheTag()->clear();
$attrServices->cacheTag()->clear();
return $this->success($id ? '保存商品信息成功' : '添加商品成功!');
}
真正复杂的是 StoreProductServices::saveData()。它不是简单更新 store_name:
php
public function saveData(int $id, array $data, int $type = 0, int $relation_id = 0, int $admin_id = 0)
{
[$data, $detail, $attr, $description, $is_copy, $relationData, $slider_image]
= $this->prepareProductSaveData($data, $type, $relation_id);
[$skuList, $id, $is_new, $data] = $this->transaction(function () use (
$id,
$relationData,
$data,
$description,
$storeDescriptionServices,
$storeProductAttrServices,
$detail,
$attr,
$storeDiscountProduct,
$productVirtual,
$slider_image
) {
if ($id) {
$this->setShow([$id], $data['is_show']);
$oldInfo = $this->get($id)->toArray();
if ($oldInfo['product_type'] != $data['product_type']) {
throw new AdminException('商品类型不能切换!');
}
unset($data['sales']);
$res = $this->dao->update($id, $data);
if (!$res) throw new AdminException('修改失败');
} else {
$data['star'] = config('admin.product_default_star');
$data['add_time'] = time();
$data['code_path'] = '';
$data['spu'] = $this->createSpu();
$res = $this->dao->save($data);
if (!$res) throw new AdminException('添加失败');
$id = (int)$res->id;
}
$storeDescriptionServices->saveDescription($id, $description);
$skuList = $storeProductAttrServices->validateProductAttr($attr, $detail, $id);
$valueGroup = $storeProductAttrServices->saveProductAttr($skuList, $id);
if (!$valueGroup) throw new AdminException('添加失败!');
$attrStockArr = array_column($valueGroup, 'stock');
$this->dao->update($id, [
'stock' => array_sum($attrStockArr),
'is_sold' => min($attrStockArr) ? 0 : 1
]);
return [$skuList, $id, $is_new, $data];
});
event('product.create', [$id, $data, $skuList, $is_new, $slider_image, $description, $is_copy, $relationData]);
$this->dao->cacheTag()->clear();
$storeProductAttrServices->cacheTag()->clear();
}
这里会处理:
text
商品主表
商品详情
SKU 规格
虚拟商品卡密
库存汇总
标签、分类、品牌、保障、参数、优惠券关联
商品创建事件
商品缓存和规格缓存
所以 AI 生成内容后,不能绕过 saveData() 直接改表。更推荐的方式是:AI 只生成候选草稿,运营确认后仍走原商品保存接口。
4. 商品标签为什么也不能乱改
商品编辑详情里,标签会被还原成可展示结构:
php
if ($productInfo['label_id']) {
$label_id = is_array($productInfo['label_id'])
? $productInfo['label_id']
: explode(',', $productInfo['label_id']);
$productInfo['label_id'] = $userLabelServices->getLabelList(
['ids' => $label_id],
['id', 'label_name']
);
} else {
$productInfo['label_id'] = [];
}
if ($productInfo['store_label_id']) {
$storeProductLabelServices = app()->make(StoreProductLabelServices::class);
$productInfo['store_label_id'] = $storeProductLabelServices->getColumn(
[['id', 'in', $productInfo['store_label_id']]],
'id,label_name'
);
} else {
$productInfo['store_label_id'] = [];
}
商品保存时,标签又会进入关联数据:
php
protected function getProductRelationData(array $data): array
{
return [
'cate_id' => $data['cate_id'] ?? [],
'brand_id' => $data['brand_id'] ?? [],
'store_label_id' => $data['store_label_id'] ?? [],
'label_id' => $data['label_id'] ?? [],
'ensure_id' => $data['ensure_id'] ?? [],
'specs_id' => $data['specs_id'] ?? [],
'coupon_ids' => $data['coupon_ids'] ?? [],
];
}
因此 AI 标签建议要遵守一个原则:
text
AI 可以建议"应该打什么标签"
系统必须校验"标签是否存在、是否属于当前平台或供应商、是否允许绑定"
运营最终确认后,再进入原商品保存链路
不要让 AI 直接生成不存在的标签 ID,更不要让 AI 直接写 store_label_id。
5. 一个安全的 AI 商品运营流程
推荐流程如下:
text
1. 后台选择商品
2. 读取商品名称、简介、分类、品牌、SKU、已有标签
3. 调用 chat_ai 生成候选标题、卖点、标签建议
4. 保存为"AI 草稿",不改商品正式数据
5. 运营勾选采用项
6. 前端带着完整商品表单提交原保存接口
7. StoreProductServices::saveData() 完成校验、事务、事件和缓存清理
如果要新增一个草稿服务,可以按项目分层写成:
php
class AiProductDraftServices extends BaseServices
{
/**
* 生成商品运营草稿
* @param int $productId 商品ID
* @param array $input 运营输入
* @return array
*/
public function makeDraft(int $productId, array $input): array
{
$product = app()->make(StoreProductServices::class)->getInfo($productId);
$prompt = [
'store_name' => $product['store_name'] ?? '',
'cate_name' => $product['cate_name'] ?? [],
'brand_name' => $product['brand_name'] ?? '',
'store_label' => $product['store_label_id'] ?? [],
'operator_require' => $input['message'] ?? '',
];
$result = app()->make(AiServices::class)->AiType([
'type' => 'product_name',
'message' => json_encode($prompt, JSON_UNESCAPED_UNICODE),
'store_name' => $product['store_name'] ?? '',
]);
return [
'product_id' => $productId,
'draft_title' => $result,
'notice' => 'AI 结果仅作为候选,保存商品前必须人工确认',
];
}
}
这段是二开思路,不建议把它塞到 Controller。后续如果要落库,也应该新增对应 Dao/Model 或复用项目已有草稿表能力。
6. Prompt 要限制输出结构
项目里的 productName() 已经做了一件很关键的事:要求 AI 只返回固定 JSON。
php
public function productName($data)
{
$systemContent = '
请严格按此规范执行:
1. 核心指令:
• 生成3条差异化标题,每条30-50字,符合电商平台SEO规则
• 必须包含:核心关键词(前10字)+核心卖点(材质/功能)+附加价值
• 禁止:重复词、违禁词、模糊描述
3. 输出格式:仅返回JSON,键名固定为t1/t2/t3:
{
"t1": "标题1",
"t2": "标题2",
"t3": "标题3"
}';
$userContent = $data['message'];
$result = $this->services->ai()->chat($systemContent, $userContent);
return array_values(json_decode($result, true));
}
二开时建议继续保留这种思路:
text
只返回 JSON
字段固定
不让 AI 输出 SQL
不让 AI 输出 HTML 脚本
不让 AI 生成商品 ID、标签 ID、价格、库存这类关键数值
7. 哪些字段适合 AI 生成,哪些字段必须人工确认
适合 AI 生成候选:
text
商品标题
商品简介
分享文案
卖点短句
规格参数模板
标签建议
详情页段落草稿
必须人工确认:
text
价格
库存
成本价
会员价
运费模板
商品分类
商品标签 ID
优惠券绑定
供应商归属
上下架状态
审核状态
活动关联
尤其是库存、价格、活动关联,不建议交给 AI 自动写入。
8. 注意事项
- AI 生成结果必须做 JSON 解析失败兜底,不能默认模型一定按格式返回。
- 不要把后台管理员信息、用户手机号、订单收货信息、敏感配置传给 AI。
- 商品正式保存必须走
StoreProductServices::saveData(),不要为了省事直接更新商品表。 - 标签建议要用标签名称匹配,再由系统查出合法标签 ID。
- AI 只做候选内容,不直接发布、不直接上架、不直接改库存价格。
- 生成内容要保留操作日志,至少记录商品 ID、管理员 ID、生成类型、采用状态。
- 供应商商品要继续走
checkSupplierProductAuth()权限边界。
标签建议
text
CRMEB Pro
二次开发
AI
商城系统
商品运营
ThinkPHP
PHP