16 KiB
16 KiB
居家服务 - 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 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| string | 是 | 用户邮箱地址 | |
| password | string | 是 | 用户密码 |
请求示例:
{
"email": "dispatcher@example.com",
"password": "password123"
}
1.4 响应信息
✅ 成功响应(200)
{
"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)
{
"code": "UNAUTHORIZED",
"msg": "Invalid email or password",
"data": null,
"traceId": "abc123-def456-ghi789"
}
用户不存在(404)
{
"code": "NOT_FOUND",
"msg": "User not found",
"data": null,
"traceId": "abc123-def456-ghi789"
}
1.5 前端调用示例
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 格式) |
请求示例:
{
"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)
{
"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)
{
"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)
{
"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)
{
"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 前端调用示例
/**
* 签到距离预校验
* @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 公式 计算两点之间的球面距离:
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 表配置,优先级如下:
- 工单级别 -
scope_type = 'WORK_ORDER' - 组织级别 -
scope_type = 'ORG' - 团队级别 -
scope_type = 'TEAM' - 全局默认 -
scope_type = 'GLOBAL'(默认 50 米)
查询配置 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 数据库验证
预校验接口调用后,验证以下数据写入:
-- 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 表中的配置:
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;
七、相关文档
文档维护: 如有接口变更,请及时更新此文档。