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