365 lines
12 KiB
Markdown
365 lines
12 KiB
Markdown
# SF快递物流状态判断指南
|
||
|
||
> **作者**: 阿瑞
|
||
> **版本**: 1.0.0
|
||
> **更新时间**: 2025-01-28
|
||
|
||
## 📖 概述
|
||
|
||
本文档详细介绍了如何在SF快递物流查询系统中准确判断和显示物流状态。基于SF快递官方API文档和实际开发经验总结。
|
||
|
||
## 🔍 状态判断数据源
|
||
|
||
### 官方标准数据结构
|
||
|
||
根据SF快递官方API文档,完整的路由节点应包含以下状态信息:
|
||
|
||
```typescript
|
||
interface RouteInfo {
|
||
acceptTime: string; // 路由节点发生时间
|
||
acceptAddress: string; // 路由节点发生地点
|
||
remark: string; // 路由节点具体描述
|
||
opCode: string; // 路由节点操作码
|
||
firstStatusCode?: string; // 一级状态编码
|
||
firstStatusName?: string; // 一级状态名称
|
||
secondaryStatusCode?: string; // 二级状态编码
|
||
secondaryStatusName?: string; // 二级状态名称
|
||
}
|
||
```
|
||
|
||
### 数据完整性检查
|
||
|
||
实际API返回的数据可能缺少状态编码字段,需要多种备用方案:
|
||
|
||
| 字段 | 重要程度 | 描述 |
|
||
|------|----------|------|
|
||
| `firstStatusName` | ⭐⭐⭐ | 官方一级状态,最准确 |
|
||
| `secondaryStatusName` | ⭐⭐⭐ | 官方二级状态,最详细 |
|
||
| `opCode` | ⭐⭐ | 操作码,可映射到状态 |
|
||
| `remark` | ⭐ | 文本描述,可通过关键词分析 |
|
||
|
||
## 🎯 状态判断方法
|
||
|
||
### 方法一:官方状态字段(推荐)
|
||
|
||
**优先级:最高**
|
||
|
||
```javascript
|
||
if (route.firstStatusName) {
|
||
return route.firstStatusName; // 直接使用官方状态
|
||
}
|
||
```
|
||
|
||
**常见状态值:**
|
||
- `待揽收` - 快件等待收取
|
||
- `已揽收` - 快件已被收取
|
||
- `运输中` - 快件正在运输
|
||
- `派送中` - 快件正在派送
|
||
- `已签收` - 快件成功签收
|
||
- `已拒收` - 快件被拒收
|
||
- `退回中` - 快件正在退回
|
||
- `已退回` - 快件已退回发件人
|
||
|
||
### 方法二:opCode映射表(备用)
|
||
|
||
**优先级:中等**
|
||
|
||
> ✅ **数据来源**: 基于真实SF快递物流数据分析,包含完整快递生命周期的opCode对应关系。
|
||
>
|
||
> 📋 **数据样本**: 分析了从揽收到退回成功的47个物流节点,包含以下新发现的opCode:
|
||
> - `43`: 已收取(重复确认)
|
||
> - `302`: 长途运输(显示距离信息)
|
||
> - `310`: 途经中转(城市中转点)
|
||
> - `44`: 准备派送(分配快递员)
|
||
> - `204`: 派送中(具体快递员信息)
|
||
> - `70`: 派送失败(多种失败原因)
|
||
> - `33`: 待派送(约定时间)
|
||
> - `517`: 退回申请中(客户申请)
|
||
> - `99`: 退回中(实际退回过程)
|
||
> - `80`: 退回成功(最终完成)
|
||
|
||
```javascript
|
||
const statusMap = {
|
||
// ========== 揽收相关 ==========
|
||
'54': { status: '已揽收', color: 'blue', description: '快件已被顺丰收取' },
|
||
'43': { status: '已收取', color: 'blue', description: '顺丰速运已收取快件' },
|
||
|
||
// ========== 分拣运输相关 ==========
|
||
'30': { status: '分拣中', color: 'processing', description: '快件正在分拣处理' },
|
||
'36': { status: '运输中', color: 'processing', description: '快件正在运输途中' },
|
||
'31': { status: '到达网点', color: 'processing', description: '快件已到达转运中心' },
|
||
'302': { status: '长途运输', color: 'processing', description: '快件正在安全运输中(长距离)' },
|
||
'310': { status: '途经中转', color: 'processing', description: '快件途经中转城市' },
|
||
|
||
// ========== 派送相关 ==========
|
||
'44': { status: '准备派送', color: 'processing', description: '正在为快件分配合适的快递员' },
|
||
'204': { status: '派送中', color: 'processing', description: '快件已交给快递员,正在派送途中' },
|
||
'70': { status: '派送失败', color: 'warning', description: '快件派送不成功,待再次派送' },
|
||
'33': { status: '待派送', color: 'warning', description: '已与客户约定新派送时间,待派送' },
|
||
|
||
// ========== 退回相关 ==========
|
||
'517': { status: '退回申请中', color: 'warning', description: '快件退回申请已提交,正在处理中' },
|
||
'99': { status: '退回中', color: 'warning', description: '应客户要求,快件正在退回中' },
|
||
'80': { status: '退回成功', color: 'success', description: '退回快件已派送成功' },
|
||
|
||
// ========== 签收相关 ==========
|
||
'81': { status: '已签收', color: 'success', description: '快件已成功签收' },
|
||
'82': { status: '已拒收', color: 'error', description: '快件被拒收' },
|
||
|
||
// ========== 其他状态 ==========
|
||
'655': { status: '待揽收', color: 'default', description: '快件等待揽收' },
|
||
'701': { status: '待揽收', color: 'default', description: '快件等待揽收' },
|
||
'101': { status: '已揽收', color: 'blue', description: '快件已揽收' },
|
||
};
|
||
```
|
||
|
||
### 方法三:文本关键词分析(最后备用)
|
||
|
||
**优先级:最低**
|
||
|
||
```javascript
|
||
const analyzeStatusByRemark = (remark) => {
|
||
const text = remark.toLowerCase();
|
||
|
||
// 揽收相关
|
||
if (text.includes('已收取快件') || text.includes('已揽收')) {
|
||
return { status: '已揽收', color: 'blue' };
|
||
}
|
||
|
||
// 分拣相关
|
||
if (text.includes('完成分拣')) {
|
||
return { status: '分拣中', color: 'processing' };
|
||
}
|
||
|
||
// 运输相关
|
||
if (text.includes('离开') || text.includes('发往') || text.includes('路上') || text.includes('安全运输中')) {
|
||
return { status: '运输中', color: 'processing' };
|
||
}
|
||
if (text.includes('途经')) {
|
||
return { status: '途经中转', color: 'processing' };
|
||
}
|
||
if (text.includes('到达')) {
|
||
return { status: '到达网点', color: 'processing' };
|
||
}
|
||
|
||
// 派送相关
|
||
if (text.includes('分配最合适的快递员')) {
|
||
return { status: '准备派送', color: 'processing' };
|
||
}
|
||
if (text.includes('交给') && text.includes('正在派送途中')) {
|
||
return { status: '派送中', color: 'processing' };
|
||
}
|
||
if (text.includes('派送不成功') || text.includes('约定新派送时间')) {
|
||
return { status: '派送失败', color: 'warning' };
|
||
}
|
||
if (text.includes('待派送')) {
|
||
return { status: '待派送', color: 'warning' };
|
||
}
|
||
|
||
// 签收相关
|
||
if (text.includes('派送成功') || text.includes('签收')) {
|
||
return { status: '已签收', color: 'success' };
|
||
}
|
||
if (text.includes('拒收') || text.includes('拒付费用')) {
|
||
return { status: '已拒收', color: 'error' };
|
||
}
|
||
|
||
// 退回相关
|
||
if (text.includes('退回申请已提交')) {
|
||
return { status: '退回申请中', color: 'warning' };
|
||
}
|
||
if (text.includes('快件正在退回中') || text.includes('应客户要求')) {
|
||
return { status: '退回中', color: 'warning' };
|
||
}
|
||
if (text.includes('退回快件已派送成功')) {
|
||
return { status: '退回成功', color: 'success' };
|
||
}
|
||
|
||
return { status: '处理中', color: 'default' };
|
||
};
|
||
```
|
||
|
||
## 🔄 状态判断流程图
|
||
|
||
```mermaid
|
||
graph TD
|
||
A[接收路由数据] --> B{是否有firstStatusName?}
|
||
B -->|有| C[使用官方一级状态]
|
||
B -->|无| D{是否有opCode?}
|
||
D -->|有| E[查询opCode映射表]
|
||
D -->|无| F[分析remark文本]
|
||
E --> G{映射表中存在?}
|
||
G -->|是| H[返回映射状态]
|
||
G -->|否| F
|
||
F --> I[返回文本分析结果]
|
||
C --> J[状态判断完成]
|
||
H --> J
|
||
I --> J
|
||
```
|
||
|
||
## 🎨 UI状态显示规范
|
||
|
||
### 状态颜色映射
|
||
|
||
| 状态类型 | 颜色 | CSS类 | 含义 |
|
||
|----------|------|-------|------|
|
||
| 已签收 | 🟢 绿色 | `bg-green-100 text-green-700` | 成功完成 |
|
||
| 已拒收/已退回 | 🔴 红色 | `bg-red-100 text-red-700` | 失败/异常 |
|
||
| 退回中 | 🟡 黄色 | `bg-yellow-100 text-yellow-700` | 警告状态 |
|
||
| 运输中/派送中 | 🔵 蓝色 | `bg-blue-100 text-blue-700` | 进行中 |
|
||
| 待揽收/处理中 | ⚪ 灰色 | `bg-gray-100 text-gray-700` | 等待/默认 |
|
||
|
||
### 时间轴节点显示
|
||
|
||
```jsx
|
||
// 状态标签
|
||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>
|
||
{status}
|
||
</span>
|
||
|
||
// 详细信息
|
||
<div className="flex flex-wrap gap-4 text-sm text-gray-500">
|
||
<span>📍 {acceptAddress}</span>
|
||
<span>🔢 opCode: {opCode}</span>
|
||
<span>📊 一级状态: {firstStatusName}</span>
|
||
<span>📋 二级状态: {secondaryStatusName}</span>
|
||
</div>
|
||
```
|
||
|
||
## 📊 API数据示例
|
||
|
||
### 完整状态数据(理想情况)
|
||
|
||
```json
|
||
{
|
||
"acceptTime": "2025-06-07 00:29:27",
|
||
"acceptAddress": "广州市",
|
||
"remark": "顺丰速运 已收取快件",
|
||
"opCode": "54",
|
||
"firstStatusCode": "1",
|
||
"firstStatusName": "已揽收",
|
||
"secondaryStatusCode": "101",
|
||
"secondaryStatusName": "已揽收"
|
||
}
|
||
```
|
||
|
||
### 缺失状态数据(当前情况)
|
||
|
||
```json
|
||
{
|
||
"acceptTime": "2025-06-07 00:29:27",
|
||
"acceptAddress": "广州市",
|
||
"remark": "顺丰速运 已收取快件",
|
||
"opCode": "54"
|
||
// 缺少 firstStatusName 等字段
|
||
}
|
||
```
|
||
|
||
## ⚙️ 实现代码示例
|
||
|
||
### 完整状态判断函数
|
||
|
||
```typescript
|
||
const getOverallStatus = (routes: RouteInfo[]): { status: string; color: string } => {
|
||
if (!routes || routes.length === 0) {
|
||
return { status: '暂无信息', color: 'default' };
|
||
}
|
||
|
||
// 取第一个(最新的)路由节点来判断当前状态
|
||
const latestRoute = routes[0];
|
||
|
||
// 优先使用官方状态字段
|
||
if (latestRoute.firstStatusName) {
|
||
const statusColorMap: Record<string, string> = {
|
||
'待揽收': 'default',
|
||
'已揽收': 'blue',
|
||
'运输中': 'processing',
|
||
'派送中': 'processing',
|
||
'已签收': 'success',
|
||
'已拒收': 'error',
|
||
'退回中': 'warning',
|
||
'已退回': 'error'
|
||
};
|
||
return {
|
||
status: latestRoute.firstStatusName,
|
||
color: statusColorMap[latestRoute.firstStatusName] || 'default'
|
||
};
|
||
}
|
||
|
||
// 备用方案:通过opCode判断
|
||
const opCodeStatus = getStatusByOpCode(latestRoute.opCode);
|
||
if (opCodeStatus.status !== '处理中') {
|
||
return { status: opCodeStatus.status, color: opCodeStatus.color };
|
||
}
|
||
|
||
// 最后备用:通过remark分析
|
||
return analyzeStatusByRemark(latestRoute.remark);
|
||
};
|
||
```
|
||
|
||
## 🔧 故障排查
|
||
|
||
### 常见问题及解决方案
|
||
|
||
#### 1. 状态字段缺失
|
||
|
||
**问题**: API返回数据缺少 `firstStatusName` 等状态字段
|
||
|
||
**可能原因**:
|
||
- API版本过旧
|
||
- 权限配置不足
|
||
- 请求参数不正确
|
||
|
||
**解决方案**:
|
||
1. 检查API版本,使用最新版本
|
||
2. 确认账号权限包含状态字段获取
|
||
3. 检查 `methodType` 参数设置
|
||
4. 使用opCode映射作为备用方案
|
||
|
||
#### 2. 状态显示不准确
|
||
|
||
**问题**: 显示的状态与实际情况不符
|
||
|
||
**排查步骤**:
|
||
1. 检查时间轴排序(最新的应该在前)
|
||
2. 验证opCode映射表的准确性
|
||
3. 检查文本关键词匹配逻辑
|
||
4. 查看原始API返回数据
|
||
|
||
#### 3. 状态更新延迟
|
||
|
||
**问题**: 状态更新不及时
|
||
|
||
**注意事项**:
|
||
- SF快递API数据更新可能有延迟
|
||
- 建议设置合理的查询间隔
|
||
- 避免频繁查询造成限流
|
||
|
||
## 📝 开发建议
|
||
|
||
### 最佳实践
|
||
|
||
1. **分层状态判断**: 按优先级使用多种方法
|
||
2. **缓存机制**: 避免重复查询相同运单
|
||
3. **错误处理**: 优雅处理API异常情况
|
||
4. **用户体验**: 提供加载状态和错误提示
|
||
5. **数据验证**: 验证API返回数据的完整性
|
||
|
||
### 性能优化
|
||
|
||
1. **状态映射表**: 预定义常用状态避免重复计算
|
||
2. **条件渲染**: 只在有数据时渲染状态组件
|
||
3. **防抖处理**: 防止用户频繁点击查询
|
||
|
||
## 📚 参考资料
|
||
|
||
- [SF快递开发者文档](https://open.sf-express.com/)
|
||
- [路由查询接口文档](https://open.sf-express.com/api/EXP_RECE_SEARCH_ROUTES)
|
||
- [Ant Design Timeline组件](https://ant.design/components/timeline)
|
||
|
||
## 📧 联系方式
|
||
|
||
如有问题或建议,请联系:
|
||
- 开发者:阿瑞
|
||
- 技术栈:TypeScript + Next.js + Ant Design
|
||
- 文档版本:v1.0.0 |