Răsfoiți Sursa

✨ feat(image): 增强图片组件功能并优化购物车页面

- 为图片组件添加重试机制和自定义错误占位符功能
- 在购物车页面集成实时商品数据查询,确保价格和库存信息准确
- 优化商品详情页的返回逻辑,根据来源页面智能跳转
- 在订单模块的用户信息中添加微信openid字段支持
- 添加调试日志以辅助排查微信小程序配置问题
yourname 1 lună în urmă
părinte
comite
d54a2592ca

+ 48 - 5
mini/src/components/ui/image.tsx

@@ -1,6 +1,6 @@
 import { View, Image as TaroImage, ImageProps as TaroImageProps } from '@tarojs/components'
 import { cn } from '@/utils/cn'
-import { useState } from 'react'
+import { useState, useEffect } from 'react'
 
 export interface ImageProps extends Omit<TaroImageProps, 'onError'> {
   /**
@@ -31,6 +31,10 @@ export interface ImageProps extends Omit<TaroImageProps, 'onError'> {
    * @default true
    */
   showError?: boolean
+  /**
+   * 错误占位符内容(React节点)
+   */
+  errorPlaceholder?: React.ReactNode
   /**
    * 圆角大小
    */
@@ -47,6 +51,16 @@ export interface ImageProps extends Omit<TaroImageProps, 'onError'> {
    * 图片加载成功的回调
    */
   onLoad?: () => void
+  /**
+   * 重试次数
+   * @default 0
+   */
+  retryCount?: number
+  /**
+   * 重试延迟(毫秒)
+   * @default 1000
+   */
+  retryDelay?: number
 }
 
 const roundedMap = {
@@ -65,14 +79,27 @@ export function Image({
   lazyLoad = true,
   showLoading = true,
   showError = true,
+  errorPlaceholder,
   rounded = 'none',
   className,
   onError,
   onLoad,
+  retryCount = 0,
+  retryDelay = 1000,
   ...props
 }: ImageProps) {
   const [loading, setLoading] = useState(true)
   const [error, setError] = useState(false)
+  const [retryAttempt, setRetryAttempt] = useState(0)
+  const [currentSrc, setCurrentSrc] = useState(src)
+
+  // 当src变化时,重置状态
+  useEffect(() => {
+    setCurrentSrc(src)
+    setLoading(true)
+    setError(false)
+    setRetryAttempt(0)
+  }, [src])
 
   const handleLoad = () => {
     setLoading(false)
@@ -81,9 +108,22 @@ export function Image({
   }
 
   const handleError = () => {
-    setLoading(false)
-    setError(true)
-    onError?.()
+    if (retryAttempt < retryCount) {
+      // 还有重试次数,延迟后重试
+      setTimeout(() => {
+        setRetryAttempt(prev => prev + 1)
+        // 通过添加时间戳参数来避免缓存
+        const timestamp = new Date().getTime()
+        setCurrentSrc(`${src}${src.includes('?') ? '&' : '?'}_t=${timestamp}`)
+        setLoading(true)
+        setError(false)
+      }, retryDelay)
+    } else {
+      // 重试次数用完,显示错误
+      setLoading(false)
+      setError(true)
+      onError?.()
+    }
   }
 
   const renderPlaceholder = () => {
@@ -96,6 +136,9 @@ export function Image({
     }
 
     if (error && showError) {
+      if (errorPlaceholder) {
+        return errorPlaceholder
+      }
       return (
         <View className="absolute inset-0 flex items-center justify-center bg-gray-100">
           <View className="i-heroicons-exclamation-triangle-20-solid w-8 h-8 text-gray-400" />
@@ -109,7 +152,7 @@ export function Image({
   return (
     <View className={cn('relative overflow-hidden', roundedMap[rounded], className)}>
       <TaroImage
-        src={src}
+        src={currentSrc}
         mode={mode}
         lazyLoad={lazyLoad}
         onLoad={handleLoad}

+ 74 - 11
mini/src/pages/cart/index.tsx

@@ -1,11 +1,13 @@
 import { View, ScrollView, Text } from '@tarojs/components'
 import { useState, useEffect } from 'react'
+import { useQueries } from '@tanstack/react-query'
 import Taro from '@tarojs/taro'
 import { Navbar } from '@/components/ui/navbar'
 import { Button } from '@/components/ui/button'
 import { Image } from '@/components/ui/image'
 import { useCart } from '@/contexts/CartContext'
 import { TabBarLayout } from '@/layouts/tab-bar-layout'
+import { goodsClient } from '@/api'
 import clsx from 'clsx'
 import './index.css'
 
@@ -14,6 +16,32 @@ export default function CartPage() {
   const [selectedItems, setSelectedItems] = useState<number[]>([])
   const [showSkeleton, setShowSkeleton] = useState(true)
 
+  // 为每个购物车商品创建查询,从数据库重新获取最新信息
+  const goodsQueries = useQueries({
+    queries: cart.items.map(item => ({
+      queryKey: ['cart-goods', item.id],
+      queryFn: async () => {
+        const response = await goodsClient[':id'].$get({
+          param: { id: item.id }
+        })
+        if (response.status !== 200) {
+          throw new Error('获取商品详情失败')
+        }
+        return response.json()
+      },
+      enabled: item.id > 0,
+      staleTime: 5 * 60 * 1000, // 5分钟缓存
+    }))
+  })
+
+  // 创建商品ID到最新商品信息的映射
+  const goodsMap = new Map()
+  goodsQueries.forEach((query, index) => {
+    if (query.data && cart.items[index]) {
+      goodsMap.set(cart.items[index].id, query.data)
+    }
+  })
+
   // 全选/取消全选
   const toggleSelectAll = () => {
     if (selectedItems.length === cart.items.length) {
@@ -32,10 +60,14 @@ export default function CartPage() {
     )
   }
 
-  // 计算选中商品的总价
+  // 计算选中商品的总价,使用从数据库获取的最新价格
   const selectedItemsTotal = cart.items
     .filter(item => selectedItems.includes(item.id))
-    .reduce((sum, item) => sum + (item.price * item.quantity), 0)
+    .reduce((sum, item) => {
+      const latestGoods = goodsMap.get(item.id)
+      const price = latestGoods?.price || item.price // 优先使用数据库价格,没有则使用本地价格
+      return sum + (price * item.quantity)
+    }, 0)
 
   // 添加骨架屏效果
   useEffect(() => {
@@ -143,8 +175,17 @@ export default function CartPage() {
         ) : (
           <View className="cart-content">
             {/* 商品列表 */}
-            <View className="cart-group">
-              {cart.items.map((item) => (
+            <View className="cart-group" >
+              {cart.items.map((item) => {
+                // 获取从数据库重新获取的最新商品信息
+                const latestGoods = goodsMap.get(item.id)
+                // 优先使用数据库中的最新信息,如果没有则使用本地保存的信息
+                const goodsName = latestGoods?.name || item.name
+                const goodsPrice = latestGoods?.price || item.price
+                const goodsImage = latestGoods?.imageFile?.fullUrl || item.image
+                const goodsStock = latestGoods?.stock || item.stock
+
+                return (
                 <View key={item.id} className="goods-item">
                   <View className="goods-item-info">
                     {/* 选择框 */}
@@ -166,24 +207,45 @@ export default function CartPage() {
 
                     {/* 商品卡片 */}
                     <View className="goods-sku-info">
-                      <View className="goods-card horizontal-wrap">
+                      <View
+                        className="goods-card horizontal-wrap"
+                        onClick={() => Taro.navigateTo({
+                          url: `/pages/goods-detail/index?id=${item.id}&from=cart`
+                        })}
+                      >
                         {/* 商品图片 */}
                         <View className="goods-thumb">
                           <Image
-                            src={item.image}
+                            src={goodsImage && goodsImage.trim() !== '' ? goodsImage : 'https://via.placeholder.com/150x150?text=商品图片'}
                             className="thumb-image"
                             mode="aspectFill"
+                            // retryCount={2} // 重试2次
+                            // retryDelay={1500} // 每次重试间隔1.5秒
+                            onError={() => {
+                              console.warn('商品图片加载失败:', item.id, goodsName, goodsImage)
+                            }}
+                            onLoad={() => {
+                              console.log('商品图片加载成功:', item.id, goodsName)
+                            }}
+                            errorPlaceholder={
+                              <View className="absolute inset-0 flex items-center justify-center bg-gray-100">
+                                <View className="text-center">
+                                  <View className="i-heroicons-photo-20-solid w-8 h-8 text-gray-400 mx-auto mb-2" />
+                                  <Text className="text-xs text-gray-500">图片加载失败</Text>
+                                </View>
+                              </View>
+                            }
                           />
-                          {item.stock <= 3 && (
+                          {goodsStock <= 3 && (
                             <View className="stock-mask">
-                              仅剩{item.stock}件
+                              仅剩{goodsStock}件
                             </View>
                           )}
                         </View>
 
                         {/* 商品信息 */}
                         <View className="goods-body">
-                          <Text className="goods-title">{item.name}</Text>
+                          <Text className="goods-title">{goodsName}</Text>
 
                           {item.spec && (
                             <View className="goods-specs">
@@ -193,7 +255,7 @@ export default function CartPage() {
                           )}
 
                           <View className="goods-price-section">
-                            <Text className="goods-price">¥{item.price.toFixed(2)}</Text>
+                            <Text className="goods-price">¥{goodsPrice.toFixed(2)}</Text>
                           </View>
 
                           {/* 数量选择器 */}
@@ -241,7 +303,8 @@ export default function CartPage() {
                     删除
                   </View>
                 </View>
-              ))}
+                )
+              })}
             </View>
 
 

+ 14 - 4
mini/src/pages/goods-detail/index.tsx

@@ -88,6 +88,7 @@ export default function GoodsDetailPage() {
   const router = useRouter()
   const params = router.params
   const goodsId = params?.id ? parseInt(params.id) : 0
+  const fromPage = params?.from || ''
 
   const { data: goods, isLoading } = useQuery({
     queryKey: ['goods', goodsId],
@@ -134,7 +135,7 @@ export default function GoodsDetailPage() {
   useShareAppMessage(() => {
     return {
       title: goods ? `${goods.name} - ¥${goods.price}` : '发现好物',
-      path: `/pages/goods-detail/index?id=${goodsId}`,
+      path: `/pages/goods-detail/index?id=${goodsId}&from=share`,
       imageUrl: goods?.slideImages?.[0]?.fullUrl || goods?.imageFile?.fullUrl
     }
   })
@@ -230,9 +231,18 @@ export default function GoodsDetailPage() {
       <Navbar
         title="商品详情"
         leftIcon="i-heroicons-chevron-left-20-solid"
-        onClickLeft={() => Taro.switchTab({
-          url: `/pages/index/index`
-        })}
+        onClickLeft={() => {
+          // 根据来源页面决定返回逻辑
+          //  分享,返回首页
+          if (fromPage === 'share') {
+             Taro.switchTab({
+              url: `/pages/index/index`
+            })
+          } else {
+           // 从其他页面来的,返回购物车
+           Taro.navigateBack()
+          }
+        }}
       />
 
       <ScrollView className="goods-detail-scroll" scrollY>

+ 5 - 0
packages/core-module-mt/auth-module-mt/src/services/mini-auth.service.mt.ts

@@ -230,6 +230,8 @@ export class MiniAuthService {
   }): Promise<any> {
     const { openid, templateId, page, data, miniprogramState = 'formal', tenantId } = params;
 
+    console.log("ssssssss");
+
     // 获取微信小程序配置
     let appId: string | null = null;
     let appSecret: string | null = null;
@@ -240,6 +242,9 @@ export class MiniAuthService {
       const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId);
       appId = configs['wx.mini.app.id'];
       appSecret = configs['wx.mini.app.secret'];
+
+      console.log("appId:",appId)
+      console.log("appSecret:",appSecret)
     }
 
     // 如果系统配置中没有找到,回退到环境变量

+ 10 - 2
packages/orders-module-mt/src/schemas/order.mt.schema.ts

@@ -221,7 +221,11 @@ export const OrderSchema = z.object({
   user: z.object({
     id: z.number().int().positive().openapi({ description: '用户ID' }),
     username: z.string().openapi({ description: '用户名', example: 'user123' }),
-    phone: z.string().nullable().openapi({ description: '手机号', example: '13800138000' })
+    phone: z.string().nullable().openapi({ description: '手机号', example: '13800138000' }),
+    openid: z.string().nullable().optional().openapi({
+      description: '微信小程序openid',
+      example: 'oABCDEFGH123456789'
+    })
   }).nullable().optional().openapi({
     description: '用户信息'
   }),
@@ -636,7 +640,11 @@ export const OrderListSchema = z.object({
   user: z.object({
     id: z.number().int().positive().openapi({ description: '用户ID' }),
     username: z.string().openapi({ description: '用户名', example: 'user123' }),
-    phone: z.string().nullable().openapi({ description: '手机号', example: '13800138000' })
+    phone: z.string().nullable().openapi({ description: '手机号', example: '13800138000' }),
+    openid: z.string().nullable().optional().openapi({
+      description: '微信小程序openid',
+      example: 'oABCDEFGH123456789'
+    })
   }).nullable().optional().openapi({
     description: '用户信息'
   }),

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

@@ -0,0 +1,680 @@
+import { z } from '@hono/zod-openapi';
+
+// 订单状态枚举
+export const OrderStatus = {
+  PENDING: 0, // 未发货
+  SHIPPED: 1, // 已发货
+  RECEIVED: 2, // 收货成功
+  RETURNED: 3, // 已退货
+} as const;
+
+// 发货方式枚举
+export const DeliveryType = {
+  NOT_SHIPPED: 0, // 未发货
+  EXPRESS: 1, // 物流快递
+  LOCAL_DELIVERY: 2, // 同城配送
+  SELF_PICKUP: 3, // 用户自提
+  VIRTUAL: 4, // 虚拟发货
+} as const;
+
+// 支付状态枚举
+export const PayStatus = {
+  UNPAID: 0, // 未支付
+  PAYING: 1, // 支付中
+  SUCCESS: 2, // 支付成功
+  REFUNDED: 3, // 已退款
+  FAILED: 4, // 支付失败
+  CLOSED: 5, // 订单关闭
+} as const;
+
+// 订单类型枚举
+export const OrderType = {
+  PHYSICAL: 1, // 实物订单
+  VIRTUAL: 2, // 虚拟订单
+} as const;
+
+// 支付类型枚举
+export const PayType = {
+  POINTS: 1, // 积分
+  COUPON: 2, // 礼券
+} as const;
+
+// 多租户订单基础Schema
+export const OrderSchema = z.object({
+  id: z.number().int().positive().optional().openapi({
+    description: '订单ID',
+    example: 1
+  }),
+  tenantId: z.number().int().positive().openapi({
+    description: '租户ID',
+    example: 1
+  }),
+  orderNo: z.string().min(1, '订单号不能为空').max(50, '订单号最多50个字符').openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  userId: z.number().int().positive().openapi({
+    description: '用户ID',
+    example: 1
+  }),
+  authCode: z.string().max(32, '付款码最多32个字符').nullable().optional().openapi({
+    description: '付款码',
+    example: '12345678901234567890123456789012'
+  }),
+  cardNo: z.string().max(32, '卡号最多32个字符').nullable().optional().openapi({
+    description: '卡号',
+    example: '6222********1234'
+  }),
+  sjtCardNo: z.string().max(32, '盛京通卡号最多32个字符').nullable().optional().openapi({
+    description: '盛京通卡号',
+    example: 'SJT1234567890'
+  }),
+  amount: z.coerce.number<number>().min(0, '订单金额不能小于0').max(999999.99, '订单金额不能超过999999.99').openapi({
+    description: '订单金额',
+    example: 99.99
+  }),
+  costAmount: z.coerce.number<number>().min(0, '成本金额不能小于0').max(999999.99, '成本金额不能超过999999.99').default(0).openapi({
+    description: '成本金额',
+    example: 50.00
+  }),
+  freightAmount: z.coerce.number<number>().min(0, '运费不能小于0').max(999999.99, '运费不能超过999999.99').default(0).openapi({
+    description: '运费',
+    example: 10.00
+  }),
+  discountAmount: z.coerce.number<number>().min(0, '优惠金额不能小于0').max(999999.99, '优惠金额不能超过999999.99').default(0).openapi({
+    description: '优惠金额',
+    example: 5.00
+  }),
+  payAmount: z.coerce.number<number>().min(0, '实际支付金额不能小于0').max(999999.99, '实际支付金额不能超过999999.99').default(0).openapi({
+    description: '实际支付金额',
+    example: 94.99
+  }),
+  deviceNo: z.string().max(255, '设备编号最多255个字符').nullable().optional().openapi({
+    description: '设备编号',
+    example: 'DEV001234'
+  }),
+  description: z.string().max(255, '订单描述最多255个字符').nullable().optional().openapi({
+    description: '订单描述',
+    example: '购买商品'
+  }),
+  goodsDetail: z.string().max(2000, '订单详情最多2000个字符').nullable().optional().openapi({
+    description: '订单详情(json格式)',
+    example: '[{"goodsId":1,"name":"商品1","price":99.99,"num":1}]'
+  }),
+  goodsTag: z.string().max(255, '订单优惠标记最多255个字符').nullable().optional().openapi({
+    description: '订单优惠标记',
+    example: '满100减5'
+  }),
+  address: z.string().max(255, '地址最多255个字符').nullable().optional().openapi({
+    description: '地址',
+    example: '北京市朝阳区xxx路xxx号'
+  }),
+  orderType: z.coerce.number().int().min(1, '订单类型最小为1').max(2, '订单类型最大为2').default(1).openapi({
+    description: '订单类型 1实物订单 2虚拟订单',
+    example: 1
+  }),
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').default(0).openapi({
+    description: '支付类型1积分2礼券',
+    example: 1
+  }),
+  payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').default(0).openapi({
+    description: '支付状态 0未支付1支付中2支付成功3已退款4支付失败5订单关闭',
+    example: 2
+  }),
+  state: z.coerce.number().int().min(0, '订单状态最小为0').max(3, '订单状态最大为3').default(0).openapi({
+    description: '订单状态 0未发货1已发货2收货成功3已退货',
+    example: 0
+  }),
+  userPhone: z.string().max(50, '用户手机号最多50个字符').nullable().optional().openapi({
+    description: '用户手机号',
+    example: '13800138000'
+  }),
+  merchantId: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '商户id',
+    example: 1
+  }),
+  merchantNo: z.coerce.number().int().min(0, '不能小于0').nullable().optional().openapi({
+    description: '商户号',
+    example: 1001
+  }),
+  supplierId: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '供货商id',
+    example: 1
+  }),
+  addressId: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '地址id',
+    example: 1
+  }),
+  receiverMobile: z.string().max(255, '收货人手机号最多255个字符').nullable().optional().openapi({
+    description: '收货人手机号',
+    example: '13800138000'
+  }),
+  recevierName: z.string().max(255, '收货人姓名最多255个字符').nullable().optional().openapi({
+    description: '收货人姓名',
+    example: '张三'
+  }),
+  recevierProvince: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '收货人所在省',
+    example: 110000
+  }),
+  recevierCity: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '收货人所在市',
+    example: 110100
+  }),
+  recevierDistrict: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '收货人所在区',
+    example: 110105
+  }),
+  recevierTown: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '收货人所在街道',
+    example: 110105001
+  }),
+  refundTime: z.coerce.date().nullable().optional().openapi({
+    description: '退款时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  closeTime: z.coerce.date().nullable().optional().openapi({
+    description: '订单关闭时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  remark: z.string().max(255, '管理员备注信息最多255个字符').nullable().optional().openapi({
+    description: '管理员备注信息',
+    example: '请尽快发货'
+  }),
+  deliveryType: z.coerce.number().int().min(0, '发货方式最小为0').max(4, '发货方式最大为4').default(0).openapi({
+    description: '发货方式 0未发货 1物流快递 2同城配送 3用户自提 4虚拟发货',
+    example: 1
+  }),
+  deliveryCompany: z.string().max(100, '快递公司最多100个字符').nullable().optional().openapi({
+    description: '快递公司',
+    example: '顺丰速运'
+  }),
+  deliveryNo: z.string().max(100, '快递单号最多100个字符').nullable().optional().openapi({
+    description: '快递单号',
+    example: 'SF1234567890'
+  }),
+  deliveryTime: z.coerce.date().nullable().optional().openapi({
+    description: '发货时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  deliveryRemark: z.string().max(500, '发货备注最多500个字符').nullable().optional().openapi({
+    description: '发货备注',
+    example: '已包装好,请注意查收'
+  }),
+  createdBy: z.number().int().positive().nullable().optional().openapi({
+    description: '创建人ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().optional().openapi({
+    description: '更新人ID',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  // 关联实体
+  user: z.object({
+    id: z.number().int().positive().openapi({ description: '用户ID' }),
+    username: z.string().openapi({ description: '用户名', example: 'user123' }),
+    phone: z.string().nullable().openapi({ description: '手机号', example: '13800138000' })
+  }).nullable().optional().openapi({
+    description: '用户信息'
+  }),
+  merchant: z.object({
+    id: z.number().int().positive().openapi({ description: '商户ID' }),
+    name: z.string().openapi({ description: '商户名称', example: '商户A' })
+  }).nullable().optional().openapi({
+    description: '商户信息'
+  }),
+  supplier: z.object({
+    id: z.number().int().positive().openapi({ description: '供货商ID' }),
+    name: z.string().openapi({ description: '供货商名称', example: '供货商A' })
+  }).nullable().optional().openapi({
+    description: '供货商信息'
+  }),
+  deliveryAddress: z.object({
+    id: z.number().int().positive().openapi({ description: '地址ID' }),
+    name: z.string().openapi({ description: '收货人姓名', example: '张三' }),
+    phone: z.string().openapi({ description: '收货人电话', example: '13800138000' }),
+    address: z.string().openapi({ description: '详细地址', example: '北京市朝阳区xxx路xxx号' })
+  }).nullable().optional().openapi({
+    description: '收货地址信息'
+  }),
+  // 订单商品信息
+  orderGoods: z.array(z.object({
+    id: z.number().int().positive().openapi({ description: '订单商品ID' }),
+    goodsId: z.number().int().positive().openapi({ description: '商品ID' }),
+    goodsName: z.string().openapi({ description: '商品名称', example: '商品A' }),
+    price: z.coerce.number<number>().openapi({ description: '商品价格', example: 99.99 }),
+    num: z.number().int().positive().openapi({ description: '商品数量', example: 1 }),
+    imageFileId: z.number().int().positive().nullable().openapi({ description: '商品图片文件ID', example: 1 }),
+    imageFile: z.object({
+      id: z.number().int().positive().openapi({ description: '文件ID' }),
+      fullUrl: z.url().openapi({ description: '完整文件访问URL', example: 'https://minio.example.com/d8dai/uploads/goods/2024/product-image.jpg' })
+    }).nullable().optional().openapi({
+      description: '商品图片信息'
+    })
+  })).optional().openapi({
+    description: '订单商品列表'
+  })
+});
+
+// 创建订单DTO
+export const CreateOrderDto = z.object({
+  orderNo: z.string().min(1, '订单号不能为空').max(50, '订单号最多50个字符').openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  userId: z.number().int().positive('用户ID必须是正整数').openapi({
+    description: '用户ID',
+    example: 1
+  }),
+  authCode: z.string().max(32, '付款码最多32个字符').nullable().optional().openapi({
+    description: '付款码',
+    example: '12345678901234567890123456789012'
+  }),
+  cardNo: z.string().max(32, '卡号最多32个字符').nullable().optional().openapi({
+    description: '卡号',
+    example: '6222********1234'
+  }),
+  sjtCardNo: z.string().max(32, '盛京通卡号最多32个字符').nullable().optional().openapi({
+    description: '盛京通卡号',
+    example: 'SJT1234567890'
+  }),
+  amount: z.coerce.number<number>().min(0, '订单金额不能小于0').max(999999.99, '订单金额不能超过999999.99').openapi({
+    description: '订单金额',
+    example: 99.99
+  }),
+  costAmount: z.coerce.number<number>().min(0, '成本金额不能小于0').max(999999.99, '成本金额不能超过999999.99').default(0).optional().openapi({
+    description: '成本金额',
+    example: 50.00
+  }),
+  freightAmount: z.coerce.number<number>().min(0, '运费不能小于0').max(999999.99, '运费不能超过999999.99').default(0).optional().openapi({
+    description: '运费',
+    example: 10.00
+  }),
+  discountAmount: z.coerce.number<number>().min(0, '优惠金额不能小于0').max(999999.99, '优惠金额不能超过999999.99').default(0).optional().openapi({
+    description: '优惠金额',
+    example: 5.00
+  }),
+  payAmount: z.coerce.number<number>().min(0, '实际支付金额不能小于0').max(999999.99, '实际支付金额不能超过999999.99').default(0).optional().openapi({
+    description: '实际支付金额',
+    example: 94.99
+  }),
+  deviceNo: z.string().max(255, '设备编号最多255个字符').nullable().optional().openapi({
+    description: '设备编号',
+    example: 'DEV001234'
+  }),
+  description: z.string().max(255, '订单描述最多255个字符').nullable().optional().openapi({
+    description: '订单描述',
+    example: '购买商品'
+  }),
+  goodsDetail: z.string().max(2000, '订单详情最多2000个字符').nullable().optional().openapi({
+    description: '订单详情(json格式)',
+    example: '[{"goodsId":1,"name":"商品1","price":99.99,"num":1}]'
+  }),
+  goodsTag: z.string().max(255, '订单优惠标记最多255个字符').nullable().optional().openapi({
+    description: '订单优惠标记',
+    example: '满100减5'
+  }),
+  address: z.string().max(255, '地址最多255个字符').nullable().optional().openapi({
+    description: '地址',
+    example: '北京市朝阳区xxx路xxx号'
+  }),
+  orderType: z.coerce.number().int().min(1, '订单类型最小为1').max(2, '订单类型最大为2').default(1).openapi({
+    description: '订单类型 1实物订单 2虚拟订单',
+    example: 1
+  }),
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').default(0).openapi({
+    description: '支付类型1积分2礼券',
+    example: 1
+  }),
+  payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').default(0).openapi({
+    description: '支付状态 0未支付1支付中2支付成功3已退款4支付失败5订单关闭',
+    example: 2
+  }),
+  state: z.coerce.number().int().min(0, '订单状态最小为0').max(3, '订单状态最大为3').default(0).openapi({
+    description: '订单状态 0未发货1已发货2收货成功3已退货',
+    example: 0
+  }),
+  userPhone: z.string().max(50, '用户手机号最多50个字符').nullable().optional().openapi({
+    description: '用户手机号',
+    example: '13800138000'
+  }),
+  merchantId: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '商户id',
+    example: 1
+  }),
+  merchantNo: z.coerce.number().int().min(0, '不能小于0').nullable().optional().openapi({
+    description: '商户号',
+    example: 1001
+  }),
+  supplierId: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '供货商id',
+    example: 1
+  }),
+  addressId: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '地址id',
+    example: 1
+  }),
+  receiverMobile: z.string().max(255, '收货人手机号最多255个字符').nullable().optional().openapi({
+    description: '收货人手机号',
+    example: '13800138000'
+  }),
+  recevierName: z.string().max(255, '收货人姓名最多255个字符').nullable().optional().openapi({
+    description: '收货人姓名',
+    example: '张三'
+  }),
+  recevierProvince: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '收货人所在省',
+    example: 110000
+  }),
+  recevierCity: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '收货人所在市',
+    example: 110100
+  }),
+  recevierDistrict: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '收货人所在区',
+    example: 110105
+  }),
+  recevierTown: z.coerce.number().int().min(0, '不能小于0').default(0).openapi({
+    description: '收货人所在街道',
+    example: 110105001
+  }),
+  refundTime: z.coerce.date().nullable().optional().openapi({
+    description: '退款时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  closeTime: z.coerce.date().nullable().optional().openapi({
+    description: '订单关闭时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  remark: z.string().max(255, '管理员备注信息最多255个字符').nullable().optional().openapi({
+    description: '管理员备注信息',
+    example: '请尽快发货'
+  }),
+  deliveryType: z.coerce.number().int().min(0, '发货方式最小为0').max(4, '发货方式最大为4').default(0).optional().openapi({
+    description: '发货方式 0未发货 1物流快递 2同城配送 3用户自提 4虚拟发货',
+    example: 1
+  }),
+  deliveryCompany: z.string().max(100, '快递公司最多100个字符').nullable().optional().openapi({
+    description: '快递公司',
+    example: '顺丰速运'
+  }),
+  deliveryNo: z.string().max(100, '快递单号最多100个字符').nullable().optional().openapi({
+    description: '快递单号',
+    example: 'SF1234567890'
+  }),
+  deliveryTime: z.coerce.date().nullable().optional().openapi({
+    description: '发货时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  deliveryRemark: z.string().max(500, '发货备注最多500个字符').nullable().optional().openapi({
+    description: '发货备注',
+    example: '已包装好,请注意查收'
+  })
+});
+
+// 更新订单DTO
+export const UpdateOrderDto = z.object({
+  orderNo: z.string().min(1, '订单号不能为空').max(50, '订单号最多50个字符').optional().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  userId: z.number().int().positive('用户ID必须是正整数').optional().openapi({
+    description: '用户ID',
+    example: 1
+  }),
+  authCode: z.string().max(32, '付款码最多32个字符').nullable().optional().openapi({
+    description: '付款码',
+    example: '12345678901234567890123456789012'
+  }),
+  cardNo: z.string().max(32, '卡号最多32个字符').nullable().optional().openapi({
+    description: '卡号',
+    example: '6222********1234'
+  }),
+  sjtCardNo: z.string().max(32, '盛京通卡号最多32个字符').nullable().optional().openapi({
+    description: '盛京通卡号',
+    example: 'SJT1234567890'
+  }),
+  amount: z.coerce.number<number>().min(0, '订单金额不能小于0').max(999999.99, '订单金额不能超过999999.99').optional().openapi({
+    description: '订单金额',
+    example: 99.99
+  }),
+  costAmount: z.coerce.number<number>().min(0, '成本金额不能小于0').max(999999.99, '成本金额不能超过999999.99').optional().openapi({
+    description: '成本金额',
+    example: 50.00
+  }),
+  freightAmount: z.coerce.number<number>().min(0, '运费不能小于0').max(999999.99, '运费不能超过999999.99').optional().openapi({
+    description: '运费',
+    example: 10.00
+  }),
+  discountAmount: z.coerce.number<number>().min(0, '优惠金额不能小于0').max(999999.99, '优惠金额不能超过999999.99').optional().openapi({
+    description: '优惠金额',
+    example: 5.00
+  }),
+  payAmount: z.coerce.number<number>().min(0, '实际支付金额不能小于0').max(999999.99, '实际支付金额不能超过999999.99').optional().openapi({
+    description: '实际支付金额',
+    example: 94.99
+  }),
+  deviceNo: z.string().max(255, '设备编号最多255个字符').nullable().optional().openapi({
+    description: '设备编号',
+    example: 'DEV001234'
+  }),
+  description: z.string().max(255, '订单描述最多255个字符').nullable().optional().openapi({
+    description: '订单描述',
+    example: '购买商品'
+  }),
+  goodsDetail: z.string().max(2000, '订单详情最多2000个字符').nullable().optional().openapi({
+    description: '订单详情(json格式)',
+    example: '[{"goodsId":1,"name":"商品1","price":99.99,"num":1}]'
+  }),
+  goodsTag: z.string().max(255, '订单优惠标记最多255个字符').nullable().optional().openapi({
+    description: '订单优惠标记',
+    example: '满100减5'
+  }),
+  address: z.string().max(255, '地址最多255个字符').nullable().optional().openapi({
+    description: '地址',
+    example: '北京市朝阳区xxx路xxx号'
+  }),
+  orderType: z.coerce.number().int().min(1, '订单类型最小为1').max(2, '订单类型最大为2').optional().openapi({
+    description: '订单类型 1实物订单 2虚拟订单',
+    example: 1
+  }),
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').optional().openapi({
+    description: '支付类型1积分2礼券',
+    example: 1
+  }),
+  payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').optional().openapi({
+    description: '支付状态 0未支付1支付中2支付成功3已退款4支付失败5订单关闭',
+    example: 2
+  }),
+  state: z.coerce.number().int().min(0, '订单状态最小为0').max(3, '订单状态最大为3').optional().openapi({
+    description: '订单状态 0未发货1已发货2收货成功3已退货',
+    example: 0
+  }),
+  userPhone: z.string().max(50, '用户手机号最多50个字符').nullable().optional().openapi({
+    description: '用户手机号',
+    example: '13800138000'
+  }),
+  merchantId: z.coerce.number().int().positive().optional().openapi({
+    description: '商户id',
+    example: 1
+  }),
+  merchantNo: z.coerce.number().int().min(0, '不能小于0').nullable().optional().openapi({
+    description: '商户号',
+    example: 1001
+  }),
+  supplierId: z.coerce.number().int().positive().optional().openapi({
+    description: '供货商id',
+    example: 1
+  }),
+  addressId: z.coerce.number().int().positive().optional().openapi({
+    description: '地址id',
+    example: 1
+  }),
+  receiverMobile: z.string().max(255, '收货人手机号最多255个字符').nullable().optional().openapi({
+    description: '收货人手机号',
+    example: '13800138000'
+  }),
+  recevierName: z.string().max(255, '收货人姓名最多255个字符').nullable().optional().openapi({
+    description: '收货人姓名',
+    example: '张三'
+  }),
+  recevierProvince: z.coerce.number().int().positive().optional().openapi({
+    description: '收货人所在省',
+    example: 110000
+  }),
+  recevierCity: z.coerce.number().int().positive().optional().openapi({
+    description: '收货人所在市',
+    example: 110100
+  }),
+  recevierDistrict: z.coerce.number().int().positive().optional().openapi({
+    description: '收货人所在区',
+    example: 110105
+  }),
+  recevierTown: z.coerce.number().int().positive().optional().openapi({
+    description: '收货人所在街道',
+    example: 110105001
+  }),
+  refundTime: z.coerce.date().nullable().optional().openapi({
+    description: '退款时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  closeTime: z.coerce.date().nullable().optional().openapi({
+    description: '订单关闭时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  remark: z.string().max(255, '管理员备注信息最多255个字符').nullable().optional().openapi({
+    description: '管理员备注信息',
+    example: '请尽快发货'
+  }),
+  deliveryType: z.coerce.number().int().min(0, '发货方式最小为0').max(4, '发货方式最大为4').default(0).optional().openapi({
+    description: '发货方式 0未发货 1物流快递 2同城配送 3用户自提 4虚拟发货',
+    example: 1
+  }),
+  deliveryCompany: z.string().max(100, '快递公司最多100个字符').nullable().optional().openapi({
+    description: '快递公司',
+    example: '顺丰速运'
+  }),
+  deliveryNo: z.string().max(100, '快递单号最多100个字符').nullable().optional().openapi({
+    description: '快递单号',
+    example: 'SF1234567890'
+  }),
+  deliveryTime: z.coerce.date().nullable().optional().openapi({
+    description: '发货时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  deliveryRemark: z.string().max(500, '发货备注最多500个字符').nullable().optional().openapi({
+    description: '发货备注',
+    example: '已包装好,请注意查收'
+  })
+});
+
+// 订单列表响应Schema
+export const OrderListResponse = z.object({
+  data: z.array(OrderSchema),
+  pagination: z.object({
+    total: z.number().openapi({ example: 100, description: '总记录数' }),
+    current: z.number().openapi({ example: 1, description: '当前页码' }),
+    pageSize: z.number().openapi({ example: 10, description: '每页数量' })
+  })
+});
+
+// 订单列表Schema - 用于列表查询,只包含必要字段且全部可选
+export const OrderListSchema = z.object({
+  id: z.coerce.number().int().positive().optional().openapi({
+    description: '订单ID',
+    example: 1
+  }),
+  tenantId: z.number().int().positive().optional().openapi({
+    description: '租户ID',
+    example: 1
+  }),
+  orderNo: z.string().min(1, '订单号不能为空').max(50, '订单号最多50个字符').optional().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  userId: z.number().int().positive().optional().openapi({
+    description: '用户ID',
+    example: 1
+  }),
+  amount: z.coerce.number<number>().min(0, '订单金额不能小于0').max(999999.99, '订单金额不能超过999999.99').optional().openapi({
+    description: '订单金额',
+    example: 99.99
+  }),
+  payAmount: z.coerce.number<number>().min(0, '实际支付金额不能小于0').max(999999.99, '实际支付金额不能超过999999.99').default(0).optional().openapi({
+    description: '实际支付金额',
+    example: 94.99
+  }),
+  orderType: z.coerce.number().int().min(1, '订单类型最小为1').max(2, '订单类型最大为2').default(1).optional().openapi({
+    description: '订单类型 1实物订单 2虚拟订单',
+    example: 1
+  }),
+  payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').default(0).optional().openapi({
+    description: '支付状态 0未支付1支付中2支付成功3已退款4支付失败5订单关闭',
+    example: 2
+  }),
+  state: z.coerce.number().int().min(0, '订单状态最小为0').max(3, '订单状态最大为3').default(0).optional().openapi({
+    description: '订单状态 0未发货1已发货2收货成功3已退货',
+    example: 0
+  }),
+  createdAt: z.coerce.date().optional().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().optional().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  // 关联实体
+  user: z.object({
+    id: z.number().int().positive().openapi({ description: '用户ID' }),
+    username: z.string().openapi({ description: '用户名', example: 'user123' }),
+    phone: z.string().nullable().openapi({ description: '手机号', example: '13800138000' })
+  }).nullable().optional().openapi({
+    description: '用户信息'
+  }),
+  merchant: z.object({
+    id: z.number().int().positive().openapi({ description: '商户ID' }),
+    name: z.string().openapi({ description: '商户名称', example: '商户A' })
+  }).nullable().optional().openapi({
+    description: '商户信息'
+  }),
+  supplier: z.object({
+    id: z.number().int().positive().openapi({ description: '供货商ID' }),
+    name: z.string().openapi({ description: '供货商名称', example: '供货商A' })
+  }).nullable().optional().openapi({
+    description: '供货商信息'
+  }),
+  deliveryAddress: z.object({
+    id: z.number().int().positive().openapi({ description: '地址ID' }),
+    name: z.string().openapi({ description: '收货人姓名', example: '张三' }),
+    phone: z.string().openapi({ description: '收货人电话', example: '13800138000' }),
+    address: z.string().openapi({ description: '详细地址', example: '北京市朝阳区xxx路xxx号' })
+  }).nullable().optional().openapi({
+    description: '收货地址信息'
+  }),
+  // 订单商品信息
+  orderGoods: z.array(z.object({
+    id: z.number().int().positive().openapi({ description: '订单商品ID' }),
+    goodsId: z.number().int().positive().openapi({ description: '商品ID' }),
+    goodsName: z.string().openapi({ description: '商品名称', example: '商品A' }),
+    price: z.coerce.number<number>().openapi({ description: '商品价格', example: 99.99 }),
+    num: z.number().int().positive().openapi({ description: '商品数量', example: 1 }),
+    imageFileId: z.number().int().positive().nullable().openapi({ description: '商品图片文件ID', example: 1 }),
+    imageFile: z.object({
+      id: z.number().int().positive().openapi({ description: '文件ID' }),
+      fullUrl: z.url().openapi({ description: '完整文件访问URL', example: 'https://minio.example.com/d8dai/uploads/goods/2024/product-image.jpg' })
+    }).nullable().optional().openapi({
+      description: '商品图片信息'
+    })
+  })).optional().openapi({
+    description: '订单商品列表'
+  })
+});

+ 13 - 0
test_schema.js

@@ -0,0 +1,13 @@
+// 简单测试schema语法
+const schema = {
+  user: {
+    id: 1,
+    username: "wx_1764673897616_aeevhi",
+    phone: null,
+    openid: "ov95P19u38KzJUwNymZXriMiXqoQ"
+  }
+};
+
+console.log("测试数据:", JSON.stringify(schema, null, 2));
+console.log("user.openid 存在:", schema.user.openid !== undefined);
+console.log("user.openid 值:", schema.user.openid);