/* logic/account.c — 账号会员态缓存与刷新策略(§20,NCM_CACHE_DESIGN §2.1 定案)。 * * 落盘:/account.json = {uid, nickname, vip, cvip_expire_ms, svip_expire_ms, * checked_at}。开 App 先用缓存渲染(account_load 进 g_st);后台重查时机(风控:不轮询): * ① 登录成功后(login worker,force=1); * ② checked_at 距今 > 1 天; * ③ 会员到期时间已过(可能自动续费,再查一次)。 * 每条件每次启动只查一次(单飞标志);查询在调用方 worker 线程上下文执行(UI 零网络)。 * account_vip() 是纯本地视图:到期时间已过即按降档后的有效态返回(不联网)。 */ #include "../ncm_app_internal.h" #include #include #define ACCOUNT_FILE "account.json" #define VIP_TTL_SEC (24 * 3600) /* 超 1 天重查 */ /* §NEW MUSIC_U 临期续期:cookie 年龄阈值默认 12h(可配 ncm.cookie_refresh_h 小时);失败冷却 30 分钟。 */ #define COOKIE_REFRESH_DEFAULT_H 12 #define COOKIE_REFRESH_COOLDOWN_SEC (30 * 60) #define COOKIE_FAIL_TS_KEY "ncm.cookie_refresh_fail_ts" /* 上次续期失败时刻(秒)持久化:跨 boot 冷却 */ /* 单飞标志(每次启动每条件最多一次请求) */ static int s_refreshed_login; /* 登录后已查 */ static int s_refreshed_stale; /* 超 1 天已查 */ static int s_refreshed_expired; /* 到期后已查 */ /* §NEW cookie 续期状态(app 单实例;跨 boot 由 account.json last_refresh_ts + config 冷却键持久化) */ static volatile int s_cookie_refreshed; /* 本 boot 已试(单飞:同一 boot 只刷一次) */ static volatile int64_t s_cookie_fail_ts; /* 上次续期失败时刻(秒;冷却窗口用,跨 boot 持久化) */ static volatile int s_cookie_need_relogin; /* 1=续期判会话失效,需回登录页(UI 消费) */ static int64_t cookie_refresh_ttl_sec(void) { int h = meow_config_int("ncm.cookie_refresh_h", COOKIE_REFRESH_DEFAULT_H); if (h < 1) h = 1; if (h > 72) h = 72; return (int64_t)h * 3600; } /* 冷却时刻读写(持久化到 config,跨 boot:失败后 30 分钟内不重刷,不猛刷/防 -460 自锁)。 */ static int64_t cookie_fail_ts_load(void) { int v = meow_config_int(COOKIE_FAIL_TS_KEY, 0); return (int64_t)(v > 0 ? v : 0); } static void cookie_fail_ts_store(int64_t now_s) { char b[24]; snprintf(b, sizeof b, "%lld", (long long)(now_s > 0 ? now_s : 0)); meow_config_set(COOKIE_FAIL_TS_KEY, b); } static void account_path(char *out, size_t sz) { snprintf(out, sz, "%s/%s", g_st.data_dir, ACCOUNT_FILE); } void account_load(void) { g_st.vip = NCM_VIP_NONE; g_st.vip_expire_ms = 0; g_st.vip_checked_at = 0; g_st.vip_cvip_exp_ms = 0; g_st.vip_svip_exp_ms = 0; g_st.cookie_last_refresh_ts = 0; char path[600], buf[512]; account_path(path, sizeof path); int len = ncm_store_read_file(path, buf, sizeof buf); if (len <= 0) return; /* 小 JSON 直接 jsmn 解析(栈上 token 数组,≤48 token 足够) */ jsmntok_t toks[48]; int parent[48]; ncm_json_t j; if (ncm_json_parse(&j, buf, (size_t)len, toks, 48, parent) < 0) return; long vip = 0, checked = 0; ncm_json_path_int(&j, "vip", &vip); ncm_json_path_int(&j, "checked_at", &checked); int rc = 0; int64_t cvip_exp = ncm_json_get_i64_at(&j, ncm_json_path(&j, "cvip_expire_ms"), &rc); if (rc != NCM_OK) cvip_exp = 0; int64_t svip_exp = ncm_json_get_i64_at(&j, ncm_json_path(&j, "svip_expire_ms"), &rc); if (rc != NCM_OK) svip_exp = 0; /* §NEW:last_refresh_ts(0=旧会话/缺失 → 视为临期,开机续期一次) */ int64_t last_refresh = ncm_json_get_i64_at(&j, ncm_json_path(&j, "last_refresh_ts"), &rc); if (rc != NCM_OK) last_refresh = 0; if (vip < NCM_VIP_NONE || vip > NCM_VIP_SUPER) vip = NCM_VIP_NONE; g_st.vip = (int)vip; g_st.vip_cvip_exp_ms = cvip_exp; g_st.vip_svip_exp_ms = svip_exp; g_st.vip_checked_at = checked; g_st.cookie_last_refresh_ts = last_refresh; /* 有效到期时间 = 当前档对应的到期字段(SUPER 无独立到期字段,沿黑胶 svip 场)。 */ g_st.vip_expire_ms = (vip == NCM_VIP_SVIP || vip == NCM_VIP_SUPER) ? svip_exp : (vip == NCM_VIP_CVIP) ? cvip_exp : 0; meow_log(MEOW_LOG_INFO, "account: loaded vip=%d expire=%lld last_refresh_ts=%lld", g_st.vip, (long long)g_st.vip_expire_ms, (long long)last_refresh); } static void account_persist(int vip, int64_t cvip_exp, int64_t svip_exp, int64_t checked_at) { char path[600], buf[512]; account_path(path, sizeof path); snprintf(buf, sizeof buf, "{\"uid\":\"%s\",\"nickname\":\"%s\",\"vip\":%d," "\"cvip_expire_ms\":%lld,\"svip_expire_ms\":%lld,\"checked_at\":%lld," "\"last_refresh_ts\":%lld}", g_st.uid, g_st.nickname, vip, (long long)cvip_exp, (long long)svip_exp, (long long)checked_at, (long long)g_st.cookie_last_refresh_ts); if (ncm_store_write_file(path, buf, strlen(buf)) != NCM_OK) meow_log(MEOW_LOG_WARN, "account: save failed"); } /* 当前有效会员档(本地视图,不联网):到期时间已过则降档。 */ int account_vip(void) { int vip = g_st.vip; if (vip > NCM_VIP_NONE && g_st.vip_expire_ms > 0 && g_st.vip_expire_ms <= ncm_now_ms()) vip = NCM_VIP_NONE; /* 已到期:按非会员对待(后台会再查一次确认续费) */ return vip; } int account_level_cap(void) { switch (account_vip()) { case NCM_VIP_SVIP: case NCM_VIP_SUPER: return NCM_LEVEL_SURROUND; case NCM_VIP_CVIP: return NCM_LEVEL_HIRES; default: return NCM_LEVEL_EXHIGH; } } void account_vip_maybe_refresh(int force) { if (!g_st.logged_in || !g_st.ctx.cookie[0]) return; int64_t now_s = ncm_now_ms() / 1000; int expired = (g_st.vip > NCM_VIP_NONE && g_st.vip_expire_ms > 0 && g_st.vip_expire_ms <= ncm_now_ms()); const char *why = NULL; if (force && !s_refreshed_login) why = "login"; else if ((g_st.vip_checked_at == 0 || now_s - g_st.vip_checked_at > VIP_TTL_SEC) && !s_refreshed_stale) why = "stale>1d"; else if (expired && !s_refreshed_expired) why = "expired"; if (!why) return; ncm_vip_info_t info; int rc = ncm_api_vip_info(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid, &info); /* 无论成败都置单飞标志:本条件本启动只查一次(失败等下次启动/下个条件,防风控) */ if (force) s_refreshed_login = 1; if (strcmp(why, "stale>1d") == 0) s_refreshed_stale = 1; if (strcmp(why, "expired") == 0) s_refreshed_expired = 1; if (rc != NCM_OK) { meow_log(MEOW_LOG_WARN, "account: vip refresh (%s) failed rc=%d, keep cached vip=%d", why, rc, g_st.vip); return; } g_st.vip = info.vip; g_st.vip_cvip_exp_ms = info.cvip_expire_ms; g_st.vip_svip_exp_ms = info.svip_expire_ms; g_st.vip_expire_ms = (info.vip == NCM_VIP_SVIP || info.vip == NCM_VIP_SUPER) ? info.svip_expire_ms : (info.vip == NCM_VIP_CVIP) ? info.cvip_expire_ms : 0; g_st.vip_checked_at = now_s; account_persist(info.vip, info.cvip_expire_ms, info.svip_expire_ms, now_s); meow_log(MEOW_LOG_INFO, "account: vip refresh (%s) -> vip=%d expire=%lld", why, info.vip, (long long)g_st.vip_expire_ms); } /* ---- §NEW MUSIC_U 临期续期 ---- * 用户定案语义:失效前用当前 cookie 换新登录态(用户无感);只有刷新失败(会话确失效)才退扫码。 * 风控:同一 boot 只试一次;失败冷却 30 分钟(下次启动再试,不猛刷)。在 worker 线程上下文调用。 * 返回:NCM_OK(已续期/未临期/冷却中/未登录);NCM_ERR_AUTH(会话失效,需回登录页); * 其它负错误码(网络/TLS 瞬断——保持旧 cookie,退避)。 */ int account_cookie_maybe_refresh(void) { if (!g_st.logged_in || !g_st.ctx.cookie[0]) return NCM_OK; if (s_cookie_refreshed) return NCM_OK; /* 本 boot 已试(单飞) */ int64_t now_s = ncm_now_ms() / 1000; s_cookie_fail_ts = cookie_fail_ts_load(); /* 跨 boot 恢复上次失败时刻 */ if (s_cookie_fail_ts > 0 && now_s - s_cookie_fail_ts < COOKIE_REFRESH_COOLDOWN_SEC) { s_cookie_refreshed = 1; /* 冷却窗口内:视为已处理,boot 内不再重复 */ return NCM_OK; } int64_t last = g_st.cookie_last_refresh_ts; int stale = (last == 0) || (now_s - last > cookie_refresh_ttl_sec()); if (!stale) { s_cookie_refreshed = 1; return NCM_OK; } /* cookie 尚新(≤阈值),无需续期 */ s_cookie_refreshed = 1; /* 单飞:本 boot 最多一次刷 */ char new_cookie[NCM_COOKIE_MAX]; int rc = ncm_login_refresh(g_st.ctx.device_id, g_st.ctx.cookie, new_cookie, sizeof new_cookie); if (rc == NCM_OK) { ncm_ctx_set_cookie(&g_st.ctx, new_cookie); /* 内存 + 加密落盘(同一机制) */ g_st.cookie_last_refresh_ts = now_s; s_cookie_fail_ts = 0; cookie_fail_ts_store(0); /* 成功:清冷却 */ account_persist(g_st.vip, g_st.vip_cvip_exp_ms, g_st.vip_svip_exp_ms, g_st.vip_checked_at); meow_log(MEOW_LOG_INFO, "account: cookie refreshed (age %llds -> now), last_refresh_ts=%lld", (long long)(now_s - (last ? last : now_s)), (long long)now_s); return NCM_OK; } if (rc == NCM_ERR_AUTH) { s_cookie_fail_ts = now_s; cookie_fail_ts_store(now_s); s_cookie_need_relogin = 1; /* UI 线程消费 → logout + 进登录页 */ meow_log(MEOW_LOG_WARN, "account: cookie refresh -> session invalid (rc=%d), re-login", rc); return NCM_ERR_AUTH; } /* 网络/TLS/HTTP 异常(瞬断):冷却 30 分钟(跨 boot 持久化),保持旧 cookie,下次启动再试 */ s_cookie_fail_ts = now_s; cookie_fail_ts_store(now_s); meow_log(MEOW_LOG_WARN, "account: cookie refresh transient rc=%d, backoff 30min", rc); return rc; } int account_cookie_need_relogin(void) { return s_cookie_need_relogin; } void account_cookie_relogin_consume(void) { s_cookie_need_relogin = 0; } /* 登录成功(落盘新 cookie)后调用:把续期年龄标为现在(新 cookie 视为全新),并重置单飞/冷却, * 使同 boot 内后续如果年龄再次临期(不可能,但幂等)可再刷;同时清除上会的「需回登录页」标志。 */ void account_cookie_mark_fresh(void) { g_st.cookie_last_refresh_ts = ncm_now_ms() / 1000; s_cookie_refreshed = 0; s_cookie_fail_ts = 0; cookie_fail_ts_store(0); s_cookie_need_relogin = 0; meow_log(MEOW_LOG_INFO, "account: cookie marked fresh (last_refresh_ts=%lld)", (long long)g_st.cookie_last_refresh_ts); }