# 居家服务 - Auth 登录与距离预校验接口联调文档 ## 文档信息 | 项目 | 说明 | |------|------| | **文档版本** | v1.0 | | **创建日期** | 2026-06-09 | | **后端地址** | http://localhost:4001 | | **Supabase 地址** | http://119.1496.131.237:9126 | | **RPC 函数** | `rpc_homecare_checkin_precheck()` | --- ## 一、邮箱登录接口 ### 1.1 接口概述 用户通过邮箱和密码登录系统,后端验证成功后返回 JWT Token 和用户信息。 ### 1.2 请求信息 ``` POST /auth/email-login Content-Type: application/json ``` ### 1.3 请求参数 | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | email | string | 是 | 用户邮箱地址 | | password | string | 是 | 用户密码 | **请求示例:** ```json { "email": "dispatcher@example.com", "password": "password123" } ``` ### 1.4 响应信息 #### ✅ 成功响应(200) ```json { "code": "OK", "msg": "success", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": "ad0cd0e6-fe95-4946-b189-536dc18cf9e5", "email": "dispatcher@example.com", "role": "HOMECARE_DISPATCHER", "full_name": "管理员", "org_id": "00000000-0000-0000-0000-000000000000", "created_at": "2026-06-01T00:00:00.000Z" } }, "traceId": "abc123-def456-ghi789" } ``` **字段说明:** | 字段 | 类型 | 说明 | |------|------|------| | token | string | JWT Token,后续请求需携带此 Token | | user.id | string | 用户唯一标识(UUID) | | user.email | string | 用户邮箱 | | user.role | string | 用户角色:`HOMECARE_DISPATCHER`(派单员)/ `HOMECARE_WORKER`(居家服务员) | | user.full_name | string | 用户姓名 | | user.org_id | string | 所属组织 ID | #### ❌ 失败响应 **邮箱或密码错误(401)** ```json { "code": "UNAUTHORIZED", "msg": "Invalid email or password", "data": null, "traceId": "abc123-def456-ghi789" } ``` **用户不存在(404)** ```json { "code": "NOT_FOUND", "msg": "User not found", "data": null, "traceId": "abc123-def456-ghi789" } ``` ### 1.5 前端调用示例 ```javascript import axios from 'axios'; const API_BASE = 'http://localhost:4001'; /** * 邮箱登录 * @param {string} email - 邮箱地址 * @param {string} password - 密码 * @returns {Promise} 登录结果 */ async function login(email, password) { try { const response = await axios.post(`${API_BASE}/auth/email-login`, { email, password }); const { token, user } = response.data.data; // 保存 Token 到本地存储 localStorage.setItem('auth_token', token); localStorage.setItem('user_info', JSON.stringify(user)); // 设置 axios 默认 Header axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; return { success: true, user }; } catch (error) { if (error.response) { console.error('登录失败:', error.response.data.msg); return { success: false, message: error.response.data.msg }; } return { success: false, message: '网络错误' }; } } // 使用示例 const result = await login('dispatcher@example.com', 'password123'); if (result.success) { console.log('登录成功,用户信息:', result.user); } ``` ### 1.6 测试账号 | 角色 | 邮箱 | 密码 | 说明 | |------|------|------|------| | 派单员 | `dispatcher@example.com` | `password123` | 具有派单权限 | | 居家服务员 | `worker@example.com` | `password123` | 具有接单/签到权限 | --- ## 二、距离预校验接口 ### 2.1 接口概述 居家服务员到达服务地点前,调用此接口校验当前位置与服务地点的距离是否在允许范围内。 ### 2.2 接口信息 ``` POST /homecare/checkin/precheck Content-Type: application/json Authorization: Bearer {token} ``` ### 2.3 请求参数 | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | workOrderId | string | 是 | 工单 ID(UUID) | | latitude | number | 是 | 当前纬度(GCJ02 坐标系) | | longitude | number | 是 | 当前经度(GCJ02 坐标系) | | coordinateType | string | 否 | 坐标系类型,默认 `gcj02` | | accuracy | number | 否 | 定位精度(米) | | reportedAt | string | 否 | 报告时间(ISO 8601 格式) | **请求示例:** ```json { "workOrderId": "a6450755-add4-4bdf-839f-165459ddff5d", "latitude": 39.9042, "longitude": 116.4074, "coordinateType": "gcj02", "accuracy": 10, "reportedAt": "2026-06-09T10:30:00.000Z" } ``` ### 2.4 响应信息 #### ✅ 校验通过(200) ```json { "code": "OK", "msg": "success", "data": { "distanceMeters": 15.5, "allowedRadiusMeters": 50, "canCheckin": true, "reasonCode": "OK", "workerLocationAccepted": true, "serviceLocationReady": true }, "traceId": "abc123-def456-ghi789" } ``` **字段说明:** | 字段 | 类型 | 说明 | |------|------|------| | distanceMeters | number | 当前位置与服务地点的距离(米) | | allowedRadiusMeters | number | 允许的签到半径(默认 50 米) | | canCheckin | boolean | 是否允许签到 | | reasonCode | string | 原因代码:`OK`(通过)/ `OUT_OF_RADIUS`(超出范围)/ `SERVICE_LOCATION_MISSING`(服务地点缺失) | | workerLocationAccepted | boolean | 人员位置是否已记录 | | serviceLocationReady | boolean | 服务地点是否已配置 | #### ❌ 校验失败 **超出签到半径(400)** ```json { "code": "BAD_REQUEST", "msg": "Checkin rejected", "data": { "distanceMeters": 150.0, "allowedRadiusMeters": 50, "canCheckin": false, "reasonCode": "OUT_OF_RADIUS", "workerLocationAccepted": true, "serviceLocationReady": true }, "traceId": "abc123-def456-ghi789" } ``` **服务地点未配置(400)** ```json { "code": "BAD_REQUEST", "msg": "Checkin rejected", "data": { "distanceMeters": null, "allowedRadiusMeters": 0, "canCheckin": false, "reasonCode": "SERVICE_LOCATION_MISSING", "workerLocationAccepted": false, "serviceLocationReady": false }, "traceId": "abc123-def456-ghi789" } ``` **工单未分配(400)** ```json { "code": "BAD_REQUEST", "msg": "Checkin rejected", "data": { "distanceMeters": null, "allowedRadiusMeters": 0, "canCheckin": false, "reasonCode": "WORK_ORDER_NOT_ASSIGNABLE", "workerLocationAccepted": false, "serviceLocationReady": false }, "traceId": "abc123-def456-ghi789" } ``` ### 2.5 前端调用示例 ```javascript /** * 签到距离预校验 * @param {string} workOrderId - 工单 ID * @param {number} latitude - 纬度 * @param {number} longitude - 经度 * @param {Object} options - 可选参数 * @returns {Promise} 校验结果 */ async function checkinPrecheck(workOrderId, latitude, longitude, options = {}) { const token = localStorage.getItem('auth_token'); if (!token) { return { success: false, message: '请先登录' }; } try { const response = await axios.post( `${API_BASE}/homecare/checkin/precheck`, { workOrderId, latitude, longitude, coordinateType: options.coordinateType || 'gcj02', accuracy: options.accuracy, reportedAt: options.reportedAt || new Date().toISOString() }, { headers: { 'Authorization': `Bearer ${token}` } } ); const { data } = response.data; if (data.canCheckin) { console.log(`✅ 签到预校验通过`); console.log(` 距离: ${data.distanceMeters} 米`); console.log(` 允许半径: ${data.allowedRadiusMeters} 米`); return { success: true, ...data }; } else { console.log(`❌ 签到预校验失败: ${data.reasonCode}`); console.log(` 距离: ${data.distanceMeters} 米`); console.log(` 允许半径: ${data.allowedRadiusMeters} 米`); return { success: false, reasonCode: data.reasonCode, ...data }; } } catch (error) { if (error.response) { console.error('预校验请求失败:', error.response.data.msg); return { success: false, message: error.response.data.msg }; } return { success: false, message: '网络错误' }; } } // 使用示例 1: 范围内签到 const result1 = await checkinPrecheck( 'a6450755-add4-4bdf-839f-165459ddff5d', 39.9042, 116.4074 ); if (result1.success) { // 显示"可以签到"按钮 } // 使用示例 2: 超出范围签到 const result2 = await checkinPrecheck( 'a6450755-add4-4bdf-839f-165459ddff5d', 39.9142, // 距离约 1.1 公里 116.4174 ); if (!result2.success && result2.reasonCode === 'OUT_OF_RADIUS') { // 显示"您超出签到范围"提示 } ``` ### 2.6 距离计算原理 后端使用 **Haversine 公式** 计算两点之间的球面距离: ```javascript function calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371000; // 地球半径(米) const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } function toRad(degrees) { return degrees * Math.PI / 180; } ``` ### 2.7 签到半径配置 签到半径通过 `sys_sla_config` 表配置,优先级如下: 1. **工单级别** - `scope_type = 'WORK_ORDER'` 2. **组织级别** - `scope_type = 'ORG'` 3. **团队级别** - `scope_type = 'TEAM'` 4. **全局默认** - `scope_type = 'GLOBAL'`(默认 50 米) **查询配置 SQL:** ```sql SELECT config_value::numeric as radius_meters FROM public.sys_sla_config WHERE config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' AND scope_type = 'GLOBAL' AND is_active = true LIMIT 1; ``` --- ## 三、完整签到流程 ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 用户登录 │ ──► │ 管理员派单 │ ──► │ 居家服务员接单 │ ──► │ 签到预校验 │ │ (邮箱登录) │ │ (派单接口) │ │ (接单接口) │ │ (距离预校验) │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ ▼ │ ┌─────────────┐ │ │ 签到提交 │ │ │ (证据文件) │ │ └─────────────┘ ▼ │ ┌─────────────┐ │ │ 获取用户信息 │ │ └─────────────┘ ▼ ┌─────────────┐ │ 数据库更新 │ │ - ec_care_ │ │ tasks │ │ - hc_work_ │ │ order_ │ │ events │ └─────────────┘ ``` --- ## 四、错误码对照表 | 错误码 | HTTP 状态码 | 说明 | 处理建议 | |--------|------------|------|---------| | `OK` | 200 | 操作成功 | - | | `UNAUTHORIZED` | 401 | 邮箱或密码错误 | 提示用户检查账号密码 | | `NOT_FOUND` | 404 | 用户不存在 | 提示用户注册 | | `OUT_OF_RADIUS` | 400 | 超出签到半径 | 提示用户靠近服务地点 | | `SERVICE_LOCATION_MISSING` | 400 | 服务地点未配置 | 联系管理员配置服务地点 | | `WORK_ORDER_NOT_ASSIGNABLE` | 400 | 工单未分配 | 等待管理员派单 | | `WORKER_NOT_MATCHED` | 400 | 人员不匹配 | 当前用户非指定服务人员 | | `SIGNATURE_REQUIRED` | 400 | 缺少签名 | 签到提交时需要电子签名 | | `EVIDENCE_FILE_NOT_EXIST` | 400 | 证据文件不存在 | 签到提交时需要上传照片 | --- ## 五、联调测试清单 ### 5.1 登录接口测试 - [ ] 使用正确账号密码登录成功 - [ ] 使用错误密码登录失败 - [ ] 使用不存在的邮箱登录失败 - [ ] 验证返回的 Token 格式正确 - [ ] 验证 Token 可正常用于后续请求 ### 5.2 距离预校验测试 - [ ] 范围内签到(0 米)- 应该通过 - [ ] 范围内签到(50 米)- 应该通过 - [ ] 超出范围签到(100 米)- 应该拒绝 - [ ] 超出范围签到(1 公里)- 应该拒绝 - [ ] 服务地点未配置 - 应该拒绝 - [ ] 工单未分配 - 应该拒绝 - [ ] 人员不匹配 - 应该拒绝 ### 5.3 数据库验证 预校验接口调用后,验证以下数据写入: ```sql -- 1. 检查 hc_worker_locations 表(预校验会插入位置记录) SELECT * FROM public.hc_worker_locations WHERE work_order_id = 'a6450755-add4-4bdf-839f-165459ddff5d' ORDER BY created_at DESC LIMIT 5; -- 2. 检查 hc_dispatch_assignments 表(派单记录) SELECT * FROM public.hc_dispatch_assignments WHERE work_order_id = 'a6450755-add4-4bdf-839f-165459ddff5d' ORDER BY created_at DESC LIMIT 5; ``` --- ## 六、常见问题 ### Q1: 为什么预校验通过但签到提交失败? **A:** 签到提交需要额外的证据文件(照片)和电子签名。预校验只检查距离,签到提交会验证: - 至少 1 个有效的证据文件 - 有效的电子签名(至少 8 个字符) - 证据文件与工单匹配 ### Q2: 距离计算为什么有误差? **A:** 距离计算使用 Haversine 公式,基于 GCJ02 坐标系。实际误差来源: - 手机 GPS 定位精度(通常 5-20 米) - 坐标系转换误差 - 地球曲率近似计算 ### Q3: 如何修改签到半径? **A:** 修改 `sys_sla_config` 表中的配置: ```sql UPDATE public.sys_sla_config SET config_value = '100' -- 改为 100 米 WHERE config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' AND scope_type = 'GLOBAL' AND is_active = true; ``` --- ## 七、相关文档 - [居家服务 RPC 迁移文档](./居家服务方案/居家sql/新/rpc/README.md) - [迁移脚本说明](./居家服务方案/居家sql/新/20260605_homecare_migration_fixed.sql) - [SLA 配置说明](./居家服务方案/居家sql/新/rpc/README.md) --- **文档维护:** 如有接口变更,请及时更新此文档。