mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 13:31:04 +00:00
refactor(订单): 移除拆单发货与拆单退款残留逻辑
- 发货服务删除 splitDelivery/getCartList/tidyCartList,通用发货任务改走 delivery() - 删除子单收货联动 checkMaster、拆单开票 splitOrderInvoice 及 pid 分支 - 普通订单模块与开放接口移除拆单控制器方法、路由与拆单文案 - 订单详情 related_orders 恒为 null,文档同步为不支持拆单
This commit is contained in:
parent
d8e2a51350
commit
22362638ef
@ -319,9 +319,8 @@ GET /api/center/order/detail/{orderNo}
|
||||
{"type": "warning", "text": "签收后请在7天内确认收货"}
|
||||
],
|
||||
|
||||
// 11. related_orders — 关联订单(拆单/换货时存在)
|
||||
// 11. related_orders — 关联订单(换货等衍生单时存在;系统不支持拆单,无子订单)
|
||||
"related_orders": [ // ← 中台查关联订单
|
||||
{"order_no": "cp123456790", "relation": "split", "title": "子订单1", "status_text": "已发货"},
|
||||
{"order_no": "cp123456791", "relation": "exchange", "title": "换货订单", "status_text": "进行中"}
|
||||
],
|
||||
|
||||
@ -593,7 +592,7 @@ eb_order_center 实体列 > extra_info JSON > 扩展订单表回退 > Handler
|
||||
| countdown | `status_code=pending_payment` | 中台 |
|
||||
| verification | `shipping_type=2` 或需核销 | 中台 |
|
||||
| notice | 按状态规则 | 中台 |
|
||||
| related_orders | 有拆单/换货 | 中台 |
|
||||
| related_orders | 有换货等衍生单 | 中台 |
|
||||
| extensions.fulfillment | 各产品类型注入 | 产品类型监听器 |
|
||||
| extensions.activity | 交易模式注入 | 交易模式监听器 |
|
||||
| extensions.after_sale | 有售后记录 | 售后模块监听器 |
|
||||
|
||||
@ -40,12 +40,6 @@ Route::group('order', function () {
|
||||
// Route::put('delivery/:id', [\app\adminapi\controller\order\OrderCenterController::class, 'updateDelivery']);
|
||||
//获取商家寄件金额 → modules/product/normal/OrderController::getSheetInfo
|
||||
// Route::post('price', [\app\adminapi\controller\order\OrderCenterController::class, 'getPrice']);
|
||||
//获取订单可拆分商品列表 → modules/product/normal/OrderController::getSplitCartInfo
|
||||
// Route::get('split_cart_info/:id', [\app\adminapi\controller\order\OrderCenterController::class, 'splitCartInfo']);
|
||||
//拆单发送货 → modules/product/normal/OrderController::splitDelivery
|
||||
// Route::put('split_delivery/:id', [\app\adminapi\controller\order\OrderCenterController::class, 'splitDelivery']);
|
||||
//获取订单拆分子订单列表 → modules/product/normal/OrderController::getSplitOrderList(TODO)
|
||||
// Route::get('split_order/:id', [\app\adminapi\controller\order\OrderCenterController::class, 'splitOrder']);
|
||||
//订单退款表单 → modules/product/normal/OrderController::getRefundForm
|
||||
// Route::get('refund/:id', [\app\adminapi\controller\order\OrderCenterController::class, 'refund']);
|
||||
//订单退款 → modules/product/normal/OrderController::processRefund
|
||||
|
||||
@ -39,13 +39,13 @@ class OrderExpressJob extends BaseJobs
|
||||
'sh_delivery_id' => '',
|
||||
'sh_delivery_uid' => '',
|
||||
'fictitious_content' => '',
|
||||
'cart_ids' => [],
|
||||
'day_type' => 0,
|
||||
'pickup_time' => [],
|
||||
'service_type' => '',
|
||||
];
|
||||
try {
|
||||
app()->make(OrderDeliveryServices::class)->splitDelivery($id, $data, false);
|
||||
// 整单发货(系统不支持拆单发货,统一走通用发货入口)
|
||||
app()->make(OrderDeliveryServices::class)->delivery($id, $data);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('订单ID' . $id . '发货失败,失败原因:' . $e->getMessage());
|
||||
}
|
||||
|
||||
@ -624,10 +624,6 @@ return [
|
||||
'请选择奖品类型' => 'Please select the type of prize',
|
||||
'请填写奖品赠送{:type}数' => 'Please fill in the {:type} number of prizes given',
|
||||
'请选择优惠券' => 'Please select a coupon',
|
||||
'生成新订单失败' => 'Failed to generate new order',
|
||||
'修改原来订单商品拆分状态失败' => 'Failed to modify the original order item split status',
|
||||
'新增拆分订单商品信息失败' => 'Failed to add product information for split order',
|
||||
'保存新订单商品信息失败' => 'Failed to save new order product information',
|
||||
'协议类型缺失' => 'Protocol type missing',
|
||||
'请填写协议名称' => 'Please fill in the agreement name',
|
||||
'请填写协议内容' => 'Please fill in the content of the agreement',
|
||||
@ -1098,7 +1094,6 @@ return [
|
||||
'正在申请退款中' => 'Applying for a refund',
|
||||
'申请失败' => 'Application failed',
|
||||
'退款件数大于订单件数' => 'The number of refunds is greater than the number of orders',
|
||||
'该订单已全部拆分' => 'The order has been split',
|
||||
'申请退款失败' => 'Failed to apply for refund',
|
||||
'存在待处理退款单' => 'There is a pending refund order',
|
||||
'该订单无法删除' => 'This order cannot be deleted',
|
||||
|
||||
@ -624,10 +624,6 @@ return [
|
||||
'请选择奖品类型' => '请选择奖品类型',
|
||||
'请填写奖品赠送{:type}数' => '请填写奖品赠送{:type}数',
|
||||
'请选择优惠券' => '请选择优惠券',
|
||||
'生成新订单失败' => '生成新订单失败',
|
||||
'修改原来订单商品拆分状态失败' => '修改原来订单商品拆分状态失败',
|
||||
'新增拆分订单商品信息失败' => '新增拆分订单商品信息失败',
|
||||
'保存新订单商品信息失败' => '保存新订单商品信息失败',
|
||||
'协议类型缺失' => '协议类型缺失',
|
||||
'请填写协议名称' => '请填写协议名称',
|
||||
'请填写协议内容' => '请填写协议内容',
|
||||
@ -1098,7 +1094,6 @@ return [
|
||||
'正在申请退款中' => '正在申请退款中',
|
||||
'申请失败' => '申请失败',
|
||||
'退款件数大于订单件数' => '退款件数大于订单件数',
|
||||
'该订单已全部拆分' => '该订单已全部拆分',
|
||||
'申请退款失败' => '申请退款失败',
|
||||
'存在待处理退款单' => '存在待处理退款单',
|
||||
'该订单无法删除' => '该订单无法删除',
|
||||
|
||||
@ -1000,7 +1000,7 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
|
||||
'order' => 100,
|
||||
];
|
||||
|
||||
// 105. 关联订单(拆单/换货)
|
||||
// 105. 关联订单(换货衍生单)
|
||||
$relatedOrders = $detail['related_orders'] ?? null;
|
||||
$sections[] = [
|
||||
'key' => 'related_orders',
|
||||
@ -1466,14 +1466,13 @@ class OrderCenterServices extends BaseService implements OrderCenterSyncInterfac
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询关联订单(拆单/换货)
|
||||
* 查询关联订单(换货等衍生单)
|
||||
* 系统不支持拆单发货/拆单退款,一个订单不会派生子订单,当前恒为 null
|
||||
* @param array $detail 订单详情
|
||||
* @return array|null 返回关联订单数组或null
|
||||
*/
|
||||
protected function buildRelatedOrders(array $detail): ?array
|
||||
{
|
||||
// 当前暂无拆单/换货功能,返回 null
|
||||
// 后续实现拆单时,按 order_no 前缀或 parent_order_no 查询关联订单
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -262,7 +262,7 @@ class OrderInvoiceServices extends BaseService
|
||||
if (!$orderCenterId || empty($invoice)) {
|
||||
return null;
|
||||
}
|
||||
// 同一订单已有申请则跳过,避免拆单/重试重复写入
|
||||
// 同一订单已有申请则跳过,避免重复写入
|
||||
if ($this->dao->getOne(['order_id' => $orderCenterId, 'is_del' => 0])) {
|
||||
return null;
|
||||
}
|
||||
@ -304,50 +304,6 @@ class OrderInvoiceServices extends BaseService
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆分订单同步拆分申请开票记录
|
||||
* @param int $oid
|
||||
* @return bool
|
||||
* @throws \think\db\exception\DataNotFoundException
|
||||
* @throws \think\db\exception\DbException
|
||||
* @throws \think\db\exception\ModelNotFoundException
|
||||
*/
|
||||
public function splitOrderInvoice(int $oid)
|
||||
{
|
||||
/** @var OrderCenterDao $orderCenterServices */
|
||||
$orderCenterServices = app()->make(OrderCenterDao::class);
|
||||
$orderInfo = $orderCenterServices->getOne(['id' => $oid, 'is_del' => 0]);
|
||||
if (!$orderInfo) {
|
||||
throw new AdminException('订单不存在');
|
||||
}
|
||||
$pid = $orderInfo['pid'] > 0 ? $orderInfo['pid'] : $orderInfo['id'];
|
||||
//查询开票记录
|
||||
$orderInvoice = $this->dao->get(['order_id' => $oid]);
|
||||
//查询子订单
|
||||
$spliteOrder = $orderCenterServices->getColumn(['pid' => $pid, 'is_system_del' => 0], 'id,order_id');
|
||||
if ($spliteOrder && $orderInvoice) {
|
||||
$data = $orderInvoice->toArray();
|
||||
unset($data['id']);
|
||||
$data['add_time'] = strtotime($data['add_time']);
|
||||
$data_all = [];
|
||||
foreach ($spliteOrder as $order) {
|
||||
if (!$this->dao->count(['order_id' => $order['id']])) {
|
||||
$data['order_id'] = $order['id'];
|
||||
$data_all[] = $data;
|
||||
}
|
||||
}
|
||||
if ($data_all) {
|
||||
$this->transaction(function () use ($data_all, $orderInvoice, $orderInfo) {
|
||||
$this->dao->saveAll($data_all);
|
||||
if ($orderInfo['pid'] <= 0) {
|
||||
$this->dao->update(['id' => $orderInvoice['id']], ['is_del' => 1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开具发票
|
||||
* @param $id
|
||||
|
||||
@ -30,8 +30,6 @@ use app\services\user\UserServices;
|
||||
use app\services\user\member\MemberCardServices;
|
||||
use app\services\order\RefundExecutorServices;
|
||||
use app\services\pay\PayServices;
|
||||
// @deprecated eb_store_order_status 表已删除
|
||||
use app\services\order\OrderTakeServices;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
@ -313,13 +311,6 @@ class OrderLifecycleServices extends BaseService
|
||||
'content' => $content,
|
||||
'add_time' => time(),
|
||||
]);
|
||||
|
||||
// 检查主订单是否需要修改状态
|
||||
if (($order['pid'] ?? 0) > 0) {
|
||||
/** @var OrderTakeServices $storeOrderTake */
|
||||
$storeOrderTake = app()->make(OrderTakeServices::class);
|
||||
$storeOrderTake->checkMaster($order['pid']);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error($e->getMessage());
|
||||
throw $e;
|
||||
|
||||
@ -42,9 +42,10 @@ class OrderOutServices extends BaseService
|
||||
|
||||
/**
|
||||
* 发货类型
|
||||
* 系统不支持拆单/分批发货,此处不声明拆分类型(旧拆分发货已随拆单功能下线)
|
||||
* @var string[]
|
||||
*/
|
||||
public $deliveryType = ['send' => '商家配送', 'express' => '快递配送', 'fictitious' => '虚拟发货', 'delivery_part_split' => '拆分部分发货', 'delivery_split' => '拆分发货完成'];
|
||||
public $deliveryType = ['send' => '商家配送', 'express' => '快递配送', 'fictitious' => '虚拟发货'];
|
||||
|
||||
/**
|
||||
* OrderOutServices constructor.
|
||||
@ -196,43 +197,6 @@ class OrderOutServices extends BaseService
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化订单商品
|
||||
* @param array $cartInfo
|
||||
* @param array $list
|
||||
* @return array
|
||||
*/
|
||||
public function tidyCartList(array $cartInfo, array $list, $cartId = 0): array
|
||||
{
|
||||
$list[] = [
|
||||
'cart_id' => $cartId,
|
||||
'store_name' => $cartInfo['productInfo']['store_name'] ?? '',
|
||||
'suk' => $cartInfo['productInfo']['attrInfo']['suk'] ?? '',
|
||||
'image' => $cartInfo['productInfo']['attrInfo']['image'] ?: $cartInfo['productInfo']['image'],
|
||||
'price' => sprintf("%.2f", $cartInfo['truePrice'] ?? '0.00'),
|
||||
'cart_num' => $cartInfo['cart_num'] ?? 0,
|
||||
'surplus_num' => $cartInfo['surplus_num'] ?? 0,
|
||||
'refund_num' => $cartInfo['refund_num'] ?? 0
|
||||
];
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单可以拆分商品信息
|
||||
* @param string $orderId 订单号
|
||||
* @return array
|
||||
*/
|
||||
public function getCartList(string $orderId): array
|
||||
{
|
||||
$order = $this->dao->get(['order_id' => $orderId]);
|
||||
if (!$order) {
|
||||
throw new ApiException('订单不存在');
|
||||
}
|
||||
|
||||
// 从订单中台 extra_info.items 构造可拆分商品列表(旧 cart_info 表已废弃)
|
||||
return $this->buildOrderItemsFromExtraInfo((string)$order['order_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单收货
|
||||
* @param string $orderId 订单号
|
||||
@ -291,27 +255,6 @@ class OrderOutServices extends BaseService
|
||||
return $deliveryServices->delivery((int)$orderInfo['id'], $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单拆单发送货
|
||||
* @param string $orderId 订单号
|
||||
* @param array $data
|
||||
* @return bool
|
||||
* @throws \think\db\exception\DataNotFoundException
|
||||
* @throws \think\db\exception\DbException
|
||||
* @throws \think\db\exception\ModelNotFoundException
|
||||
*/
|
||||
public function splitDelivery(string $orderId, array $data): bool
|
||||
{
|
||||
$orderInfo = $this->dao->get(['order_id' => $orderId]);
|
||||
if (!$orderInfo) {
|
||||
throw new ApiException('订单未能查到,不能发货');
|
||||
}
|
||||
|
||||
/** @var OrderDeliveryServices $deliveryServices */
|
||||
$deliveryServices = app()->make(OrderDeliveryServices::class);
|
||||
return $deliveryServices->splitDelivery((int)$orderInfo['id'], $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置发票
|
||||
* @param string $orderId 订单号
|
||||
@ -381,8 +324,8 @@ class OrderOutServices extends BaseService
|
||||
|
||||
/**
|
||||
* 从订单中台 extra_info.items 构造订单商品列表
|
||||
* 用于订单列表/详情/拆单等场景(旧订单购物车明细服务已删除)
|
||||
* 字段映射保持与 tidyCartList 输出一致,refund_num 默认 0(items 中不存储退款数)
|
||||
* 用于订单列表/详情等场景(旧订单购物车明细服务已删除)
|
||||
* 字段映射与后台发货列表一致,refund_num 默认 0(items 中不存储退款数)
|
||||
* @param string $orderNo 订单号
|
||||
* @return array 商品列表数组
|
||||
*/
|
||||
|
||||
@ -723,19 +723,4 @@ class OrderTakeServices extends BaseService
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查主订单是否需要修改状态
|
||||
* @param $pid
|
||||
* @throws \think\db\exception\DataNotFoundException
|
||||
* @throws \think\db\exception\DbException
|
||||
* @throws \think\db\exception\ModelNotFoundException
|
||||
*/
|
||||
public function checkMaster($pid)
|
||||
{
|
||||
$p_order = $this->dao->get((int)$pid, ['id,pid,status']);
|
||||
//主订单全部发货 且子订单没有待收货 有待评价
|
||||
if ($p_order['status'] == 1 && !$this->dao->count(['pid' => $pid, 'status' => 2]) && $this->dao->count(['pid' => $pid, 'status' => 3])) {
|
||||
$this->dao->update($p_order['id'], ['status' => 2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -667,34 +667,6 @@ class OrderController extends AuthController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可拆分发货商品列表
|
||||
*/
|
||||
public function getSplitCartInfo(OrderCenterServices $centerServices)
|
||||
{
|
||||
$orderNo = $this->request->param('order_no', '');
|
||||
if (empty($orderNo)) {
|
||||
return app('json')->fail('参数错误');
|
||||
}
|
||||
try {
|
||||
$centerOrder = $centerServices->getOne(['order_no' => $orderNo], 'extra_info');
|
||||
if (!$centerOrder) {
|
||||
return app('json')->fail('订单不存在');
|
||||
}
|
||||
$centerOrder = $centerOrder->toArray();
|
||||
|
||||
$extraInfo = $centerOrder['extra_info'] ?? [];
|
||||
if (is_string($extraInfo)) $extraInfo = json_decode($extraInfo, true) ?: [];
|
||||
// 商品快照:下单时写入 extra_info.items(新架构唯一商品快照来源)
|
||||
$items = $extraInfo['items'] ?? [];
|
||||
|
||||
return app('json')->success($items);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[NormalOrder] getSplitCartInfo failed: ' . $e->getMessage());
|
||||
return app('json')->fail('获取拆分商品列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发货
|
||||
* 上传 Excel 文件(列:订单号、快递公司、快递单号),逐条发货
|
||||
@ -1447,100 +1419,6 @@ class OrderController extends AuthController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆单发货(中台化)
|
||||
* 将订单中部分商品拆出为独立子订单并单独发货(OrderServices::splitOrderDelivery)
|
||||
* 基于 extra_info.items 快照整行拆分,金额按比例分摊
|
||||
*/
|
||||
public function splitDelivery(OrderCenterServices $centerServices, OrderServices $orderServices)
|
||||
{
|
||||
$orderNo = $this->request->param('order_no', '');
|
||||
$deliveryType = $this->request->param('delivery_type', 'express');
|
||||
$cartIds = $this->request->param('cart_ids', []);
|
||||
|
||||
if (empty($orderNo)) {
|
||||
return app('json')->fail('参数错误');
|
||||
}
|
||||
if (empty($cartIds) || !is_array($cartIds)) {
|
||||
return app('json')->fail('请选择要拆单发货的商品');
|
||||
}
|
||||
|
||||
// 校验 cart_ids 格式
|
||||
foreach ($cartIds as &$cart) {
|
||||
if (!isset($cart['cart_id']) || !$cart['cart_id'] || !isset($cart['cart_num']) || !$cart['cart_num']) {
|
||||
return app('json')->fail('请重新选择发货商品,或填写发货件数');
|
||||
}
|
||||
$cart['cart_id'] = (int)$cart['cart_id'];
|
||||
$cart['cart_num'] = (int)$cart['cart_num'];
|
||||
}
|
||||
|
||||
$centerOrder = $centerServices->getOne(['order_no' => $orderNo], 'id, uid, status_code');
|
||||
if (!$centerOrder) {
|
||||
return app('json')->fail('订单不存在');
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建发货数据
|
||||
$deliveryData = [
|
||||
'delivery_type' => $deliveryType,
|
||||
];
|
||||
|
||||
switch ($deliveryType) {
|
||||
case 'express':
|
||||
$deliveryData['delivery_name'] = $this->request->param('delivery_name', '');
|
||||
$deliveryData['delivery_id'] = $this->request->param('delivery_id', '');
|
||||
if (empty($deliveryData['delivery_name'])) {
|
||||
return app('json')->fail('请选择快递公司');
|
||||
}
|
||||
if (empty($deliveryData['delivery_id'])) {
|
||||
return app('json')->fail('请填写快递单号');
|
||||
}
|
||||
$deliveryName = $deliveryData['delivery_name'];
|
||||
break;
|
||||
case 'send':
|
||||
$deliveryData['delivery_name'] = $this->request->param('sh_delivery_name', '');
|
||||
$deliveryData['delivery_id'] = $this->request->param('sh_delivery_id', '');
|
||||
if (empty($deliveryData['delivery_name'])) {
|
||||
return app('json')->fail('请选择送货人');
|
||||
}
|
||||
if (empty($deliveryData['delivery_id'])) {
|
||||
return app('json')->fail('请输入送货人电话');
|
||||
}
|
||||
$deliveryName = $deliveryData['delivery_name'];
|
||||
break;
|
||||
case 'fictitious':
|
||||
$deliveryData['delivery_name'] = '虚拟发货';
|
||||
$deliveryData['delivery_id'] = $this->request->param('fictitious_content', '');
|
||||
$deliveryName = '虚拟发货';
|
||||
break;
|
||||
default:
|
||||
return app('json')->fail('不支持的发货类型');
|
||||
}
|
||||
|
||||
// 中台拆单发货:拆出子订单并立即发货(模块内自治实现)
|
||||
[$childOrderNo] = $orderServices->splitOrderDelivery($orderNo, $cartIds, $deliveryData);
|
||||
|
||||
// 写入订单记录
|
||||
/** @var OrderLogDao $logDao */
|
||||
$logDao = app()->make(OrderLogDao::class);
|
||||
$logDao->save([
|
||||
'order_no' => $orderNo,
|
||||
'uid' => (int)($centerOrder['uid'] ?? 0),
|
||||
'operator_id' => (int)($this->adminInfo['id'] ?? 0),
|
||||
'operator_type' => 'admin',
|
||||
'from_status' => $centerOrder['status_code'] ?? '',
|
||||
'to_status' => $centerOrder['status_code'] ?? '',
|
||||
'content' => '管理员拆单发货:' . $deliveryName . ',子订单:' . $childOrderNo,
|
||||
'add_time' => time(),
|
||||
]);
|
||||
|
||||
return app('json')->success(['child_order_no' => $childOrderNo], '拆单发货成功');
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[NormalOrder] splitDelivery failed: ' . $e->getMessage());
|
||||
return app('json')->fail('拆单发货失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 物流查询
|
||||
* 根据订单号查询物流轨迹
|
||||
|
||||
@ -56,8 +56,6 @@ Route::group('normal', function () {
|
||||
Route::get('order/shipment/delivery_persons', [OrderController::class, 'getDeliveryPersons'])->option(['real_name' => '配送员列表']);
|
||||
Route::get('order/shipment/express_temp', [OrderController::class, 'getExpressTemp'])->option(['real_name' => '电子面单模板']);
|
||||
Route::get('order/shipment/sheet_info', [OrderController::class, 'getSheetInfo'])->option(['real_name' => '发件人配置信息']);
|
||||
Route::get('order/shipment/split_cart_info/:order_no', [OrderController::class, 'getSplitCartInfo'])->option(['real_name' => '可拆单商品列表']);
|
||||
Route::post('order/split_delivery', [OrderController::class, 'splitDelivery'])->option(['real_name' => '拆单发货']);
|
||||
// 修改配送信息
|
||||
Route::put('order/update_distribution', [OrderController::class, 'updateDistribution'])->option(['real_name' => '修改配送信息']);
|
||||
// 备注|删除|改价
|
||||
|
||||
@ -208,219 +208,6 @@ class OrderServices extends BaseService
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆单发货(中台化,旧拆单服务已随 eb_store_order_cart_info 表删除)
|
||||
* 将订单中部分商品拆出为独立子订单并立即发货,原订单保留剩余商品待后续发货
|
||||
* 仅支持整行拆分(不支持同商品部分数量拆分,部分数量场景走售后引擎)
|
||||
* @param string $orderNo 原订单号
|
||||
* @param array $cartIds 拆分商品行 [{cart_id, cart_num}],cart_num 须等于快照行数量
|
||||
* @param array $deliveryData 发货数据 {delivery_type, delivery_name, delivery_id}
|
||||
* @return array [子订单号, 原订单号]
|
||||
*/
|
||||
public function splitOrderDelivery(string $orderNo, array $cartIds, array $deliveryData): array
|
||||
{
|
||||
// 1. 读取中台订单与商品快照
|
||||
$centerOrder = $this->centerServices->getOne(['order_no' => $orderNo]);
|
||||
if (!$centerOrder) {
|
||||
throw new \RuntimeException('订单不存在');
|
||||
}
|
||||
$centerOrder = $centerOrder->toArray();
|
||||
// 仅已支付、未退款/未取消的订单可拆单发货
|
||||
if ((int)($centerOrder['pay_status'] ?? 0) !== 1) {
|
||||
throw new \RuntimeException('订单未支付,不能拆单发货');
|
||||
}
|
||||
if (in_array($centerOrder['status_code'] ?? '', ['refunded', 'canceled', 'after_sale'], true)) {
|
||||
throw new \RuntimeException('订单当前状态不支持拆单发货');
|
||||
}
|
||||
$extraInfo = $centerOrder['extra_info'] ?? [];
|
||||
if (is_string($extraInfo)) {
|
||||
$extraInfo = json_decode($extraInfo, true) ?: [];
|
||||
}
|
||||
$items = $extraInfo['items'] ?? [];
|
||||
if (count($items) < 2) {
|
||||
throw new \RuntimeException('订单商品不足两行,无需拆单');
|
||||
}
|
||||
|
||||
// 2. 整行拆分校验:按 cart_id 匹配快照行,数量必须整行
|
||||
$selectMap = [];
|
||||
foreach ($cartIds as $cart) {
|
||||
$cartId = (int)($cart['cart_id'] ?? 0);
|
||||
$cartNum = (int)($cart['cart_num'] ?? 0);
|
||||
if ($cartId <= 0 || $cartNum <= 0) {
|
||||
throw new \RuntimeException('拆分商品参数错误');
|
||||
}
|
||||
$matched = null;
|
||||
foreach ($items as $item) {
|
||||
if ((int)($item['cart_id'] ?? 0) === $cartId) {
|
||||
$matched = $item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($matched === null) {
|
||||
throw new \RuntimeException('拆分商品不在订单内');
|
||||
}
|
||||
if ($cartNum !== (int)($matched['cart_num'] ?? 0)) {
|
||||
throw new \RuntimeException('仅支持整行拆分,拆分数量须等于商品数量');
|
||||
}
|
||||
$selectMap[$cartId] = true;
|
||||
}
|
||||
|
||||
// 3. 快照分组:拆分行 / 剩余行
|
||||
$selected = $remain = [];
|
||||
foreach ($items as $item) {
|
||||
if (isset($selectMap[(int)($item['cart_id'] ?? 0)])) {
|
||||
$selected[] = $item;
|
||||
} else {
|
||||
$remain[] = $item;
|
||||
}
|
||||
}
|
||||
if (!$selected || !$remain) {
|
||||
throw new \RuntimeException('拆分后原订单与子订单商品均不能为空');
|
||||
}
|
||||
|
||||
// 4. 金额分摊:按商品金额占比拆分,尾差归原订单(保证两单之和等于原单)
|
||||
$selGoods = $remGoods = '0';
|
||||
$selPostage = $remPostage = '0';
|
||||
foreach ($selected as $item) {
|
||||
$selGoods = bcadd($selGoods, bcmul((string)($item['true_price'] ?? 0), (string)($item['cart_num'] ?? 0), 2), 2);
|
||||
$selPostage = bcadd($selPostage, (string)($item['postage_price'] ?? 0), 2);
|
||||
}
|
||||
foreach ($remain as $item) {
|
||||
$remGoods = bcadd($remGoods, bcmul((string)($item['true_price'] ?? 0), (string)($item['cart_num'] ?? 0), 2), 2);
|
||||
$remPostage = bcadd($remPostage, (string)($item['postage_price'] ?? 0), 2);
|
||||
}
|
||||
$goodsTotal = bcadd($selGoods, $remGoods, 2);
|
||||
if (bccomp($goodsTotal, '0', 2) <= 0) {
|
||||
throw new \RuntimeException('订单金额异常,不能拆单');
|
||||
}
|
||||
// 比例分摊的金额列:商品总价/实付/优惠/积分抵扣/会员折/运费(运费按行归集已算出,其余按比例)
|
||||
$ratioFields = ['total_price', 'pay_price', 'coupon_price', 'deduction_price', 'member_discount', 'vip_discount', 'use_integral_num'];
|
||||
$childAmounts = [];
|
||||
foreach ($ratioFields as $field) {
|
||||
$original = (string)($centerOrder[$field] ?? 0);
|
||||
$isInt = $field === 'use_integral_num';
|
||||
$scale = $isInt ? 0 : 2;
|
||||
$childVal = $isInt
|
||||
? bcmul($original, bcdiv($selGoods, $goodsTotal, 4), 0)
|
||||
: bcdiv(bcmul($original, $selGoods, 4), $goodsTotal, 2);
|
||||
$childAmounts[$field] = $childVal;
|
||||
// 原单金额 = 原值 - 子单值(尾差归原单)
|
||||
$centerOrder[$field] = $isInt
|
||||
? bcsub($original, $childVal, 0)
|
||||
: bcsub($original, $childVal, 2);
|
||||
}
|
||||
// 运费按 items 行归集
|
||||
$childAmounts['pay_postage'] = $selPostage;
|
||||
$centerOrder['pay_postage'] = $remPostage;
|
||||
// 件数按行数量
|
||||
$selNum = array_sum(array_column($selected, 'cart_num'));
|
||||
$remNum = array_sum(array_column($remain, 'cart_num'));
|
||||
|
||||
// 5. 事务:子订单创建 + 原订单扣减 + 扩展表同步 + 子订单发货
|
||||
$childNo = $this->centerServices->getNewOrderId('cp');
|
||||
$now = time();
|
||||
$this->transaction(function () use ($orderNo, $childNo, $centerOrder, $extraInfo, $selected, $remain, $childAmounts, $selNum, $remNum, $now) {
|
||||
/** @var \app\dao\order\OrderCenterDao $centerDao */
|
||||
$centerDao = app(\app\dao\order\OrderCenterDao::class);
|
||||
// 5.1 扩展表:复制原扩展行为子订单(发货字段清空,状态待发货)
|
||||
$extRow = $this->dao->getOne(['order_no' => $orderNo]);
|
||||
$extData = $extRow ? $extRow->toArray() : [];
|
||||
unset($extData['id']);
|
||||
$extData['order_no'] = $childNo;
|
||||
$extData['product_id'] = (int)($selected[0]['product_id'] ?? 0);
|
||||
$extData['sku_id'] = $selected[0]['attr_info']['unique'] ?? ($extData['sku_id'] ?? '');
|
||||
$extData['pay_price'] = $childAmounts['pay_price'];
|
||||
$extData['total_price'] = $childAmounts['total_price'];
|
||||
$extData['status'] = NormalOrderAdapter::EXT_STATUS_PENDING_SHIPPED;
|
||||
$extData['delivery_name'] = '';
|
||||
$extData['delivery_id'] = '';
|
||||
$extData['delivery_type'] = '';
|
||||
$extData['add_time'] = $now;
|
||||
$extData['update_time'] = $now;
|
||||
$childExt = $this->dao->save($extData);
|
||||
if (!$childExt) {
|
||||
throw new \RuntimeException('生成子订单扩展记录失败');
|
||||
}
|
||||
|
||||
// 5.2 中台表:复制原行为子订单(items=拆分行,金额分摊,发货状态清零)
|
||||
$childCenter = $centerOrder;
|
||||
unset($childCenter['id']);
|
||||
$childCenter['order_no'] = $childNo;
|
||||
$childCenter['source_id'] = (int)$childExt->id;
|
||||
foreach ($childAmounts as $field => $val) {
|
||||
$childCenter[$field] = $val;
|
||||
}
|
||||
$childCenter['total_num'] = $selNum;
|
||||
$childCenter['title'] = $selected[0]['store_name'] ?? '';
|
||||
$childCenter['image'] = $selected[0]['image'] ?? '';
|
||||
$childCenter['refund_status'] = 0;
|
||||
$childCenter['refund_price'] = '0.00';
|
||||
$childCenter['delivery_time'] = 0;
|
||||
$childCenter['take_time'] = 0;
|
||||
$childCenter['refund_time'] = 0;
|
||||
$childCenter['add_time'] = $now;
|
||||
// 子单 extra_info:items=拆分行 + 记录拆单来源
|
||||
$childExtra = $extraInfo;
|
||||
$childExtra['items'] = $selected;
|
||||
$childExtra['sku_info'] = $selected[0]['attr_info'] ?? ($childExtra['sku_info'] ?? []);
|
||||
$childExtra['split_parent'] = $orderNo;
|
||||
$childCenter['extra_info'] = json_encode($childExtra, JSON_UNESCAPED_UNICODE);
|
||||
if (!$centerDao->save($childCenter)) {
|
||||
throw new \RuntimeException('生成子订单失败');
|
||||
}
|
||||
|
||||
// 5.3 中台表:原订单扣减(items=剩余行 + 金额 + 标题)
|
||||
$parentExtra = $extraInfo;
|
||||
$parentExtra['items'] = $remain;
|
||||
$parentExtra['sku_info'] = $remain[0]['attr_info'] ?? ($parentExtra['sku_info'] ?? []);
|
||||
// 拆单血缘记录(追加,不覆盖既有子单)
|
||||
$children = $parentExtra['split_children'] ?? [];
|
||||
$children[] = $childNo;
|
||||
$parentExtra['split_children'] = $children;
|
||||
$centerDao->update(['order_no' => $orderNo], [
|
||||
'total_price' => $centerOrder['total_price'],
|
||||
'pay_price' => $centerOrder['pay_price'],
|
||||
'pay_postage' => $centerOrder['pay_postage'],
|
||||
'coupon_price' => $centerOrder['coupon_price'],
|
||||
'deduction_price'=> $centerOrder['deduction_price'],
|
||||
'member_discount'=> $centerOrder['member_discount'],
|
||||
'vip_discount' => $centerOrder['vip_discount'],
|
||||
'use_integral_num'=> $centerOrder['use_integral_num'],
|
||||
'total_num' => $remNum,
|
||||
'title' => $remain[0]['store_name'] ?? '',
|
||||
'image' => $remain[0]['image'] ?? '',
|
||||
'extra_info' => json_encode($parentExtra, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
|
||||
// 5.4 扩展表:原订单金额同步
|
||||
$this->dao->update(['order_no' => $orderNo], [
|
||||
'pay_price' => $centerOrder['pay_price'],
|
||||
'total_price' => $centerOrder['total_price'],
|
||||
]);
|
||||
});
|
||||
|
||||
// 6. 子订单发货(复用统一发货落库:扩展表状态+中台 delivery/extra_info.delivery/delivery_time)
|
||||
$this->updateDeliveryInfo($childNo, (string)($deliveryData['delivery_name'] ?? ''), (string)($deliveryData['delivery_id'] ?? ''), (string)($deliveryData['delivery_type'] ?? 'express'));
|
||||
|
||||
// 7. 双订单日志
|
||||
/** @var \app\dao\order\OrderLogDao $logDao */
|
||||
$logDao = app(\app\dao\order\OrderLogDao::class);
|
||||
$uid = (int)($centerOrder['uid'] ?? 0);
|
||||
$statusCode = (string)($centerOrder['status_code'] ?? '');
|
||||
$logDao->save([
|
||||
'order_no' => $orderNo, 'uid' => $uid, 'operator_id' => 0, 'operator_type' => 'admin',
|
||||
'from_status' => $statusCode, 'to_status' => $statusCode,
|
||||
'content' => '拆单发货:拆出子订单 ' . $childNo . '(' . $selNum . ' 件)', 'add_time' => $now,
|
||||
]);
|
||||
$logDao->save([
|
||||
'order_no' => $childNo, 'uid' => $uid, 'operator_id' => 0, 'operator_type' => 'admin',
|
||||
'from_status' => $statusCode, 'to_status' => $statusCode,
|
||||
'content' => '拆单发货(来源订单 ' . $orderNo . '):' . ($deliveryData['delivery_name'] ?? ''), 'add_time' => $now,
|
||||
]);
|
||||
|
||||
return [$childNo, $orderNo];
|
||||
}
|
||||
|
||||
// ===== 价格 =====
|
||||
|
||||
/**
|
||||
|
||||
@ -174,7 +174,7 @@ AllowOriginMiddleware
|
||||
统一订单入口位于 `center.php`/`order.php`:
|
||||
|
||||
- 订单列表、详情和各订单类型扩展。
|
||||
- 订单备注、状态、发货、拆单、配送和收货。
|
||||
- 订单备注、状态、发货、配送和收货。
|
||||
- 订单打印、导出和发票。
|
||||
- 售后列表、详情、审核、退货和退款。
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
- 订单中台统一列表和详情。
|
||||
- 按订单号、用户、状态、来源、支付和时间筛选。
|
||||
- 订单备注、改价、取消、删除和打印。
|
||||
- 发货、拆单发货、配送信息修改和物流查询。
|
||||
- 发货、配送信息修改和物流查询。
|
||||
- 用户确认收货与后台确认收货。
|
||||
- 订单商品快照、状态日志和操作记录。
|
||||
- 不同 `source_type` 的订单状态、按钮和详情扩展。
|
||||
@ -146,7 +146,7 @@
|
||||
- 运费模板、指定包邮、不配送区域和区域运费。
|
||||
- 快递公司与物流查询。
|
||||
- 电子面单公司、模板、业务配置和打印记录。
|
||||
- 订单发货、拆单发货、物流轨迹和收货。
|
||||
- 订单发货、物流轨迹和收货。
|
||||
- 云打印渠道、打印机和小票配置。
|
||||
|
||||
### 2.11 财务
|
||||
@ -367,7 +367,7 @@ App/H5:
|
||||
|
||||
### 4.3 发货与收货
|
||||
|
||||
1. 管理员在订单中台选择整单或拆单发货。
|
||||
1. 管理员在订单中台整单发货(系统不支持拆单/分批发货)。
|
||||
2. 选择快递/配送方式并写入单号和商品明细。
|
||||
3. 可调用电子面单和打印驱动。
|
||||
4. 通知用户并记录订单日志。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user