组装修(Route Group Decoration)
针对同一类详情页(商品、专辑、优惠券等),按对象划分不同装修方案。
上级文档:theme-decoration-and-liquid-rendering.md
一、要解决什么问题
默认情况下,所有商品详情页共用 group_id = 0(默认组) 下 o_theme_block 的装修。
组装修允许商家:
- 创建分组(如「爆款商品组」「清仓组」)
- 把指定商品/专辑/优惠券等 obj 绑定到分组
- 为每个分组维护独立的积木块配置(
o_theme_block.group_id > 0)
访客打开某个商品详情时,系统根据 商品 ID → group_id → 该组的 blocks 渲染,而不是默认组。
flowchart LR subgraph Default["默认组 group_id=0"] D1[商品 A 详情] D2[商品 B 详情] DB0[(o_theme_block<br/>group_id=0)] end subgraph Custom["自定义组 group_id=5"] C1[商品 C 详情] DB5[(o_theme_block<br/>group_id=5)] end D1 --> DB0 D2 --> DB0 C1 --> DB5
二、数据表
2.1 o_theme_route_group(分组定义)
Model:ThemeRouteGroupModel
| 字段 | 说明 |
|---|---|
store_id / theme_id | 租户 + 主题隔离 |
name | 分组名称(同 theme + obj_type 下不可重名) |
obj_type | 对象类型(见 § 三) |
is_default | 标记用;虚拟默认组不在表中 |
2.2 o_theme_route_group_item(对象 ↔ 分组绑定)
Model:ThemeRouteGroupItemModel
| 字段 | 说明 |
|---|---|
group_id | 所属分组 |
obj_id | 商品 ID、专辑 ID 等 |
obj_type | 与 group 一致 |
唯一性约束(业务层):同一 store_id + theme_id + obj_id + obj_type 只能属于一个组。绑到新组前会 deleteObjIdRelation 解绑旧组。
2.3 o_theme_block.group_id(分组下的积木)
| group_id | 含义 |
|---|---|
0 | 默认组(未绑定到自定义组的对象使用) |
> 0 | 自定义组专属积木块 |
默认组与自定义组的 blocks 分库存储,互不影响。发布、预览字段与普通积木相同(params / preview_params)。
三、支持的 obj_type 与 route
ThemeRouteGroupModel::getAllObjTypeList() 共 8 种:
| obj_type | 对应 route | 前台 Controller 示例 |
|---|---|---|
PRODUCT | product/detail | Product::detail |
COLLECTION | collection/detail | Collection::detail |
TOPIC | topic/detail | Topic::detail |
BLOG | blog/detail | Blog::detail |
COUPON | coupon/detail | Coupon::detail |
PROMOTION | promotion/detail | Promotion::detail |
NEWS | news/detail | News::detail |
ACCOUNT | account/default | 个人中心(obj_id 为客户级别 ID) |
映射方法:ThemeRouteGroupHandlerService::objTypeToRoute() / routeToObjType()。
支持组装修的路由列表:getAllSupportGroupRouteList() = 上述 8 种 route。
GET themes/{id}/configs 经 ThemesConfigResponse 为每个 pages_setting 项追加 is_support_group: true/false。
四、虚拟「默认组」
未在 o_theme_route_group_item 中绑定的对象,走默认组。
默认组 不存在于 DB,由代码构造:
// ThemeRouteGroupHandlerService::generateDefaultGroup()
id => 0 (DEFAULT_GROUP_ID)
name => 'default'
obj_type => 当前类型
is_default => 1后台列表 GET themes-group/ 第一页会在结果头部插入这条虚拟记录(ThemeGroup::list)。
五、后台 API(themes-group)
路由:app/api/route/route.php → 组 themes-group
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | / | 分组列表(含虚拟 default);参数 theme_id, obj_type, name |
| GET | /:id | 分组详情 + items |
| POST | / | 创建分组并复制积木 |
| PUT | /:id | 更新名称 + 重绑 obj |
| DELETE | /:id | 删组、items、该组 blocks、清缓存 |
| GET | /find | 按 obj_id + obj_type 查组(商品详情跳装修) |
| GET | /search | 按 route + route_handle 查组(装修内页跳转) |
| GET | /:id/item | 组内第一个对象(预览用) |
| GET | /:id/:objType/item | 默认组 fallback 第一个对象 |
| GET | /:id/items | 组内对象分页 |
| GET | /:id/product-collection | 商品组下商品所属专辑(下拉) |
5.1 创建分组 POST themes-group/
Body(ThemeGroupCreateRequest):
| 字段 | 说明 |
|---|---|
theme_id | 主题 ID |
name | 组名 |
obj_type | 如 PRODUCT |
obj_ids | 逗号分隔对象 ID,可选 |
copy_group_id | 从哪复制积木(见下) |
复制积木逻辑(ThemeRouteGroupService::add):
| copy_group_id | 行为 |
|---|---|
| 未传 / 空字符串 | 复制默认组下该 route 的 require 块(如 product_detail)到新组 |
0 | 复制默认组该 route 全部 blocks |
> 0 | 复制指定组已发布 blocks(copyThemeBlock) |
5.2 装修编辑器拉数据
GET themes/{id}/data?route=product/detail&route_handle=xxx&group_id=
| group_id 参数 | 行为 |
|---|---|
| 不传 | 按 obj 自动匹配 group |
0 | 强制默认组(themeDataByHandler) |
> 0 | 强制指定组(themeDataByGroup) |
编辑器固定 preview=1。
新增积木:POST themes/{id}/sections,body 含 group_id(非支持 route 时强制为 0)。
六、前台匹配流程
6.1 入口:ThemesService::themeData()
route 不在 supportGroupRouteList
→ themeDataByHandler(group_id=0 的 blocks)
group_id === 0(显式默认组)
→ themeDataByHandler
group_id 为空:
obj_id ← themeGroupExtData / Context / getObjIdByHandler
group_id ← ThemeRouteGroupCacheService::getGroupIdByObjIdAndType()
group_id 仍空 → themeDataByHandler
group_id > 0 → themeDataByGroup → getThemeGroupSectionsData
6.2 obj_id 从哪来
| 来源 | 场景 |
|---|---|
ThemeRouteGroupHandlerService::setCurrentRequestObjId($id) | 详情 Controller 在 fetch 前写入 Context(Product、Collection 等) |
TagGetBlocks 的 obj_id 参数 | Liquid 显式传参 |
getObjIdByHandler(objType, routeHandle) | 由 URL handle 反查 ID;商品强制 preview=1 忽略上下架 |
getObjIdByHandler 失败时回退 getObjIdByRawHandler(保留大小写再查一次)。
6.3 obj_id → group_id
ThemeRouteGroupItemModel::getGroupByObjIdAndType(store, theme, obj_id, obj_type)
→ 有记录:返回对应 o_theme_route_group
→ 无记录:generateDefaultGroup(),group_id = 0
Redis 缓存:CacheKeyHelper::themeGroupIdByObjIdAndType($themeId, $objId, $objType),TTL 1 天。
组或 item 变更时 ThemeRouteGroupCacheService::cleanCacheBy* 删除。
6.4 读取哪批 blocks
| 路径 | 查询 |
|---|---|
| 默认组 | ThemeBlockService::list() → group_id=0 + route |
| 自定义组 | ThemeBlockService::listByGroupId() / previewListByGroupId() → 仅 group_id |
之后均走 sectionsFormat() 补 header/footer/require 等固定块。
组 blocks Redis 缓存 field:{route}:routeGroup:{groupId}(默认主题、非 preview)。
6.5 Liquid:TagGetBlocks
{% get_blocks route={routes.current_route} route_handle={routes.current_route_handle} obj_id=xxx group_id=xxx limit=80 %}- 不传
obj_id时用getCurrentRequestObjId() - 装修器左侧预览商品区:需传
group_id/obj_id,避免默认组与绑定组 blocks 混用(见TagGetBlocks注释)
七、后台 UI 对应(推断)
| 后台区域 | 数据 / API |
|---|---|
| 商品详情页「组装修」入口 | GET themes-group/find?obj_id=&obj_type=PRODUCT |
| 装修器内切换分组 | group_id + GET themes/{id}/data |
| 分组管理列表 | GET themes-group/?obj_type=PRODUCT(首条 default) |
| 创建分组弹窗 | POST themes-group/ + 选 obj + copy_group_id |
| 组内对象列表 | GET themes-group/{id}/items |
| 左侧预览对象 | GET themes-group/{id}/item 或 getDefaultGroupFirstItem |
八、与默认组 / 单页 handle 装修的关系
- 默认组(
group_id=0):所有未绑定对象的详情页共用;route_handle可为空或具体 handle(专辑/博客等有 per-handle 装修逻辑)。 - 组装修:同一 route 下按 obj 切换整组 blocks;组内 blocks 的
route_handle通常为空(博客/新闻详情在sectionsFormat里会强制route_handle=''补 fixed 块)。
两者可并存:未绑定的商品走默认组;绑定的走自定义组。
九、主题复制
ThemesService::copy() → ThemeRouteGroupHandlerService::copy():
- 查新主题下
group_id > 0的 blocks - 逐组
ThemeRouteGroupService::copy()重建 group + items ThemeBlockModel::changeGroupId()回写 block 的 group_id
十、扩展新 obj_type 检查清单
ThemeRouteGroupModel::getAllObjTypeList()增加常量ThemeRouteGroupHandlerService::objTypeToRoute()getObjIdByHandler()/getObjIdByRawHandler()defaultGroupFirstItem()默认预览对象- 详情 Controller 调用
setCurrentRequestObjId() settings_schema.pages_setting增加 route(若新详情页)getThemeConfig中needPagePreviewUrl的 switch 补预览 URL(可选)
十一、关键代码索引
| 组件 | 路径 |
|---|---|
| 匹配编排 | common/services/ThemesService.php — themeData, themeDataByGroup, themeDataByHandler |
| 分组 CRUD | common/services/ThemeRouteGroupService.php |
| obj ↔ group | common/services/ThemeRouteGroupHandlerService.php |
| 缓存 | common/services/ThemeRouteGroupCacheService.php |
| 前台 tag | extend/liquidExtend/tags/TagGetBlocks.php |
| API | app/api/controller/ThemeGroup.php |
| 写 obj_id | app/home/controller/Product.php 等 detail 方法 |
十二、排障
| 现象 | 排查 |
|---|---|
| 绑定商品仍显示默认组装修 | o_theme_route_group_item 是否有行;Redis theme_group_id:* 是否 stale |
| 装修器与线上一致但组不对 | 编辑器是否传了 group_id;TagGetBlocks 的 obj_id |
| 新组无积木 | 创建时 copy_group_id;默认组是否有已发布 blocks |
| 组内改 block 不影响其他商品 | 确认 o_theme_block.group_id 是否为该组 ID |