'GET', 'callback' => [$this, 'rest_today'], 'permission_callback' => function () { return is_user_logged_in(); }, ]); register_rest_route('aksrt/v1', '/signin', [ 'methods' => 'POST', 'callback' => [$this, 'rest_signin'], 'permission_callback' => function () { return is_user_logged_in(); }, ]); register_rest_route('aksrt/v1', '/signin/history', [ 'methods' => 'GET', 'callback' => [$this, 'rest_history'], 'permission_callback' => function () { return is_user_logged_in(); }, ]); register_rest_route('aksrt/v1', '/signin/details', [ 'methods' => 'GET', 'callback' => [$this, 'rest_details_html'], 'permission_callback' => function () { return is_user_logged_in(); }, ]); } /** 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(); 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(); $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) { $msg = is_string($verify) ? $verify : __('人机验证失败', 'aksrt'); return new \WP_Error('aksrt_captcha_failed', $msg, ['status' => 400]); } } $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]); } 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 (zibll 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 zibll 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); // zibll alias update_user_meta($user_id, 'aksrt_checkin_continuous_day', $new_streak); update_user_meta($user_id, 'checkin_continuous_day', $new_streak); // zibll alias do_action('aksrt_checkin_done', $user_id); 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' => class_exists('AKSRT\Credits\Credits') ? Credits::balance($user_id) : 0, ], 200); } /** GET /signin/history — last N days for calendar/modals. */ public function rest_history(\WP_REST_Request $request) { $user_id = get_current_user_id(); $days = max(7, min(90, (int) ($request->get_param('days') ?: 30))); $rows = $this->get_recent_signins($user_id, $days); $dates = []; foreach ($rows as $row) { if (!empty($row['signin_date'])) { $dates[] = (string) $row['signin_date']; } } $today = $this->today_date(); return new \WP_REST_Response([ 'dates' => $dates, 'streak' => $this->compute_current_streak($user_id, $today), 'total' => $this->get_total_days($user_id), 'signed' => (bool) $this->get_record($user_id, $today), 'all_day' => (int) get_user_meta($user_id, 'aksrt_checkin_all_day', true) ?: $this->get_total_days($user_id), ], 200); } /** 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); } /** admin-ajax HTML (remote modal fetch often hits admin-ajax). */ public function ajax_details_modal(): void { if (!is_user_logged_in()) { wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401); } $html = $this->render_details_modal_html(get_current_user_id()); wp_send_json_success(['html' => $html]); } 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; } /** * zibll checkin_details_modal lite HTML. */ public function render_details_modal_html(int $user_id): string { $user = get_userdata($user_id); if (!$user) { return '

' . esc_html__('用户不存在', 'aksrt') . '

'; } $cfg = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_signin_reward_settings() : ['enabled' => false, 'base' => 0, 'streak' => []]; $today = $this->today_date(); $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; $currency = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt'); $base = (int) ($cfg['base'] ?? 0); $avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user_id, ['size' => 64]) : get_avatar_url($user_id, ['size' => 64]); $center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('assets') : home_url('/'); $html = '
'; $html .= '
'; $html .= ''; $html .= '' . esc_html__('签到领奖励', 'aksrt') . ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= ''; $html .= '
'; $html .= '
' . esc_html($user->display_name) . '
'; $html .= '
' . esc_html(sprintf(__('累计签到 %d 天', 'aksrt'), $total)) . '
'; $html .= '
'; $html .= '
'; $html .= ''; $html .= '' . esc_html($currency) . ''; $html .= '' . esc_html(number_format_i18n($balance)) . ''; $html .= '
'; $html .= '
'; $html .= '
'; $html .= '
' . esc_html__('每日签到奖励', 'aksrt') . '
'; $html .= '
' . esc_html($currency) . ' +' . esc_html((string) $base) . '
'; $html .= '
'; if ($rec) { $html .= '
' . esc_html__('今日已签到', 'aksrt'); $html .= '
+' . esc_html((string) (int) ($rec['reward'] ?? 0)) . '
'; } else { $html .= ''; } $html .= '
'; $streak_table = is_array($cfg['streak'] ?? null) ? $cfg['streak'] : []; $max = 7; foreach (array_keys($streak_table) as $k) { $max = max($max, (int) $k); } $max = min(14, max(2, $max)); $html .= '
'; $html .= '
' . esc_html__('连续签到奖励', 'aksrt') . ''; if ($streak > 1) { $html .= '' . esc_html(sprintf(__('已连续 %d 天', 'aksrt'), $streak)) . ''; } $html .= '
'; for ($d = 1; $d <= $max; $d++) { $extra = 0; foreach ($streak_table as $k => $v) { if ((int) $k === $d) { $extra = (int) $v; } } $pts = $base + $extra; $is_done = $streak >= $d; $html .= '
'; $html .= '
D' . (int) $d . '
'; $html .= '
+' . esc_html((string) $pts) . '
'; $html .= '
'; } $html .= '
'; $dates = []; foreach ($this->get_recent_signins($user_id, 30) as $row) { if (!empty($row['signin_date'])) { $dates[] = (string) $row['signin_date']; } } $html .= '
' . esc_html__('最近 30 天', 'aksrt') . '
'; $html .= '
'; $html .= '
'; return $html; } // ---------- 工具方法 ---------- /** 站点时区下的今日日期 'Y-m-d' */ private function today_date(): string { return (new \DateTimeImmutable('today', wp_timezone()))->format('Y-m-d'); } /** 取签到记录 */ private function get_record(int $user_id, string $date): ?array { global $wpdb; $table = \AKSRT\Core\DBMigrator::table('signin_log'); if (!$table) return null; $row = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table} WHERE user_id = %d AND signin_date = %s", $user_id, $date ), ARRAY_A); return $row ?: null; } /** * 获取最近 N 天的签到记录 * * @param int $user_id * @param int $days * @return array */ public function get_recent_signins(int $user_id, int $days = 30): array { global $wpdb; $table = \AKSRT\Core\DBMigrator::table('signin_log'); if (!$table) return []; $from_date = (new \DateTimeImmutable("-{$days} days", wp_timezone()))->format('Y-m-d'); $rows = $wpdb->get_results($wpdb->prepare( "SELECT signin_date FROM {$table} WHERE user_id = %d AND signin_date >= %s ORDER BY signin_date ASC", $user_id, $from_date ), ARRAY_A); return $rows ?: []; } /** * 算"今天之前"的连续签到天数。 */ private function compute_current_streak(int $user_id, string $today_str): int { global $wpdb; $table = \AKSRT\Core\DBMigrator::table('signin_log'); if (!$table) return 0; // 拿最近 60 天的签到记录降序 $rows = $wpdb->get_results($wpdb->prepare( "SELECT signin_date FROM {$table} WHERE user_id = %d AND signin_date <= %s ORDER BY signin_date DESC LIMIT 60", $user_id, $today_str ), ARRAY_A); if (empty($rows)) return 0; $dates = array_map(fn($r) => $r['signin_date'], $rows); $today = new \DateTimeImmutable($today_str); $tz = wp_timezone(); // 起点:若今天已签则从今天回看,否则从昨天回看 $cursor = in_array($today_str, $dates, true) ? $today : $today->sub(new \DateInterval('P1D')); $count = 0; foreach ($dates as $d) { if ($d === $cursor->format('Y-m-d')) { $count++; $cursor = $cursor->sub(new \DateInterval('P1D')); } else { break; } } return $count; } /** * 根据连续天数计算奖励 = base + streak 段额外奖励。 */ private function calc_reward(int $streak, array $cfg): int { $base = (int) ($cfg['base'] ?? 1); $extra = 0; $streak_table = is_array($cfg['streak'] ?? null) ? $cfg['streak'] : []; // streak_table 是 { N: extra } —— 当 streak 恰好命中 N 时给 extra // 对 array_keys 不稳定的存储格式做容错 $normalized = []; foreach ($streak_table as $k => $v) { $normalized[(int) $k] = (int) $v; } if (isset($normalized[$streak])) { $extra = $normalized[$streak]; } return max(0, $base + $extra); } }