From d8398afa12c31e17c3863e949092871e88abd0f2 Mon Sep 17 00:00:00 2001
From: RUI <298977887@qq.com>
Date: Mon, 9 Jun 2025 01:27:51 +0800
Subject: [PATCH] 0609.1
---
docs/sf-express-logistics-status-guide.md | 365 ++++++++++++
src/components/TaskController.tsx | 170 ++++++
src/components/layout/Layout.tsx | 14 +-
src/components/logistics/status-tag.tsx | 373 +++++++++++++
src/components/logistics/status.tsx | 526 +++++++++++++++---
src/models/index.ts | 1 -
src/models/types.ts | 1 -
src/pages/api/backstage/sales/[id].ts | 42 ++
src/pages/api/backstage/sales/index.ts | 87 +++
src/pages/api/sse.ts | 52 ++
src/pages/api/test/batch-query-logistics.bak | 176 ++++++
src/pages/api/test/batch-query-logistics.ts | 227 ++++++++
src/pages/api/test/logistic-detail.ts | 137 +++++
src/pages/api/test/logistics-records.ts | 87 +++
src/pages/api/test/sf-query.ts | 201 +++++++
src/pages/api/tools/test4.ts | 73 ---
.../backstage/accounts/account-modal.tsx | 162 ++++++
src/pages/backstage/accounts/index.tsx | 251 +++++++++
src/pages/team/sale/index.tsx | 15 +-
src/pages/test/sf.tsx | 439 +++++++++++++++
src/pages/test/sfall.tsx | 402 +++++++++++++
src/types/enum.ts | 90 ++-
22 files changed, 3734 insertions(+), 157 deletions(-)
create mode 100644 docs/sf-express-logistics-status-guide.md
create mode 100644 src/components/TaskController.tsx
create mode 100644 src/components/logistics/status-tag.tsx
create mode 100644 src/pages/api/backstage/sales/[id].ts
create mode 100644 src/pages/api/backstage/sales/index.ts
create mode 100644 src/pages/api/sse.ts
create mode 100644 src/pages/api/test/batch-query-logistics.bak
create mode 100644 src/pages/api/test/batch-query-logistics.ts
create mode 100644 src/pages/api/test/logistic-detail.ts
create mode 100644 src/pages/api/test/logistics-records.ts
create mode 100644 src/pages/api/test/sf-query.ts
delete mode 100644 src/pages/api/tools/test4.ts
create mode 100644 src/pages/backstage/accounts/account-modal.tsx
create mode 100644 src/pages/backstage/accounts/index.tsx
create mode 100644 src/pages/test/sf.tsx
create mode 100644 src/pages/test/sfall.tsx
diff --git a/docs/sf-express-logistics-status-guide.md b/docs/sf-express-logistics-status-guide.md
new file mode 100644
index 0000000..a4d6e82
--- /dev/null
+++ b/docs/sf-express-logistics-status-guide.md
@@ -0,0 +1,365 @@
+# 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
+// 状态标签
+
+ {status}
+
+
+// 详细信息
+
+ 📍 {acceptAddress}
+ 🔢 opCode: {opCode}
+ 📊 一级状态: {firstStatusName}
+ 📋 二级状态: {secondaryStatusName}
+
+```
+
+## 📊 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 = {
+ '待揽收': '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
\ No newline at end of file
diff --git a/src/components/TaskController.tsx b/src/components/TaskController.tsx
new file mode 100644
index 0000000..f89db4e
--- /dev/null
+++ b/src/components/TaskController.tsx
@@ -0,0 +1,170 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { Button, message, Popover } from 'antd';
+import { PoweroffOutlined } from '@ant-design/icons';
+
+// 自定义 Hook,用于管理定时器
+function useInterval(callback: () => void, delay: number | null) {
+ const savedCallback = useRef<() => void>(() => {});
+
+ // 保存最新的回调函数,以便在定时器触发时使用
+ useEffect(() => {
+ savedCallback.current = callback;
+ }, [callback]);
+
+ // 当 delay 发生变化时,设置或清除定时器
+ useEffect(() => {
+ if (delay !== null) {
+ const id = setInterval(() => savedCallback.current && savedCallback.current(), delay);
+ return () => clearInterval(id); // 清除定时器以防止内存泄漏
+ }
+ }, [delay]);
+}
+
+const TaskController: React.FC = () => {
+ // 剩余时间的状态
+ const [remainingTime, setRemainingTime] = useState(null);
+ // 是否正在计时的状态
+ const [isCounting, setIsCounting] = useState(false);
+ // 用于存储 EventSource 的引用
+ const eventSourceRef = useRef(null);
+ const lastQueryResultRef = useRef(null); // 用于存储上次查询的结果
+
+ // 开始任务并启动倒计时的函数
+ const startTask = async () => {
+ if (!isCounting) {
+ setIsCounting(true);
+ // 在计时开始时进行物流查询
+ await updateLogisticsDetails();
+ console.log('使用 EventSource 启动倒计时...');
+
+ // 启动 SSE 连接
+ const eventSource = new EventSource('/api/sse');
+ eventSourceRef.current = eventSource;
+
+ // 当 SSE 连接成功时触发
+ eventSource.onopen = () => {
+ console.log('客户端 SSE 连接已打开');
+ };
+
+ // 当收到服务器发送的消息时触发
+ eventSource.onmessage = (event) => {
+ const time = parseInt(event.data, 10);
+ setRemainingTime(time);
+ //console.log('接收到事件:', time);
+ };
+
+ // 当发生错误时触发
+ eventSource.onerror = (error) => {
+ console.error('SSE 错误:', error);
+ eventSource.close();
+ setIsCounting(false); // 关闭倒计时
+ };
+ }
+ };
+
+ // 本地定时器,每秒更新 UI 中的剩余时间
+ useInterval(() => {
+ if (remainingTime !== null && remainingTime > 0) {
+ setRemainingTime(remainingTime - 1);
+ } else if (remainingTime === 0) {
+ // 移除或注释掉关闭 SSE 连接的代码
+ //if (eventSourceRef.current) {
+ // eventSourceRef.current.close(); // 关闭 SSE 连接
+ //}
+ setIsCounting(false); // 停止倒计时
+ startTask(); // 计时结束后自动开始新的一轮查询
+ }
+ }, isCounting ? 1000 : null);
+
+ useEffect(() => {
+ // 页面加载时检查是否有剩余时间
+ if (remainingTime === null) {
+ // 创建 EventSource 连接以检查服务器端是否有剩余时间
+ const eventSource = new EventSource('/api/sse');
+ eventSourceRef.current = eventSource;
+
+ eventSource.onmessage = (event) => {
+ const time = parseInt(event.data, 10);
+ setRemainingTime(time);
+ if (time > 0) {
+ setIsCounting(true); // 如果有剩余时间,则开始倒计时
+ }
+ };
+
+ eventSource.onerror = (error) => {
+ console.error('SSE 错误:', error);
+ eventSource.close();
+ };
+
+ // 检查本地存储或其他持久化方式,恢复上次查询结果
+ const savedLastQueryResult = localStorage.getItem('lastQueryResult');
+ if (savedLastQueryResult) {
+ lastQueryResultRef.current = savedLastQueryResult; // 恢复查询结果
+ }
+
+ // 在组件卸载时关闭 SSE 连接,防止内存泄漏
+ return () => {
+ if (eventSourceRef.current) {
+ eventSourceRef.current.close();
+ }
+ };
+ }
+ }, [remainingTime]);
+
+ // 物流查询并保存结果的函数
+ const updateLogisticsDetails = async () => {
+ try {
+ const response = await fetch('/api/tools/SFExpress/updateLogisticsDetails', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ // 处理响应数据
+ const data = await response.json();
+ message.success('更新物流详情成功');
+ //console.log('更新物流详情成功:', data);
+ lastQueryResultRef.current = data.message;
+ // 将查询结果保存到 localStorage
+ localStorage.setItem('lastQueryResult', data.message);
+ } catch (error) {
+ message.error('更新物流详情失败');
+ //console.error('更新物流详情失败:', error);
+ }
+ };
+
+ // 用于显示在 Popover 中的内容
+ /*const popoverContent = (
+
+ {lastQueryResultRef.current ? (
+
{lastQueryResultRef.current}
+ ) : (
+
暂无最新物流详情
+ )}
+
+ );*/
+
+ // 计算按钮显示的文字
+ const displayText = isCounting ? `剩余: ${remainingTime} 秒` : '物流查询';
+
+ return (
+
+ }
+ loading={isCounting} // 按钮加载状态取决于是否在计时中
+ onClick={startTask} // 点击按钮启动任务
+ disabled={isCounting || (remainingTime !== null && remainingTime > 0)} // 在计时中或剩余时间大于0时禁用按钮
+ >
+ {displayText}
+
+
+ );
+};
+
+export default TaskController;
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx
index e65fcf5..d7016a1 100644
--- a/src/components/layout/Layout.tsx
+++ b/src/components/layout/Layout.tsx
@@ -136,13 +136,13 @@ const UserInfoDisplay: React.FC<{ userInfo: any; collapsed?: boolean; }> = React
UserInfoDisplay.displayName = 'UserInfoDisplay';
// 菜单底部渲染组件 - 优化版本,使用React.memo
-const MenuFooter: React.FC<{
- collapsed?: boolean;
- userInfo: any;
+const MenuFooter: React.FC<{
+ collapsed?: boolean;
+ userInfo: any;
onShowScriptLibrary: () => void;
onShowTodoManager: () => void;
-}> = React.memo(({
- collapsed,
+}> = React.memo(({
+ collapsed,
userInfo,
onShowScriptLibrary,
onShowTodoManager
@@ -241,7 +241,7 @@ const Layout: React.FC = ({ children }) => {
const router = useRouter();
const userInfo = useUserInfo();
const userActions = useUserActions();
-
+
// 初始化 Socket.IO 连接
useSocket();
const [isClient, setIsClient] = useState(false);
@@ -278,7 +278,7 @@ const Layout: React.FC = ({ children }) => {
const handleClosePersonalInfo = useCallback(() => {
setShowPersonalInfo(false);
}, []);
-
+
const handleShowScriptLibrary = useCallback(() => {
setShowScriptLibrary(true);
}, []);
diff --git a/src/components/logistics/status-tag.tsx b/src/components/logistics/status-tag.tsx
new file mode 100644
index 0000000..2589ae9
--- /dev/null
+++ b/src/components/logistics/status-tag.tsx
@@ -0,0 +1,373 @@
+/**
+ * 物流状态标签组件
+ * @author 阿瑞
+ * @version 1.0.0
+ * @description 显示物流状态标签,支持鼠标悬停查看详细轨迹
+ */
+
+import React, { useState, useCallback } from 'react';
+import { Tag, Tooltip, Spin, Typography, Timeline } from 'antd';
+import { ClockCircleOutlined, EnvironmentOutlined, TruckOutlined } from '@ant-design/icons';
+import { SF_OPCODE_STATUS_MAP } from '@/types/enum';
+
+const { Text } = Typography;
+
+// ==================== 接口定义 ====================
+
+/**
+ * 物流轨迹节点接口
+ */
+interface LogisticsRouteNode {
+ time: string;
+ opCode: string;
+ remark: string;
+ location?: string;
+}
+
+/**
+ * 物流详情数据接口
+ */
+interface LogisticsDetailData {
+ trackingNumber: string;
+ currentStatus: string;
+ routes: LogisticsRouteNode[];
+ lastUpdateTime: string;
+}
+
+/**
+ * 状态标签组件属性接口
+ */
+interface StatusTagProps {
+ recordId: string;
+ opCode: string | null;
+ className?: string;
+}
+
+// ==================== 主组件 ====================
+
+/**
+ * 物流状态标签组件
+ * @param props 组件属性
+ * @returns 状态标签JSX元素
+ */
+const StatusTag: React.FC = ({ recordId, opCode, className }) => {
+ // ==================== 状态管理 ====================
+
+ const [loading, setLoading] = useState(false);
+ const [detailData, setDetailData] = useState(null);
+ const [error, setError] = useState('');
+
+ // ==================== 业务逻辑函数 ====================
+
+ /**
+ * 获取物流详情数据
+ */
+ const fetchLogisticsDetail = useCallback(async () => {
+ if (detailData || loading) return; // 避免重复请求
+
+ setLoading(true);
+ setError('');
+
+ try {
+ const response = await fetch(`/api/test/logistic-detail?id=${recordId}`);
+ const result = await response.json();
+
+ if (result.success && result.data) {
+ setDetailData(result.data);
+ } else {
+ setError(result.message || '获取物流详情失败');
+ }
+ } catch (err) {
+ console.error('获取物流详情失败:', err);
+ setError('网络请求失败');
+ } finally {
+ setLoading(false);
+ }
+ }, [recordId, detailData, loading]);
+
+ /**
+ * 渲染物流轨迹内容
+ * @returns 轨迹内容JSX元素
+ */
+ const renderTrackingContent = () => {
+ if (loading) {
+ return (
+
+
+
+ 正在加载物流详情...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+ );
+ }
+
+ if (!detailData) {
+ return (
+
+ 鼠标悬停查看详情
+
+ );
+ }
+
+ return (
+
+ {/* 物流单号标题区域 */}
+
+
+
+
+
+
+
+ {detailData.trackingNumber}
+
+
+
+
+
+ {detailData.lastUpdateTime}
+
+
+
+
+
+ {/* 物流轨迹详情 */}
+
+
{
+ // 获取状态信息
+ const statusInfo = SF_OPCODE_STATUS_MAP[route.opCode];
+ const isLatest = index === 0;
+
+ return {
+ color: isLatest ? (statusInfo?.color || '#1890ff') : '#d9d9d9',
+ dot: isLatest ? (
+
+ ) : undefined,
+ children: (
+
+ {/* 第一行:状态 地点 时间 最新标签 */}
+
+
+ {statusInfo?.status || `状态码: ${route.opCode}`}
+
+ {route.location && (
+
+
+ {route.location}
+
+ )}
+
+ {route.time}
+
+ {isLatest && (
+
+ 最新
+
+ )}
+
+ {/* 第二行:详细备注 */}
+
+ {route.remark}
+
+
+ ),
+ };
+ })}
+ />
+
+
+ {/* 记录总数提示 */}
+ {detailData.routes.length > 0 && (
+
+
+ 共 {detailData.routes.length} 条物流记录
+
+
+ )}
+
+ );
+ };
+
+ // ==================== 渲染主标签 ====================
+
+ /**
+ * 渲染状态标签
+ * @returns 状态标签JSX元素
+ */
+ const renderStatusTag = () => {
+ // 处理null值:显示为待发货
+ if (opCode === null || opCode === undefined || opCode === '') {
+ return (
+
+ 待发货
+
+ );
+ }
+
+ // 使用opCode直接从映射表获取状态信息
+ const statusInfo = SF_OPCODE_STATUS_MAP[opCode];
+
+ if (statusInfo) {
+ return (
+
+ {statusInfo.status}
+
+ );
+ }
+
+ // 如果opCode不在映射表中,显示原始opCode
+ return (
+
+ 状态码: {opCode}
+
+ );
+ };
+
+ // ==================== 渲染组件 ====================
+
+ return (
+ {
+ if (visible) {
+ fetchLogisticsDetail();
+ }
+ }}
+ overlayStyle={{
+ maxWidth: 'none',
+ boxShadow: '0 12px 32px 4px rgba(0, 0, 0, 0.04), 0 8px 20px rgba(0, 0, 0, 0.08)',
+ borderRadius: '8px',
+ border: 'none',
+ padding: '0'
+ }}
+ >
+
+ {renderStatusTag()}
+
+
+ );
+};
+
+export default StatusTag;
\ No newline at end of file
diff --git a/src/components/logistics/status.tsx b/src/components/logistics/status.tsx
index 9df3d4e..32fc7c1 100644
--- a/src/components/logistics/status.tsx
+++ b/src/components/logistics/status.tsx
@@ -1,8 +1,17 @@
-//src\components\logistics\status.tsx
+/**
+ * 物流状态组件
+ * @author 阿瑞
+ * @version 1.0.0
+ * @description 显示物流状态标签,支持鼠标悬停查看详细轨迹
+ */
+
import React, { useEffect, useState, useCallback, useRef } from 'react';
-import { Tag, Spin } from 'antd';
-import MyTooltip from '@/components/tooltip/MyTooltip';
-import { debounce } from 'lodash'; // 导入lodash的debounce函数
+import { Tag, Tooltip, Spin, Typography, Timeline } from 'antd';
+import { ClockCircleOutlined, EnvironmentOutlined, TruckOutlined } from '@ant-design/icons';
+import { SF_OPCODE_STATUS_MAP } from '@/types/enum';
+import { debounce } from 'lodash';
+
+const { Text } = Typography;
interface LogisticsStatusProps {
recordId: string; // 关联记录的id
@@ -16,18 +25,6 @@ const detailsCache: Record `${recordId}_${productId}`;
@@ -37,10 +34,10 @@ const LogisticsStatus: React.FC = React.memo(({ recordId,
const [loading, setLoading] = useState(true);
const [detailsLoading, setDetailsLoading] = useState(false);
const [logisticsNumber, setLogisticsNumber] = useState(null);
-
+
// 使用 ref 来跟踪当前组件是否已挂载,避免在组件卸载后设置状态
const isMountedRef = useRef(true);
-
+
// 使用 ref 来存储当前进行中的请求,用于取消
const currentRequestRef = useRef(null);
@@ -53,7 +50,7 @@ const LogisticsStatus: React.FC = React.memo(({ recordId,
isMountedRef.current = true;
const cacheKey = getCacheKey(recordId, productId);
const cachedStatus = statusCache[cacheKey];
-
+
// 如果缓存存在且未过期,使用缓存
if (cachedStatus && isCacheValid(cachedStatus.timestamp)) {
if (isMountedRef.current) {
@@ -65,23 +62,23 @@ const LogisticsStatus: React.FC = React.memo(({ recordId,
const fetchLogisticsStatus = async () => {
if (!isMountedRef.current) return;
-
+
setLoading(true);
-
+
// 取消之前的请求
if (currentRequestRef.current) {
currentRequestRef.current.abort();
}
-
+
// 创建新的 AbortController
const abortController = new AbortController();
currentRequestRef.current = abortController;
-
+
try {
const response = await fetch(`/api/logistics/status?recordId=${recordId}&productId=${productId}`, {
signal: abortController.signal
});
-
+
if (!response.ok) {
console.error('获取物流状态失败:', `HTTP ${response.status}`);
if (isMountedRef.current) {
@@ -89,16 +86,16 @@ const LogisticsStatus: React.FC = React.memo(({ recordId,
}
return;
}
-
+
const responseData = await response.json();
const status = responseData.data.物流状态;
-
+
// 更新缓存
statusCache[cacheKey] = {
status,
timestamp: Date.now()
};
-
+
if (isMountedRef.current) {
setLogisticsStatus(status);
}
@@ -119,13 +116,13 @@ const LogisticsStatus: React.FC = React.memo(({ recordId,
};
fetchLogisticsStatus();
-
+
// 当依赖项变化时,重置物流详情和物流单号
if (isMountedRef.current) {
setLogisticsDetails(null);
setLogisticsNumber(null);
}
-
+
// 清理函数
return () => {
if (currentRequestRef.current) {
@@ -134,7 +131,7 @@ const LogisticsStatus: React.FC = React.memo(({ recordId,
}
};
}, [recordId, productId]);
-
+
// 组件卸载时的清理
useEffect(() => {
return () => {
@@ -149,10 +146,10 @@ const LogisticsStatus: React.FC = React.memo(({ recordId,
const fetchLogisticsDetails = useCallback(
debounce(async () => {
if (!isMountedRef.current) return;
-
+
const cacheKey = getCacheKey(recordId, productId);
const cachedDetails = detailsCache[cacheKey];
-
+
// 如果缓存存在且未过期,使用缓存
if (cachedDetails && isCacheValid(cachedDetails.timestamp)) {
if (isMountedRef.current) {
@@ -161,14 +158,14 @@ const LogisticsStatus: React.FC = React.memo(({ recordId,
}
return;
}
-
+
if (isMountedRef.current) {
setDetailsLoading(true);
}
-
+
try {
const response = await fetch(`/api/logistics/details?recordId=${recordId}&productId=${productId}`);
-
+
if (!response.ok) {
console.error('获取物流详情失败:', `HTTP ${response.status}`);
if (isMountedRef.current) {
@@ -177,18 +174,18 @@ const LogisticsStatus: React.FC = React.memo(({ recordId,
}
return;
}
-
+
const responseData = await response.json();
const details = responseData[0]?.物流详情 || '暂无物流详情';
const number = responseData[0]?.物流单号 || '';
-
+
// 更新缓存
detailsCache[cacheKey] = {
details,
number,
timestamp: Date.now()
};
-
+
if (isMountedRef.current) {
setLogisticsDetails(details);
setLogisticsNumber(number);
@@ -208,39 +205,436 @@ const LogisticsStatus: React.FC = React.memo(({ recordId,
[recordId, productId]
);
- if (loading) {
- return ;
- }
+ /**
+ * 检查字符串是否是有效的JSON格式
+ */
+ const isValidJSON = (str: string): boolean => {
+ if (typeof str !== 'string') return false;
- if (!logisticsStatus) {
- return null; // 返回null而不是div,减少不必要的DOM元素
- }
+ // 简单检查:JSON应该以 { 或 [ 开头
+ const trimmed = str.trim();
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
+ return false;
+ }
- return (
-
- ) : (
-
-
物流单号:{logisticsNumber}
-
{logisticsDetails || '暂无物流详情'}
-
- )
+ try {
+ JSON.parse(str);
+ return true;
+ } catch {
+ return false;
+ }
+ };
+
+ /**
+ * 解析物流详情JSON并格式化
+ */
+ const parseLogisticsDetails = () => {
+ if (!logisticsDetails) return { routes: [], hasValidData: false, lastUpdateTime: '' };
+
+ try {
+ let detailData;
+
+ if (typeof logisticsDetails === 'string') {
+ // 检查是否是有效的JSON字符串
+ if (!isValidJSON(logisticsDetails)) {
+ console.log('物流详情不是JSON格式:', logisticsDetails);
+ return { routes: [], hasValidData: false, lastUpdateTime: '' };
+ }
+ detailData = JSON.parse(logisticsDetails);
+ } else {
+ detailData = logisticsDetails;
}
- onMouseEnter={fetchLogisticsDetails}
- >
-
+
+ // 从SF快递API响应中提取路由信息
+ if (detailData?.msgData?.routeResps?.length > 0) {
+ const routeResp = detailData.msgData.routeResps[0];
+ const routes = routeResp.routes || [];
+
+ // 获取最新更新时间(从最新的路由记录中获取)
+ const lastUpdateTime = routes.length > 0
+ ? routes[routes.length - 1]?.acceptTime || ''
+ : '';
+
+ // 如果有有效的路由数据
+ if (routes.length > 0) {
+ return {
+ routes: routes.map((route: any) => ({
+ time: route.acceptTime || '未知时间',
+ opCode: route.opCode || '未知',
+ remark: route.remark || '无备注',
+ location: route.acceptAddress || undefined
+ })).reverse(), // 反转数组,让最新的记录在前面
+ hasValidData: true,
+ lastUpdateTime
+ };
+ } else {
+ // 有响应但routes为空,说明是已创建单但未发货的状态
+ return {
+ routes: [],
+ hasValidData: true, // 标记为有效数据,只是暂无物流信息
+ isEmpty: true,
+ lastUpdateTime: new Date().toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ }).replace(/\//g, '-')
+ };
+ }
+ }
+ } catch (parseError) {
+ console.error('解析物流详情失败:', parseError);
+ }
+
+ return { routes: [], hasValidData: false, lastUpdateTime: '' };
+ };
+
+ /**
+ * 渲染物流详情内容
+ */
+ const renderLogisticsContent = () => {
+ if (detailsLoading) {
+ return (
+
+
+
+ 正在加载物流详情...
+
+
+ );
+ }
+
+ if (!logisticsDetails && !logisticsNumber) {
+ return (
+
+ 鼠标悬停查看详情
+
+ );
+ }
+
+ const { routes: routesData, hasValidData, isEmpty, lastUpdateTime } = parseLogisticsDetails();
+
+ return (
+
+ {/* 物流单号标题区域 */}
+
+
+
+
+
+
+
+ {logisticsNumber || '未知单号'}
+
+
+ {lastUpdateTime && (
+
+
+
+ {lastUpdateTime}
+
+
+ )}
+
+
+
+ {/* 物流轨迹详情 */}
+
+ {routesData.length > 0 ? (
+
{
+ // 获取状态信息
+ const statusInfo = SF_OPCODE_STATUS_MAP[route.opCode];
+ const isLatest = index === 0;
+
+ return {
+ color: isLatest ? (statusInfo?.color || '#1890ff') : '#d9d9d9',
+ dot: isLatest ? (
+
+ ) : undefined,
+ children: (
+
+ {/* 第一行:状态 地点 时间 最新标签 */}
+
+
+ {statusInfo?.status || `${route.opCode}`}
+
+ {route.location && (
+
+
+ {route.location}
+
+ )}
+
+ {route.time}
+
+ {isLatest && (
+
+ 最新
+
+ )}
+
+ {/* 第二行:详细备注 */}
+
+ {route.remark}
+
+
+ ),
+ };
+ })}
+ />
+ ) : (
+
+ {hasValidData && isEmpty ? (
+
+
+ 待发货
+
+
+ 暂无详细物流信息
+
+
+ ) : (
+
+ 暂无物流详情
+
+ )}
+
+ )}
+
+
+ {/* 记录总数提示 */}
+ {routesData.length > 0 && (
+
+
+ 共 {routesData.length} 条物流记录
+
+
+ )}
+
+ );
+ };
+
+ /**
+ * 渲染状态标签
+ */
+ const renderStatusTag = () => {
+ if (loading) {
+ return ;
+ }
+
+ if (!logisticsStatus) {
+ return null;
+ }
+
+ // 处理null值:显示为待发货
+ if (logisticsStatus === null || logisticsStatus === undefined || logisticsStatus === '') {
+ return (
+
+ 待发货
+
+ );
+ }
+
+ // 处理API返回的特殊状态
+ if (logisticsStatus === '待填单') {
+ return (
+
+ 待填单
+
+ );
+ }
+
+ if (logisticsStatus === '待发货') {
+ return (
+
+ 待发货
+
+ );
+ }
+
+ // 直接使用opCode从映射表获取状态信息
+ const statusInfo = SF_OPCODE_STATUS_MAP[logisticsStatus];
+
+ if (statusInfo) {
+ return (
+
+ {statusInfo.status}
+
+ );
+ }
+
+ // 如果opCode不在映射表中,显示原始opCode
+ return (
+
{logisticsStatus}
-
+ );
+ };
+
+ return (
+ {
+ if (visible) {
+ fetchLogisticsDetails();
+ }
+ }}
+ overlayStyle={{
+ maxWidth: 'none',
+ boxShadow: '0 12px 32px 4px rgba(0, 0, 0, 0.04), 0 8px 20px rgba(0, 0, 0, 0.08)',
+ borderRadius: '8px',
+ border: 'none',
+ padding: '0'
+ }}
+ >
+
+ {renderStatusTag()}
+
+
);
});
diff --git a/src/models/index.ts b/src/models/index.ts
index 7fda40b..0081b25 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -363,7 +363,6 @@ const LogisticsRecordSchema: Schema = new Schema({
物流单号: { type: String },
是否查询: { type: Boolean, default: true }, // 设置默认值为true
客户尾号: { type: String },
- 物流公司: { type: String },
物流详情: { type: String },
物流状态: { type: String },
更新时间: { type: Date, default: Date.now },
diff --git a/src/models/types.ts b/src/models/types.ts
index 032119f..c55ed9d 100644
--- a/src/models/types.ts
+++ b/src/models/types.ts
@@ -332,7 +332,6 @@ export interface ILogisticsRecord {
物流单号: string;
是否查询: boolean;
客户尾号: string;
- 物流公司: string;
物流详情: string;
更新时间: Date;
物流状态: string;
diff --git a/src/pages/api/backstage/sales/[id].ts b/src/pages/api/backstage/sales/[id].ts
new file mode 100644
index 0000000..7b6105f
--- /dev/null
+++ b/src/pages/api/backstage/sales/[id].ts
@@ -0,0 +1,42 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+import { SalesRecord } from '@/models';
+import connectDB from '@/utils/connectDB';
+
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { query: { id }, method } = req;
+
+ switch (method) {
+ case 'GET':
+ try {
+ const salesRecord = await SalesRecord.findById(id)
+ .populate('客户')
+ .populate('产品')
+ .populate('订单来源')
+ .populate('导购')
+ .populate('付款平台');
+ if (!salesRecord) {
+ return res.status(404).json({ message: '未找到销售记录' });
+ }
+ res.status(200).json(salesRecord);
+ } catch (error) {
+ res.status(500).json({ message: '服务器错误' });
+ }
+ break;
+ case 'DELETE':
+ try {
+ const deletedSalesRecord = await SalesRecord.findByIdAndDelete(id);
+ if (!deletedSalesRecord) {
+ return res.status(404).json({ message: '未找到销售记录' });
+ }
+ res.status(200).json({ message: '销售记录删除成功' });
+ } catch (error) {
+ res.status(500).json({ message: '服务器错误' });
+ }
+ break;
+ default:
+ res.setHeader('Allow', ['GET', 'DELETE']);
+ res.status(405).end(`不允许 ${method} 方法`);
+ }
+};
+
+export default connectDB(handler);
diff --git a/src/pages/api/backstage/sales/index.ts b/src/pages/api/backstage/sales/index.ts
new file mode 100644
index 0000000..c01dc53
--- /dev/null
+++ b/src/pages/api/backstage/sales/index.ts
@@ -0,0 +1,87 @@
+//src\pages\api\backstage\sales\index.ts
+import type { NextApiRequest, NextApiResponse } from 'next';
+import { SalesRecord, Customer } from '@/models';
+import connectDB from '@/utils/connectDB';
+interface CouponUsage {
+ _id: string;
+ 券码: string;
+ 已使用: boolean;
+ 使用日期: Date | null;
+}
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ if (req.method === 'GET') {
+ try {
+ const { teamId } = req.query;
+ const salesRecords = await SalesRecord.find({ 团队: teamId })
+ .populate('客户')
+ .populate('产品')
+ .populate('订单来源')
+ .populate('导购')
+ .populate('付款平台');
+ res.status(200).json({ salesRecords });
+ } catch (error) {
+ res.status(500).json({ message: '服务器错误' });
+ }
+ } else if (req.method === 'POST') {
+ //打印接收到的请求体
+ console.log(req.body);
+ try {
+ const { 团队, 客户, 产品, 订单来源, 导购,
+ 应收金额, 收款金额, 待收款,
+ 成交日期, 收款状态, 货款状态, 收款平台, 备注,
+ 优惠券, // 从请求中获取优惠券信息
+ 余额抵用 // 从请求中获取优惠券信息和余额抵用信息
+ } = req.body;
+ const newSalesRecord = new SalesRecord({
+ 团队,
+ 客户,
+ 产品,
+ 订单来源,
+ 导购,
+ 应收金额,
+ 收款金额,
+ 待收款,
+ 成交日期,
+ 收款状态,
+ 货款状态,
+ 收款平台,
+ 备注,
+ 优惠券: [], // 初始化优惠券字段
+ 余额抵用: 余额抵用 || null, // 将余额抵用信息记录到销售记录中
+ });
+
+ // 处理优惠券的使用
+ if (优惠券 && 优惠券.length > 0) {
+ const customer = await Customer.findById(客户);
+ if (!customer) {
+ return res.status(404).json({ message: '客户未找到' });
+ }
+
+ 优惠券.forEach((couponCode: string) => {
+ const customerCoupon = customer.优惠券.find((c: CouponUsage) => c.券码 === couponCode);
+ if (customerCoupon && !customerCoupon.已使用) {
+ customerCoupon.已使用 = true;
+ customerCoupon.使用日期 = new Date();
+
+ // 确保 newSalesRecord.优惠券 是一个有效的数组
+ newSalesRecord.优惠券 = newSalesRecord.优惠券 || [];
+ newSalesRecord.优惠券.push(customerCoupon); // 将已使用的优惠券添加到销售记录中
+ }
+ });
+
+ await customer.save(); // 保存客户的优惠券状态更新
+ }
+
+ await newSalesRecord.save();
+ res.status(201).json({ message: '销售记录创建成功', salesRecord: newSalesRecord });
+ } catch (error) {
+ console.error('创建销售记录失败:', error);
+ res.status(400).json({ message: '创建销售记录失败' });
+ }
+ } else {
+ res.setHeader('Allow', ['GET', 'POST']);
+ res.status(405).end(`不允许 ${req.method} 方法`);
+ }
+};
+
+export default connectDB(handler);
diff --git a/src/pages/api/sse.ts b/src/pages/api/sse.ts
new file mode 100644
index 0000000..0cec577
--- /dev/null
+++ b/src/pages/api/sse.ts
@@ -0,0 +1,52 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+// 全局变量,用于存储剩余时间
+let remainingTimeGlobal: number | null = null;
+// 全局变量,用于存储计时器的 ID
+let intervalIdGlobal: NodeJS.Timeout | null = null;
+
+export default function handler(req: NextApiRequest, res: NextApiResponse) {
+ console.log("SSE 连接已打开");
+
+ // 设置响应头,指定为 SSE 流式传输
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.flushHeaders(); // 立即刷新响应头
+
+ // 如果全局倒计时未开始,则初始化
+ if (remainingTimeGlobal === null) {
+ remainingTimeGlobal = 600; // 默认30秒倒计时
+ }
+
+ // 发送时间更新的函数
+ const sendTimeUpdate = () => {
+ if (remainingTimeGlobal !== null && remainingTimeGlobal >= 0) {
+ //console.log(`发送更新: ${remainingTimeGlobal}`);
+ res.write(`data: ${remainingTimeGlobal}\n\n`); // 发送剩余时间给客户端
+ remainingTimeGlobal -= 1;
+ } else {
+ // 当倒计时结束时清除计时器并关闭 SSE 连接
+ /*
+ if (intervalIdGlobal) clearInterval(intervalIdGlobal);
+ remainingTimeGlobal = null;
+ intervalIdGlobal = null;
+ console.log("SSE 连接已关闭(倒计时结束)");
+ res.end(); // 结束 SSE 响应*/
+ // 当倒计时结束时重置倒计时,并继续保持连接
+ remainingTimeGlobal = 600; // 重置倒计时时间
+ }
+ };
+
+ // 如果计时器未启动,则启动
+ if (!intervalIdGlobal) {
+ intervalIdGlobal = setInterval(sendTimeUpdate, 1000);
+ }
+
+ // 立即发送当前的剩余时间
+ sendTimeUpdate();
+
+ req.on('close', () => {
+ console.log("SSE 连接已关闭(客户端断开连接)");
+ });
+}
diff --git a/src/pages/api/test/batch-query-logistics.bak b/src/pages/api/test/batch-query-logistics.bak
new file mode 100644
index 0000000..eb49ffa
--- /dev/null
+++ b/src/pages/api/test/batch-query-logistics.bak
@@ -0,0 +1,176 @@
+/**
+ * 批量物流查询API
+ * @author 阿瑞
+ * @version 1.0.0
+ * @description 批量查询物流信息并更新数据库记录
+ */
+
+import type { NextApiRequest, NextApiResponse } from 'next';
+import connectDB from '@/utils/connectDB';
+import { LogisticsRecord } from '@/models';
+
+/**
+ * 批量查询响应数据接口
+ */
+interface BatchQueryResponse {
+ success: boolean;
+ message?: string;
+ results?: {
+ total: number;
+ successCount: number;
+ failedCount: number;
+ details: Array<{
+ _id: string;
+ 物流单号: string;
+ status: 'success' | 'failed';
+ message: string;
+ newStatus?: string;
+ }>;
+ };
+ error?: string;
+}
+
+/**
+ * 处理批量物流查询请求
+ * @param req HTTP请求对象
+ * @param res HTTP响应对象
+ */
+const handler = async (
+ req: NextApiRequest,
+ res: NextApiResponse
+) => {
+ // 只允许POST请求
+ if (req.method !== 'POST') {
+ return res.status(405).json({
+ success: false,
+ message: '只支持POST请求'
+ });
+ }
+
+ try {
+ // 获取需要查询的记录
+ const recordsToQuery = await LogisticsRecord.find({
+ 物流单号: { $exists: true, $ne: null, $ne: '' },
+ 客户尾号: { $exists: true, $ne: null, $ne: '' },
+ 是否查询: true
+ }).lean();
+
+ if (!recordsToQuery || recordsToQuery.length === 0) {
+ return res.status(200).json({
+ success: true,
+ message: '没有需要查询的物流记录',
+ results: {
+ total: 0,
+ successCount: 0,
+ failedCount: 0,
+ details: []
+ }
+ });
+ }
+
+ console.log(`开始批量查询 ${recordsToQuery.length} 条物流记录`);
+
+ const results = {
+ total: recordsToQuery.length,
+ successCount: 0,
+ failedCount: 0,
+ details: [] as Array<{
+ _id: string;
+ 物流单号: string;
+ status: 'success' | 'failed';
+ message: string;
+ newStatus?: string;
+ }>
+ };
+
+ // 逐个查询物流信息
+ for (const record of recordsToQuery) {
+ try {
+ console.log(`正在查询物流单号: ${record.物流单号}`);
+
+ // 调用SF快递查询API
+ const queryResponse = await fetch(`${req.headers.origin}/api/test/sf-query`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ trackingNumber: record.物流单号,
+ phoneLast4Digits: record.客户尾号,
+ }),
+ });
+
+ const queryResult: any = await queryResponse.json();
+
+ if (queryResult.success && queryResult.msgData?.msgData?.routeResps?.length > 0) {
+ // 获取最新的opCode(routes数组中的最后一个元素是最新的)
+ const routes = queryResult.msgData.msgData.routeResps[0].routes;
+ const latestRoute = routes && routes.length > 0 ? routes[routes.length - 1] : null;
+ const newStatus = latestRoute?.opCode || '未知';
+
+ // 更新数据库记录
+ await LogisticsRecord.findByIdAndUpdate(record._id, {
+ 物流状态: newStatus,
+ 物流详情: JSON.stringify(queryResult.msgData),
+ 更新时间: new Date(),
+ });
+
+ results.successCount++;
+ results.details.push({
+ _id: String(record._id),
+ 物流单号: String(record.物流单号),
+ status: 'success',
+ message: '查询成功并更新',
+ newStatus: newStatus
+ });
+
+ console.log(`✅ ${record.物流单号} 查询成功,状态: ${newStatus}`);
+ } else {
+ // 查询失败
+ results.failedCount++;
+ results.details.push({
+ _id: String(record._id),
+ 物流单号: String(record.物流单号),
+ status: 'failed',
+ message: queryResult.errorMsg || queryResult.msgData?.errorMsg || '查询失败'
+ });
+
+ console.log(`❌ ${record.物流单号} 查询失败`);
+ }
+
+ // 添加延迟以避免请求过于频繁
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ } catch (error) {
+ console.error(`查询 ${record.物流单号} 时发生错误:`, error);
+
+ results.failedCount++;
+ results.details.push({
+ _id: String(record._id),
+ 物流单号: String(record.物流单号),
+ status: 'failed',
+ message: '网络请求失败'
+ });
+ }
+ }
+
+ console.log(`批量查询完成,成功: ${results.successCount}, 失败: ${results.failedCount}`);
+
+ return res.status(200).json({
+ success: true,
+ message: `批量查询完成,成功 ${results.successCount} 条,失败 ${results.failedCount} 条`,
+ results
+ });
+
+ } catch (error) {
+ console.error('批量查询物流信息失败:', error);
+
+ return res.status(500).json({
+ success: false,
+ error: '服务器内部错误',
+ message: '批量查询失败,请稍后重试'
+ });
+ }
+};
+
+export default connectDB(handler);
\ No newline at end of file
diff --git a/src/pages/api/test/batch-query-logistics.ts b/src/pages/api/test/batch-query-logistics.ts
new file mode 100644
index 0000000..b94cda0
--- /dev/null
+++ b/src/pages/api/test/batch-query-logistics.ts
@@ -0,0 +1,227 @@
+/**
+ * 批量物流查询API
+ * @author 阿瑞
+ * @version 1.0.0
+ * @description 批量查询物流信息并更新数据库记录
+ */
+
+import type { NextApiRequest, NextApiResponse } from 'next';
+import connectDB from '@/utils/connectDB';
+import { LogisticsRecord } from '@/models';
+
+/**
+ * 批量查询响应数据接口
+ */
+interface BatchQueryResponse {
+ success: boolean;
+ message?: string;
+ results?: {
+ total: number;
+ successCount: number;
+ failedCount: number;
+ details: Array<{
+ _id: string;
+ trackingNumber: string;
+ status: 'success' | 'failed';
+ message: string;
+ newStatus?: string | null;
+ }>;
+ };
+ error?: string;
+}
+
+/**
+ * 处理批量物流查询请求
+ */
+const handler = async (
+ req: NextApiRequest,
+ res: NextApiResponse
+) => {
+ if (req.method !== 'POST') {
+ return res.status(405).json({
+ success: false,
+ message: '只支持POST请求'
+ });
+ }
+
+ try {
+ // 首先处理历史数据:将所有状态为"80"但仍标记为需要查询的记录设置为不需要查询
+ const updateResult = await LogisticsRecord.updateMany(
+ {
+ 物流状态: '80',
+ 是否查询: true
+ },
+ {
+ 是否查询: false,
+ 更新时间: new Date()
+ }
+ );
+
+ if (updateResult.modifiedCount > 0) {
+ console.log(`🔧 清理历史数据:${updateResult.modifiedCount} 条已签收记录已设置为不需要查询`);
+ }
+
+ // 获取需要查询的记录
+ const recordsToQuery = await LogisticsRecord.find({
+ $and: [
+ { 物流单号: { $exists: true, $nin: [null, ''] } },
+ { 客户尾号: { $exists: true, $nin: [null, ''] } },
+ { 是否查询: true },
+ { 物流状态: { $ne: '80' } } // 排除已签收的包裹
+ ]
+ }).lean();
+
+ if (!recordsToQuery || recordsToQuery.length === 0) {
+ return res.status(200).json({
+ success: true,
+ message: '没有需要查询的物流记录',
+ results: {
+ total: 0,
+ successCount: 0,
+ failedCount: 0,
+ details: []
+ }
+ });
+ }
+
+ console.log(`开始批量查询 ${recordsToQuery.length} 条物流记录`);
+
+ // 初始化结果统计
+ const batchResults = {
+ total: recordsToQuery.length,
+ successCount: 0,
+ failedCount: 0,
+ details: [] as Array<{
+ _id: string;
+ trackingNumber: string;
+ status: 'success' | 'failed';
+ message: string;
+ newStatus?: string | null;
+ }>
+ };
+
+ // 逐个查询物流信息
+ for (const record of recordsToQuery) {
+ try {
+ const trackingNumber = String(record.物流单号);
+ const phoneLast4 = String(record.客户尾号);
+
+ console.log(`正在查询物流单号: ${trackingNumber}`);
+
+ // 调用SF快递查询API
+ const queryResponse = await fetch(`${req.headers.origin}/api/test/sf-query`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ trackingNumber: trackingNumber,
+ phoneLast4Digits: phoneLast4,
+ }),
+ });
+
+ const queryResult: any = await queryResponse.json();
+
+ if (queryResult.success && queryResult.msgData?.msgData?.routeResps?.length > 0) {
+ // 获取最新的opCode(routes数组中的最后一个元素是最新的记录)
+ const routeResp = queryResult.msgData.msgData.routeResps[0];
+ const routes = routeResp.routes;
+
+ if (routes && routes.length > 0) {
+ // 取最后一个路由记录(最新的)
+ const latestRoute = routes[routes.length - 1];
+ const newOpCode = latestRoute?.opCode || '未知';
+
+ // 更新数据库记录
+ const updateData: any = {
+ 物流状态: newOpCode,
+ 物流详情: JSON.stringify(queryResult.msgData),
+ 更新时间: new Date(),
+ };
+
+ // 如果状态为80(已签收),则停止后续查询
+ if (newOpCode === '80') {
+ updateData.是否查询 = false;
+ }
+
+ await LogisticsRecord.findByIdAndUpdate(record._id, updateData);
+
+ batchResults.successCount++;
+ batchResults.details.push({
+ _id: String(record._id),
+ trackingNumber: trackingNumber,
+ status: 'success',
+ message: '查询成功并更新',
+ newStatus: newOpCode
+ });
+
+ console.log(`✅ ${trackingNumber} 查询成功,最新状态: ${newOpCode}${newOpCode === '80' ? ' [已签收,停止查询]' : ''}`);
+ } else {
+ // 查询成功但没有路由信息(可能是无效单号或暂无物流更新)
+ await LogisticsRecord.findByIdAndUpdate(record._id, {
+ 物流状态: null,
+ 物流详情: JSON.stringify(queryResult.msgData),
+ 更新时间: new Date(),
+ });
+
+ batchResults.successCount++;
+ batchResults.details.push({
+ _id: String(record._id),
+ trackingNumber: trackingNumber,
+ status: 'success',
+ message: '查询成功,但暂无物流信息',
+ newStatus: null
+ });
+
+ console.log(`⚠️ ${trackingNumber} 查询成功,但暂无物流信息`);
+ }
+ } else {
+ // 查询失败
+ batchResults.failedCount++;
+ batchResults.details.push({
+ _id: String(record._id),
+ trackingNumber: trackingNumber,
+ status: 'failed',
+ message: queryResult.errorMsg || queryResult.msgData?.errorMsg || '查询失败'
+ });
+
+ console.log(`❌ ${trackingNumber} 查询失败`);
+ }
+
+ // 添加延迟以避免请求过于频繁(每次查询间隔1秒)
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ } catch (error) {
+ const trackingNumber = String(record.物流单号);
+ console.error(`查询 ${trackingNumber} 时发生错误:`, error);
+
+ batchResults.failedCount++;
+ batchResults.details.push({
+ _id: String(record._id),
+ trackingNumber: trackingNumber,
+ status: 'failed',
+ message: '网络请求失败'
+ });
+ }
+ }
+
+ console.log(`批量查询完成,成功: ${batchResults.successCount}, 失败: ${batchResults.failedCount}`);
+
+ return res.status(200).json({
+ success: true,
+ message: `批量查询完成,成功 ${batchResults.successCount} 条,失败 ${batchResults.failedCount} 条`,
+ results: batchResults
+ });
+
+ } catch (error) {
+ console.error('批量查询物流信息失败:', error);
+
+ return res.status(500).json({
+ success: false,
+ error: '服务器内部错误',
+ message: '批量查询失败,请稍后重试'
+ });
+ }
+};
+
+export default connectDB(handler);
\ No newline at end of file
diff --git a/src/pages/api/test/logistic-detail.ts b/src/pages/api/test/logistic-detail.ts
new file mode 100644
index 0000000..f0c2ef8
--- /dev/null
+++ b/src/pages/api/test/logistic-detail.ts
@@ -0,0 +1,137 @@
+/**
+ * 物流详情查询API
+ * @author 阿瑞
+ * @version 1.0.0
+ * @description 根据记录ID查询物流详细轨迹信息
+ */
+
+import type { NextApiRequest, NextApiResponse } from 'next';
+import connectDB from '@/utils/connectDB';
+import { LogisticsRecord } from '@/models';
+
+/**
+ * 物流轨迹节点接口
+ */
+interface LogisticsRouteNode {
+ time: string;
+ opCode: string;
+ remark: string;
+ location?: string;
+}
+
+/**
+ * 物流详情响应接口
+ */
+interface LogisticsDetailResponse {
+ success: boolean;
+ data?: {
+ trackingNumber: string;
+ currentStatus: string;
+ routes: LogisticsRouteNode[];
+ lastUpdateTime: string;
+ };
+ message?: string;
+ error?: string;
+}
+
+/**
+ * 处理物流详情查询请求
+ * @param req HTTP请求对象
+ * @param res HTTP响应对象
+ */
+const handler = async (
+ req: NextApiRequest,
+ res: NextApiResponse
+) => {
+ // 只允许GET请求
+ if (req.method !== 'GET') {
+ return res.status(405).json({
+ success: false,
+ message: '只支持GET请求'
+ });
+ }
+
+ try {
+ const { id } = req.query;
+
+ // 参数验证
+ if (!id || typeof id !== 'string') {
+ return res.status(400).json({
+ success: false,
+ message: '缺少必要参数:记录ID'
+ });
+ }
+
+ // 查询物流记录
+ const record = await LogisticsRecord.findById(id)
+ .select('物流单号 物流状态 物流详情 更新时间')
+ .lean();
+
+ if (!record) {
+ return res.status(404).json({
+ success: false,
+ message: '未找到对应的物流记录'
+ });
+ }
+
+ // 解析物流详情
+ let routesData: LogisticsRouteNode[] = [];
+ const recordData = record as any;
+ let currentStatus = recordData.物流状态 || '未知';
+
+ if (recordData.物流详情) {
+ try {
+ const logisticsDetail = typeof recordData.物流详情 === 'string'
+ ? JSON.parse(recordData.物流详情)
+ : recordData.物流详情;
+
+ // 从SF快递API响应中提取路由信息
+ if (logisticsDetail?.msgData?.routeResps?.length > 0) {
+ const routeResp = logisticsDetail.msgData.routeResps[0];
+ const routes = routeResp.routes || [];
+
+ routesData = routes.map((route: any) => ({
+ time: route.acceptTime || '未知时间',
+ opCode: route.opCode || '未知',
+ remark: route.remark || '无备注',
+ location: route.acceptAddress || undefined
+ })).reverse(); // 反转数组,让最新的记录在前面
+ }
+ } catch (parseError) {
+ console.error('解析物流详情失败:', parseError);
+ }
+ }
+
+ // 如果没有路由数据,返回基本信息
+ if (routesData.length === 0) {
+ routesData = [{
+ time: recordData.更新时间 ? new Date(recordData.更新时间).toLocaleString('zh-CN') : '未知时间',
+ opCode: currentStatus,
+ remark: currentStatus === null ? '包裹尚未发出' : '暂无详细物流信息',
+ location: undefined
+ }];
+ }
+
+ return res.status(200).json({
+ success: true,
+ data: {
+ trackingNumber: recordData.物流单号 || '未知',
+ currentStatus: currentStatus,
+ routes: routesData,
+ lastUpdateTime: recordData.更新时间 ? new Date(recordData.更新时间).toLocaleString('zh-CN') : '未知'
+ },
+ message: '获取物流详情成功'
+ });
+
+ } catch (error) {
+ console.error('查询物流详情失败:', error);
+
+ return res.status(500).json({
+ success: false,
+ error: '服务器内部错误',
+ message: '查询物流详情失败,请稍后重试'
+ });
+ }
+};
+
+export default connectDB(handler);
\ No newline at end of file
diff --git a/src/pages/api/test/logistics-records.ts b/src/pages/api/test/logistics-records.ts
new file mode 100644
index 0000000..47aef2d
--- /dev/null
+++ b/src/pages/api/test/logistics-records.ts
@@ -0,0 +1,87 @@
+/**
+ * 物流记录查询API
+ * @author 阿瑞
+ * @version 1.0.0
+ * @description 获取所有物流记录数据的API接口
+ */
+
+import type { NextApiRequest, NextApiResponse } from 'next';
+import connectDB from '@/utils/connectDB';
+import { LogisticsRecord } from '@/models';
+
+/**
+ * 物流记录响应数据接口
+ */
+interface LogisticsRecordResponse {
+ success: boolean;
+ data?: any[];
+ total?: number;
+ message?: string;
+ error?: string;
+}
+
+/**
+ * 处理物流记录查询请求
+ * @param req HTTP请求对象
+ * @param res HTTP响应对象
+ */
+const handler = async (
+ req: NextApiRequest,
+ res: NextApiResponse
+) => {
+ // 只允许GET请求
+ if (req.method !== 'GET') {
+ return res.status(405).json({
+ success: false,
+ message: '只支持GET请求'
+ });
+ }
+
+ try {
+ // 获取查询参数
+ const { page = 1, pageSize = 20 } = req.query;
+ const skip = (Number(page) - 1) * Number(pageSize);
+
+ // 查询物流记录数据
+ const records = await LogisticsRecord.find({})
+ .select('物流单号 客户尾号 是否查询 物流状态 物流详情 更新时间 类型 createdAt') // 只选择需要的字段
+ .sort({ 更新时间: -1 }) // 按更新时间倒序
+ .skip(skip)
+ .limit(Number(pageSize))
+ .lean(); // 使用lean()提高查询性能
+
+ // 获取总记录数
+ const total = await LogisticsRecord.countDocuments({});
+
+ // 格式化返回数据
+ const formattedRecords = records.map((record: any) => ({
+ _id: record._id,
+ 物流单号: record.物流单号,
+ 客户尾号: record.客户尾号,
+ 是否查询: record.是否查询,
+ 物流状态: record.物流状态,
+ 物流详情: record.物流详情,
+ 更新时间: record.更新时间,
+ 类型: record.类型,
+ createdAt: record.createdAt,
+ }));
+
+ return res.status(200).json({
+ success: true,
+ data: formattedRecords,
+ total,
+ message: '获取物流记录成功'
+ });
+
+ } catch (error) {
+ console.error('获取物流记录失败:', error);
+
+ return res.status(500).json({
+ success: false,
+ error: '服务器内部错误',
+ message: '获取物流记录失败,请稍后重试'
+ });
+ }
+};
+
+export default connectDB(handler);
\ No newline at end of file
diff --git a/src/pages/api/test/sf-query.ts b/src/pages/api/test/sf-query.ts
new file mode 100644
index 0000000..af47c7c
--- /dev/null
+++ b/src/pages/api/test/sf-query.ts
@@ -0,0 +1,201 @@
+/**
+ * SF快递物流查询测试API
+ * @author 阿瑞
+ * @version 1.0.0
+ * @description 专门用于测试SF快递物流查询功能的API接口
+ */
+
+import type { NextApiRequest, NextApiResponse } from 'next';
+import querystring from 'querystring';
+import { getAccessToken } from '@/utils/getAccessToken';
+
+// ==================== 接口定义 ====================
+
+/**
+ * 查询请求参数接口
+ */
+interface QueryRequest {
+ trackingNumber: string;
+ phoneLast4Digits: string;
+}
+
+/**
+ * SF快递API响应接口
+ */
+interface SFApiResponse {
+ success: boolean;
+ errorCode?: string;
+ errorMsg?: string;
+ apiResultCode?: string;
+ apiErrorMsg?: string;
+ apiResponseID?: string;
+ msgData?: any;
+}
+
+// ==================== 主处理函数 ====================
+
+/**
+ * API请求处理函数
+ * @param req NextJS API请求对象
+ * @param res NextJS API响应对象
+ */
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ // 只允许POST请求
+ if (req.method !== 'POST') {
+ return res.status(405).json({
+ success: false,
+ errorMsg: '只支持POST请求方法'
+ });
+ }
+
+ try {
+ // ==================== 参数验证 ====================
+
+ const { trackingNumber, phoneLast4Digits }: QueryRequest = req.body;
+
+ // 验证必需参数
+ if (!trackingNumber || !phoneLast4Digits) {
+ return res.status(400).json({
+ success: false,
+ errorMsg: '缺少必需参数:trackingNumber 和 phoneLast4Digits 都不能为空'
+ });
+ }
+
+ // 验证运单号格式
+ if (typeof trackingNumber !== 'string' || trackingNumber.trim().length < 8) {
+ return res.status(400).json({
+ success: false,
+ errorMsg: '运单号格式错误:长度至少为8位'
+ });
+ }
+
+ // 验证手机号后4位格式
+ if (typeof phoneLast4Digits !== 'string' || !/^\d{4}$/.test(phoneLast4Digits)) {
+ return res.status(400).json({
+ success: false,
+ errorMsg: '手机号后4位格式错误:必须是4位数字'
+ });
+ }
+
+ console.log('开始查询SF快递物流信息:', { trackingNumber, phoneLast4Digits });
+
+ // ==================== 获取访问令牌 ====================
+
+ let accessToken: string;
+ try {
+ accessToken = await getAccessToken();
+ console.log('成功获取访问令牌');
+ } catch (tokenError) {
+ console.error('获取访问令牌失败:', tokenError);
+ return res.status(500).json({
+ success: false,
+ errorMsg: '获取访问令牌失败,请检查API配置'
+ });
+ }
+
+ // ==================== 构建查询请求 ====================
+
+ const partnerID = process.env.PARTNER_ID || 'YPD607MO';
+ const reqUrl = 'https://bspgw.sf-express.com/std/service';
+ const requestID = crypto.randomUUID();
+ const serviceCode = "EXP_RECE_SEARCH_ROUTES";
+ const timestamp = Date.now().toString();
+
+ // 构建消息数据对象
+ const msgData = {
+ language: "zh-CN",
+ trackingType: "1",
+ trackingNumber: [trackingNumber.trim()],
+ methodType: "1",
+ checkPhoneNo: phoneLast4Digits.trim()
+ };
+
+ console.log('构建的查询参数:', {
+ partnerID,
+ requestID,
+ serviceCode,
+ timestamp,
+ msgData
+ });
+
+ // 构建请求体数据
+ const requestData = querystring.stringify({
+ partnerID,
+ requestID,
+ serviceCode,
+ timestamp,
+ accessToken,
+ msgData: JSON.stringify(msgData)
+ });
+
+ // ==================== 调用SF快递API ====================
+
+ const response = await fetch(reqUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'User-Agent': 'SF-Express-Query-Test'
+ },
+ body: requestData
+ });
+
+ if (!response.ok) {
+ console.error('SF API HTTP错误:', response.status, response.statusText);
+ return res.status(500).json({
+ success: false,
+ errorMsg: `SF快递API请求失败: HTTP ${response.status}`
+ });
+ }
+
+ const responseData = await response.json();
+ console.log('SF API原始响应:', JSON.stringify(responseData, null, 2));
+
+ // ==================== 处理API响应 ====================
+
+ // 检查API级别的错误
+ if (responseData.apiResultCode && responseData.apiResultCode !== 'A1000') {
+ console.error('SF API业务错误:', responseData);
+ return res.status(400).json({
+ success: false,
+ errorCode: responseData.apiResultCode,
+ errorMsg: responseData.apiErrorMsg || '未知的SF快递API错误',
+ apiResponseID: responseData.apiResponseID,
+ rawResponse: responseData
+ });
+ }
+
+ // 解析业务数据
+ let parsedMsgData = null;
+ if (responseData.apiResultData) {
+ try {
+ parsedMsgData = JSON.parse(responseData.apiResultData);
+ } catch (parseError) {
+ console.error('解析msgData失败:', parseError);
+ return res.status(500).json({
+ success: false,
+ errorMsg: '解析物流数据失败'
+ });
+ }
+ }
+
+ // ==================== 格式化响应数据 ====================
+
+ const result: SFApiResponse = {
+ success: true,
+ msgData: parsedMsgData,
+ apiResponseID: responseData.apiResponseID
+ };
+
+ console.log('查询成功,返回结果');
+ res.status(200).json(result);
+
+ } catch (error: any) {
+ console.error('SF快递查询过程中发生错误:', error);
+ res.status(500).json({
+ success: false,
+ errorMsg: error.message || '服务器内部错误'
+ });
+ }
+};
+
+export default handler;
\ No newline at end of file
diff --git a/src/pages/api/tools/test4.ts b/src/pages/api/tools/test4.ts
deleted file mode 100644
index 7b9aed7..0000000
--- a/src/pages/api/tools/test4.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { NextApiRequest, NextApiResponse } from 'next';
-import connectDB from '@/utils/connectDB';
-
-//const extractInfoAPI = 'http://192.168.1.8:8006/extract_info/';
-//const parseLocationAPI = 'http://192.168.1.8:8000/parse_location/';
-// 从环境变量获取 API 的 URL
-const extractInfoAPI = process.env.EXTRACT_INFO_API_URL || '';
-const parseLocationAPI = process.env.PARSE_LOCATION_API_URL || '';
-
-const handler = async (req: NextApiRequest, res: NextApiResponse) => {
- if (req.method !== 'POST') {
- return res.status(405).json({ error: 'Method not allowed' });
- }
-
- try {
- const { text } = req.body;
-
- // 1. 使用 extract_info API 提取信息
- const extractResponse = await fetch(extractInfoAPI, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ text })
- });
-
- if (!extractResponse.ok) {
- throw new Error(`Extract info API failed: ${extractResponse.status}`);
- }
-
- const extractData = await extractResponse.json();
- const extractedInfo = extractData.extracted_info[0];
-
- // 提取信息失败时抛出异常
- if (!extractedInfo) {
- throw new Error('Failed to extract information');
- }
-
- // 2. 将城市、县区、详细地址拼接为完整地址
- const city = extractedInfo['城市'][0]?.text || '';
- const county = extractedInfo['县区'][0]?.text || '';
- const detailAddress = extractedInfo['详细地址'][0]?.text || '';
- const fullAddress = `${city}${county}${detailAddress}`;
-
- // 3. 使用 parse_location API 解析完整地址
- const parseResponse = await fetch(parseLocationAPI, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ text: fullAddress })
- });
-
- if (!parseResponse.ok) {
- throw new Error(`Parse location API failed: ${parseResponse.status}`);
- }
-
- const parsedLocation = await parseResponse.json();
-
- // 4. 返回组合后的结果
- const result = {
- ...extractedInfo,
- 解析地址: parsedLocation,
- };
-
- res.status(200).json(result);
- } catch (error) {
- console.error('Error in processing request:', error);
- res.status(500).json({ error: 'Internal Server Error' });
- }
-};
-
-export default connectDB(handler);
diff --git a/src/pages/backstage/accounts/account-modal.tsx b/src/pages/backstage/accounts/account-modal.tsx
new file mode 100644
index 0000000..229222c
--- /dev/null
+++ b/src/pages/backstage/accounts/account-modal.tsx
@@ -0,0 +1,162 @@
+import React, { useEffect, useState } from 'react';
+import { Modal, Form, Input, message, Select } from 'antd';
+import axios from 'axios';
+import { useUserInfo } from '@/store/userStore'; // 使用 Zustand 获取用户信息
+import { IAccount, IUser, ICategory } from '@/models/types';
+
+interface AccountModalProps {
+ visible: boolean;
+ onCancel: () => void;
+ onOk: () => void;
+ account: IAccount | null;
+}
+
+const AccountModal: React.FC = ({ visible, onOk, onCancel, account }) => {
+ const [form] = Form.useForm();
+ const [users, setUsers] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const userInfo = useUserInfo(); // 获取当前用户信息
+
+ /*useEffect(() => {
+ fetchUsersAndCategories(userInfo.团队?._id);
+ if (account) {
+ form.setFieldsValue({
+ 账号编号: account.账号编号,
+ 微信号: account.微信号,
+ 微信昵称: account.微信昵称,
+ 手机编号: account.手机编号,
+ 账号状态: account.账号状态,
+ 备注: account.备注,
+ 账号负责人: account.账号负责人?._id,
+ 前端引流人员: account.前端引流人员?._id,
+ 账号类型: account.账号类型?.map(type => type._id),
+ });
+ } else {
+ form.resetFields();
+ }
+ }, [account, form, userInfo.团队]);*/
+ useEffect(() => {
+ const teamId = userInfo.团队?._id;
+ if (teamId) {
+ fetchUsersAndCategories(teamId);
+ }
+ if (account) {
+ form.setFieldsValue({
+ 账号编号: account.账号编号,
+ 微信号: account.微信号,
+ 微信昵称: account.微信昵称,
+ 手机编号: account.手机编号,
+ 账号状态: account.账号状态,
+ 备注: account.备注,
+ 账号负责人: account.账号负责人?._id,
+ 前端引流人员: account.前端引流人员?._id,
+ 账号类型: account.账号类型?.map(type => type._id),
+ });
+ } else {
+ form.resetFields();
+ }
+ }, [account, form, userInfo.团队]);
+
+ const fetchUsersAndCategories = async (teamId: string) => {
+ try {
+ const [usersResponse, categoriesResponse] = await Promise.all([
+ axios.get(`/api/backstage/users?teamId=${teamId}`),
+ axios.get(`/api/backstage/categories?teamId=${teamId}`)
+ ]);
+ setUsers(usersResponse.data.users);
+ setCategories(categoriesResponse.data.categories);
+ } catch (error) {
+ message.error('加载用户或品类数据失败');
+ }
+ };
+
+ const handleSubmit = async () => {
+ try {
+ const values = await form.validateFields();
+ const method = account ? 'PUT' : 'POST';
+ const url = account ? `/api/backstage/accounts/${account._id}` : '/api/backstage/accounts';
+
+ await axios({
+ method,
+ url,
+ data: {
+ ...values,
+ 团队: userInfo.团队?._id,
+ },
+ });
+
+ message.success('店铺账号操作成功');
+ onOk();
+ } catch (error) {
+ console.error('Validate Failed:', error);
+ message.error('店铺账号操作失败');
+ }
+ };
+
+ return (
+ {
+ form.resetFields();
+ onCancel();
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AccountModal;
diff --git a/src/pages/backstage/accounts/index.tsx b/src/pages/backstage/accounts/index.tsx
new file mode 100644
index 0000000..0055c32
--- /dev/null
+++ b/src/pages/backstage/accounts/index.tsx
@@ -0,0 +1,251 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { Table, Button, message, Card, Tag, Typography, Popconfirm } from 'antd';
+import axios from 'axios';
+import AccountModal from './account-modal'; // 引入店铺账号模态框组件
+import { IAccount, ICategory } from '@/models/types'; // 确保有正确的类型定义
+import { useUserInfo } from '@/store/userStore'; // 使用 Zustand 获取用户信息
+import { ColumnsType } from 'antd/es/table';
+import { ShopOutlined, WechatOutlined } from '@ant-design/icons';
+import { Icon } from '@iconify/react';
+import { IconButton, Iconify } from '@/components/icon';
+const { Text } = Typography;
+
+const AccountsPage = () => {
+ const [accounts, setAccounts] = useState([]);
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [currentAccount, setCurrentAccount] = useState(null);
+ const userInfo = useUserInfo(); // 获取当前用户信息
+
+ useEffect(() => {
+ if (userInfo.团队?._id) {
+ fetchAccounts(userInfo.团队._id);
+ }
+ }, [userInfo]);
+
+ const fetchAccounts = async (teamId: string) => {
+ try {
+ const { data } = await axios.get(`/api/backstage/accounts?teamId=${teamId}`);
+ setAccounts(data.accounts);
+ } catch (error) {
+ message.error('加载店铺账号数据失败');
+ }
+ };
+
+ const handleModalOk = () => {
+ setIsModalVisible(false);
+ if (userInfo.团队?._id) {
+ fetchAccounts(userInfo.团队._id);
+ }
+ };
+
+ const handleEdit = (account: IAccount) => {
+ setCurrentAccount(account);
+ setIsModalVisible(true);
+ };
+
+ const handleDelete = async (id: string) => {
+ try {
+ await axios.delete(`/api/backstage/accounts/${id}`);
+ if (userInfo.团队?._id) {
+ fetchAccounts(userInfo.团队._id);
+ }
+ message.success('店铺账号删除成功');
+ } catch (error) {
+ message.error('删除店铺账号失败');
+ }
+ };
+
+ // 从所有账户中提取所有唯一的类别名称
+ const uniqueCategories = Array.from(new Set(
+ accounts.flatMap(acc => acc.账号类型?.map(cat => cat.name) ?? [])
+ ));
+ // 将这些类别转换成筛选所需的格式
+ const categoryFilters = uniqueCategories.map(cat => ({
+ text: cat,
+ value: cat,
+ }));
+
+ const columns: ColumnsType = useMemo(() => [
+ //序号
+ {
+ title: '序号',
+ dataIndex: '序号',
+ width: 60,
+ key: 'index',
+ render: (_text: any, _record: any, index: number) => index + 1,
+ },
+ {
+ title: '账号编号',
+ dataIndex: '账号编号',
+ width: 100, // 定义列宽度
+ align: 'center',
+ key: '账号编号',
+ render: (text: string) => (
+
+
+ {text}
+
+
+ ),
+ },
+ {
+ title: '账号资料',
+ dataIndex: 'name',
+ width: 160,
+ render: (_, record) => {
+ return (
+
+

+
+
+ {record.微信昵称}
+
+
+ } color="success">{record.微信号}
+
+
+
+ );
+ },
+ },
+ {
+ title: '账号类型',
+ dataIndex: '账号类型',
+ filters: categoryFilters,
+ onFilter: (value: any, record: IAccount) => record.账号类型?.some((cat: ICategory) => cat.name === value) ?? false,
+ render: (categories: ICategory[]) => (
+ <>
+ {categories.map((category: ICategory) => (
+
+
+ {category.icon && (
+
+ )}
+ {category.name}
+
+
+ ))}
+ >
+ )
+ },
+ {
+ title: '手机编号',
+ dataIndex: '手机编号',
+ key: 'phoneNumber',
+ },
+ {
+ title: '账号状态',
+ dataIndex: '账号状态',
+ key: 'status',
+ render: (status: number) => (
+
+ {status === 1 ? '正常' : status === 0 ? '停用' : '异常'}
+
+ )
+ },
+ {
+ title: '账号负责人',
+ dataIndex: '账号负责人',
+ align: 'center',
+ width: 100, // 定义列宽度
+ key: '账号负责人',
+ render: (账号负责人: any) => (
+ {账号负责人?.姓名 ?? 账号负责人}
// 如果归属人是个对象,则显示realname,否则直接显示归属人(ID)
+ ),
+ },
+ {
+ title: '前端引流人员',
+ dataIndex: '前端引流人员',
+ align: 'center',
+ width: 120, // 定义列宽度
+ key: '前端引流人员',
+ //filters: frontendHandlersFilters,
+ //onFilter: (value, record) => record.前端引流人员?.realname === value,
+ render: (前端引流人员: any) => (
+ {前端引流人员?.姓名 ?? '未知'}
+ ),
+ },
+ //备注
+ {
+ title: '备注',
+ dataIndex: '备注',
+ key: 'remark',
+ },
+ {
+ title: '操作',
+ key: 'action',
+ /*render: (_: any, record: IAccount) => (
+
+
+
+
+ ),*/
+ width: 90,
+ align: 'center',
+ render: (_: any, record: IAccount) => (
+
+
handleEdit(record)}>
+
+
+
handleDelete(record._id)}
+ okText="是"
+ cancelText="否"
+ placement="left"
+ >
+
+
+
+
+
+ ),
+ },
+ ], [accounts]);
+
+ return (
+ { setIsModalVisible(true); setCurrentAccount(null); }}>
+ 添加账号
+
+ }
+ >
+
+ {isModalVisible && (
+ setIsModalVisible(false)}
+ account={currentAccount}
+ />
+ )}
+
+ );
+};
+
+export default AccountsPage;
diff --git a/src/pages/team/sale/index.tsx b/src/pages/team/sale/index.tsx
index 8dec1df..af60318 100644
--- a/src/pages/team/sale/index.tsx
+++ b/src/pages/team/sale/index.tsx
@@ -1,4 +1,9 @@
-//src\pages\team\sale\index.tsx
+/**
+ * 文件: team/sale/index.tsx
+ * 作者: 阿瑞
+ * 功能: 销售记录页面 - 创建和管理销售记录
+ * 版本: v2.0.0 - 使用 fetch 替换 axios,修复搜索过滤错误
+ */
import React, { useEffect, useState } from 'react';
import { Form, Input, Select, DatePicker, Button, Row, Col, Card, Divider, Tag, App } from 'antd';
import { ICustomer, ICustomerCoupon, IProduct, ISalesRecord } from '@/models/types';
@@ -482,10 +487,10 @@ const SalesRecordPage: React.FC = ({ salesRecord, onCancel
const account = accounts.find(acc => acc._id === option?.value);
if (!account) return false;
const { 微信号, 微信昵称 } = account;
- return (
- 微信号.toLowerCase().includes(input.toLowerCase()) ||
- 微信昵称.toLowerCase().includes(input.toLowerCase())
- );
+ const inputLower = input.toLowerCase();
+ const wechatIdMatch = 微信号 ? 微信号.toLowerCase().includes(inputLower) : false;
+ const wechatNameMatch = 微信昵称 ? 微信昵称.toLowerCase().includes(inputLower) : false;
+ return wechatIdMatch || wechatNameMatch;
}}
>
{accounts.map(account => (
diff --git a/src/pages/test/sf.tsx b/src/pages/test/sf.tsx
new file mode 100644
index 0000000..d7fa463
--- /dev/null
+++ b/src/pages/test/sf.tsx
@@ -0,0 +1,439 @@
+/**
+ * 物流查询测试页面
+ * @author 阿瑞
+ * @version 1.0.0
+ * @description SF快递物流查询功能测试页面,用于测试运单号查询功能
+ */
+
+import { useState } from 'react';
+import { Card, Form, Input, Button, Alert, Timeline, Spin, Typography, Tag } from 'antd';
+import { SearchOutlined, TruckOutlined, PhoneOutlined, CodeOutlined, SyncOutlined } from '@ant-design/icons';
+import { SF_OPCODE_STATUS_MAP, type SFExpressStatusInfo } from '@/types/enum';
+
+const { Title, Text } = Typography;
+
+// ==================== 接口定义 ====================
+
+/**
+ * 物流查询表单数据接口
+ */
+interface QueryFormData {
+ trackingNumber: string;
+ phoneLast4Digits: string;
+}
+
+/**
+ * 物流路由信息接口
+ */
+interface RouteInfo {
+ opCode: string;
+ remark: string;
+ acceptTime: string;
+ acceptAddress?: string;
+}
+
+/**
+ * 查询响应数据接口
+ */
+interface QueryResponse {
+ success: boolean;
+ errorCode?: string;
+ errorMsg?: string;
+ apiErrorMsg?: string;
+ apiResponseID?: string;
+ msgData?: {
+ success?: boolean;
+ errorCode?: string;
+ errorMsg?: string;
+ msgData?: {
+ routeResps?: Array<{
+ mailNo: string;
+ routes?: RouteInfo[];
+ }>;
+ };
+ };
+}
+
+// ==================== 主组件 ====================
+
+/**
+ * SF快递物流查询测试页面组件
+ * @returns 物流查询页面JSX元素
+ */
+const SFExpressQueryPage: React.FC = () => {
+ // ==================== 状态管理 ====================
+
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+ const [queryResult, setQueryResult] = useState(null);
+ const [error, setError] = useState('');
+
+ // ==================== 业务逻辑函数 ====================
+
+ /**
+ * 根据opCode获取状态信息
+ * @param opCode SF快递opCode
+ * @returns 状态信息对象
+ */
+ const getStatusByOpCode = (opCode: string): SFExpressStatusInfo => {
+ return SF_OPCODE_STATUS_MAP[opCode] || { status: '处理中', color: 'default', description: '快件正在处理中' };
+ };
+
+ /**
+ * 获取当前物流的整体状态
+ * @param routes 物流路由信息数组
+ * @returns 状态信息对象
+ */
+ const getOverallStatus = (routes: RouteInfo[]): { status: string; color: string } => {
+ if (!routes || routes.length === 0) {
+ return { status: '暂无信息', color: 'default' };
+ }
+
+ // 取最新的路由节点状态
+ const latestRoute = routes[0];
+ const statusInfo = getStatusByOpCode(latestRoute.opCode);
+
+ return { status: statusInfo.status, color: statusInfo.color };
+ };
+
+ /**
+ * 处理物流查询提交
+ * @param values 表单数据
+ */
+ const handleQuery = async (values: QueryFormData) => {
+ setLoading(true);
+ setError('');
+ setQueryResult(null);
+
+ try {
+ const response = await fetch('/api/test/sf-query', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ trackingNumber: values.trackingNumber,
+ phoneLast4Digits: values.phoneLast4Digits,
+ }),
+ });
+
+ const result = await response.json();
+ console.log('查询结果:', result);
+
+ setQueryResult(result);
+
+ if (!result.success) {
+ setError(result.errorMsg || '查询失败,请稍后重试');
+ }
+ } catch (err) {
+ console.error('查询过程中发生错误:', err);
+ setError('网络请求失败,请检查网络连接或稍后重试');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ /**
+ * 重置查询表单和结果
+ */
+ const handleReset = () => {
+ form.resetFields();
+ setQueryResult(null);
+ setError('');
+ };
+
+ // ==================== 渲染函数 ====================
+
+ /**
+ * 渲染物流时间轴组件
+ * @param routes 物流路由信息数组
+ * @returns 时间轴JSX元素
+ */
+ const renderTimelineItems = (routes: RouteInfo[]) => {
+ return routes.map((route, index) => {
+ const statusInfo = getStatusByOpCode(route.opCode);
+
+ // 时间轴节点颜色
+ const timelineColor = index === 0 ? 'green' :
+ statusInfo.color === 'success' ? 'green' :
+ statusInfo.color === 'error' || statusInfo.color === 'red' ? 'red' :
+ statusInfo.color === 'warning' ? 'orange' : 'blue';
+
+ return {
+ color: timelineColor,
+ children: (
+
+
+
+
+
+ {statusInfo.status}
+
+
+
+ opCode: {route.opCode}
+
+
+
+
+ {route.remark}
+
+
+ {route.acceptAddress && (
+
+ 📍 {route.acceptAddress}
+
+ )}
+
+
+
+
+ {route.acceptTime}
+
+
+
+
+ ),
+ };
+ });
+ };
+
+ return (
+
+
+ {/* ==================== 页面标题 ==================== */}
+
+
+
+
+ SF快递物流查询
+
+
+ 输入运单号和手机号后4位查询物流信息
+
+
+
+
+ {/* ==================== 上半部分:左1/3查询表单+运单信息,右2/3开发者数据 ==================== */}
+
+ {/* 左侧1/3:查询表单和运单信息 */}
+
+ {/* 查询表单 */}
+
+
+ }
+ />
+
+
+
+ }
+ maxLength={4}
+ />
+
+
+
+
+ : }
+ className="flex-1"
+ >
+ {loading ? '查询中...' : '查询物流'}
+
+
+
+
+
+
+
+ {/* 运单信息和当前状态 */}
+ {queryResult?.success && queryResult.msgData?.msgData?.routeResps?.length && queryResult.msgData.msgData.routeResps.length > 0 && (
+
+ {queryResult.msgData?.msgData?.routeResps?.map((routeResp: any, index: number) => {
+ const overallStatus = getOverallStatus(routeResp.routes?.slice().reverse() || []);
+
+ return (
+
+
+
+ {routeResp.mailNo}
+
+
+ SF快递运单
+
+
+
+
+ 当前状态
+
+ {overallStatus.status}
+
+
+
+
+
+
+ {routeResp.routes?.length || 0}
+
+
+ 物流节点
+
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* 错误信息显示 */}
+ {error && (
+
+ )}
+
+
+ {/* 右侧2/3:开发者数据 */}
+
+ {queryResult && !loading && (
+
+
+
+
+ 开发者数据 ({JSON.stringify(queryResult).length} 字符)
+
+
+
+
+ {JSON.stringify(queryResult, null, 2)}
+
+
+
+ )}
+
+ {/* 加载状态 */}
+ {loading && (
+
+
+
+
+
+ 正在查询物流信息
+
+
+ 请稍等片刻...
+
+
+
+
+ )}
+
+ {/* 右侧占位内容 */}
+ {!queryResult && !loading && (
+
+
+
+ 查询后将在此处显示开发者数据
+
+
+ )}
+
+
+
+ {/* ==================== 下半部分:物流轨迹 ==================== */}
+ {queryResult?.success && queryResult.msgData?.msgData?.routeResps?.length && queryResult.msgData.msgData.routeResps.length > 0 && !loading && (
+
+
+ {queryResult.msgData?.msgData?.routeResps?.map((routeResp: any, index: number) => (
+
+ {routeResp.routes?.length > 0 ? (
+
+ ) : (
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* 查询失败时的错误信息 */}
+ {queryResult && !loading && !queryResult.success && (
+
+
+
+ )}
+
+ {/* 无查询结果 */}
+ {queryResult?.success && !queryResult.msgData?.msgData?.routeResps?.length && !loading && (
+
+
+
+ )}
+
+
+ );
+};
+
+export default SFExpressQueryPage;
diff --git a/src/pages/test/sfall.tsx b/src/pages/test/sfall.tsx
new file mode 100644
index 0000000..c588a9d
--- /dev/null
+++ b/src/pages/test/sfall.tsx
@@ -0,0 +1,402 @@
+/**
+ * 物流记录管理页面
+ * @author 阿瑞
+ * @version 1.0.0
+ * @description 显示所有物流记录的管理页面,包含物流单号、客户尾号、查询状态、物流状态等信息
+ */
+
+import { useState, useEffect } from 'react';
+import { Card, Table, Tag, Typography, Alert, Button, Progress, Modal, List } from 'antd';
+import { TruckOutlined, ReloadOutlined, ThunderboltOutlined } from '@ant-design/icons';
+import StatusTag from '@/components/logistics/status-tag';
+import type { ColumnsType } from 'antd/es/table';
+
+const { Title, Text } = Typography;
+
+// ==================== 接口定义 ====================
+
+/**
+ * 物流记录数据接口
+ */
+interface LogisticsRecordData {
+ _id: string;
+ 物流单号: string;
+ 客户尾号: string;
+ 是否查询: boolean;
+ 物流状态: string;
+ 物流详情: any;
+ 更新时间: string;
+ 类型: string;
+ createdAt: string;
+}
+
+/**
+ * API响应数据接口
+ */
+interface ApiResponse {
+ success: boolean;
+ data?: LogisticsRecordData[];
+ total?: number;
+ message?: string;
+ error?: string;
+}
+
+// ==================== 主组件 ====================
+
+/**
+ * 物流记录管理页面组件
+ * @returns 物流记录管理页面JSX元素
+ */
+const LogisticsRecordsPage: React.FC = () => {
+ // ==================== 状态管理 ====================
+
+ const [loading, setLoading] = useState(false);
+ const [records, setRecords] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [error, setError] = useState('');
+ const [pagination, setPagination] = useState({
+ current: 1,
+ pageSize: 20,
+ });
+
+ // 批量查询相关状态
+ const [batchLoading, setBatchLoading] = useState(false);
+ const [batchModalVisible, setBatchModalVisible] = useState(false);
+ const [batchResults, setBatchResults] = useState(null);
+
+ // ==================== 业务逻辑函数 ====================
+
+ /**
+ * 获取物流记录数据
+ * @param page 页码
+ * @param pageSize 每页数量
+ */
+ const fetchLogisticsRecords = async (page: number = 1, pageSize: number = 20) => {
+ setLoading(true);
+ setError('');
+
+ try {
+ const response = await fetch(`/api/test/logistics-records?page=${page}&pageSize=${pageSize}`);
+ const result: ApiResponse = await response.json();
+
+ if (result.success && result.data) {
+ setRecords(result.data);
+ setTotal(result.total || 0);
+ } else {
+ setError(result.message || result.error || '获取数据失败');
+ }
+ } catch (err) {
+ console.error('获取物流记录失败:', err);
+ setError('网络请求失败,请检查网络连接');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ /**
+ * 处理表格分页变化
+ * @param page 页码
+ * @param pageSize 每页数量
+ */
+ const handleTableChange = (page: number, pageSize?: number) => {
+ const newPagination = {
+ current: page,
+ pageSize: pageSize || pagination.pageSize,
+ };
+ setPagination(newPagination);
+ fetchLogisticsRecords(page, pageSize || pagination.pageSize);
+ };
+
+ /**
+ * 刷新数据
+ */
+ const handleRefresh = () => {
+ fetchLogisticsRecords(pagination.current, pagination.pageSize);
+ };
+
+
+
+ /**
+ * 批量查询物流信息
+ */
+ const handleBatchQuery = async () => {
+ setBatchLoading(true);
+ setBatchResults(null);
+
+ try {
+ const response = await fetch('/api/test/batch-query-logistics', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ setBatchResults(result.results);
+ setBatchModalVisible(true);
+ // 批量查询完成后刷新列表
+ fetchLogisticsRecords(pagination.current, pagination.pageSize);
+ } else {
+ setError(result.message || '批量查询失败');
+ }
+ } catch (err) {
+ console.error('批量查询失败:', err);
+ setError('网络请求失败,请检查网络连接');
+ } finally {
+ setBatchLoading(false);
+ }
+ };
+
+ // ==================== 表格列定义 ====================
+
+ const columns: ColumnsType = [
+ {
+ title: '物流单号',
+ dataIndex: '物流单号',
+ key: '物流单号',
+ width: 180,
+ render: (text: string) => (
+
+ {text || '未填写'}
+
+ ),
+ },
+ {
+ title: '客户尾号',
+ dataIndex: '客户尾号',
+ key: '客户尾号',
+ width: 100,
+ align: 'center',
+ render: (text: string) => (
+
+ {text || '-'}
+
+ ),
+ },
+ {
+ title: '是否查询',
+ dataIndex: '是否查询',
+ key: '是否查询',
+ width: 100,
+ align: 'center',
+ render: (isQueried: boolean) => (
+
+ {isQueried ? '已查询' : '未查询'}
+
+ ),
+ },
+ {
+ title: '物流状态',
+ dataIndex: '物流状态',
+ key: '物流状态',
+ width: 120,
+ render: (opCode: string | null, record: LogisticsRecordData) => {
+ return (
+
+ );
+ },
+ },
+ ];
+
+ // ==================== 生命周期 ====================
+
+ useEffect(() => {
+ fetchLogisticsRecords();
+ }, []);
+
+ // ==================== 渲染组件 ====================
+
+ return (
+
+
+ {/* ==================== 页面标题 ==================== */}
+
+
+
+
+
+ 物流记录管理
+
+
+ 查看和管理所有物流记录信息
+
+
+
}
+ onClick={handleRefresh}
+ loading={loading}
+ >
+ 刷新数据
+
+
+
+
+ {/* ==================== 主要内容区域 ==================== */}
+
+ {/* 左侧2/3:物流记录列表 */}
+
+
+ {/* 错误信息显示 */}
+ {error && (
+
+ )}
+
+ {/* 数据统计 */}
+
+
+
+ 共 {total} 条记录
+
+
+
+
+ {/* 物流记录表格 */}
+
+ columns={columns}
+ dataSource={records}
+ rowKey="_id"
+ loading={loading}
+ pagination={{
+ current: pagination.current,
+ pageSize: pagination.pageSize,
+ total: total,
+ showSizeChanger: true,
+ showQuickJumper: true,
+ showTotal: (total, range) =>
+ `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
+ onChange: handleTableChange,
+ onShowSizeChange: handleTableChange,
+ }}
+ scroll={{ x: 600 }}
+ size="middle"
+ />
+
+
+
+ {/* 右侧1/3:批量查询功能 */}
+
+
+
+
+
+
批量查询物流
+
+ 自动查询所有物流记录的最新状态
+
+
+
+
+
+
查询规则:
+
+
• 只查询有物流单号的记录
+
• 只查询有客户尾号的记录
+
• 只查询标记为"需要查询"的记录
+
• 逐个查询,避免并发请求
+
+
+
+
}
+ loading={batchLoading}
+ onClick={handleBatchQuery}
+ block
+ >
+ {batchLoading ? '正在批量查询...' : '开始批量查询'}
+
+
+ {batchLoading && (
+
+ )}
+
+
+
+
+
+
+ {/* 批量查询结果弹窗 */}
+ setBatchModalVisible(false)}
+ footer={[
+
+ ]}
+ width={600}
+ >
+ {batchResults && (
+
+
+
+
+ {batchResults.total}
+ 总数
+
+
+ {batchResults.successCount}
+ 成功
+
+
+ {batchResults.failedCount}
+ 失败
+
+
+
+
+
(
+
+
+
+
{item.trackingNumber}
+
{item.message}
+
+
+ {item.newStatus && (
+ {item.newStatus}
+ )}
+
+ {item.status === 'success' ? '成功' : '失败'}
+
+
+
+
+ )}
+ size="small"
+ style={{ maxHeight: '400px', overflow: 'auto' }}
+ />
+
+ )}
+
+
+
+ );
+};
+
+export default LogisticsRecordsPage;
diff --git a/src/types/enum.ts b/src/types/enum.ts
index e204798..43e4c54 100644
--- a/src/types/enum.ts
+++ b/src/types/enum.ts
@@ -53,8 +53,90 @@ export enum BasicStatus {
}
export enum PermissionType {
- CATALOGUE,
- MENU,
- BUTTON,
- }
+ CATALOGUE,
+ MENU,
+ BUTTON,
+}
+
+// ==================== SF快递物流状态映射 ====================
+
+/**
+ * SF快递状态信息接口
+ * @author 阿瑞
+ * @version 1.0.0
+ */
+export interface SFExpressStatusInfo {
+ status: string;
+ color: string;
+ description: string;
+}
+
+/**
+ * SF快递opCode状态映射表
+ * @description 基于真实SF快递物流数据分析得出的opCode对应状态
+ * @author 阿瑞
+ * @version 1.0.0
+ */
+export const SF_OPCODE_STATUS_MAP: Record = {
+ // ========== 揽收相关 ==========
+ '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: '快件途经中转城市' },
+ '570': { 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: '快件已成功签收(含正常签收和退回签收)' },
+} as const;
+
+/**
+ * SF快递状态颜色映射表
+ * @description 将状态字符串映射到对应的UI颜色
+ */
+export const SF_STATUS_COLOR_MAP: Record = {
+ '待揽收': 'default',
+ '已揽收': 'blue',
+ '已收取': 'blue',
+ '分拣中': 'processing',
+ '运输中': 'processing',
+ '铁路运输': 'processing',
+ '到达网点': 'processing',
+ '长途运输': 'processing',
+ '途经中转': 'processing',
+ '准备派送': 'processing',
+ '派送中': 'processing',
+ '派送失败': 'warning',
+ '待派送': 'warning',
+ '退回申请中': 'warning',
+ '退回中': 'warning',
+ '已签收': 'success',
+} as const;
+
+/**
+ * SF快递状态图标映射表
+ * @description 将状态颜色映射到对应的emoji图标
+ */
+export const SF_STATUS_ICON_MAP: Record = {
+ 'success': '✅',
+ 'error': '❌',
+ 'warning': '⚠️',
+ 'processing': '🔄',
+ 'blue': '📦',
+ 'default': '⏳',
+} as const;
\ No newline at end of file