积分与余额账务系统硬切重构

This commit is contained in:
kate522 committed 2026-08-24 15:59:11 +08:00
1 parent ca12226b39
commit 5cae5b5c93
159 files changed
+7275 -4718

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@ class DBMigrator {
use DBMigratorVip;
/** Current schema version; changing it re-runs dbDelta. */
const SCHEMA_VERSION = '1.24.0';
const SCHEMA_VERSION = '2.0.0';
const OPT_KEY = 'aksrt_db_schema_version';
+3 -2
View File
@@ -16,6 +16,7 @@ trait DBMigratorIdentity {
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
application_uuid CHAR(36) NOT NULL,
client_request_id CHAR(36) NOT NULL,
request_payload_hash CHAR(64) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
type VARCHAR(20) NOT NULL,
channel VARCHAR(20) NOT NULL DEFAULT 'cloud',
@@ -59,7 +60,7 @@ trait DBMigratorIdentity {
KEY idx_status_updated (status, updated_at),
KEY idx_id_number_hash (id_number_hash),
KEY idx_company_code_hash (company_code_hash)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($application);
$event = "CREATE TABLE {$prefix}aksrt_identity_event (
@@ -81,7 +82,7 @@ trait DBMigratorIdentity {
UNIQUE KEY uniq_dedupe_key (dedupe_key),
KEY idx_application_created (application_id, created_at),
KEY idx_source_external (source, external_event_id)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($event);
}
}
+67 -70
View File
@@ -20,43 +20,55 @@ trait DBMigratorPart2 {
$charset_collate = $wpdb->get_charset_collate();
$prefix = $wpdb->prefix;
// ---------- 1. 积分流水 ----------
$sql_credit_log = "CREATE TABLE {$prefix}aksrt_credit_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
// ---------- 1. 统一资产账户与不可变账本 ----------
$sql_asset_account = "CREATE TABLE {$prefix}aksrt_asset_account (
user_id BIGINT UNSIGNED NOT NULL,
delta INT NOT NULL,
balance_after INT NOT NULL,
reason VARCHAR(64) NOT NULL,
remark VARCHAR(255) DEFAULT NULL,
ref_type VARCHAR(32) DEFAULT NULL,
ref_id BIGINT UNSIGNED DEFAULT NULL,
ip VARCHAR(64) DEFAULT NULL,
ua VARCHAR(255) DEFAULT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_user_time (user_id, created_at),
KEY idx_reason (reason)
) {$charset_collate};";
dbDelta($sql_credit_log);
asset VARCHAR(16) NOT NULL,
balance_minor BIGINT UNSIGNED NOT NULL DEFAULT 0,
version BIGINT UNSIGNED NOT NULL DEFAULT 0,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (user_id, asset),
KEY idx_asset_balance (asset, balance_minor)
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_asset_account);
// ---------- 1b. 现金余额流水(站内钱包,与积分分离) ----------
$sql_balance_log = "CREATE TABLE {$prefix}aksrt_balance_log (
$sql_asset_transaction = "CREATE TABLE {$prefix}aksrt_asset_transaction (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
delta DECIMAL(18,2) NOT NULL,
balance_after DECIMAL(18,2) NOT NULL,
operation_key VARCHAR(191) NOT NULL,
payload_hash CHAR(64) NOT NULL,
reason VARCHAR(64) NOT NULL,
remark VARCHAR(255) DEFAULT NULL,
ref_type VARCHAR(32) DEFAULT NULL,
ref_id BIGINT UNSIGNED DEFAULT NULL,
ref_key VARCHAR(191) DEFAULT NULL,
actor_user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
metadata LONGTEXT DEFAULT NULL,
ip VARCHAR(64) DEFAULT NULL,
ua VARCHAR(255) DEFAULT NULL,
created_at DATETIME NOT NULL,
created_at DATETIME(6) NOT NULL,
PRIMARY KEY (id),
KEY idx_user_time (user_id, created_at),
KEY idx_reason (reason)
) {$charset_collate};";
dbDelta($sql_balance_log);
UNIQUE KEY uniq_operation_key (operation_key),
KEY idx_reason_time (reason, created_at),
KEY idx_reference (ref_type, ref_key)
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_asset_transaction);
$sql_asset_entry = "CREATE TABLE {$prefix}aksrt_asset_entry (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
transaction_id BIGINT UNSIGNED NOT NULL,
line_no SMALLINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
asset VARCHAR(16) NOT NULL,
entry_type VARCHAR(32) NOT NULL,
delta_minor BIGINT NOT NULL,
balance_after_minor BIGINT UNSIGNED NOT NULL,
counterparty_user_id BIGINT UNSIGNED DEFAULT NULL,
remark VARCHAR(255) DEFAULT NULL,
created_at DATETIME(6) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_transaction_line (transaction_id, line_no),
KEY idx_user_asset_time (user_id, asset, created_at, id),
KEY idx_transaction (transaction_id)
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_asset_entry);
// ---------- 2. 签到 ----------
$sql_signin = "CREATE TABLE {$prefix}aksrt_signin_log (
@@ -68,7 +80,7 @@ trait DBMigratorPart2 {
PRIMARY KEY (id),
UNIQUE KEY uniq_user_date (user_id, signin_date),
KEY idx_user (user_id)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_signin);
// ---------- 3. 下载 token ----------
@@ -88,7 +100,7 @@ trait DBMigratorPart2 {
KEY idx_expires (expires_at),
KEY idx_user (user_id),
KEY idx_post (post_id)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_dl_token);
// ---------- 5. 用户通知收件箱 ----------
@@ -105,13 +117,14 @@ trait DBMigratorPart2 {
PRIMARY KEY (id),
KEY idx_user_created (user_id, created_at),
KEY idx_user_read (user_id, is_read)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_notification);
// ---------- 8. 订单表 ----------
$sql_order = "CREATE TABLE {$prefix}aksrt_order (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_number VARCHAR(32) NOT NULL,
request_key VARCHAR(191) DEFAULT NULL,
user_id BIGINT UNSIGNED NOT NULL,
product_type VARCHAR(32) NOT NULL DEFAULT '',
product_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
@@ -131,12 +144,13 @@ trait DBMigratorPart2 {
note TEXT DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_order_number (order_number),
UNIQUE KEY uniq_request_key (request_key),
KEY idx_user (user_id),
KEY idx_user_status_created (user_id, status, created_at),
KEY idx_status (status),
KEY idx_created (created_at),
KEY idx_product (product_type, product_id)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_order);
// ---------- 9. 作者收益台账 ----------
@@ -168,7 +182,7 @@ trait DBMigratorPart2 {
KEY idx_currency (currency),
KEY idx_status (status),
KEY idx_created (created_at)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_revenue);
// ---------- 10. 提现台账 ----------
@@ -180,15 +194,17 @@ trait DBMigratorPart2 {
method VARCHAR(32) NOT NULL DEFAULT '',
account_info TEXT DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
ledger_transaction_id BIGINT UNSIGNED DEFAULT NULL,
created_at DATETIME NOT NULL,
processed_at DATETIME DEFAULT NULL,
note TEXT DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_ledger_transaction (ledger_transaction_id),
KEY idx_user (user_id),
KEY idx_user_created (user_id, created_at),
KEY idx_status (status),
KEY idx_created (created_at)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_withdrawal);
// ---------- 11. 举报工单 ----------
@@ -208,7 +224,7 @@ trait DBMigratorPart2 {
KEY idx_target (target_type, target_id),
KEY idx_reporter (reporter_id),
KEY idx_created (created_at)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_report);
// ---------- 12. 用户限制/封禁 ----------
@@ -227,7 +243,7 @@ trait DBMigratorPart2 {
KEY idx_scope (scope),
KEY idx_expires (expires_at),
KEY idx_created (created_at)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_restriction);
// ── follow ──
@@ -240,7 +256,7 @@ trait DBMigratorPart2 {
UNIQUE KEY idx_pair (follower_id, following_id),
KEY idx_following (following_id),
KEY idx_created (created_at)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_follow);
// ── coupon ──
@@ -262,7 +278,7 @@ trait DBMigratorPart2 {
UNIQUE KEY idx_code (code),
KEY idx_status (status),
KEY idx_expires (expires_at)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_coupon);
// ── coupon_usage (R68) ──
@@ -276,11 +292,11 @@ trait DBMigratorPart2 {
PRIMARY KEY (id),
KEY idx_coupon (coupon_id),
KEY idx_user (user_id),
KEY idx_order (order_number)
) {$charset_collate};";
UNIQUE KEY uniq_coupon_usage_order (order_number)
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_coupon_usage);
// ── charge_card (legacy-style typed card_pass) ──
// ── typed charge cards ──
$sql_charge_card = "CREATE TABLE {$prefix}aksrt_charge_card (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
card VARCHAR(60) NOT NULL DEFAULT '',
@@ -304,31 +320,9 @@ trait DBMigratorPart2 {
KEY idx_used_by (used_by),
KEY idx_order_number (order_number),
KEY idx_expires (expires_at)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_charge_card);
// ── rebate_log ──
$sql_rebate_log = "CREATE TABLE {$prefix}aksrt_rebate_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
order_number VARCHAR(32) NOT NULL DEFAULT '',
source_user_id BIGINT UNSIGNED DEFAULT NULL,
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
type VARCHAR(32) NOT NULL DEFAULT 'order',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
settled_at DATETIME DEFAULT NULL,
note TEXT DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_user (user_id),
KEY idx_order (order_number),
KEY idx_source (source_user_id),
KEY idx_status (status),
KEY idx_type (type),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_rebate_log);
// ── canonical Ref attribution and permanent user binding ──
$sql_affiliate_relation = "CREATE TABLE {$prefix}aksrt_affiliate_relation (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -349,7 +343,7 @@ trait DBMigratorPart2 {
KEY idx_status_touch (status, first_touch_at),
KEY idx_reward_status (reward_status),
KEY idx_reward_ip (registration_ip, rewarded_at)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_affiliate_relation);
// ── immutable commission ledger ──
@@ -365,17 +359,20 @@ trait DBMigratorPart2 {
adjustment_total DECIMAL(18,2) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
idempotency_key VARCHAR(128) NOT NULL,
cancel_operation_key VARCHAR(191) DEFAULT NULL,
cancel_payload_hash CHAR(64) DEFAULT NULL,
available_at DATETIME NOT NULL,
settled_at DATETIME DEFAULT NULL,
created_at DATETIME NOT NULL,
note TEXT DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_commission_idempotency (idempotency_key),
UNIQUE KEY uniq_commission_cancel_operation (cancel_operation_key),
KEY idx_promoter_status (promoter_id, status),
KEY idx_order_type (order_number, order_type),
KEY idx_available (status, available_at),
KEY idx_created (created_at)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_commission_ledger);
// ── immutable commission adjustments (refund/debt/manual) ──
@@ -393,7 +390,7 @@ trait DBMigratorPart2 {
UNIQUE KEY uniq_adjustment_source (source_key),
KEY idx_ledger (ledger_id),
KEY idx_promoter (promoter_id)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_commission_adjustment);
// ── payout account snapshots ──
@@ -409,7 +406,7 @@ trait DBMigratorPart2 {
PRIMARY KEY (id),
UNIQUE KEY uniq_user_channel (user_id, channel),
KEY idx_user_status (user_id, status)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_payout_account);
// ── order_items (§6.5 多商品多商家) ──
@@ -434,7 +431,7 @@ trait DBMigratorPart2 {
PRIMARY KEY (id),
KEY idx_order (order_number),
KEY idx_product (product_id)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_order_items);
self::migrate_identity_tables($prefix, $charset_collate);
+3 -3
View File
@@ -16,8 +16,9 @@ trait DBMigratorPart3 {
public static function table(string $name): string {
global $wpdb;
$allowed = [
'credit_log' => 'aksrt_credit_log',
'balance_log' => 'aksrt_balance_log',
'asset_account' => 'aksrt_asset_account',
'asset_transaction' => 'aksrt_asset_transaction',
'asset_entry' => 'aksrt_asset_entry',
'signin_log' => 'aksrt_signin_log',
'dl_token' => 'aksrt_dl_token',
'notification' => 'aksrt_notification',
@@ -29,7 +30,6 @@ trait DBMigratorPart3 {
'follow' => 'aksrt_follow',
'coupon' => 'aksrt_coupon',
'charge_card' => 'aksrt_charge_card',
'rebate_log' => 'aksrt_rebate_log',
'affiliate_relation' => 'aksrt_affiliate_relation',
'commission_ledger' => 'aksrt_commission_ledger',
'commission_adjustment' => 'aksrt_commission_adjustment',
+1 -1
View File
@@ -36,7 +36,7 @@ trait DBMigratorVip {
KEY idx_batch (batch_uuid),
KEY idx_status_created (status, created_at),
KEY idx_operation_created (operation, created_at)
) {$charset_collate};";
) ENGINE=InnoDB {$charset_collate};";
dbDelta($event);
}
}
+44 -16
View File
@@ -22,7 +22,6 @@ class DomainModel {
public const RESOURCE_SALES_VOLUME_KEY = '_aksrt_sales_volume';
public const POST_VIEW_COUNT_KEY = 'aksrt_post_views';
public const ORDER_MIGRATED_OPTION = 'aksrt_order_migrated_to_table';
public const RESOURCE_PAY_META_MIGRATED = 'aksrt_resource_pay_meta_migrated';
public const ORDER_PRODUCT_RESOURCE = 'resource';
@@ -116,11 +115,11 @@ class DomainModel {
return [
'pay_type' => self::PAY_TYPE_NO,
'pay_modo' => '0', // '0' cash | 'points' credits (legacy)
'price' => 0.0,
'price' => '0.00',
'points_price' => 0,
'original_price' => 0.0,
'vip1_price' => 0.0,
'vip2_price' => 0.0,
'original_price' => '0.00',
'vip1_price' => '0.00',
'vip2_price' => '0.00',
'vip1_points' => 0,
'vip2_points' => 0,
'pay_limit' => 0, // 0 all | 1 VIP1+ | 2 VIP2 only (legacy)
@@ -143,11 +142,11 @@ class DomainModel {
'free_preview' => '',
'show_sales' => true,
// legacy: free daily quota overbuy price (cash or points by pay_modo).
'download_limit_over_price' => 0.0,
'download_limit_over_price' => '0.00',
// Content cashier: coupon + buyer-facing 推广折扣 (legacy posts_aksrtpay).
'coupon_s' => false,
'coupon_desc' => '',
'pay_rebate_discount' => 0.0,
'pay_rebate_discount' => '0.00',
];
}
@@ -319,13 +318,13 @@ class DomainModel {
$meta['pay_type'] = self::normalize_pay_type((string) ($raw['pay_type'] ?? $meta['pay_type']));
$pay_modo = sanitize_key((string) ($raw['pay_modo'] ?? '0'));
$meta['pay_modo'] = $pay_modo === 'points' ? 'points' : '0';
$meta['price'] = max(0, (float) ($raw['price'] ?? 0));
$meta['points_price'] = max(0, (int) ($raw['points_price'] ?? $raw['price'] ?? 0));
$meta['original_price'] = max(0, (float) ($raw['original_price'] ?? 0));
$meta['vip1_price'] = max(0, (float) ($raw['vip1_price'] ?? 0));
$meta['vip2_price'] = max(0, (float) ($raw['vip2_price'] ?? 0));
$meta['vip1_points'] = max(0, (int) ($raw['vip1_points'] ?? $raw['vip1_price'] ?? 0));
$meta['vip2_points'] = max(0, (int) ($raw['vip2_points'] ?? $raw['vip2_price'] ?? 0));
$meta['price'] = self::sanitize_balance_decimal($raw['price'] ?? '0');
$meta['points_price'] = self::sanitize_points($raw['points_price'] ?? 0);
$meta['original_price'] = self::sanitize_balance_decimal($raw['original_price'] ?? '0');
$meta['vip1_price'] = self::sanitize_balance_decimal($raw['vip1_price'] ?? '0');
$meta['vip2_price'] = self::sanitize_balance_decimal($raw['vip2_price'] ?? '0');
$meta['vip1_points'] = self::sanitize_points($raw['vip1_points'] ?? 0);
$meta['vip2_points'] = self::sanitize_points($raw['vip2_points'] ?? 0);
$pay_limit = (int) ($raw['pay_limit'] ?? 0);
$meta['pay_limit'] = in_array($pay_limit, [0, 1, 2], true) ? $pay_limit : 0;
$meta['version'] = sanitize_text_field((string) ($raw['version'] ?? ''));
@@ -347,14 +346,43 @@ class DomainModel {
$meta['purchase_limit'] = max(0, (int) ($raw['purchase_limit'] ?? 0));
$meta['free_preview'] = wp_kses_post((string) ($raw['free_preview'] ?? ''));
$meta['show_sales'] = !isset($raw['show_sales']) || !empty($raw['show_sales']);
$meta['download_limit_over_price'] = max(0, (float) ($raw['download_limit_over_price'] ?? 0));
$meta['download_limit_over_price'] = $meta['pay_modo'] === 'points'
? self::sanitize_points($raw['download_limit_over_price'] ?? 0)
: self::sanitize_balance_decimal($raw['download_limit_over_price'] ?? '0');
$meta['coupon_s'] = !empty($raw['coupon_s']);
$meta['coupon_desc'] = sanitize_textarea_field((string) ($raw['coupon_desc'] ?? ''));
$meta['pay_rebate_discount'] = max(0, (float) ($raw['pay_rebate_discount'] ?? 0));
$meta['pay_rebate_discount'] = self::sanitize_balance_decimal($raw['pay_rebate_discount'] ?? '0');
return $meta;
}
private static function sanitize_balance_decimal($value): string {
if (!is_string($value)) {
return '0.00';
}
try {
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
} catch (\InvalidArgumentException $e) {
return '0.00';
}
}
private static function sanitize_points($value): int {
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 (int) $value;
}
/**
* @return array<string, string>
*/
+78 -33
View File
@@ -67,16 +67,16 @@ trait FrontSubmitPay {
$meta['pay_type'] = $pay_type;
$meta['pay_modo'] = $pay_modo;
$min = (float) ($settings['pay_price_min'] ?? 0);
$max = (float) ($settings['pay_price_max'] ?? 0);
$allow_free = !empty($settings['pay_free_s']);
$vip_manual = !empty($settings['pay_vip_price_s']);
$vip1_disc = (int) ($settings['pay_vip_1_discount'] ?? 100);
$vip2_disc = (int) ($settings['pay_vip_2_discount'] ?? 100);
if ($pay_modo === 'points') {
$points = isset($raw['points_price']) ? (int) $raw['points_price'] : (int) ($data['points_price'] ?? 0);
if ($points < 0) {
$points = $this->parse_front_points($raw['points_price'] ?? $data['points_price'] ?? null);
$min = $this->parse_front_points_limit($settings['pay_price_min'] ?? '0.00');
$max = $this->parse_front_points_limit($settings['pay_price_max'] ?? '0.00');
if ($points === null) {
return ['success' => false, 'message' => __('请设置正确的积分价格', 'aksrt')];
}
if (!$allow_free && $points <= 0) {
@@ -90,59 +90,64 @@ trait FrontSubmitPay {
}
if ($vip_manual) {
$vip1 = isset($raw['vip1_points']) ? (int) $raw['vip1_points'] : (int) ($raw['vip_1_points'] ?? 0);
$vip2 = isset($raw['vip2_points']) ? (int) $raw['vip2_points'] : (int) ($raw['vip_2_points'] ?? 0);
$vip1 = $this->parse_front_points($raw['vip1_points'] ?? $raw['vip_1_points'] ?? null);
$vip2 = $this->parse_front_points($raw['vip2_points'] ?? $raw['vip_2_points'] ?? null);
if ($vip1 === null || $vip2 === null) {
return ['success' => false, 'message' => __('请设置正确的会员积分价格', 'aksrt')];
}
} else {
$vip1 = (int) floor($points * $vip1_disc / 100);
$vip2 = (int) floor($points * $vip2_disc / 100);
$vip1 = (intdiv($points, 100) * $vip1_disc) + intdiv(($points % 100) * $vip1_disc, 100);
$vip2 = (intdiv($points, 100) * $vip2_disc) + intdiv(($points % 100) * $vip2_disc, 100);
}
$vip1 = max(0, min($points, $vip1));
$vip2 = max(0, min($vip1, $vip2));
$meta['points_price'] = $points;
$meta['price'] = 0.0;
$meta['price'] = '0.00';
$meta['vip1_points'] = $vip1;
$meta['vip2_points'] = $vip2;
$meta['vip1_price'] = 0.0;
$meta['vip2_price'] = 0.0;
$meta['vip1_price'] = '0.00';
$meta['vip2_price'] = '0.00';
} else {
$price = isset($raw['price']) ? (float) $raw['price'] : (float) ($raw['pay_price'] ?? $data['pay_price'] ?? 0);
$price = round($price, 2);
if ($price < 0) {
$price_minor = $this->parse_front_cash_minor($raw['price'] ?? $raw['pay_price'] ?? $data['pay_price'] ?? null);
$min_minor = $this->parse_front_cash_minor($settings['pay_price_min'] ?? '0') ?? 0;
$max_minor = $this->parse_front_cash_minor($settings['pay_price_max'] ?? '0') ?? 0;
if ($price_minor === null) {
return ['success' => false, 'message' => __('请设置正确的价格', 'aksrt')];
}
if (!$allow_free && $price <= 0) {
if (!$allow_free && $price_minor <= 0) {
return ['success' => false, 'message' => __('请设置付费内容的价格', 'aksrt')];
}
if ($min > 0 && $price < $min) {
return ['success' => false, 'message' => sprintf(__('价格不能低于:%s', 'aksrt'), (string) $min)];
if ($min_minor > 0 && $price_minor < $min_minor) {
return ['success' => false, 'message' => sprintf(__('价格不能低于:%s', 'aksrt'), \AKSRT\Ledger\AssetAmount::decimal_from_minor($min_minor))];
}
if ($max > 0 && $price > $max) {
return ['success' => false, 'message' => sprintf(__('价格不能高于:%s', 'aksrt'), (string) $max)];
if ($max_minor > 0 && $price_minor > $max_minor) {
return ['success' => false, 'message' => sprintf(__('价格不能高于:%s', 'aksrt'), \AKSRT\Ledger\AssetAmount::decimal_from_minor($max_minor))];
}
if ($vip_manual) {
$vip1 = isset($raw['vip1_price']) ? (float) $raw['vip1_price'] : (float) ($raw['vip_1_price'] ?? 0);
$vip2 = isset($raw['vip2_price']) ? (float) $raw['vip2_price'] : (float) ($raw['vip_2_price'] ?? 0);
$vip1 = round($vip1, 2);
$vip2 = round($vip2, 2);
if ($vip1 > $price) {
$vip1_minor = $this->parse_front_cash_minor($raw['vip1_price'] ?? $raw['vip_1_price'] ?? null);
$vip2_minor = $this->parse_front_cash_minor($raw['vip2_price'] ?? $raw['vip_2_price'] ?? null);
if ($vip1_minor === null || $vip2_minor === null) {
return ['success' => false, 'message' => __('请设置正确的会员价格', 'aksrt')];
}
if ($vip1_minor > $price_minor) {
return ['success' => false, 'message' => __('会员价不能高于普通价', 'aksrt')];
}
if ($vip2 > $vip1) {
if ($vip2_minor > $vip1_minor) {
return ['success' => false, 'message' => __('更高等级会员价不能高于较低等级会员价', 'aksrt')];
}
} else {
$vip1 = round($price * $vip1_disc / 100, 2);
$vip2 = round($price * $vip2_disc / 100, 2);
$vip1_minor = \AKSRT\Ledger\AssetAmount::percentage($price_minor, max(0, min(100, $vip1_disc)));
$vip2_minor = \AKSRT\Ledger\AssetAmount::percentage($price_minor, max(0, min(100, $vip2_disc)));
}
$vip1 = max(0, min($price, $vip1));
$vip2 = max(0, min($vip1, $vip2));
$vip1_minor = max(0, min($price_minor, $vip1_minor));
$vip2_minor = max(0, min($vip1_minor, $vip2_minor));
$meta['price'] = $price;
$meta['price'] = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
$meta['points_price'] = 0;
$meta['vip1_price'] = $vip1;
$meta['vip2_price'] = $vip2;
$meta['vip1_price'] = \AKSRT\Ledger\AssetAmount::decimal_from_minor($vip1_minor);
$meta['vip2_price'] = \AKSRT\Ledger\AssetAmount::decimal_from_minor($vip2_minor);
$meta['vip1_points'] = 0;
$meta['vip2_points'] = 0;
}
@@ -159,6 +164,40 @@ trait FrontSubmitPay {
return ['success' => true];
}
private function parse_front_points($value): ?int {
if (is_int($value)) {
return $value >= 0 ? $value : null;
}
if (!is_string($value) || !preg_match('/^(0|[1-9]\d*)$/', trim($value))) {
return null;
}
$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 null;
}
return (int) $value;
}
private function parse_front_cash_minor($value): ?int {
if (!is_string($value)) {
return null;
}
try {
return \AKSRT\Ledger\AssetAmount::balance_minor($value);
} catch (\InvalidArgumentException $e) {
return null;
}
}
private function parse_front_points_limit($value): int {
if (!is_string($value) || !preg_match('/^(0|[1-9]\d*)(?:\.00)?$/', trim($value), $matches)) {
return 0;
}
return $this->parse_front_points((string) $matches[1]) ?? 0;
}
/**
* @param mixed $raw
* @return array<int, array<string, string>>
@@ -210,7 +249,13 @@ trait FrontSubmitPay {
$meta = DomainModel::sanitize_pay_meta($meta);
update_post_meta($post_id, DomainModel::PAY_META_KEY, $meta);
update_post_meta($post_id, DomainModel::PAY_TYPE_KEY, (string) ($meta['pay_type'] ?? DomainModel::PAY_TYPE_NO));
update_post_meta($post_id, DomainModel::PAY_PRICE_KEY, (float) ($meta['price'] ?? 0));
update_post_meta(
$post_id,
DomainModel::PAY_PRICE_KEY,
($meta['pay_modo'] ?? '0') === 'points'
? (int) ($meta['points_price'] ?? 0)
: (string) ($meta['price'] ?? '0.00')
);
update_post_meta(
$post_id,
DomainModel::RESOURCE_MARKER_KEY,
+23 -10
View File
@@ -473,13 +473,13 @@ class PostMetaBox {
$type_label = $labels[$pay_type] ?? $pay_type;
$modo = (($pay['pay_modo'] ?? '0') === 'points') ? 'points' : '0';
if ($modo === 'points') {
$price_text = sprintf(__('%d 积分', 'aksrt'), (int) ceil((float) ($pay['points_price'] ?? $pay['price'] ?? 0)));
$price_text = sprintf(__('%d 积分', 'aksrt'), max(0, (int) ($pay['points_price'] ?? 0)));
} else {
$price_text = '¥' . number_format((float) ($pay['price'] ?? 0), 2);
$price_text = '¥' . (string) ($pay['price'] ?? '0.00');
}
$meta_sales = (int) get_post_meta($post_id, DomainModel::RESOURCE_SALES_VOLUME_KEY, true);
$db_sales = 0;
$revenue = 0.0;
$revenue = '0.00';
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$map = \AKSRT\Payment\OrderRepository::get_instance()->get_content_sales_map(
[$post_id],
@@ -487,7 +487,7 @@ class PostMetaBox {
);
if (isset($map[$post_id])) {
$db_sales = (int) ($map[$post_id]['sales_count'] ?? 0);
$revenue = (float) ($map[$post_id]['revenue'] ?? 0);
$revenue = (string) ($map[$post_id]['revenue'] ?? '0.00');
}
}
$orders_url = admin_url('admin.php?page=aksrt_orders&product_type=resource&product_id=' . $post_id);
@@ -502,7 +502,7 @@ class PostMetaBox {
'<br><a href="%s" title="%s">%s</a>',
esc_url($orders_url),
esc_attr__('查看销售明细', 'aksrt'),
esc_html(sprintf(__('真实销量 %d · ¥%s', 'aksrt'), $db_sales, number_format($revenue, 2)))
esc_html(sprintf(__('真实销量 %d · ¥%s', 'aksrt'), $db_sales, $revenue))
);
} else {
echo '<br><span class="description">' . esc_html__('暂无订单', 'aksrt') . '</span>';
@@ -682,11 +682,18 @@ class PostMetaBox {
$modo = sanitize_key($modo_raw);
$meta['pay_modo'] = ($modo === 'points') ? 'points' : '0';
}
if ($price_raw !== '' && is_numeric($price_raw)) {
$meta['price'] = max(0, (float) $price_raw);
if ($price_raw !== '') {
try {
$meta['price'] = \AKSRT\Ledger\AssetAmount::balance($price_raw)->decimal();
} catch (\InvalidArgumentException $e) {
return;
}
}
if ($points_raw !== '' && is_numeric($points_raw)) {
$meta['points_price'] = max(0, (int) $points_raw);
if ($points_raw !== '') {
if (!preg_match('/^(0|[1-9]\d*)$/', $points_raw)) {
return;
}
$meta['points_price'] = (int) $points_raw;
}
if ($virtual_raw !== '' && is_numeric($virtual_raw)) {
$meta['virtual_sales'] = max(0, (int) $virtual_raw);
@@ -695,7 +702,13 @@ class PostMetaBox {
$meta = DomainModel::sanitize_pay_meta($meta);
update_post_meta($post_id, DomainModel::PAY_META_KEY, $meta);
update_post_meta($post_id, DomainModel::PAY_TYPE_KEY, (string) ($meta['pay_type'] ?? DomainModel::PAY_TYPE_NO));
update_post_meta($post_id, DomainModel::PAY_PRICE_KEY, (float) ($meta['price'] ?? 0));
update_post_meta(
$post_id,
DomainModel::PAY_PRICE_KEY,
($meta['pay_modo'] ?? '0') === 'points'
? (int) ($meta['points_price'] ?? 0)
: (string) ($meta['price'] ?? '0.00')
);
if (class_exists('AKSRT\\Core\\DomainModel') && method_exists('AKSRT\\Core\\DomainModel', 'is_resource_pay_type')) {
update_post_meta(
$post_id,
+8 -15
View File
@@ -456,18 +456,11 @@ class ResourceMetaBox {
if ($pay_modo !== 'points') {
$pay_modo = '0';
}
$price = max(0, (float) ($_POST['aksrt_pay_price'] ?? 0));
$price = trim((string) wp_unslash($_POST['aksrt_pay_price'] ?? '0'));
$points_price = max(0, (int) ($_POST['aksrt_pay_points_price'] ?? 0));
// Keep legacy single-price editors usable: if points field empty and modo points, mirror price.
if ($pay_modo === 'points' && $points_price <= 0 && $price > 0) {
$points_price = (int) ceil($price);
}
if ($pay_modo !== 'points' && $price <= 0 && $points_price > 0) {
$price = (float) $points_price;
}
$original_price = max(0, (float) ($_POST['aksrt_pay_original_price'] ?? 0));
$vip1_price = max(0, (float) ($_POST['aksrt_pay_vip1_price'] ?? 0));
$vip2_price = max(0, (float) ($_POST['aksrt_pay_vip2_price'] ?? 0));
$original_price = trim((string) wp_unslash($_POST['aksrt_pay_original_price'] ?? '0'));
$vip1_price = trim((string) wp_unslash($_POST['aksrt_pay_vip1_price'] ?? '0'));
$vip2_price = trim((string) wp_unslash($_POST['aksrt_pay_vip2_price'] ?? '0'));
$vip1_points = max(0, (int) ($_POST['aksrt_pay_vip1_points'] ?? 0));
$vip2_points = max(0, (int) ($_POST['aksrt_pay_vip2_points'] ?? 0));
$pay_limit = (int) ($_POST['aksrt_pay_limit'] ?? 0);
@@ -527,10 +520,10 @@ class ResourceMetaBox {
'card_keys' => isset($_POST['aksrt_pay_card_keys']) ? array_filter(array_map('trim', explode("\n", (string) $_POST['aksrt_pay_card_keys']))) : [],
'free_preview' => $_POST['aksrt_pay_free_preview'] ?? '',
'show_sales' => !empty($_POST['aksrt_pay_show_sales']),
'download_limit_over_price' => max(0, (float) ($_POST['aksrt_pay_limit_over_price'] ?? 0)),
'download_limit_over_price' => trim((string) wp_unslash($_POST['aksrt_pay_limit_over_price'] ?? '0')),
'coupon_s' => !empty($_POST['aksrt_pay_coupon_s']),
'coupon_desc' => (string) ($_POST['aksrt_pay_coupon_desc'] ?? ''),
'pay_rebate_discount' => max(0, (float) ($_POST['aksrt_pay_rebate_discount'] ?? 0)),
'pay_rebate_discount' => trim((string) wp_unslash($_POST['aksrt_pay_rebate_discount'] ?? '0')),
]);
update_post_meta($post_id, DomainModel::PAY_META_KEY, $pay_meta);
@@ -538,8 +531,8 @@ class ResourceMetaBox {
// 同步标量字段用于查询(模仿 legacy 的 aksrtpay_type 同步)
update_post_meta($post_id, DomainModel::PAY_TYPE_KEY, (string) $pay_meta['pay_type']);
$query_price = ($pay_meta['pay_modo'] === 'points')
? (float) $pay_meta['points_price']
: (float) $pay_meta['price'];
? (int) $pay_meta['points_price']
: (string) $pay_meta['price'];
update_post_meta($post_id, DomainModel::PAY_PRICE_KEY, $query_price);
update_post_meta($post_id, '_aksrt_pay_modo', (string) $pay_meta['pay_modo']);
update_post_meta($post_id, '_aksrt_pay_limit', (int) $pay_meta['pay_limit']);
+3 -4
View File
@@ -143,8 +143,7 @@ trait ScriptsPart1b {
&& class_exists('\AKSRT\Settings\Settings')
&& \AKSRT\Settings\Settings::credits_enabled();
$balance_on = is_user_logged_in()
&& class_exists('\AKSRT\Credits\Balance')
&& \AKSRT\Credits\Balance::enabled();
&& !empty(\AKSRT\Settings\Settings::get_payment_recharge_settings()['enabled']);
$affiliate_on = is_user_logged_in()
&& class_exists('\\AKSRT\\Settings\\Settings')
&& !empty(\AKSRT\Settings\Settings::get_affiliate_settings()['enabled']);
@@ -656,7 +655,7 @@ trait ScriptsPart1b {
wp_enqueue_script(
'aksrt-cms-download',
$base_uri . '/assets/js/cms-download.js',
['loader_js', 'aksrt-runtime'],
['loader_js', 'aksrt-runtime', 'aksrt-core-request'],
file_exists($cms_download_js_path) ? (string) filemtime($cms_download_js_path) : $version,
true
);
@@ -709,7 +708,7 @@ trait ScriptsPart1b {
wp_enqueue_script(
'aksrt-credits',
$base_uri . '/assets/js/credits.js',
['loader_js', 'aksrt-lazy-image-attr', 'aksrt-core-modal-runtime', 'aksrt-vip-coordinator', 'aksrt-notyf'],
['loader_js', 'aksrt-lazy-image-attr', 'aksrt-core-request', 'aksrt-core-modal-runtime', 'aksrt-vip-coordinator', 'aksrt-notyf'],
file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version,
true
);
+1 -1
View File
@@ -334,7 +334,7 @@ trait ScriptsPart1c {
wp_enqueue_script(
'aksrt-reward',
$base_uri . '/assets/js/reward.js',
['loader_js'],
['loader_js', 'aksrt-core-request'],
(string) filemtime($reward_js),
true
);
+18 -8
View File
@@ -161,11 +161,11 @@ class Setup {
'enum' => array_keys(DomainModel::pay_type_labels()),
],
'pay_modo' => ['type' => 'string', 'enum' => ['0', 'points']],
'price' => ['type' => 'number'],
'price' => ['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
'points_price' => ['type' => 'integer'],
'original_price' => ['type' => 'number'],
'vip1_price' => ['type' => 'number'],
'vip2_price' => ['type' => 'number'],
'original_price' => ['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
'vip1_price' => ['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
'vip2_price' => ['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
'vip1_points' => ['type' => 'integer'],
'vip2_points' => ['type' => 'integer'],
'pay_limit' => ['type' => 'integer', 'enum' => [0, 1, 2]],
@@ -221,13 +221,23 @@ class Setup {
]);
register_post_meta('post', DomainModel::PAY_PRICE_KEY, [
'type' => 'number',
'type' => 'string',
'description' => 'Query-friendly paid content price.',
'single' => true,
'default' => 0,
'default' => '0.00',
'show_in_rest' => true,
'sanitize_callback' => static function ($value) {
return max(0, (float) $value);
'sanitize_callback' => static function ($value): string {
if (is_int($value)) {
return (string) max(0, $value);
}
if (!is_string($value)) {
return '0.00';
}
try {
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
} catch (\InvalidArgumentException $e) {
return '0.00';
}
},
'auth_callback' => $post_edit_auth,
]);