qqmusic-api-enhanced 1.0.2 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/module/search.d.ts +1 -0
- package/module/search.js +7 -1
- package/module/search.ts +9 -1
- package/module/user_vip_info.d.ts +1 -1
- package/module/user_vip_info.js +115 -24
- package/module/user_vip_info.ts +103 -20
- package/package.json +1 -1
package/module/search.d.ts
CHANGED
package/module/search.js
CHANGED
|
@@ -21,6 +21,7 @@ export default async function search(query = {}, req = request) {
|
|
|
21
21
|
const page = Math.max(Number(query.page) || 1, 1);
|
|
22
22
|
const limit = Math.min(Math.max(Number(query.limit) || 20, 1), 100);
|
|
23
23
|
const searchId = query.searchid || getSearchId();
|
|
24
|
+
const platform = query.platform || "desktop";
|
|
24
25
|
const cgiRes = await req("music.search.SearchCgiService", "DoSearchForQQMusicMobile", {
|
|
25
26
|
query: keyword,
|
|
26
27
|
search_type: searchType,
|
|
@@ -30,13 +31,18 @@ export default async function search(query = {}, req = request) {
|
|
|
30
31
|
highlight: 1,
|
|
31
32
|
}, {
|
|
32
33
|
cookie: query.cookie,
|
|
33
|
-
platform
|
|
34
|
+
platform,
|
|
34
35
|
});
|
|
36
|
+
const rawBody = cgiRes.body || {};
|
|
37
|
+
const dataBody = rawBody.body || rawBody.data?.body || rawBody.data || rawBody;
|
|
38
|
+
const songItems = dataBody.item_song || dataBody.song?.list || dataBody.songs || [];
|
|
39
|
+
const songs = Array.isArray(songItems) ? songItems : (songItems.items || []);
|
|
35
40
|
return {
|
|
36
41
|
status: cgiRes.status,
|
|
37
42
|
body: {
|
|
38
43
|
code: cgiRes.body?.code ?? 200,
|
|
39
44
|
data: cgiRes.body,
|
|
45
|
+
songs,
|
|
40
46
|
},
|
|
41
47
|
cookie: cgiRes.cookie,
|
|
42
48
|
};
|
package/module/search.ts
CHANGED
|
@@ -19,6 +19,7 @@ export default async function search(
|
|
|
19
19
|
limit?: number;
|
|
20
20
|
cookie?: any;
|
|
21
21
|
searchid?: string;
|
|
22
|
+
platform?: string;
|
|
22
23
|
} = {},
|
|
23
24
|
req = request
|
|
24
25
|
): Promise<ApiResponse> {
|
|
@@ -34,6 +35,7 @@ export default async function search(
|
|
|
34
35
|
const page = Math.max(Number(query.page) || 1, 1);
|
|
35
36
|
const limit = Math.min(Math.max(Number(query.limit) || 20, 1), 100);
|
|
36
37
|
const searchId = query.searchid || getSearchId();
|
|
38
|
+
const platform = query.platform || "desktop";
|
|
37
39
|
|
|
38
40
|
const cgiRes = await req(
|
|
39
41
|
"music.search.SearchCgiService",
|
|
@@ -48,15 +50,21 @@ export default async function search(
|
|
|
48
50
|
},
|
|
49
51
|
{
|
|
50
52
|
cookie: query.cookie,
|
|
51
|
-
platform
|
|
53
|
+
platform,
|
|
52
54
|
}
|
|
53
55
|
);
|
|
54
56
|
|
|
57
|
+
const rawBody = cgiRes.body || {};
|
|
58
|
+
const dataBody = rawBody.body || rawBody.data?.body || rawBody.data || rawBody;
|
|
59
|
+
const songItems = dataBody.item_song || dataBody.song?.list || dataBody.songs || [];
|
|
60
|
+
const songs = Array.isArray(songItems) ? songItems : (songItems.items || []);
|
|
61
|
+
|
|
55
62
|
return {
|
|
56
63
|
status: cgiRes.status,
|
|
57
64
|
body: {
|
|
58
65
|
code: cgiRes.body?.code ?? 200,
|
|
59
66
|
data: cgiRes.body,
|
|
67
|
+
songs,
|
|
60
68
|
},
|
|
61
69
|
cookie: cgiRes.cookie,
|
|
62
70
|
};
|
|
@@ -7,7 +7,7 @@ export interface NormalizedVipInfo {
|
|
|
7
7
|
vipExpireTime?: number;
|
|
8
8
|
}
|
|
9
9
|
export declare function parseVipFromIcons(iconList?: any[], icons?: any[]): Partial<NormalizedVipInfo> | null;
|
|
10
|
-
export declare function deriveVipInfo(body: any): NormalizedVipInfo;
|
|
10
|
+
export declare function deriveVipInfo(body: any, now?: number): NormalizedVipInfo;
|
|
11
11
|
/**
|
|
12
12
|
* 获取当前登录用户的 VIP 会员信息及身份明细.
|
|
13
13
|
*
|
package/module/user_vip_info.js
CHANGED
|
@@ -2,9 +2,33 @@ import { request } from "../util/request.js";
|
|
|
2
2
|
import { cookieToJson } from "../util/cookie.js";
|
|
3
3
|
import user_detail_fn from "./user_detail.js";
|
|
4
4
|
export function parseVipFromIcons(iconList = [], icons = []) {
|
|
5
|
-
|
|
5
|
+
// 1. 优先从用户主页活跃佩戴的 iconList (NewIconInfo.iconlist) 查找
|
|
6
|
+
for (const item of iconList) {
|
|
6
7
|
if (!item)
|
|
7
8
|
continue;
|
|
9
|
+
if (item.GreyURL && String(item.GreyURL).trim().length > 0)
|
|
10
|
+
continue;
|
|
11
|
+
const url = String(item.srcUrl || item.IconURL || "");
|
|
12
|
+
const helptxt = String(item.Helptxt || item.Title || "");
|
|
13
|
+
const m = url.match(/(?:^|\/)(n)?(s?vip)(\d+)\.(?:png|webp)/i);
|
|
14
|
+
if (m) {
|
|
15
|
+
const isYear = Boolean(m[1]) || helptxt.includes("年费");
|
|
16
|
+
const tier = m[2].toLowerCase() === "svip" ? "svip" : "vip";
|
|
17
|
+
const level = parseInt(m[3], 10);
|
|
18
|
+
return {
|
|
19
|
+
vipStatus: "vip",
|
|
20
|
+
vipTier: tier,
|
|
21
|
+
vipLevel: level > 0 ? Math.min(Math.max(level, 1), 10) : 1,
|
|
22
|
+
vipYear: isYear,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// 2. 其次才从旧版快捷入口 icons 查找
|
|
27
|
+
for (const item of icons) {
|
|
28
|
+
if (!item)
|
|
29
|
+
continue;
|
|
30
|
+
if (item.GreyURL && String(item.GreyURL).trim().length > 0)
|
|
31
|
+
continue;
|
|
8
32
|
const url = String(item.srcUrl || item.IconURL || "");
|
|
9
33
|
const helptxt = String(item.Helptxt || item.Title || "");
|
|
10
34
|
const m = url.match(/(?:^|\/)(n)?(s?vip)(\d+)\.(?:png|webp)/i);
|
|
@@ -22,7 +46,27 @@ export function parseVipFromIcons(iconList = [], icons = []) {
|
|
|
22
46
|
}
|
|
23
47
|
return null;
|
|
24
48
|
}
|
|
25
|
-
|
|
49
|
+
function parseDateMs(val) {
|
|
50
|
+
if (val === undefined || val === null || val === "" || val === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (typeof val === "number" && val > 0) {
|
|
53
|
+
return val > 1e11 ? val : val * 1000;
|
|
54
|
+
}
|
|
55
|
+
if (typeof val === "string") {
|
|
56
|
+
const trimmed = val.trim();
|
|
57
|
+
if (!trimmed)
|
|
58
|
+
return undefined;
|
|
59
|
+
const num = Number(trimmed);
|
|
60
|
+
if (!isNaN(num) && num > 0) {
|
|
61
|
+
return num > 1e11 ? num : num * 1000;
|
|
62
|
+
}
|
|
63
|
+
const parsed = Date.parse(trimmed.replace(/-/g, "/"));
|
|
64
|
+
if (!isNaN(parsed))
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
export function deriveVipInfo(body, now = Date.now()) {
|
|
26
70
|
if (!body) {
|
|
27
71
|
return { vipStatus: "unknown" };
|
|
28
72
|
}
|
|
@@ -33,24 +77,71 @@ export function deriveVipInfo(body) {
|
|
|
33
77
|
const level = Number(identity.level || 0);
|
|
34
78
|
const isYear = Boolean(Number(identity.HugeYearFlag || identity.yearflag || identity.is_year || identity.year_vip || 0) > 0 ||
|
|
35
79
|
Number(vipData.ystar || vipData.year || 0) > 0);
|
|
80
|
+
// 1. 分别提取 SVIP 专有到期时间
|
|
81
|
+
let svipExpire = undefined;
|
|
82
|
+
if (vipData.starend)
|
|
83
|
+
svipExpire = parseDateMs(vipData.starend);
|
|
84
|
+
else if (vipData.ystarend)
|
|
85
|
+
svipExpire = parseDateMs(vipData.ystarend);
|
|
86
|
+
else if (vipData.send)
|
|
87
|
+
svipExpire = parseDateMs(vipData.send);
|
|
88
|
+
else if (identity.SvipEnd || identity.svip_end)
|
|
89
|
+
svipExpire = parseDateMs(identity.SvipEnd || identity.svip_end);
|
|
90
|
+
// 2. 分别提取普通 VIP / 豪华绿钻专有到期时间
|
|
91
|
+
let vipExpire = undefined;
|
|
92
|
+
if (identity.HugeVipEnd)
|
|
93
|
+
vipExpire = parseDateMs(identity.HugeVipEnd);
|
|
94
|
+
else if (identity.twelveEnd)
|
|
95
|
+
vipExpire = parseDateMs(identity.twelveEnd);
|
|
96
|
+
else if (identity.eightEnd)
|
|
97
|
+
vipExpire = parseDateMs(identity.eightEnd);
|
|
98
|
+
else if (identity.overdate)
|
|
99
|
+
vipExpire = parseDateMs(identity.overdate);
|
|
100
|
+
else if (vipData.mygreen)
|
|
101
|
+
vipExpire = parseDateMs(vipData.mygreen);
|
|
102
|
+
// 3. 通用 userinfo.expire 归集
|
|
103
|
+
const userinfoExpire = Number(vipData.userinfo?.expire || 0) > 0 ? parseDateMs(vipData.userinfo.expire) : undefined;
|
|
104
|
+
if (userinfoExpire !== undefined) {
|
|
105
|
+
if (svipExpire === undefined && vipExpire === undefined) {
|
|
106
|
+
if (svip > 0 && userinfoExpire > now) {
|
|
107
|
+
svipExpire = userinfoExpire;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
vipExpire = userinfoExpire;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else if (vipExpire === undefined) {
|
|
114
|
+
// 若已解析出 svipExpire(如已过期的 starend/send),则通用的 userinfo.expire 归属于绿钻 vipExpire
|
|
115
|
+
vipExpire = userinfoExpire;
|
|
116
|
+
}
|
|
117
|
+
else if (svipExpire === undefined && svip > 0) {
|
|
118
|
+
svipExpire = userinfoExpire;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const isSvipExpired = svipExpire !== undefined && svipExpire <= now;
|
|
122
|
+
const isVipExpired = vipExpire !== undefined && vipExpire <= now;
|
|
36
123
|
let vipStatus = "nonVip";
|
|
37
124
|
let vipTier = undefined;
|
|
38
125
|
let vipLevel = undefined;
|
|
39
126
|
let vipYear = undefined;
|
|
40
127
|
let vipExpireTime = undefined;
|
|
41
|
-
|
|
128
|
+
// 1. 如果存在未过期的 SVIP
|
|
129
|
+
if (svip > 0 && !isSvipExpired) {
|
|
42
130
|
vipStatus = "vip";
|
|
43
131
|
vipTier = "svip";
|
|
44
132
|
vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
|
|
45
133
|
vipYear = isYear;
|
|
134
|
+
vipExpireTime = svipExpire ?? (vipExpire && !isVipExpired ? vipExpire : undefined);
|
|
46
135
|
}
|
|
47
|
-
else if (isVip > 0) {
|
|
136
|
+
else if (isVip > 0 && !isVipExpired) {
|
|
137
|
+
// 2. 如果存在普通 VIP(若 SVIP 已过期但 VIP 未过期,降级并展示未过期的 VIP 到期时间)
|
|
48
138
|
vipStatus = "vip";
|
|
49
139
|
vipTier = "vip";
|
|
50
140
|
vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
|
|
51
141
|
vipYear = isYear;
|
|
142
|
+
vipExpireTime = vipExpire ?? (svipExpire && !isSvipExpired ? svipExpire : undefined);
|
|
52
143
|
}
|
|
53
|
-
//
|
|
144
|
+
// 3. 若 identity 未命中有效 VIP(例如 SVIP 已过期,且普通 VIP 缺少基础标志或需通过勋章校准)
|
|
54
145
|
if (vipStatus === "nonVip") {
|
|
55
146
|
const info = vipData.Info || vipData.info || {};
|
|
56
147
|
const iconList = info.NewIconInfo?.iconlist || vipData.NewIconInfo?.iconlist || [];
|
|
@@ -61,21 +152,17 @@ export function deriveVipInfo(body) {
|
|
|
61
152
|
vipTier = iconVip.vipTier;
|
|
62
153
|
vipLevel = iconVip.vipLevel;
|
|
63
154
|
vipYear = iconVip.vipYear;
|
|
155
|
+
if (iconVip.vipTier === "vip") {
|
|
156
|
+
vipExpireTime = vipExpire && vipExpire > now ? vipExpire : (userinfoExpire && userinfoExpire > now ? userinfoExpire : undefined);
|
|
157
|
+
}
|
|
158
|
+
else if (iconVip.vipTier === "svip") {
|
|
159
|
+
vipExpireTime = svipExpire && svipExpire > now ? svipExpire : (userinfoExpire && userinfoExpire > now ? userinfoExpire : undefined);
|
|
160
|
+
}
|
|
64
161
|
}
|
|
65
162
|
}
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
vipExpireTime =
|
|
69
|
-
}
|
|
70
|
-
else if (identity.HugeVipEnd) {
|
|
71
|
-
const parsed = Date.parse(identity.HugeVipEnd);
|
|
72
|
-
if (!isNaN(parsed))
|
|
73
|
-
vipExpireTime = parsed;
|
|
74
|
-
}
|
|
75
|
-
else if (identity.twelveEnd) {
|
|
76
|
-
const parsed = Date.parse(identity.twelveEnd);
|
|
77
|
-
if (!isNaN(parsed))
|
|
78
|
-
vipExpireTime = parsed;
|
|
163
|
+
// 4. 严苛防线:任何已过期的到期时间绝对不允许外露
|
|
164
|
+
if (vipExpireTime !== undefined && vipExpireTime <= now) {
|
|
165
|
+
vipExpireTime = undefined;
|
|
79
166
|
}
|
|
80
167
|
return {
|
|
81
168
|
vipStatus,
|
|
@@ -108,17 +195,21 @@ export default async function user_vip_info(query = {}, req = request) {
|
|
|
108
195
|
}
|
|
109
196
|
catch { }
|
|
110
197
|
let vip = deriveVipInfo(cgiRes.body);
|
|
111
|
-
// 若 vip_login_base 无法推断出有效 VIP
|
|
112
|
-
|
|
198
|
+
// 若 vip_login_base 无法推断出有效 VIP,或推断出的 SVIP 已经过期,或需要主页勋章校准,
|
|
199
|
+
// 自动通过主页头部接口获取勋章母版特征
|
|
200
|
+
if ((vip.vipStatus === "nonVip" || vip.vipTier === "svip") && uin && uin !== "0") {
|
|
113
201
|
try {
|
|
114
202
|
const detailRes = await user_detail_fn({ uin, cookie: query.cookie }, req);
|
|
115
203
|
const detailData = detailRes.body?.data || {};
|
|
116
204
|
const fallbackVip = deriveVipInfo(detailData);
|
|
117
205
|
if (fallbackVip.vipStatus === "vip") {
|
|
118
|
-
vip
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
206
|
+
if (vip.vipStatus === "nonVip" || fallbackVip.vipTier === "vip" || !vip.vipExpireTime || vip.vipExpireTime <= Date.now()) {
|
|
207
|
+
vip = {
|
|
208
|
+
...vip,
|
|
209
|
+
...fallbackVip,
|
|
210
|
+
vipExpireTime: (vip.vipExpireTime && vip.vipExpireTime > Date.now()) ? vip.vipExpireTime : fallbackVip.vipExpireTime,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
122
213
|
}
|
|
123
214
|
}
|
|
124
215
|
catch { }
|
package/module/user_vip_info.ts
CHANGED
|
@@ -11,8 +11,30 @@ export interface NormalizedVipInfo {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function parseVipFromIcons(iconList: any[] = [], icons: any[] = []): Partial<NormalizedVipInfo> | null {
|
|
14
|
-
|
|
14
|
+
// 1. 优先从用户主页活跃佩戴的 iconList (NewIconInfo.iconlist) 查找
|
|
15
|
+
for (const item of iconList) {
|
|
15
16
|
if (!item) continue;
|
|
17
|
+
if (item.GreyURL && String(item.GreyURL).trim().length > 0) continue;
|
|
18
|
+
const url = String(item.srcUrl || item.IconURL || "");
|
|
19
|
+
const helptxt = String(item.Helptxt || item.Title || "");
|
|
20
|
+
const m = url.match(/(?:^|\/)(n)?(s?vip)(\d+)\.(?:png|webp)/i);
|
|
21
|
+
if (m) {
|
|
22
|
+
const isYear = Boolean(m[1]) || helptxt.includes("年费");
|
|
23
|
+
const tier = m[2].toLowerCase() === "svip" ? "svip" : "vip";
|
|
24
|
+
const level = parseInt(m[3], 10);
|
|
25
|
+
return {
|
|
26
|
+
vipStatus: "vip",
|
|
27
|
+
vipTier: tier,
|
|
28
|
+
vipLevel: level > 0 ? Math.min(Math.max(level, 1), 10) : 1,
|
|
29
|
+
vipYear: isYear,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. 其次才从旧版快捷入口 icons 查找
|
|
35
|
+
for (const item of icons) {
|
|
36
|
+
if (!item) continue;
|
|
37
|
+
if (item.GreyURL && String(item.GreyURL).trim().length > 0) continue;
|
|
16
38
|
const url = String(item.srcUrl || item.IconURL || "");
|
|
17
39
|
const helptxt = String(item.Helptxt || item.Title || "");
|
|
18
40
|
const m = url.match(/(?:^|\/)(n)?(s?vip)(\d+)\.(?:png|webp)/i);
|
|
@@ -31,7 +53,25 @@ export function parseVipFromIcons(iconList: any[] = [], icons: any[] = []): Part
|
|
|
31
53
|
return null;
|
|
32
54
|
}
|
|
33
55
|
|
|
34
|
-
|
|
56
|
+
function parseDateMs(val: any): number | undefined {
|
|
57
|
+
if (val === undefined || val === null || val === "" || val === 0) return undefined;
|
|
58
|
+
if (typeof val === "number" && val > 0) {
|
|
59
|
+
return val > 1e11 ? val : val * 1000;
|
|
60
|
+
}
|
|
61
|
+
if (typeof val === "string") {
|
|
62
|
+
const trimmed = val.trim();
|
|
63
|
+
if (!trimmed) return undefined;
|
|
64
|
+
const num = Number(trimmed);
|
|
65
|
+
if (!isNaN(num) && num > 0) {
|
|
66
|
+
return num > 1e11 ? num : num * 1000;
|
|
67
|
+
}
|
|
68
|
+
const parsed = Date.parse(trimmed.replace(/-/g, "/"));
|
|
69
|
+
if (!isNaN(parsed)) return parsed;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function deriveVipInfo(body: any, now = Date.now()): NormalizedVipInfo {
|
|
35
75
|
if (!body) {
|
|
36
76
|
return { vipStatus: "unknown" };
|
|
37
77
|
}
|
|
@@ -45,25 +85,64 @@ export function deriveVipInfo(body: any): NormalizedVipInfo {
|
|
|
45
85
|
Number(vipData.ystar || vipData.year || 0) > 0
|
|
46
86
|
);
|
|
47
87
|
|
|
88
|
+
// 1. 分别提取 SVIP 专有到期时间
|
|
89
|
+
let svipExpire: number | undefined = undefined;
|
|
90
|
+
if (vipData.starend) svipExpire = parseDateMs(vipData.starend);
|
|
91
|
+
else if (vipData.ystarend) svipExpire = parseDateMs(vipData.ystarend);
|
|
92
|
+
else if (vipData.send) svipExpire = parseDateMs(vipData.send);
|
|
93
|
+
else if (identity.SvipEnd || identity.svip_end) svipExpire = parseDateMs(identity.SvipEnd || identity.svip_end);
|
|
94
|
+
|
|
95
|
+
// 2. 分别提取普通 VIP / 豪华绿钻专有到期时间
|
|
96
|
+
let vipExpire: number | undefined = undefined;
|
|
97
|
+
if (identity.HugeVipEnd) vipExpire = parseDateMs(identity.HugeVipEnd);
|
|
98
|
+
else if (identity.twelveEnd) vipExpire = parseDateMs(identity.twelveEnd);
|
|
99
|
+
else if (identity.eightEnd) vipExpire = parseDateMs(identity.eightEnd);
|
|
100
|
+
else if (identity.overdate) vipExpire = parseDateMs(identity.overdate);
|
|
101
|
+
else if (vipData.mygreen) vipExpire = parseDateMs(vipData.mygreen);
|
|
102
|
+
|
|
103
|
+
// 3. 通用 userinfo.expire 归集
|
|
104
|
+
const userinfoExpire = Number(vipData.userinfo?.expire || 0) > 0 ? parseDateMs(vipData.userinfo.expire) : undefined;
|
|
105
|
+
if (userinfoExpire !== undefined) {
|
|
106
|
+
if (svipExpire === undefined && vipExpire === undefined) {
|
|
107
|
+
if (svip > 0 && userinfoExpire > now) {
|
|
108
|
+
svipExpire = userinfoExpire;
|
|
109
|
+
} else {
|
|
110
|
+
vipExpire = userinfoExpire;
|
|
111
|
+
}
|
|
112
|
+
} else if (vipExpire === undefined) {
|
|
113
|
+
// 若已解析出 svipExpire(如已过期的 starend/send),则通用的 userinfo.expire 归属于绿钻 vipExpire
|
|
114
|
+
vipExpire = userinfoExpire;
|
|
115
|
+
} else if (svipExpire === undefined && svip > 0) {
|
|
116
|
+
svipExpire = userinfoExpire;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const isSvipExpired = svipExpire !== undefined && svipExpire <= now;
|
|
121
|
+
const isVipExpired = vipExpire !== undefined && vipExpire <= now;
|
|
122
|
+
|
|
48
123
|
let vipStatus: "vip" | "nonVip" | "unknown" = "nonVip";
|
|
49
124
|
let vipTier: "svip" | "vip" | undefined = undefined;
|
|
50
125
|
let vipLevel: number | undefined = undefined;
|
|
51
126
|
let vipYear: boolean | undefined = undefined;
|
|
52
127
|
let vipExpireTime: number | undefined = undefined;
|
|
53
128
|
|
|
54
|
-
|
|
129
|
+
// 1. 如果存在未过期的 SVIP
|
|
130
|
+
if (svip > 0 && !isSvipExpired) {
|
|
55
131
|
vipStatus = "vip";
|
|
56
132
|
vipTier = "svip";
|
|
57
133
|
vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
|
|
58
134
|
vipYear = isYear;
|
|
59
|
-
|
|
135
|
+
vipExpireTime = svipExpire ?? (vipExpire && !isVipExpired ? vipExpire : undefined);
|
|
136
|
+
} else if (isVip > 0 && !isVipExpired) {
|
|
137
|
+
// 2. 如果存在普通 VIP(若 SVIP 已过期但 VIP 未过期,降级并展示未过期的 VIP 到期时间)
|
|
60
138
|
vipStatus = "vip";
|
|
61
139
|
vipTier = "vip";
|
|
62
140
|
vipLevel = level > 0 ? Math.min(Math.max(level, 1), 10) : 1;
|
|
63
141
|
vipYear = isYear;
|
|
142
|
+
vipExpireTime = vipExpire ?? (svipExpire && !isSvipExpired ? svipExpire : undefined);
|
|
64
143
|
}
|
|
65
144
|
|
|
66
|
-
//
|
|
145
|
+
// 3. 若 identity 未命中有效 VIP(例如 SVIP 已过期,且普通 VIP 缺少基础标志或需通过勋章校准)
|
|
67
146
|
if (vipStatus === "nonVip") {
|
|
68
147
|
const info = vipData.Info || vipData.info || {};
|
|
69
148
|
const iconList = info.NewIconInfo?.iconlist || vipData.NewIconInfo?.iconlist || [];
|
|
@@ -74,18 +153,17 @@ export function deriveVipInfo(body: any): NormalizedVipInfo {
|
|
|
74
153
|
vipTier = iconVip.vipTier;
|
|
75
154
|
vipLevel = iconVip.vipLevel;
|
|
76
155
|
vipYear = iconVip.vipYear;
|
|
156
|
+
if (iconVip.vipTier === "vip") {
|
|
157
|
+
vipExpireTime = vipExpire && vipExpire > now ? vipExpire : (userinfoExpire && userinfoExpire > now ? userinfoExpire : undefined);
|
|
158
|
+
} else if (iconVip.vipTier === "svip") {
|
|
159
|
+
vipExpireTime = svipExpire && svipExpire > now ? svipExpire : (userinfoExpire && userinfoExpire > now ? userinfoExpire : undefined);
|
|
160
|
+
}
|
|
77
161
|
}
|
|
78
162
|
}
|
|
79
163
|
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
vipExpireTime =
|
|
83
|
-
} else if (identity.HugeVipEnd) {
|
|
84
|
-
const parsed = Date.parse(identity.HugeVipEnd);
|
|
85
|
-
if (!isNaN(parsed)) vipExpireTime = parsed;
|
|
86
|
-
} else if (identity.twelveEnd) {
|
|
87
|
-
const parsed = Date.parse(identity.twelveEnd);
|
|
88
|
-
if (!isNaN(parsed)) vipExpireTime = parsed;
|
|
164
|
+
// 4. 严苛防线:任何已过期的到期时间绝对不允许外露
|
|
165
|
+
if (vipExpireTime !== undefined && vipExpireTime <= now) {
|
|
166
|
+
vipExpireTime = undefined;
|
|
89
167
|
}
|
|
90
168
|
|
|
91
169
|
return {
|
|
@@ -131,17 +209,21 @@ export default async function user_vip_info(
|
|
|
131
209
|
|
|
132
210
|
let vip = deriveVipInfo(cgiRes.body);
|
|
133
211
|
|
|
134
|
-
// 若 vip_login_base 无法推断出有效 VIP
|
|
135
|
-
|
|
212
|
+
// 若 vip_login_base 无法推断出有效 VIP,或推断出的 SVIP 已经过期,或需要主页勋章校准,
|
|
213
|
+
// 自动通过主页头部接口获取勋章母版特征
|
|
214
|
+
if ((vip.vipStatus === "nonVip" || vip.vipTier === "svip") && uin && uin !== "0") {
|
|
136
215
|
try {
|
|
137
216
|
const detailRes = await user_detail_fn({ uin, cookie: query.cookie }, req);
|
|
138
217
|
const detailData = detailRes.body?.data || {};
|
|
139
218
|
const fallbackVip = deriveVipInfo(detailData);
|
|
140
219
|
if (fallbackVip.vipStatus === "vip") {
|
|
141
|
-
vip
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
220
|
+
if (vip.vipStatus === "nonVip" || fallbackVip.vipTier === "vip" || !vip.vipExpireTime || vip.vipExpireTime <= Date.now()) {
|
|
221
|
+
vip = {
|
|
222
|
+
...vip,
|
|
223
|
+
...fallbackVip,
|
|
224
|
+
vipExpireTime: (vip.vipExpireTime && vip.vipExpireTime > Date.now()) ? vip.vipExpireTime : fallbackVip.vipExpireTime,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
145
227
|
}
|
|
146
228
|
} catch {}
|
|
147
229
|
}
|
|
@@ -156,3 +238,4 @@ export default async function user_vip_info(
|
|
|
156
238
|
cookie: cgiRes.cookie,
|
|
157
239
|
};
|
|
158
240
|
}
|
|
241
|
+
|