docs: 护理员 APP Flutter 实施计划

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-15 08:42:47 +08:00
parent ff3b6c5042
commit e8a702eb93

View File

@ -0,0 +1,159 @@
# 护理员 APP(Flutter)实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在 `sources/ec_flutter`(全新 Flutter 工程)落地护理员 APP:登录、M1 今日任务、M2 服务执行(签到/勾项目/拍照/签退)、异常上报、扑空、离线队列+自动补传。
**Architecture:** 全新客户端,feature-first。Riverpod(状态)+ dio(HTTP)+ sqflite(离线队列)+ go_router(路由)+ shared_preferences(token)。对接 C3 后端(无后端改动)。
**Tech Stack:** Flutter 3.38 + Dart 3.10、riverpod、dio、sqflite、go_router。
**Spec:** `openspec/changes/nurse-app-flutter/`;设计背景 `docs/superpowers/specs/2026-10-15-护理员APP-Flutter-design.md`。
## Global Constraints
- 后端 base `http://localhost:3000/api/v1`(用 `--dart-define=API_BASE_URL=` 可覆盖)。
- 统一响应 `{code,message,data}`:code=0 成功、非 0 抛业务错误(message 含规则号 3001/1001/2001/3003)。
- token 走 `Authorization: Bearer`;token 落 shared_preferences。
- 签到/签退时间**不传客户端值**(服务器取时);定位失败不阻塞(标「未获取」)。
- exec_result 枚举 `done/undo_not_done/undo_refused`(禁用 not_done)。
- 必拍未拍:前端签退按钮前置校验 + 后端 3001 双保险;拒拍出路(选原因放行)。
- 措辞纪律:留痕口径「签到/签退/服务执行」,禁「打卡/考勤」;异常不禁「报警」。
- 界面大字 ≥20pt、常态 ≤3 击、首屏即任务、误操作可撤销。
## Review Focus
1. **服务器取时**:checkin/checkout 请求体不得含 checkin_time/checkout_time/duration_min。
2. **必拍未拍**:必拍项未拍照时签退按钮禁用;拒拍出路选原因后放行。
3. **离线幂等**:每条离线记录生成 client_md5(内容 hash),补传按 md5 幂等,不重复。
4. **exec_result 措辞**:未执行用 undo_not_done(非 not_done);老人拒做用 undo_refused。
5. **扑空证据**:abnormal_type=not_met 时到访时间/定位/原因必填,缺一不可提交。
---
### Task 1: 工程骨架(依赖 + core)
**Files:** `sources/ec_flutter/pubspec.yaml`、`lib/core/**`、`lib/main.dart`。
- [ ] **Step 1: pubspec.yaml 加依赖**
```yaml
dependencies:
flutter: { sdk: flutter }
cupertino_icons: ^1.0.8
flutter_riverpod: ^2.5.1
dio: ^5.4.0
sqflite: ^2.3.0
shared_preferences: ^2.2.2
go_router: ^14.0.0
path_provider: ^2.1.2
path: ^1.9.0
image_picker: ^1.0.7
geolocator: ^12.0.0
crypto: ^3.0.3
```
- [ ] **Step 2: `lib/core/api/api_client.dart`**:dio 实例(baseUrl 从 `String.fromEnvironment('API_BASE_URL', defaultValue:'http://localhost:3000/api/v1')`);拦截器加 `Authorization: Bearer <token>`;响应拦截器解包 `{code,message,data}`(code≠0 抛 `ApiException(code,message)`)。
- [ ] **Step 3: `lib/core/api/`**:`auth_api.dart`(login)、`order_api.dart`(today)、`record_api.dart`(checkin/itemSave/photoUpload/checkout/offlineSync)、`abnormal_api.dart`(report)。
- [ ] **Step 4: `lib/core/auth/auth_provider.dart`**:token 存取(shared_preferences)+ 登录状态 provider。
- [ ] **Step 5: `lib/core/router.dart`**:go_router 路由(login/today/service/abnormal)。
- [ ] **Step 6: `lib/core/theme.dart`**:Material 3 主题,正文 ≥20pt。
- [ ] **Step 7: `lib/main.dart`**:ProviderScope + MaterialApp.router。
- [ ] **Step 8: `flutter analyze` 干净 + commit**
```bash
git -C sources/ec_flutter add -A
git -C sources/ec_flutter commit -m "feat(nurse): 工程骨架(dio/riverpod/sqflite/router/theme)"
```
---
### Task 2: 登录 + M1 今日任务
**Files:** `lib/features/auth/**`、`lib/features/today/**`。
- [ ] **Step 1: 登录页**:租户 code + 账号 + 密码三个输入框 → `POST /auth/login`(body `{tenantCode, username, password}`,返回 token)→ 存 token → `context.go('/today')`。登录失败显示 `ApiException.message`。
- [ ] **Step 2: 今日任务页**:`GET /order/today` → 卡片列表(老人名/时段/项目名/工单状态),空态「今日暂无任务」,点卡片 `context.go('/service?orderId=...')`。
- [ ] **Step 3: flutter analyze + widget test(登录/今日任务基本渲染)+ commit**
```bash
git -C sources/ec_flutter commit -am "feat(nurse): 登录 + M1 今日任务"
```
---
### Task 3: M2 服务执行(签到/勾项目/拍照/签退)
**Files:** `lib/features/service/**`。
- [ ] **Step 1: 签到**:超大「签到」按钮 → geolocator 取定位(失败标「未获取」不阻塞)→ `POST /record/checkin`(body `{orderId, locateStatus, checkinLat?, checkinLng?}`,**不传 checkin_time**)→ 记 recordId。
- [ ] **Step 2: 勾项目**:项目列表(从 order.itemList 或 GET /order/:id),默认全勾 `done`,可改为 `undo_not_done`/`undo_refused`(老人拒做)→ `POST /record/item/save`(body `{recordId, items:[{svcId, execResult}]}`)。
- [ ] **Step 3: 拍照**:必拍项(photoTier==='must')显式标「需拍照」→ image_picker 拍照 + 压缩 ≤500KB → `POST /record/photo/upload`(multipart,file + recordId + svcId)→ 压疮项附结构化标签(部位 9 选/分期 6 选,可跳过);「无法拍摄」出路(选原因)→ 该项放行。
- [ ] **Step 4: 签退**:超大「签退」按钮——前置校验「必拍项已拍或已拒拍」,未满足则禁用+提示「这项需要拍一张照片」→ `POST /record/checkout`(body `{recordId}`,**不传 checkout_time/duration_min**)→ 成功回今日任务。
- [ ] **Step 5: 测试 + analyze + commit**
```bash
git -C sources/ec_flutter commit -am "feat(nurse): M2 服务执行(签到/勾项目/拍照/签退)"
```
---
### Task 4: 异常上报 + 扑空
**Files:** `lib/features/abnormal/**`。
- [ ] **Step 1: 异常上报**:9 宫格分类(必选,dict abnormal_type)+ 描述/语音/照片(可选)→ `POST /abnormal`(body `{abnormalType, abnormalDesc?, recordId?}`)。
- [ ] **Step 2: 扑空**:服务执行页「未遇」入口 → 到访时间(自动)/定位(自动)/原因(必选下拉:老人不在/拒绝服务/临时改期)→ `POST /abnormal`(body `{abnormalType:'not_met', orderId, elderId, visitTime, visitLat, visitLng, abnormalDesc}`)→ 工单转 not_met。
- [ ] **Step 3: 测试 + commit**
```bash
git -C sources/ec_flutter commit -am "feat(nurse): 异常上报 + 扑空"
```
---
### Task 5: 离线队列
**Files:** `lib/core/db/**`、`lib/core/offline/**`。
- [ ] **Step 1: sqflite 队列表**:`offline_queue(id, type, payload_json, client_md5, created_at, synced)`;本地照片文件存 path_provider 临时目录。
- [ ] **Step 2: 落队逻辑**:签到/签退/勾项目/照片/异常在无网时写本地队列 + 生成 `client_md5`(crypto md5 对 payload 内容 hash);有网时直接走正常 API。
- [ ] **Step 3: 补传**:联网检测(dio 错误判定)→ 逐条 `POST /record/offline/sync`(body 含 client_md5 + 完整记录)→ 成功标 synced;照片补传后保留 ≥7 天(本地文件)。
- [ ] **Step 4: 测试 + commit**
```bash
git -C sources/ec_flutter commit -am "feat(nurse): 离线队列 + 自动补传"
```
---
### Task 6: 验证
- [ ] **Step 1:** `flutter analyze` 0 error + `flutter test` 全绿。
- [ ] **Step 2:** 起后端,用 `nurse01`/`Touyun@123`/`TY20261015` 冒烟(模拟器或 web):登录 → 今日任务(张三工单)→ 签到 → 勾项目 → 拍照 → 签退 → 后端留痕 submitted;异常上报/扑空落库;断网签到恢复补传。
- [ ] **Step 3:** 停后端清理。
---
## Self-Review Notes
- Spec 覆盖:frontend/nurse-app 7 Requirement → T2(登录/M1)、T3(M2/拍照)、T4(异常/扑空)、T5(离线)。
- Review Focus 5 条均有测试/校验(T3/T4/T5)。
- 类型一致:`ApiClient`/`ApiException`/API 方法名在 T1 定义、T2–T5 使用一致。