shop = $shop; } public function preview_checkout(int $user_id, array $payload): array { if ($user_id <= 0) { return ['success' => false, 'message' => __('请先登录后再结算。', 'aksrt')]; } $items = $this->normalize_checkout_items($payload['items'] ?? []); if (empty($items)) { return ['success' => false, 'message' => __('请选择至少一个商品。', 'aksrt')]; } $product_views = []; $subtotal = 0.0; foreach ($items as $item) { $product = $this->shop->get_product_detail_view((int) $item['product_id']); if (empty($product) || empty($product['purchase_enabled'])) { return ['success' => false, 'message' => __('存在不可购买商品,请刷新后重试。', 'aksrt')]; } if ((int) $product['stock'] < (int) $item['quantity']) { return ['success' => false, 'message' => sprintf(__('商品 %s 库存不足。', 'aksrt'), $product['title'])]; } $line_total = round(((float) $product['price']) * (int) $item['quantity'], 2); // R69: Apply discount engine (shop_discount taxonomy campaigns) $discount_result = ['discount_amount' => 0, 'gifts' => [], 'applied' => []]; if (class_exists('\AKSRT\Payment\Discount')) { $discount_result = \AKSRT\Payment\Discount::get_instance()->calculate( (float) $product['price'], (int) $item['quantity'], (int) $product['id'], $user_id ); $line_total = $discount_result['final_price']; } $subtotal += $line_total; $product_views[] = [ 'product_id' => (int) $product['id'], 'title' => $product['title'], 'price' => (float) $product['price'], 'quantity' => (int) $item['quantity'], 'line_total' => $line_total, 'image' => $product['image'], 'shipping' => $product['shipping'], 'discount_amount' => (float) ($discount_result['discount_amount'] ?? 0), 'gifts' => (array) ($discount_result['gifts'] ?? []), 'applied_discounts' => (array) ($discount_result['applied'] ?? []), ]; } $shipping_method = sanitize_key((string) ($payload['shipping_method'] ?? 'express')); $shipping_profile = $this->resolve_shipping_quote($product_views, $shipping_method); $coupon_code = sanitize_text_field((string) ($payload['coupon_code'] ?? '')); $product_ids = array_values(array_unique(array_map(static function ($row) { return (int) ($row['product_id'] ?? $row['id'] ?? 0); }, $product_views))); $coupon_result = $this->resolve_coupon_discount($coupon_code, $subtotal + $shipping_profile['fee'], $product_ids); if (!$coupon_result['success']) { return $coupon_result; } $total = max(0, round($subtotal + $shipping_profile['fee'] - $coupon_result['discount'], 2)); $address = $this->resolve_checkout_address($user_id, (int) ($payload['address_id'] ?? 0)); $payment_method = $this->resolve_payment_method((string) ($payload['payment_method'] ?? 'balance')); return [ 'success' => true, 'data' => [ 'items' => $product_views, 'subtotal' => round($subtotal, 2), 'shipping_fee' => $shipping_profile['fee'], 'shipping_method' => $shipping_profile['method'], 'discount' => $coupon_result['discount'], 'coupon' => $coupon_result['coupon'], 'address' => $address, 'payment_method' => $payment_method, 'total' => $total, ], ]; } public function create_checkout_order(int $user_id, array $payload): array { $preview = $this->preview_checkout($user_id, $payload); if (empty($preview['success'])) { return $preview; } $data = $preview['data']; $primary_item = $data['items'][0] ?? null; if (!$primary_item || !class_exists('\AKSRT\Payment\OrderRepository')) { return ['success' => false, 'message' => __('订单创建失败。', 'aksrt')]; } $payment_method = sanitize_key((string) ($data['payment_method']['value'] ?? 'balance')); $meta_payload = [ 'customer_note' => sanitize_textarea_field((string) ($payload['customer_note'] ?? '')), 'items' => $data['items'], 'coupon' => $data['coupon'], 'shipping_fee' => $data['shipping_fee'], 'shipping_method' => $data['shipping_method'], 'address' => $data['address'], 'payment_method' => $data['payment_method'], 'discount' => $data['discount'], ]; $expiry_minutes = 30; if (class_exists('\AKSRT\Settings\Settings')) { $order_settings = \AKSRT\Settings\Settings::get_order_settings(); $expiry_minutes = max(5, min(1440, (int) ($order_settings['expiry'] ?? 30))); } $order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([ 'user_id' => $user_id, 'product_type' => 'shop_product', 'product_id' => (int) $primary_item['product_id'], 'amount' => (float) $data['total'], 'currency' => 'CNY', 'status' => \AKSRT\Payment\Order::STATUS_PENDING, 'payment_method' => $payment_method, 'expires_at' => date('Y-m-d H:i:s', strtotime('+' . $expiry_minutes . ' minutes')), 'note' => wp_json_encode($meta_payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]); if ($order_number === '') { return ['success' => false, 'message' => __('订单创建失败。', 'aksrt')]; } $this->shop->save_order_items($order_number, $data['items']); if (!empty($data['coupon']['code']) && class_exists('\AKSRT\Payment\Coupon')) { $coupon_instance = \AKSRT\Payment\Coupon::get_instance(); $coupon_instance->use_coupon($data['coupon']['code']); // R69: Log coupon usage for per-user tracking $coupon_row = $coupon_instance->validate($data['coupon']['code']); $coupon_id = (int) ($coupon_row['coupon']['id'] ?? 0); if ($coupon_id > 0) { $coupon_instance->log_usage( $coupon_id, $user_id, $order_number, (float) ($data['discount'] ?? 0) ); } } $detail = $this->shop->get_order_detail_view($user_id, $order_number); return [ 'success' => true, 'data' => [ 'order_number' => $order_number, 'order' => $detail, ], ]; } public function pay_order(int $user_id, string $order_number, string $payment_method): array { if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) { return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; } $repo = \AKSRT\Payment\OrderRepository::get_instance(); $order = $repo->get_user_order($user_id, $order_number); if (!$order) { return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; } $status = (string) ($order['status'] ?? ''); if (!in_array($status, [ \AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_PENDING, ], true)) { return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')]; } // Payment attempts are bound to the method selected when the order was created. $stored_method = sanitize_key((string) ($order['payment_method'] ?? '')); $payment_method = sanitize_key($payment_method ?: ($stored_method ?: 'balance')); if ($stored_method !== '' && $payment_method !== $stored_method) { return ['success' => false, 'message' => __('支付方式与订单不一致,请重新下单。', 'aksrt')]; } $gateway_manager = class_exists('\AKSRT\Payment\Gateway') ? \AKSRT\Payment\Gateway::get_instance() : null; $gateway = $gateway_manager ? $gateway_manager->get_gateway($payment_method) : null; if (!$gateway) { return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')]; } if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) { return ['success' => false, 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')]; } // Points payment is a purely internal settlement (no provider attempt), // but still needs the attempt lock against double-click double deduction. if ($payment_method === 'points') { $lock = class_exists('\AKSRT\Payment\PaymentAttemptLock') ? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number) : false; if ($lock === false) { return ['success' => false, 'message' => __('该订单正在发起支付,请稍后重试。', 'aksrt')]; } try { return $this->pay_with_points($user_id, $order_number, $order); } finally { \AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock); } } // 携带订单号与摘要,供网关生成支付单。 // PaymentOrchestrator 内部持有 attempt 锁并分配 provider out_trade_no。 $order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number); $order['payment_method'] = $payment_method; $payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($gateway, $order, [ 'paid_note' => __('用户在前台完成支付。', 'aksrt'), ]); if (empty($payment_result['success'])) { return ['success' => false, 'message' => $payment_result['message']]; } // 跳转/扫码型网关(支付宝/微信/PayPal/Stripe):返回支付凭据,订单保持待支付, // 由 /payment/{gateway}/notify|return 回调确认到账;此处绝不预先标记已支付。 if (!empty($payment_result['redirect'])) { return [ 'success' => true, 'data' => [ 'redirect' => true, 'payment_url' => (string) ($payment_result['payment_url'] ?? ''), 'qr_content' => (string) ($payment_result['qr_content'] ?? ''), 'order_number' => $order_number, 'message' => $payment_result['message'], ], ]; } return [ 'success' => true, 'data' => [ 'order' => $this->shop->get_order_detail_view($user_id, $order_number), 'message' => $payment_result['message'], ], ]; } private function pay_with_points(int $user_id, string $order_number, array $order): array { if (!class_exists('\AKSRT\Credits\Credits')) { return ['success' => false, 'message' => __('积分系统不可用。', 'aksrt')]; } $amount = (int) round((float) ($order['amount'] ?? 0)); if ($amount <= 0) { return ['success' => false, 'message' => __('订单金额无效。', 'aksrt')]; } $balance = \AKSRT\Credits\Credits::balance($user_id); if ($balance < $amount) { return ['success' => false, 'message' => sprintf(__('积分不足,需要 %d 积分,当前余额 %d。', 'aksrt'), $amount, $balance)]; } $deducted = \AKSRT\Credits\Credits::deduct( $user_id, $amount, 'shop_points_pay', ['order' => $order_number], sprintf(__('积分购买订单 %s', 'aksrt'), $order_number) ); if (is_wp_error($deducted)) { return ['success' => false, 'message' => $deducted->get_error_message() ?: __('积分扣除失败。', 'aksrt')]; } $repo = \AKSRT\Payment\OrderRepository::get_instance(); $paid = $repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [ 'payment_method' => 'points', 'payment_transaction_id' => 'points_' . $order_number, 'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount), 'require_transition' => true, ]); if (!$paid) { $compensation = \AKSRT\Credits\Credits::add( $user_id, $amount, 'shop_points_pay_rollback', ['order' => $order_number], sprintf(__('订单 %s 支付失败退回积分', 'aksrt'), $order_number) ); if (is_wp_error($compensation)) { error_log(sprintf( '[AKSRT Payment] points rollback failed order=%s: %s', $order_number, $compensation->get_error_message() )); return ['success' => false, 'message' => __('订单状态已变化,积分补偿失败,请联系管理员处理。', 'aksrt')]; } return ['success' => false, 'message' => __('订单状态已变化,积分已退回。', 'aksrt')]; } return [ 'success' => true, 'data' => [ 'order' => $this->shop->get_order_detail_view($user_id, $order_number), 'message' => __('积分支付成功。', 'aksrt'), ], ]; } private function normalize_checkout_items($items): array { if (!is_array($items)) { return []; } $normalized = []; foreach ($items as $item) { $product_id = (int) ($item['product_id'] ?? 0); $quantity = max(1, (int) ($item['quantity'] ?? 1)); if ($product_id <= 0) { continue; } $normalized[] = [ 'product_id' => $product_id, 'quantity' => $quantity, ]; } return $normalized; } private function resolve_coupon_discount(string $coupon_code, float $order_amount, array $product_ids = []): array { if ($coupon_code === '') { return ['success' => true, 'discount' => 0.0, 'coupon' => []]; } if (!class_exists('\AKSRT\Payment\Coupon')) { return ['success' => false, 'message' => __('优惠券系统不可用。', 'aksrt')]; } $result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $order_amount, [ 'user_id' => get_current_user_id(), 'product_ids' => array_values(array_filter(array_map('intval', $product_ids))), 'context' => 'shop', ]); if (empty($result['valid'])) { return ['success' => false, 'message' => $result['message'] ?? __('优惠券不可用。', 'aksrt')]; } return [ 'success' => true, 'discount' => (float) ($result['discount'] ?? 0), 'coupon' => [ 'code' => (string) ($result['coupon']['code'] ?? $coupon_code), 'type' => (string) ($result['coupon']['type'] ?? ''), 'value' => (float) ($result['coupon']['value'] ?? 0), ], ]; } private function resolve_shipping_quote(array $items, string $shipping_method): array { $methods = $this->shop->get_shipping_method_options(); $method = $methods[$shipping_method] ?? $methods['express']; $base_fee = (float) ($method['fee'] ?? 0); foreach ($items as $item) { $base_fee += (float) ($item['shipping']['fee'] ?? 0); } return [ 'method' => $method, 'fee' => round($base_fee, 2), ]; } private function resolve_checkout_address(int $user_id, int $address_id): array { $addresses = $this->shop->get_address_book($user_id); foreach ($addresses as $address) { if ((int) ($address['id'] ?? 0) === $address_id) { return $address; } } return $addresses[0] ?? []; } private function resolve_payment_method(string $value): array { $methods = $this->shop->get_payment_method_options(); return $methods[$value] ?? reset($methods); } public function get_checkout_hint(array $product): array { return [ 'delivery' => $product['shipping']['delivery_note'] ?: __('付款后自动进入订单中心,可继续查看物流和售后进度。', 'aksrt'), 'service' => $product['after_sale']['policy'] ?: __('支持在订单中心发起售后申请。', 'aksrt'), ]; } }