积分与余额账务系统硬切重构
This commit is contained in:
@@ -124,7 +124,9 @@ class Download {
|
||||
$pay_modo = is_array($priced)
|
||||
? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0')
|
||||
: ((($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0');
|
||||
$display_price = is_array($priced) ? (float) ($priced['amount'] ?? 0) : (float) ($meta['price'] ?? 0);
|
||||
$display_price = is_array($priced)
|
||||
? ($priced['amount'] ?? ($pay_modo === 'points' ? 0 : '0.00'))
|
||||
: ($pay_modo === 'points' ? (int) ($meta['points_price'] ?? 0) : (string) ($meta['price'] ?? '0.00'));
|
||||
|
||||
$access = class_exists('AKSRT\\Content\\Visibility')
|
||||
? Visibility::get_post_access_state($post, $uid)
|
||||
@@ -135,14 +137,14 @@ class Download {
|
||||
return new \WP_REST_Response([
|
||||
'post_id' => $post_id,
|
||||
'price' => $display_price,
|
||||
'base_price' => is_array($priced) ? (float) ($priced['base'] ?? $display_price) : (float) ($meta['price'] ?? 0),
|
||||
'base_price' => is_array($priced) ? ($priced['base'] ?? $display_price) : $display_price,
|
||||
'pay_modo' => $pay_modo,
|
||||
'pay_limit' => (int) ($meta['pay_limit'] ?? 0),
|
||||
'cost' => $cost,
|
||||
'require_login' => !empty($access['need_login']),
|
||||
'require_captcha' => $need_captcha,
|
||||
'logged_in' => $uid > 0,
|
||||
'balance' => $uid > 0 && class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::balance($uid) : 0,
|
||||
'balance' => $uid > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($uid, \AKSRT\Ledger\AssetType::POINTS) : 0,
|
||||
'has_paid_access' => !empty($access['has_paid_access']),
|
||||
'need_purchase' => !empty($access['need_purchase']),
|
||||
'need_vip' => !empty($access['need_vip']),
|
||||
@@ -242,7 +244,7 @@ class Download {
|
||||
'source_type' => $source['type'] ?? 'direct',
|
||||
'has_extract' => $extract_code !== '',
|
||||
'extract_code' => $extract_code,
|
||||
'balance' => $uid > 0 && class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::balance($uid) : 0,
|
||||
'balance' => $uid > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($uid, \AKSRT\Ledger\AssetType::POINTS) : 0,
|
||||
], 200);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class DownloadState {
|
||||
* @return array{
|
||||
* state:string,can_download:bool,message:string,action_label:string,
|
||||
* action_url:string,cost:int,balance:int,login_url:string,pay_modo:string,
|
||||
* amount:float,need_purchase:bool,need_vip:bool,has_paid_access:bool
|
||||
* amount:int|string,need_purchase:bool,need_vip:bool,has_paid_access:bool
|
||||
* }
|
||||
*/
|
||||
public function resolve(int $post_id): array {
|
||||
@@ -72,13 +72,14 @@ class DownloadState {
|
||||
$pay_modo = is_array($priced)
|
||||
? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0')
|
||||
: '0';
|
||||
$amount = is_array($priced) ? (float) ($priced['amount'] ?? 0) : 0.0;
|
||||
$amount = is_array($priced)
|
||||
? ($priced['amount'] ?? ($pay_modo === 'points' ? 0 : '0.00'))
|
||||
: ($pay_modo === 'points' ? 0 : '0.00');
|
||||
$amount_minor = $this->amount_minor($amount, $pay_modo);
|
||||
$cost = function_exists('aksrt_resource_download_cost')
|
||||
? (int) aksrt_resource_download_cost($post_id)
|
||||
: 0;
|
||||
$balance = ($user_id > 0 && class_exists('\AKSRT\Credits\Credits'))
|
||||
? \AKSRT\Credits\Credits::balance($user_id)
|
||||
: 0;
|
||||
$balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS) : 0;
|
||||
|
||||
$gate = function_exists('aksrt_can_download') ? aksrt_can_download($post_id, $user_id) : true;
|
||||
if (is_wp_error($gate)) {
|
||||
@@ -130,9 +131,9 @@ class DownloadState {
|
||||
}
|
||||
|
||||
// need_purchase / legacy insufficient credits → cashier.
|
||||
if (in_array($code, ['aksrt_need_purchase', 'aksrt_credits_insufficient'], true)) {
|
||||
if (in_array($code, ['aksrt_need_purchase', 'aksrt_points_insufficient'], true)) {
|
||||
$label = $pay_modo === 'points'
|
||||
? sprintf(__('购买(%s 积分)', 'aksrt'), (string) (int) ceil($amount > 0 ? $amount : $cost))
|
||||
? sprintf(__('购买(%s 积分)', 'aksrt'), (string) ($amount_minor > 0 ? $amount_minor : $cost))
|
||||
: __('去购买', 'aksrt');
|
||||
return $this->build_result(
|
||||
self::STATE_NEED_PURCHASE,
|
||||
@@ -145,7 +146,7 @@ class DownloadState {
|
||||
$login_url,
|
||||
[
|
||||
'pay_modo' => $pay_modo,
|
||||
'amount' => $amount > 0 ? $amount : (float) $cost,
|
||||
'amount' => $amount_minor > 0 ? $amount : ($pay_modo === 'points' ? $cost : '0.00'),
|
||||
'need_purchase' => true,
|
||||
'need_vip' => false,
|
||||
'has_paid_access' => false,
|
||||
@@ -174,8 +175,8 @@ class DownloadState {
|
||||
|
||||
// Paid / free unlock.
|
||||
$has_paid = !empty($access['has_paid_access']);
|
||||
if ($has_paid || $amount <= 0) {
|
||||
$owned = $has_paid && $amount > 0;
|
||||
if ($has_paid || $amount_minor <= 0) {
|
||||
$owned = $has_paid && $amount_minor > 0;
|
||||
return $this->build_result(
|
||||
$owned ? self::STATE_OWNED : self::STATE_AVAILABLE,
|
||||
true,
|
||||
@@ -189,7 +190,7 @@ class DownloadState {
|
||||
$login_url,
|
||||
[
|
||||
'pay_modo' => $pay_modo,
|
||||
'amount' => 0.0,
|
||||
'amount' => $pay_modo === 'points' ? 0 : '0.00',
|
||||
'need_purchase' => false,
|
||||
'need_vip' => false,
|
||||
'has_paid_access' => true,
|
||||
@@ -241,10 +242,24 @@ class DownloadState {
|
||||
'balance' => $balance,
|
||||
'login_url' => $login_url,
|
||||
'pay_modo' => '0',
|
||||
'amount' => 0.0,
|
||||
'amount' => '0.00',
|
||||
'need_purchase' => false,
|
||||
'need_vip' => false,
|
||||
'has_paid_access' => false,
|
||||
], $extra);
|
||||
}
|
||||
|
||||
private function amount_minor($value, string $pay_modo): int {
|
||||
if ($pay_modo === 'points') {
|
||||
return is_int($value) ? max(0, $value) : 0;
|
||||
}
|
||||
if (!is_string($value)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance_minor($value);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ class Pay {
|
||||
|
||||
public function init(): void {
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
add_action('aksrt_order_paid', [$this, 'on_order_paid'], 15, 2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+133
-93
@@ -31,34 +31,35 @@ trait PayHelpers {
|
||||
$uid = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
$priced = function_exists('aksrt_get_user_pay_price')
|
||||
? aksrt_get_user_pay_price($post, $uid)
|
||||
: ['amount' => 0.0, 'base' => 0.0, 'modo' => '0', 'currency' => '¥', 'is_free' => true, 'pay_limit' => 0, 'vip_level' => 0, 'original' => 0.0, 'vip1' => 0.0, 'vip2' => 0.0];
|
||||
: ['amount' => '0.00', 'base' => '0.00', 'modo' => '0', 'currency' => '¥', 'is_free' => true, 'pay_limit' => 0, 'vip_level' => 0, 'original' => '0.00', 'vip1' => '0.00', 'vip2' => '0.00'];
|
||||
|
||||
$pay_modo = (($priced['modo'] ?? '0') === 'points') ? 'points' : '0';
|
||||
|
||||
// Optional overbuy override for preview display.
|
||||
if ($price_type === 'download_limit_over') {
|
||||
$over = max(0.0, (float) ($meta['download_limit_over_price'] ?? 0));
|
||||
if ($over > 0) {
|
||||
$priced['amount'] = $over;
|
||||
$priced['base'] = $over;
|
||||
$over_minor = $this->parse_asset_minor($meta['download_limit_over_price'] ?? ($pay_modo === 'points' ? 0 : '0'), $pay_modo);
|
||||
if ($over_minor > 0) {
|
||||
$priced['amount'] = $this->asset_value($over_minor, $pay_modo);
|
||||
$priced['base'] = $this->asset_value($over_minor, $pay_modo);
|
||||
$priced['is_free'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
$pay_modo = (($priced['modo'] ?? '0') === 'points') ? 'points' : '0';
|
||||
$pay_limit = (int) ($priced['pay_limit'] ?? 0);
|
||||
$vip_level = (int) ($priced['vip_level'] ?? 0);
|
||||
$amount = (float) ($priced['amount'] ?? 0);
|
||||
$base = (float) ($priced['base'] ?? $amount);
|
||||
$over_price = max(0.0, (float) ($meta['download_limit_over_price'] ?? 0));
|
||||
$amount_minor = $this->parse_asset_minor($priced['amount'] ?? ($pay_modo === 'points' ? 0 : '0'), $pay_modo);
|
||||
$base_minor = $this->parse_asset_minor($priced['base'] ?? $priced['amount'] ?? ($pay_modo === 'points' ? 0 : '0'), $pay_modo);
|
||||
$over_minor = $this->parse_asset_minor($meta['download_limit_over_price'] ?? ($pay_modo === 'points' ? 0 : '0'), $pay_modo);
|
||||
|
||||
$list_amount = $amount;
|
||||
$rebate_discount = 0.0;
|
||||
$coupon_discount = 0.0;
|
||||
$list_minor = $amount_minor;
|
||||
$rebate_discount_minor = 0;
|
||||
$coupon_discount_minor = 0;
|
||||
$coupon_payload = null;
|
||||
$coupon_error = '';
|
||||
$referrer_active = false;
|
||||
$referrer_id = 0;
|
||||
$rebate_price = 0.0;
|
||||
$payable = $amount;
|
||||
$rebate_price_minor = 0;
|
||||
$payable_minor = $amount_minor;
|
||||
|
||||
$coupon_allowed = $pay_modo !== 'points' && $this->is_coupon_allowed($post_id, $meta);
|
||||
$coupon_desc = '';
|
||||
@@ -77,15 +78,15 @@ trait PayHelpers {
|
||||
$discounted = $this->apply_cash_discounts($priced, $post, $meta, $uid, '', true);
|
||||
}
|
||||
if (!is_wp_error($discounted)) {
|
||||
$list_amount = (float) ($discounted['list_amount'] ?? $amount);
|
||||
$rebate_discount = (float) ($discounted['rebate_discount'] ?? 0);
|
||||
$coupon_discount = (float) ($discounted['coupon_discount'] ?? 0);
|
||||
$list_minor = (int) ($discounted['list_minor'] ?? $amount_minor);
|
||||
$rebate_discount_minor = (int) ($discounted['rebate_discount_minor'] ?? 0);
|
||||
$coupon_discount_minor = (int) ($discounted['coupon_discount_minor'] ?? 0);
|
||||
$coupon_payload = $discounted['coupon'] ?? null;
|
||||
$payable = (float) ($discounted['payable'] ?? $amount);
|
||||
$payable_minor = (int) ($discounted['payable_minor'] ?? $amount_minor);
|
||||
$referrer_id = (int) ($discounted['referrer_id'] ?? 0);
|
||||
$referrer_active = $referrer_id > 0;
|
||||
$rebate_price = (float) ($discounted['rebate_price'] ?? 0);
|
||||
$amount = $payable;
|
||||
$rebate_price_minor = (int) ($discounted['rebate_price_minor'] ?? 0);
|
||||
$amount_minor = $payable_minor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,19 +122,16 @@ trait PayHelpers {
|
||||
: '¥'));
|
||||
|
||||
// Points cashier uses Credits; cash cashier uses wallet (元). Keep both for UI.
|
||||
$points_balance = ($uid > 0 && class_exists('\AKSRT\Credits\Credits'))
|
||||
? (int) \AKSRT\Credits\Credits::balance($uid)
|
||||
: 0;
|
||||
$wallet_balance = ($uid > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled())
|
||||
? (float) \AKSRT\Credits\Balance::balance($uid)
|
||||
: 0.0;
|
||||
$points_balance = $uid > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($uid, \AKSRT\Ledger\AssetType::POINTS) : 0;
|
||||
$wallet_minor = $uid > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($uid, \AKSRT\Ledger\AssetType::BALANCE) : 0;
|
||||
$wallet_balance = \AKSRT\Ledger\AssetAmount::decimal_from_minor($wallet_minor);
|
||||
$balance = $pay_modo === 'points' ? $points_balance : $wallet_balance;
|
||||
$methods = $this->available_payment_methods($pay_modo, $uid > 0);
|
||||
|
||||
$amount_label = $this->format_amount_label($amount, $pay_modo, $currency);
|
||||
$base_label = $this->format_amount_label($base, $pay_modo, $currency);
|
||||
$list_label = $this->format_amount_label($list_amount, $pay_modo, $currency);
|
||||
$over_label = $over_price > 0 ? $this->format_amount_label($over_price, $pay_modo, $currency) : '';
|
||||
$amount_label = $this->format_amount_label($amount_minor, $pay_modo, $currency);
|
||||
$base_label = $this->format_amount_label($base_minor, $pay_modo, $currency);
|
||||
$list_label = $this->format_amount_label($list_minor, $pay_modo, $currency);
|
||||
$over_label = $over_minor > 0 ? $this->format_amount_label($over_minor, $pay_modo, $currency) : '';
|
||||
|
||||
$vip_page = function_exists('aksrt_get_user_center_tab_url')
|
||||
? aksrt_get_user_center_tab_url('credits')
|
||||
@@ -147,10 +145,10 @@ trait PayHelpers {
|
||||
? aksrt_get_user_free_downloaded_number($uid) : 0;
|
||||
$quota_over = $free_limit > 0 && $free_used >= $free_limit;
|
||||
|
||||
$payable_amount = $pay_modo === 'points' ? (int) ceil($payable) : round($payable, 2);
|
||||
$payable_amount = $this->asset_value($payable_minor, $pay_modo);
|
||||
$balance_enough = $pay_modo === 'points'
|
||||
? ($points_balance >= (int) ceil($amount))
|
||||
: ($wallet_balance + 0.0001 >= (float) $payable_amount);
|
||||
? ($points_balance >= $payable_minor)
|
||||
: ($wallet_minor >= $payable_minor);
|
||||
|
||||
return [
|
||||
'post_id' => $post_id,
|
||||
@@ -161,36 +159,38 @@ trait PayHelpers {
|
||||
'pay_modo' => $pay_modo,
|
||||
'pay_limit' => $pay_limit,
|
||||
'vip_level' => $vip_level,
|
||||
'amount' => $pay_modo === 'points' ? (int) ceil($amount) : round($amount, 2),
|
||||
'base' => $pay_modo === 'points' ? (int) ceil($base) : round($base, 2),
|
||||
'list_amount' => $pay_modo === 'points' ? (int) ceil($list_amount) : round($list_amount, 2),
|
||||
'amount' => $this->asset_value($amount_minor, $pay_modo),
|
||||
'base' => $this->asset_value($base_minor, $pay_modo),
|
||||
'list_amount' => $this->asset_value($list_minor, $pay_modo),
|
||||
'list_amount_label' => $list_label,
|
||||
'payable' => $payable_amount,
|
||||
'original' => (float) ($priced['original'] ?? 0),
|
||||
'vip1' => (float) ($priced['vip1'] ?? 0),
|
||||
'vip2' => (float) ($priced['vip2'] ?? 0),
|
||||
'original' => $this->asset_value($this->parse_asset_minor($priced['original'] ?? ($pay_modo === 'points' ? 0 : '0'), $pay_modo), $pay_modo),
|
||||
'vip1' => $this->asset_value($this->parse_asset_minor($priced['vip1'] ?? ($pay_modo === 'points' ? 0 : '0'), $pay_modo), $pay_modo),
|
||||
'vip2' => $this->asset_value($this->parse_asset_minor($priced['vip2'] ?? ($pay_modo === 'points' ? 0 : '0'), $pay_modo), $pay_modo),
|
||||
'currency' => $currency,
|
||||
'amount_label' => $amount_label,
|
||||
'base_label' => $base_label,
|
||||
'is_free' => !empty($priced['is_free']) || $amount <= 0,
|
||||
'is_free' => !empty($priced['is_free']) || $amount_minor <= 0,
|
||||
'has_paid_access' => $has_paid,
|
||||
'paid_order' => $paid_order ? [
|
||||
'order_number' => (string) ($paid_order['order_number'] ?? ''),
|
||||
'paid_at' => (string) ($paid_order['paid_at'] ?? $paid_order['created_at'] ?? ''),
|
||||
'amount' => (float) ($paid_order['amount'] ?? 0),
|
||||
'amount' => (string) ($paid_order['currency'] ?? '') === 'points'
|
||||
? (int) ($paid_order['amount'] ?? 0)
|
||||
: (string) ($paid_order['amount'] ?? '0.00'),
|
||||
'currency' => (string) ($paid_order['currency'] ?? ''),
|
||||
'payment_method' => (string) ($paid_order['payment_method'] ?? ''),
|
||||
] : null,
|
||||
'pending_order' => $pending_order ? [
|
||||
'order_number' => (string) ($pending_order['order_number'] ?? ''),
|
||||
'amount' => (float) ($pending_order['amount'] ?? 0),
|
||||
'amount' => (string) ($pending_order['amount'] ?? '0.00'),
|
||||
'payment_method' => (string) ($pending_order['payment_method'] ?? ''),
|
||||
'created_at' => (string) ($pending_order['created_at'] ?? ''),
|
||||
] : null,
|
||||
'need_login' => $uid <= 0 && !$allow_guest,
|
||||
'allow_guest' => $allow_guest,
|
||||
'need_vip' => $need_vip,
|
||||
'need_purchase' => !$has_paid && !$need_vip && $amount > 0,
|
||||
'need_purchase' => !$has_paid && !$need_vip && $amount_minor > 0,
|
||||
'logged_in' => $uid > 0,
|
||||
'balance' => $balance,
|
||||
'wallet_balance' => $wallet_balance,
|
||||
@@ -200,9 +200,9 @@ trait PayHelpers {
|
||||
'login_url' => wp_login_url(get_permalink($post)),
|
||||
'vip_url' => $vip_page,
|
||||
'credits_url' => $vip_page,
|
||||
'download_limit_over_price' => $pay_modo === 'points' ? (int) ceil($over_price) : round($over_price, 2),
|
||||
'download_limit_over_price' => $this->asset_value($over_minor, $pay_modo),
|
||||
'download_limit_over_label' => $over_label,
|
||||
'allow_limit_over' => $over_price > 0,
|
||||
'allow_limit_over' => $over_minor > 0,
|
||||
'quota_over' => $quota_over,
|
||||
'free_limit' => $free_limit,
|
||||
'free_used' => $free_used,
|
||||
@@ -211,18 +211,18 @@ trait PayHelpers {
|
||||
'coupon_code' => $coupon_code,
|
||||
'coupon' => $coupon_payload,
|
||||
'coupon_error' => $coupon_error,
|
||||
'rebate_discount' => round($rebate_discount, 2),
|
||||
'rebate_discount_label' => $rebate_discount > 0 ? ('-' . $this->format_amount_label($rebate_discount, $pay_modo, $currency)) : '',
|
||||
'coupon_discount' => round($coupon_discount, 2),
|
||||
'coupon_discount_label' => $coupon_discount > 0 ? ('-' . $this->format_amount_label($coupon_discount, $pay_modo, $currency)) : '',
|
||||
'rebate_discount' => $this->asset_value($rebate_discount_minor, $pay_modo),
|
||||
'rebate_discount_label' => $rebate_discount_minor > 0 ? ('-' . $this->format_amount_label($rebate_discount_minor, $pay_modo, $currency)) : '',
|
||||
'coupon_discount' => $this->asset_value($coupon_discount_minor, $pay_modo),
|
||||
'coupon_discount_label' => $coupon_discount_minor > 0 ? ('-' . $this->format_amount_label($coupon_discount_minor, $pay_modo, $currency)) : '',
|
||||
'referrer_active' => $referrer_active,
|
||||
'discounts' => [
|
||||
'list_amount' => round($list_amount, 2),
|
||||
'rebate_discount' => round($rebate_discount, 2),
|
||||
'coupon_discount' => round($coupon_discount, 2),
|
||||
'total_discount' => round(max(0, $list_amount - $payable), 2),
|
||||
'payable' => round($payable, 2),
|
||||
'rebate_price' => round($rebate_price, 2),
|
||||
'list_amount' => $this->asset_value($list_minor, $pay_modo),
|
||||
'rebate_discount' => $this->asset_value($rebate_discount_minor, $pay_modo),
|
||||
'coupon_discount' => $this->asset_value($coupon_discount_minor, $pay_modo),
|
||||
'total_discount' => $this->asset_value(max(0, $list_minor - $payable_minor), $pay_modo),
|
||||
'payable' => $this->asset_value($payable_minor, $pay_modo),
|
||||
'rebate_price' => $this->asset_value($rebate_price_minor, $pay_modo),
|
||||
],
|
||||
// legacy: points_deduction runtime hard-off; card_pass not a content payment method.
|
||||
'points_deduction_allowed' => function_exists('aksrt_is_allow_points_deduction')
|
||||
@@ -269,18 +269,18 @@ trait PayHelpers {
|
||||
|
||||
$priced = function_exists('aksrt_get_user_pay_price')
|
||||
? aksrt_get_user_pay_price($post, $user_id)
|
||||
: ['amount' => 0.0, 'modo' => '0', 'pay_limit' => 0, 'vip_level' => 0, 'is_free' => true, 'base' => 0.0];
|
||||
: ['amount' => '0.00', 'modo' => '0', 'pay_limit' => 0, 'vip_level' => 0, 'is_free' => true, 'base' => '0.00'];
|
||||
|
||||
// Free daily quota overbuy: override amount, ignore VIP discount (legacy).
|
||||
if ($price_type === 'download_limit_over') {
|
||||
$over = max(0.0, (float) ($meta['download_limit_over_price'] ?? 0));
|
||||
if ($over <= 0) {
|
||||
$over_minor = $this->parse_asset_minor($meta['download_limit_over_price'] ?? ($pay_modo_meta === 'points' ? 0 : '0'), $pay_modo_meta);
|
||||
if ($over_minor <= 0) {
|
||||
return new \WP_Error('aksrt_no_overbuy', __('未开放超额下载购买。', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$priced['amount'] = $over;
|
||||
$priced['base'] = $over;
|
||||
$priced['amount'] = $this->asset_value($over_minor, $pay_modo_meta);
|
||||
$priced['base'] = $this->asset_value($over_minor, $pay_modo_meta);
|
||||
$priced['is_free'] = false;
|
||||
$priced['vip_amount'] = -1.0;
|
||||
$priced['vip_amount'] = null;
|
||||
}
|
||||
|
||||
$pay_limit = (int) ($priced['pay_limit'] ?? 0);
|
||||
@@ -365,10 +365,10 @@ trait PayHelpers {
|
||||
|
||||
|
||||
/**
|
||||
* @return array{id:int,ratio:float}
|
||||
* @return array{id:int,ratio:int}
|
||||
*/
|
||||
private function resolve_referrer_context(int $user_id): array {
|
||||
$empty = ['id' => 0, 'ratio' => 0.0];
|
||||
$empty = ['id' => 0, 'ratio' => 0];
|
||||
if (!class_exists('\AKSRT\Settings\Settings') || !method_exists('\AKSRT\Settings\Settings', 'get_payment_affiliate_settings')) {
|
||||
return $empty;
|
||||
}
|
||||
@@ -383,7 +383,7 @@ trait PayHelpers {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$ratio = (float) \AKSRT\Settings\Settings::affiliate_ratio_for_user($referrer_id);
|
||||
$ratio = (int) \AKSRT\Settings\Settings::affiliate_ratio_for_user($referrer_id);
|
||||
if ($ratio <= 0) {
|
||||
return $empty;
|
||||
}
|
||||
@@ -400,21 +400,21 @@ trait PayHelpers {
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
private function apply_cash_discounts(array $priced, \WP_Post $post, array $meta, int $user_id, string $coupon_code = '', bool $soft_coupon = false) {
|
||||
$list_amount = round(max(0.0, (float) ($priced['amount'] ?? 0)), 2);
|
||||
$pay = $list_amount;
|
||||
$rebate_discount = 0.0;
|
||||
$coupon_discount = 0.0;
|
||||
$list_minor = $this->parse_asset_minor($priced['amount'] ?? '0', '0');
|
||||
$pay_minor = $list_minor;
|
||||
$rebate_discount_minor = 0;
|
||||
$coupon_discount_minor = 0;
|
||||
$coupon_meta = [];
|
||||
|
||||
$ref = $this->resolve_referrer_context($user_id);
|
||||
$referrer_id = (int) ($ref['id'] ?? 0);
|
||||
$ratio = (float) ($ref['ratio'] ?? 0);
|
||||
$ratio = max(0, min(100, (int) ($ref['ratio'] ?? 0)));
|
||||
|
||||
// 1) Buyer-facing 推广折扣 (fixed).
|
||||
$fixed = max(0.0, (float) ($meta['pay_rebate_discount'] ?? 0));
|
||||
if ($referrer_id > 0 && $ratio > 0 && $fixed > 0 && $pay > 0) {
|
||||
$rebate_discount = round(min($pay, $fixed), 2);
|
||||
$pay = round(max(0.0, $pay - $rebate_discount), 2);
|
||||
$fixed_minor = $this->parse_asset_minor($meta['pay_rebate_discount'] ?? '0', '0');
|
||||
if ($referrer_id > 0 && $ratio > 0 && $fixed_minor > 0 && $pay_minor > 0) {
|
||||
$rebate_discount_minor = min($pay_minor, $fixed_minor);
|
||||
$pay_minor -= $rebate_discount_minor;
|
||||
}
|
||||
|
||||
// 2) Coupon after rebate discount.
|
||||
@@ -431,11 +431,15 @@ trait PayHelpers {
|
||||
return $err;
|
||||
}
|
||||
} else {
|
||||
$result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $pay, [
|
||||
$result = \AKSRT\Payment\Coupon::get_instance()->validate(
|
||||
$coupon_code,
|
||||
\AKSRT\Ledger\AssetAmount::decimal_from_minor($pay_minor),
|
||||
[
|
||||
'user_id' => max(0, $user_id),
|
||||
'product_ids' => [(int) $post->ID],
|
||||
'context' => 'content',
|
||||
]);
|
||||
]
|
||||
);
|
||||
if (empty($result['valid'])) {
|
||||
$err = new \WP_Error(
|
||||
'aksrt_coupon_error',
|
||||
@@ -446,47 +450,50 @@ trait PayHelpers {
|
||||
return $err;
|
||||
}
|
||||
} else {
|
||||
$coupon_discount = round(max(0.0, (float) ($result['discount'] ?? 0)), 2);
|
||||
if ($coupon_discount > $pay) {
|
||||
$coupon_discount = $pay;
|
||||
}
|
||||
$pay = round(max(0.0, $pay - $coupon_discount), 2);
|
||||
$coupon_discount_minor = min($pay_minor, max(0, (int) ($result['discount_minor'] ?? 0)));
|
||||
$pay_minor -= $coupon_discount_minor;
|
||||
$c = is_array($result['coupon'] ?? null) ? $result['coupon'] : [];
|
||||
$coupon_meta = [
|
||||
'id' => (int) ($c['id'] ?? 0),
|
||||
'code' => (string) ($c['code'] ?? $coupon_code),
|
||||
'type' => (string) ($c['type'] ?? ''),
|
||||
'value' => (float) ($c['value'] ?? 0),
|
||||
'discount' => $coupon_discount,
|
||||
'value' => $c['value'] ?? 0,
|
||||
'discount' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($coupon_discount_minor),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$payable = round(max(0.0, $pay), 2);
|
||||
$rebate_price = 0.0;
|
||||
if ($referrer_id > 0 && $ratio > 0 && $payable > 0) {
|
||||
$rebate_price = round($payable * $ratio / 100, 2);
|
||||
$payable_minor = max(0, $pay_minor);
|
||||
$rebate_price_minor = 0;
|
||||
if ($referrer_id > 0 && $ratio > 0 && $payable_minor > 0) {
|
||||
$rebate_price_minor = \AKSRT\Ledger\AssetAmount::percentage($payable_minor, $ratio);
|
||||
}
|
||||
|
||||
return [
|
||||
'list_amount' => $list_amount,
|
||||
'rebate_discount' => $rebate_discount,
|
||||
'coupon_discount' => $coupon_discount,
|
||||
'list_amount' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($list_minor),
|
||||
'list_minor' => $list_minor,
|
||||
'rebate_discount' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($rebate_discount_minor),
|
||||
'rebate_discount_minor' => $rebate_discount_minor,
|
||||
'coupon_discount' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($coupon_discount_minor),
|
||||
'coupon_discount_minor' => $coupon_discount_minor,
|
||||
'coupon' => $coupon_meta,
|
||||
'payable' => $payable,
|
||||
'payable' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($payable_minor),
|
||||
'payable_minor' => $payable_minor,
|
||||
'referrer_id' => $referrer_id,
|
||||
'rebate_price' => $rebate_price,
|
||||
'rebate_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($rebate_price_minor),
|
||||
'rebate_price_minor' => $rebate_price_minor,
|
||||
'ratio' => $ratio,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function format_amount_label(float $amount, string $pay_modo, string $currency): string {
|
||||
if ($amount <= 0) {
|
||||
private function format_amount_label(int $minor, string $pay_modo, string $currency): string {
|
||||
if ($minor <= 0) {
|
||||
return (string) __('免费', 'aksrt');
|
||||
}
|
||||
$amount = $this->asset_value($minor, $pay_modo);
|
||||
$n = function_exists('aksrt_format_price') ? aksrt_format_price($amount) : (string) $amount;
|
||||
if ($pay_modo === 'points') {
|
||||
return $n . ' ' . $currency;
|
||||
@@ -494,4 +501,37 @@ trait PayHelpers {
|
||||
return $currency . $n;
|
||||
}
|
||||
|
||||
private function parse_asset_minor($value, string $pay_modo): int {
|
||||
if ($pay_modo === 'points') {
|
||||
if (is_int($value)) {
|
||||
return max(0, $value);
|
||||
}
|
||||
if (!is_string($value) || !preg_match('/^(0|[1-9]\d*)$/', trim($value))) {
|
||||
return 0;
|
||||
}
|
||||
$value = trim($value);
|
||||
if (strlen($value) > strlen((string) PHP_INT_MAX)
|
||||
|| (strlen($value) === strlen((string) PHP_INT_MAX) && strcmp($value, (string) PHP_INT_MAX) > 0)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
return \AKSRT\Ledger\AssetAmount::points((int) $value)->minor();
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance_minor($value);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private function asset_value(int $minor, string $pay_modo) {
|
||||
return $pay_modo === 'points'
|
||||
? $minor
|
||||
: \AKSRT\Ledger\AssetAmount::decimal_from_minor($minor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+208
-108
@@ -17,19 +17,19 @@ trait PayOrders {
|
||||
* @param string $order_number
|
||||
* @param array $paid_order
|
||||
*/
|
||||
public function on_order_paid(string $order_number, array $paid_order): void {
|
||||
public function fulfill_paid_order(string $order_number, array $paid_order) {
|
||||
$product_type = sanitize_key((string) ($paid_order['product_type'] ?? ''));
|
||||
if ($product_type !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
$post_id = (int) ($paid_order['product_id'] ?? 0);
|
||||
if ($post_id <= 0) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
// Only count normal posts for content purchases.
|
||||
$post = get_post($post_id);
|
||||
if (!($post instanceof \WP_Post) || $post->post_type !== 'post') {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (function_exists('aksrt_bump_content_sales_volume')) {
|
||||
aksrt_bump_content_sales_volume($post_id, 1);
|
||||
@@ -53,21 +53,31 @@ trait PayOrders {
|
||||
}
|
||||
|
||||
// Consume coupon + write affiliate commission (cash content only; idempotent).
|
||||
$this->finalize_content_order_side_effects($order_number, $paid_order);
|
||||
$side_effects = $this->finalize_content_order_side_effects($order_number, $paid_order);
|
||||
if (is_wp_error($side_effects)) {
|
||||
return $side_effects;
|
||||
}
|
||||
if (class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$completed = \AKSRT\Payment\OrderRepository::get_instance()->update_status(
|
||||
$order_number,
|
||||
\AKSRT\Payment\Order::STATUS_COMPLETED,
|
||||
['note' => __('内容权益已发放。', 'aksrt')]
|
||||
);
|
||||
if ($completed && class_exists('\AKSRT\Payment\CreatorRevenue')) {
|
||||
if (!$completed) {
|
||||
return new \WP_Error('aksrt_content_fulfillment_failed', __('内容权益发放失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
if (class_exists('\AKSRT\Payment\CreatorRevenue')) {
|
||||
$completed_order = array_merge($paid_order, [
|
||||
'order_number' => $order_number,
|
||||
'status' => \AKSRT\Payment\Order::STATUS_COMPLETED,
|
||||
]);
|
||||
\AKSRT\Payment\CreatorRevenue::get_instance()->create_from_paid_order($completed_order);
|
||||
}
|
||||
$revenue_id = \AKSRT\Payment\CreatorRevenue::get_instance()->create_from_paid_order($completed_order);
|
||||
if ($revenue_id < 0) {
|
||||
return new \WP_Error('aksrt_creator_revenue_conflict', __('同一订单的收益载荷不一致。', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,25 +85,29 @@ trait PayOrders {
|
||||
/**
|
||||
* On paid: burn coupon (if any) and create affiliate rebate log.
|
||||
*/
|
||||
private function finalize_content_order_side_effects(string $order_number, array $order): void {
|
||||
private function finalize_content_order_side_effects(string $order_number, array $order) {
|
||||
$meta = $this->decode_order_note_meta($order);
|
||||
if (empty($meta['content_pay'])) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
// Points: no coupon burn / no cash commission.
|
||||
if (($meta['pay_modo'] ?? '') === 'points') {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
$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);
|
||||
$coupon = is_array($meta['coupon'] ?? null) ? $meta['coupon'] : [];
|
||||
$coupon_code = (string) ($coupon['code'] ?? $meta['coupon_code'] ?? '');
|
||||
if ($coupon_code !== '' && !aksrt_burn_order_coupon($order_number, $meta, $buyer_id)) {
|
||||
return new \WP_Error('aksrt_coupon_consume_failed', __('优惠券核销失败。', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
}
|
||||
|
||||
// Affiliate commission is recorded exactly once by CommissionLedger on
|
||||
// the canonical aksrt_order_paid event. No order-side legacy writes.
|
||||
// Affiliate commission is written by the order transition transaction.
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +135,7 @@ trait PayOrders {
|
||||
*
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function create_and_pay_points(int $post_id, int $user_id, string $price_type = '') {
|
||||
public function create_and_pay_points(int $post_id, int $user_id, string $price_type = '', string $operation_key = '') {
|
||||
$assert = $this->assert_purchasable($post_id, $user_id, $price_type);
|
||||
if (is_wp_error($assert)) {
|
||||
return $assert;
|
||||
@@ -132,7 +146,7 @@ trait PayOrders {
|
||||
return new \WP_Error('aksrt_wrong_modo', __('该内容不支持积分支付。', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
$amount = (int) ceil((float) ($priced['amount'] ?? 0));
|
||||
$amount = $this->parse_asset_minor($priced['amount'] ?? 0, 'points');
|
||||
if ($amount <= 0) {
|
||||
return [
|
||||
'success' => true,
|
||||
@@ -144,13 +158,14 @@ trait PayOrders {
|
||||
];
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Credits\Credits')) {
|
||||
if (!class_exists('\AKSRT\Payment\Order')) {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$balance = \AKSRT\Credits\Credits::balance($user_id);
|
||||
$ledger = \AKSRT\Ledger\LedgerService::get_instance();
|
||||
$balance = $ledger->balance($user_id, \AKSRT\Ledger\AssetType::POINTS);
|
||||
if ($balance < $amount) {
|
||||
return new \WP_Error('aksrt_credits_insufficient', __('积分不足,暂时无法支付。', 'aksrt'), [
|
||||
return new \WP_Error('aksrt_points_insufficient', __('积分不足,暂时无法支付。', 'aksrt'), [
|
||||
'status' => 402,
|
||||
'balance' => $balance,
|
||||
'cost' => $amount,
|
||||
@@ -163,69 +178,93 @@ trait PayOrders {
|
||||
'post_id' => $post_id,
|
||||
'price_type' => $price_type,
|
||||
'vip_level' => (int) ($priced['vip_level'] ?? 0),
|
||||
'base' => (float) ($priced['base'] ?? $amount),
|
||||
'base' => $this->parse_asset_minor($priced['base'] ?? $amount, 'points'),
|
||||
'amount' => $amount,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
|
||||
'user_id' => $user_id,
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
|
||||
'product_id' => $post_id,
|
||||
'amount' => (float) $amount,
|
||||
'currency' => 'points',
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => 'points',
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
$existing = $operation_key !== '' ? $repo->get_by_request_key($operation_key) : null;
|
||||
$is_new_order = !is_array($existing);
|
||||
if (is_array($existing)) {
|
||||
$existing_meta = $this->decode_order_note_meta($existing);
|
||||
if ((int) ($existing['user_id'] ?? 0) !== $user_id
|
||||
|| sanitize_key((string) ($existing['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE
|
||||
|| (int) ($existing['product_id'] ?? 0) !== $post_id
|
||||
|| (string) ($existing['currency'] ?? '') !== 'points'
|
||||
|| (int) ($existing['amount'] ?? 0) !== $amount
|
||||
|| sanitize_key((string) ($existing['payment_method'] ?? '')) !== 'points'
|
||||
|| (string) ($existing_meta['price_type'] ?? '') !== $price_type
|
||||
) {
|
||||
return new \WP_Error('aksrt_ledger_idempotency_conflict', __('相同操作标识已用于其他购买请求。', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
$order_number = (string) $existing['order_number'];
|
||||
} else {
|
||||
$order_number = $repo->allocate_order_number();
|
||||
}
|
||||
|
||||
if ($order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$guard = static function () use ($is_new_order, $order_number, $user_id, $post_id, $amount) {
|
||||
if ($is_new_order) {
|
||||
return true;
|
||||
}
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('order');
|
||||
$current = $wpdb->get_row($wpdb->prepare("SELECT user_id, product_id, amount, currency, status FROM {$table} WHERE order_number = %s FOR UPDATE", $order_number), ARRAY_A);
|
||||
if (!is_array($current)
|
||||
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
||||
|| (int) ($current['product_id'] ?? 0) !== $post_id
|
||||
|| (int) ($current['amount'] ?? 0) !== $amount
|
||||
|| (string) ($current['currency'] ?? '') !== 'points'
|
||||
|| !in_array((string) ($current['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_PENDING], true)
|
||||
) {
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态或金额已变化。', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
$lock = class_exists('\AKSRT\Payment\PaymentAttemptLock')
|
||||
? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number)
|
||||
: false;
|
||||
if ($lock === false) {
|
||||
return new \WP_Error('aksrt_pay_locked', __('该订单正在发起支付,请稍后重试。', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
|
||||
try {
|
||||
$deducted = \AKSRT\Credits\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\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\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\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
|
||||
$deducted = $ledger->debit(
|
||||
$user_id,
|
||||
\AKSRT\Ledger\AssetType::POINTS,
|
||||
$amount,
|
||||
$operation_key,
|
||||
'content_points_pay',
|
||||
['type' => 'order', 'key' => $order_number],
|
||||
sprintf(__('积分购买《%s》', 'aksrt'), get_the_title($post_id)),
|
||||
$user_id,
|
||||
['post_id' => $post_id],
|
||||
static function () use ($is_new_order, $repo, $order_number, $operation_key, $user_id, $post_id, $amount, $note) {
|
||||
if ($is_new_order) {
|
||||
$created = $repo->create([
|
||||
'order_number' => $order_number,
|
||||
'user_id' => $user_id,
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
|
||||
'product_id' => $post_id,
|
||||
'amount' => $amount,
|
||||
'currency' => 'points',
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => 'points',
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
'request_key' => $operation_key,
|
||||
]);
|
||||
if ($created !== $order_number) {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
}
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->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) {
|
||||
\AKSRT\Credits\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]);
|
||||
}
|
||||
} finally {
|
||||
\AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock);
|
||||
]);
|
||||
return $paid ? true : new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
|
||||
},
|
||||
$guard
|
||||
);
|
||||
if (is_wp_error($deducted)) {
|
||||
return $deducted;
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -234,7 +273,9 @@ trait PayOrders {
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'pay_modo' => 'points',
|
||||
'balance' => \AKSRT\Credits\Credits::balance($user_id),
|
||||
'balance' => $ledger->balance($user_id, \AKSRT\Ledger\AssetType::POINTS),
|
||||
'transaction_id' => $deducted->transaction_id,
|
||||
'replayed' => $deducted->replayed,
|
||||
'message' => __('支付成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
],
|
||||
@@ -248,7 +289,7 @@ trait PayOrders {
|
||||
*
|
||||
* @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 = '') {
|
||||
public function create_cash_order(int $post_id, int $user_id, string $payment_method = 'balance', string $price_type = '', string $coupon_code = '', string $operation_key = '') {
|
||||
$assert = $this->assert_purchasable($post_id, $user_id, $price_type);
|
||||
if (is_wp_error($assert)) {
|
||||
return $assert;
|
||||
@@ -279,15 +320,20 @@ trait PayOrders {
|
||||
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);
|
||||
$amount_minor = max(0, (int) ($discounted['payable_minor'] ?? 0));
|
||||
$list_minor = max(0, (int) ($discounted['list_minor'] ?? $amount_minor));
|
||||
$rebate_discount_minor = max(0, (int) ($discounted['rebate_discount_minor'] ?? 0));
|
||||
$coupon_discount_minor = max(0, (int) ($discounted['coupon_discount_minor'] ?? 0));
|
||||
$amount = \AKSRT\Ledger\AssetAmount::decimal_from_minor($amount_minor);
|
||||
$list_amount = \AKSRT\Ledger\AssetAmount::decimal_from_minor($list_minor);
|
||||
$rebate_discount = \AKSRT\Ledger\AssetAmount::decimal_from_minor($rebate_discount_minor);
|
||||
$coupon_discount = \AKSRT\Ledger\AssetAmount::decimal_from_minor($coupon_discount_minor);
|
||||
$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);
|
||||
$rebate_price_minor = max(0, (int) ($discounted['rebate_price_minor'] ?? 0));
|
||||
$rebate_price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($rebate_price_minor);
|
||||
|
||||
if ($amount <= 0) {
|
||||
if ($amount_minor <= 0) {
|
||||
// Free after discount (or free content): create 0-paid order for access + burn coupon.
|
||||
if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
|
||||
@@ -298,50 +344,71 @@ trait PayOrders {
|
||||
'post_id' => $post_id,
|
||||
'price_type' => $price_type,
|
||||
'vip_level' => (int) ($priced['vip_level'] ?? 0),
|
||||
'base' => (float) ($priced['base'] ?? $list_amount),
|
||||
'base' => $this->asset_value($this->parse_asset_minor($priced['base'] ?? $list_amount, '0'), '0'),
|
||||
'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,
|
||||
'amount' => '0.00',
|
||||
'payable' => '0.00',
|
||||
'referrer_id' => $referrer_id,
|
||||
'rebate_price' => 0,
|
||||
'rebate_price' => '0.00',
|
||||
'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\Payment\OrderRepository::get_instance()->create([
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
$existing = $operation_key !== '' ? $repo->get_by_request_key($operation_key) : null;
|
||||
$replayed = is_array($existing);
|
||||
if (is_array($existing)) {
|
||||
if ((int) ($existing['user_id'] ?? 0) !== max(0, $user_id)
|
||||
|| sanitize_key((string) ($existing['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE
|
||||
|| (int) ($existing['product_id'] ?? 0) !== $post_id
|
||||
|| sanitize_key((string) ($existing['payment_method'] ?? '')) !== 'free'
|
||||
|| (string) ($existing['amount'] ?? '') !== '0.00'
|
||||
) {
|
||||
return new \WP_Error('aksrt_ledger_idempotency_conflict', __('相同操作标识已用于其他内容请求。', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
$order_number = (string) $existing['order_number'];
|
||||
} else {
|
||||
$order_number = $repo->create([
|
||||
'user_id' => max(0, $user_id),
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
|
||||
'product_id' => $post_id,
|
||||
'amount' => 0.0,
|
||||
'amount' => '0.00',
|
||||
'currency' => 'CNY',
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => 'free',
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
'request_key' => $operation_key,
|
||||
]);
|
||||
}
|
||||
if ($order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\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]);
|
||||
}
|
||||
$status = is_array($existing) ? (string) ($existing['status'] ?? '') : '';
|
||||
if (!in_array($status, [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true)) {
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\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,
|
||||
'amount' => '0.00',
|
||||
'pay_modo' => '0',
|
||||
'free' => true,
|
||||
'transaction_id' => 'free_' . $order_number,
|
||||
'replayed' => $replayed,
|
||||
'message' => __('无需支付,已可访问。', 'aksrt'),
|
||||
'reload' => true,
|
||||
],
|
||||
@@ -358,21 +425,31 @@ trait PayOrders {
|
||||
if ($user_id <= 0 && in_array($payment_method, ['balance', 'points'], true)) {
|
||||
$payment_method = 'alipay';
|
||||
}
|
||||
$amount_decimal = $amount;
|
||||
|
||||
$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;
|
||||
// A retry resolves only through its request key; a new operation never reuses another pending order.
|
||||
$pending = $operation_key !== '' ? \AKSRT\Payment\OrderRepository::get_instance()->get_by_request_key($operation_key) : null;
|
||||
$can_resume = false;
|
||||
if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) {
|
||||
if (is_array($pending)) {
|
||||
$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;
|
||||
}
|
||||
$can_resume = (int) ($pending['user_id'] ?? 0) === max(0, $user_id)
|
||||
&& sanitize_key((string) ($pending['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE
|
||||
&& (int) ($pending['product_id'] ?? 0) === $post_id
|
||||
&& (string) ($pending['amount'] ?? '') === $amount_decimal
|
||||
&& strtoupper((string) ($pending['currency'] ?? '')) === 'CNY'
|
||||
&& sanitize_key((string) ($pending['payment_method'] ?? '')) === $payment_method
|
||||
&& (string) ($pending_meta['price_type'] ?? '') === $price_type
|
||||
&& (int) ($pending_meta['post_id'] ?? 0) === $post_id
|
||||
&& $pending_coupon === $coupon_code_norm;
|
||||
if (!$can_resume) {
|
||||
return new \WP_Error('aksrt_ledger_idempotency_conflict', __('相同操作标识已用于其他内容购买请求。', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
}
|
||||
if ($can_resume) {
|
||||
$order_number = (string) ($pending['order_number'] ?? '');
|
||||
@@ -384,15 +461,15 @@ trait PayOrders {
|
||||
'post_id' => $post_id,
|
||||
'price_type' => $price_type,
|
||||
'vip_level' => (int) ($priced['vip_level'] ?? 0),
|
||||
'base' => (float) ($priced['base'] ?? $list_amount),
|
||||
'base' => $this->asset_value($this->parse_asset_minor($priced['base'] ?? $list_amount, '0'), '0'),
|
||||
'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,
|
||||
'amount' => $amount_decimal,
|
||||
'payable' => $amount_decimal,
|
||||
'referrer_id' => $referrer_id,
|
||||
'rebate_price' => $rebate_price,
|
||||
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
|
||||
@@ -403,12 +480,13 @@ trait PayOrders {
|
||||
'user_id' => max(0, $user_id),
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
|
||||
'product_id' => $post_id,
|
||||
'amount' => $amount,
|
||||
'amount' => $amount_decimal,
|
||||
'currency' => 'CNY',
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => $payment_method,
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
'request_key' => $operation_key,
|
||||
]);
|
||||
|
||||
if ($order_number === '') {
|
||||
@@ -425,6 +503,23 @@ trait PayOrders {
|
||||
if ($payment_method === '') {
|
||||
$payment_method = $user_id > 0 ? 'balance' : 'alipay';
|
||||
}
|
||||
if ($can_resume && in_array((string) ($order['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true)) {
|
||||
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('order-payment', [$order_number]);
|
||||
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount_decimal,
|
||||
'pay_modo' => '0',
|
||||
'transaction_id' => (int) ($history['items'][0]['id'] ?? 0),
|
||||
'replayed' => true,
|
||||
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
'message' => __('支付成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$order['subject'] = sprintf(
|
||||
/* translators: %s: post title */
|
||||
@@ -433,7 +528,7 @@ trait PayOrders {
|
||||
);
|
||||
$order['payment_method'] = $payment_method;
|
||||
// Ensure gateway uses discounted amount.
|
||||
$order['amount'] = $amount;
|
||||
$order['amount'] = $amount_decimal;
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\Gateway')) {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]);
|
||||
@@ -455,7 +550,7 @@ trait PayOrders {
|
||||
return new \WP_Error(
|
||||
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
|
||||
(string) ($payment_result['message'] ?? __('支付失败。', 'aksrt')),
|
||||
['status' => 402, 'order_number' => $order_number]
|
||||
['status' => (int) ($payment_result['status'] ?? 400), 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -467,11 +562,13 @@ trait PayOrders {
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'amount' => $amount_decimal,
|
||||
'pay_modo' => '0',
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
'transaction_id' => 0,
|
||||
'replayed' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -480,8 +577,11 @@ trait PayOrders {
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'amount' => $amount_decimal,
|
||||
'pay_modo' => '0',
|
||||
'transaction_id' => (int) ($payment_result['ledger_transaction_id'] ?? 0),
|
||||
'replayed' => !empty($payment_result['replayed']),
|
||||
'balance' => $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : '0.00',
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => true,
|
||||
],
|
||||
|
||||
+31
-6
@@ -17,11 +17,23 @@ trait PayRest {
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_preview'],
|
||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||
'args' => [
|
||||
'id' => ['type' => 'integer', 'required' => true, 'minimum' => 1],
|
||||
'price_type' => ['type' => 'string', 'enum' => ['', 'download_limit_over']],
|
||||
'coupon_code' => ['type' => 'string', 'maxLength' => 64, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_purchase'],
|
||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||
'args' => [
|
||||
'id' => ['type' => 'integer', 'required' => true, 'minimum' => 1],
|
||||
'mode' => ['type' => 'string', 'enum' => ['points', 'cash']],
|
||||
'payment_method' => ['type' => 'string', 'maxLength' => 32, 'sanitize_callback' => 'sanitize_key'],
|
||||
'price_type' => ['type' => 'string', 'enum' => ['', 'download_limit_over']],
|
||||
'coupon_code' => ['type' => 'string', 'maxLength' => 64, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -29,6 +41,15 @@ trait PayRest {
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_order_status'],
|
||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||
'args' => [
|
||||
'order_number' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'pattern' => '^[A-Za-z0-9_-]+$',
|
||||
'maxLength' => 64,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -60,7 +81,7 @@ trait PayRest {
|
||||
public function rest_preview(\WP_REST_Request $request) {
|
||||
$post_id = (int) $request['id'];
|
||||
$uid = get_current_user_id();
|
||||
$coupon_code = sanitize_text_field((string) ($request->get_param('coupon_code') ?? $request->get_param('coupon') ?? ''));
|
||||
$coupon_code = sanitize_text_field((string) $request->get_param('coupon_code'));
|
||||
$price_type = sanitize_key((string) ($request->get_param('price_type') ?? ''));
|
||||
if ($price_type !== 'download_limit_over') {
|
||||
$price_type = '';
|
||||
@@ -81,6 +102,10 @@ trait PayRest {
|
||||
public function rest_purchase(\WP_REST_Request $request) {
|
||||
$post_id = (int) $request['id'];
|
||||
$uid = get_current_user_id();
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'content-purchase', $uid);
|
||||
if (is_wp_error($operation_key)) {
|
||||
return $operation_key;
|
||||
}
|
||||
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
@@ -91,7 +116,7 @@ trait PayRest {
|
||||
if ($price_type !== 'download_limit_over') {
|
||||
$price_type = '';
|
||||
}
|
||||
$coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? ''));
|
||||
$coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? ''));
|
||||
|
||||
// Guest allowed only for cash + pay_limit=0 + setting on.
|
||||
if ($uid <= 0) {
|
||||
@@ -121,8 +146,8 @@ trait PayRest {
|
||||
if ($uid <= 0) {
|
||||
return new \WP_Error('aksrt_need_login', __('积分支付请先登录。', 'aksrt'), ['status' => 401]);
|
||||
}
|
||||
// Points path: ignore coupon (legacy parity).
|
||||
return $this->rest_wrap_result($this->create_and_pay_points($post_id, $uid, $price_type));
|
||||
// Point purchases do not support coupons.
|
||||
return $this->rest_wrap_result($this->create_and_pay_points($post_id, $uid, $price_type, $operation_key));
|
||||
}
|
||||
|
||||
$payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance'));
|
||||
@@ -133,7 +158,7 @@ trait PayRest {
|
||||
if ($uid <= 0 && in_array($payment_method, ['balance', 'points'], true)) {
|
||||
$payment_method = 'alipay';
|
||||
}
|
||||
return $this->rest_wrap_result($this->create_cash_order($post_id, $uid, $payment_method, $price_type, $coupon_code));
|
||||
return $this->rest_wrap_result($this->create_cash_order($post_id, $uid, $payment_method, $price_type, $coupon_code, $operation_key));
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +202,7 @@ trait PayRest {
|
||||
'order_number' => $order_number,
|
||||
'status' => $status,
|
||||
'paid' => $paid,
|
||||
'amount' => (float) ($order['amount'] ?? 0),
|
||||
'amount' => (string) ($order['amount'] ?? '0.00'),
|
||||
'currency' => (string) ($order['currency'] ?? ''),
|
||||
'product_id' => (int) ($order['product_id'] ?? 0),
|
||||
'product_type' => (string) ($order['product_type'] ?? ''),
|
||||
|
||||
@@ -29,7 +29,6 @@ class VipService {
|
||||
|
||||
public function init(): void {
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
add_action('aksrt_order_paid', [$this, 'on_order_paid'], 20, 2);
|
||||
if (class_exists(VipMembershipExpiry::class)) {
|
||||
VipMembershipExpiry::init();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ trait VipServicePackages {
|
||||
* Preferred: price|show_price|time|unit|tag|title
|
||||
* Legacy: price|days|title
|
||||
*
|
||||
* @return array<int, array{price:float,show_price:float,time:int,unit:string,tag:string,tag_class:string,title:string,days:int}>
|
||||
* @return array<int, array{price:string,show_price:string,time:int,unit:string,tag:string,tag_class:string,title:string,days:int}>
|
||||
*/
|
||||
public function parse_level_products(string $raw): array {
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
@@ -28,8 +28,8 @@ trait VipServicePackages {
|
||||
continue;
|
||||
}
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
$price = isset($parts[0]) ? (float) $parts[0] : 0.0;
|
||||
if ($price <= 0) {
|
||||
$price_minor = $this->parse_vip_money_minor($parts[0] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ trait VipServicePackages {
|
||||
// Or: price|show_price|time|unit|title (5)
|
||||
if ($n >= 4 && (in_array(strtolower((string) ($parts[3] ?? '')), ['day', 'month', 'd', 'm'], true)
|
||||
|| (isset($parts[3]) && $parts[3] !== '' && !is_numeric($parts[3])))) {
|
||||
$show_price = isset($parts[1]) ? (float) $parts[1] : 0.0;
|
||||
$show_minor = $this->parse_vip_money_minor($parts[1] ?? '0');
|
||||
$time = isset($parts[2]) ? (int) $parts[2] : 0;
|
||||
$unit_raw = strtolower((string) ($parts[3] ?? 'month'));
|
||||
$unit = ($unit_raw === 'day' || $unit_raw === 'd') ? 'day' : 'month';
|
||||
@@ -53,7 +53,7 @@ trait VipServicePackages {
|
||||
}
|
||||
} else {
|
||||
// Legacy: price|days|title
|
||||
$show_price = 0.0;
|
||||
$show_minor = 0;
|
||||
$days_legacy = isset($parts[1]) ? (int) $parts[1] : 0;
|
||||
$time = $days_legacy;
|
||||
$unit = 'day';
|
||||
@@ -74,8 +74,8 @@ trait VipServicePackages {
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'price' => round($price, 2),
|
||||
'show_price' => max(0.0, round($show_price, 2)),
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
||||
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor),
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'tag' => $tag,
|
||||
@@ -103,14 +103,14 @@ trait VipServicePackages {
|
||||
/**
|
||||
* @deprecated Use parse_level_products — kept as thin adapter for upgrade fixed lines.
|
||||
*
|
||||
* @return array<int, array{price:float,days:int,title:string}>
|
||||
* @return array<int, array{price:string,days:int,title:string}>
|
||||
*/
|
||||
public function parse_packages(string $raw): array {
|
||||
$parsed = $this->parse_level_products($raw);
|
||||
$out = [];
|
||||
foreach ($parsed as $p) {
|
||||
$out[] = [
|
||||
'price' => (float) $p['price'],
|
||||
'price' => (string) $p['price'],
|
||||
'days' => (int) $p['days'],
|
||||
'title' => (string) $p['title'],
|
||||
];
|
||||
@@ -161,12 +161,16 @@ trait VipServicePackages {
|
||||
if ($is_permanent || $expires <= time() || $has_unit) {
|
||||
continue;
|
||||
}
|
||||
$daily = isset($parts[1]) ? (float) $parts[1] : 0.0;
|
||||
if ($daily <= 0) {
|
||||
$daily_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($daily_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
$remaining = (int) ceil(max(1, ($expires - time()) / DAY_IN_SECONDS));
|
||||
$price = round(max(0.01, $remaining * $daily), 2);
|
||||
if ($daily_minor > intdiv(PHP_INT_MAX, $remaining)) {
|
||||
continue;
|
||||
}
|
||||
$price_minor = $remaining * $daily_minor;
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = sprintf(__('按剩余 %d 天升级', 'aksrt'), $remaining);
|
||||
@@ -178,7 +182,7 @@ trait VipServicePackages {
|
||||
'action' => 'upgrade',
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => 0.0,
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => __('剩余天数', 'aksrt'),
|
||||
'days' => $remaining,
|
||||
'title' => $title,
|
||||
@@ -198,28 +202,30 @@ trait VipServicePackages {
|
||||
if ($is_permanent || $has_jump) {
|
||||
continue;
|
||||
}
|
||||
$price = isset($parts[1]) ? (float) $parts[1] : 0.0;
|
||||
if ($price <= 0) {
|
||||
$price_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = __('升级为永久会员', 'aksrt');
|
||||
}
|
||||
$show = isset($parts[3]) ? (float) $parts[3] : 0.0;
|
||||
$show_minor = $this->parse_vip_money_minor($parts[3] ?? '0');
|
||||
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 903),
|
||||
'level' => 2,
|
||||
'index' => 903,
|
||||
'action' => 'upgrade',
|
||||
'price' => round($price, 2),
|
||||
'base_price' => round($price, 2),
|
||||
'show_price' => max(0.0, $show),
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => $show,
|
||||
'discount_tag' => __('永久升级', 'aksrt'),
|
||||
'days' => 0,
|
||||
'title' => $title,
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => $show > 0 ? $this->format_cash_label($show) : '',
|
||||
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
||||
'days_label' => __('永久', 'aksrt'),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
@@ -234,28 +240,30 @@ trait VipServicePackages {
|
||||
if ($has_permanent) {
|
||||
continue;
|
||||
}
|
||||
$price = isset($parts[1]) ? (float) $parts[1] : 0.0;
|
||||
if ($price <= 0) {
|
||||
$price_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = __('永久会员升级', 'aksrt');
|
||||
}
|
||||
$show = isset($parts[3]) ? (float) $parts[3] : 0.0;
|
||||
$show_minor = $this->parse_vip_money_minor($parts[3] ?? '0');
|
||||
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 901),
|
||||
'level' => 2,
|
||||
'index' => 901,
|
||||
'action' => 'upgrade',
|
||||
'price' => round($price, 2),
|
||||
'base_price' => round($price, 2),
|
||||
'show_price' => max(0.0, $show),
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => $show,
|
||||
'discount_tag' => __('升级专享', 'aksrt'),
|
||||
'days' => 0,
|
||||
'title' => $title,
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => $show > 0 ? $this->format_cash_label($show) : '',
|
||||
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
||||
'days_label' => __('永久', 'aksrt'),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
@@ -280,13 +288,13 @@ trait VipServicePackages {
|
||||
'level' => 2,
|
||||
'index' => 901,
|
||||
'action' => 'upgrade',
|
||||
'price' => (float) $pkg['price'],
|
||||
'base_price' => (float) $pkg['price'],
|
||||
'show_price' => 0.0,
|
||||
'price' => (string) $pkg['price'],
|
||||
'base_price' => (string) $pkg['price'],
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => __('升级专享', 'aksrt'),
|
||||
'days' => (int) $pkg['days'],
|
||||
'title' => (string) $pkg['title'],
|
||||
'price_label' => $this->format_cash_label((float) $pkg['price']),
|
||||
'price_label' => $this->format_cash_label((string) $pkg['price']),
|
||||
'show_price_label' => '',
|
||||
'days_label' => ((int) $pkg['days'] <= 0)
|
||||
? __('永久', 'aksrt')
|
||||
@@ -399,10 +407,10 @@ trait VipServicePackages {
|
||||
*
|
||||
* @param array<string,mixed> $renew
|
||||
* @param array<string,mixed> $upgrade
|
||||
* @return array{price:float,show_price:float,tag:string}|null
|
||||
* @return array{price:string,show_price:string,tag:string}|null
|
||||
*/
|
||||
private function apply_action_price(
|
||||
float $base_price,
|
||||
string $base_price,
|
||||
string $action,
|
||||
array $renew,
|
||||
array $upgrade,
|
||||
@@ -412,7 +420,8 @@ trait VipServicePackages {
|
||||
bool $is_permanent,
|
||||
int $days
|
||||
): ?array {
|
||||
if ($base_price <= 0) {
|
||||
$base_minor = $this->parse_vip_money_minor($base_price);
|
||||
if ($base_minor <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -422,57 +431,59 @@ trait VipServicePackages {
|
||||
|
||||
if ($action !== 'renew') {
|
||||
return [
|
||||
'price' => round($base_price, 2),
|
||||
'show_price' => 0.0,
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($base_minor),
|
||||
'show_price' => '0.00',
|
||||
'tag' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$type = sanitize_key((string) ($renew['price_type'] ?? 'discount'));
|
||||
$price = $base_price;
|
||||
$show = $base_price;
|
||||
$price_minor = $base_minor;
|
||||
$show_minor = $base_minor;
|
||||
$tag = '';
|
||||
|
||||
if ($type === 'discount') {
|
||||
$discount = (float) ($renew['discount'] ?? 8);
|
||||
$discount = (int) ($renew['discount'] ?? 8);
|
||||
if ($discount <= 0) {
|
||||
$discount = 8;
|
||||
}
|
||||
if ($discount <= 10) {
|
||||
$price = round($base_price * ($discount / 10), 2);
|
||||
$percent = $discount * 10;
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $percent);
|
||||
$tag = sprintf(
|
||||
/* translators: %s: discount like 8 */
|
||||
__('%s折续费', 'aksrt'),
|
||||
rtrim(rtrim(number_format($discount, 1, '.', ''), '0'), '.')
|
||||
(string) $discount
|
||||
);
|
||||
} else {
|
||||
$price = round($base_price * (min(100, $discount) / 100), 2);
|
||||
$percent = min(100, $discount);
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $percent);
|
||||
$tag = sprintf(__('%s%% 续费价', 'aksrt'), (string) (int) $discount);
|
||||
}
|
||||
} elseif ($type === 'reduce') {
|
||||
$reduce = max(0, (float) ($renew['reduce'] ?? 20));
|
||||
$price = round(max(0.01, $base_price - $reduce), 2);
|
||||
$reduce_minor = $this->parse_vip_money_minor($renew['reduce'] ?? '20');
|
||||
$price_minor = max(1, $base_minor - $reduce_minor);
|
||||
$tag = sprintf(
|
||||
/* translators: %s: amount */
|
||||
__('续费立减%s', 'aksrt'),
|
||||
$this->format_cash_label($reduce)
|
||||
$this->format_cash_label(\AKSRT\Ledger\AssetAmount::decimal_from_minor($reduce_minor))
|
||||
);
|
||||
} else {
|
||||
$show = 0.0;
|
||||
$show_minor = 0;
|
||||
$tag = __('续费同价', 'aksrt');
|
||||
}
|
||||
|
||||
if ($price < 0.01) {
|
||||
$price = 0.01;
|
||||
if ($price_minor < 1) {
|
||||
$price_minor = 1;
|
||||
}
|
||||
if ($show > 0 && $price >= $show) {
|
||||
$show = 0.0;
|
||||
if ($show_minor > 0 && $price_minor >= $show_minor) {
|
||||
$show_minor = 0;
|
||||
$tag = __('续费', 'aksrt');
|
||||
}
|
||||
|
||||
return [
|
||||
'price' => $price,
|
||||
'show_price' => $show > $price ? $show : 0.0,
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
||||
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor > $price_minor ? $show_minor : 0),
|
||||
'tag' => $tag,
|
||||
];
|
||||
}
|
||||
@@ -532,7 +543,7 @@ trait VipServicePackages {
|
||||
|
||||
|
||||
|
||||
private function format_cash_label(float $amount): string {
|
||||
private function format_cash_label(string $amount): string {
|
||||
$symbol = '¥';
|
||||
if (method_exists($this, 'get_paid_display_context')) {
|
||||
$paid = $this->get_paid_display_context();
|
||||
@@ -545,6 +556,17 @@ trait VipServicePackages {
|
||||
return $symbol . $n;
|
||||
}
|
||||
|
||||
private function parse_vip_money_minor($value): int {
|
||||
if (!is_string($value)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance_minor($value);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function format_expires_label(int $expires, int $level): string {
|
||||
|
||||
@@ -143,7 +143,7 @@ trait VipServicePurchase {
|
||||
? $this->parse_level_products((string) ($levels[$lv]['products_raw'] ?? ''))
|
||||
: [];
|
||||
foreach ($packages as $idx => $pkg) {
|
||||
$base_price = (float) $pkg['price'];
|
||||
$base_price = (string) $pkg['price'];
|
||||
$days = (int) $pkg['days'];
|
||||
$action = 'pay';
|
||||
if ($level > 0 && $lv === $level) {
|
||||
@@ -157,9 +157,11 @@ trait VipServicePurchase {
|
||||
continue;
|
||||
}
|
||||
|
||||
$show_price = (float) ($priced['show_price'] ?? 0);
|
||||
if ($show_price <= 0 && !empty($pkg['show_price']) && $action === 'pay') {
|
||||
$show_price = (float) $pkg['show_price'];
|
||||
$show_price = (string) ($priced['show_price'] ?? '0.00');
|
||||
$show_minor = $this->parse_vip_money_minor($show_price);
|
||||
if ($show_minor <= 0 && $this->parse_vip_money_minor($pkg['show_price'] ?? '0') > 0 && $action === 'pay') {
|
||||
$show_price = (string) $pkg['show_price'];
|
||||
$show_minor = $this->parse_vip_money_minor($show_price);
|
||||
}
|
||||
$discount_tag = (string) ($priced['tag'] ?? '');
|
||||
if ($discount_tag === '' && $action === 'pay' && !empty($pkg['tag'])) {
|
||||
@@ -178,7 +180,7 @@ trait VipServicePurchase {
|
||||
'level' => $lv,
|
||||
'index' => $idx + 1,
|
||||
'action' => $action,
|
||||
'price' => (float) $priced['price'],
|
||||
'price' => (string) $priced['price'],
|
||||
'base_price' => $base_price,
|
||||
'show_price' => $show_price,
|
||||
'discount_tag' => $discount_tag,
|
||||
@@ -186,8 +188,8 @@ trait VipServicePurchase {
|
||||
'time' => (int) ($pkg['time'] ?? 0),
|
||||
'unit' => (string) ($pkg['unit'] ?? 'day'),
|
||||
'title' => (string) $pkg['title'],
|
||||
'price_label' => $this->format_cash_label((float) $priced['price']),
|
||||
'show_price_label' => $show_price > 0
|
||||
'price_label' => $this->format_cash_label((string) $priced['price']),
|
||||
'show_price_label' => $show_minor > 0
|
||||
? $this->format_cash_label($show_price)
|
||||
: '',
|
||||
'days_label' => $days_label,
|
||||
@@ -230,12 +232,8 @@ trait VipServicePurchase {
|
||||
}
|
||||
}
|
||||
|
||||
$points_balance = ($user_id > 0 && class_exists('\AKSRT\Credits\Credits'))
|
||||
? (int) \AKSRT\Credits\Credits::balance($user_id)
|
||||
: 0;
|
||||
$wallet_balance = ($user_id > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled())
|
||||
? (float) \AKSRT\Credits\Balance::balance($user_id)
|
||||
: 0.0;
|
||||
$points_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS) : 0;
|
||||
$wallet_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : '0.00';
|
||||
$methods = $this->available_payment_methods();
|
||||
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
|
||||
@@ -304,7 +302,7 @@ trait VipServicePurchase {
|
||||
/**
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function purchase(int $user_id, int $package_id, string $payment_method = 'balance') {
|
||||
public function purchase(int $user_id, int $package_id, string $payment_method = 'balance', string $operation_key = '') {
|
||||
if ($user_id <= 0) {
|
||||
return new \WP_Error('aksrt_need_login', __('请先登录后再开通会员。', 'aksrt'), ['status' => 401]);
|
||||
}
|
||||
@@ -322,8 +320,9 @@ trait VipServicePurchase {
|
||||
$level = (int) ($pkg['level'] ?? 0);
|
||||
$index = (int) ($pkg['index'] ?? 0);
|
||||
$action = sanitize_key((string) ($pkg['action'] ?? 'pay'));
|
||||
$amount = round((float) ($pkg['price'] ?? 0), 2);
|
||||
$base_price = (float) ($pkg['base_price'] ?? $amount);
|
||||
$amount = is_string($pkg['price'] ?? null) ? (string) $pkg['price'] : '';
|
||||
$amount_minor = $this->parse_vip_money_minor($amount);
|
||||
$base_price = is_string($pkg['base_price'] ?? null) ? (string) $pkg['base_price'] : $amount;
|
||||
$days = (int) ($pkg['days'] ?? 0);
|
||||
$pkg_time = array_key_exists('time', $pkg) ? (int) $pkg['time'] : $days;
|
||||
$pkg_unit = sanitize_key((string) ($pkg['unit'] ?? 'day'));
|
||||
@@ -341,9 +340,10 @@ trait VipServicePurchase {
|
||||
return new \WP_Error('aksrt_invalid_package', __('套餐无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
if ($amount <= 0) {
|
||||
if ($amount_minor <= 0) {
|
||||
return new \WP_Error('aksrt_invalid_price', __('套餐价格无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$amount_decimal = \AKSRT\Ledger\AssetAmount::decimal_from_minor($amount_minor);
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用', 'aksrt'), ['status' => 500]);
|
||||
@@ -370,21 +370,63 @@ trait VipServicePurchase {
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$expires_at = \AKSRT\Settings\PaymentRuntime::get_order_expiry_at();
|
||||
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
$existing = $operation_key !== '' ? $repo->get_by_request_key($operation_key) : null;
|
||||
$is_new_order = !is_array($existing);
|
||||
if (is_array($existing)) {
|
||||
$existing_meta = json_decode((string) ($existing['note'] ?? ''), true);
|
||||
$existing_meta = is_array($existing_meta) ? $existing_meta : [];
|
||||
if ((int) ($existing['user_id'] ?? 0) !== $user_id
|
||||
|| sanitize_key((string) ($existing['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP
|
||||
|| (int) ($existing['product_id'] ?? 0) !== $package_id
|
||||
|| (string) ($existing['amount'] ?? '') !== $amount_decimal
|
||||
|| strtoupper((string) ($existing['currency'] ?? '')) !== 'CNY'
|
||||
|| sanitize_key((string) ($existing['payment_method'] ?? '')) !== $payment_method
|
||||
|| (int) ($existing_meta['level'] ?? 0) !== $level
|
||||
|| (int) ($existing_meta['days'] ?? 0) !== $days
|
||||
|| (int) ($existing_meta['time'] ?? 0) !== $pkg_time
|
||||
|| sanitize_key((string) ($existing_meta['unit'] ?? '')) !== $pkg_unit
|
||||
|| sanitize_key((string) ($existing_meta['action'] ?? '')) !== $action
|
||||
) {
|
||||
return new \WP_Error('aksrt_ledger_idempotency_conflict', __('相同操作标识已用于其他会员购买请求', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
$order_number = (string) $existing['order_number'];
|
||||
} else {
|
||||
$order_number = $repo->create([
|
||||
'user_id' => $user_id,
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
|
||||
'product_id' => $package_id,
|
||||
'amount' => $amount,
|
||||
'amount' => $amount_decimal,
|
||||
'currency' => 'CNY',
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => $payment_method,
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => $expires_at,
|
||||
]);
|
||||
'request_key' => $operation_key,
|
||||
]);
|
||||
}
|
||||
if ($order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
if (is_array($existing) && in_array((string) ($existing['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true)) {
|
||||
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('order-payment', [$order_number]);
|
||||
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount_decimal,
|
||||
'level' => $level,
|
||||
'days' => $days,
|
||||
'action' => $action,
|
||||
'transaction_id' => (int) ($history['items'][0]['id'] ?? 0),
|
||||
'replayed' => true,
|
||||
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
'message' => __('会员购买已完成', 'aksrt'),
|
||||
'reload' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
// Build the request-local order context from the same server-derived
|
||||
// package used for validation. PaymentOrchestrator still reloads the
|
||||
// persisted row under its attempt lock before touching the gateway.
|
||||
@@ -395,7 +437,7 @@ trait VipServicePurchase {
|
||||
'user_id' => $user_id,
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
|
||||
'product_id' => $package_id,
|
||||
'amount' => $amount,
|
||||
'amount' => $amount_decimal,
|
||||
'currency' => 'CNY',
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => $payment_method,
|
||||
@@ -430,7 +472,7 @@ trait VipServicePurchase {
|
||||
return new \WP_Error(
|
||||
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
|
||||
(string) ($payment_result['message'] ?? __('支付失败', 'aksrt')),
|
||||
['status' => 402, 'order_number' => $order_number]
|
||||
['status' => (int) ($payment_result['status'] ?? 400), 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -442,10 +484,12 @@ trait VipServicePurchase {
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'amount' => $amount_decimal,
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
'transaction_id' => 0,
|
||||
'replayed' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -454,10 +498,13 @@ trait VipServicePurchase {
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'amount' => $amount_decimal,
|
||||
'level' => $level,
|
||||
'days' => $days,
|
||||
'action' => $action,
|
||||
'transaction_id' => (int) ($payment_result['ledger_transaction_id'] ?? 0),
|
||||
'replayed' => !empty($payment_result['replayed']),
|
||||
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
'message' => $action === 'renew'
|
||||
? __('会员续费成功', 'aksrt')
|
||||
: ($action === 'upgrade' ? __('会员升级成功', 'aksrt') : __('会员开通成功', 'aksrt')),
|
||||
@@ -474,16 +521,16 @@ trait VipServicePurchase {
|
||||
* @param string $order_number
|
||||
* @param array $paid_order
|
||||
*/
|
||||
public function on_order_paid(string $order_number, array $paid_order): void {
|
||||
public function fulfill_paid_order(string $order_number, array $paid_order) {
|
||||
if (sanitize_key((string) ($paid_order['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (sanitize_key((string) ($paid_order['payment_method'] ?? '')) === 'card_pass') {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
$user_id = (int) ($paid_order['user_id'] ?? 0);
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
$meta = [];
|
||||
@@ -497,7 +544,7 @@ trait VipServicePurchase {
|
||||
$meta = $paid_order['meta_payload'];
|
||||
}
|
||||
if (!empty($meta['card_pass']) || sanitize_key((string) ($meta['vip_pay_type'] ?? '')) === 'card_pass') {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
$level = (int) ($meta['level'] ?? 0);
|
||||
@@ -561,13 +608,20 @@ trait VipServicePurchase {
|
||||
$fulfilled = (int) get_user_meta($user_id, 'aksrt_vip_level', true) >= max($previous_level, $level)
|
||||
&& ($new_expiry === 0 || $days <= 0 || $new_expiry >= max($previous_expiry, time()) + ($days * DAY_IN_SECONDS));
|
||||
}
|
||||
if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
|
||||
if (!$fulfilled) {
|
||||
return new \WP_Error('aksrt_vip_fulfillment_failed', __('会员权益发放失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
if (class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$completed = \AKSRT\Payment\OrderRepository::get_instance()->update_status(
|
||||
$order_number,
|
||||
\AKSRT\Payment\Order::STATUS_COMPLETED,
|
||||
['note' => __('会员权益已发放。', 'aksrt')]
|
||||
);
|
||||
if (!$completed) {
|
||||
return new \WP_Error('aksrt_vip_fulfillment_failed', __('会员订单完成失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -673,16 +727,13 @@ trait VipServicePurchase {
|
||||
*
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function exchange_with_points(int $user_id, int $package_id) {
|
||||
public function exchange_with_points(int $user_id, int $package_id, string $operation_key = '') {
|
||||
if ($user_id <= 0) {
|
||||
return new \WP_Error('aksrt_need_login', __('请先登录后再兑换。', 'aksrt'), ['status' => 401]);
|
||||
}
|
||||
if (!class_exists('\AKSRT\Credits\Credits')) {
|
||||
return new \WP_Error('aksrt_credits_unavailable', __('积分系统不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$catalog = $this->get_catalog($user_id);
|
||||
if (array_key_exists('credits_enabled', $catalog) && empty($catalog['credits_enabled'])) {
|
||||
return new \WP_Error('aksrt_credits_disabled', __('积分功能未启用', 'aksrt'), ['status' => 400]);
|
||||
return new \WP_Error('aksrt_points_disabled', __('积分功能未启用', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$points_ex = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : [];
|
||||
if (empty($points_ex['enabled'])) {
|
||||
@@ -719,7 +770,7 @@ trait VipServicePurchase {
|
||||
|
||||
$balance = (int) ($catalog['user_points'] ?? 0);
|
||||
if ($balance < $cost) {
|
||||
return new \WP_Error('aksrt_credits_insufficient', __('积分不足,暂时无法兑换', 'aksrt'), [
|
||||
return new \WP_Error('aksrt_points_insufficient', __('积分不足,暂时无法兑换', 'aksrt'), [
|
||||
'status' => 402,
|
||||
'balance' => $balance,
|
||||
'cost' => $cost,
|
||||
@@ -741,62 +792,91 @@ trait VipServicePurchase {
|
||||
'action' => 'points_exchange',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
|
||||
'user_id' => $user_id,
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
|
||||
'product_id' => $package_id,
|
||||
'amount' => (float) $cost,
|
||||
'currency' => 'points',
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => 'points',
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
if ($order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
$existing = $operation_key !== '' ? $repo->get_by_request_key($operation_key) : null;
|
||||
$is_new_order = !is_array($existing);
|
||||
if (is_array($existing)) {
|
||||
$existing_meta = json_decode((string) ($existing['note'] ?? ''), true);
|
||||
$existing_meta = is_array($existing_meta) ? $existing_meta : [];
|
||||
if ((int) ($existing['user_id'] ?? 0) !== $user_id
|
||||
|| sanitize_key((string) ($existing['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP
|
||||
|| (int) ($existing['product_id'] ?? 0) !== $package_id
|
||||
|| (string) ($existing['currency'] ?? '') !== 'points'
|
||||
|| (int) ($existing['amount'] ?? 0) !== $cost
|
||||
|| sanitize_key((string) ($existing['payment_method'] ?? '')) !== 'points'
|
||||
|| (int) ($existing_meta['level'] ?? 0) !== $level
|
||||
|| (int) ($existing_meta['days'] ?? 0) !== $days
|
||||
) {
|
||||
return new \WP_Error('aksrt_ledger_idempotency_conflict', __('相同操作标识已用于其他会员兑换请求', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
$order_number = (string) $existing['order_number'];
|
||||
} else {
|
||||
$order_number = $repo->allocate_order_number();
|
||||
}
|
||||
if ($order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$guard = static function () use ($is_new_order, $order_number, $user_id, $package_id, $cost) {
|
||||
if ($is_new_order) {
|
||||
return true;
|
||||
}
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('order');
|
||||
$current = $wpdb->get_row($wpdb->prepare("SELECT user_id, product_id, amount, currency, status FROM {$table} WHERE order_number = %s FOR UPDATE", $order_number), ARRAY_A);
|
||||
if (!is_array($current)
|
||||
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
||||
|| (int) ($current['product_id'] ?? 0) !== $package_id
|
||||
|| (int) ($current['amount'] ?? 0) !== $cost
|
||||
|| (string) ($current['currency'] ?? '') !== 'points'
|
||||
|| !in_array((string) ($current['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_PENDING], true)
|
||||
) {
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态或金额已变化', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
$lock = class_exists('\AKSRT\Payment\PaymentAttemptLock')
|
||||
? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number)
|
||||
: false;
|
||||
if ($lock === false) {
|
||||
return new \WP_Error('aksrt_pay_locked', __('该订单正在发起支付,请稍后重试。', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
|
||||
try {
|
||||
$deducted = \AKSRT\Credits\Credits::deduct(
|
||||
$user_id,
|
||||
$cost,
|
||||
'vip_points_exchange',
|
||||
['order' => $order_number, 'level' => $level, 'days' => $days],
|
||||
sprintf(__('积分兑换会员 · %s', 'aksrt'), (string) ($match['title'] ?? ''))
|
||||
);
|
||||
if (is_wp_error($deducted) || $deducted === false) {
|
||||
\AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [
|
||||
'note' => __('积分扣除失败,自动取消', 'aksrt'),
|
||||
$ledger = \AKSRT\Ledger\LedgerService::get_instance();
|
||||
$deducted = $ledger->debit(
|
||||
$user_id,
|
||||
\AKSRT\Ledger\AssetType::POINTS,
|
||||
$cost,
|
||||
$operation_key,
|
||||
'vip_points_exchange',
|
||||
['type' => 'order', 'key' => $order_number],
|
||||
sprintf(__('积分兑换会员 · %s', 'aksrt'), (string) ($match['title'] ?? '')),
|
||||
$user_id,
|
||||
['level' => $level, 'days' => $days, 'package_id' => $package_id],
|
||||
static function () use ($is_new_order, $repo, $order_number, $operation_key, $user_id, $package_id, $cost, $note) {
|
||||
if ($is_new_order) {
|
||||
$created = $repo->create([
|
||||
'order_number' => $order_number,
|
||||
'user_id' => $user_id,
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
|
||||
'product_id' => $package_id,
|
||||
'amount' => $cost,
|
||||
'currency' => 'points',
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => 'points',
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
'request_key' => $operation_key,
|
||||
]);
|
||||
if ($created !== $order_number) {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
}
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
|
||||
'payment_method' => 'points',
|
||||
'payment_transaction_id' => 'points_' . $order_number,
|
||||
'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost),
|
||||
'require_transition' => true,
|
||||
]);
|
||||
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt');
|
||||
return new \WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
|
||||
'payment_method' => 'points',
|
||||
'payment_transaction_id' => 'points_' . $order_number,
|
||||
'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost),
|
||||
'require_transition' => true,
|
||||
]);
|
||||
if (!$paid) {
|
||||
\AKSRT\Credits\Credits::add(
|
||||
$user_id,
|
||||
$cost,
|
||||
'vip_points_exchange_refund',
|
||||
['order' => $order_number],
|
||||
__('会员兑换失败退款', 'aksrt')
|
||||
);
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('兑换确认失败,已尝试退回积分', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
} finally {
|
||||
\AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock);
|
||||
return $paid ? true : new \WP_Error('aksrt_order_confirm_failed', __('兑换确认失败', 'aksrt'), ['status' => 500]);
|
||||
},
|
||||
$guard
|
||||
);
|
||||
if (is_wp_error($deducted)) {
|
||||
return $deducted;
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -806,7 +886,9 @@ trait VipServicePurchase {
|
||||
'points' => $cost,
|
||||
'level' => $level,
|
||||
'days' => $days,
|
||||
'balance' => \AKSRT\Credits\Credits::balance($user_id),
|
||||
'balance' => $ledger->balance($user_id, \AKSRT\Ledger\AssetType::POINTS),
|
||||
'transaction_id' => $deducted->transaction_id,
|
||||
'replayed' => $deducted->replayed,
|
||||
'message' => __('兑换成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
],
|
||||
|
||||
@@ -22,6 +22,10 @@ trait VipServiceRest {
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_purchase'],
|
||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||
'args' => [
|
||||
'package_id' => ['type' => 'integer', 'required' => true, 'minimum' => 1],
|
||||
'payment_method' => ['type' => 'string', 'sanitize_callback' => 'sanitize_key'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -29,6 +33,7 @@ trait VipServiceRest {
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_points_exchange'],
|
||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||
'args' => ['package_id' => ['type' => 'integer', 'required' => true, 'minimum' => 1]],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/pay/vip/modal', [
|
||||
@@ -112,12 +117,16 @@ trait VipServiceRest {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$package_id = (int) ($params['package_id'] ?? 0);
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'vip-purchase', $uid);
|
||||
if (is_wp_error($operation_key)) {
|
||||
return $operation_key;
|
||||
}
|
||||
$payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance'));
|
||||
if ($payment_method === '' || $payment_method === 'cash') {
|
||||
$payment_method = 'balance';
|
||||
}
|
||||
|
||||
$result = $this->purchase($uid, $package_id, $payment_method);
|
||||
$result = $this->purchase($uid, $package_id, $payment_method, $operation_key);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
@@ -147,7 +156,11 @@ trait VipServiceRest {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$package_id = (int) ($params['package_id'] ?? 0);
|
||||
$result = $this->exchange_with_points($uid, $package_id);
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'vip-points-purchase', $uid);
|
||||
if (is_wp_error($operation_key)) {
|
||||
return $operation_key;
|
||||
}
|
||||
$result = $this->exchange_with_points($uid, $package_id, $operation_key);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
+34
-20
@@ -43,10 +43,11 @@ class Visibility {
|
||||
? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0')
|
||||
: ((($pay_meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0');
|
||||
$price = is_array($priced)
|
||||
? (float) ($priced['amount'] ?? 0)
|
||||
? ($priced['amount'] ?? ($pay_modo === 'points' ? 0 : '0.00'))
|
||||
: (($pay_modo === 'points')
|
||||
? (float) ($pay_meta['points_price'] ?? 0)
|
||||
: (float) ($pay_meta['price'] ?? 0));
|
||||
? (int) ($pay_meta['points_price'] ?? 0)
|
||||
: (string) ($pay_meta['price'] ?? '0.00'));
|
||||
$price_minor = self::asset_minor($price, $pay_modo);
|
||||
$state = [
|
||||
'post_id' => $resource_post instanceof \WP_Post ? (int) $resource_post->ID : 0,
|
||||
'pay_type' => $pay_type,
|
||||
@@ -75,7 +76,7 @@ class Visibility {
|
||||
return $state;
|
||||
}
|
||||
|
||||
$has_free_access = $price <= 0 && $pay_type !== \AKSRT\Core\DomainModel::PAY_TYPE_VIP;
|
||||
$has_free_access = $price_minor <= 0 && $pay_type !== \AKSRT\Core\DomainModel::PAY_TYPE_VIP;
|
||||
$pay_limit = (int) ($pay_meta['pay_limit'] ?? 0);
|
||||
if (!in_array($pay_limit, [0, 1, 2], true)) {
|
||||
$pay_limit = 0;
|
||||
@@ -120,19 +121,19 @@ class Visibility {
|
||||
if ($pay_type === \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD) {
|
||||
// Cash: order gate only. Points: cashier once → paid order zeros cost.
|
||||
$download_cost = 0;
|
||||
if ($pay_modo === 'points' && $price > 0 && !$has_paid_order) {
|
||||
$download_cost = (int) ceil($price);
|
||||
if ($pay_modo === 'points' && $price_minor > 0 && !$has_paid_order) {
|
||||
$download_cost = $price_minor;
|
||||
}
|
||||
if ($allowed_member && $price <= 0) {
|
||||
if ($allowed_member && $price_minor <= 0) {
|
||||
$download_cost = 0;
|
||||
}
|
||||
$can_download = function_exists('aksrt_user_can') && aksrt_user_can('resource_download', $uid, (int) $resource_post->ID);
|
||||
$state['download_cost'] = max(0, $download_cost);
|
||||
$state['need_login'] = !$can_download && $uid <= 0;
|
||||
$state['need_purchase'] = $allowed_member && $price > 0 && !$has_paid_order;
|
||||
$state['has_paid_access'] = $allowed_member && ($has_paid_order || $price <= 0);
|
||||
$state['need_purchase'] = $allowed_member && $price_minor > 0 && !$has_paid_order;
|
||||
$state['has_paid_access'] = $allowed_member && ($has_paid_order || $price_minor <= 0);
|
||||
$state['can_view'] = true;
|
||||
$state['can_download'] = $can_download && $allowed_member && ($has_paid_order || $price <= 0);
|
||||
$state['can_download'] = $can_download && $allowed_member && ($has_paid_order || $price_minor <= 0);
|
||||
if (!$allowed_member) {
|
||||
$state['can_download'] = false;
|
||||
$state['has_paid_access'] = false;
|
||||
@@ -144,7 +145,7 @@ class Visibility {
|
||||
|
||||
// reading/gallery/video with pay_limit
|
||||
if ($allowed_member) {
|
||||
$state['has_paid_access'] = $has_paid_order || $price <= 0;
|
||||
$state['has_paid_access'] = $has_paid_order || $price_minor <= 0;
|
||||
$state['need_purchase'] = !$state['has_paid_access'];
|
||||
$state['need_vip'] = false;
|
||||
} else {
|
||||
@@ -160,8 +161,8 @@ class Visibility {
|
||||
if ($pay_type === \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD) {
|
||||
// legacy: cash = order unlock; points = one-shot cashier order (not per-download deduct).
|
||||
$download_cost = 0;
|
||||
if ($pay_modo === 'points' && $price > 0 && !$has_paid_order) {
|
||||
$download_cost = (int) ceil($price);
|
||||
if ($pay_modo === 'points' && $price_minor > 0 && !$has_paid_order) {
|
||||
$download_cost = $price_minor;
|
||||
}
|
||||
$state['download_cost'] = max(0, $download_cost);
|
||||
|
||||
@@ -169,10 +170,10 @@ class Visibility {
|
||||
$state['need_login'] = !$can_download && $uid <= 0;
|
||||
$state['require_captcha'] = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
|
||||
|
||||
$state['need_purchase'] = !$has_paid_order && $price > 0;
|
||||
$state['has_paid_access'] = $has_paid_order || $price <= 0 || $has_free_access;
|
||||
$state['need_purchase'] = !$has_paid_order && $price_minor > 0;
|
||||
$state['has_paid_access'] = $has_paid_order || $price_minor <= 0 || $has_free_access;
|
||||
$state['can_view'] = true;
|
||||
$state['can_download'] = $can_download && ($has_paid_order || $price <= 0);
|
||||
$state['can_download'] = $can_download && ($has_paid_order || $price_minor <= 0);
|
||||
|
||||
return $state;
|
||||
}
|
||||
@@ -190,7 +191,7 @@ class Visibility {
|
||||
$state['need_purchase'] = !$has_free_access && !$has_paid_order;
|
||||
$state['has_paid_access'] = $has_free_access || $has_paid_order;
|
||||
// VIP free tier for reading etc. via amount.
|
||||
if (!$state['has_paid_access'] && $price <= 0 && $pay_type !== \AKSRT\Core\DomainModel::PAY_TYPE_VIP) {
|
||||
if (!$state['has_paid_access'] && $price_minor <= 0 && $pay_type !== \AKSRT\Core\DomainModel::PAY_TYPE_VIP) {
|
||||
$state['has_paid_access'] = true;
|
||||
$state['need_purchase'] = false;
|
||||
}
|
||||
@@ -253,11 +254,10 @@ class Visibility {
|
||||
// Cash or points: require cashier purchase (resource order) before download token.
|
||||
if (!empty($state['need_purchase']) && empty($state['has_paid_access'])) {
|
||||
$pay_modo = (string) ($state['pay_modo'] ?? '0');
|
||||
$price = (float) ($state['price'] ?? 0);
|
||||
return new \WP_Error('aksrt_need_purchase', __('请先购买后再下载。', 'aksrt'), [
|
||||
'status' => 402,
|
||||
'pay_modo' => $pay_modo,
|
||||
'amount' => $pay_modo === 'points' ? (int) ceil($price) : $price,
|
||||
'amount' => $state['price'] ?? ($pay_modo === 'points' ? 0 : '0.00'),
|
||||
'post_id' => (int) ($state['post_id'] ?? 0),
|
||||
]);
|
||||
}
|
||||
@@ -270,7 +270,7 @@ class Visibility {
|
||||
return $over;
|
||||
}
|
||||
$m = aksrt_get_pay_meta($resource_post);
|
||||
return max(0.0, (float) ($m['download_limit_over_price'] ?? 0));
|
||||
return $m['download_limit_over_price'] ?? 0;
|
||||
};
|
||||
add_filter('aksrt_download_limit_over_price', $over_cb, 10, 2);
|
||||
$quota = aksrt_check_free_download_quota($uid, $cost);
|
||||
@@ -340,6 +340,20 @@ class Visibility {
|
||||
return $html;
|
||||
}
|
||||
|
||||
private static function asset_minor($value, string $pay_modo): int {
|
||||
if ($pay_modo === 'points') {
|
||||
return is_int($value) ? max(0, $value) : 0;
|
||||
}
|
||||
if (!is_string($value)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance_minor($value);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private function is_unlocked(string $mode, int $pid): bool {
|
||||
switch ($mode) {
|
||||
case 'comment':
|
||||
|
||||
Reference in New Issue
Block a user