Files
wordpress-theme-aksrt/inc/classes/content/content-pay-traits/trait-aksrt-content-pay-orders.php
T
kate522 9c309b4c7f refactor(classes): split oversize modules + bootstrap hygiene
- Trait-split content-pay, vip-service, balance-service, order-repository
- Procedural split of user module into user-parts/*
- Split scripts enqueue_public into part-1b/1c; settings bulk trait
- Bootstrap: explicit notifications, widgets glob-only, home-modules init
- ADR: freeze domain placement for pay/revenue/notify boundaries
2026-07-17 11:42:29 +08:00

548 lines
22 KiB
PHP

<?php
/**
* Extracted trait: AKSRT_Content_Pay_Orders
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Content_Pay_Orders {
/**
* Bump content sales when a resource order is paid.
*
* @param string $order_number
* @param array $paid_order
*/
public function on_order_paid(string $order_number, array $paid_order): void {
$product_type = sanitize_key((string) ($paid_order['product_type'] ?? ''));
if ($product_type !== AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE) {
return;
}
$post_id = (int) ($paid_order['product_id'] ?? 0);
if ($post_id <= 0) {
return;
}
// Only count normal posts (content pay), not necessarily forum topics.
$post = get_post($post_id);
if (!($post instanceof WP_Post) || $post->post_type !== 'post') {
return;
}
if (function_exists('aksrt_bump_content_sales_volume')) {
aksrt_bump_content_sales_volume($post_id, 1);
} else {
$key = AKSRT_Domain_Model::RESOURCE_SALES_VOLUME_KEY;
$cur = (int) get_post_meta($post_id, $key, true);
update_post_meta($post_id, $key, $cur + 1);
}
// Guest cookie unlock (user_id=0).
$uid = (int) ($paid_order['user_id'] ?? 0);
if ($uid <= 0 && function_exists('aksrt_set_guest_content_pay_cookies')) {
// Ensure full order row for token material.
if (class_exists('AKSRT_Order_Repository')) {
$full = AKSRT_Order_Repository::get_instance()->get($order_number);
if (is_array($full)) {
$paid_order = array_merge($full, $paid_order);
}
}
aksrt_set_guest_content_pay_cookies($paid_order);
}
// Consume coupon + write affiliate commission (cash content only; idempotent).
$this->finalize_content_order_side_effects($order_number, $paid_order);
}
/**
* On paid: burn coupon (if any) and create affiliate rebate log.
*/
private function finalize_content_order_side_effects(string $order_number, array $order): void {
$meta = $this->decode_order_note_meta($order);
if (empty($meta['content_pay'])) {
return;
}
// Points: no coupon burn / no cash commission.
if (($meta['pay_modo'] ?? '') === 'points') {
return;
}
$buyer_id = (int) ($order['user_id'] ?? 0);
// Coupon burn (shared helper; idempotent via coupon_usage.order_number).
if (function_exists('aksrt_burn_order_coupon')) {
aksrt_burn_order_coupon($order_number, $meta, $buyer_id);
}
// Affiliate commission (does not reduce buyer pay).
$referrer_id = (int) ($meta['referrer_id'] ?? 0);
$rebate_price = round((float) ($meta['rebate_price'] ?? 0), 2);
$pay_amount = round((float) ($order['amount'] ?? $meta['payable'] ?? 0), 2);
if ($referrer_id <= 0 || $rebate_price <= 0 || $pay_amount <= 0) {
return;
}
if (!class_exists('AKSRT_Rebate')) {
return;
}
if ($this->rebate_log_exists($order_number, $referrer_id)) {
return;
}
AKSRT_Rebate::get_instance()->create([
'user_id' => $referrer_id,
'order_number' => $order_number,
'source_user_id' => $buyer_id,
'amount' => $rebate_price,
'type' => AKSRT_Rebate::TYPE_ORDER,
'note' => sprintf(
/* translators: 1: order number 2: post id */
__('内容推广返佣 订单 %1$s 内容 #%2$d', 'aksrt'),
$order_number,
(int) ($order['product_id'] ?? $meta['post_id'] ?? 0)
),
]);
}
/**
* @param array<string,mixed> $order
* @return array<string,mixed>
*/
private function decode_order_note_meta(array $order): array {
if (is_array($order['meta_payload'] ?? null) && $order['meta_payload'] !== []) {
return $order['meta_payload'];
}
$note = (string) ($order['note'] ?? '');
if ($note === '') {
return [];
}
$decoded = json_decode($note, true);
return is_array($decoded) ? $decoded : [];
}
private function rebate_log_exists(string $order_number, int $referrer_id): bool {
global $wpdb;
$table = AKSRT_DB_Migrator::table('rebate_log');
if ($table === '' || $order_number === '') {
return false;
}
$found = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$table} WHERE order_number = %s AND user_id = %d LIMIT 1",
$order_number,
$referrer_id
));
return !empty($found);
}
/**
* Points one-shot purchase → permanent resource order.
*
* @return array|WP_Error
*/
public function create_and_pay_points(int $post_id, int $user_id, string $price_type = '') {
$assert = $this->assert_purchasable($post_id, $user_id, $price_type);
if (is_wp_error($assert)) {
return $assert;
}
$priced = $assert['priced'];
if ((($priced['modo'] ?? '0') !== 'points')) {
return new WP_Error('aksrt_wrong_modo', __('该内容不支持积分支付。', 'aksrt'), ['status' => 400]);
}
$amount = (int) ceil((float) ($priced['amount'] ?? 0));
if ($amount <= 0) {
return [
'success' => true,
'data' => [
'free' => true,
'message' => __('无需支付,已可访问。', 'aksrt'),
'reload' => true,
],
];
}
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Credits')) {
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
}
$balance = AKSRT_Credits::balance($user_id);
if ($balance < $amount) {
return new WP_Error('aksrt_credits_insufficient', __('积分不足,暂时无法支付。', 'aksrt'), [
'status' => 402,
'balance' => $balance,
'cost' => $amount,
]);
}
$note = wp_json_encode([
'content_pay' => 1,
'pay_modo' => 'points',
'post_id' => $post_id,
'price_type' => $price_type,
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => (float) ($priced['base'] ?? $amount),
'amount' => $amount,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => $user_id,
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
'product_id' => $post_id,
'amount' => (float) $amount,
'currency' => 'points',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => 'points',
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
}
$deducted = AKSRT_Credits::deduct(
$user_id,
$amount,
'content_points_pay',
['type' => 'post', 'id' => $post_id, 'order' => $order_number],
sprintf(__('积分购买《%s》', 'aksrt'), get_the_title($post_id))
);
if (is_wp_error($deducted) || $deducted === false) {
AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [
'note' => __('积分扣除失败,自动取消', 'aksrt'),
]);
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败。', 'aksrt');
return new WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败。', 'aksrt'), ['status' => 400]);
}
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => 'points',
'payment_transaction_id' => 'points_' . $order_number,
'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount),
]);
if (!$paid) {
AKSRT_Credits::add(
$user_id,
$amount,
'content_points_pay_refund',
['type' => 'post', 'id' => $post_id, 'order' => $order_number],
sprintf(__('购买失败退款:%s', 'aksrt'), get_the_title($post_id))
);
return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败,已尝试退回积分。', 'aksrt'), ['status' => 500]);
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => $amount,
'pay_modo' => 'points',
'balance' => AKSRT_Credits::balance($user_id),
'message' => __('支付成功', 'aksrt'),
'reload' => true,
],
];
}
/**
* Cash order create + balance/gateway pay.
*
* @return array|WP_Error
*/
public function create_cash_order(int $post_id, int $user_id, string $payment_method = 'balance', string $price_type = '', string $coupon_code = '') {
$assert = $this->assert_purchasable($post_id, $user_id, $price_type);
if (is_wp_error($assert)) {
return $assert;
}
$post = $assert['post'];
$meta = $assert['meta'];
$priced = $assert['priced'];
if ((($priced['modo'] ?? '0') === 'points')) {
return new WP_Error('aksrt_wrong_modo', __('该内容仅支持积分支付。', 'aksrt'), ['status' => 400]);
}
// Card pass cannot stack coupon (zibll). Content cashier does not sell via card_pass.
$payment_method = sanitize_key($payment_method);
if ($payment_method === 'card_pass') {
if (!function_exists('aksrt_is_content_card_pass_pay_allowed') || !aksrt_is_content_card_pass_pay_allowed($post)) {
return new WP_Error(
'aksrt_card_pass_not_allowed',
__('内容购买不支持卡密支付,请在用户中心使用卡密兑换余额/会员/积分。', 'aksrt'),
['status' => 400]
);
}
$coupon_code = '';
}
$discounted = $this->apply_cash_discounts($priced, $post, $meta, $user_id, $coupon_code, false);
if (is_wp_error($discounted)) {
return $discounted;
}
$amount = round((float) ($discounted['payable'] ?? 0), 2);
$list_amount = round((float) ($discounted['list_amount'] ?? $amount), 2);
$rebate_discount = round((float) ($discounted['rebate_discount'] ?? 0), 2);
$coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2);
$coupon_meta = is_array($discounted['coupon'] ?? null) ? $discounted['coupon'] : [];
$referrer_id = (int) ($discounted['referrer_id'] ?? 0);
$rebate_price = round((float) ($discounted['rebate_price'] ?? 0), 2);
if ($amount <= 0) {
// Free after discount (or free content): create 0-paid order for access + burn coupon.
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
}
$note = wp_json_encode([
'content_pay' => 1,
'pay_modo' => '0',
'post_id' => $post_id,
'price_type' => $price_type,
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => (float) ($priced['base'] ?? $list_amount),
'list_amount' => $list_amount,
'rebate_discount' => $rebate_discount,
'coupon_discount' => $coupon_discount,
'discount' => $coupon_discount,
'coupon' => $coupon_meta,
'coupon_code' => (string) ($coupon_meta['code'] ?? $coupon_code),
'amount' => 0,
'payable' => 0,
'referrer_id' => $referrer_id,
'rebate_price' => 0,
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
'guest' => $user_id <= 0 ? 1 : 0,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => max(0, $user_id),
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
'product_id' => $post_id,
'amount' => 0.0,
'currency' => 'CNY',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => 'free',
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
}
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => 'free',
'payment_transaction_id' => 'free_' . $order_number,
'note' => __('优惠后无需支付。', 'aksrt'),
]);
if (!$paid) {
return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => 0,
'pay_modo' => '0',
'free' => true,
'message' => __('无需支付,已可访问。', 'aksrt'),
'reload' => true,
],
];
}
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
}
if ($payment_method === '') {
$payment_method = $user_id > 0 ? 'balance' : 'alipay';
}
if ($user_id <= 0 && in_array($payment_method, ['balance', 'points'], true)) {
$payment_method = 'alipay';
}
$coupon_code_norm = (string) ($coupon_meta['code'] ?? $coupon_code);
// Resume existing pending content order for this post when amount + coupon match (logged-in only).
$pending = $user_id > 0 ? $this->find_pending_content_order($user_id, $post_id) : null;
$can_resume = false;
if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) {
$pending_meta = $this->decode_order_note_meta($pending);
$pending_coupon = (string) ($pending_meta['coupon_code'] ?? '');
if (is_array($pending_meta['coupon'] ?? null)) {
$pending_coupon = (string) ($pending_meta['coupon']['code'] ?? $pending_coupon);
}
if ($pending_coupon === $coupon_code_norm) {
$can_resume = true;
}
}
if ($can_resume) {
$order_number = (string) ($pending['order_number'] ?? '');
$order = $pending;
} else {
$note = wp_json_encode([
'content_pay' => 1,
'pay_modo' => '0',
'post_id' => $post_id,
'price_type' => $price_type,
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => (float) ($priced['base'] ?? $list_amount),
'list_amount' => $list_amount,
'rebate_discount' => $rebate_discount,
'coupon_discount' => $coupon_discount,
'discount' => $coupon_discount,
'coupon' => $coupon_meta,
'coupon_code' => $coupon_code_norm,
'amount' => $amount,
'payable' => $amount,
'referrer_id' => $referrer_id,
'rebate_price' => $rebate_price,
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
'guest' => $user_id <= 0 ? 1 : 0,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => max(0, $user_id),
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
'product_id' => $post_id,
'amount' => $amount,
'currency' => 'CNY',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => $payment_method,
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
}
$order = AKSRT_Order_Repository::get_instance()->get($order_number);
if (!$order) {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
}
}
$payment_method = sanitize_key($payment_method ?: (string) ($order['payment_method'] ?? 'balance'));
if ($payment_method === '') {
$payment_method = $user_id > 0 ? 'balance' : 'alipay';
}
$order['subject'] = sprintf(
/* translators: %s: post title */
__('购买《%s》', 'aksrt'),
get_the_title($post_id)
);
$order['payment_method'] = $payment_method;
// Ensure gateway uses discounted amount.
$order['amount'] = $amount;
if (!class_exists('AKSRT_Payment_Gateway')) {
return new WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]);
}
$gateway_manager = AKSRT_Payment_Gateway::get_instance();
$gateway = $gateway_manager->get_gateway($payment_method);
if (!$gateway) {
return new WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用。', 'aksrt'), [
'status' => 400,
'order_number' => $order_number,
]);
}
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
return new WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [
'status' => 400,
'order_number' => $order_number,
]);
}
$payment_result = $gateway->create_payment($order);
if (empty($payment_result['success'])) {
$msg = (string) ($payment_result['message'] ?? __('支付失败。', 'aksrt'));
$code = ($payment_method === 'balance' && (str_contains($msg, '余额') || str_contains($msg, '不足')))
? 'aksrt_balance_insufficient'
: 'aksrt_pay_failed';
return new WP_Error($code, $msg, [
'status' => 402,
'order_number' => $order_number,
]);
}
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
$payment_url = (string) ($payment_result['payment_url'] ?? '');
if ($payment_url !== '' || $code_url !== '') {
return [
'success' => true,
'data' => [
'redirect' => true,
'payment_url' => $payment_url,
'qr_content' => $code_url,
'order_number' => $order_number,
'amount' => $amount,
'pay_modo' => '0',
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
'reload' => false,
'poll' => true,
],
];
}
// Sync gateway (balance): mark paid.
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => $payment_method,
'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')),
'note' => __('用户完成内容购买支付。', 'aksrt'),
]);
if (!$paid) {
return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => $amount,
'pay_modo' => '0',
'message' => $payment_result['message'] ?? __('支付成功', 'aksrt'),
'reload' => true,
],
];
}
/**
* @return array<string,mixed>|null
*/
private function find_pending_content_order(int $user_id, int $post_id): ?array {
if ($user_id <= 0 || $post_id <= 0 || !class_exists('AKSRT_Order_Repository')) {
return null;
}
$orders = AKSRT_Order_Repository::get_instance()->get_user_orders($user_id, [
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
'status' => AKSRT_Domain_Model::ORDER_STATUS_PENDING,
'limit' => 20,
'offset' => 0,
]);
foreach ($orders as $order) {
if ((int) ($order['product_id'] ?? 0) === $post_id) {
return $order;
}
}
return null;
}
}