LikeShop 的 DIY 装修能力是完整的。组件在 web/components/decorate 下按 pc-widgets 两套体系组织,数据结构定义在 web/utils/decorate.ts,每个组件由 title、name、content、styles 四部分构成,装修结果以 JSON 形式持久化。多商户版的店铺主页也支持商家自主装修。
但这套能力解决的是“怎么装”,没有解决“装完怎么留着下次用”。大促前花两小时拖出来的首页,活动结束后被新装修覆盖,JSON 被新数据顶掉;下次大促想复用上次的版式,只能凭记忆重新拖一遍。组件本身是可复用的,页面方案是不可复用的——这是原生装修最实际的一个缺口。
更麻烦的是多店铺场景。同一个商户开了三家店,三家店的首页版本可以共用一套“大促模版”,但原生没有模版概念,每家店都要单独拖一遍。平台想给所有商家下发一套“双11标准首页”,也只能靠文档截图加人工照做。
二开的核心策略是:不修改原生装修编辑器和组件代码,在装修数据之上加一层“模版层”。模版不是装修 JSON 的简单拷贝,而是脱敏后的可复用方案——商品ID、优惠券ID 这些店铺特有的数据被抽象成占位符,套用时再按当前店铺的实际数据替换。
2.1 模版主表
模版以“场景 + 页面类型”为组织维度,一个模版对应一套完整的页面方案:
sql
CREATE TABLE `ls_decorate_template` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`template_name` varchar(100) NOT NULL COMMENT '模板名称',
`template_code` varchar(50) NOT NULL COMMENT '模板编码,导出导入的标识',
`page_type` varchar(30) NOT NULL DEFAULT 'home' COMMENT '页面类型:home/category/custom',
`scene_tag` varchar(30) NOT NULL DEFAULT 'daily' COMMENT '场景标签:daily/double11/spring_festival/new_year',
`cover_image` varchar(500) NOT NULL DEFAULT '' COMMENT '模板封面图',
`decoration_json` longtext NOT NULL COMMENT '装修数据,已脱敏,含占位符',
`variable_schema` text COMMENT '变量定义,JSON格式',
`version` varchar(20) NOT NULL DEFAULT '1.0' COMMENT '模板版本号',
`scope_type` tinyint NOT NULL DEFAULT 1 COMMENT '适用范围:1-平台公共,2-商户私有',
`owner_shop_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '归属店铺,scope_type=2时使用',
`allow_shop_levels` varchar(255) NOT NULL DEFAULT '' COMMENT '允许使用的商户等级',
`use_count` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '被套用次数',
`status` tinyint NOT NULL DEFAULT 1,
`create_time` int UNSIGNED NOT NULL DEFAULT 0,
`update_time` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_template_code` (`template_code`),
KEY `idx_scene_page` (`scene_tag`, `page_type`),
KEY `idx_owner` (`owner_shop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='装修模板主表';
template_code 是导出导入的稳定标识。模板在店铺之间、系统之间流转时,自增 ID 会变,但 template_code 不变。导入时按 template_code 判断是“新增”还是“覆盖更新”,而不是按名称匹配——名称可以改,编码是模板的身份。
scope_type 区分平台公共模板和商户私有模板。平台公共模板由平台运营维护,所有符合条件的商户可套用;商户私有模板只属于创建它的商户,其他商户不可见。这个边界是多店铺场景下模板权限的基础。
2.2 变量定义
模板脱敏的关键是变量占位。装修 JSON 中的商品 ID、优惠券 ID、分类 ID 被替换为 {{goods_id_1}} 这样的占位符,模板套用时按当前店铺的实际数据替换:
sql
CREATE TABLE `ls_decorate_template_variable` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`template_id` int UNSIGNED NOT NULL,
`var_key` varchar(50) NOT NULL COMMENT '变量键,如 goods_id_1',
`var_type` varchar(20) NOT NULL COMMENT '类型:goods/coupon/category/custom',
`var_label` varchar(50) NOT NULL COMMENT '展示名称,如 主推商品',
`default_value` varchar(100) NOT NULL DEFAULT '' COMMENT '默认值',
`is_required` tinyint NOT NULL DEFAULT 1 COMMENT '套用时是否必须指定',
`create_time` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_template_var` (`template_id`, `var_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模板变量定义表';
变量分两类:必须指定的(如主推商品的 goods_id,不指定模板就没有意义)和可留空的(如某个营销位,留空则组件隐藏)。套用模板时,前端弹出变量填写面板,运营为每个必填变量选择当前店铺的商品或券,系统替换后生成最终装修 JSON。
2.3 套用记录表
sql
CREATE TABLE `ls_decorate_template_apply_log` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`template_id` int UNSIGNED NOT NULL,
`shop_id` int UNSIGNED NOT NULL COMMENT '套用的店铺',
`decoration_id` int UNSIGNED NOT NULL COMMENT '生成的装修配置ID',
`variable_values` text COMMENT '本次套用的变量值快照',
`operator_id` int UNSIGNED NOT NULL DEFAULT 0,
`create_time` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_template` (`template_id`),
KEY `idx_shop` (`shop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模板套用记录表';
套用记录的价值在于可追溯——某个店铺的首页是哪次套用生成的、用了哪些变量值,都有记录。出问题时能定位到“是这个模板的这组变量导致的”,而不是面对一个被手工改过的装修 JSON 无从下手。
3.1 脱敏不是删数据,是替换为占位符
“保存为模板”最粗糙的实现是直接拷贝当前店铺的装修 JSON。这样拷出来的模板里硬编码着 A 店铺的商品 ID,B 店铺套用后展示的还是 A 店铺的商品,模板复用就变成了“错误复用”。
正确的脱敏逻辑是识别并替换店铺特有的实体引用:遍历装修 JSON 中的组件,对商品组件、优惠券组件、分类组件中的 ID 字段,替换为 {{var_key}},同时在变量定义表中登记这个变量。图片这类资源 URL 保留原样,但模板套用后应提示运营“图片可能需要替换为当前店铺的素材”,而不是自动替换——自动替换图片的误判率太高。
php
public function extractVariables(&$json, $templateId)
{
foreach ($json['widgets'] as &$widget) {
if ($widget['name'] === 'goods-list') {
foreach ($widget['content']['goods'] as $i => &$goods) {
$varKey = 'goods_id_' . ($i + 1);
$this->registerVariable($templateId, $varKey, 'goods',
'商品位' . ($i + 1), $goods['goods_id']);
$goods['goods_id'] = '{{' . $varKey . '}}';
}
}
if ($widget['name'] === 'coupon') {
foreach ($widget['content']['coupons'] as $i => &$coupon) {
$varKey = 'coupon_id_' . ($i + 1);
$this->registerVariable($templateId, $varKey, 'coupon',
'优惠券位' . ($i + 1), $coupon['coupon_id']);
$coupon['coupon_id'] = '{{' . $varKey . '}}';
}
}
}
}
3.2 保存时的校验
保存为模板前,必须校验三件事:装修 JSON 的结构完整性(能否被解析)、变量提取后是否还有残留的硬编码店铺数据、模板名称和编码是否重复。结构不完整的 JSON 不能存为模板——一个坏模板被套用 N 次,就是 N 个坏页面。
3.3 版本管理
模板修改后,version 递增,旧版本不覆盖,而是保留在历史记录中。已套用旧版本的店铺不受影响,只有新套用才使用新版本。这是模板和装修配置的关键差异——装修配置改了就改了,模板需要“向下兼容已套用的实例”。
4.1 套用的执行流程
套用模板分为四步:选择模板 → 填写变量 → 预览效果 → 确认生成。填写变量时,前端按 variable_schema 动态渲染表单,商品类变量调起商品选择器,优惠券类调起券选择器。变量填完后,服务端执行占位符替换,生成最终的装修 JSON,写入 ls_shop_decoration 并生成新的版本记录。
php
public function applyTemplate($templateId, $shopId, $variableValues, $operatorId)
{
$template = DecorateTemplateModel::find($templateId);
$json = $template['decoration_json'];
// 校验必填变量
$schema = json_decode($template['variable_schema'], true);
foreach ($schema as $var) {
if ($var['is_required'] && empty($variableValues[$var['var_key']])) {
throw new \Exception("请填写变量:{$var['var_label']}");
}
}
// 占位符替换
foreach ($variableValues as $key => $value) {
$json = str_replace('{{' . $key . '}}', $value, $json);
}
// 写入装修配置,走版本管理的 saveConfig
$result = ShopDecorationService::saveConfig(
$shopId, $template['page_type'],
json_decode($json, true),
json_decode($template['theme_config'] ?? '{}', true),
$operatorId
);
// 记录套用日志
DecorateTemplateApplyLogModel::create([
'template_id' => $templateId,
'shop_id' => $shopId,
'decoration_id' => $result['decoration_id'],
'variable_values' => json_encode($variableValues, JSON_UNESCAPED_UNICODE),
'operator_id' => $operatorId,
'create_time' => time(),
]);
return $result;
}
套用是覆盖,但覆盖前保留版本历史。套用会取消当前生效版本、创建新版本,旧版本仍在 ls_shop_decoration_history 中,运营套用后不满意可以回滚。没有版本历史的“一键覆盖”是危险的——模板套错了,当前装修就没了。
4.2 套用后的差异提示
模板中的商品可能在当前店铺不存在、优惠券可能已过期。套用完成后,系统应给出一份差异清单:哪些变量对应的实体在当前店铺不可用、哪些组件因为数据缺失会被降级隐藏。运营看到清单后可以决定“先套用再补数据”还是“先补数据再套用”。
降级处理不能报错。商品下架了,组件应该隐藏或展示“商品已下架”的占位,而不是让整个页面白屏。模板套用的容错边界是“页面能打开,缺失的部分降级”,而不是“数据不全就拒绝套用”。
5.1 导出包的结构
模板导出为一个 JSON 包,包含模板主数据、变量定义和装修 JSON:
json
{
"format_version": "1.0",
"template_code": "double11_2026_home",
"template_name": "2026双11大促首页",
"page_type": "home",
"scene_tag": "double11",
"theme_config": {},
"decoration_json": {},
"variables": []
}
format_version 是导入兼容性的闸门。不同 LikeShop 版本的装修组件结构可能不同,导入时先校验 format_version,不兼容的包直接拒绝并提示“该模板由更高版本导出,请升级后再导入”,而不是导入一个半坏的模板。
5.2 导入的冲突处理
导入时按 template_code 判断:编码不存在则新增,已存在则按导入者的选择“覆盖更新”或“另存为新模板”。覆盖更新保留原模板的套用记录,但已套用的店铺不会自动更新——它们用的是旧版本,新版本只影响后续套用。这个规则必须明确,否则平台更新一个公共模板,所有店铺的首页被静默改变,是运营事故。
5.3 跨店铺复用的权限边界
- 商户私有模板:仅归属店铺可见、可套用。同商户下的其他店铺可以申请“复制为我的模板”,复制后生成新的
template_code,与原模板脱钩。 - 平台公共模板:所有符合
allow_shop_levels的商户可见、可套用,但不可编辑。平台运营在后台维护公共模板,商户只能套用。 - 跨商户的模板流转通过导出导入完成,不走直接引用。直接引用会让 A 商户的模板修改影响 B 商户的页面,模板的独立性被破坏。
6.1 后台配置清单
| 配置项 | 类型 | 说明 |
|---|---|---|
| 模板功能开关 | 开关 | 关闭后装修页不出现“保存为模板”入口 |
| 模板套用权限 | 按商户等级 | 哪些等级的商户可套用平台公共模板 |
| 单商户模板数量上限 | 数字 | 防止私有模板无限增长,默认 50 |
| 导出权限 | 开关 | 是否允许商户导出私有模板 |
| 导入冲突策略 | 单选 | 覆盖 / 另存为新模板 |
| 套用后差异提示 | 开关 | 默认开启 |
| 模板版本保留数 | 数字 | 默认保留最近 10 个版本 |
6.2 开发注意事项
模板不是装修 JSON 的拷贝,必须脱敏。直接拷贝会把源店铺的商品 ID、优惠券 ID 硬编码进模板,套用后展示的是别人的商品。商品、券、分类这三类实体引用必须替换为占位符,图片资源保留但提示替换。
套用必须走版本管理,覆盖前保留历史。模板套用是“一键替换当前装修”,如果没有版本历史,套错了就回不去。复用原生 ls_shop_decoration_history 的版本机制,套用生成新版本,旧版本可回滚。
导出包的 format_version 不能省。装修组件的 JSON 结构会随版本变化,没有版本标识的导入包在新旧版本之间流转时,可能产生“组件认识但字段对不上”的半坏模板。导入时校验版本,不兼容直接拒绝,比导入一个坏模板安全。
公共模板的更新不能静默影响已套用的店铺。已套用旧版本的店铺继续用旧版本,新版本只影响后续套用。如果平台需要强制更新,应该提供“批量重新套用”的显式操作,而不是让模板更新自动穿透。
兼容性。新增的模板主表、变量表、套用记录表独立于 ls_shop_decoration 和原生的装修编辑器。模板的保存和套用通过独立的 Service 实现,不修改原生组件的渲染逻辑和 web/utils/decorate.ts 的数据结构。模板功能关闭时,装修页行为与原生完全一致。导出导入作为可选的跨系统流转能力,不参与日常的模板套用链路。
功能原型来自 LikeShop 最新版本更新,单商户和多商户版均已适配,感兴趣可以去他们 demo 站看看实际效果。
