积分与余额账务系统硬切重构
This commit is contained in:
1 parent
ca12226b39
commit
5cae5b5c93
159 files changed
+7275
-4718
No files matched your search
@@ -1,379 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Credits;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Balance {
|
||||
|
||||
public const META_KEY = 'aksrt_balance';
|
||||
|
||||
/**
|
||||
* Current cash balance (yuan).
|
||||
*/
|
||||
public static function balance(int $user_id): float {
|
||||
if ($user_id <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
$val = get_user_meta($user_id, self::META_KEY, true);
|
||||
return is_numeric($val) ? round((float) $val, 2) : 0.0;
|
||||
}
|
||||
|
||||
public static function get_balance(int $user_id): float {
|
||||
return self::balance($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit cash wallet.
|
||||
*
|
||||
* @param array<string,mixed> $ref
|
||||
* @return float|\WP_Error New balance.
|
||||
*/
|
||||
public static function add(int $user_id, float $delta, string $reason, array $ref = [], string $remark = '', bool $dispatch_hooks = true) {
|
||||
if ($user_id <= 0) {
|
||||
return new \WP_Error('aksrt_balance_user', __('用户无效', 'aksrt'));
|
||||
}
|
||||
$delta = round((float) $delta, 2);
|
||||
if ($delta <= 0) {
|
||||
return new \WP_Error('aksrt_balance_delta', __('增加余额必须为正数', 'aksrt'));
|
||||
}
|
||||
|
||||
$new = self::atomic_update($user_id, $delta, false);
|
||||
if ($new === null) {
|
||||
return new \WP_Error('aksrt_balance_db', __('余额写入失败', 'aksrt'));
|
||||
}
|
||||
self::log($user_id, $delta, $new, $reason, $ref, $remark);
|
||||
if ($dispatch_hooks) {
|
||||
do_action('aksrt_balance_added', $user_id, $delta, $reason, $new);
|
||||
}
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debit cash wallet.
|
||||
*
|
||||
* @param array<string,mixed> $ref
|
||||
* @return float|\WP_Error New balance.
|
||||
*/
|
||||
public static function deduct(int $user_id, float $cost, string $reason, array $ref = [], string $remark = '') {
|
||||
if ($user_id <= 0) {
|
||||
return new \WP_Error('aksrt_balance_user', __('用户无效', 'aksrt'));
|
||||
}
|
||||
$cost = round((float) $cost, 2);
|
||||
if ($cost < 0) {
|
||||
return new \WP_Error('aksrt_balance_delta', __('扣减余额必须为非负数', 'aksrt'));
|
||||
}
|
||||
if ($cost === 0.0) {
|
||||
return self::balance($user_id);
|
||||
}
|
||||
|
||||
$new = self::atomic_update($user_id, $cost, true);
|
||||
if ($new === null) {
|
||||
$current = self::balance($user_id);
|
||||
return new \WP_Error('aksrt_balance_insufficient', __('余额不足', 'aksrt'), [
|
||||
'balance' => $current,
|
||||
'cost' => $cost,
|
||||
]);
|
||||
}
|
||||
self::log($user_id, -$cost, $new, $reason, $ref, $remark);
|
||||
do_action('aksrt_balance_deducted', $user_id, $cost, $reason, $new);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float|null
|
||||
*/
|
||||
private static function atomic_update(int $user_id, float $abs_delta, bool $is_deduct) {
|
||||
global $wpdb;
|
||||
|
||||
$abs_delta = round(abs($abs_delta), 2);
|
||||
$meta_key = self::META_KEY;
|
||||
$table = $wpdb->usermeta;
|
||||
|
||||
// Ensure meta row exists.
|
||||
$exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT umeta_id FROM {$table} WHERE user_id = %d AND meta_key = %s LIMIT 1",
|
||||
$user_id,
|
||||
$meta_key
|
||||
));
|
||||
if (!$exists) {
|
||||
add_user_meta($user_id, $meta_key, '0', true);
|
||||
}
|
||||
|
||||
if ($is_deduct) {
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$table}
|
||||
SET meta_value = CAST(ROUND(CAST(meta_value AS DECIMAL(18,2)) - %f, 2) AS CHAR)
|
||||
WHERE user_id = %d AND meta_key = %s
|
||||
AND CAST(meta_value AS DECIMAL(18,2)) >= %f",
|
||||
$abs_delta,
|
||||
$user_id,
|
||||
$meta_key,
|
||||
$abs_delta
|
||||
));
|
||||
} else {
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$table}
|
||||
SET meta_value = CAST(ROUND(CAST(meta_value AS DECIMAL(18,2)) + %f, 2) AS CHAR)
|
||||
WHERE user_id = %d AND meta_key = %s",
|
||||
$abs_delta,
|
||||
$user_id,
|
||||
$meta_key
|
||||
));
|
||||
}
|
||||
|
||||
if ($updated !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
wp_cache_delete($user_id, 'user_meta');
|
||||
return self::balance($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $ref
|
||||
*/
|
||||
public static function log(int $user_id, float $delta, float $balance_after, string $reason, array $ref = [], string $remark = ''): void {
|
||||
global $wpdb;
|
||||
if (!class_exists('\AKSRT\Core\DBMigrator')) {
|
||||
return;
|
||||
}
|
||||
$table = \AKSRT\Core\DBMigrator::table('balance_log');
|
||||
if ($table === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$ip = '';
|
||||
if (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip = sanitize_text_field(wp_unslash((string) $_SERVER['REMOTE_ADDR']));
|
||||
}
|
||||
$ua = '';
|
||||
if (!empty($_SERVER['HTTP_USER_AGENT'])) {
|
||||
$ua = substr(sanitize_text_field(wp_unslash((string) $_SERVER['HTTP_USER_AGENT'])), 0, 255);
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
[
|
||||
'user_id' => $user_id,
|
||||
'delta' => round($delta, 2),
|
||||
'balance_after' => round($balance_after, 2),
|
||||
'reason' => sanitize_key(substr($reason, 0, 64)),
|
||||
'remark' => $remark !== '' ? sanitize_text_field(substr($remark, 0, 255)) : null,
|
||||
'ref_type' => isset($ref['type']) ? sanitize_key((string) $ref['type']) : null,
|
||||
'ref_id' => isset($ref['id']) ? (int) $ref['id'] : (isset($ref['ref_id']) ? (int) $ref['ref_id'] : null),
|
||||
'ip' => $ip !== '' ? $ip : null,
|
||||
'ua' => $ua !== '' ? $ua : null,
|
||||
'created_at' => current_time('mysql', true),
|
||||
],
|
||||
['%d', '%f', '%f', '%s', '%s', '%s', '%d', '%s', '%s', '%s']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $args
|
||||
* @return array{items:array<int,array<string,mixed>>,total:int}
|
||||
*/
|
||||
public static function get_log(array $args = []): array {
|
||||
global $wpdb;
|
||||
if (!class_exists('\AKSRT\Core\DBMigrator')) {
|
||||
return ['items' => [], 'total' => 0];
|
||||
}
|
||||
$table = \AKSRT\Core\DBMigrator::table('balance_log');
|
||||
if ($table === '') {
|
||||
return ['items' => [], 'total' => 0];
|
||||
}
|
||||
|
||||
$user_id = max(0, (int) ($args['user_id'] ?? 0));
|
||||
$page = max(1, (int) ($args['page'] ?? 1));
|
||||
$per_page = max(1, min(50, (int) ($args['per_page'] ?? 10)));
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
$where = '1=1';
|
||||
$params = [];
|
||||
if ($user_id > 0) {
|
||||
$where .= ' AND user_id = %d';
|
||||
$params[] = $user_id;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$total_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$total = (int) ($params ? $wpdb->get_var($wpdb->prepare($total_sql, $params)) : $wpdb->get_var($total_sql));
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$list_sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY id DESC LIMIT %d OFFSET %d";
|
||||
$list_params = array_merge($params, [$per_page, $offset]);
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$rows = $wpdb->get_results($wpdb->prepare($list_sql, $list_params), ARRAY_A);
|
||||
if (!is_array($rows)) {
|
||||
$rows = [];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($rows as $row) {
|
||||
$items[] = [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'user_id' => (int) ($row['user_id'] ?? 0),
|
||||
'delta' => round((float) ($row['delta'] ?? 0), 2),
|
||||
'balance_after' => round((float) ($row['balance_after'] ?? 0), 2),
|
||||
'reason' => (string) ($row['reason'] ?? ''),
|
||||
'remark' => (string) ($row['remark'] ?? ''),
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return ['items' => $items, 'total' => $total];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether cash wallet feature is enabled (legacy pay_balance_s).
|
||||
*/
|
||||
public static function enabled(): bool {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return false;
|
||||
}
|
||||
if (method_exists('\AKSRT\Settings\Settings', 'get_payment_recharge_settings')) {
|
||||
$cfg = \AKSRT\Settings\Settings::get_payment_recharge_settings();
|
||||
return !empty($cfg['enabled']);
|
||||
}
|
||||
return !empty(\AKSRT\Settings\Settings::get_all()['pay_balance_s']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically transfer cash balance between two users.
|
||||
*
|
||||
* @param array<string,mixed> $ref
|
||||
* @return array{sender_balance:float,recipient_balance:float}|\WP_Error
|
||||
*/
|
||||
public static function transfer(int $sender_id, int $recipient_id, float $amount, string $reason_out = 'tip_sent', string $reason_in = 'tip_received', array $ref = [], string $remark = '') {
|
||||
global $wpdb;
|
||||
|
||||
$amount = round($amount, 2);
|
||||
if ($sender_id <= 0 || $recipient_id <= 0 || $sender_id === $recipient_id) {
|
||||
return new \WP_Error('aksrt_balance_transfer_user', __('转账用户无效', 'aksrt'));
|
||||
}
|
||||
if ($amount <= 0) {
|
||||
return new \WP_Error('aksrt_balance_transfer_amount', __('转账金额必须大于零', 'aksrt'));
|
||||
}
|
||||
if (!class_exists('\AKSRT\Core\DBMigrator')) {
|
||||
return new \WP_Error('aksrt_balance_transfer_unavailable', __('余额服务不可用', 'aksrt'));
|
||||
}
|
||||
|
||||
foreach ([$sender_id, $recipient_id] as $user_id) {
|
||||
if (!metadata_exists('user', $user_id, self::META_KEY)) {
|
||||
add_user_meta($user_id, self::META_KEY, '0', true);
|
||||
}
|
||||
}
|
||||
|
||||
$meta_table = $wpdb->usermeta;
|
||||
$log_table = \AKSRT\Core\DBMigrator::table('balance_log');
|
||||
$locked_ids = [$sender_id, $recipient_id];
|
||||
sort($locked_ids, SORT_NUMERIC);
|
||||
|
||||
$wpdb->query('START TRANSACTION');
|
||||
try {
|
||||
foreach ($locked_ids as $user_id) {
|
||||
$wpdb->get_var($wpdb->prepare(
|
||||
"SELECT umeta_id FROM {$meta_table} WHERE user_id = %d AND meta_key = %s FOR UPDATE",
|
||||
$user_id,
|
||||
self::META_KEY
|
||||
));
|
||||
}
|
||||
|
||||
$sender_before = (float) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT meta_value FROM {$meta_table} WHERE user_id = %d AND meta_key = %s LIMIT 1",
|
||||
$sender_id,
|
||||
self::META_KEY
|
||||
));
|
||||
if ($sender_before < $amount) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
return new \WP_Error('aksrt_balance_insufficient', __('余额不足', 'aksrt'));
|
||||
}
|
||||
|
||||
$sender_after = round($sender_before - $amount, 2);
|
||||
$recipient_before = (float) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT meta_value FROM {$meta_table} WHERE user_id = %d AND meta_key = %s LIMIT 1",
|
||||
$recipient_id,
|
||||
self::META_KEY
|
||||
));
|
||||
$recipient_after = round($recipient_before + $amount, 2);
|
||||
|
||||
$sender_updated = $wpdb->update($meta_table, ['meta_value' => (string) $sender_after], ['user_id' => $sender_id, 'meta_key' => self::META_KEY], ['%s'], ['%d', '%s']);
|
||||
$recipient_updated = $wpdb->update($meta_table, ['meta_value' => (string) $recipient_after], ['user_id' => $recipient_id, 'meta_key' => self::META_KEY], ['%s'], ['%d', '%s']);
|
||||
if ($sender_updated !== 1 || $recipient_updated !== 1) {
|
||||
throw new \RuntimeException('balance_update_failed');
|
||||
}
|
||||
|
||||
$ref_type = isset($ref['type']) ? sanitize_key((string) $ref['type']) : 'tip';
|
||||
$ref_id = isset($ref['id']) ? (int) $ref['id'] : null;
|
||||
$created_at = current_time('mysql', true);
|
||||
$log_rows = [
|
||||
[$sender_id, -$amount, $sender_after, $reason_out],
|
||||
[$recipient_id, $amount, $recipient_after, $reason_in],
|
||||
];
|
||||
foreach ($log_rows as [$user_id, $delta, $balance_after, $reason]) {
|
||||
$inserted = $wpdb->insert($log_table, [
|
||||
'user_id' => $user_id,
|
||||
'delta' => $delta,
|
||||
'balance_after' => $balance_after,
|
||||
'reason' => sanitize_key($reason),
|
||||
'remark' => $remark !== '' ? sanitize_text_field(substr($remark, 0, 255)) : null,
|
||||
'ref_type' => $ref_type,
|
||||
'ref_id' => $ref_id,
|
||||
'created_at' => $created_at,
|
||||
], ['%d', '%f', '%f', '%s', '%s', '%s', '%d', '%s']);
|
||||
if ($inserted !== 1) {
|
||||
throw new \RuntimeException('balance_log_failed');
|
||||
}
|
||||
}
|
||||
|
||||
$wpdb->query('COMMIT');
|
||||
wp_cache_delete($sender_id, 'user_meta');
|
||||
wp_cache_delete($recipient_id, 'user_meta');
|
||||
do_action('aksrt_balance_transferred', $sender_id, $recipient_id, $amount, $ref);
|
||||
return ['sender_balance' => $sender_after, 'recipient_balance' => $recipient_after];
|
||||
} catch (\Throwable $e) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
return new \WP_Error('aksrt_balance_transfer_failed', __('打赏入账失败,请稍后重试', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format yuan for display.
|
||||
*/
|
||||
public static function format(float $amount): string {
|
||||
return number_format_i18n(round($amount, 2), 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reason label for ledger UI.
|
||||
*/
|
||||
public static function reason_label(string $reason): string {
|
||||
$map = [
|
||||
'card_pass_balance' => __('卡密充值', 'aksrt'),
|
||||
'order_payment' => __('订单支付', 'aksrt'),
|
||||
'order_refund' => __('订单退款', 'aksrt'),
|
||||
'recharge' => __('余额充值', 'aksrt'),
|
||||
'transfer_in' => __('转入', 'aksrt'),
|
||||
'transfer_out' => __('转出', 'aksrt'),
|
||||
'tip_sent' => __('打赏支出', 'aksrt'),
|
||||
'tip_received' => __('收到打赏', 'aksrt'),
|
||||
'admin' => __('管理员调整', 'aksrt'),
|
||||
'points_exchange_out' => __('余额兑换积分', 'aksrt'),
|
||||
'points_exchange_refund' => __('余额兑换积分退款', 'aksrt'),
|
||||
'creator_income' => __('创作收益结算', 'aksrt'),
|
||||
'rebate' => __('推广返佣结算', 'aksrt'),
|
||||
'withdraw_hold' => __('提现冻结', 'aksrt'),
|
||||
'withdraw_refund' => __('提现退回', 'aksrt'),
|
||||
];
|
||||
$key = sanitize_key($reason);
|
||||
return $map[$key] ?? ($reason !== '' ? $reason : __('余额变动', 'aksrt'));
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ class BalanceService {
|
||||
|
||||
public function init(): void {
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
add_action('aksrt_order_paid', [$this, 'on_order_paid'], 20, 2);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,93 +12,96 @@ trait BalanceServiceCharge {
|
||||
|
||||
|
||||
/**
|
||||
* When a recharge order is paid, credit the wallet.
|
||||
* Atomically confirms a recharge order and credits its cash account.
|
||||
*
|
||||
* @param string $order_number Order number.
|
||||
* @param array<string,mixed> $order Order row.
|
||||
* @param array<string,mixed> $order
|
||||
*/
|
||||
public function on_order_paid(string $order_number, array $order = []): void {
|
||||
if ($order === [] && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number) ?: [];
|
||||
}
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
$type = sanitize_key((string) ($order['product_type'] ?? ''));
|
||||
public function complete_recharge_payment(array $order, string $payment_method, string $provider_transaction_id, string $status_note = ''): bool {
|
||||
$order_number = sanitize_text_field((string) ($order['order_number'] ?? ''));
|
||||
$user_id = (int) ($order['user_id'] ?? 0);
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) {
|
||||
return;
|
||||
}
|
||||
if (!class_exists('AKSRT\\Credits\\Balance')) {
|
||||
return;
|
||||
}
|
||||
// Honor pay_auto_to_balance (default on). When off, leave order paid without wallet credit.
|
||||
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_payment_recharge_settings')) {
|
||||
$recharge_cfg = \AKSRT\Settings\Settings::get_payment_recharge_settings();
|
||||
if (isset($recharge_cfg['auto_to_balance']) && empty($recharge_cfg['auto_to_balance'])) {
|
||||
return;
|
||||
}
|
||||
if ($order_number === '' || $user_id <= 0 || sanitize_key((string) ($order['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) {
|
||||
return false;
|
||||
}
|
||||
$note = json_decode((string) ($order['note'] ?? ''), true);
|
||||
if (!is_array($note)) {
|
||||
$note = [];
|
||||
$note = is_array($note) ? $note : [];
|
||||
if (!isset($note['credit_amount']) || !is_string($note['credit_amount'])) {
|
||||
return false;
|
||||
}
|
||||
if (!empty($note['balance_credited'])) {
|
||||
return;
|
||||
$amount_raw = $note['credit_amount'];
|
||||
try {
|
||||
$credit = \AKSRT\Ledger\AssetAmount::balance($amount_raw);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return false;
|
||||
}
|
||||
$credit_amount = isset($note['credit_amount'])
|
||||
? round((float) $note['credit_amount'], 2)
|
||||
: round((float) ($order['amount'] ?? 0), 2);
|
||||
if ($credit_amount <= 0) {
|
||||
return;
|
||||
if ($credit->minor() <= 0) {
|
||||
return false;
|
||||
}
|
||||
global $wpdb;
|
||||
if ($wpdb->query('START TRANSACTION') === false) {
|
||||
return;
|
||||
}
|
||||
$result = Balance::add(
|
||||
$payment_method = sanitize_key($payment_method);
|
||||
$provider_transaction_id = sanitize_text_field($provider_transaction_id);
|
||||
$locked_status = '';
|
||||
$locked_note = [];
|
||||
$guard = function () use ($order_number, $user_id, &$locked_status, &$locked_note) {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('order');
|
||||
$current = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE order_number = %s FOR UPDATE", $order_number), ARRAY_A);
|
||||
if (!is_array($current)
|
||||
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
||||
|| sanitize_key((string) ($current['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE
|
||||
|| !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]);
|
||||
}
|
||||
$locked_status = (string) $current['status'];
|
||||
$decoded = json_decode((string) ($current['note'] ?? ''), true);
|
||||
$locked_note = is_array($decoded) ? $decoded : [];
|
||||
return true;
|
||||
};
|
||||
$mutation = function () use ($order_number, $payment_method, $provider_transaction_id, $status_note, $credit, &$locked_status, &$locked_note) {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('order');
|
||||
$locked_note['ledger_credited'] = 1;
|
||||
$locked_note['credited_amount'] = $credit->decimal();
|
||||
if ($status_note !== '') {
|
||||
$locked_note['status_note'] = sanitize_textarea_field($status_note);
|
||||
}
|
||||
$now = current_time('mysql');
|
||||
$updated = $wpdb->update($table, [
|
||||
'status' => \AKSRT\Payment\Order::STATUS_COMPLETED,
|
||||
'payment_method' => $payment_method,
|
||||
'payment_transaction_id' => $provider_transaction_id,
|
||||
'paid_at' => $now,
|
||||
'completed_at' => $now,
|
||||
'note' => wp_json_encode($locked_note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
], ['order_number' => $order_number, 'status' => $locked_status], ['%s', '%s', '%s', '%s', '%s', '%s'], ['%s', '%s']);
|
||||
return $updated === 1 ? true : new \WP_Error('aksrt_order_confirm_failed', __('订单状态确认失败', 'aksrt'), ['status' => 409]);
|
||||
};
|
||||
$result = \AKSRT\Ledger\LedgerService::get_instance()->credit(
|
||||
$user_id,
|
||||
$credit_amount,
|
||||
\AKSRT\Ledger\AssetType::BALANCE,
|
||||
$credit->minor(),
|
||||
\AKSRT\Ledger\OperationKey::deterministic('recharge', [$order_number]),
|
||||
'recharge',
|
||||
['type' => 'order', 'order' => $order_number],
|
||||
sprintf(__('余额充值订单 %s', 'aksrt'), $order_number),
|
||||
false
|
||||
$user_id,
|
||||
['payment_method' => $payment_method, 'provider_transaction_id' => $provider_transaction_id],
|
||||
$mutation,
|
||||
$guard
|
||||
);
|
||||
if (is_wp_error($result)) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
$note['balance_credited'] = 1;
|
||||
$note['credited_amount'] = $credit_amount;
|
||||
$updated = $wpdb->update(
|
||||
$wpdb->prefix . 'aksrt_order',
|
||||
['note' => wp_json_encode($note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)],
|
||||
['order_number' => $order_number],
|
||||
['%s'],
|
||||
['%s']
|
||||
);
|
||||
if ($updated === false) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
wp_cache_delete($user_id, 'user_meta');
|
||||
return;
|
||||
}
|
||||
if ($wpdb->query('COMMIT') === false) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
wp_cache_delete($user_id, 'user_meta');
|
||||
return;
|
||||
}
|
||||
do_action('aksrt_balance_added', $user_id, $credit_amount, 'recharge', (float) $result);
|
||||
if (class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
|
||||
$order_number,
|
||||
\AKSRT\Payment\Order::STATUS_COMPLETED,
|
||||
['note' => __('充值金额已到账。', 'aksrt')]
|
||||
);
|
||||
if (!$result->replayed) {
|
||||
$paid_order = array_merge($order, [
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PAID,
|
||||
'payment_method' => $payment_method,
|
||||
'payment_transaction_id' => $provider_transaction_id,
|
||||
]);
|
||||
do_action('aksrt_order_status_changed', $order_number, \AKSRT\Payment\Order::STATUS_PAID, $paid_order);
|
||||
do_action('aksrt_order_paid', $order_number, $paid_order);
|
||||
do_action('aksrt_order_status_changed', $order_number, \AKSRT\Payment\Order::STATUS_COMPLETED, array_merge($paid_order, ['status' => \AKSRT\Payment\Order::STATUS_COMPLETED]));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +128,7 @@ trait BalanceServiceCharge {
|
||||
return [
|
||||
'enabled' => !empty($cfg['enabled']),
|
||||
'desc' => (string) ($cfg['desc'] ?? ''),
|
||||
'wallet' => class_exists('AKSRT\\Credits\\Balance') ? Balance::balance($user_id) : 0.0,
|
||||
'wallet' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
'packages' => $packages,
|
||||
'custom_s' => $custom_s,
|
||||
'custom_limit' => $custom_limit,
|
||||
@@ -143,7 +146,7 @@ trait BalanceServiceCharge {
|
||||
|
||||
|
||||
/**
|
||||
* @return array<int,array{key:string,price:float,pay_price:float,title:string,tag:string}>
|
||||
* @return array<int,array{key:string,price:string,pay_price:string,title:string,tag:string}>
|
||||
*/
|
||||
public function parse_packages(string $raw): array {
|
||||
$out = [];
|
||||
@@ -154,23 +157,25 @@ trait BalanceServiceCharge {
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
// pay|credit|title OR credit|pay|title — legacy uses price(credit)|pay_price|tag
|
||||
// Admin help: 支付金额|到账金额|标题
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
if (count($parts) < 2) {
|
||||
continue;
|
||||
}
|
||||
$pay = (float) $parts[0];
|
||||
$credit = (float) $parts[1];
|
||||
try {
|
||||
$pay = \AKSRT\Ledger\AssetAmount::balance((string) $parts[0]);
|
||||
$credit = \AKSRT\Ledger\AssetAmount::balance((string) $parts[1]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
continue;
|
||||
}
|
||||
$title = $parts[2] ?? '';
|
||||
if ($pay <= 0 || $credit <= 0) {
|
||||
if ($pay->minor() <= 0 || $credit->minor() <= 0) {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'key' => (string) $i,
|
||||
'pay_price' => round($pay, 2),
|
||||
'price' => round($credit, 2),
|
||||
'title' => $title !== '' ? $title : sprintf(__('充值 %s 元', 'aksrt'), (string) $credit),
|
||||
'pay_price' => $pay->decimal(),
|
||||
'price' => $credit->decimal(),
|
||||
'title' => $title !== '' ? $title : sprintf(__('充值 %s 元', 'aksrt'), $credit->decimal()),
|
||||
'tag' => $title,
|
||||
];
|
||||
$i++;
|
||||
@@ -183,7 +188,7 @@ trait BalanceServiceCharge {
|
||||
/**
|
||||
* @return array<string,mixed>|\WP_Error
|
||||
*/
|
||||
public function create_charge_order(int $user_id, string $package_key, float $custom_price, string $payment_method) {
|
||||
public function create_charge_order(int $user_id, string $package_key, string $custom_price, string $payment_method, string $operation_key) {
|
||||
if ($user_id <= 0) {
|
||||
return new \WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]);
|
||||
}
|
||||
@@ -191,23 +196,26 @@ trait BalanceServiceCharge {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('订单系统不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$view = $this->get_charge_view($user_id);
|
||||
$credit_amount = 0.0;
|
||||
$pay_amount = 0.0;
|
||||
$credit_amount = null;
|
||||
$pay_amount = null;
|
||||
$title = __('余额充值', 'aksrt');
|
||||
|
||||
if ($package_key === 'custom' || $package_key === '') {
|
||||
if (empty($view['custom_s'])) {
|
||||
return new \WP_Error('aksrt_custom_closed', __('未开放自定义金额', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$custom_price = round($custom_price, 2);
|
||||
$min = 1.0;
|
||||
$max = (float) $view['custom_limit'];
|
||||
if ($custom_price < $min || $custom_price > $max) {
|
||||
return new \WP_Error('aksrt_custom_limit', sprintf(__('充值金额需在 %s ~ %s 之间', 'aksrt'), (string) $min, (string) $max), ['status' => 400]);
|
||||
try {
|
||||
$custom_amount = \AKSRT\Ledger\AssetAmount::balance($custom_price);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new \WP_Error('aksrt_invalid_price', __('金额格式无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$pay_amount = $custom_price;
|
||||
$credit_amount = $custom_price;
|
||||
$title = sprintf(__('自定义充值 %s 元', 'aksrt'), (string) $custom_price);
|
||||
$max = max(1, (int) $view['custom_limit']);
|
||||
if ($custom_amount->minor() < 100 || $custom_amount->minor() > $max * 100) {
|
||||
return new \WP_Error('aksrt_custom_limit', sprintf(__('充值金额需在 1 ~ %d 之间', 'aksrt'), $max), ['status' => 400]);
|
||||
}
|
||||
$pay_amount = $custom_amount;
|
||||
$credit_amount = $custom_amount;
|
||||
$title = sprintf(__('自定义充值 %s 元', 'aksrt'), $custom_amount->decimal());
|
||||
$package_key = 'custom';
|
||||
} else {
|
||||
$found = null;
|
||||
@@ -220,14 +228,24 @@ trait BalanceServiceCharge {
|
||||
if (!$found) {
|
||||
return new \WP_Error('aksrt_invalid_package', __('充值套餐无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$pay_amount = (float) $found['pay_price'];
|
||||
$credit_amount = (float) $found['price'];
|
||||
try {
|
||||
$pay_amount = \AKSRT\Ledger\AssetAmount::balance((string) $found['pay_price']);
|
||||
$credit_amount = \AKSRT\Ledger\AssetAmount::balance((string) $found['price']);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new \WP_Error('aksrt_invalid_package', __('充值套餐金额无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$title = (string) $found['title'];
|
||||
}
|
||||
|
||||
if ($pay_amount <= 0 || $credit_amount <= 0) {
|
||||
if (!($pay_amount instanceof \AKSRT\Ledger\AssetAmount)
|
||||
|| !($credit_amount instanceof \AKSRT\Ledger\AssetAmount)
|
||||
|| $pay_amount->minor() <= 0
|
||||
|| $credit_amount->minor() <= 0
|
||||
) {
|
||||
return new \WP_Error('aksrt_invalid_price', __('金额无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$pay_decimal = $pay_amount->decimal();
|
||||
$credit_decimal = $credit_amount->decimal();
|
||||
|
||||
$payment_method = sanitize_key($payment_method);
|
||||
if ($payment_method === '' || $payment_method === 'balance') {
|
||||
@@ -239,22 +257,46 @@ trait BalanceServiceCharge {
|
||||
$note = wp_json_encode([
|
||||
'balance_recharge' => 1,
|
||||
'package' => $package_key,
|
||||
'credit_amount' => $credit_amount,
|
||||
'pay_amount' => $pay_amount,
|
||||
'credit_amount' => $credit_decimal,
|
||||
'pay_amount' => $pay_decimal,
|
||||
'title' => $title,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
$existing = $repo->get_by_request_key($operation_key);
|
||||
if (is_array($existing)) {
|
||||
$existing_note = json_decode((string) ($existing['note'] ?? ''), true);
|
||||
$existing_note = is_array($existing_note) ? $existing_note : [];
|
||||
$existing_credit = isset($existing_note['credit_amount']) && is_string($existing_note['credit_amount'])
|
||||
? $existing_note['credit_amount']
|
||||
: '';
|
||||
$existing_pay = isset($existing_note['pay_amount']) && is_string($existing_note['pay_amount'])
|
||||
? $existing_note['pay_amount']
|
||||
: '';
|
||||
$same = (int) ($existing['user_id'] ?? 0) === $user_id
|
||||
&& sanitize_key((string) ($existing['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE
|
||||
&& sanitize_key((string) ($existing['payment_method'] ?? '')) === $payment_method
|
||||
&& (string) ($existing_note['package'] ?? '') === $package_key
|
||||
&& hash_equals($existing_credit, $credit_decimal)
|
||||
&& hash_equals($existing_pay, $pay_decimal);
|
||||
if (!$same) {
|
||||
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_RECHARGE,
|
||||
'product_id' => 0,
|
||||
'amount' => $pay_amount,
|
||||
'amount' => $pay_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 === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
@@ -263,6 +305,28 @@ trait BalanceServiceCharge {
|
||||
if (!$order) {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
if (is_array($existing) && in_array((string) ($order['status'] ?? ''), [
|
||||
\AKSRT\Payment\Order::STATUS_PAID,
|
||||
\AKSRT\Payment\Order::STATUS_COMPLETED,
|
||||
], true)) {
|
||||
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('recharge', [$order_number]);
|
||||
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
||||
$transaction_id = (int) ($history['items'][0]['id'] ?? 0);
|
||||
if ($transaction_id <= 0) {
|
||||
return new \WP_Error('aksrt_recharge_state', __('充值订单与账本状态不一致', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
return [
|
||||
'redirect' => false,
|
||||
'order_number' => $order_number,
|
||||
'amount' => $pay_decimal,
|
||||
'credit_amount' => $credit_decimal,
|
||||
'transaction_id' => $transaction_id,
|
||||
'replayed' => true,
|
||||
'wallet' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
'message' => __('充值成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
];
|
||||
}
|
||||
$order['subject'] = $title;
|
||||
$order['payment_method'] = $payment_method;
|
||||
|
||||
@@ -285,7 +349,7 @@ trait BalanceServiceCharge {
|
||||
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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,23 +359,29 @@ trait BalanceServiceCharge {
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'amount' => $pay_amount,
|
||||
'credit_amount' => $credit_amount,
|
||||
'amount' => $pay_decimal,
|
||||
'credit_amount' => $credit_decimal,
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
'transaction_id' => 0,
|
||||
'replayed' => false,
|
||||
'wallet' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
];
|
||||
}
|
||||
|
||||
// Synchronous gateway: PaymentOrchestrator's paid transition already fired
|
||||
// aksrt_order_paid, so fulfillment has run through the single hook path.
|
||||
// The synchronous gateway has already committed the recharge and order state.
|
||||
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('recharge', [$order_number]);
|
||||
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
||||
|
||||
return [
|
||||
'redirect' => false,
|
||||
'order_number' => $order_number,
|
||||
'amount' => $pay_amount,
|
||||
'credit_amount' => $credit_amount,
|
||||
'wallet' => Balance::balance($user_id),
|
||||
'amount' => $pay_decimal,
|
||||
'credit_amount' => $credit_decimal,
|
||||
'transaction_id' => (int) ($history['items'][0]['id'] ?? 0),
|
||||
'replayed' => false,
|
||||
'wallet' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
'message' => __('充值成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
];
|
||||
|
||||
+164
-182
@@ -16,16 +16,17 @@ trait BalanceServiceRest {
|
||||
[
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'rest_charge_options'],
|
||||
'permission_callback' => static function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
'permission_callback' => [$this, 'check_asset_rest'],
|
||||
],
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'rest_charge_create'],
|
||||
'permission_callback' => static function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
'permission_callback' => [$this, 'check_asset_rest'],
|
||||
'args' => [
|
||||
'package' => ['type' => 'string', 'default' => 'custom', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'custom_price' => ['type' => 'string', 'default' => '0', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
|
||||
'payment_method' => ['type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_key'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -33,9 +34,18 @@ trait BalanceServiceRest {
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'rest_transfer'],
|
||||
'permission_callback' => static function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
'permission_callback' => [$this, 'check_asset_rest'],
|
||||
'args' => [
|
||||
'type' => ['type' => 'string', 'required' => true, 'enum' => ['balance', 'points']],
|
||||
'recipient' => ['type' => 'integer', 'required' => true, 'minimum' => 1],
|
||||
'amount' => [
|
||||
'required' => true,
|
||||
'oneOf' => [
|
||||
['type' => 'integer', 'minimum' => 1],
|
||||
['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -43,9 +53,7 @@ trait BalanceServiceRest {
|
||||
[
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'rest_transfer_search'],
|
||||
'permission_callback' => static function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
'permission_callback' => [$this, 'check_asset_rest'],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -53,21 +61,30 @@ trait BalanceServiceRest {
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'rest_exchange_balance_for_points'],
|
||||
'permission_callback' => static function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
'permission_callback' => [$this, 'check_asset_rest'],
|
||||
'args' => ['amount' => ['type' => 'string', 'required' => true, 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$']],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/pay/balance/withdraw', [
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'rest_balance_withdraw'],
|
||||
'permission_callback' => static function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function check_asset_rest(\WP_REST_Request $request) {
|
||||
if (!is_user_logged_in()) {
|
||||
return new \WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]);
|
||||
}
|
||||
$nonce = (string) $request->get_header('X-WP-Nonce');
|
||||
if ($nonce === '' || !wp_verify_nonce($nonce, 'wp_rest')) {
|
||||
return new \WP_Error('aksrt_rest_nonce', __('安全验证失败,请刷新页面后重试', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function balance_feature_enabled(): bool {
|
||||
if (!class_exists('\\AKSRT\\Settings\\Settings')) {
|
||||
return false;
|
||||
}
|
||||
$cfg = \AKSRT\Settings\Settings::get_payment_recharge_settings();
|
||||
return !empty($cfg['enabled']);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +93,7 @@ trait BalanceServiceRest {
|
||||
* GET charge options (packages + methods + limits).
|
||||
*/
|
||||
public function rest_charge_options(\WP_REST_Request $request) {
|
||||
if (!class_exists('AKSRT\\Credits\\Balance') || !Balance::enabled()) {
|
||||
if (!$this->balance_feature_enabled()) {
|
||||
return new \WP_Error('aksrt_balance_disabled', __('余额功能未启用', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
@@ -92,19 +109,23 @@ trait BalanceServiceRest {
|
||||
* POST create recharge order and initiate payment.
|
||||
*/
|
||||
public function rest_charge_create(\WP_REST_Request $request) {
|
||||
if (!class_exists('AKSRT\\Credits\\Balance') || !Balance::enabled()) {
|
||||
if (!$this->balance_feature_enabled()) {
|
||||
return new \WP_Error('aksrt_balance_disabled', __('余额功能未启用', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'recharge-create', $user_id);
|
||||
if (is_wp_error($operation_key)) {
|
||||
return $operation_key;
|
||||
}
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$package_key = sanitize_text_field((string) ($params['package'] ?? 'custom'));
|
||||
$custom_price = isset($params['custom_price']) ? (float) $params['custom_price'] : 0.0;
|
||||
$custom_price = isset($params['custom_price']) ? (string) $params['custom_price'] : '0';
|
||||
$payment_method = sanitize_key((string) ($params['payment_method'] ?? 'alipay'));
|
||||
|
||||
$result = $this->create_charge_order($user_id, $package_key, $custom_price, $payment_method);
|
||||
$result = $this->create_charge_order($user_id, $package_key, $custom_price, $payment_method, $operation_key);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
@@ -123,8 +144,12 @@ trait BalanceServiceRest {
|
||||
}
|
||||
$type = sanitize_key((string) ($params['type'] ?? 'balance'));
|
||||
$recipient = (int) ($params['recipient'] ?? 0);
|
||||
$amount_raw = $params['amount'] ?? $params['price'] ?? 0;
|
||||
$amount_raw = $params['amount'] ?? '';
|
||||
$from_id = get_current_user_id();
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'transfer', $from_id);
|
||||
if (is_wp_error($operation_key)) {
|
||||
return $operation_key;
|
||||
}
|
||||
|
||||
if (!in_array($type, ['balance', 'points'], true)) {
|
||||
return new \WP_Error('aksrt_transfer_type', __('转账类型无效', 'aksrt'), ['status' => 400]);
|
||||
@@ -147,45 +172,43 @@ trait BalanceServiceRest {
|
||||
if (class_exists('\AKSRT\Settings\Capabilities') && !\AKSRT\Settings\Capabilities::user_can('balance_transfer', $from_id)) {
|
||||
return new \WP_Error('aksrt_transfer_forbidden', __('当前账号无权转账余额', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$amount = round((float) $amount_raw, 2);
|
||||
if ($amount <= 0) {
|
||||
try {
|
||||
$amount = \AKSRT\Ledger\AssetAmount::balance(is_string($amount_raw) ? $amount_raw : '');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new \WP_Error('aksrt_transfer_amount', __('请输入有效金额', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if (!class_exists('AKSRT\\Credits\\Balance')) {
|
||||
return new \WP_Error('aksrt_balance_unavailable', __('余额系统不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$from_bal = Balance::balance($from_id);
|
||||
if ($from_bal < $amount) {
|
||||
return new \WP_Error('aksrt_balance_insufficient', __('余额不足', 'aksrt'), ['status' => 402]);
|
||||
if ($amount->minor() <= 0) {
|
||||
return new \WP_Error('aksrt_transfer_amount', __('请输入有效金额', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$fee_rate = max(0, min(50, (int) ($cfg['transfer']['service_charge'] ?? 0)));
|
||||
$credit = $fee_rate > 0 ? round($amount * (100 - $fee_rate) / 100, 2) : $amount;
|
||||
$fee_minor = $this->percentage_minor($amount->minor(), $fee_rate);
|
||||
|
||||
$from_user = wp_get_current_user();
|
||||
$to_user = get_userdata($recipient);
|
||||
$deduct = Balance::deduct(
|
||||
$result = \AKSRT\Ledger\LedgerService::get_instance()->transfer(
|
||||
$from_id,
|
||||
$amount,
|
||||
'transfer_out',
|
||||
['type' => 'user', 'id' => $recipient],
|
||||
sprintf(__('转账给用户[%s]', 'aksrt'), $to_user ? $to_user->display_name : (string) $recipient)
|
||||
);
|
||||
if (is_wp_error($deduct)) {
|
||||
return $deduct;
|
||||
}
|
||||
Balance::add(
|
||||
$recipient,
|
||||
$credit,
|
||||
'transfer_in',
|
||||
['type' => 'user', 'id' => $from_id],
|
||||
sprintf(__('来自用户[%s]的转账', 'aksrt'), $from_user->display_name ?: (string) $from_id)
|
||||
\AKSRT\Ledger\AssetType::BALANCE,
|
||||
$amount->minor(),
|
||||
$fee_minor,
|
||||
$operation_key,
|
||||
'transfer',
|
||||
['type' => 'user', 'id' => $recipient],
|
||||
sprintf(__('转账给用户[%s]', 'aksrt'), $to_user ? $to_user->display_name : (string) $recipient),
|
||||
sprintf(__('来自用户[%s]的转账', 'aksrt'), $from_user->display_name ?: (string) $from_id),
|
||||
['fee_rate' => $fee_rate]
|
||||
);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'message' => __('转账成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
'balance' => Balance::balance($from_id),
|
||||
'transaction_id' => $result->transaction_id,
|
||||
'replayed' => $result->replayed,
|
||||
'balance' => \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, $result->balance($from_id, \AKSRT\Ledger\AssetType::BALANCE))->decimal(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -201,57 +224,42 @@ trait BalanceServiceRest {
|
||||
if (class_exists('\AKSRT\Settings\Capabilities') && !\AKSRT\Settings\Capabilities::user_can('points_transfer', $from_id)) {
|
||||
return new \WP_Error('aksrt_transfer_forbidden', __('当前账号无权转账积分', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
if (filter_var($amount_raw, FILTER_VALIDATE_INT) === false || (int) $amount_raw <= 0) {
|
||||
if (!is_int($amount_raw) || $amount_raw <= 0) {
|
||||
return new \WP_Error('aksrt_transfer_amount', __('请输入有效积分', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$amount = (int) $amount_raw;
|
||||
$amount = $amount_raw;
|
||||
$min_amount = max(1, (int) ($points_settings['points_transfer_min'] ?? 1));
|
||||
if ($amount < $min_amount) {
|
||||
return new \WP_Error('aksrt_transfer_minimum', sprintf(__('单笔转账至少需要 %d 积分', 'aksrt'), $min_amount), ['status' => 400]);
|
||||
}
|
||||
if (!class_exists('AKSRT\\Credits\\Credits')) {
|
||||
return new \WP_Error('aksrt_credits_unavailable', __('积分系统不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$fee_rate = max(0, min(50, (int) ($points_settings['points_service_charge'] ?? 0)));
|
||||
$credit = $fee_rate > 0 ? (int) floor($amount * (100 - $fee_rate) / 100) : $amount;
|
||||
$fee = $this->percentage_minor($amount, $fee_rate);
|
||||
$from_user = wp_get_current_user();
|
||||
$to_user = get_userdata($recipient);
|
||||
$deduct = Credits::deduct(
|
||||
$result = \AKSRT\Ledger\LedgerService::get_instance()->transfer(
|
||||
$from_id,
|
||||
$amount,
|
||||
'transfer_out',
|
||||
['type' => 'user', 'id' => $recipient],
|
||||
sprintf(__('转账给用户[%s]', 'aksrt'), $to_user ? $to_user->display_name : (string) $recipient)
|
||||
);
|
||||
if (is_wp_error($deduct)) {
|
||||
return $deduct;
|
||||
}
|
||||
$add = Credits::add(
|
||||
$recipient,
|
||||
$credit,
|
||||
'transfer_in',
|
||||
['type' => 'user', 'id' => $from_id],
|
||||
sprintf(__('来自用户[%s]的转账', 'aksrt'), $from_user->display_name ?: (string) $from_id)
|
||||
\AKSRT\Ledger\AssetType::POINTS,
|
||||
$amount,
|
||||
$fee,
|
||||
$operation_key,
|
||||
'transfer',
|
||||
['type' => 'user', 'id' => $recipient],
|
||||
sprintf(__('转账给用户[%s]', 'aksrt'), $to_user ? $to_user->display_name : (string) $recipient),
|
||||
sprintf(__('来自用户[%s]的转账', 'aksrt'), $from_user->display_name ?: (string) $from_id),
|
||||
['fee_rate' => $fee_rate]
|
||||
);
|
||||
if (is_wp_error($add)) {
|
||||
$refund = Credits::add(
|
||||
$from_id,
|
||||
$amount,
|
||||
'transfer_refund',
|
||||
['type' => 'user', 'id' => $recipient],
|
||||
__('收款积分入账失败,已退回转账积分', 'aksrt')
|
||||
);
|
||||
if (is_wp_error($refund)) {
|
||||
return new \WP_Error('aksrt_transfer_recovery_failed', __('收款积分入账失败,转账积分退回失败,请联系管理员', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
return $add;
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'message' => __('转账成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
'balance' => Credits::balance($from_id),
|
||||
'transaction_id' => $result->transaction_id,
|
||||
'replayed' => $result->replayed,
|
||||
'balance' => $result->balance($from_id, \AKSRT\Ledger\AssetType::POINTS),
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -289,11 +297,11 @@ trait BalanceServiceRest {
|
||||
* Exchange integer cash-wallet balance for points.
|
||||
*/
|
||||
public function rest_exchange_balance_for_points(\WP_REST_Request $request) {
|
||||
if (!class_exists('AKSRT\\Credits\\Balance') || !Balance::enabled()) {
|
||||
if (!$this->balance_feature_enabled()) {
|
||||
return new \WP_Error('aksrt_balance_disabled', __('余额功能未启用', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
if (!class_exists('AKSRT\\Credits\\Credits') || !class_exists('AKSRT\\Settings\\Settings') || !\AKSRT\Settings\Settings::credits_enabled()) {
|
||||
return new \WP_Error('aksrt_credits_disabled', __('积分功能未启用', 'aksrt'), ['status' => 403]);
|
||||
if (!class_exists('AKSRT\\Settings\\Settings') || !\AKSRT\Settings\Settings::credits_enabled()) {
|
||||
return new \WP_Error('aksrt_points_disabled', __('积分功能未启用', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$cfg = \AKSRT\Settings\Settings::get_payment_recharge_settings();
|
||||
$exchange = is_array($cfg['points_exchange'] ?? null) ? $cfg['points_exchange'] : [];
|
||||
@@ -304,121 +312,95 @@ trait BalanceServiceRest {
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$raw_amount = $params['amount'] ?? 0;
|
||||
if (filter_var($raw_amount, FILTER_VALIDATE_INT) === false || (int) $raw_amount <= 0) {
|
||||
return new \WP_Error('aksrt_exchange_amount', __('兑换余额必须为正整数', 'aksrt'), ['status' => 400]);
|
||||
$raw_amount = $params['amount'] ?? '';
|
||||
try {
|
||||
$amount = \AKSRT\Ledger\AssetAmount::balance(is_string($raw_amount) ? $raw_amount : '');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new \WP_Error('aksrt_exchange_amount', __('兑换余额格式无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if ($amount->minor() <= 0) {
|
||||
return new \WP_Error('aksrt_exchange_amount', __('兑换余额必须大于零', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$amount = (int) $raw_amount;
|
||||
$rate = max(1, (int) ($exchange['rate'] ?? 100));
|
||||
$daily_max = max(1, (int) ($exchange['daily_max_points'] ?? 100000));
|
||||
if ($amount > intdiv($daily_max, $rate)) {
|
||||
$scaled = $this->multiply_non_negative($amount->minor(), $rate);
|
||||
if (is_wp_error($scaled)) {
|
||||
return $scaled;
|
||||
}
|
||||
if ($scaled % 100 !== 0) {
|
||||
return new \WP_Error('aksrt_exchange_precision', __('该金额无法兑换为整数积分', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$points = intdiv($scaled, 100);
|
||||
if ($points > $daily_max) {
|
||||
return new \WP_Error('aksrt_exchange_daily_limit', sprintf(__('今日购买积分上限为 %d', 'aksrt'), $daily_max), ['status' => 400]);
|
||||
}
|
||||
$points = $amount * $rate;
|
||||
$min_points = max(1, (int) ($exchange['min_points'] ?? 100));
|
||||
if ($points < $min_points) {
|
||||
return new \WP_Error('aksrt_exchange_min', sprintf(__('单笔至少购买 %d 积分', 'aksrt'), $min_points), ['status' => 400]);
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
$today_points = $this->today_points_exchange_total($user_id);
|
||||
if ($today_points + $points > $daily_max) {
|
||||
return new \WP_Error('aksrt_exchange_daily_limit', sprintf(__('今日购买积分上限为 %d,当前还可购买 %d', 'aksrt'), $daily_max, max(0, $daily_max - $today_points)), ['status' => 400]);
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'points-exchange', $user_id);
|
||||
if (is_wp_error($operation_key)) {
|
||||
return $operation_key;
|
||||
}
|
||||
try {
|
||||
$exchange_ref = random_int(1, PHP_INT_MAX);
|
||||
} catch (\Throwable $e) {
|
||||
$exchange_ref = (int) round(microtime(true) * 1000000);
|
||||
$site_now = current_datetime();
|
||||
$start = (clone $site_now)->setTime(0, 0, 0);
|
||||
$end = (clone $start)->modify('+1 day');
|
||||
$utc = new \DateTimeZone('UTC');
|
||||
$start_gmt = $start->setTimezone($utc)->format('Y-m-d H:i:s.u');
|
||||
$end_gmt = $end->setTimezone($utc)->format('Y-m-d H:i:s.u');
|
||||
$guard = static function () use ($user_id, $points, $daily_max, $start_gmt, $end_gmt) {
|
||||
$used = \AKSRT\Ledger\LedgerQuery::get_instance()->sum_credits($user_id, 'points_exchange', $start_gmt, $end_gmt);
|
||||
if ($used + $points > $daily_max) {
|
||||
return new \WP_Error('aksrt_exchange_daily_limit', sprintf(__('今日购买积分上限为 %d,当前还可购买 %d', 'aksrt'), $daily_max, max(0, $daily_max - $used)), ['status' => 400]);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
$business_mutation = static function () use ($user_id, $amount, $points, $operation_key) {
|
||||
if (class_exists('\\AKSRT\\Payment\\CommissionLedger')) {
|
||||
return \AKSRT\Payment\CommissionLedger::get_instance()->record_points_exchange($user_id, $amount->decimal(), $points, $operation_key);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
$result = \AKSRT\Ledger\LedgerService::get_instance()->exchange(
|
||||
$user_id,
|
||||
$amount->minor(),
|
||||
$points,
|
||||
$operation_key,
|
||||
'points_exchange',
|
||||
['type' => 'points_exchange', 'key' => $operation_key],
|
||||
sprintf(__('余额兑换 %d 积分', 'aksrt'), $points),
|
||||
['rate' => $rate],
|
||||
$guard,
|
||||
$business_mutation
|
||||
);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
$ref = ['type' => 'points_exchange', 'id' => $exchange_ref];
|
||||
$deduct = Balance::deduct($user_id, (float) $amount, 'points_exchange_out', $ref, sprintf(__('余额兑换 %d 积分', 'aksrt'), $points));
|
||||
if (is_wp_error($deduct)) return $deduct;
|
||||
$add = Credits::add($user_id, $points, 'points_exchange_in', $ref, sprintf(__('余额兑换:扣除 %d 余额', 'aksrt'), $amount));
|
||||
if (is_wp_error($add)) {
|
||||
$refund = Balance::add($user_id, (float) $amount, 'points_exchange_refund', $ref, __('积分入账失败,已退回兑换余额', 'aksrt'));
|
||||
if (is_wp_error($refund)) return new \WP_Error('aksrt_exchange_recovery_failed', __('积分入账失败,余额退回失败,请联系管理员', 'aksrt'), ['status' => 500]);
|
||||
return $add;
|
||||
}
|
||||
do_action('aksrt_points_exchange_completed', $user_id, (float) $amount, $points, (string) $exchange_ref);
|
||||
return rest_ensure_response(['success' => true, 'data' => [
|
||||
'message' => sprintf(__('兑换成功,获得 %d 积分', 'aksrt'), $points),
|
||||
'wallet_balance' => Balance::balance($user_id),
|
||||
'credit_balance' => Credits::balance($user_id),
|
||||
'transaction_id' => $result->transaction_id,
|
||||
'replayed' => $result->replayed,
|
||||
'wallet_balance' => \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, $result->balance($user_id, \AKSRT\Ledger\AssetType::BALANCE))->decimal(),
|
||||
'credit_balance' => $result->balance($user_id, \AKSRT\Ledger\AssetType::POINTS),
|
||||
'points' => $points,
|
||||
'reload' => true,
|
||||
]]);
|
||||
}
|
||||
|
||||
private function today_points_exchange_total(int $user_id): int {
|
||||
if ($user_id <= 0 || !class_exists('AKSRT\\Core\\DBMigrator')) return 0;
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('credit_log');
|
||||
if ($table === '') return 0;
|
||||
$site_now = current_datetime();
|
||||
$start = (clone $site_now)->setTime(0, 0, 0);
|
||||
$end = (clone $start)->modify('+1 day');
|
||||
$utc = new \DateTimeZone('UTC');
|
||||
$sql = "SELECT COALESCE(SUM(delta), 0) FROM {$table} WHERE user_id = %d AND reason = %s AND created_at >= %s AND created_at < %s";
|
||||
$start_gmt = $start->setTimezone($utc)->format('Y-m-d H:i:s');
|
||||
$end_gmt = $end->setTimezone($utc)->format('Y-m-d H:i:s');
|
||||
return max(0, (int) $wpdb->get_var($wpdb->prepare($sql, $user_id, 'points_exchange_in', $start_gmt, $end_gmt)));
|
||||
private function percentage_minor(int $amount, int $rate): int {
|
||||
if ($amount <= 0 || $rate <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (intdiv($amount, 100) * $rate) + intdiv(($amount % 100) * $rate, 100);
|
||||
}
|
||||
|
||||
public function rest_balance_withdraw(\WP_REST_Request $request) {
|
||||
if (!class_exists('AKSRT\\Credits\\Balance') || !Balance::enabled()) {
|
||||
return new \WP_Error('aksrt_balance_disabled', __('余额功能未启用', 'aksrt'), ['status' => 403]);
|
||||
/** @return int|\WP_Error */
|
||||
private function multiply_non_negative(int $left, int $right) {
|
||||
if ($left < 0 || $right < 0 || ($right > 0 && $left > intdiv(PHP_INT_MAX, $right))) {
|
||||
return new \WP_Error('aksrt_ledger_overflow', __('资产金额超出可用范围', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
if (empty($settings['pay_balance_withdraw_s'])) {
|
||||
return new \WP_Error('aksrt_withdraw_closed', __('余额提现未开启', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
if (!class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
return new \WP_Error('aksrt_withdraw_unavailable', __('提现系统不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
if (class_exists('\AKSRT\Settings\Capabilities') && !\AKSRT\Settings\Capabilities::user_can('balance_withdraw', $user_id)) {
|
||||
return new \WP_Error('aksrt_withdraw_forbidden', __('当前账号无权提现', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$raw_amount = $params['amount'] ?? 0;
|
||||
if (filter_var($raw_amount, FILTER_VALIDATE_INT) === false || (int) $raw_amount <= 0) {
|
||||
return new \WP_Error('aksrt_withdraw_amount', __('提现金额必须为正整数', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$amount = (int) $raw_amount;
|
||||
$method = sanitize_key((string) ($params['method'] ?? 'alipay'));
|
||||
$method = $method !== '' ? $method : 'alipay';
|
||||
$account = sanitize_textarea_field((string) ($params['account_info'] ?? $params['account'] ?? ''));
|
||||
$withdraw_settings = class_exists('\\AKSRT\\Settings\\Settings')
|
||||
? \AKSRT\Settings\Settings::get_withdrawal_settings()
|
||||
: ['min_amount' => 50, 'service_charge_rate' => 5, 'daily_count' => 3, 'daily_limit' => 50, 'reset_time' => '00:00'];
|
||||
$lowest = (float) $withdraw_settings['min_amount'];
|
||||
if ($amount < $lowest) {
|
||||
return new \WP_Error('withdraw_min_amount', sprintf(__('提现金额低于最低提现门槛(%s 元)', 'aksrt'), (string) $lowest), ['status' => 400]);
|
||||
}
|
||||
if ($account === '') {
|
||||
return new \WP_Error('aksrt_withdraw_account', __('请填写收款账号信息', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if (!in_array($method, (array) ($withdraw_settings['payout_channels'] ?? []), true)) {
|
||||
return new \WP_Error('aksrt_withdraw_channel', __('当前收款渠道不可用', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$result = \AKSRT\Payment\Withdrawal::get_instance()->create_balance_request($user_id, $amount, $method, $account, $withdraw_settings);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'message' => __('提现申请已提交,请等待处理', 'aksrt'),
|
||||
'withdrawal_id' => $result['withdrawal_id'],
|
||||
'amount' => $amount,
|
||||
'net_amount' => $result['net_amount'],
|
||||
'wallet_balance' => $result['wallet_balance'],
|
||||
'remaining_count' => $result['usage']['remaining_count'],
|
||||
'remaining_amount' => $result['usage']['remaining_amount'],
|
||||
],
|
||||
]);
|
||||
return $left * $right;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Credits;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Credits {
|
||||
|
||||
/** user_meta 键名 */
|
||||
const META_KEY = 'aksrt_credits';
|
||||
|
||||
/**
|
||||
* 取用户余额。
|
||||
*/
|
||||
public static function balance(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
$val = get_user_meta($user_id, self::META_KEY, true);
|
||||
return is_numeric($val) ? (int) $val : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible alias for credit spending.
|
||||
*/
|
||||
public static function get_balance(int $user_id): int {
|
||||
return self::balance($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加积分。
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param int $delta 必须 > 0
|
||||
* @param string $reason 例 register/signin/comment/referral/admin
|
||||
* @param array $ref ['type' => 'post|comment|user|referral', 'id' => int]
|
||||
* @return int|\WP_Error 新余额或错误
|
||||
*/
|
||||
public static function add(int $user_id, int $delta, string $reason, array $ref = [], string $remark = '', bool $dispatch_hooks = true) {
|
||||
if ($user_id <= 0) return new \WP_Error('aksrt_credits_user', __('用户无效', 'aksrt'));
|
||||
if ($delta <= 0) return new \WP_Error('aksrt_credits_delta', __('增加积分必须为正数', 'aksrt'));
|
||||
|
||||
$new_balance = self::atomic_update($user_id, $delta, false);
|
||||
if ($new_balance === null) {
|
||||
return new \WP_Error('aksrt_credits_db', __('积分写入失败', 'aksrt'));
|
||||
}
|
||||
self::log($user_id, $delta, $new_balance, $reason, $ref, $remark);
|
||||
|
||||
if ($dispatch_hooks) {
|
||||
do_action('aksrt_credits_added', $user_id, $delta, $reason, $new_balance);
|
||||
}
|
||||
return $new_balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扣减积分。
|
||||
*
|
||||
* @return int|\WP_Error 新余额或错误
|
||||
*/
|
||||
public static function deduct(int $user_id, int $cost, string $reason, array $ref = [], string $remark = '') {
|
||||
if ($user_id <= 0) return new \WP_Error('aksrt_credits_user', __('用户无效', 'aksrt'));
|
||||
if ($cost < 0) return new \WP_Error('aksrt_credits_delta', __('扣减积分必须为非负数', 'aksrt'));
|
||||
if ($cost === 0) {
|
||||
return self::balance($user_id);
|
||||
}
|
||||
|
||||
$new_balance = self::atomic_update($user_id, $cost, true);
|
||||
if ($new_balance === null) {
|
||||
$current = self::balance($user_id);
|
||||
return new \WP_Error('aksrt_credits_insufficient', __('积分不足', 'aksrt'), [
|
||||
'balance' => $current,
|
||||
'cost' => $cost,
|
||||
]);
|
||||
}
|
||||
|
||||
self::log($user_id, -$cost, $new_balance, $reason, $ref, $remark);
|
||||
|
||||
do_action('aksrt_credits_deducted', $user_id, $cost, $reason, $new_balance);
|
||||
return $new_balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子加减:用单条 SQL UPDATE 修改 user_meta,避免 read-modify-write 竞态。
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param int $abs_delta 绝对值
|
||||
* @param bool $is_deduct true=扣减(带余额检查),false=增加
|
||||
* @return int|null 新余额;失败(余额不足或 DB 错误)返回 null
|
||||
*/
|
||||
private static function atomic_update(int $user_id, int $abs_delta, bool $is_deduct): ?int {
|
||||
global $wpdb;
|
||||
|
||||
// 确保 meta 行存在(首次扣费可能没有 row)
|
||||
$existing = get_user_meta($user_id, self::META_KEY, true);
|
||||
if ($existing === '' || $existing === false || $existing === null) {
|
||||
update_user_meta($user_id, self::META_KEY, 0);
|
||||
}
|
||||
|
||||
$meta_key = self::META_KEY;
|
||||
|
||||
if ($is_deduct) {
|
||||
// 扣减:仅当 meta_value >= cost 时才更新
|
||||
$sql = $wpdb->prepare(
|
||||
"UPDATE {$wpdb->usermeta}
|
||||
SET meta_value = CAST(meta_value AS SIGNED) - %d
|
||||
WHERE user_id = %d
|
||||
AND meta_key = %s
|
||||
AND CAST(meta_value AS SIGNED) >= %d",
|
||||
$abs_delta,
|
||||
$user_id,
|
||||
$meta_key,
|
||||
$abs_delta
|
||||
);
|
||||
} else {
|
||||
$sql = $wpdb->prepare(
|
||||
"UPDATE {$wpdb->usermeta}
|
||||
SET meta_value = CAST(meta_value AS SIGNED) + %d
|
||||
WHERE user_id = %d
|
||||
AND meta_key = %s",
|
||||
$abs_delta,
|
||||
$user_id,
|
||||
$meta_key
|
||||
);
|
||||
}
|
||||
|
||||
$affected = $wpdb->query($sql);
|
||||
if ($affected === false || $affected === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 失效 user meta cache,下次 get_user_meta 会重读
|
||||
wp_cache_delete($user_id, 'user_meta');
|
||||
|
||||
$new = (int) get_user_meta($user_id, self::META_KEY, true);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写一条流水。
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param int $delta 本次变动(正/负/0)
|
||||
* @param int $balance_after 操作后余额
|
||||
* @param string $reason
|
||||
* @param array $ref
|
||||
*/
|
||||
public static function log(int $user_id, int $delta, int $balance_after, string $reason, array $ref = [], string $remark = ''): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::table('credit_log');
|
||||
if (!$table) return;
|
||||
|
||||
$ref_type = isset($ref['type']) ? sanitize_key((string) $ref['type']) : null;
|
||||
$ref_id = isset($ref['id']) ? (int) $ref['id'] : null;
|
||||
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
[
|
||||
'user_id' => $user_id,
|
||||
'delta' => $delta,
|
||||
'balance_after' => $balance_after,
|
||||
'reason' => substr($reason, 0, 64),
|
||||
'remark' => $remark !== '' ? substr($remark, 0, 255) : null,
|
||||
'ref_type' => $ref_type,
|
||||
'ref_id' => $ref_id,
|
||||
'ip' => self::client_ip(),
|
||||
'ua' => self::client_ua(),
|
||||
'created_at' => current_time('mysql', true),
|
||||
],
|
||||
['%d', '%d', '%d', '%s', '%s', '%s', '%d', '%s', '%s', '%s']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取流水(管理员页面用)。
|
||||
*
|
||||
* @param array $args { user_id, reason, page, per_page, date_from, date_to }
|
||||
* @return array { items, total, page, per_page }
|
||||
*/
|
||||
public static function get_log(array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::table('credit_log');
|
||||
if (!$table) {
|
||||
return ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
|
||||
}
|
||||
|
||||
$page = max(1, (int) ($args['page'] ?? 1));
|
||||
$per_page = min(100, max(1, (int) ($args['per_page'] ?? 20)));
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if (!empty($args['user_id'])) {
|
||||
$where[] = 'user_id = %d';
|
||||
$params[] = (int) $args['user_id'];
|
||||
}
|
||||
if (!empty($args['reason'])) {
|
||||
$where[] = 'reason = %s';
|
||||
$params[] = sanitize_key((string) $args['reason']);
|
||||
}
|
||||
if (!empty($args['date_from'])) {
|
||||
$where[] = 'created_at >= %s';
|
||||
$params[] = (string) $args['date_from'];
|
||||
}
|
||||
if (!empty($args['date_to'])) {
|
||||
$where[] = 'created_at <= %s';
|
||||
$params[] = (string) $args['date_to'];
|
||||
}
|
||||
|
||||
$where_sql = implode(' AND ', $where);
|
||||
|
||||
// total
|
||||
$count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
||||
$total = (int) ($params
|
||||
? $wpdb->get_var($wpdb->prepare($count_sql, $params))
|
||||
: $wpdb->get_var($count_sql));
|
||||
|
||||
// items
|
||||
$items_sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d";
|
||||
$items_params = array_merge($params, [$per_page, $offset]);
|
||||
$items = $wpdb->get_results($wpdb->prepare($items_sql, $items_params), ARRAY_A);
|
||||
if (!is_array($items)) $items = [];
|
||||
|
||||
// user 名字预填
|
||||
$uid_set = array_unique(array_map(fn($r) => (int) $r['user_id'], $items));
|
||||
$names = [];
|
||||
foreach ($uid_set as $uid) {
|
||||
$u = get_userdata($uid);
|
||||
if ($u) $names[$uid] = $u->display_name;
|
||||
}
|
||||
foreach ($items as &$row) {
|
||||
$row['user_display_name'] = $names[(int) $row['user_id']] ?? '#' . $row['user_id'];
|
||||
$row['delta'] = (int) $row['delta'];
|
||||
$row['balance_after'] = (int) $row['balance_after'];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户余额列表(管理员)。
|
||||
*
|
||||
* @param array $args { search, page, per_page }
|
||||
* @return array
|
||||
*/
|
||||
public static function list_users(array $args = []): array {
|
||||
$page = max(1, (int) ($args['page'] ?? 1));
|
||||
$per_page = min(100, max(1, (int) ($args['per_page'] ?? 20)));
|
||||
|
||||
$q = [
|
||||
'number' => $per_page,
|
||||
'paged' => $page,
|
||||
'orderby' => 'registered',
|
||||
'order' => 'DESC',
|
||||
];
|
||||
if (!empty($args['search'])) {
|
||||
$q['search'] = '*' . esc_attr((string) $args['search']) . '*';
|
||||
$q['search_columns'] = ['user_login', 'user_email', 'display_name', 'user_nicename'];
|
||||
}
|
||||
$user_query = new \WP_User_Query($q);
|
||||
$users = $user_query->get_results();
|
||||
$items = [];
|
||||
foreach ($users as $u) {
|
||||
$items[] = [
|
||||
'id' => (int) $u->ID,
|
||||
'login' => $u->user_login,
|
||||
'email' => $u->user_email,
|
||||
'display_name' => $u->display_name,
|
||||
'balance' => self::balance((int) $u->ID),
|
||||
'registered' => $u->user_registered,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => (int) $user_query->get_total(),
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
];
|
||||
}
|
||||
|
||||
// ---------- 工具方法 ----------
|
||||
|
||||
private static function client_ip(): string {
|
||||
if (function_exists('aksrt_get_client_ip')) {
|
||||
return (string) aksrt_get_client_ip();
|
||||
}
|
||||
if (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
return (string) $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function client_ua(): string {
|
||||
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
|
||||
return substr($ua, 0, 255);
|
||||
}
|
||||
}
|
||||
+18
-46
@@ -40,17 +40,10 @@ class PointsFree {
|
||||
*/
|
||||
public static function settings(): array {
|
||||
$all = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$credit = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_credit_settings() : [];
|
||||
|
||||
$tasks = [];
|
||||
foreach (self::catalog() as $key => $meta) {
|
||||
$flat_key = 'points_free_' . $key;
|
||||
if (array_key_exists($flat_key, $all)) {
|
||||
$tasks[$key] = max(0, (int) $all[$flat_key]);
|
||||
continue;
|
||||
}
|
||||
// Fallback mapping from legacy / parallel settings.
|
||||
$tasks[$key] = self::legacy_task_points($key, $all, $credit, (int) $meta[2]);
|
||||
$tasks[$key] = max(0, (int) ($all[$flat_key] ?? $meta[2]));
|
||||
}
|
||||
|
||||
$day_max = array_key_exists('points_earn_daily_limit', $all)
|
||||
@@ -75,7 +68,7 @@ class PointsFree {
|
||||
* Whether free points system can run (credits enabled).
|
||||
*/
|
||||
public static function enabled(): bool {
|
||||
return class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled() && class_exists('AKSRT\\Credits\\Credits');
|
||||
return class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,19 +136,31 @@ class PointsFree {
|
||||
}
|
||||
|
||||
$reason = 'free_' . $task_key;
|
||||
$result = Credits::add(
|
||||
$today = self::today_key();
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::deterministic('points-free', [$user_id, $task_key, $today]);
|
||||
$result = \AKSRT\Ledger\LedgerService::get_instance()->credit(
|
||||
$user_id,
|
||||
\AKSRT\Ledger\AssetType::POINTS,
|
||||
$points,
|
||||
$operation_key,
|
||||
$reason,
|
||||
$ref,
|
||||
self::catalog()[$task_key][0] ?? $task_key
|
||||
self::catalog()[$task_key][0] ?? $task_key,
|
||||
0,
|
||||
['task' => $task_key, 'date' => $today],
|
||||
static function () use ($user_id, $points, $task_key) {
|
||||
if (self::remaining_today($user_id) < $points) {
|
||||
return new \WP_Error('aksrt_points_daily_limit', __('今日积分奖励已达上限', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
self::bump_detail($user_id, $points);
|
||||
self::mark_task_done_today($user_id, $task_key);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
self::bump_detail($user_id, $points);
|
||||
self::mark_task_done_today($user_id, $task_key);
|
||||
return $points;
|
||||
}
|
||||
|
||||
@@ -297,37 +302,4 @@ class PointsFree {
|
||||
update_user_meta($user_id, self::META_DETAIL, $detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $all
|
||||
* @param array<string,mixed> $credit
|
||||
*/
|
||||
private static function legacy_task_points(string $key, array $all, array $credit, int $default): int {
|
||||
switch ($key) {
|
||||
case 'sign_up':
|
||||
return max(0, (int) ($credit['register_bonus'] ?? $default));
|
||||
case 'sign_in':
|
||||
$signin = is_array($credit['daily_signin'] ?? null) ? $credit['daily_signin'] : [];
|
||||
if (isset($signin['enabled']) && empty($signin['enabled'])) {
|
||||
return 0;
|
||||
}
|
||||
return max(0, (int) ($signin['base'] ?? $default));
|
||||
case 'login':
|
||||
return max(0, (int) ($all['points_earn_login'] ?? $default));
|
||||
case 'post_new':
|
||||
return max(0, (int) ($all['points_earn_post'] ?? $default));
|
||||
case 'post_like':
|
||||
return max(0, (int) ($all['points_earn_like_received'] ?? $default));
|
||||
case 'comment_new':
|
||||
if (isset($all['points_earn_comment'])) {
|
||||
return max(0, (int) $all['points_earn_comment']);
|
||||
}
|
||||
$cr = is_array($credit['comment_reward'] ?? null) ? $credit['comment_reward'] : [];
|
||||
if (isset($cr['enabled']) && empty($cr['enabled'])) {
|
||||
return 0;
|
||||
}
|
||||
return max(0, (int) ($cr['amount'] ?? $default));
|
||||
default:
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
-64
@@ -27,33 +27,36 @@ class Signin {
|
||||
register_rest_route('aksrt/v1', '/signin/today', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_today'],
|
||||
'permission_callback' => function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
'permission_callback' => [$this, 'check_rest_permission'],
|
||||
]);
|
||||
register_rest_route('aksrt/v1', '/signin', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_signin'],
|
||||
'permission_callback' => function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
'permission_callback' => [$this, 'check_rest_permission'],
|
||||
]);
|
||||
register_rest_route('aksrt/v1', '/signin/history', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_history'],
|
||||
'permission_callback' => function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
'permission_callback' => [$this, 'check_rest_permission'],
|
||||
]);
|
||||
register_rest_route('aksrt/v1', '/signin/details', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_details_html'],
|
||||
'permission_callback' => function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
'permission_callback' => [$this, 'check_rest_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function check_rest_permission(\WP_REST_Request $request) {
|
||||
if (!is_user_logged_in()) {
|
||||
return new \WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]);
|
||||
}
|
||||
$nonce = (string) $request->get_header('X-WP-Nonce');
|
||||
if ($nonce === '' || !wp_verify_nonce($nonce, 'wp_rest')) {
|
||||
return new \WP_Error('aksrt_rest_nonce', __('安全验证失败,请刷新页面后重试', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** GET /signin/today */
|
||||
public function rest_today(\WP_REST_Request $request) {
|
||||
$user_id = get_current_user_id();
|
||||
@@ -99,10 +102,6 @@ class Signin {
|
||||
}
|
||||
|
||||
$today_str = $this->today_date();
|
||||
if ($this->get_record($user_id, $today_str)) {
|
||||
return new \WP_Error('aksrt_signin_dup', __('今日已签到', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
|
||||
// 人机验证(若启用)
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$body = $request->get_json_params();
|
||||
@@ -116,59 +115,75 @@ class Signin {
|
||||
}
|
||||
}
|
||||
|
||||
$current_streak = $this->compute_current_streak($user_id, $today_str);
|
||||
$new_streak = $current_streak + 1;
|
||||
$reward = $this->calc_reward($new_streak, $signin_cfg);
|
||||
$existing_record = $this->get_record($user_id, $today_str);
|
||||
if (is_array($existing_record)) {
|
||||
$new_streak = max(1, (int) ($existing_record['streak'] ?? 1));
|
||||
$reward = max(0, (int) ($existing_record['reward'] ?? 0));
|
||||
} else {
|
||||
$current_streak = $this->compute_current_streak($user_id, $today_str);
|
||||
$new_streak = $current_streak + 1;
|
||||
$reward = $this->calc_reward($new_streak, $signin_cfg);
|
||||
}
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::table('signin_log');
|
||||
if (!$table) {
|
||||
return new \WP_Error('aksrt_signin_db', __('数据表不存在', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$inserted = $wpdb->insert(
|
||||
$table,
|
||||
[
|
||||
'user_id' => $user_id,
|
||||
'signin_date' => $today_str,
|
||||
'streak' => $new_streak,
|
||||
'reward' => $reward,
|
||||
],
|
||||
['%d', '%s', '%d', '%d']
|
||||
);
|
||||
if ($inserted === false) {
|
||||
return new \WP_Error('aksrt_signin_dup', __('签到失败', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::deterministic('signin', [$user_id, $today_str]);
|
||||
$transaction_id = null;
|
||||
$replayed = false;
|
||||
if ($reward > 0) {
|
||||
$guard = static function () use ($wpdb, $table, $user_id, $today_str) {
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND signin_date = %s FOR UPDATE",
|
||||
$user_id,
|
||||
$today_str
|
||||
));
|
||||
return $existing === null
|
||||
? true
|
||||
: new \WP_Error('aksrt_signin_dup', __('今日已签到', 'aksrt'), ['status' => 409]);
|
||||
};
|
||||
$mutation = static function () use ($wpdb, $table, $user_id, $today_str, $new_streak, $reward) {
|
||||
$inserted = $wpdb->insert($table, [
|
||||
'user_id' => $user_id,
|
||||
'signin_date' => $today_str,
|
||||
'streak' => $new_streak,
|
||||
'reward' => $reward,
|
||||
], ['%d', '%s', '%d', '%d']);
|
||||
return $inserted === 1
|
||||
? true
|
||||
: new \WP_Error('aksrt_signin_db', __('签到记录写入失败', 'aksrt'), ['status' => 500]);
|
||||
};
|
||||
$ledger_result = \AKSRT\Ledger\LedgerService::get_instance()->credit(
|
||||
$user_id,
|
||||
\AKSRT\Ledger\AssetType::POINTS,
|
||||
$reward,
|
||||
$operation_key,
|
||||
'signin',
|
||||
['type' => 'signin', 'key' => $today_str],
|
||||
__('每日签到奖励', 'aksrt'),
|
||||
$user_id,
|
||||
['signin_date' => $today_str, 'streak' => $new_streak],
|
||||
$mutation,
|
||||
$guard
|
||||
);
|
||||
if (is_wp_error($ledger_result)) {
|
||||
return $ledger_result;
|
||||
}
|
||||
$transaction_id = $ledger_result->transaction_id;
|
||||
$replayed = $ledger_result->replayed;
|
||||
} else {
|
||||
$stored = $this->store_zero_reward_signin($user_id, $today_str, $new_streak);
|
||||
if (is_wp_error($stored)) {
|
||||
return $stored;
|
||||
}
|
||||
$replayed = $stored;
|
||||
}
|
||||
|
||||
do_action('aksrt_user_signed_in', $user_id, $reward);
|
||||
|
||||
if ($reward > 0) {
|
||||
if (class_exists('AKSRT\\Credits\\PointsFree')) {
|
||||
$granted = PointsFree::grant_amount($user_id, $reward, 'sign_in', ['type' => 'user', 'id' => $user_id]);
|
||||
if (is_wp_error($granted)) {
|
||||
if (class_exists('AKSRT\\Credits\\Credits')) {
|
||||
Credits::add($user_id, $reward, 'signin', ['type' => 'user', 'id' => $user_id]);
|
||||
}
|
||||
} else {
|
||||
$reward = (int) $granted;
|
||||
}
|
||||
} elseif (class_exists('AKSRT\\Credits\\Credits')) {
|
||||
Credits::add($user_id, $reward, 'signin', ['type' => 'user', 'id' => $user_id]);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep lightweight meta for header/badge consumers (legacy checkin_all_day).
|
||||
update_user_meta($user_id, 'aksrt_signin_' . $today_str, 1);
|
||||
$all_day = (int) get_user_meta($user_id, 'aksrt_checkin_all_day', true);
|
||||
if ($all_day <= 0) {
|
||||
// Fallback to legacy key if migrated users only have that meta.
|
||||
$all_day = (int) get_user_meta($user_id, 'checkin_all_day', true);
|
||||
}
|
||||
$all_day = $all_day + 1;
|
||||
update_user_meta($user_id, 'aksrt_checkin_all_day', $all_day);
|
||||
update_user_meta($user_id, 'checkin_all_day', $all_day); // legacy alias
|
||||
update_user_meta($user_id, 'aksrt_checkin_continuous_day', $new_streak);
|
||||
update_user_meta($user_id, 'checkin_continuous_day', $new_streak); // legacy alias
|
||||
do_action('aksrt_checkin_done', $user_id);
|
||||
if (!$replayed) {
|
||||
$this->after_signin_committed($user_id, $today_str, $new_streak, $reward);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response([
|
||||
'success' => true,
|
||||
@@ -180,10 +195,49 @@ class Signin {
|
||||
),
|
||||
'streak' => $new_streak,
|
||||
'reward' => $reward,
|
||||
'balance' => class_exists('AKSRT\Credits\Credits') ? Credits::balance($user_id) : 0,
|
||||
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS),
|
||||
'transaction_id' => $transaction_id,
|
||||
'replayed' => $replayed,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/** @return bool|\WP_Error True means the existing row was replayed. */
|
||||
private function store_zero_reward_signin(int $user_id, string $date, int $streak) {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('signin_log');
|
||||
if ($wpdb->query('START TRANSACTION') === false) {
|
||||
return new \WP_Error('aksrt_signin_db', __('无法开启签到事务', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND signin_date = %s FOR UPDATE",
|
||||
$user_id,
|
||||
$date
|
||||
));
|
||||
if ($existing !== null) {
|
||||
$wpdb->query('COMMIT');
|
||||
return true;
|
||||
}
|
||||
$inserted = $wpdb->insert($table, [
|
||||
'user_id' => $user_id,
|
||||
'signin_date' => $date,
|
||||
'streak' => $streak,
|
||||
'reward' => 0,
|
||||
], ['%d', '%s', '%d', '%d']);
|
||||
if ($inserted !== 1 || $wpdb->query('COMMIT') === false) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
return new \WP_Error('aksrt_signin_db', __('签到记录写入失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function after_signin_committed(int $user_id, string $date, int $streak, int $reward): void {
|
||||
update_user_meta($user_id, 'aksrt_signin_' . $date, 1);
|
||||
update_user_meta($user_id, 'aksrt_checkin_all_day', $this->get_total_days($user_id));
|
||||
update_user_meta($user_id, 'aksrt_checkin_continuous_day', $streak);
|
||||
do_action('aksrt_user_signed_in', $user_id, $reward);
|
||||
do_action('aksrt_checkin_done', $user_id);
|
||||
}
|
||||
|
||||
/** GET /signin/history — last N days for calendar/modals. */
|
||||
public function rest_history(\WP_REST_Request $request) {
|
||||
$user_id = get_current_user_id();
|
||||
@@ -260,7 +314,7 @@ class Signin {
|
||||
$rec = $this->get_record($user_id, $today);
|
||||
$streak = $this->compute_current_streak($user_id, $today);
|
||||
$total = $this->get_total_days($user_id);
|
||||
$balance = class_exists('\AKSRT\Credits\Credits') ? (int) Credits::balance($user_id) : 0;
|
||||
$balance = \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS);
|
||||
$currency = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_currency_name()
|
||||
: __('积分', 'aksrt');
|
||||
|
||||
Reference in new issue
Block a user