返回 AiToEarn
gzh.service.ts
根目录 / project / aitoearn-electron / server / src / modules / plat / gzh / gzh.service.ts
1 /*
2 * @Author: nevin
3 * @Date: 2025-02-15 20:59:55
4 * @LastEditTime: 2025-04-27 17:58:21
5 * @LastEditors: nevin
6 * @Description: gzh Gzh 公众号
7 */
8 import { Injectable } from '@nestjs/common';
9 import axios from 'axios';
10 import { v4 as uuidv4 } from 'uuid';
11 import { createHash, createHmac } from 'crypto';
12 import { VideoUTypes } from './comment';
13
14 @Injectable()
15 export class GzhService {
16 private componentAppid = '';
17 private componentAppsecret = '';
18 constructor() {}
19
20 /**
21 * 获取授权Token
22 * @param ticket 票据
23 * @returns
24 */
25 async getComponentAccessToken(ticket: string) {
26 const url = `https://api.weixin.qq.com/cgi-bin/component/api_component_token`;
27 const result = await axios.post<{
28 component_access_token: string; // 'd30bedaa4d8eb3128cf35ddc1030e27d';
29 expires_in: number; // 1630220614;
30 }>(url, {
31 component_appid: this.componentAppid,
32 component_appsecret: this.componentAppsecret,
33 component_verify_ticket: ticket,
34 });
35
36 return result.data;
37 }
38
39 /**
40 * 刷新授权Token
41 * @param refreshToken
42 * @returns
43 */
44 async refreshAccessToken(refreshToken: string) {
45 return null;
46 }
47
48 /**
49 * 查询用户已授权权限列表
50 * @returns
51 */
52 async getAccountScopes(accessToken: string) {
53 const url = `https://member.bilibili.com/arcopen/fn/user/account/scopes`;
54 const result = await axios.get<{
55 code: number; // 0;
56 message: string; // '0';
57 ttl: number; // 1;
58 data: {
59 openid: string; // 'd30bedaa4d8eb3128cf35ddc1030e27d';
60 scopes: string[]; // ['USER_INFO', 'ATC_DATA', 'ATC_BASE'];
61 };
62 }>(url, {
63 headers: this.generateBilibiliHeader({
64 accessToken,
65 isForm: true,
66 }),
67 });
68
69 return result.data.data;
70 }
71
72 /**
73 * 生成请求头
74 * @param data
75 */
76 private generateBilibiliHeader(data: {
77 accessToken: string;
78 body?: { [key: string]: any };
79 isForm?: boolean;
80 }) {
81 const { accessToken, body, isForm } = data;
82 const xBiliContentMd5 = body
83 ? createHash('md5').update(JSON.stringify(body)).digest('hex')
84 : '';
85
86 const header = {
87 Accept: 'application/json',
88 'Content-Type': isForm ? 'multipart/form-data' : 'application/json', // 或者 multipart/form-data
89 'x-bili-content-md5': xBiliContentMd5,
90 'x-bili-timestamp': Math.floor(Date.now() / 1000),
91 'x-bili-signature-method': 'HMAC-SHA256',
92 'x-bili-signature-nonce': uuidv4(),
93 'x-bili-signature-version': '1.0',
94 'access-token': accessToken, // 需要在请求头中添加access-token
95 Authorization: '',
96 };
97
98 // 抽取带”x-bili-“前缀的自定义header,按字典排序拼接,构建完整的待签名字符串:
99 // 待签名字符串包含换行符\n
100 const headerStr = Object.keys(header)
101 .filter((key) => key.startsWith('x-bili-'))
102 .sort()
103 .map((key) => `${key}:${header[key]}\n`)
104 .join('');
105
106 // 使用 createHmac 正确创建签名
107 const signature = createHmac('sha256', '').update(headerStr).digest('hex');
108
109 // 将签名加入 header
110 header.Authorization = signature;
111
112 return header;
113 }
114
115 /**
116 * 视频初始化
117 * @param fileName
118 * @param utype // 1-单个小文件(不超过100M)。默认值为0
119 * @returns
120 */
121 async videoInit(fileName: string, utype: VideoUTypes = 0): Promise<string> {
122 const body = {
123 name: fileName, // test.mp4
124 utype,
125 };
126
127 const url = `https://member.bilibili.com/arcopen/fn/archive/video/init`;
128 const result = await axios.post<{
129 code: number; // 0;
130 message: string; // '0';
131 ttl: number; // 1;
132 request_id: string; // '7b753a287405461f5afa526a1f672094';
133 data: {
134 upload_token: string; // 'd30bedaa4d8eb3128cf35ddc1030e27d';
135 };
136 }>(url, body);
137 return result.data.data.upload_token;
138 }
139
140 /**
141 * 封面上传
142 * @param accessToken
143 * @param file
144 * @returns
145 */
146 async coverUpload(
147 accessToken: string,
148 file: Express.Multer.File,
149 ): Promise<string> {
150 const url = `https://member.bilibili.com/arcopen/fn/archive/cover/upload`;
151
152 const formData = new FormData();
153 const blob = new Blob([file.buffer], { type: file.mimetype });
154 formData.append('file', blob, file.originalname);
155
156 const result = await axios.post<{
157 code: number; // 0;
158 message: string; // '0';
159 ttl: number; // 1;
160 request_id: string; // '7b753a287405461f5afa526a1f672094';
161 data: {
162 url: string; // "https://archive.biliimg.com/bfs/..."
163 };
164 }>(url, formData, {
165 headers: this.generateBilibiliHeader({
166 accessToken: accessToken,
167 isForm: true,
168 }),
169 });
170 return result.data.data.url;
171 }
172
173 /**
174 * 公众号素材发布
175 * @param accessToken
176 * @param mediaId
177 * @returns
178 */
179 async freepublishSubmit(
180 accessToken: string,
181 mediaId: string,
182 ): Promise<string> {
183 const url = `https://api.weixin.qq.com/cgi-bin/freepublish/submit?access_token=${accessToken}`;
184
185 const result = await axios.post<{
186 errcode: number; // 0;
187 errmsg: string; // 'ok';
188 publish_id: string; // '100000001';
189 }>(
190 url,
191 {
192 media_id: mediaId,
193 },
194 {
195 headers: this.generateBilibiliHeader({
196 accessToken,
197 }),
198 },
199 );
200 return result.data.publish_id;
201 }
202
203 /**
204 * 分区查询
205 * @param accessToken
206 * @returns
207 */
208 async archiveTypeList(accessToken: string) {
209 const url = `https://member.bilibili.com/arcopen/fn/archive/type/list`;
210
211 const result = await axios.get<{
212 code: number; // 0;
213 message: string; // '0';
214 ttl: number; // 1;
215 request_id: string; // '35f4a1e0d3765a92510f919d0b6721dd';
216 data: any;
217 }>(url, {
218 headers: this.generateBilibiliHeader({
219 accessToken,
220 }),
221 });
222 return result.data.data;
223 }
224 }
225
225 lines TYPESCRIPT