文件
mashangyoujiu/Application/Common/Business/OrderSyncService.class.php

462 行
18 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?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
);
}
}