Appearance
装修体系升级规划
平台级专题。覆盖「装修数据模型 → 跳转链接 → 组件库 → C 端运行时」整条链路的重构与补齐。
状态:P0⁻ / P0 / P1 / P2 主体已完成(见 §8.1 落地进展),剩 P2 尾巴与 P3 未开工。 创建于 2026-08-04,方案经两轮拍板确定(2026-08-04),2026-08-05 起连续实施。
⚠️ 两处硬阻塞:框架
hiapi-fast-frame1.0.5 未发版(否则只有本机能编 public / vem);@hiapi/hiapi-cloud-web-basic0.0.24 未发 npm(否则三个前端pnpm install会失败)。关联:H5发布流水线规划(产物构建层)、子应用交付-发布安装升级全流程(安装层)。 本文只谈运行时配置层——装修内容、链接、组件、页面,这些都不需要重新构建。
0. 已定案的决策
| # | 决策 | 影响 |
|---|---|---|
| 1 | 链接纯动态,不做 DB 快照兜底 | 静态上报链路(UploadPlugin.pages → pages.json → hiapi_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
└ reloadApi1.2 关键代码位置
| 环节 | 位置 |
|---|---|
| 组件模板声明 | <子应用>/src/components/index.ts 的 widgetExport |
| 组件/页面上报 | 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#updateCallback → PageLayout + PageSchema |
| C 端取数 | pub/PageLayoutController#getData + PageLayoutLogic |
| C 端渲染 | hiapi-cloud-public-web/src/layouts/DynamicRenderer.vue(生成物,勿手改) |
| 跳转能力 | hiapi-cloud-web-basic/src/index.ts 的 HiapiUtils |
2. 缺陷清单
S 级 — 结构性,必须先于功能开发修掉
| # | 问题 | 位置 | 说明 |
|---|---|---|---|
| S1 | 删除是空操作,数据只增不减 | PageSchemaService.deleteByPageId | 该方法只删 Redis 缓存,一行数据库都不删,却固定 return 1 表示成功。配合 updateCallback 每次保存 _index + 1 写一整份新行 ⇒ 每保存一次装修页,DB 永久多一份该页全部组件行。10 组件的页面保存 50 次 = 500 行永久垃圾。表还是 ShardingSphere 按 page_id % 3 分表的,事后清理更麻烦 |
| S2 | PageSchema.version 是死字段 | entity/PageSchema.java | 字段存在,全仓从未赋值。⇒ 无从知道某页面是用哪个组件版本装修的 ⇒ 组件 data 结构升级时没有任何迁移依据 |
| S3 | 行存换不来任何收益,却付出三重复杂度 | PageSchema + PageSchemaService | 实际访问模式只有"按整页读、读完内存里 sort",没有任何按组件的查询。行存却带来:分表复杂度、缓存复杂度(setMap 无 TTL,靠手动删)、删除复杂度、版本管理昂贵(一个版本 = 一整批行) |
| S4 | HiapiUtils 是可变全局单例 | 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 | 子应用只能在发版时写死路径清单。带参数的页面(商品详情、分类页、设备详情)、运行时才知道的动态链接,全都表达不了 |
| 2 | AppLink 没有 type 字段 | entity/AppLink.java | 但前端读的是 score.row.type,选「应用链接」得到 type === undefined,存进装修 schema 就是脏数据 |
| 3 | type 语义两套混用 | LayoutPageLink.type 是 PageType(PAGE/USER),Link.type 是 LinkType(page/app/links) | 大小写和取值都对不上 |
| 4 | href 不按类型分发,且会崩 | public-web/src/main.ts:47 | params.forEach(...) —— 未传时抛异常,传对象也没有 forEach;且不分类型一律 uni.navigateTo |
| 5 | 跳转没有跨端能力 | 全仓 | 无小程序互跳 / 跳 APP / 降级引导;组件里还有直接 uni.navigateTo |
| 6 | 导航栏(tabbar)装修不生效 | public-web/src/composables/useTabbar.ts | 后台配的导航菜单 C 端根本没读,tabbar 写死两项 |
| 7 | 组件目录不过滤平台/租户订阅 | componentsTreeList | 用 findAll();前端传的 platform/type 被忽略,Components 连 platform 字段都没建。租户没订阅的应用的组件照样出现 |
| 8 | 页面级样式没落地 | pages/index、pages/custom | PageLayout 的 navbar/title/backgroundColor/backgroundImage/styles C 端一个没用 |
P1 — 链路残缺
| # | 问题 |
|---|---|
| 9 | 没有草稿/发布态(保存即上线,不可回滚) |
| 10 | 预览是死按钮(/micro/design/preview 路由不存在) |
| 11 | 删除 / 导出 / 导入 都是死按钮;删除若接上还得级联清理 |
| 12 | PageLayout.thumbnail 没人生成 |
| 13 | 没有页面复制 / 模板 |
| 14 | 主题风格页是 12 行空壳 |
| 15 | 组件缩略图全空(image: '') |
| 16 | 装修态无 mock 数据,数据类组件在画布里空白 |
P2 — 体验 / 观感
| # | 问题 |
|---|---|
| 17 | 组件只有 12 个,缺口很大 |
| 18 | 组件样式写死颜色,无主题令牌 |
| 19 | buildStyle 不支持边框、字号、字重、字色、对齐、行高、宽度 —— 属性面板给不出能力,组件不可能好看 |
| 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_version、published_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/page | C 端:读 published_version 的文档;带 preview=1&version= 时读指定版本(预览用) |
POST /page-layout/edit | 保留一个版本做兼容(内部 = 存草稿 + 立即发布),给装修器留迁移窗口(装修器是独立发布的 qiankun 微应用,版本不一定同步) |
3.7 迁移方案(⚠️ 有坑,必须按这个来)
旧表是 ShardingSphere 分表的,不能用纯 SQL 迁移 —— Flyway 走物理连接,绕不过分片逻辑表,历史上已经踩过 Flyway 撞 ShardingSphere 的雷。
正确做法:
- Flyway 只建新表 + 加
PageLayout新列,不迁数据。 - 数据迁移写成一次性平台端接口 / 启动任务(走应用层,经 ShardingSphere 逻辑表读),把
(mid, page_id, _index)的现存行聚合成 JSON 写入page_version(status=PUBLISHED, version=1),回填published_version=1。 - 迁移任务幂等 + 可重跑 + 输出对账报告(页面数、组件数、失败清单)。
- 双读校验一版:C 端优先读新表,读不到回落旧表并告警。
- 观察一版无告警 → 删
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.pages、AppStoreLogic.reloadPages、AppLink实体/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=¶mKey=&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 | 新增,内置功能页目录(登录/个人中心/消息/余额/充值/提现/地址/实名/客服…),集中一处维护 |
4.4 统一 Link 协议(hiapi-cloud-web-basic/src/schema.ts)
先解决命名冲突:现有 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 + 有 appId | kind: '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> // 旧签名保留- 入参先过
normalizeLink()。 needLogin且未登录 → 先登录,带redirect回跳。- 按
kind+ 当前端分发(§4.7);不支持 →fallback;仍没有 → toast 提示,绝不静默失败。 kind: 'sub'先校验子应用是否在本份产物里(build-record.json/ 已装应用清单),不在则提示「该功能未开通」而不是白屏。- 修掉
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.open | web-view 中转页(域名需白名单) | 内置 webview / 系统浏览器 |
mini | 微信内:JSSDK wx.miniProgram.navigateTo(需公众号关联)微信外: shortLink / Scheme 唤起,失败 → 降级页 | wx.navigateToMiniProgram(目标 appId 需在关联白名单) | 开放平台 openSDK;未集成 → fallback |
native | universalLink 优先 → scheme 兜底 → 1.5s 未离开页面则跳 fallback(下载引导) | ⚠️ 微信禁止小程序直接唤起第三方 APP(仅"从 APP 分享进入"场景可用 launchApp),其余一律 fallback | plus.runtime.openURL / Android Intent;未安装 → fallback |
action:phone | tel: | wx.makePhoneCall | uni.makePhoneCall |
action:copy | Clipboard API | wx.setClipboardData | uni.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)
Components增platforms(逗号串)、scenes(PAGE/USER),由widgetExport声明 →components.json上报 → 安装期入库。componentsTreeList按platform+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 先有主题令牌,再谈组件
- 主题令牌:租户级配置(主色/辅色/圆角/间距/阴影/字号阶梯/暗色)存
Config,C 端启动注入 CSS 变量(--hi-color-primary、--hi-radius-md、--hi-space-3…)。 - 组件禁止写死颜色,一律用变量 —— 换肤 = 改配置。
theme-style空壳补齐:主题配置页 + 实时预览 + 3~5 套预设风格。- 补
buildStyle:边框、字体(size/weight/color/align/lineHeight)、宽度/对齐、渐变方向、阴影 spread、暗色变体。属性面板没这些能力,组件就不可能好看。 - 统一组件外壳:一致的内外边距/圆角/阴影/背景 + 统一的空态、骨架屏、加载态、错误态。
- 每个组件配 3~5 套预设样式。让商户调 20 个数值结果一定难看,给他挑预设才守得住下限。
- 补组件缩略图:每组件一张 SVG 随
components.json上报,左侧面板从文字列表变卡片选择。 - 装修态 mock:
getEnv() === 'DESIGN'时数据类组件用假数据。
6.2 组件补齐清单
现有 12 个(轮播、宫格菜单、公告、余额、用户头部、用户信息、导航列表、魔方、单图、辅助空白、消息入口、公告条)—— 全部重写样式(数据结构不动)。
第一批(通用,10 个):分割线、标题栏、卡片容器(需装修器支持嵌套)、图文列表、富文本(小程序用 rich-text)、视频、按钮组、搜索框、倒计时、悬浮按钮。
第二批(业务,只做后端已就绪的):会员等级卡、积分/购物金卡片、快捷充值、订单入口(依赖 §4 动态链接)、数据看板卡(参考 vem overview/trend)。
优惠券领取 / 签到—— 后端不存在,本期不立项(决策 7)。
6.3 组件的跨端约束(决策 6)
| 约束 | 说明 | 卡口 |
|---|---|---|
禁 window / document / localStorage / alert | 小程序无 DOM;存储走 uni.getStorageSync | CI 检查组件目录 |
尺寸走 HiapiUtils.transformStyleUnit | 不得硬编码 px | CI + Review |
不用 v-html,富文本用 rich-text | 小程序不支持 | CI |
不用深层选择器 / >>> | 小程序样式隔离 | CI |
| 不动态加载远程脚本 | 小程序禁远程代码 | 已有 |
跳转只用 HiapiUtils.href | §4.6 | check-navigate.sh |
7. 改造四:基础功能页面缺口
7.1 C 端(hiapi-cloud-public-web)
已有:登录/注册/找回、个人中心(头像/昵称/性别/生日/手机/邮箱/实名/密码/支付密码/安全/登录记录/绑定/地址/反馈/关于/隐私/设置/消息)、财务(资产/收银台/充值/转账/提现)、首页、自定义页。
| 页面 | 优先级 | 备注 |
|---|---|---|
| 登录回跳 / 未登录访问 USER 页 | P0 | needLogin 的前提 |
| 错误页(404/无权限/维护中)+ 网络异常兜底 | P1 | 现在直接白屏 |
| 搜索页 / 搜索结果 | P1 | 搜索框组件落点 |
| 消息详情 / 公告详情 | P1 | 现在只有列表 |
| 会员中心(等级/权益/成长值) | P1 | 后端已就绪,前端完全没有 |
| 积分 / 购物金明细 | P1 | 同上 |
| 客服 / 在线咨询 | P1 | action: service 落点 |
| web-view 中转页 | P1 | kind: 'web' 在小程序端落点,H5 期先建路由 |
| 跳转降级引导页(下载 APP / 打开方式) | P1 | native / mini 的 fallback 落点 |
| 我的订单(跨子应用聚合) | 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 落地进展
| 项 | 状态 | 位置 |
|---|---|---|
| 文档数据模型(实体 / 枚举 / 文档信封) | ✅ 已完成 | PageVersion、PageVersionStatus、document/{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(此前不存在)、usePageLayout、common/PageShell.vue |
| C 端错误兜底 + HTTP 故障统一处理 | ✅ 已完成 | utils/http-fault.ts、pages/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.ts 的 withDesignMock,已用在 notice / grid / nav-list / balance |
| 主题配置页(预设 + 取色 + 实时预览) | ✅ 已完成 | admin-ts/views/design/theme-style,此前是空壳且无菜单入口 |
| 装修器画布套用商户主题 | ✅ 已完成 | admin-page-design 的 device-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 生成器 + WidgetNode 加 children 三处——重新评估后发现都不需要:子组件塞进组件自己的 data.children(本来就是不透明 JSON,后端/HiapiCloudSchema 零改动),渲染直接复用生成产物 DynamicRenderer.vue 递归一层;子组件的增删排序收在卡片容器自己的属性面板里,复用被选中类型(标题栏/单图/富文本/按钮组/分割线,白名单 5 个)各自现成的 property.vue,不需要画布支持嵌套拖拽。三路调研(装修器拖拽模型、生成器机制、后端 WidgetNode 模型)详见 git 提交记录 |
| 跳转降级引导页 | ✅ 已完成 | pages/guide/index,native/mini 的 fallback 默认落点 |
| 错误页 / 网络异常兜底 | ✅ 已完成(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 期间的两次自我修正
写门禁和写预设各返工了一次,都是实现之后跑一次才暴露的设计错误,记在这里:
check-widget.sh最初一视同仁地扫了property.vue,报了 40 多条全是误报。 那是装修器的属性面板,只在浏览器跑、永远不会打进小程序包, 用document.elementFromPoint做拖拽热区完全正当。已限定只扫渲染文件。 顺带:注释行也要放过,否则「不要写死 #333」这句注释会被自己拦下来。预设最初按「垫底 + 用户覆盖」实现,根本不生效。 组件注册表的默认
styles自带一堆显式值(marginLeft: 0、borderTopLeftRadius: 0、backgroundColor: '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 撞 ShardingSphere | Flyway 只建表,数据迁移走应用层任务(§3.7)。历史上已踩过这个雷 |
| 装修器与 admin 独立发布,版本可能不同步 | 旧 /edit 接口保留一版做兼容;装修器顶部显示协议版本 |
改 Link 协议影响线上装修 JSON | 只增不删 + normalizeLink() 兜底 + type 永久保留 + 迁移时顺便归一化;上线前用线上数据跑归一化回归 |
| 纯动态 = 子应用离线就选不了链接 | 决策 1 明确接受的取舍。错误提示具体到应用名 + 一键重试 |
| 静态链路下线要跨仓改 | 先上新后删旧;UploadPlugin.pages 保留一版打 deprecated |
| 框架加 SPI 要发版并抬 10 仓 parent | SPI 全部 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-05 | P0 链接协议全链路打通;P2 主题令牌与 12 组件重做落地,新增 check-widget 门禁 |
| 2026-08-04 | 架构复核发现 S1~S5 结构性问题(删除是空操作、version 死字段、行存无收益、HiapiUtils 全局单例、双渲染路径)。第二轮拍板:装修数据模型重构为「整页文档 + 版本」,新增 P0⁻ 地基修正期;表结构与信封含预留扩展位(§10),避免后期返工 |