原来的重复申请闸门只看「进行中」的工单(find_active = pending/processing), 于是工单已 completed、但订单没被另标「已退款」时,订单还能再发起一次售后。 已退款订单之所以不能再申请,是因为 status_group 变了导致 enabled 为 false, 属于间接效果;工单层面的规则一直是漏的。 - create() 改为以 latest_for_order() 为准:completed 直接挡掉;待审核/处理中 仍走同人同因幂等(返回原工单),rejected / cancelled 放行再次发起 - after_sale_state() 的 can_apply 同步收紧,与 can_reapply 同一口径: 有工单时只有 rejected / cancelled 才放行,completed 不再漏过去 - 界面上售后详细弹窗的「再次申请」本就只对 rejected / cancelled 渲染, 这条改动把服务端与 API 口径补齐,堵住绕过前端直接打 REST 的口子 harness 补两个场景:completed 挡住而 rejected / cancelled 放行;待审核工单 同因幂等返回原单、换因挡住。 验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;after-sale-log-harness 7/7(新增两场景);dev harness 渲染两个后台页面与 unchecked 处理场景均正常。 未做浏览器视觉验收。
555 lines
25 KiB
PHP
555 lines
25 KiB
PHP
<?php
|
|
/**
|
|
* Front-end knowledge-payment order view model.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Payment;
|
|
|
|
use AKSRT\Core\DomainModel;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class UserOrders {
|
|
|
|
private static $instance = null;
|
|
|
|
/** @var array<string,string> */
|
|
private array $post_title_cache = [];
|
|
|
|
/** @var array<string,string> */
|
|
private array $post_image_cache = [];
|
|
|
|
/** @var array<string,string> */
|
|
private array $type_labels_cache = [];
|
|
|
|
public static function get_instance(): self {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $args
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function get_order_center_view(int $user_id, array $args = []): array {
|
|
$query = OrderCenterQuery::from_array($args);
|
|
$empty = $this->empty_view($query);
|
|
if ($user_id <= 0 || !class_exists(OrderRepository::class)) {
|
|
return $empty;
|
|
}
|
|
|
|
$repo_args = $query->repository_args();
|
|
$repo = OrderRepository::get_instance();
|
|
$orders = $repo->get_user_orders($user_id, $repo_args);
|
|
$summary = $repo->get_user_order_center_summary($user_id, $repo_args);
|
|
$total = (int) ($summary['total'] ?? 0);
|
|
$total_pages = max(1, (int) ceil($total / $query->per_page()));
|
|
$page = min($query->page(), $total_pages);
|
|
|
|
if ($page !== $query->page() && $total > 0) {
|
|
$repo_args['offset'] = ($page - 1) * $query->per_page();
|
|
$orders = $repo->get_user_orders($user_id, $repo_args);
|
|
}
|
|
|
|
return [
|
|
'items' => $this->decorate_order_views($orders),
|
|
'total' => $total,
|
|
'total_pages' => $total_pages,
|
|
'page' => $page,
|
|
'query' => array_merge($query->url_values(), ['order_page' => $page]),
|
|
'stats' => is_array($summary['stats'] ?? null) ? $summary['stats'] : ['total' => 0, 'pending' => 0, 'paid' => 0],
|
|
'invalidation_tags' => ['orders'],
|
|
'product_types' => $this->product_type_filter_options(),
|
|
];
|
|
}
|
|
|
|
/** @return array<string, mixed>|null */
|
|
public function get_order_detail_view(int $user_id, string $order_number, bool $reconcile = false): ?array {
|
|
if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) {
|
|
return null;
|
|
}
|
|
|
|
$order = OrderRepository::get_instance()->get_user_order($user_id, $order_number);
|
|
if (!$order) {
|
|
return null;
|
|
}
|
|
|
|
$reconciliation = [
|
|
'state' => 'not_needed',
|
|
'required' => false,
|
|
];
|
|
if (class_exists(Gateway::class)) {
|
|
$gateway = Gateway::get_instance();
|
|
$required = $gateway->needs_reconciliation($order);
|
|
if ($required && $reconcile) {
|
|
$order = $gateway->reconcile_order($order_number, $order) ?: $order;
|
|
$required = $gateway->needs_reconciliation($order);
|
|
$reconciliation['state'] = $required ? 'refresh_available' : 'checked';
|
|
} elseif ($required) {
|
|
$reconciliation['state'] = 'refresh_available';
|
|
}
|
|
$reconciliation['required'] = $required;
|
|
}
|
|
|
|
return $this->decorate_order_view($order, true, $reconciliation);
|
|
}
|
|
|
|
/** @return array<string, mixed>|null */
|
|
public function refresh_order(int $user_id, string $order_number): ?array {
|
|
return $this->get_order_detail_view($user_id, $order_number, true);
|
|
}
|
|
|
|
/** @return array<string, int> */
|
|
public function get_user_order_stats(int $user_id): array {
|
|
if ($user_id <= 0 || !class_exists(OrderRepository::class)) {
|
|
return ['total' => 0, 'pending' => 0, 'paid' => 0];
|
|
}
|
|
|
|
$summary = OrderRepository::get_instance()->get_user_order_center_summary($user_id);
|
|
return is_array($summary['stats'] ?? null)
|
|
? $summary['stats']
|
|
: ['total' => 0, 'pending' => 0, 'paid' => 0];
|
|
}
|
|
|
|
/** @return array<string, string> */
|
|
public function product_type_filter_options(): array {
|
|
if ($this->type_labels_cache === []) {
|
|
$this->type_labels_cache = DomainModel::order_product_type_labels();
|
|
}
|
|
return array_merge(['' => __('全部类型', 'aksrt')], $this->type_labels_cache);
|
|
}
|
|
|
|
/**
|
|
* Prime post and post-meta caches for all resource rows before decoration.
|
|
*
|
|
* @param array<int,array<string,mixed>> $orders
|
|
*/
|
|
private function prime_resource_caches(array $orders): void {
|
|
$post_ids = [];
|
|
foreach ($orders as $order) {
|
|
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
|
foreach ([(int) ($order['product_id'] ?? 0), (int) ($meta['post_id'] ?? 0)] as $post_id) {
|
|
if ($post_id > 0) {
|
|
$post_ids[$post_id] = $post_id;
|
|
}
|
|
}
|
|
}
|
|
if ($post_ids === []) {
|
|
return;
|
|
}
|
|
|
|
_prime_post_caches(array_values($post_ids), false, true);
|
|
$thumbnail_ids = [];
|
|
foreach ($post_ids as $post_id) {
|
|
$thumbnail_id = (int) get_post_thumbnail_id($post_id);
|
|
if ($thumbnail_id > 0) {
|
|
$thumbnail_ids[$thumbnail_id] = $thumbnail_id;
|
|
}
|
|
}
|
|
if ($thumbnail_ids !== []) {
|
|
_prime_post_caches(array_values($thumbnail_ids), false, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<int,array<string,mixed>> $orders
|
|
* @return array<int,array<string,mixed>>
|
|
*/
|
|
private function decorate_order_views(array $orders): array {
|
|
$this->prime_resource_caches($orders);
|
|
return array_map(fn(array $order): array => $this->decorate_order_view($order, false), $orders);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $order
|
|
* @param array<string, mixed>|null $reconciliation
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function decorate_order_view(array $order, bool $detail = true, ?array $reconciliation = null): array {
|
|
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
|
$product_type = DomainModel::resolve_order_product_type(sanitize_key((string) ($order['product_type'] ?? '')), $meta);
|
|
$product_id = (int) ($order['product_id'] ?? 0);
|
|
$status = sanitize_key((string) ($order['status'] ?? Order::STATUS_PENDING));
|
|
$currency = sanitize_key((string) ($order['currency'] ?? 'CNY'));
|
|
$is_points = $currency === 'points'
|
|
|| sanitize_key((string) ($order['payment_method'] ?? '')) === 'points'
|
|
|| sanitize_key((string) ($meta['pay_modo'] ?? '')) === 'points';
|
|
if ($this->type_labels_cache === []) {
|
|
$this->type_labels_cache = DomainModel::order_product_type_labels();
|
|
}
|
|
$type_label = $this->type_labels_cache[$product_type] ?? __('订单', 'aksrt');
|
|
$title = $this->resolve_title($meta, $product_type, $product_id, $type_label);
|
|
$image = $this->resolve_image($meta, $product_id);
|
|
$group = $this->status_group($status);
|
|
$payment_method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
|
// 卡密核销不发生任何真实支付(现金在售卡时已确认),实付恒为 0,
|
|
// 视图层统一归零以覆盖历史行(历史数据不做迁移)。
|
|
$amount = (string) ($order['amount'] ?? '0.00');
|
|
if ($payment_method === 'card_pass') {
|
|
$amount = $is_points ? '0' : '0.00';
|
|
}
|
|
$can_pay = $group === OrderCenterQuery::STATUS_PENDING;
|
|
$can_cancel = $group === OrderCenterQuery::STATUS_PENDING;
|
|
$order_number = (string) ($order['order_number'] ?? '');
|
|
$after_sale_service = class_exists(AfterSaleService::class) ? AfterSaleService::get_instance() : null;
|
|
$after_sales = $detail && $after_sale_service ? $after_sale_service->list_for_order($order_number) : [];
|
|
$after_sale_state = $this->after_sale_state($group, $order_number, (string) ($order['paid_at'] ?? ''), $after_sale_service, $after_sales);
|
|
$updated_at = $this->resolve_updated_at($order);
|
|
|
|
$view = [
|
|
'id' => (int) ($order['id'] ?? 0),
|
|
'order_number' => $order_number,
|
|
'amount' => $amount,
|
|
'currency' => $currency,
|
|
'currency_symbol' => $is_points
|
|
? (class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt'))
|
|
: '¥',
|
|
'is_points' => $is_points,
|
|
'status' => $status,
|
|
'status_group' => $group,
|
|
'status_label' => $this->status_label($group),
|
|
'product_type' => $product_type,
|
|
'product_type_label' => $type_label,
|
|
'product_id' => $product_id,
|
|
'product_name' => $title,
|
|
'image' => $image,
|
|
'created_at' => (string) ($order['created_at'] ?? ''),
|
|
'payment_method' => [
|
|
'value' => $payment_method,
|
|
'label' => $this->payment_method_label($payment_method),
|
|
],
|
|
'allowed_actions' => [
|
|
'can_pay' => $can_pay,
|
|
'can_cancel' => $can_cancel,
|
|
],
|
|
'after_sale' => $after_sale_state,
|
|
'time_left' => $this->get_order_countdown((string) ($order['expires_at'] ?? ''), $status),
|
|
'updated_at' => $updated_at,
|
|
'status_version' => $this->resolve_status_version($order, $updated_at),
|
|
'invalidation_tags' => array_values(array_filter([
|
|
'orders',
|
|
$order_number !== '' ? 'order:' . sanitize_key($order_number) : '',
|
|
])),
|
|
];
|
|
if ($detail) {
|
|
// 前台订单详情只暴露统一流水号(order_number);账本流水号为后台口径,
|
|
// 不下发到前台,避免同一单据出现两个对外号码。
|
|
$view['paid_at'] = (string) ($order['paid_at'] ?? '');
|
|
$view['expires_at'] = (string) ($order['expires_at'] ?? '');
|
|
$view['reconciliation'] = $reconciliation ?: ['state' => 'not_needed', 'required' => false];
|
|
$view['after_sales'] = $after_sales;
|
|
}
|
|
return $view;
|
|
}
|
|
|
|
/**
|
|
* 售后入口状态——前台「申请售后 / 售后详细」按钮与三个弹窗的唯一数据源。
|
|
*
|
|
* 列表模式只拉最新一条工单(latest_for_order);详情模式直接复用已取到的
|
|
* 全量工单(list_for_order 按 id DESC,首条即最新)。
|
|
*
|
|
* @param array<int,array<string,mixed>> $records
|
|
* @return array<string,mixed>
|
|
*/
|
|
private function after_sale_state(string $group, string $order_number, string $paid_at, ?AfterSaleService $service, array $records = []): array {
|
|
$settings = class_exists('\AKSRT\Settings\PaymentRuntime')
|
|
? \AKSRT\Settings\PaymentRuntime::get_after_sale_settings()
|
|
: [];
|
|
$settings = is_array($settings) ? $settings : [];
|
|
|
|
$open_statuses = ['pending', 'processing'];
|
|
$latest = null;
|
|
$open = false;
|
|
if ($service !== null) {
|
|
if ($records) {
|
|
foreach ($records as $record) {
|
|
if ($latest === null) {
|
|
$latest = $record;
|
|
}
|
|
if (in_array((string) ($record['status'] ?? ''), $open_statuses, true)) {
|
|
$open = true;
|
|
}
|
|
}
|
|
} elseif ($order_number !== '') {
|
|
$latest = $service->latest_for_order($order_number);
|
|
$open = $latest !== null && in_array((string) ($latest['status'] ?? ''), $open_statuses, true);
|
|
}
|
|
}
|
|
|
|
// 售后只对已支付订单开放;开关关闭时整体不处理售后。
|
|
$enabled = $group === OrderCenterQuery::STATUS_PAID && !empty($settings['enabled']);
|
|
$expired = $enabled && class_exists('\AKSRT\Settings\PaymentRuntime')
|
|
&& \AKSRT\Settings\PaymentRuntime::after_sale_expired($paid_at);
|
|
$has_application = $latest !== null;
|
|
|
|
// 可再次申请售后的状态判定(按工单状态推理,不再以「不能再申请」为常态):
|
|
// - pending / processing:工单仍在流转(待审核 / 处理中),不允许重复申请;
|
|
// - rejected:审核未通过,允许补充资料后再次申请;
|
|
// - cancelled:用户自行取消,允许重新发起;
|
|
// - completed:退款已完成,工单终结,不允许再次申请。
|
|
$reapplyable_statuses = [AfterSaleService::STATUS_REJECTED, AfterSaleService::STATUS_CANCELLED];
|
|
$latest_status = (string) ($latest['status'] ?? '');
|
|
// 有工单时只有 rejected / cancelled 才放行再次申请:completed 的工单已终结,
|
|
// 订单有没有另标「已退款」都不影响——AfterSaleService::create() 按同一口径拦截。
|
|
$reapplyable = !$has_application || in_array($latest_status, $reapplyable_statuses, true);
|
|
|
|
return [
|
|
'enabled' => $enabled,
|
|
'expired' => (bool) $expired,
|
|
'has_application' => $has_application,
|
|
'open' => $open,
|
|
'can_apply' => $enabled && !$expired && !$open && $reapplyable,
|
|
'can_reapply' => $enabled && !$expired && $has_application && in_array($latest_status, $reapplyable_statuses, true),
|
|
'latest' => $has_application ? [
|
|
'status' => (string) ($latest['status'] ?? ''),
|
|
'reason' => (string) ($latest['reason'] ?? ''),
|
|
'apply_form' => is_array($latest['apply_form'] ?? null) ? $latest['apply_form'] : null,
|
|
'images' => array_values((array) ($latest['images'] ?? [])),
|
|
'refund_amount' => (string) ($latest['refund_amount'] ?? '0'),
|
|
'created_at' => (string) ($latest['created_at'] ?? ''),
|
|
// 处理记录:每次状态提交一条(状态 + 备注 + 来源 + 时间),前台时间线的唯一数据源。
|
|
'handling_log' => array_values((array) ($latest['handling_log'] ?? [])),
|
|
] : null,
|
|
];
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private function empty_view(OrderCenterQuery $query): array {
|
|
return [
|
|
'items' => [],
|
|
'total' => 0,
|
|
'total_pages' => 1,
|
|
'page' => 1,
|
|
'query' => $query->url_values(),
|
|
'stats' => ['total' => 0, 'pending' => 0, 'paid' => 0, 'refunded' => 0, 'closed' => 0, 'after_sale_pending' => 0],
|
|
'invalidation_tags' => ['orders'],
|
|
'product_types' => $this->product_type_filter_options(),
|
|
];
|
|
}
|
|
|
|
/** @param array<string,mixed> $order */
|
|
private function resolve_updated_at(array $order): string {
|
|
$latest = '';
|
|
$latest_timestamp = 0;
|
|
foreach (['created_at', 'paid_at', 'cancelled_at', 'refunded_at'] as $field) {
|
|
$value = trim((string) ($order[$field] ?? ''));
|
|
if ($value === '') {
|
|
continue;
|
|
}
|
|
$timestamp = strtotime($value);
|
|
if ($timestamp !== false && $timestamp >= $latest_timestamp) {
|
|
$latest_timestamp = $timestamp;
|
|
$latest = $value;
|
|
}
|
|
}
|
|
return $latest;
|
|
}
|
|
|
|
/** @param array<string,mixed> $order */
|
|
private function resolve_status_version(array $order, string $updated_at): string {
|
|
return substr(hash('sha256', implode('|', [
|
|
(int) ($order['id'] ?? 0),
|
|
(string) ($order['order_number'] ?? ''),
|
|
sanitize_key((string) ($order['status'] ?? '')),
|
|
$updated_at,
|
|
])), 0, 16);
|
|
}
|
|
|
|
private function status_group(string $status): string {
|
|
return isset(DomainModel::order_status_labels()[$status]) ? $status : '';
|
|
}
|
|
|
|
private function status_label(string $group): string {
|
|
return DomainModel::order_status_labels()[$group] ?? __('未知状态', 'aksrt');
|
|
}
|
|
|
|
/** @param array<string, mixed> $meta */
|
|
private function resolve_title(array $meta, string $type, int $product_id, string $fallback): string {
|
|
// 卡密核销订单:按 note.meta 生成可读名称(余额/积分卡记面额,会员卡记档位与时长)。
|
|
if (!empty($meta['card_pass'])) {
|
|
$title = $this->resolve_card_pass_title($meta);
|
|
if ($title !== '') {
|
|
return $title;
|
|
}
|
|
}
|
|
foreach (['product_title', 'title', 'package_title'] as $key) {
|
|
if (!empty($meta[$key]) && is_string($meta[$key])) {
|
|
return (string) $meta[$key];
|
|
}
|
|
}
|
|
if ($type === DomainModel::ORDER_PRODUCT_VIP) {
|
|
return __('VIP 会员', 'aksrt');
|
|
}
|
|
if ($type === DomainModel::ORDER_PRODUCT_RECHARGE) {
|
|
return __('余额充值', 'aksrt');
|
|
}
|
|
if ($type === DomainModel::ORDER_PRODUCT_POINTS) {
|
|
return __('积分购买', 'aksrt');
|
|
}
|
|
if ($product_id > 0) {
|
|
$cache_key = (string) $product_id;
|
|
if (!array_key_exists($cache_key, $this->post_title_cache)) {
|
|
$title = get_the_title($product_id);
|
|
$this->post_title_cache[$cache_key] = is_string($title) ? $title : '';
|
|
}
|
|
if ($this->post_title_cache[$cache_key] !== '') {
|
|
return $this->post_title_cache[$cache_key];
|
|
}
|
|
}
|
|
return $fallback;
|
|
}
|
|
|
|
/**
|
|
* 卡密核销订单名称:
|
|
* - 余额卡:卡密兑换-余额充值{面额}
|
|
* - 积分卡:卡密兑换-积分充值{点数}
|
|
* - 会员卡:购入[{会员档位名}]{N}天(永久卡为「永久」)
|
|
*
|
|
* @param array<string, mixed> $meta
|
|
*/
|
|
private function resolve_card_pass_title(array $meta): string {
|
|
$card_meta = is_array($meta['meta'] ?? null) ? $meta['meta'] : [];
|
|
$card_type = sanitize_key((string) ($meta['card_type'] ?? ''));
|
|
if ($card_type === ChargeCard::TYPE_BALANCE) {
|
|
$amount = (string) ($card_meta['amount'] ?? '');
|
|
if (str_contains($amount, '.')) {
|
|
$amount = rtrim(rtrim($amount, '0'), '.');
|
|
}
|
|
return __('卡密兑换-余额充值', 'aksrt') . $amount;
|
|
}
|
|
if ($card_type === ChargeCard::TYPE_CREDITS) {
|
|
return __('卡密兑换-积分充值', 'aksrt') . (string) max(0, (int) ($card_meta['points'] ?? 0));
|
|
}
|
|
if ($card_type === ChargeCard::TYPE_VIP) {
|
|
$level = max(1, min(2, (int) ($card_meta['level'] ?? 1)));
|
|
$days = max(0, (int) ($card_meta['days'] ?? 0));
|
|
$levels = class_exists('\AKSRT\Settings\Settings')
|
|
? (\AKSRT\Settings\Settings::get_payment_vip_settings()['levels'] ?? [])
|
|
: [];
|
|
$level_name = is_array($levels) ? (string) ($levels[$level]['name'] ?? '') : '';
|
|
if ($level_name === '') {
|
|
$level_name = 'VIP' . $level;
|
|
}
|
|
$suffix = $days > 0 ? sprintf(__('%d天', 'aksrt'), $days) : __('永久', 'aksrt');
|
|
return sprintf(__('购入[%1$s]%2$s', 'aksrt'), $level_name, $suffix);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/** @param array<string, mixed> $meta */
|
|
private function resolve_image(array $meta, int $product_id): string {
|
|
if (!empty($meta['thumb']) && is_string($meta['thumb'])) {
|
|
return esc_url_raw($meta['thumb']);
|
|
}
|
|
if ($product_id > 0) {
|
|
$cache_key = (string) $product_id;
|
|
if (!array_key_exists($cache_key, $this->post_image_cache)) {
|
|
$image = has_post_thumbnail($product_id)
|
|
? get_the_post_thumbnail_url($product_id, 'thumbnail')
|
|
: '';
|
|
$this->post_image_cache[$cache_key] = is_string($image) ? $image : '';
|
|
}
|
|
return $this->post_image_cache[$cache_key];
|
|
}
|
|
return '';
|
|
}
|
|
|
|
private function payment_method_label(string $method): string {
|
|
$labels = [
|
|
'balance' => __('站内余额', 'aksrt'),
|
|
'points' => __('积分支付', 'aksrt'),
|
|
'alipay' => __('支付宝', 'aksrt'),
|
|
'wechatpay' => __('微信支付', 'aksrt'),
|
|
'paypal' => 'PayPal',
|
|
'stripe' => 'Stripe',
|
|
];
|
|
return $labels[$method] ?? ($method !== '' ? $method : __('—', 'aksrt'));
|
|
}
|
|
|
|
private function get_order_countdown(string $expires_at, string $status): int {
|
|
if ($status !== Order::STATUS_PENDING || $expires_at === '') {
|
|
return 0;
|
|
}
|
|
$expires = strtotime($expires_at);
|
|
return $expires ? max(0, $expires - current_time('timestamp')) : 0;
|
|
}
|
|
|
|
/** @return array{success:bool,message?:string,data?:array} */
|
|
public function cancel_order(int $user_id, string $order_number): array {
|
|
if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) {
|
|
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
|
}
|
|
$repo = OrderRepository::get_instance();
|
|
$order = $repo->get_user_order($user_id, $order_number);
|
|
if (!$order || $this->status_group((string) ($order['status'] ?? '')) !== OrderCenterQuery::STATUS_PENDING) {
|
|
return ['success' => false, 'message' => __('当前订单不可关闭。', 'aksrt')];
|
|
}
|
|
if (!$repo->update_status($order_number, Order::STATUS_CLOSED, [
|
|
'note' => __('用户在前台关闭订单。', 'aksrt'),
|
|
])) {
|
|
return ['success' => false, 'message' => __('关闭订单失败,请稍后重试。', 'aksrt')];
|
|
}
|
|
return ['success' => true, 'data' => ['order' => $this->get_order_detail_view($user_id, $order_number)]];
|
|
}
|
|
|
|
/** @return array{success:bool,message?:string,data?:array} */
|
|
public function pay_order(int $user_id, string $order_number, string $payment_method = ''): array {
|
|
if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) {
|
|
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
|
}
|
|
$repo = OrderRepository::get_instance();
|
|
$order = $repo->get_user_order($user_id, $order_number);
|
|
if (!$order) {
|
|
return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')];
|
|
}
|
|
if ($this->status_group((string) ($order['status'] ?? '')) === OrderCenterQuery::STATUS_PAID) {
|
|
return ['success' => true, 'data' => [
|
|
'redirect' => false,
|
|
'payment_url' => '',
|
|
'order_number' => $order_number,
|
|
'order' => $this->get_order_detail_view($user_id, $order_number),
|
|
'replayed' => true,
|
|
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
|
'message' => __('支付成功。', 'aksrt'),
|
|
]];
|
|
}
|
|
if ($this->status_group((string) ($order['status'] ?? '')) !== OrderCenterQuery::STATUS_PENDING) {
|
|
return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')];
|
|
}
|
|
$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')];
|
|
}
|
|
$manager = class_exists(Gateway::class) ? Gateway::get_instance() : null;
|
|
$gateway = $manager ? $manager->get_gateway($payment_method) : null;
|
|
if (!$gateway || (method_exists($gateway, 'is_configured') && !$gateway->is_configured())) {
|
|
return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')];
|
|
}
|
|
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
|
|
$order['payment_method'] = $payment_method;
|
|
$result = PaymentOrchestrator::pay($gateway, $order, ['paid_note' => __('用户在前台完成支付。', 'aksrt')]);
|
|
if (empty($result['success'])) {
|
|
return [
|
|
'success' => false,
|
|
'code' => (string) ($result['code'] ?? 'aksrt_pay_failed'),
|
|
'status' => (int) ($result['status'] ?? 400),
|
|
'message' => (string) ($result['message'] ?? __('支付失败。', 'aksrt')),
|
|
];
|
|
}
|
|
return ['success' => true, 'data' => [
|
|
'redirect' => !empty($result['redirect']),
|
|
'payment_url' => (string) ($result['payment_url'] ?? ''),
|
|
'order_number' => $order_number,
|
|
'order' => empty($result['redirect']) ? $this->get_order_detail_view($user_id, $order_number) : null,
|
|
'replayed' => !empty($result['replayed']),
|
|
'balance' => $payment_method === 'balance' ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : null,
|
|
'message' => (string) ($result['message'] ?? ''),
|
|
]];
|
|
}
|
|
}
|