Files

384 lines
16 KiB
PHP
Raw Permalink Normal View History

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