feat: add engineering conventions and CI gates documentation
- Introduced a new document outlining SDK version locking, static analysis, formatting, generated artifacts management, branching and commit conventions, and CI gate checks. - Updated README to include the new conventions document. - Modified API design to use numeric error codes instead of strings, with a dedicated ErrorCode object for better maintainability. - Adjusted global exception handling to return numeric error codes. - Updated tests to reflect changes in error code handling.
This commit is contained in:
+119
-38
@@ -8,51 +8,100 @@
|
||||
|
||||
```
|
||||
conti-app/
|
||||
melos.yaml
|
||||
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 封装、拦截器、统一异常
|
||||
core_storage/ # 本地存储抽象(Drift/secure storage 封装)
|
||||
core_auth/ # 登录态、token 管理
|
||||
core_router/ # 路由聚合、公共 route guard
|
||||
feature_scan/ # 原「扫码」小程序
|
||||
feature_payment/ # 原「支付」小程序
|
||||
feature_store/ # 原「门店」小程序
|
||||
feature_.../
|
||||
native_scan/ # 原生插件包:扫码(android/ios/ohos 三端实现)
|
||||
native_payment/
|
||||
native_bluetooth/
|
||||
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](./Architecture-Diagram/202606-Continental-Retail-APP-PRD.md) 的模块划分列出,实际开工时按迭代顺序逐个建,不需要一次性全建出来。
|
||||
|
||||
## 依赖规则(编译期强制边界,是这套结构的核心价值)
|
||||
|
||||
- `app` 可以依赖所有 `core_*` 和 `feature_*`。
|
||||
- `feature_*` **只能**依赖 `core_*`,**不能**相互依赖(`feature_payment` 的 `pubspec.yaml` 里不允许出现 `feature_store` 的 path dependency)。
|
||||
- `core_*` 之间尽量不互相依赖;唯一允许的例外是 `core_network` 依赖 `core_auth`(取 token 做请求签名/刷新)。
|
||||
- `feature_*` 可以依赖对应的 `native_*` 包(如 `feature_scan` 依赖 `native_scan`)。
|
||||
- `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_store` 跳到 `feature_payment`,通过 `core_router` 声明的路径 + query/extra 参数传递,不直接引用对方的 Dart 类型。
|
||||
2. **通过 `core_*` 定义的抽象接口 + DI 注册实现**——真正需要跨 feature 拿数据或发通知的场景(比如支付完成后要清空购物车),在某个 `core_*` 包里定义接口,各 feature 各自实现并在 `app` 层注册,调用方只依赖 `core_*` 里的抽象类型。
|
||||
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/` 三套原生实现目录。
|
||||
- `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 都成立 |
|
||||
|
||||
**为什么不跟最新 stable(3.47.0 / Dart 3.13.0,2026-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`。要求 **Dart SDK ≥ 3.6.0**(我们的基线 Flutter 3.44.8 对应 Dart 3.12.2,满足要求)。
|
||||
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`:
|
||||
|
||||
@@ -69,12 +118,14 @@ workspace:
|
||||
- packages/core_storage
|
||||
- packages/core_auth
|
||||
- packages/core_router
|
||||
- packages/feature_scan
|
||||
- packages/feature_payment
|
||||
- packages/feature_store
|
||||
- packages/core_webview
|
||||
- packages/core_analytics
|
||||
- packages/core_logging
|
||||
- packages/feature_auth
|
||||
- packages/feature_home
|
||||
- packages/feature_purchase
|
||||
- packages/native_scan
|
||||
- packages/native_payment
|
||||
- packages/native_bluetooth
|
||||
# ... 其余包按实际建包进度追加
|
||||
|
||||
dev_dependencies:
|
||||
melos: ^8.2.2
|
||||
@@ -82,17 +133,25 @@ dev_dependencies:
|
||||
melos:
|
||||
scripts:
|
||||
analyze:
|
||||
run: melos exec -- flutter analyze
|
||||
run: melos exec --fail-fast -- flutter analyze
|
||||
test:
|
||||
run: melos exec -- flutter 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_scan/pubspec.yaml`):
|
||||
每个子包(比如 `packages/feature_purchase/pubspec.yaml`):
|
||||
|
||||
```yaml
|
||||
name: feature_scan
|
||||
name: feature_purchase
|
||||
resolution: workspace
|
||||
|
||||
dependencies:
|
||||
@@ -100,8 +159,21 @@ dependencies:
|
||||
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 包的标准脚手架
|
||||
|
||||
```
|
||||
@@ -116,7 +188,7 @@ feature_xxx/
|
||||
test/
|
||||
```
|
||||
|
||||
`src/` 目录下的内容视为包内私有实现,只有 `feature_xxx.dart` 这一个文件是对外契约——这条靠 code review 检查,Dart 语言本身没有强制的 package-private 关键字。`domain/` 目录的取舍规则详见 [02-layering.md](./02-layering.md)(待写)。
|
||||
`src/` 目录下的内容视为包内私有实现,只有 `feature_xxx.dart` 这一个文件是对外契约——这条靠 code review 检查,Dart 语言本身没有强制的 package-private 关键字。`domain/` 目录的取舍规则详见 [02-layering.md](./02-layering.md)。
|
||||
|
||||
## 版本管理
|
||||
|
||||
@@ -134,26 +206,34 @@ Dart 官方的包管理工具 `pub` 天生只认"一个 `pubspec.yaml` = 一个
|
||||
|
||||
### 核心概念
|
||||
|
||||
1. **根目录 `pubspec.yaml` 里的 `workspace:` 字段 + `melos:` 配置块**:8.x 版本不再有独立的 `melos.yaml` 文件(7.0 之前是独立文件,现已合并进 Dart 官方原生的 Pub Workspaces 机制)。`workspace:` 列出所有子包路径,`melos:` 块下的 `scripts:` 定义可复用脚本(见上文示例)。要求 Dart SDK ≥ 3.6.0。
|
||||
2. **`melos bootstrap`**(简写 `melos bs`):一键解析 workspace 内所有包之间的依赖关系——自动生成 `pubspec_overrides.yaml`,把 `feature_scan` 依赖 `core_network` 这种关系用本地路径链接起来,不用手写相对路径,也不需要真的发布到 pub.dev 才能互相依赖。**新人拉下代码后第一步永远是跑这个命令**。
|
||||
3. **`melos exec`**:在每一个包目录下依次/并行执行同一条命令,比如 `melos exec -- flutter test` 就是把所有包都跑一遍测试,替代手动 `cd packages/feature_scan && flutter test && cd ../feature_payment && ...`。
|
||||
4. **`melos run <script-name>`**:调用 `melos.yaml` 里预定义的脚本别名(比如上文的 `melos run test`),团队里统一敲固定命令,不用记 `exec` 的完整写法。
|
||||
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_scan 里改一个页面
|
||||
cd packages/feature_scan
|
||||
# 2. 正常改代码,比如在 feature_purchase 里改一个页面
|
||||
cd packages/feature_purchase
|
||||
flutter run # 这一步跟平时写单个 Flutter 项目完全一样,感觉不到 melos 的存在
|
||||
|
||||
# 3. 提交前,跑一遍全仓库检查
|
||||
# 3. 改了带注解的代码(Riverpod / Drift / json_serializable)之后
|
||||
melos run gen
|
||||
|
||||
# 4. 提交前,跑一遍全仓库检查
|
||||
melos run analyze
|
||||
melos run test
|
||||
|
||||
# 4. 新增了 feature 包,或者某个包新增了对另一个包的依赖之后
|
||||
# 5. 新增了 feature 包,或者某个包新增了对另一个包的依赖之后
|
||||
melos bootstrap # 重新解析依赖关系
|
||||
```
|
||||
|
||||
@@ -179,6 +259,7 @@ dart pub global activate 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)
|
||||
- [FVM(Flutter Version Management)](https://fvm.app/)
|
||||
- [Pigeon | Dart package](https://pub.dev/packages/pigeon)
|
||||
- [Drift | Dart package](https://pub.dev/packages/drift)
|
||||
- [Lerna(JS 生态对标工具)](https://lerna.js.org/)
|
||||
|
||||
Reference in New Issue
Block a user