Files
CloudHome-Ops/docs/system_design.md

293 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 云端管理端 — 增量架构设计(4 项功能:前端占位实现)
> 版本:v1.0|架构师:高见远|基于 PRD + 现有代码事实
> 关键决策:**4 项功能全部「前端先占位」实现**——本地模型 + 预留接口 TODO 占位,不强制依赖真实后台(后端当前缺 `themeId`/`splashUrl`/`logoUrl` 字段、无图片上传/扫码绑定/门店 save 接口)。
---
## 1. 实现方案 + 框架选型
### 1.1 技术难点分析
| 功能 | 难点 | 解决思路 |
|------|------|----------|
| 门店主题/皮肤 | 后台缺主题字段,需纯前端内置主题库 + 本地预览 | 主题枚举 + 内置占位资源(纯色块/Assets 图),本地模型存 `themeId`,预留 `setStoreTheme` |
| 省市级联 | 34 省三级联动数据加载与回显映射 | 复用 `province.json`,封装 `RegionPicker` 组件,结构化存 `code` + 拼接文本,既有文本地址尽力映射 |
| 扫码绑定 | 跨平台扫码(Android/iOS/鸿蒙),替代老 ScanKit | 引入 `mobile_scanner`(通用、维护活跃),解析后本地查找设备并跳转 |
| 启动图/Logo | 图片选择 + 手机外框预览,后台无上传接口 | `image_picker` 选图本地预览,`branding` 字段存本地路径,预留 `uploadAsset`/`setStoreBranding` |
### 1.2 框架 / 包选型
沿用现有栈 **Flutter 3.27.4 + riverpod + go_router + Dio**,不引入新状态框架。新增 2 个能力包:
| 包 | 版本(建议) | 用途 |
|----|------|------|
| `mobile_scanner` | ^5.2.0 | 跨平台相机扫码 + 相册识别二维码(替代华为 ScanKit) |
| `image_picker` | ^1.1.2 | 启动图/Logo 图片选择(相册/拍照) |
> 注:`mobile_scanner` 需在 `AndroidManifest.xml`/`Info.plist`/`ohos` 配置相机权限,本设计只到「接口与组件占位」层级,权限字符串由工程师按平台补。
### 1.3 pubspec.yaml 改动
```yaml
dependencies:
# 现有:flutter / go_router / flutter_riverpod / dio / intl
mobile_scanner: ^5.2.0
image_picker: ^1.1.2
flutter:
assets:
- assets/data/MCityPiker/province.json
- assets/theme/ # 新增:内置主题占位图(如 theme_1.png ...)
```
### 1.4 架构模式
保持现有 **Widget + Service + Provider** 分层,无新增抽象层:
- 模型(`lib/models/`):`StoreInfo` 扩展字段
- 服务(`lib/services/`):`OrgService`/`DeviceService` 新增预留 TODO 方法
- 组件(`lib/widgets/`):`RegionPicker`、`StoreThemeGrid`、`BrandingEditor`
- 页面(`lib/screens/`):门店表单接入、设备扫码入口
---
## 2. 文件列表(新增 / 修改,相对路径)
### 新增文件
- `lib/data/region_data.dart` — 加载 `province.json` 并解析为 `RegionNode` 树;提供 `provinceName(code)`/`cityName`/`districtName` 反查工具
- `lib/models/store_theme.dart` — `StoreTheme` 枚举 + 内置主题清单(`StoreThemePresets`)
- `lib/widgets/region_picker.dart` — 省→市→区三级联动底部弹起选择器
- `lib/widgets/store_theme_grid.dart` — 主题皮肤网格卡片 + 选中态
- `lib/widgets/store_theme_preview.dart` — 模拟主页头部预览(依据 `themeId`)
- `lib/widgets/branding_editor.dart` — 启动图/Logo 卡片:选图 + 手机外框预览
- `lib/screens/device/device_scan_page.dart` — 扫码页(`mobile_scanner` 封装,含相册识别)
- `lib/utils/store_local_cache.dart` — 门店扩展字段本地持久化(SharedPreferences 封装,存 `themeId`/`branding` 等)
### 修改文件
- `lib/models/store_info.dart` — 增加 `themeId`/`splashUrl`/`logoUrl`/`provinceCode`/`cityCode`/`districtCode`/`addressDetail` + `fromJson`/`toJson` 兼容
- `lib/screens/store/store_list_page.dart` — `_StoreFormSheet` 接入 `RegionPicker` + 主题 + `BrandingEditor`;详情 sheet 展示主题/品牌
- `lib/screens/home/device_list_page.dart` — AppBar 加扫码入口 → 跳 `device_scan_page`
- `lib/screens/home/device_detail_page.dart` — 加「扫二维码绑定」按钮 → 跳 `device_scan_page`(`mode=bind`)
- `lib/services/org_service.dart` — 加 `saveStore`/`updateStore`/`setStoreTheme`/`setStoreBranding` TODO
- `lib/services/device_service.dart` — 加 `bindDeviceByScan` TODO
- `lib/app/router.dart` — 注册 `/device/scan` 路由(含 `mode` 参数)
- `pubspec.yaml` — 加依赖 + `assets` 注册(见 1.3)
---
## 3. 数据模型与接口(类结构)
### 3.1 类图(Mermaid classDiagram)
```mermaid
classDiagram
class StoreInfo {
+String orgId
+String name
+String address
+String contactPhone
+String activationCode
+int status
+DateTime? expireTime
+DateTime? createTime
<<新增>>
+String? themeId
+String? splashUrl
+String? logoUrl
+String? provinceCode
+String? cityCode
+String? districtCode
+String? addressDetail
+StoreInfo.fromJson(Map)
+Map toJson()
+String get regionText()
}
class RegionNode {
+String code
+String name
+List~RegionNode~ sub
+RegionNode.fromJson(Map)
}
class StoreTheme {
<<enum>>
classic
ocean
forest
sunset
mono
+String get id
+String get label
+Color get primary
}
class StoreThemePresets {
<<const>>
+List~StoreTheme~ all
+StoreTheme? byId(String)
}
class RegionData {
-List~RegionNode~ _tree
+Future~void~ load()
+List~RegionNode~ get provinces
+List~RegionNode~ citiesOf(String)
+List~RegionNode~ districtsOf(String,String)
+String nameOf(String)
+String tryMapAddress(String)~List~String?~
}
class StoreLocalCache {
+Future~void~ saveTheme(String orgId, String themeId)
+Future~String?~ getTheme(String orgId)
+Future~void~ saveBranding(String orgId, String splashUrl, String logoUrl)
+Future~Map?~ getBranding(String orgId)
+Future~void~ saveRegion(String orgId, String p, String c, String d, String detail)
}
class OrgService {
+Future~void~ saveStore(Map) // TODO 对接 POST /sys/store/save
+Future~void~ updateStore(Map) // TODO 对接 POST /sys/store/update
+Future~void~ setStoreTheme(String,String) // TODO POST /sys/store/theme
+Future~void~ setStoreBranding(String,String,String) // TODO POST /sys/store/branding
}
class DeviceService {
+Future~void~ bindDeviceByScan(String scanPayload, String storeId, String? groupId) // TODO POST /device/bind
}
class DeviceScanPage {
+String mode // 'query' | 'bind'
+String? storeId
+String? groupId
}
StoreInfo "1" *-- "1" StoreTheme : themeId 关联
RegionData "1" *-- "0..*" RegionNode : 树
OrgService ..> StoreInfo : 读写
DeviceService ..> DeviceScanPage : 扫码结果
```
### 3.2 关键模型说明
**StoreInfo 新增字段**(全部可空,向后兼容既有后端响应):
```dart
String? themeId; // 主题枚举 id,如 'ocean'
String? splashUrl; // 启动图本地路径 / 远程 URL(占位)
String? logoUrl; // Logo 本地路径 / 远程 URL(占位)
String? provinceCode; // 省 code(6 位)
String? cityCode; // 市 code
String? districtCode; // 区 code
String? addressDetail; // 街道/门牌等详情文本
// 拼接展示:regionText = 省名+市名+区名+addressDetail
```
**预留 Service 方法签名(含 TODO)**:
```dart
// OrgService — 当前后端无门店 save/update/theme/branding 接口
Future<void> saveStore(Map<String, dynamic> data) async {
// TODO(后端): POST /sys/store/save,body 含 themeId/splashUrl/logoUrl/省市区
// 前端占位:仅写 StoreLocalCache,不发起真实请求
}
Future<void> setStoreTheme(String orgId, String themeId) async {
// TODO(后端): POST /sys/store/setTheme {orgId, themeId}
}
Future<void> setStoreBranding(String orgId, String splashUrl, String logoUrl) async {
// TODO(后端): POST /sys/store/setBranding {orgId, splashUrl, logoUrl}
// 注意:真实上传需先调 uploadAsset 拿 URL
}
// DeviceService — 当前后端无扫码绑定接口
Future<void> bindDeviceByScan(String scanPayload, String storeId, String? groupId) async {
// TODO(后端): POST /device/bind {scanPayload, storeId, groupId}
// 前端占位:本地匹配设备后用 updateDevice 写 groupId/storeName
}
```
---
## 4. 程序调用流程(时序,文字描述)
### 4.1 门店编辑 — 省市区 + 主题 + 品牌(主流程)
1. 用户点「新增/编辑门店」→ `_StoreFormSheet` 打开。
2. 点「地址」→ `RegionPicker` 从 `RegionData`(已加载 `province.json`)弹起三级选择 → 存 `provinceCode/cityCode/districtCode` + 回填文本到 `addressDetail`。
3. 点「主题皮肤」→ `StoreThemeGrid` 展示 `StoreThemePresets.all` → 选 `themeId` → `StoreThemePreview` 实时预览模拟头部。
4. 点「启动图/Logo」→ `BrandingEditor` 调 `image_picker` 选图 → 手机外框预览 → 存本地路径到 `splashUrl/logoUrl`。
5. 点「保存」→ `_StoreFormSheet._save()` 组装 data → 调 `OrgService.saveStore`(占位写 `StoreLocalCache`,并留 `setStoreTheme`/`setStoreBranding` TODO)→ 关闭 sheet,`_loadData()` 刷新。
### 4.2 设备扫码 — 查询模式
1. 设备列表 AppBar 扫码图标 → `context.go('/device/scan?mode=query')`。
2. `DeviceScanPage` 用 `mobile_scanner` 实时扫码(或相册识别)→ 得到 `scanPayload`(设备标识,如 mac/sn)。
3. 本地用 `DeviceService.fetchDevices(mac:...)` 或列表缓存查找 → 命中则 `context.go('/device/:id')` 跳详情;未命中提示「未找到设备」。
### 4.3 设备扫码 — 绑定模式
1. 设备详情「扫二维码绑定」→ `context.go('/device/scan?mode=bind&deviceId=xxx')`。
2. 扫码得到目标门店/分组码 → 弹窗确认「绑定到当前门店/分组」→ `DeviceService.bindDeviceByScan`(占位:本地 `updateDevice` 写 `groupId`/`storeName`,留真实 POST TODO)。
3. 返回详情,展示更新后的归属。
### 4.4 既有门店地址回显
- 打开编辑既有门店:`_StoreFormSheet.initState` 若 `store.provinceCode` 非空 → `RegionPicker` 直接定位;若为空但有旧文本 `address` → `RegionData.tryMapAddress()` 尽力用关键词匹配省/市/区,匹配不到则仅填 `addressDetail` 文本。
---
## 5. 有序任务列表(按实现顺序,含依赖)
> 规则:不超过 5 个任务,每任务 ≥3 文件,首个为基础设施。
| 任务 | 名称 | 源文件 | 依赖 | 优先级 |
|------|------|--------|------|--------|
| **T01** | 项目基础设施(依赖 + assets + 数据/主题基座) | `pubspec.yaml`、`lib/data/region_data.dart`、`lib/models/store_theme.dart`、`lib/utils/store_local_cache.dart` | — | P0 |
| **T02** | 扩展 StoreInfo 模型 + 预留 Service 接口 | `lib/models/store_info.dart`、`lib/services/org_service.dart`、`lib/services/device_service.dart` | T01 | P0 |
| **T03** | 门店表单增强组件(省市区 + 主题 + 品牌) | `lib/widgets/region_picker.dart`、`lib/widgets/store_theme_grid.dart`、`lib/widgets/store_theme_preview.dart`、`lib/widgets/branding_editor.dart` | T01、T02 | P1 |
| **T04** | 门店表单接入 + 设备扫码模块 + 路由 | `lib/screens/store/store_list_page.dart`、`lib/screens/home/device_list_page.dart`、`lib/screens/home/device_detail_page.dart`、`lib/screens/device/device_scan_page.dart`、`lib/app/router.dart` | T03、T02 | P1 |
| **T05** | 构建验证与联调(编译/权限/占位跑通) | 复用上述全部文件 + `android/Info.plist/ohos` 权限配置 | T04 | P2 |
### 任务依赖图
```mermaid
graph TD
T01[T01 基础设施] --> T02[T02 模型+Service]
T01 --> T03[T03 组件]
T02 --> T03
T03 --> T04[T04 接入+扫码+路由]
T02 --> T04
T04 --> T05[T05 构建验证]
```
---
## 6. 依赖包列表
```
- mobile_scanner@^5.2.0 : 跨平台相机/相册二维码扫码(替代华为 ScanKit)
- image_picker@^1.1.2 : 启动图/Logo 本地图片选择(相册/拍照)
# 以下为现有包(已存在,仅列示上下文)
- flutter / go_router@^14.2.0 / flutter_riverpod@^2.5.1 / dio@^5.4.3 / intl@^0.19.0
```
---
## 7. 共享知识(跨文件约定)
- **province.json 加载**:统一由 `lib/data/region_data.dart` 的 `RegionData.load()` 懒加载并缓存为 `List<RegionNode>` 树;组件禁止直接 `rootBundle` 读取,避免重复解析(文件 284KB)。
- **主题枚举位置**:`lib/models/store_theme.dart` 定义 `StoreTheme` 枚举与 `StoreThemePresets`;所有主题相关 UI 仅引用枚举 id(`themeId`),不硬编码颜色。
- **预留接口 TODO 规范**:所有待对接后台的方法必须含 `// TODO(后端): POST /sys/...` 注释,写明预期路径与 body;前端占位实现只写 `StoreLocalCache`,并 `print`/debug 标注「占位未上报」。
- **本地持久化**:门店扩展字段统一经 `StoreLocalCache`(SharedPreferences,key 形如 `store_theme_$orgId`),不在内存态以外散落存储。
- **扫码 payload**:约定扫码内容优先解析为 `mac` 或 `deviceSn`;无法识别时原样透传,由 `DeviceService` 模糊匹配。
- **图片路径**:`image_picker` 返回的 `XFile.path` 直接存 `splashUrl/logoUrl`(占位);真实上传走 `uploadAsset` 后再替换为 URL(TODO)。
- **路由参数**:`/device/scan?mode=query|bind&deviceId=xxx&storeId=xxx`,由 `device_scan_page` 解析。
---
## 8. 待明确事项
1. **主题占位资源形式**:内置纯色块 + 名称,还是需要 `assets/theme/*.png` 缩略图?当前设计按「纯色块 + 名称」实现,若需图片请在 T01 前提供 4–5 张占位图。
2. **扫码绑定语义**:「绑定到当前门店/分组」中「当前门店」取自设备详情页的 `storeName` 还是用户手动选?本设计默认取详情页所属门店,绑定弹窗可改分组。
3. **省市级联回显映射**:既有纯文本地址(如「北京市朝阳区xx路」)能否映射到 code 取决于关键词命中,映射不到仅回填 `addressDetail`,是否接受?
4. **鸿蒙权限**:`mobile_scanner` 在 ohos 平台的相机权限声明方式需工程师按鸿蒙规范确认(本设计不含具体 ohos 权限代码)。
5. **本地缓存生命周期**:`StoreLocalCache` 数据在重新登录/切换组织时是否清空?默认不清,如需隔离按 `orgId` 已做 key 隔离,可后续细化。
---
> 交付完毕:本设计为纯架构/任务分解,**不含实现代码**。工程师按 T01→T05 顺序落地即可。