384 lines
16 KiB
PHP
384 lines
16 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace App\Service\business;
|
|||
|
|
|
|||
|
|
use App\Models\business\ListItemModel;
|
|||
|
|
use App\Models\business\ListModel;
|
|||
|
|
use App\Models\business\OrderDeliveryModel;
|
|||
|
|
use App\Models\business\OrderItemModel;
|
|||
|
|
use App\Models\business\OrderModel;
|
|||
|
|
use App\Models\business\OrderPaymentModel;
|
|||
|
|
use App\Models\business\WxUserModel;
|
|||
|
|
use App\Service\common\UtilsService;
|
|||
|
|
use Illuminate\Support\Facades\DB;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 订单核心逻辑(不含鉴权)
|
|||
|
|
*
|
|||
|
|
* 后台(OrderService,管理员身份)与小程序(WxOrderService,微信用户身份)都要用同一套
|
|||
|
|
* 建单、收款、发货规则。鉴权基类不同,所以规则收在这个中立类里,两边只做身份校验与参数整理。
|
|||
|
|
*
|
|||
|
|
* 三条纪律:金额一律整数分;状态只能通过 transition 迁移;支付确认必须行锁 + 幂等。
|
|||
|
|
*/
|
|||
|
|
class OrderCoreService
|
|||
|
|
{
|
|||
|
|
private static mixed $_instance;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 允许的状态迁移,其余一律拒绝
|
|||
|
|
*/
|
|||
|
|
private const TRANSITIONS = [
|
|||
|
|
OrderModel::STATUS_UNPAID => [OrderModel::STATUS_PAID, OrderModel::STATUS_CANCELLED],
|
|||
|
|
OrderModel::STATUS_PAID => [OrderModel::STATUS_SHIPPED, OrderModel::STATUS_CANCELLED],
|
|||
|
|
OrderModel::STATUS_SHIPPED => [OrderModel::STATUS_DONE],
|
|||
|
|
OrderModel::STATUS_DONE => [],
|
|||
|
|
OrderModel::STATUS_CANCELLED => [],
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
public static function getInstance(): null|static
|
|||
|
|
{
|
|||
|
|
$name = get_called_class();
|
|||
|
|
if (!isset(self::$_instance[$name])) {
|
|||
|
|
self::$_instance[$name] = new static();
|
|||
|
|
}
|
|||
|
|
return self::$_instance[$name];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 由清单生成订单
|
|||
|
|
*
|
|||
|
|
* @param int $listId 清单 ID
|
|||
|
|
* @param int $userId 下单用户(cc_wx_user.id)
|
|||
|
|
* @param array $params receiver_name/receiver_phone/receiver_address/delivery_type/remark
|
|||
|
|
* @return array 新订单详情
|
|||
|
|
*/
|
|||
|
|
public function createFromList(int $listId, int $userId, array $params = []): array
|
|||
|
|
{
|
|||
|
|
$list = ListModel::where('id', $listId)->where('deleted_at', 0)->first();
|
|||
|
|
if (empty($list)) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('清单不存在');
|
|||
|
|
}
|
|||
|
|
if ($userId > 0 && (int) $list['user_id'] !== $userId) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('不能对他人的清单下单');
|
|||
|
|
}
|
|||
|
|
$userId = (int) $list['user_id'];
|
|||
|
|
|
|||
|
|
$items = ListItemModel::with([
|
|||
|
|
'catalogue',
|
|||
|
|
'priceSheet',
|
|||
|
|
])->where('list_id', $listId)->where('deleted_at', 0)->get();
|
|||
|
|
if ($items->isEmpty()) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('清单里还没有商品');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$user = WxUserModel::where('id', $userId)->first();
|
|||
|
|
$multiplier = $user['price_number'] ?? 1;
|
|||
|
|
$price = PriceService::getInstance();
|
|||
|
|
|
|||
|
|
$rows = [];
|
|||
|
|
$missing = [];
|
|||
|
|
$total = 0;
|
|||
|
|
foreach ($items as $item) {
|
|||
|
|
$catalogue = $item->catalogue;
|
|||
|
|
if (empty($catalogue)) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
$sheet = $item->priceSheet;
|
|||
|
|
if (empty($sheet)) {
|
|||
|
|
// 老清单没有 price_sheet_id,缺规格的行必须让用户回清单补选,不能瞎猜一个价格
|
|||
|
|
$missing[] = $catalogue['title'] ?? ('#' . $item['catalogue_id']);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
$quantity = max(1, (int) $item['quantity']);
|
|||
|
|
$unitPrice = (int) $item['unit_price'];
|
|||
|
|
if ($unitPrice <= 0) {
|
|||
|
|
$unitPrice = $price->resolveUnitPrice($sheet['routine'] ?? '', (string) $item['material_key'], $multiplier);
|
|||
|
|
}
|
|||
|
|
$lineTotal = $unitPrice * $quantity;
|
|||
|
|
$total += $lineTotal;
|
|||
|
|
$rows[] = [
|
|||
|
|
'catalogue_id' => (int) $item['catalogue_id'],
|
|||
|
|
'price_sheet_id' => (int) $item['price_sheet_id'],
|
|||
|
|
'title' => (string) ($catalogue['title'] ?? ''),
|
|||
|
|
'cover' => (string) ($catalogue['cover'] ?? ''),
|
|||
|
|
'alias' => (string) ($catalogue['alias'] ?? ''),
|
|||
|
|
'specification' => (string) ($sheet['specification'] ?? ''),
|
|||
|
|
'dimension' => (string) ($sheet['dimension'] ?? ''),
|
|||
|
|
'material_key' => (string) ($item['material_key'] ?? 'routine'),
|
|||
|
|
'quantity' => $quantity,
|
|||
|
|
'unit_price' => $unitPrice,
|
|||
|
|
'total_price' => $lineTotal,
|
|||
|
|
'remark' => (string) ($item['remark'] ?? ''),
|
|||
|
|
'created_at' => time(),
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
if (!empty($missing)) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('以下商品还没有选规格:' . implode('、', array_slice($missing, 0, 5)));
|
|||
|
|
}
|
|||
|
|
if (empty($rows)) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('清单里没有可下单的商品');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$orderId = 0;
|
|||
|
|
DB::connection('business')->transaction(function () use (&$orderId, $list, $userId, $user, $params, $rows, $total) {
|
|||
|
|
$now = time();
|
|||
|
|
$orderId = OrderModel::insertGetId([
|
|||
|
|
'order_no' => SerialNoService::getInstance()->generate(SerialNoService::PREFIX_ORDER, 'order', 'order_no'),
|
|||
|
|
'list_id' => (int) $list['id'],
|
|||
|
|
'user_id' => $userId,
|
|||
|
|
'enterprise_id' => (int) ($list['enterprise_id'] ?: ($user['enterprise_id'] ?? 0)),
|
|||
|
|
'total_amount' => $total,
|
|||
|
|
'delivery_type' => (int) ($params['delivery_type'] ?? 0),
|
|||
|
|
'receiver_name' => (string) ($params['receiver_name'] ?? ($user['nick_name'] ?? '')),
|
|||
|
|
'receiver_phone' => (string) ($params['receiver_phone'] ?? ($user['phone'] ?? '')),
|
|||
|
|
'receiver_address' => (string) ($params['receiver_address'] ?? ''),
|
|||
|
|
'remark' => (string) ($params['remark'] ?? ''),
|
|||
|
|
'status' => OrderModel::STATUS_UNPAID,
|
|||
|
|
'created_at' => $now,
|
|||
|
|
]);
|
|||
|
|
foreach ($rows as &$row) {
|
|||
|
|
$row['order_id'] = $orderId;
|
|||
|
|
}
|
|||
|
|
unset($row);
|
|||
|
|
OrderItemModel::insert($rows);
|
|||
|
|
ListModel::where('id', $list['id'])->update(['status' => 1, 'updated_at' => $now]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return $this->detail($orderId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 订单详情(含明细、支付记录、发货记录)
|
|||
|
|
*/
|
|||
|
|
public function detail(int $orderId): array
|
|||
|
|
{
|
|||
|
|
$order = OrderModel::with([
|
|||
|
|
'items' => fn ($query) => $query->where('deleted_at', 0),
|
|||
|
|
'payments' => fn ($query) => $query->where('deleted_at', 0)->orderBy('id', 'desc'),
|
|||
|
|
'deliveries' => fn ($query) => $query->where('deleted_at', 0)->orderBy('id', 'desc'),
|
|||
|
|
'user',
|
|||
|
|
'enterprise',
|
|||
|
|
])->where('id', $orderId)->where('deleted_at', 0)->first();
|
|||
|
|
if (empty($order)) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('订单不存在');
|
|||
|
|
}
|
|||
|
|
$order = $order->toArray();
|
|||
|
|
$order['voucher_list'] = [];
|
|||
|
|
foreach ($order['payments'] ?? [] as $payment) {
|
|||
|
|
foreach (array_filter(explode(',', (string) $payment['voucher'])) as $image) {
|
|||
|
|
$order['voucher_list'][] = $image;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return $order;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 提交转账凭证,进入待审核
|
|||
|
|
*/
|
|||
|
|
public function submitVoucher(int $orderId, array $params, int $userId = 0): array
|
|||
|
|
{
|
|||
|
|
$order = $this->lockOrder($orderId, $userId);
|
|||
|
|
if ((int) $order['pay_status'] === OrderModel::PAY_STATUS_PAID) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('订单已付款');
|
|||
|
|
}
|
|||
|
|
$voucher = $params['voucher'] ?? '';
|
|||
|
|
$voucher = is_array($voucher) ? implode(',', array_filter($voucher)) : (string) $voucher;
|
|||
|
|
if ($voucher === '') {
|
|||
|
|
UtilsService::getInstance()->errorThrow('请上传转账凭证');
|
|||
|
|
}
|
|||
|
|
$amount = (int) ($params['amount'] ?? 0);
|
|||
|
|
if ($amount <= 0) {
|
|||
|
|
$amount = (int) $order['total_amount'] - (int) $order['paid_amount'];
|
|||
|
|
}
|
|||
|
|
$now = time();
|
|||
|
|
DB::connection('business')->transaction(function () use ($orderId, $voucher, $amount, $now) {
|
|||
|
|
OrderPaymentModel::insert([
|
|||
|
|
'order_id' => $orderId,
|
|||
|
|
'pay_type' => OrderModel::PAY_TYPE_VOUCHER,
|
|||
|
|
'amount' => $amount,
|
|||
|
|
'voucher' => $voucher,
|
|||
|
|
// 转账没有微信单号,用订单 + 时间占位,仍受唯一索引约束防重复提交
|
|||
|
|
'out_trade_no' => 'TR' . $orderId . '_' . $now,
|
|||
|
|
'status' => OrderPaymentModel::STATUS_AUDITING,
|
|||
|
|
'created_at' => $now,
|
|||
|
|
]);
|
|||
|
|
OrderModel::where('id', $orderId)->update([
|
|||
|
|
'pay_type' => OrderModel::PAY_TYPE_VOUCHER,
|
|||
|
|
'pay_status' => OrderModel::PAY_STATUS_AUDITING,
|
|||
|
|
'updated_at' => $now,
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
return $this->detail($orderId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 审核转账凭证
|
|||
|
|
*
|
|||
|
|
* @param int $status OrderPaymentModel::STATUS_CONFIRMED|STATUS_REJECTED
|
|||
|
|
*/
|
|||
|
|
public function auditPayment(int $paymentId, int $status, int $adminId, string $remark = ''): array
|
|||
|
|
{
|
|||
|
|
$orderId = 0;
|
|||
|
|
DB::connection('business')->transaction(function () use ($paymentId, $status, $adminId, $remark, &$orderId) {
|
|||
|
|
$payment = OrderPaymentModel::where('id', $paymentId)->lockForUpdate()->first();
|
|||
|
|
if (empty($payment)) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('支付记录不存在');
|
|||
|
|
}
|
|||
|
|
if ((int) $payment['status'] !== OrderPaymentModel::STATUS_AUDITING) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('该支付记录已处理');
|
|||
|
|
}
|
|||
|
|
$orderId = (int) $payment['order_id'];
|
|||
|
|
$now = time();
|
|||
|
|
OrderPaymentModel::where('id', $paymentId)->update([
|
|||
|
|
'status' => $status,
|
|||
|
|
'auditor_id' => $adminId,
|
|||
|
|
'audited_at' => $now,
|
|||
|
|
'audit_remark' => $remark,
|
|||
|
|
'updated_at' => $now,
|
|||
|
|
]);
|
|||
|
|
if ($status !== OrderPaymentModel::STATUS_CONFIRMED) {
|
|||
|
|
OrderModel::where('id', $orderId)->update([
|
|||
|
|
'pay_status' => OrderModel::PAY_STATUS_REJECTED,
|
|||
|
|
'updated_at' => $now,
|
|||
|
|
]);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
$this->applyPaid($orderId, (int) $payment['amount'], $now);
|
|||
|
|
});
|
|||
|
|
return $this->detail($orderId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 记一笔收款并推进订单状态
|
|||
|
|
*
|
|||
|
|
* 收款可能分多笔,只有累计金额够了才算付清,否则停在部分收款。
|
|||
|
|
*/
|
|||
|
|
public function applyPaid(int $orderId, int $amount, int $now = 0): void
|
|||
|
|
{
|
|||
|
|
$now = $now ?: time();
|
|||
|
|
$order = OrderModel::where('id', $orderId)->lockForUpdate()->first();
|
|||
|
|
if (empty($order)) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('订单不存在');
|
|||
|
|
}
|
|||
|
|
$paid = (int) $order['paid_amount'] + $amount;
|
|||
|
|
$update = [
|
|||
|
|
'paid_amount' => $paid,
|
|||
|
|
'updated_at' => $now,
|
|||
|
|
];
|
|||
|
|
if ($paid >= (int) $order['total_amount']) {
|
|||
|
|
$update['pay_status'] = OrderModel::PAY_STATUS_PAID;
|
|||
|
|
$update['paid_at'] = $now;
|
|||
|
|
if ($this->canTransition((int) $order['status'], OrderModel::STATUS_PAID)) {
|
|||
|
|
$update['status'] = OrderModel::STATUS_PAID;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
OrderModel::where('id', $orderId)->update($update);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 微信支付回调落账(幂等)
|
|||
|
|
*
|
|||
|
|
* 微信会重复推送同一笔,靠 out_trade_no 唯一索引 + 状态判断挡住重复入账。
|
|||
|
|
*/
|
|||
|
|
public function confirmWechatPay(string $outTradeNo, string $transactionId, int $amount): bool
|
|||
|
|
{
|
|||
|
|
$done = false;
|
|||
|
|
DB::connection('business')->transaction(function () use ($outTradeNo, $transactionId, $amount, &$done) {
|
|||
|
|
$payment = OrderPaymentModel::where('out_trade_no', $outTradeNo)->lockForUpdate()->first();
|
|||
|
|
if (empty($payment)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if ((int) $payment['status'] === OrderPaymentModel::STATUS_CONFIRMED) {
|
|||
|
|
$done = true;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
$now = time();
|
|||
|
|
OrderPaymentModel::where('id', $payment['id'])->update([
|
|||
|
|
'status' => OrderPaymentModel::STATUS_CONFIRMED,
|
|||
|
|
'transaction_id' => $transactionId,
|
|||
|
|
'amount' => $amount > 0 ? $amount : (int) $payment['amount'],
|
|||
|
|
'audited_at' => $now,
|
|||
|
|
'updated_at' => $now,
|
|||
|
|
]);
|
|||
|
|
$this->applyPaid((int) $payment['order_id'], $amount > 0 ? $amount : (int) $payment['amount'], $now);
|
|||
|
|
$done = true;
|
|||
|
|
});
|
|||
|
|
return $done;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 发货:物流 / 自提 / 公司配送
|
|||
|
|
*/
|
|||
|
|
public function ship(int $orderId, array $params, int $adminId): array
|
|||
|
|
{
|
|||
|
|
$order = $this->lockOrder($orderId);
|
|||
|
|
$type = (int) ($params['delivery_type'] ?? OrderModel::DELIVERY_EXPRESS);
|
|||
|
|
if ($type === OrderModel::DELIVERY_EXPRESS && trim((string) ($params['tracking_no'] ?? '')) === '') {
|
|||
|
|
UtilsService::getInstance()->errorThrow('请填写运单号');
|
|||
|
|
}
|
|||
|
|
if ($type === OrderModel::DELIVERY_PICKUP && trim((string) ($params['pickup_point'] ?? '')) === '') {
|
|||
|
|
UtilsService::getInstance()->errorThrow('请填写自提点');
|
|||
|
|
}
|
|||
|
|
if (!$this->canTransition((int) $order['status'], OrderModel::STATUS_SHIPPED)) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('当前订单状态不允许发货');
|
|||
|
|
}
|
|||
|
|
$now = time();
|
|||
|
|
DB::connection('business')->transaction(function () use ($orderId, $params, $type, $adminId, $now) {
|
|||
|
|
OrderDeliveryModel::insert([
|
|||
|
|
'order_id' => $orderId,
|
|||
|
|
'delivery_type' => $type,
|
|||
|
|
'company' => (string) ($params['company'] ?? ''),
|
|||
|
|
'tracking_no' => (string) ($params['tracking_no'] ?? ''),
|
|||
|
|
'pickup_point' => (string) ($params['pickup_point'] ?? ''),
|
|||
|
|
'driver_info' => (string) ($params['driver_info'] ?? ''),
|
|||
|
|
'shipped_at' => $now,
|
|||
|
|
'remark' => (string) ($params['remark'] ?? ''),
|
|||
|
|
'operator_id' => $adminId,
|
|||
|
|
'created_at' => $now,
|
|||
|
|
]);
|
|||
|
|
OrderModel::where('id', $orderId)->update([
|
|||
|
|
'delivery_type' => $type,
|
|||
|
|
'status' => OrderModel::STATUS_SHIPPED,
|
|||
|
|
'updated_at' => $now,
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
return $this->detail($orderId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 状态迁移(取消、完成等)
|
|||
|
|
*/
|
|||
|
|
public function transition(int $orderId, int $target, int $userId = 0): array
|
|||
|
|
{
|
|||
|
|
$order = $this->lockOrder($orderId, $userId);
|
|||
|
|
if (!$this->canTransition((int) $order['status'], $target)) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('当前状态不允许该操作');
|
|||
|
|
}
|
|||
|
|
OrderModel::where('id', $orderId)->update([
|
|||
|
|
'status' => $target,
|
|||
|
|
'updated_at' => time(),
|
|||
|
|
]);
|
|||
|
|
return $this->detail($orderId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function canTransition(int $from, int $to): bool
|
|||
|
|
{
|
|||
|
|
return in_array($to, self::TRANSITIONS[$from] ?? [], true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 取订单并做归属校验($userId > 0 时限定本人)
|
|||
|
|
*/
|
|||
|
|
private function lockOrder(int $orderId, int $userId = 0): array
|
|||
|
|
{
|
|||
|
|
$order = OrderModel::where('id', $orderId)->where('deleted_at', 0)->first();
|
|||
|
|
if (empty($order)) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('订单不存在');
|
|||
|
|
}
|
|||
|
|
if ($userId > 0 && (int) $order['user_id'] !== $userId) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('无权操作该订单');
|
|||
|
|
}
|
|||
|
|
return $order->toArray();
|
|||
|
|
}
|
|||
|
|
}
|