Files
medical-mall/docs/AUTH_CHECKIN_API_GUIDE.md
2026-06-10 20:20:47 +08:00

529 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 居家服务 - 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 | 是 | 工单 IDUUID |
| 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)
---
**文档维护:** 如有接口变更,请及时更新此文档。