修复订单显示bug

This commit is contained in:
2026-06-10 20:20:47 +08:00
parent de62513987
commit 9fbc6f8cd1
45 changed files with 7514 additions and 2025 deletions

View File

@@ -0,0 +1,528 @@
# 居家服务 - 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)
---
**文档维护:** 如有接口变更,请及时更新此文档。