buildRequestPayload('cityList', $body); // 发送 POST 请求 $resp = HttpClient::post("{$this->baseUrl}/cityList", $payload); return $resp; } /** * 转化订单数据 * @param $order * @param string $orderType 订单类型:xhOrder 或 xhGhsOrder * @param $shop * @param $params * @return array * @throws \Exception */ public function formatOrderData($order, $orderType, $shop, $params) { $hasOldOrder = false; if($orderType == 'xhOrder'){ $deliveryOrders = HdDeliveryOrderClass::getByCondition([ 'deliveryId' => 'dada', 'hdOrderId' => $order['id'] ], 'id DESC', 'id,mainId,orderId'); if(is_array($deliveryOrders) && count($deliveryOrders) >= 1) { $hasOldOrder = true; } } elseif($orderType == 'xhGhsOrder') { // 查找批发配送订单 $deliveryOrders = GhsDeliveryOrderClass::getAllByCondition([ 'deliveryId' => 'dada', 'ghsOrderId' => $order['id'] ], 'id DESC', 'id,mainId,orderId'); if(is_array($deliveryOrders) && count($deliveryOrders) >= 1) { $hasOldOrder = true; } } $orderData = [ 'deliveryNo' => $params['deliveryNo'], 'fee' => $params['price'], 'distance' => $params['distance'], 'hasOldOrder' => $hasOldOrder, 'orderSn' => $order['orderSn'], 'params' => $hasOldOrder ? $this->formatOrderDataByResourceData($order, $orderType, $shop, $params) : [], ]; return $orderData; } /** * 转化订单数据 * @param $order * @param string $orderType 订单类型:xhOrder 或 xhGhsOrder * @param $shop * @param $params * @return array * @throws \Exception */ public function formatOrderDataByResourceData($order, $orderType, $shop, $params) { $shopNo = $this->shopNo; if(empty($shopNo)){ util::fail("DadaAdapter formatOrderData shopNo is empty"); } $productList = []; if($orderType == 'xhGhsOrder'){ $itemInfos = OrderItemClass::getAllByCondition(['orderSn' => $order['orderSn']], null, 'id, name, unitPrice, num, xhUnitName'); foreach ($itemInfos as $item){ $productList[] = [ 'sku_name' => $item['name'], 'src_product_no' => (string)$item['id'], 'count' => (float)$item['num'], 'unit' => $item['xhUnitName'], ]; } } else if($orderType == 'xhOrder'){ $itemInfos = OrderGoodsClass::getAllByCondition(['orderSn' => $order['orderSn']], null, 'id, name, unitPrice, num'); foreach ($itemInfos as $item){ $productList[] = [ 'sku_name' => $item['name'], 'src_product_no' => (string)$item['id'], 'count' => (float)$item['num'], 'unit' => '件', ]; } } else { Yii::error('订单编号出错: '.$order['orderSn']); util::fail('订单编号出错'); } // 默认回调地址 $callbackUrl = Yii::$app->params['ghsHost'] . '/delivery/dada-callback'; return [ 'order_id' => $order['orderSn'], 'origin_id' => $order['orderSn'], // 达达API通常使用 origin_id 'shop_no' => $shopNo, 'receiver_name' => $order['customName'], 'receiver_address' => $order['fullAddress'], 'receiver_phone' => Functions::getMobile($order), 'receiver_lat' => (float)$order['lat'], 'receiver_lng' => (float)$order['long'], 'cargo_price' => (float)$order['actPrice'], // 单位:元 'tips' => 0, 'info' => $order['remark'] ?? '', 'callback' => $callbackUrl, 'cargo_weight' => isset($params['weight']) ? (float)$params['weight'] : 1.0, 'product_list' => $productList, 'is_prepay' => 0, // 是否需要垫付 'pickupTime' => $params['pickupTime'], ]; } /** * 查询运费后下单 -- addAfterQuery * @param array $data 包含 deliveryNo 等参数 * @return array */ public function createOrder($data) { if($data['hasOldOrder']){ $deliveryNo = $data['deliveryNo']; $orderId = $data['orderSn']; $params = $data['params']; $ret = $this->reCreateOrder($deliveryNo, $orderId, $params); }else{ $ret = $this->firstCreateOrder($data); } return $ret; } /** * 查询运费后下单 -- addAfterQuery * @param array $data 包含 deliveryNo 等参数 * @return array */ public function firstCreateOrder($data) { // 验证必需字段 if (empty($data['deliveryNo'])) { return [ 'code' => -1, 'msg' => '缺少必需参数: deliveryNo' ]; } $apiData = [ 'deliveryNo' => $data['deliveryNo'] ]; if (isset($data['enableReset'])) { $apiData['enableReset'] = (bool)$data['enableReset']; } if (!empty($data['info'])) { $apiData['info'] = $data['info']; } $body = json_encode($apiData, JSON_UNESCAPED_UNICODE); $payload = $this->buildRequestPayload('api/order/addAfterQuery', $body); $resp = HttpClient::post("{$this->baseUrl}/api/order/addAfterQuery", $payload); if (isset($resp['code']) && $resp['code'] == 0) { return [ 'code' => 0, 'platform' => 'dada', 'data' => [ 'orderId' => $data['deliveryNo'], 'deliveryNo' => $data['deliveryNo'], 'fee' => $data['fee'], 'distance' => $data['distance'], 'deliverFee' => $data['deliverFee'] ?? 0, 'couponName' => $data['couponName'] ?? '', 'couponFee' => $data['couponFee'] ?? 0, ], ]; } return [ 'code' => $resp['code'] ?? -1, 'msg' => $resp['msg'] ?? 'Unknown error', 'data' => null ]; } /** * 创建订单(指派订单给配送员) * * 根据达达官方文档实现下单功能 * * @param array $data 订单数据 * @return array API 响应结果 */ public function createOrderByResourceData($data) { // 验证必需字段 if (empty($data['order_id']) || empty($data['shop_no'])) { return [ 'code' => -1, 'msg' => '缺少必需参数: order_id 和 shop_no' ]; } // 构建业务参数(按达达 API 规范) // 达达新增订单接口(orderAddRequest)主要参数:shop_no, origin_id, ... $apiData = [ 'shop_no' => $data['shop_no'], 'origin_id' => $data['origin_id'] ?? $data['order_id'], // 优先使用 origin_id 'city_code' => '', // 可选 'cargo_price' => isset($data['cargo_price']) ? (float)$data['cargo_price'] : 0, 'is_prepay' => isset($data['is_prepay']) ? (int)$data['is_prepay'] : 0, 'receiver_name' => $data['receiver_name'] ?? '', 'receiver_address' => $data['receiver_address'] ?? '', 'receiver_phone' => $data['receiver_phone'] ?? '', 'receiver_lat' => isset($data['receiver_lat']) ? (double)$data['receiver_lat'] : 0, 'receiver_lng' => isset($data['receiver_lng']) ? (double)$data['receiver_lng'] : 0, 'callback' => $data['callback'] ?? '', ]; // 如果指定了配送员ID,添加到请求 if (isset($data['transporter_id'])) { $apiData['transporter_id'] = (int)$data['transporter_id']; } if (isset($data['tips'])) { $apiData['tips'] = (float)$data['tips']; } if (isset($data['distance'])) { $apiData['distance'] = (double)$data['distance']; } if (isset($data['prepare_time'])) { // 废弃字段,但保留兼容 //$apiData['prepare_time'] = (int)$data['prepare_time']; } if (!empty($data['info'])) { // 订单备注 $apiData['info'] = $data['info']; } elseif (!empty($data['remark'])) { $apiData['info'] = $data['remark']; } if (isset($data['cargo_weight'])) { $apiData['cargo_weight'] = (double)$data['cargo_weight']; } if (isset($data['product_list'])) { $apiData['product_list'] = $data['product_list']; } // 将业务参数转换为 JSON 字符串 $body = json_encode($apiData, JSON_UNESCAPED_UNICODE); // 构建完整的 API 请求参数 $payload = $this->buildRequestPayload('orderAddRequest', $body); // 发送 POST 请求 $resp = HttpClient::post("{$this->baseUrl}/orderAddRequest", $payload); if(isset($resp['code']) && $resp['code'] == 0){ return [ 'code' => 0, 'platform' => 'dada', 'data' => [ 'orderId' => $resp['order_id'], ], ]; } return [ 'code' => $resp['code'] ?? -1, 'msg' => $resp['msg'] ?? 'Unknown error', 'data' => null ]; } /** * 重新下单 -- /api/order/reAddOrder * 同一个商户的单号(根据三方单号识别)如果已经在达达侧创建了订单,在该订单被取消(已取消=5)、拒收返还(妥投异常物品返还完成=10)后,可以调用该接口重发订单,达达将根据传入的配送信息重新计算运费并创建达达物流订单。 * @param string $deliveryNo * @param string $orderId 第三方订单ID * @param array $params * @return array */ public function reCreateOrder($deliveryNo, $orderId, $params) { // 验证必需字段 if (empty($orderId) || empty($params['shop_no'])) { return [ 'code' => -1, 'msg' => '缺少必需参数: order_id 和 shop_no' ]; } // 构建业务参数 $apiData = [ 'shop_no' => $params['shop_no'], 'origin_id' => $orderId, 'cargo_price' => isset($params['cargo_price']) ? (float)$params['cargo_price'] : 0, 'is_prepay' => isset($params['is_prepay']) ? (int)$params['is_prepay'] : 0, 'receiver_name' => $params['receiver_name'] ?? '', 'receiver_address' => $params['receiver_address'] ?? '', 'receiver_lat' => isset($params['receiver_lat']) ? (double)$params['receiver_lat'] : 0, 'receiver_lng' => isset($params['receiver_lng']) ? (double)$params['receiver_lng'] : 0, 'callback' => $params['callback'] ?? '', 'cargo_weight' => isset($params['cargo_weight']) ? (double)$params['cargo_weight'] : 0, ]; // 可选参数列表 $optionalFields = [ 'receiver_phone', 'receiver_tel', 'tips', 'info', 'cargo_type', 'cargo_num', 'invoice_title', 'origin_mark', 'origin_mark_no', 'is_use_insurance', 'is_finish_code_needed', 'delay_publish_time', 'is_expect_finish_order', 'expect_finish_time_limit', 'is_direct_delivery', 'product_list', 'pick_up_pos', 'fetch_code', 'business_type', 'supplier_name', 'supplier_address', 'supplier_phone', 'supplier_lat', 'supplier_lng' ]; foreach ($optionalFields as $field) { if (isset($params[$field])) { $apiData[$field] = $params[$field]; } } // 处理预约单 $ts = strtotime($params['pickupTime']); if ($ts > time() + 30) { // delay_publish_time // 预约发单时间(unix时间戳(10位),精确到分),可以支持未来3天内的订单发预约单,如预约发单时间早于当前时间会作为即时单下发。 $apiData['delay_publish_time'] = $ts; $apiData['is_expect_finish_order'] = 1; // 是否根据期望送达时间预约发单(0-否,即时发单;1-是,预约发单),如传1则期望送达时间必传。 $apiData['expect_finish_time_limit'] = $ts; // 期望送达时间(单位秒,不早于当前时间) } // 特殊处理:如果 info 没传但有 remark,尝试使用 remark if (!isset($apiData['info']) && !empty($params['remark'])) { $apiData['info'] = $params['remark']; } $body = json_encode($apiData, JSON_UNESCAPED_UNICODE); $payload = $this->buildRequestPayload('api/order/reAddOrder', $body); $resp = HttpClient::post("{$this->baseUrl}/api/order/reAddOrder", $payload); if(isset($resp['code']) && $resp['code'] == 0){ $data = $resp['result']; return [ 'code' => 0, 'platform' => 'dada', 'data' => [ 'orderId' => $deliveryNo, 'deliveryNo' => $deliveryNo, 'fee' => $data['fee'], 'distance' => $data['distance'], 'deliverFee' => $data['deliverFee'], 'couponName' => $data['couponName'] ?? '', 'couponFee' => $data['couponFee'] ?? 0, ], ]; } return [ 'code' => $resp['code'] ?? -1, 'msg' => $resp['msg'] ?? 'Unknown error', 'data' => null ]; } /** * 取消订单 * 在订单待接单或待取货情况下,调用此接口可取消订单 * @param $orderId * @param $params * @return array */ public function cancelOrder($orderId, $params) { // 验证必需参数 if (empty($orderId)) { return [ 'code' => -1, 'msg' => '缺少订单号' ]; } if (empty($params['order_cancel_code'])) { return [ 'code' => -1, 'msg' => '缺少取消原因ID' ]; } if ($params['order_cancel_code'] == 10000 && empty($params['order_cancel_reason'])) { return [ 'code' => -1, 'msg' => '当取消原因ID为其他时,取消原因必填' ]; } // 构建业务参数 $apiData = [ 'order_id' => $orderId, 'cancel_reason_id' => (int)$params['order_cancel_code'], ]; if (!empty($params['order_cancel_reason'])) { $apiData['cancel_reason'] = $params['order_cancel_reason']; } $body = json_encode($apiData, JSON_UNESCAPED_UNICODE); $payload = $this->buildRequestPayload('api/order/formalCancel', $body); $resp = HttpClient::post("{$this->baseUrl}/api/order/formalCancel", $payload); // 处理响应 if (isset($resp['status']) && $resp['status'] === 'success' && $resp['code'] == 0) { $result = $resp['result'] ?? []; return [ 'code' => 0, 'platform' => 'dada', 'msg' => '取消成功', 'data' => [ 'deductionFee' => $result['deduct_fee'] ?? 0.0, // 扣除的违约金 ], ]; } return [ 'code' => $resp['code'] ?? -1, 'platform' => 'dada', 'msg' => $resp['msg'] ?? '取消失败', 'data' => null ]; } /** * 获取订单运费报价 * * 根据达达官方文档实现订单运费估价功能 * * @param array $data 运费估价参数,结构参考: * [ * 'shop_no' => '门店编号' (必需), * 'origin_id' => '原始订单ID' (必需), // 商户侧订单编号 * 'receiver_address' => '收货地址' (必需), * 'receiver_phone' => '收货人电话' (必需), * 'receiver_lng' => 经度 (必需), * 'receiver_lat' => 纬度 (必需), * 'cargo_price' => 货物价格(分) (可选), // 商品价格,单位为分 * 'distance' => 距离(米) (可选), * 'prepare_time' => 商品准备时间 (可选), // 单位:秒 * ] * @return array|null 运费报价结果 */ public function getPrice($data) { // 验证必需字段 if (empty($data['shop_no']) || empty($data['origin_id']) || empty($data['receiver_address']) || empty($data['receiver_phone']) || !isset($data['receiver_lng']) || !isset($data['receiver_lat'])) { return [ 'code' => -1, 'msg' => '缺少必需参数' ]; } // 构建业务参数(按达达 API 规范) $apiData = [ 'shop_no' => $data['shop_no'], 'origin_id' => $data['origin_id'], 'receiver_address' => $data['receiver_address'], 'receiver_phone' => $data['receiver_phone'], 'receiver_lng' => (double)$data['receiver_lng'], 'receiver_lat' => (double)$data['receiver_lat'], ]; // 可选参数 if (isset($data['cargo_price'])) { $apiData['cargo_price'] = (int)$data['cargo_price']; } if (isset($data['distance'])) { $apiData['distance'] = (int)$data['distance']; } if (isset($data['prepare_time'])) { $apiData['prepare_time'] = (int)$data['prepare_time']; } $body = json_encode($apiData, JSON_UNESCAPED_UNICODE); $payload = $this->buildRequestPayload('queryDeliverFee', $body); $resp = HttpClient::post("{$this->baseUrl}/queryDeliverFee", $payload); // 处理响应 if (isset($resp['status']) && $resp['status'] === 'success' && $resp['code'] == 0) { $result = $resp['result'] ?? []; return [ 'code' => 0, 'platform' => 'dada', 'deliver_fee' => $result['deliver_fee'] ?? 0, // 配送费(分) 'distance' => $result['distance'] ?? 0, // 距离(米) 'duration' => $result['duration'] ?? 0, // 配送时间(分钟) 'tips' => $result['tips'] ?? 0, // 小费(分) ]; } return [ 'code' => $resp['code'] ?? -1, 'msg' => $resp['msg'] ?? 'Unknown error', 'data' => null ]; } /** * 生成随机数字符串(备用方法,如需要使用) * * @param int $length 长度 * @return string 随机字符串 */ protected function generateNonceStr($length = 16) { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $str = ''; for ($i = 0; $i < $length; $i++) { $str .= $chars[random_int(0, strlen($chars) - 1)]; } return $str; } /** * 增加小费 * @param array $data 包含 deliveryNo 等参数 * @return array */ public function addTip($data) { // 验证必需字段 if (empty($data['order_id'])) { return [ 'code' => -1, 'msg' => '缺少必需参数: order_id' ]; } if (!isset($data['tips'])) { return [ 'code' => -1, 'msg' => '缺少必需参数: tips' ]; } $apiData = [ 'order_id' => $data['order_id'], 'tips' => (float)$data['tips'], ]; if (!empty($data['info'])) { $apiData['info'] = $data['info']; } $body = json_encode($apiData, JSON_UNESCAPED_UNICODE); $payload = $this->buildRequestPayload('api/order/addTip', $body); $resp = HttpClient::post("{$this->baseUrl}/api/order/addTip", $payload); return $resp; } public function setSourceId($sourceId) { $this->sourceId = $sourceId; } public function getSourceId($sourceId) { return $this->sourceId; } /** * 构建运费报价请求信息 * @param array $order 预处理后的订单数据 * @param string $orderTime 配送时间 * @return array */ public function buildPriceRequest($order, $orderTime) { // 1. 准备业务参数 $apiData = [ 'shop_no' => $order['shop_no'] ?? $this->getShopNo(), 'origin_id' => $order['origin_id'], 'cargo_price' => (float)$order['cargo_price'], 'is_prepay' => (int)$order['is_prepay'], 'receiver_name' => $order['receiver_name'], 'receiver_address' => $order['receiver_address'], 'receiver_lat' => (double)$order['receiver_lat'], 'receiver_lng' => (double)$order['receiver_lng'], 'callback' => $order['callback'], 'cargo_weight' => (double)$order['cargo_weight'], 'receiver_phone' => $order['receiver_phone'], ]; // 处理预约单 $ts = strtotime($orderTime); if ($ts > time() + 30) { // delay_publish_time // 预约发单时间(unix时间戳(10位),精确到分),可以支持未来3天内的订单发预约单,如预约发单时间早于当前时间会作为即时单下发。 $apiData['delay_publish_time'] = $ts; $apiData['is_expect_finish_order'] = 1; // 是否根据期望送达时间预约发单(0-否,即时发单;1-是,预约发单),如传1则期望送达时间必传。 $apiData['expect_finish_time_limit'] = $ts + 3000; // 期望送达时间(单位秒)== 预约发单时间 + 50分钟 } // 添加可选参数 if (isset($order['tips'])) { $apiData['tips'] = (float)$order['tips']; } if (isset($order['info'])) { $apiData['info'] = $order['info']; } if (isset($order['cargo_type'])) { $apiData['cargo_type'] = (int)$order['cargo_type']; } if (isset($order['cargo_num'])) { $apiData['cargo_num'] = (int)$order['cargo_num']; } if (isset($order['is_use_insurance'])) { $apiData['is_use_insurance'] = (int)$order['is_use_insurance']; } if (isset($order['product_list']) && !empty($order['product_list'])) { $apiData['product_list'] = $order['product_list']; } // 2. 构建完整请求 Payload (含签名) $body = json_encode($apiData, JSON_UNESCAPED_UNICODE); $payload = $this->buildRequestPayload('order/queryDeliverFee', $body); // 3. 返回请求配置 (供 HttpClient::postConcurrent 使用) return [ 'url' => $this->baseUrl . '/api/order/queryDeliverFee', 'data' => $payload, 'headers' => ['Content-Type' => 'application/json'], 'timeout' => 5.0, ]; } public function processPriceResponse($resp) { //Yii::info("[DadaAdapter] processPriceResponse: " . json_encode($resp)); if (isset($resp['code']) && $resp['code'] == 0 && isset($resp['result'])) { $result = $resp['result']; //"result":{ // "distance":3192, // "fee":7.99, //实际运费(单位:元),运费减去优惠券费用 // "deliverFee":9.4, //运费(单位:元) // "couponFee":1.41, //优惠券费用(单位:元) // "couponName":"\u60ca\u559c\u4f18\u60e0", // "deliveryNo":"Dadabebc27e1ec78410cbb497897f5f22a88", //平台订单号 // "insuranceFee":0, //保价费(单位:元) // "tips":0, // "expiredTime":1765361146 //平台订单号的过期时间 // } $ret = [ 'platform' => 'dada', 'fee' => $result['fee'] * 100, // *100 把单位:元 变为 分 'distance' => $result['distance'], 'tips' => $result['tips'], 'deliverFee' => $result['deliverFee'], 'deliveryNo' => $result['deliveryNo'], ]; // 只有在有优惠券时,才进行coupon相关设置 if(isset($result['couponName']) && isset($result['couponFee'])){ $ret['couponName'] = $result['couponName']; $ret['couponFee'] = $result['couponFee']; } return $ret; } return [ 'code' => $resp['code'] ?? -1, 'error' => $resp['msg'] ?? 'Unknown error', 'data' => null ]; } /** * 查询订单详情 * @param $orderId * @return mixed */ public function selectOrder($orderId) { // 验证必需参数 if (empty($orderId)) { return [ 'code' => -1, 'msg' => '缺少必需参数: order_id' ]; } // 构建业务参数 $apiData = [ 'order_id' => $orderId, ]; $body = json_encode($apiData, JSON_UNESCAPED_UNICODE); $payload = $this->buildRequestPayload('api/order/status/query', $body); $resp = HttpClient::post("{$this->baseUrl}/api/order/status/query", $payload); if (isset($resp['code']) && $resp['code'] == 0 && isset($resp['result'])) { $result = $resp['result']; return [ 'code' => 0, 'platform' => 'dada', 'data' => $result, ]; } return [ 'code' => $resp['code'] ?? -1, 'platform' => 'dada', 'msg' => $resp['msg'] ?? 'Unknown error', 'data' => null ]; } }