- 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。未部署真机(留主会话)
130 lines
5.3 KiB
C
130 lines
5.3 KiB
C
/* 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);
|
||
}
|