From 04443bcb5ee5ab457d3d8bdc41fce30c1b9e9fb5 Mon Sep 17 00:00:00 2001 From: rensumo <15206641+rensumo@user.noreply.gitee.com> Date: Thu, 19 Feb 2026 12:59:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=8D=90=E8=B5=A0?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E5=AD=98=E5=82=A8=E5=92=8C=E9=9A=8F?= =?UTF-8?q?=E6=9C=BA=E9=80=89=E6=8B=A9=E8=B4=A6=E5=8F=B7=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web-front/src/views/Home.vue | 243 +++++++++++++++++- .../cn/qaiu/lz/web/controller/ParserApi.java | 28 ++ .../cn/qaiu/lz/web/model/DonatedAccount.java | 52 ++++ .../cn/qaiu/lz/web/service/DbService.java | 17 ++ .../lz/web/service/impl/DbServiceImpl.java | 90 +++++++ 5 files changed, 417 insertions(+), 13 deletions(-) create mode 100644 web-service/src/main/java/cn/qaiu/lz/web/model/DonatedAccount.java diff --git a/web-front/src/views/Home.vue b/web-front/src/views/Home.vue index 92fa027..e6857f1 100644 --- a/web-front/src/views/Home.vue +++ b/web-front/src/views/Home.vue @@ -2,7 +2,7 @@
+ + + + + + + +
+ 当前账号池(共 {{ donateAccountCounts.total }} 个) + + {{ getPanDisplayName(panType) }}: {{ count }} 个 + +
+
+ 暂无捐赠账号,成为第一个捐赠者吧! +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
@@ -436,7 +517,21 @@ export default { ext5: '' }, // 所有网盘的认证配置 { panType: config } - allAuthConfigs: {} + allAuthConfigs: {}, + + // 捐赠账号相关 + showDonateDialog: false, + donateSubmitting: false, + donateConfig: { + panType: '', + authType: 'cookie', + username: '', + password: '', + token: '', + remark: '' + }, + // 捐赠账号数量统计 { panType: count, total: N } + donateAccountCounts: { total: 0 } } }, computed: { @@ -460,6 +555,7 @@ export default { if (url.includes('drive.uc.cn') || url.includes('fast.uc.cn')) return 'UC' if (url.includes('feijipan.com') || url.includes('feijihe.com') || url.includes('xiaofeiyang.com')) return 'FJ' if (url.includes('ilanzou.com') || url.includes('lanzouv.com')) return 'IZ' + if (url.includes('123pan.com') || url.includes('123684.com') || url.includes('123865.com')) return 'YE' return '' }, @@ -469,7 +565,8 @@ export default { 'QK': '夸克网盘', 'UC': 'UC网盘', 'FJ': '小飞机网盘', - 'IZ': '蓝奏优享' + 'IZ': '蓝奏优享', + 'YE': '123云盘' } return names[panType] || panType }, @@ -663,14 +760,36 @@ export default { } }, - // 生成加密的 auth 参数(根据当前链接的网盘类型) - generateAuthParam() { + // 生成加密的 auth 参数(优先使用个人配置,否则从后端随机获取捐赠账号) + async generateAuthParam() { const panType = this.getCurrentPanType() - if (!panType || !this.allAuthConfigs[panType]) { - return '' + if (!panType) return '' + + let config = null + + // 优先使用个人配置 + if (this.allAuthConfigs[panType]) { + config = this.allAuthConfigs[panType] + console.log(`[认证] 使用个人配置: ${this.getPanDisplayName(panType)}`) + } else { + // 从后端随机获取捐赠账号 + try { + const response = await axios.get(`${this.baseAPI}/v2/randomAuth`, { params: { panType } }) + // 解包 JsonResult 嵌套 + let data = response.data + while (data && data.data !== undefined && data.code !== undefined) { + data = data.data + } + if (data && (data.token || data.username)) { + config = data + console.log(`[认证] 使用捐赠账号: ${this.getPanDisplayName(panType)}`) + } + } catch (e) { + console.log(`[认证] 无可用捐赠账号: ${this.getPanDisplayName(panType)}`) + } } - const config = this.allAuthConfigs[panType] + if (!config) return '' // 构建 JSON 对象 const authObj = {} @@ -710,9 +829,9 @@ export default { }, // 更新智能直链 - updateDirectLink() { + async updateDirectLink() { if (this.link) { - const authParam = this.generateAuthParam() + const authParam = await this.generateAuthParam() const authSuffix = authParam ? `&auth=${authParam}` : '' this.directLink = `${this.baseAPI}/parser?url=${this.link}${this.password ? `&pwd=${this.password}` : ''}${authSuffix}` } @@ -766,8 +885,8 @@ export default { this.errorButtonVisible = false try { this.isLoading = true - // 添加认证参数 - const authParam = this.generateAuthParam() + // 添加认证参数(异步获取) + const authParam = await this.generateAuthParam() if (authParam) { params.auth = authParam } @@ -1086,7 +1205,7 @@ export default { if (this.password) params.pwd = this.password // 添加认证参数 - const authParam = this.generateAuthParam() + const authParam = await this.generateAuthParam() if (authParam) params.auth = authParam const response = await axios.get(`${this.baseAPI}/v2/clientLinks`, { params }) @@ -1115,6 +1234,101 @@ export default { } finally { this.isLoading = false } + }, + + // ========== 捐赠账号相关方法 ========== + + // 捐赠弹窗中网盘类型变更 + onDonatePanTypeChange(panType) { + const types = this.getDonateAuthTypes() + this.donateConfig.authType = types.length > 0 ? types[0].value : 'cookie' + this.donateConfig.username = '' + this.donateConfig.password = '' + this.donateConfig.token = '' + this.donateConfig.remark = '' + }, + + // 获取捐赠弹窗支持的认证类型 + getDonateAuthTypes() { + const pt = (this.donateConfig.panType || '').toLowerCase() + const allTypes = { + cookie: { label: 'Cookie', value: 'cookie' }, + accesstoken: { label: 'AccessToken', value: 'accesstoken' }, + authorization: { label: 'Authorization', value: 'authorization' }, + password: { label: '用户名密码', value: 'password' }, + custom: { label: '自定义', value: 'custom' } + } + switch (pt) { + case 'qk': case 'uc': return [allTypes.cookie] + case 'fj': case 'iz': return [allTypes.password] + case 'ye': return [allTypes.password, allTypes.authorization] + default: return Object.values(allTypes) + } + }, + + // 提交捐赠账号(调用后端 API) + async submitDonateAccount() { + if (!this.donateConfig.panType) { + this.$message.warning('请选择网盘类型') + return + } + if (!this.donateConfig.token && !this.donateConfig.username) { + this.$message.warning('请填写认证信息(Cookie/Token 或 用户名密码)') + return + } + + this.donateSubmitting = true + try { + const payload = { + panType: this.donateConfig.panType, + authType: this.donateConfig.authType, + username: this.donateConfig.username || '', + password: this.donateConfig.password || '', + token: this.donateConfig.token || '', + remark: this.donateConfig.remark || '' + } + await axios.post(`${this.baseAPI}/v2/donateAccount`, payload) + this.$message.success(`已捐赠 ${this.getPanDisplayName(this.donateConfig.panType)} 账号,感谢您的贡献!`) + + // 重置表单 + this.donateConfig.username = '' + this.donateConfig.password = '' + this.donateConfig.token = '' + this.donateConfig.remark = '' + + // 刷新计数 + await this.loadDonateAccountCounts() + } catch (e) { + console.error('捐赠账号失败:', e) + this.$message.error('捐赠失败,请稍后重试') + } finally { + this.donateSubmitting = false + } + }, + + // 从后端加载捐赠账号数量统计 + async loadDonateAccountCounts() { + try { + const response = await axios.get(`${this.baseAPI}/v2/donateAccountCounts`) + // 解包可能的 JsonResult 嵌套: { code, data: { code, data: { QK: 3, total: 4 } } } + let data = response.data + while (data && data.data !== undefined && data.code !== undefined) { + data = data.data + } + if (data && typeof data === 'object') { + // 确保有 total 字段 + if (data.total === undefined) { + let total = 0 + for (const [key, val] of Object.entries(data)) { + if (typeof val === 'number') total += val + } + data.total = total + } + this.donateAccountCounts = data + } + } catch (e) { + console.error('加载捐赠账号统计失败:', e) + } } }, @@ -1128,6 +1342,9 @@ export default { // 加载认证配置 this.loadAuthConfig() + // 加载捐赠账号统计 + this.loadDonateAccountCounts() + // 获取初始统计信息 this.getInfo() diff --git a/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java b/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java index 5f6f458..29dc5e5 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java @@ -477,4 +477,32 @@ public class ParserApi { ClientLinkType type = ClientLinkType.valueOf(clientType.toUpperCase()); return clientLinks.get(type); } + + // ========== 捐赠账号 API ========== + + /** + * 捐赠网盘账号 + */ + @RouteMapping(value = "/donateAccount", method = RouteMethod.POST) + public Future donateAccount(HttpServerRequest request, JsonObject body) { + String ip = request.remoteAddress().host(); + body.put("ip", ip); + return dbService.saveDonatedAccount(body); + } + + /** + * 获取各网盘捐赠账号数量 + */ + @RouteMapping(value = "/donateAccountCounts", method = RouteMethod.GET) + public Future getDonateAccountCounts() { + return dbService.getDonatedAccountCounts(); + } + + /** + * 随机获取指定网盘类型的捐赠账号(内部使用,返回加密后的 auth 参数) + */ + @RouteMapping(value = "/randomAuth", method = RouteMethod.GET) + public Future getRandomAuth(String panType) { + return dbService.getRandomDonatedAccount(panType); + } } diff --git a/web-service/src/main/java/cn/qaiu/lz/web/model/DonatedAccount.java b/web-service/src/main/java/cn/qaiu/lz/web/model/DonatedAccount.java new file mode 100644 index 0000000..f374a04 --- /dev/null +++ b/web-service/src/main/java/cn/qaiu/lz/web/model/DonatedAccount.java @@ -0,0 +1,52 @@ +package cn.qaiu.lz.web.model; + +import cn.qaiu.db.ddl.Constraint; +import cn.qaiu.db.ddl.Length; +import cn.qaiu.db.ddl.Table; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +/** + * 捐赠账号实体 + * 用户捐赠的网盘认证信息,解析时随机选择使用 + */ +@Data +@Table("donated_account") +public class DonatedAccount { + + private static final long serialVersionUID = 1L; + + @Constraint(autoIncrement = true, notNull = true) + private Long id; + + @Length(varcharSize = 16) + @Constraint(notNull = true) + private String panType; // 网盘类型: QK, UC, FJ, IZ, YE + + @Length(varcharSize = 32) + @Constraint(notNull = true) + private String authType; // 认证类型: cookie, accesstoken, authorization, password, custom + + @Length(varcharSize = 128) + private String username; // 用户名 + + @Length(varcharSize = 128) + private String password; // 密码 + + @Length(varcharSize = 4096) + private String token; // Cookie/Token + + @Length(varcharSize = 64) + private String remark; // 备注 + + @Length(varcharSize = 64) + private String ip; // 捐赠者IP + + @Constraint(notNull = true, defaultValue = "true") + private Boolean enabled = true; // 是否启用 + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime = new Date(); +} diff --git a/web-service/src/main/java/cn/qaiu/lz/web/service/DbService.java b/web-service/src/main/java/cn/qaiu/lz/web/service/DbService.java index 0256d2e..8dcc8d6 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/service/DbService.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/service/DbService.java @@ -50,4 +50,21 @@ public interface DbService extends BaseAsyncService { */ Future getPlaygroundParserById(Long id); + // ========== 捐赠账号相关 ========== + + /** + * 保存捐赠账号 + */ + Future saveDonatedAccount(JsonObject account); + + /** + * 获取各网盘捐赠账号数量统计 + */ + Future getDonatedAccountCounts(); + + /** + * 随机获取指定网盘类型的一个启用账号 + */ + Future getRandomDonatedAccount(String panType); + } diff --git a/web-service/src/main/java/cn/qaiu/lz/web/service/impl/DbServiceImpl.java b/web-service/src/main/java/cn/qaiu/lz/web/service/impl/DbServiceImpl.java index 87aa228..7ebee36 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/service/impl/DbServiceImpl.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/service/impl/DbServiceImpl.java @@ -265,4 +265,94 @@ public class DbServiceImpl implements DbService { return promise.future(); } + + // ========== 捐赠账号相关 ========== + + @Override + public Future saveDonatedAccount(JsonObject account) { + JDBCPool client = JDBCPoolInit.instance().getPool(); + Promise promise = Promise.promise(); + + String sql = """ + INSERT INTO donated_account + (pan_type, auth_type, username, password, token, remark, ip, enabled, create_time) + VALUES (?, ?, ?, ?, ?, ?, ?, true, NOW()) + """; + + client.preparedQuery(sql) + .execute(Tuple.of( + account.getString("panType"), + account.getString("authType"), + account.getString("username"), + account.getString("password"), + account.getString("token"), + account.getString("remark"), + account.getString("ip") + )) + .onSuccess(res -> { + promise.complete(JsonResult.success("捐赠成功").toJsonObject()); + }) + .onFailure(e -> { + log.error("saveDonatedAccount failed", e); + promise.fail(e); + }); + + return promise.future(); + } + + @Override + public Future getDonatedAccountCounts() { + JDBCPool client = JDBCPoolInit.instance().getPool(); + Promise promise = Promise.promise(); + + String sql = "SELECT pan_type, COUNT(*) as count FROM donated_account WHERE enabled = true GROUP BY pan_type"; + + client.query(sql).execute().onSuccess(rows -> { + JsonObject counts = new JsonObject(); + int total = 0; + for (Row row : rows) { + String panType = row.getString("pan_type"); + Integer count = row.getInteger("count"); + counts.put(panType, count); + total += count; + } + counts.put("total", total); + promise.complete(JsonResult.data(counts).toJsonObject()); + }).onFailure(e -> { + log.error("getDonatedAccountCounts failed", e); + promise.fail(e); + }); + + return promise.future(); + } + + @Override + public Future getRandomDonatedAccount(String panType) { + JDBCPool client = JDBCPoolInit.instance().getPool(); + Promise promise = Promise.promise(); + + String sql = "SELECT * FROM donated_account WHERE pan_type = ? AND enabled = true ORDER BY RAND() LIMIT 1"; + + client.preparedQuery(sql) + .execute(Tuple.of(panType)) + .onSuccess(rows -> { + if (rows.size() > 0) { + Row row = rows.iterator().next(); + JsonObject account = new JsonObject(); + account.put("authType", row.getString("auth_type")); + account.put("username", row.getString("username")); + account.put("password", row.getString("password")); + account.put("token", row.getString("token")); + promise.complete(JsonResult.data(account).toJsonObject()); + } else { + promise.complete(JsonResult.data(new JsonObject()).toJsonObject()); + } + }) + .onFailure(e -> { + log.error("getRandomDonatedAccount failed", e); + promise.fail(e); + }); + + return promise.future(); + } }