Files
conti-retail-app/docs/01-project-structure.md
2026-08-17 15:29:55 +08:00

267 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 01. 工程结构 / 分包策略
## 决策
使用 **[Melos](https://melos.invertase.dev/) monorepo**,按 **feature** 拆分成独立 Dart package,而不是单一 Flutter package 内部用文件夹分层。
## 包结构总览
```
conti-app/
pubspec.yaml # 根 workspace 配置(melos 8.x 不再有独立 melos.yaml,见下文)
.fvmrc # 锁定 Flutter SDK 版本
analysis_options.yaml # 全仓库共享 lint 规则
app/ # 壳工程:唯一的 Flutter application,负责路由汇总、DI 装配、编译出 ipa/apk
packages/
core_ui/ # 通用组件、主题、设计 token
core_network/ # dio 封装、拦截器、统一异常、ApiResult 解包
core_storage/ # 本地存储抽象(Drift + shared_preferences 封装)
core_auth/ # 登录态、token 管理、secure storage、门店上下文
core_router/ # 路由聚合、公共 route guard、动态菜单映射
core_webview/ # F6 H5 容器 + JSBridge(见 10-webview-h5.md
core_analytics/ # 埋点统一 API(见 13-observability-analytics.md
core_logging/ # 日志规范、脱敏、崩溃上报接入
feature_auth/ # 登录、验证码、用户协议与隐私政策
feature_home/ # 首页工作台:动态菜单、待办、预警、公告、促销位
feature_store_mgmt/ # 店铺管理:基础信息、服务信息、执照、人员管理
feature_sales/ # 销售流程:客户查询、历史工单、商机(H5 承载的部分走 core_webview
feature_purchase/ # 采购:产品查询、购物车、结算、订单、收货
feature_inventory/ # 库存:明细、安全库存、盘点、DOT
feature_analytics/ # 经营分析:对账单、核销收入、返利、报表
feature_profile/ # 个人中心:地址、热线、客服
feature_scan/ # 扫码业务入口(VIN/车牌/二维码/条码 → 分发到对应业务)
native_scan/ # 原生插件包:扫码能力(android/ios 两端实现)
native_media/ # 相机、相册、文件选择/上传
native_device/ # 拨号、设备信息、权限申请
```
包清单按 [PRD](../../conti-docs/Architecture-Diagram/202606-Continental-Retail-APP-PRD.md) 的模块划分列出,实际开工时按迭代顺序逐个建,不需要一次性全建出来。
## 依赖规则(编译期强制边界,是这套结构的核心价值)
- `app` 可以依赖所有 `core_*``feature_*`
- `feature_*` **只能**依赖 `core_*``native_*`**不能**相互依赖(`feature_purchase``pubspec.yaml` 里不允许出现 `feature_inventory` 的 path dependency)。
- `core_*` 可以依赖 `native_*``core_webview` 的 JSBridge 需要调起扫码/相机/上传)。
- `native_*` 只依赖 Flutter SDK 和 [Pigeon](https://pub.dev/packages/pigeon) 生成的代码,不依赖任何 `core_*` / `feature_*`——保证原生插件包可以脱离业务单独编译、单独测试(详见 [07-native-integration.md](./07-native-integration.md))。
`core_*` 之间原则上不互相依赖,允许的例外只有下面三条,多一条都要走评审:
| 允许的依赖 | 原因 |
|---|---|
| `core_network``core_auth` | 取 token 附加到请求头、401 时触发刷新 |
| `core_router``core_auth` | 路由 `redirect` 里判断登录态(见 [04-routing.md](./04-routing.md) |
| `core_webview``core_auth` | H5 换票需要当前登录态与门店上下文(见 [10-webview-h5.md](./10-webview-h5.md) |
两条容易踩的反向约束,必须记住:
- **`core_auth` 不依赖 `core_network`**。`core_auth` 要发 refresh 请求,如果依赖 `core_network` 就和上表第一行构成循环依赖。做法是:`core_auth` 直接依赖 `dio` 包,内部自建一个**不挂任何拦截器的裸 `Dio` 实例**专门用于刷新——这同时也避免了"刷新请求本身被 `AuthInterceptor` 拦截 → 401 → 再刷新"的递归(见 [05-networking.md](./05-networking.md))。
- **`core_auth` 不依赖 `core_storage`**。token / refresh token 走 `flutter_secure_storage`,这个依赖**归 `core_auth` 独占**`core_storage` 只负责 Drift 和 `shared_preferences`(见 [06-local-storage.md](./06-local-storage.md))。这样划分是为了不让 `core_*` 之间再多一条依赖边。
`feature_*` 不直接依赖 `go_router`,路由相关类型由 `core_router` 统一 re-export`export 'package:go_router/go_router.dart';`),这样将来换路由库时只有 `core_router` 一个包要改。
这些规则由 Dart 的包依赖机制**物理强制**:`feature_a` 根本 import 不到 `feature_b` 的任何符号,不是靠代码规范或 review 口头约束。
## Feature 间通信怎么处理
这是最容易被绕开、也是这套边界能否守住的关键点,必须写清楚合法方式:
1. **路由跳转 + 可序列化参数**(多数场景)——比如从 `feature_home` 跳到 `feature_purchase`,通过 `core_router` 声明的路径 + query/extra 参数传递,不直接引用对方的 Dart 类型。
2. **通过 `core_*` 定义的抽象接口 + DI 注册实现**——真正需要跨 feature 拿数据或发通知的场景(比如切换门店后要清空购物车),在某个 `core_*` 包里定义接口,各 feature 各自实现并在 `app` 层注册,调用方只依赖 `core_*` 里的抽象类型(门店切换的级联失效见 [11-store-context-and-session.md](./11-store-context-and-session.md))。
**不允许**的做法:任何 `feature_*``pubspec.yaml` 里直接 path dependency 另一个 `feature_*`,哪怕只是想复用一个 widget——这种情况应该把这个 widget 提到 `core_ui`
## 命名规范
- `core_xxx`:基础设施层,不含具体业务逻辑。
- `feature_xxx`:对应一个业务域(多数是原来的某个小程序,也有全新的,如首页工作台)。
- `native_xxx`:原生能力插件包,首版含 `android/``ios/` 两套原生实现目录(OHOS 不在首版范围,见 [07-native-integration.md](./07-native-integration.md))。
## SDK 版本基线
| 项 | 版本 | 说明 |
|---|---|---|
| Flutter | **3.44.9** | 用 [FVM](https://fvm.app/) 锁定,仓库根目录提交 `.fvmrc` |
| Dart | 随 Flutter 3.44.9 附带(3.12.x | 具体号以 `flutter --version` 实测为准;`environment: sdk: ^3.12.0` 对整个 3.12.x 都成立 |
**为什么不跟最新 stable3.47.0 / Dart 3.13.02026-08-12 发布)**:鸿蒙(OpenHarmony)的 Flutter 分支适配落后于官方 stable 一段时间,虽然 OHOS 不在首版范围(见 [07-native-integration.md](./07-native-integration.md) 的「OHOS 后续演进」),但 SDK 基线要为后续接 OHOS 留出兼容窗口,所以刻意停在 3.44.9 而不是追最新。这条约束在决定升级 Flutter 版本时必须重新评估,不要因为"新版本有新特性"就单方面升。
**为什么必须用 FVM 锁**monorepo 里各人本地 Flutter 版本不一致,会导致同一份代码有人 `flutter analyze` 过、有人不过,生成代码(`build_runner` 产物)也可能不一致——这类问题排查成本远高于装一次 FVM。CI 也用 `.fvmrc` 里的版本,保证本地和流水线一致。
```json
// .fvmrc
{ "flutter": "3.44.9" }
```
## Melos 配置示例(8.x,基于 Dart Pub Workspaces
Melos 7.0 起改用 Dart 官方原生的 **[Pub Workspaces](https://dart.dev/tools/pub/workspaces)** 机制,不再有独立的 `melos.yaml` 文件,配置写进根目录 `pubspec.yaml`;每个子包的 `pubspec.yaml` 需要加 `resolution: workspace`
两个不同的 SDK 下限,别搞混:
- **Pub Workspaces 机制本身**要求 Dart SDK ≥ **3.6.0**
- **melos 8.2.2 这个工具**自己要求 Dart SDK **^3.9.0**。
我们的基线(Dart 3.12.x)两条都满足。
根目录 `pubspec.yaml`
```yaml
name: conti_app
publish_to: none
environment:
sdk: ^3.12.0
workspace:
- app
- packages/core_ui
- packages/core_network
- packages/core_storage
- packages/core_auth
- packages/core_router
- packages/core_webview
- packages/core_analytics
- packages/core_logging
- packages/feature_auth
- packages/feature_home
- packages/feature_purchase
- packages/native_scan
# ... 其余包按实际建包进度追加
dev_dependencies:
melos: ^8.2.2
melos:
scripts:
analyze:
run: melos exec --fail-fast -- flutter analyze
test:
# --dir-exists=test 跳过还没有测试目录的包(如新建的 native_*),
# 否则批量命令会因为「找不到 test 目录」整体失败
run: melos exec --dir-exists=test --fail-fast -- flutter test
format:
run: melos exec -- dart format --set-exit-if-changed .
gen:
# 代码生成:riverpod_generator / drift_dev / json_serializable
run: melos exec --depends-on=build_runner -- dart run build_runner build --delete-conflicting-outputs
pigeon:
# 原生接口生成,见 07-native-integration.md
run: melos exec --scope="native_*" -- dart run pigeon --input pigeons/
```
每个子包(比如 `packages/feature_purchase/pubspec.yaml`):
```yaml
name: feature_purchase
resolution: workspace
dependencies:
core_ui:
path: ../core_ui
core_network:
path: ../core_network
core_router:
path: ../core_router
```
## 共享 lint 配置
根目录一份 `analysis_options.yaml`,各子包 include 它,不允许各包自己维护一套规则(选型与具体规则见 [14-conventions-and-ci-gates.md](./14-conventions-and-ci-gates.md)):
```yaml
# packages/feature_purchase/analysis_options.yaml
include: ../../analysis_options.yaml
```
用到 `custom_lint``riverpod_lint` 依赖它)的包,需要各自在 `dev_dependencies` 里加 `custom_lint`,并在自己的 `analysis_options.yaml` 里启用 `custom_lint` 插件——`custom_lint` 是按包运行的,不能只在根目录配一次(见 [03-state-management.md](./03-state-management.md))。
## 新增 feature 包的标准脚手架
```
feature_xxx/
pubspec.yaml # resolution: workspace + 依赖 core_ui / core_network / core_router 等,不依赖其他 feature
lib/
feature_xxx.dart # 唯一对外导出文件(barrel file):只暴露路由注册函数和必要的 public widget
src/
presentation/
domain/ # 可选,见下方分层规范文档
data/
test/
```
`src/` 目录下的内容视为包内私有实现,只有 `feature_xxx.dart` 这一个文件是对外契约——这条靠 code review 检查,Dart 语言本身没有强制的 package-private 关键字。`domain/` 目录的取舍规则详见 [02-layering.md](./02-layering.md)。
## 版本管理
不发布到 pub.dev,全部用 melos 的 path dependency,包版本号跟随 `app` 的整体版本号统一管理(fixed versioning),不做 melos 的 independent versioning——没有对外发布需求,独立版本号只会增加维护负担。
## 附录:Melos 是什么,日常怎么用
给没接触过 Dart 多包仓库工具的同学看的入门说明。
### 要解决的问题
Dart 官方的包管理工具 `pub` 天生只认"一个 `pubspec.yaml` = 一个包"。如果要在同一个 git 仓库里维护多个互相依赖的私有包(比如 `app` 依赖 `feature_scan``feature_scan` 依赖 `core_network`),原生 pub 只支持手动在每个包的 `pubspec.yaml` 里写 `path: ../../packages/core_network` 这种相对路径依赖——能跑,但没有任何批量操作能力:想给所有包统一跑一次 `flutter analyze``flutter test`,或者统一升级某个第三方库版本,都得一个包一个包手动进去执行。
**Melos 就是给这种多包仓库提供批量管理能力的工具**,类似 JS 生态里的 [Lerna](https://lerna.js.org/)/Nx,只不过是 Dart/Flutter 版本。它不改变 Dart 语言或 pub 本身的机制,只是在多个包外面包一层"批处理脚本 + 配置"。
### 核心概念
1. **根目录 `pubspec.yaml` 里的 `workspace:` 字段 + `melos:` 配置块**8.x 版本不再有独立的 `melos.yaml` 文件(7.0 之前是独立文件,现已合并进 Dart 官方原生的 Pub Workspaces 机制)。`workspace:` 列出所有子包路径,`melos:` 块下的 `scripts:` 定义可复用脚本(见上文示例)。
2. **`melos bootstrap`**(简写 `melos bs`):一键解析 workspace 内所有包之间的依赖关系。在 8.x 的 Pub Workspaces 模式下,它的效果约等于"在仓库根目录跑一次 `flutter pub get` + 校验各包 `resolution: workspace` 配置是否正确"——包间链接由 pub 原生的 workspace 机制完成,**不再生成 `pubspec_overrides.yaml`**(那是 7.0 之前的实现方式)。**新人拉下代码后第一步永远是跑这个命令**。
3. **`melos exec`**:在每一个包目录下依次/并行执行同一条命令,比如 `melos exec -- flutter test` 就是把所有包都跑一遍测试,替代手动 `cd packages/feature_purchase && flutter test && cd ../feature_inventory && ...`。常用过滤参数:`--scope`(只跑匹配名字的包)、`--dir-exists=test`(只跑有测试目录的包)、`--fail-fast`(有一个包失败就停)。
4. **`melos run <script-name>`**:调用根目录 `pubspec.yaml``melos: scripts:` 下预定义的脚本别名(比如上文的 `melos run test`),团队里统一敲固定命令,不用记 `exec` 的完整写法。
### 日常开发流程(拿本仓库举例)
```bash
# 0. 一次性:安装 fvm 并装上基线版本的 Flutter
dart pub global activate fvm
fvm install # 读 .fvmrc,装 3.44.9
fvm flutter --version
# 1. 第一次拉代码,或者别人加了新包/新依赖之后
melos bootstrap
# 2. 正常改代码,比如在 feature_purchase 里改一个页面
cd packages/feature_purchase
flutter run # 这一步跟平时写单个 Flutter 项目完全一样,感觉不到 melos 的存在
# 3. 改了带注解的代码(Riverpod / Drift / json_serializable)之后
melos run gen
# 4. 提交前,跑一遍全仓库检查
melos run analyze
melos run test
# 5. 新增了 feature 包,或者某个包新增了对另一个包的依赖之后
melos bootstrap # 重新解析依赖关系
```
**关键体感**:平时在某一个包里写代码、`flutter run`、热重载,跟没有 melos 时完全一样——melos 只在"跨包操作"(装依赖、批量测试、批量分析)时才会用到,不侵入日常单包开发的手感。
### 常见的坑
- 加了新包,或改了某个包的依赖之后忘记跑 `melos bootstrap`,会出现"明明加了依赖但 import 不到"的报错——看到这个报错先跑一遍 bootstrap 再排查。
- 8.x 基于 Pub Workspaces 后,正常的包间链接**不再**依赖 `pubspec_overrides.yaml`(这是 7.0 之前版本的机制);只有配置了额外的 `dependencyOverridePaths`(用于覆盖外部第三方依赖,不是本仓库包之间的常规场景)时才会生成这个文件。如果看到这个文件出现却不记得配置过覆盖路径,说明配置可能有误,需要检查。
### 安装
```bash
dart pub global activate melos
```
全局命令,装一次即可,不需要每个项目单独安装。
## 参考链接
- [Melos 官方文档](https://melos.invertase.dev/)
- [melos | Dart package (pub.dev)](https://pub.dev/packages/melos)
- [Melos changelog](https://pub.dev/packages/melos/changelog)
- [Melos Configuration overview](https://melos.invertase.dev/configuration/overview)
- [Dart Pub Workspaces 官方文档](https://dart.dev/tools/pub/workspaces)
- [FVMFlutter Version Management](https://fvm.app/)
- [Pigeon | Dart package](https://pub.dev/packages/pigeon)
- [Drift | Dart package](https://pub.dev/packages/drift)
- [LernaJS 生态对标工具)](https://lerna.js.org/)