refactor(订单): 移除拆单发货与拆单退款残留逻辑

- 发货服务删除 splitDelivery/getCartList/tidyCartList,通用发货任务改走 delivery()
- 删除子单收货联动 checkMaster、拆单开票 splitOrderInvoice 及 pid 分支
- 普通订单模块与开放接口移除拆单控制器方法、路由与拆单文案
- 订单详情 related_orders 恒为 null,文档同步为不支持拆单
This commit is contained in:
吴昊天 2026-10-23 16:11:02 +08:00
parent d8e2a51350
commit 22362638ef
15 changed files with 16 additions and 496 deletions

View File

@ -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 | 有售后记录 | 售后模块监听器 |

View File

@ -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

View File

@ -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());
}

View File

@ -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',

View File

@ -624,10 +624,6 @@ return [
'请选择奖品类型' => '请选择奖品类型',
'请填写奖品赠送{:type}数' => '请填写奖品赠送{:type}数',
'请选择优惠券' => '请选择优惠券',
'生成新订单失败' => '生成新订单失败',
'修改原来订单商品拆分状态失败' => '修改原来订单商品拆分状态失败',
'新增拆分订单商品信息失败' => '新增拆分订单商品信息失败',
'保存新订单商品信息失败' => '保存新订单商品信息失败',
'协议类型缺失' => '协议类型缺失',
'请填写协议名称' => '请填写协议名称',
'请填写协议内容' => '请填写协议内容',
@ -1098,7 +1094,6 @@ return [
'正在申请退款中' => '正在申请退款中',
'申请失败' => '申请失败',
'退款件数大于订单件数' => '退款件数大于订单件数',
'该订单已全部拆分' => '该订单已全部拆分',
'申请退款失败' => '申请退款失败',
'存在待处理退款单' => '存在待处理退款单',
'该订单无法删除' => '该订单无法删除',

View File

@ -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;
}

View File

@ -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

View File

@ -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;

View File

@ -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 商品列表数组
*/

View File

@ -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]);
}
}
}

View File

@ -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());
}
}
/**
* 物流查询
* 根据订单号查询物流轨迹

View File

@ -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' => '修改配送信息']);
// 备注|删除|改价

View File

@ -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];
}
// ===== 价格 =====
/**

View File

@ -174,7 +174,7 @@ AllowOriginMiddleware
统一订单入口位于 `center.php`/`order.php`:
- 订单列表、详情和各订单类型扩展。
- 订单备注、状态、发货、拆单、配送和收货。
- 订单备注、状态、发货、配送和收货。
- 订单打印、导出和发票。
- 售后列表、详情、审核、退货和退款。

View File

@ -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. 通知用户并记录订单日志。