529 lines
16 KiB
Markdown
529 lines
16 KiB
Markdown
# 居家服务 - 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<Object>} 登录结果
|
||
*/
|
||
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<Object>} 校验结果
|
||
*/
|
||
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)
|
||
|
||
---
|
||
|
||
**文档维护:** 如有接口变更,请及时更新此文档。
|