后台「积分配置 → 自定义货币名称」改名后,用户中心仍有大量硬编码 「积分」:做任务赚积分 / 购买积分 / 签到领积分、积分记录 / 积分任务、 每日可免费获取 XX 积分、我的积分、积分+1、创作中心积分收益等。 get_currency_name() 早已存在且可用,问题只是这些文案没有走它。 本PR 全部替换,无双路、无迁移: 服务端渲染(用户中心 renderers) - credits-balance.php:购买/做任务赚/签到领 + 整个购买弹窗 - profile-credits.php:积分记录 / 积分任务 页签 - credits-points-tasks.php:每日上限三态与空态 - sidebar-creator.php / profile-creator.php:积分收益 - transfer-modal.php:积分转账提示 - Signin.php:签到详情弹窗(我的积分 / 奖励 tooltip / 积分+N) - VipServiceModal.php:我的积分 / 积分兑换会员 - download-permission-card.php:购买价格 客户端渲染(JS 无法调用 PHP,注入 currencyName 到 aksrtData) - signin.js:签到状态行「已签到 N 天 +1」 - credits.js:转账标题/金额标签 + 购买提示 toast - cms-download.js:积分不足视图、我的积分 入口、余额不足文案 - pay-vip.js:积分兑换会员入口(优先 catalog.currency_name) - order-center.js:订单行单位回退 契约测试同步改为断言模板构造而非死字符串,并新增两例: 改名后跟随、未注入时回退「积分」。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
473 lines
21 KiB
PHP
473 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Credits;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class Signin {
|
|
|
|
private static $instance = null;
|
|
|
|
private function __construct() {}
|
|
|
|
public static function get_instance(): self {
|
|
if (null === self::$instance) self::$instance = new self();
|
|
return self::$instance;
|
|
}
|
|
|
|
public function init(): void {
|
|
add_action('rest_api_init', [$this, 'register_routes']);
|
|
}
|
|
|
|
public function register_routes(): void {
|
|
register_rest_route('aksrt/v1', '/signin/today', [
|
|
'methods' => '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 '<div class="modal-body"><p class="muted-color padding-10 text-center">' . esc_html__('用户不存在', 'aksrt') . '</p></div>';
|
|
}
|
|
$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);
|
|
$base = (int) ($cfg['base'] ?? 0);
|
|
$currency = class_exists('\AKSRT\Settings\Settings')
|
|
? \AKSRT\Settings\Settings::get_currency_name()
|
|
: __('积分', 'aksrt');
|
|
$points_svg = static fn(string $class = 'icon'): string =>
|
|
'<svg class="' . esc_attr($class) . '" aria-hidden="true"><use xlink:href="#icon-points"></use></svg>';
|
|
$gift_svg = static fn(string $class = 'icon'): string =>
|
|
'<svg class="' . esc_attr($class) . '" aria-hidden="true"><use xlink:href="#icon-gift-color"></use></svg>';
|
|
$signed_today = (bool) $rec;
|
|
|
|
// 炫彩头部
|
|
$html = '<div class="modal-body">';
|
|
$html .= '<div class="modal-colorful-header colorful-bg c-blue">';
|
|
$html .= '<button class="close" data-aksrt-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>';
|
|
$html .= '<div class="colorful-make"></div>';
|
|
$html .= '<div class="text-center">';
|
|
$html .= '<div class="em2x"><i class="fa fas fa-calendar-check" aria-hidden="true"></i></div>';
|
|
$html .= '<div class="mt10 em12 padding-w10">' . esc_html__('签到领奖励', 'aksrt') . '</div>';
|
|
$html .= '</div></div>';
|
|
|
|
// 用户卡片(头像/昵称 + 我的{货币名} + 我的等级)
|
|
$points_svg_mini = $points_svg('px12 mr6 muted-2-color');
|
|
$my_points_label = sprintf(__('我的%s', 'aksrt'), $currency);
|
|
$user_card = '<a rel="nofollow" class="block flex1 muted-box padding-10" href="' . esc_url(aksrt_get_user_center_url('credits')) . '">
|
|
<div class="muted-2-color em09 mb6">' . esc_html($my_points_label) . '</div>
|
|
<div class="em12">' . $points_svg_mini . esc_html(number_format_i18n(aksrt_user_points($user_id))) . '<i class="muted-2-color px12 fa fas fa-angle-right ml6" aria-hidden="true"></i></div>
|
|
</a>';
|
|
|
|
$level_s = class_exists('\AKSRT\User\Level')
|
|
&& \AKSRT\User\Level::settings()['enabled']
|
|
&& \AKSRT\User\Level::settings()['public_display'];
|
|
if ($level_s) {
|
|
$level_badge = \AKSRT\User\Level::get_badge_html($user_id, 'em12 mr10');
|
|
$user_card .= '<a rel="nofollow" class="block flex1 muted-box padding-10 ml6" href="' . esc_url(aksrt_get_user_center_url('level')) . '">
|
|
<div class="muted-2-color em09 mb6 flex jsb ac"><div>' . esc_html__('我的等级', 'aksrt') . '</div>' . $level_badge . '</div>
|
|
<div class="em12">' . esc_html(number_format_i18n(\AKSRT\User\Level::get_integral($user_id))) . '<span class="px12 ml6 muted-2-color">' . esc_html__('经验值', 'aksrt') . '<i class="fa fas fa-angle-right ml6" aria-hidden="true"></i></span></div>
|
|
</a>';
|
|
}
|
|
|
|
$checkin_all_day = $total ? '<div class="mb6 em09">' . sprintf(__('累计签到%s天', 'aksrt'), '<badge class="c-blue">' . esc_html((string) $total) . '</badge>') . '</div>' : '';
|
|
// 会员图标不再挂头像角标,改到昵称行最前;禁封徽章依旧占据会员图标位。
|
|
$avatar_img = aksrt_get_avatar_box($user_id, 'avatar-img avatar-mini mr6', false, false);
|
|
$name_badge = function_exists('aksrt_get_user_ban_name_badge') ? aksrt_get_user_ban_name_badge($user_id) : '';
|
|
if ($name_badge === '' && function_exists('aksrt_get_vip_icon')) {
|
|
$name_badge = aksrt_get_vip_icon($user_id);
|
|
}
|
|
$html .= '<div class="mb20"><div class="flex ac jsb"><div class="mb10">' . $avatar_img . esc_html($user->display_name) . $name_badge . '</div>' . $checkin_all_day . '</div><div class="flex ab jsb">' . $user_card . '</div></div>';
|
|
|
|
// 每日签到奖励卡片(参考主题:货币徽章 + 经验值徽章,经验值随等级系统展示)
|
|
// tooltip 先拼入数值再转义,避免把 %s 送进 esc_attr。
|
|
$reward_badge = $base
|
|
? '<badge class=" p2-10 mr6 c-green" data-toggle="tooltip" title="' . esc_attr(sprintf(__('%s奖励%s', 'aksrt'), $currency, $base)) . '">' . $points_svg('icon') . '+' . esc_html((string) $base) . '</badge>'
|
|
: '';
|
|
$reward_badge .= ($level_s && $base)
|
|
? '<badge class=" p2-10 mr6 c-purple" data-toggle="tooltip" title="' . esc_attr(sprintf(__('经验值奖励%s', 'aksrt'), $base)) . '"><i class="fa fas fa-chart-line" aria-hidden="true"></i>+' . esc_html((string) $base) . '</badge>'
|
|
: '';
|
|
|
|
if ($signed_today) {
|
|
$theday_reward = sprintf(__('%s', 'aksrt') . '+%d', $currency, (int) ($rec['reward'] ?? 0));
|
|
if ($level_s) {
|
|
// 签到经验值与积分奖励同额发放(Level::on_checkin)。
|
|
$theday_reward .= ' ' . sprintf(__('经验值', 'aksrt') . '+%d', (int) ($rec['reward'] ?? 0));
|
|
}
|
|
$checkin_btn = '<div class="em09 c-blue">
|
|
<div class="mb10"><i class="fa fas fa-calendar-check mr6" aria-hidden="true"></i>' . esc_html__('今日已签到', 'aksrt') . '</div>
|
|
<div class="px12">' . esc_html($theday_reward) . '</div>
|
|
</div>';
|
|
} else {
|
|
$checkin_btn = '<button type="button" class="but c-blue ml10 p2-10 radius" data-signin-btn'
|
|
. ' data-signin-done-label="' . esc_attr__('今日已签到', 'aksrt') . '">'
|
|
. '<span data-signin-label>' . esc_html__('立即签到', 'aksrt') . '</span></button>';
|
|
}
|
|
|
|
$card_1 = '<div class="zib-widget flex ac jsb">
|
|
<div class="mr10">
|
|
<div class="em09 mb10">' . $gift_svg('mr6 em12') . esc_html__('每日签到奖励', 'aksrt') . '</div>
|
|
<div class="flex ac jsb">' . $reward_badge . '</div>
|
|
</div>
|
|
' . $checkin_btn . '
|
|
</div>';
|
|
|
|
// 连续签到奖励卡片(参考主题固定 7 天)
|
|
$streak_table = is_array($cfg['streak'] ?? null) ? $cfg['streak'] : [];
|
|
$reward_continuous_day_html = $streak > 1 ? '<div class="mb6 em09">' . sprintf(__('已连续签到%s天', 'aksrt'), '<badge class="c-blue">' . esc_html((string) $streak) . '</badge>') . '</div>' : '';
|
|
$continuous_lists = '';
|
|
$max_day = 7;
|
|
for ($d = 1; $d <= $max_day; $d++) {
|
|
$extra = 0;
|
|
foreach ($streak_table as $k => $v) {
|
|
if ((int) $k === $d) {
|
|
$extra = (int) $v;
|
|
}
|
|
}
|
|
$_points = $base + $extra;
|
|
$is_ed = $signed_today && $streak >= $d;
|
|
$_mini_card = $_points ? '<div class="badg badg-sm ' . ($is_ed ? 'b' : 'c') . '-green" data-toggle="tooltip" title="' . esc_attr(sprintf(__('%s奖励%s', 'aksrt'), $currency, $_points)) . '">' . $points_svg('icon') . '<br/>+' . esc_html((string) $_points) . '</div>' : '';
|
|
if ($level_s && $_points) {
|
|
// 经验值与积分奖励同额(Level::on_checkin),参考主题为紫色经验值迷你卡。
|
|
$_mini_card .= '<div class="badg badg-sm ' . ($is_ed ? 'b' : 'c') . '-purple" data-toggle="tooltip" title="' . esc_attr(sprintf(__('经验值奖励%s', 'aksrt'), $_points)) . '"><i class="fa fas fa-chart-line" aria-hidden="true"></i><br/>+' . esc_html((string) $_points) . '</div>';
|
|
}
|
|
|
|
$continuous_lists .= '<div class="flex1 text-center" style="flex: 1;">
|
|
<div class="mb6 checkin-mini-box">' . $_mini_card . '</div>
|
|
<div class="muted-2-color smail' . ($is_ed ? ' c-blue' : '') . '">' . ($is_ed ? '<i class="fa fas fa-check-circle" aria-hidden="true"></i>' : '') . sprintf(__('第%d天', 'aksrt'), $d) . '</div>
|
|
</div>';
|
|
}
|
|
|
|
$card_2 = '<div class="mt10">
|
|
<div class="flex ac jsb"><div class="em09 mb10">' . $gift_svg('mr6 em12') . esc_html__('连续签到奖励', 'aksrt') . '</div>' . $reward_continuous_day_html . '</div>
|
|
<div class="flex ac jsb">' . $continuous_lists . '</div>
|
|
</div>';
|
|
|
|
$html .= $card_1;
|
|
$html .= $card_2;
|
|
$html .= '</div>';
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 算"今天之前"的连续签到天数。
|
|
*/
|
|
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);
|
|
}
|
|
}
|