Files
CloudHome-Ops/docs/intro_v38_design.md

17 KiB
Raw Blame History

公司介绍(Company Intro)模块增强 — 架构设计文档

版本演进:v6-fix37 → v6-fix38 模块:lib/screens/admin/intro/*(公司介绍 / 类型编辑 / 内容编辑) 后端:https://ips-test.ainetv.com(测试),合同参考 screen-front/src/api/companyIntro.ts 目标:多门店筛选、表单字段与 Web 后台对齐、内容图片上传预览、兼容既有代码风格。


1. Scope & Assumptions(范围与假设)

1.1 范围(In Scope)

  • 大类列表页:门店多选筛选(客户端按 orgId 过滤)。
  • 类型编辑(Category) 表单对齐 Web:新增 orgId/orgName 选择;移除 Web 不存在的 status 字段。
  • 内容编辑(Subcategory / Detail) 表单对齐 Web:补齐 sortOrder、修正 status 语义、Subcategory 补齐可选的 categoryType。
  • 内容编辑支持 图片上传 + 本地预览 + 回写 mediaUrl。
  • save / update 按 id 是否存在分流,对齐 Web 后台。

1.2 假设(Assumptions)

  • A1. 分类列表已全量加载(getIntroCategoryList(limit: 200)),客户端多选过滤足够;后端 list 接口仅支持单 orgId,不支持多门店服务端过滤,故采用客户端过滤。
  • A2. 门店数据源复用既有 OrgService.getStoreListByLoginOrg()(返回 List<StoreInfo>,含 orgId/name),替代当前从分类推导 _orgMap 的脆弱写法。
  • A3. 图片上传后端端点假定为 POST /sys/upload(表单字段名 file,返回 URL 字符串或 {url})。确切端点与返回结构待后端确认(见 §9 风险)。若端点不可用,保留手动 mediaUrl 输入作为兜底。
  • A4. 描述(description)为纯文本字段,不做富文本图片内嵌;"插入图片" 等价于上传并写入 mediaUrl + mediaType='image'(与 Web 模型一致)。
  • A5. 不改动后端、不改 pubspec 依赖(image_picker、dio 已具备)。
  • A6. 既有 ApiInterceptor 已统一解包 {code,msg,data} 信封并附带 Bearer 令牌,新上传请求复用 ApiClient.instance.dio 即可,无需额外鉴权处理。

2. File Changes List(文件变更清单)

相对路径 变更类型 说明
lib/utils/i10n.dart 修改 新增门店筛选 / 表单字段 / 上传相关 i18n 键(见 §8)。
lib/models/intro_category.dart 修改 修正 status 语义(= 后端原值,1=启用/0=禁用),删除 _parseStatus 反转逻辑;模型字段本身已含 orgId/orgName/sort,无需新增字段。
lib/services/intro_service.dart 修改 saveIntroCategory / saveIntroSubcategory / saveIntroDetail 按 id 是否存在分流 /save↔/update(对齐 Web)。
lib/services/upload_service.dart 新增 UploadService.uploadImage(XFile):Dio FormData 多部件上传,返回 URL 字符串。
lib/screens/admin/intro/intro_category_page.dart 修改 (a) 门店筛选改为 FilterChip 多选;(b) 大类表单加门店下拉、移除 status;(c) 小类/详情表单加 sortOrder、status 对齐、图片上传+预览;(d) initState 预载门店列表。
lib/services/org_service.dart 复用(不改) 复用 getStoreListByLoginOrg() 提供门店列表。
lib/models/store_info.dart 复用(不改) 复用 StoreInfo.orgId / StoreInfo.name。

变更集中在 1 个页面 + 2 个 service + 1 个 model + 1 个 i18n,符合"最小侵入、兼容既有风格"。


3. Data Model / Field Changes(数据模型与字段变更)

3.1 后端字段 vs 当前 Flutter 缺口对照

实体 后端字段(companyIntro.ts) 当前 Flutter 缺口
Category id, orgId, orgName, categoryType, name, createTime, updateTime 表单缺 orgId/orgName 选择;表单多出 Web 不存在的 status
Subcategory id, categoryId, categoryType, mediaType, mediaUrl, title, description, status, sortOrder 表单缺 sortOrder;status 语义反转;缺 categoryType(可选);无图片上传
Detail id, subcategoryId, subcategoryTitle, mediaType, mediaUrl, title, description, status, sortOrder 表单缺 sortOrder;status 语义反转;无图片上传

3.2 IntroCategory / IntroSubcategory / IntroDetail 模型变更

  • status 语义统一为后端原值:1=启用(正常)、0=禁用。
    • 模型 _parseStatus(dynamic) 改为:int.tryParse(value?.toString() ?? '') ?? 1(缺省启用,对齐 Web ?? 1);删除 "1→0 / 0→1" 反转映射。
    • toJson() 输出 status 原值,保存时不再反转。
  • sort 字段(已存在,映射 sortOrder)保持不变;仅需在表单中暴露编辑(见 §5)。
  • orgId / orgName(已存在)保持不变;仅需在 Category 表单中可编辑并随保存上送。
  • 模型不再需要新增字段;categoryType(Subcategory 可选)可由表单 TextEditingController 直接读取父级大类值,无需模型新增。

3.3 新增 UploadService(伪代码,非实现)

class UploadService {
  final Dio _dio;                       // = ApiClient.instance.dio
  Future<String> uploadImage(XFile file) async {
    // FormData: { 'file': MultipartFile.fromFile(file.path, filename: basename) }
    // POST /sys/upload  (复用 ApiInterceptor 解包)
    // 返回 data 为 URL 字符串,或 {url: '...'} 时取 url
  }
}

4. API Call Changes(接口调用变更)

4.1 IntroService 保存分流(对齐 Web save* / update*)

方法 当前 目标
saveIntroCategory(data) 始终 POST /company/introCategory/save id 非空 → …/update,否则 …/save
saveIntroSubcategory(data) 始终 …/save 同上(含 categoryId, categoryType?, mediaType, mediaUrl, title, description, status, sortOrder)
saveIntroDetail(data) 始终 …/save 同上(含 subcategoryId, mediaType, mediaUrl, title, description, status, sortOrder)

端点、list/info/delete 不变;list 仍客户端过滤(见 A1)。

4.2 新增上传调用

  • UploadService.uploadImage(XFile) → POST /sys/upload(多部件)。(端点待确认,见 §9)

4.3 门店列表(复用,不改写)

  • OrgService().getStoreListByLoginOrg() → GET /sys/store/listByLoginOrg,用于筛选 chips 与 Category 表单门店下拉。

5. UI/UX Changes(界面与交互变更)

5.1 大类列表 — 多门店筛选

  • 替换现有单选 DropdownButton<String?> 为 Wrap + FilterChip 多选:
    • 一项 All("全部门店");其余每项一个门店,selected = _selectedOrgIds.contains(orgId)。
    • 空选集合 = 全部(与现有"全部门店"语义一致)。
  • 状态:List<String> _selectedOrgIds = []。
  • _applyOrgFilter():_selectedOrgIds.isEmpty ? _allCategories : _allCategories.where((c) => _selectedOrgIds.contains(c.orgId)).toList()。
  • 门店来源:initState 中 OrgService().getStoreListByLoginOrg() 构建 Map<orgId, name>(替代从分类推导,更完整)。

5.2 类型编辑(Category)表单

  • 新增 门店下拉 DropdownButtonFormField<String>(必填):选项来自门店列表,value=orgId,联动 orgName。
  • 保留 name、categoryType(类型编码,纯字符串无中文)。
  • 移除 status 下拉(Web 大类无 status 字段)。
  • 保存 body:{id?, orgId, orgName, categoryType, name}(无 status/sort)。

5.3 小类(Subcategory)/ 详情(Detail)表单

  • 新增 sortOrder:TextField(keyboardType: TextInputType.number),上送 sortOrder。
  • status 对齐:下拉项 1=启用/正常、0=禁用,默认 editing?.status ?? 1;保存上送原值(不再反转)。
  • 小类新增可选 categoryType:TextField,预填父大类 categoryType(Web 允许与大类一致)。
  • 媒体区改造(图片上传):
    • 保留 mediaType 下拉(image/video)与 mediaUrl 手动输入(兜底)。
    • 新增「上传图片」按钮:ImagePicker.pickImage(source: gallery) → 本地 Image.file 预览 → 调 UploadService.uploadImage → 成功后回填 mediaUrlCtrl.text=url 且 mediaType='image',并切换为 Image.network(url) 预览。
    • 上传中显示 CircularProgressIndicator + introUploading 文案;失败 SnackBar(introUploadFailed)。
  • 列表 tile 的 status Chip 文案改为 status==1 ? introEnabled : introDisabled。

5.4 交互一致性

  • 继续复用现有 showModalBottomSheet + StatefulBuilder 表单骨架、PopupMenuButton 编辑/删除、ScaffoldMessenger 提示,保持与既有代码风格一致。

6. Image Upload Implementation Approach(图片上传实现方案)

  1. 选型:复用既有 image_picker(相册选择)+ dio(FormData 多部件)。不引入新依赖。
  2. 服务层:新增 UploadService(lib/services/upload_service.dart),封装 POST /sys/upload,复用 ApiClient.instance.dio(自动带令牌、解包信封、401 刷新)。
  3. 预览:
    • 选中后本地即时预览:Image.file(File(xFile.path))(需 dart:io)。
    • 上传成功回填 URL 后:Image.network(url) 预览(与现有 tile 预览一致)。
  4. 存储:上传返回 URL 字符串写入 mediaUrlCtrl → 随 save* 上送 mediaUrl + mediaType='image';不本地持久化原文件路径(与 Web 模型一致)。
  5. 兜底:保留 mediaUrl 手动输入,支持直接填视频/已托管图片地址;上传失败不阻断保存(可后补)。
  6. 文件名:用 xFile.path.split(Platform.pathSeparator).last 取 basename,避免引入 path 包。

注意:上传端点 /sys/upload 为假设,返回结构(字符串 vs {url})需与后端确认;UploadService 两种形态均兼容。


7. Incremental Task List(增量任务清单,按实现顺序)

ID 任务 涉及文件 依赖 优先级
T1 i18n 键新增:门店筛选/表单字段/上传文案(§8 全部键) lib/utils/i10n.dart — P0
T2 模型 status 语义修正:_parseStatus 改为后端原值(1=启用),删除反转;toJson 不再反转 lib/models/intro_category.dart T1 P0
T3 新增 UploadService.uploadImage(Dio 多部件,兼容字符串/{url} 返回) lib/services/upload_service.dart(新) — P1
T4 IntroService 保存分流:三个 save* 按 id 是否存在走 /save↔/update lib/services/intro_service.dart — P0
T5 多门店筛选 UI:FilterChip 多选 + initState 预载 getStoreListByLoginOrg 构建门店 map + _applyOrgFilter 改为多值过滤 lib/screens/admin/intro/intro_category_page.dart T1 P0
T6 大类表单对齐:加门店下拉(必填,orgId/orgName 联动)、移除 status lib/screens/admin/intro/intro_category_page.dart T2, T4, T5 P0
T7 小类表单对齐:加 sortOrder、categoryType(预填)、status 对齐(1=启用)、图片上传+预览 lib/screens/admin/intro/intro_category_page.dart T2, T3, T4 P0
T8 详情表单对齐:加 sortOrder、status 对齐、图片上传+预览、tile 状态文案改用 introEnabled/introDisabled lib/screens/admin/intro/intro_category_page.dart T2, T3, T4 P0
T9 联调与冒烟:flutter analyze + 真机/模拟器跑通多店筛选、增改、图片上传,v6-fix38 构建 全部 T1–T8 P1

依赖示意:T1→T2→{T6,T7,T8};T4→{T6,T7,T8};T3→{T7,T8};T5→T6。


8. i18n Key Additions(需新增的 i18n 键)

新增于 lib/utils/i10n.dart 的「公司介绍」分区(中 / 英):

Key 中文 English 用途
introFilterStores 筛选门店 Filter Stores 多选 chip 区标题
introSelectStoresHint 可多选门店 Select multiple stores 空态提示
introAllStores 已存在 已存在 全部门店(复用)
introCategoryStore 所属门店 Store 大类表单门店下拉 label
introStoreRequired 请选择所属门店 Please select a store 门店必填校验
introSortOrder 排序号 Sort Order 小类/详情 sortOrder label
introEnabled 启用 Enabled status==1 显示
introDisabled 禁用 Disabled status==0 显示
introUploadImage 上传图片 Upload Image 图片上传按钮
introPickImage 选择图片 Pick Image 选择前占位按钮
introImagePreview 图片预览 Image Preview 预览区标题
introUploading 上传中… Uploading… 上传 loading 文案
introUploadFailed 图片上传失败 Image upload failed 上传失败提示
introInsertImage 插入图片 Insert Image (可选)上传并写入 mediaUrl 按钮

注:statusNormal/statusDisabled 已存在但语义与 Web 相反,改用新增的 introEnabled/introDisabled 以避免歧义;如团队倾向复用既有键,可二选一,但需同步修正显示判断。


9. Potential Risks / Open Questions(风险与待确认项)

  1. 上传端点未知(高):/sys/upload 为假设;后端实际路径、字段名(file?)、返回结构(字符串 / {url} / {data})需后端确认。UploadService 已兼容常见形态,但仍可能需微调。
  2. status 反转回归风险(中):当前模型反转 + 表单再反转"巧合正确",修改后需重点回归小类/详情的显示与保存往返,确保 1=启用、0=禁用。
  3. 大类 status 移除(中):现有 Flutter 大类模型含 status 且仅列表展示;移除表单字段后,模型字段保留(防御性默认 0/1 不影响),但需确认后端 save 忽略该字段或忽略 null。
  4. 门店列表权限(低):getStoreListByLoginOrg 返回当前登录组织下门店;若运维账号可见全部门店则符合预期,否则筛选范围受登录态限制(与现有门店管理一致)。
  5. 分类超过 200 条(低):列表 limit:200 上限下,多选过滤可能漏掉未加载分类;属既有限制,v6-fix38 暂不处理分页服务端过滤(后端不支持多 orgId)。
  6. categoryType 必填性(低):Web 小类 categoryType 为可选,表单预填父大类值即可;若后端要求必填需补校验。
  7. 鸿蒙端 image_picker(低):image_picker 在 HarmonyOS 需对应插件适配;若不可用,图片上传在该端降级为手动 mediaUrl 输入(兜底已设计)。
  8. 视频上传(低):本期仅做图片上传;视频仍走 mediaUrl 手动输入(与 Web 一致),不做视频文件上传。

附录:关键关系与流程(Mermaid)

A.1 类 / 服务关系

classDiagram
    class IntroCategoryPage {
        +List~String~ _selectedOrgIds
        +Map~String,String~ _orgMap
        +_buildOrgFilter() FilterChip
        +_showCategoryForm() 加门店/去status
        +_showSubcategoryForm() 加sortOrder+上传
        +_showDetailForm() 加sortOrder+上传
    }
    class IntroService {
        +saveIntroCategory(data)  /save|/update
        +saveIntroSubcategory(data) /save|/update
        +saveIntroDetail(data) /save|/update
    }
    class UploadService {
        +uploadImage(XFile) String
    }
    class OrgService {
        +getStoreListByLoginOrg() List~StoreInfo~
    }
    class StoreInfo {
        +String orgId
        +String name
    }
    class IntroCategory {
        +String orgId
        +String orgName
        +int status  // =后端原值 1启用/0禁用
    }
    class IntroSubcategory {
        +int sort  // sortOrder
        +int status
    }
    class IntroDetail {
        +int sort
        +int status
    }
    IntroCategoryPage ..> IntroService : 保存分流
    IntroCategoryPage ..> UploadService : 图片上传
    IntroCategoryPage ..> OrgService : 门店列表
    OrgService ..> StoreInfo
    IntroService ..> IntroCategory
    IntroService ..> IntroSubcategory
    IntroService ..> IntroDetail

A.2 小类保存 + 图片上传时序

sequenceDiagram
    participant U as 用户
    participant P as IntroCategoryPage
    participant UP as UploadService
    participant S as IntroService
    participant B as 后端

    U->>P: 打开小类表单,选图「上传图片」
    P->>P: ImagePicker.pickImage → 本地预览
    P->>UP: uploadImage(xFile)
    UP->>B: POST /sys/upload (FormData file)
    B-->>UP: {code,data: url}
    UP-->>P: url
    P->>P: mediaUrlCtrl=url, mediaType='image', 网络预览
    U->>P: 填 sortOrder/status/title → 保存
    P->>S: saveIntroSubcategory({id?,categoryId,...,sortOrder,status,mediaUrl})
    S->>B: POST /update (有id) 或 /save
    B-->>S: {code,data}
    S-->>P: 成功
    P->>P: 关闭表单 + 刷新列表