背景
1. 业务动机:为什么需要跨项目复用
本 monorepo 中有两个面向用户的产品:
- 主站项目:社区型 Web 应用,提供模型浏览、AI 生图/生视频、训练等核心功能,是最早的产品形态
- TV 项目:面向大屏/新场景的独立应用,承载新的用户入口和交互体验
两个产品共享同一套会员体系——同一套后端 API、同一套 SKU 定义、同一套支付/订单流程。用户在任一端购买的会员权益全平台通用。
如果各自维护一套商业化实现,会带来严重的一致性风险:SKU 变更、定价策略、促销活动需在两个项目各改一遍,极易产生数据不一致和体验割裂。因此,统一的商业化层是"一个会员身份、多端体验"的技术保障。
2. 体系演进历史
商业化体系经历了五个阶段的渐进式演化:
阶段一 阶段二 阶段三 阶段四 阶段五 内聚于主站 → API 层抽取 → Store/Hooks → Facade 门面 → TV 项目接入 抽取 包创建- 阶段一:完全内聚。所有商业化功能(组件、Store、Hooks、API)集中在主站
src/下,通过@/相互引用,无跨项目复用能力 - 阶段二:API 层抽取。将 VIP、支付、订单等 API 请求封装抽取到独立的
_services_ts包,实现服务调用的共享 - 阶段三:Store/Hooks 抽取。将
vipInfoStore、commercialStore等状态管理抽取到app-store包;将useUserIdentificationHooks、useVipBenefitHooks等业务钩子抽取到app-hooks包 - 阶段四:Facade 门面包。创建
packages/commercial作为统一导出层,采用 Facade 模式对外暴露组件、Hooks、Store 和工具函数。组件实现仍驻留在主站src/components/中 - 阶段五:TV 项目接入。TV 项目通过依赖
@org/commercial包复用商业化能力,当前正在进行 TV 端的技术栈适配工作
3. 两个项目的技术栈差异
两个项目在框架版本和构建工具上存在显著差异,这直接影响了商业化组件的复用方式:
| 维度 | 主站项目 | TV 项目 |
|---|---|---|
| Next.js | 13.x(Pages Router) | 16.x(App Router) |
| 构建工具 | Webpack | Turbopack |
| UI 框架 | Mantine 6 | Mantine 8 |
| Tailwind | v3 | v4 |
| SCSS 引擎 | sass 1.69 | sass-embedded 1.86 |
| 路由 API | next/router | next/navigation |
| CSS Modules | 支持:global {}block 语法 | 仅支持:global(selector)函数语法 |
| 国际化 | 无 | @org/i18n |
| 动画库 | — | GSAP / framer-motion |
关键影响点:
- 路由 API 不兼容:共享组件需统一使用
next/navigation(App Router 兼容)而非next/router - Mantine 版本跨代:v6 与 v8 API 差异大(如
sx/createStyles在 v8 中已移除),TV 端通过turbopack.resolveAlias将 Mantine 统一到 v8 - CSS Modules 语法限制:Turbopack 不支持
:global { }block 语法,所有共享样式必须使用:global(selector)函数语法 - SSR 行为差异:商业化组件在 TV 端需通过
dynamic(() => import(...), { ssr: false })禁用服务端渲染
一、体系概览
本项目的商业化体系是一套跨项目复用的会员/付费/权益系统,同时为主站项目(社区主站)和TV 项目(TV 应用)提供服务。核心架构采用Facade 模式,通过packages/commercial包作为统一门面,对外暴露组件、Hooks、Store、工具函数等。
┌─────────────────────┐ ┌─────────────────────┐ │ TV 应用 │ │ 社区主站 │ │ (apps/tv) │ │ (packages/main) │ └──────────┬──────────┘ └──────────┬──────────┘ │ │ ▼ ▼ ┌──────────────────────────────────────────────────┐ │ packages/commercial │ │ (Facade 门面层:统一导出组件/Hooks/Store/Utils) │ └──────────┬───────────┬───────────┬───────────────┘ │ │ │ ▼ ▼ ▼ ┌────────────┐ ┌──────────┐ ┌──────────────────┐ │ app-store │ │ app- │ │ main/src/ │ │ (状态管理) │ │ hooks │ │ components/ │ │ │ │ (业务钩子)│ │ (UI 组件实现) │ └─────┬──────┘ └────┬─────┘ └────────┬─────────┘ │ │ │ └─────────────┴────────────────┘ │ ▼ ┌─────────────────────────────┐ │ _services / _services_ts │ │ (VIP/支付/权益 API 服务) │ └─────────────────────────────┘二、基础层面支撑
1. UI 组件层
商业化组件的实现体位于packages/main/src/components/,由commercial包 re-export:
| 组件 | 功能 |
|---|---|
VipModel | 个人 VIP 购买弹窗(含升级、续费、Tab 切换、QR 码) |
VipSku/VipSkuPop | VIP SKU 展示与弹窗 |
VipSuccess | 支付成功弹窗 |
VipEquity/VipPromoteLabel | VIP 权益展示与推广标签 |
TeamVipModel | 团队 VIP 购买弹窗(含 Header/Content/Footer/PayModal) |
TeamSku/TeamVipTable | 团队 SKU 与明细表 |
TimelimitDiscount | 限时折扣组件 |
PopGuide | 购买引导(含PurchaseVipModal、PopMiniWindow) |
BuyPower/BuySpeed | 积分购买 / 加速包购买 |
OnlyPurchase | 纯购买模式 |
vipQrCode | 扫码支付 QR 码组件 |
AccountCenter | 用户账户中心 |
VipHomeContent | VIP 主页内容 |
2. 状态管理层(Store)
通过packages/app-store提供全局状态:
| Store | 职责 |
|---|---|
vipInfoStore | VIP 信息、团队信息、用户权益、限速状态、折扣信息、倒计时 |
commercialStore | 套餐列表、支付弹窗状态、远程配置、限时折扣、加速卡 |
useTeamVipPurchaseModalStore | 团队 VIP 购买弹窗可见性与意图 |
3. 业务 Hooks 层
通过packages/app-hooks和packages/commercial/src/提供:
| Hook | 职责 |
|---|---|
useUserIdentificationHooks | 用户身份识别(VIP 等级、团队状态、isVip、isTeamUser) |
useVipBenefitHooks | 权益查询(剩余积分、加速优先级、训练 VIP 等级) |
usePurchaseGoodsHooks | 下单、支付轮询 |
useCombineOrderHooks | 个人+团队合并下单入口 |
useTimelimitHooks | 限时优惠状态与操作 |
useTeamInfoHooks | 团队信息、邀请链接、成员列表 |
useCommercialInitData | 商业化数据初始化(加载配置与套餐) |
usePointChargeSkuHooks | 积分充值 SKU |
useCanPurchaseCoupons | 可购买优惠券 |
syncCommercialConfig | 加载远程配置并更新 commercialStore |
4. API 服务层
| 包 | 职责 |
|---|---|
_services/src/vip.js | 微信/支付宝支付参数获取、续费、限时优惠、用户权益、积分、订阅管理 |
_services_ts/src/services/vip.ts | 会员套餐、积分、体验卡、团队购买、支付结果查询 |
_services_ts/src/types/vip.ts | VIP 相关 TypeScript 类型 |
_services_ts/src/types/order.ts | 订单类型定义 |
5. 页面层
| 页面 | 功能 |
|---|---|
viphome/ | VIP 中心(仪表盘、套餐列表、留存、折扣弹窗) |
vip-model-page/ | VIP 模型页 |
wxpay/ | 微信/支付宝支付页 |
transaction/subscribe/ | 订阅管理 |
incomeR2//incomeReport/ | 收入报告 |
_authorIncome/ | 作者收入、账号绑定 |
membershipInvitationBonus/ | 会员邀请奖励 |
6. 通信机制层
通过postMsg.ts实现跨组件/跨 iframe 通信:
openVipModel()— 发送show_purchase_dialogpostMessage,触发 VipModel 弹窗communityPostMessage()— 通用 postMessage 通信openTeamVipPurchase()— 打开团队 VIP 购买
7. 共享 UI 基础层(common-ui)
packages/common-ui提供两个项目共用的基础 UI 组件:
ui-notification.js— 通知(notifyError, notifySuccess, notifyWarn)ui-button.jsx/ui-color.jsx/ui-iconfont.jsx— 基础 UIui-responsive.jsx— 响应式工具ui-theme.jsx— 主题配置ui-upload.jsx— 上传组件
8. 路径别名系统(#/ alias)
通过#/路径别名实现跨包引用主站src/的模块:
| 项目 | #/*映射目标 |
|---|---|
| 主站项目 | ./src/* |
commercial包 | ../main/src/* |
| TV 项目 | ../../packages/main/src/* |
9. 静态资源层
packages/commercial/src/assets.ts提供STATIC_CDN_MAP,统一管理 VIP 相关图标、图片等 CDN 资源。
三、两个项目同时使用带来的挑战
1. 循环依赖风险
commercial包 re-export 主站src/components/的组件,而这些组件又通过#/别名引用主站src/下的模块,甚至部分模块又 import@org/commercial。目前通过主站utils/postMsg.ts和manage/commerical.ts中的中间层 re-export来打断循环,但这种模式非常脆弱,新增 import 时容易不慎引入循环。
2. 组件实现与门面层的耦合
商业化组件的实际代码驻留在packages/main/src/components/中,commercial包只是一个 re-export 门面。这意味着:
- 组件代码与主站项目的内部模块(如
#/manage/callbackAction、#/pages/viphome/*)深度绑定 - TV 项目使用时,这些依赖必须通过
#/别名正确解析,增加了配置复杂度 - 组件难以真正独立于主站存在
3.#/路径别名的脆弱性
#/别名将三个项目绑定到主站src/的目录结构上:
- 任何主站
src/下的文件移动、重命名都可能同时影响主站、TV 项目和 commercial 包 - 不同项目的 tsconfig 需要手动保持
#/映射的一致性 - IDE 的自动重构(如文件移动)通常不会同时更新所有项目的
#/引用
4. 运行时环境差异
两个项目的运行时环境存在差异:
- 主站:Next.js 13 + Webpack
- TV 项目:Next.js + Turbopack
- CSS Modules 语法兼容性要求不同(Turbopack 不支持
:global { }block 语法) - SSR 行为差异导致商业组件需要
dynamic(() => import(...), { ssr: false })特殊处理 - 路由系统差异(主站用
.page.tsx约定,TV 用 App Router)
5. 状态同步与初始化时机
两个项目有不同的应用生命周期:
useCommercialInitData的调用时机在两个项目中不同vipInfoStore和commercialStore的初始化依赖于用户登录状态,但两个项目的登录流程不同syncVipInfo等数据同步函数需要在正确的时机被调用,但各项目的布局和路由守卫不同
6. 功能子集与 UI 适配
TV 项目只使用商业化体系的部分功能(如VipModel、AccountCenter、openVipModel),但整个 commercial 包及其依赖链会被打入。这带来:
- 不必要的包体积
- TV 端的 UI 适配需求(屏幕尺寸、交互方式不同)未在组件层面得到系统性支持
- 移动端支付组件(
MobileComponents/Pay)等与 TV 无关的代码也在依赖树中
7. 服务层的 JS/TS 双轨制
API 服务分散在_services(JS)和_services_ts(TS)两个包中,且命名不一致(目录用下划线_services_ts,包名用services-ts)。两个项目都需要依赖这两套服务,增加了维护成本和理解难度。
8. 代码规范与命名不一致
存在历史遗留问题:
commerical拼写错误(应为commercial)出现在多个文件中- 混合使用
@org/commercial和#/两种导入方式,缺乏统一规范 - 部分组件用
.jsx,部分用.tsx,风格不统一
9. 测试覆盖困难
由于商业组件依赖于主站src/的大量模块和 Store,在 TV 项目中进行独立的单元测试非常困难——需要 mock 整个#/别名链路和 Store 初始化过程。
10. 版本与发布耦合
两个项目共享workspace:*依赖,意味着任何商业化组件的变更都会同时影响两个项目。缺乏独立的版本管理机制,无法为不同项目发布不同版本的商业化组件。
四、跨项目复用的收益
尽管存在上述挑战,跨项目复用商业化体系带来的收益是显著的。
业务角度
- 会员体系一致性:同一套 SKU、定价、促销规则在两端保持同步,用户在任一端购买的权益全平台生效,避免出现"A 端有折扣 B 端没有"的体验割裂
- 上线效率:新增套餐、调整定价、上线限时活动只需改一处,两端同时生效,显著缩短商业化需求的交付周期
- 收入归一:统一的支付流程和订单系统,确保收入数据口径一致,财务对账与运营分析无需跨系统聚合
- 用户体验连贯:会员身份、权益展示、购买流程在两端视觉和交互上保持一致,增强品牌认知与用户信任
技术角度
- 单一事实来源:VIP 状态、权益计算、SKU 列表等核心逻辑只有一份实现,消除多份代码之间的行为漂移风险
- Bug 修复效率:支付流程、权益判断等关键路径的 bug 只需修复一次,两端同时受益
- Store/Hooks 复用:
vipInfoStore、commercialStore、useUserIdentificationHooks等状态管理和业务逻辑共享,减少大量重复代码 - API 服务层共享:两端对接同一套后端 API,不需要各自维护请求封装、类型定义和错误处理
- 渐进式解耦架构:Facade 模式允许 TV 端按需引入功能子集,未来可通过 tree-shaking 或拆分 entry point 进一步优化包体积
- 统一技术演进路径:商业化相关的技术升级(如 Mantine 版本迁移、Turbopack 兼容、CSS Modules 语法统一)只需在共享层推进一次,两端同步受益