Quellcode durchsuchen

✨ feat(order): 为订单添加支付倒计时功能

- 在订单卡片和详情页添加支付倒计时显示
- 新增订单过期时间字段(expireTime)到订单实体和schema
- 创建订单超时自动取消调度器服务,定期检查并取消超时未支付订单
- 前端组件支持实时倒计时显示,包括订单卡片和详情页
- 添加倒计时相关样式和国际化支持
- 更新订单创建逻辑,自动设置24小时过期时间
- 添加完整的集成测试,覆盖超时订单检测、自动取消、库存恢复等功能
- 调度器支持多租户隔离和手动触发处理
- 优化订单类型定义,支持expireTime和cancelTime字段

📦 build(deps): 添加node-cron依赖并更新package.json

- 添加node-cron依赖用于定时任务调度
- 更新package.json依赖顺序,保持一致性
- 添加@types/node-cron类型定义

✅ test(order): 添加订单超时调度器集成测试

- 测试超时订单检测逻辑(24小时前创建、已支付、已发货等场景)
- 测试手动触发处理功能
- 测试调度器生命周期管理(启动、停止、健康检查)
- 测试租户数据隔离和多租户处理
- 测试商品库存恢复功能
- 验证订单状态变化和取消原因设置

♻️ refactor(order): 优化订单服务和测试代码

- 重构getOrderItemCount函数,移除不必要的参数
- 更新订单类型定义,支持泛型查询
- 优化测试数据工厂,使用正确的实体类型
- 修复测试中的类型错误和查询问题
- 改进订单取消逻辑,确保库存正确恢复
yourname vor 1 Monat
Ursprung
Commit
629a09f914

+ 69 - 7
mini/src/components/order/OrderCard/index.tsx

@@ -2,9 +2,13 @@ import { View, Text, Image } from '@tarojs/components'
 import { InferResponseType } from 'hono'
 import { InferResponseType } from 'hono'
 import { orderClient } from '@/api'
 import { orderClient } from '@/api'
 import OrderButtonBar from '../OrderButtonBar'
 import OrderButtonBar from '../OrderButtonBar'
+import { useState, useEffect, useRef } from 'react'
 
 
 type OrderResponse = InferResponseType<typeof orderClient.$get, 200>
 type OrderResponse = InferResponseType<typeof orderClient.$get, 200>
-type Order = OrderResponse['data'][0]
+type Order = OrderResponse['data'][0] & {
+  expireTime?: string | Date | null
+  cancelTime?: string | Date | null
+}
 
 
 interface OrderCardProps {
 interface OrderCardProps {
   order: Order
   order: Order
@@ -18,11 +22,11 @@ export default function OrderCard({ order, orderStatusMap, payStatusMap, onViewD
   const orderGoods = order.orderGoods || []
   const orderGoods = order.orderGoods || []
 
 
   // 计算订单商品数量
   // 计算订单商品数量
-  const getOrderItemCount = (order: Order) => {
+  const getOrderItemCount = () => {
     return orderGoods.reduce((sum: number, item: any) => sum + (item.num || 0), 0)
     return orderGoods.reduce((sum: number, item: any) => sum + (item.num || 0), 0)
   }
   }
 
 
-  const totalQuantity = getOrderItemCount(order)
+  const totalQuantity = getOrderItemCount()
 
 
   // 安全获取支付状态信息
   // 安全获取支付状态信息
   const getPayStatusInfo = (payState: number) => {
   const getPayStatusInfo = (payState: number) => {
@@ -39,6 +43,57 @@ export default function OrderCard({ order, orderStatusMap, payStatusMap, onViewD
   const payStatusInfo = getPayStatusInfo(order.payState)
   const payStatusInfo = getPayStatusInfo(order.payState)
   const orderStatusInfo = getOrderStatusInfo(order.state)
   const orderStatusInfo = getOrderStatusInfo(order.state)
 
 
+  // 倒计时逻辑 - 仅对未支付订单显示
+  const [countdown, setCountdown] = useState<string>('')
+  const timerRef = useRef<NodeJS.Timeout | null>(null)
+
+  useEffect(() => {
+    // 只有未支付订单且有过期时间时才显示倒计时
+    if (order.payState === 0 && order.expireTime) {
+      const updateCountdown = () => {
+        const now = new Date()
+        const expire = new Date(order.expireTime!)
+        const diff = expire.getTime() - now.getTime()
+
+        if (diff <= 0) {
+          setCountdown('已超时')
+          if (timerRef.current) {
+            clearInterval(timerRef.current)
+            timerRef.current = null
+          }
+          return
+        }
+
+        // 格式化为 HH:mm:ss
+        const hours = Math.floor(diff / (1000 * 60 * 60))
+        const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
+        const seconds = Math.floor((diff % (1000 * 60)) / 1000)
+
+        if (hours > 0) {
+          setCountdown(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`)
+        } else {
+          setCountdown(`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`)
+        }
+      }
+
+      // 立即更新一次
+      updateCountdown()
+
+      // 每秒更新
+      timerRef.current = setInterval(updateCountdown, 1000)
+
+      // 清理定时器
+      return () => {
+        if (timerRef.current) {
+          clearInterval(timerRef.current)
+          timerRef.current = null
+        }
+      }
+    } else {
+      setCountdown('')
+    }
+  }, [order.payState, order.expireTime])
+
   return (
   return (
     <View className="bg-white rounded-lg shadow-sm mb-4 overflow-hidden">
     <View className="bg-white rounded-lg shadow-sm mb-4 overflow-hidden">
       {/* 订单头部 */}
       {/* 订单头部 */}
@@ -46,10 +101,17 @@ export default function OrderCard({ order, orderStatusMap, payStatusMap, onViewD
         <View className="flex items-center">
         <View className="flex items-center">
           <Text className="text-sm text-gray-600">订单号: {order.orderNo}</Text>
           <Text className="text-sm text-gray-600">订单号: {order.orderNo}</Text>
         </View>
         </View>
-        <View
-          className={`px-2 py-1 rounded-full text-xs font-medium ${payStatusInfo.bgColor} ${payStatusInfo.color}`}
-        >
-          {payStatusInfo.text}
+        <View className="flex flex-col items-end">
+          <View
+            className={`px-2 py-1 rounded-full text-xs font-medium ${payStatusInfo.bgColor} ${payStatusInfo.color} mb-1`}
+          >
+            {payStatusInfo.text}
+          </View>
+          {countdown && (
+            <Text className="text-xs text-red-500 font-medium">
+              支付剩余: {countdown}
+            </Text>
+          )}
         </View>
         </View>
       </View>
       </View>
 
 

+ 34 - 0
mini/src/pages/order-detail/index.css

@@ -301,4 +301,38 @@
     width: 140rpx;
     width: 140rpx;
     height: 140rpx;
     height: 140rpx;
   }
   }
+}
+
+/* 订单支付倒计时样式 */
+.order-countdown {
+  margin-top: 16rpx;
+  padding: 12rpx 20rpx;
+  background: rgba(255, 255, 255, 0.2);
+  border-radius: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.order-countdown-text {
+  font-size: 24rpx;
+  font-weight: 500;
+  color: #fff;
+  letter-spacing: 1rpx;
+}
+
+.order-countdown-expired {
+  margin-top: 16rpx;
+  padding: 12rpx 20rpx;
+  background: rgba(255, 68, 68, 0.2);
+  border-radius: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.order-countdown-expired-text {
+  font-size: 24rpx;
+  font-weight: 500;
+  color: #ffdddd;
 }
 }

+ 77 - 3
mini/src/pages/order-detail/index.tsx

@@ -5,10 +5,18 @@ import { orderClient } from '@/api'
 import { Navbar } from '@/components/ui/navbar'
 import { Navbar } from '@/components/ui/navbar'
 import OrderButtonBar from '@/components/order/OrderButtonBar'
 import OrderButtonBar from '@/components/order/OrderButtonBar'
 import CancelReasonDialog from '@/components/common/CancelReasonDialog'
 import CancelReasonDialog from '@/components/common/CancelReasonDialog'
-import { useState } from 'react'
+import { useState, useEffect } from 'react'
 import dayjs from 'dayjs'
 import dayjs from 'dayjs'
+import { InferResponseType } from 'hono'
 import './index.css'
 import './index.css'
 
 
+type OrderByIdResponse = InferResponseType<typeof orderClient[':id'].$get, 200>
+type OrderListResponse = InferResponseType<typeof orderClient.$get, 200>
+type Order = (OrderByIdResponse | OrderListResponse['data'][0]) & {
+  expireTime?: string | Date | null
+  cancelTime?: string | Date | null
+}
+
 export default function OrderDetailPage() {
 export default function OrderDetailPage() {
   // 使用useRouter钩子获取路由参数
   // 使用useRouter钩子获取路由参数
   const router = useRouter()
   const router = useRouter()
@@ -20,8 +28,16 @@ export default function OrderDetailPage() {
   // 取消原因对话框状态
   // 取消原因对话框状态
   const [showCancelDialog, setShowCancelDialog] = useState(false)
   const [showCancelDialog, setShowCancelDialog] = useState(false)
 
 
+  // 订单支付倒计时
+  const [countdown, setCountdown] = useState<{
+    hours: number
+    minutes: number
+    seconds: number
+    expired: boolean
+  } | null>(null)
+
   // 通过订单ID或订单号查询订单详情
   // 通过订单ID或订单号查询订单详情
-  const { data: order, isLoading } = useQuery({
+  const { data: order, isLoading } = useQuery<Order>({
     queryKey: ['order', orderId, orderNo],
     queryKey: ['order', orderId, orderNo],
     queryFn: async () => {
     queryFn: async () => {
       // 如果提供了订单ID,直接通过ID查询
       // 如果提供了订单ID,直接通过ID查询
@@ -61,6 +77,45 @@ export default function OrderDetailPage() {
     staleTime: 5 * 60 * 1000,
     staleTime: 5 * 60 * 1000,
   })
   })
 
 
+  // 倒计时效果钩子
+  useEffect(() => {
+    if (!order || order.payState !== 0) {
+      setCountdown(null)
+      return
+    }
+
+    // 计算过期时间:优先使用expireTime,如果没有则使用createdAt + 24小时
+    const expireTime = order.expireTime
+      ? new Date(order.expireTime!)
+      : new Date(new Date(order.createdAt!).getTime() + 24 * 60 * 60 * 1000)
+
+    const updateCountdown = () => {
+      const now = new Date()
+      const diff = expireTime.getTime() - now.getTime()
+
+      if (diff <= 0) {
+        setCountdown({ hours: 0, minutes: 0, seconds: 0, expired: true })
+        return
+      }
+
+      const hours = Math.floor(diff / (1000 * 60 * 60))
+      const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
+      const seconds = Math.floor((diff % (1000 * 60)) / 1000)
+
+      setCountdown({ hours, minutes, seconds, expired: false })
+    }
+
+    // 立即更新一次
+    updateCountdown()
+
+    // 每秒更新一次
+    const intervalId = setInterval(updateCountdown, 1000)
+
+    return () => {
+      clearInterval(intervalId)
+    }
+  }, [order])
+
   // 使用Taro全局钩子 - 下拉刷新
   // 使用Taro全局钩子 - 下拉刷新
   usePullDownRefresh(() => {
   usePullDownRefresh(() => {
     queryClient.invalidateQueries({ queryKey: ['order', orderId, orderNo] })
     queryClient.invalidateQueries({ queryKey: ['order', orderId, orderNo] })
@@ -166,7 +221,7 @@ export default function OrderDetailPage() {
   }
   }
 
 
   // 使用orderGoods关联关系获取商品信息
   // 使用orderGoods关联关系获取商品信息
-  const orderGoods = (order as any)?.orderGoods || []
+  const orderGoods = order?.orderGoods || []
   //console.log("ordergoods:",orderGoods);
   //console.log("ordergoods:",orderGoods);
 
 
   if (isLoading) {
   if (isLoading) {
@@ -232,6 +287,25 @@ export default function OrderDetailPage() {
         <View className="order-status-header">
         <View className="order-status-header">
           <Text className="order-status-text">{getOrderStatusText()}</Text>
           <Text className="order-status-text">{getOrderStatusText()}</Text>
           <Text className="order-status-desc">{getOrderStatusDesc()}</Text>
           <Text className="order-status-desc">{getOrderStatusDesc()}</Text>
+
+          {/* 支付倒计时显示 */}
+          {order.payState === 0 && countdown && !countdown.expired && (
+            <View className="order-countdown">
+              <Text className="order-countdown-text">
+                剩余支付时间: {countdown.hours.toString().padStart(2, '0')}:
+                {countdown.minutes.toString().padStart(2, '0')}:
+                {countdown.seconds.toString().padStart(2, '0')}
+              </Text>
+            </View>
+          )}
+
+          {order.payState === 0 && countdown && countdown.expired && (
+            <View className="order-countdown-expired">
+              <Text className="order-countdown-expired-text">
+                订单已超时,请重新下单
+              </Text>
+            </View>
+          )}
         </View>
         </View>
 
 
         {/* 内容区域 */}
         {/* 内容区域 */}

+ 4 - 1
mini/src/pages/order-list/index.tsx

@@ -10,7 +10,10 @@ import OrderCard from '@/components/order/OrderCard'
 import './index.css'
 import './index.css'
 
 
 type OrderResponse = InferResponseType<typeof orderClient.$get, 200>
 type OrderResponse = InferResponseType<typeof orderClient.$get, 200>
-type Order = OrderResponse['data'][0]
+type Order = OrderResponse['data'][0] & {
+  expireTime?: string | Date | null
+  cancelTime?: string | Date | null
+}
 
 
 // 订单状态映射 - 根据tcb-shop-demo设计规范
 // 订单状态映射 - 根据tcb-shop-demo设计规范
 const orderStatusMap = {
 const orderStatusMap = {

+ 14 - 12
packages/orders-module-mt/package.json

@@ -51,31 +51,33 @@
     "typecheck": "tsc --noEmit"
     "typecheck": "tsc --noEmit"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@d8d/shared-types": "workspace:*",
-    "@d8d/shared-utils": "workspace:*",
-    "@d8d/shared-crud": "workspace:*",
     "@d8d/auth-module-mt": "workspace:*",
     "@d8d/auth-module-mt": "workspace:*",
-    "@d8d/user-module-mt": "workspace:*",
-    "@d8d/goods-module-mt": "workspace:*",
+    "@d8d/credit-balance-module-mt": "workspace:*",
     "@d8d/delivery-address-module-mt": "workspace:*",
     "@d8d/delivery-address-module-mt": "workspace:*",
-    "@d8d/merchant-module-mt": "workspace:*",
-    "@d8d/supplier-module-mt": "workspace:*",
     "@d8d/file-module-mt": "workspace:*",
     "@d8d/file-module-mt": "workspace:*",
     "@d8d/geo-areas-mt": "workspace:*",
     "@d8d/geo-areas-mt": "workspace:*",
+    "@d8d/goods-module-mt": "workspace:*",
+    "@d8d/merchant-module-mt": "workspace:*",
     "@d8d/mini-payment-mt": "workspace:*",
     "@d8d/mini-payment-mt": "workspace:*",
-    "@d8d/credit-balance-module-mt": "workspace:*",
+    "@d8d/shared-crud": "workspace:*",
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-utils": "workspace:*",
+    "@d8d/supplier-module-mt": "workspace:*",
+    "@d8d/user-module-mt": "workspace:*",
     "@hono/zod-openapi": "^1.0.2",
     "@hono/zod-openapi": "^1.0.2",
+    "node-cron": "^3.0.3",
     "typeorm": "^0.3.20",
     "typeorm": "^0.3.20",
     "zod": "^4.1.12"
     "zod": "^4.1.12"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@types/node": "^22.10.2",
-    "typescript": "^5.8.3",
-    "vitest": "^3.2.4",
     "@d8d/shared-test-util": "workspace:*",
     "@d8d/shared-test-util": "workspace:*",
+    "@types/node": "^22.10.2",
+    "@types/node-cron": "^3.0.11",
     "@typescript-eslint/eslint-plugin": "^8.18.1",
     "@typescript-eslint/eslint-plugin": "^8.18.1",
     "@typescript-eslint/parser": "^8.18.1",
     "@typescript-eslint/parser": "^8.18.1",
-    "eslint": "^9.17.0"
+    "eslint": "^9.17.0",
+    "typescript": "^5.8.3",
+    "vitest": "^3.2.4"
   },
   },
   "peerDependencies": {
   "peerDependencies": {
     "hono": "^4.8.5"
     "hono": "^4.8.5"

+ 3 - 0
packages/orders-module-mt/src/entities/order.mt.entity.ts

@@ -121,6 +121,9 @@ export class OrderMt {
   @Column({ name: 'cancel_time', type: 'timestamp', nullable: true, comment: '取消时间' })
   @Column({ name: 'cancel_time', type: 'timestamp', nullable: true, comment: '取消时间' })
   cancelTime!: Date | null;
   cancelTime!: Date | null;
 
 
+  @Column({ name: 'expire_time', type: 'timestamp', nullable: true, comment: '订单过期时间(超过此时间未支付自动取消)' })
+  expireTime!: Date | null;
+
   @Column({ name: 'remark', type: 'varchar', length: 255, nullable: true, comment: '管理员备注信息' })
   @Column({ name: 'remark', type: 'varchar', length: 255, nullable: true, comment: '管理员备注信息' })
   remark!: string | null;
   remark!: string | null;
 
 

+ 16 - 0
packages/orders-module-mt/src/schemas/order.mt.schema.ts

@@ -179,6 +179,14 @@ export const OrderSchema = z.object({
     description: '订单关闭时间',
     description: '订单关闭时间',
     example: '2024-01-01T12:00:00Z'
     example: '2024-01-01T12:00:00Z'
   }),
   }),
+  expireTime: z.coerce.date().nullable().optional().openapi({
+    description: '订单过期时间(超过此时间未支付自动取消)',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  cancelTime: z.coerce.date().nullable().optional().openapi({
+    description: '取消时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
   remark: z.string().max(255, '管理员备注信息最多255个字符').nullable().optional().openapi({
   remark: z.string().max(255, '管理员备注信息最多255个字符').nullable().optional().openapi({
     description: '管理员备注信息',
     description: '管理员备注信息',
     example: '请尽快发货'
     example: '请尽快发货'
@@ -404,6 +412,14 @@ export const CreateOrderDto = z.object({
     description: '订单关闭时间',
     description: '订单关闭时间',
     example: '2024-01-01T12:00:00Z'
     example: '2024-01-01T12:00:00Z'
   }),
   }),
+  expireTime: z.coerce.date().nullable().optional().openapi({
+    description: '订单过期时间(超过此时间未支付自动取消)',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  cancelTime: z.coerce.date().nullable().optional().openapi({
+    description: '取消时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
   remark: z.string().max(255, '管理员备注信息最多255个字符').nullable().optional().openapi({
   remark: z.string().max(255, '管理员备注信息最多255个字符').nullable().optional().openapi({
     description: '管理员备注信息',
     description: '管理员备注信息',
     example: '请尽快发货'
     example: '请尽快发货'

+ 2 - 1
packages/orders-module-mt/src/services/index.ts

@@ -2,4 +2,5 @@ export { OrderMtService } from './order.mt.service';
 export { OrderGoodsMtService } from './order-goods.mt.service';
 export { OrderGoodsMtService } from './order-goods.mt.service';
 export { OrderRefundMtService } from './order-refund.mt.service';
 export { OrderRefundMtService } from './order-refund.mt.service';
 export { UserOrderGoodsMtService } from './user-order-goods.mt.service';
 export { UserOrderGoodsMtService } from './user-order-goods.mt.service';
-export { UserRefundsMtService } from './user-refunds.mt.service';
+export { UserRefundsMtService } from './user-refunds.mt.service';
+export { OrderTimeoutSchedulerService } from './order-timeout-scheduler.service';

+ 246 - 0
packages/orders-module-mt/src/services/order-timeout-scheduler.service.ts

@@ -0,0 +1,246 @@
+import { DataSource, Repository } from 'typeorm';
+import * as cron from 'node-cron';
+import { OrderMt } from '../entities/order.mt.entity';
+import { OrderMtService } from './order.mt.service';
+import { PayStatus } from '../schemas/order.mt.schema';
+
+export class OrderTimeoutSchedulerService {
+  private isRunning: boolean = false;
+  private cronJob: cron.ScheduledTask | null = null;
+  private orderRepository: Repository<OrderMt>;
+  private orderMtService: OrderMtService;
+  private defaultCheckInterval: string = '*/5 * * * *'; // 每5分钟检查一次
+  private tenantId: number | null = null;
+
+  constructor(dataSource: DataSource, tenantId?: number) {
+    this.orderRepository = dataSource.getRepository(OrderMt);
+    this.orderMtService = new OrderMtService(dataSource);
+    this.tenantId = tenantId || null;
+  }
+
+  async start(): Promise<void> {
+    if (this.isRunning) {
+      throw new Error('订单超时调度器已经在运行中');
+    }
+
+    console.log('启动订单超时自动取消调度器...');
+
+    this.cronJob = cron.schedule(this.defaultCheckInterval, async () => {
+      await this.processTimeoutOrders();
+    });
+
+    this.isRunning = true;
+    console.log('订单超时自动取消调度器已启动');
+  }
+
+  async stop(): Promise<void> {
+    if (!this.isRunning) {
+      throw new Error('订单超时调度器未在运行中');
+    }
+
+    console.log('停止订单超时自动取消调度器...');
+
+    if (this.cronJob) {
+      this.cronJob.stop();
+      this.cronJob = null;
+    }
+
+    this.isRunning = false;
+    console.log('订单超时自动取消调度器已停止');
+  }
+
+  private async processTimeoutOrders(): Promise<void> {
+    try {
+      console.log('开始检查超时未支付订单...');
+
+      if (this.tenantId !== null) {
+        await this.processTenantTimeoutOrders(this.tenantId);
+      } else {
+        const tenantIds = await this.getTenantsWithTimeoutOrders();
+        if (tenantIds.length === 0) {
+          console.log('没有找到有超时订单的租户');
+          return;
+        }
+
+        console.log(`找到 ${tenantIds.length} 个有超时订单的租户`);
+        for (const tenantId of tenantIds) {
+          await this.processTenantTimeoutOrders(tenantId);
+        }
+      }
+
+      console.log('超时订单检查完成');
+    } catch (error) {
+      console.error('处理超时订单失败:', error);
+    }
+  }
+
+  private async processTenantTimeoutOrders(tenantId: number): Promise<void> {
+    try {
+      const timeoutOrders = await this.getTimeoutOrders(tenantId);
+      if (timeoutOrders.length === 0) {
+        console.log(`租户 ${tenantId} 没有超时未支付订单`);
+        return;
+      }
+
+      console.log(`租户 ${tenantId} 有 ${timeoutOrders.length} 个超时未支付订单`);
+      for (const order of timeoutOrders) {
+        await this.processSingleTimeoutOrder(tenantId, order);
+      }
+    } catch (error) {
+      console.error(`处理租户 ${tenantId} 的超时订单失败:`, error);
+    }
+  }
+
+  private async processSingleTimeoutOrder(tenantId: number, order: OrderMt): Promise<void> {
+    try {
+      console.log(`处理超时订单: 订单ID ${order.id}, 订单号 ${order.orderNo}`);
+
+      const currentOrder = await this.orderRepository.findOne({
+        where: { id: order.id, tenantId, payState: PayStatus.UNPAID }
+      });
+
+      if (!currentOrder) {
+        console.log(`订单 ${order.id} 状态已变更,跳过处理`);
+        return;
+      }
+
+      await this.orderMtService.cancelOrder(
+        tenantId,
+        order.id,
+        '订单超时未支付,系统自动取消',
+        order.userId
+      );
+
+      console.log(`订单 ${order.id} 已自动取消`);
+    } catch (error) {
+      console.error(`处理超时订单失败 订单ID ${order.id}:`, error);
+    }
+  }
+
+  private async getTimeoutOrders(tenantId: number): Promise<OrderMt[]> {
+    try {
+      const twentyFourHoursAgo = new Date();
+      twentyFourHoursAgo.setHours(twentyFourHoursAgo.getHours() - 24);
+
+      const query = this.orderRepository.createQueryBuilder('order')
+        .where('order.tenantId = :tenantId', { tenantId })
+        .andWhere('order.payState = :payState', { payState: PayStatus.UNPAID })
+        .andWhere('order.state = :state', { state: 0 })
+        .andWhere('(order.expireTime IS NOT NULL AND order.expireTime < CURRENT_TIMESTAMP) OR (order.expireTime IS NULL AND order.createdAt < :twentyFourHoursAgo)', { twentyFourHoursAgo })
+        .orderBy('order.createdAt', 'ASC')
+        .take(100);
+
+      return await query.getMany();
+    } catch (error) {
+      console.error(`获取租户 ${tenantId} 的超时订单失败:`, error);
+      return [];
+    }
+  }
+
+  private async getTenantsWithTimeoutOrders(): Promise<number[]> {
+    try {
+      const twentyFourHoursAgo = new Date();
+      twentyFourHoursAgo.setHours(twentyFourHoursAgo.getHours() - 24);
+
+      const query = `
+        SELECT DISTINCT tenant_id
+        FROM orders_mt
+        WHERE pay_state = $1
+          AND state = $2
+          AND ((expire_time IS NOT NULL AND expire_time < CURRENT_TIMESTAMP) OR (expire_time IS NULL AND created_at < $3))
+        ORDER BY tenant_id
+      `;
+
+      const result = await this.orderRepository.query(query, [
+        PayStatus.UNPAID,
+        0,
+        twentyFourHoursAgo
+      ]);
+
+      return result.map((row: any) => row.tenant_id);
+    } catch (error) {
+      console.error('获取有超时订单的租户失败:', error);
+      return [];
+    }
+  }
+
+  async triggerManualProcess(tenantId?: number): Promise<{
+    success: boolean;
+    processedOrders: number;
+    message: string;
+  }> {
+    try {
+      const targetTenantId = tenantId !== undefined ? tenantId : this.tenantId;
+      if (targetTenantId === null) {
+        return {
+          success: false,
+          processedOrders: 0,
+          message: '未指定租户ID,无法手动处理超时订单'
+        };
+      }
+
+      const timeoutOrders = await this.getTimeoutOrders(targetTenantId);
+      let processedCount = 0;
+
+      for (const order of timeoutOrders) {
+        try {
+          await this.orderMtService.cancelOrder(
+            targetTenantId,
+            order.id,
+            '订单超时未支付,系统自动取消',
+            order.userId
+          );
+          processedCount++;
+        } catch (error) {
+          console.error(`手动处理订单失败 ${order.id}:`, error);
+        }
+      }
+
+      return {
+        success: true,
+        processedOrders: processedCount,
+        message: `成功处理 ${processedCount} 个超时订单`
+      };
+    } catch (error) {
+      return {
+        success: false,
+        processedOrders: 0,
+        message: `手动处理失败: ${error instanceof Error ? error.message : '未知错误'}`
+      };
+    }
+  }
+
+  async healthCheck(): Promise<{
+    healthy: boolean;
+    isRunning: boolean;
+    lastError?: string;
+    timestamp: Date;
+  }> {
+    try {
+      return {
+        healthy: this.isRunning,
+        isRunning: this.isRunning,
+        timestamp: new Date()
+      };
+    } catch (error) {
+      return {
+        healthy: false,
+        isRunning: this.isRunning,
+        lastError: error instanceof Error ? error.message : '健康检查失败',
+        timestamp: new Date()
+      };
+    }
+  }
+
+  getStatus(): {
+    isRunning: boolean;
+    checkInterval: string;
+    tenantId: number | null;
+  } {
+    return {
+      isRunning: this.isRunning,
+      checkInterval: this.defaultCheckInterval,
+      tenantId: this.tenantId
+    };
+  }
+}

+ 330 - 0
packages/orders-module-mt/src/services/order-timeout-scheduler.service.ts.backup

@@ -0,0 +1,330 @@
+import { DataSource, Repository } from 'typeorm';
+import * as cron from 'node-cron';
+import { OrderMt } from '../entities/order.mt.entity';
+import { OrderMtService } from './order.mt.service';
+import { PayStatus } from '../schemas/order.mt.schema';
+
+export class OrderTimeoutSchedulerService {
+  private isRunning: boolean = false;
+  private cronJob: cron.ScheduledTask | null = null;
+  private orderRepository: Repository<OrderMt>;
+  private orderMtService: OrderMtService;
+  private defaultCheckInterval: string = '*/5 * * * *'; // 每5分钟检查一次
+  private tenantId: number | null = null; // 租户ID,null表示处理所有租户
+
+  constructor(dataSource: DataSource, tenantId?: number) {
+    this.orderRepository = dataSource.getRepository(OrderMt);
+    this.orderMtService = new OrderMtService(dataSource);
+    this.tenantId = tenantId || null;
+  }
+
+  /**
+   * 启动调度器
+   */
+  async start(): Promise<void> {
+    if (this.isRunning) {
+      throw new Error('订单超时调度器已经在运行中');
+    }
+
+    console.log('启动订单超时自动取消调度器...');
+
+    // 使用配置的检查间隔
+    this.cronJob = cron.schedule(this.defaultCheckInterval, async () => {
+      await this.processTimeoutOrders();
+    });
+
+    this.isRunning = true;
+    console.log('订单超时自动取消调度器已启动');
+  }
+
+  /**
+   * 停止调度器
+   */
+  async stop(): Promise<void> {
+    if (!this.isRunning) {
+      throw new Error('订单超时调度器未在运行中');
+    }
+
+    console.log('停止订单超时自动取消调度器...');
+
+    if (this.cronJob) {
+      this.cronJob.stop();
+      this.cronJob = null;
+    }
+
+    this.isRunning = false;
+    console.log('订单超时自动取消调度器已停止');
+  }
+
+  /**
+   * 处理超时订单
+   */
+  private async processTimeoutOrders(): Promise<void> {
+    try {
+      console.log('开始检查超时未支付订单...');
+
+      if (this.tenantId !== null) {
+        // 只处理指定租户的超时订单
+        console.log(`处理租户 ${this.tenantId} 的超时订单`);
+        await this.processTenantTimeoutOrders(this.tenantId);
+      } else {
+        // 处理所有租户的超时订单
+        const tenantIds = await this.getTenantsWithTimeoutOrders();
+
+        if (tenantIds.length === 0) {
+          console.log('没有找到有超时订单的租户');
+          return;
+        }
+
+        console.log(`找到 ${tenantIds.length} 个有超时订单的租户`);
+
+        // 处理每个租户的超时订单
+        for (const tenantId of tenantIds) {
+          await this.processTenantTimeoutOrders(tenantId);
+        }
+      }
+
+      console.log('超时订单检查完成');
+    } catch (error) {
+      console.error('处理超时订单失败:', error);
+    }
+  }
+
+  /**
+   * 处理指定租户的超时订单
+   */
+  private async processTenantTimeoutOrders(tenantId: number): Promise<void> {
+    try {
+      // 获取超时未支付的订单
+      const timeoutOrders = await this.getTimeoutOrders(tenantId);
+
+      if (timeoutOrders.length === 0) {
+        console.log(`租户 ${tenantId} 没有超时未支付订单`);
+        return;
+      }
+
+      console.log(`租户 ${tenantId} 有 ${timeoutOrders.length} 个超时未支付订单`);
+
+      // 处理每个超时订单
+      for (const order of timeoutOrders) {
+        await this.processSingleTimeoutOrder(tenantId, order);
+      }
+    } catch (error) {
+      console.error(`处理租户 ${tenantId} 的超时订单失败:`, error);
+    }
+  }
+
+  /**
+   * 处理单个超时订单
+   */
+  private async processSingleTimeoutOrder(tenantId: number, order: OrderMt): Promise<void> {
+    try {
+      console.log(`处理超时订单: 订单ID ${order.id}, 订单号 ${order.orderNo}`);
+
+      // 双重检查订单状态(防止并发处理)
+      const currentOrder = await this.orderRepository.findOne({
+        where: { id: order.id, tenantId, payState: PayStatus.UNPAID }
+      });
+
+      if (!currentOrder) {
+        console.log(`订单 ${order.id} 状态已变更,跳过处理`);
+        return;
+      }
+
+      // 自动取消订单,原因为"超时未支付"
+      await this.orderMtService.cancelOrder(
+        tenantId,
+        order.id,
+        '订单超时未支付,系统自动取消',
+        order.userId
+      );
+
+      console.log(`订单 ${order.id} 已自动取消`);
+    } catch (error) {
+      console.error(`处理超时订单失败 订单ID ${order.id}:`, error);
+    }
+  }
+
+  /**
+   * 获取超时未支付的订单
+   * 条件:payState = 0(未支付)且 createdAt < 当前时间-24小时
+   * 或者 expireTime < 当前时间(如果设置了过期时间)
+   */
+  private async getTimeoutOrders(tenantId: number): Promise<OrderMt[]> {
+    try {
+      // 计算24小时前的时间
+      const twentyFourHoursAgo = new Date();
+      twentyFourHoursAgo.setHours(twentyFourHoursAgo.getHours() - 24);
+
+      // 构建查询条件
+      const query = this.orderRepository.createQueryBuilder('order')
+        .where('order.tenantId = :tenantId', { tenantId })
+        .andWhere('order.payState = :payState', { payState: PayStatus.UNPAID })
+        .andWhere('order.state = :state', { state: 0 }) // 未发货状态
+        // 检查是否超过24小时未支付
+        .andWhere('order.createdAt < :twentyFourHoursAgo', { twentyFourHoursAgo })
+        // 同时检查expireTime(如果设置了)
+        .andWhere('(order.expireTime IS NULL OR order.expireTime < CURRENT_TIMESTAMP)')
+        .orderBy('order.createdAt', 'ASC') // 按创建时间升序,先处理最早创建的
+        .take(100); // 每次最多处理100个订单,避免一次处理太多
+
+      return await query.getMany();
+    } catch (error) {
+      console.error(`获取租户 ${tenantId} 的超时订单失败:`, error);
+      return [];
+    }
+  }
+
+  /**
+   * 获取有超时订单的租户ID列表
+   */
+  private async getTenantsWithTimeoutOrders(): Promise<number[]> {
+    try {
+      // 计算24小时前的时间
+      const twentyFourHoursAgo = new Date();
+      twentyFourHoursAgo.setHours(twentyFourHoursAgo.getHours() - 24);
+
+      const query = `
+        SELECT DISTINCT tenant_id
+        FROM orders_mt
+        WHERE pay_state = $1
+          AND state = $2
+          AND created_at < $3
+          AND (expire_time IS NULL OR expire_time < CURRENT_TIMESTAMP)
+        ORDER BY tenant_id
+      `;
+
+      const result = await this.orderRepository.query(query, [
+        PayStatus.UNPAID,
+        0, // 未发货状态
+        twentyFourHoursAgo
+      ]);
+
+      return result.map((row: any) => row.tenant_id);
+    } catch (error) {
+      console.error('获取有超时订单的租户失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 设置检查间隔
+   * @param interval cron表达式,例如 '*/5 * * * *' 表示每5分钟
+   */
+  setCheckInterval(interval: string): void {
+    if (!cron.validate(interval)) {
+      throw new Error('无效的cron表达式');
+    }
+
+    this.defaultCheckInterval = interval;
+    console.log(`订单超时检查间隔已设置为: ${interval}`);
+
+    // 如果调度器正在运行,需要重启以应用新间隔
+    if (this.isRunning && this.cronJob) {
+      console.log('重新启动调度器以应用新间隔...');
+      this.cronJob.stop();
+      this.cronJob = cron.schedule(this.defaultCheckInterval, async () => {
+        await this.processTimeoutOrders();
+      });
+    }
+  }
+
+  /**
+   * 获取检查间隔
+   */
+  getCheckInterval(): string {
+    return this.defaultCheckInterval;
+  }
+
+  /**
+   * 手动触发超时订单处理
+   */
+  async triggerManualProcess(tenantId?: number): Promise<{
+    success: boolean;
+    processedOrders: number;
+    message: string;
+  }> {
+    try {
+      // 使用指定的tenantId或调度器的tenantId
+      const targetTenantId = tenantId !== undefined ? tenantId : this.tenantId;
+
+      if (targetTenantId === null) {
+        return {
+          success: false,
+          processedOrders: 0,
+          message: '未指定租户ID,无法手动处理超时订单'
+        };
+      }
+
+      const timeoutOrders = await this.getTimeoutOrders(targetTenantId);
+      let processedCount = 0;
+
+      for (const order of timeoutOrders) {
+        try {
+          await this.orderMtService.cancelOrder(
+            targetTenantId,
+            order.id,
+            '订单超时未支付,系统自动取消',
+            order.userId
+          );
+          processedCount++;
+        } catch (error) {
+          console.error(`手动处理订单失败 ${order.id}:`, error);
+        }
+      }
+
+      return {
+        success: true,
+        processedOrders: processedCount,
+        message: `成功处理 ${processedCount} 个超时订单`
+      };
+    } catch (error) {
+      return {
+        success: false,
+        processedOrders: 0,
+        message: `手动处理失败: ${error instanceof Error ? error.message : '未知错误'}`
+      };
+    }
+  }
+
+  /**
+   * 健康检查
+   */
+  async healthCheck(): Promise<{
+    healthy: boolean;
+    isRunning: boolean;
+    lastError?: string;
+    timestamp: Date;
+  }> {
+    try {
+      // 简单的健康检查:检查调度器是否在运行
+      return {
+        healthy: this.isRunning,
+        isRunning: this.isRunning,
+        timestamp: new Date()
+      };
+    } catch (error) {
+      return {
+        healthy: false,
+        isRunning: this.isRunning,
+        lastError: error instanceof Error ? error.message : '健康检查失败',
+        timestamp: new Date()
+      };
+    }
+  }
+
+  /**
+   * 检查调度器状态
+   */
+  getStatus(): {
+    isRunning: boolean;
+    checkInterval: string;
+    tenantId: number | null;
+  } {
+    return {
+      isRunning: this.isRunning,
+      checkInterval: this.defaultCheckInterval,
+      tenantId: this.tenantId
+    };
+  }
+}

+ 6 - 1
packages/orders-module-mt/src/services/order.mt.service.ts

@@ -117,6 +117,10 @@ export class OrderMtService extends GenericCrudService<OrderMt> {
       const orderNo = this.generateOrderNo();
       const orderNo = this.generateOrderNo();
 
 
       // 创建订单
       // 创建订单
+      // 计算订单过期时间(24小时后)
+      const expireTime = new Date();
+      expireTime.setHours(expireTime.getHours() + 24);
+
       const order = this.repository.create({
       const order = this.repository.create({
         tenantId,
         tenantId,
         orderNo,
         orderNo,
@@ -134,6 +138,7 @@ export class OrderMtService extends GenericCrudService<OrderMt> {
         remark: remark || '',
         remark: remark || '',
         createdBy: userId,
         createdBy: userId,
         updatedBy: userId,
         updatedBy: userId,
+        expireTime, // 设置订单过期时间
         // 设置收货地址信息(如果提供)
         // 设置收货地址信息(如果提供)
         ...(deliveryAddress && {
         ...(deliveryAddress && {
           receiverMobile: deliveryAddress.phone,
           receiverMobile: deliveryAddress.phone,
@@ -517,7 +522,7 @@ export class OrderMtService extends GenericCrudService<OrderMt> {
       }
       }
 
 
       const hasWechatTransactionId = !!paymentRecord.wechatTransactionId;
       const hasWechatTransactionId = !!paymentRecord.wechatTransactionId;
-      let paymentStatusDetail = paymentRecord.paymentStatus || '未知状态';
+      let paymentStatusDetail: string = paymentRecord.paymentStatus || '未知状态';
 
 
       // 添加详细的支付状态说明
       // 添加详细的支付状态说明
       if (!hasWechatTransactionId) {
       if (!hasWechatTransactionId) {

+ 9 - 9
packages/orders-module-mt/tests/factories/orders-test-factory.ts

@@ -308,14 +308,14 @@ export class OrdersTestFactory {
     };
     };
 
 
     // 按依赖关系顺序删除数据
     // 按依赖关系顺序删除数据
-    await repositories.orderRefund.delete({});
-    await repositories.orderGoods.delete({});
-    await repositories.order.delete({});
-    await repositories.goods.delete({});
-    await repositories.deliveryAddress.delete({});
-    await repositories.supplier.delete({});
-    await repositories.merchant.delete({});
-    await repositories.user.delete({});
-    await repositories.area.delete({});
+    await repositories.orderRefund.createQueryBuilder().delete().execute();
+    await repositories.orderGoods.createQueryBuilder().delete().execute();
+    await repositories.order.createQueryBuilder().delete().execute();
+    await repositories.goods.createQueryBuilder().delete().execute();
+    await repositories.deliveryAddress.createQueryBuilder().delete().execute();
+    await repositories.supplier.createQueryBuilder().delete().execute();
+    await repositories.merchant.createQueryBuilder().delete().execute();
+    await repositories.user.createQueryBuilder().delete().execute();
+    await repositories.area.createQueryBuilder().delete().execute();
   }
   }
 }
 }

+ 479 - 0
packages/orders-module-mt/tests/integration/order-timeout-scheduler.integration.test.ts

@@ -0,0 +1,479 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { DataSource } from 'typeorm';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
+import { DeliveryAddressMt } from '@d8d/delivery-address-module-mt';
+import { AreaEntityMt } from '@d8d/geo-areas-mt';
+import { MerchantMt } from '@d8d/merchant-module-mt';
+import { SupplierMt } from '@d8d/supplier-module-mt';
+import { FileMt } from '@d8d/file-module-mt';
+import { GoodsMt, GoodsCategoryMt } from '@d8d/goods-module-mt';
+import { OrderMt, OrderGoodsMt, OrderRefundMt } from '../../src/entities';
+import { OrderTimeoutSchedulerService } from '../../src/services/order-timeout-scheduler.service';
+import { OrdersTestFactory } from '../factories/orders-test-factory';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([
+  UserEntityMt, RoleMt, OrderMt, OrderGoodsMt, OrderRefundMt, DeliveryAddressMt, MerchantMt, SupplierMt, FileMt, AreaEntityMt, GoodsMt, GoodsCategoryMt
+])
+
+describe('订单超时调度器集成测试', () => {
+  let dataSource: DataSource;
+  let schedulerService: OrderTimeoutSchedulerService;
+  let testFactory: OrdersTestFactory;
+  let testUser: UserEntityMt;
+
+  beforeEach(async () => {
+    // 获取数据源
+    dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试工厂
+    testFactory = new OrdersTestFactory(dataSource);
+
+    // 创建测试用户
+    testUser = await testFactory.createTestUser(1);
+
+    // 创建调度器服务(特定租户)
+    schedulerService = new OrderTimeoutSchedulerService(dataSource, 1);
+  });
+
+  afterEach(async () => {
+    // 清理测试数据
+    await testFactory.cleanup();
+  });
+
+  describe('超时订单检测逻辑', () => {
+    it('应该正确识别24小时前创建的未支付订单为超时订单', async () => {
+      // 创建一个未支付订单
+      const testOrder = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        payState: 0, // 未支付
+        state: 0,    // 未发货
+        expireTime: null // 无过期时间
+      });
+
+      // 更新订单创建时间为25小时前
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+      const orderRepository = dataSource.getRepository(OrderMt);
+      await orderRepository.update(
+        { id: testOrder.id, tenantId: 1 },
+        { createdAt: twentyFiveHoursAgo }
+      );
+
+      // 手动触发处理以检查是否能检测到超时订单
+      const result = await schedulerService.triggerManualProcess(1);
+
+      // 验证处理结果
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBeGreaterThan(0);
+      expect(result.message).toContain('成功处理');
+
+      // 验证订单状态已更新为关闭
+      const updatedOrder = await orderRepository.findOne({
+        where: { id: testOrder.id, tenantId: 1 }
+      });
+
+      expect(updatedOrder).toBeDefined();
+      expect(updatedOrder!.payState).toBe(5); // 订单关闭
+      expect(updatedOrder!.cancelReason).toBe('订单超时未支付,系统自动取消');
+      expect(updatedOrder!.cancelTime).toBeDefined();
+    });
+
+    it('不应该识别23小时前创建的未支付订单为超时订单', async () => {
+      // 创建一个23小时前创建的未支付订单(不到24小时)
+      const twentyThreeHoursAgo = new Date();
+      twentyThreeHoursAgo.setHours(twentyThreeHoursAgo.getHours() - 23);
+
+      await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        createdAt: twentyThreeHoursAgo,
+        payState: 0, // 未支付
+        state: 0     // 未发货
+      });
+
+      // 手动触发处理
+      const result = await schedulerService.triggerManualProcess(1);
+
+      // 验证没有处理任何订单
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(0);
+    });
+
+    it('不应该识别已支付的订单为超时订单', async () => {
+      // 创建一个25小时前创建的已支付订单
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const testOrder = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        createdAt: twentyFiveHoursAgo,
+        payState: 2, // 已支付
+        state: 0     // 未发货
+      });
+
+      // 手动触发处理
+      const result = await schedulerService.triggerManualProcess(1);
+
+      // 验证没有处理已支付订单
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(0);
+
+      // 验证订单状态未改变
+      const orderRepository = dataSource.getRepository(OrderMt);
+      const updatedOrder = await orderRepository.findOne({
+        where: { id: testOrder.id, tenantId: 1 }
+      });
+
+      expect(updatedOrder).toBeDefined();
+      expect(updatedOrder!.payState).toBe(2); // 保持已支付状态
+    });
+
+    it('不应该识别已发货的订单为超时订单', async () => {
+      // 创建一个25小时前创建的未支付但已发货的订单
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const testOrder = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0, // 未支付
+        state: 1     // 已发货
+      });
+
+      // 手动触发处理
+      const result = await schedulerService.triggerManualProcess(1);
+
+      // 验证没有处理已发货订单
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(0);
+    });
+
+    it('应该考虑订单过期时间(expireTime)', async () => {
+      // 创建一个25小时前创建的订单,已设置过期时间为10小时前
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const tenHoursAgo = new Date();
+      tenHoursAgo.setHours(tenHoursAgo.getHours() - 10);
+
+      const testOrder = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0, // 未支付
+        state: 0,    // 未发货
+        expireTime: tenHoursAgo // 已过期
+      });
+
+      // 手动触发处理
+      const result = await schedulerService.triggerManualProcess(1);
+
+      // 验证处理了过期订单(创建时间超过24小时且已过期)
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(1);
+    });
+
+    it('不应该处理未来过期时间的订单', async () => {
+      // 创建一个25小时前创建的订单,但过期时间在未来
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const futureTime = new Date();
+      futureTime.setHours(futureTime.getHours() + 1); // 1小时后过期
+
+      await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0, // 未支付
+        state: 0,    // 未发货
+        expireTime: futureTime // 未来才过期
+      });
+
+      // 手动触发处理
+      const result = await schedulerService.triggerManualProcess(1);
+
+      // 验证没有处理(因为过期时间还没到)
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(0);
+    });
+  });
+
+  describe('手动触发处理功能', () => {
+    it('应该能够手动触发处理指定租户的超时订单', async () => {
+      // 为租户1创建超时订单
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const order1 = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0,
+        state: 0
+      });
+
+      // 手动触发处理租户1
+      const result = await schedulerService.triggerManualProcess(1);
+
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(1);
+      expect(result.message).toContain('成功处理 1 个超时订单');
+    });
+
+    it('应该正确处理多个超时订单', async () => {
+      // 创建多个超时订单
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      // 创建3个超时订单
+      for (let i = 0; i < 3; i++) {
+        await testFactory.createTestOrder(testUser.id, {
+          tenantId: 1,
+          createdAt: twentyFiveHoursAgo,
+          payState: 0,
+          state: 0
+        });
+      }
+
+      // 手动触发处理
+      const result = await schedulerService.triggerManualProcess(1);
+
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(3);
+    });
+
+    it('应该处理订单状态在检查期间发生变化的情况', async () => {
+      // 创建一个超时订单
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const testOrder = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0,
+        state: 0
+      });
+
+      // 在手动处理前,先手动支付订单
+      const orderRepository = dataSource.getRepository(OrderMt);
+      await orderRepository.update(
+        { id: testOrder.id, tenantId: 1 },
+        { payState: 2, updatedAt: new Date() }
+      );
+
+      // 手动触发处理
+      const result = await schedulerService.triggerManualProcess(1);
+
+      // 验证没有处理已支付的订单
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(0);
+
+      // 验证订单状态仍然是已支付
+      const updatedOrder = await orderRepository.findOne({
+        where: { id: testOrder.id, tenantId: 1 }
+      });
+      expect(updatedOrder!.payState).toBe(2);
+    });
+
+    it('应该恢复超时订单的商品库存', async () => {
+      // 创建测试商品
+      const testGoods = await testFactory.createTestGoods(testUser.id, {
+        tenantId: 1,
+        stock: 10
+      });
+
+      // 创建超时订单
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const testOrder = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0,
+        state: 0
+      });
+
+      // 创建订单商品(购买2个)
+      await testFactory.createTestOrderGoods(testOrder.id, testGoods.id, {
+        tenantId: 1,
+        num: 2
+      });
+
+      // 手动触发处理
+      const result = await schedulerService.triggerManualProcess(1);
+
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(1);
+
+      // 验证商品库存已恢复
+      const goodsRepository = dataSource.getRepository(GoodsMt);
+      const updatedGoods = await goodsRepository.findOne({
+        where: { id: testGoods.id, tenantId: 1 }
+      });
+
+      expect(Number(updatedGoods!.stock)).toBe(12); // 原库存10 + 退回2 = 12
+    });
+
+    it('处理失败时应该返回错误信息', async () => {
+      // 创建未指定租户的调度器(tenantId为null)
+      const invalidScheduler = new OrderTimeoutSchedulerService(dataSource);
+
+      // 尝试处理但不指定租户ID
+      const result = await invalidScheduler.triggerManualProcess();
+
+      expect(result.success).toBe(false);
+      expect(result.processedOrders).toBe(0);
+      expect(result.message).toContain('未指定租户ID');
+    });
+  });
+
+  describe('调度器生命周期管理', () => {
+    it('应该能够成功启动和停止调度器', async () => {
+      // 启动调度器
+      await schedulerService.start();
+
+      // 验证调度器状态
+      const status = schedulerService.getStatus();
+      expect(status.isRunning).toBe(true);
+      expect(status.tenantId).toBe(1);
+      expect(status.checkInterval).toBe('*/5 * * * *'); // 默认检查间隔
+
+      // 停止调度器
+      await schedulerService.stop();
+
+      // 验证调度器已停止
+      const stoppedStatus = schedulerService.getStatus();
+      expect(stoppedStatus.isRunning).toBe(false);
+    });
+
+    it('重复启动调度器应该抛出错误', async () => {
+      // 第一次启动
+      await schedulerService.start();
+
+      // 第二次启动应该失败
+      await expect(schedulerService.start()).rejects.toThrow('订单超时调度器已经在运行中');
+
+      // 清理:停止调度器
+      await schedulerService.stop();
+    });
+
+    it('停止未运行的调度器应该抛出错误', async () => {
+      await expect(schedulerService.stop()).rejects.toThrow('订单超时调度器未在运行中');
+    });
+
+    it('健康检查应该返回正确的状态', async () => {
+      // 调度器未启动时的健康检查
+      let health = await schedulerService.healthCheck();
+      expect(health.healthy).toBe(false);
+      expect(health.isRunning).toBe(false);
+      expect(health.timestamp).toBeInstanceOf(Date);
+
+      // 启动调度器后的健康检查
+      await schedulerService.start();
+      health = await schedulerService.healthCheck();
+      expect(health.healthy).toBe(true);
+      expect(health.isRunning).toBe(true);
+
+      // 停止调度器
+      await schedulerService.stop();
+    });
+  });
+
+  describe('租户数据隔离', () => {
+    it('应该只处理指定租户的超时订单', async () => {
+      // 创建租户1的用户和订单
+      const tenant1User = await testFactory.createTestUser(1);
+      const tenant2User = await testFactory.createTestUser(2);
+
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      // 租户1的超时订单
+      await testFactory.createTestOrder(tenant1User.id, {
+        tenantId: 1,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0,
+        state: 0
+      });
+
+      // 租户2的超时订单
+      await testFactory.createTestOrder(tenant2User.id, {
+        tenantId: 2,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0,
+        state: 0
+      });
+
+      // 创建租户1的调度器
+      const tenant1Scheduler = new OrderTimeoutSchedulerService(dataSource, 1);
+
+      // 处理租户1
+      const result = await tenant1Scheduler.triggerManualProcess(1);
+
+      // 验证只处理了租户1的订单
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(1);
+
+      // 验证租户2的订单未被处理
+      const orderRepository = dataSource.getRepository(OrderMt);
+      const tenant2Orders = await orderRepository.find({
+        where: { tenantId: 2, payState: 0 }
+      });
+
+      expect(tenant2Orders.length).toBe(1); // 租户2的订单仍然是未支付状态
+    });
+
+    it('应该能够处理多个租户的超时订单', async () => {
+      // 创建多个租户的用户和订单
+      const tenant1User = await testFactory.createTestUser(1);
+      const tenant2User = await testFactory.createTestUser(2);
+      const tenant3User = await testFactory.createTestUser(3);
+
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      // 每个租户创建一个超时订单
+      await testFactory.createTestOrder(tenant1User.id, {
+        tenantId: 1,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0,
+        state: 0
+      });
+
+      await testFactory.createTestOrder(tenant2User.id, {
+        tenantId: 2,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0,
+        state: 0
+      });
+
+      await testFactory.createTestOrder(tenant3User.id, {
+        tenantId: 3,
+        createdAt: twentyFiveHoursAgo,
+        payState: 0,
+        state: 0
+      });
+
+      // 创建未指定租户的调度器(可以处理多个租户)
+      const globalScheduler = new OrderTimeoutSchedulerService(dataSource);
+
+      // 为每个租户单独处理
+      const result1 = await globalScheduler.triggerManualProcess(1);
+      const result2 = await globalScheduler.triggerManualProcess(2);
+      const result3 = await globalScheduler.triggerManualProcess(3);
+
+      // 验证每个租户都处理成功
+      expect(result1.success).toBe(true);
+      expect(result2.success).toBe(true);
+      expect(result3.success).toBe(true);
+      expect(result1.processedOrders + result2.processedOrders + result3.processedOrders).toBe(3);
+
+      // 验证所有订单都被关闭
+      const orderRepository = dataSource.getRepository(OrderMt);
+      const openOrders = await orderRepository.find({
+        where: { payState: 0 }
+      });
+
+      expect(openOrders.length).toBe(0); // 所有未支付订单都应被关闭
+    });
+  });
+});

+ 313 - 2
packages/orders-module-mt/tests/integration/user-orders-routes.integration.test.ts

@@ -184,7 +184,7 @@ describe('多租户用户订单管理API集成测试', () => {
         expect(Array.isArray(orderDetail.orderGoods)).toBe(true);
         expect(Array.isArray(orderDetail.orderGoods)).toBe(true);
         expect(orderDetail.orderGoods).toHaveLength(1);
         expect(orderDetail.orderGoods).toHaveLength(1);
 
 
-        const goods = orderDetail.orderGoods[0];
+        const goods = orderDetail.orderGoods![0];
         expect(goods.id).toBe(orderGoods.id);
         expect(goods.id).toBe(orderGoods.id);
         expect(goods.goodsId).toBe(orderGoods.goodsId);
         expect(goods.goodsId).toBe(orderGoods.goodsId);
         expect(goods.goodsName).toBe(orderGoods.goodsName);
         expect(goods.goodsName).toBe(orderGoods.goodsName);
@@ -218,7 +218,7 @@ describe('多租户用户订单管理API集成测试', () => {
         expect(Array.isArray(data.data[0].orderGoods)).toBe(true);
         expect(Array.isArray(data.data[0].orderGoods)).toBe(true);
         expect(data.data[0].orderGoods).toHaveLength(1);
         expect(data.data[0].orderGoods).toHaveLength(1);
 
 
-        const goods = data.data[0].orderGoods[0];
+        const goods = data.data[0].orderGoods![0];
         expect(goods.goodsId).toBeGreaterThan(0);
         expect(goods.goodsId).toBeGreaterThan(0);
         expect(goods.goodsName).toBeDefined();
         expect(goods.goodsName).toBeDefined();
         expect(goods.price).toBeGreaterThan(0);
         expect(goods.price).toBeGreaterThan(0);
@@ -581,4 +581,315 @@ describe('多租户用户订单管理API集成测试', () => {
       expect(result.message).toBe('订单不存在');
       expect(result.message).toBe('订单不存在');
     });
     });
   });
   });
+
+  describe('订单超时自动取消功能验证', () => {
+    it('应该自动取消超过24小时未支付的订单', async () => {
+      // 创建未支付订单,设置创建时间为25小时前
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const order = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        payState: 0, // 未支付
+        state: 0,
+        createdAt: twentyFiveHoursAgo,
+        updatedAt: twentyFiveHoursAgo,
+        expireTime: new Date() // 设置为当前时间,表示已过期
+      });
+
+      console.debug(`创建的超时订单: ID=${order.id}, createdAt=${order.createdAt}, expireTime=${order.expireTime}`);
+
+      // 验证订单初始状态
+      expect(order.payState).toBe(0);
+      expect(order.state).toBe(0);
+
+      // 创建调度器服务并手动触发超时订单处理
+      const { OrderTimeoutSchedulerService } = await import('../../src/services/order-timeout-scheduler.service');
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const schedulerService = new OrderTimeoutSchedulerService(dataSource, 1);
+
+      const result = await schedulerService.triggerManualProcess(1);
+      console.debug(`手动处理结果:`, result);
+
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(1);
+      expect(result.message).toContain('成功处理 1 个超时订单');
+
+      // 验证订单状态已更新为取消
+      const updatedOrder = await dataSource.getRepository(OrderMt).findOne({
+        where: { id: order.id, tenantId: 1 }
+      });
+
+      expect(updatedOrder).toBeDefined();
+      expect(updatedOrder?.payState).toBe(5); // 订单关闭
+      expect(updatedOrder?.cancelReason).toBe('订单超时未支付,系统自动取消');
+      expect(updatedOrder?.cancelTime).toBeInstanceOf(Date);
+    });
+
+    it('不应该取消未超过24小时的订单', async () => {
+      // 创建未支付订单,设置创建时间为23小时前(未超过24小时)
+      const twentyThreeHoursAgo = new Date();
+      twentyThreeHoursAgo.setHours(twentyThreeHoursAgo.getHours() - 23);
+
+      const order = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        payState: 0,
+        state: 0,
+        createdAt: twentyThreeHoursAgo,
+        updatedAt: twentyThreeHoursAgo,
+        expireTime: new Date(Date.now() + 3600000) // 1小时后过期
+      });
+
+      console.debug(`创建的未超时订单: ID=${order.id}, createdAt=${order.createdAt}, expireTime=${order.expireTime}`);
+
+      // 创建调度器服务并手动触发超时订单处理
+      const { OrderTimeoutSchedulerService } = await import('../../src/services/order-timeout-scheduler.service');
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const schedulerService = new OrderTimeoutSchedulerService(dataSource, 1);
+
+      const result = await schedulerService.triggerManualProcess(1);
+      console.debug(`手动处理结果:`, result);
+
+      // 应该没有处理任何订单
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(0);
+      expect(result.message).toContain('成功处理 0 个超时订单');
+
+      // 验证订单状态未改变
+      const updatedOrder = await dataSource.getRepository(OrderMt).findOne({
+        where: { id: order.id, tenantId: 1 }
+      });
+
+      expect(updatedOrder).toBeDefined();
+      expect(updatedOrder?.payState).toBe(0); // 保持未支付状态
+      expect(updatedOrder?.cancelReason).toBeNull();
+      expect(updatedOrder?.cancelTime).toBeNull();
+    });
+
+    it('不应该取消已支付的订单,即使超过24小时', async () => {
+      // 创建已支付订单,设置创建时间为25小时前
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const order = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        payState: 2, // 支付成功
+        state: 0,
+        createdAt: twentyFiveHoursAgo,
+        updatedAt: twentyFiveHoursAgo,
+        expireTime: new Date() // 已过期
+      });
+
+      console.debug(`创建的已支付超时订单: ID=${order.id}, payState=${order.payState}, createdAt=${order.createdAt}`);
+
+      // 创建调度器服务并手动触发超时订单处理
+      const { OrderTimeoutSchedulerService } = await import('../../src/services/order-timeout-scheduler.service');
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const schedulerService = new OrderTimeoutSchedulerService(dataSource, 1);
+
+      const result = await schedulerService.triggerManualProcess(1);
+      console.debug(`手动处理结果:`, result);
+
+      // 应该没有处理已支付的订单
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(0);
+      expect(result.message).toContain('成功处理 0 个超时订单');
+
+      // 验证订单状态未改变
+      const updatedOrder = await dataSource.getRepository(OrderMt).findOne({
+        where: { id: order.id, tenantId: 1 }
+      });
+
+      expect(updatedOrder).toBeDefined();
+      expect(updatedOrder?.payState).toBe(2); // 保持支付成功状态
+      expect(updatedOrder?.cancelReason).toBeNull();
+      expect(updatedOrder?.cancelTime).toBeNull();
+    });
+
+    it('不应该取消已发货的订单,即使超过24小时未支付', async () => {
+      // 创建已发货订单,设置创建时间为25小时前
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const order = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        payState: 0, // 未支付
+        state: 1, // 已发货
+        createdAt: twentyFiveHoursAgo,
+        updatedAt: twentyFiveHoursAgo,
+        expireTime: new Date() // 已过期
+      });
+
+      console.debug(`创建的已发货超时订单: ID=${order.id}, state=${order.state}, createdAt=${order.createdAt}`);
+
+      // 创建调度器服务并手动触发超时订单处理
+      const { OrderTimeoutSchedulerService } = await import('../../src/services/order-timeout-scheduler.service');
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const schedulerService = new OrderTimeoutSchedulerService(dataSource, 1);
+
+      const result = await schedulerService.triggerManualProcess(1);
+      console.debug(`手动处理结果:`, result);
+
+      // 应该没有处理已发货的订单
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(0);
+      expect(result.message).toContain('成功处理 0 个超时订单');
+
+      // 验证订单状态未改变
+      const updatedOrder = await dataSource.getRepository(OrderMt).findOne({
+        where: { id: order.id, tenantId: 1 }
+      });
+
+      expect(updatedOrder).toBeDefined();
+      expect(updatedOrder?.payState).toBe(0); // 保持未支付状态
+      expect(updatedOrder?.state).toBe(1); // 保持已发货状态
+      expect(updatedOrder?.cancelReason).toBeNull();
+      expect(updatedOrder?.cancelTime).toBeNull();
+    });
+
+    it('应该恢复未支付超时订单的商品库存', async () => {
+      // 创建测试商品
+      const testGoods = await testFactory.createTestGoods(testUser.id, {
+        tenantId: 1,
+        stock: 100,
+        salesNum: 0
+      });
+
+      // 创建未支付订单,设置创建时间为25小时前
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const order = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        payState: 0,
+        state: 0,
+        createdAt: twentyFiveHoursAgo,
+        updatedAt: twentyFiveHoursAgo,
+        expireTime: new Date()
+      });
+
+      // 创建订单商品记录
+      const orderGoods = await testFactory.createTestOrderGoods(order.id, testGoods.id, {
+        tenantId: 1,
+        num: 2
+      });
+
+      console.debug(`创建带商品的超时订单: orderId=${order.id}, goodsId=${testGoods.id}, num=${orderGoods.num}`);
+
+      // 获取商品初始库存
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const goodsRepository = dataSource.getRepository(GoodsMt);
+      const initialGoods = await goodsRepository.findOne({
+        where: { id: testGoods.id, tenantId: 1 }
+      });
+
+      console.debug(`商品初始库存: stock=${initialGoods?.stock}, salesNum=${initialGoods?.salesNum}`);
+
+      // 模拟订单创建时的库存减少:在真实订单创建流程中,库存会减少,销售量会增加
+      // 这里我们手动更新库存,模拟订单创建后的状态
+      await goodsRepository.update(
+        { id: testGoods.id, tenantId: 1 },
+        {
+          stock: () => `stock - ${orderGoods.num}`,
+          salesNum: () => `sales_num + ${orderGoods.num}`
+        }
+      );
+
+      // 验证库存已减少
+      const afterOrderCreationGoods = await goodsRepository.findOne({
+        where: { id: testGoods.id, tenantId: 1 }
+      });
+      console.debug(`订单创建后商品库存: stock=${afterOrderCreationGoods?.stock}, salesNum=${afterOrderCreationGoods?.salesNum}`);
+
+      // 创建调度器服务并手动触发超时订单处理
+      const { OrderTimeoutSchedulerService } = await import('../../src/services/order-timeout-scheduler.service');
+      const schedulerService = new OrderTimeoutSchedulerService(dataSource, 1);
+
+      const result = await schedulerService.triggerManualProcess(1);
+      console.debug(`手动处理结果:`, result);
+
+      expect(result.success).toBe(true);
+      expect(result.processedOrders).toBe(1);
+
+      // 验证商品库存已恢复(取消订单后库存应恢复到初始值)
+      const updatedGoods = await goodsRepository.findOne({
+        where: { id: testGoods.id, tenantId: 1 }
+      });
+
+      console.debug(`订单取消后商品库存: stock=${updatedGoods?.stock}, salesNum=${updatedGoods?.salesNum}`);
+
+      expect(updatedGoods).toBeDefined();
+      // 库存应该恢复到100(初始值)
+      expect(Number(updatedGoods?.stock)).toBe(100);
+      // 销售量应该恢复到0(初始值)
+      expect(Number(updatedGoods?.salesNum)).toBe(0);
+    });
+
+    it('应该支持跨租户订单超时处理', async () => {
+      // 创建租户1的超时订单
+      const twentyFiveHoursAgo = new Date();
+      twentyFiveHoursAgo.setHours(twentyFiveHoursAgo.getHours() - 25);
+
+      const tenant1Order = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        payState: 0,
+        state: 0,
+        createdAt: twentyFiveHoursAgo,
+        updatedAt: twentyFiveHoursAgo,
+        expireTime: new Date()
+      });
+
+      // 创建租户2的超时订单
+      const tenant2Order = await testFactory.createTestOrder(otherTenantUser.id, {
+        tenantId: 2,
+        payState: 0,
+        state: 0,
+        createdAt: twentyFiveHoursAgo,
+        updatedAt: twentyFiveHoursAgo,
+        expireTime: new Date()
+      });
+
+      console.debug(`跨租户测试: 租户1订单ID=${tenant1Order.id}, 租户2订单ID=${tenant2Order.id}`);
+
+      // 创建调度器服务(tenantId = null,处理所有租户)
+      const { OrderTimeoutSchedulerService } = await import('../../src/services/order-timeout-scheduler.service');
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const schedulerService = new OrderTimeoutSchedulerService(dataSource); // 不指定租户ID
+
+      // 获取所有有超时订单的租户
+      const tenantIds = await (schedulerService as any).getTenantsWithTimeoutOrders();
+      console.debug(`有超时订单的租户:`, tenantIds);
+
+      // 应该包含两个租户
+      expect(tenantIds).toContain(1);
+      expect(tenantIds).toContain(2);
+      expect(tenantIds.length).toBeGreaterThanOrEqual(2);
+
+      // 手动处理租户1的超时订单
+      const result1 = await schedulerService.triggerManualProcess(1);
+      console.debug(`租户1处理结果:`, result1);
+
+      expect(result1.success).toBe(true);
+      expect(result1.processedOrders).toBe(1);
+
+      // 验证租户1订单已取消
+      const updatedTenant1Order = await dataSource.getRepository(OrderMt).findOne({
+        where: { id: tenant1Order.id, tenantId: 1 }
+      });
+      expect(updatedTenant1Order?.payState).toBe(5);
+
+      // 手动处理租户2的超时订单
+      const result2 = await schedulerService.triggerManualProcess(2);
+      console.debug(`租户2处理结果:`, result2);
+
+      expect(result2.success).toBe(true);
+      expect(result2.processedOrders).toBe(1);
+
+      // 验证租户2订单已取消
+      const updatedTenant2Order = await dataSource.getRepository(OrderMt).findOne({
+        where: { id: tenant2Order.id, tenantId: 2 }
+      });
+      expect(updatedTenant2Order?.payState).toBe(5);
+    });
+  });
 });
 });

+ 61 - 61
packages/orders-module-mt/tests/utils/test-data-factory.ts

@@ -1,10 +1,10 @@
 import { DataSource } from 'typeorm';
 import { DataSource } from 'typeorm';
-import { UserEntity } from '@d8d/user-module';
-import { AreaEntity } from '@d8d/geo-areas';
-import { DeliveryAddress } from '@d8d/delivery-address-module';
-import { Merchant } from '@d8d/merchant-module';
-import { Supplier } from '@d8d/supplier-module';
-import { Order, OrderRefund } from '../../src/entities';
+import { UserMt } from '@d8d/user-module-mt';
+import { AreaEntityMt } from '@d8d/geo-areas-mt';
+import { DeliveryAddressMt } from '@d8d/delivery-address-module-mt';
+import { MerchantMt } from '@d8d/merchant-module-mt';
+import { SupplierMt } from '@d8d/supplier-module-mt';
+import { OrderMt, OrderRefundMt } from '../../src/entities';
 import { JWTUtil } from '@d8d/shared-utils';
 import { JWTUtil } from '@d8d/shared-utils';
 
 
 /**
 /**
@@ -14,7 +14,7 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 创建测试用户数据
    * 创建测试用户数据
    */
    */
-  static createUserData(overrides: Partial<UserEntity> = {}): Partial<UserEntity> {
+  static createUserData(overrides: Partial<UserMt> = {}): Partial<UserMt> {
     const timestamp = Math.floor(Math.random() * 100000);
     const timestamp = Math.floor(Math.random() * 100000);
     return {
     return {
       username: `test_user_${timestamp}`,
       username: `test_user_${timestamp}`,
@@ -28,9 +28,9 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 在数据库中创建测试用户
    * 在数据库中创建测试用户
    */
    */
-  static async createTestUser(dataSource: DataSource, overrides: Partial<UserEntity> = {}): Promise<UserEntity> {
+  static async createTestUser(dataSource: DataSource, overrides: Partial<UserMt> = {}): Promise<UserMt> {
     const userData = this.createUserData(overrides);
     const userData = this.createUserData(overrides);
-    const userRepository = dataSource.getRepository(UserEntity);
+    const userRepository = dataSource.getRepository(UserMt);
     const user = userRepository.create(userData);
     const user = userRepository.create(userData);
     return await userRepository.save(user);
     return await userRepository.save(user);
   }
   }
@@ -38,7 +38,7 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 为测试用户生成JWT token
    * 为测试用户生成JWT token
    */
    */
-  static generateUserToken(user: UserEntity): string {
+  static generateUserToken(user: UserMt): string {
     return JWTUtil.generateToken({
     return JWTUtil.generateToken({
       id: user.id,
       id: user.id,
       username: user.username,
       username: user.username,
@@ -49,7 +49,7 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 创建测试地区数据
    * 创建测试地区数据
    */
    */
-  static createAreaData(overrides: Partial<AreaEntity> = {}): Partial<AreaEntity> {
+  static createAreaData(overrides: Partial<AreaEntityMt> = {}): Partial<AreaEntityMt> {
     const timestamp = Math.floor(Math.random() * 100000);
     const timestamp = Math.floor(Math.random() * 100000);
     return {
     return {
       name: '广东省',
       name: '广东省',
@@ -63,9 +63,9 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 在数据库中创建测试地区
    * 在数据库中创建测试地区
    */
    */
-  static async createTestArea(dataSource: DataSource, overrides: Partial<AreaEntity> = {}): Promise<AreaEntity> {
+  static async createTestArea(dataSource: DataSource, overrides: Partial<AreaEntityMt> = {}): Promise<AreaEntityMt> {
     const areaData = this.createAreaData(overrides);
     const areaData = this.createAreaData(overrides);
-    const areaRepository = dataSource.getRepository(AreaEntity);
+    const areaRepository = dataSource.getRepository(AreaEntityMt);
     const area = areaRepository.create(areaData);
     const area = areaRepository.create(areaData);
     return await areaRepository.save(area);
     return await areaRepository.save(area);
   }
   }
@@ -74,10 +74,10 @@ export class OrdersTestDataFactory {
    * 创建完整的地区层级(省、市、区、街道)
    * 创建完整的地区层级(省、市、区、街道)
    */
    */
   static async createCompleteAreaHierarchy(dataSource: DataSource): Promise<{
   static async createCompleteAreaHierarchy(dataSource: DataSource): Promise<{
-    province: AreaEntity;
-    city: AreaEntity;
-    district: AreaEntity;
-    town: AreaEntity;
+    province: AreaEntityMt;
+    city: AreaEntityMt;
+    district: AreaEntityMt;
+    town: AreaEntityMt;
   }> {
   }> {
     const timestamp = Math.floor(Math.random() * 100000);
     const timestamp = Math.floor(Math.random() * 100000);
 
 
@@ -114,11 +114,11 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 创建测试配送地址数据
    * 创建测试配送地址数据
    */
    */
-  static createDeliveryAddressData(
+  static createDeliveryAddressMtData(
     userId: number,
     userId: number,
-    areaHierarchy: { province: AreaEntity; city: AreaEntity; district: AreaEntity; town: AreaEntity },
-    overrides: Partial<DeliveryAddress> = {}
-  ): Partial<DeliveryAddress> {
+    areaHierarchy: { province: AreaEntityMt; city: AreaEntityMt; district: AreaEntityMt; town: AreaEntityMt },
+    overrides: Partial<DeliveryAddressMt> = {}
+  ): Partial<DeliveryAddressMt> {
     return {
     return {
       userId,
       userId,
       name: '收货人姓名',
       name: '收货人姓名',
@@ -138,14 +138,14 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 在数据库中创建测试配送地址
    * 在数据库中创建测试配送地址
    */
    */
-  static async createTestDeliveryAddress(
+  static async createTestDeliveryAddressMt(
     dataSource: DataSource,
     dataSource: DataSource,
     userId: number,
     userId: number,
-    areaHierarchy: { province: AreaEntity; city: AreaEntity; district: AreaEntity; town: AreaEntity },
-    overrides: Partial<DeliveryAddress> = {}
-  ): Promise<DeliveryAddress> {
-    const addressData = this.createDeliveryAddressData(userId, areaHierarchy, overrides);
-    const addressRepository = dataSource.getRepository(DeliveryAddress);
+    areaHierarchy: { province: AreaEntityMt; city: AreaEntityMt; district: AreaEntityMt; town: AreaEntityMt },
+    overrides: Partial<DeliveryAddressMt> = {}
+  ): Promise<DeliveryAddressMt> {
+    const addressData = this.createDeliveryAddressMtData(userId, areaHierarchy, overrides);
+    const addressRepository = dataSource.getRepository(DeliveryAddressMt);
     const address = addressRepository.create(addressData);
     const address = addressRepository.create(addressData);
     return await addressRepository.save(address);
     return await addressRepository.save(address);
   }
   }
@@ -153,7 +153,7 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 创建测试商户数据
    * 创建测试商户数据
    */
    */
-  static createMerchantData(overrides: Partial<Merchant> = {}): Partial<Merchant> {
+  static createMerchantMtData(overrides: Partial<MerchantMt> = {}): Partial<MerchantMt> {
     const timestamp = Math.floor(Math.random() * 100000);
     const timestamp = Math.floor(Math.random() * 100000);
     return {
     return {
       name: '测试商户',
       name: '测试商户',
@@ -168,9 +168,9 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 在数据库中创建测试商户
    * 在数据库中创建测试商户
    */
    */
-  static async createTestMerchant(dataSource: DataSource, overrides: Partial<Merchant> = {}): Promise<Merchant> {
-    const merchantData = this.createMerchantData(overrides);
-    const merchantRepository = dataSource.getRepository(Merchant);
+  static async createTestMerchantMt(dataSource: DataSource, overrides: Partial<MerchantMt> = {}): Promise<MerchantMt> {
+    const merchantData = this.createMerchantMtData(overrides);
+    const merchantRepository = dataSource.getRepository(MerchantMt);
     const merchant = merchantRepository.create(merchantData);
     const merchant = merchantRepository.create(merchantData);
     return await merchantRepository.save(merchant);
     return await merchantRepository.save(merchant);
   }
   }
@@ -178,7 +178,7 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 创建测试供应商数据
    * 创建测试供应商数据
    */
    */
-  static createSupplierData(overrides: Partial<Supplier> = {}): Partial<Supplier> {
+  static createSupplierMtData(overrides: Partial<SupplierMt> = {}): Partial<SupplierMt> {
     const timestamp = Math.floor(Math.random() * 100000);
     const timestamp = Math.floor(Math.random() * 100000);
     return {
     return {
       name: '测试供应商',
       name: '测试供应商',
@@ -193,9 +193,9 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 在数据库中创建测试供应商
    * 在数据库中创建测试供应商
    */
    */
-  static async createTestSupplier(dataSource: DataSource, overrides: Partial<Supplier> = {}): Promise<Supplier> {
-    const supplierData = this.createSupplierData(overrides);
-    const supplierRepository = dataSource.getRepository(Supplier);
+  static async createTestSupplierMt(dataSource: DataSource, overrides: Partial<SupplierMt> = {}): Promise<SupplierMt> {
+    const supplierData = this.createSupplierMtData(overrides);
+    const supplierRepository = dataSource.getRepository(SupplierMt);
     const supplier = supplierRepository.create(supplierData);
     const supplier = supplierRepository.create(supplierData);
     return await supplierRepository.save(supplier);
     return await supplierRepository.save(supplier);
   }
   }
@@ -203,14 +203,14 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 创建测试订单数据
    * 创建测试订单数据
    */
    */
-  static createOrderData(
+  static createOrderMtData(
     userId: number,
     userId: number,
     deliveryAddressId: number,
     deliveryAddressId: number,
     merchantId: number,
     merchantId: number,
     supplierId: number,
     supplierId: number,
-    areaHierarchy: { province: AreaEntity; city: AreaEntity; district: AreaEntity; town: AreaEntity },
-    overrides: Partial<Order> = {}
-  ): Partial<Order> {
+    areaHierarchy: { province: AreaEntityMt; city: AreaEntityMt; district: AreaEntityMt; town: AreaEntityMt },
+    overrides: Partial<OrderMt> = {}
+  ): Partial<OrderMt> {
     const timestamp = Math.floor(Math.random() * 100000);
     const timestamp = Math.floor(Math.random() * 100000);
     return {
     return {
       orderNo: `ORDER_${timestamp}`,
       orderNo: `ORDER_${timestamp}`,
@@ -238,17 +238,17 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 在数据库中创建测试订单
    * 在数据库中创建测试订单
    */
    */
-  static async createTestOrder(
+  static async createTestOrderMt(
     dataSource: DataSource,
     dataSource: DataSource,
     userId: number,
     userId: number,
     deliveryAddressId: number,
     deliveryAddressId: number,
     merchantId: number,
     merchantId: number,
     supplierId: number,
     supplierId: number,
-    areaHierarchy: { province: AreaEntity; city: AreaEntity; district: AreaEntity; town: AreaEntity },
-    overrides: Partial<Order> = {}
-  ): Promise<Order> {
-    const orderData = this.createOrderData(userId, deliveryAddressId, merchantId, supplierId, areaHierarchy, overrides);
-    const orderRepository = dataSource.getRepository(Order);
+    areaHierarchy: { province: AreaEntityMt; city: AreaEntityMt; district: AreaEntityMt; town: AreaEntityMt },
+    overrides: Partial<OrderMt> = {}
+  ): Promise<OrderMt> {
+    const orderData = this.createOrderMtData(userId, deliveryAddressId, merchantId, supplierId, areaHierarchy, overrides);
+    const orderRepository = dataSource.getRepository(OrderMt);
     const order = orderRepository.create(orderData);
     const order = orderRepository.create(orderData);
     return await orderRepository.save(order);
     return await orderRepository.save(order);
   }
   }
@@ -256,13 +256,13 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 创建完整的测试订单环境(包含所有依赖实体)
    * 创建完整的测试订单环境(包含所有依赖实体)
    */
    */
-  static async createCompleteOrderEnvironment(dataSource: DataSource, userId?: number): Promise<{
-    user: UserEntity;
-    areaHierarchy: { province: AreaEntity; city: AreaEntity; district: AreaEntity; town: AreaEntity };
-    deliveryAddress: DeliveryAddress;
-    merchant: Merchant;
-    supplier: Supplier;
-    order: Order;
+  static async createCompleteOrderMtEnvironment(dataSource: DataSource, userId?: number): Promise<{
+    user: UserMt;
+    areaHierarchy: { province: AreaEntityMt; city: AreaEntityMt; district: AreaEntityMt; town: AreaEntityMt };
+    deliveryAddress: DeliveryAddressMt;
+    merchant: MerchantMt;
+    supplier: SupplierMt;
+    order: OrderMt;
   }> {
   }> {
     // 创建用户
     // 创建用户
     const user = await this.createTestUser(dataSource, userId ? { id: userId } : {});
     const user = await this.createTestUser(dataSource, userId ? { id: userId } : {});
@@ -271,14 +271,14 @@ export class OrdersTestDataFactory {
     const areaHierarchy = await this.createCompleteAreaHierarchy(dataSource);
     const areaHierarchy = await this.createCompleteAreaHierarchy(dataSource);
 
 
     // 创建商户和供应商
     // 创建商户和供应商
-    const merchant = await this.createTestMerchant(dataSource, { createdBy: user.id });
-    const supplier = await this.createTestSupplier(dataSource, { createdBy: user.id });
+    const merchant = await this.createTestMerchantMt(dataSource, { createdBy: user.id });
+    const supplier = await this.createTestSupplierMt(dataSource, { createdBy: user.id });
 
 
     // 创建配送地址
     // 创建配送地址
-    const deliveryAddress = await this.createTestDeliveryAddress(dataSource, user.id, areaHierarchy);
+    const deliveryAddress = await this.createTestDeliveryAddressMt(dataSource, user.id, areaHierarchy);
 
 
     // 创建订单
     // 创建订单
-    const order = await this.createTestOrder(dataSource, user.id, deliveryAddress.id, merchant.id, supplier.id, areaHierarchy);
+    const order = await this.createTestOrderMt(dataSource, user.id, deliveryAddress.id, merchant.id, supplier.id, areaHierarchy);
 
 
     return { user, areaHierarchy, deliveryAddress, merchant, supplier, order };
     return { user, areaHierarchy, deliveryAddress, merchant, supplier, order };
   }
   }
@@ -286,11 +286,11 @@ export class OrdersTestDataFactory {
   /**
   /**
    * 创建测试退款数据
    * 创建测试退款数据
    */
    */
-  static createRefundData(orderNo: string, overrides: Partial<OrderRefund> = {}): Partial<OrderRefund> {
+  static createRefundData(orderNo: string, overrides: Partial<OrderRefundMt> = {}): Partial<OrderRefundMt> {
     const timestamp = Math.floor(Math.random() * 100000);
     const timestamp = Math.floor(Math.random() * 100000);
     return {
     return {
       orderNo,
       orderNo,
-      refundOrderNo: `REFUND_${timestamp}`,
+      refundOrderMtNo: `REFUND_${timestamp}`,
       refundAmount: 50.00,
       refundAmount: 50.00,
       state: 0,
       state: 0,
       createdBy: 1,
       createdBy: 1,
@@ -304,10 +304,10 @@ export class OrdersTestDataFactory {
   static async createTestRefund(
   static async createTestRefund(
     dataSource: DataSource,
     dataSource: DataSource,
     orderNo: string,
     orderNo: string,
-    overrides: Partial<OrderRefund> = {}
-  ): Promise<OrderRefund> {
+    overrides: Partial<OrderRefundMt> = {}
+  ): Promise<OrderRefundMt> {
     const refundData = this.createRefundData(orderNo, overrides);
     const refundData = this.createRefundData(orderNo, overrides);
-    const refundRepository = dataSource.getRepository(OrderRefund);
+    const refundRepository = dataSource.getRepository(OrderRefundMt);
     const refund = refundRepository.create(refundData);
     const refund = refundRepository.create(refundData);
     return await refundRepository.save(refund);
     return await refundRepository.save(refund);
   }
   }

+ 15 - 0
packages/server/src/index.ts

@@ -113,6 +113,21 @@ try {
   console.warn('飞鹅打印防退款延迟调度器初始化失败,延迟打印功能可能不可用:', error);
   console.warn('飞鹅打印防退款延迟调度器初始化失败,延迟打印功能可能不可用:', error);
 }
 }
 
 
+// 初始化订单超时自动取消调度器
+try {
+  const { OrderTimeoutSchedulerService } = await import('@d8d/orders-module-mt');
+
+  // 获取所有租户(这里可以根据需要调整,例如只针对有订单的租户)
+  // 暂时为所有租户启动调度器,或者可以配置为只对特定租户启动
+  // 这里我们启动一个全局调度器(tenantId = null)来处理所有租户
+  const orderTimeoutSchedulerService = new OrderTimeoutSchedulerService(AppDataSource);
+  await orderTimeoutSchedulerService.start();
+
+  console.log('订单超时自动取消调度器已启动');
+} catch (error) {
+  console.warn('订单超时自动取消调度器初始化失败,订单超时自动取消功能可能不可用:', error);
+}
+
 const app = new Hono();
 const app = new Hono();
 const api = new OpenAPIHono<AuthContext>()
 const api = new OpenAPIHono<AuthContext>()
 
 

+ 6 - 0
pnpm-lock.yaml

@@ -3604,6 +3604,9 @@ importers:
       hono:
       hono:
         specifier: ^4.8.5
         specifier: ^4.8.5
         version: 4.8.5
         version: 4.8.5
+      node-cron:
+        specifier: ^3.0.3
+        version: 3.0.3
       typeorm:
       typeorm:
         specifier: ^0.3.20
         specifier: ^0.3.20
         version: 0.3.27(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.2.2)
         version: 0.3.27(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.2.2)
@@ -3617,6 +3620,9 @@ importers:
       '@types/node':
       '@types/node':
         specifier: ^22.10.2
         specifier: ^22.10.2
         version: 22.19.1
         version: 22.19.1
+      '@types/node-cron':
+        specifier: ^3.0.11
+        version: 3.0.11
       '@typescript-eslint/eslint-plugin':
       '@typescript-eslint/eslint-plugin':
         specifier: ^8.18.1
         specifier: ^8.18.1
         version: 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
         version: 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)