Files
lgp-admin-plus-api/app/Service/business/OrderCoreService.php
LQ bcf54c2727 初始化
缺陷:主题配色需要优化整体的同风格
2026-08-14 23:21:21 +08:00

384 lines
16 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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();
}
}