commit 54c001a793e0a7032852adb1b7f96e171baa8533 Author: Guangfei.Zhao Date: Wed Aug 12 17:49:57 2026 +0800 Add architecture documentation and retail system workshop PPT diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8abc3dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Python (Architecture-Diagram/ 提取脚本用到的虚拟环境) +.venv/ +__pycache__/ +*.pyc + +# Claude Code 本地设置(个人权限配置,不进仓库) +.claude/settings.local.json + +# drawio 临时/备份文件 +.$*.drawio.bkp +*.drawio.bkp + +# OS junk +.DS_Store +Thumbs.db +desktop.ini + +# 编辑器 +.vscode/ +.idea/ + +# agents +.claude +.agents diff --git a/01-project-structure.md b/01-project-structure.md new file mode 100644 index 0000000..23591c0 --- /dev/null +++ b/01-project-structure.md @@ -0,0 +1,185 @@ +# 01. 工程结构 / 分包策略 + +## 决策 + +使用 **[Melos](https://melos.invertase.dev/) monorepo**,按 **feature** 拆分成独立 Dart package,而不是单一 Flutter package 内部用文件夹分层。 + +## 包结构总览 + +``` +conti-app/ + melos.yaml + 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/ +``` + +## 依赖规则(编译期强制边界,是这套结构的核心价值) + +- `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`)。 +- `native_*` 只依赖 Flutter SDK 和 [Pigeon](https://pub.dev/packages/pigeon) 生成的代码,不依赖任何 `core_*` / `feature_*`——保证原生插件包可以脱离业务单独编译、单独测试(详见 [07-native-integration.md](./07-native-integration.md))。 + +这些规则由 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_*` 里的抽象类型。 + +**不允许**的做法:任何 `feature_*` 在 `pubspec.yaml` 里直接 path dependency 另一个 `feature_*`,哪怕只是想复用一个 widget——这种情况应该把这个 widget 提到 `core_ui`。 + +## 命名规范 + +- `core_xxx`:基础设施层,不含具体业务逻辑。 +- `feature_xxx`:对应一个原小程序/业务域。 +- `native_xxx`:原生能力插件包,内部含 `android/`、`ios/`、`ohos/` 三套原生实现目录。 + +## 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,满足要求)。 + +根目录 `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/feature_scan + - packages/feature_payment + - packages/feature_store + - packages/native_scan + - packages/native_payment + - packages/native_bluetooth + +dev_dependencies: + melos: ^8.2.2 + +melos: + scripts: + analyze: + run: melos exec -- flutter analyze + test: + run: melos exec -- flutter test + format: + run: melos exec -- dart format --set-exit-if-changed . +``` + +每个子包(比如 `packages/feature_scan/pubspec.yaml`): + +```yaml +name: feature_scan +resolution: workspace + +dependencies: + core_ui: + path: ../core_ui + core_network: + path: ../core_network +``` + +## 新增 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:` 定义可复用脚本(见上文示例)。要求 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 `**:调用 `melos.yaml` 里预定义的脚本别名(比如上文的 `melos run test`),团队里统一敲固定命令,不用记 `exec` 的完整写法。 + +### 日常开发流程(拿本仓库举例) + +```bash +# 1. 第一次拉代码,或者别人加了新包/新依赖之后 +melos bootstrap + +# 2. 正常改代码,比如在 feature_scan 里改一个页面 +cd packages/feature_scan +flutter run # 这一步跟平时写单个 Flutter 项目完全一样,感觉不到 melos 的存在 + +# 3. 提交前,跑一遍全仓库检查 +melos run analyze +melos run test + +# 4. 新增了 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) +- [Pigeon | Dart package](https://pub.dev/packages/pigeon) +- [Drift | Dart package](https://pub.dev/packages/drift) +- [Lerna(JS 生态对标工具)](https://lerna.js.org/) + diff --git a/02-layering.md b/02-layering.md new file mode 100644 index 0000000..07a18a4 --- /dev/null +++ b/02-layering.md @@ -0,0 +1,157 @@ +# 02. 分层架构规范 + +## 决策 + +每个 `feature_*` 包内部采用简化版分层,`domain` 层**可选**,不强制每个 feature 都有: + +``` +feature_xxx/ + lib/ + feature_xxx.dart # 对外唯一导出文件 + src/ + presentation/ # widgets + Riverpod provider/notifier + domain/ # 可选:entity + repository 接口 + use case + data/ # repository 实现 + remote/local datasource + test/ +``` + +## 各层职责 + +- **presentation**:widgets、Riverpod `Notifier`/`Provider`。只处理 UI 状态和用户交互,不直接调用 `data` 层的具体实现类,通过依赖注入拿到抽象类型。 +- **domain**(可选):`entity` 定义业务模型,`repository` 接口声明数据契约,`use case` 封装跨 repository 协调或多步骤业务规则。 +- **data**:`repository` 接口的具体实现,内部再拆 `remote_datasource`(走 `core_network`)和 `local_datasource`(走 `core_storage`)。 + +## 何时可以跳过 domain 层 + +判断标准: + +- **可以跳过**:功能是简单 CRUD、没有跨 repository 协调、没有多步骤业务规则——`presentation` 直接依赖 `data` 层定义的 repository 接口即可,`repository` 接口挪到 `data` 层里声明。 +- **必须要有**:涉及多步骤业务规则(如支付的多步校验)、需要协调多个 repository、包含状态机或需要独立于 UI 单元测试的核心业务逻辑——`repository` 接口放在 `domain`,`data` 层依赖 `domain` 反向实现接口。 + +## Repository 接口的位置规则 + +- 有 `domain` 层:接口定义在 `domain/repository/`,`data/repository_impl/` 实现它,`presentation` 只依赖 `domain` 里的抽象类型。 +- 无 `domain` 层:接口直接定义在 `data/repository/`,同文件或同目录下给出实现类,`presentation` 依赖这个接口类型。 + +两种情况下,`presentation` 都不允许直接依赖 `data` 层的具体实现类(如 `XxxRepositoryImpl`),只依赖接口——这条不因为是否跳过 domain 层而改变。 + +## 跨层依赖规则 + +``` +presentation → domain(或直接 → data 的接口,若跳过 domain) +domain → 不依赖 presentation / data +data → 依赖 domain 的接口(若有),依赖 core_network / core_storage +``` + +`domain` 层禁止 import 任何 Flutter SDK(`package:flutter/...`)——保持纯 Dart,可脱离 UI 单独做 unit test。 + +## 附录:分层架构是什么,为什么要分层 + +给还没接触过这套分层习惯的同学看的入门说明。 + +### 要解决的问题 + +如果 UI 代码里直接写网络请求、直接 new 一个 `Dio` 实例、直接操作数据库——短期能跑,但会导致两个问题: + +1. **没法单独测试业务逻辑**:想验证"支付金额校验规则对不对",得连 widget 一起跑测试,跑得慢还容易因为 UI 变了导致业务逻辑测试跟着挂。 +2. **换底层实现要动 UI 代码**:比如把网络库从 `dio` 换掉,或者把本地存储从 `shared_preferences` 换成 `Drift`,如果 UI 直接依赖具体实现类,改动会散落得到处都是。 + +**分层的本质**:把"业务规则"和"业务规则的具体实现方式(用什么网络库、存什么数据库)"分开,中间用抽象接口隔开。这就是 [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 这套思想的核心,我们只取最简化的三层版本,不套用它完整的同心圆规则。 + +### 依赖方向是关键 + +三层最重要的不是"分了几层",而是**依赖只能单向流动**: + +``` +presentation ──依赖──> domain ──定义接口,不依赖任何人 + ▲ + │ 实现接口(依赖倒置) + data +``` + +`domain` 不 import `data`,也不 import `presentation`——它甚至不知道 `data` 层是用 `dio` 还是别的什么网络库实现的,只定义"我需要一个能拿到 `PaymentOrder` 的东西"(接口),至于这个东西具体怎么实现,由 `data` 层负责,这就是[依赖倒置原则](https://en.wikipedia.org/wiki/Dependency_inversion_principle)。好处是:`domain` 层的业务规则可以完全脱离网络、脱离 UI 单独写单元测试。 + +### 示例一:有 domain 层(`feature_payment`,支付确认——多步骤业务规则) + +```dart +// domain/entity/payment_order.dart +class PaymentOrder { + final String orderId; + final int amountCents; + final PaymentStatus status; + const PaymentOrder({required this.orderId, required this.amountCents, required this.status}); +} + +// domain/repository/payment_repository.dart +abstract class PaymentRepository { + Future fetchOrder(String orderId); + Future confirmPayment(String orderId, String pinToken); +} + +// domain/use_case/confirm_payment_use_case.dart +class ConfirmPaymentUseCase { + final PaymentRepository _repository; + ConfirmPaymentUseCase(this._repository); + + Future call(String orderId, String pinToken) async { + final order = await _repository.fetchOrder(orderId); + if (order.status != PaymentStatus.pending) { + throw StateError('订单状态不允许支付: ${order.status}'); + } + if (order.amountCents <= 0) { + throw ArgumentError('订单金额非法'); + } + await _repository.confirmPayment(orderId, pinToken); + } +} + +// data/repository/payment_repository_impl.dart +class PaymentRepositoryImpl implements PaymentRepository { + final Dio _dio; // 来自 core_network + PaymentRepositoryImpl(this._dio); + + @override + Future fetchOrder(String orderId) async { + final res = await _dio.get('/orders/$orderId'); + return PaymentOrder( + orderId: res.data['orderId'], + amountCents: res.data['amountCents'], + status: PaymentStatus.values.byName(res.data['status']), + ); + } + + @override + Future confirmPayment(String orderId, String pinToken) => + _dio.post('/orders/$orderId/confirm', data: {'pinToken': pinToken}); +} +``` + +`ConfirmPaymentUseCase` 的多步校验规则可以直接用假的 `PaymentRepository` 实现来做单元测试,完全不需要启动 Flutter engine 或起一个 mock server。 + +### 示例二:跳过 domain 层(`feature_store`,门店列表——简单 CRUD) + +```dart +// data/repository/store_repository.dart +abstract class StoreRepository { + Future> fetchNearbyStores(double lat, double lng); +} + +class StoreRepositoryImpl implements StoreRepository { + final Dio _dio; + StoreRepositoryImpl(this._dio); + + @override + Future> fetchNearbyStores(double lat, double lng) async { + final res = await _dio.get('/stores', queryParameters: {'lat': lat, 'lng': lng}); + return (res.data as List).map((e) => Store.fromJson(e)).toList(); + } +} +``` + +没有多步骤规则、没有跨 repository 协调,接口和实现直接放在 `data` 层,`presentation` 直接依赖 `StoreRepository` 这个接口,省掉一层 `domain` 目录和 use case 模板代码。 + +## 参考链接 + +- [Flutter 官方状态管理文档](https://docs.flutter.dev/data-and-backend/state-mgmt) +- [The Clean Architecture(Uncle Bob 原文)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +- [依赖倒置原则(Dependency Inversion Principle)](https://en.wikipedia.org/wiki/Dependency_inversion_principle) diff --git a/03-state-management.md b/03-state-management.md new file mode 100644 index 0000000..0c1cdf5 --- /dev/null +++ b/03-state-management.md @@ -0,0 +1,123 @@ +# 03. 状态管理方案 + +## 决策 + +使用 **[Riverpod](https://riverpod.dev/)**(`flutter_riverpod` + `riverpod_generator` 代码生成),不使用 Bloc/Provider/GetX。 + +版本基线:`flutter_riverpod: ^3.4.2`(当前 stable,2026-08 快照,需在实际开工时用 `flutter pub outdated` 复核)。 + +## 依赖 + +```yaml +dependencies: + flutter_riverpod: ^3.4.2 + riverpod_annotation: ^3.4.2 + +dev_dependencies: + riverpod_generator: ^3.4.2 + build_runner: ^2.4.0 + custom_lint: ^0.6.0 + riverpod_lint: ^3.0.0 +``` + +## 使用规则 + +- 所有跨 widget 共享的状态、依赖注入,统一通过 Riverpod provider 暴露,不额外引入 `get_it`/`provider` 等其他 DI 方案。 +- 优先使用 `riverpod_generator` 的注解写法(`@riverpod`),不手写裸 `Provider`/`StateNotifierProvider` 模板代码。 +- `Notifier`/`AsyncNotifier` 用于承载可变的 feature 状态;无状态的计算/依赖注入用普通 `Provider`。 +- `domain`/`data` 层的 repository 实现通过 provider 注入到 `presentation` 层,`presentation` 只依赖 provider 暴露的接口类型(见 [02-layering.md](./02-layering.md))。 +- 每个 `feature_*` 包各自维护自己的 provider,不跨包直接引用另一个 feature 的 provider(同 [01-project-structure.md](./01-project-structure.md) 的 feature 隔离规则);跨 feature 共享的 provider 定义在对应的 `core_*` 包里。 + +## 测试 + +- `Notifier`/`AsyncNotifier` 的单元测试用 `ProviderContainer` 直接实例化,不依赖 widget tree。 +- Widget 测试中用 `ProviderScope(overrides: [...])` 注入 mock 依赖。 + +## 附录:Riverpod 是什么,日常怎么用 + +给还没接触过 Riverpod 的同学看的入门说明。 + +### 要解决的问题 + +Flutter 官方最早推荐的状态管理方式是 [`InheritedWidget`](https://docs.flutter.dev/data-and-backend/state-mgmt/inherited-widget)——通过 widget 树往下传数据。写法繁琐,社区后来做了一层封装叫 [`Provider`](https://pub.dev/packages/provider),但 `Provider` 本质还是绑定在 widget 树上:拿依赖必须要有 `BuildContext`,写错了会在运行时才报错(比如 `ProviderNotFoundException`),而且没法很方便地在 widget 树之外(比如后台任务、单元测试)读取状态。 + +**Riverpod** 是 `Provider` 的原作者 Remi Rousselet 重新设计的下一代方案:把状态容器从 widget 树里剥离出来,变成一套独立的依赖图,`BuildContext` 不再是拿依赖的必要条件,错误也从运行时提前到**编译期**发现(比如 provider 类型不匹配会直接编译报错,而不是运行时崩溃)。 + +### 核心概念 + +1. **`Provider`**:声明一个"如何创建某个值"的配方,值可以是同步的、异步的(`Future`/`Stream`)、也可以是可变的状态。 +2. **`Notifier` / `AsyncNotifier`**:承载**可变**状态的载体,通过方法修改状态(类似过去 `StateNotifier` 的角色,3.x 里统一成 `Notifier`)。 +3. **`ref.watch(xxxProvider)`**:在 widget 或另一个 provider 里订阅某个 provider,值变化时自动触发重建/重新计算。 +4. **`ref.read(xxxProvider)`**:只读取一次当前值,不订阅变化(一般用在按钮点击等一次性事件回调里)。 +5. **`@riverpod` 注解 + 代码生成**:手写 `Provider`/`NotifierProvider` 样板代码容易出错(尤其是泛型),项目统一用 `riverpod_generator` 的注解写法,跑 `build_runner` 自动生成对应的 provider。 + +### 使用示例(`feature_store`:拉取附近门店列表) + +```dart +// presentation/store_list_notifier.dart +part 'store_list_notifier.g.dart'; + +@riverpod +class StoreListNotifier extends _$StoreListNotifier { + @override + Future> build() async { + final repository = ref.watch(storeRepositoryProvider); + return repository.fetchNearbyStores(_currentLat, _currentLng); + } + + Future refresh() async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() => build()); + } +} +``` + +```dart +// presentation/store_list_page.dart +class StoreListPage extends ConsumerWidget { + const StoreListPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final storesAsync = ref.watch(storeListNotifierProvider); + + return storesAsync.when( + data: (stores) => ListView.builder( + itemCount: stores.length, + itemBuilder: (_, i) => ListTile(title: Text(stores[i].name)), + ), + loading: () => const CircularProgressIndicator(), + error: (err, _) => Text('加载失败: $err'), + ); + } +} +``` + +`storeRepositoryProvider` 定义在 `data` 层(见 [02-layering.md](./02-layering.md) 的跳过 domain 层示例),`StoreListNotifier` 通过 `ref.watch` 拿到接口类型,不关心具体实现——这就是 Riverpod 承担依赖注入职责的地方,不需要额外的 `get_it`。 + +### 测试示例 + +```dart +test('刷新后状态应更新为最新门店列表', () async { + final container = ProviderContainer( + overrides: [ + storeRepositoryProvider.overrideWithValue(FakeStoreRepository()), + ], + ); + addTearDown(container.dispose); + + final stores = await container.read(storeListNotifierProvider.future); + expect(stores, isNotEmpty); +}); +``` + +`ProviderContainer` 让整个依赖图脱离 widget 树单独运行,`overrides` 直接替换掉真实的 repository,这也是"编译期安全 + 好测试"这条评价的具体体现。 + +## 参考链接 + +- [Riverpod 官方文档](https://riverpod.dev/) +- [riverpod_generator | Dart package](https://pub.dev/packages/riverpod_generator) +- [flutter_riverpod | Dart package](https://pub.dev/packages/flutter_riverpod) +- [riverpod_lint | Dart package](https://pub.dev/packages/riverpod_lint) +- [InheritedWidget(Flutter 官方文档)](https://docs.flutter.dev/data-and-backend/state-mgmt/inherited-widget) +- [provider | Dart package](https://pub.dev/packages/provider) diff --git a/04-routing.md b/04-routing.md new file mode 100644 index 0000000..91773b8 --- /dev/null +++ b/04-routing.md @@ -0,0 +1,104 @@ +# 04. 路由方案 + +## 决策 + +使用 **[go_router](https://pub.dev/packages/go_router)**(`^17.5.0`,2026-08 快照,Flutter 官方维护),声明式路由 + 嵌套 `ShellRoute`,不使用 `Navigator 1.0` 命令式 push/pop 作为主路由方式。 + +## 依赖 + +```yaml +dependencies: + go_router: ^17.5.0 +``` + +## 路由注册规则 + +- 每个 `feature_*` 包在自己的 `feature_xxx.dart`(对外唯一导出文件)里暴露一个 `List buildXxxRoutes()` 函数,只声明属于自己的路由,不感知其他 feature。 +- `core_router` 包负责把所有 feature 的路由函数聚合成最终的 `GoRouter` 实例,是唯一知道"全部路由长什么样"的地方。 +- 路径命名统一用 `kebab-case`,前缀按业务域分组,例如 `/store/:storeId`、`/payment/confirm`。 +- 底部导航等常驻 UI 用 `ShellRoute`/`StatefulShellRoute` 包裹对应的 feature 路由,不在每个页面里重复搭一遍导航栏。 +- 登录态校验统一在 `core_router` 聚合层用 `redirect` 实现,不在每个页面里各自判断 token 是否过期。 +- 跨 feature 跳转只能传**可序列化参数**(path 参数、query 参数,或可序列化的 `extra`),不允许把一个 feature 内部的 Dart 类实例通过 `extra` 传给另一个 feature——这是 [01-project-structure.md](./01-project-structure.md) "Feature 间通信" 规则在路由层的具体落地。 + +## 参考链接 + +- [go_router 官方文档](https://pub.dev/packages/go_router) +- [go_router | Dart package](https://pub.dev/packages/go_router) + +## 附录:go_router 是什么,日常怎么用 + +给还没接触过声明式路由的同学看的入门说明。 + +### 要解决的问题 + +`Navigator 1.0` 的命令式写法(`Navigator.push(context, MaterialPageRoute(...))`)在页面不多的时候很直观,但规模上来后有几个明显问题: + +1. **深链接(deep link)/ Web URL 支持差**:命令式 push 本质是"从当前页面跳到下一个页面",很难直接根据一个 URL 字符串恢复出正确的页面栈——比如从推送通知直接打开"门店详情页",命令式写法需要手动拼一串 `push` 调用重建整个栈。 +2. **没有统一的登录拦截点**:每个需要登录态的页面都要自己在 `initState` 里判断要不要跳转到登录页,逻辑散落在各处。 +3. **底部导航这种"多个 tab 各自维护自己的页面栈"的场景很难优雅表达**。 + +**go_router** 是 Flutter 官方团队维护的声明式路由方案:路由表是一份**声明式配置**(一棵 `GoRoute` 树),当前 URL 决定当前应该显示什么页面栈,而不是"一步步 push 出来的"。因为路由是声明式的、和 URL 强绑定,深链接、Web 浏览器前进/后退、登录拦截都能用同一套机制解决。 + +### 核心概念 + +1. **`GoRoute`**:一条路由规则,`path` 是路径模板(支持 `:id` 这种参数),`builder`/`pageBuilder` 返回对应页面。 +2. **`ShellRoute` / `StatefulShellRoute`**:包一层常驻 UI(比如带底部导航栏的外壳),内部嵌套的子路由切换时,外壳本身不重建;`StatefulShellRoute` 还能让每个 tab 各自保留自己的页面栈(切 tab 不丢失之前的浏览位置)。 +3. **`GoRouterState`**:在 `builder` 里能拿到当前路由的 path 参数(`state.pathParameters`)、query 参数(`state.uri.queryParameters`)、`extra` 对象。 +4. **`redirect`**:每次路由变化前会先跑一遍 `redirect` 回调,返回非空字符串就强制跳转——这是实现"未登录访问需要登录的页面 → 自动跳登录页"的地方。 +5. **`context.go()` / `context.push()`**:`go` 是替换当前路由(浏览器前进后退语义),`push` 是在当前栈上叠加一层(可以 `pop` 回去)——日常最容易混淆的两个 API,选错会导致返回键行为不符合预期。 + +### 使用示例(底部导航 + 门店详情页 + 登录拦截) + +```dart +// packages/core_router/lib/src/app_router.dart +final goRouterProvider = Provider((ref) { + final authState = ref.watch(authStateProvider); + + return GoRouter( + initialLocation: '/store', + redirect: (context, state) { + final loggingIn = state.matchedLocation == '/login'; + if (!authState.isLoggedIn && !loggingIn) return '/login'; + if (authState.isLoggedIn && loggingIn) return '/store'; + return null; // 不需要重定向 + }, + routes: [ + GoRoute(path: '/login', builder: (context, state) => const LoginPage()), + StatefulShellRoute.indexedStack( + builder: (context, state, navigationShell) => + MainShell(navigationShell: navigationShell), + branches: [ + StatefulShellBranch(routes: buildStoreRoutes()), + StatefulShellBranch(routes: buildPaymentRoutes()), + ], + ), + ], + ); +}); +``` + +```dart +// packages/feature_store/lib/feature_store.dart +List buildStoreRoutes() => [ + GoRoute( + path: '/store', + builder: (context, state) => const StoreListPage(), + routes: [ + GoRoute( + path: ':storeId', // 完整路径 /store/:storeId + builder: (context, state) { + final storeId = state.pathParameters['storeId']!; + return StoreDetailPage(storeId: storeId); + }, + ), + ], + ), +]; +``` + +```dart +// 从任意页面跳转到门店详情 +context.push('/store/${store.id}'); +``` + +`buildStoreRoutes()` 只在 `feature_store` 包内声明,`app_router.dart` 里只 import 这个函数、不 import `feature_store` 的任何页面 widget 类型——保持 [01-project-structure.md](./01-project-structure.md) 的编译期边界。 diff --git a/05-networking.md b/05-networking.md new file mode 100644 index 0000000..52d7d8d --- /dev/null +++ b/05-networking.md @@ -0,0 +1,151 @@ +# 05. 网络层设计 + +## 决策 + +使用 **[dio](https://pub.dev/packages/dio)**(`^5.11.0`,2026-08 快照)作为唯一 HTTP client,统一封装在 `core_network` 包里,通过拦截器链处理鉴权、日志、异常归一化,不允许各 `feature_*` 自建 `Dio` 实例。 + +## 依赖 + +```yaml +dependencies: + dio: ^5.11.0 +``` + +## 使用规则 + +- `core_network` 暴露一个单例 `Dio` 实例(通过 Riverpod provider 注入,见 [03-state-management.md](./03-state-management.md)),所有 `feature_*` 的 repository 只能通过依赖注入拿这个实例,不允许 `Dio()` 直接 new。 +- 拦截器按固定顺序注册:`LogInterceptor`(仅 dev/staging 环境开启)→ `AuthInterceptor`(附加 token,401 时排队刷新)→ `ErrorMappingInterceptor`(把 `DioException` 统一转成项目自定义的 `AppException` 体系)。 +- 业务代码只捕获 `AppException` 及其子类(如 `NetworkException`、`UnauthorizedException`、`BusinessException`),不直接处理 `DioException`——异常归一化只在 `core_network` 内部发生一次。 +- 每个 feature 的 repository 只声明自己需要的接口方法,不直接拼接完整 URL 字符串到处写,`baseUrl` 和超时时间统一在 `core_network` 里按环境配置(见 [08-build-flavors.md](./08-build-flavors.md),待写)。 +- 需要取消请求的场景(比如页面销毁时中断未完成的请求)统一使用 `CancelToken`,在对应 `Notifier`/`State` 的 `dispose` 时调用 `cancel()`。 + +## 参考链接 + +- [dio 官方文档](https://pub.dev/packages/dio) +- [dio | Dart package](https://pub.dev/packages/dio) +- [Dio Interceptors 文档](https://pub.dev/packages/dio#interceptors) + +## 附录:dio 是什么,日常怎么用 + +给还没接触过这套网络层封装方式的同学看的入门说明。 + +### 要解决的问题 + +Dart 内置的 `http` 包只提供最基础的请求能力,实际项目里几乎总会遇到这些需求: + +1. **每个请求都要带 token**,token 过期了要先刷新再重试,不能让每个业务代码自己判断"要不要刷新 token"。 +2. **统一的错误处理**:网络超时、服务端返回的业务错误码、HTTP 状态码错误,最终都要转换成 UI 层能直接判断的统一异常类型,而不是每个 feature 各自写一遍 `try/catch` 判断状态码。 +3. **请求/响应日志**:调试时想看到完整的请求参数和返回值,上线后又不希望日志暴露敏感数据。 + +`http` 包本身没有"拦截器"这个概念,要实现上面这些都得自己在每次调用前后手动包一层逻辑,容易漏、容易不一致。**dio** 内置了 [`Interceptor`](https://pub.dev/packages/dio#interceptors) 机制,可以把这些横切逻辑集中写一次,所有请求自动生效。 + +### 核心概念 + +1. **`Dio` 实例**:一个 client 对象,带 `BaseOptions`(`baseUrl`、`connectTimeout` 等默认配置),项目里应该只有一个全局实例,而不是每个地方各建一个。 +2. **`Interceptor`**:可以拦截请求发出前(`onRequest`)、响应回来后(`onResponse`)、出错时(`onError`)三个时机,多个拦截器按注册顺序像洋葱一样依次包裹。 +3. **`DioException`**:dio 所有的错误(超时、连接失败、非 2xx 状态码等)都会包装成这个类型,可以在拦截器里统一识别、转换。 +4. **`CancelToken`**:用来主动取消一个还没完成的请求,常见场景是页面销毁或用户切走时中断请求,避免拿到结果时 widget 已经不存在了。 + +### 使用示例(token 自动附加 + 401 自动刷新排队 + 异常归一化) + +```dart +// packages/core_network/lib/src/dio_client.dart +final dioProvider = Provider((ref) { + final dio = Dio(BaseOptions( + baseUrl: ref.watch(appEnvProvider).apiBaseUrl, // 见 08-build-flavors.md + connectTimeout: const Duration(seconds: 10), + )); + + dio.interceptors.addAll([ + if (ref.watch(appEnvProvider).enableLog) LogInterceptor(responseBody: false), + AuthInterceptor(ref), + ErrorMappingInterceptor(), + ]); + + return dio; +}); +``` + +```dart +// packages/core_network/lib/src/auth_interceptor.dart +class AuthInterceptor extends Interceptor { + final Ref _ref; + bool _isRefreshing = false; + final _pendingRequests = >[]; + + AuthInterceptor(this._ref); + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + final token = _ref.read(authStateProvider).accessToken; + if (token != null) options.headers['Authorization'] = 'Bearer $token'; + handler.next(options); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) async { + if (err.response?.statusCode != 401) return handler.next(err); + + if (_isRefreshing) { + // 已经有一个刷新请求在跑,排队等它完成,避免并发刷新 + final completer = Completer(); + _pendingRequests.add(completer); + await completer.future; + return handler.resolve(await _retry(err.requestOptions)); + } + + _isRefreshing = true; + try { + await _ref.read(authRepositoryProvider).refreshToken(); + for (final c in _pendingRequests) { + c.complete(); + } + _pendingRequests.clear(); + handler.resolve(await _retry(err.requestOptions)); + } catch (_) { + _ref.read(authStateProvider.notifier).logout(); + handler.next(err); + } finally { + _isRefreshing = false; + } + } + + Future _retry(RequestOptions options) { + final dio = _ref.read(dioProvider); + return dio.fetch(options); + } +} +``` + +```dart +// packages/core_network/lib/src/error_mapping_interceptor.dart +class ErrorMappingInterceptor extends Interceptor { + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + final mapped = switch (err.type) { + DioExceptionType.connectionTimeout || + DioExceptionType.receiveTimeout => NetworkException('网络超时'), + DioExceptionType.badResponse when err.response?.statusCode == 401 => + UnauthorizedException(), + DioExceptionType.badResponse => BusinessException( + err.response?.data['message'] ?? '请求失败', + code: err.response?.statusCode, + ), + _ => NetworkException('网络异常,请稍后重试'), + }; + handler.reject(DioException(requestOptions: err.requestOptions, error: mapped)); + } +} +``` + +业务层代码只需要这样写,不用关心 dio 内部细节: + +```dart +try { + final stores = await storeRepository.fetchNearbyStores(lat, lng); +} on UnauthorizedException { + // 跳登录页 +} on AppException catch (e) { + // 统一展示 e.message +} +``` diff --git a/06-local-storage.md b/06-local-storage.md new file mode 100644 index 0000000..4de696b --- /dev/null +++ b/06-local-storage.md @@ -0,0 +1,158 @@ +# 06. 本地存储方案 + +## 决策 + +按数据类型分三档存储,全部封装在 `core_storage` 包内,`feature_*` 不直接依赖底层存储库: + +| 数据类型 | 方案 | 版本(2026-08 快照) | +|---|---|---| +| 结构化/关系型数据(门店列表缓存、订单历史等) | **[Drift](https://pub.dev/packages/drift)** | `^2.34.3` | +| 敏感数据(token、refresh token) | **[flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage)** | `^11.0.0` | +| 简单非敏感 KV(是否看过引导页、用户偏好设置) | **[shared_preferences](https://pub.dev/packages/shared_preferences)** | `^2.5.5` | + +## 依赖 + +```yaml +dependencies: + drift: ^2.34.3 + sqlite3_flutter_libs: ^0.5.0 + flutter_secure_storage: ^11.0.0 + shared_preferences: ^2.5.5 + +dev_dependencies: + drift_dev: ^2.34.3 + build_runner: ^2.4.0 +``` + +## 使用规则 + +- 全仓库**只有一个 Drift 数据库实例**,定义在 `core_storage` 里,不允许每个 `feature_*` 各自建一个 SQLite 文件——避免多个数据库文件之间做跨 feature 查询/事务的麻烦。 +- 每个 feature 拥有自己的表(`Table` 类)和 DAO(`DriftAccessor`),表名加 feature 前缀(如 `store_cache`、`payment_history`)避免命名冲突,但都注册进同一个 `AppDatabase`。 +- feature 的 `data` 层 `local_datasource` 只依赖自己的 DAO 类型,不直接操作 `AppDatabase` 或访问其他 feature 的表。 +- token / refresh token 只能经过 `core_auth` 包里封装的 secure storage 读写方法,不允许其他 `core_*`/`feature_*` 直接调用 `FlutterSecureStorage` 实例。 +- 数据库表结构变更必须写 migration(`onUpgrade` + `schemaVersion` 递增),不允许直接改字段定义后期望"重装了事"——线上用户已有数据需要平滑迁移。 + +## 参考链接 + +- [Drift 官方文档](https://drift.simonbinder.eu/) +- [drift | Dart package](https://pub.dev/packages/drift) +- [flutter_secure_storage | Dart package](https://pub.dev/packages/flutter_secure_storage) +- [shared_preferences | Dart package](https://pub.dev/packages/shared_preferences) + +## 附录:Drift 是什么,日常怎么用 + +给还没接触过这套本地数据库封装方式的同学看的入门说明。 + +### 要解决的问题 + +Flutter 生态里直接操作本地 SQLite 最常见的是 [`sqflite`](https://pub.dev/packages/sqflite),但它是纯 SQL 字符串拼接: + +```dart +// sqflite 写法,容易手滑打错字段名/表名,编译期完全发现不了 +await db.rawQuery('SELECT * FROM stroe WHERE nmae = ?', [name]); +``` + +字段名、表名全靠字符串,拼错了只有运行时才报错;查询结果是 `Map`,还得手动转成业务对象;数据变化了想让 UI 自动刷新,也得自己手写一套通知机制。 + +**Drift** 在 `sqflite`(或更底层的 `sqlite3`)之上加了一层代码生成:用 Dart 类定义表结构,`build_runner` 生成类型安全的查询代码,写错字段名/类型在编译期就会报错;查询结果直接是强类型的 Dart 对象;还内置了 `.watch()` 方法,数据变化时自动推送新结果,天然适合配合 Riverpod 的 `StreamProvider`/`AsyncNotifier` 做响应式 UI。 + +### 核心概念 + +1. **`Table` 类**:用 Dart 代码声明表结构(字段名、类型、约束),而不是手写 `CREATE TABLE` 语句。 +2. **`DriftAccessor`(DAO)**:给一组相关表写查询/增删改方法的地方,业务代码只调用 DAO 方法,不直接写 SQL。 +3. **`.watch()` vs `.get()`**:`.get()` 是一次性查询,`.watch()` 返回一个 `Stream`,只要底层数据变化(哪怕是另一个页面改的)就会自动推送新结果——不需要手动刷新。 +4. **`schemaVersion` + `onUpgrade`**:数据库版本号和迁移回调,改表结构时递增版本号并在 `onUpgrade` 里写迁移逻辑(加字段、建索引等),保证已安装用户的本地数据不会因为升级直接报错或丢失。 + +### 使用示例(`feature_store`:门店列表本地缓存) + +```dart +// packages/core_storage/lib/src/tables/store_table.dart +class StoreCache extends Table { + TextColumn get id => text()(); + TextColumn get name => text()(); + RealColumn get lat => real()(); + RealColumn get lng => real()(); + DateTimeColumn get cachedAt => dateTime()(); + + @override + Set get primaryKey => {id}; +} +``` + +```dart +// packages/core_storage/lib/src/daos/store_dao.dart +part 'store_dao.g.dart'; + +@DriftAccessor(tables: [StoreCache]) +class StoreDao extends DatabaseAccessor with _$StoreDaoMixin { + StoreDao(super.db); + + Future upsertAll(List stores) => + batch((b) => b.insertAllOnConflictUpdate(storeCache, stores)); + + Stream> watchAll() => select(storeCache).watch(); +} +``` + +```dart +// packages/core_storage/lib/src/app_database.dart +@DriftDatabase(tables: [StoreCache, PaymentHistory], daos: [StoreDao, PaymentHistoryDao]) +class AppDatabase extends _$AppDatabase { + AppDatabase() : super(_openConnection()); + + @override + int get schemaVersion => 2; + + @override + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + if (from < 2) { + await m.addColumn(storeCache, storeCache.cachedAt); + } + }, + ); +} +``` + +```dart +// feature_store 的 local_datasource 只依赖 StoreDao,不直接碰 AppDatabase +class StoreLocalDataSource { + final StoreDao _dao; + StoreLocalDataSource(this._dao); + + Stream> watchCachedStores() => + _dao.watchAll().map((rows) => rows.map(Store.fromCacheRow).toList()); +} +``` + +配合 Riverpod 做响应式 UI(离线也能展示上次缓存的门店列表,等网络数据回来再刷新): + +```dart +@riverpod +Stream> cachedStores(Ref ref) { + final localDataSource = ref.watch(storeLocalDataSourceProvider); + return localDataSource.watchCachedStores(); +} +``` + +### secure storage 使用示例(token 存取) + +```dart +// packages/core_auth/lib/src/token_storage.dart +class TokenStorage { + final FlutterSecureStorage _storage; + TokenStorage(this._storage); + + Future saveTokens({required String accessToken, required String refreshToken}) => + Future.wait([ + _storage.write(key: 'access_token', value: accessToken), + _storage.write(key: 'refresh_token', value: refreshToken), + ]); + + Future readAccessToken() => _storage.read(key: 'access_token'); + + Future clear() => _storage.deleteAll(); +} +``` + +`TokenStorage` 是全仓库唯一直接持有 `FlutterSecureStorage` 实例的类,其他包只能通过 `core_auth` 暴露的 provider 间接读写 token。 diff --git a/07-native-integration.md b/07-native-integration.md new file mode 100644 index 0000000..f5ae98a --- /dev/null +++ b/07-native-integration.md @@ -0,0 +1,137 @@ +# 07. 原生能力集成方式 + +## 决策 + +原生能力(扫码、支付、蓝牙等)统一封装成独立的 `native_*` Dart package(结构见 [01-project-structure.md](./01-project-structure.md)),跨语言接口用 **[Pigeon](https://pub.dev/packages/pigeon)**(`^27.3.0`,2026-08 快照)生成,不手写裸 `MethodChannel`/`invokeMethod` 字符串调用。 + +## 依赖 + +```yaml +dev_dependencies: + pigeon: ^27.3.0 +``` + +## 包结构规则 + +``` +native_scan/ + pigeons/ + scan_api.dart # 接口 schema 定义,唯一手写的源文件 + lib/ + native_scan.dart # 对外导出:封装好的公共 API 类(feature 只调这个) + src/ + generated/ # pigeon 生成的 Dart 端代码,不手动修改 + android/ + src/main/kotlin/.../ScanApiImpl.kt # 生成的 Kotlin host API 接口的具体实现 + ios/ + Classes/ScanApiImpl.swift # 生成的 Swift host API 接口的具体实现 + ohos/ + src/main/ets/ScanApiImpl.ets # 生成的 ArkTS host API 接口的具体实现 +``` + +## 使用规则 + +- `pigeons/xxx_api.dart` 是**唯一手写**的接口定义文件,Dart 端和三端原生的桩代码全部由 `dart run pigeon --input pigeons/xxx_api.dart` 生成,生成产物不手动修改,改需求就改 schema 重新生成。 +- Dart 调原生用 `@HostApi()`;原生主动推事件给 Dart(比如扫码结果的持续回调)用 `@FlutterApi()`——不允许为了图省事用 `@HostApi()` 硬凑双向通信。 +- `feature_*` 只允许依赖对应 `native_*` 包 `lib/native_xxx.dart` 导出的公共 API 类,不允许直接 import `src/generated/` 里的生成代码。 +- 原生侧异常需要在生成的 host API 实现里捕获并转换成 Pigeon schema 里声明的错误类型,Dart 侧统一映射成 [05-networking.md](./05-networking.md) 里同一套 `AppException` 体系,不让原生异常类型(如 `PlatformException`)直接抛到 `feature_*` 业务代码里。 +- 三端(Android/iOS/OHOS)中若某一端暂未实现,公共 API 类里对应平台分支返回明确的 `UnimplementedError`,不允许静默返回空值或占位假数据。 + +## 参考链接 + +- [Pigeon 官方文档](https://pub.dev/packages/pigeon) +- [pigeon | Dart package](https://pub.dev/packages/pigeon) +- [Flutter 平台通道官方文档](https://docs.flutter.dev/platform-integration/platform-channels) + +## 附录:Pigeon 是什么,日常怎么用 + +给还没接触过跨语言原生集成的同学看的入门说明。 + +### 要解决的问题 + +Flutter 原生的 [`MethodChannel`](https://docs.flutter.dev/platform-integration/platform-channels) 机制本质是"字符串方法名 + 弱类型参数"的消息传递: + +```dart +// 手写 MethodChannel,容易出的问题: +final result = await MethodChannel('scan_channel').invokeMethod('startScan', {'timeout': 5000}); +// 1. 'startScan' 是字符串,原生那边方法名打错了,运行时才报 "not implemented" +// 2. 参数是 Map,字段名/类型对不上,运行时才崩,编译期完全看不出来 +// 3. 返回值类型是 dynamic,还要自己强转、自己判断 null +``` + +三个问题的共性是:**Dart 和原生代码之间没有共享的类型系统**,接口的一致性完全靠开发者手动保证、runtime 才能发现错误。 + +**Pigeon** 用一个 Dart 文件定义"接口 schema"(有哪些方法、参数和返回值类型),然后生成 Dart 端 + Android(Kotlin) + iOS(Swift) 三端的强类型桩代码——方法名、参数、返回类型三端保持一致,改了 schema 忘记同步实现,编译期就会报错(生成的原生接口是抽象类/协议,没实现完整会编译不过),彻底消灭"方法名打错""参数字段对不上"这类只有运行时才发现的问题。 + +### 核心概念 + +1. **Schema 文件**(`pigeons/xxx_api.dart`):用普通 Dart 类和注解描述接口,不是真的可执行代码,只是给 pigeon 生成器读的"接口契约"。 +2. **`@HostApi()`**:声明一个"Dart 调用原生"的接口,pigeon 生成 Dart 端可直接调用的类,以及原生端需要实现的抽象类/协议。 +3. **`@FlutterApi()`**:声明一个"原生调用 Dart"的接口(方向相反),用于原生侧主动推送事件(比如蓝牙扫描持续上报发现的设备)。 +4. **生成命令**:`dart run pigeon --input pigeons/xxx_api.dart` 会同时生成 Dart、Kotlin、Swift 三份代码,开发者只需要去实现原生那两个抽象类/协议里的方法体。 + +### 使用示例(`native_scan`:扫码能力) + +```dart +// native_scan/pigeons/scan_api.dart —— 唯一手写的 schema 文件 +@HostApi() +abstract class ScanHostApi { + @async + ScanResult startScan(ScanOptions options); + void stopScan(); +} + +class ScanOptions { + ScanOptions({required this.timeoutMs}); + final int timeoutMs; +} + +class ScanResult { + ScanResult({required this.code, required this.format}); + final String code; + final String format; +} +``` + +```bash +dart run pigeon \ + --input pigeons/scan_api.dart \ + --dart_out lib/src/generated/scan_api.g.dart \ + --kotlin_out android/src/main/kotlin/com/conti/native_scan/ScanApi.g.kt \ + --swift_out ios/Classes/ScanApi.g.swift +``` + +Android 端实现生成的抽象类(`ScanApiImpl.kt`,非生成代码,是需要手写的实现): + +```kotlin +class ScanApiImpl(private val activity: Activity) : ScanHostApi { + override fun startScan(options: ScanOptions, callback: (Result) -> Unit) { + // 调用具体的扫码 SDK,拿到结果后: + callback(Result.success(ScanResult(code = "123456", format = "QR_CODE"))) + } + + override fun stopScan() { + // 停止扫码 SDK + } +} +``` + +Dart 端对外的公共 API(`native_scan.dart`,`feature_scan` 唯一能调用的入口): + +```dart +class NativeScan { + final ScanHostApi _api = ScanHostApi(); + + Future startScan({Duration timeout = const Duration(seconds: 5)}) async { + try { + return await _api.startScan(ScanOptions(timeoutMs: timeout.inMilliseconds)); + } on PlatformException catch (e) { + throw NativeCapabilityException('扫码失败: ${e.message}'); + } + } + + Future stopScan() => _api.stopScan(); +} +``` + +`feature_scan` 只 import `NativeScan` 这一个类,完全不知道底层是 Pigeon 生成的还是手写 `MethodChannel`——这也是把原生能力做成独立 `native_*` package(而不是散落在各 feature 里直接写平台通道代码)的意义:原生实现细节被这一层完全封装。 diff --git a/08-build-flavors.md b/08-build-flavors.md new file mode 100644 index 0000000..4a99874 --- /dev/null +++ b/08-build-flavors.md @@ -0,0 +1,126 @@ +# 08. 多环境构建 + +## 决策 + +App 侧维护 **3 个 flavor:`dev` / `uat` / `prod`**,环境划分与现有后端 CI/CD(见 `gitlab-cicd-azure-deployment-diagram.drawio` 里的 Dev/UAT/Prod Azure 环境)保持一致命名,三个 flavor 各自对应不同的 API 地址、应用图标/名称、包名后缀,可在同一台设备上同时安装、互不覆盖。CI 沿用现有 GitLab CI(Runner 与后端共用),但产物是 App 二进制(apk/ipa),分发渠道与后端的 ACR/Azure App Hosting 不同。 + +## Flavor 划分规则 + +| Flavor | Application ID / Bundle ID | API 目标 | 分发渠道 | +|---|---|---|---| +| `dev` | `com.conti.retail.dev` | Dev 环境(对应后端 Dev) | Firebase App Distribution(内部测试) | +| `uat` | `com.conti.retail.uat` | UAT 环境(对应后端 UAT) | Firebase App Distribution(验收测试) | +| `prod` | `com.conti.retail` | Prod 环境(对应后端 Prod) | App Store Connect / Google Play(生产发布) | + +## 使用规则 + +- 每个 flavor 对应一个独立的 Dart 入口文件(`main_dev.dart`/`main_uat.dart`/`main_prod.dart`),三者都只是设置好环境标识后调用同一个共享的 `bootstrap()` 启动函数,不允许在入口文件里写业务逻辑分支。 +- 环境相关的可变配置(API base URL、是否开启日志等,见 [05-networking.md](./05-networking.md) 的 `appEnvProvider`)通过 `--dart-define-from-file=env/{flavor}.json` 注入,不写死在代码里、也不用 `if (flavor == 'dev')` 这种运行时字符串判断来分支配置。 +- `env/*.json` 只包含非敏感配置(API 地址等);密钥类配置(如第三方 SDK App Key)通过 CI 变量在构建时注入,不提交进仓库。 +- Android 侧用 Gradle `productFlavors` 区分 `applicationId`/图标/`versionNameSuffix`;iOS 侧用对应的 xcconfig + Scheme 区分 `Bundle Identifier`/图标;两端 flavor 名称必须完全一致(`dev`/`uat`/`prod`),不允许两端用不同命名。 +- CI 流水线阶段固定为:`melos run analyze` → `melos run test` → 按 flavor `flutter build apk/ipa --flavor {flavor} --dart-define-from-file=env/{flavor}.json` → 上传对应分发渠道。`prod` flavor 的构建触发条件是打 tag,不是每次 push 都触发(避免误发生产包)。 + +## 参考链接 + +- [Flutter 官方 Flavors 文档](https://docs.flutter.dev/deployment/flavors) +- [--dart-define-from-file 官方说明](https://docs.flutter.dev/deployment/flavors#configuration-approaches) +- [Firebase App Distribution](https://firebase.google.com/docs/app-distribution) + +## 附录:Flavor 是什么,日常怎么用 + +给还没接触过多环境构建方式的同学看的入门说明。 + +### 要解决的问题 + +一个 App 通常需要同时存在"开发中还没上线的版本"和"已经上线的正式版本",测试期间还需要一个给验收测试用的版本——这三个版本理想情况下要能**同时装在同一台测试手机上**,方便对比测试,而不是每次切换环境都要卸载重装。同时它们各自要打到不同的后端地址(Dev/UAT/Prod API),不能一个 apk 通过运行时开关切换后端就完事——因为如果只用运行时环境变量,三个环境包的 `applicationId`/`Bundle ID` 完全一样,装第二个会直接覆盖第一个。 + +**Flavor** 是 Android(Gradle `productFlavors`,历史悠久的原生概念)和 iOS(Xcode Build Configuration/Scheme)本来就有的机制:在同一份代码基础上,用不同的编译配置产出`applicationId`/图标/名称都不同的多个安装包。Flutter 从工具链层面(`flutter build --flavor xxx`)把两端的 flavor 机制包装成统一的命令行接口。 + +### 核心概念 + +1. **Android `productFlavors`**:在 `android/app/build.gradle` 里声明多套 `applicationId`/`versionNameSuffix`/资源目录,编译时用 `--flavor` 选择其中一套。 +2. **iOS Scheme + xcconfig**:iOS 没有 Gradle 那样的单文件配置,而是通过 Xcode 里多个 Build Configuration(对应不同的 `.xcconfig` 文件设置 Bundle ID 等)+ 多个 Scheme 组合实现同样的效果,`flutter build ipa --flavor xxx` 背后就是选中同名 Scheme。 +3. **Dart 入口文件(`main_xxx.dart`)**:flavor 决定的是"编译出什么样的原生外壳",Dart 代码本身默认只有一个 `main.dart` 入口——项目约定用多个入口文件对应各 flavor,让每个 flavor 能设置不同的启动参数(比如传给 `bootstrap()` 一个环境枚举)。 +4. **`--dart-define-from-file`**:flavor 解决的是原生层面的差异(图标、包名),但 API 地址这类 Dart 侧读取的配置,用编译期注入的 JSON 文件解决,避免打包进一个写死 `http://dev-api...` 的字符串常量。 + +### 使用示例 + +Android 侧 flavor 声明(`android/app/build.gradle`): + +```gradle +android { + flavorDimensions "env" + productFlavors { + dev { + dimension "env" + applicationId "com.conti.retail.dev" + versionNameSuffix "-dev" + } + uat { + dimension "env" + applicationId "com.conti.retail.uat" + versionNameSuffix "-uat" + } + prod { + dimension "env" + applicationId "com.conti.retail" + } + } +} +``` + +环境配置文件(`env/dev.json`,非敏感部分): + +```json +{ + "API_BASE_URL": "https://dev-api.conti-retail.com", + "ENABLE_LOG": true +} +``` + +共享启动入口 + 各 flavor 的 Dart 入口文件: + +```dart +// lib/bootstrap.dart —— 三个 flavor 共用的启动逻辑 +Future bootstrap(AppEnv env) async { + runApp(ProviderScope( + overrides: [appEnvProvider.overrideWithValue(env)], + child: const App(), + )); +} +``` + +```dart +// lib/main_dev.dart +void main() => bootstrap(AppEnv.fromDartDefine(name: 'dev')); +``` + +构建命令: + +```bash +flutter build apk \ + --flavor dev \ + --target lib/main_dev.dart \ + --dart-define-from-file=env/dev.json +``` + +GitLab CI 片段(衔接现有 Runner,产物走 Firebase App Distribution 而非后端用的 ACR): + +```yaml +build_dev: + stage: build + script: + - melos run analyze + - melos run test + - flutter build apk --flavor dev --target lib/main_dev.dart --dart-define-from-file=env/dev.json + - firebase appdistribution:distribute build/app/outputs/.../dev/release/app-dev-release.apk + rules: + - if: '$CI_COMMIT_BRANCH == "develop"' + +build_prod: + stage: build + script: + - flutter build appbundle --flavor prod --target lib/main_prod.dart --dart-define-from-file=env/prod.json + rules: + - if: '$CI_COMMIT_TAG' # 只在打 tag 时触发,避免误发生产包 +``` diff --git a/09-testing.md b/09-testing.md new file mode 100644 index 0000000..79c8229 --- /dev/null +++ b/09-testing.md @@ -0,0 +1,148 @@ +# 09. 测试策略 + +## 决策 + +采用三层测试金字塔,覆盖顺序从多到少:**单元测试**(domain 业务规则 + Riverpod `Notifier`)> **Widget 测试**(关键页面的 loading/data/error 状态)> **集成测试**(仅覆盖 1-2 条黄金路径,如登录→下单→支付)。Mock 框架统一用 **[mocktail](https://pub.dev/packages/mocktail)**(`^1.0.5`),不用 `mockito`——避免再引入一套 `build_runner` codegen 目标(项目里 `riverpod_generator`/`drift_dev`/`pigeon` 已经用了 codegen,`mocktail` 不需要生成代码,减少构建链路复杂度)。 + +## 依赖 + +```yaml +dev_dependencies: + mocktail: ^1.0.5 + test: any # 纯 Dart 单元测试 + flutter_test: + sdk: flutter + integration_test: + sdk: flutter +``` + +## 分层测试规则 + +- **domain 层(有 domain 的 feature)**:use case 用纯 Dart 单元测试,mock 掉 `repository` 接口,覆盖多步骤业务规则的分支(如 [02-layering.md](./02-layering.md) 里 `ConfirmPaymentUseCase` 的状态校验、金额校验)。 +- **data 层**:repository 实现用单元测试,mock 掉 `Dio`(或用 dio 自带的 `DioAdapter`/假响应),验证请求参数拼装和响应解析是否正确,不发真实网络请求。 +- **presentation 层(Notifier)**:用 `ProviderContainer` + `overrides` 直接测试 `Notifier`/`AsyncNotifier` 的状态流转(见 [03-state-management.md](./03-state-management.md) 的测试示例),不需要启动完整 widget 树。 +- **Widget 测试**:只覆盖有实际业务分支的页面(比如列表的 loading/data/error 三态渲染是否正确),纯展示型 widget(无状态分支)不强制要求。 +- **集成测试**:只覆盖黄金路径(1-2 条最核心的用户旅程),跑在真实/模拟设备上,验证跨 feature 的路由跳转和端到端流程;不追求覆盖所有页面组合,避免集成测试维护成本超过收益。 +- 每个 `feature_*` 包的 `test/` 目录结构镜像 `lib/src/`(如 `test/domain/`、`test/data/`、`test/presentation/`),单元测试和 Widget 测试都通过 `melos run test`(见 [01-project-structure.md](./01-project-structure.md))统一跑;集成测试单独一个 CI job,不并入这条批量命令(跑得慢、需要设备/模拟器,不适合每次 `analyze`/`test` 都触发)。 + +## 参考链接 + +- [Flutter 官方测试文档](https://docs.flutter.dev/testing) +- [mocktail | Dart package](https://pub.dev/packages/mocktail) +- [integration_test 官方文档](https://docs.flutter.dev/testing/integration-tests) + +## 附录:分层怎么测,日常怎么写 + +给还没接触过这套测试分层习惯的同学看的入门说明。 + +### 为什么要分层测 + +不同层次的代码,"测试成本"和"能捕获的问题"是不对称的:domain 层的一条业务规则用纯 Dart 单元测试几毫秒就能跑完,覆盖所有分支;同样的规则如果只写在集成测试里验证,跑一次要几十秒甚至更久(要真的启动 App、走完整个页面流程),而且大部分时间花在跟这条业务规则无关的 UI 渲染上。**金字塔的意思是:能在下层用低成本测试覆盖的逻辑,就不要指望上层的少量集成测试兜底**——集成测试数量少,只用来确认"各层拼在一起没有断裂",不负责覆盖业务规则细节。 + +### 这不是 Flutter 独有的能力 + +原生 iOS([XCTest](https://developer.apple.com/documentation/xctest),2013 年至今)和 Android(JUnit + [Espresso](https://developer.android.com/training/testing/espresso)/[Robolectric](http://robolectric.org/))的单元测试、UI 自动化测试工具链其实比这里用的这套还要成熟。真正决定"业务逻辑好不好单独测"的是**架构**,不是工具:传统 MVC/MVP 项目里业务逻辑和 `ViewController`/`Activity` 强耦合(网络回调直接写在 `viewDidLoad`/`onCreate` 里),想测一条规则得连带整个页面生命周期一起启动测试环境,成本高、写起来别扭。domain 层纯 Dart、UI 状态与业务逻辑分离,本质是分层架构把业务逻辑从 UI 里解耦的结果——同样的分层思路(Clean Architecture + MVVM)搬到原生 iOS/Android 上,一样能达到这种测试体验。 + +### 单元测试示例:domain use case + +```dart +class MockPaymentRepository extends Mock implements PaymentRepository {} + +void main() { + late MockPaymentRepository repository; + late ConfirmPaymentUseCase useCase; + + setUp(() { + repository = MockPaymentRepository(); + useCase = ConfirmPaymentUseCase(repository); + }); + + test('订单状态非 pending 时应抛出 StateError', () async { + when(() => repository.fetchOrder('order1')).thenAnswer( + (_) async => PaymentOrder(orderId: 'order1', amountCents: 100, status: PaymentStatus.paid), + ); + + expect(() => useCase.call('order1', 'pin'), throwsA(isA())); + }); + + test('校验通过时应调用 confirmPayment', () async { + when(() => repository.fetchOrder('order1')).thenAnswer( + (_) async => PaymentOrder(orderId: 'order1', amountCents: 100, status: PaymentStatus.pending), + ); + when(() => repository.confirmPayment('order1', 'pin')).thenAnswer((_) async {}); + + await useCase.call('order1', 'pin'); + + verify(() => repository.confirmPayment('order1', 'pin')).called(1); + }); +} +``` + +### 单元测试示例:Riverpod Notifier + +```dart +void main() { + test('刷新失败时状态应变为 AsyncError', () async { + final repository = MockStoreRepository(); + when(() => repository.fetchNearbyStores(any(), any())) + .thenThrow(NetworkException('超时')); + + final container = ProviderContainer( + overrides: [storeRepositoryProvider.overrideWithValue(repository)], + ); + addTearDown(container.dispose); + + await container.read(storeListNotifierProvider.future).catchError((_) {}); + final state = container.read(storeListNotifierProvider); + + expect(state, isA()); + }); +} +``` + +### Widget 测试示例:门店列表三态 + +```dart +void main() { + testWidgets('加载失败时应展示错误文案', (tester) async { + final repository = MockStoreRepository(); + when(() => repository.fetchNearbyStores(any(), any())) + .thenThrow(NetworkException('网络异常')); + + await tester.pumpWidget(ProviderScope( + overrides: [storeRepositoryProvider.overrideWithValue(repository)], + child: const MaterialApp(home: StoreListPage()), + )); + await tester.pumpAndSettle(); + + expect(find.textContaining('加载失败'), findsOneWidget); + }); +} +``` + +### 集成测试示例:黄金路径骨架 + +```dart +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('登录 -> 浏览门店 -> 完成支付', (tester) async { + await tester.pumpWidget(const ProviderScope(child: App())); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key('login_username')), 'test_user'); + await tester.tap(find.byKey(const Key('login_submit'))); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('store_item_0'))); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('confirm_payment'))); + await tester.pumpAndSettle(); + + expect(find.text('支付成功'), findsOneWidget); + }); +} +``` + +集成测试用真实的(或半真实的、通过测试环境后端的)依赖跑通整条链路,不 mock 掉 repository——这条测试的意义就是验证各层真实拼接在一起没有问题,跟单元测试的定位互补而不是重复。 diff --git a/Architecture-Diagram/.gitignore b/Architecture-Diagram/.gitignore new file mode 100644 index 0000000..e8ba685 --- /dev/null +++ b/Architecture-Diagram/.gitignore @@ -0,0 +1,14 @@ +# agents +.agents +.codegrapg +.omo + + +# python +.venv +*/__pycache__/ + + +# drawio temp files +.$*.drawio.bkp + diff --git a/Architecture-Diagram/202606-Conti-Retail-APP-Component-data-source.md b/Architecture-Diagram/202606-Conti-Retail-APP-Component-data-source.md new file mode 100644 index 0000000..69ae8a3 --- /dev/null +++ b/Architecture-Diagram/202606-Conti-Retail-APP-Component-data-source.md @@ -0,0 +1,899 @@ +# 202606 Conti Retail APP Component data source + +## Continental Retail APP + +### 1 用户登录 + +**功能** + +用户登录功能,支持用户通过合法账号凭证进入APP系统,是APP所有功能的入口权限校验模块。 + +#### 1.1 手机验证码登录 + +**功能** + +基于手机号的验证码登录方式,用户输入手机号后获取短信验证码,通过验证码校验完成登录; + +**数据集** + +- 1. 第三方验证码; +- 2. 登录成功结果集; + +**来源** + +APP Backend + +**安全** + +HTTPS + +**备注** + +短信调用第三方短信网关 + +#### 1.2 账号密码登录 + +**功能** + +传统的账号+密码登录方式,用户输入已注册的账号和对应密码,通过系统校验后完成登录,; + +**数据集** + +1. 登录成功结果集; + +**来源** + +APP Backend + +**安全** + +HTTPS + +#### 1.3 用户协议 / 隐私政策 + +**功能** + +用户注册/登录前需查看并确认的用户服务协议与隐私政策,明确用户与平台的权利义务、个人信息使用规则等合规内容。 + +**数据集** + +1. 用户协议与隐私政策完整内容; + +**来源** + +APP Backend + +**安全** + +HTTPS + +### 2 APP首页及功能 + +**功能** + +APP的核心主页面,是用户登录后进入的首个页面,整合了核心功能入口、包括扫码、待办事项、动态预警、施工队列等核心数据展示;底部菜单会包含入库,采购,延保,个人中心等核心导航菜单。其所有展示信息均调用O2O后台相关接口; + +**前置任务** + +3.1 + +#### 2.1 首页展示 + +**功能** + +展示用户登录成功能的页面 + +**数据集** + +- 1.获取切换后的店铺信息集 +- 2.获取店铺列表结果集,用于店铺切换 +- 3.当前用户的菜单数据集 + +**来源** + +APP Backend + +**安全** + +HTTPS + +#### 2.2 首页展示 + +**功能** + +展示用户登录成功能的页面 + +**数据集** + +- 1.动态预警 +- 2.促销信息 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 2.3 店铺切换 + +**功能** + +点击左上角的店铺名称可以切换到另一个店铺 + +**数据集** + +1.获取切换后的店铺信息集 + +**来源** + +APP Backend + +**安全** + +HTTPS + +#### 2.4 扫一扫 + +**功能** + +基于F6的扫码功能,支持扫描VIN, 车牌,二维码/条形码,在弹窗选择不同功能后,实现快速跳转对应功能流程的快捷操作,页面直接嵌入F6的扫码页面;合并扫码接车,扫码核销,扫码入库,延保扫车牌等功能; + +**数据集** + +- 嵌入F6扫码页: +- 1. 验证可正常调用扫码功能; +- 2. 验证可正常识别VIN, 车牌,有效二维码/条形码,快速跳转对应功能/展示对应信息; + +**来源** + +Involve F6 Page + +**安全** + +HTTPS + +#### 2.5 公告/通知 + +**功能** + +平台向用户发布的官方公告、活动通知、系统通知、业务提醒等消息内容,支持用户查看、标记已读、删除等操作,是平台与用户的信息触达渠道,且促销活动通知需要在首页顶部滚动提示,数据接口来自于O2O后台; + +**数据集** + +1. 公告/通知列表,按发布时间倒序排列; + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 2.6 待办事项 + +**功能** + +集中展示用户需要处理的各类待办任务,包含订单待处理/待安装,活动报名问卷、待完善门店基本资料/渠道资料、待确认/待上传视频保单等业务待办,待办事项数据接口来自于O2O后台和延保后台; + +**数据集** + +1. 当前用户的所有待办事项,按优先级/截止时间排序; + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 2.7 动态预警 + +**功能** + +针对门店进货和O2O等业务场景的异常情况进行实时预警提醒,包含月度签约量达成预警,O2O补货率达成预警,O2O 时效订单预警等,帮助用户及时发现并处理业务风险;预警数据接口来自于ROOS和O2O后台; + +**数据集** + +1.当前用户的所有预警信息,按预警等级/时间排序; + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 2.8 店铺管理 + +**功能** + +门店相关信息的全生命周期管理模块,是门店运营的基础管理模块,以“马上下单”中的店铺管理为蓝板,数据接口调用“马上下单”后台店铺管理接口; + +**前置任务** + +3.1, 3.2 + +##### 2.8.1 基础信息 + +**功能** + +- 管理门店的基础信息,包含门店名称,门店住编码,负责人姓名,联系方式,门店地址,门店评分,门头照片等;支持查看,编辑,保存功能; +- 维护功能调用“马上下单”后台接口; + +**数据集** + +1.门店基础数据结果集; + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +##### 2.8.2 店铺服务信息 + +**功能** + +- 管理门店服务信息,包含门店基础信息,门店合作的线上线下渠道、分销渠道、合作平台信息,以及服务项目信息等;支持查看,编辑,保存功能; +- 维护功能调用“马上下单”后台接口; + +**数据集** + +1.门店服务信息数据结果集 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +##### 2.8.3 营业执照信息 + +**功能** + +维护门店营业执照,企业名称,统一社会信用代码等信息,调用“马上下单”后台接口 + +**数据集** + +1.营业执照信息结果集 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +##### 2.8.4 渠道信息 + +**功能** + +渠道信息,调用“马上下单”后台接口 + +**数据集** + +1.渠道信息结果集; + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +##### 2.8.5 经营范围 + +**功能** + +维护会员体系门店开通申请,认证轮胎技术检测中心开通申请,单独服务项目管理,以及开票方式等,调用“O2O”后台接口 + +**数据集** + +1.经营范围信息结果集; + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +##### 2.8.6 收款信息 + +**功能** + +O2O的平安账号模块,维护企业的银行账号信息,支持修改绑定的手机号及解绑银行卡功能;调用“O2O”后台接口 + +**数据集** + +1. 收款信息结果集 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +##### 2.8.7 人员管理 + +**功能** + +门店员工账号、角色权限全生命周期管理功能,支持员工账号的创建、编辑、权限分配、启停、删除的配置管理, 主要数据来源于马上下单后台接口 + +**数据集** + +- 1. 门店员工列表结果集; +- 2. 员工详细信息; + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +### 3 销售流程 + +**功能** + +门店线上线下销售、O2O运营、订单管理、库存管理、营销推广的全流程管理模块,包含O2O接单、库存查询、核销记录等核心功能,是门店销售运营的核心管理模块,所有销售运营数据接口来源于O2O后台。 + +**前置任务** + +3.1, 3.2 + +#### 3.1 准备 + +**功能** + +首页待办事项,消息公告 + +#### 3.2 客户查询 + +##### 3.2.1 首页扫码 + +**功能** + +首页扫车牌入口(要考虑未接F6的车辆如何处理?) + +**数据集** + +嵌入F6扫码页:F6扫码页面 + +**来源** + +Involve F6 Page + +**安全** + +HTTPS + +##### 3.2.2 历史工单记录 + +**功能** + +查看历史工单记录 + +**数据集** + +- 1.历史工单记录结果集 +- 2.单条工单详细信息 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +##### 3.2.3 新建工单 + +**功能** + +新建工单 + +**数据集** + +工单数据集(保存到O2O后台) + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +##### 3.2.4 销售商机 + +**功能** + +销售商机 + +**数据集** + +- 1.服务提醒记录集 +- 2.意向池 + +**来源** + +F6 Backend + +**安全** + +HTTPS + +##### 3.2.5 延保历史记录 + +**功能** + +延保历史记录 + +**数据集** + +- 1.延保历史记录集 +- 2. 单条延保详细信息集 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 3.3 报价开单 + +**功能** + +显示到店记录页(与3.2.1或3.2.2可能有冲突) + +**数据集** + +- 嵌入F6到店记录页: +- 1.到店记录页 +- 2.新车辆,完善信息 +- 3.车辆详情 +- 4.开单 + +**来源** + +Involve F6 Page + +**安全** + +HTTPS + +#### 3.4 施工查车 + +**功能** + +接上一步,开单后相关操作 + +**数据集** + +- 嵌入F6开单记录页: +- 5.施工查车,选择查车检测模板,记录查车异常结果,批量更新查车正常结果,查车报告发送车主(SMS短信,微信) +- 6.查车结果转工单, 查车结果转商机 + +**来源** + +Involve F6 Page + +**安全** + +HTTPS + +#### 3.5 结算 + +**功能** + +接上一步,工单完成后结算 + +**数据集** + +- 嵌入F6结算收银页: +- 7.添加其它项目 +- 8.结算收款 + +**来源** + +Involve F6 Page + +**安全** + +HTTPS + +### 结算 + +**功能** + +进入延保流程 + +**数据集** + +- 1.延保数据集 +- 2.延保出库,总数量减1 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 3.6 提醒 + +**功能** + +后台直接触发提醒 + +**数据集** + +F6后台自动按规则生成提醒单,生成提醒作业 + +#### 3.7 售后 + +##### 3.7.1 延保服务 + +**功能** + +延保服务 + +**数据集** + +- 1.延保服务信息集(保存到延保后台) +- 2.延保出库 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +##### 3.7.2 延保注册 + +**功能** + +延保注册 + +**数据集** + +延保注册数据集(保存到延保后台) + +**来源** + +User Input + +**安全** + +HTTPS + +### 4 采购流程 + +**功能** + +- 1.所有采购(马牌相关/其它非马牌配件)都应该能在App内完成。可以直接区分马牌/非马牌的入口 +- 2.针对非轮产品,需要有清晰的树状分类,方便出库与后续统计(可以参考开思/途虎) +- 3.下单前,同一个SKU下,可以显示不同DOT,不同仓库,不同价格 +- 4.打造供应链平台,引进三方询价整合(如开思) + +**前置任务** + +3.1, 3.2 + +#### 4.1 触发采购 + +**功能** + +平台可采购商品的全维度信息展示模块,包含商品名称、规格、型号、价格、库存、详情介绍、参数等信息,支持商品搜索、筛选、分类查看,是进货采购的基础商品信息模块。数据接口来源于ROOS后台接口。 + +**数据集** + +1. 当前用户购物车信息数据集 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 4.2 产品查询 + +**功能** + +展示门店订货促销/O2O售出平台促销/运营活动等的日历视图,按时间维度展示活动的开始/结束时间、活动类型、活动内容,支持用户快速查看活动详情、报名参与活动;活动数据接口待确定(待确定); + +**数据集** + +1. 促销日历数据集; + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 4.3 添加购物车 + +**功能** + +用户采购商品的临时存放与结算管理模块,支持商品加入购物车、数量修改、规格选择、商品删除、批量选择、结算下单等功能,是采购流程的核心中间环节。后台数据调用ROOS接口。 + +**数据集** + +1.购物车数据集(保存至ROOS后台) + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 4.4 结算 + +**功能** + +采购订单的结算确认模块,支持用户确认收货地址、商品信息、订单金额、优惠信息、支付方式,提交订单生成采购单,是采购流程的订单生成环节,后台接口调用O2O结算接口和F6接口。 + +**数据集** + +1.结算信息结果集 + +**来源** + +APP Backend + +**安全** + +证书令牌 + +**备注** + +需要调用三方接口,银行接口可能需要令牌或证书 + +#### 4.5 收货 + +**功能** + +门店商品的入库、出库、库存盘点、库存明细管理的功能模块,包含入库单、出库单的创建、审核、查看,库存明细查询,库存盘点,库存预警等功能,是门店库存管理的核心模块。数据会调用O2O的出入库接口。 + +**数据集** + +嵌入F6扫码入库页面 + +**来源** + +Involve F6 Page + +**安全** + +HTTPS + +#### 4.6 其它 + +**功能** + +采购订单列表和采购订单详情 + +**数据集** + +- 1.采购订单列表 +- 2.采购订单详情 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +### 5 经营分析 + +**功能** + +门店全业务线的经营数据统计与分析模块,包含采购数据、销售数据、O2O订单数据、核销收入、返利数据、库存数据、延保业务数据等全维度经营指标,支持多维度筛选、数据趋势分析、报表导出等功能,为门店经营决策提供数据支撑。 + +**前置任务** + +3.1, 3.2 + +#### 5.1 采购及返利对账单(马牌) + +**功能** + +针对马牌品牌的商品采购、返利金额的对账管理模块,包含采购明细、返利规则、返利金额计算、对账单生成、对账状态跟踪、差异处理等功能,是门店与品牌方采购返利对账的核心模块。它的数据来源于ROOS接口和F6接口。 + +**数据集** + +对账单 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 5.2 门店核销收入(马牌) + +**功能** + +针对马牌品牌相关商品、服务的门店核销收入的统计与管理模块,包含核销明细、收入金额、结算规则、结算周期、到账状态、收入明细查询、对账管理等功能,是门店马牌相关业务收入的核心管理模块,它的数据来源于ROOS接口。 + +**数据集** + +核销结果集 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +#### 5.3 返利中心 + +**功能** + +门店所有品牌、所有业务线的返利金额、返利规则、返利进度、返利提现的集中管理模块,包含返利余额展示、返利明细查询、返利规则查看、返利提现申请、提现进度跟踪等功能,是门店返利权益的核心管理模块,它的数据来源于ROOS接口。 + +**数据集** + +返利结果集 + +**来源** + +Mini Program Backend + +**安全** + +HTTPS + +### 6 个人中心 + +**功能** + +APP用户的个人信息、账号设置、功能入口的集中管理模块,包含个人信息维护、账号安全设置、收货地址管理、客服入口、消息通知、系统设置、退出登录等核心功能,是用户个人账号与系统设置的核心管理模块 + +**前置任务** + +3.1, 3.2 + +#### 6.1 收货地址 + +**功能** + +用户采购商品、O2O服务的收货/服务地址的管理模块,支持地址的新增、编辑、删除、设为默认地址、地址搜索等功能,是订单结算、服务下单的基础地址配置模块,它的数据来源于马上下单接口(待确认)。 + +**数据集** + +- 1. 验证可正常加载展示用户已保存的收货地址列表,按使用时间/默认状态排序; +- 2. 验证支持新增收货地址,可正常填写收货人、联系电话、所在地区、详细地址等信息,提交后可正常保存; +- 3. 验证支持对已有地址进行编辑、删除、设为默认地址操作,操作后状态可正常更新; +- 4. 验证订单结算、服务下单时可正常选择已保存的收货地址,默认地址可自动选中; +- 5. 验证地址信息可正常同步至订单、物流模块,地址数据准确无错误 + +**来源** + +APP Backend + +**安全** + +HTTPS + +#### 6.2 服务热线 + +**功能** + +平台官方服务热线、客服电话的展示与快速拨打功能模块,展示不同业务线的服务热线、服务时间、服务范围,支持一键拨打热线电话,是用户快速联系官方客服的渠道。 + +**数据集** + +- 1. 验证可正常加载展示所有业务线的服务热线列表,包含热线号码、服务时间、服务范围、服务类型;? +- 2. 验证点击热线号码可正常调起手机拨号界面,一键拨打对应热线电话; +- 3. 验证服务热线的服务时间、状态可实时更新,非服务时间有明确的提示; +- 4. 验证支持按业务类型搜索筛选服务热线,可快速定位目标热线; +- 5. 验证热线相关的常见问题、服务说明可正常展示,内容清晰准确 + +**来源** + +APP Backend + +**安全** + +HTTPS + +#### 6.3 经销商客服 + +**功能** + +对应经销商的专属客服联系、咨询、服务功能模块,展示经销商客服的联系方式、服务时间、服务范围,支持在线咨询、留言反馈、问题提交等功能,是用户对接经销商专属服务的渠道。 + +**数据集** + +- 1. 验证可正常加载展示对应经销商的客服信息,包含客服名称、联系方式、服务时间、服务范围; +- 2. 验证支持在线咨询功能,可正常发送文字、图片、文件等咨询内容,客服回复可实时接收; +- 3. 验证支持问题反馈、留言提交功能,可正常填写问题描述、上传相关材料,提交后可正常查看处理进度; +- 4. 验证客服服务时间、状态可实时更新,非服务时间有明确的提示; +- 5. 验证历史咨询、反馈记录可正常查询、查看,数据准确无遗漏 + +**来源** + +APP Backend + +**安全** + +HTTPS + +#### 6.4 O2O /延保客服 (企微) + +**功能** + +O2O业务、延保业务的专属企业微信客服功能模块,支持一键添加企业微信客服、在线咨询、问题反馈、服务跟进等功能,是用户对接O2O与延保业务专属服务的渠道,它的数据来源于企业微信接口(待确认)。 + +**数据集** + +- 1. 验证可正常加载展示O2O/延保业务的企业微信客服信息,包含客服名称、企业微信二维码、服务时间、服务范围; +- 2. 验证可正常扫码/一键添加对应企业微信客服,添加后可正常发起在线咨询; +- 3. 验证支持在线咨询功能,可正常发送文字、图片、文件、订单等内容,客服回复可实时接收; +- 4. 验证支持问题反馈、服务单提交功能,可正常填写问题描述、上传相关材料,提交后可正常查看处理进度; +- 5. 验证历史咨询、服务记录可正常查询、查看,客服服务状态可实时更新 + +**来源** + +APP Backend + +**安全** + +HTTPS + +#### 6.5 退出登录 + +**功能** + +用户账号的安全退出功能,支持用户主动退出当前登录的账号,清除当前设备的登录状态,退出后需重新登录才可访问需登录的功能,保障用户账号安全。 + +**数据集** + +- 1. 验证在个人中心可正常点击退出登录按钮,弹出确认退出的提示框; +- 2. 验证确认退出后,可正常清除当前设备的登录状态,自动跳转至登录页面; +- 3. 验证退出登录后,无法访问需登录的功能页面,点击相关功能会自动跳转至登录页面; +- 4. 验证退出登录后,重新打开APP不会自动登录,需手动输入账号凭证登录; +- 5. 验证多设备登录时,单设备退出登录不影响其他设备的登录状态 + +**来源** + +APP Backend + +**安全** + +HTTPS diff --git a/Architecture-Diagram/202606-Continental-Retail-APP-PRD.md b/Architecture-Diagram/202606-Continental-Retail-APP-PRD.md new file mode 100644 index 0000000..37868c1 --- /dev/null +++ b/Architecture-Diagram/202606-Continental-Retail-APP-PRD.md @@ -0,0 +1,1524 @@ +# 202606 Continental Retail APP PRD + +| 项目 | Continental Retail APP | +| --- | --- | +| 文档类型 | 产品需求文档 PRD | +| 版本 | v2.0 重构版 | +| 日期 | 2026-06 | +| 文档定位 | 研发交付级需求文档 | +| 适用对象 | 产品、业务、架构、移动端、后端、集成、测试、项目管理 | +| 主技术路线 | React Native | +| 备选技术路线 | Flutter | +| 参考资料 | `202606 Conti Retail APP Component data source.md`、`零售商系统方案研讨会PPT.retro.md`、`architecture-diagram.drawio` | + +## 1. 文档说明 + +本文档用于定义 Continental Retail APP 定档版本的完整产品需求、系统边界、集成方式、业务流程、异常处理、权限要求和验收标准,目标是让研发、测试、集成和项目管理团队在不依赖口头补充说明的情况下理解系统如何建设、业务如何流转、不同服务如何协同。 + +本文档不是: + +- UI 视觉稿。 +- 数据库设计文档。 +- 接口定义文档。 +- 技术方案详设文档。 + +本文档必须达到以下深度: + +- 可以作为研发任务拆解基础。 +- 可以作为接口联调边界依据。 +- 可以作为测试用例编写基础。 +- 可以作为业务评审、架构评审和项目排期基线。 + +## 2. 项目背景 + +Continental Retail APP 是面向零售商门店和一线员工的一体化移动工作平台。项目目标不是简单把历史小程序搬到APP,而是建立一个统一移动入口,将门店经营过程中分散在多个后台和供应商系统里的能力重新组织成可落地、可扩展、可管控的业务平台。 + +当前业务环境主要由三类后台能力构成: + +- `App Backend`:APP主后台,位于中间主域,负责账号、门店、权限、配置、统一聚合和业务编排。 +- `Mini Program Backend`:多个历史小程序后台域,仍然保留独立后台边界,承担 O2O、延保、马上下单、ROOS/返利等既有业务能力。 +- `F6 Supplier Domain`:供应商提供的采购、促销、ERP及部分销售施工相关能力,既包含 H5 页面,也包含能力接口。 + +当前业务存在的核心问题包括: + +- 门店日常经营依赖多个小程序和多个后台,入口分散,角色切换成本高。 +- 销售、开单、施工、结算、入库等关键流程需要 F6 能力,但移动端接入方式不统一。 +- 采购、库存、返利、核销、延保、客服、消息等业务来自不同后台,数据来源和访问链路混杂。 +- 总分店、多门店、门店角色、员工权限、门店资料等基础能力缺乏统一中心。 +- 历史小程序后台仍需继续复用,无法在短期内完全替换。 +- F6 是外部供应商能力域,移动端不应将其视为主后台系统,必须通过主后台进行主流程控制和集成隔离。 + +因此,本项目需要重新定义统一访问架构、服务边界和产品流程,使 Mobile App 成为唯一统一入口,App Backend 成为主业务编排中心,Mini Program Backend 作为独立业务域继续承载既有业务,F6 作为供应商能力域通过标准化方式被接入。 + +## 3. 产品目标 + +### 3.1 业务目标 + +- 建立零售商统一移动入口,承接门店员工高频日常作业。 +- 统一账号、门店、角色、菜单、待办、通知和配置管理。 +- 将历史小程序后台能力整合到APP工作台中,减少多入口切换。 +- 将 F6 的采购、促销、ERP和相关 H5 页面能力纳入 APP 业务闭环。 +- 通过 App Backend 实现统一身份、统一权限、统一链路治理和统一集成策略。 +- 提升销售、采购、库存、延保、返利和门店管理的执行效率与可视化水平。 + +### 3.2 用户目标 + +- 一线员工用一个APP完成登录、扫码、开单、施工、采购、入库、售后和客服咨询。 +- 店长可以在APP内维护门店资料、员工权限、库存和经营数据。 +- 多门店用户可以统一切换门店并获得对应上下文数据。 +- 平台可以在首页集中下发公告、待办、预警和活动信息。 + +### 3.3 成功标准 + +- 用户登录后可在一个APP中完成核心工作,不需要频繁切换小程序。 +- APP 内主业务访问链路统一收敛到 App Backend。 +- F6 供应商能力成功接入,且只在 H5 嵌入场景对移动端开放直接访问。 +- 关键模块具备完整业务流程、异常提示、权限校验和测试标准。 +- 研发可以基于本文档直接拆解需求并发起接口联调。 + +## 4. 术语表 + +| 术语 | 说明 | +| --- | --- | +| Mobile App | Continental Retail APP 移动端,采用 React Native 实现 | +| App Backend | APP主后台,中间主域,统一 BFF/聚合/编排服务 | +| Mini Program Backend | 历史小程序后台集合,在PRD中按独立服务表达 | +| O2O Backend | O2O 订单、核销、门店业务相关后台 | +| Warranty Backend | 延保业务后台 | +| Order Backend | “马上下单”相关门店、人员、地址、资料能力后台 | +| ROOS Backend | 返利、采购、核销、经营分析相关后台 | +| F6 Supplier Domain | 供应商管理域,提供采购、促销、ERP及相关页面/接口能力 | +| F6 Web Portal / H5 | F6 提供的嵌入式 H5 页面能力 | +| F6 Capability APIs | F6 提供的后端接口能力 | +| F6 Integration Adapter | App Backend 内用于接入 F6 的集成适配层 | +| Embedded H5 Access | Mobile App 直接嵌入访问 F6 H5 页面 | +| Source of Truth | 某业务数据的最终归属和主数据拥有方 | +| Store Context | 当前门店上下文,包括门店ID、组织、角色、权限范围 | + +## 5. 角色与使用场景 + +### 5.1 角色定义 + +| 角色 | 核心职责 | 常用功能 | +| --- | --- | --- | +| 店长 | 门店经营、人员、库存、数据管理 | 门店切换、店铺管理、人员管理、采购、经营分析、财务、返利 | +| 前台客服 | 客户接待、查询、登记 | 登录、扫码、客户查询、历史工单、新建工单、待办、公告 | +| 收银 | 收款、核销、订单结算 | 结算、收款、订单详情、核销收入、延保出库 | +| 维修技师 | 施工检测和查车 | 查车模板、异常记录、拍照、报告发送、完工 | +| 美容技师 | 美容服务执行 | 施工任务、项目处理、完工确认 | +| 市场专员 | 活动和营销管理 | 促销信息、CRM任务、客户触达、活动日历 | +| 经销商管理员 | 多门店和业务监督 | 门店切换、经营汇总、员工权限、客服入口 | +| 平台运营 | 触达和内容运营 | 公告、问卷、活动、待办配置、提醒配置 | + +### 5.2 典型使用场景 + +- 门店员工每天打开APP,查看待办、预警、公告,并快速进入扫码、开单或采购流程。 +- 店长在APP内切换门店,补全门店资料、调整员工权限并查看经营表现。 +- 前台或技师通过扫描 VIN 或车牌进入客户查询、到店记录、报价开单、施工查车和结算。 +- 采购人员在APP内查询商品、查看促销、下单采购、确认收货并完成入库。 +- 财务或店长查看返利、核销收入和门店经营报表。 + +## 6. 系统架构与服务边界 + +### 6.1 总体架构原则 + +系统架构以 `architecture-diagram.drawio` 为准,遵循以下原则: + +- `Mobile App -> App Backend` 是唯一主访问链路。 +- `App Backend` 是 APP 的主后台与中间主域。 +- `Mini Program Backend` 仍然是独立后台域,不并入 App Backend。 +- `F6` 是供应商能力域,不被定义为主后台系统。 +- `App Backend -> F6` 通过 `F6 Integration Adapter` 发起 B2B allowlisted 访问。 +- `Mobile App -> F6` 只保留 `Embedded H5 Access`,仅用于嵌入F6页面。 +- 历史 Mini 后台能力逻辑上优先经 App Backend 聚合;若短期存在历史直连,则按兼容链路表达,不作为主链路。 + +### 6.2 系统域定义 + +#### 6.2.1 Mobile App + +Mobile App 是用户唯一可见的统一移动入口,负责: + +- 登录页、首页、个人中心和原生模块页面承载。 +- RN 容器、WebView 容器、扫码、相机、相册、拨号、文件上传等原生能力调度。 +- 本地登录态管理、本地缓存、权限申请和设备级提示。 +- 接收 App Backend 返回的菜单、权限、配置和业务数据。 +- 嵌入并管理 F6 H5 页面生命周期。 + +#### 6.2.2 App Backend + +App Backend 位于主业务域,是整个 APP 的主后台和统一编排中心,负责: + +- 用户认证、Token签发、刷新和退出。 +- 用户、门店、角色、菜单、配置、权限统一管理。 +- 首页聚合服务。 +- 客服、协议、地址、消息、系统配置等 APP 基础能力。 +- 统一错误码、统一日志、统一审计、统一安全控制。 +- 对 Mini Program Backend 的数据聚合、字段标准化和兼容封装。 +- 对 F6 进行换票、能力调用、鉴权和供应商链路治理。 + +#### 6.2.3 Mini Program Backend 独立后台域 + +Mini Program Backend 仍然保留为独立后台域。PRD 内按业务服务拆分如下: + +| 服务 | 业务责任 | +| --- | --- | +| O2O Backend | O2O 订单、消息、待办、核销、门店业务相关能力 | +| Warranty Backend | 延保服务、延保注册、延保历史、延保出库 | +| Order Backend | 马上下单相关门店资料、人员、地址、门店管理 | +| ROOS Backend | 采购辅助、返利、核销收入、经营分析、部分商品能力 | +| Shared Mini Capabilities | 公告、促销、活动、问卷等历史渠道能力,按实际归属服务承接 | + +这些服务的特点: + +- 它们仍然是独立运行的后台服务,不被 App Backend 替代。 +- 它们可以是数据主来源,但移动端不应再把它们视为第一入口。 +- App Backend 对其进行统一适配和聚合。 +- 某些历史能力在过渡期允许保留兼容直连。 + +#### 6.2.4 F6 Supplier Domain + +F6 是供应商能力域,不属于 APP 主系统。F6 对外提供两类能力: + +- `F6 Web Portal / H5 Pages` +- `F6 Capability APIs` + +F6 在本项目中的定位: + +- 提供采购、促销、ERP相关能力。 +- 提供部分门店销售/施工/结算/扫码入库相关 H5 页面能力。 +- 作为供应商能力域被接入,而不是被当成 APP 的主后台。 +- 移动端只在嵌入 H5 时直接访问 F6。 +- 所有非 H5 主链路能力原则上通过 App Backend 和 F6 Integration Adapter 访问。 + +#### 6.2.5 F6 Integration Adapter + +F6 Integration Adapter 是 App Backend 侧的集成适配组件,负责: + +- F6 B2B allowlisted 服务访问。 +- F6 鉴权、票据换取、签名、Header组装。 +- 字段映射、状态映射、错误码转换。 +- 超时重试、熔断、日志追踪、供应商异常隔离。 +- 业务编排时对 F6 API 的统一出口控制。 + +### 6.3 主访问链路 + +#### 6.3.1 主链路 + +主链路定义为: + +`Mobile App -> App Backend` + +以下功能默认必须走主链路: + +- 登录认证。 +- 用户、门店、角色、菜单。 +- 首页工作台。 +- 用户协议、隐私政策。 +- 地址、客服、系统配置。 +- 对 Mini 服务的聚合展示。 +- 对 F6 页面入口的签发与上下文准备。 + +#### 6.3.2 Mini Program Backend 兼容链路 + +对于历史 Mini 服务,系统采用: + +`主链路 App BFF + 保留少量兼容直连` + +即: + +- 新设计或重构后的移动端能力优先由 App Backend 聚合暴露。 +- 历史上需要直接连接 Mini Program Backend 的能力允许保留,但必须在模块中明确标注。 +- 兼容直连不是推荐架构,不可在新模块中无约束扩散。 + +#### 6.3.3 F6 H5 链路 + +嵌入 F6 页面时,链路为: + +`Mobile App -> Embedded H5 Access -> F6 Web Portal / H5 Pages` + +此链路仅适用于: + +- 扫码页。 +- 到店记录页。 +- 报价开单页。 +- 施工查车页。 +- 结算收银页。 +- 扫码入库页。 +- 与采购、促销、ERP直接相关的 F6 页面。 + +#### 6.3.4 F6 API 链路 + +需要由主后台调用供应商能力时,链路为: + +`Mobile App -> App Backend -> Business Service Orchestration -> F6 Integration Adapter -> F6 Capability APIs` + +适用于: + +- 获取H5票据或免登参数。 +- 获取采购、促销、ERP相关补充能力。 +- 供应商侧需要主后台协同的查询或写入场景。 + +### 6.4 数据与上下文治理 + +#### 6.4.1 用户上下文 + +用户上下文至少包括: + +- 用户ID。 +- 员工ID。 +- 手机号。 +- 角色编码。 +- 权限集。 +- 渠道标识。 +- 登录态 Token。 + +#### 6.4.2 门店上下文 + +门店上下文至少包括: + +- 当前门店ID。 +- 门店编码。 +- 组织ID。 +- 所属总店/分店关系。 +- 门店角色范围。 +- 当前门店可访问菜单。 + +#### 6.4.3 Source of Truth 原则 + +同一业务必须定义主数据归属,不允许前端混用多个返回结果作为最终口径。PRD中每个功能需明确: + +- 运行时访问链路。 +- 逻辑数据来源。 +- 最终保存/回写目标。 +- 状态判定主来源。 + +## 7. Embedded H5 接入规范 + +### 7.1 适用范围 + +Embedded H5 仅用于承载 F6 页面,不扩展为通用任意外链容器。 + +### 7.2 H5 入口流程 + +1. 用户在APP中点击某个需要F6页面的功能入口。 +2. APP调用 App Backend 获取该页面的 H5 启动信息。 +3. App Backend 判断用户登录态、门店上下文、角色权限是否合法。 +4. App Backend 通过 F6 Integration Adapter 获取 F6 访问票据、签名或临时访问参数。 +5. App Backend 返回最终 H5 访问 URL 和必要上下文。 +6. APP 使用 WebView 打开目标 H5 页面。 + +### 7.3 H5 启动上下文 + +H5 启动上下文至少包含: + +- `ticket` 或等效一次性票据。 +- `userId` +- `employeeId` +- `storeId` +- `storeCode` +- `orgId` +- `roleCode` +- `source=app` +- `targetPage` +- `traceId` + +### 7.4 H5 与 App 的桥接能力 + +App 必须为 F6 H5 提供以下桥接能力: + +- 打开扫码。 +- 打开相机。 +- 打开相册。 +- 上传图片/文件。 +- 调起拨号。 +- 关闭当前页。 +- 返回上一页。 +- 刷新页面。 +- 获取当前登录态或票据刷新结果。 +- 获取当前门店上下文。 +- 弹出 Toast、Dialog、Loading。 +- 跳转到 APP 原生页面。 + +### 7.5 H5 生命周期管理 + +APP 必须处理: + +- 页面标题同步。 +- 返回按钮与关闭按钮。 +- 首次加载与二次进入缓存策略。 +- Token 过期后的重新换票。 +- 页面白屏、超时、网络失败兜底。 +- H5 上传中断和重新提交提示。 +- 门店切换后已打开 H5 页面是否允许继续使用。 + +默认策略: + +- 门店切换后,当前 H5 页面必须失效并提示用户重新进入。 +- 用户退出登录后,所有 H5 会话必须同步失效。 + +### 7.6 H5 安全要求 + +- 仅允许白名单域名打开。 +- 所有 H5 访问使用 HTTPS/TLS。 +- H5 页面不得直接保存 APP 明文 Token。 +- 供应商错误信息需转换为用户可理解提示。 +- H5 与 APP 之间的桥接事件必须有来源校验。 + +## 8. 功能需求总则 + +### 8.1 统一描述模板 + +本PRD所有功能模块统一使用以下描述维度: + +- 业务目标 +- 目标角色 +- 入口 +- 前置条件 +- 页面内容 +- 主流程 +- 异常流程 +- 业务规则 +- 权限规则 +- 访问链路 +- 逻辑数据来源 +- 回写目标 +- 状态变化 +- 验收标准 + +### 8.2 统一交互规则 + +- 所有按钮操作必须有明确提交结果反馈。 +- 提交中必须防重复点击。 +- 网络失败必须有错误提示和重试能力。 +- 高风险操作需要二次确认。 +- 表单类页面必须定义保存成功、保存失败和离开未保存提醒。 + +### 8.3 统一权限规则 + +- 未登录用户不可访问需登录功能。 +- 无门店权限用户不可进入业务首页。 +- 菜单显示由 App Backend 返回的权限控制。 +- 敏感能力需叠加角色权限校验。 +- H5 页面不可绕过 App Backend 直接获得业务权限。 + +## 9. 模块访问矩阵 + +| 模块 | 主访问路径 | 逻辑数据来源 | 编排责任方 | 回写目标 | +| --- | --- | --- | --- | --- | +| 登录与协议 | App -> App Backend | App Backend | App Backend | App Backend | +| 首页工作台 | App -> App Backend | App Backend + O2O + ROOS + Warranty + 配置服务 | App Backend | 各原业务后台 | +| 门店切换 | App -> App Backend | App Backend | App Backend | App Backend | +| 店铺管理 | App -> App Backend | Order Backend / O2O / App Backend | App Backend | 对应 Mini 服务 | +| 人员管理 | App -> App Backend | Order Backend | App Backend | Order Backend | +| 扫一扫 | App -> Embedded F6 H5 | F6 H5 / F6 API | App Backend + F6 H5 | F6 或下游业务系统 | +| 客户查询 | App -> App Backend 或 Embedded F6 H5 | F6 / O2O / Warranty | App Backend / F6 H5 | F6 / O2O / Warranty | +| 报价开单 | App -> Embedded F6 H5 | F6 | F6 H5 | F6 | +| 施工查车 | App -> Embedded F6 H5 | F6 | F6 H5 | F6 | +| 结算收银 | App -> Embedded F6 H5 | F6 + Warranty | F6 H5 + App Backend | F6 / Warranty | +| 提醒 | App -> App Backend | F6 / O2O / 短信服务 | App Backend | F6 / O2O | +| 延保服务 | App -> App Backend | Warranty Backend | App Backend | Warranty Backend | +| 采购商品/促销 | App -> App Backend / Embedded F6 H5 | F6 + ROOS + 活动服务 | App Backend / F6 H5 | F6 / ROOS | +| 购物车/结算 | App -> App Backend | F6 / ROOS / O2O | App Backend | F6 / O2O / ROOS | +| 收货/扫码入库 | App -> Embedded F6 H5 | F6 | F6 H5 | F6 | +| 采购订单 | App -> App Backend | F6 / ROOS / O2O | App Backend | F6 / O2O | +| 库存管理 | App -> App Backend / Embedded F6 H5 | F6 / O2O / ROOS | App Backend / F6 H5 | F6 / O2O | +| 经营分析 | App -> App Backend | ROOS / O2O / F6 / Warranty | App Backend | 只读 | +| CRM | App -> App Backend | F6 / O2O / 营销服务 | App Backend | 对应营销服务 | +| 财务 | App -> App Backend | F6 / O2O / ROOS | App Backend | 对应财务系统 | +| 个人中心 | App -> App Backend | App Backend / Order Backend | App Backend | 对应后台 | + +## 10. 登录与合规 + +### 10.1 手机验证码登录 + +**业务目标** +支持用户通过手机号和验证码登录 APP,并在登录完成后建立用户、门店、角色和菜单上下文。 + +**目标角色** +所有用户。 + +**入口** +APP 启动页默认进入登录页。 + +**前置条件** + +- 用户已在后台存在可登录账号。 +- 用户手机号已被业务系统绑定。 + +**页面内容** + +- 手机号输入框。 +- 验证码输入框。 +- 获取验证码按钮。 +- 登录按钮。 +- 用户协议与隐私政策勾选项。 + +**主流程** + +1. 用户输入手机号。 +2. 点击获取验证码。 +3. APP 调用 App Backend 请求下发验证码。 +4. App Backend 调用短信服务发送验证码。 +5. 用户输入验证码并点击登录。 +6. App Backend 校验验证码、账号状态和门店权限。 +7. 登录成功后返回 Token、用户资料、门店列表、默认门店、菜单权限、协议状态。 +8. APP 保存登录态并进入首页。 + +**异常流程** + +- 手机号格式错误。 +- 验证码发送失败。 +- 验证码错误或过期。 +- 用户被禁用。 +- 用户无门店权限。 +- 协议未勾选。 + +**业务规则** + +- 验证码发送需限频。 +- 同一手机号连续失败超过阈值时触发风控。 +- 登录成功后必须立即获取门店上下文。 + +**权限规则** + +- 未勾选协议不可继续登录。 + +**访问链路** +App -> App Backend -> 短信服务。 + +**逻辑数据来源** +App Backend。 + +**回写目标** +App Backend。 + +**状态变化** + +- 未登录 -> 验证中 -> 登录成功 / 登录失败。 + +**验收标准** + +- 合法手机号可获取验证码。 +- 正确验证码可登录。 +- 登录成功后能拿到门店和菜单。 +- 错误验证码不能登录并有明确提示。 + +### 10.2 账号密码登录 + +**业务目标** +支持传统账号密码登录。 + +**页面内容** + +- 账号输入框。 +- 密码输入框。 +- 显示/隐藏密码。 +- 登录按钮。 + +**主流程** + +1. 用户输入账号和密码。 +2. App Backend 校验账号、密码和状态。 +3. 登录成功后返回统一登录上下文。 +4. APP 进入首页。 + +**异常流程** + +- 账号不存在。 +- 密码错误。 +- 账号锁定。 +- 无门店权限。 + +**访问链路** +App -> App Backend。 + +**逻辑数据来源** +App Backend。 + +**回写目标** +App Backend。 + +**验收标准** + +- 正确凭据可以登录。 +- 错误凭据不可登录。 + +### 10.3 用户协议与隐私政策 + +**业务目标** +满足合规要求,告知用户 APP 对手机号、门店信息、相机、相册、扫码、拨号等权限的使用目的。 + +**业务规则** + +- 首次登录必须确认协议。 +- 协议内容从 App Backend 下发。 +- 协议版本升级后可要求重新确认。 + +### 10.4 退出登录 + +**业务目标** +清理本地登录态并失效当前 H5 会话。 + +**业务规则** + +- 清理 Token、门店上下文、本地用户信息和缓存。 +- 关闭所有已打开的 F6 H5 会话。 +- 退出后返回登录页。 + +## 11. 首页工作台 + +### 11.1 模块目标 + +首页是 APP 的核心工作台,负责展示当前用户、当前门店和当前角色下最重要的业务入口和实时信息。 + +### 11.2 首页组成 + +- 当前门店信息。 +- 门店切换入口。 +- 当前角色菜单。 +- 扫一扫快捷入口。 +- 待办事项。 +- 动态预警。 +- 公告/通知。 +- 促销信息。 +- 施工队列。 +- 快捷入口区。 + +### 11.3 首页展示 + +**业务目标** +在一个页面中聚合用户需要优先关注的任务、异常和高频动作。 + +**目标角色** +所有登录用户。 + +**入口** +登录成功默认进入。 + +**前置条件** + +- 已完成登录。 +- 已建立默认门店上下文。 + +**页面内容** + +- 顶部:门店名称、门店切换、消息入口。 +- 中部:动态菜单、扫码、待办、预警、公告、促销。 +- 底部:Tab 导航。 + +**主流程** + +1. APP 进入首页。 +2. APP 调用 App Backend 获取首页聚合数据。 +3. App Backend 获取当前门店、菜单、用户权限。 +4. App Backend 聚合 O2O、Warranty、ROOS、公告和预警数据。 +5. 返回首页展示数据。 + +**异常流程** + +- 某个子模块接口失败时仅该模块展示异常态。 +- 门店上下文缺失时引导重新选择门店。 + +**业务规则** + +- 首页展示内容必须与当前门店绑定。 +- 菜单由权限控制,不可前端写死。 +- 高优先级待办和高等级预警置顶。 + +**访问链路** +App -> App Backend。 + +**逻辑数据来源** +App Backend + O2O Backend + Warranty Backend + ROOS Backend。 + +**回写目标** +各业务后台。 + +**验收标准** + +- 首页能展示门店、菜单、待办、预警、公告和促销。 +- 任意单个模块失败不导致整页不可用。 + +### 11.4 门店切换 + +**业务目标** +为多门店用户切换当前业务上下文。 + +**主流程** + +1. 用户点击门店名称。 +2. APP 调用 App Backend 获取门店列表。 +3. 用户选择目标门店。 +4. App Backend 返回切换成功后的门店上下文。 +5. APP 刷新菜单、首页数据和相关缓存。 +6. 如当前存在打开的 F6 H5 页面,强制失效并提示重新进入。 + +**业务规则** + +- 当前门店影响所有业务数据。 +- 门店切换后,购物车、待办、预警、订单和 H5 页面上下文必须同步切换。 + +### 11.5 扫一扫 + +**业务目标** +提供统一扫码入口,支持 VIN、车牌、二维码和条形码识别,并将用户带入正确的后续业务。 + +**目标角色** +前台客服、店长、收银、技师等。 + +**入口** + +- 首页快捷入口。 +- 某些业务页的二级扫码按钮。 + +**前置条件** + +- 当前用户有扫码权限。 +- 已授权相机权限。 + +**页面内容** + +- F6 嵌入扫码页。 +- 识别结果提示。 +- 功能选择弹窗。 + +**主流程** + +1. 用户点击扫一扫。 +2. APP 向 App Backend 请求 F6 扫码页访问参数。 +3. APP 打开 Embedded F6 H5 扫码页。 +4. 用户扫描 VIN、车牌或二维码。 +5. H5 返回识别结果并触发对应业务动作。 +6. 根据结果进入客户查询、核销、入库、延保或其他流程。 + +**异常流程** + +- 相机未授权。 +- H5 加载失败。 +- 识别失败。 +- 识别结果没有对应业务上下文。 +- 未接入 F6 的车辆无法直接匹配。 + +**业务规则** + +- 对未接入 F6 的车辆,默认进入车辆信息补录或新建工单兜底流程。 +- 识别结果必须和当前门店上下文绑定。 + +**访问链路** +App -> App Backend -> Embedded F6 H5。 + +**逻辑数据来源** +F6 H5 / F6 API。 + +**回写目标** +F6 或下游业务系统。 + +**验收标准** + +- 可识别 VIN、车牌、二维码、条形码。 +- 可根据识别结果进入对应流程。 +- 未识别或无权限时有清晰提示。 + +### 11.6 公告/通知 + +**业务目标** +统一触达官方公告、系统通知、业务提醒和促销内容。 + +**业务规则** + +- 列表按发布时间倒序。 +- 促销类通知可在首页滚动。 +- 支持已读状态。 +- 是否支持删除由后台能力决定。 + +### 11.7 待办事项 + +**待办范围** + +- O2O 订单接单提醒。 +- 待安装订单。 +- 延保视频上传提醒。 +- 问卷提醒。 +- 门店资料完善提醒。 +- 渠道资料提醒。 +- 支付提醒。 + +**业务规则** + +- 待办按优先级和时效排序。 +- 点击待办跳转对应处理页。 + +### 11.8 动态预警 + +**预警范围** + +- 月度签约量预警。 +- O2O 补货率预警。 +- O2O 时效订单预警。 +- 库存预警。 +- 缺货提醒。 +- 滞销提醒。 + +**业务规则** + +- 预警按等级和时间排序。 +- 支持跳转处理页面。 + +## 12. 店铺管理 + +### 12.1 模块目标 + +店铺管理用于维护门店基础资料、经营资料、人员权限和收款信息,是门店基础管理中心。 + +### 12.2 访问定义 + +- 主访问路径:App -> App Backend +- 逻辑数据来源:Order Backend / O2O Backend +- 编排责任:App Backend +- 回写目标:对应 Mini 服务 + +### 12.3 基础信息 + +**页面内容** + +- 门店名称 +- 门店编码 +- 负责人姓名 +- 联系方式 +- 门店地址 +- 门店评分 +- 门头照片 + +**业务规则** + +- 支持查看、编辑、保存。 +- 无编辑权限用户只读。 +- 门头照片支持上传和替换。 +- 资料缺失时产生待办。 + +### 12.4 店铺服务信息 + +**页面内容** + +- 合作线上渠道 +- 合作线下渠道 +- 分销渠道 +- 合作平台 +- 服务项目 + +**业务规则** + +- 修改后保存到对应 Order Backend 服务。 + +### 12.5 营业执照信息 + +**页面内容** + +- 企业名称 +- 社会信用代码 +- 营业执照图片 +- 有效期 + +**业务规则** + +- 证照缺失或过期生成提醒。 + +### 12.6 渠道信息 + +- 展示渠道类别、状态、绑定关系。 +- 支持编辑保存。 + +### 12.7 经营范围 + +- 会员体系开通申请。 +- 认证轮胎技术检测中心申请。 +- 服务项目管理。 +- 开票方式维护。 + +### 12.8 收款信息 + +**业务规则** + +- 展示脱敏银行信息。 +- 修改手机号、解绑银行卡需二次确认。 +- 仅高权限角色可见。 + +### 12.9 人员管理 + +**页面内容** + +- 员工列表 +- 员工详情 +- 角色分配 +- 启停状态 + +**主流程** + +1. 店长进入人员管理。 +2. 查看员工列表。 +3. 创建或编辑员工。 +4. 分配角色和权限。 +5. 保存并同步后台。 + +**业务规则** + +- 停用后员工不可登录。 +- 权限变更后重新登录生效。 + +## 13. 销售流程 + +### 13.1 模块目标 + +销售流程覆盖客户到店前后的完整服务链路,包括准备、客户查询、报价开单、施工查车、结算、提醒和售后。 + +### 13.2 流程总览 + +1. 首页进入销售准备。 +2. 通过扫码或查询进入客户查询。 +3. 进入报价开单。 +4. 进入施工查车。 +5. 完成结算收银。 +6. 触发提醒和售后。 + +### 13.3 准备 + +**功能内容** + +- 待办事项。 +- 消息公告。 +- O2O 订单接单提醒。 +- 延保上传提醒。 + +### 13.4 客户查询 + +#### 13.4.1 首页扫码入口 + +**访问链路** +App -> App Backend -> Embedded F6 H5。 + +**逻辑数据来源** +F6 H5 / F6 API。 + +**主流程** + +1. 扫描车牌或 VIN。 +2. 系统识别车辆。 +3. 进入历史记录或新建流程。 + +#### 13.4.2 历史工单记录 + +**业务目标** +让用户快速查看车辆或客户的历史维修/服务记录。 + +**页面内容** + +- 历史工单列表。 +- 工单号。 +- 时间。 +- 门店。 +- 状态。 +- 明细入口。 + +**访问链路** +App -> App Backend。 + +**逻辑数据来源** +F6 Capability APIs 为推荐默认方案;如部分历史记录仍来自 O2O,则通过 App Backend 聚合。 + +**回写目标** +只读。 + +#### 13.4.3 新建工单 + +**业务目标** +为未匹配历史记录或需要新增服务的客户创建工单。 + +**页面内容** + +- 车牌 +- VIN +- 车型 +- 车主信息 +- 服务项目 +- 备注 + +**推荐方案** + +- 新建工单主数据默认写入 F6。 +- 若某些业务仍需写入 O2O,则由 App Backend 负责双系统编排或补同步,后续待接口确认。 + +#### 13.4.4 销售商机 + +**逻辑数据来源** +F6 Capability APIs。 + +**功能内容** + +- 服务提醒记录。 +- 意向池。 +- 商机详情。 +- 跟进动作。 + +#### 13.4.5 延保历史记录 + +**逻辑数据来源** +Warranty Backend。 + +### 13.5 报价开单 + +**业务目标** +支持在到店后完成车辆资料补全、报价和开工单。 + +**访问链路** +App -> App Backend -> Embedded F6 H5。 + +**逻辑数据来源** +F6 H5 / F6 APIs。 + +**主流程** + +1. 从客户查询进入报价开单。 +2. APP 获取 F6 页面访问参数。 +3. 打开 F6 到店记录或开单页。 +4. 补全 VIN、车型、车主信息。 +5. 选择服务项目,生成报价。 +6. 用户确认后开单。 + +**业务规则** + +- 门店上下文必须在进入 H5 前就确定。 +- 报价金额和工单状态以 F6 为准。 + +### 13.6 施工查车 + +**业务目标** +完成查车、异常记录、检测报告和结果转化。 + +**页面内容** + +- 检测模板 +- 检测项目 +- 异常项 +- 正常项批量确认 +- 拍照上传 +- 报告发送入口 + +**主流程** + +1. 从开单结果进入查车。 +2. 选择模板。 +3. 记录正常与异常项。 +4. 上传照片。 +5. 生成报告。 +6. 发送给车主。 +7. 选择转工单或转商机。 + +**异常流程** + +- 模板为空。 +- 图片上传失败。 +- 发送短信失败。 + +### 13.7 结算收银 + +**业务目标** +完成收银、附加项目、结算确认和延保关联。 + +**访问链路** +App -> Embedded F6 H5,必要时通过 App Backend 调用延保或补充状态服务。 + +**主流程** + +1. 进入 F6 结算页。 +2. 添加其他项目。 +3. 确认金额。 +4. 完成收款。 +5. 如命中延保场景,进入延保处理。 + +**业务规则** + +- 结算金额以 F6 为准。 +- 延保出库必须同步到 Warranty Backend。 + +### 13.8 提醒 + +**功能内容** + +- 设置提醒规则。 +- 生成提醒单。 +- 跟进提醒单。 +- SA 发券。 +- 电话或短信提醒。 + +**访问链路** +App -> App Backend -> F6 APIs / 短信服务。 + +### 13.9 售后 + +**功能内容** + +- 延保服务。 +- 延保注册。 +- 质量理赔。 +- 延保理赔。 + +**访问链路** +App -> App Backend。 + +**逻辑数据来源** +Warranty Backend。 + +## 14. 采购流程 + +### 14.1 模块目标 + +采购流程覆盖门店采购、促销、购物车、订单、收货与入库,并允许区分马牌和非马牌产品。 + +### 14.2 架构原则 + +- 采购、促销、ERP相关主能力归属于 F6 供应商域。 +- APP 不将 F6 视为主后台,而是通过 App Backend 编排或 H5 承载接入。 +- 非轮产品、返利、部分活动信息如仍来自 ROOS 或 Mini 服务,由 App Backend 聚合输出。 + +### 14.3 触发采购 + +**入口** + +- 首页快捷入口 +- 缺货预警跳转 +- 库存页面跳转 +- 采购 Tab + +**业务规则** + +- 所有采购都应能在 APP 内发起。 +- 区分马牌和非马牌入口。 + +### 14.4 产品查询 + +**页面内容** + +- 品类树 +- 品牌筛选 +- 搜索框 +- 商品卡片 +- 促销标签 +- 仓库信息 +- DOT 信息 +- 价格信息 + +**业务规则** + +- 非轮产品必须支持清晰树状分类。 +- 同一 SKU 下支持多个 DOT、多仓库、多价格展示。 +- 促销信息需清晰展示生效时间和规则。 + +**访问链路** + +- 默认:App -> App Backend。 +- 如某些页面由 F6 提供现成 H5,则 App Backend 返回 H5 入口。 + +**逻辑数据来源** + +- F6 APIs / F6 H5 为主。 +- ROOS 或活动服务作为补充来源。 + +### 14.5 添加购物车 + +**页面内容** + +- 商品清单 +- 数量修改 +- DOT 选择 +- 仓库选择 +- 批量选择 +- 删除 +- 去结算 + +**业务规则** + +- 购物车以主后台编排结果为准。 +- 价格变化和库存变化必须在结算前再次校验。 + +### 14.6 采购结算 + +**页面内容** + +- 收货地址 +- 商品信息 +- 优惠信息 +- 金额汇总 +- 支付方式 +- 提交订单 + +**主流程** + +1. 用户确认商品。 +2. 选择地址和支付方式。 +3. App 调用 App Backend 提交订单。 +4. App Backend 调用 F6/相关服务完成订单生成。 +5. 返回下单结果。 + +**业务规则** + +- 支付、证书、令牌逻辑由后端控制。 +- 提交后需同步 ERP 采购模块。 + +### 14.7 收货与扫码入库 + +**业务目标** +完成到货确认和扫码入库。 + +**访问链路** +App -> App Backend -> Embedded F6 H5。 + +**逻辑数据来源** +F6 H5 / F6 APIs。 + +**主流程** + +1. 从采购订单进入收货。 +2. 打开扫码入库页。 +3. 扫描商品条码。 +4. 绑定门店与条码。 +5. 完成入库。 + +### 14.8 采购订单 + +**功能内容** + +- 订单列表 +- 订单详情 +- 支付状态 +- 发货状态 +- 收货状态 + +**访问链路** +App -> App Backend。 + +**逻辑数据来源** +F6 + O2O/ROOS 聚合,按订单类型区分。 + +### 14.9 线下采购 + +**功能内容** + +- 缺货提醒转采购单 +- 快速采购 +- 一键入库 +- 安全库存下限采购 +- 批量采购入库 +- 销售单拍照识别 +- Excel 导入入库 + +**推荐默认方案** + +- 线下采购流程由 App Backend 统一编排。 +- 涉及 OCR、导入和库存写入的能力按 F6/ERP 优先承接。 + +## 15. 库存管理 + +### 15.1 模块目标 + +库存管理负责展示库存状态、管理入库、盘点、安全库存、DOT 和预警。 + +### 15.2 库存明细 + +**页面内容** + +- 商品名称 +- SKU +- 仓库 +- DOT +- 可用库存 +- 锁定库存 +- 在途库存 + +### 15.3 安全库存 + +**业务规则** + +- 支持按动态公式计算。 +- 支持按固定值设置。 +- 支持表格导入。 + +### 15.4 库存盘点 + +**功能范围** + +- 全盘点 +- 品类盘点 +- 动销盘点 +- 自定义盘点 +- 临时盘点 + +### 15.5 DOT 管理 + +**业务规则** + +- 同一 SKU 的不同 DOT 必须独立管理。 +- DOT 必须在采购、入库、库存、出库和销售链路中保留。 + +### 15.6 缺货与滞销提醒 + +**业务规则** + +- 缺货提醒跳转采购。 +- 滞销提醒跳转库存处理或退货。 + +## 16. 经营分析 + +### 16.1 模块目标 + +经营分析提供跨采购、销售、核销、返利、库存和延保的经营数据。 + +### 16.2 采购及返利对账单 + +**逻辑数据来源** +ROOS + F6。 + +**页面内容** + +- 采购明细 +- 返利规则 +- 对账状态 +- 差异处理标记 + +### 16.3 门店核销收入 + +**逻辑数据来源** +ROOS。 + +### 16.4 返利中心 + +**页面内容** + +- 返利余额 +- 返利明细 +- 提现申请 +- 提现进度 + +### 16.5 经营报表 + +**功能内容** + +- 门店经营报表 +- 马牌相关报表 +- O2O 订单报表 +- 延保报表 +- 采购报表 +- 库存报表 + +## 17. CRM 客户管理 + +### 17.1 功能范围 + +- 创建营销任务 +- 选择用户 +- 设置发送内容 +- 查看发送记录 + +### 17.2 推荐默认方案 + +- 由 App Backend 统一接入营销服务。 +- 营销用户选择和发送结果通过主后台统一回显。 + +## 18. 员工绩效 + +### 18.1 功能范围 + +- 业绩规则设置 +- 规则列表 +- 业绩明细列表 +- 员工提成查询 + +### 18.2 规则类型 + +- 计件业绩 +- 阶梯业绩 +- 叠加业绩 + +## 19. 财务 + +### 19.1 功能范围 + +- 财务看板 +- 营业收入 +- 营业支出 +- 其他收支 +- 应收应付 +- 开票管理 +- 企业钱包 + +### 19.2 权限规则 + +- 默认仅店长和授权角色可见。 +- 所有金额字段以后台返回为准。 + +## 20. 个人中心 + +### 20.1 收货地址 + +**访问链路** +App -> App Backend。 + +**逻辑数据来源** +App Backend / Order Backend。 + +**功能内容** + +- 地址列表 +- 新增地址 +- 编辑地址 +- 删除地址 +- 默认地址 + +### 20.2 服务热线 + +**功能内容** + +- 热线号码 +- 服务时间 +- 服务范围 +- 一键拨打 + +### 20.3 经销商客服 + +**功能内容** + +- 客服信息 +- 留言反馈 +- 进度查询 + +### 20.4 O2O / 延保客服(企微) + +**功能内容** + +- 企业微信二维码 +- 添加客服 +- 咨询与反馈 + +### 20.5 退出登录 + +参见登录与合规模块。 + +## 21. 非功能需求 + +### 21.1 性能 + +- 首页支持部分失败降级。 +- H5 打开必须有超时与重试策略。 +- 列表页面支持分页或分段加载。 + +### 21.2 稳定性 + +- App Backend 与 F6 之间调用必须有超时和重试策略。 +- F6 异常不得导致主 APP 全部不可用。 +- Mini 某一服务失败应仅影响对应模块。 + +### 21.3 安全 + +- 全链路 HTTPS/TLS。 +- Token 安全存储。 +- 敏感字段脱敏。 +- H5 域名白名单。 +- F6 B2B 接口仅允许 allowlisted 访问。 + +### 21.4 可观测性 + +- 主链路必须具备 Trace ID。 +- H5 打开、关闭、失败需记录事件。 +- 关键业务提交需记录审计日志。 + +## 22. 埋点与运营需求 + +### 22.1 埋点 + +- 登录成功/失败 +- 首页曝光 +- 门店切换 +- 扫码成功/失败 +- H5 页面打开/关闭/异常 +- 待办点击 +- 采购下单 +- 入库成功 +- 客服点击 + +### 22.2 运营配置 + +- 菜单配置 +- 公告配置 +- 促销位配置 +- 热线配置 +- H5 页面入口配置 +- 功能开关配置 + +## 23. 验收标准 + +### 23.1 架构验收 + +- Mobile App 的主访问链路为 App Backend。 +- F6 不作为主后台描述。 +- App Backend 通过 F6 Integration Adapter 调用 F6。 +- App -> F6 仅出现在 Embedded H5 场景。 +- Mini Program Backend 保持独立后台域表达。 + +### 23.2 功能验收 + +- 登录、门店切换、首页工作台可正常使用。 +- 店铺管理、人员管理可查看和维护。 +- 扫码、客户查询、报价开单、施工查车、结算闭环可执行。 +- 采购、购物车、结算、订单、收货、入库可执行。 +- 库存、返利、核销、客服、地址、经营报表可访问。 + +### 23.3 集成验收 + +- H5 访问票据获取成功。 +- F6 页面可获得正确用户和门店上下文。 +- Mini 服务聚合结果正确。 +- 延保、返利、采购、库存数据与原系统一致。 + +### 23.4 异常验收 + +- Token 失效 +- 门店上下文缺失 +- F6 H5 加载失败 +- Mini 服务超时 +- 短信失败 +- 图片上传失败 +- 支付失败 + +## 24. 风险、默认方案与待确认项 + +### 24.1 默认方案 + +- 新版 PRD 以 App Backend 作为统一主后台。 +- 新能力优先走 App Backend 聚合。 +- F6 页面通过 Embedded H5 承载。 +- F6 API 通过 F6 Integration Adapter 访问。 +- 历史 Mini 直连仅作为兼容链路保留。 + +### 24.2 待确认项 + +| 编号 | 待确认项 | 当前默认方案 | 影响 | +| --- | --- | --- | --- | +| R1 | 历史工单主数据源最终是否全部归 F6 | 默认 F6 为主,O2O 为兼容补充 | 客户查询设计 | +| R2 | 新建工单是否需要双写 O2O | 默认主写 F6,后续按联调确认是否补同步 | 开单编排 | +| R3 | 施工队列的主来源 | 默认 App Backend 聚合 O2O/F6 结果 | 首页展示 | +| R4 | 采购商品与促销哪些页面走 H5、哪些走原生/接口 | 默认页面优先复用 F6 能力 | 采购体验 | +| R5 | 线下采购 OCR 和 Excel 导入由谁承接 | 默认 F6/ERP 优先 | 线下采购 | +| R6 | 财务模块完整数据口径和权限边界 | 默认只开放授权角色 | 财务交付 | +| R7 | 企业微信客服的具体集成模式 | 默认由 App Backend 下发二维码与入口 | 客服体验 | + +## 25. 附录:服务拆分与职责清单 + +| 服务 | 主要职责 | +| --- | --- | +| App Backend | 登录、用户、门店、菜单、权限、配置、首页聚合、客服、地址、H5票据、统一编排 | +| O2O Backend | O2O 订单、待办、预警、核销、部分门店业务能力 | +| Warranty Backend | 延保历史、延保服务、延保注册、延保出库 | +| Order Backend | 门店基础信息、服务信息、执照、渠道、人员、地址 | +| ROOS Backend | 返利、核销收入、经营分析、部分采购辅助数据 | +| F6 Web Portal / H5 | 扫码、到店记录、报价开单、施工查车、结算收银、扫码入库、部分采购页面 | +| F6 Capability APIs | 采购、促销、ERP、商机、提醒、补充查询能力 | +| F6 Integration Adapter | F6 鉴权、换票、接口适配、错误转换、链路治理 | diff --git a/Architecture-Diagram/ODP ELK Logging Solution Project - Overview.pdf b/Architecture-Diagram/ODP ELK Logging Solution Project - Overview.pdf new file mode 100644 index 0000000..2649a43 Binary files /dev/null and b/Architecture-Diagram/ODP ELK Logging Solution Project - Overview.pdf differ diff --git a/Architecture-Diagram/app-architecture-diagram.drawio b/Architecture-Diagram/app-architecture-diagram.drawio new file mode 100644 index 0000000..0dd2e6a --- /dev/null +++ b/Architecture-Diagram/app-architecture-diagram.drawio @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Architecture-Diagram/architecture-diagram-explanation.md b/Architecture-Diagram/architecture-diagram-explanation.md new file mode 100644 index 0000000..7fa50d3 --- /dev/null +++ b/Architecture-Diagram/architecture-diagram-explanation.md @@ -0,0 +1,801 @@ +# Continental Retail APP Architecture Diagram 详细说明 + +本文用于解释 `architecture-diagram.drawio` 中的 3 张系统架构图,帮助产品、研发、架构、移动端、后端、集成、测试和评审人员快速理解这套系统为什么这样设计、每一页分别在说明什么、不同域之间的关系如何划分,以及关键访问链路和治理规则如何落地。 + +这 3 张图不是数据库设计图,也不是接口字段定义图,更不是某台服务器部署细节图。它们的定位是: + +- 用于统一架构认知。 +- 用于评审系统边界和责任归属。 +- 用于说明主访问链路和例外链路。 +- 用于帮助研发和测试理解关键业务运行方式。 + +--- + +## 1. 这 3 张图整体在表达什么 + +这套图表达的是一套“统一移动入口、主后台统一编排、历史后台保留独立边界、供应商能力受控接入”的系统架构。 + +它的核心目标不是把所有历史系统都并进一个后台,而是在保留实际业务边界的前提下,把移动端访问链路重新组织成一个清晰、可治理、可演进的体系。 + +3 张图分别承担不同职责: + +- `System Overview` + 说明系统能力分别归谁拥有,重点是“谁负责什么”。 +- `System Architecture` + 说明系统按层分布后的结构关系,重点是“系统怎么连接、怎么治理”。 +- `Key Flows and Context Governance` + 说明关键业务链路在运行时如何流转,重点是“具体流程怎么走、异常和上下文怎么管”。 + +这 3 张图共同强调以下架构原则: + +- `Mobile App -> App Backend` 是唯一主访问链路。 +- `App Backend` 是 APP 的主后台和统一业务编排中心。 +- `Mini Program Backend` 继续保持独立后台域,不直接并入主后台。 +- `F6 Supplier Domain` 是供应商能力域,不被定义为 APP 主后台。 +- `Mobile App -> F6` 只允许发生在嵌入式 `WebView` 场景。 +- `App Backend -> F6 APIs` 必须经过 `F6 Integration Adapter`。 +- 历史 Mini 直连链路只保留为兼容路径,不作为推荐默认方案。 + +--- + +## 2. 图例和颜色到底在表达什么 + +为了让读图的人在 3 页之间建立统一认知,这套图对颜色和线条语义做了固定约定。 + +### 2.1 颜色语义 + +- 蓝色区域和蓝色节点 + 表示 APP 本身和 `App Backend` 主域相关能力。 +- 绿色区域和绿色节点 + 表示保留的 `Mini Program Backend` 历史业务域。 +- 橙色区域和橙色节点 + 表示 `F6 Supplier Domain`、WebView 入口、供应商接入和集成适配层。 +- 灰色区域和灰色说明 + 表示通用治理、跨域聚合、外部服务或非主业务实体说明。 +- 红色点线 + 表示历史兼容链路,只是过渡保留,不是推荐设计。 + +### 2.2 连线语义 + +- 蓝色实线 + 表示主访问链路,也就是 APP 面向主后台的标准访问方式。 +- 橙色虚线 + 表示 F6 相关的例外链路,包括嵌入式 WebView、换票和供应商访问路径。 +- 灰色虚线 + 表示聚合访问、外部服务访问或跨域读取。 +- 红色点线 + 表示历史兼容直连,不建议在新功能中继续复制。 + +这组语义在 3 页中都保持一致。第三页底部的 `Route Semantics` 也用线段样例再次标注这些规则,避免读图时只靠颜色文字理解链路含义。因此这套图并不是 3 张孤立页面,而是一套统一的架构表达。 + +--- + +## 3. 第一页:System Overview 详细说明 + +`System Overview` 是能力归属图。它的重点不是展示某个接口先调谁、后调谁,而是回答一个更基础的问题: + +“整套系统里,不同能力到底归哪个域负责?” + +这一页把系统划分成 4 个主域和 1 个底部治理带: + +- `Mobile App` +- `App Backend` +- `Mini Program Backend Domain` +- `F6 Supplier Domain` +- `External Services + Governance Rules` + +### 3.1 Mobile App:统一移动入口 + +左侧第一列是 `Mobile App`,代表用户真正使用的移动端容器。 + +这部分说明的是,APP 在架构中的职责主要是“承载、呈现、调用原生能力、维持当前会话”,而不是直接承担复杂跨系统编排。 + +它包含几个关键块: + +- `Login / Agreement / App Shell` + 表示 APP 是唯一统一入口,用户从这里进入整个系统。 +- `WebView + JSBridge` + 表示 APP 具备嵌入式 WebView 承载能力,但这里特别强调“只用于 F6 portal pages”,不是泛化的任意外部浏览器。 +- `Native Capabilities` + 表示扫码、相机、上传、蓝牙、通知等设备能力由 APP 容器掌控。 +- `Store-Aware Session Holder` + 表示 APP 本地会持有当前门店和角色上下文,但上下文本身的权威归属仍然在主后台。 + +换句话说,APP 主要负责: + +- 提供统一入口。 +- 承担页面容器。 +- 调度设备能力。 +- 保存当前会话状态。 + +但 APP 不负责: + +- 多系统聚合编排。 +- 供应商 API 直接调用。 +- 业务权限主判断。 +- 门店上下文主定义。 + +### 3.2 App Backend:主业务域和控制中心 + +中间第二列是 `App Backend`,这是整页最核心的部分。 + +这一列强调的是:APP 主后台不只是一个普通 API 服务,而是整个移动平台的控制面和编排中心。 + +它包含以下能力: + +- `Auth and Token Center` + 负责登录、令牌签发、刷新、退出和身份可信控制。 +- `Store Context + Menu + Permission + Config` + 负责当前门店、角色范围、菜单权限和配置下发。 +- `Workbench Aggregation` + 负责首页工作台、提醒、摘要和多个系统结果的统一返回。 +- `Business Orchestration` + 负责订单、采购、流程上下文和字段标准化。 +- `WebView Ticket and Signing Entry` + 负责 F6 WebView 场景的换票、上下文准备和 trace handoff。 + +这一列的含义非常明确: + +- APP 所有主流程都应先到这里。 +- 这里决定用户是谁、当前在哪个门店、可以看什么菜单、有什么权限。 +- 这里负责把复杂的后端异构系统整合成移动端可用结果。 +- 这里也是所有供应商接入和链路治理的统一出口。 + +如果没有这一层,移动端就会变成一个多后台直连客户端,导致: + +- 门店和角色上下文不统一。 +- 权限边界难以收口。 +- 多系统异常无法统一处理。 +- 供应商接入逻辑扩散到多个模块。 + +### 3.3 Mini Program Backend Domain:保留独立边界的历史系统域 + +第三列是 `Mini Program Backend Domain`。 + +这部分要表达的不是“历史系统不重要”,而是: + +“这些系统仍然重要,但它们不是 APP 的统一主后台。” + +图中列出了典型保留系统: + +- `O2O` +- `Warranty` +- `Retail Store` +- `ROOS` + +其中 `Retail Store` 对应“马上下单”体系中的门店注册、门店信息修改、店员管理等门店基础资料能力,不再使用容易被误解为交易订单的旧英文表述。 + +并通过说明卡强调: + +- 它们仍然拥有历史业务数据和流程。 +- 它们仍然可以是某些能力的 `Source of Truth`。 +- 但面向 APP 的访问,原则上应通过 `App Backend` 聚合和标准化后暴露。 + +这部分架构表达非常重要,因为它避免了两个极端: + +- 一个极端是误以为历史系统可以被立刻彻底替换。 +- 另一个极端是继续把移动端做成多个小程序后台的直连入口。 + +正确做法是: + +- 保留历史系统边界。 +- 承认它们的业务归属。 +- 但将移动端访问统一收束到主后台。 + +### 3.4 F6 Supplier Domain:供应商能力域 + +第四列是 `F6 Supplier Domain`。 + +这部分专门用来说明 F6 的定位: + +- 它是供应商域。 +- 它提供能力,但不是 APP 的主系统。 +- 它既包含页面能力,也包含接口能力。 + +图中分成两类: + +- `F6 WebView Pages` +- `F6 Capability APIs` + +并给出代表性示例: + +- WebView 例子:`Sales, scan, settlement, inbound` +- API 例子:`Procurement, activity, inventory sync` + +这里的核心意思是: + +- 页面可以在受控情况下嵌入到 APP WebView 中。 +- API 不允许 APP 前端直接调用。 +- 所有 API 级访问应通过 `App Backend` 内部适配层完成。 + +### 3.5 底部:External Services 和 Governance Rules + +最底部是一条横向带,补充说明两件事: + +- 系统还会依赖外部服务。 +- 整套系统必须遵守统一治理规则。 + +外部服务示例包括: + +- `SMS` +- `Marketing / CRM` +- `WeCom` + +治理规则则明确写出 4 条核心边界: + +- `App -> App Backend is primary` +- `App -> F6 only in Embedded WebView` +- `F6 APIs go through Adapter` +- `Mini direct access is legacy-only` + +这 4 条规则可以理解为第一页的结论。整张图不只是介绍组件,而是在告诉所有参与方: + +“系统边界和默认访问方式已经定了,新需求不要再随意绕开它。” + +--- + +## 4. 第二页:System Architecture 详细说明 + +`System Architecture` 是分层架构图。它建立在第一页能力归属基础上,进一步解释系统按层组织后的技术结构。 + +如果第一页回答的是“谁负责什么”,那么第二页回答的是: + +“这些能力在逻辑架构上如何分层组织,链路如何穿过这些层?” + +这一页分成以下层次: + +- `Client Tier` +- `Access & Security` +- `App Backend` +- `Integration Layer` +- `Mini Program Backend` +- `F6 Supplier Domain` +- `External Services` +- `Cross-Cutting` + +### 4.1 Client Tier:客户端层 + +最上层是 `Client Tier`。 + +这里包含: + +- `Store User Persona` +- `Mobile App` +- `Native Capabilities` +- `WebView + JSBridge` + +这层说明的是: + +- 用户并不是直接面对多个后端系统,而是面对一个统一 APP。 +- 原生能力和 WebView 容器都属于客户端能力域。 +- 客户端负责发起请求、展示页面、承载设备能力和 WebView 容器。 + +这里再次强调:`WebView + JSBridge` 是一种容器能力,不等于前端可以直接把供应商接口当成主链路使用。 + +### 4.2 Access & Security:接入与安全层 + +第二层是 `Access & Security`。 + +它包含: + +- `API Gateway` +- `JWT / Auth` +- `WebView Allowlist` + +这层表达的是系统接入不能裸奔,所有访问必须经过入口控制和安全校验。 + +#### API Gateway + +`API Gateway` 代表统一 API 接入入口,负责: + +- HTTPS / TLS 接入。 +- 路由分发。 +- 基础限流和治理。 +- 将移动端和后端实现细节隔离。 + +#### JWT / Auth + +`JWT / Auth` 代表统一身份凭证体系,负责: + +- 令牌签发。 +- 令牌刷新。 +- 请求身份识别。 +- 为后续权限和上下文建立可信基础。 + +#### WebView Allowlist + +`WebView Allowlist` 是这一层里很关键的一个治理点。 + +它表达的是: + +- 并不是任何外部 WebView 页面都可以被 APP 打开。 +- 嵌入式 WebView 域名必须是受控、登记和允许的。 +- 这既是安全规则,也是架构边界规则。 + +### 4.3 App Backend:主后台层 + +第三层是 `App Backend`,也是架构主域。 + +这里包含: + +- `Identity & Store Center` +- `BFF Orchestration` +- `Workbench Aggregation` +- `WebView Ticket Center` + +#### Identity & Store Center + +这个模块负责: + +- 用户身份。 +- 门店主数据。 +- 角色范围。 +- 菜单权限。 + +它的意义是让系统具备统一上下文中心,否则每个后端域都按自己的方式解释用户和门店,会导致移动端体验严重割裂。 + +#### BFF Orchestration + +这是面向移动端的统一 BFF 编排层,负责: + +- 统一接口形态。 +- 聚合和标准化多个后端返回。 +- 减少前端直接理解多个后端协议的复杂度。 + +在架构里它是非常重要的一层,因为 APP 实际看到的“后端世界”,应该优先由它统一封装,而不是暴露历史系统差异。 + +#### Workbench Aggregation + +这个模块更聚焦首页和工作台聚合能力,典型包括: + +- 待办。 +- 提醒。 +- 门店经营摘要。 +- 多来源业务 tile 数据。 + +它说明首页不是由单一系统直接返回,而是由主后台整合多个来源后下发。 + +#### WebView Ticket Center + +这个模块专门解决 F6 WebView 场景下的入口控制问题,负责: + +- 生成或换取 WebView 票据。 +- 绑定当前用户和门店上下文。 +- 传递 trace 信息。 +- 为嵌入式 WebView 提供受控启动入口。 + +这意味着 APP 打开 F6 WebView 时,并不是裸跳 URL,而是先由主后台准备好上下文和票据条件。 + +### 4.4 Integration Layer:集成适配层 + +第四层是 `Integration Layer`。 + +这一层存在的意义,是把供应商接入复杂性控制在一个专门层里,而不是扩散进整个业务系统。 + +包含两个关键组件: + +- `F6 Integration Adapter` +- `Timeout / Retry` + +#### F6 Integration Adapter + +它负责: + +- 换票。 +- 供应商访问上下文准备。 +- 将主后台调用收口到统一适配出口。 + +本质上它是主后台访问 F6 的统一适配出口。 + +#### Timeout / Retry + +这部分表达的是供应商链路治理能力,包括: + +- 超时控制。 +- 重试策略。 + +因为 F6 是外部能力域,不稳定性和响应波动风险都比内部模块更高,所以这里保留最关键的超时和重试治理。 + +### 4.5 Mini Program Backend:历史后台层 + +第五层是 `Mini Program Backend`。 + +这一层包含: + +- `O2O Backend` +- `Warranty Backend` +- `Retail Store Backend` +- `ROOS Backend` + +其中 `Retail Store Backend` 对应原“马上下单”相关的门店、人员、地址和资料维护能力,命名上按业务域表达为门店基础资料后台。 + +这层的架构含义不是“这些服务很次要”,而是: + +- 它们继续承担原有业务责任。 +- 它们可以是数据主来源。 +- 但移动端调用它们时,优先通过主后台聚合。 + +这页里灰色虚线从 `BFF Orchestration` 向下扇出,就是在表达: + +- 主后台聚合多个 Mini 域。 +- 主后台对外统一返回。 +- APP 不应把它们当多个主后台直接使用。 + +### 4.6 F6 Supplier Domain:供应商能力层 + +右侧是 `F6 Supplier Domain`。 + +这里分成两个主要能力: + +- `F6 WebView Pages` +- `F6 Capability APIs` + +它们分别对应两种不同访问模式: + +#### F6 WebView Pages + +用于页面嵌入场景,例如: + +- 销售开单。 +- 扫码。 +- 结算。 +- 入库。 + +访问特点是: + +- 页面最终可能由移动端 WebView 直接打开。 +- 但打开前需要经过主后台准备票据和上下文。 +- 域名必须在 Allowlist 中。 + +#### F6 Capability APIs + +用于系统间能力调用,例如: + +- 采购。 +- 促销。 +- ERP 补充能力。 + +访问特点是: + +- 前端不能直接调用。 +- 只能从 `App Backend -> Integration Adapter -> F6 APIs` 发起。 + +### 4.7 External Services:外部服务层 + +底部右侧是 `External Services`,包括: + +- `SMS` +- `Marketing / CRM` +- `WeCom` + +这说明 APP 主后台除了对接历史内部域和供应商域外,也会调用一些外部平台服务。 + +这些服务通常承担: + +- 短信验证码。 +- 营销或客户关系数据。 +- 企业协同消息。 + +它们本质上也是外部依赖,因此在架构里被单独表达,而不是混在主后台内部。 + +### 4.8 Cross-Cutting:横切关注点 + +右上方还有一组 `Cross-Cutting` 模块: + +- `Observability` +- `Audit / Security` +- `Config Center` + +这些不是某个独立业务流程,而是所有流程都必须共享的治理能力。 + +#### Observability + +负责: + +- Trace ID +- 日志 +- 指标 + +确保跨域链路可追踪。 + +#### Audit / Security + +负责: + +- 敏感信息控制。 +- 关键行为留痕。 +- 审计证据保留。 + +#### Config Center + +负责: + +- 菜单配置。 +- 开关配置。 +- WebView 入口策略。 + +这部分说明 APP 平台不是硬编码系统,而是需要较强配置化能力支撑运营和演进。 + +### 4.9 第二页想强调的核心结论 + +第二页最终想让读图人形成以下认知: + +- 客户端只有一个统一入口。 +- 安全和接入必须前置。 +- 主后台必须承担统一 BFF 和统一上下文职责。 +- 供应商接入必须通过适配层治理。 +- 历史 Mini 域继续存在,但面向 APP 的输出需要经由主后台整合。 +- 所有跨域链路都要可观测、可审计、可配置、可降级。 + +--- + +## 5. 第三页:Key Flows and Context Governance 详细说明 + +第三页是运行时流程图。它不是在重新画系统结构,而是在解释: + +“当业务真实运行起来时,这套架构的关键行为是怎么发生的?” + +这一页选了 4 条最关键的流程: + +- `Flow 1 Login and Store Context Bootstrap` +- `Flow 2 F6 WebView Launch and Session Rules` +- `Flow 3 Home Aggregation and Mini Compatibility` +- `Flow 4 Hybrid Procurement and Inbound Flow` + +同时底部补充统一治理规则。 + +### 5.1 Flow 1:Login and Store Context Bootstrap + +这条流程解释登录后的上下文建立过程。 + +流程步骤是: + +1. `App Login` +2. `Token Issue` +3. `Store List + Default Store` +4. `Menu / Permission / Config Baseline` +5. `Context Established` + +这个流程在架构上的真正含义不是“登录成功返回 token”这么简单,而是: + +- 用户身份只是第一步。 +- 系统真正可运行还需要建立当前门店上下文。 +- 菜单、权限和配置要跟门店和角色一起确定。 +- APP 登录后不是只有一个用户态,而是一个“用户 + 门店 + 角色 + 菜单 + 配置”的完整工作上下文。 + +所以这条流程强调的是: + +- `Store Context` 是核心控制对象。 +- 登录完成不代表业务上下文完成。 +- APP 中很多行为都依赖这一步建立出的当前门店语义。 + +### 5.2 Flow 2:F6 WebView Launch and Session Rules + +这条流程解释 F6 WebView 嵌入为什么是“例外链路但仍然受控”。 + +流程步骤是: + +1. `App asks for WebView entry` +2. `Backend exchanges / prepares ticket` +3. `Embedded open in WebView` + +并补充 3 条强规则: + +- `Store switch invalidates WebView` +- `Expired ticket needs refresh` +- `Failure fallback shows safe message + trace ID` + +这条流程表达的重点有 4 个: + +#### 第一,WebView 入口必须由主后台签发 + +APP 不是拿到一个 URL 就直接打开,而是要先通过主后台获取受控入口。 + +#### 第二,WebView 会话受当前门店上下文约束 + +如果用户切换门店,当前 WebView 会话不能继续沿用旧上下文,否则会造成门店数据串用和权限风险。 + +#### 第三,票据是时效性的 + +过期票据需要刷新,而不是无限复用。 + +#### 第四,失败时必须可支持排障 + +如果 F6 打不开,不能只给用户一个空白页或技术错误,而是要给出安全提示并带 trace 信息,方便客服、研发和集成排查。 + +### 5.3 Flow 3:Home Aggregation and Mini Compatibility + +这条流程解释首页工作台为什么一定是聚合页,而不是某个单体后端直接吐数据。 + +流程步骤是: + +1. `Workbench request` +2. `Backend aggregates O2O / Warranty / ROOS / Config` +3. `Partial failure returns degradable tiles` +4. `Legacy Mini direct access is compatibility only` + +这条流程的重点非常明确: + +#### 第一,首页是聚合结果 + +首页的数据来源天然分散,不可能全部只来自一个系统。 + +#### 第二,主后台要负责标准化 + +不同 Mini 域返回结构、状态和错误处理方式可能不同,需要统一加工后再交给 APP。 + +#### 第三,降级方式必须是“局部降级” + +如果其中一个来源失败,不应该让整个首页白屏,而应该: + +- 哪个 tile 有问题就降级哪个 tile。 +- 其他可用部分继续返回。 + +#### 第四,历史直连不是默认解法 + +即使某些 Mini 场景短期仍保留兼容直连,也应明确标记为历史兼容路径,而不是继续作为新功能访问方式。 + +### 5.4 Flow 4:Hybrid Procurement and Inbound Flow + +这条流程解释采购和入库为什么是“混合模式”。 + +流程步骤是: + +1. `Retail store context + permission + normalization` +2. `F6 owns procurement / promotion / ERP` +3. `Inbound execution uses WebView or API path` +4. `Historical data may still come from Mini aggregation` + +这条流程在业务上很关键,因为它说明采购和入库类能力并不是纯内部能力,也不是纯供应商能力,而是一个混合场景。 + +它的架构含义是: + +- 主后台负责上下文、权限和统一口径。 +- F6 负责其擅长和实际拥有的供应商能力。 +- 页面型操作可能通过 WebView 完成。 +- 接口型能力通过适配层访问。 +- 部分历史辅助数据可能仍来自 Mini 域聚合。 + +这也解释了为什么: + +- 不能把 F6 当主后台。 +- 也不能假装采购和入库完全脱离 F6。 +- 更不能让前端自己到处直连多个系统完成业务。 + +正确解法是由主后台掌控主流程,由供应商承接其能力边界内的部分。 + +--- + +## 6. 底部治理规则和线段语义在告诉我们什么 + +第三页底部的 `Shared Governance Rules`、`Route Semantics` 和 `Review checks` 不是补充装饰,而是整套架构落地的检查清单和读图规则。 + +### 6.1 Shared Governance Rules + +这里明确了 4 条总规则: + +- `Store context is owned by App Backend` +- `F6 WebView is an embedded exception` +- `Mini systems remain independent` +- `Every degraded or supplier-facing failure returns a user-safe message and a trace ID` + +它们分别约束: + +- 上下文归属。 +- 例外链路边界。 +- 历史系统独立性。 +- 失败处理方式。 + +### 6.2 Route Semantics + +这里用线段样例说明第三页所有关键链路的读法: + +- `Primary App route` + 对应蓝色实线,表示 APP 到主后台的标准主链路。 +- `F6 embedded / exception` + 对应橙色虚线,表示 F6 WebView 嵌入、票据换取或供应商例外链路。 +- `Aggregation / external` + 对应灰色虚线,表示主后台向 Mini 域、外部服务或聚合来源发起的访问。 +- `Legacy compatibility` + 对应红色点线,表示历史兼容路径,只能作为过渡保留,不能作为新功能默认链路。 + +这部分和第二页的图例保持一致,目的是让评审人员在看流程图时能直接区分“主链路、例外链路、聚合链路、历史链路”,避免把 F6 WebView 或 Mini 直连误解成默认访问方式。 + +### 6.3 Review Checks + +这里的检查项可以直接作为评审问题来问: + +1. 门店切换是否会关闭当前供应商 WebView 会话。 +2. 首页失败是否按 tile 降级而不是整页失败。 +3. 采购和入库是否保持 F6-first 的供应商能力归属。 +4. Mini 直连是否被严格控制为过渡链路。 + +如果某个新需求或技术方案违反了这里的检查项,通常意味着它已经偏离当前架构原则。 + +--- + +## 7. 为什么必须这样设计 + +这套架构不是为了“画得好看”,而是为了解决当前业务现实中的几个核心问题。 + +### 7.1 解决多入口和多后台混乱 + +历史上不同能力分散在多个小程序和后台里,APP 如果继续直接对接多个后端,移动端复杂度会越来越高。 + +统一 `App Backend` 作为主后台后,可以: + +- 收敛前端访问入口。 +- 统一账号和门店上下文。 +- 统一权限和配置。 + +### 7.2 保留历史资产而不是强行推倒重来 + +Mini Program Backend 仍然保留业务边界,避免为了“统一”而做不现实的大拆大建。 + +这使系统具备: + +- 现实可落地性。 +- 渐进式演进能力。 +- 更低迁移风险。 + +### 7.3 把 F6 当成供应商能力,而不是主系统 + +这是整个架构中最重要的认知之一。 + +因为 F6: + +- 不是自有主后台。 +- 协议和稳定性不完全可控。 +- 同时提供页面和接口两类能力。 + +所以必须: + +- 页面入口受控。 +- API 接口后端统一适配。 +- 供应商异常和主系统隔离。 + +### 7.4 支持真实业务运行中的上下文治理 + +系统不是“登录就结束”,而是多门店、多角色、多链路环境下持续运行。 + +因此必须把以下能力做成架构级规则: + +- 当前门店上下文。 +- WebView 会话绑定和失效控制。 +- 跨域链路可追踪。 +- 失败时可降级、可审计、可支持排障。 + +--- + +## 8. 读完这 3 页后应该形成的统一认知 + +如果要用一句话总结这 3 页图,它表达的是: + +“Continental Retail APP 是一个以 `App Backend` 为统一主后台、以 `Mobile App` 为统一入口、以 `Mini Program Backend` 为保留独立历史域、以 `F6` 为受控供应商能力域的混合架构系统。” + +更具体地说,所有参与方应形成以下共识: + +- APP 不是多个后台的并列客户端,而是统一入口。 +- `App Backend` 不是普通接口层,而是控制面和编排中心。 +- Mini 后台继续存在,但面向 APP 时应优先通过主后台聚合。 +- F6 是供应商域,页面和接口都要按受控方式接入。 +- 门店上下文是系统运行的核心治理对象。 +- 首页聚合必须支持局部降级。 +- WebView 是例外链路,不是默认业务通路。 +- 所有跨域链路都必须可治理、可观测、可审计。 + +--- + +## 9. 这份说明适合怎么使用 + +这份文档适合以下用途: + +- 架构评审时作为讲解稿。 +- 产品、研发、测试 onboarding 时作为系统认知材料。 +- 需求讨论时用于确认是否违反既定架构原则。 +- 接口联调前用于统一系统边界和责任分工。 + +如果后续需要,我还可以继续补两类配套文档: + +- 一份“面向评审汇报”的精简版讲稿。 +- 一份“按模块拆解”的实现说明,把图里的每个块映射到实际研发任务和接口边界。 diff --git a/Architecture-Diagram/architecture-diagram.drawio b/Architecture-Diagram/architecture-diagram.drawio new file mode 100644 index 0000000..19b8dd0 --- /dev/null +++ b/Architecture-Diagram/architecture-diagram.drawio @@ -0,0 +1,1312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Architecture-Diagram/deployment-architecture-diagram.drawio b/Architecture-Diagram/deployment-architecture-diagram.drawio new file mode 100644 index 0000000..0858d82 --- /dev/null +++ b/Architecture-Diagram/deployment-architecture-diagram.drawio @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Architecture-Diagram/extract_component_data_to_md.py b/Architecture-Diagram/extract_component_data_to_md.py new file mode 100644 index 0000000..e44ab44 --- /dev/null +++ b/Architecture-Diagram/extract_component_data_to_md.py @@ -0,0 +1,101 @@ +# pyright: reportMissingImports=false + +from __future__ import annotations + +from pathlib import Path +import re + +from openpyxl import load_workbook + + +ROOT = Path(__file__).resolve().parent +SOURCE_FILE = ROOT.parent / "202606 Conti Retail APP Component data source.xlsx" +OUTPUT_FILE = ROOT / f"{SOURCE_FILE.stem}.md" + +FIELDS = [ + ("编号", 2), + ("模块", 3), + ("功能", 4), + ("负责人", 5), + ("前置任务", 6), + ("数据集", 7), + ("来源", 8), + ("安全", 9), + ("备注", 10), +] + + +def normalize_cell(value: object) -> str: + if value is None: + return "" + + if isinstance(value, float) and value.is_integer(): + value = int(value) + + text = str(value).replace("\r\n", "\n").replace("\r", "\n") + lines = [re.sub(r"\s+", " ", line).strip() for line in text.split("\n")] + return "\n".join(line for line in lines if line) + + +def add_field(parts: list[str], name: str, value: str) -> None: + if not value: + return + + parts.append(f"**{name}**") + parts.append("") + + lines = [line.strip() for line in value.splitlines() if line.strip()] + if len(lines) > 1: + parts.extend(f"- {line}" for line in lines) + else: + parts.extend(lines) + + parts.append("") + + +def extract_workbook_to_md() -> None: + workbook = load_workbook(SOURCE_FILE, data_only=True) + parts: list[str] = [f"# {SOURCE_FILE.stem}", ""] + + for worksheet in workbook.worksheets: + rows: list[dict[str, str]] = [] + + for row_index in range(2, worksheet.max_row + 1): + row = { + field: normalize_cell(worksheet.cell(row=row_index, column=column).value) + for field, column in FIELDS + } + if not any(row.values()): + continue + if not any(row[key] for key in ("编号", "模块", "功能")): + continue + rows.append(row) + + if not rows: + continue + + parts.append(f"## {worksheet.title}") + parts.append("") + + for row in rows: + number = row["编号"] + module = row["模块"] + title = " ".join(part for part in (number, module) if part).strip() + level = min(6, 3 + number.count(".")) if number else 3 + + parts.append(f"{'#' * level} {title or 'Untitled'}") + parts.append("") + add_field(parts, "功能", row["功能"]) + add_field(parts, "负责人", row["负责人"]) + add_field(parts, "前置任务", row["前置任务"]) + add_field(parts, "数据集", row["数据集"]) + add_field(parts, "来源", row["来源"]) + add_field(parts, "安全", row["安全"]) + add_field(parts, "备注", row["备注"]) + + OUTPUT_FILE.write_text("\n".join(parts).strip() + "\n", encoding="utf-8") + print("Wrote workbook markdown output") + + +if __name__ == "__main__": + extract_workbook_to_md() diff --git a/Architecture-Diagram/extract_requirements_to_md.py b/Architecture-Diagram/extract_requirements_to_md.py new file mode 100644 index 0000000..d054cc0 --- /dev/null +++ b/Architecture-Diagram/extract_requirements_to_md.py @@ -0,0 +1,128 @@ +# pyright: reportMissingImports=false + +from __future__ import annotations + +from pathlib import Path +import re + +import fitz +import numpy as np +from pptx import Presentation +from pptx.enum.shapes import MSO_SHAPE_TYPE +from rapidocr_onnxruntime import RapidOCR + + +ROOT = Path(__file__).resolve().parent +SOURCE_DIR = ROOT.parent + +PDF_PATH = SOURCE_DIR / "User Journeys.pdf" +PPTX_PATH = SOURCE_DIR / "零售商系统方案研讨会PPT.retro.pptx" + +PDF_OUTPUT = ROOT / "User Journeys.md" +PPTX_OUTPUT = ROOT / "零售商系统方案研讨会PPT.retro.md" + +OCR = RapidOCR() + + +def normalize_text(text: str) -> str: + text = text.replace("\r\n", "\n").replace("\r", "\n") + lines = [re.sub(r"\s+", " ", line).strip() for line in text.split("\n")] + return "\n".join(line for line in lines if line) + + +def extract_page_ocr_text(page: fitz.Page) -> str: + pix = page.get_pixmap(matrix=fitz.Matrix(4, 4), alpha=False) + image = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, pix.n) + result, _ = OCR(image) + if not result: + return "" + return normalize_text("\n".join(str(item[1]) for item in result)) + + +def extract_pdf_to_md(pdf_path: Path, output_path: Path) -> None: + doc = fitz.open(pdf_path) + parts: list[str] = [f"# {pdf_path.stem}", ""] + + try: + for index in range(doc.page_count): + page = doc.load_page(index) + text = normalize_text(str(page.get_text("text") or "")) + if not text: + text = extract_page_ocr_text(page) + parts.append(f"## Page {index + 1}") + parts.append("") + parts.append(text or "[No extractable text]") + parts.append("") + finally: + doc.close() + + output_path.write_text("\n".join(parts).strip() + "\n", encoding="utf-8") + + +def iter_shape_text(shape) -> list[str]: + chunks: list[str] = [] + + if hasattr(shape, "has_text_frame") and shape.has_text_frame: + text = normalize_text(shape.text_frame.text) + if text: + chunks.append(text) + + if hasattr(shape, "has_table") and shape.has_table: + for row in shape.table.rows: + cells = [normalize_text(cell.text) for cell in row.cells] + cells = [cell for cell in cells if cell] + if cells: + chunks.append(" | ".join(cells)) + + if shape.shape_type == MSO_SHAPE_TYPE.GROUP: + for subshape in shape.shapes: + chunks.extend(iter_shape_text(subshape)) + + return chunks + + +def extract_pptx_to_md(pptx_path: Path, output_path: Path) -> None: + prs = Presentation(str(pptx_path)) + parts: list[str] = [f"# {pptx_path.stem}", ""] + + for index, slide in enumerate(prs.slides, start=1): + title = "" + parts.append(f"## Slide {index}") + parts.append("") + + if slide.shapes.title and slide.shapes.title.text: + title = normalize_text(slide.shapes.title.text) + if title: + parts.append(f"### {title}") + parts.append("") + + seen: set[str] = set() + collected = [] + + for shape in slide.shapes: + for chunk in iter_shape_text(shape): + if title and chunk == title: + continue + if chunk and chunk not in seen: + seen.add(chunk) + collected.append(chunk) + + if collected: + parts.extend(f"- {chunk}" for chunk in collected) + else: + parts.append("[No extractable text]") + + parts.append("") + + output_path.write_text("\n".join(parts).strip() + "\n", encoding="utf-8") + + +def main() -> None: + extract_pdf_to_md(PDF_PATH, PDF_OUTPUT) + extract_pptx_to_md(PPTX_PATH, PPTX_OUTPUT) + print("Wrote PDF markdown output") + print("Wrote PPTX markdown output") + + +if __name__ == "__main__": + main() diff --git a/Architecture-Diagram/gitlab-cicd-azure-deployment-diagram.drawio b/Architecture-Diagram/gitlab-cicd-azure-deployment-diagram.drawio new file mode 100644 index 0000000..5b4f1f4 --- /dev/null +++ b/Architecture-Diagram/gitlab-cicd-azure-deployment-diagram.drawio @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Architecture-Diagram/network-architecture-diagram.drawio b/Architecture-Diagram/network-architecture-diagram.drawio new file mode 100644 index 0000000..92b9006 --- /dev/null +++ b/Architecture-Diagram/network-architecture-diagram.drawio @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Architecture-Diagram/network-architecture-explanation.md b/Architecture-Diagram/network-architecture-explanation.md new file mode 100644 index 0000000..92a62ea --- /dev/null +++ b/Architecture-Diagram/network-architecture-explanation.md @@ -0,0 +1,691 @@ +# Multicloud Network Architecture 详细说明 + +本文用于解释 `network-architecture-diagram.drawio` 中的两个页面,帮助产品、研发、集成和评审人员快速理解这套架构为什么这样设计、每个区域代表什么、各条访问链路分别承担什么职责,以及从网络拓扑角度如何理解各 VNet / VPC 之间的连通关系。 + +这个 draw.io 文件现在包含两个视图: + +- `Network Architecture`:架构评审视图,重点说明业务域边界、主访问链路、供应商接入方式和历史系统兼容策略。 +- `Network Topology`:网络拓扑视图,重点说明 VNet / VPC 边界、公网入口、私有服务区、跨网络访问路径和允许 / 例外链路。 + +两张图表达的是同一套系统,但侧重点不同:第一张回答“为什么这样分域、谁应该访问谁”,第二张回答“网络上从哪里进、跨哪些边界、通过哪些受控入口连通”。 + +## 1. 这张图整体在表达什么 + +这张图表达的是一套“三域分离、主链路统一、供应商隔离、历史系统兼容”的多云网络架构。 + +它的核心目标不是展示某个具体服务器怎么部署,而是说明以下几个关键架构原则: + +- `Mobile App -> App Backend` 是唯一主访问链路。 +- `App Backend` 是 APP 的统一主后台和业务编排中心。 +- `F6` 是供应商能力域,不是 APP 的主后台。 +- `Mobile App -> F6` 只允许发生在嵌入式 `WebView` 场景。 +- `App Backend -> F6 API` 必须通过 `F6 Integration Adapter` 访问。 +- `Mini Program Backend` 保持独立后台域,但面向 APP 的能力应优先通过 `App Backend` 聚合。 +- `App Backend -> Mini Program Backend` 和 `App Backend -> F6` 都是跨独立 VPC / VNet 边界的受控后端访问,不是主域内部本地调用。 +- 历史上少量 APP 直连 Mini 后台的场景可以兼容保留,但不能继续扩散。 + +从架构表达上看,这张图并不是一张“部署拓扑详图”,而是一张“面向架构评审的网络边界与主访问链路图”。 + +## 2. 顶部:Mobile App 和 Internet + +图最上方是 `Mobile App`,下面是 `Internet`。 + +这部分表示: + +- 用户所有访问都从移动端发起。 +- APP 访问不同后端域时,需要通过公网链路进入各自对外暴露的入口层。 +- 移动端不是直接连内部服务,而是只访问被允许暴露的受控入口。 + +这张图中,APP 主要存在两类访问路径: + +- 主访问路径:`Mobile App -> App Backend` +- 特殊访问路径:`Mobile App -> F6 WebView Pages` + +其中第一条是主链路,第二条是例外链路,而且这个例外只允许发生在 F6 的嵌入式 WebView 场景。 + +## 3. 中间 Azure 域:Primary App Backend Domain + +中间区域是 `Azure China (Beijing) VNet`,它代表 APP 主业务域,也是整张图最核心的部分。 + +这一块承担的是: + +- APP 的统一主后台。 +- 身份、门店、权限、菜单、配置等基础能力中心。 +- 多系统数据聚合和业务编排中心。 +- F6 和 Mini Program Backend 的统一接入和治理出口。 + +### 3.1 为什么这块是核心 + +整张图最重要的一条架构原则就是: + +`Mobile App -> App Backend` + +这意味着 APP 不应该把多个后台都当成自己的“直接主后台”。真正的主后台只有一个,就是这里的 `App Backend`。 + +这样做的好处是: + +- APP 不需要分别理解多个后端的认证和权限模型。 +- 门店上下文、角色上下文、菜单权限可以统一收口。 +- F6 和历史 Mini 服务的复杂性可以被隔离在后端。 +- 前端链路更稳定,后端也更方便统一治理。 + +### 3.2 DMZ / Public Ingress Zone + +Azure 域上半部分是 `DMZ / Public Ingress Zone`。 + +这部分是公网入口区,作用是先接住来自 APP 的外部请求,不让外部流量直接打进私有服务区。 + +里面包括: + +- `WAF / Application Gateway` +- `Public Load Balancer` + +这两个组件组合表达的是标准公网入口模式: + +- 先经过网关和安全过滤。 +- 再经过负载均衡。 +- 最后把请求转发到私有区中的后端服务。 + +这里的 `WAF / Application Gateway` 主要表示: + +- Web 应用防护。 +- 统一入口控制。 +- 基础七层流量治理。 +- 对异常流量、非法请求做第一层拦截。 + +`Public Load Balancer` 则表示: + +- 对后面的 APP 后端服务实例进行流量分发。 +- 屏蔽单实例细节。 +- 让后面服务集群保持弹性扩缩能力。 + +### 3.3 Private Application Zone + +Azure 域下半部分是 `Private Application Zone`,也就是私有业务服务区。 + +这里才是真正承载 APP 业务能力的地方。 + +它包含以下几个关键组件: + +#### App Backend BFF / Unified APIs + +这是 APP 的统一接口层,可以理解成 APP 面向移动端的统一后端入口。 + +它负责的事情包括: + +- 登录认证。 +- 用户信息返回。 +- 门店上下文建立和切换。 +- 菜单与权限返回。 +- 系统配置下发。 +- F6 WebView 启动参数准备。 +- 为移动端提供统一接口风格。 + +图中旁边的说明框: + +`Identity / Store Context / Menu / Config / WebView Ticket` + +就是为了强调这件事:APP 依赖的核心控制面能力必须由主后台统一掌握。 + +#### Business Aggregation / Orchestration + +这是业务聚合与编排层。 + +它和 `App Backend BFF / Unified APIs` 的区别在于: + +- `BFF / Unified APIs` 更偏向“给移动端一个统一的接口入口”。 +- `Business Aggregation / Orchestration` 更偏向“把多个后端系统的数据和流程编排成一个完整业务结果”。 + +比如: + +- 首页工作台聚合多个系统数据。 +- 采购、库存、延保、返利等场景的跨系统整合。 +- 对 Mini 后台返回结果做标准化。 +- 把 F6 能力、Mini 后台能力和 APP 自身基础能力组合起来。 + +这个层次存在的意义是: + +- 把复杂的多系统业务放在后端做,而不是让 APP 自己拼装。 +- 让 APP 更像一个统一入口,而不是一个多系统直连终端。 + +#### F6 Integration Adapter + +这是 F6 供应商接入适配层。 + +它的职责是: + +- 发起对 F6 的 allowlisted B2B 访问。 +- 处理 F6 鉴权、签名、票据换取。 +- 组装调用 F6 所需的 Header 或上下文。 +- 做字段映射和错误码转换。 +- 做超时、重试、熔断、异常隔离。 +- 把供应商侧不稳定性隔离在适配层内。 + +它存在的意义非常重要: + +- APP 主后台不应该让每个业务模块都自己直接调 F6。 +- F6 是外部供应商域,协议、鉴权、返回结构都可能与主系统不同。 +- 所有访问 F6 API 的逻辑都集中在这里,才能实现统一治理。 + +#### NSG / Access Control + +这个框表示私有区里的访问控制层。 + +`NSG` 一般可以理解为 `Network Security Group`,也就是网络安全组。 + +它在图里的意思不是某个独立业务服务,而是一层安全控制能力,用来表达: + +- 私有业务区不是公网可直接访问的。 +- 即使请求已经通过前面的 `WAF / Application Gateway` 和 `Public Load Balancer`,进入私有区后仍要受访问控制规则约束。 +- 私有区内部组件之间也不应该默认全部互通,而应按来源、目标、协议、端口进行规则控制。 + +用更直白的话说,这一层是在强调: + +“后端服务即使在一个私有网络里,也不是谁都能访问谁,必须按规则放行。” + +#### Unified Error / Audit / Logging / Security + +这个说明框不是单独服务,而是架构责任说明。 + +它表达的是: + +- 错误处理要统一。 +- 安全能力要统一。 +- 审计记录要统一。 +- 日志追踪要统一。 + +这也是为什么 `App Backend` 被定义为主业务域,而不是单纯一个 API 容器。 + +## 4. 右侧 F6 Supplier VPC:F6 供应商域 + +右侧区域是 `F6 Supplier VPC (Alibaba Cloud)`,它表示供应商 F6 所在的独立云网络域。图中同时标注 `Supplier-managed VPC / network boundary`,用来强调 F6 不只是一个业务系统框,而是供应商侧独立管理的 VPC 网络边界。 + +这块的标题是: + +`F6 Supplier VPC (Alibaba Cloud)` + +这句话非常关键,它强调: + +- F6 由供应商管理。 +- F6 是外部能力域。 +- F6 不是 APP 主后台的一部分。 +- APP 需要接入 F6,但不能把 F6 当成自己的主系统。 + +### 4.1 F6 域的两类能力 + +F6 域中被分成两类对外能力: + +- `F6 WebView Pages` +- `F6 Capability APIs (Backend Only)` + +这两者是整张图中最重要的区别之一。 + +#### F6 WebView Pages + +这是给用户页面使用的能力,也就是嵌入到 APP WebView 里的页面。 + +适用于: + +- 扫码开单页。 +- 到店记录页。 +- 报价开单页。 +- 施工查车页。 +- 结算收银页。 +- 扫码收货页。 +- 部分采购和促销页面。 + +访问链路是: + +`Mobile App -> Internet -> Access Gateway / WAF -> Load Balancer -> F6 WebView Pages` + +这条链路的含义是: + +- APP 可以直接访问 F6,但仅限于嵌入式 WebView 页面。 +- 这里的“直接访问”不是绕过主后台做业务控制,而是在进入 WebView 之前,由 `App Backend` 先完成票据、上下文和权限准备。 + +#### F6 Capability APIs (Backend Only) + +这是系统间调用的接口能力。 + +图上明确标注了: + +`Backend Only` + +这表示: + +- 移动端不能直接调这些接口。 +- 这些接口只能由 `App Backend` 经由 `F6 Integration Adapter` 发起访问。 + +典型用途包括: + +- 获取 WebView 免登票据。 +- 获取采购、促销、ERP补充能力。 +- 发起供应商相关查询或写入。 +- 执行需要后端协同的业务流程。 + +### 4.2 F6 域的入口层 + +F6 域上部也分为公网入口区: + +- `Access Gateway / WAF` +- `Load Balancer` +- `Allowlisted B2B API Gateway` + +这三者分别表达不同入口: + +- WebView 页面流量走 Access Gateway / WAF 和 Load Balancer。 +- 后端 API 调用走专门的 allowlisted B2B API Gateway。 + +这正是图中为什么要同时区分页面链路和 API 链路,因为它们不是一回事。 + +### 4.3 App Backend 到 F6 的受控后端集成链路是什么意思 + +图中从 `F6 Integration Adapter` 指向 `Allowlisted B2B API Gateway` 的绿色后端集成链路,表示: + +- 这是系统间受控访问。 +- 不是终端用户流量。 +- 需要白名单放通。 +- 这是从 App Backend 所在 Azure VNet 到 F6 Supplier VPC 的跨网络边界访问。 +- 访问入口必须落在 F6 VPC 对外暴露的受控 B2B API Gateway 上,而不是直接访问 F6 私有服务节点。 +- 同时承担获取 WebView 票据、签名、免登参数等职责。 +- 图上不再在线路上写长标签,而是通过颜色和图例表达其语义。 + +这也是为什么 PRD 里强调: + +- F6 API 必须通过 App Backend 调用。 +- APP 对 F6 的直接访问只保留 WebView 场景。 + +## 5. 左侧 Mini Program Backend VPC:历史小程序后台域 + +左侧区域表示历史小程序后台域。图中外层标注为 `Mini Program Backend VPC`,用来说明 Mini 后台不是 App Backend 私有区里的内部模块,而是保留在独立 VPC / 网络域中的历史后台集合。 + +这一域在图中的定位是: + +- 它仍然存在。 +- 它仍然是独立后台边界。 +- 它不会被简单视为 APP 的主后台。 +- 面向 APP 的能力应逐步被 `App Backend` 聚合。 + +### 5.1 为什么这里采用聚合表示而不是展开拓扑 + +图中把它写成: + +`Independent Mini Services` + +并列出: + +- `O2O` +- `Warranty` +- `Retail Store` +- `ROOS` +- `Shared Capabilities` + +这样写是为了强调: + +- Mini Program Backend 不是一个单体后台。 +- 它是多个历史业务后台的集合。 +- 它们仍然可能各自承担不同的数据主来源和业务责任。 +- 其中 `Retail Store` 对应“马上下单”体系中的门店注册、门店信息修改、店员管理等门店基础资料能力,不再使用容易误解为交易订单的 `Order` 表述。 +- 这张图是评审视图,因此只保留“入口层 + 独立服务集合”的表达,不再展开每个历史服务之间的内部转发关系。 + +图这样画,更符合 PRD 对历史系统边界的定义,也能避免内部箭头过多影响主链路可读性。 + +### 5.2 主推荐链路:通过 App Backend 聚合 + +从 `Business Aggregation / Orchestration` 指向 Mini 域入口的是绿色后端集成链路。 + +这条线表达的是推荐架构: + +- 新能力优先通过 `App Backend` 聚合后暴露给 APP。 +- 历史 Mini 后台继续作为独立服务存在。 +- 但它们不再被 APP 当成第一入口直接大量访问。 +- 从网络角度看,这条链路是 App Backend 所在 Azure VNet 到 Mini Program Backend VPC 的受控后端调用。 +- App Backend 应访问 Mini VPC 暴露的网关、防火墙或受控入口,不应把 Mini 私有服务当作同一 VNet 内的本地服务直接访问。 + +这是“统一主后台”原则的重要一部分。 + +### 5.3 兼容链路:少量保留直连 + +图中从 `Internet` 指向 Mini 域入口的灰色虚线标注为: + +`Legacy only` + +这条线表达的是: + +- 某些历史模块短期还保留 APP 直连 Mini 后台的方式。 +- 但这种方式只是兼容,不是推荐方案。 +- 这不是未来架构方向。 +- 新模块不应该继续沿用这种方式。 + +这条线的存在是为了如实表达现状,同时避免评审误解为: + +“APP 会长期平行直连所有历史后台。” + +## 6. 这张图里的主要链路怎么理解 + +### 6.1 主链路:APP 到主后台 + +链路为: + +`Mobile App -> Internet -> WAF / Application Gateway -> Public Load Balancer -> App Backend BFF / Unified APIs` + +这条链路表示: + +- 用户所有主业务请求先进入主后台域。 +- 主后台对移动端暴露统一接口。 +- APP 的身份、权限、门店上下文和主流程控制都从这里进入。 + +这是整个系统最重要的访问路径。 + +### 6.2 F6 H5 页面链路 + +链路为: + +`Mobile App -> Internet -> Access Gateway / WAF -> Load Balancer -> F6 WebView Pages` + +这条链路表示: + +- APP 可直接以 WebView 方式打开 F6 页面。 +- 但这里的“直接打开”是页面访问级别,而不是随意直接调用供应商 API。 +- 页面进入前的上下文准备、票据获取、权限判断仍由 `App Backend` 完成。 + +### 6.3 F6 API 链路 + +链路为: + +`Mobile App -> App Backend -> Business Aggregation / Orchestration -> F6 Integration Adapter -> F6 Supplier VPC / Allowlisted B2B API Gateway -> F6 Capability APIs` + +这条链路表示: + +- 所有后端级 F6 能力都应该由主后台发起调用。 +- 供应商集成逻辑不直接暴露给移动端。 +- 主后台统一承担安全、审计、错误处理和链路治理。 +- 网络上这是跨 VPC / VNet 的后端集成链路,需要通过 F6 VPC 暴露的 allowlisted API 入口进入。 + +### 6.4 Mini 聚合链路 + +链路为: + +`Mobile App -> App Backend -> Business Aggregation / Orchestration -> Mini Program Backend VPC -> Mini Gateway / Firewall -> Independent Mini Services` + +这条链路表示: + +- 历史后台能力仍被使用。 +- 但 APP 访问时优先走主后台聚合。 +- 这有利于统一接口、统一权限和统一数据口径。 +- 网络上这不是 App Backend 内部进程调用,而是从主后台 VNet 到 Mini 后台 VPC 的受控后端访问。 +- Mini VPC 的外层入口承担网络隔离、访问控制和历史服务保护职责。 + +### 6.5 Mini 兼容直连链路 + +链路为: + +`Mobile App -> Internet -> Mini Program Backend Domain` + +但图中明确标注这是: + +`Legacy compatibility direct access only` + +它表示: + +- 当前还有少量历史兼容模块未完成收敛。 +- 架构上允许暂时存在。 +- 但这不是目标态。 + +## 7. 为什么说这是多云架构 + +这张图之所以叫 `Multicloud`,是因为主系统不是部署在单一云环境里,而是跨越了多个云网络边界: + +- 移动端经公网访问。 +- 主业务域在 `Azure China`。 +- F6 供应商域在 `Alibaba Cloud`。 +- 历史 Mini Program Backend 在独立 VPC / 云网络域中。 + +这意味着: + +- 不同域之间不是天然内网互通。 +- 跨域访问必须经过明确入口。 +- 供应商域和主域的边界要清晰。 +- 历史系统不能被简单视为主域内部服务。 + +这也是为什么图中如此强调: + +- VPC / VNet 边界。 +- DMZ 与私有区分层。 +- 受控 API 链路。 +- 兼容直连和推荐主链路的区别。 + +## 8. 这张图的价值是什么 + +这张图的价值不在于说明某台机器部署在哪,而在于回答下面这些评审问题: + +- APP 到底应该先访问谁。 +- F6 是主后台还是供应商域。 +- 为什么 F6 页面能直开,但 F6 API 不能让 APP 直接调。 +- 历史 Mini 后台还保不保留。 +- App Backend 到底是不是只是一个 API 网关,还是主编排中心。 +- 多个系统的数据、权限和上下文由谁统一控制。 + +如果用一句话总结整张图: + +“APP 只有一个主后台入口,供应商能力通过受控方式接入,历史小程序后台继续存在但逐步被主后台聚合,整个系统通过多云边界和访问控制实现职责分离与链路治理。” + +## 9. 如何理解图中的关键标签 + +### Primary App Backend Domain + +表示 APP 的统一主业务域,是控制面和编排面的中心。 + +### F6 Supplier VPC + +表示 F6 是位于 Alibaba Cloud 的供应商管理独立 VPC,不属于 App Backend 主后台域。 + +### Independent Mini Services + +表示历史小程序后台不是一个单系统,而是一组位于独立 VPC / 网络域内的后台服务。 + +### DMZ / Public Ingress Zone + +表示对外暴露入口区,用于承接公网流量,隔离内网服务。 + +### Private Application Zone / Private Service Zone + +表示真正承载业务服务的私有区域,不直接向公网开放。 + +### NSG / Access Control + +表示私有区域内的访问控制和安全规则层,强调后端服务是受控访问而非默认互通。 + +### F6 Integration Adapter + +表示对 F6 的统一适配与治理出口。 + +### Backend integration path + +表示推荐路径,即主后台通过受控 VPC 网络入口优先聚合历史系统能力。 + +### Legacy only + +表示兼容性保留链路,不是目标架构,不应扩散到新模块。 + +## 10. 第二页 Network Topology 怎么理解 + +`Network Topology` 是对第一张 `Network Architecture` 的补充,不是重复画一张架构图。 + +第一张图更偏“架构职责和访问原则”,所以使用了较多业务组件、说明框和颜色区分;第二张图更偏“网络连通和边界关系”,所以采用黑白灰拓扑表达,避免让颜色承担业务语义。 + +这张拓扑图重点表达四件事: + +- 三个核心网络边界:`Primary App Backend VNet`、`Mini Program Backend VPC`、`F6 Supplier VPC`。 +- 公网访问统一从 `Mobile App -> Internet -> Public DNS / Domain` 进入。 +- 每个网络域只通过自己的受控入口暴露能力,而不是暴露内部服务。 +- 跨 VNet / VPC 的访问必须经过网关、防火墙、API Gateway 或适配层。 + +### 10.1 为什么拓扑图是黑白灰 + +网络拓扑图的目标不是强调业务域颜色,而是强调: + +- 网络边界在哪里。 +- 公网入口在哪里。 +- 私有服务在哪里。 +- 哪些链路是允许的。 +- 哪些链路只是兼容例外。 + +因此第二页没有继续沿用第一张图里的蓝色、绿色、橙色业务域配色,而是使用黑白灰表达。 + +这样更符合网络拓扑图的阅读习惯:读者主要通过形状、边界框、线型和线条粗细理解网络关系,而不是通过颜色理解业务语义。 + +### 10.2 拓扑图里的主要区域 + +#### Public Network + +中间的 `Public Network` 表示公网访问区。 + +里面包括: + +- `Mobile App` +- `Internet` +- `Public DNS / Domain` + +它表达的是:移动端访问后端系统时,首先进入公网和域名解析层,然后再被路由到不同网络域暴露出来的公网入口。 + +这里不是一个业务系统,而是网络访问路径的公共部分。 + +#### Azure China / Primary App Backend VNet + +左上区域表示 APP 主后台所在的 Azure China VNet。 + +它内部用虚线框标出 `VNet boundary`,并分成两层: + +- `Public DMZ Subnet` +- `Private Application Subnet` + +`Public DMZ Subnet` 中放的是 `WAF / App Gateway + Public LB`,表示 APP 主链路必须先经过公网入口、防护和负载均衡。 + +`Private Application Subnet` 中放的是: + +- `NSG / Access Rules` +- `App Backend BFF + Orchestration` +- `F6 Integration Adapter` + +这表示真正的 APP 后台服务不直接暴露公网,而是在私有子网中运行,并受到 NSG / Access Rules 约束。 + +#### Mini Program Backend Independent VPC + +左下区域表示历史小程序后台所在的独立 VPC。 + +它内部包括: + +- `Mini Gateway / Firewall` +- `Mini Services` + +这里强调的是:Mini 后台不是 Azure App Backend VNet 里的内部模块,而是另一个独立网络边界中的历史服务集合。 + +APP 面向 Mini 能力的目标链路应当是: + +`App Backend -> Mini Gateway / Firewall -> Mini Services` + +而不是让 APP 长期绕过主后台直接访问 Mini 服务。 + +#### F6 Supplier VPC / Alibaba Cloud + +右侧区域表示供应商 F6 所在的 Alibaba Cloud VPC。 + +它内部用 `Supplier-managed VPC boundary` 表示这是供应商管理的独立网络边界,不属于 APP 主后台网络。 + +F6 侧有两个不同入口: + +- `Access Gateway / WAF + Load Balancer` +- `Allowlisted B2B API Gateway` + +前者服务于 F6 WebView 页面访问,后者服务于主后台到 F6 API 的后端集成。 + +这两个入口不能混在一起理解,因为它们的访问来源、访问目的和安全要求都不同。 + +### 10.3 拓扑图里的线型含义 + +第二页不用颜色区分链路,而是用线型和粗细表达语义。 + +#### Primary App route + +主访问链路是: + +`Mobile App -> Internet -> Public DNS / Domain -> WAF / App Gateway + Public LB -> App Backend` + +这条链路表示 APP 的主业务请求进入 Azure App Backend VNet,由主后台统一承接。 + +#### Backend integration + +后端集成链路有两类: + +- `App Backend -> Mini Gateway / Firewall -> Mini Services` +- `App Backend -> F6 Integration Adapter -> Allowlisted B2B API Gateway -> F6 Capability APIs` + +这两类链路都表示受控的跨网络边界访问。 + +它们不是同一 VNet 内部的本地调用,也不是移动端直接访问第三方或历史后台。 + +#### F6 WebView route + +F6 WebView 链路是: + +`Mobile App -> Internet -> Public DNS / Domain -> F6 Access Gateway / WAF + Load Balancer -> F6 WebView Pages` + +这条链路表示 APP 可以打开供应商 F6 的嵌入式页面。 + +但它仍然只是 WebView 页面访问例外,不代表 APP 可以直接访问 F6 API。 + +#### Legacy compatibility + +Mini legacy 兼容链路是: + +`Mobile App -> Internet -> Public DNS / Domain -> Mini Gateway / Firewall` + +这条线用虚线表达,含义是: + +- 这是历史兼容链路。 +- 不是目标态主链路。 +- 不应继续扩散到新功能。 +- 后续应尽量收敛到 `App Backend -> Mini` 的后端聚合路径。 + +### 10.4 Network Architecture 和 Network Topology 的区别 + +两张图的区别可以这样理解: + +| 页面 | 主要回答的问题 | 适合谁看 | 重点 | +| --- | --- | --- | --- | +| `Network Architecture` | 为什么这样分域、主链路是谁、F6 和 Mini 怎么定位 | 产品、研发、架构评审 | 架构原则、职责边界、访问治理 | +| `Network Topology` | 网络上怎么连、从哪里进、跨哪些 VNet / VPC | 网络、安全、运维、集成 | 网络边界、入口节点、连通路径、线型语义 | + +因此,第二张图看起来应该比第一张更“网络化”: + +- 更少业务说明。 +- 更强调 VNet / VPC boundary。 +- 更强调 ingress / gateway / firewall。 +- 更强调公网和私网分层。 +- 更强调跨网络访问不是默认互通,而是通过受控入口连接。 + +## 11. 结论 + +这两张图合起来已经把系统最关键的几件事表达清楚了: + +- APP 的统一主入口是 `App Backend`。 +- F6 API 由主后台统一接入。 +- 历史 Mini 后台独立保留在自己的 VPC / 网络域中,但应逐步收敛到主后台聚合。 +- 各域通过公网入口区、私有服务区和访问控制层进行隔离。 + +因此,`Network Architecture` 本质上是一张“面向架构评审的网络边界与访问链路图”,重点在于: + +- 说明系统边界。 +- 说明主访问链路。 +- 说明供应商接入方式。 +- 说明历史系统兼容策略。 +- 说明为什么要把 APP 主后台定义为统一编排中心。 + +而 `Network Topology` 则是一张“面向网络、安全和集成理解的拓扑图”,重点在于: + +- 说明公网入口。 +- 说明 VNet / VPC 边界。 +- 说明私有服务区不直接暴露公网。 +- 说明跨网络边界访问必须通过受控网关。 +- 说明 legacy direct access 只是兼容例外,不是目标态。 diff --git a/Architecture-Diagram/system-architecture-diagram.drawio b/Architecture-Diagram/system-architecture-diagram.drawio new file mode 100644 index 0000000..52f98a3 --- /dev/null +++ b/Architecture-Diagram/system-architecture-diagram.drawio @@ -0,0 +1,807 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Architecture-Diagram/零售商系统方案研讨会PPT.retro.md b/Architecture-Diagram/零售商系统方案研讨会PPT.retro.md new file mode 100644 index 0000000..d75229a --- /dev/null +++ b/Architecture-Diagram/零售商系统方案研讨会PPT.retro.md @@ -0,0 +1,640 @@ +# 零售商系统方案研讨会PPT.retro + +## Slide 1 + +### 零售商系统方案研讨会(复盘) + +- 6/12 +- China + +## Slide 2 + +### 零售流程 + +[No extractable text] + +## Slide 3 + +### 首页功能简介 + +- 登录 +门店切换(针对总分店) +维护员工信息(App Web 同步) +店长 +前台客服 +收银 +维修技师 +美容技师 +市场专员 +维护门店基本信息 +- 基本功能 + +## Slide 4 + +### 首页功能简介 + +- 登录 +门店切换(针对总分店) +维护员工信息(App Web 同步) +店长 +前台客服 +收银 +维修技师 +美容技师 +市场专员 +维护门店基本信息 +- 基本功能 + +## Slide 5 + +### 销售流程 – 准备(任务提醒) + +- 报价开单 +- 施工查车 +- 结算 +- 提醒 +- 售后 +- 待办事项: +O2O订单接单提醒 +CDMS采购单支付提醒 +延保视频上传提醒 +问卷提醒 +过期门店信息更新提醒 +消息公告(站内信/促销) +产品库存预警 +- 客户查询 +- 准备 + +## Slide 6 + +### 销售流程 – 客户查询 + +- 首页扫车牌入口 +OCR(文本识别)功能 +显示车牌历史交易记录,包含: +历史工单(数据ERP提供) +展示历史工单内容 +新建工单 +延保历史记录 +延保注册 +- 销售商机 +- 报价开单 +- 施工查车 +- 结算 +- 提醒 +- 售后 +- 客户查询 +- 准备 + +## Slide 7 + +### 销售流程 – 客户查询 + +- 销售商机 +服务提醒 +意向池 +- 施工查车 +- 结算 +- 提醒 +- 售后 +- 准备 +- 报价开单 +- 客户查询 + +## Slide 8 + +### 销售流程 – 报价开工单 + +- 提醒 +- 售后 +- 准备 +- 施工查车 +- 结算 +- 报价开单 +- 客户查询 +- 完善VIN码及车主信息 +- 维护车辆信息 +VIN码匹配车型 + +## Slide 9 + +### 销售流程 – 施工查车 + +- 提醒 +- 售后 +- 准备 +- 施工查车 +- 结算 +- 报价开单 +- 客户查询 +- 检测开单 +- 选择检测项目 +- 查车 +选择查车检测模板 + +## Slide 10 + +### 销售流程 – 施工查车 + +- 提醒 +- 售后 +- 准备 +- 施工查车 +- 结算 +- 报价开单 +- 客户查询 +- 异常项拍照,正常项批量通过 +- 检测项目 +- 记录查车异常结果 +批量更新查车正常结果 + +## Slide 11 + +### 销售流程 – 施工查车 + +- 提醒 +- 售后 +- 准备 +- 施工查车 +- 结算 +- 报价开单 +- 客户查询 +- 生成检测报告发送车主 +- 微信/企业微信/公众号/短信 +- 查车报告发送车主 +SMS短信 +微信 + +## Slide 12 + +### 销售流程 – 施工查车 + +- 提醒 +- 售后 +- 准备 +- 施工查车 +- 结算 +- 报价开单 +- 客户查询 +- 检测单转工单/商机 +- 查车结果转工单 +查车结果转商机 + +## Slide 13 + +### 销售流程 – 施工查车 + +- 提醒 +- 售后 +- 准备 +- 施工查车 +- 结算 +- 报价开单 +- 客户查询 +- 转工单 +- 完工 +- 查车结果转工单 +叉车结果转商机 + +## Slide 14 + +### 销售流程 – 结算 + +- 添加其它项目 +结算收款 +- 报价开单 +- 施工查车 +- 结算 +- 提醒 +- 售后 +- 客户查询 +- 准备 + +## Slide 15 + +### 销售流程 – 结算 + +- 当前延保流程 +- 报价开单 +- 施工查车 +- 结算 +- 提醒 +- 售后 +- 客户查询 +- 准备 + +## Slide 16 + +### 销售流程 – 提醒 + +- 1 +- 设置提醒规则 +- 2 +- 生成提醒单 +- 3 +- 跟进提醒单 +- 设置提醒规则 +生成提醒单 +跟进提醒单 +- 报价开单 +- 施工查车 +- 结算 +- 提醒 +- 售后 +- 客户查询 +- 准备 + +## Slide 17 + +### 销售流程 – 提醒 + +- 1 +- 设置提醒规则 +- 2 +- 生成提醒单 +- 3 +- 跟进提醒单 +- 设置提醒规则 +生成提醒单 +跟进提醒单 +- 报价开单 +- 施工查车 +- 结算 +- 提醒 +- 售后 +- 客户查询 +- 准备 + +## Slide 18 + +### 销售流程 – 提醒 + +- 1 +- 设置提醒规则 +- 2 +- 生成提醒单 +- 3 +- 跟进提醒单 +- SA发券 +SA电话跟进 +SA主动发短信提醒 +临近服务期系统自动发送短信提醒 +- 设置提醒规则 +生成提醒单 +跟进提醒单 +- 报价开单 +- 施工查车 +- 结算 +- 提醒 +- 售后 +- 客户查询 +- 准备 + +## Slide 19 + +### 销售流程 – 提醒 + +- 报价开单 +- 施工查车 +- 结算 +- 提醒 +- 售后 +- 客户查询 +- 准备 +- 消费者收到短信 + +## Slide 20 + +### 销售流程 – 售后 + +- 质量理赔 +延保理赔 +- 报价开单 +- 施工查车 +- 结算 +- 提醒 +- 售后 +- 客户查询 +- 准备 +- 销售商机 + +## Slide 21 + +### 请分组讨论 有哪些功能,与门店期望有差异? 有哪些重要的功能,但没有提及? 其他建议和意见? + +[No extractable text] + +## Slide 22 + +### 采购流程(线上采购马牌轮胎/非轮产品) – 触发采购/产品查询 + +- 触发采购 +- 产品查询 +- 添加购物车 +- 结算 +- 收货 +- Feature demo: +进入采购首页,选择/搜索产品 +先选择品类(轮胎,机油 +,雨刮) +再选择品牌(马牌,维京) +直接搜索产品 +加入购物车 +- 其它 + +## Slide 23 + +### 采购流程(线上采购马牌轮胎/非轮产品) – 购物车 + +- 触发采购 +- 产品查询 +- 添加购物车 +- 结算 +- 收货 +- 其它 + +## Slide 24 + +### 采购流程(线上采购马牌轮胎/非轮产品) – 结算 + +- 触发采购 +- 产品查询 +- 添加购物车 +- 结算 +- 收货 +- 其它 +- 提交订单后同步ERP的采购模块 + +## Slide 25 + +### 采购流程(线上采购马牌轮胎/非轮产品) – 收货 + +- 触发采购 +- 产品查询 +- 添加购物车 +- 结算 +- 收货 +- 扫码入库(条码和门店的关系绑定) +- 其它 + +## Slide 26 + +### 采购流程(线上采购马牌轮胎/非轮产品) – 其它 + +- 触发采购 +- 产品查询 +- 添加购物车 +- 结算 +- 收货 +- 采购订单列表 +采购订单详情 +- 其它 + +## Slide 27 + +### 采购流程(线下) + +- 安全库存设置 +方式一:此模式将根据您设置的【日均销量】及【备货天数上、下限】自动计算出安全库存上、下限【设置成功后材料的安全库存将会进行每日动态更新】 +方式二:按固定值,安全库存上限/下限 +- 支持表格导入 +- 缺货提醒 +- 转采购单 +- 快速采购 +- 一键入库 + +## Slide 28 + +### 采购流程(线下) + +- 根据安全库存下限采购入库 +- 手动选择批量采购入库 +- 供应商销售单拍照识别入库 +- EXCEL批量导入入库 +- 缺货提醒 +- 转采购单 +- 快速采购 +- 一键入库 + +## Slide 29 + +### 采购流程(线下) + +- 缺货提醒 +- 转采购单 +- 快速采购 +- 一键入库 +- 马牌一键入库 + +## Slide 30 + +### 请分组讨论 + +[No extractable text] + +## Slide 31 + +### 库存管理亮点 + +- 快速库存调整 +- 安全库存 +- 定期盘点 +全盘点 +品类盘点 +动销盘点 +自定义盘点 +临时盘点 + +## Slide 32 + +### 库存管理亮点 + +- 快速库存调整 +- 安全库存 +- 定期盘点 +全盘点 +品类盘点 +动销盘点 +自定义盘点 +临时盘点 + +## Slide 33 + +### 库存管理亮点 + +- 快速库存调整 +- 安全库存 +- 定期盘点 +全盘点 +品类盘点 +动销盘点 +自定义盘点 +临时盘点 + +## Slide 34 + +### 库存管理亮点 + +- DOT管理 +- 安全库存 +- 安全库存设置 +方式一:此模式将根据您设置的【日均销量】及【备货天数上、下限】自动计算出安全库存上、下限【设置成功后材料的安全库存将会进行每日动态更新】 +方式二:按固定值,安全库存上限/下限 +- 支持表格导入 + +## Slide 35 + +### 库存管理亮点 + +- DOT管理 +- 安全库存 +- 缺货提醒—一键跳转采购页面 +- 滞销库存提醒—一键跳转退货页面 + +## Slide 36 + +### 请分组讨论 + +[No extractable text] + +## Slide 37 + +### CRM客户管理 + +- 创建营销任务 +- 选择发送用户 +- 设置发送内容 + +## Slide 38 + +### CRM客户管理 + +- 创建营销任务 +- 选择发送用户 +- 设置发送内容 + +## Slide 39 + +### CRM客户管理 + +- 创建营销任务 +- 选择发送用户 +- 设置发送内容 + +## Slide 40 + +### CRM客户管理 + +- 创建营销任务 +- 选择发送用户 +- 设置发送内容 + +## Slide 41 + +### 请分组讨论 + +[No extractable text] + +## Slide 42 + +### 员工绩效 + +- 设置业绩规则 +- 业绩规则列表 +- 业绩明细列表 +- 计件业绩:按完成具体工作量来计算业绩提成,结清后实时产生; +阶梯业绩:按配置金额范围来判断所属阶梯,直接使用阶梯上的规则计算提成,按自然月生成,每天凌晨1点更新; +叠加业绩:使用不同阶梯上对应提成比例,将多个阶梯的提成累加,按自然月生成,每天凌晨1点更新; + +## Slide 43 + +### 员工绩效 + +- 设置业绩规则 +- 业绩规则列表 +- 业绩明细列表 + +## Slide 44 + +### 员工绩效 + +- 设置业绩规则 +- 业绩规则列表 +- 业绩明细列表 + +## Slide 45 + +### 员工绩效 + +- 对应员工姓名 +对应工单号 +对应提成金额 +- 设置业绩规则 +- 业绩规则列表 +- 业绩明细列表 + +## Slide 46 + +### 请分组讨论 + +[No extractable text] + +## Slide 47 + +### 报表和其它 + +- 报表 +门店经营报表 +马牌相关报表 +公告与提醒 +马牌公告(announcement for all retailers) +提醒(notification for specific retailers) +马牌相关的提醒 +来自ERP的相关提醒(库存预警由ERP提供) +车型轮胎/非轮产品匹配查询 +需要F6确认是否能够加入这次的workshop内容 + +## Slide 48 + +### 财务 + +- Feature demo: +财务看板 +营业收入 +营业支出 +其他收支 +预收处理 +定金管理 +预付管理 +应收账款 +应付账款 +开票管理 +收支明细 +企业钱包 +- 财务看板 +营业收入 +营业支出 +其他收支 +预收处理 +定金管理 +预付管理 +应收账款 +应付账款 +开票管理 +收支明细 +企业钱包 + +## Slide 49 + +### 请分组讨论 + +[No extractable text] + +## Slide 50 + +### 其它建议 + +- 接入店内摄像头,实现信息自动录入及客户提醒 +探讨门店自有系统与 APP 的融合方案。 +不同类型门店是否能兼容一套系统体系。 +销售相关数据是否能对马牌开放(权限问题)。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ae93fe --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# conti-docs + +Continental Retail APP 相关的文档参考仓库,用于沉淀架构决策、系统/网络架构图,以及后续会陆续补充的后端设计和 API 文档。 + +## 目录说明 + +### App 架构决策文档 + +Flutter APP 的分包、分层、技术选型等决策记录,按序号阅读: + +| 文档 | 内容 | +| --- | --- | +| [01-project-structure.md](./01-project-structure.md) | 工程结构 / 分包策略(Melos monorepo) | +| [02-layering.md](./02-layering.md) | 分层架构规范(presentation/domain/data) | +| [03-state-management.md](./03-state-management.md) | 状态管理方案(Riverpod) | +| [04-routing.md](./04-routing.md) | 路由方案(go_router) | +| [05-networking.md](./05-networking.md) | 网络层设计(dio) | +| [06-local-storage.md](./06-local-storage.md) | 本地存储方案(Drift / secure storage / shared_preferences) | +| [07-native-integration.md](./07-native-integration.md) | 原生能力集成方式(Pigeon) | +| [08-build-flavors.md](./08-build-flavors.md) | 多环境构建(dev/uat/prod flavor) | +| [09-testing.md](./09-testing.md) | 测试策略(单元/Widget/集成测试) | + +### Architecture-Diagram/ + +前期系统架构、网络架构梳理阶段产出的图和说明文档,先保留作为历史参考,后续可能会做精简: + +- `architecture-diagram.drawio` / `architecture-diagram-explanation.md` — 系统架构图(System Overview / System Architecture / Key Flows)及说明 +- `network-architecture-diagram.drawio` / `network-architecture-explanation.md` — 多云网络架构图(业务域边界、VNet/VPC 拓扑)及说明 +- `app-architecture-diagram.drawio` — APP 侧架构图 +- `deployment-architecture-diagram.drawio` / `system-architecture-diagram.drawio` — 部署架构图 / 系统架构图 +- `gitlab-cicd-azure-deployment-diagram.drawio` — GitLab CI/CD 到 Azure 的部署流程图 +- `202606-Continental-Retail-APP-PRD.md` — 产品需求文档(PRD) +- `202606-Conti-Retail-APP-Component-data-source.md` — 功能模块与数据来源整理 +- `零售商系统方案研讨会PPT.retro.md` — 方案研讨会复盘记录 +- `extract_requirements_to_md.py` / `extract_component_data_to_md.py` — 从原始材料提取内容生成上述 md 文档的脚本 + +`.drawio` 文件可用 [draw.io 桌面版](https://github.com/jgraph/drawio-desktop) 或 VS Code 的 Draw.io Integration 插件打开查看。 + +## 待补充 + +- Backend 设计文档 +- API 文档 + +## 语言约定 + +文档以中文为主。