0 * @param string $reason 例 register/signin/comment/invite/admin * @param array $ref ['type' => 'post|comment|user|invite', 'id' => int] * @return int|WP_Error 新余额或错误 */ public static function add(int $user_id, int $delta, string $reason, array $ref = [], string $remark = '') { 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); 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_DB_Migrator::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_DB_Migrator::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); } }