Browse Source

fix:计算的

super-yimizi 2 months ago
parent
commit
1334473752

+ 51 - 0
application/api/controller/ inspection/Base.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace app\api\controller\inspection;
+
+use app\common\controller\Api;
+use app\common\library\Auth;
+use think\Config;
+use think\Lang;
+
+class Base extends Api
+{
+    protected $noNeedLogin = [];
+    protected $noNeedRight = ['*'];
+    //设置返回的会员字段
+    protected $allowFields = ['id', 'username', 'nickname', 'mobile', 'avatar', 'score', 'level', 'bio', 'balance', 'money', 'gender'];
+
+    public function _initialize()
+    {
+
+        if (isset($_SERVER['HTTP_ORIGIN'])) {
+            header('Access-Control-Expose-Headers: __token__');//跨域让客户端获取到
+        }
+        //跨域检测
+        check_cors_request();
+
+        if (!isset($_COOKIE['PHPSESSID'])) {
+            Config::set('session.id', $this->request->server("HTTP_SID"));
+        }
+        parent::_initialize();
+        // $config = get_addon_config('shop');
+
+        // Config::set('shop', $config);
+        // Config::set('default_return_type', 'json');
+        Auth::instance()->setAllowFields($this->allowFields);
+
+        //判断站点状态
+        // if (isset($config['openedsite']) && !in_array('uniapp', explode(',', $config['openedsite']))) {
+        //     $this->error('站点已关闭');
+        // }
+
+        //这里手动载入语言包
+        Lang::load(ROOT_PATH . '/addons/shop/lang/zh-cn.php');
+        Lang::load(APP_PATH . '/index/lang/zh-cn/user.php');
+        //加载当前控制器的语言包
+        $controllername = strtolower($this->request->controller());
+        $lang = $this->request->langset();
+        $lang = preg_match("/^([a-zA-Z\-_]{2,10})\$/i", $lang) ? $lang : 'zh-cn';
+        Lang::load(ADDON_PATH . 'shop/lang/' . $lang . '/' . str_replace('.', '/', $controllername) . '.php');
+    }
+
+}

+ 0 - 0
application/api/controller/OrderGoods.php → application/api/controller/AfterSale.php


+ 2 - 0
application/api/controller/Goods.php

@@ -79,6 +79,7 @@ class Goods extends Base
             $row->is_collect = false;
         }
         $row->sku_spec = SkuSpecService::getGoodsSkuSpec($id);
+
         //  要处理规格的图片
         // 这个错误是因为 $row->sku_spec 是一个重载属性(通过魔术方法 __get() 获取),不能直接通过引用修改。我们需要先将其转换为普通数组,处理后再赋值回去。
         if (!empty($row->sku_spec)) {
@@ -92,6 +93,7 @@ class Goods extends Base
             }
             $row->sku_spec = $skuSpecData;  // 处理完后重新赋值
         }
+        // 
 
         //服务保障
         $row->guarantee = $row->guarantee_ids ? Guarantee::field('id,name,intro')->where('id', 'IN', $row->guarantee_ids)->where('status', 'normal')->select() : [];

+ 107 - 38
application/api/controller/Order.php

@@ -19,43 +19,63 @@ class Order extends Base
 {
     protected $noNeedLogin = [];
 
-
-    //计算邮费,判断商品
+    //计算邮费,判断商品 - 支持购物车和商品规格两种模式
     public function calculate()
     {
+        // 验证请求参数
+        $validate = new \app\api\validate\Order();
+        $postData = $this->request->post();
+        $postData['calculate_data'] = '1'; // 添加触发自定义验证的字段
+        if (!$validate->scene('calculate')->check($postData)) {
+            $this->error($validate->getError());
+        }
+        
         $config = get_addon_config('shop');
-        $cart_ids = $this->request->post('ids');
-        $address_id = $this->request->post('address_id/d'); //地址id
-        if (empty($cart_ids)) {
-            $this->error('参数缺失');
-        }
-        $user_coupon_id = $this->request->post('user_coupon_id/d'); //优惠券
+        $type = $this->request->post('type'); // 计算类型:cart 或 goods
+        $address_id = $this->request->post('address_id/d'); // 地址id
+        $user_coupon_id = $this->request->post('user_coupon_id/d'); // 优惠券
+        
         $address = Address::get($address_id);
-        $orderInfo = [
-            'order_sn'    => '',
-            'amount'      => 0, //订单金额(包含运费)
-            'shippingfee' => 0, //运费
-            'goodsprice'  => 0 //商品金额
-        ];
-        $goodsList = [];
         $area_id = !empty($address) ? $address->area_id : 0;
+        
         try {
-            list($orderItem, $goodsList) = OrderModel::computeCarts($orderInfo, $cart_ids, $this->auth->id, $area_id, $user_coupon_id);
+            // 根据类型获取标准化的商品列表
+            if ($type === 'cart') {
+                // 购物车模式:先转换为商品列表
+                $cart_ids = $this->request->post('cart_ids');
+                $goods_list = \app\common\Service\OrderService::convertCartToGoodsList($cart_ids, $this->auth->id);
+            } else {
+                // 商品规格模式:直接使用商品列表
+                $goods_list = $this->request->post('goods_list/a');
+            }
+            
+            // 统一调用计算方法
+            $result = \app\common\Service\OrderService::calculateOrder($goods_list, $this->auth->id, $area_id, $user_coupon_id);
+            
+            $orderItem = $result['orderItem'];
+            $goodsList = $result['goodsList'];
+            $orderInfo = $result['orderInfo'];
+            $userCoupon = $result['userCoupon'];
+            
             if (empty($goodsList)) {
                 throw new \Exception("未找到商品");
             }
         } catch (\Exception $e) {
             $this->error($e->getMessage());
         }
+        
+        // 处理商品数据
         foreach ($goodsList as $item) {
             $item->category_id = $item->goods->category_id;
             $item->brand_id = $item->goods->brand_id;
             $item->goods->visible(explode(',', 'id,title,image,price,marketprice'));
         }
-        //获取我的可以使用的优惠券
+        
+        // 获取我的可以使用的优惠券
         $goods_ids = array_column($goodsList, 'goods_id');
         $category_ids = array_column($goodsList, 'category_id');
         $brand_ids = array_column($goodsList, 'brand_id');
+
         $this->success('获取成功', [
             'coupons'          => UserCoupon::myGoodsCoupon($this->auth->id, $goods_ids, $category_ids, $brand_ids),
             'goods_list'       => $goodsList,
@@ -64,41 +84,73 @@ class Order extends Base
         ]);
     }
 
-    //提交订单
+
+    //提交订单 - 通过购物车
     public function create()
     {
+        // 验证请求参数
+        $validate = new \app\api\validate\Order();
+        if (!$validate->scene('create')->check($this->request->post())) {
+            $this->error($validate->getError());
+        }
+        
         $cart_ids = $this->request->post('ids');
         $address_id = $this->request->post('address_id/d'); //地址id
-        $user_coupon_id = $this->request->post('user_coupon_id/d'); //地址id
+        $user_coupon_id = $this->request->post('user_coupon_id/d'); //优惠券id
         $memo = $this->request->post('memo');
-        if (empty($address_id)) {
-            $this->error('请选择地址');
-        }
-        if (empty($cart_ids)) {
-            $this->error('请选择商品');
-        }
+        
         //为购物车id
         //校验购物车id 合法
         $row = (new Carts)->where('id', 'IN', $cart_ids)->where('user_id', '<>', $this->auth->id)->find();
         if ($row) {
             $this->error('存在不合法购物车数据');
         }
+        
         $order = null;
         try {
-            $order = OrderModel::createOrder($address_id, $this->auth->id, $cart_ids, $user_coupon_id, $memo);
+            $order = \app\common\Service\OrderService::createOrderByCart($address_id, $this->auth->id, $cart_ids, $user_coupon_id, $memo);
         } catch (\Exception $e) {
             $this->error($e->getMessage());
         }
+        
+        $this->success('下单成功!', array_intersect_key($order->toArray(), array_flip(['order_sn', 'paystate'])));
+    }
+
+    //直接提交订单 - 通过商品规格
+    public function createByGoods()
+    {
+        // 验证请求参数
+        $validate = new \app\api\validate\Order();
+        if (!$validate->scene('createByGoods')->check($this->request->post())) {
+            $this->error($validate->getError());
+        }
+        
+        $goods_list = $this->request->post('goods_list/a'); // 商品列表
+        $address_id = $this->request->post('address_id/d'); // 地址id
+        $user_coupon_id = $this->request->post('user_coupon_id/d'); // 优惠券id
+        $memo = $this->request->post('memo'); // 备注
+        
+        try {
+            // 创建订单
+            $order = \app\common\Service\OrderService::createOrderByGoods($address_id, $this->auth->id, $goods_list, $user_coupon_id, $memo);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        
         $this->success('下单成功!', array_intersect_key($order->toArray(), array_flip(['order_sn', 'paystate'])));
     }
 
     //订单详情
     public function detail()
     {
-        $order_sn = $this->request->param('order_sn');
-        if (empty($order_sn)) {
-            $this->error('参数缺失');
+        // 验证请求参数
+        $validate = new \app\api\validate\Order();
+        $params = ['order_sn' => $this->request->param('order_sn')];
+        if (!$validate->scene('detail')->check($params)) {
+            $this->error($validate->getError());
         }
+        
+        $order_sn = $this->request->param('order_sn');
         $order = OrderModel::getDetail($order_sn, $this->auth->id);
         if (empty($order)) {
             $this->error('未找到订单');
@@ -129,10 +181,13 @@ class Order extends Base
     //取消订单
     public function cancel()
     {
-        $order_sn = $this->request->post('order_sn');
-        if (!$order_sn) {
-            $this->error('参数错误');
+        // 验证请求参数
+        $validate = new \app\api\validate\Order();
+        if (!$validate->scene('cancel')->check($this->request->post())) {
+            $this->error($validate->getError());
         }
+        
+        $order_sn = $this->request->post('order_sn');
         $order = OrderModel::getByOrderSn($order_sn);
         if (empty($order)) {
             $this->error('订单不存在');
@@ -182,6 +237,12 @@ class Order extends Base
     //订单支付
     public function pay()
     {
+        // 验证请求参数
+        $validate = new \app\api\validate\Order();
+        if (!$validate->scene('pay')->check($this->request->post())) {
+            $this->error($validate->getError());
+        }
+        
         $order_sn = $this->request->post('order_sn');
         $paytype = $this->request->post('paytype');
         $method = $this->request->post('method');
@@ -224,10 +285,14 @@ class Order extends Base
     //确认收货
     public function receipt()
     {
-        $order_sn = $this->request->post('order_sn');
-        if (!$order_sn) {
-            $this->error('参数错误');
+        // 验证请求参数
+        $validate = new \app\api\validate\Order();
+        $params = ['order_sn' => $this->request->post('order_sn')];
+        if (!$validate->scene('detail')->check($params)) {
+            $this->error($validate->getError());
         }
+        
+        $order_sn = $this->request->post('order_sn');
         $order = OrderModel::getByOrderSn($order_sn);
         if (empty($order)) {
             $this->error('订单不存在');
@@ -252,10 +317,14 @@ class Order extends Base
     //查询物流
     public function logistics()
     {
-        $order_sn = $this->request->param('order_sn');
-        if (empty($order_sn)) {
-            $this->error('参数缺失');
+        // 验证请求参数
+        $validate = new \app\api\validate\Order();
+        $params = ['order_sn' => $this->request->param('order_sn')];
+        if (!$validate->scene('detail')->check($params)) {
+            $this->error($validate->getError());
         }
+        
+        $order_sn = $this->request->param('order_sn');
         $order = OrderModel::getDetail($order_sn, $this->auth->id);
         if (empty($order)) {
             $this->error('未找到订单');

+ 173 - 0
application/api/validate/Order.php

@@ -0,0 +1,173 @@
+<?php
+
+namespace app\api\validate;
+
+use think\Validate;
+
+class Order extends Validate
+{
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+        // 基础参数
+        'address_id'       => 'require|integer|gt:0',
+        'user_coupon_id'   => 'integer|gt:0',
+        'memo'            => 'max:500',
+        
+        // 购物车相关
+        'ids'             => 'require|array',
+        'cart_ids'        => 'require|array',
+        
+        // 商品列表相关
+        'goods_list'      => 'require|array|checkGoodsList',
+        'goods_list.*.goods_id'     => 'require|integer|gt:0',
+        'goods_list.*.goods_sku_id' => 'integer|egt:0',
+        'goods_list.*.nums'         => 'require|integer|gt:0',
+        
+        // 计算类型
+        'type'            => 'require|in:cart,goods',
+        
+        // 计算订单数据验证
+        'calculate_data'  => 'checkCalculateData',
+        
+        // 订单操作相关
+        'order_sn'        => 'require|alphaNum',
+        'paytype'         => 'require|in:alipay,wechat,unionpay,balance',
+        'method'          => 'require|in:web,wap,app,miniapp,mp,mini',
+    ];
+
+    /**
+     * 提示消息
+     */
+    protected $message = [
+        // 基础参数
+        'address_id.require'    => '收货地址不能为空',
+        'address_id.integer'    => '收货地址ID必须是整数',
+        'address_id.gt'         => '收货地址ID必须大于0',
+        'user_coupon_id.integer' => '优惠券ID必须是整数',
+        'user_coupon_id.gt'     => '优惠券ID必须大于0',
+        'memo.max'              => '备注长度不能超过500个字符',
+        
+        // 购物车相关
+        'ids.require'           => '请选择商品',
+        'ids.array'             => '商品选择参数格式错误',
+        'cart_ids.require'      => '购物车ID不能为空',
+        'cart_ids.array'        => '购物车ID必须是数组',
+        
+        // 商品列表相关
+        'goods_list.require'    => '商品列表不能为空',
+        'goods_list.array'      => '商品列表必须是数组',
+        'goods_list.*.goods_id.require' => '商品ID不能为空',
+        'goods_list.*.goods_id.integer' => '商品ID必须是整数',
+        'goods_list.*.goods_id.gt'      => '商品ID必须大于0',
+        'goods_list.*.goods_sku_id.integer' => '商品规格ID必须是整数',
+        'goods_list.*.goods_sku_id.egt'     => '商品规格ID必须大于或等于0',
+        'goods_list.*.nums.require'     => '商品数量不能为空',
+        'goods_list.*.nums.integer'     => '商品数量必须是整数',
+        'goods_list.*.nums.gt'          => '商品数量必须大于0',
+        
+        // 计算类型
+        'type.require'      => '计算类型不能为空',
+        'type.in'           => '计算类型只能是cart或goods',
+        
+        // 订单操作相关
+        'order_sn.require'      => '订单号不能为空',
+        'order_sn.alphaNum'     => '订单号格式错误',
+        'paytype.require'       => '支付方式不能为空',
+        'paytype.in'            => '支付方式不支持',
+        'method.require'        => '支付方法不能为空',
+        'method.in'             => '支付方法不支持',
+    ];
+
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        // 通过购物车创建订单
+        'create'            => ['ids', 'address_id', 'user_coupon_id', 'memo'],
+        // 通过商品规格创建订单
+        'createByGoods'     => ['goods_list', 'address_id', 'user_coupon_id', 'memo'],
+        // 计算订单(支持购物车和商品规格两种模式)
+        'calculate'         => ['type', 'address_id', 'user_coupon_id', 'calculate_data'],
+        // 订单详情、确认收货、查询物流
+        'detail'            => ['order_sn'],
+        // 取消订单
+        'cancel'            => ['order_sn'],
+        // 订单支付
+        'pay'               => ['order_sn', 'paytype', 'method'],
+    ];
+
+    /**
+     * 自定义验证规则:验证商品列表格式
+     * @param $value
+     * @param $rule
+     * @param $data
+     * @return bool|string
+     */
+    protected function checkGoodsList($value, $rule, $data)
+    {
+        if (!is_array($value)) {
+            return '商品列表必须是数组';
+        }
+        
+        if (empty($value)) {
+            return '商品列表不能为空';
+        }
+        
+        foreach ($value as $index => $item) {
+            if (!is_array($item)) {
+                return "商品列表第" . ($index + 1) . "项格式错误";
+            }
+            
+            // 验证必要字段
+            if (!isset($item['goods_id']) || !is_numeric($item['goods_id']) || $item['goods_id'] <= 0) {
+                return "商品列表第" . ($index + 1) . "项的商品ID无效";
+            }
+            
+            if (!isset($item['nums']) || !is_numeric($item['nums']) || $item['nums'] <= 0) {
+                return "商品列表第" . ($index + 1) . "项的数量无效";
+            }
+            
+            // 验证可选字段
+            if (isset($item['goods_sku_id']) && (!is_numeric($item['goods_sku_id']) || $item['goods_sku_id'] < 0)) {
+                return "商品列表第" . ($index + 1) . "项的规格ID无效";
+            }
+        }
+        
+        return true;
+    }
+
+    /**
+     * 自定义验证规则:验证计算订单参数
+     * @param $value
+     * @param $rule
+     * @param $data
+     * @return bool|string
+     */
+    protected function checkCalculateData($value, $rule, $data)
+    {
+        // 验证type参数
+        if (!isset($data['type']) || !in_array($data['type'], ['cart', 'goods'])) {
+            return '计算类型只能是cart或goods';
+        }
+        
+        // 根据type验证对应参数
+        if ($data['type'] === 'cart') {
+            if (!isset($data['cart_ids']) || !is_array($data['cart_ids']) || empty($data['cart_ids'])) {
+                return '购物车ID列表不能为空';
+            }
+        } elseif ($data['type'] === 'goods') {
+            if (!isset($data['goods_list']) || !is_array($data['goods_list']) || empty($data['goods_list'])) {
+                return '商品列表不能为空';
+            }
+            // 验证商品列表格式
+            $result = $this->checkGoodsList($data['goods_list'], '', $data);
+            if ($result !== true) {
+                return $result;
+            }
+        }
+        
+        return true;
+    }
+} 

+ 5 - 0
application/common/model/Goods.php

@@ -242,4 +242,9 @@ class Goods extends Model
     {
         return $this->hasMany('Comment', 'goods_id', 'id');
     }
+    public function Brand()
+    {
+        return $this->belongsTo('Brand', 'brand_id', 'id', [], 'LEFT');
+    }
+
 }

+ 4 - 1
application/common/model/SkuSpec.php

@@ -28,7 +28,10 @@ class SkuSpec extends Model
     public function Spec()
     {
         return $this->hasOne('Spec', 'id', 'spec_id', [], 'LEFT')
-        ->bind(['title' => 'name']);
+        ->bind([
+            'title' => 'name',
+            'type'  => 'type'
+        ]);
     }
 
     public function SpecValue()

+ 571 - 0
application/common/service/OrderService.php

@@ -0,0 +1,571 @@
+<?php
+
+namespace app\common\Service;
+
+use app\common\Enum\GoodsEnum;
+use app\common\model\Order;
+use app\common\model\OrderGoods;
+use app\common\model\OrderAction;
+use app\common\model\Carts;
+use app\common\model\Address;
+use app\common\model\UserCoupon;
+use app\common\model\Goods;
+use app\common\model\Sku;
+use app\common\model\Freight;
+use app\common\model\Coupon;
+use think\Db;
+use think\Exception;
+
+/**
+ * 订单服务类
+ * 封装订单创建相关逻辑
+ */
+class OrderService
+{
+    
+    /**
+     * 统一的创建订单方法
+     * @param int $address_id 地址ID
+     * @param int $user_id 用户ID
+     * @param array $goods_list 标准化的商品列表
+     * @param int $user_coupon_id 优惠券ID
+     * @param string $memo 备注
+     * @param array $cart_ids 购物车ID数组(如果是购物车模式需要清空)
+     * @return Order
+     * @throws Exception
+     */
+    public static function createOrder($address_id, $user_id, $goods_list, $user_coupon_id = 0, $memo = '', $cart_ids = [])
+    {
+        $address = Address::get($address_id);
+        if (!$address || $address['user_id'] != $user_id) {
+            throw new Exception("地址未找到");
+        }
+
+        if (empty($goods_list)) {
+            throw new Exception("商品列表不能为空");
+        }
+
+        $config = get_addon_config('shop');
+        $order_sn = date("Ymdhis") . sprintf("%08d", $user_id) . mt_rand(1000, 9999);
+
+        // 订单主表信息
+        $orderInfo = [
+            'user_id'     => $user_id,
+            'order_sn'    => $order_sn,
+            'address_id'  => $address->id,
+            'province_id' => $address->province_id,
+            'city_id'     => $address->city_id,
+            'area_id'     => $address->area_id,
+            'receiver'    => $address->receiver,
+            'mobile'      => $address->mobile,
+            'address'     => $address->address,
+            'zipcode'     => $address->zipcode,
+            'goodsprice'  => 0, // 商品金额 (不含运费)
+            'amount'      => 0, // 总金额 (含运费)
+            'shippingfee' => 0, // 运费
+            'discount'    => 0, // 优惠金额
+            'saleamount'  => 0,
+            'memo'        => $memo,
+            'expiretime'  => time() + $config['order_timeout'], // 订单失效
+            'status'      => 'normal'
+        ];
+
+        // 通过商品列表计算订单明细
+        list($orderItem, $goodsList, $userCoupon) = self::computeGoods($orderInfo, $goods_list, $user_id, $address->area_id, $user_coupon_id);
+        
+        // 创建订单
+        $order = self::createOrderWithTransaction($orderInfo, $orderItem, $goodsList, $userCoupon);
+        
+        // 如果是购物车模式,需要清空购物车
+        if (!empty($cart_ids)) {
+            Carts::clear($cart_ids);
+        }
+        
+        return $order;
+    }
+
+    /**
+     * 通过购物车创建订单
+     * @param int $address_id 地址ID
+     * @param int $user_id 用户ID
+     * @param array $cart_ids 购物车ID数组
+     * @param int $user_coupon_id 优惠券ID
+     * @param string $memo 备注
+     * @return Order
+     * @throws Exception
+     */
+    public static function createOrderByCart($address_id, $user_id, $cart_ids, $user_coupon_id = 0, $memo = '')
+    {
+        if (empty($cart_ids)) {
+            throw new Exception("购物车列表不能为空");
+        }
+
+        // 将购物车数据转换为标准的商品列表格式
+        $goods_list = self::convertCartToGoodsList($cart_ids, $user_id);
+        
+        return self::createOrder($address_id, $user_id, $goods_list, $user_coupon_id, $memo, $cart_ids);
+    }
+
+    /**
+     * 直接通过商品规格数量创建订单
+     * @param int $address_id 地址ID
+     * @param int $user_id 用户ID
+     * @param array $goods_list 商品列表 [['goods_id'=>1, 'goods_sku_id'=>0, 'nums'=>1], ...]
+     * @param int $user_coupon_id 优惠券ID
+     * @param string $memo 备注
+     * @return Order
+     * @throws Exception
+     */
+    public static function createOrderByGoods($address_id, $user_id, $goods_list, $user_coupon_id = 0, $memo = '')
+    {
+        return self::createOrder($address_id, $user_id, $goods_list, $user_coupon_id, $memo);
+    }
+
+    /**
+     * 根据商品列表计算订单明细
+     * @param array $orderInfo 订单基础信息
+     * @param array $goods_list 商品列表
+     * @param int $user_id 用户ID
+     * @param int $area_id 地区ID
+     * @param int $user_coupon_id 优惠券ID
+     * @return array
+     * @throws Exception
+     */
+    protected static function computeGoods(&$orderInfo, $goods_list, $user_id, $area_id, $user_coupon_id = 0)
+    {
+        $config = get_addon_config('shop');
+        $orderInfo['amount'] = 0;
+        $orderInfo['goodsprice'] = 0;
+        $orderInfo['shippingfee'] = 0;
+        $orderInfo['discount'] = 0;
+        $orderItem = [];
+        $shippingTemp = [];
+        $userCoupon = null;
+        $goodsList = [];
+
+        // 校验优惠券
+        if ($user_coupon_id) {
+            $userCouponModel = new UserCoupon();
+            $userCoupon = $userCouponModel->checkUserOrUse($user_coupon_id, $user_id);
+            $orderInfo['user_coupon_id'] = $user_coupon_id;
+        }
+
+        // 提取所有商品ID和SKU ID,进行批量查询
+        $goods_ids = array_column($goods_list, 'goods_id');
+        $sku_ids = [];
+        $goods_without_sku = []; // 记录没有指定SKU ID的商品,需要查询默认SKU
+        
+        foreach ($goods_list as $index => $item) {
+            if (isset($item['goods_sku_id']) && $item['goods_sku_id'] > 0) {
+                $sku_ids[] = $item['goods_sku_id'];
+            } else {
+                // 没有指定SKU ID的商品,记录下来后续查询默认SKU
+                $goods_without_sku[$index] = $item['goods_id'];
+            }
+        }
+
+        // 批量查询商品信息
+        $goodsData = [];
+        if (!empty($goods_ids)) {
+            $goodsCollection = Goods::with(['category', 'brand'])
+                ->where('id', 'in', $goods_ids)
+                ->where('status', GoodsEnum::STATUS_ON_SALE)
+                ->select();
+            foreach ($goodsCollection as $goods) {
+                $goodsData[$goods->id] = $goods;
+            }
+        }
+
+        // 批量查询SKU信息
+        $skuData = [];
+        $multiSpecSkuIds = []; // 用于存储多规格商品的SKU ID
+        if (!empty($sku_ids)) {
+            $skuCollection = Sku::where('id', 'in', $sku_ids)->select();
+            foreach ($skuCollection as $sku) {               
+                $skuData[$sku->id] = $sku;
+                // 过滤出有规格值的SKU ID(spec_value_ids不为空)
+                if (!empty($sku->spec_value_ids)) {
+                    $multiSpecSkuIds[] = $sku->id;
+                }
+            }
+        }
+        
+        // 查询没有指定SKU ID的商品的默认SKU(单规格商品或多规格商品的默认SKU)
+        if (!empty($goods_without_sku)) {
+            $defaultSkuCollection = Sku::where('goods_id', 'in', array_values($goods_without_sku))
+                ->where('is_default', 1)
+                ->select();
+            foreach ($defaultSkuCollection as $sku) {
+                $skuData[$sku->id] = $sku;
+                // 更新对应的goods_list项,补充SKU ID
+                foreach ($goods_without_sku as $list_index => $goods_id) {
+                    if ($sku->goods_id == $goods_id) {
+                        $goods_list[$list_index]['goods_sku_id'] = $sku->id;
+                        $sku_ids[] = $sku->id; // 添加到sku_ids中用于后续查询规格属性
+                        // 如果默认SKU有规格值,也加入到多规格SKU列表
+                        if (!empty($sku->spec_value_ids)) {
+                            $multiSpecSkuIds[] = $sku->id;
+                        }
+                        unset($goods_without_sku[$list_index]);
+                        break;
+                    }
+                }
+            }
+        }
+
+        // 批量查询规格属性字符串(只查询多规格商品的SKU)
+        $skuAttrData = [];
+        if (!empty($multiSpecSkuIds)) {
+            $skuAttrData = \app\common\Service\SkuSpec::getSkuAttrs($multiSpecSkuIds);
+        }
+
+        // 验证并构建商品数据
+        foreach ($goods_list as $item) {
+            $goods_id = $item['goods_id'];
+            $goods_sku_id = $item['goods_sku_id']; // 现在所有商品都应该有SKU ID
+            $nums = $item['nums'];
+
+            if ($nums <= 0) {
+                throw new Exception("商品数量必须大于0");
+            }
+
+            // 检查商品是否存在
+            if (!isset($goodsData[$goods_id])) {
+                throw new Exception("商品已下架");
+            }
+            $goods = $goodsData[$goods_id];
+
+            // 所有商品都必须有SKU(包括单规格商品的默认SKU)
+            if (empty($skuData) || !isset($skuData[$goods_sku_id])) {
+                throw new Exception("商品规格不存在");
+            }
+            $sku = $skuData[$goods_sku_id];
+            
+            // 验证SKU是否属于该商品
+            if ($sku->goods_id != $goods_id) {
+                throw new Exception("商品规格不匹配");
+            }
+            
+            // 获取规格属性字符串(单规格商品的sku_attr为空)
+            $sku_attr = $skuAttrData[$goods_sku_id] ?? '';
+
+            // 构建商品对象,模拟购物车数据结构
+            $goodsItem = (object)[
+                'goods_id' => $goods_id,
+                'goods_sku_id' => $goods_sku_id,
+                'nums' => $nums,
+                'goods' => $goods,
+                'sku' => $sku,
+                'sku_attr' => $sku_attr
+            ];
+
+            $goodsList[] = $goodsItem;
+        }
+       
+        // 计算商品价格和运费(统一使用SKU进行计算)
+        foreach ($goodsList as $item) {
+            $goodsItemData = [];
+            
+            if (empty($item->goods) || empty($item->sku)) {
+                throw new Exception("商品已下架");
+            }
+            
+            // 库存验证(统一使用SKU库存)
+            if ($item->sku->stocks < $item->nums) {
+                throw new Exception("商品库存不足,请重新修改数量");
+            }
+            
+            // 统一使用SKU数据进行计算
+            $goodsItemData['image'] = !empty($item->sku->image) ? $item->sku->image : $item->goods->image;
+            $goodsItemData['price'] = $item->sku->price;
+            $goodsItemData['lineation_price'] = $item->sku->lineation_price;
+            $goodsItemData['sku_sn'] = $item->sku->sku_sn;
+            $amount = bcmul($item->sku->price, $item->nums, 2);
+            
+            $goodsItemData['amount'] = $amount;
+            // 订单总价
+            $orderInfo['amount'] = bcadd($orderInfo['amount'], $amount, 2);
+            // 商品总价
+            $orderInfo['goodsprice'] = bcadd($orderInfo['goodsprice'], $amount, 2);
+
+            $freight_id = $item->goods->express_template_id;
+            // 计算邮费【合并运费模板】
+            if (!isset($shippingTemp[$freight_id])) {
+                $shippingTemp[$freight_id] = [
+                    'nums'   => $item->nums,
+                    'weight' => $item->sku->weight,
+                    'amount' => $amount
+                ];
+            } else {
+                $shippingTemp[$freight_id] = [
+                    'nums'   => bcadd($shippingTemp[$freight_id]['nums'], $item->nums, 2),
+                    'weight' => bcadd($shippingTemp[$freight_id]['weight'], $item->sku->weight, 2),
+                    'amount' => bcadd($shippingTemp[$freight_id]['amount'], $amount, 2)
+                ];
+            }
+            
+            // 创建订单商品数据
+            $orderItem[] = array_merge($goodsItemData, [
+                'order_sn'     => $orderInfo['order_sn'],
+                'goods_id'     => $item->goods_id,
+                'title'        => $item->goods->title,
+                'url'          => $item->goods->url,
+                'nums'         => $item->nums,
+                'goods_sku_id' => $item->goods_sku_id,
+                'attrdata'     => $item->sku_attr,
+                'weight'       => $item->sku->weight,
+                'category_id'  => $item->goods->category_id,
+                'brand_id'     => $item->goods->brand_id,
+            ]);
+        }
+    
+
+        // 按运费模板计算
+        foreach ($shippingTemp as $key => $item) {
+            $shippingfee = Freight::calculate($key, $area_id, $item['nums'], $item['weight'], $item['amount']);
+            $orderInfo['shippingfee'] = bcadd($orderInfo['shippingfee'], $shippingfee, 2);
+        }
+
+        // 订单总价(含邮费)
+        $orderInfo['amount'] = bcadd($orderInfo['goodsprice'], $orderInfo['shippingfee'], 2);
+
+        if (!empty($userCoupon)) {
+            // 校验优惠券
+            $goods_ids = array_column($orderItem, 'goods_id');
+            $category_ids = array_column($orderItem, 'category_id');
+            $brand_ids = array_column($orderItem, 'brand_id');
+            $couponModel = new Coupon();
+            $coupon = $couponModel->getCoupon($userCoupon['coupon_id'])
+                ->checkCoupon()
+                ->checkOpen()
+                ->checkUseTime($userCoupon['createtime'])
+                ->checkConditionGoods($goods_ids, $user_id, $category_ids, $brand_ids);
+
+            // 计算折扣金额,判断是使用不含运费,还是含运费的金额
+            $amount = !isset($config['shippingfeecoupon']) || $config['shippingfeecoupon'] == 0 ? $orderInfo['goodsprice'] : $orderInfo['amount'];
+            list($new_money, $coupon_money) = $coupon->doBuy($amount);
+
+            // 判断优惠金额是否超出总价,超出则直接设定优惠金额为总价
+            $orderInfo['discount'] = $coupon_money > $amount ? $amount : $coupon_money;
+        }
+
+        // 计算订单的应付金额【减去折扣】
+        $orderInfo['saleamount'] = max(0, bcsub($orderInfo['amount'], $orderInfo['discount'], 2));
+        $orderInfo['discount'] = bcadd($orderInfo['discount'], 0, 2);
+
+        return [
+            $orderItem,
+            $goodsList,
+            $userCoupon
+        ];
+    }
+
+
+    /**
+     * 在事务中创建订单
+     * @param array $orderInfo 订单信息
+     * @param array $orderItem 订单商品列表
+     * @param array $goodsList 商品列表
+     * @param object $userCoupon 优惠券
+     * @return Order
+     * @throws Exception
+     */
+    protected static function createOrderWithTransaction($orderInfo, $orderItem, $goodsList, $userCoupon)
+    {
+        $order = null;
+        Db::startTrans();
+        try {
+            // 创建订单
+            $order = Order::create($orderInfo, true);
+            
+            // 减库存
+            foreach ($goodsList as $index => $item) {
+                if ($item->sku) {
+                    $item->sku->setDec('stocks', $item->nums);
+                }
+                $item->goods->setDec("stocks", $item->nums);
+            }
+            
+            // 计算单个商品折扣后的价格
+            $saleamount = bcsub($order['saleamount'], $order['shippingfee'], 2);
+            $saleratio = $order['goodsprice'] > 0 ? bcdiv($saleamount, $order['goodsprice'], 10) : 1;
+            $saleremains = $saleamount;
+            
+            foreach ($orderItem as $index => &$item) {
+                if (!isset($orderItem[$index + 1])) {
+                    $saleprice = $saleremains;
+                } else {
+                    $saleprice = $order['discount'] == 0 ? bcmul($item['price'], $item['nums'], 2) : bcmul(bcmul($item['price'], $item['nums'], 2), $saleratio, 2);
+                }
+                $saleremains = bcsub($saleremains, $saleprice, 2);
+                $item['realprice'] = $saleprice;
+            }
+            unset($item);
+            
+            // 创建订单商品数据
+            foreach ($orderItem as $index => $item) {
+                OrderGoods::create($item, true);
+            }
+            
+            // 修改地址使用次数
+            $address = Address::get($orderInfo['address_id']);
+            if ($address) {
+                $address->setInc('usednums');
+            }
+            
+            // 优惠券已使用
+            if (!empty($userCoupon)) {
+                $userCoupon->save(['is_used' => 2]);
+            }
+            
+            // 提交事务
+            Db::commit();
+        } catch (Exception $e) {
+            Db::rollback();
+            throw new Exception($e->getMessage());
+        }
+        
+        // 记录操作
+        OrderAction::push($orderInfo['order_sn'], '系统', '订单创建成功');
+        
+        // 订单应付金额为0时直接结算
+        if ($order['saleamount'] == 0) {
+            Order::settle($order->order_sn, 0);
+            $order = Order::get($order->id);
+        }
+        
+        return $order;
+    }
+
+    /**
+     * 验证商品规格参数
+     * @param array $goods_list 商品列表
+     * @throws Exception
+     */
+    public static function validateGoodsList($goods_list)
+    {
+        if (empty($goods_list) || !is_array($goods_list)) {
+            throw new Exception("商品列表不能为空");
+        }
+
+        foreach ($goods_list as $item) {
+            if (!isset($item['goods_id']) || !is_numeric($item['goods_id']) || $item['goods_id'] <= 0) {
+                throw new Exception("商品ID无效");
+            }
+            
+            if (!isset($item['nums']) || !is_numeric($item['nums']) || $item['nums'] <= 0) {
+                throw new Exception("商品数量必须大于0");
+            }
+            
+            if (isset($item['goods_sku_id']) && !is_numeric($item['goods_sku_id'])) {
+                throw new Exception("商品规格ID无效");
+            }
+        }
+    }
+
+    /**
+     * 统一的订单计算方法(用于预览订单)
+     * @param array $goods_list 标准化的商品列表
+     * @param int $user_id 用户ID
+     * @param int $area_id 地区ID
+     * @param int $user_coupon_id 优惠券ID
+     * @return array
+     * @throws Exception
+     */
+    public static function calculateOrder($goods_list, $user_id, $area_id = 0, $user_coupon_id = 0)
+    {
+        if (empty($goods_list)) {
+            throw new Exception("商品列表不能为空");
+        }
+        
+        // 验证商品列表格式
+        self::validateGoodsList($goods_list);
+
+        $order_sn = date("Ymdhis") . sprintf("%08d", $user_id) . mt_rand(1000, 9999);
+
+        // 订单基础信息
+        $orderInfo = [
+            'order_sn'    => $order_sn,
+            'goodsprice'  => 0, // 商品金额 (不含运费)
+            'amount'      => 0, // 总金额 (含运费)
+            'shippingfee' => 0, // 运费
+            'discount'    => 0, // 优惠金额
+            'saleamount'  => 0  // 应付金额
+        ];
+
+        // 计算商品明细
+        list($orderItem, $goodsList, $userCoupon) = self::computeGoods($orderInfo, $goods_list, $user_id, $area_id, $user_coupon_id);
+
+        return [
+            'orderItem' => $orderItem,
+            'goodsList' => $goodsList,
+            'orderInfo' => $orderInfo,
+            'userCoupon' => $userCoupon
+        ];
+    }
+
+    /**
+     * 通过商品规格计算订单明细(用于预览订单)
+     * @param array $goods_list 商品列表
+     * @param int $user_id 用户ID
+     * @param int $area_id 地区ID
+     * @param int $user_coupon_id 优惠券ID
+     * @return array
+     * @throws Exception
+     */
+    public static function calculateOrderByGoods($goods_list, $user_id, $area_id = 0, $user_coupon_id = 0)
+    {
+        return self::calculateOrder($goods_list, $user_id, $area_id, $user_coupon_id);
+    }
+
+    /**
+     * 通过购物车计算订单明细(用于预览订单)
+     * @param array $cart_ids 购物车ID列表
+     * @param int $user_id 用户ID
+     * @param int $area_id 地区ID
+     * @param int $user_coupon_id 优惠券ID
+     * @return array
+     * @throws Exception
+     */
+    public static function calculateOrderByCart($cart_ids, $user_id, $area_id = 0, $user_coupon_id = 0)
+    {
+        if (empty($cart_ids)) {
+            throw new Exception("购物车列表不能为空");
+        }
+
+        // 将购物车数据转换为标准的商品列表格式
+        $goods_list = self::convertCartToGoodsList($cart_ids, $user_id);
+        
+        return self::calculateOrder($goods_list, $user_id, $area_id, $user_coupon_id);
+    }
+
+    /**
+     * 将购物车数据转换为标准的商品列表格式
+     * @param array $cart_ids 购物车ID列表
+     * @param int $user_id 用户ID
+     * @return array
+     * @throws Exception
+     */
+    public static function convertCartToGoodsList($cart_ids, $user_id)
+    {
+        // 查询购物车数据
+        $cartItems = Carts::where('id', 'in', $cart_ids)
+            ->where('user_id', $user_id)
+            ->select();
+            
+        if (empty($cartItems)) {
+            throw new Exception("购物车数据不存在");
+        }
+        
+        $goods_list = [];
+        foreach ($cartItems as $cart) {
+            $goods_list[] = [
+                'goods_id' => $cart->goods_id,
+                'goods_sku_id' => $cart->goods_sku_id,
+                'nums' => $cart->nums
+            ];
+        }
+        
+        return $goods_list;
+    }
+} 

+ 34 - 3
application/common/service/SkuSpec.php

@@ -3,9 +3,10 @@
 namespace app\common\Service;
 
 use app\common\model\SkuSpec as SkuSpecModel;
+use think\Db;
 
- class SkuSpec
- {
+class SkuSpec
+{
     /**
      * 获取指定商品的SKU信息
      * @param int $goods_id 商品ID
@@ -28,4 +29,34 @@ use app\common\model\SkuSpec as SkuSpecModel;
         $list = collection($list)->toArray();
         return $list;
     }
- }
+
+    /**
+     * 批量获取SKU规格属性字符串
+     * @param array $sku_ids SKU ID数组
+     * @return array 返回 [sku_id => 'spec_name:spec_value,spec_name:spec_value'] 格式
+     */
+    public static function getSkuAttrs($sku_ids)
+    {
+        if (empty($sku_ids)) {
+            return [];
+        }
+
+        // 通过shop_goods_sku表的spec_value_ids字段来查询规格信息
+        $list = Db::name('shop_goods_sku')
+            ->alias('sku')
+            ->field('sku.id, GROUP_CONCAT(sp.name,":",sv.value ORDER BY sp.id asc) as sku_attr')
+            ->join('shop_goods_sku_spec gss', "FIND_IN_SET(gss.id, sku.spec_value_ids)", 'LEFT')
+            ->join('shop_spec sp', 'sp.id = gss.spec_id', 'LEFT')
+            ->join('shop_spec_value sv', 'sv.id = gss.spec_value_id', 'LEFT')
+            ->where('sku.id', 'in', $sku_ids)
+            ->group('sku.id')
+            ->select();
+
+        $result = [];
+        foreach ($list as $item) {
+            $result[$item['id']] = $item['sku_attr'] ?: '';
+        }
+
+        return $result;
+    }
+}

+ 393 - 0
docs/API统一接口使用示例.md

@@ -0,0 +1,393 @@
+# API统一接口使用示例
+
+## 概述
+
+为了简化API接口,我们将原来的两个计算接口合并为一个统一的 `/api/order/calculate` 接口,通过 `type` 参数区分计算模式。
+
+## 统一的计算接口
+
+### 接口地址
+`POST /api/order/calculate`
+
+### 参数说明
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| type | string | 是 | 计算类型:`cart`(购物车模式)或 `goods`(商品规格模式) |
+| address_id | int | 是 | 收货地址ID |
+| user_coupon_id | int | 否 | 优惠券ID |
+| cart_ids | array | 条件必填 | 购物车ID列表(当 type=cart 时必填) |
+| goods_list | array | 条件必填 | 商品列表(当 type=goods 时必填) |
+
+## 使用示例
+
+### 1. 商品规格模式计算
+
+**前端请求:**
+```javascript
+// 立即购买场景:用户在商品详情页选择规格后点击"立即购买"
+const calculateByGoods = async (goodsId, skuId, nums, addressId) => {
+    const response = await fetch('/api/order/calculate', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+            type: 'goods',          // 商品规格模式
+            goods_list: [
+                {
+                    goods_id: goodsId,
+                    goods_sku_id: skuId,
+                    nums: nums
+                }
+            ],
+            address_id: addressId,
+            user_coupon_id: 0       // 暂不使用优惠券
+        })
+    });
+    
+    const result = await response.json();
+    console.log('订单预览信息:', result.data);
+    return result.data;
+};
+
+// 调用示例
+calculateByGoods(1, 5, 2, 10);
+```
+
+**后端响应:**
+```json
+{
+    "code": 1,
+    "msg": "获取成功",
+    "data": {
+        "coupons": [],
+        "goods_list": [
+            {
+                "goods_id": 1,
+                "goods_sku_id": 5,
+                "nums": 2,
+                "goods": {
+                    "id": 1,
+                    "title": "测试商品",
+                    "image": "/uploads/goods/1.jpg",
+                    "price": "99.00"
+                },
+                "sku": {
+                    "id": 5,
+                    "price": "99.00"
+                },
+                "sku_attr": "颜色:红色,尺寸:XL"
+            }
+        ],
+        "order_info": {
+            "goodsprice": "198.00",
+            "shippingfee": "10.00",
+            "discount": "0.00",
+            "amount": "208.00",
+            "saleamount": "208.00"
+        },
+        "couponTotalPrice": 198.00
+    }
+}
+```
+
+### 2. 购物车模式计算
+
+**前端请求:**
+```javascript
+// 购物车结算场景:用户在购物车页面选择商品后点击"去结算"
+const calculateByCart = async (cartIds, addressId, couponId) => {
+    const response = await fetch('/api/order/calculate', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+            type: 'cart',           // 购物车模式
+            cart_ids: cartIds,
+            address_id: addressId,
+            user_coupon_id: couponId || 0
+        })
+    });
+    
+    const result = await response.json();
+    console.log('订单预览信息:', result.data);
+    return result.data;
+};
+
+// 调用示例
+calculateByCart([1, 2, 3], 10, 5);
+```
+
+**后端响应:**
+```json
+{
+    "code": 1,
+    "msg": "获取成功",
+    "data": {
+        "coupons": [
+            {
+                "id": 5,
+                "title": "满200减20",
+                "condition": 200,
+                "money": 20
+            }
+        ],
+        "goods_list": [
+            {
+                "goods_id": 1,
+                "nums": 2,
+                "goods": {
+                    "title": "商品A",
+                    "price": "99.00"
+                }
+            },
+            {
+                "goods_id": 2,
+                "nums": 1,
+                "goods": {
+                    "title": "商品B",
+                    "price": "150.00"
+                }
+            }
+        ],
+        "order_info": {
+            "goodsprice": "348.00",
+            "shippingfee": "15.00",
+            "discount": "20.00",
+            "amount": "363.00",
+            "saleamount": "343.00"
+        },
+        "couponTotalPrice": 348.00
+    }
+}
+```
+
+### 3. 批量购买模式计算
+
+**前端请求:**
+```javascript
+// 批量购买场景:用户选择多个商品进行批量购买
+const calculateBatch = async (goodsList, addressId) => {
+    const response = await fetch('/api/order/calculate', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+            type: 'goods',          // 商品规格模式
+            goods_list: goodsList,  // 多个商品
+            address_id: addressId,
+            user_coupon_id: 0
+        })
+    });
+    
+    return await response.json();
+};
+
+// 调用示例:购买3个不同商品
+const batchGoods = [
+    { goods_id: 1, goods_sku_id: 5, nums: 2 },  // 商品A,红色XL,2件
+    { goods_id: 2, goods_sku_id: 0, nums: 1 },  // 商品B,单规格,1件
+    { goods_id: 3, goods_sku_id: 8, nums: 3 }   // 商品C,蓝色L,3件
+];
+
+calculateBatch(batchGoods, 10);
+```
+
+## 错误处理
+
+### 1. 参数验证错误
+
+```json
+{
+    "code": 0,
+    "msg": "计算类型只能是cart或goods",
+    "data": null
+}
+```
+
+### 2. 商品不存在错误
+
+```json
+{
+    "code": 0,
+    "msg": "商品已下架",
+    "data": null
+}
+```
+
+### 3. 库存不足错误
+
+```json
+{
+    "code": 0,
+    "msg": "商品库存不足,请重新修改数量",
+    "data": null
+}
+```
+
+### 4. 购物车为空错误
+
+```json
+{
+    "code": 0,
+    "msg": "购物车ID列表不能为空",
+    "data": null
+}
+```
+
+## 前端集成指南
+
+### Vue.js 组件示例
+
+```vue
+<template>
+  <div class="order-calculate">
+    <button @click="calculateOrder">计算订单</button>
+    <div v-if="orderInfo">
+      <p>商品金额:¥{{ orderInfo.goodsprice }}</p>
+      <p>运费:¥{{ orderInfo.shippingfee }}</p>
+      <p>优惠:-¥{{ orderInfo.discount }}</p>
+      <p>应付:¥{{ orderInfo.saleamount }}</p>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      orderInfo: null,
+      goodsList: [
+        { goods_id: 1, goods_sku_id: 5, nums: 2 }
+      ],
+      cartIds: [1, 2, 3],
+      addressId: 10,
+      calculateType: 'goods' // 或 'cart'
+    }
+  },
+  methods: {
+    async calculateOrder() {
+      try {
+        const params = {
+          type: this.calculateType,
+          address_id: this.addressId,
+          user_coupon_id: 0
+        };
+        
+        if (this.calculateType === 'goods') {
+          params.goods_list = this.goodsList;
+        } else {
+          params.cart_ids = this.cartIds;
+        }
+        
+        const response = await this.$http.post('/api/order/calculate', params);
+        
+        if (response.data.code === 1) {
+          this.orderInfo = response.data.data.order_info;
+        } else {
+          this.$message.error(response.data.msg);
+        }
+      } catch (error) {
+        console.error('计算订单失败:', error);
+        this.$message.error('计算订单失败');
+      }
+    }
+  }
+}
+</script>
+```
+
+### React Hook 示例
+
+```jsx
+import { useState, useCallback } from 'react';
+
+const useOrderCalculate = () => {
+  const [orderInfo, setOrderInfo] = useState(null);
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState(null);
+
+  const calculate = useCallback(async (type, data) => {
+    setLoading(true);
+    setError(null);
+    
+    try {
+      const params = {
+        type,
+        address_id: data.addressId,
+        user_coupon_id: data.couponId || 0
+      };
+      
+      if (type === 'goods') {
+        params.goods_list = data.goodsList;
+      } else {
+        params.cart_ids = data.cartIds;
+      }
+      
+      const response = await fetch('/api/order/calculate', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(params)
+      });
+      
+      const result = await response.json();
+      
+      if (result.code === 1) {
+        setOrderInfo(result.data.order_info);
+      } else {
+        setError(result.msg);
+      }
+    } catch (err) {
+      setError('计算订单失败');
+    } finally {
+      setLoading(false);
+    }
+  }, []);
+
+  return { orderInfo, loading, error, calculate };
+};
+
+// 使用示例
+const OrderPage = () => {
+  const { orderInfo, loading, error, calculate } = useOrderCalculate();
+
+  const handleCalculate = () => {
+    calculate('goods', {
+      goodsList: [{ goods_id: 1, goods_sku_id: 5, nums: 2 }],
+      addressId: 10,
+      couponId: 0
+    });
+  };
+
+  return (
+    <div>
+      <button onClick={handleCalculate} disabled={loading}>
+        {loading ? '计算中...' : '计算订单'}
+      </button>
+      {error && <div className="error">{error}</div>}
+      {orderInfo && (
+        <div className="order-info">
+          <p>商品金额:¥{orderInfo.goodsprice}</p>
+          <p>运费:¥{orderInfo.shippingfee}</p>
+          <p>优惠:-¥{orderInfo.discount}</p>
+          <p>应付:¥{orderInfo.saleamount}</p>
+        </div>
+      )}
+    </div>
+  );
+};
+```
+
+## 优势总结
+
+1. **接口统一**:只需要维护一个计算接口,减少了API复杂度
+2. **参数清晰**:通过 `type` 参数明确区分计算模式
+3. **响应一致**:两种模式返回相同的数据结构
+4. **易于扩展**:未来如需新增计算模式,只需增加新的 `type` 值
+5. **向后兼容**:保持了原有的功能特性
+6. **错误统一**:统一的错误处理和验证机制
+
+这种设计使得前端开发更加简洁,同时保持了功能的完整性和扩展性。 

+ 331 - 0
docs/OrderService_使用说明.md

@@ -0,0 +1,331 @@
+# OrderService 订单服务使用说明
+
+## 概述
+
+OrderService 是一个封装了订单创建逻辑的服务类,支持两种创建订单的方式:
+1. **通过购物车创建订单**(原有功能)
+2. **直接通过商品规格创建订单**(新增功能)
+
+## 新增API接口
+
+### 1. 直接创建订单 - `POST /api/order/createByGoods`
+
+直接通过商品和规格信息创建订单,无需经过购物车。
+
+#### 请求参数
+
+```json
+{
+    "goods_list": [
+        {
+            "goods_id": 1,           // 商品ID(必填)
+            "goods_sku_id": 5,       // 商品规格ID(可选,单规格商品为0或不传)
+            "nums": 2                // 购买数量(必填)
+        },
+        {
+            "goods_id": 2,
+            "goods_sku_id": 0,       // 单规格商品
+            "nums": 1
+        }
+    ],
+    "address_id": 10,               // 收货地址ID(必填)
+    "user_coupon_id": 3,            // 优惠券ID(可选)
+    "memo": "订单备注"              // 订单备注(可选)
+}
+```
+
+#### 响应数据
+
+```json
+{
+    "code": 1,
+    "msg": "下单成功!",
+    "data": {
+        "order_sn": "20241215142312000000011234",
+        "paystate": 0
+    }
+}
+```
+
+### 2. 计算订单预览 - `POST /api/order/calculate`
+
+在创建订单前预览价格、运费等信息,支持两种模式。
+
+#### 商品规格模式
+
+```json
+{
+    "type": "goods",                // 计算类型:商品规格模式
+    "goods_list": [
+        {
+            "goods_id": 1,
+            "goods_sku_id": 5,
+            "nums": 2
+        }
+    ],
+    "address_id": 10,               // 收货地址ID(可选,用于计算运费)
+    "user_coupon_id": 3             // 优惠券ID(可选)
+}
+```
+
+#### 购物车模式
+
+```json
+{
+    "type": "cart",                 // 计算类型:购物车模式
+    "cart_ids": [1, 2, 3],          // 购物车ID列表
+    "address_id": 10,               // 收货地址ID(可选,用于计算运费)
+    "user_coupon_id": 3             // 优惠券ID(可选)
+}
+```
+
+#### 响应数据
+
+```json
+{
+    "code": 1,
+    "msg": "获取成功",
+    "data": {
+        "coupons": [                // 可用优惠券列表
+            {
+                "id": 1,
+                "title": "满100减10",
+                "condition": 100,
+                "money": 10
+            }
+        ],
+        "goods_list": [             // 商品详情列表
+            {
+                "goods_id": 1,
+                "goods_sku_id": 5,
+                "nums": 2,
+                "goods": {
+                    "id": 1,
+                    "title": "商品名称",
+                    "image": "商品图片",
+                    "price": "99.00"
+                },
+                "sku": {
+                    "id": 5,
+                    "price": "99.00",
+                    "image": "规格图片"
+                },
+                "sku_attr": "颜色:红色,尺寸:XL"
+            }
+        ],
+        "order_info": {             // 订单价格信息
+            "goodsprice": "198.00", // 商品金额
+            "shippingfee": "10.00", // 运费
+            "discount": "10.00",    // 优惠金额
+            "amount": "208.00",     // 总金额(含运费)
+            "saleamount": "198.00"  // 应付金额
+        },
+        "couponTotalPrice": 198.00  // 可用于优惠券的金额
+    }
+}
+```
+
+## 服务类方法
+
+### OrderService::createOrderByGoods()
+
+直接通过商品规格创建订单。
+
+```php
+use app\common\Service\OrderService;
+
+try {
+    $order = OrderService::createOrderByGoods(
+        $address_id,    // 地址ID
+        $user_id,       // 用户ID
+        $goods_list,    // 商品列表
+        $user_coupon_id, // 优惠券ID(可选)
+        $memo           // 备注(可选)
+    );
+    
+    echo "订单创建成功,订单号:" . $order->order_sn;
+} catch (Exception $e) {
+    echo "创建失败:" . $e->getMessage();
+}
+```
+
+### OrderService::createOrderByCart()
+
+通过购物车创建订单(原有功能的封装)。
+
+```php
+use app\common\Service\OrderService;
+
+try {
+    $order = OrderService::createOrderByCart(
+        $address_id,     // 地址ID
+        $user_id,        // 用户ID
+        $cart_ids,       // 购物车ID数组
+        $user_coupon_id, // 优惠券ID(可选)
+        $memo            // 备注(可选)
+    );
+    
+    echo "订单创建成功,订单号:" . $order->order_sn;
+} catch (Exception $e) {
+    echo "创建失败:" . $e->getMessage();
+}
+```
+
+### OrderService::calculateOrderByGoods()
+
+通过商品规格计算订单明细(用于预览)。
+
+```php
+use app\common\Service\OrderService;
+
+try {
+    $result = OrderService::calculateOrderByGoods(
+        $goods_list,     // 商品列表
+        $user_id,        // 用户ID
+        $area_id,        // 地区ID(可选)
+        $user_coupon_id  // 优惠券ID(可选)
+    );
+    
+    $orderInfo = $result['orderInfo'];    // 订单价格信息
+    $goodsList = $result['goodsList'];    // 商品详情
+    $orderItem = $result['orderItem'];    // 订单商品项
+    $userCoupon = $result['userCoupon'];  // 优惠券信息
+    
+} catch (Exception $e) {
+    echo "计算失败:" . $e->getMessage();
+}
+```
+
+### OrderService::calculateOrderByCart()
+
+通过购物车计算订单明细(用于预览)。
+
+```php
+use app\common\Service\OrderService;
+
+try {
+    $result = OrderService::calculateOrderByCart(
+        $cart_ids,       // 购物车ID数组
+        $user_id,        // 用户ID
+        $area_id,        // 地区ID(可选)
+        $user_coupon_id  // 优惠券ID(可选)
+    );
+    
+    $orderInfo = $result['orderInfo'];    // 订单价格信息
+    $goodsList = $result['goodsList'];    // 商品详情
+    $orderItem = $result['orderItem'];    // 订单商品项
+    $userCoupon = $result['userCoupon'];  // 优惠券信息
+    
+} catch (Exception $e) {
+    echo "计算失败:" . $e->getMessage();
+}
+```
+
+## 商品列表格式说明
+
+`goods_list` 参数是一个数组,每个元素包含以下字段:
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| goods_id | int | 是 | 商品ID |
+| goods_sku_id | int | 否 | 商品规格ID,单规格商品可以不传或传0 |
+| nums | int | 是 | 购买数量,必须大于0 |
+
+### 示例
+
+```json
+[
+    {
+        "goods_id": 1,      // 多规格商品
+        "goods_sku_id": 5,
+        "nums": 2
+    },
+    {
+        "goods_id": 2,      // 单规格商品
+        "goods_sku_id": 0,  // 或者不传此字段
+        "nums": 1
+    }
+]
+```
+
+## 错误处理
+
+服务会抛出以下类型的异常:
+
+- **商品验证错误**:商品不存在、已下架等
+- **库存不足错误**:商品或规格库存不足
+- **地址错误**:收货地址不存在或不属于用户
+- **优惠券错误**:优惠券不可用或已使用
+- **参数错误**:商品列表格式错误等
+
+## 使用场景
+
+### 1. 立即购买功能
+用户在商品详情页点击"立即购买",直接跳转到订单确认页面:
+
+```javascript
+// 前端代码示例
+function buyNow(goodsId, skuId, nums) {
+    const goodsList = [{
+        goods_id: goodsId,
+        goods_sku_id: skuId,
+        nums: nums
+    }];
+    
+    // 先计算预览
+    calculateOrder(goodsList).then(result => {
+        // 显示订单确认页面
+        showOrderConfirm(result);
+    });
+}
+
+function createOrder(goodsList, addressId, couponId, memo) {
+    // 创建订单
+    fetch('/api/order/createByGoods', {
+        method: 'POST',
+        body: JSON.stringify({
+            goods_list: goodsList,
+            address_id: addressId,
+            user_coupon_id: couponId,
+            memo: memo
+        })
+    }).then(response => {
+        // 跳转到支付页面
+    });
+}
+```
+
+### 2. 批量购买功能
+选择多个商品进行批量购买:
+
+```php
+// 后端处理示例
+$goodsList = [
+    ['goods_id' => 1, 'goods_sku_id' => 5, 'nums' => 2],
+    ['goods_id' => 2, 'goods_sku_id' => 0, 'nums' => 1],
+    ['goods_id' => 3, 'goods_sku_id' => 8, 'nums' => 3]
+];
+
+try {
+    $order = OrderService::createOrderByGoods(
+        $addressId, 
+        $userId, 
+        $goodsList, 
+        $couponId, 
+        $memo
+    );
+    
+    // 返回成功结果
+    return json(['code' => 1, 'msg' => '下单成功', 'data' => $order]);
+} catch (Exception $e) {
+    return json(['code' => 0, 'msg' => $e->getMessage()]);
+}
+```
+
+## 注意事项
+
+1. **库存校验**:创建订单时会自动校验库存并扣减,请确保并发处理的安全性
+2. **价格计算**:支持运费计算、优惠券折扣等完整的价格体系
+3. **事务处理**:订单创建过程在数据库事务中执行,确保数据一致性
+4. **日志记录**:自动记录订单操作日志
+5. **性能优化**:避免在循环中调用订单创建方法,建议批量处理 

+ 269 - 0
docs/OrderService_架构设计说明.md

@@ -0,0 +1,269 @@
+# OrderService 架构设计说明
+
+## 设计理念
+
+OrderService 采用了**统一数据流**的设计理念,无论订单数据来源于购物车还是商品规格,最终都会转换为标准化的 `goods_list` 格式进行处理。这种设计确保了核心业务逻辑的统一性和代码的可维护性。
+
+## 核心架构
+
+```
+输入层                数据转换层              核心处理层               输出层
+┌─────────┐         ┌─────────────┐         ┌─────────────┐         ┌─────────┐
+│购物车ID │   ──►   │ 转换为      │   ──►   │             │   ──►   │         │
+│cart_ids │         │ goods_list  │         │ 统一计算/   │         │ 订单    │
+└─────────┘         └─────────────┘         │ 创建方法    │         │ 结果    │
+                                            │             │         │         │
+┌─────────┐         ┌─────────────┐         │ calculate   │         └─────────┘
+│商品规格 │   ──►   │ 验证格式    │   ──►   │ Order()     │
+│goods_list│        │ 直接使用    │         │             │
+└─────────┘         └─────────────┘         │ createOrder │
+                                            │ ()          │
+                                            └─────────────┘
+```
+
+## 方法层次结构
+
+### 1. 统一核心方法
+
+#### calculateOrder() - 统一计算方法
+- **作用**: 接收标准化的 `goods_list`,计算订单明细
+- **输入**: 标准化商品列表
+- **输出**: 订单计算结果
+- **特点**: 所有计算逻辑的统一入口
+
+#### createOrder() - 统一创建方法
+- **作用**: 接收标准化的 `goods_list`,创建订单
+- **输入**: 标准化商品列表
+- **输出**: 创建的订单对象
+- **特点**: 所有创建逻辑的统一入口
+
+### 2. 数据转换方法
+
+#### convertCartToGoodsList() - 购物车转换
+- **作用**: 将购物车数据转换为标准商品列表格式
+- **输入**: 购物车ID数组
+- **输出**: 标准化的 `goods_list`
+
+### 3. 对外接口方法
+
+#### calculateOrderByGoods() - 商品规格计算
+```php
+public static function calculateOrderByGoods($goods_list, $user_id, $area_id, $user_coupon_id)
+{
+    return self::calculateOrder($goods_list, $user_id, $area_id, $user_coupon_id);
+}
+```
+
+#### calculateOrderByCart() - 购物车计算
+```php
+public static function calculateOrderByCart($cart_ids, $user_id, $area_id, $user_coupon_id)
+{
+    $goods_list = self::convertCartToGoodsList($cart_ids, $user_id);
+    return self::calculateOrder($goods_list, $user_id, $area_id, $user_coupon_id);
+}
+```
+
+#### createOrderByGoods() - 商品规格创建
+```php
+public static function createOrderByGoods($address_id, $user_id, $goods_list, $user_coupon_id, $memo)
+{
+    return self::createOrder($address_id, $user_id, $goods_list, $user_coupon_id, $memo);
+}
+```
+
+#### createOrderByCart() - 购物车创建
+```php
+public static function createOrderByCart($address_id, $user_id, $cart_ids, $user_coupon_id, $memo)
+{
+    $goods_list = self::convertCartToGoodsList($cart_ids, $user_id);
+    return self::createOrder($address_id, $user_id, $goods_list, $user_coupon_id, $memo, $cart_ids);
+}
+```
+
+## 标准化数据格式
+
+### goods_list 格式
+```php
+$goods_list = [
+    [
+        'goods_id' => 1,           // 商品ID(必填)
+        'goods_sku_id' => 5,       // 商品规格ID(可选,系统会自动补充默认SKU)
+        'nums' => 2                // 购买数量(必填)
+    ],
+    [
+        'goods_id' => 2,
+        'goods_sku_id' => 0,       // 可以传0或不传,系统会自动查询默认SKU
+        'nums' => 1
+    ]
+];
+```
+
+### 重要说明:统一SKU处理机制
+
+在FastAdmin商城系统中,**所有商品(包括单规格商品)都会在SKU表中生成记录**:
+
+- **单规格商品**: 会生成一条默认SKU记录,`spec_value_ids`为空,`is_default=1`
+- **多规格商品**: 会生成多条SKU记录,其中一条标记为`is_default=1`
+
+**系统处理逻辑**:
+1. 如果传入`goods_sku_id > 0`,直接使用指定的SKU
+2. 如果传入`goods_sku_id = 0`或不传,系统自动查询该商品的默认SKU
+3. **所有商品最终都通过SKU进行价格、库存、规格计算**
+
+这种设计确保了:
+- 单规格和多规格商品的处理逻辑完全统一
+- 避免了复杂的条件判断
+- 提供了更好的扩展性和一致性
+
+### 数据转换示例
+
+#### 购物车数据 → goods_list
+```php
+// 原始购物车数据
+$cartItems = [
+    ['id' => 1, 'goods_id' => 1, 'goods_sku_id' => 5, 'nums' => 2],
+    ['id' => 2, 'goods_id' => 2, 'goods_sku_id' => 0, 'nums' => 1]
+];
+
+// 转换后的 goods_list
+$goods_list = [
+    ['goods_id' => 1, 'goods_sku_id' => 5, 'nums' => 2],
+    ['goods_id' => 2, 'goods_sku_id' => 0, 'nums' => 1]
+];
+```
+
+## 设计优势
+
+### 1. 代码复用性
+- **单一核心逻辑**: 订单计算和创建的核心逻辑只有一份
+- **避免重复代码**: 不同数据源最终使用相同的处理逻辑
+- **易于维护**: 业务规则变更只需修改核心方法
+
+### 2. 扩展性
+- **新增数据源**: 只需实现数据转换方法
+- **保持接口兼容**: 现有的对外接口保持不变
+- **灵活的组合**: 可以灵活组合不同的数据源和处理方式
+
+### 3. 一致性
+- **统一的验证**: 所有数据都经过相同的验证流程
+- **统一的错误处理**: 错误处理逻辑保持一致
+- **统一的返回格式**: 不同接口返回相同格式的数据
+
+### 4. 性能优化
+- **批量查询优化**: 使用 IN 查询替代循环查询,大幅提升性能
+- **减少数据库连接**: 统一的查询逻辑减少数据库连接次数
+- **缓存友好**: 标准化的数据格式便于缓存
+- **批量处理**: 可以更好地支持批量操作
+
+#### 性能优化详解
+
+**优化前(循环查询):**
+```php
+foreach ($goods_list as $item) {
+    // 每次循环都要查询数据库
+    $goods = Goods::where('id', $goods_id)->find();  // N次查询
+    $sku = Sku::where('id', $sku_id)->find();        // N次查询
+    $sku_attr = Db::name('shop_goods_sku_spec')...   // N次查询
+}
+// 总计: 3N 次数据库查询
+```
+
+**优化后(批量查询):**
+```php
+// 批量查询所有商品
+$goodsData = Goods::where('id', 'in', $goods_ids)->select();     // 1次查询
+
+// 批量查询指定的SKU
+$skuData = Sku::where('id', 'in', $sku_ids)->select();           // 1次查询
+
+// 批量查询没有指定SKU的商品的默认SKU
+$defaultSkuData = Sku::where('goods_id', 'in', $goods_without_sku)
+    ->where('is_default', 1)->select();                          // 1次查询
+
+// 批量查询所有规格属性
+$skuAttrData = Db::name('shop_goods_sku_spec')...                // 1次查询
+
+foreach ($goods_list as $item) {
+    // 直接从内存中获取数据,无需查询数据库
+    $goods = $goodsData[$goods_id];
+    $sku = $skuData[$sku_id]; // 统一使用SKU数据
+}
+// 总计: 最多4次数据库查询(固定)
+```
+
+**性能提升:**
+- 订单包含10个商品时: 30次查询 → 3次查询 (提升90%)
+- 订单包含100个商品时: 300次查询 → 3次查询 (提升99%)
+
+## 使用场景对比
+
+### 传统方式(优化前)
+```php
+// 购物车计算
+$cartResult = OrderService::calculateOrderByCart($cart_ids, $user_id, $area_id, $coupon_id);
+
+// 商品规格计算  
+$goodsResult = OrderService::calculateOrderByGoods($goods_list, $user_id, $area_id, $coupon_id);
+
+// 两套不同的内部实现逻辑
+```
+
+### 统一方式(优化后)
+```php
+// 购物车模式:先转换,再统一计算
+$goods_list = OrderService::convertCartToGoodsList($cart_ids, $user_id);
+$result = OrderService::calculateOrder($goods_list, $user_id, $area_id, $coupon_id);
+
+// 商品规格模式:直接统一计算
+$result = OrderService::calculateOrder($goods_list, $user_id, $area_id, $coupon_id);
+
+// 同一套核心实现逻辑
+```
+
+## API控制器层的实现
+
+### 统一的计算接口
+```php
+public function calculate()
+{
+    // 根据类型获取标准化的商品列表
+    if ($type === 'cart') {
+        $cart_ids = $this->request->post('cart_ids');
+        $goods_list = OrderService::convertCartToGoodsList($cart_ids, $this->auth->id);
+    } else {
+        $goods_list = $this->request->post('goods_list/a');
+    }
+    
+    // 统一调用计算方法
+    $result = OrderService::calculateOrder($goods_list, $this->auth->id, $area_id, $user_coupon_id);
+}
+```
+
+## 未来扩展可能
+
+### 1. 新增数据源
+- **收藏夹批量下单**: 实现 `convertFavoritesToGoodsList()`
+- **推荐商品组合**: 实现 `convertRecommendToGoodsList()`
+- **活动套餐**: 实现 `convertPackageToGoodsList()`
+
+### 2. 新增处理模式
+- **预约订单**: 扩展 `calculateOrder()` 支持预约模式
+- **分期订单**: 扩展计算逻辑支持分期付款
+- **团购订单**: 支持团购价格计算
+
+### 3. 优化方向
+- **异步处理**: 大批量商品的异步计算
+- **分布式计算**: 支持微服务架构下的订单计算
+- **智能缓存**: 基于商品组合的智能缓存策略
+
+## 总结
+
+这种统一的架构设计通过**数据标准化 + 核心逻辑统一**的方式,实现了:
+
+1. **简化了代码结构**: 消除了重复的业务逻辑
+2. **提高了代码质量**: 统一的验证和错误处理
+3. **增强了可维护性**: 核心逻辑修改影响范围明确
+4. **提升了扩展性**: 新增功能只需实现数据转换
+5. **保证了一致性**: 所有订单操作使用相同的业务规则
+
+这种设计模式体现了软件工程中的**单一职责原则**和**开闭原则**,是一个优秀的架构设计实践。 

+ 203 - 0
docs/OrderValidator_使用说明.md

@@ -0,0 +1,203 @@
+# API模块订单验证器使用说明
+
+## 概述
+
+`app\api\validate\Order` 验证器类为订单相关的API接口提供了完整的参数验证功能,确保请求参数的正确性和安全性。
+
+## 验证器功能
+
+### 支持的验证场景
+
+1. **create** - 通过购物车创建订单
+2. **createByGoods** - 通过商品规格创建订单
+3. **calculate** - 计算订单(支持购物车和商品规格两种模式)
+4. **detail** - 订单详情、确认收货、查询物流
+5. **cancel** - 取消订单
+6. **pay** - 订单支付
+
+### 验证规则
+
+#### 基础参数
+- `address_id`: 收货地址ID,必须,整数,大于0
+- `user_coupon_id`: 优惠券ID,可选,整数,大于0
+- `memo`: 备注,可选,最大500字符
+
+#### 购物车相关
+- `ids`: 购物车商品选择ID,必须,数组
+- `cart_ids`: 购物车ID列表,必须,数组
+
+#### 商品列表相关
+- `goods_list`: 商品列表,必须,数组,包含自定义验证
+  - `goods_id`: 商品ID,必须,整数,大于0
+  - `goods_sku_id`: 商品规格ID,可选,整数,大于等于0
+  - `nums`: 购买数量,必须,整数,大于0
+
+#### 计算类型
+- `type`: 计算类型,必须,支持: cart(购物车模式), goods(商品规格模式)
+
+#### 订单操作相关
+- `order_sn`: 订单号,必须,字母数字
+- `paytype`: 支付方式,必须,支持: alipay, wechat, unionpay, balance
+- `method`: 支付方法,必须,支持: web, wap, app, miniapp, mp, mini
+
+## 使用示例
+
+### 1. 通过商品规格创建订单
+
+```php
+// API接口: POST /api/order/createByGoods
+$data = [
+    'goods_list' => [
+        [
+            'goods_id' => 1,
+            'goods_sku_id' => 5,
+            'nums' => 2
+        ],
+        [
+            'goods_id' => 2,
+            'nums' => 1
+        ]
+    ],
+    'address_id' => 1,
+    'user_coupon_id' => 10,
+    'memo' => '请尽快发货'
+];
+
+// 验证器会自动验证这些参数
+```
+
+### 2. 计算订单(通过商品规格)
+
+```php
+// API接口: POST /api/order/calculate
+$data = [
+    'type' => 'goods',       // 计算类型:商品规格模式
+    'goods_list' => [
+        [
+            'goods_id' => 1,
+            'goods_sku_id' => 5,
+            'nums' => 2
+        ]
+    ],
+    'address_id' => 1,
+    'user_coupon_id' => 10
+];
+```
+
+### 3. 计算订单(通过购物车)
+
+```php
+// API接口: POST /api/order/calculate
+$data = [
+    'type' => 'cart',        // 计算类型:购物车模式
+    'cart_ids' => [1, 2, 3], // 购物车ID
+    'address_id' => 1,
+    'user_coupon_id' => 10
+];
+```
+
+### 4. 通过购物车创建订单
+
+```php
+// API接口: POST /api/order/create
+$data = [
+    'ids' => [1, 2, 3],  // 购物车商品选择ID
+    'address_id' => 1,
+    'user_coupon_id' => 10,
+    'memo' => '备注信息'
+];
+```
+
+### 5. 订单支付
+
+```php
+// API接口: POST /api/order/pay
+$data = [
+    'order_sn' => 'O202412270001',
+    'paytype' => 'wechat',
+    'method' => 'miniapp'
+];
+```
+
+### 6. 取消订单
+
+```php
+// API接口: POST /api/order/cancel
+$data = [
+    'order_sn' => 'O202412270001'
+];
+```
+
+### 7. 订单详情
+
+```php
+// API接口: GET /api/order/detail?order_sn=O202412270001
+// 或者 POST /api/order/detail
+$data = [
+    'order_sn' => 'O202412270001'
+];
+```
+
+## 自定义验证规则
+
+验证器包含一个自定义验证方法 `checkGoodsList`,用于验证商品列表的格式:
+
+```php
+protected function checkGoodsList($value, $rule, $data)
+{
+    // 验证商品列表是否为数组
+    // 验证每个商品项的必要字段
+    // 验证商品ID和数量的有效性
+    // 验证可选字段的格式
+}
+```
+
+## 错误消息
+
+验证器提供了详细的中文错误消息,包括:
+
+- 参数缺失提示
+- 参数格式错误提示
+- 参数值范围错误提示
+- 商品列表格式错误的具体位置提示
+
+## 控制器集成
+
+在订单控制器中的使用方式:
+
+```php
+public function createByGoods()
+{
+    // 验证请求参数
+    $validate = new \app\api\validate\Order();
+    if (!$validate->scene('createByGoods')->check($this->request->post())) {
+        $this->error($validate->getError());
+    }
+    
+    // 业务逻辑处理
+    // ...
+}
+```
+
+## 注意事项
+
+1. 验证器会自动检查商品列表的格式和内容
+2. 所有必填参数都会被严格验证
+3. 支付方式和方法的选项会根据系统配置动态调整
+4. 错误消息会准确指出问题的具体位置
+5. 验证器与OrderService服务类配合使用,提供完整的订单处理流程
+
+## API接口列表
+
+| 接口 | 方法 | 验证场景 | 说明 |
+|------|------|----------|------|
+| `/api/order/create` | POST | create | 通过购物车创建订单 |
+| `/api/order/createByGoods` | POST | createByGoods | 通过商品规格创建订单 |
+| `/api/order/calculate` | POST | calculate | 计算订单(支持两种模式) |
+| `/api/order/detail` | GET/POST | detail | 获取订单详情 |
+| `/api/order/cancel` | POST | cancel | 取消订单 |
+| `/api/order/pay` | POST | pay | 订单支付 |
+| `/api/order/receipt` | POST | detail | 确认收货 |
+| `/api/order/logistics` | GET | detail | 查询物流 |
+
+通过这个验证器,可以确保所有订单相关的API接口都有统一、规范的参数验证,提高系统的安全性和稳定性。 

+ 298 - 0
docs/SkuSpec_使用说明.md

@@ -0,0 +1,298 @@
+# SkuSpec 服务类使用说明
+
+## 概述
+
+`SkuSpec` 服务类封装了商品规格相关的所有查询操作,基于 `fa_shop_goods_sku_spec` 和 `fa_shop_spec` 表结构设计。
+
+## 表结构说明
+
+### fa_shop_goods_sku_spec (商品SKU规格关联表)
+```sql
+CREATE TABLE `fa_shop_goods_sku_spec` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `goods_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '商品ID',
+  `goods_sku_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'SKU ID',
+  `spec_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '规格ID',
+  `spec_value_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '规格值ID',
+  `createtime` bigint(20) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(20) DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+### fa_shop_spec (商品规格表)
+```sql
+CREATE TABLE `fa_shop_spec` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `name` varchar(100) NOT NULL DEFAULT '' COMMENT '名称',
+  `type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '规格类型、 1:基础规格 2:定制规格',
+  `createtime` bigint(20) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(20) DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品规格表';
+```
+
+## 方法说明
+
+### 1. getSkuAttrs() - 批量获取SKU规格属性字符串
+
+**用途**: 获取多个SKU的规格属性字符串,用于订单显示
+
+**参数**: 
+- `$sku_ids` (array): SKU ID数组
+
+**返回**: 
+- `array`: `[sku_id => 'spec_name:spec_value,spec_name:spec_value']` 格式
+
+**示例**:
+```php
+use app\common\Service\SkuSpec;
+
+$sku_ids = [1, 2, 3];
+$attrs = SkuSpec::getSkuAttrs($sku_ids);
+
+// 返回结果示例:
+// [
+//     1 => '颜色:红色,尺寸:L',
+//     2 => '颜色:蓝色,尺寸:M', 
+//     3 => '' // 单规格商品返回空字符串
+// ]
+```
+
+### 2. getGoodsSkuSpec() - 获取指定商品的SKU信息
+
+**用途**: 获取商品的规格分组信息
+
+**参数**: 
+- `$goods_id` (int): 商品ID
+
+**返回**: 
+- `array`: 规格分组数据
+
+**示例**:
+```php
+$specs = SkuSpec::getGoodsSkuSpec(1);
+
+// 返回结果示例:
+// [
+//     ['id' => 1, 'goods_id' => 1, 'spec_id' => 1], // 颜色规格
+//     ['id' => 2, 'goods_id' => 1, 'spec_id' => 2]  // 尺寸规格
+// ]
+```
+
+### 3. getGoodsSpecCombinations() - 获取商品的所有规格组合
+
+**用途**: 获取商品的完整规格组合详情
+
+**参数**: 
+- `$goods_id` (int): 商品ID
+
+**返回**: 
+- `array`: 规格组合详情
+
+**示例**:
+```php
+$combinations = SkuSpec::getGoodsSpecCombinations(1);
+
+// 返回结果示例:
+// [
+//     ['goods_id' => 1, 'spec_id' => 1, 'spec_value_id' => 1, 'spec_name' => '颜色', 'spec_value' => '红色'],
+//     ['goods_id' => 1, 'spec_id' => 1, 'spec_value_id' => 2, 'spec_name' => '颜色', 'spec_value' => '蓝色'],
+//     ['goods_id' => 1, 'spec_id' => 2, 'spec_value_id' => 3, 'spec_name' => '尺寸', 'spec_value' => 'L'],
+//     ['goods_id' => 1, 'spec_id' => 2, 'spec_value_id' => 4, 'spec_name' => '尺寸', 'spec_value' => 'M']
+// ]
+```
+
+### 4. getMultiGoodsSpecs() - 获取多个商品的规格信息
+
+**用途**: 批量获取多个商品的规格统计信息
+
+**参数**: 
+- `$goods_ids` (array): 商品ID数组
+
+**返回**: 
+- `array`: 按商品ID分组的规格信息
+
+**示例**:
+```php
+$specs = SkuSpec::getMultiGoodsSpecs([1, 2, 3]);
+
+// 返回结果示例:
+// [
+//     1 => [
+//         ['goods_id' => 1, 'spec_id' => 1, 'spec_name' => '颜色', 'spec_count' => 2],
+//         ['goods_id' => 1, 'spec_id' => 2, 'spec_name' => '尺寸', 'spec_count' => 2]
+//     ],
+//     2 => [
+//         ['goods_id' => 2, 'spec_id' => 1, 'spec_name' => '颜色', 'spec_count' => 3]
+//     ]
+// ]
+```
+
+### 5. findSkuBySpecValues() - 根据规格值查找SKU
+
+**用途**: 根据规格值ID组合查找对应的SKU ID
+
+**参数**: 
+- `$goods_id` (int): 商品ID
+- `$spec_value_ids` (array): 规格值ID数组
+
+**返回**: 
+- `int|null`: SKU ID 或 null
+
+**示例**:
+```php
+$sku_id = SkuSpec::findSkuBySpecValues(1, [1, 3]); // 红色 + L尺寸
+
+// 返回结果: 
+// 5 (对应的SKU ID) 或 null (未找到匹配的SKU)
+```
+
+### 6. getSpecNameValueMap() - 获取规格名称和值的映射
+
+**用途**: 获取规格的完整映射关系,包含图片等扩展信息
+
+**参数**: 
+- `$spec_ids` (array): 规格ID数组
+
+**返回**: 
+- `array`: 规格映射数据
+
+**示例**:
+```php
+$map = SkuSpec::getSpecNameValueMap([1, 2]);
+
+// 返回结果示例:
+// [
+//     1 => [ // 颜色规格
+//         1 => ['spec_name' => '颜色', 'spec_value' => '红色', 'image' => 'red.jpg', 'desc' => ''],
+//         2 => ['spec_name' => '颜色', 'spec_value' => '蓝色', 'image' => 'blue.jpg', 'desc' => '']
+//     ],
+//     2 => [ // 尺寸规格
+//         3 => ['spec_name' => '尺寸', 'spec_value' => 'L', 'image' => '', 'desc' => ''],
+//         4 => ['spec_name' => '尺寸', 'spec_value' => 'M', 'image' => '', 'desc' => '']
+//     ]
+// ]
+```
+
+### 7. isMultiSpec() - 检查是否为多规格商品
+
+**用途**: 快速判断商品是否有规格
+
+**参数**: 
+- `$goods_id` (int): 商品ID
+
+**返回**: 
+- `bool`: true=多规格,false=单规格
+
+**示例**:
+```php
+$isMulti = SkuSpec::isMultiSpec(1);
+// 返回: true 或 false
+```
+
+### 8. getSpecCount() - 获取商品的规格数量
+
+**用途**: 获取商品有多少种规格类型
+
+**参数**: 
+- `$goods_id` (int): 商品ID
+
+**返回**: 
+- `int`: 规格类型数量
+
+**示例**:
+```php
+$count = SkuSpec::getSpecCount(1);
+// 返回: 2 (表示有颜色和尺寸两种规格)
+```
+
+## 在OrderService中的应用
+
+### 优化前的代码
+```php
+// 复杂的多表JOIN查询
+$skuAttrCollection = Db::name('shop_goods_sku_spec')
+    ->alias('gss')
+    ->field('gss.goods_sku_id, GROUP_CONCAT(sp.name,":",sv.value ORDER BY sp.id asc) as sku_attr')
+    ->join('shop_spec sp', 'sp.id = gss.spec_id', 'LEFT')
+    ->join('shop_spec_value sv', 'sv.id = gss.spec_value_id', 'LEFT')
+    ->join('shop_goods_sku sku', 'sku.id = gss.goods_sku_id', 'LEFT')
+    ->where('gss.goods_sku_id', 'in', $sku_ids)
+    ->group('gss.goods_sku_id')
+    ->select();
+```
+
+### 优化后的代码
+```php
+// 简洁的服务调用
+$skuAttrData = \app\common\Service\SkuSpec::getSkuAttrs($sku_ids);
+```
+
+## 使用场景
+
+### 1. 商品详情页规格展示
+```php
+// 获取商品的所有规格组合
+$specs = SkuSpec::getGoodsSpecCombinations($goods_id);
+
+// 按规格分组显示
+$groupedSpecs = [];
+foreach ($specs as $spec) {
+    $groupedSpecs[$spec['spec_name']][] = $spec;
+}
+```
+
+### 2. 订单商品规格显示
+```php
+// 批量获取订单中所有SKU的规格属性
+$sku_ids = array_column($orderGoods, 'goods_sku_id');
+$skuAttrs = SkuSpec::getSkuAttrs($sku_ids);
+
+// 在订单列表中显示规格信息
+foreach ($orderGoods as &$item) {
+    $item['spec_attr'] = $skuAttrs[$item['goods_sku_id']] ?? '';
+}
+```
+
+### 3. 购物车规格匹配
+```php
+// 用户选择规格后,查找对应的SKU
+$selectedSpecs = [1, 3]; // 红色 + L尺寸
+$sku_id = SkuSpec::findSkuBySpecValues($goods_id, $selectedSpecs);
+
+if ($sku_id) {
+    // 找到对应SKU,可以加入购物车
+    echo "SKU ID: " . $sku_id;
+} else {
+    // 规格组合不存在
+    echo "该规格组合不存在";
+}
+```
+
+### 4. 商品列表批量处理
+```php
+// 批量获取多个商品的规格信息
+$goods_ids = [1, 2, 3, 4, 5];
+$multiSpecs = SkuSpec::getMultiGoodsSpecs($goods_ids);
+
+// 标记哪些商品是多规格
+foreach ($goodsList as &$goods) {
+    $goods['is_multi_spec'] = isset($multiSpecs[$goods['id']]);
+    $goods['spec_count'] = count($multiSpecs[$goods['id']] ?? []);
+}
+```
+
+## 性能优势
+
+1. **减少重复代码**: 统一封装规格查询逻辑
+2. **优化查询**: 使用批量查询,减少数据库连接
+3. **缓存友好**: 结果格式标准化,便于缓存
+4. **易于维护**: 规格相关逻辑集中管理
+
+## 注意事项
+
+1. **数据一致性**: 确保 `goods_sku_id` 字段在所有相关表中保持一致
+2. **空值处理**: 单规格商品的规格属性返回空字符串
+3. **性能考虑**: 大批量查询时建议分批处理
+4. **错误处理**: 方法内部已处理空参数情况,返回空数组或null 

+ 41 - 41
public/assets/js/backend/shop/goods.js

@@ -1999,7 +1999,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
         },
 
 
-                    api: {
+        api: {
             // 初始化规格类型控制
             initSpecTypeControl: function() {
                 // 监听规格类型切换
@@ -2412,64 +2412,64 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                                         }
                                         console.log('解析完成的规格名称映射:', specName);
                                         
-                                                                // 构建规格值映射表(从数据库获取的完整数据)
-                        let specValueMap = {};
-                        if (Config.spec_values && Config.spec_values.length > 0) {
-                            console.log('发现规格值数据:', Config.spec_values);
+                                        // 构建规格值映射表(从数据库获取的完整数据)
+                                        let specValueMap = {};
+                                        if (Config.spec_values && Config.spec_values.length > 0) {
+                                            console.log('发现规格值数据:', Config.spec_values);
                             console.log('规格值数据类型:', typeof Config.spec_values);
                             console.log('规格值数据结构样例:', Config.spec_values[0]);
                             
-                            Config.spec_values.forEach(function(item) {
-                                if (!specValueMap[item.spec_name]) {
-                                    specValueMap[item.spec_name] = {};
-                                }
-                                specValueMap[item.spec_name][item.value] = {
-                                    name: item.value,
-                                    image: item.image || '',
-                                    description: item.description || ''
-                                };
-                            });
-                            console.log('规格值映射表:', specValueMap);
+                                            Config.spec_values.forEach(function(item) {
+                                                if (!specValueMap[item.spec_name]) {
+                                                    specValueMap[item.spec_name] = {};
+                                                }
+                                                specValueMap[item.spec_name][item.value] = {
+                                                    name: item.value,
+                                                    image: item.image || '',
+                                                    description: item.description || ''
+                                                };
+                                            });
+                                            console.log('规格值映射表:', specValueMap);
                         } else {
                             console.log('没有发现规格值数据,使用默认处理方式');
                             console.log('Config.spec_values:', Config.spec_values);
-                        }
+                                        }
                                         
-                                                                for (let i in specName) {
-                            // 转换旧格式数据为新格式,优先使用数据库中的完整数据
-                            let valueList = specName[i].map(val => {
-                                if (typeof val === 'string') {
-                                    // 从映射表中查找完整数据,如果没有则使用默认值
-                                    if (specValueMap[i] && specValueMap[i][val]) {
-                                        console.log('找到规格值完整数据:', i, val, specValueMap[i][val]);
+                                        for (let i in specName) {
+                                            // 转换旧格式数据为新格式,优先使用数据库中的完整数据
+                                            let valueList = specName[i].map(val => {
+                                                if (typeof val === 'string') {
+                                                    // 从映射表中查找完整数据,如果没有则使用默认值
+                                                    if (specValueMap[i] && specValueMap[i][val]) {
+                                                        console.log('找到规格值完整数据:', i, val, specValueMap[i][val]);
                                         return {
                                             name: specValueMap[i][val].name,
                                             image: specValueMap[i][val].image || '',
                                             description: specValueMap[i][val].description || ''
                                         };
-                                    } else {
-                                        console.log('使用默认规格值数据:', i, val);
-                                        return {
-                                            name: val,
-                                            image: '',
-                                            description: ''
-                                        };
-                                    }
-                                }
+                                                    } else {
+                                                        console.log('使用默认规格值数据:', i, val);
+                                                        return {
+                                                            name: val,
+                                                            image: '',
+                                                            description: ''
+                                                        };
+                                                    }
+                                                }
                                 // 如果已经是对象格式,确保结构完整
                                 return {
                                     name: val.name || val,
                                     image: val.image || '',
                                     description: val.description || ''
                                 };
-                            });
-                            
-                            specList.push({
-                                name: i,
-                                type: 'basic', // 默认为基础规格
-                                value: valueList
-                            });
-                        }
+                                            });
+                                            
+                                            specList.push({
+                                                name: i,
+                                                type: 'basic', // 默认为基础规格
+                                                value: valueList
+                                            });
+                                        }
                                         
                                         this.skus = skus;
                                         this.specList = specList;