# Delivery 端完整实现方案 > 基于 5 轮需求采集 + 代码审计 > 后端 API:已有 86 个接口,`WorkOrderService` 的 checkIn/finish/reportException 已就绪 > 当前状态:12 个页面路由中仅实现 2 个(login + index),10 个缺失 --- ## 1. 需求与约束摘要 | 项 | 内容 | |---|---| | **任务类型** | 功能扩展 | | **目标** | 实现 delivery-miniapp 全功能:GPS签到 + 轨迹 + 拍照水印 + 执行记录 + 异常上报 + 签退 + 离线缓存 + 断点续传 | | **硬边界** | uni-app + Vue、可改后端 API、离线缓存、对象存储(MinIO/OSS)、双端条件编译、Toast 提示 | --- ## 2. 代码依据 | 文件 | 职责 | |------|------| | `delivery-miniapp/common/api.js` | 现有 API 封装:BASE_URL、getHeaders、apiGet、apiPost、idempotency key | | `delivery-miniapp/pages/delivery/login/login.vue` | 登录页实现参考(uni.request、token存储、switchTab) | | `delivery-miniapp/pages/delivery/index/index.vue` | 工作台实现参考(apiGet、navTo、状态映射) | | `delivery-miniapp/pages.json` | 12 个路由已注册 | | `delivery-miniapp/manifest.json` | GPS/Camera 权限已声明 | | `module/workorder/controller/WorkOrderController.java` | 7 个端点(assign/accept/reject/check-in/start/finish/exception) | | `module/workorder/controller/DeliveryController.java` | 5 个端点(workbench/today/detail/messages/read) | | `module/execution/controller/ExecutionController.java` | 证据上传(presignUpload/commit/signedUrl/list) | | `module/workorder/service/WorkOrderService.java` | checkIn(Haversine+200m)、finish(执行记录+签退+必做检查)、reportException | --- ## 3. 改动范围清单 ### 前端(delivery-miniapp) | 路径 | 增/删/改 | 摘要 | |------|----------|------| | `common/api.js` | 改 | 新增 getLocation/chooseImage/uploadFile/offlineQueue/watermark | | `common/offline.js` | 增 | 离线缓存队列 + 断点续传 | | `pages/delivery/checkin/checkin.vue` | 增 | GPS签到 + 拍照水印 + 对象确认 | | `pages/delivery/order-detail/order-detail.vue` | 增 | 工单详情(患者/项目/操作入口) | | `pages/delivery/execute/execute.vue` | 增 | 逐项执行记录 + 证据上传 | | `pages/delivery/exception/exception.vue` | 增 | 一键异常上报 | | `pages/delivery/finish/finish.vue` | 增 | 服务总结 + 签退定位 | | `pages/delivery/accept/accept.vue` | 增 | 接单/拒单确认 | | `pages/delivery/orders/orders.vue` | 增 | 工单列表(按状态筛选) | | `pages/delivery/offline-sync/offline-sync.vue` | 增 | 离线数据补传 | | `pages/delivery/messages/messages.vue` | 增 | 消息通知列表 | | `pages/delivery/profile/profile.vue` | 增 | 我的资质 + 统计 | ### 后端 | 路径 | 增/删/改 | 摘要 | |------|----------|------| | `module/workorder/controller/WorkOrderController.java` | 改 | checkIn 增加 `photoFileId` → 实际文件上传 URL | | `module/execution/service/EvidenceService.java` | 改 | commit 支持多种 fileType、增加真实文件上传 | | `module/workorder/controller/GpsController.java` | 增 | GPS 轨迹上报端点 | --- ## 4. 方案概述 ### 数据流 ``` Service Person opens app → login → workbench → orders list (filtered by status) → order detail (patient, items, actions) → accept order → GPS check-in (location + photo watermark + patient confirm) → start service → execute items (per-item recording + evidence upload) → [exception report if needed] → finish service (summary + sign-off location) → wait for acceptance & settlement Offline mode: Any POST → if network fails → save to offline queue → offline-sync page → manual retry with progress ``` ### 组件关系 ``` common/ api.js → getLocation(), chooseImage(), uploadFile(), apiGet(), apiPost() offline.js → OfflineQueue class (add, retry, clear) ``` --- ## 5. 代码级修改说明 ### 5.1 common/api.js — 扩展工具函数 **当前逻辑**:仅 `apiGet` / `apiPost`,无 GPS/拍照/上传能力。 **拟议改动**: 1. 新增 `getLocation()` — 封装 `uni.getLocation({ type: 'gcj02' })` 2. 新增 `chooseImage(count)` — 封装 `uni.chooseImage({ count, sizeType, sourceType })` 3. 新增 `uploadFile(filePath, presignResp)` — 封装 `uni.uploadFile` 上传到对象存储 4. 新增 `uploadEvidence(filePath, entityType, entityId)` — 完整 presign→upload→commit 流程 5. 新增 `addWatermark(tempFilePath, text)` — Canvas 绘制水印(时间+地点+工单号) 6. 新增 `gpsTrajectory` — 每 30 秒上报一次位置到后端 **建议代码**(新增内容追加到现有 api.js): ```js const BASE_URL = 'http://localhost:18080/api/hss'; // ... 保留现有的 getHeaders, generateIdempotencyKey, apiGet, apiPost ... // ==================== GPS ==================== /** 获取当前位置(GCJ-02坐标系,微信/高德兼容) */ function getLocation() { return new Promise((resolve, reject) => { // #ifdef MP-WEIXIN uni.getLocation({ type: 'gcj02', success: resolve, fail: reject }); // #endif // #ifdef APP-PLUS uni.getLocation({ type: 'gcj02', geocode: true, success: resolve, fail: reject }); // #endif }); } /** GPS 轨迹上报(每30秒) */ let trajectoryTimer = null; function startTrajectory(workOrderId) { stopTrajectory(); trajectoryTimer = setInterval(async () => { try { const loc = await getLocation(); await apiPost('/gps/trajectory', { workOrderId, latitude: loc.latitude, longitude: loc.longitude, timestamp: new Date().toISOString() }); } catch (e) { /* 静默失败,不影响主流程 */ } }, 30000); } function stopTrajectory() { if (trajectoryTimer) { clearInterval(trajectoryTimer); trajectoryTimer = null; } } // ==================== 拍照与水印 ==================== /** 拍照(支持相册选择) */ function chooseImage(count = 1) { return new Promise((resolve, reject) => { uni.chooseImage({ count, sizeType: ['compressed'], // #ifdef MP-WEIXIN sourceType: ['camera'], // #endif // #ifdef APP-PLUS sourceType: ['camera', 'album'], // #endif success: resolve, fail: reject }); }); } /** Canvas 水印:时间 + 地点 + 工单号 */ function addWatermark(tempFilePath, { workOrderId, address }) { return new Promise((resolve, reject) => { // #ifdef MP-WEIXIN const ctx = uni.createCanvasContext('watermark-canvas'); // ... draw text on canvas, then save // #endif // #ifdef APP-PLUS // 使用 plus.nativeObj.Bitmap 或直接跳过(APP 端拍照自带 EXIF) resolve(tempFilePath); // #endif }); } // ==================== 文件上传 ==================== /** 完整证据上传流程:presign → upload → commit */ async function uploadEvidence(filePath, entityType, entityId) { const fileName = filePath.split('/').pop() || 'photo.jpg'; const fileType = fileName.endsWith('.mp4') ? 'VIDEO' : fileName.endsWith('.mp3') ? 'AUDIO' : 'PHOTO'; // 1. 获取预签名上传 URL const presignRes = await apiPost('/evidence/presign-upload', { fileName, fileType, entityType, entityId }); if (presignRes.data.code !== 200) throw new Error('PreSign failed'); const { fileKey, uploadUrl } = presignRes.data.data; // 2. 上传文件 const uploadRes = await new Promise((resolve, reject) => { uni.uploadFile({ url: BASE_URL + '/evidence/commit', filePath, name: 'file', formData: { fileKey, fileName, fileType, entityType: String(entityType), entityId: String(entityId), }, header: getHeaders(), success: resolve, fail: reject }); }); return { fileKey, uploadRes }; } // ==================== 导出 ==================== module.exports = { BASE_URL, apiGet, apiPost, generateIdempotencyKey, getLocation, startTrajectory, stopTrajectory, chooseImage, addWatermark, uploadEvidence, }; ``` ### 5.2 common/offline.js — 离线缓存队列 **当前逻辑**:不存在。 **拟议改动**:新建离线队列,失败请求缓存到本地,恢复网络后批量补传。 ```js const OFFLINE_QUEUE_KEY = 'hss_offline_queue'; class OfflineQueue { constructor() { this.queue = uni.getStorageSync(OFFLINE_QUEUE_KEY) || []; } add(item) { this.queue.push({ ...item, createdAt: Date.now() }); this.save(); } save() { uni.setStorageSync(OFFLINE_QUEUE_KEY, this.queue); } getPending() { return [...this.queue]; } getCount() { return this.queue.length; } async retry(apiPost) { const failed = []; for (const item of this.queue) { try { const res = await apiPost(item.path, item.data); if (res.data.code === 200) continue; failed.push(item); } catch (e) { failed.push(item); } } this.queue = failed; this.save(); return { success: this.getCount() === 0, remaining: this.queue.length }; } clear() { this.queue = []; this.save(); } } module.exports = { OfflineQueue }; ``` ### 5.3 pages/delivery/checkin/checkin.vue — GPS签到 **当前逻辑**:不存在。 **拟议改动**:三步签到流程。 ```vue ``` ### 5.4 pages/delivery/execute/execute.vue — 服务执行 **当前逻辑**:不存在。 **拟议改动**:逐项执行记录 + 证据上传 + 时间记录。 核心逻辑: 1. `onLoad` → 调用 `/delivery/work-orders/{id}/detail` 获取工单项目列表 2. 每个项目显示:名称、标准时长、完成状态(PENDING / COMPLETED / SKIPPED)、证据要求 3. 点击项目 → 弹出执行记录面板:实际开始/结束时间、备注、拍照/录像证据 4. 实时显示已用时长计时器 5. 所有项目完成后可进入签退 ### 5.5 pages/delivery/exception/exception.vue — 异常上报 **当前逻辑**:不存在。 **拟议改动**:一键选择异常类型 + 描述 + 证据上传。 异常类型列表(与后端 `ExceptionRequest.exceptionType` 对齐): - `PATIENT_ABSENT` — 对象不在家 - `PATIENT_REFUSE` — 对象拒绝服务 - `WRONG_ADDRESS` — 地址错误 - `CANNOT_CONTACT` — 无法联系 - `CONDITION_NOT_READY` — 服务条件不具备 - `HEALTH_EMERGENCY` — 身体突发异常 - `QUALITY_DISPUTE` — 服务质量争议 ### 5.6 pages/delivery/finish/finish.vue — 签退完成 **当前逻辑**:不存在。 **拟议改动**:服务总结 + 签退定位 + 提交完成。 核心逻辑: 1. 展示所有执行记录摘要 2. 输入服务总结 3. 获取签退位置 4. 调用 `POST /work-orders/{id}/finish` 5. 检查必做项目是否都已非 PENDING ### 5.7 pages/delivery/accept/accept.vue — 接单确认 **当前逻辑**:不存在。 **拟议改动**:展示工单基本信息 + 接单/拒单按钮。 ### 5.8 pages/delivery/orders/orders.vue — 工单列表 **当前逻辑**:不存在。 **拟议改动**:按状态筛选 + 上拉加载更多。 ### 5.9 pages/delivery/order-detail/order-detail.vue — 工单详情 **当前逻辑**:不存在。 **拟议改动**:调用 `/delivery/work-orders/{id}/detail`,展示完整信息 + 操作入口。 ### 5.10 pages/delivery/offline-sync/offline-sync.vue — 离线补传 **当前逻辑**:不存在。 **拟议改动**:展示离线队列数量,一键补传,进度展示。 ### 5.11 pages/delivery/messages/messages.vue — 消息通知 **当前逻辑**:不存在。 **拟议改动**:调用 `/delivery/messages`,展示消息列表,标记已读。 ### 5.12 pages/delivery/profile/profile.vue — 我的资质 **当前逻辑**:不存在。 **拟议改动**:展示服务人员信息、资质列表、评分、工单统计。 --- ## 6. 后端改动 ### 6.1 GPS 轨迹端点(新增) **文件**:`module/workorder/controller/GpsController.java`(新增) ```java @RestController @RequestMapping("/api/hss/gps") @Tag(name = "GPS轨迹") public class GpsController { @PostMapping("/trajectory") @PreAuthorize("hasRole('STAFF')") public ApiResponse recordTrajectory(@RequestBody TrajectoryRequest req) { jdbcTemplate.update( "INSERT INTO hss_gps_trajectory (work_order_id, latitude, longitude, recorded_at, created_by) " + "VALUES (?, ?, ?, NOW(), ?)", req.workOrderId(), req.latitude(), req.longitude(), RequestContext.getUserId()); return ApiResponse.ok(null); } public record TrajectoryRequest(Long workOrderId, Double latitude, Double longitude, String timestamp) {} } ``` ### 6.2 数据库(新增表) ```sql CREATE TABLE IF NOT EXISTS hss_gps_trajectory ( id BIGSERIAL PRIMARY KEY, work_order_id BIGINT NOT NULL, latitude DECIMAL(10,7) NOT NULL, longitude DECIMAL(10,7) NOT NULL, recorded_at TIMESTAMP NOT NULL DEFAULT NOW(), created_by BIGINT ); CREATE INDEX idx_hss_gps_wo ON hss_gps_trajectory(work_order_id); ``` ### 6.3 EvidenceService 修复 **当前问题**:`commit()` 方法硬编码 `file_type = 'PHOTO'`,不支持 VIDEO/AUDIO。 **拟议改动**:从请求参数中读取 `fileType`。 --- ## 7. 自测与回归 ### 手动验证步骤 1. 登录 → 工作台 → 点击待接单 → 工单列表 2. 点击工单 → 工单详情(患者信息、服务项目、操作入口) 3. 点击接单 → 确认接单 4. 点击签到 → GPS定位 → 拍照 → 对象确认 → 提交签到 5. 点击开始服务 6. 逐项执行:每项记录时间+拍照/录像证据 7. (可选)上报异常 8. 点击完成 → 填写总结 → 签退定位 → 提交 9. 离线模式:断网操作 → 联网 → 离线补传 ### 关键回归场景 - GPS 定位失败时显示友好提示 - 200 米外签到应被后端拒绝 - 必做项目未完成时 finish 应被拒绝 - 离线队列在网络恢复后能补传 - 微信小程序端 `#ifdef MP-WEIXIN` 条件编译生效 --- ## 8. 短 Prompt ```txt 在 delivery-miniapp/ 中实现完整的服务人员移动作业端。 已有:login.vue + index.vue + common/api.js。 需要:10个缺失页面 + 扩展 api.js(GPS/拍照/上传/离线队列/水印/轨迹)。 技术栈:uni-app + Vue,微信小程序 + APP 双端条件编译。 后端 API 已就绪(work-orders/check-in/finish/report-exception + delivery/workbench/today/detail + evidence/presign-upload/commit)。 严格按 IMPLEMENTATION.md 实现,页面结构参考已有 login.vue 和 index.vue 的风格。 ```