feat: 新增虚拟支付订单与网盘链接检测功能

This commit is contained in:
王鹏
2026-07-28 13:42:30 +08:00
parent 4070179f46
commit 2250f744d1
60 changed files with 3668 additions and 106 deletions

View File

@@ -0,0 +1,30 @@
package com.ruoyi.office.config;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 网盘检测线程池。限制并发数,避免批量检测时触发平台风控。
*/
@Configuration
public class NetDiskCheckExecutorConfig
{
@Bean(name = "netDiskCheckExecutor", destroyMethod = "shutdown")
public ExecutorService netDiskCheckExecutor()
{
AtomicInteger threadNumber = new AtomicInteger(1);
ThreadFactory threadFactory = runnable ->
{
Thread thread = new Thread(runnable,
"net-disk-check-" + threadNumber.getAndIncrement());
thread.setDaemon(true);
return thread;
};
return Executors.newFixedThreadPool(4, threadFactory);
}
}

View File

@@ -10,6 +10,7 @@ import javax.servlet.http.HttpServletResponse;
import com.ruoyi.common.utils.CoverGenerator;
import com.ruoyi.office.domain.TtCode;
import com.ruoyi.office.service.ITtCodeService;
import com.ruoyi.office.service.IProjectLinkCheckService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -42,6 +43,8 @@ public class TtProjectInfoController extends BaseController {
private ITtProjectInfoService ttProjectInfoService;
@Autowired
private ITtCodeService ttCodeService;
@Autowired
private IProjectLinkCheckService projectLinkCheckService;
/**
@@ -76,6 +79,26 @@ public class TtProjectInfoController extends BaseController {
return success(ttProjectInfoService.selectTtProjectInfoById(id));
}
/**
* 检测单个项目的网盘链接。
*/
@PreAuthorize("@ss.hasPermi('office:project:edit')")
@Log(title = "检测项目网盘链接", businessType = BusinessType.OTHER)
@PostMapping("/link-check/single/{id}")
public AjaxResult checkProjectLink(@PathVariable("id") Integer id) {
return success(projectLinkCheckService.checkProject(id));
}
/**
* 检测选中项目的网盘链接。
*/
@PreAuthorize("@ss.hasPermi('office:project:edit')")
@Log(title = "批量检测项目网盘链接", businessType = BusinessType.OTHER)
@PostMapping("/link-check/batch")
public AjaxResult checkProjectLinks(@RequestBody Integer[] ids) {
return success(projectLinkCheckService.checkProjects(ids));
}
/**
* 新增项目清单
*/

View File

@@ -4,6 +4,7 @@ import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import java.util.Date;
/**
* 项目清单对象 tt_project_info
@@ -50,6 +51,24 @@ public class TtProjectInfo extends BaseEntity
@Excel(name = "项目百度链接")
private String projectBaiduUrl;
/** 夸克网盘最近检测状态 */
private String quarkCheckStatus;
/** 夸克网盘最近检测说明 */
private String quarkCheckMessage;
/** 夸克网盘最近检测时间 */
private Date quarkCheckedAt;
/** 百度网盘最近检测状态 */
private String baiduCheckStatus;
/** 百度网盘最近检测说明 */
private String baiduCheckMessage;
/** 百度网盘最近检测时间 */
private Date baiduCheckedAt;
public void setId(Integer id)
{
this.id = id;
@@ -131,6 +150,54 @@ public class TtProjectInfo extends BaseEntity
this.projectBaiduUrl = projectBaiduUrl;
}
public String getQuarkCheckStatus() {
return quarkCheckStatus;
}
public void setQuarkCheckStatus(String quarkCheckStatus) {
this.quarkCheckStatus = quarkCheckStatus;
}
public String getQuarkCheckMessage() {
return quarkCheckMessage;
}
public void setQuarkCheckMessage(String quarkCheckMessage) {
this.quarkCheckMessage = quarkCheckMessage;
}
public Date getQuarkCheckedAt() {
return quarkCheckedAt;
}
public void setQuarkCheckedAt(Date quarkCheckedAt) {
this.quarkCheckedAt = quarkCheckedAt;
}
public String getBaiduCheckStatus() {
return baiduCheckStatus;
}
public void setBaiduCheckStatus(String baiduCheckStatus) {
this.baiduCheckStatus = baiduCheckStatus;
}
public String getBaiduCheckMessage() {
return baiduCheckMessage;
}
public void setBaiduCheckMessage(String baiduCheckMessage) {
this.baiduCheckMessage = baiduCheckMessage;
}
public Date getBaiduCheckedAt() {
return baiduCheckedAt;
}
public void setBaiduCheckedAt(Date baiduCheckedAt) {
this.baiduCheckedAt = baiduCheckedAt;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@@ -141,6 +208,7 @@ public class TtProjectInfo extends BaseEntity
.append("projectName1", getProjectName1())
.append("projectDesc", getProjectDesc())
.append("projectUrl", getProjectUrl())
.append("projectBaiduUrl", getProjectBaiduUrl())
.append("projectVurl", getProjectVurl())
.toString();
}

View File

@@ -0,0 +1,117 @@
package com.ruoyi.office.domain;
import java.util.Date;
/**
* 项目网盘链接检测结果对象 tt_project_link_check。
*/
public class TtProjectLinkCheck
{
private Long id;
private Integer projectId;
private String diskType;
private String linkUrl;
private String checkStatus;
private String providerCode;
private String checkMessage;
private Long responseTimeMs;
private Date checkedAt;
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public Integer getProjectId()
{
return projectId;
}
public void setProjectId(Integer projectId)
{
this.projectId = projectId;
}
public String getDiskType()
{
return diskType;
}
public void setDiskType(String diskType)
{
this.diskType = diskType;
}
public String getLinkUrl()
{
return linkUrl;
}
public void setLinkUrl(String linkUrl)
{
this.linkUrl = linkUrl;
}
public String getCheckStatus()
{
return checkStatus;
}
public void setCheckStatus(String checkStatus)
{
this.checkStatus = checkStatus;
}
public String getProviderCode()
{
return providerCode;
}
public void setProviderCode(String providerCode)
{
this.providerCode = providerCode;
}
public String getCheckMessage()
{
return checkMessage;
}
public void setCheckMessage(String checkMessage)
{
this.checkMessage = checkMessage;
}
public Long getResponseTimeMs()
{
return responseTimeMs;
}
public void setResponseTimeMs(Long responseTimeMs)
{
this.responseTimeMs = responseTimeMs;
}
public Date getCheckedAt()
{
return checkedAt;
}
public void setCheckedAt(Date checkedAt)
{
this.checkedAt = checkedAt;
}
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.office.mapper;
import com.ruoyi.office.domain.TtProjectLinkCheck;
/**
* 项目网盘链接检测结果 Mapper。
*/
public interface TtProjectLinkCheckMapper
{
int upsertTtProjectLinkCheck(TtProjectLinkCheck linkCheck);
int deleteByProjectId(Integer projectId);
int deleteByProjectIds(Integer[] projectIds);
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.office.service;
import java.util.Map;
/**
* 项目网盘链接检测服务。
*/
public interface IProjectLinkCheckService
{
Map<String, Object> checkProject(Integer projectId);
Map<String, Object> checkProjects(Integer[] projectIds);
}

View File

@@ -0,0 +1,229 @@
package com.ruoyi.office.service.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.office.domain.TtProjectInfo;
import com.ruoyi.office.domain.TtProjectLinkCheck;
import com.ruoyi.office.mapper.TtProjectInfoMapper;
import com.ruoyi.office.mapper.TtProjectLinkCheckMapper;
import com.ruoyi.office.service.IProjectLinkCheckService;
import com.ruoyi.office.service.netdisk.NetDiskCheckResult;
import com.ruoyi.office.service.netdisk.NetDiskConstants;
import com.ruoyi.office.service.netdisk.NetDiskLinkChecker;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
/**
* 项目网盘链接检测服务实现。
*/
@Service
public class ProjectLinkCheckServiceImpl implements IProjectLinkCheckService
{
private static final int MAX_BATCH_SIZE = 20;
private static final int MAX_MESSAGE_LENGTH = 250;
private final TtProjectInfoMapper projectInfoMapper;
private final TtProjectLinkCheckMapper linkCheckMapper;
private final Map<String, NetDiskLinkChecker> checkerMap;
private final ExecutorService netDiskCheckExecutor;
public ProjectLinkCheckServiceImpl(
TtProjectInfoMapper projectInfoMapper,
TtProjectLinkCheckMapper linkCheckMapper,
List<NetDiskLinkChecker> checkers,
@Qualifier("netDiskCheckExecutor") ExecutorService netDiskCheckExecutor)
{
this.projectInfoMapper = projectInfoMapper;
this.linkCheckMapper = linkCheckMapper;
this.netDiskCheckExecutor = netDiskCheckExecutor;
this.checkerMap = new LinkedHashMap<>();
for (NetDiskLinkChecker checker : checkers)
{
this.checkerMap.put(checker.getDiskType(), checker);
}
}
@Override
public Map<String, Object> checkProject(Integer projectId)
{
if (projectId == null)
{
throw new ServiceException("项目编号不能为空");
}
ProjectCheckOutcome outcome = checkProjectInternal(projectId);
return buildSummary(Collections.singletonList(outcome));
}
@Override
public Map<String, Object> checkProjects(Integer[] projectIds)
{
if (projectIds == null || projectIds.length == 0)
{
throw new ServiceException("请至少选择一个项目");
}
Set<Integer> uniqueIds = new LinkedHashSet<>();
for (Integer projectId : projectIds)
{
if (projectId != null)
{
uniqueIds.add(projectId);
}
}
if (uniqueIds.isEmpty())
{
throw new ServiceException("请至少选择一个项目");
}
if (uniqueIds.size() > MAX_BATCH_SIZE)
{
throw new ServiceException("一次最多检测 " + MAX_BATCH_SIZE + " 个项目");
}
List<CompletableFuture<ProjectCheckOutcome>> futures = new ArrayList<>();
for (Integer projectId : uniqueIds)
{
futures.add(CompletableFuture.supplyAsync(
() -> checkProjectInternal(projectId), netDiskCheckExecutor));
}
List<ProjectCheckOutcome> outcomes = new ArrayList<>();
for (CompletableFuture<ProjectCheckOutcome> future : futures)
{
outcomes.add(future.join());
}
return buildSummary(outcomes);
}
private ProjectCheckOutcome checkProjectInternal(Integer projectId)
{
TtProjectInfo project = projectInfoMapper.selectTtProjectInfoById(projectId);
if (project == null)
{
throw new ServiceException("项目不存在:" + projectId);
}
List<NetDiskCheckResult> results = new ArrayList<>(2);
checkAndSave(projectId, project.getProjectUrl(), NetDiskConstants.DISK_QUARK, results);
checkAndSave(projectId, project.getProjectBaiduUrl(), NetDiskConstants.DISK_BAIDU, results);
return new ProjectCheckOutcome(project.getId(), project.getProjectNum(), results);
}
private void checkAndSave(Integer projectId, String linkUrl, String diskType,
List<NetDiskCheckResult> results)
{
if (StringUtils.isBlank(linkUrl))
{
return;
}
NetDiskLinkChecker checker = checkerMap.get(diskType);
NetDiskCheckResult result;
if (checker == null)
{
result = new NetDiskCheckResult(diskType, linkUrl,
NetDiskConstants.STATUS_UNKNOWN, "NO_CHECKER",
"未找到对应的网盘检测器", 0L);
}
else
{
result = checker.check(linkUrl);
}
TtProjectLinkCheck linkCheck = new TtProjectLinkCheck();
linkCheck.setProjectId(projectId);
linkCheck.setDiskType(diskType);
linkCheck.setLinkUrl(linkUrl);
linkCheck.setCheckStatus(result.getStatus());
linkCheck.setProviderCode(truncate(result.getProviderCode(), 32));
linkCheck.setCheckMessage(truncate(result.getMessage(), MAX_MESSAGE_LENGTH));
linkCheck.setResponseTimeMs(result.getResponseTimeMs());
linkCheck.setCheckedAt(new Date());
linkCheckMapper.upsertTtProjectLinkCheck(linkCheck);
results.add(result);
}
private Map<String, Object> buildSummary(List<ProjectCheckOutcome> outcomes)
{
int validCount = 0;
int invalidCount = 0;
int warningCount = 0;
int unknownCount = 0;
List<Map<String, Object>> resultItems = new ArrayList<>();
for (ProjectCheckOutcome outcome : outcomes)
{
for (NetDiskCheckResult result : outcome.results)
{
if (NetDiskConstants.STATUS_VALID.equals(result.getStatus()))
{
validCount++;
}
else if (NetDiskConstants.STATUS_INVALID.equals(result.getStatus()))
{
invalidCount++;
}
else if (NetDiskConstants.STATUS_UNKNOWN.equals(result.getStatus()))
{
unknownCount++;
}
else
{
warningCount++;
}
Map<String, Object> item = new LinkedHashMap<>();
item.put("projectId", outcome.projectId);
item.put("projectNum", outcome.projectNum);
item.put("diskType", result.getDiskType());
item.put("status", result.getStatus());
item.put("message", result.getMessage());
resultItems.add(item);
}
}
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("projectCount", outcomes.size());
summary.put("linkCount", resultItems.size());
summary.put("validCount", validCount);
summary.put("invalidCount", invalidCount);
summary.put("warningCount", warningCount);
summary.put("unknownCount", unknownCount);
summary.put("results", resultItems);
return summary;
}
private String truncate(String value, int maxLength)
{
if (value == null || value.length() <= maxLength)
{
return value;
}
return value.substring(0, maxLength);
}
private static class ProjectCheckOutcome
{
private final Integer projectId;
private final String projectNum;
private final List<NetDiskCheckResult> results;
private ProjectCheckOutcome(Integer projectId, String projectNum,
List<NetDiskCheckResult> results)
{
this.projectId = projectId;
this.projectNum = projectNum;
this.results = results;
}
}
}

View File

@@ -6,8 +6,10 @@ import java.util.List;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.office.domain.TtCode;
import com.ruoyi.office.mapper.TtCodeMapper;
import com.ruoyi.office.mapper.TtProjectLinkCheckMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.office.mapper.TtProjectInfoMapper;
import com.ruoyi.office.domain.TtProjectInfo;
import com.ruoyi.office.service.ITtProjectInfoService;
@@ -25,6 +27,8 @@ public class TtProjectInfoServiceImpl implements ITtProjectInfoService
private TtProjectInfoMapper ttProjectInfoMapper;
@Autowired
private TtCodeMapper ttCodeMapper;
@Autowired
private TtProjectLinkCheckMapper ttProjectLinkCheckMapper;
/**
* 查询项目清单
@@ -81,8 +85,10 @@ public class TtProjectInfoServiceImpl implements ITtProjectInfoService
* @return 结果
*/
@Override
@Transactional
public int deleteTtProjectInfoByIds(Integer[] ids)
{
ttProjectLinkCheckMapper.deleteByProjectIds(ids);
return ttProjectInfoMapper.deleteTtProjectInfoByIds(ids);
}
@@ -93,8 +99,10 @@ public class TtProjectInfoServiceImpl implements ITtProjectInfoService
* @return 结果
*/
@Override
@Transactional
public int deleteTtProjectInfoById(Integer id)
{
ttProjectLinkCheckMapper.deleteByProjectId(id);
return ttProjectInfoMapper.deleteTtProjectInfoById(id);
}

View File

@@ -0,0 +1,218 @@
package com.ruoyi.office.service.netdisk;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
/**
* 百度网盘分享链接检测器。
*
* 这里只检测分享是否存在,不下载分享文件。
*/
@Component
public class BaiduNetDiskLinkChecker implements NetDiskLinkChecker
{
private static final String USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131 Safari/537.36";
private static final int MAX_REDIRECTS = 5;
private static final Set<String> ALLOWED_HOSTS =
new HashSet<>(Arrays.asList("pan.baidu.com", "yun.baidu.com"));
private static final String[] INVALID_MARKERS = {
"页面不存在",
"你所访问的页面不存在了",
"分享的文件已经被取消了",
"分享已过期",
"该分享文件已过期",
"啊哦,你来晚了"
};
private static final String[] RISK_MARKERS = {
"访问过于频繁",
"请输入验证码",
"系统繁忙,请稍候再试"
};
private final OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(8, TimeUnit.SECONDS)
.followRedirects(false)
.followSslRedirects(false)
.build();
@Override
public String getDiskType()
{
return NetDiskConstants.DISK_BAIDU;
}
@Override
public NetDiskCheckResult check(String linkUrl)
{
long startedAt = System.currentTimeMillis();
String url = StringUtils.trim(linkUrl);
if (!isAllowedUrl(url) || !hasSharePath(url))
{
return result(linkUrl, NetDiskConstants.STATUS_FORMAT_ERROR, "FORMAT",
"百度网盘链接格式不正确", startedAt);
}
String passcode = getQueryParameter(url, "pwd");
String currentUrl = url;
try
{
for (int redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount++)
{
Request request = new Request.Builder()
.url(currentUrl)
.header("User-Agent", USER_AGENT)
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.header("Accept-Encoding", "identity")
.get()
.build();
try (Response response = httpClient.newCall(request).execute())
{
int httpCode = response.code();
if (httpCode >= 300 && httpCode < 400)
{
String location = response.header("Location");
if (StringUtils.isBlank(location))
{
return result(linkUrl, NetDiskConstants.STATUS_UNKNOWN,
String.valueOf(httpCode), "百度网盘返回了无目标地址的跳转", startedAt);
}
String redirectUrl = resolveUrl(currentUrl, location);
if (!isAllowedUrl(redirectUrl))
{
return result(linkUrl, NetDiskConstants.STATUS_UNKNOWN,
String.valueOf(httpCode), "百度网盘跳转到了非预期地址", startedAt);
}
currentUrl = redirectUrl;
continue;
}
String body = response.peekBody(1024L * 1024L).string();
if (httpCode == 404 || containsAny(body, INVALID_MARKERS))
{
return result(linkUrl, NetDiskConstants.STATUS_INVALID,
String.valueOf(httpCode), "分享已失效、取消或不存在", startedAt);
}
if (httpCode == 403 || httpCode == 429 || httpCode >= 500 || containsAny(body, RISK_MARKERS))
{
return result(linkUrl, NetDiskConstants.STATUS_UNKNOWN,
String.valueOf(httpCode), "百度网盘暂时拒绝检测或访问受限", startedAt);
}
if (httpCode >= 200 && httpCode < 300)
{
boolean needsCode = currentUrl.contains("/share/init");
if (needsCode && StringUtils.isBlank(passcode))
{
return result(linkUrl, NetDiskConstants.STATUS_NEED_CODE,
String.valueOf(httpCode), "分享存在,但链接中没有提取码", startedAt);
}
String message = needsCode
? "分享存在,链接中包含提取码"
: "分享链接有效";
return result(linkUrl, NetDiskConstants.STATUS_VALID,
String.valueOf(httpCode), message, startedAt);
}
return result(linkUrl, NetDiskConstants.STATUS_UNKNOWN,
String.valueOf(httpCode), "百度网盘返回了未识别的状态", startedAt);
}
}
return result(linkUrl, NetDiskConstants.STATUS_UNKNOWN, "REDIRECT",
"百度网盘跳转次数过多", startedAt);
}
catch (IOException | RuntimeException e)
{
return result(linkUrl, NetDiskConstants.STATUS_UNKNOWN, "NETWORK",
"检测请求失败:" + safeMessage(e), startedAt);
}
}
private NetDiskCheckResult result(String linkUrl, String status, String providerCode,
String message, long startedAt)
{
return new NetDiskCheckResult(getDiskType(), linkUrl, status, providerCode, message,
System.currentTimeMillis() - startedAt);
}
private boolean hasSharePath(String url)
{
try
{
String path = new URI(url).getPath();
return path != null && (path.startsWith("/s/") || path.startsWith("/share/"));
}
catch (URISyntaxException e)
{
return false;
}
}
private boolean isAllowedUrl(String url)
{
if (StringUtils.isBlank(url))
{
return false;
}
try
{
URI uri = new URI(url);
return "https".equalsIgnoreCase(uri.getScheme())
&& uri.getUserInfo() == null
&& (uri.getPort() == -1 || uri.getPort() == 443)
&& uri.getHost() != null
&& ALLOWED_HOSTS.contains(uri.getHost().toLowerCase());
}
catch (URISyntaxException e)
{
return false;
}
}
private String resolveUrl(String baseUrl, String location)
{
HttpUrl base = HttpUrl.parse(baseUrl);
HttpUrl resolved = base == null ? null : base.resolve(location);
return resolved == null ? null : resolved.toString();
}
private String getQueryParameter(String url, String name)
{
HttpUrl httpUrl = HttpUrl.parse(url);
return httpUrl == null ? null : httpUrl.queryParameter(name);
}
private boolean containsAny(String body, String[] markers)
{
if (StringUtils.isBlank(body))
{
return false;
}
for (String marker : markers)
{
if (body.contains(marker))
{
return true;
}
}
return false;
}
private String safeMessage(Exception e)
{
return StringUtils.defaultIfBlank(e.getMessage(), e.getClass().getSimpleName());
}
}

View File

@@ -0,0 +1,55 @@
package com.ruoyi.office.service.netdisk;
/**
* 单个网盘链接的检测结果。
*/
public class NetDiskCheckResult
{
private final String diskType;
private final String linkUrl;
private final String status;
private final String providerCode;
private final String message;
private final long responseTimeMs;
public NetDiskCheckResult(String diskType, String linkUrl, String status,
String providerCode, String message, long responseTimeMs)
{
this.diskType = diskType;
this.linkUrl = linkUrl;
this.status = status;
this.providerCode = providerCode;
this.message = message;
this.responseTimeMs = responseTimeMs;
}
public String getDiskType()
{
return diskType;
}
public String getLinkUrl()
{
return linkUrl;
}
public String getStatus()
{
return status;
}
public String getProviderCode()
{
return providerCode;
}
public String getMessage()
{
return message;
}
public long getResponseTimeMs()
{
return responseTimeMs;
}
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.office.service.netdisk;
/**
* 网盘类型和检测状态常量。
*/
public final class NetDiskConstants
{
public static final String DISK_QUARK = "QUARK";
public static final String DISK_BAIDU = "BAIDU";
public static final String STATUS_VALID = "VALID";
public static final String STATUS_INVALID = "INVALID";
public static final String STATUS_NEED_CODE = "NEED_CODE";
public static final String STATUS_CODE_ERROR = "CODE_ERROR";
public static final String STATUS_FORMAT_ERROR = "FORMAT_ERROR";
public static final String STATUS_UNKNOWN = "UNKNOWN";
private NetDiskConstants()
{
}
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.office.service.netdisk;
/**
* 网盘链接检测器。
*/
public interface NetDiskLinkChecker
{
String getDiskType();
NetDiskCheckResult check(String linkUrl);
}

View File

@@ -0,0 +1,197 @@
package com.ruoyi.office.service.netdisk;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
/**
* 夸克网盘分享链接检测器。
*/
@Component
public class QuarkNetDiskLinkChecker implements NetDiskLinkChecker
{
private static final String TOKEN_API =
"https://drive-pc.quark.cn/1/clouddrive/share/sharepage/token"
+ "?pr=ucpro&fr=pc&uc_param_str=";
private static final String USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131 Safari/537.36";
private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
private static final Set<Integer> INVALID_CODES = new HashSet<>(Arrays.asList(
41006, 41009, 41010, 41011, 41012, 41019, 41026, 41028, 41029, 41030, 41031
));
private final OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(8, TimeUnit.SECONDS)
.followRedirects(false)
.build();
@Override
public String getDiskType()
{
return NetDiskConstants.DISK_QUARK;
}
@Override
public NetDiskCheckResult check(String linkUrl)
{
long startedAt = System.currentTimeMillis();
String url = StringUtils.trim(linkUrl);
String shareId = parseShareId(url);
if (shareId == null)
{
return result(linkUrl, NetDiskConstants.STATUS_FORMAT_ERROR, "FORMAT",
"夸克网盘链接格式不正确", startedAt);
}
String passcode = getQueryParameter(url, "pwd");
if (StringUtils.isBlank(passcode))
{
passcode = getQueryParameter(url, "passcode");
}
JSONObject payload = new JSONObject();
payload.put("pwd_id", shareId);
payload.put("passcode", StringUtils.defaultString(passcode));
payload.put("support_visit_limit_private_share", true);
Request request = new Request.Builder()
.url(TOKEN_API)
.header("User-Agent", USER_AGENT)
.header("Origin", "https://pan.quark.cn")
.header("Referer", "https://pan.quark.cn/")
.post(RequestBody.create(JSON_MEDIA_TYPE, payload.toJSONString()))
.build();
try (Response response = httpClient.newCall(request).execute())
{
String body = readBody(response.body());
JSONObject json = parseJson(body);
int providerCode = json == null || !json.containsKey("code")
? Integer.MIN_VALUE : json.getIntValue("code");
String message = json == null ? null : json.getString("message");
if (providerCode == 0 && response.isSuccessful())
{
return result(linkUrl, NetDiskConstants.STATUS_VALID, "0",
"分享链接有效", startedAt);
}
if (providerCode == 41008)
{
return result(linkUrl, NetDiskConstants.STATUS_NEED_CODE,
String.valueOf(providerCode), "分享存在,但链接中没有提取码", startedAt);
}
if (providerCode == 41007 || providerCode == 41021)
{
return result(linkUrl, NetDiskConstants.STATUS_CODE_ERROR,
String.valueOf(providerCode), "分享存在,但提取码错误", startedAt);
}
if (INVALID_CODES.contains(providerCode))
{
return result(linkUrl, NetDiskConstants.STATUS_INVALID,
String.valueOf(providerCode),
StringUtils.defaultIfBlank(message, "分享已失效、取消或不存在"), startedAt);
}
if (providerCode == 41022 || providerCode == 41023)
{
return result(linkUrl, NetDiskConstants.STATUS_UNKNOWN,
String.valueOf(providerCode), "分享内容正在审核,暂时无法确认", startedAt);
}
if (providerCode == 45058 || response.code() == 403 || response.code() == 429
|| response.code() >= 500)
{
return result(linkUrl, NetDiskConstants.STATUS_UNKNOWN,
providerCode == Integer.MIN_VALUE
? String.valueOf(response.code()) : String.valueOf(providerCode),
"夸克网盘暂时拒绝检测或访问受限", startedAt);
}
return result(linkUrl, NetDiskConstants.STATUS_UNKNOWN,
providerCode == Integer.MIN_VALUE
? String.valueOf(response.code()) : String.valueOf(providerCode),
StringUtils.defaultIfBlank(message, "夸克网盘返回了未识别的状态"), startedAt);
}
catch (IOException | RuntimeException e)
{
return result(linkUrl, NetDiskConstants.STATUS_UNKNOWN, "NETWORK",
"检测请求失败:" + safeMessage(e), startedAt);
}
}
private NetDiskCheckResult result(String linkUrl, String status, String providerCode,
String message, long startedAt)
{
return new NetDiskCheckResult(getDiskType(), linkUrl, status, providerCode, message,
System.currentTimeMillis() - startedAt);
}
private String parseShareId(String url)
{
try
{
URI uri = new URI(url);
if (!"https".equalsIgnoreCase(uri.getScheme())
|| uri.getUserInfo() != null
|| (uri.getPort() != -1 && uri.getPort() != 443)
|| uri.getHost() == null
|| !"pan.quark.cn".equalsIgnoreCase(uri.getHost()))
{
return null;
}
String[] pathParts = StringUtils.defaultString(uri.getPath()).split("/");
if (pathParts.length < 3 || !"s".equals(pathParts[1])
|| !pathParts[2].matches("[A-Za-z0-9_-]{6,64}"))
{
return null;
}
return pathParts[2];
}
catch (URISyntaxException e)
{
return null;
}
}
private String getQueryParameter(String url, String name)
{
okhttp3.HttpUrl httpUrl = okhttp3.HttpUrl.parse(url);
return httpUrl == null ? null : httpUrl.queryParameter(name);
}
private JSONObject parseJson(String body)
{
try
{
return StringUtils.isBlank(body) ? null : JSON.parseObject(body);
}
catch (RuntimeException e)
{
return null;
}
}
private String readBody(ResponseBody responseBody) throws IOException
{
return responseBody == null ? "" : responseBody.string();
}
private String safeMessage(Exception e)
{
return StringUtils.defaultIfBlank(e.getMessage(), e.getClass().getSimpleName());
}
}

View File

@@ -36,7 +36,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="pictureFile != null and pictureFile != ''"> and picture_file = #{pictureFile}</if>
<if test="videoFile != null and videoFile != ''"> and video_file = #{videoFile}</if>
</where>
ORDER BY code_name DESC
ORDER BY code_id DESC
</select>
<select id="selectTtCodeByCodeId" parameterType="Long" resultMap="TtCodeResult">
@@ -106,4 +106,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{codeId}
</foreach>
</delete>
</mapper>
</mapper>

View File

@@ -16,12 +16,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</sql>
<select id="selectTtFileList" parameterType="TtFile" resultMap="TtFileResult">
<include refid="selectTtFileVo"/>
select f.file_id, f.file_name, f.file_url, f.code_name
from tt_file f
left join (
select code_name, max(code_id) as source_order
from tt_code
group by code_name
) c on c.code_name = f.code_name
<where>
<if test="fileName != null and fileName != ''"> and file_name like concat('%', #{fileName}, '%')</if>
<if test="codeName != null and codeName != ''"> and code_name like concat('%', #{codeName}, '%')</if>
<if test="fileName != null and fileName != ''"> and f.file_name like concat('%', #{fileName}, '%')</if>
<if test="codeName != null and codeName != ''"> and f.code_name like concat('%', #{codeName}, '%')</if>
</where>
ORDER BY code_name DESC, file_id ASC
ORDER BY c.source_order DESC, f.file_id ASC
</select>
<select id="selectTtFileByFileId" parameterType="Long" resultMap="TtFileResult">
@@ -68,4 +74,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{fileId}
</foreach>
</delete>
</mapper>
</mapper>

View File

@@ -14,34 +14,64 @@
<result property="projectUrl" column="project_url" />
<result property="projectVurl" column="project_vurl" />
<result property="projectBaiduUrl" column="project_baidu_url" />
<result property="quarkCheckStatus" column="quark_check_status" />
<result property="quarkCheckMessage" column="quark_check_message" />
<result property="quarkCheckedAt" column="quark_checked_at" />
<result property="baiduCheckStatus" column="baidu_check_status" />
<result property="baiduCheckMessage" column="baidu_check_message" />
<result property="baiduCheckedAt" column="baidu_checked_at" />
</resultMap>
<sql id="selectTtProjectInfoVo">
select id, project_num, project_num1, project_name, project_name1, project_desc, project_url, project_vurl, project_baidu_url from tt_project_info
select p.id,
p.project_num,
p.project_num1,
p.project_name,
p.project_name1,
p.project_desc,
p.project_url,
p.project_vurl,
p.project_baidu_url,
quark_check.check_status as quark_check_status,
quark_check.check_message as quark_check_message,
quark_check.checked_at as quark_checked_at,
baidu_check.check_status as baidu_check_status,
baidu_check.check_message as baidu_check_message,
baidu_check.checked_at as baidu_checked_at
from tt_project_info p
left join tt_project_link_check quark_check
on quark_check.project_id = p.id
and quark_check.disk_type = 'QUARK'
and quark_check.link_url = p.project_url
left join tt_project_link_check baidu_check
on baidu_check.project_id = p.id
and baidu_check.disk_type = 'BAIDU'
and baidu_check.link_url = p.project_baidu_url
</sql>
<select id="selectTtProjectInfoList" parameterType="TtProjectInfo" resultMap="TtProjectInfoResult">
<include refid="selectTtProjectInfoVo"/>
<where>
<if test="projectNum != null and projectNum != ''"> and project_num like concat('%', #{projectNum}, '%')</if>
<if test="projectNum1 != null and projectNum1 != ''"> and project_num1 = #{projectNum1}</if>
<if test="projectName != null and projectName != ''"> and project_name like concat('%', #{projectName}, '%')</if>
<if test="projectName1 != null and projectName1 != ''"> and project_name1 like concat('%', #{projectName1}, '%')</if>
<if test="projectDesc != null and projectDesc != ''"> and project_desc = #{projectDesc}</if>
<if test="projectUrl != null and projectUrl != ''"> and project_url = #{projectUrl}</if>
<if test="projectVurl != null and projectVurl != ''"> and project_vurl = #{projectVurl}</if>
<if test="projectBaiduUrl != null and projectBaiduUrl != ''"> and project_baidu_url = #{projectBaiduUrl}</if>
<if test="projectNum != null and projectNum != ''"> and p.project_num like concat('%', #{projectNum}, '%')</if>
<if test="projectNum1 != null and projectNum1 != ''"> and p.project_num1 = #{projectNum1}</if>
<if test="projectName != null and projectName != ''"> and p.project_name like concat('%', #{projectName}, '%')</if>
<if test="projectName1 != null and projectName1 != ''"> and p.project_name1 like concat('%', #{projectName1}, '%')</if>
<if test="projectDesc != null and projectDesc != ''"> and p.project_desc = #{projectDesc}</if>
<if test="projectUrl != null and projectUrl != ''"> and p.project_url = #{projectUrl}</if>
<if test="projectVurl != null and projectVurl != ''"> and p.project_vurl = #{projectVurl}</if>
<if test="projectBaiduUrl != null and projectBaiduUrl != ''"> and p.project_baidu_url = #{projectBaiduUrl}</if>
</where>
ORDER BY p.id DESC
</select>
<select id="selectTtProjectInfoById" parameterType="Integer" resultMap="TtProjectInfoResult">
<include refid="selectTtProjectInfoVo"/>
where id = #{id}
where p.id = #{id}
</select>
<select id="selectTtProjectInfoByName" parameterType="String" resultMap="TtProjectInfoResult">
<include refid="selectTtProjectInfoVo"/>
where project_name1 = #{codeName}
where p.project_name1 = #{codeName}
</select>
<insert id="insertTtProjectInfo" parameterType="TtProjectInfo" useGeneratedKeys="true" keyProperty="id">
@@ -96,8 +126,8 @@
<select id="lastUpdateList" resultMap="TtProjectInfoResult" parameterType="string">
<include refid="selectTtProjectInfoVo"/>
where project_vurl is not null
<if test="searchKey != null and searchKey != ''">AND IFNULL(project_name1, project_name) like CONCAT('%',#{searchKey},'%') </if>
ORDER BY project_name DESC
where p.project_vurl is not null
<if test="searchKey != null and searchKey != ''">AND IFNULL(p.project_name1, p.project_name) like CONCAT('%',#{searchKey},'%') </if>
ORDER BY p.project_name DESC
</select>
</mapper>
</mapper>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.office.mapper.TtProjectLinkCheckMapper">
<insert id="upsertTtProjectLinkCheck" parameterType="TtProjectLinkCheck" useGeneratedKeys="true" keyProperty="id">
insert into tt_project_link_check
(
project_id,
disk_type,
link_url,
check_status,
provider_code,
check_message,
response_time_ms,
checked_at,
create_time,
update_time
)
values
(
#{projectId},
#{diskType},
#{linkUrl},
#{checkStatus},
#{providerCode},
#{checkMessage},
#{responseTimeMs},
#{checkedAt},
now(),
now()
)
on duplicate key update
link_url = values(link_url),
check_status = values(check_status),
provider_code = values(provider_code),
check_message = values(check_message),
response_time_ms = values(response_time_ms),
checked_at = values(checked_at),
update_time = now()
</insert>
<delete id="deleteByProjectId" parameterType="Integer">
delete from tt_project_link_check where project_id = #{projectId}
</delete>
<delete id="deleteByProjectIds">
delete from tt_project_link_check
where project_id in
<foreach item="projectId" collection="array" open="(" separator="," close=")">
#{projectId}
</foreach>
</delete>
</mapper>