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
+
+
+
+
+ 📍 位置确认
+ 经度 {{location.longitude}} 纬度 {{location.latitude}}
+
+
+
+
+
+ 📷 现场拍照
+
+
+
+
+
+
+ ✍️ 服务对象确认
+
+ {{ confirmed ? '已确认' : '请服务对象确认' }}
+
+
+
+
+
+
+
+
+```
+
+### 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 @@
+
+
+ 工单 #{{ workOrderId }}请在确认接单前查看服务详情
+
+
+
+
+
+ 拒单原因
+
+
+
+
+
+
+
+
+
diff --git a/hss-home-service/delivery-miniapp/pages/delivery/checkin/checkin.vue b/hss-home-service/delivery-miniapp/pages/delivery/checkin/checkin.vue
new file mode 100644
index 0000000..c620d25
--- /dev/null
+++ b/hss-home-service/delivery-miniapp/pages/delivery/checkin/checkin.vue
@@ -0,0 +1,108 @@
+
+
+
+ 📍 位置确认
+
+ 经度{{ location.longitude.toFixed(6) }}
+ 纬度{{ location.latitude.toFixed(6) }}
+
+
+
+
+
+ 📷 现场拍照
+
+
+
+
+
+ ✍️ 服务对象确认
+
+ {{ confirmed ? '已确认' : '请服务对象确认签到' }}
+
+
+
+
+
+
+
+
+
diff --git a/hss-home-service/delivery-miniapp/pages/delivery/exception/exception.vue b/hss-home-service/delivery-miniapp/pages/delivery/exception/exception.vue
new file mode 100644
index 0000000..698a37f
--- /dev/null
+++ b/hss-home-service/delivery-miniapp/pages/delivery/exception/exception.vue
@@ -0,0 +1,48 @@
+
+
+ 异常上报
+
+ {{ t.label }}
+
+
+
+
+
+
+
+
+
diff --git a/hss-home-service/delivery-miniapp/pages/delivery/execute/execute.vue b/hss-home-service/delivery-miniapp/pages/delivery/execute/execute.vue
new file mode 100644
index 0000000..acf4e8a
--- /dev/null
+++ b/hss-home-service/delivery-miniapp/pages/delivery/execute/execute.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+ 需拍照留证
+
+
+
+
+
+
+
+
+ 所有项目已完成,可前往签退
+
+
+
+
+
+
diff --git a/hss-home-service/delivery-miniapp/pages/delivery/finish/finish.vue b/hss-home-service/delivery-miniapp/pages/delivery/finish/finish.vue
new file mode 100644
index 0000000..3e970b0
--- /dev/null
+++ b/hss-home-service/delivery-miniapp/pages/delivery/finish/finish.vue
@@ -0,0 +1,65 @@
+
+
+ 签退完成
+ 执行摘要{{ completedCount }} / {{ totalCount }} 项已完成
+
+ 签退定位经度 {{ loc.longitude.toFixed(6) }} 纬度 {{ loc.latitude.toFixed(6) }}
+
+
+
+
+
+
+
diff --git a/hss-home-service/delivery-miniapp/pages/delivery/messages/messages.vue b/hss-home-service/delivery-miniapp/pages/delivery/messages/messages.vue
new file mode 100644
index 0000000..335c72c
--- /dev/null
+++ b/hss-home-service/delivery-miniapp/pages/delivery/messages/messages.vue
@@ -0,0 +1,34 @@
+
+
+ 消息通知
+
+ {{ eventMap[m.event_type] || m.event_type }}
+ {{ m.created_at?.substring(0,16) || '' }}
+
+ 暂无消息
+
+
+
+
+
+
diff --git a/hss-home-service/delivery-miniapp/pages/delivery/offline-sync/offline-sync.vue b/hss-home-service/delivery-miniapp/pages/delivery/offline-sync/offline-sync.vue
new file mode 100644
index 0000000..6eb16f1
--- /dev/null
+++ b/hss-home-service/delivery-miniapp/pages/delivery/offline-sync/offline-sync.vue
@@ -0,0 +1,39 @@
+
+
+ 离线补传
+ {{ count }}条待补传
+
+ {{ result.success ? '✓ 全部补传成功' : '剩余 ' + result.remaining + ' 条失败' }}
+
+
+
+
+
+
diff --git a/hss-home-service/delivery-miniapp/pages/delivery/order-detail/order-detail.vue b/hss-home-service/delivery-miniapp/pages/delivery/order-detail/order-detail.vue
new file mode 100644
index 0000000..c8fd3ac
--- /dev/null
+++ b/hss-home-service/delivery-miniapp/pages/delivery/order-detail/order-detail.vue
@@ -0,0 +1,57 @@
+
+
+ 加载中...
+
+ 服务对象{{ detail.patient_name || '-' }}{{ detail.patient_address || '-' }}
+ 服务时间{{ detail.time_window_start }} - {{ detail.time_window_end }}
+ 风险等级{{ detail.risk_level || '低' }}
+ 服务项目
+ {{ item.item_name }}{{ item.required ? '必做' : '选做' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hss-home-service/delivery-miniapp/pages/delivery/orders/orders.vue b/hss-home-service/delivery-miniapp/pages/delivery/orders/orders.vue
new file mode 100644
index 0000000..b428f91
--- /dev/null
+++ b/hss-home-service/delivery-miniapp/pages/delivery/orders/orders.vue
@@ -0,0 +1,49 @@
+
+
+
+ {{ t.label }}
+
+ 加载中...
+
+ {{ o.patient_name || '服务对象' }}{{ statusMap[o.status] || o.status }}
+ {{ o.time_window_start }} - {{ o.time_window_end }}
+
+ 暂无工单
+
+
+
+
+
+
diff --git a/hss-home-service/delivery-miniapp/pages/delivery/profile/profile.vue b/hss-home-service/delivery-miniapp/pages/delivery/profile/profile.vue
new file mode 100644
index 0000000..a084750
--- /dev/null
+++ b/hss-home-service/delivery-miniapp/pages/delivery/profile/profile.vue
@@ -0,0 +1,45 @@
+
+
+
+
+ {{ stats.todayOrders || 0 }}今日工单
+ {{ stats.completedToday || 0 }}已完成
+ {{ stats.exceptionCount || 0 }}异常
+
+
+
+
+
+
+
+
diff --git a/hss-home-service/docker-compose.yml b/hss-home-service/docker-compose.yml
index 09ee496..9f75170 100644
--- a/hss-home-service/docker-compose.yml
+++ b/hss-home-service/docker-compose.yml
@@ -54,13 +54,12 @@ services:
restart: unless-stopped
website:
- image: nginx:alpine
+ build:
+ context: ./website
+ dockerfile: Dockerfile
container_name: hss-website
ports:
- "3080:80"
- volumes:
- - ./website/.output/public:/usr/share/nginx/html:ro
- - ./website/nginx-site.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
restart: unless-stopped
diff --git a/hss-home-service/pom.xml b/hss-home-service/pom.xml
index a2564fb..00db5f9 100644
--- a/hss-home-service/pom.xml
+++ b/hss-home-service/pom.xml
@@ -35,6 +35,10 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
org.springframework.boot
spring-boot-starter-security
@@ -83,25 +87,6 @@
${springdoc.version}
-
-
- org.mapstruct
- mapstruct
- ${mapstruct.version}
-
-
- org.mapstruct
- mapstruct-processor
- ${mapstruct.version}
- provided
-
-
-
-
- cn.hutool
- hutool-all
- ${hutool.version}
-
@@ -110,11 +95,25 @@
true
-
+
- com.fasterxml.jackson.datatype
- jackson-datatype-jsr310
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+ runtime
+
+
@@ -166,11 +165,6 @@
lombok
${lombok.version}
-
- org.mapstruct
- mapstruct-processor
- ${mapstruct.version}
-
diff --git a/hss-home-service/src/main/java/com/meizhou/hss/action/ActionExecutor.java b/hss-home-service/src/main/java/com/meizhou/hss/action/ActionExecutor.java
deleted file mode 100644
index 63917ec..0000000
--- a/hss-home-service/src/main/java/com/meizhou/hss/action/ActionExecutor.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.meizhou.hss.action;
-
-import com.meizhou.hss.common.Auditable;
-import com.meizhou.hss.common.RequestContext;
-import com.meizhou.hss.module.notification.service.NotificationService;
-import com.meizhou.hss.statemachine.StateMachine;
-import com.meizhou.hss.statemachine.StateTransitionException;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-/**
- * 标准 Action 执行器。供各模块 Service 调用,统一编排状态流转 + 副作用。
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class ActionExecutor {
-
- private final StateMachine stateMachine;
- private final NotificationService notificationService;
-
- /**
- * 执行一次状态动作。
- * Service 中只需调用此方法,无需手动管理状态机、流转记录、Outbox、审计。
- */
- @Transactional
- @Auditable(action = "#handler.actionName()", entityType = "#handler.entityType()")
- public S execute(ActionHandler handler, Long entityId,
- R request, T entity, String currentStatus) {
-
- // 1. 校验
- handler.validate(entity, request);
-
- // 2. 状态机校验
- String role = RequestContext.getUserRole();
- if (!stateMachine.canTransition(handler.entityType(), currentStatus,
- handler.actionName(), role)) {
- throw new StateTransitionException(handler.entityType(), currentStatus,
- handler.actionName(), "状态转换不合法");
- }
-
- // 3. 执行副作用
- handler.executeSideEffects(entity, request);
-
- // 4. 状态流转(记录 hss_state_transitions)
- String targetStatus = stateMachine.transition(handler.entityType(), entityId,
- currentStatus, handler.actionName(), role, null);
-
- // 5. Outbox 事件
- for (ActionHandler.OutboxEvent event : handler.getOutboxEvents(entity, request)) {
- notificationService.enqueue(event.businessType(), event.businessId(),
- event.eventType(), event.receiverId(), event.channel(), event.params());
- }
-
- // 6. 返回
- log.info("Action executed: type={}, id={}, action={}, {} -> {}",
- handler.entityType(), entityId, handler.actionName(), currentStatus, targetStatus);
-
- return handler.buildResult(entity);
- }
-}
diff --git a/hss-home-service/src/main/java/com/meizhou/hss/action/ActionHandler.java b/hss-home-service/src/main/java/com/meizhou/hss/action/ActionHandler.java
deleted file mode 100644
index 978beb5..0000000
--- a/hss-home-service/src/main/java/com/meizhou/hss/action/ActionHandler.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.meizhou.hss.action;
-
-/**
- * 状态动作处理器接口。
- *
- * 职责分层(§16 设计):
- * StateMachine — 只判断动作是否合法(fromStatus + action + role → toStatus)
- * ActionHandler — 编排所有业务副作用(写表、Outbox、审计、冻结快照、后续联动)
- *
- * 标准执行流程:
- * 1. 加载业务对象
- * 2. 校验租户、机构、角色、数据范围
- * 3. 校验 Idempotency-Key
- * 4. 调用 StateMachine 校验动作合法性
- * 5. 执行业务副作用
- * 6. 更新业务状态
- * 7. 写 hss_state_transitions
- * 8. 写 hss_audit_logs
- * 9. 写 hss_notification_outbox
- * 10. 提交事务
- * 11. 返回动作结果
- *
- * @param 请求参数类型
- * @param 业务对象实体类型
- * @param 返回结果类型
- */
-public interface ActionHandler {
-
- /** 加载业务对象 */
- T loadEntity(Long entityId);
-
- /** 校验数据范围与业务规则 */
- void validate(T entity, R request);
-
- /** 执行业务副作用(在状态机通过之后、状态更新之前) */
- void executeSideEffects(T entity, R request);
-
- /** 返回结果转换 */
- S buildResult(T entity);
-
- /** 获取实体类型名(用于状态机查询) */
- String entityType();
-
- /** 获取动作名 */
- String actionName();
-
- /** 获取允许的角色 */
- String requiredRole();
-
- /** 获取需要写入的 Outbox 事件列表(可为空) */
- default java.util.List getOutboxEvents(T entity, R request) {
- return java.util.List.of();
- }
-
- record OutboxEvent(String businessType, Long businessId, String eventType,
- Long receiverId, String channel, java.util.Map params) {}
-}
diff --git a/hss-home-service/src/main/java/com/meizhou/hss/common/BaseEntity.java b/hss-home-service/src/main/java/com/meizhou/hss/common/BaseEntity.java
index c0afa4e..b5b4c49 100644
--- a/hss-home-service/src/main/java/com/meizhou/hss/common/BaseEntity.java
+++ b/hss-home-service/src/main/java/com/meizhou/hss/common/BaseEntity.java
@@ -1,9 +1,6 @@
package com.meizhou.hss.common;
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableLogic;
-import com.baomidou.mybatisplus.annotation.Version;
+import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@@ -23,8 +20,10 @@ public abstract class BaseEntity {
@Version
private Integer version;
+ @TableField(updateStrategy = FieldStrategy.NEVER)
private LocalDateTime createdAt;
+ @TableField(updateStrategy = FieldStrategy.NEVER)
private LocalDateTime updatedAt;
private Long createdBy;
diff --git a/hss-home-service/src/main/java/com/meizhou/hss/common/DigestUtil.java b/hss-home-service/src/main/java/com/meizhou/hss/common/DigestUtil.java
new file mode 100644
index 0000000..219cd53
--- /dev/null
+++ b/hss-home-service/src/main/java/com/meizhou/hss/common/DigestUtil.java
@@ -0,0 +1,23 @@
+package com.meizhou.hss.common;
+
+import java.security.MessageDigest;
+
+/** JDK 内置 MD5/SHA 摘要工具(替代 hutool DigestUtil,零外部依赖) */
+public final class DigestUtil {
+
+ private DigestUtil() {}
+
+ public static String md5Hex(String input) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] digest = md.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ StringBuilder sb = new StringBuilder(32);
+ for (byte b : digest) {
+ sb.append(String.format("%02x", b & 0xff));
+ }
+ return sb.toString();
+ } catch (Exception e) {
+ throw new RuntimeException("MD5 digest failed", e);
+ }
+ }
+}
diff --git a/hss-home-service/src/main/java/com/meizhou/hss/common/GeoUtil.java b/hss-home-service/src/main/java/com/meizhou/hss/common/GeoUtil.java
new file mode 100644
index 0000000..390be78
--- /dev/null
+++ b/hss-home-service/src/main/java/com/meizhou/hss/common/GeoUtil.java
@@ -0,0 +1,26 @@
+package com.meizhou.hss.common;
+
+/** 地理计算工具 — Haversine 公式计算两点间距离(米) */
+public final class GeoUtil {
+
+ private static final double EARTH_RADIUS_M = 6_371_000.0;
+
+ private GeoUtil() {}
+
+ /**
+ * 计算两个经纬度坐标之间的地球表面距离(米)。
+ * @param lat1 纬度1
+ * @param lng1 经度1
+ * @param lat2 纬度2
+ * @param lng2 经度2
+ * @return 距离(米)
+ */
+ public static double haversineDistance(double lat1, double lng1, double lat2, double lng2) {
+ double dLat = Math.toRadians(lat2 - lat1);
+ double dLng = Math.toRadians(lng2 - lng1);
+ double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
+ + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
+ * Math.sin(dLng / 2) * Math.sin(dLng / 2);
+ return EARTH_RADIUS_M * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ }
+}
diff --git a/hss-home-service/src/main/java/com/meizhou/hss/common/IdempotentAspect.java b/hss-home-service/src/main/java/com/meizhou/hss/common/IdempotentAspect.java
index 81ca68a..c1060bb 100644
--- a/hss-home-service/src/main/java/com/meizhou/hss/common/IdempotentAspect.java
+++ b/hss-home-service/src/main/java/com/meizhou/hss/common/IdempotentAspect.java
@@ -1,6 +1,6 @@
package com.meizhou.hss.common;
-import cn.hutool.crypto.digest.DigestUtil;
+import com.meizhou.hss.common.DigestUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
diff --git a/hss-home-service/src/main/java/com/meizhou/hss/common/JdbcUtil.java b/hss-home-service/src/main/java/com/meizhou/hss/common/JdbcUtil.java
new file mode 100644
index 0000000..3cc433c
--- /dev/null
+++ b/hss-home-service/src/main/java/com/meizhou/hss/common/JdbcUtil.java
@@ -0,0 +1,46 @@
+package com.meizhou.hss.common;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/** JdbcTemplate 辅助方法 — 消除 count/round/paginate 重复 */
+public final class JdbcUtil {
+
+ private JdbcUtil() {}
+
+ /** 执行计数查询,null 返回 0 */
+ public static long count(JdbcTemplate jt, String sql, Object... args) {
+ Long r = jt.queryForObject(sql, Long.class, args);
+ return r != null ? r : 0;
+ }
+
+ /** 四舍五入到两位小数 */
+ public static double round(Double val) {
+ return val != null ? Math.round(val * 100.0) / 100.0 : 0.0;
+ }
+
+ /**
+ * 统一分页查询。
+ * @return Map 包含 "list" (当前页数据) 和 "total" (总记录数)
+ */
+ public static Map paginate(JdbcTemplate jt,
+ String selectClause, String fromWhereClause,
+ String orderBy, int page, int size, Object... args) {
+ long total = count(jt, "SELECT COUNT(*) " + fromWhereClause, args);
+ int offset = (page - 1) * size;
+ List