Bläddra i källkod

✨ feat(quantity-selector): 新增通用数量选择器组件

- 创建可复用的数量选择器组件,支持加减按钮和直接输入
- 提供两种样式变体:默认购物车样式和商品详情页样式
- 实现库存验证和单次购买限制功能
- 支持事件冒泡控制和禁用状态

🐛 fix(cart): 修复购物车数量选择器交互问题

- 替换购物车页面的数量选择器为新的通用组件
- 修复数量输入时事件冒泡导致的商品卡片误点击问题
- 优化数量输入验证逻辑,支持空输入状态
- 修复数量小于等于0时自动设为1而不是删除商品

♻️ refactor(cart): 重构购物车数量选择器样式

- 为购物车页面创建专用的数量选择器样式类名
- 移除旧的商品步进器样式代码
- 优化按钮和输入框的布局和交互效果

✅ test(cart): 更新购物车相关测试用例

- 修复测试中购物车hook的导入路径
- 更新测试配置以支持新的上下文结构
- 确保测试用例与重构后的代码兼容

✨ feat(auth): 新增微信小店发货功能

- 添加微信小店发货API接口,支持标记订单为已发货
- 实现快递公司名称到微信小店delivery_id的映射功能
- 重构微信access_token获取逻辑为独立方法
- 优化模板消息发送功能,复用access_token获取逻辑
yourname 1 månad sedan
förälder
incheckning
a6a7e5ad36

+ 81 - 0
mini/src/components/quantity-selector/index.css

@@ -0,0 +1,81 @@
+.quantity-controls {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+}
+
+.quantity-btn {
+  width: 60rpx;
+  height: 60rpx;
+  min-width: 60rpx;
+  padding: 0;
+  border-radius: 8rpx;
+  border: 1rpx solid #e5e7eb;
+  background-color: #f9fafb;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  color: #374151;
+}
+
+.quantity-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.quantity-input {
+  width: 60rpx;
+  height: 60rpx;
+  text-align: center;
+  border: 1rpx solid #e5e7eb;
+  border-radius: 8rpx;
+  font-size: 28rpx;
+  color: #374151;
+  background-color: #ffffff;
+}
+
+.quantity-input:disabled {
+  background-color: #f9fafb;
+  opacity: 0.5;
+}
+
+/* 商品详情页样式 - 宽输入框 */
+.quantity-detail .quantity-input {
+  width: 300rpx;
+  height: 64rpx;
+  border: none;
+  font-size: 32rpx;
+  color: #333;
+  font-weight: 600;
+  text-align: center;
+  background: transparent;
+  padding: 0;
+  margin: 0;
+}
+
+.quantity-detail .quantity-input:focus {
+  outline: none;
+}
+
+.quantity-detail .quantity-btn {
+  width: 64rpx;
+  height: 64rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: transparent;
+  border: none;
+  color: #ff9500;
+  font-size: 32rpx;
+  font-weight: bold;
+  padding: 0;
+}
+
+.quantity-detail .quantity-btn:active {
+  opacity: 0.7;
+}
+
+.quantity-detail .quantity-btn:disabled {
+  color: #ccc;
+}

+ 210 - 0
mini/src/components/quantity-selector/index.tsx

@@ -0,0 +1,210 @@
+import { View, Input } from '@tarojs/components'
+import { Button } from '@/components/ui/button'
+import Taro from '@tarojs/taro'
+import { useState, useEffect } from 'react'
+import './index.css'
+
+interface QuantitySelectorProps {
+  /** 当前数量 */
+  value: number
+  /** 数量变化时的回调函数 */
+  onChange: (quantity: number) => void
+  /** 最大可购买数量(考虑库存和单次购买限制) */
+  maxQuantity?: number
+  /** 商品库存 */
+  stock?: number
+  /** 是否禁用 */
+  disabled?: boolean
+  /** 自定义类名 */
+  className?: string
+  /** 是否阻止事件冒泡 */
+  stopPropagation?: boolean
+  /** 样式变体:'default' 为购物车样式(窄输入框),'detail' 为商品详情样式(宽输入框) */
+  variant?: 'default' | 'detail'
+}
+
+export function QuantitySelector({
+  value,
+  onChange,
+  maxQuantity = 999,
+  stock = 999,
+  disabled = false,
+  className = '',
+  stopPropagation = true,
+  variant = 'default'
+}: QuantitySelectorProps) {
+  const [localQuantity, setLocalQuantity] = useState(value)
+  const [inputValue, setInputValue] = useState(value === 0 ? '' : value.toString())
+
+  // 同步外部value变化
+  useEffect(() => {
+    setLocalQuantity(value)
+    setInputValue(value === 0 ? '' : value.toString())
+  }, [value])
+
+  // 获取实际最大可购买数量
+  const getActualMaxQuantity = () => {
+    return Math.min(stock, maxQuantity)
+  }
+
+  // 处理减少数量
+  const handleDecrease = (e?: any) => {
+    if (stopPropagation && e) {
+      e.stopPropagation()
+    }
+
+    if (disabled) return
+
+    const currentQty = localQuantity === 0 ? 1 : localQuantity
+    const newQuantity = Math.max(1, currentQty - 1)
+    setLocalQuantity(newQuantity)
+    setInputValue(newQuantity.toString())
+    onChange(newQuantity)
+  }
+
+  // 处理增加数量
+  const handleIncrease = (e?: any) => {
+    if (stopPropagation && e) {
+      e.stopPropagation()
+    }
+
+    if (disabled) return
+
+    const currentQty = localQuantity === 0 ? 1 : localQuantity
+    const actualMaxQuantity = getActualMaxQuantity()
+
+    if (currentQty >= actualMaxQuantity) {
+      if (actualMaxQuantity === stock) {
+        Taro.showToast({
+          title: `库存只有${stock}件`,
+          icon: 'none',
+          duration: 1500
+        })
+      } else {
+        Taro.showToast({
+          title: `单次最多购买${maxQuantity}件`,
+          icon: 'none',
+          duration: 1500
+        })
+      }
+      return
+    }
+
+    const newQuantity = currentQty + 1
+    setLocalQuantity(newQuantity)
+    setInputValue(newQuantity.toString())
+    onChange(newQuantity)
+  }
+
+  // 处理数量输入变化
+  const handleQuantityChange = (e: any) => {
+    if (stopPropagation && e) {
+      e.stopPropagation()
+    }
+
+    const value = e.detail.value
+    if (disabled) return
+
+    // 清除非数字字符
+    const cleanedValue = value.replace(/[^\d]/g, '')
+
+    // 如果输入为空,设为空字符串(允许用户删除)
+    if (cleanedValue === '') {
+      setInputValue('')
+      setLocalQuantity(0)
+      onChange(0)
+      return
+    }
+
+    const numValue = parseInt(cleanedValue)
+
+    // 验证最小值
+    if (numValue < 1) {
+      setInputValue('1')
+      setLocalQuantity(1)
+      onChange(1)
+      Taro.showToast({
+        title: '数量不能小于1',
+        icon: 'none',
+        duration: 1500
+      })
+      return
+    }
+
+    // 验证最大值
+    const actualMaxQuantity = getActualMaxQuantity()
+    if (numValue > actualMaxQuantity) {
+      setInputValue(actualMaxQuantity.toString())
+      setLocalQuantity(actualMaxQuantity)
+      onChange(actualMaxQuantity)
+      if (actualMaxQuantity === stock) {
+        Taro.showToast({
+          title: `库存只有${stock}件`,
+          icon: 'none',
+          duration: 1500
+        })
+      } else {
+        Taro.showToast({
+          title: `单次最多购买${maxQuantity}件`,
+          icon: 'none',
+          duration: 1500
+        })
+      }
+      return
+    }
+
+    setInputValue(cleanedValue)
+    setLocalQuantity(numValue)
+    onChange(numValue)
+  }
+
+  // 处理输入框失去焦点(完成输入)
+  const handleQuantityBlur = (e?: any) => {
+    if (stopPropagation && e) {
+      e.stopPropagation()
+    }
+
+    if (disabled) return
+
+    // 如果数量为0(表示空输入),设为1
+    if (localQuantity === 0) {
+      setInputValue('1')
+      setLocalQuantity(1)
+      onChange(1)
+    }
+  }
+
+  return (
+    <View className={`quantity-controls ${className} quantity-${variant}`}>
+      <Button
+        size="sm"
+        variant="ghost"
+        className="quantity-btn"
+        onClick={handleDecrease}
+        disabled={disabled || localQuantity <= 1}
+      >
+        -
+      </Button>
+      <Input
+        className="quantity-input"
+        type="number"
+        value={inputValue}
+        onInput={handleQuantityChange}
+        onBlur={handleQuantityBlur}
+        placeholder="1"
+        maxlength={3}
+        confirmType="done"
+        disabled={disabled}
+      />
+      <Button
+        size="sm"
+        variant="ghost"
+        className="quantity-btn"
+        onClick={handleIncrease}
+        disabled={disabled || localQuantity >= getActualMaxQuantity()}
+      >
+        +
+      </Button>
+    </View>
+  )
+}

+ 2 - 2
mini/src/contexts/CartContext.tsx

@@ -141,9 +141,9 @@ export const CartProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
     const item = cart.items.find(item => item.id === id)
     const item = cart.items.find(item => item.id === id)
     if (!item) return
     if (!item) return
 
 
+    // 当数量小于等于0时,设为1而不是删除商品
     if (quantity <= 0) {
     if (quantity <= 0) {
-      removeFromCart(id)
-      return
+      quantity = 1
     }
     }
 
 
     if (quantity > item.stock) {
     if (quantity > item.stock) {

+ 54 - 2
mini/src/pages/cart/index.css

@@ -265,7 +265,55 @@
   margin-left: 12rpx;
   margin-left: 12rpx;
 }
 }
 
 
-/* 数量选择器样式 */
+/* 购物车数量选择器样式 - 使用更具体的类名避免冲突 */
+.cart-quantity-controls {
+  display: flex;
+  align-items: center;
+  border: 1rpx solid #e5e7eb;
+  border-radius: 8rpx;
+  overflow: hidden;
+  width: fit-content;
+}
+
+.cart-quantity-btn {
+  width: 60rpx;
+  height: 60rpx;
+  min-width: 60rpx;
+  padding: 0;
+  background-color: #f9fafb;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  color: #374151;
+  border: none;
+  border-radius: 0;
+}
+
+.cart-quantity-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.cart-quantity-input {
+  width: 80rpx; /* 调整为80rpx */
+  height: 60rpx;
+  text-align: center;
+  border: none;
+  border-left: 1rpx solid #e5e7eb;
+  border-right: 1rpx solid #e5e7eb;
+  border-radius: 0;
+  font-size: 28rpx;
+  color: #374151;
+  background-color: #ffffff;
+  padding: 0;
+}
+
+.cart-quantity-input:disabled {
+  background-color: #f9fafb;
+  opacity: 0.5;
+}
+
 .goods-stepper {
 .goods-stepper {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -323,6 +371,8 @@
   border-radius: 8rpx;
   border-radius: 8rpx;
   font-size: 24rpx;
   font-size: 24rpx;
   cursor: pointer;
   cursor: pointer;
+  pointer-events: auto; /* 确保只有点击删除按钮本身才触发事件 */
+  z-index: 1; /* 较低的z-index */
 }
 }
 
 
 
 
@@ -398,4 +448,6 @@
 .settle-btn.disabled {
 .settle-btn.disabled {
   background: #ccc;
   background: #ccc;
   color: #999;
   color: #999;
-}
+}
+
+

+ 246 - 5
mini/src/pages/cart/index.tsx

@@ -1,4 +1,4 @@
-import { View, ScrollView, Text } from '@tarojs/components'
+import { View, ScrollView, Text, Input } from '@tarojs/components'
 import { useState, useEffect } from 'react'
 import { useState, useEffect } from 'react'
 import { useQueries } from '@tanstack/react-query'
 import { useQueries } from '@tanstack/react-query'
 import Taro from '@tarojs/taro'
 import Taro from '@tarojs/taro'
@@ -15,6 +15,8 @@ export default function CartPage() {
   const { cart, updateQuantity, removeFromCart, clearCart, isLoading } = useCart()
   const { cart, updateQuantity, removeFromCart, clearCart, isLoading } = useCart()
   const [selectedItems, setSelectedItems] = useState<number[]>([])
   const [selectedItems, setSelectedItems] = useState<number[]>([])
   const [showSkeleton, setShowSkeleton] = useState(true)
   const [showSkeleton, setShowSkeleton] = useState(true)
+  // 为每个商品维护本地输入值,用于显示空字符串
+  const [inputValues, setInputValues] = useState<{[key: number]: string}>({})
 
 
   // 为每个购物车商品创建查询,从数据库重新获取最新信息
   // 为每个购物车商品创建查询,从数据库重新获取最新信息
   const goodsQueries = useQueries({
   const goodsQueries = useQueries({
@@ -103,6 +105,7 @@ export default function CartPage() {
     })
     })
   }
   }
 
 
+
   // 骨架屏组件
   // 骨架屏组件
   const CartSkeleton = () => (
   const CartSkeleton = () => (
     <View className="cart-skeleton">
     <View className="cart-skeleton">
@@ -259,12 +262,246 @@ export default function CartPage() {
                           </View>
                           </View>
 
 
                           {/* 数量选择器 */}
                           {/* 数量选择器 */}
-                          <View className="goods-stepper">
+                          <View
+                            className="cart-quantity-controls"
+                            onClick={(e) => {
+                              e.stopPropagation(); // 阻止事件冒泡到商品卡片
+                            }}
+                            onTouchStart={(e) => {
+                              e.stopPropagation(); // 阻止触摸事件冒泡
+                            }}
+                          >
+                            <Button
+                              size="sm"
+                              variant="ghost"
+                              className="cart-quantity-btn"
+                              onClick={(e) => {
+                                e.stopPropagation(); // 阻止事件冒泡
+                                // 清除本地输入值
+                                setInputValues(prev => {
+                                  const newValues = { ...prev };
+                                  delete newValues[item.id];
+                                  return newValues;
+                                });
+                                updateQuantity(item.id, Math.max(1, item.quantity - 1));
+                              }}
+                              onTouchStart={(e) => {
+                                e.stopPropagation(); // 阻止触摸开始事件冒泡
+                              }}
+                              onTouchEnd={(e) => {
+                                e.stopPropagation(); // 阻止触摸结束事件冒泡
+                              }}
+                              disabled={item.quantity <= 1}
+                            >
+                              -
+                            </Button>
+                            <Input
+                              className="cart-quantity-input"
+                              type="number"
+                              value={inputValues[item.id] !== undefined ? inputValues[item.id] : item.quantity.toString()}
+                              onClick={(e) => {
+                                e.stopPropagation(); // 阻止点击事件冒泡
+                              }}
+                              onTouchStart={(e) => {
+                                e.stopPropagation(); // 阻止触摸开始事件冒泡
+                              }}
+                              onTouchEnd={(e) => {
+                                e.stopPropagation(); // 阻止触摸结束事件冒泡
+                              }}
+                              onInput={(e) => {
+                                e.stopPropagation(); // 阻止事件冒泡
+                                const value = e.detail.value;
+                                // 清除非数字字符
+                                const cleanedValue = value.replace(/[^\d]/g, '');
+
+                                // 更新本地输入值
+                                setInputValues(prev => ({
+                                  ...prev,
+                                  [item.id]: value // 使用原始值,允许显示空字符串
+                                }));
+
+                                // 如果输入为空,只更新本地状态,不更新购物车
+                                if (cleanedValue === '') {
+                                  return;
+                                }
+
+                                const numValue = parseInt(cleanedValue);
+
+                                // 验证最小值
+                                if (numValue < 1) {
+                                  updateQuantity(item.id, 1);
+                                  Taro.showToast({
+                                    title: '数量不能小于1',
+                                    icon: 'none',
+                                    duration: 1500
+                                  });
+                                  return;
+                                }
+
+                                // 验证最大值(考虑库存和单次购买限制)
+                                const maxQuantity = Math.min(goodsStock, 999);
+                                if (numValue > maxQuantity) {
+                                  updateQuantity(item.id, maxQuantity);
+                                  if (maxQuantity === goodsStock) {
+                                    Taro.showToast({
+                                      title: `库存只有${goodsStock}件`,
+                                      icon: 'none',
+                                      duration: 1500
+                                    });
+                                  } else {
+                                    Taro.showToast({
+                                      title: '单次最多购买999件',
+                                      icon: 'none',
+                                      duration: 1500
+                                    });
+                                  }
+                                  return;
+                                }
+
+                                updateQuantity(item.id, numValue);
+                              }}
+                              onBlur={(e) => {
+                                e.stopPropagation(); // 阻止事件冒泡
+                                const value = e.detail.value;
+                                const cleanedValue = value.replace(/[^\d]/g, '');
+
+                                // 清除本地输入值,恢复显示购物车数量
+                                setInputValues(prev => {
+                                  const newValues = { ...prev };
+                                  delete newValues[item.id];
+                                  return newValues;
+                                });
+
+                                // 如果输入为空,不更新数量
+                                if (cleanedValue === '') {
+                                  return;
+                                }
+
+                                const numValue = parseInt(cleanedValue);
+
+                                // 如果输入无效(小于1),设为1
+                                if (numValue < 1) {
+                                  updateQuantity(item.id, 1);
+                                  Taro.showToast({
+                                    title: '数量不能小于1',
+                                    icon: 'none',
+                                    duration: 1500
+                                  });
+                                }
+                              }}
+                              onFocus={(e) => {
+                                e.stopPropagation(); // 阻止焦点事件冒泡
+                              }}
+                              onConfirm={(e) => {
+                                e.stopPropagation(); // 阻止事件冒泡
+                                const value = e.detail.value;
+                                const cleanedValue = value.replace(/[^\d]/g, '');
+
+                                // 清除本地输入值
+                                setInputValues(prev => {
+                                  const newValues = { ...prev };
+                                  delete newValues[item.id];
+                                  return newValues;
+                                });
+
+                                // 如果输入为空,设为1
+                                if (cleanedValue === '') {
+                                  updateQuantity(item.id, 1);
+                                  return;
+                                }
+
+                                const numValue = parseInt(cleanedValue);
+
+                                // 验证最小值
+                                if (numValue < 1) {
+                                  updateQuantity(item.id, 1);
+                                  Taro.showToast({
+                                    title: '数量不能小于1',
+                                    icon: 'none',
+                                    duration: 1500
+                                  });
+                                  return;
+                                }
+
+                                // 验证最大值(考虑库存和单次购买限制)
+                                const maxQuantity = Math.min(goodsStock, 999);
+                                if (numValue > maxQuantity) {
+                                  updateQuantity(item.id, maxQuantity);
+                                  if (maxQuantity === goodsStock) {
+                                    Taro.showToast({
+                                      title: `库存只有${goodsStock}件`,
+                                      icon: 'none',
+                                      duration: 1500
+                                    });
+                                  } else {
+                                    Taro.showToast({
+                                      title: '单次最多购买999件',
+                                      icon: 'none',
+                                      duration: 1500
+                                    });
+                                  }
+                                  return;
+                                }
+
+                                updateQuantity(item.id, numValue);
+                              }}
+                              placeholder="1"
+                              maxlength={3}
+                              confirmType="done"
+                            />
+                            <Button
+                              size="sm"
+                              variant="ghost"
+                              className="cart-quantity-btn"
+                              onClick={(e) => {
+                                e.stopPropagation(); // 阻止事件冒泡
+                                // 清除本地输入值
+                                setInputValues(prev => {
+                                  const newValues = { ...prev };
+                                  delete newValues[item.id];
+                                  return newValues;
+                                });
+                                // 验证最大值(考虑库存和单次购买限制)
+                                const maxQuantity = Math.min(goodsStock, 999);
+                                if (item.quantity >= maxQuantity) {
+                                  if (maxQuantity === goodsStock) {
+                                    Taro.showToast({
+                                      title: `库存只有${goodsStock}件`,
+                                      icon: 'none',
+                                      duration: 1500
+                                    });
+                                  } else {
+                                    Taro.showToast({
+                                      title: '单次最多购买999件',
+                                      icon: 'none',
+                                      duration: 1500
+                                    });
+                                  }
+                                  return;
+                                }
+                                updateQuantity(item.id, item.quantity + 1);
+                              }}
+                              onTouchStart={(e) => {
+                                e.stopPropagation(); // 阻止触摸开始事件冒泡
+                              }}
+                              onTouchEnd={(e) => {
+                                e.stopPropagation(); // 阻止触摸结束事件冒泡
+                              }}
+                              disabled={item.quantity >= Math.min(goodsStock, 999)}
+                            >
+                              +
+                            </Button>
+                          </View>
+
+                          {/* <View className="goods-stepper">
                             <Button
                             <Button
                               size="sm"
                               size="sm"
                               variant="ghost"
                               variant="ghost"
                               className="stepper-btn minus"
                               className="stepper-btn minus"
-                              onClick={() => updateQuantity(item.id, Math.max(1, item.quantity - 1))}
+                              onClick={(e) => {
+                                e.stopPropagation(); // 阻止事件冒泡
+                                updateQuantity(item.id, Math.max(1, item.quantity - 1));
+                              }}
                               disabled={item.quantity <= 1}
                               disabled={item.quantity <= 1}
                             >
                             >
                               <View className="i-heroicons-minus-20-solid w-3 h-3" />
                               <View className="i-heroicons-minus-20-solid w-3 h-3" />
@@ -274,11 +511,15 @@ export default function CartPage() {
                               size="sm"
                               size="sm"
                               variant="ghost"
                               variant="ghost"
                               className="stepper-btn plus"
                               className="stepper-btn plus"
-                              onClick={() => updateQuantity(item.id, item.quantity + 1)}
+                              onClick={(e) => {
+                                e.stopPropagation(); // 阻止事件冒泡
+                                updateQuantity(item.id, item.quantity + 1);
+                              }}
                             >
                             >
                               <View className="i-heroicons-plus-20-solid w-3 h-3" />
                               <View className="i-heroicons-plus-20-solid w-3 h-3" />
                             </Button>
                             </Button>
-                          </View>
+                          </View> */}
+
                         </View>
                         </View>
                       </View>
                       </View>
                     </View>
                     </View>

+ 1 - 1
mini/tests/unit/pages/cart/basic.test.tsx

@@ -15,7 +15,7 @@ jest.mock('@tarojs/taro', () => ({
 }))
 }))
 
 
 // Mock购物车hook
 // Mock购物车hook
-jest.mock('@/utils/cart', () => ({
+jest.mock('@/contexts/CartContext', () => ({
   useCart: () => ({
   useCart: () => ({
     cart: {
     cart: {
       items: [
       items: [

+ 4 - 4
mini/tests/unit/pages/cart/index.test.tsx

@@ -1,5 +1,6 @@
 import React from 'react'
 import React from 'react'
 import { render, fireEvent } from '@testing-library/react'
 import { render, fireEvent } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
 import Taro from '@tarojs/taro'
 import Taro from '@tarojs/taro'
 import CartPage from '@/pages/cart/index'
 import CartPage from '@/pages/cart/index'
 
 
@@ -9,14 +10,15 @@ jest.mock('@tarojs/taro', () => ({
     navigateBack: jest.fn(),
     navigateBack: jest.fn(),
     navigateTo: jest.fn(),
     navigateTo: jest.fn(),
     showToast: jest.fn(),
     showToast: jest.fn(),
-    showModal: jest.fn(),
+    showModal: jest.fn(() => Promise.resolve({ confirm: true })),
     getStorageSync: jest.fn(),
     getStorageSync: jest.fn(),
     setStorageSync: jest.fn(),
     setStorageSync: jest.fn(),
+    removeStorageSync: jest.fn(),
   },
   },
 }))
 }))
 
 
 // Mock购物车hook
 // Mock购物车hook
-jest.mock('@/utils/cart', () => ({
+jest.mock('@/contexts/CartContext', () => ({
   useCart: () => ({
   useCart: () => ({
     cart: {
     cart: {
       items: [
       items: [
@@ -83,8 +85,6 @@ jest.mock('@/components/ui/image', () => ({
 describe('购物车页面', () => {
 describe('购物车页面', () => {
   beforeEach(() => {
   beforeEach(() => {
     jest.clearAllMocks()
     jest.clearAllMocks()
-    // Mock showModal返回确认
-    ;(Taro.showModal as any).mockResolvedValue({ confirm: true })
   })
   })
 
 
   it('应该正确渲染购物车页面标题', () => {
   it('应该正确渲染购物车页面标题', () => {

+ 1 - 1
mini/tests/unit/pages/search-result/basic.test.tsx

@@ -61,7 +61,7 @@ jest.mock('@/api', () => ({
 
 
 // Mock cart hook
 // Mock cart hook
 const mockAddToCart = jest.fn()
 const mockAddToCart = jest.fn()
-jest.mock('@/utils/cart', () => ({
+jest.mock('@/contexts/CartContext', () => ({
   useCart: () => ({
   useCart: () => ({
     addToCart: mockAddToCart
     addToCart: mockAddToCart
   })
   })

+ 137 - 42
packages/core-module-mt/auth-module-mt/src/services/mini-auth.service.mt.ts

@@ -6,6 +6,7 @@ import { JWTUtil, redisUtil } from '@d8d/shared-utils';
 import axios from 'axios';
 import axios from 'axios';
 import process from 'node:process'
 import process from 'node:process'
 import { log } from 'node:console';
 import { log } from 'node:console';
+import { nullable } from 'zod/mini';
 
 
 export class MiniAuthService {
 export class MiniAuthService {
   private userRepository: Repository<UserEntityMt>;
   private userRepository: Repository<UserEntityMt>;
@@ -218,6 +219,10 @@ export class MiniAuthService {
     }
     }
   }
   }
 
 
+
+
+ 
+
   /**
   /**
    * 发送微信模板消息
    * 发送微信模板消息
    */
    */
@@ -231,41 +236,6 @@ export class MiniAuthService {
   }): Promise<any> {
   }): Promise<any> {
     const { openid, templateId, page, data, miniprogramState = 'formal', tenantId } = params;
     const { openid, templateId, page, data, miniprogramState = 'formal', tenantId } = params;
 
 
-    // 获取微信小程序配置
-    let appId: string | null = null;
-    let appSecret: string | null = null;
-
-    if (tenantId !== undefined) {
-      // 从系统配置获取
-      const configKeys = ['wx.mini.app.id', 'wx.mini.app.secret'];
-      try {
-        const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId);
-        appId = configs['wx.mini.app.id'];
-        appSecret = configs['wx.mini.app.secret'];
-
-      } catch (error) {
-        console.error("获取系统配置失败:", error);
-        throw error;
-      }
-    } else {
-      console.debug("tenantId未提供,将使用环境变量");
-    }
-
-    // 如果系统配置中没有找到,回退到环境变量
-    if (!appId) {
-      appId = process.env.WX_MINI_APP_ID || null;
-    }
-    if (!appSecret) {
-      appSecret = process.env.WX_MINI_APP_SECRET || null;
-    }
-
-    if (!appId || !appSecret) {
-      throw new Error('微信小程序配置缺失');
-    }
-
-    // 获取access_token
-    const accessToken = await this.getAccessToken(appId, appSecret);
-
     // 构建模板消息请求数据
     // 构建模板消息请求数据
     const templateMessageData = {
     const templateMessageData = {
       touser: openid,
       touser: openid,
@@ -275,13 +245,7 @@ export class MiniAuthService {
       miniprogram_state: miniprogramState
       miniprogram_state: miniprogramState
     };
     };
 
 
-    console.debug('发送微信模板消息:', {
-      appId,
-      openid,
-      templateId,
-      page,
-      dataKeys: Object.keys(data)
-    });
+    const accessToken = this.getWxAccessToken(tenantId);
 
 
     // 调用微信模板消息API
     // 调用微信模板消息API
     const url = `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`;
     const url = `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`;
@@ -309,6 +273,137 @@ export class MiniAuthService {
     }
     }
   }
   }
 
 
+  
+  /**
+   * 微信小店发货(原生API)
+   * 在微信小店后台标记订单为已发货
+   * 需要微信小店订单ID和物流信息
+   */
+  async sendWechatShopDelivery(params: {
+    wechatOrderId: string; // 微信小店订单ID
+    deliveryCompany?: string; // 快递公司
+    deliveryNo?: string; // 快递单号
+    tenantId?: number;
+  }): Promise<any> {
+    const { wechatOrderId, deliveryCompany, deliveryNo, tenantId } = params;
+
+    const accessToken = await this.getWxAccessToken(tenantId);
+
+    // 构建微信小店发货请求数据
+    // 根据微信小店API文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html
+    const deliveryData: any = {
+      order_id: wechatOrderId
+    };
+
+    // 如果有物流信息,添加delivery_list
+    if (deliveryCompany && deliveryNo) {
+      // 这里需要将快递公司名称转换为微信小店支持的delivery_id
+      // 实际项目中需要维护一个快递公司映射表
+      const deliveryId = this.mapDeliveryCompanyToId(deliveryCompany);
+
+      if (deliveryId) {
+        deliveryData.delivery_list = [
+          {
+            delivery_id: deliveryId,
+            waybill_id: deliveryNo
+          }
+        ];
+      }
+    }
+
+    const url = `https://api.weixin.qq.com/shop/delivery/send?access_token=${accessToken}`;
+
+    try {
+      const response = await axios.post(url, deliveryData, {
+        timeout: 10000,
+        headers: {
+          'Content-Type': 'application/json'
+        }
+      });
+
+      if (response.data.errcode && response.data.errcode !== 0) {
+        throw new Error(`微信小店发货失败: ${response.data.errmsg} (errcode: ${response.data.errcode})`);
+      }
+
+      console.debug('微信小店发货成功:', response.data);
+      return response.data;
+
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        throw new Error(`微信小店服务器连接失败: ${error.message}`);
+      }
+      throw error;
+    }
+  }
+
+  /**
+   * 快递公司名称到微信小店delivery_id的映射
+   * 实际项目中需要从数据库或配置中获取
+   */
+  private mapDeliveryCompanyToId(companyName: string): string | null {
+    // 简单的映射表,实际项目需要完整的映射
+    const deliveryMap: Record<string, string> = {
+      '顺丰速运': 'SF',
+      '申通快递': 'STO',
+      '圆通速递': 'YTO',
+      '韵达快递': 'YD',
+      '中通快递': 'ZTO',
+      '百世快递': 'HTKY',
+      '邮政快递': 'YZPY',
+      '京东物流': 'JD'
+    };
+
+    // 查找匹配的快递公司
+    for (const [key, value] of Object.entries(deliveryMap)) {
+      if (companyName.includes(key)) {
+        return value;
+      }
+    }
+
+    console.warn(`未找到快递公司映射: ${companyName}`);
+    return null;
+  }
+
+
+  async getWxAccessToken(tenantId?: number): Promise<string> {
+  
+    // 获取微信小程序配置
+    let appId: string | null = null;
+    let appSecret: string | null = null;
+ 
+    if (tenantId !== undefined) {
+      // 从系统配置获取
+      const configKeys = ['wx.mini.app.id', 'wx.mini.app.secret'];
+      try {
+        const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId);
+        appId = configs['wx.mini.app.id'];
+        appSecret = configs['wx.mini.app.secret'];
+ 
+      } catch (error) {
+        console.error("获取系统配置失败:", error);
+        throw error;
+      }
+    } else {
+      console.debug("tenantId未提供,将使用环境变量");
+    }
+ 
+    // 如果系统配置中没有找到,回退到环境变量
+    if (!appId) {
+      appId = process.env.WX_MINI_APP_ID || null;
+    }
+    if (!appSecret) {
+      appSecret = process.env.WX_MINI_APP_SECRET || null;
+    }
+ 
+    if (!appId || !appSecret) {
+      throw new Error('微信小程序配置缺失');
+    }
+ 
+    // 获取access_token
+    const accessToken = await this.getAccessToken(appId, appSecret);
+    return accessToken;
+   }
+
   /**
   /**
    * 获取微信access_token(带缓存机制)
    * 获取微信access_token(带缓存机制)
    */
    */