Skip to content

装修体系升级规划

平台级专题。覆盖「装修数据模型 → 跳转链接 → 组件库 → C 端运行时」整条链路的重构与补齐。

状态:P0⁻ / P0 / P1 / P2 主体已完成(见 §8.1 落地进展),剩 P2 尾巴与 P3 未开工。 创建于 2026-08-04,方案经两轮拍板确定(2026-08-04),2026-08-05 起连续实施。

⚠️ 两处硬阻塞:框架 hiapi-fast-frame 1.0.5 未发版(否则只有本机能编 public / vem); @hiapi/hiapi-cloud-web-basic 0.0.24 未发 npm(否则三个前端 pnpm install 会失败)。

关联:H5发布流水线规划(产物构建层)、子应用交付-发布安装升级全流程(安装层)。 本文只谈运行时配置层——装修内容、链接、组件、页面,这些都不需要重新构建。


0. 已定案的决策

#决策影响
1链接纯动态,不做 DB 快照兜底静态上报链路(UploadPlugin.pagespages.jsonhiapi_core_system_applink)整体下线;子应用离线 = 明确报错不降级
2前端跳转唯一入口 = HiapiUtils.href不新增 navigate();全仓禁止直接 uni.navigateTo,加 CI 卡口
3必须支持跨端自定义跳转小程序跳小程序、H5/小程序/APP 跳 APP…… 用能力矩阵 + 降级链,见 §4.7
4引入草稿/发布态保存 ≠ 上线,可回滚
5组件视觉整体重做主题令牌 + 存量 12 个组件全部重写样式(data 结构不动)
6本期只落地 H5,架构必须容得下小程序 / APP组件禁用 H5 专属 API、样式单位走 transformStyleUnit、跳转按端降级
7优惠券 / 签到不立后端项相关组件从本期清单移除
8装修数据模型重构:行存 → 整页文档 + 版本新增 P0⁻ 地基修正期,先改地基再盖楼。理由与设计见 §3
9宁可现在大改,不接受上线后返工表结构与文档信封一次到位(含暂不实现的扩展位);所有"以后一定会加"的东西现在留好位置

1. 现状全景

1.1 链路图(含代码落点)

子应用开发期                     发版/安装期                       装修期                        运行期
─────────────────────────────────────────────────────────────────────────────────────────────────────
uniapp-design /                UploadPlugin(closeBundle)      admin-ts /design/page         主项目 H5
public-web                     ├ components.json ─┐           └→ qiankun 装修器             pages/index
├ components/index.ts          ├ pages.json ──────┤              (admin-page-design)        pages/custom
│  widgetExport(组件模板)      ├ menus.json       │              ├ 左:组件目录 ←─ /page-    pages/user-center
├ <widget>/index.vue           ├ admin.json       │              │   layout/components        ↓
├ <widget>/property.vue        └ zip.zip(源码)   │              └ 右:property.vue 面板     DynamicRenderer
└ pages/(对外页面)                  ↓ POST        │                    ↓ 保存                (构建期生成
                               nuwa 应用市场 OSS  │              /page-layout/edit            的白名单)
                                    ↓ 安装        │              PageLayout + PageSchema         ↑
                               AppStoreLogic      │              (← §3 重构为文档模型)     /public/page-
                               ├ reloadComponents─┘                                          layout/page
                               ├ reloadPages → hiapi_core_system_applink   ← 本期整条下线
                               ├ reloadMenus / reloadAdminEntry
                               └ reloadApi

1.2 关键代码位置

环节位置
组件模板声明<子应用>/src/components/index.tswidgetExport
组件/页面上报hiapi-cloud-components-upload/src/index.ts(UploadPlugin)
安装期入库hiapi-cloud-public AppStoreLogic.reloadComponents/reloadPages
组件目录接口PageLayoutController#componentsTreeList(merchant)
链接接口AppLinkController#pageLink / #appLink(merchant)
装修器hiapi-cloud-admin-page-design(qiankun 微应用)
链接选择弹窗hiapi-cloud-admin-ts/src/components/AppLinkDialog/index.vue + stores/app-link.ts
装修数据落库PageLayoutController#updateCallbackPageLayout + PageSchema
C 端取数pub/PageLayoutController#getData + PageLayoutLogic
C 端渲染hiapi-cloud-public-web/src/layouts/DynamicRenderer.vue(生成物,勿手改)
跳转能力hiapi-cloud-web-basic/src/index.tsHiapiUtils

2. 缺陷清单

S 级 — 结构性,必须先于功能开发修掉

#问题位置说明
S1删除是空操作,数据只增不减PageSchemaService.deleteByPageId该方法只删 Redis 缓存,一行数据库都不删,却固定 return 1 表示成功。配合 updateCallback 每次保存 _index + 1 写一整份新行 ⇒ 每保存一次装修页,DB 永久多一份该页全部组件行。10 组件的页面保存 50 次 = 500 行永久垃圾。表还是 ShardingSphere 按 page_id % 3 分表的,事后清理更麻烦
S2PageSchema.version 是死字段entity/PageSchema.java字段存在,全仓从未赋值。⇒ 无从知道某页面是用哪个组件版本装修的 ⇒ 组件 data 结构升级时没有任何迁移依据
S3行存换不来任何收益,却付出三重复杂度PageSchema + PageSchemaService实际访问模式只有"按整页读、读完内存里 sort",没有任何按组件的查询。行存却带来:分表复杂度、缓存复杂度(setMap 无 TTL,靠手动删)、删除复杂度、版本管理昂贵(一个版本 = 一整批行)
S4HiapiUtils 是可变全局单例hiapi-cloud-web-basic/src/index.ts所有能力靠宿主启动时 monkey-patch,默认实现是 Promise.reject("Method not implemented.")。组件依赖已深(buildStyle 19 处、href 15 处、get 13 处);三个宿主各注入一份,已出现签名漂移(装修器 selectLink(type, platform) vs 接口 selectLink(platform))。决策 2 会让它载荷更集中
S5同组件两份代码、两条加载路径装修器 CDN UMD ↔ 运行时构建期合并装修器从 window[project][component] 取(全局命名空间、无沙箱、无版本隔离、仅 H5);运行时用生成的 DynamicRenderer.vue 的 n 个 v-if。⇒ 装修所见 ≠ 用户所得;小程序/APP 永远无法可视化预览;组件到 100 个时每节点判 100 次

S5 不推翻:小程序禁远程代码是硬约束,"预览用远程 + 运行用编译"是业界标准解法。要做的是两条路径契约对齐,不是消灭一条。

P0 — 功能不可用 / 数据错误

#问题位置说明
1链接是静态的AppStoreLogic.reloadPages子应用只能在发版时写死路径清单。带参数的页面(商品详情、分类页、设备详情)、运行时才知道的动态链接,全都表达不了
2AppLink 没有 type 字段entity/AppLink.java但前端读的是 score.row.type,选「应用链接」得到 type === undefined,存进装修 schema 就是脏数据
3type 语义两套混用LayoutPageLink.typePageType(PAGE/USER),Link.typeLinkType(page/app/links)大小写和取值都对不上
4href 不按类型分发,且会崩public-web/src/main.ts:47params.forEach(...) —— 未传时抛异常,传对象也没有 forEach;且不分类型一律 uni.navigateTo
5跳转没有跨端能力全仓无小程序互跳 / 跳 APP / 降级引导;组件里还有直接 uni.navigateTo
6导航栏(tabbar)装修不生效public-web/src/composables/useTabbar.ts后台配的导航菜单 C 端根本没读,tabbar 写死两项
7组件目录不过滤平台/租户订阅componentsTreeListfindAll();前端传的 platform/type 被忽略,Componentsplatform 字段都没建。租户没订阅的应用的组件照样出现
8页面级样式没落地pages/indexpages/customPageLayoutnavbar/title/backgroundColor/backgroundImage/styles C 端一个没用

P1 — 链路残缺

#问题
9没有草稿/发布态(保存即上线,不可回滚)
10预览是死按钮(/micro/design/preview 路由不存在)
11删除 / 导出 / 导入 都是死按钮;删除若接上还得级联清理
12PageLayout.thumbnail 没人生成
13没有页面复制 / 模板
14主题风格页是 12 行空壳
15组件缩略图全空(image: '')
16装修态无 mock 数据,数据类组件在画布里空白

P2 — 体验 / 观感

#问题
17组件只有 12 个,缺口很大
18组件样式写死颜色,无主题令牌
19buildStyle 不支持边框、字号、字重、字色、对齐、行高、宽度 —— 属性面板给不出能力,组件不可能好看
20装修器缺撤销/重做、组件复制、组件搜索、多分辨率预览、快捷键
21链接选择弹窗没有搜索、分组、最近使用

3. 改造零:数据模型重构与地基修正(P0⁻,决策 8/9)

3.1 为什么现在改

发布态一上,每页数据量还要再翻一倍。改造成本随时间线性上涨,现在是最便宜的时刻。 不做这一步直接上发布态,等于在一个"删除是空操作、数据只增不减"的存储上再叠一层版本管理。

3.2 新数据模型

职责切分:PageLayout = 页面身份与路由(可查询);PageVersion = 一切参与渲染的东西(可版本化、可回滚)。

sql
-- 页面版本:一页一份文档
CREATE TABLE hiapi_core_system_page_version (
  id                 BIGINT       PRIMARY KEY AUTO_INCREMENT,
  mid                BIGINT       NOT NULL,       -- 租户
  page_id            BIGINT       NOT NULL,       -- 关联 PageLayout
  version            INT          NOT NULL,       -- 页面内递增版本号
  status             VARCHAR(16)  NOT NULL,       -- DRAFT / PUBLISHED / ARCHIVED
  schema_version     VARCHAR(16)  NOT NULL,       -- 文档协议版本,如 "1.0" ← 协议演进的锚点
  schema_json        LONGTEXT     NOT NULL,       -- 整页文档(信封见 3.3)
  component_versions TEXT,                        -- {"hiapi-public-swiper":"1.0.0", ...} 快照(补 S2)
  thumbnail          VARCHAR(255),
  remark             VARCHAR(255),                -- 版本备注,如"双11首页"
  operator_fid       BIGINT,                      -- 谁改的
  publish_at         BIGINT       DEFAULT 0,      -- 定时发布(0=立即)。本期不实现,先留列
  published_at       BIGINT       DEFAULT 0,
  created            BIGINT,
  updated            BIGINT,
  UNIQUE KEY uk_page_version (mid, page_id, version),
  KEY idx_page_status (mid, page_id, status)
);

PageLayout 调整:

动作字段
保留name / platform / type / isDefault / sort(列表查询要用)
新增draft_versionpublished_version(两个指针,INT)
迁出到文档title / navbar / navbarBack / backgroundColor / backgroundImage / styles(它们参与渲染 ⇒ 必须能随版本回滚)。旧列保留一版做兼容,下一版删
废弃_index(被 version 取代)

不再分表。 一页一行,mid + page_id 有索引,量级完全不需要 ShardingSphere,顺带把 S3 的分表复杂度整条去掉。

3.3 文档信封(一次到位,含预留位)

jsonc
{
  "schemaVersion": "1.0",              // 协议版本。将来大改协议时按它分支解析,不用迁数据
  "page": {
    "title": "首页",
    "navbar": true,
    "navbarBack": false,
    "backgroundColor": "#f5f5f5",
    "backgroundImage": "",
    "styles": {},
    "share": { "title": "", "image": "", "desc": "" }   // H5 分享,本期不实现,留位
  },
  "widgets": [
    {
      "schemaId": "id-x8f2k1",         // 实例唯一
      "component": "hiapi-public-swiper",
      "project": "hiapi-cloud-public",
      "appId": "A-10000",
      "version": "1.0.0",              // 装修时该组件的版本(补 S2)
      "data": {},
      "styles": {}
    }
  ],

  // ↓ 以下为预留扩展位:本期不实现,但信封结构先定,将来加功能不用迁数据(决策 9)
  "audience": null,                    // 投放规则:按人群/会员等级/地区展示不同页面
  "i18n": null,                        // 装修文案多语言(平台已有 i18n 体系,装修内容迟早要跟上)
  "experiments": null                  // A/B 实验分流
}

预留位的原则:只定位置,不写实现,不建索引。它们是 JSON 里的一个键,加功能时填上即可;不预留的话,将来就是一次全量数据迁移。

3.4 附带解决的四件事

原问题文档模型下如何消失
S1 删除是空操作删除 = 删行,真实有效。GC 策略:已发布版本永不删,其余保留最近 20 个,定时任务清理
S3 缓存无 TTL、要手动删缓存 key 带版本号(mid:pageId:publishedVersion)⇒ 版本一变缓存天然失效,永远不会脏。这是文档模型的额外红利
发布/回滚原子性发布 = 一行 UPDATE(published_version = N),不存在"删一半写一半"的中间态;回滚 = 指针指回去
C 端读取性能一次主键读拿整份 JSON,不再 N 行拼装 + 内存排序

3.5 并发保护(现在做最便宜)

两个人同时装修同一页,现在是后写覆盖先写,无感知。文档模型下顺手加乐观锁:

  • 保存草稿携带 baseVersion;服务端 CAS,不匹配返回 409 + 「该页面已被 XXX 修改,请刷新」。
  • 发布同理,防止发布一个已过期的草稿。

3.6 接口调整

接口说明
POST /merchant/system-basic/page-layout/draft保存草稿(带 baseVersion 乐观锁)
POST /merchant/system-basic/page-layout/publish发布指定版本
POST /merchant/system-basic/page-layout/rollback回滚到历史版本(实为新建一个 PUBLISHED 版本,不覆盖历史)
GET /merchant/system-basic/page-layout/versions版本列表(时间、操作人、备注、缩略图)
GET /merchant/system-basic/page-layout/{id}?version=取指定版本;缺省取草稿
GET /public/page-layout/pageC 端:读 published_version 的文档;带 preview=1&version= 时读指定版本(预览用)
POST /page-layout/edit保留一个版本做兼容(内部 = 存草稿 + 立即发布),给装修器留迁移窗口(装修器是独立发布的 qiankun 微应用,版本不一定同步)

3.7 迁移方案(⚠️ 有坑,必须按这个来)

旧表是 ShardingSphere 分表的,不能用纯 SQL 迁移 —— Flyway 走物理连接,绕不过分片逻辑表,历史上已经踩过 Flyway 撞 ShardingSphere 的雷。

正确做法:

  1. Flyway 只建新表 + 加 PageLayout 新列,不迁数据。
  2. 数据迁移写成一次性平台端接口 / 启动任务(走应用层,经 ShardingSphere 逻辑表读),把 (mid, page_id, _index) 的现存行聚合成 JSON 写入 page_version(status=PUBLISHED, version=1),回填 published_version=1
  3. 迁移任务幂等 + 可重跑 + 输出对账报告(页面数、组件数、失败清单)。
  4. 双读校验一版:C 端优先读新表,读不到回落旧表并告警。
  5. 观察一版无告警 → 删 PageSchema 实体/Service/Jpa + Flyway 删旧表。

3.8 同期修掉的另外两处地基

S4 — HiapiUtils 加固(决策 2 会让它载荷更集中,必须先加固):

  • 启动期契约校验:宿主注入完调用 HiapiUtils.assertReady(),缺哪个方法直接 fail fast 并列出缺失清单,而不是等到用户点下去才 Method not implemented.
  • 三个宿主(装修器 standalone / 装修器 qiankun / C 端 H5)的注入收敛成一份共享工厂,消灭签名漂移。
  • HiapiUtils.version,组件可声明所需最低版本。

S5 — 两条渲染路径契约对齐(不推翻,只对齐):

  • 装修器按 ComponentType.version 拼 UMD 地址(已如此),新增一致性校验:装修器顶部显示"当前组件版本 vs 运行时版本",不一致给醒目提示。
  • 统一 props 契约(schema / type / platform)与降级表现,两条路径共用同一套「未知组件占位」UI。
  • DynamicRenderer 的 n 个 v-if 改为 map 查表 + <component :is>,组件数量增长时不再线性劣化(生成器插件里改,DynamicRenderer.vue 是产物勿手改)。

4. 改造一:跳转链接协议

4.1 纯动态(决策 1)

链接目录只在打开链接选择器的那一刻,由 public 实时 Feign 问子应用要:

  • 子应用离线 / 超时(3s)→ 明确提示「XX 应用当前不可用」+ 重试按钮。不给旧数据,避免商户选到已不存在的路径。
  • 允许 30s 进程内缓存(纯性能,防止切 tab 反复拉),过期即失效,不作为离线兜底
  • 静态链路整体下线:UploadPlugin.pagesAppStoreLogic.reloadPagesAppLink 实体/Service/Jpa/表,全部删除。

    节奏:先上新接口 → 后台切新接口 → 观察一版 → 删表和旧代码。UploadPlugin.pages 保留一版打 deprecated 警告,给子应用迁移窗口。

4.2 后端契约(框架层,hiapi-core-app-install)

可选 SPI,子应用不实现就用默认空实现,存量 10 个仓零改动:

java
public interface AppLinkProvider {
    default List<AppLinkGroup> links(AppLinkQuery query) { return List.of(); }
    default LinkOptionPage options(LinkOptionQuery query) { return LinkOptionPage.empty(); }
}
java
AppLinkGroup { String key; String name; String icon; List<AppLinkItem> items; }

AppLinkItem {
    String key;              // shop.goods.detail
    String name;             // 商品详情
    String path;             // /subShop/pages/goods/detail
    String icon;
    List<Platform> platforms;// 空 = 全部
    boolean needLogin;
    List<LinkParam> params;  // 空 = 直接可用
}

LinkParam { String key; String name; ParamType type; boolean required; String defaultValue; String optionSource; }
LinkOptionPage { List<LinkOption> list; long total; }   // LinkOption { value, label, image, description }

HTTP 端点(框架 InstallController 同址扩展):

GET /public/app/links?platform=H5&scene=PAGE
GET /public/app/link-options?linkKey=&paramKey=&keyword=&page=&size=

ICloudAppService@RequestLine 方法,经 FeignFactory.createService(serverId, ...) 动态调用 —— 和 reloadApi 同一条路,不新增 feign 模块、不动 -contract-feign 结构。

4.3 hiapi-cloud-public

接口变化
GET .../link/apps新增,当前租户可选应用(按 SubscriptionApp 订阅 + install==2 过滤)
GET .../link/app-link重写,实时 Feign 取目录;失败返回错误码,不降级
GET .../link/link-options新增,按 appId 透传子应用
GET .../link/page-link保留;修正 type 语义,按 platform 过滤
GET .../link/builtin新增,内置功能页目录(登录/个人中心/消息/余额/充值/提现/地址/实名/客服…),集中一处维护

先解决命名冲突:现有 type: 'app'子应用页面,而决策 3 的"跳 APP"指原生应用。两个 app 挤一个字段,后面每个组件都要猜。拆开:

ts
export type LinkKind =
  | 'page'    // 本站装修页面
  | 'sub'     // 子应用页面(旧值 'app')
  | 'web'     // 外部网页(旧值 'links')
  | 'mini'    // 小程序
  | 'native'  // 原生 APP(Scheme / Universal Link / App Link)
  | 'tab'     // 切换底部导航
  | 'action'  // 端能力:拨号/复制/分享/客服/返回

export type Link = {
  kind: LinkKind
  url: string
  name: string
  appId?: string
  project?: string
  params?: Record<string, string | number>
  needLogin?: boolean
  openType?: 'navigate' | 'redirect' | 'switchTab' | 'reLaunch' | 'webview'
  platforms?: Platform[]

  mini?: { appId: string; path?: string; envVersion?: 'release'|'trial'|'develop';
           extraData?: Record<string, any>; shortLink?: string }

  native?: { scheme?: string; universalLink?: string; androidPackage?: string; androidAction?: string }

  fallback?: Link                      // 当前端不支持时的降级目标
  type?: string                        // ⚠️ 兼容字段:线上老数据
}

兼容线上老数据是硬要求 —— 装修 schema 是已落库 JSON,不能要求商户重配。normalizeLink(raw): Link 渲染期兜底:

老数据归一化
type: 'PAGE' / 'USER' / 'page'kind: 'page'
type: 'app'kind: 'sub'
type: 'links'kind: 'web'
undefined + 有 appIdkind: 'sub'
undefined + url 以 http 开头kind: 'web'

只增不删,type 永久保留。

迁移加速:§3.7 的数据迁移任务顺便把老 link 归一化写进新文档,新数据一次性干净;normalizeLink 仍保留作运行时兜底。

4.5 后台链接选择器重做

┌ 选择链接 ──────────────────────────────────────────────────────┐
│ [搜索框                                      ]                  │
│ ┌ 页面 │ 应用 │ 功能页 │ 外链 │ 小程序 │ APP │ 动作 ┐          │
│ │ 应用: [商城 ▾]   分组: 商品 / 订单 / 会员          │          │
│ │  ○ 商品详情      /subShop/pages/goods/detail        │          │
│ ├─ 参数(选中带参链接后出现)──────────────────────┤          │
│ │  商品 *  [搜索选择商品…                        ▾]  │          │
│ ├─ 降级(当前链接在某些端不支持时)────────────────┤          │
│ │  不支持时跳转到: [选择链接…]                       │          │
│ ├─ 最终链接:/subShop/pages/goods/detail?id=12 ──────┤          │
│ └────────────────────────────────────────────────────┘          │
└────────────────────────────────────────────────────────────────┘
  • 应用离线 → 该行标红 +「应用不可用,重试」,不显示任何旧链接。
  • 参数按 LinkParam.type 自动渲染;SELECT 走远程搜索,显示图片+名称。
  • APP tab 强制要求配 fallback —— 没有降级的跳 APP 在多数端上就是死按钮。
  • 顶部显示"当前页面平台",选到该端不支持的链接给黄色提示(不禁止,因为有降级)。
  • 底部实时显示最终 URL;「最近使用」本地存 10 条。

4.6 运行时:唯一入口 HiapiUtils.href(决策 2)

ts
href(link: Link): Promise<void>
href(url: string, params?: Record<string, any>, openType?: OpenType): Promise<void>   // 旧签名保留
  1. 入参先过 normalizeLink()
  2. needLogin 且未登录 → 先登录,带 redirect 回跳。
  3. kind + 当前端分发(§4.7);不支持 → fallback;仍没有 → toast 提示,绝不静默失败
  4. kind: 'sub' 先校验子应用是否在本份产物里(build-record.json / 已装应用清单),不在则提示「该功能未开通」而不是白屏。
  5. 修掉 params.forEach 崩溃(Object.entries + 空值保护 + URL 编码)。

收敛卡口:禁止业务代码直接 uni.navigateTo / redirectTo / reLaunch / switchTab / location.href

  • 先清存量(public-web/src/pages/auth/*uniapp-design/src/subVem/pages/*)。
  • hiapi-chart/ci/check-navigate.sh,与现有五个门禁同款(纯静态、零依赖、棘轮名单 navigate-baseline.txt 只能变少)。

4.7 跨端跳转能力矩阵(决策 3 核心)

目标 \ 源端H5微信小程序APP(uni-app)
page / sub / tab内部路由内部路由内部路由
web同窗 / window.openweb-view 中转页(域名需白名单)内置 webview / 系统浏览器
mini微信内:JSSDK wx.miniProgram.navigateTo(需公众号关联)
微信外:shortLink / Scheme 唤起,失败 → 降级页
wx.navigateToMiniProgram(目标 appId 需在关联白名单)开放平台 openSDK;未集成 → fallback
nativeuniversalLink 优先 → scheme 兜底 → 1.5s 未离开页面则跳 fallback(下载引导)⚠️ 微信禁止小程序直接唤起第三方 APP(仅"从 APP 分享进入"场景可用 launchApp),其余一律 fallbackplus.runtime.openURL / Android Intent;未安装 → fallback
action:phonetel:wx.makePhoneCalluni.makePhoneCall
action:copyClipboard APIwx.setClipboardDatauni.setClipboardData
action:share分享面板 / 复制链接引导右上角转发原生分享面板
action:service客服页 / 企微链接<button open-type="contact">客服页

实现结构(hiapi-cloud-web-basic,各端注入适配器):

href(link)
  └→ normalizeLink
  └→ resolvePlatform()
  └→ capability(kind, platform)
        ├ 能 → 端适配器执行
        └ 不能 → link.fallback ? href(fallback) : toast
  • 能力表是数据不是 if-else:CAPABILITY[platform][kind] = 'direct' | 'bridge' | 'unsupported'。加一个端 = 加一列。
  • 本期只实现 H5 列(决策 6);小程序/APP 列先落表、落接口、落降级,到那一端补适配器即可,协议和装修数据不用动。

5. 改造二:装修链路补齐

5.1 草稿 / 发布 / 回滚(决策 4,建立在 §3 之上)

  • 保存 → 新建/更新 status=DRAFT 的版本(带 baseVersion 乐观锁)。
  • 发布 → 该版本 status=PUBLISHED + PageLayout.published_version = N,一行 UPDATE 完成
  • 回滚 → 以历史版本内容新建一个 PUBLISHED 版本(不覆盖历史,留审计痕迹)。
  • C 端读 published_version;装修器读 draft_version;预览可指定任意版本。
  • 页面列表状态:草稿 / 已发布 / 有未发布改动
  • GC:已发布版本永不删,其余保留最近 20 个,定时任务清理。

5.2 预览

真环境预览:保存草稿 → 后台弹窗内嵌 iframe 打开 H5 站点 ?preview=1&pageId=&version=,C 端识别后读指定版本。比在装修器里再实现一遍渲染更真实,也顺带验证了 S5 两条路径的一致性。

同时把 /design/page 的预览按钮接上(现在指向不存在的 /micro/design/preview)。

5.3 页面管理补齐

  • 删除(级联删该页所有版本;默认页不允许删)
  • 复制页面(复制文档即可,文档模型下是一行 INSERT)
  • 导出 / 导入 JSON(文档模型下天然就是导出 schema_json;也是「模板库」的地基)
  • 缩略图:装修器保存时截图上传,写入版本行
  • 页面列表改 TablePage,补搜索/平台/发布状态筛选

5.4 组件目录过滤(缺陷 7)

  • Componentsplatforms(逗号串)、scenes(PAGE/USER),由 widgetExport 声明 → components.json 上报 → 安装期入库。
  • componentsTreeListplatform + type + 租户已订阅应用(SubscriptionApp.status=1 且未过期)过滤。

5.5 页面级样式与导航栏生效

  • C 端页面读文档里的 page.title / navbar / navbarBack / backgroundColor / backgroundImage / styles 并应用(缺陷 8)。
  • useTabbar 改为拉 /public/page-layout/navbar;NavbarItem 升级到新 Link 协议(现在只存一个 path 字符串),支持角标(消息未读数)、登录可见性、按平台显示。

6. 改造三:组件体系与视觉(决策 5)

6.1 先有主题令牌,再谈组件

  1. 主题令牌:租户级配置(主色/辅色/圆角/间距/阴影/字号阶梯/暗色)存 Config,C 端启动注入 CSS 变量(--hi-color-primary--hi-radius-md--hi-space-3…)。
  2. 组件禁止写死颜色,一律用变量 —— 换肤 = 改配置。
  3. theme-style 空壳补齐:主题配置页 + 实时预览 + 3~5 套预设风格。
  4. buildStyle:边框、字体(size/weight/color/align/lineHeight)、宽度/对齐、渐变方向、阴影 spread、暗色变体。属性面板没这些能力,组件就不可能好看。
  5. 统一组件外壳:一致的内外边距/圆角/阴影/背景 + 统一的空态、骨架屏、加载态、错误态。
  6. 每个组件配 3~5 套预设样式。让商户调 20 个数值结果一定难看,给他挑预设才守得住下限。
  7. 补组件缩略图:每组件一张 SVG 随 components.json 上报,左侧面板从文字列表变卡片选择。
  8. 装修态 mock:getEnv() === 'DESIGN' 时数据类组件用假数据。

6.2 组件补齐清单

现有 12 个(轮播、宫格菜单、公告、余额、用户头部、用户信息、导航列表、魔方、单图、辅助空白、消息入口、公告条)—— 全部重写样式(数据结构不动)。

第一批(通用,10 个):分割线、标题栏、卡片容器(需装修器支持嵌套)、图文列表、富文本(小程序用 rich-text)、视频、按钮组、搜索框、倒计时、悬浮按钮。

第二批(业务,只做后端已就绪的):会员等级卡、积分/购物金卡片、快捷充值、订单入口(依赖 §4 动态链接)、数据看板卡(参考 vem overview/trend)。

优惠券领取 / 签到 —— 后端不存在,本期不立项(决策 7)。

6.3 组件的跨端约束(决策 6)

约束说明卡口
window / document / localStorage / alert小程序无 DOM;存储走 uni.getStorageSyncCI 检查组件目录
尺寸走 HiapiUtils.transformStyleUnit不得硬编码 pxCI + Review
不用 v-html,富文本用 rich-text小程序不支持CI
不用深层选择器 / >>>小程序样式隔离CI
不动态加载远程脚本小程序禁远程代码已有
跳转只用 HiapiUtils.href§4.6check-navigate.sh

7. 改造四:基础功能页面缺口

7.1 C 端(hiapi-cloud-public-web)

已有:登录/注册/找回、个人中心(头像/昵称/性别/生日/手机/邮箱/实名/密码/支付密码/安全/登录记录/绑定/地址/反馈/关于/隐私/设置/消息)、财务(资产/收银台/充值/转账/提现)、首页、自定义页。

页面优先级备注
登录回跳 / 未登录访问 USER 页P0needLogin 的前提
错误页(404/无权限/维护中)+ 网络异常兜底P1现在直接白屏
搜索页 / 搜索结果P1搜索框组件落点
消息详情 / 公告详情P1现在只有列表
会员中心(等级/权益/成长值)P1后端已就绪,前端完全没有
积分 / 购物金明细P1同上
客服 / 在线咨询P1action: service 落点
web-view 中转页P1kind: 'web' 在小程序端落点,H5 期先建路由
跳转降级引导页(下载 APP / 打开方式)P1native / minifallback 落点
我的订单(跨子应用聚合)P2依赖动态链接
帮助中心 / FAQ、分享/邀请、语言与主题入口、账号注销P2

7.2 商户后台(hiapi-cloud-admin-ts)

页面状态
装修页面列表有,但删除/导入/导出/预览是死按钮
个人中心装修 / 导航菜单 / 素材库有(导航菜单 C 端不生效)
主题风格空壳
页面模板库
H5 发布页(触发构建 + 历史)(后端接口已就绪)
组件管理(启停某组件)
版本历史 / 回滚(§5.1 新增)

8. 分期计划

内容交付验收
P0⁻ 地基修正§3 全部:文档数据模型 + 迁移(含对账/双读)+ 真删除与 GC + 乐观锁 + HiapiUtils 契约校验与注入收敛 + 两条渲染路径对齐 + DynamicRenderer 改查表现网页面 100% 迁移且渲染一致;保存 100 次数据量不增长;缺方法在启动期报错;装修器显示组件版本
P0 链接打通§4 全部 + 缺陷 1~5 + C 端登录回跳/降级引导页子应用能动态返回带参链接;后台能配参数与小程序/APP 跳转及降级;C 端正确跳转;老装修数据不炸
P1 链路闭环草稿/发布/回滚 + 版本历史页、预览、删除/复制/导入导出、缩略图、组件过滤、页面级样式、导航栏生效、错误兜底页建页 → 装修 → 预览 → 发布 → 回滚 全流程可用
P2 视觉重做主题令牌 + buildStyle 扩展 + 组件外壳统一 + 预设样式 + 缩略图 + 装修态 mock + 主题配置页 + 存量 12 组件重写 + §6.3 跨端 CI换主色一处生效;12 个组件全部重做并通过跨端检查
P3 组件与页面第一批 10 个通用组件 + 第二批业务组件 + C 端缺失页面 + 后台模板库 / H5 发布页组件数 12 → 27+;C 端 P1 页面补齐

P0⁻ 必须先做完再进 P0;P0 → P1 有依赖(P1 组件过滤用到 P0 的平台字段);P2/P3 可与 P1 并行。


8.1 落地进展

状态位置
文档数据模型(实体 / 枚举 / 文档信封)✅ 已完成PageVersionPageVersionStatusdocument/{PageDocument,PageMeta,PageShare,WidgetNode}
PageLayout 版本指针✅ 已完成新增 draftVersion / publishedVersion;_index@Deprecated
版本服务(真删除 + GC + 版本号分配)✅ 已完成PageVersionService(deleteByPage / gc / nextVersion)
Flyway 建表 + 加列(不搬数据)✅ 已完成V1__page_version.sql
迁移任务(逐页事务 / 可重复执行 / 断点修复 / 对账)✅ 已完成PageVersionMigrationService + PageMigrationWriter + PageMigrationReport
平台端触发接口(演练 / 正式)✅ 已完成POST /platform/page-migration/run?dryRun=
文档序列化回归测试✅ 已完成PageDocumentTest(10 例,纯静态零依赖)
版本编排(存草稿 / 发布 / 回滚 / 乐观锁 / GC)✅ 已完成PageVersionLogic
商户端接口(草稿 / 发布 / 回滚 / 版本列表 / 版本详情)✅ 已完成/merchant/system-basic/page-layout/{draft,publish,rollback,versions,version}
/edit 兼容(= 存草稿 + 立即发布)✅ 已完成PageLayoutController#updateCallback
页面删除级联清版本 + 租户校验✅ 已完成deleteBeforeIntercept
C 端读已发布版本(+ 迁移观察期回落旧表告警)✅ 已完成PageLayoutLogic#toVo / legacySchemas
版本回收规则测试✅ 已完成PageVersionServiceTest(6 例)
组件目录按平台/场景/租户订阅过滤✅ 已完成Components.platforms/scenes + V2__components_platform_scene.sql + componentsTreeList;判定抽成纯函数 Components.supports
页面管理:删除 / 复制 / 导出 / 导入✅ 已完成后端新建 PageTemplateController;前端接上四个死按钮
装修器预览 + 页面缩略图✅ 已完成uploadBlob 契约 + 两宿主注入 + html2canvas 截图
C 端页面壳:tabbar 布局 + 页面级配置 + 预览参数✅ 已完成新建 layouts/tabbar.vue(此前不存在)、usePageLayoutcommon/PageShell.vue
C 端错误兜底 + HTTP 故障统一处理✅ 已完成utils/http-fault.tspages/error/index.vue;修 401 双跳/不清 token/401 与 403 混淆
装修器接入草稿/发布✅ 已完成admin-page-design:顶栏「保存草稿 / 发布」+ 发布状态标签,提交带 baseVersion
宿主注入补全(showToast / showAlert)✅ 已完成两个宿主原先都没注入,子应用调的是空实现 —— 保存成功用户看不到任何反馈
后台版本历史 + 回滚 UI✅ 已完成views/design/components/version-history.vue;页面列表加「发布状态」列与「版本」入口
PageSchema 及旧表下线⬜ 未开始双读观察一版、确认无回落告警之后

P0 链接协议(§4)

状态位置
框架 SPI:子应用运行期公布可跳转页面✅ 已完成hiapi-core-app-install/link/:AppLinkProvider + AppLinkGroup/Item + LinkParam/Option
VEM 参考实现(9 条链接,4 条带参)✅ 已完成VemLinkProvider,SELECT 参数源按 mid 过滤
public 侧实时目录接口✅ 已完成AppLinkController:/apps /app-link /link-options /builtin /page-link
统一 Link 协议 + 跨端能力矩阵✅ 已完成web-basic/src/{schema,link,navigator}.ts;能力矩阵是数据表不是 if-else,加一个端 = 加一列
后台链接选择器重做(7 页签 + 参数表单 + 强制降级)✅ 已完成AppLinkDialog + LinkParamForm
C 端跳转适配器(五端条件编译)✅ 已完成public-web/src/utils/link-adapter.ts + pages/webview(小程序外链中转,只放行 http(s))
跳转收敛门禁✅ 已完成check-navigate.sh + navigate-baseline.txt

P2 视觉重做(§6)

状态位置
主题令牌(5 套预设 + 合并规则 + 转 CSS 变量)✅ 已完成web-basic/src/theme.ts。尺寸存设计像素,rpx 端 ×2
buildStyle 扩展(边框/字体/宽高/渐变/阴影 spread/布局)✅ 已完成数字补单位、字符串原样透传 —— 后者是令牌能用起来的关键
组件统一外壳(卡片/文字层级/空错加载态/按压反馈)✅ 已完成public-web/src/styles/widget.scss,由 App.vue 全局引入
C 端主题注入✅ 已完成useBrandTheme:H5 挂 documentElement,小程序走 PageShell 内联变量继承
存量 12 组件改用令牌✅ 已完成顺带修了 4 个真 bug,见下
装修态 mock 数据✅ 已完成utils/widget.tswithDesignMock,已用在 notice / grid / nav-list / balance
主题配置页(预设 + 取色 + 实时预览)✅ 已完成admin-ts/views/design/theme-style,此前是空壳且无菜单入口
装修器画布套用商户主题✅ 已完成admin-page-designdevice-screen 挂同一份 themeToCssVars
跨端约束 CI(§6.3)✅ 已完成check-widget.sh + 单行 hi-allow-color 豁免 + widget-baseline.txt
每组件 3~5 套预设样式✅ 已完成common/presets.ts(12 组件各 3~4 套)+ PresetPicker.vue 挂进 12 个属性面板
组件缩略图✅ 已完成common/thumbnails.ts 12 张内联 SVG;Components.image 放宽到 TEXT(V3)
暗色模式✅ 已完成内置深色板(web-basic 0.0.25)+ useSystemDark + 后台开关,默认关闭

P3 组件与页面(进行中)

状态位置 / 说明
第一批通用组件 9 个✅ 已完成分割线、标题栏、图文列表、富文本、视频、按钮组、搜索框、倒计时、悬浮按钮。组件数 12 → 21,各配缩略图与 3~4 套预设
卡片容器(第 10 个)✅ 已完成hiapi-public-card-container。组件数 21 → 22。原计划要动装修器拖拽模型 + DynamicRenderer 生成器 + WidgetNodechildren 三处——重新评估后发现都不需要:子组件塞进组件自己的 data.children(本来就是不透明 JSON,后端/HiapiCloudSchema 零改动),渲染直接复用生成产物 DynamicRenderer.vue 递归一层;子组件的增删排序收在卡片容器自己的属性面板里,复用被选中类型(标题栏/单图/富文本/按钮组/分割线,白名单 5 个)各自现成的 property.vue,不需要画布支持嵌套拖拽。三路调研(装修器拖拽模型、生成器机制、后端 WidgetNode 模型)详见 git 提交记录
跳转降级引导页✅ 已完成pages/guide/index,native/minifallback 默认落点
错误页 / 网络异常兜底✅ 已完成(P1)pages/error/index + utils/http-fault.ts
web-view 中转页✅ 已完成(P0)pages/webview/index
客服 / 在线咨询✅ 已有pages/user-center/feedback,action: 'service' 已指向它
消息详情 / 公告详情✅ 已有不是缺口 —— message.vue 里是底部弹层详情,不是死列表。原清单记错了
第二批业务组件⬜ 未开始积分/购物金/快捷充值三个不再卡后端(见下面「重新判定」);会员等级卡、订单入口仍待
搜索页 / 搜索结果⚠️ 不做见下面「重新判定」
积分 / 购物金明细✅ 后端已就绪,待接前端见下面「重新判定」——之前判断为被后端阻塞是误判
会员中心(等级/权益/成长值)⛔ 被后端阻塞见下面「先要后端」
后台页面模板库✅ 已完成admin-ts/views/design/template + data/page-templates.ts,5 套起手式;复用现成的 /page-template/import,后端零改动
H5 发布页(平台)✅ 已完成admin-ts/views/platform/h5-build;/platform/h5-build 后端一直是齐的,只是没有入口

记一条教训:admin-ts 的类型检查要用 pnpm build

npx vue-tsc --noEmit 会因为 tsconfig 里 baseUrl 的弃用警告直接退出, 看起来像"只有一条无关警告、没有错误",实际上一个文件都没检查。 主题页 5 处 implicit any 就是这么漏过去的 —— 直到跑 pnpm build(内部是 vue-tsc -b)才暴露。

admin-ts 一律以 pnpm build 为准。

重新判定:搜索页不该由 public 做

原清单把「搜索页 / 搜索结果」列为搜索框组件的落点。实际盘下来, public 没有、也不该有一个通用搜索后端 —— 能搜的东西(商品、设备……) 都在各子应用里,各有各的字段和权限。

搜索框组件已经通过 Link 协议跳到商户配置的目标页,那才是正确的落点。 在 public 里放一个搜索页,只能是个搜不出东西的壳。本项从清单移除。

重新判定:积分/购物金不归 user 管,归 finance 的 Assets 管

上一版这里把"积分/购物金明细"和"会员中心"混成一件事,判断成同样被后端阻塞—— 这是误判,而且判断方向都找错了服务。积分、购物金和余额是并列的三种 Assets(资产)类型(FinanceConst.ASSET_TYPE_POINTS / _BALANCE / _SHOPPING_CREDIT, hiapi-cloud-finance/hiapi-finance-entity/.../entity/Assets.java),商户注册时 AssetTypeService#ensureDefaultAssetTypes 自动开通,不在 user 服务里

C 端接口早就有,在 hiapi-finance-user-api:

  • GET /user/finance/assets/list —— 我的资产列表(含积分/购物金余额)
  • GET /user/finance/assets/detail?type=POINTS —— 单个资产详情(图标/精度/是否可充值)
  • GET /user/finance/asset-record/query —— 变动明细(继承 BasicQueryController, 按 type/incomeType/时间区间过滤,AssetRecordQuery 天然就是"积分明细页"要的接口)

真正要确认的不是"接口在不在",而是:①网关有没有把 finance 的 /user/** 前缀路由到位;②该商户后台有没有把 POINTS 的收支权限开关打开 (/merchant/finance/assets-type,默认全关);③业务侧下单链路 (VEM/Shop 等)有没有真的调 ICloudFinanceAssetsService.changeAssets(..., POINTS, ...) 产生流水——没接的话页面能查通但永远是空的,这是业务侧要不要接的问题, 不是接口缺失。

第二批业务组件里,积分卡片/购物金卡片/快捷充值三个可以直接对接 finance 的 Assets/AssetRecord 接口,不再卡后端;会员等级卡、订单入口仍按下条卡着。

先要后端:会员中心(等级 / 权益 / 成长值)

GradeController / GradeRightController(hiapi-cloud-user,api/merchant 下)管的是 等级规则与权益配置(等级 CRUD、权益类型如折扣/免邮/积分倍率/礼包),是商户后台用的 规则层,和积分账本(见上条)是两件事——一个定规则,一个记数字。

hiapi-user-api(C 端模块)目前没有任何"查我的等级 + 当前权益"的接口;唯一相关的 FeignGradeRightController(/feign/user/grade-right/list)是服务间调用入口, 不是能挂网关给前端调的路径。要做会员中心,得先在 hiapi-user-api 新增一个 C 端 Controller,把 GradeService/GradeRightService 包一层暴露出来。

"成长值"这个数据模型在代码里完全不存在 —— 现有等级升级靠 UpgradeTask(定时任务)按"消费金额门槛 + 邀请人数门槛"实时判定,User 实体 没有 growthValue 字段,Grade.configText 也没有积分门槛这类配置被用上。 如果产品要"成长值进度条"这种 UI,不是补个查询接口那么简单,是要新增数据模型 的新需求,要不要做需要业主拍板。

会员等级卡组件、订单入口组件仍卡在这里(订单入口另需 §4 动态链接支持"我的订单"聚合)。

P2 期间的两次自我修正

写门禁和写预设各返工了一次,都是实现之后跑一次才暴露的设计错误,记在这里:

  1. check-widget.sh 最初一视同仁地扫了 property.vue,报了 40 多条全是误报。 那是装修器的属性面板,只在浏览器跑、永远不会打进小程序包, 用 document.elementFromPoint 做拖拽热区完全正当。已限定只扫渲染文件。 顺带:注释行也要放过,否则「不要写死 #333」这句注释会被自己拦下来。

  2. 预设最初按「垫底 + 用户覆盖」实现,根本不生效。 组件注册表的默认 styles 自带一堆显式值(marginLeft: 0borderTopLeftRadius: 0backgroundColor: 'transparent')—— 语义上是"没设置",数据上是实打实的用户值,于是永远压过预设,点哪个都没反应。 改成点击即写入 schema.styles:所见即所得,后续手动调整自然覆盖, 渲染路径一行不用改。

共同点:两处都是"看起来合理的分层设计",在真实数据面前才发现分层的前提不成立。

P2 期间发现并修掉的既有缺陷(都是"看起来在工作、其实从来没工作过"这一类):

缺陷原因
notice 的「更多」按钮从不跳转模板传 {url:...},handler 读 e.link,取的字段不存在
nav-list 的角标配了也看不见.nav-badge 有样式,模板里从来没有对应元素
grid / swiper 空数据时骨架屏永远转数据是属性面板里配的静态内容,"空"= 商户没配,不是没加载完
默认头像取自 Element UI 演示图床(3 处)fuss10.elemecdn.com,第三方图床 + 每次访问外发请求。换内联 SVG
ConfigService 配置缓存从未命中读写 getMapKey 实参顺序反了,两个参数都是 String,编译期发现不了

⚠️ web-basic 0.0.25 尚未发布到 npm(需要 2FA,只有业主能发)。 三个消费方(public-web / admin-ts / admin-page-design)的依赖已经抬到 ^0.0.25, 发布之前它们 pnpm install 会失败。 注意 ^0.0.x 不包含 0.0.(x+1):主次版本都是 0 时 ^ 等价于精确匹配, 所以这个抬版本是必须的,不是可选的。

写路径已切换:新的保存只写 PageVersion,不再写 PageSchema。因此旧表从切换之日起 是一份冻结快照(停在迁移那一刻),不再是可回退的实时副本 —— 切换后再想退回旧模型, 只能靠备份恢复。所以务必按顺序来:先 dryRun 对账 → 正式迁移 → 再发布这版代码。

已知取舍:C 端公开接口的 version 参数只允许读已发布/已归档版本,不给草稿 (公开接口只要知道域名和页面 ID 就能访问,放开草稿等于提前泄露未发布的活动页)。 后台预览草稿走商户端 /page-layout/version(带登录态)。将来要在真实 C 端页面里预览草稿, 需要商户端签发一个短期预览令牌 —— 归到 P1 的预览功能一起做。

迁移执行顺序(生产):备份 → dryRun=true 看对账(consistent 必须为 true、sourceWidgets == targetWidgets)→ 正式执行 → 再切读路径。 迁移只读旧表、只写新表,一行旧数据都不删,随时可退回旧读路径。


9. 风险与约束

风险对策
数据迁移出错 = 商户装修内容丢失(本期最大风险)迁移任务幂等可重跑 + 对账报告 + 双读一版(新表读不到回落旧表并告警)+ 旧表冻结观察一版再删。迁移前全量备份
Flyway 撞 ShardingSphereFlyway 只建表,数据迁移走应用层任务(§3.7)。历史上已踩过这个雷
装修器与 admin 独立发布,版本可能不同步/edit 接口保留一版做兼容;装修器顶部显示协议版本
Link 协议影响线上装修 JSON只增不删 + normalizeLink() 兜底 + type 永久保留 + 迁移时顺便归一化;上线前用线上数据跑归一化回归
纯动态 = 子应用离线就选不了链接决策 1 明确接受的取舍。错误提示具体到应用名 + 一键重试
静态链路下线要跨仓改先上新后删旧;UploadPlugin.pages 保留一版打 deprecated
框架加 SPI 要发版并抬 10 仓 parentSPI 全部 default 空实现,子应用不改也能编过
小程序跳小程序/跳 APP 有平台级限制不是我们能绕的。fallback 在选择器里是强制项,产品上要认这个现实
组件视觉重做碰所有存量组件只改样式实现与 styles 可选项,不改 data 结构
本期只做 H5,小程序/APP 未验证能力矩阵按端分列先落表;组件按 §6.3 约束写并用 CI 卡住 —— 到那一端是"补适配器"不是"重做组件"

10. 明确不做(但已留好位置)

决策 9 要求"不返工",以下功能本期不实现,但数据结构/信封已预留,将来加功能不需要迁移数据:

能力预留位置
定时发布 / 定时下线page_version.publish_at
装修文案多语言信封 i18n
人群投放(不同会员等级看不同首页)信封 audience
A/B 实验分流信封 experiments
H5 分享配置(标题/图/描述)信封 page.share
协议大版本演进schema_version 列 + 信封 schemaVersion
"哪些页面用了组件 X"(组件下线影响面)不建索引表 —— 可随时从 schema_json 扫描重建,属于纯派生数据,后补不算返工

变更记录

日期变更
2026-08-04创建文档:现状盘点、缺陷清单、四项改造设计、四期计划
2026-08-04第一轮拍板:链接纯动态、跳转唯一入口 + 跨端能力矩阵、要发布态、组件整体重做、只做 H5 但按三端约束、优惠券/签到不立项
2026-08-05P0 链接协议全链路打通;P2 主题令牌与 12 组件重做落地,新增 check-widget 门禁
2026-08-04架构复核发现 S1~S5 结构性问题(删除是空操作、version 死字段、行存无收益、HiapiUtils 全局单例、双渲染路径)。第二轮拍板:装修数据模型重构为「整页文档 + 版本」,新增 P0⁻ 地基修正期;表结构与信封含预留扩展位(§10),避免后期返工