'GET', 'callback' => [$this, 'rest_today'], 'permission_callback' => [$this, 'check_rest_permission'], ]); register_rest_route('aksrt/v1', '/signin', [ 'methods' => 'POST', 'callback' => [$this, 'rest_signin'], 'permission_callback' => [$this, 'check_rest_permission'], ]); register_rest_route('aksrt/v1', '/signin/details', [ 'methods' => 'GET', 'callback' => [$this, 'rest_details_html'], '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(); $signin_cfg = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_signin_reward_settings() : [ 'enabled' => false, 'base' => 0, 'streak' => [], ]; $today_str = $this->today_date(); $rec = $this->get_record($user_id, $today_str); $current_streak = $this->compute_current_streak($user_id, $today_str); $next_streak = $current_streak + ($rec ? 0 : 1); return new \WP_REST_Response([ 'enabled' => !empty($signin_cfg['enabled']), 'signed' => (bool) $rec, 'streak' => $current_streak, 'reward_today' => $rec ? (int) $rec['reward'] : 0, 'reward_if_signin' => $rec ? 0 : $this->calc_reward($next_streak, $signin_cfg), 'date' => $today_str, ], 200); } /** POST /signin */ public function rest_signin(\WP_REST_Request $request) { global $wpdb; $user_id = get_current_user_id(); $signin_cfg = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_signin_reward_settings() : [ 'enabled' => false, 'base' => 0, 'streak' => [], ]; if (empty($signin_cfg['enabled'])) { return new \WP_Error('aksrt_signin_disabled', __('签到功能未启用', 'aksrt'), ['status' => 403]); } $today_str = $this->today_date(); // 人机验证(若启用) $body = $request->get_json_params(); $cap_payload = is_array($body) && isset($body['captcha']) && is_array($body['captcha']) ? $body['captcha'] : (is_array($body) ? $body : []); $verify = aksrt_require_captcha('signin', $cap_payload); if ($verify !== true) { $code = is_wp_error($verify) ? $verify->get_error_code() : 'aksrt_captcha_failed'; $msg = is_wp_error($verify) ? $verify->get_error_message() : __('人机验证失败', 'aksrt'); return new \WP_Error($code ?: 'aksrt_captcha_failed', $msg, ['status' => 400]); } $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]); } $operation_key = \AKSRT\Ledger\OperationKey::deterministic('signin', [$user_id, $today_str]); $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; } $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; } if (!$replayed) { $this->after_signin_committed($user_id, $today_str, $new_streak, $reward); } return new \WP_REST_Response([ 'success' => true, 'message' => sprintf( /* translators: 1: reward points 2: streak days */ __('签到成功,积分 +%1$d(连续 %2$d 天)', 'aksrt'), (int) $reward, (int) $new_streak ), 'streak' => $new_streak, 'reward' => $reward, 'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS), '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/details — HTML fragment for remote modal. */ public function rest_details_html(\WP_REST_Request $request) { $html = $this->render_details_modal_html(get_current_user_id()); return new \WP_REST_Response(['success' => true, 'html' => $html], 200); } public function is_signed_today(int $user_id = 0): bool { $user_id = $user_id > 0 ? $user_id : get_current_user_id(); if ($user_id <= 0) { return false; } $today = $this->today_date(); if ((bool) get_user_meta($user_id, 'aksrt_signin_' . $today, true)) { return true; } return (bool) $this->get_record($user_id, $today); } public function get_total_days(int $user_id): int { global $wpdb; $table = \AKSRT\Core\DBMigrator::table('signin_log'); if (!$table) { return (int) get_user_meta($user_id, 'aksrt_checkin_all_day', true); } $n = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE user_id = %d", $user_id )); return $n; } /** * 签到详情弹窗 HTML(参考主题 zib_get_user_checkin_details_modal 1:1): * 炫彩头部 + 用户卡片(我的{货币名}/我的等级)+ 每日签到奖励卡 + 连续签到奖励 7 天迷你卡。 */ public function render_details_modal_html(int $user_id): string { $user = get_userdata($user_id); if (!$user) { return '
' . esc_html__('用户不存在', 'aksrt') . '