Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/logic/account.c
T
MeowOS bdd9e898f4 feat(ncm §20): 音质体系全量——八档UI/VIP检测缓存/逐级降级下首恢复/fee门禁/默认极高HQ
- core: 9档枚举(顺序=降级链, DOLBY映射保留不进UI/链) + level_key(sky/sky_c51)
  + level_gate(CVIP/SVIP门槛) + song_url surround档附加immerseType=c51
  + song_url_auto全档表降级(out_level回填/响应level静默降档采信/try_fn可注入)
  + vip_info(weapi v2优先v1兜底) + parse_vip_info离线可测 + NCM_ERR_VIP(-15)
- app/logic/account.c(新): account.json会员态缓存(uid/nick/vip/双expire/checked_at)
  刷新=登录后/超1天/到期后各一次不轮询; account_vip本地到期降档视图
- player_manager: 用户档按会员钳顶后逐级降级(不硬拒,缓存键仍用户档),
  fee∈{1,4}且非会员→NCM_ERR_VIP零请求(缓存命中优先), eff_level回填
- UI: 设置页八档(规格+门槛+未解锁标注,允许选定), 播放页时间行中央
  生效档/已降级至xx小字, VIP专享文案; 默认档Hi-Res→极高HQ(存量设置保留)
- 验证(0真实请求): degrade_sim 90例矩阵全PASS; player_flow_sim P1-P8全PASS;
  test_ncm_core vip解析9例宿主+qemu全PASS; 窗口/分页/缓存/封面/audiod sim全PASS;
  mipsel -Wall -Wextra 0 warning; 6页离屏rc=0。未部署真机(留主会话)
2026-09-01 15:55:42 +08:00

130 lines
5.3 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* logic/account.c — 账号会员态缓存与刷新策略(§20,NCM_CACHE_DESIGN §2.1 定案)。
*
* 落盘:<data_dir>/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 <stdio.h>
#include <string.h>
#define ACCOUNT_FILE "account.json"
#define VIP_TTL_SEC (24 * 3600) /* 超 1 天重查 */
/* 单飞标志(每次启动每条件最多一次请求) */
static int s_refreshed_login; /* 登录后已查 */
static int s_refreshed_stale; /* 超 1 天已查 */
static int s_refreshed_expired; /* 到期后已查 */
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;
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;
if (vip < NCM_VIP_NONE || vip > NCM_VIP_SVIP) vip = NCM_VIP_NONE;
g_st.vip = (int)vip;
g_st.vip_checked_at = checked;
/* 有效到期时间 = 当前档对应的到期字段 */
g_st.vip_expire_ms = (vip == NCM_VIP_SVIP) ? svip_exp
: (vip == NCM_VIP_CVIP) ? cvip_exp : 0;
meow_log(MEOW_LOG_INFO, "account: loaded vip=%d expire=%lld checked_at=%ld",
g_st.vip, (long long)g_st.vip_expire_ms, (long)g_st.vip_checked_at);
}
static void account_save(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}",
g_st.uid, g_st.nickname, vip,
(long long)cvip_exp, (long long)svip_exp, (long long)checked_at);
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: 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_expire_ms = (info.vip == NCM_VIP_SVIP) ? info.svip_expire_ms
: (info.vip == NCM_VIP_CVIP) ? info.cvip_expire_ms : 0;
g_st.vip_checked_at = now_s;
account_save(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);
}