462 行
18 KiB
PHP
462 行
18 KiB
PHP
<?php
|
||
namespace Common\Business;
|
||
use Think\Model;
|
||
use Exception;
|
||
|
||
/**
|
||
* 订单同步服务类
|
||
* 负责将小程序订单同步到骑手订单系统
|
||
*/
|
||
class OrderSyncService {
|
||
|
||
/**
|
||
* 同步单个订单到骑手系统
|
||
* @param int $billId 订单ID
|
||
* @return array 同步结果
|
||
*/
|
||
public function syncSingleOrder($billId) {
|
||
try {
|
||
// 获取订单信息
|
||
$orderInfo = $this->getOrderInfo($billId);
|
||
if (!$orderInfo) {
|
||
return array('success' => false, 'message' => '订单不存在');
|
||
}
|
||
|
||
// 检查是否需要配送
|
||
if (!$this->needDelivery($orderInfo)) {
|
||
return array('success' => false, 'message' => '订单不需要配送');
|
||
}
|
||
|
||
// 检查是否已同步
|
||
if ($this->isOrderSynced($billId)) {
|
||
return array('success' => false, 'message' => '订单已同步,无需重复同步');
|
||
}
|
||
|
||
// 创建骑手订单
|
||
$riderOrderId = $this->createRiderOrder($orderInfo);
|
||
if (!$riderOrderId) {
|
||
return array('success' => false, 'message' => '创建骑手订单失败');
|
||
}
|
||
|
||
// 记录同步日志
|
||
$this->logSync($billId, $riderOrderId, 'manual', '手动同步');
|
||
|
||
return array(
|
||
'success' => true,
|
||
'message' => '同步成功',
|
||
'rider_order_id' => $riderOrderId
|
||
);
|
||
|
||
} catch (Exception $e) {
|
||
return array('success' => false, 'message' => '同步失败:' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 测试入口:使用外部传入的订单信息直接创建骑手订单
|
||
* 无需订单状态与权限校验,仅用于测试接口
|
||
* @param array $orderInfo
|
||
* @return int|false 骑手订单ID
|
||
*/
|
||
public function createRiderOrderFromPayload($orderInfo) {
|
||
if (!is_array($orderInfo)) {
|
||
return false;
|
||
}
|
||
// 基础字段校验
|
||
if (empty($orderInfo['merchant_id']) || empty($orderInfo['store_id'])) {
|
||
return false;
|
||
}
|
||
// 直接调用内部创建逻辑
|
||
return $this->createRiderOrder($orderInfo);
|
||
}
|
||
|
||
/**
|
||
* 批量同步订单
|
||
* @param array $conditions 同步条件
|
||
* @return array 同步结果
|
||
*/
|
||
public function batchSyncOrders($conditions = array()) {
|
||
try {
|
||
$where = array();
|
||
$where['b.pay_state'] = 1; // 已支付
|
||
$where['o.pickup_type'] = 2; // 需要配送
|
||
$where['o.status'] = 1; // 只同步待发货订单
|
||
|
||
// 排除退款订单
|
||
$where['b.order_type'] = 1; // 只同步购买订单,排除退款订单(2)
|
||
$where['b.refund_status'] = 0; // 只同步正常订单,排除申请退款/退款成功的订单
|
||
|
||
// 添加时间条件
|
||
if (!empty($conditions['start_date'])) {
|
||
$where['b.add_time'] = array('egt', $conditions['start_date']);
|
||
}
|
||
if (!empty($conditions['end_date'])) {
|
||
$where['b.add_time'] = array('elt', $conditions['end_date']);
|
||
}
|
||
|
||
// 添加门店条件
|
||
if (!empty($conditions['store_id'])) {
|
||
$where['b.store_id'] = $conditions['store_id'];
|
||
}
|
||
|
||
// 获取需要同步的订单
|
||
$orders = M('retail_bill_order')->alias('b')
|
||
->join('ls_retail_bill_online o ON b.id = o.bill_id')
|
||
->where($where)
|
||
->field('b.*, b.order_type, b.refund_status, o.user_name, o.user_phone, o.user_address, o.delivery_fee')
|
||
->select();
|
||
|
||
$successCount = 0;
|
||
$failCount = 0;
|
||
$skipCount = 0;
|
||
$errors = array();
|
||
|
||
foreach ($orders as $order) {
|
||
// 检查是否已同步
|
||
if ($this->isOrderSynced($order['id'])) {
|
||
$skipCount++;
|
||
continue;
|
||
}
|
||
|
||
$result = $this->syncSingleOrder($order['id']);
|
||
if ($result['success']) {
|
||
$successCount++;
|
||
} else {
|
||
$failCount++;
|
||
$errors[] = "订单{$order['order_no']}:{$result['message']}";
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'success' => true,
|
||
'message' => "同步完成:成功{$successCount}个,失败{$failCount}个,跳过{$skipCount}个",
|
||
'details' => array(
|
||
'success_count' => $successCount,
|
||
'fail_count' => $failCount,
|
||
'skip_count' => $skipCount,
|
||
'errors' => $errors
|
||
)
|
||
);
|
||
|
||
} catch (Exception $e) {
|
||
return array('success' => false, 'message' => '批量同步失败:' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取订单信息
|
||
* @param int $billId 订单ID
|
||
* @return array|false 订单信息
|
||
*/
|
||
private function getOrderInfo($billId) {
|
||
return M('retail_bill_order')->alias('b')
|
||
->join('ls_retail_bill_online o ON b.id = o.bill_id')
|
||
->join('ls_retail_store s ON b.store_id = s.id', 'LEFT')
|
||
->where(array('b.id' => $billId))
|
||
->field('b.*, b.order_type, b.refund_status, o.user_name, o.user_phone, o.user_address, o.delivery_fee, o.pickup_type, o.status as online_status, s.storetag as store_name, s.address as store_address, s.phone as store_phone, s.longitude as store_lng, s.latitude as store_lat')
|
||
->find();
|
||
}
|
||
|
||
/**
|
||
* 检查订单是否需要配送
|
||
* @param array $orderInfo 订单信息
|
||
* @return bool
|
||
*/
|
||
private function needDelivery($orderInfo) {
|
||
// 检查:需要配送、已支付、非退款订单
|
||
$isDeliveryType = $orderInfo['pickup_type'] == 2;
|
||
$isPaid = $orderInfo['pay_state'] == 1;
|
||
$isPurchaseOrder = isset($orderInfo['order_type']) && $orderInfo['order_type'] == 1; // 购买订单
|
||
$isNotRefunded = !isset($orderInfo['refund_status']) || $orderInfo['refund_status'] == 0; // 未退款
|
||
|
||
return $isDeliveryType && $isPaid && $isPurchaseOrder && $isNotRefunded;
|
||
}
|
||
|
||
/**
|
||
* 检查订单是否已同步
|
||
* @param int $billId 订单ID
|
||
* @return bool
|
||
*/
|
||
private function isOrderSynced($billId) {
|
||
$count = M('rider_order')->where(array('bill_id' => $billId))->count();
|
||
return $count > 0;
|
||
}
|
||
|
||
/**
|
||
* 创建骑手订单
|
||
* @param array $orderInfo 订单信息
|
||
* @return int|false 骑手订单ID
|
||
*/
|
||
private function createRiderOrder($orderInfo) {
|
||
// 调试日志:初始化
|
||
$debug = array(
|
||
'bill_id' => isset($orderInfo['id']) ? $orderInfo['id'] : 0,
|
||
'merchant_id' => isset($orderInfo['merchant_id']) ? $orderInfo['merchant_id'] : 0,
|
||
'store_id' => isset($orderInfo['store_id']) ? $orderInfo['store_id'] : 0,
|
||
'user_address' => isset($orderInfo['user_address']) ? $orderInfo['user_address'] : ''
|
||
);
|
||
// 解析客户地址经纬度
|
||
$customerLat = 0;
|
||
$customerLng = 0;
|
||
if (!empty($orderInfo['user_address'])) {
|
||
$coords = \getAddress($orderInfo['user_address'], 1); // 使用系统地图设置进行解析,返回高德坐标
|
||
if (is_array($coords) && isset($coords['lat']) && isset($coords['lng'])) {
|
||
$customerLat = floatval($coords['lat']);
|
||
$customerLng = floatval($coords['lng']);
|
||
}
|
||
}
|
||
// 调试日志:地址解析结果
|
||
$debug['customer_coords'] = array('lat' => $customerLat, 'lng' => $customerLng);
|
||
|
||
// 门店经纬度
|
||
$storeLat = isset($orderInfo['store_lat']) ? floatval($orderInfo['store_lat']) : 0;
|
||
$storeLng = isset($orderInfo['store_lng']) ? floatval($orderInfo['store_lng']) : 0;
|
||
$debug['store_coords'] = array('lat' => $storeLat, 'lng' => $storeLng);
|
||
|
||
// 计算门店到客户的距离
|
||
$distance = 0;
|
||
$deliveryFee = $orderInfo['delivery_fee'] ? floatval($orderInfo['delivery_fee']) : 0.0;
|
||
$feeService = null;
|
||
if ($storeLat && $storeLng && $customerLat && $customerLng) {
|
||
$feeService = new \Retail\Service\DeliveryFeeService($orderInfo['merchant_id']);
|
||
// 使用地图API骑行路线距离,失败自动回退到直线距离
|
||
// 读取地图类型,便于诊断
|
||
$mapConf = M('sms_config')->find();
|
||
$mapTypeCode = isset($mapConf['server_map_type']) ? (string)$mapConf['server_map_type'] : '';
|
||
$mapType = ($mapTypeCode === '1') ? 'baidu' : (($mapTypeCode === '2') ? 'amap' : (($mapTypeCode === '3') ? 'tencent' : 'unknown'));
|
||
$debug['map_type'] = $mapType;
|
||
|
||
$distance = $feeService->calculateRouteDistance($storeLat, $storeLng, $customerLat, $customerLng, 'bicycling');
|
||
$debug['distance_km'] = $distance;
|
||
// 根据骑手端配送费配置计算配送费
|
||
$orderAmount = isset($orderInfo['all_money']) ? floatval($orderInfo['all_money']) : 0.0;
|
||
$feeResult = $feeService->calculateDeliveryFee(array(
|
||
'distance' => $distance,
|
||
'order_amount' => $orderAmount,
|
||
));
|
||
if (is_array($feeResult) && isset($feeResult['success']) && $feeResult['success']) {
|
||
$deliveryFee = floatval($feeResult['delivery_fee']);
|
||
}
|
||
// 记录计费详情(包含失败原因)
|
||
$debug['order_amount'] = $orderAmount;
|
||
$debug['fee_result'] = is_array($feeResult) ? $feeResult : array('raw' => $feeResult);
|
||
$debug['final_delivery_fee'] = $deliveryFee;
|
||
} else {
|
||
// 坐标缺失,记录原因
|
||
$debug['coords_missing'] = array(
|
||
'store_lat' => $storeLat,
|
||
'store_lng' => $storeLng,
|
||
'customer_lat' => $customerLat,
|
||
'customer_lng' => $customerLng
|
||
);
|
||
|
||
}
|
||
|
||
// 组装写入数据(字段映射与 ls_rider_order 保持一致)
|
||
$riderOrderData = array(
|
||
'order_no' => $this->generateRiderOrderNumber(),
|
||
'bill_id' => $orderInfo['id'],
|
||
'merchant_id' => $orderInfo['merchant_id'],
|
||
'store_id' => $orderInfo['store_id'],
|
||
'rider_id' => 0,
|
||
'order_type' => 1, // 即时单
|
||
'delivery_fee' => $deliveryFee,
|
||
'distance' => $distance,
|
||
'estimated_time' => 30, // 预计30分钟
|
||
|
||
// 商家信息
|
||
'store_name' => !empty($orderInfo['store_name']) ? $orderInfo['store_name'] : '',
|
||
'store_address' => !empty($orderInfo['store_address']) ? $orderInfo['store_address'] : '',
|
||
'store_phone' => !empty($orderInfo['store_phone']) ? $orderInfo['store_phone'] : '',
|
||
'store_lat' => $storeLat ? $storeLat : null,
|
||
'store_lng' => $storeLng ? $storeLng : null,
|
||
|
||
// 客户信息
|
||
'customer_name' => !empty($orderInfo['user_name']) ? $orderInfo['user_name'] : '',
|
||
'customer_phone' => !empty($orderInfo['user_phone']) ? $orderInfo['user_phone'] : '',
|
||
'customer_address' => !empty($orderInfo['user_address']) ? $orderInfo['user_address'] : '',
|
||
'customer_lat' => $customerLat ? $customerLat : null,
|
||
'customer_lng' => $customerLng ? $customerLng : null,
|
||
|
||
// 订单状态
|
||
'order_status' => 0, // 待接单
|
||
'add_time' => date('Y-m-d H:i:s'),
|
||
'update_time' => date('Y-m-d H:i:s'),
|
||
);
|
||
|
||
$riderOrderId = M('rider_order')->add($riderOrderData);
|
||
return $riderOrderId;
|
||
}
|
||
|
||
/**
|
||
* 生成骑手订单号
|
||
* @return string
|
||
*/
|
||
private function generateRiderOrderNumber() {
|
||
return 'RO' . date('YmdHis') . rand(1000, 9999);
|
||
}
|
||
|
||
/**
|
||
* 记录同步日志
|
||
* @param int $billId 原订单ID
|
||
* @param int $riderOrderId 骑手订单ID
|
||
* @param string $syncType 同步类型
|
||
* @param string $remark 备注
|
||
*/
|
||
private function logSync(
|
||
$billId,
|
||
$riderOrderId,
|
||
$operationType,
|
||
$remark = '',
|
||
$syncResult = 1,
|
||
$errorMsg = '',
|
||
$requestData = '',
|
||
$responseData = '',
|
||
$oldStatus = null,
|
||
$newStatus = null,
|
||
$riderId = null,
|
||
$retryCount = 0
|
||
) {
|
||
// 根据 ls_rider_order_sync_log 表结构规范化日志字段
|
||
$now = date('Y-m-d H:i:s');
|
||
// 规范化 operation_type,确保不超过 varchar(20)
|
||
$op = (string)$operationType;
|
||
$opMap = array(
|
||
'create_rider_order_geocode' => 'geocode',
|
||
'create_rider_order_distance' => 'distance',
|
||
'create_rider_order_fee_calc' => 'fee_calc',
|
||
'create_rider_order_coords_missing' => 'coords_miss',
|
||
'create_rider_order_saved' => 'saved',
|
||
'manual' => 'manual',
|
||
'auto' => 'auto',
|
||
);
|
||
$opNorm = isset($opMap[$op]) ? $opMap[$op] : $op;
|
||
if (strlen($opNorm) > 20) {
|
||
$opNorm = substr($opNorm, 0, 20);
|
||
}
|
||
if ($opNorm === '') {
|
||
$opNorm = 'unknown';
|
||
}
|
||
$logData = array(
|
||
'bill_id' => intval($billId),
|
||
'rider_order_id' => $riderOrderId ? intval($riderOrderId) : null,
|
||
'rider_id' => $riderId ? intval($riderId) : null,
|
||
'operation_type' => $opNorm,
|
||
'old_status' => is_null($oldStatus) ? null : intval($oldStatus),
|
||
'new_status' => is_null($newStatus) ? null : intval($newStatus),
|
||
'sync_result' => $syncResult ? 1 : 0,
|
||
'error_msg' => $errorMsg !== '' ? (string)$errorMsg : null,
|
||
// 兼容旧调用:将 remark 优先记录到 request_data,若提供了 requestData 则覆盖
|
||
'request_data' => $requestData !== '' ? (string)$requestData : ($remark !== '' ? (string)$remark : null),
|
||
'response_data' => $responseData !== '' ? (string)$responseData : null,
|
||
'sync_time' => $now,
|
||
'retry_count' => intval($retryCount),
|
||
'add_time' => $now,
|
||
'update_time' => $now,
|
||
);
|
||
|
||
M('rider_order_sync_log')->add($logData);
|
||
}
|
||
|
||
/**
|
||
* 获取同步统计信息
|
||
* @param array $conditions 查询条件
|
||
* @return array
|
||
*/
|
||
public function getSyncStatistics($conditions = array()) {
|
||
$where = array();
|
||
|
||
// 时间条件
|
||
if (!empty($conditions['start_date'])) {
|
||
$where['sync_time'] = array('egt', $conditions['start_date']);
|
||
}
|
||
if (!empty($conditions['end_date'])) {
|
||
$where['sync_time'] = array('elt', $conditions['end_date']);
|
||
}
|
||
|
||
$syncLogModel = M('rider_order_sync_log');
|
||
|
||
// 总同步数
|
||
$totalSync = $syncLogModel->where($where)->count();
|
||
|
||
// 成功同步数
|
||
$successSync = $syncLogModel->where(array_merge($where, array('sync_status' => 'success')))->count();
|
||
|
||
// 今日同步数
|
||
$todaySync = $syncLogModel->where(array_merge($where, array(
|
||
'sync_time' => array('like', date('Y-m-d') . '%')
|
||
)))->count();
|
||
|
||
// 手动同步数
|
||
$manualSync = $syncLogModel->where(array_merge($where, array('sync_type' => 'manual')))->count();
|
||
|
||
// 自动同步数
|
||
$autoSync = $syncLogModel->where(array_merge($where, array('sync_type' => 'auto')))->count();
|
||
|
||
return array(
|
||
'total_sync' => $totalSync,
|
||
'success_sync' => $successSync,
|
||
'today_sync' => $todaySync,
|
||
'manual_sync' => $manualSync,
|
||
'auto_sync' => $autoSync,
|
||
'success_rate' => $totalSync > 0 ? round($successSync / $totalSync * 100, 2) : 0
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 获取可同步的订单列表
|
||
* @param array $conditions 查询条件
|
||
* @return array
|
||
*/
|
||
public function getSyncableOrders($conditions = array()) {
|
||
$where = array();
|
||
$where['b.pay_state'] = 1; // 已支付
|
||
$where['o.pickup_type'] = 2; // 需要配送
|
||
$where['o.status'] = array('in', array('1', '2')); // 未发货或已发货
|
||
|
||
// 排除退款订单
|
||
$where['b.order_type'] = 1; // 只同步购买订单,排除退款订单(2)
|
||
$where['b.refund_status'] = 0; // 只同步正常订单,排除申请退款/退款成功的订单
|
||
|
||
// 排除已同步的订单
|
||
$syncedBillIds = M('rider_order')->getField('bill_id', true);
|
||
if (!empty($syncedBillIds)) {
|
||
$where['b.id'] = array('not in', $syncedBillIds);
|
||
}
|
||
|
||
// 添加查询条件
|
||
if (!empty($conditions['start_date'])) {
|
||
$where['b.add_time'] = array('egt', $conditions['start_date']);
|
||
}
|
||
if (!empty($conditions['end_date'])) {
|
||
$where['b.add_time'] = array('elt', $conditions['end_date']);
|
||
}
|
||
if (!empty($conditions['store_id'])) {
|
||
$where['b.store_id'] = $conditions['store_id'];
|
||
}
|
||
|
||
$page = isset($conditions['page']) ? $conditions['page'] : 1;
|
||
$pageSize = isset($conditions['pageSize']) ? $conditions['pageSize'] : 20;
|
||
|
||
$orders = M('retail_bill_order')->alias('b')
|
||
->join('ls_retail_bill_online o ON b.id = o.bill_id')
|
||
->join('ls_retail_store s ON b.store_id = s.id', 'LEFT')
|
||
->where($where)
|
||
->field('b.id, b.order_no, b.all_money, b.add_time, b.store_id, b.order_type, b.refund_status, o.user_name, o.user_phone, o.user_address, o.delivery_fee, s.storetag as store_name')
|
||
->order('b.add_time DESC')
|
||
->page($page, $pageSize)
|
||
->select();
|
||
|
||
$total = M('retail_bill_order')->alias('b')
|
||
->join('ls_retail_bill_online o ON b.id = o.bill_id')
|
||
->where($where)
|
||
->count();
|
||
|
||
return array(
|
||
'list' => $orders,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'pageSize' => $pageSize
|
||
);
|
||
}
|
||
} |