diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c4de2bf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + backend: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: hss_home_service_test + POSTGRES_USER: hss_test + POSTGRES_PASSWORD: test123 + ports: ["5432:5432"] + options: --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 5 + redis: + image: redis:7-alpine + ports: ["6379:6379"] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: { java-version: '17', distribution: 'temurin' } + - name: Build & Test + run: cd hss-home-service && mvn package -B + env: + DB_URL: jdbc:postgresql://localhost:5432/hss_home_service_test + DB_USERNAME: hss_test + DB_PASSWORD: test123 + REDIS_HOST: localhost + SPRING_PROFILES_ACTIVE: test + + website: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: '20' } + - name: Install & Build + run: cd hss-home-service/website && npm ci && npm run generate diff --git a/.gitignore b/.gitignore index 66f11a0..8cbb960 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,40 @@ -# Dependencies -**/node_modules/ - -# Claude / AI agent files -.claude/ - -# AI-generated planning docs & prompts -IMPLEMENTATION_PLAN.md -居家上门服务系统_可执行实现方案_补齐可开工版.md -hss-home-service/官网设计方案Prompt.txt - -# Build output -**/.output/ -**/target/ -**/test-results/ +# Build +target/ +*.class +*.jar +*.war +dist/ +*.db +*.sqlite # IDE .idea/ +*.iml .vscode/ -*.swp -*.swo + +# Environment +.env +.env.* + +# Node +node_modules/ +.nuxt/ +.output/ + +# AI / Skills +.claude/ + +# Test artifacts +test-results/ +*.trace.zip +*.png + +# OS +.DS_Store +Thumbs.db + +# Misc +*.log +mall/ +/tmp/ +playwright-report/ diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..8bd0334 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,1413 @@ +# 居家上门服务系统 — 可执行实现方案 + +> 生成日期:2026-05-15 +> 基于:V2.0 工程落地增强版 + V2.1 行业经验增强版 + V2.2 边界限制增强版 +> 项目状态:全新仓库,从零搭建 + +--- + +## 1. 需求与约束摘要 + +| 项 | 内容 | +|---|---| +| **任务类型** | 新接口 / 新能力开发(全新系统) | +| **目标** | 实现居家上门服务全链路闭环系统:需求受理 → 评估定级 → 方案制定 → 派单调度 → 上门执行 → 过程监管 → 验收反馈 → 结算归档 | +| **硬边界** | Spring Boot / Java 17+、PostgreSQL、Redis、MQTT(通知)、对象存储(文件)、RESTful+action、OpenAPI 3.1、hss_表前缀、Outbox通知、四层状态机、幂等写、前端只传动作不传状态、敏感数据授权脱敏审计 | +| **范围** | 全部模块 + delivery 端小程序 | + +--- + +## 2. 代码依据 + +本项目为全新仓库,无现有后端代码。以下为架构文档依据: + +| 依据文件 | 路径 | 职责 | +|---|---|---| +| V2.0 工程落地增强版 | `docs/ architecture/居家上门服务闭环流程文档_V2工程落地增强版.md` | 业务闭环流程、四层状态机、数据模型、调度算法、通知设计、合规安全、结算对账 | +| V2.1 行业经验增强版 | `docs/ architecture/居家上门服务闭环流程文档_V2.1_行业经验增强版.md` | 美团三层调度、ETA预测、VRPTW建模、delivery智能助手、运力运营中心、主数据平台、SRE稳定性 | +| V2.2 边界限制增强版 | `/home/akoo/居家服务/居家上门服务闭环流程文档_V2.2_边界限制增强版.md` | 12条硬约束、技术栈锁定、DDL边界、接口风格、部署环境、协作边界 | + +--- + +## 3. DDL 依据 + +当前仓库无 `db/init.sql` 与 `db/update.sql`。以下设计基于 V2.0 文档 4.1 节数据域划分(第 939-958 行),结合 V2.2 第 16.3 节 DDL 边界约束。 + +| 数据域 | 设计表名(hss_前缀) | 来源 | +|---|---|---| +| 申请域 | `hss_service_applications` | V2.0 §4.1 | +| 用户画像域 | `hss_patient_profiles` | V2.0 §4.1 | +| 评估域 | `hss_assessment_tasks`、`hss_assessment_reports` | V2.0 §4.1 | +| 异议域 | `hss_objections` | V2.0 §4.2 | +| 方案域 | `hss_service_plans`、`hss_service_plan_items`、`hss_plan_versions` | V2.0 §4.1 | +| 计划域 | `hss_service_schedules` | V2.0 §4.1 | +| 工单域 | `hss_work_orders`、`hss_work_order_items` | V2.0 §4.1 | +| 执行域 | `hss_checkins`、`hss_execution_records`、`hss_evidence_files` | V2.0 §4.1 | +| 异常域 | `hss_exceptions`、`hss_exception_actions` | V2.0 §4.1 | +| 监管域 | `hss_spot_checks`、`hss_violations`、`hss_corrections` | V2.0 §4.1 | +| 验收域 | `hss_acceptances`、`hss_complaints` | V2.0 §4.1 | +| 结算域 | `hss_settlements`、`hss_settlement_items`、`hss_payments`、`hss_refunds` | V2.0 §4.1 | +| 归档域 | `hss_ledgers`、`hss_archive_files` | V2.0 §4.1 | +| 通知域 | `hss_notification_outbox`、`hss_notification_receipts` | V2.0 §7.2 | +| 审计域 | `hss_audit_logs`、`hss_state_transitions` | V2.0 §3.6、§8.4 | +| 合规域 | `hss_consent_records`、`hss_data_access_logs` | V2.0 §8.2 | +| 调度域(V2.1扩展) | `hss_service_grids`、`hss_grid_capacity_daily`、`hss_staff_skill_capacity`、`hss_capacity_forecasts`、`hss_capacity_alerts` | V2.1 §15.2.1 | +| 主数据域(V2.1扩展) | `hss_md_orgs`、`hss_md_staff`、`hss_md_qualifications`、`hss_md_service_items`、`hss_md_regions`、`hss_md_price_rules` | V2.1 §15.7 | + +--- + +## 4. 改动范围清单 + +全部为新增文件。 + +| 路径 | 增/删/改 | 摘要 | +|---|---|---| +| `pom.xml` | 增 | Maven 构建配置,Spring Boot 3.x + Java 17 | +| `src/main/java/com/meizhou/hss/` | 增 | 根包目录 | +| `src/main/java/.../common/` | 增 | 公共模块:响应体、异常、工具类、幂等注解、审计切面 | +| `src/main/java/.../config/` | 增 | Spring 配置:Security、Redis、MQTT、对象存储、定时任务、OpenAPI | +| `src/main/java/.../statemachine/` | 增 | 四层状态机引擎:申请、方案、工单、结算 | +| `src/main/java/.../module/application/` | 增 | 申请域:controller/service/repository/entity/dto | +| `src/main/java/.../module/assessment/` | 增 | 评估域:controller/service/repository/entity/dto | +| `src/main/java/.../module/plan/` | 增 | 方案域:controller/service/repository/entity/dto | +| `src/main/java/.../module/schedule/` | 增 | 计划与调度域 | +| `src/main/java/.../module/workorder/` | 增 | 工单域 | +| `src/main/java/.../module/execution/` | 增 | 执行域:签到、执行记录、证据链 | +| `src/main/java/.../module/exception/` | 增 | 异常域:异常上报与处理 | +| `src/main/java/.../module/supervision/` | 增 | 监管域:抽查、违规、整改 | +| `src/main/java/.../module/acceptance/` | 增 | 验收域:验收、评价、投诉 | +| `src/main/java/.../module/settlement/` | 增 | 结算域:结算、支付、退款 | +| `src/main/java/.../module/archive/` | 增 | 归档域:台账、归档文件 | +| `src/main/java/.../module/notification/` | 增 | 通知域:Outbox 与回执 | +| `src/main/java/.../module/audit/` | 增 | 审计域:审计日志、状态流转 | +| `src/main/java/.../module/compliance/` | 增 | 合规域:授权同意、数据访问日志 | +| `src/main/java/.../module/masterdata/` | 增 | 主数据域:机构、人员、资质、服务项目、区域、价格 | +| `src/main/java/.../module/capacity/` | 增 | 运力域:服务网格、容量、预测 | +| `src/main/resources/db/migration/` | 增 | Flyway 迁移脚本 | +| `src/main/resources/application.yml` | 增 | 主配置 | +| `src/test/java/.../` | 增 | 单元测试与集成测试 | +| `delivery-miniapp/` | 增 | delivery 端小程序(uni-app) | + +--- + +## 5. 方案概述 + +### 5.1 分层架构 + +``` +┌──────────────────────────────────────────────┐ +│ Controller 层(REST + action-style REST) │ +│ - 参数校验(Spring Validation) │ +│ - 幂等拦截(Idempotency-Key) │ +│ - 权限注解(@PreAuthorize) │ +│ - OpenAPI 注解(SpringDoc) │ +└──────────────────┬───────────────────────────┘ + │ +┌──────────────────▼───────────────────────────┐ +│ Service 层(业务编排) │ +│ - 状态机调用(不跳过状态检查) │ +│ - 事务边界(@Transactional) │ +│ - Outbox 写入(同事务) │ +│ - 审计日志写入 │ +└──────────────────┬───────────────────────────┘ + │ +┌──────────────────▼───────────────────────────┐ +│ Repository 层(数据访问) │ +│ - MyBatis-Plus BaseMapper │ +│ - 自定义 SQL(复杂查询/统计) │ +│ - 乐观锁 version 字段 │ +└──────────────────┬───────────────────────────┘ + │ +┌──────────────────▼───────────────────────────┐ +│ Domain 层(领域对象) │ +│ - Entity(映射表) │ +│ - DTO / VO / Request / Response │ +│ - 状态枚举 │ +│ - 业务异常 │ +└──────────────────────────────────────────────┘ +``` + +### 5.2 核心数据流 + +```text +POST /applications → 创建申请(DRAFT) +POST /applications/{id}/submit → 提交申请(PENDING_ACCEPTANCE) +POST /applications/{id}/accept → 受理通过(PENDING_ASSESSMENT) +POST /assessments/{id}/assign → 派发评估(ASSESSING) +POST /assessments/{id}/submit → 提交评估(ASSESSMENT_PASSED) +POST /plans → 创建方案(PLAN_DRAFT) +POST /plans/{id}/submit → 提交签署(PLAN_PENDING_SIGN) +POST /plans/{id}/sign → 签署通过(PLAN_EFFECTIVE) +POST /schedules/{id}/generate → 生成服务计划 +POST /work-orders/{id}/dispatch → 派单(ORDER_ASSIGNED) +POST /work-orders/{id}/accept → 接单(ORDER_ACCEPTED) +POST /work-orders/{id}/check-in → 签到(ORDER_CHECKED_IN) +POST /work-orders/{id}/start → 开始服务(ORDER_IN_SERVICE) +POST /execution-records → 逐项记录执行 +POST /work-orders/{id}/finish → 完成服务(ORDER_COMPLETED) +POST /acceptances → 发起验收(ACCEPTANCE_PENDING) +POST /acceptances/{id}/confirm → 验收通过(ACCEPTED) +POST /settlements/{id}/generate → 生成结算单 +POST /settlements/{id}/approve → 审核通过 +POST /settlements/{id}/pay → 支付完成(SETTLEMENT_PAID) +POST /settlements/{id}/archive → 归档(ARCHIVED) +``` + +### 5.3 异常流 + +```text +任何非终态 → POST /{entity}/{id}/cancel → CANCELLED(需原因) +评估中 → POST /assessments/{id}/object → REVIEWING → 维持/重评 +方案待签署 → POST /plans/{id}/reject → PLAN_REJECTED → 重新编制 +工单已派单 → POST /work-orders/{id}/reassign → 改派 +工单服务中 → POST /work-orders/{id}/report-exception → ORDER_EXCEPTION → 协调/改派/关闭 +验收拒绝 → POST /acceptances/{id}/reject → 创建问题处理单 +结算不通过 → POST /settlements/{id}/return → 退回修改 +``` + +--- + +## 6. 代码级修改说明(核心) + +### 6.1 项目初始化 + +**文件**:`pom.xml`(新增) + +当前逻辑:不存在。 +拟议改动:创建 Maven 父 POM,管理所有模块依赖。 + +```xml + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.0 + + com.meizhou + hss-home-service + 1.0.0-SNAPSHOT + HSS Home Service + + 17 + 3.5.6 + 2.5.0 + 10.11.0 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-data-redis + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + org.postgresql + postgresql + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-postgresql + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + +``` + +### 6.2 公共模块 + +**目录**:`src/main/java/com/meizhou/hss/common/`(新增) + +#### 6.2.1 统一响应体 + +**文件**:`ApiResponse.java`(新增) + +当前逻辑:不存在。 +拟议改动:所有接口统一使用此响应格式。 + +```java +// 新增 +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ApiResponse { + private int code; // 200 成功 + private String message; + private T data; + private String requestId; // UUID + private long timestamp; // System.currentTimeMillis() + + public static ApiResponse ok(T data) { ... } + public static ApiResponse fail(int code, String message) { ... } +} +``` + +#### 6.2.2 幂等拦截 + +**文件**:`@Idempotent.java` + `IdempotentAspect.java`(新增) + +当前逻辑:不存在。 +拟议改动:基于 Redis 实现 request_id 幂等。相同 `requestId + action + entityId` 在 TTL 内重复提交返回首次结果。 + +```java +// 新增注解 +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Idempotent { + String prefix() default "idempotent"; + long ttlSeconds() default 300; +} + +// 新增切面 +@Aspect +@Component +public class IdempotentAspect { + @Around("@annotation(idempotent)") + public Object around(ProceedingJoinPoint pjp, Idempotent idempotent) { + // 从请求头获取 Idempotency-Key + // 拼接 key = prefix + ":" + idempotencyKey + // Redis SET NX EX,成功则执行,失败则返回缓存结果 + } +} +``` + +#### 6.2.3 审计字段基类 + +**文件**:`BaseEntity.java`(新增) + +```java +// 新增 - 所有 hss_ 表 Entity 继承此类 +@Data +public abstract class BaseEntity { + @TableId(type = IdType.AUTO) + private Long id; + private Long tenantId; + private Long orgId; + private String status; + private Integer version; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private Long createdBy; + private Long updatedBy; + @TableLogic + private Integer deleted; // 0 正常, 1 已删除 +} +``` + +### 6.3 状态机引擎 + +**目录**:`src/main/java/com/meizhou/hss/statemachine/`(新增) + +#### 文件清单 + +| 文件 | 职责 | +|---|---| +| `StateMachine.java` | 通用状态机接口:`transition(entity, action, context) → targetState` | +| `StateMachineConfig.java` | 状态转换规则配置(从 YAML/DB 加载) | +| `TransitionRule.java` | 单条转换规则:fromStatus + action + role → toStatus + constraints | +| `ApplicationStateMachine.java` | 申请状态机实现 | +| `PlanStateMachine.java` | 方案状态机实现 | +| `WorkOrderStateMachine.java` | 工单状态机实现 | +| `SettlementStateMachine.java` | 结算状态机实现 | +| `StateTransitionException.java` | 非法状态转换异常 | + +**当前逻辑**:不存在。 +**拟议改动**:基于 V2.0 文档 §3.2~3.5 的状态转换表实现。 + +```java +// 新增 - TransitionRule +public record TransitionRule( + String entityType, // application / plan / work_order / settlement + String fromStatus, + String action, // submit / accept / reject / check-in / finish / approve / ... + String requiredRole, // 允许执行此动作的角色 + String toStatus, + List constraints // 约束条件:如 "hasAssessmentReport"、"amountMatched" +) {} + +// 新增 - StateMachine 核心方法 +public class StateMachine { + // 校验转换是否合法 + public boolean canTransition(String entityType, String fromStatus, + String action, String role); + // 执行转换(不通过抛 StateTransitionException) + public String transition(String entityType, String fromStatus, + String action, String role, Map context); + // 记录状态流转到 hss_state_transitions + private void recordTransition(...); +} +``` + +状态转换规则初始化(基于文档 §3.2~3.5,部分示例): + +```java +// 新增 - 申请状态机规则 +rules.add(new TransitionRule("application", "DRAFT", "submit", "APPLICANT", + "PENDING_ACCEPTANCE", List.of("materialsComplete"))); +rules.add(new TransitionRule("application", "PENDING_ACCEPTANCE", "accept", "RECEPTIONIST", + "PENDING_ASSESSMENT", List.of("ageCheck", "qualificationCheck", "duplicateCheck"))); +rules.add(new TransitionRule("application", "PENDING_ACCEPTANCE", "return", "RECEPTIONIST", + "RETURNED", List.of("returnReasonRequired"))); +// ... 等全部规则 +``` + +### 6.4 申请域模块 + +**目录**:`src/main/java/com/meizhou/hss/module/application/` + +#### 文件清单 + +| 文件 | 职责 | +|---|---| +| `entity/ServiceApplicationEntity.java` | 映射 `hss_service_applications` | +| `dto/ApplicationCreateRequest.java` | 创建申请请求 DTO | +| `dto/ApplicationSubmitRequest.java` | 提交申请请求 DTO | +| `dto/ApplicationResponse.java` | 申请响应 DTO | +| `dto/ApplicationListQuery.java` | 列表查询参数 | +| `repository/ServiceApplicationMapper.java` | MyBatis-Plus Mapper | +| `service/ApplicationService.java` | 业务逻辑 | +| `service/ApplicationValidationService.java` | 自动校验(材料完整性、资格、重复申请) | +| `controller/ApplicationController.java` | REST 接口 | + +**当前逻辑**:不存在。 + +**拟议改动**: + +`ApplicationController.java`: + +```java +// 新增 +@RestController +@RequestMapping("/api/hss/applications") +@Tag(name = "服务申请") +public class ApplicationController { + + @PostMapping + @Idempotent(prefix = "app:create") + public ApiResponse create( + @Valid @RequestBody ApplicationCreateRequest req) { + // Service: 校验 → 创建 DRAFT 状态申请 → 记录状态流转 + } + + @PostMapping("/{id}/submit") + @Idempotent(prefix = "app:submit") + public ApiResponse submit( + @PathVariable Long id, @Valid @RequestBody ApplicationSubmitRequest req) { + // Service: 状态机 DRAFT→PENDING_ACCEPTANCE → 写 Outbox 通知受理员 + } + + @PostMapping("/{id}/accept") + @Idempotent(prefix = "app:accept") + @PreAuthorize("hasRole('RECEPTIONIST')") + public ApiResponse accept(@PathVariable Long id) { + // Service: 校验(材料+资格+重复)→ 状态机 PENDING_ACCEPTANCE→PENDING_ASSESSMENT + } + + @PostMapping("/{id}/return") + @Idempotent(prefix = "app:return") + @PreAuthorize("hasRole('RECEPTIONIST')") + public ApiResponse returnApplication( + @PathVariable Long id, @Valid @RequestBody ReturnRequest req) { + // 约束:必须填写退回原因 + } + + @PostMapping("/{id}/cancel") + @Idempotent(prefix = "app:cancel") + public ApiResponse cancel(@PathVariable Long id) { + // 约束:申请人本人或授权家属 + } + + @GetMapping + public ApiResponse> list(ApplicationListQuery query) { + // 数据范围:按 tenant_id、org_id、角色过滤 + } + + @GetMapping("/{id}") + public ApiResponse getById(@PathVariable Long id) { + // 脱敏:非授权角色隐藏完整地址、电话 + } +} +``` + +**注**:其余模块(评估、方案、调度、工单、执行、异常、监管、验收、结算、归档、通知、审计、合规、主数据、运力)的代码结构遵循相同模式,此处为避免文档过度膨胀,仅输出申请域作为样板。完整代码在实施阶段逐模块编写。 + +### 6.5 通知 Outbox 模块 + +**目录**:`src/main/java/com/meizhou/hss/module/notification/` + +关键设计: + +```java +// 新增 - 通知创建(在业务事务内调用) +@Service +public class NotificationService { + // 与业务在同一事务中写入 Outbox + @Transactional + public void createNotification(Long entityId, String entityType, + String channel, String template, Map params) { + NotificationOutboxEntity outbox = new NotificationOutboxEntity(); + outbox.setEntityId(entityId); + outbox.setChannel(channel); // MQTT / WECHAT / SMS + outbox.setStatus("CREATED"); + outbox.setRequestId(IdempotencyContext.getRequestId()); + outboxMapper.insert(outbox); + } +} + +// 新增 - 定时任务:扫描 Outbox 并异步发送 +@Component +public class NotificationSender { + @Scheduled(fixedDelay = 2000) // 每2秒扫描 + public void sendPending() { + // SELECT * FROM hss_notification_outbox WHERE status='CREATED' LIMIT 100 + // 发送 → 更新 status=SENT → 记录 receipt + // 失败 → status=FAILED, retry_count+1 + } +} +``` + +### 6.6 审计模块 + +**目录**:`src/main/java/com/meizhou/hss/module/audit/` + +```java +// 新增 - AOP 切面自动记录审计 +@Aspect +@Component +public class AuditAspect { + @Around("@annotation(auditable)") + public Object around(ProceedingJoinPoint pjp, Auditable auditable) { + // 记录:操作人、时间、IP、设备、操作类型、业务对象ID、前后值 + } +} + +// 新增 - 状态流转记录(状态机内部调用) +// 写入 hss_state_transitions: +// entity_type, entity_id, from_status, action, to_status, +// operator_id, operator_role, reason, request_id, created_at +``` + +### 6.7 调度算法模块 + +**目录**:`src/main/java/com/meizhou/hss/module/schedule/algorithm/` + +```java +// 新增 - 两阶段调度 +@Service +public class DispatchAlgorithm { + + // 阶段1:硬约束过滤 + public List hardFilter(List allStaff, WorkOrder order) { + return allStaff.stream() + .filter(s -> s.getStatus() == StaffStatus.ACTIVE) + .filter(s -> s.hasQualification(order.getRequiredSkills())) + .filter(s -> s.getRegion().contains(order.getServiceAddress().getRegion())) + .filter(s -> !hasTimeConflict(s, order)) + .filter(s -> !s.isBlacklisted()) + .filter(s -> s.getTodayOrderCount() < s.getMaxDailyOrders()) + .toList(); + } + + // 阶段2:软约束评分 + public List scoreAndRank(List candidates, WorkOrder order) { + // score = distance * 0.25 + skill * 0.25 + workload * 0.20 + // + rating * 0.15 + response * 0.10 + familiarity * 0.05 + // 返回 Top5,附解释 + } +} +``` + +--- + +## 7. 接口设计方案(任务类型=1 强制) + +### 7.1 核心接口清单 + +按主链路阶段组织,以下为核心接口设计。 + +#### 7.1.1 服务申请接口组 + +**接口1:创建服务申请** + +| 接口名称 | 创建服务申请 | +| -------- | ------------------------------------------------------ | +| 接口地址 | `/api/hss/applications` | +| 请求方式 | `POST` | +| 接口描述 | 服务对象/家属提交服务申请,创建后状态为 DRAFT | + +| 参数名 | 类型 | 是否必填 | 说明 | +| ----------- | ------ | -------- | --------------------------- | +| patientId | string | 是 | 服务对象ID | +| serviceType | string | 是 | 服务类型枚举:HOME_CARE / REHABILITATION / BATH_ASSIST / ASSESSMENT | +| channel | string | 是 | 申请渠道:APP / WECHAT / PHONE / COMMUNITY / HOSPITAL | +| contactName | string | 是 | 联系人姓名 | +| contactPhone | string | 是 | 联系人电话 | +| address | string | 是 | 服务地址 | +| regionCode | string | 是 | 区域编码 | +| notes | string | 否 | 备注说明 | +| attachmentIds | array | 否 | 附件ID列表 | + +| 字段路径 | 类型 | 示例 | 说明 | +| -------- | ------- | -------------- | -------------------- | +| code | int | 200 | 状态码 | +| success | boolean | true | 是否成功 | +| message | string | 申请创建成功 | 响应消息 | +| data.id | string | 1001 | 申请ID | +| data.status | string | DRAFT | 申请状态 | +| data.createdAt | string | 2026-05-15T10:30:00 | 创建时间 | + +请求示例: +```json +{ + "patientId": "2001", + "serviceType": "HOME_CARE", + "channel": "WECHAT", + "contactName": "张三", + "contactPhone": "138****5678", + "address": "梅江区金山街道XX小区3栋201", + "regionCode": "441402001", + "notes": "老人独居,需要日常护理", + "attachmentIds": ["att_001", "att_002"] +} +``` + +返回示例: +```json +{ + "code": 200, + "message": "申请创建成功", + "data": { + "id": "1001", + "status": "DRAFT", + "createdAt": "2026-05-15T10:30:00" + }, + "requestId": "req-uuid-abc123", + "timestamp": 1715747400000 +} +``` + +--- + +**接口2:提交服务申请** + +| 接口名称 | 提交服务申请 | +| -------- | ------------------------------------------------------ | +| 接口地址 | `/api/hss/applications/{id}/submit` | +| 请求方式 | `POST` | +| 接口描述 | 将草稿状态申请提交为待受理,触发自动校验 | + +| 参数名 | 类型 | 是否必填 | 说明 | +| ----------- | ------ | -------- | --------------------------- | +| id | string | 是 | 路径参数,申请ID | + +| 字段路径 | 类型 | 示例 | 说明 | +| -------- | ------- | -------------- | -------------------- | +| code | int | 200 | 状态码 | +| data.status | string | PENDING_ACCEPTANCE | 新状态 | +| data.validationResult.passed | boolean | true | 自动校验结果 | + +--- + +**接口3:受理通过** + +| 接口名称 | 受理通过 | +| -------- | ------------------------------------------------------ | +| 接口地址 | `/api/hss/applications/{id}/accept` | +| 请求方式 | `POST` | +| 接口描述 | 受理员审核通过,申请进入待评估状态 | + +| 参数名 | 类型 | 是否必填 | 说明 | +| ----------- | ------ | -------- | --------------------------- | +| id | string | 是 | 路径参数,申请ID | +| reason | string | 否 | 审核备注 | + +--- + +**接口4:受理退回** + +| 接口名称 | 受理退回 | +| -------- | ------------------------------------------------------ | +| 接口地址 | `/api/hss/applications/{id}/return` | +| 请求方式 | `POST` | +| 接口描述 | 受理员退回申请,需填写退回原因 | + +| 参数名 | 类型 | 是否必填 | 说明 | +| ----------- | ------ | -------- | --------------------------- | +| id | string | 是 | 路径参数 | +| reason | string | 是 | 退回原因(必填) | + +--- + +#### 7.1.2 工单执行接口组 + +**接口5:GPS 签到** + +| 接口名称 | GPS签到 | +| -------- | ------------------------------------------------------ | +| 接口地址 | `/api/hss/work-orders/{id}/check-in` | +| 请求方式 | `POST` | +| 接口描述 | 服务人员在服务地址200米内签到,需GPS+照片+对象确认 | + +| 参数名 | 类型 | 是否必填 | 说明 | +| ----------- | ------ | -------- | --------------------------- | +| id | string | 是 | 工单ID | +| latitude | number | 是 | 纬度 | +| longitude | number | 是 | 经度 | +| photoFileId | string | 是 | 现场照片文件ID | +| patientConfirmed | boolean | 是 | 服务对象是否确认 | +| patientSignatureId | string | 否 | 对象签名文件ID | + +| 字段路径 | 类型 | 示例 | 说明 | +| -------- | ------- | -------------- | -------------------- | +| code | int | 200 | 状态码 | +| data.status | string | ORDER_CHECKED_IN | 新状态 | +| data.distance | number | 85.5 | 签到位置距服务地址距离(米) | +| data.checkinTime | string | 2026-05-15T09:05:00 | 签到时间 | + +请求示例: +```json +{ + "latitude": 24.2878, + "longitude": 116.1271, + "photoFileId": "file_checkin_001", + "patientConfirmed": true, + "patientSignatureId": "sig_001" +} +``` + +返回示例: +```json +{ + "code": 200, + "message": "签到成功", + "data": { + "id": "5001", + "status": "ORDER_CHECKED_IN", + "distance": 85.5, + "checkinTime": "2026-05-15T09:05:00" + }, + "requestId": "req-uuid-def456", + "timestamp": 1715747700000 +} +``` + +--- + +**接口6:服务完成提交** + +| 接口名称 | 服务完成提交 | +| -------- | ------------------------------------------------------ | +| 接口地址 | `/api/hss/work-orders/{id}/finish` | +| 请求方式 | `POST` | +| 接口描述 | 服务人员完成所有必做项目后提交,工单进入已完成状态 | + +| 参数名 | 类型 | 是否必填 | 说明 | +| ----------- | ------ | -------- | --------------------------- | +| id | string | 是 | 工单ID | +| executionRecords | array | 是 | 项目执行记录列表 | +| executionRecords[].planItemId | string | 是 | 方案项目ID | +| executionRecords[].status | string | 是 | COMPLETED / PARTIAL / NOT_COMPLETED / SKIPPED / USER_REFUSED | +| executionRecords[].actualStartTime | string | 是 | 实际开始时间 | +| executionRecords[].actualEndTime | string | 是 | 实际结束时间 | +| executionRecords[].evidenceFileIds | array | 否 | 证据文件ID列表 | +| executionRecords[].notes | string | 否 | 执行备注 | +| signOffLatitude | number | 是 | 签退纬度 | +| signOffLongitude | number | 是 | 签退经度 | +| serviceSummary | string | 否 | 服务总结 | + +--- + +#### 7.1.3 结算接口组 + +**接口7:生成结算单** + +| 接口名称 | 生成结算单 | +| -------- | ------------------------------------------------------ | +| 接口地址 | `/api/hss/settlements/generate` | +| 请求方式 | `POST` | +| 接口描述 | 基于已验收工单生成结算单,自动汇总金额与抵扣 | + +| 参数名 | 类型 | 是否必填 | 说明 | +| ----------- | ------ | -------- | --------------------------- | +| workOrderIds | array | 是 | 已验收工单ID列表 | +| periodStart | string | 是 | 结算周期开始日期 | +| periodEnd | string | 是 | 结算周期结束日期 | + +| 字段路径 | 类型 | 示例 | 说明 | +| -------- | ------- | -------------- | -------------------- | +| data.id | string | 7001 | 结算单ID | +| data.totalAmount | number | 1200.00 | 总金额 | +| data.insuranceDeduction | number | 800.00 | 长护险抵扣 | +| data.selfPayAmount | number | 400.00 | 自费金额 | +| data.items | array | [...] | 结算明细 | + +--- + +### 7.2 接口契约约定 + +- 所有写接口必须带 `Idempotency-Key` 请求头 +- 所有分页接口参数:`page`(默认1)、`size`(默认20,最大100)、`sort`(如 `createdAt,desc`) +- 状态动作接口路径格式:`POST /api/hss/{resource}/{id}/{action}` +- 资源 CRUD:`GET/POST/PUT/DELETE /api/hss/{resource}` 和 `/{id}` +- 统一错误响应: +```json +{ + "code": 40001, + "message": "参数校验失败", + "details": [ + {"field": "contactPhone", "message": "联系电话格式不正确"} + ], + "requestId": "req-uuid-err123" +} +``` + +### 7.3 错误码规划 + +| 错误码范围 | 含义 | +|---|---| +| 200 | 成功 | +| 40001-40099 | 参数校验错误 | +| 40101-40199 | 认证/授权错误 | +| 40301-40399 | 权限不足 | +| 40401-40499 | 资源不存在 | +| 40901-40999 | 状态冲突(非法状态转换) | +| 40910-40919 | 幂等冲突(重复提交) | +| 42201-42299 | 业务规则校验失败 | +| 50001-50099 | 服务器内部错误 | +| 50301-50399 | 依赖服务不可用 | + +--- + +## 8. 数据库与迁移计划 + +### 8.1 DDL 变更明细 + +迁移脚本位于 `src/main/resources/db/migration/`,采用 Flyway 管理。 + +#### V1__baseline.sql(初始基线) + +核心表结构(部分示例): + +```sql +-- ========================================== +-- 基线迁移 V1:创建居家服务核心业务表 +-- ========================================== + +-- 1. 申请域 +CREATE TABLE hss_service_applications ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + org_id BIGINT NOT NULL, + patient_id BIGINT NOT NULL, + service_type VARCHAR(32) NOT NULL, -- HOME_CARE / REHABILITATION / BATH_ASSIST / ASSESSMENT + channel VARCHAR(32) NOT NULL, -- APP / WECHAT / PHONE / COMMUNITY / HOSPITAL + contact_name VARCHAR(64) NOT NULL, + contact_phone VARCHAR(20) NOT NULL, + address TEXT NOT NULL, + region_code VARCHAR(20) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', + notes TEXT, + version INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_by BIGINT, + deleted SMALLINT NOT NULL DEFAULT 0 +); +CREATE INDEX idx_hss_app_status ON hss_service_applications(status); +CREATE INDEX idx_hss_app_patient ON hss_service_applications(patient_id); +CREATE INDEX idx_hss_app_created ON hss_service_applications(created_at); + +-- 2. 评估域 +CREATE TABLE hss_assessment_tasks ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + org_id BIGINT NOT NULL, + application_id BIGINT NOT NULL REFERENCES hss_service_applications(id), + assessor_id BIGINT, + status VARCHAR(32) NOT NULL DEFAULT 'PENDING_ASSIGNMENT', + -- 评估结果快照 + care_level VARCHAR(16), -- LEVEL_1 ~ LEVEL_5 + risk_level VARCHAR(16), -- LOW / MEDIUM / HIGH / CRITICAL + report_content JSONB, + version INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_by BIGINT, + deleted SMALLINT NOT NULL DEFAULT 0 +); +CREATE INDEX idx_hss_at_status ON hss_assessment_tasks(status); +CREATE INDEX idx_hss_at_app ON hss_assessment_tasks(application_id); +CREATE INDEX idx_hss_at_assessor ON hss_assessment_tasks(assessor_id); + +CREATE TABLE hss_assessment_reports ( + id BIGSERIAL PRIMARY KEY, + task_id BIGINT NOT NULL REFERENCES hss_assessment_tasks(id), + version INTEGER NOT NULL DEFAULT 1, + content JSONB NOT NULL, -- 评估指标详情 + conclusion TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT +); + +-- 3. 方案域 +CREATE TABLE hss_service_plans ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + org_id BIGINT NOT NULL, + application_id BIGINT NOT NULL, + assessment_task_id BIGINT NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'PLAN_DRAFT', + version_number INTEGER NOT NULL DEFAULT 1, + total_amount DECIMAL(12,2), -- 方案总金额 + insurance_deduction DECIMAL(12,2), -- 长护险抵扣 + self_pay_amount DECIMAL(12,2), -- 自费金额 + signed_at TIMESTAMP, + signed_by BIGINT, + effective_at TIMESTAMP, + terminated_at TIMESTAMP, + version INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_by BIGINT, + deleted SMALLINT NOT NULL DEFAULT 0 +); + +CREATE TABLE hss_service_plan_items ( + id BIGSERIAL PRIMARY KEY, + plan_id BIGINT NOT NULL REFERENCES hss_service_plans(id), + service_item_id BIGINT NOT NULL, -- 引用主数据服务项目 + item_name VARCHAR(128) NOT NULL, -- 版本化快照 + unit_price DECIMAL(10,2) NOT NULL, -- 版本化快照 + frequency INTEGER NOT NULL, -- 服务频次 + standard_duration INTEGER, -- 标准服务时长(分钟) + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE TABLE hss_plan_versions ( + id BIGSERIAL PRIMARY KEY, + plan_id BIGINT NOT NULL, + version_number INTEGER NOT NULL, + snapshot_data JSONB NOT NULL, -- 完整方案快照 + change_reason TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT +); + +-- 4. 服务计划域 +CREATE TABLE hss_service_schedules ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + plan_id BIGINT NOT NULL REFERENCES hss_service_plans(id), + scheduled_date DATE NOT NULL, + time_window_start TIME, -- 预约时间窗开始 + time_window_end TIME, -- 预约时间窗结束 + status VARCHAR(32) NOT NULL DEFAULT 'SCHEDULED', + version INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_hss_ss_date ON hss_service_schedules(scheduled_date); +CREATE INDEX idx_hss_ss_plan ON hss_service_schedules(plan_id); + +-- 5. 工单域 +CREATE TABLE hss_work_orders ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + org_id BIGINT NOT NULL, + schedule_id BIGINT REFERENCES hss_service_schedules(id), + plan_id BIGINT NOT NULL, + patient_id BIGINT NOT NULL, + staff_id BIGINT, + status VARCHAR(32) NOT NULL DEFAULT 'ORDER_CREATED', + service_date DATE NOT NULL, + scheduled_start TIME, + scheduled_end TIME, + risk_level VARCHAR(16), + is_high_risk BOOLEAN DEFAULT FALSE, + version INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_by BIGINT, + deleted SMALLINT NOT NULL DEFAULT 0 +); +CREATE INDEX idx_hss_wo_status ON hss_work_orders(status); +CREATE INDEX idx_hss_wo_staff ON hss_work_orders(staff_id); +CREATE INDEX idx_hss_wo_date ON hss_work_orders(service_date); +CREATE INDEX idx_hss_wo_patient ON hss_work_orders(patient_id); + +CREATE TABLE hss_work_order_items ( + id BIGSERIAL PRIMARY KEY, + work_order_id BIGINT NOT NULL REFERENCES hss_work_orders(id), + plan_item_id BIGINT NOT NULL, + item_name VARCHAR(128) NOT NULL, + unit_price DECIMAL(10,2) NOT NULL, + required BOOLEAN DEFAULT TRUE, -- 是否必做 + status VARCHAR(32) DEFAULT 'PENDING', -- COMPLETED/PARTIAL/NOT_COMPLETED/SKIPPED/USER_REFUSED + evidence_required BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- 6. 执行域 +CREATE TABLE hss_checkins ( + id BIGSERIAL PRIMARY KEY, + work_order_id BIGINT NOT NULL REFERENCES hss_work_orders(id), + checkin_type VARCHAR(16) NOT NULL, -- CHECKIN / CHECKOUT + latitude DECIMAL(10,7) NOT NULL, + longitude DECIMAL(10,7) NOT NULL, + distance_meters INTEGER, -- 距目标地址距离 + photo_file_id VARCHAR(64), + patient_confirmed BOOLEAN, + patient_signature_id VARCHAR(64), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT +); + +CREATE TABLE hss_execution_records ( + id BIGSERIAL PRIMARY KEY, + work_order_id BIGINT NOT NULL, + work_order_item_id BIGINT NOT NULL, + status VARCHAR(32) NOT NULL, -- COMPLETED/PARTIAL/NOT_COMPLETED/SKIPPED/USER_REFUSED + actual_start TIMESTAMP, + actual_end TIMESTAMP, + notes TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT +); + +CREATE TABLE hss_evidence_files ( + id BIGSERIAL PRIMARY KEY, + entity_type VARCHAR(32) NOT NULL, -- CHECKIN / EXECUTION / EXCEPTION / ACCEPTANCE + entity_id BIGINT NOT NULL, + file_type VARCHAR(16) NOT NULL, -- PHOTO / VIDEO / AUDIO + file_key VARCHAR(256) NOT NULL, -- 对象存储 key + file_size BIGINT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT +); + +-- 7. 异常域 +CREATE TABLE hss_exceptions ( + id BIGSERIAL PRIMARY KEY, + work_order_id BIGINT NOT NULL REFERENCES hss_work_orders(id), + exception_type VARCHAR(32) NOT NULL, -- PATIENT_ABSENT / PATIENT_REFUSE / WRONG_ADDRESS / ... + description TEXT NOT NULL, + evidence_file_ids TEXT, -- JSON 数组 + status VARCHAR(32) NOT NULL DEFAULT 'REPORTED', + handled_by BIGINT, + resolution TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT +); + +-- 8. 验收域 +CREATE TABLE hss_acceptances ( + id BIGSERIAL PRIMARY KEY, + work_order_id BIGINT NOT NULL REFERENCES hss_work_orders(id), + status VARCHAR(32) NOT NULL DEFAULT 'ACCEPTANCE_PENDING', + result VARCHAR(16), -- ACCEPTED / REJECTED + rating SMALLINT, -- 1-5 星 + tags TEXT, -- JSON 标签数组 + comment TEXT, + rejected_reason TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT +); + +-- 9. 结算域 +CREATE TABLE hss_settlements ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + org_id BIGINT NOT NULL, + patient_id BIGINT NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'SETTLEMENT_READY', + period_start DATE NOT NULL, + period_end DATE NOT NULL, + total_amount DECIMAL(12,2), + insurance_deduction DECIMAL(12,2), + self_pay_amount DECIMAL(12,2), + paid_at TIMESTAMP, + payment_channel VARCHAR(32), + payment_transaction_id VARCHAR(64), + version INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by BIGINT, + updated_by BIGINT, + deleted SMALLINT NOT NULL DEFAULT 0 +); + +CREATE TABLE hss_settlement_items ( + id BIGSERIAL PRIMARY KEY, + settlement_id BIGINT NOT NULL REFERENCES hss_settlements(id), + work_order_id BIGINT NOT NULL, + execution_record_id BIGINT, + item_name VARCHAR(128), + unit_price DECIMAL(10,2), + actual_amount DECIMAL(10,2), + deduction DECIMAL(10,2), + self_pay DECIMAL(10,2), + status VARCHAR(32) +); + +-- 10. 通知域 +CREATE TABLE hss_notification_outbox ( + id BIGSERIAL PRIMARY KEY, + entity_type VARCHAR(32) NOT NULL, + entity_id BIGINT NOT NULL, + channel VARCHAR(16) NOT NULL, -- MQTT / WECHAT / SMS + template_code VARCHAR(64), + params JSONB, + recipient_id BIGINT, + status VARCHAR(16) NOT NULL DEFAULT 'CREATED', + retry_count INTEGER DEFAULT 0, + max_retries INTEGER DEFAULT 3, + next_retry_at TIMESTAMP, + request_id VARCHAR(64), + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_hss_nf_status ON hss_notification_outbox(status, next_retry_at); + +CREATE TABLE hss_notification_receipts ( + id BIGSERIAL PRIMARY KEY, + outbox_id BIGINT NOT NULL, + status VARCHAR(16) NOT NULL, -- SENT / DELIVERED / READ / CONFIRMED / FAILED / EXPIRED + sent_at TIMESTAMP, + delivered_at TIMESTAMP, + read_at TIMESTAMP, + error_message TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- 11. 审计域 +CREATE TABLE hss_state_transitions ( + id BIGSERIAL PRIMARY KEY, + entity_type VARCHAR(32) NOT NULL, -- application / plan / work_order / settlement + entity_id BIGINT NOT NULL, + from_status VARCHAR(32), + action VARCHAR(32) NOT NULL, + to_status VARCHAR(32) NOT NULL, + operator_id BIGINT, + operator_role VARCHAR(32), + reason TEXT, + request_id VARCHAR(64), + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_hss_st_entity ON hss_state_transitions(entity_type, entity_id); +CREATE INDEX idx_hss_st_request ON hss_state_transitions(request_id); + +CREATE TABLE hss_audit_logs ( + id BIGSERIAL PRIMARY KEY, + entity_type VARCHAR(32), + entity_id BIGINT, + action VARCHAR(32) NOT NULL, + operator_id BIGINT, + operator_name VARCHAR(64), + operator_role VARCHAR(32), + client_ip VARCHAR(45), + device_info VARCHAR(256), + detail JSONB, + is_sensitive BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_hss_al_entity ON hss_audit_logs(entity_type, entity_id); +CREATE INDEX idx_hss_al_operator ON hss_audit_logs(operator_id); +CREATE INDEX idx_hss_al_time ON hss_audit_logs(created_at); + +-- 12. 合规域 +CREATE TABLE hss_consent_records ( + id BIGSERIAL PRIMARY KEY, + patient_id BIGINT NOT NULL, + consent_type VARCHAR(32) NOT NULL, -- GPS / PHOTO / HEALTH / AUDIO_VIDEO + consent_status VARCHAR(16) NOT NULL, -- GRANTED / REVOKED / EXPIRED + granted_at TIMESTAMP, + revoked_at TIMESTAMP, + expires_at TIMESTAMP, + created_by BIGINT, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE TABLE hss_data_access_logs ( + id BIGSERIAL PRIMARY KEY, + operator_id BIGINT NOT NULL, + data_type VARCHAR(32) NOT NULL, -- PERSONAL / HEALTH / LOCATION / FINANCIAL + entity_type VARCHAR(32), + entity_id BIGINT, + access_action VARCHAR(16) NOT NULL, -- VIEW / EXPORT / DOWNLOAD + client_ip VARCHAR(45), + reason TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- 唯一约束:幂等键 +CREATE UNIQUE INDEX uk_hss_st_request ON hss_state_transitions(request_id, entity_type, entity_id); +``` + +### 8.2 执行顺序与窗口建议 + +| 阶段 | 脚本 | 内容 | 建议窗口 | +|---|---|---|---| +| 1 | V1__baseline.sql | 全部核心表创建 | 首次部署(停机窗口 10min) | +| 2 | V2__seed_master_data.sql | 主数据初始化(区域、服务项目、资质字典) | 紧随 V1 | +| 3 | V3__add_indexes.sql | 性能索引补充 | 在线(CREATE INDEX CONCURRENTLY) | +| 后续 | V4+ | 增量变更 | 按需,遵循 flyway 版本号 | + +### 8.3 回滚策略 + +- 每次迁移对应一个 `U` 版本回滚脚本(如 `U1__baseline_rollback.sql`) +- 回滚原则:非生产环境可直接 DROP;生产环境采用 `migrate down + data restore` +- 关键变更必须备份受影响数据:`CREATE TABLE xxx_backup AS SELECT * FROM xxx WHERE ...` +- 禁止 `DROP COLUMN` 直接执行:先标记废弃 → 观察一个版本 → 再删除 + +--- + +## 9. 接口契约与兼容性 + +### 9.1 兼容性说明 + +| 方面 | 策略 | +|---|---| +| 账号/登录 | 兼容现有平台 JWT Token 格式 | +| 角色/权限 | 新增居家服务角色(受理员、评估员、方案制定员、调度员、服务人员、监管员、结算员),不影响现有角色 | +| 机构/用户 | 复用现有 org_id / user_id 体系 | +| 文件上传 | 复用现有对象存储接口,居家服务文件单独 folder | +| 业务表 | 全新 `hss_` 前缀,不影响现有业务表 | + +### 9.2 版本管理 + +- API 路径不加版本号,通过 Header `Accept-Version` 管理 +- 破坏性变更走新接口 + 旧接口保留一个版本后废弃 + +--- + +## 10. 自测与回归 + +### 10.1 单元测试范围 + +| 测试对象 | 覆盖内容 | +|---|---| +| 状态机 | 所有合法转换、非法转换拦截、重复提交幂等 | +| 校验服务 | 年龄校验、资格校验、重复申请校验 | +| 调度算法 | 硬约束过滤逻辑、软约束评分计算、Top5排序 | +| 结算计算 | 金额汇总、抵扣计算、部分完成折算 | + +### 10.2 集成测试范围 + +| 测试场景 | 验证点 | +|---|---| +| 主链路 | 申请→评估→方案→工单→执行→验收→结算→归档 全流程 | +| 异常链路 | 退回、异议、拒签、改派、异常上报、拒绝验收、结算退回 | +| 通知链路 | Outbox 写入→定时发送→回执记录 | +| 权限链路 | 角色隔离、数据范围过滤、字段脱敏 | +| 并发场景 | 同 request_id 重复提交、派单并发冲突 | + +### 10.3 关键回归场景 + +- 状态机规则变更后,所有上游调用方不受影响(规则集中管理) +- 结算公式变更后,历史结算单金额不变(版本化快照) +- 服务项目价格调整后,已签署方案不受影响(版本冻结) + +--- + +## 11. 工程目录结构总览 + +``` +hss-home-service/ +├── pom.xml +├── src/ +│ ├── main/ +│ │ ├── java/com/meizhou/hss/ +│ │ │ ├── HssApplication.java # Spring Boot 启动类 +│ │ │ ├── common/ +│ │ │ │ ├── ApiResponse.java # 统一响应 +│ │ │ │ ├── BaseEntity.java # 审计字段基类 +│ │ │ │ ├── ErrorCode.java # 错误码枚举 +│ │ │ │ ├── BusinessException.java # 业务异常 +│ │ │ │ ├── Idempotent.java # 幂等注解 +│ │ │ │ ├── IdempotentAspect.java # 幂等切面 +│ │ │ │ ├── Auditable.java # 审计注解 +│ │ │ │ └── AuditAspect.java # 审计切面 +│ │ │ ├── config/ +│ │ │ │ ├── SecurityConfig.java +│ │ │ │ ├── RedisConfig.java +│ │ │ │ ├── MqttConfig.java +│ │ │ │ ├── ObjectStorageConfig.java +│ │ │ │ ├── SchedulerConfig.java +│ │ │ │ ├── OpenApiConfig.java +│ │ │ │ └── MyBatisPlusConfig.java +│ │ │ ├── statemachine/ +│ │ │ │ ├── StateMachine.java +│ │ │ │ ├── TransitionRule.java +│ │ │ │ ├── StateTransitionException.java +│ │ │ │ ├── ApplicationStateMachine.java +│ │ │ │ ├── PlanStateMachine.java +│ │ │ │ ├── WorkOrderStateMachine.java +│ │ │ │ └── SettlementStateMachine.java +│ │ │ └── module/ +│ │ │ ├── application/ +│ │ │ │ ├── controller/ApplicationController.java +│ │ │ │ ├── service/ApplicationService.java +│ │ │ │ ├── service/ApplicationValidationService.java +│ │ │ │ ├── repository/ServiceApplicationMapper.java +│ │ │ │ ├── entity/ServiceApplicationEntity.java +│ │ │ │ └── dto/{CreateRequest,SubmitRequest,Response,ListQuery}.java +│ │ │ ├── assessment/ +│ │ │ │ ├── controller/AssessmentController.java +│ │ │ │ ├── service/AssessmentService.java +│ │ │ │ ├── repository/{AssessmentTaskMapper,AssessmentReportMapper}.java +│ │ │ │ ├── entity/{AssessmentTaskEntity,AssessmentReportEntity}.java +│ │ │ │ └── dto/{...}.java +│ │ │ ├── plan/ +│ │ │ │ └── ... (同模式) +│ │ │ ├── schedule/ +│ │ │ │ ├── controller/ScheduleController.java +│ │ │ │ ├── service/ScheduleService.java +│ │ │ │ ├── service/DispatchService.java +│ │ │ │ ├── algorithm/DispatchAlgorithm.java +│ │ │ │ ├── algorithm/HardConstraintFilter.java +│ │ │ │ ├── algorithm/SoftScoringRanker.java +│ │ │ │ └── ... +│ │ │ ├── workorder/ +│ │ │ ├── execution/ +│ │ │ ├── exception/ +│ │ │ ├── supervision/ +│ │ │ ├── acceptance/ +│ │ │ ├── settlement/ +│ │ │ ├── archive/ +│ │ │ ├── notification/ +│ │ │ │ ├── service/NotificationService.java +│ │ │ │ ├── sender/NotificationSender.java +│ │ │ │ ├── sender/MqttSender.java +│ │ │ │ ├── sender/WechatSender.java +│ │ │ │ └── sender/SmsSender.java +│ │ │ ├── audit/ +│ │ │ ├── compliance/ +│ │ │ ├── masterdata/ +│ │ │ └── capacity/ +│ │ └── resources/ +│ │ ├── application.yml +│ │ ├── application-dev.yml +│ │ ├── application-test.yml +│ │ ├── application-prod.yml +│ │ └── db/migration/ +│ │ ├── V1__baseline.sql +│ │ ├── V2__seed_master_data.sql +│ │ └── V3__add_indexes.sql +│ └── test/java/com/meizhou/hss/ +│ ├── statemachine/ +│ │ ├── ApplicationStateMachineTest.java +│ │ ├── PlanStateMachineTest.java +│ │ ├── WorkOrderStateMachineTest.java +│ │ └── SettlementStateMachineTest.java +│ ├── module/ +│ │ ├── application/ApplicationControllerTest.java +│ │ ├── workorder/WorkOrderIntegrationTest.java +│ │ └── settlement/SettlementCalculationTest.java +│ └── algorithm/DispatchAlgorithmTest.java +└── delivery-miniapp/ + ├── pages/delivery/ + │ ├── login/ + │ ├── index/ # 工作台 + │ ├── orders/ # 工单列表 + │ ├── order-detail/ # 工单详情 + │ ├── accept/ # 接单确认 + │ ├── checkin/ # GPS签到 + │ ├── execute/ # 服务执行 + │ ├── exception/ # 异常上报 + │ ├── finish/ # 签退完成 + │ ├── offline-sync/ # 离线补传 + │ ├── messages/ # 通知消息 + │ └── profile/ # 我的资质 + └── ... +``` + +--- + +## 12. 实施优先级与分批计划 + +### 第一批(MVP 核心链路)- 预计 4~6 周 + +1. 项目初始化:pom.xml、application.yml、启动类 +2. 公共模块:ApiResponse、BaseEntity、异常、幂等、审计切面 +3. 数据库基线:V1__baseline.sql(全部核心表) +4. 状态机:四层状态机 + 状态流转记录 +5. 申请域:创建、提交、受理通过/退回、取消 +6. 评估域:派发评估、签到、提交报告、异议 +7. 方案域:创建、编辑、提交签署、签署/拒签、版本管理 +8. 计划域:服务计划生成 +9. 工单域:工单创建、派单、接单/拒单 +10. 执行域:签到、逐项执行记录、完成 +11. 验收域:验收确认/拒绝、评价 +12. 结算域:生成结算单、审核、支付确认、归档 +13. 通知域:Outbox 写入 + 定时发送 +14. 审计域:状态流转记录 + 审计日志 + +### 第二批(增强能力)- 预计 3~4 周 + +1. 调度算法:硬约束过滤 + 软约束评分 + Top5推荐 +2. 监管域:抽查计划、违规记录、整改跟踪 +3. 合规域:授权同意、敏感数据访问审计 +4. 主数据域:机构、人员、资质、服务项目、区域、价格 +5. 异常域:异常上报、处理动作、紧急事件 +6. delivery 端小程序开发 + +### 第三批(智能化与运营)- 后续迭代 + +1. 运力域:服务网格、容量预测 +2. ETA 预测 +3. 调度算法优化(局部搜索/OR-Tools) +4. 数据看板和质量分析 +5. 人员绩效结算 + +--- + +## 13. 短 Prompt + +```txt +基于 /home/akoo/居家服务/IMPLEMENTATION_PLAN.md 实现 MVP 第一批(项目初始化 + 公共模块 + 数据库基线 + 状态机 + 申请域 + 评估域)。 +技术栈:Spring Boot 3.3 + Java 17 + MyBatis-Plus + PostgreSQL + Flyway。 +- 严格遵循 hss_ 表前缀和审计字段规范 +- 前端只传动作不传目标状态 +- 所有写接口带 Idempotency-Key +- 通知走 Outbox 模式 +- 输出 Flyway V1 迁移脚本 + Java 代码 +``` + + +/goal 将当前系统中所有的简化实现,替代实现,未完善实现,后续需补充实现,需要接入服务的实现等等包括不完整的功能实现全部整理成文档,标注清楚 \ No newline at end of file diff --git a/README.md b/README.md index fccb8c1..ced2590 100644 --- a/README.md +++ b/README.md @@ -130,3 +130,37 @@ hss_consent_records + hss_data_access_logs (合规) | V4 | TIMESTAMPTZ→TIMESTAMP 兼容修复 | | V5 | 状态流转唯一约束扩展 | | V6 | 官网线索表 | +| V7 | 服务地址经纬度(GPS 签到) | +| V8 | GPS 轨迹表 | +| V9 | 认证字段(密码哈希 + 登录时间) | +| V10 | 外键约束补充 | +| V11 | 消息表 + 系统配置表 + 离线同步日志 | +| V12 | 剩余外键 + 性能索引(phone/settlement_id 等) | + +## 项目状态 + +### 已完成 +- 后端 93 个 REST API + 四层状态机(49 条规则) +- 数据库 49 张表 + 12 个 Flyway 迁移 +- 官网 11 个页面(Nuxt3 + Tailwind CSS) +- 平台管理:注册/登录/申请管理/工单管理/全流程测试 +- delivery-miniapp 12 个页面(uni-app,含 GPS 签到/执行/异常/离线补传) +- 微信支付 SDK(JSAPI 下单 + 回调验签 + 退款) +- 调度算法(硬约束过滤 + Haversine 距离 + 资质匹配 + Top5 推荐) +- JWT 认证 + 角色权限 + 数据脱敏 +- Playwright E2E 测试(89 用例)+ CI/CD 管道 + +### 需外部服务接入 +| 服务 | 用途 | 优先级 | +|------|------|--------| +| MQTT Broker | delivery 端实时通知 | P0 | +| 微信订阅消息 API | 用户通知推送 | P0 | +| 短信 SDK | 紧急通知兜底 | P0 | +| 对象存储(MinIO/S3) | 证据文件存储 | P1 | +| 地图 API(高德/百度) | 调度距离 + ETA | P1 | +| 域名 + SSL + ICP 备案 | 生产上线 | P1 | + +### 相关文档 +- [待完善清单(终极版)](hss-home-service/待完善清单_终极版.md) — 62 项,含修复状态 +- [外部服务接入清单](hss-home-service/外部服务接入清单.md) — 9 项外部服务详细说明 +- [官网设计方案 Prompt](hss-home-service/官网设计方案Prompt.txt) diff --git a/hss-home-service/Dockerfile b/hss-home-service/Dockerfile index f507fe6..54c41a9 100644 --- a/hss-home-service/Dockerfile +++ b/hss-home-service/Dockerfile @@ -1,18 +1,29 @@ FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build -COPY settings.xml /root/.m2/settings.xml COPY pom.xml . -RUN mvn dependency:go-offline -B -q || true -COPY src/ src/ -RUN mvn package -DskipTests -B -q +COPY settings.xml /root/.m2/settings.xml -FROM eclipse-temurin:17-jre +# Maven proxy 配置:通过 172.17.0.1 (Docker 网关) 访问主机 Clash 代理 +RUN mvn dependency:go-offline -B -q \ + -Dhttp.proxyHost=172.17.0.1 -Dhttp.proxyPort=7890 \ + -Dhttps.proxyHost=172.17.0.1 -Dhttps.proxyPort=7890 \ + -Dhttp.nonProxyHosts=localhost\|127.0.0.1\|172.17.0.1 || true + +COPY src/ src/ + +RUN mvn package -DskipTests -B -q \ + -Dhttp.proxyHost=172.17.0.1 -Dhttp.proxyPort=7890 \ + -Dhttps.proxyHost=172.17.0.1 -Dhttps.proxyPort=7890 \ + -Dhttp.nonProxyHosts=localhost\|127.0.0.1\|172.17.0.1 + +FROM eclipse-temurin:17-jre-jammy AS runtime WORKDIR /app COPY --from=builder /build/target/*.jar app.jar -RUN addgroup --system hss && adduser --system --ingroup hss hss +RUN addgroup --system hss && adduser --system --ingroup hss hss 2>/dev/null; exit 0 USER hss EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD wget -qO- http://localhost:8080/actuator/health || exit 1 ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"] diff --git a/hss-home-service/delivery-miniapp/IMPLEMENTATION.md b/hss-home-service/delivery-miniapp/IMPLEMENTATION.md new file mode 100644 index 0000000..7588457 --- /dev/null +++ b/hss-home-service/delivery-miniapp/IMPLEMENTATION.md @@ -0,0 +1,515 @@ +# 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 的风格。 +``` diff --git a/hss-home-service/delivery-miniapp/common/api.js b/hss-home-service/delivery-miniapp/common/api.js index 97dfd64..9a54846 100644 --- a/hss-home-service/delivery-miniapp/common/api.js +++ b/hss-home-service/delivery-miniapp/common/api.js @@ -1,4 +1,6 @@ -const BASE_URL = 'http://localhost:18080/api/hss'; +// BASE_URL configured via manifest.json or build-time env +// For production, update this to your actual server URL +const BASE_URL = 'http://172.31.12.249:18080/api/hss'; function getHeaders() { const token = uni.getStorageSync('token'); @@ -23,4 +25,80 @@ function apiPost(path, data = {}) { return uni.request({ url: BASE_URL + path, method: 'POST', data, header: headers }); } -module.exports = { BASE_URL, apiGet, apiPost, generateIdempotencyKey }; +// ==================== GPS ==================== + +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 + }); +} + +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 + }); + }); +} + +// ==================== 文件上传 ==================== + +async function uploadEvidence(filePath, entityType, entityId) { + const fileName = filePath.split('/').pop() || 'photo.jpg'; + const ft = fileName.endsWith('.mp4') ? 'VIDEO' : fileName.endsWith('.mp3') ? 'AUDIO' : 'PHOTO'; + + const presignRes = await apiPost('/evidence/presign-upload', { fileName, fileType: ft, entityType, entityId }); + if (presignRes.data.code !== 200) throw new Error('PreSign failed'); + const { fileKey } = presignRes.data.data; + + return new Promise((resolve, reject) => { + uni.uploadFile({ + url: BASE_URL + '/evidence/commit', + filePath, name: 'file', + formData: { fileKey, fileName, fileHash: '', fileSize: '0', contentType: 'image/jpeg' }, + header: getHeaders(), + success: (r) => { + try { resolve({ fileKey, response: JSON.parse(r.data) }); } + catch (e) { resolve({ fileKey, response: r.data }); } + }, + fail: reject + }); + }); +} + +module.exports = { + addWatermark, + BASE_URL, apiGet, apiPost, generateIdempotencyKey, + getLocation, startTrajectory, stopTrajectory, + chooseImage, uploadEvidence, +}; diff --git a/hss-home-service/delivery-miniapp/common/offline.js b/hss-home-service/delivery-miniapp/common/offline.js new file mode 100644 index 0000000..756b339 --- /dev/null +++ b/hss-home-service/delivery-miniapp/common/offline.js @@ -0,0 +1,36 @@ +const OFFLINE_QUEUE_KEY = 'hss_offline_queue'; + +class OfflineQueue { + constructor() { + try { this.queue = uni.getStorageSync(OFFLINE_QUEUE_KEY) || []; } + catch (e) { this.queue = []; } + } + + 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 && (res.data.code === 200 || res.data.code === '200')) continue; + failed.push(item); + } catch (e) { failed.push(item); } + } + this.queue = failed; + this.save(); + return { success: failed.length === 0, total: this.getCount(), remaining: failed.length }; + } + + clear() { this.queue = []; this.save(); } +} + +module.exports = { OfflineQueue }; diff --git a/hss-home-service/delivery-miniapp/manifest.json b/hss-home-service/delivery-miniapp/manifest.json index c41bf62..13f215a 100644 --- a/hss-home-service/delivery-miniapp/manifest.json +++ b/hss-home-service/delivery-miniapp/manifest.json @@ -29,7 +29,14 @@ "" ] }, - "ios": {} + "ios": { + "app-plus": { + "plists": { + "NSLocationWhenInUseUsageDescription": "需要获取位置进行GPS签到和轨迹记录", + "NSCameraUsageDescription": "需要拍照记录服务现场情况" + } + } + } } }, "quickapp": {}, diff --git a/hss-home-service/delivery-miniapp/pages/delivery/accept/accept.vue b/hss-home-service/delivery-miniapp/pages/delivery/accept/accept.vue new file mode 100644 index 0000000..ed77e38 --- /dev/null +++ b/hss-home-service/delivery-miniapp/pages/delivery/accept/accept.vue @@ -0,0 +1,56 @@ +