Merge branch 'aiResourceMRCS' of code.gitlink.org.cn:Gitlink/microservices into aiResourceMRCS

This commit is contained in:
otto 2026-07-02 15:33:19 +08:00
commit 318d565cd6
12 changed files with 400 additions and 6 deletions

View File

@ -4,6 +4,7 @@ import com.microservices.common.core.constant.SecurityConstants;
import com.microservices.common.core.constant.ServiceNameConstants;
import com.microservices.common.core.domain.R;
import com.microservices.system.api.domain.SysFile;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.factory.RemoteFileFallbackFactory;
import feign.Response;
@ -131,4 +132,15 @@ public interface RemoteFileService {
@PostMapping(value = "/common/uploadFileToForge/{fileIdentifiers}")
R<List<String>> uploadFileToForge(@PathVariable("fileIdentifiers") String fileIdentifiers,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source);
/**
* 根据文件ID列表获取下载记录内部接口
*
* @param fileIds 文件ID列表逗号分隔
* @param source 内部调用鉴权
* @return 下载记录列表
*/
@GetMapping("/common/getDownloadUserList/{fileIds}")
R<List<SysFileDownloadLogVo>> getDownloadUserList(@PathVariable("fileIds") String fileIds,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source);
}

View File

@ -0,0 +1,46 @@
package com.microservices.system.api.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* 文件下载记录VO供跨服务调用
*
* @author microservices
* @date 2026-06-29
*/
@Data
@ApiModel("文件下载记录")
public class SysFileDownloadLogVo {
@ApiModelProperty(value = "文件ID")
private Long fileId;
@ApiModelProperty(value = "文件原始名称")
private String fileOriginName;
@ApiModelProperty(value = "下载用户ID")
private Long userId;
@ApiModelProperty(value = "下载用户")
private String userName;
@ApiModelProperty(value = "用户昵称")
private String nickName;
@ApiModelProperty(value = "手机号码")
private String phonenumber;
@ApiModelProperty(value = "邮箱")
private String email;
@ApiModelProperty(value = "IP地址")
private String ipAddr;
@ApiModelProperty(value = "下载时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date downloadTime;
}

View File

@ -4,6 +4,7 @@ import com.microservices.common.core.domain.R;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.system.api.RemoteFileService;
import com.microservices.system.api.domain.SysFile;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.system.api.domain.SysFileInfo;
import feign.Response;
import org.slf4j.Logger;
@ -85,6 +86,11 @@ public class RemoteFileFallbackFactory implements FallbackFactory<RemoteFileServ
public R<List<String>> uploadFileToForge(String fileIdentifiers, String source) {
return R.fail("上传本地文件到Forge失败:" + throwable.getMessage());
}
@Override
public R<List<SysFileDownloadLogVo>> getDownloadUserList(String fileIds, String source) {
return R.fail("获取下载记录列表失败:" + throwable.getMessage());
}
};
}
}

View File

@ -9,6 +9,7 @@ import com.microservices.common.core.web.controller.BaseController;
import com.microservices.common.core.web.domain.AjaxResult;
import com.microservices.common.security.annotation.InnerAuth;
import com.microservices.file.service.ISysFileInfoService;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.system.api.domain.SysFileInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
@ -247,4 +248,17 @@ public class CommonController extends BaseController {
sysFileInfo.setFilePath(localFilePath + sysFileInfo.getFilePath());
return R.ok(sysFileInfo);
}
/**
* 根据文件ID列表获取下载记录内部接口
*
* @param fileIds 文件ID列表逗号分隔
* @return 下载记录列表
*/
@InnerAuth
@GetMapping("/getDownloadUserList/{fileIds}")
@ApiOperation(value = "根据文件ID列表获取下载记录", hidden = true)
public R<List<SysFileDownloadLogVo>> getDownloadUserList(@PathVariable("fileIds") String fileIds) {
return R.ok(sysFileInfoService.getDownloadUserListByFileIds(fileIds));
}
}

View File

@ -0,0 +1,34 @@
package com.microservices.file.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* 文件下载记录对象 sys_file_download_log
*
* @author microservices
* @date 2026-06-29
*/
@Data
public class SysFileDownloadLog {
/** 主键 */
private Long id;
/** 文件ID */
private Long fileId;
/** 下载用户ID */
private Long userId;
/** 下载用户 */
private String userName;
/** 下载时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date downloadTime;
/** IP地址 */
private String ipAddr;
}

View File

@ -0,0 +1,33 @@
package com.microservices.file.mapper;
import com.microservices.file.domain.SysFileDownloadLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 文件下载记录Mapper接口
*
* @author microservices
* @date 2026-06-29
*/
@Mapper
public interface SysFileDownloadLogMapper {
/**
* 新增文件下载记录
*
* @param sysFileDownloadLog 文件下载记录
* @return 结果
*/
int insertSysFileDownloadLog(SysFileDownloadLog sysFileDownloadLog);
/**
* 根据文件ID列表查询下载用户列表
*
* @param fileIds 文件ID列表
* @return 文件下载记录列表
*/
List<SysFileDownloadLog> selectDownloadLogByFileIds(@Param("fileIds") Long[] fileIds);
}

View File

@ -1,6 +1,7 @@
package com.microservices.file.service;
import com.microservices.file.domain.FileUploadSession;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.system.api.domain.SysFileInfo;
import org.springframework.web.multipart.MultipartFile;
@ -8,6 +9,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 文件管理Service接口
@ -183,4 +185,12 @@ public interface ISysFileInfoService {
* 新记录拥有独立的fileId和downloadCount(=0)共享物理文件
*/
SysFileInfo cloneFileInfo(SysFileInfo source);
/**
* 根据文件ID列表获取下载记录列表
*
* @param fileIds 文件ID列表逗号分隔
* @return 下载记录列表
*/
List<SysFileDownloadLogVo> getDownloadUserListByFileIds(String fileIds);
}

View File

@ -13,18 +13,27 @@ import com.microservices.common.core.utils.file.FileTypeUtils;
import com.microservices.common.core.utils.file.FileUtils;
import com.microservices.common.core.utils.html.EscapeUtil;
import com.microservices.common.core.utils.uuid.IdUtils;
import com.microservices.common.core.constant.SecurityConstants;
import com.microservices.common.core.utils.ip.IpUtils;
import com.microservices.common.httpClient.domain.GitLinkRequestUrl;
import com.microservices.common.httpClient.util.GitLinkRequestHelper;
import com.microservices.common.redis.service.RedisService;
import com.microservices.common.security.auth.AuthUtil;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.file.domain.FileUploadSession;
import com.microservices.file.domain.SysFileDownloadLog;
import com.microservices.file.mapper.SysFileDownloadLogMapper;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.file.mapper.SysFileInfoMapper;
import com.microservices.file.service.IChunkedUploadService;
import com.microservices.file.service.IFileCommonAsyncService;
import com.microservices.file.service.ISysFileInfoAsyncService;
import com.microservices.file.service.ISysFileInfoService;
import com.microservices.file.utils.FileUploadUtils;
import com.microservices.system.api.RemoteUserService;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.domain.SysUser;
import com.microservices.system.api.utils.FeignUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -44,6 +53,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 文件管理Service业务层处理
@ -57,6 +67,10 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
@Autowired
private SysFileInfoMapper sysFileInfoMapper;
@Autowired
private SysFileDownloadLogMapper sysFileDownloadLogMapper;
@Autowired
private RemoteUserService remoteUserService;
@Autowired
private GitLinkRequestHelper gitLinkRequestHelper;
@Autowired
private ISysFileInfoAsyncService fileInfoAsyncService;
@ -277,7 +291,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
// if (StringUtils.isNotEmpty(sysFileInfo.getFileIdentifier())) {
// throw new ServiceException("当前文件无法下载");
// }
fileDownload(sysFileInfo, response);
fileDownload(sysFileInfo, response, request);
}
@Override
@ -286,7 +300,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
if (StringUtils.isEmpty(sysFileInfo.getFilePath())) {
throw new ServiceException("当前文件无法下载");
}
fileDownload(sysFileInfo, response);
fileDownload(sysFileInfo, response, request);
}
@Override
@ -610,7 +624,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
return fileIdentifier;
}
private void fileDownload(SysFileInfo sysFileInfo, HttpServletResponse response) {
private void fileDownload(SysFileInfo sysFileInfo, HttpServletResponse response, HttpServletRequest request) {
try {
String fileName = sysFileInfo.getFileOriginName();
String filePath = localFilePath + sysFileInfo.getFilePath();
@ -620,6 +634,8 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
}
FileUtils.setAttachmentResponseHeader(response, fileName);
// 在写文件之前记录下载日志避免写文件异常导致日志丢失
recordDownloadLog(sysFileInfo.getFileId(), request);
FileUtils.writeBytes(filePath, response.getOutputStream());
int downloadCount = 1;
if (sysFileInfo.getDownloadCount() != null) {
@ -627,12 +643,43 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
}
sysFileInfo.setDownloadCount(downloadCount);
updateSysFileInfo(sysFileInfo);
} catch (Exception e) {
throw new ServiceException("文件下载异常:%s", e.getMessage());
}
}
/**
* 记录文件下载日志登录用户记录用户ID和用户名未登录仅记录IP
* 参考 zone 模块 open 接口获取用户信息的方式通过请求中的 GitLink Token 解析用户
*/
private void recordDownloadLog(Long fileId, HttpServletRequest request) {
try {
SysFileDownloadLog downloadLog = new SysFileDownloadLog();
downloadLog.setFileId(fileId);
// 通过 GitLink Token 获取用户信息参考 zone OpenController.checkZoneRole 实现
String gitLinkToken = AuthUtil.getGitLinkRequestToken(request);
if (StringUtils.isNotEmpty(gitLinkToken)) {
String username = FeignUtils.getReturnData(
remoteUserService.getUserNameByGitLinkToken(gitLinkToken, SecurityConstants.INNER)
);
if (StringUtils.isNotEmpty(username)) {
downloadLog.setUserName(username);
SysUser sysUser = FeignUtils.getReturnData(
remoteUserService.getSysUserByUserName(username, SecurityConstants.INNER)
);
if (sysUser != null) {
downloadLog.setUserId(sysUser.getUserId());
}
}
}
downloadLog.setDownloadTime(DateUtils.getNowDate());
downloadLog.setIpAddr(IpUtils.getIpAddr(request));
sysFileDownloadLogMapper.insertSysFileDownloadLog(downloadLog);
} catch (Exception e) {
logger.error("记录文件下载日志失败:{}", e.getMessage());
}
}
/**
* 文件大小校验
*
@ -694,6 +741,40 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
}
}
@Override
public List<SysFileDownloadLogVo> getDownloadUserListByFileIds(String fileIds) {
if (StringUtils.isBlank(fileIds)) {
return new ArrayList<>();
}
String[] fileIdStrList = fileIds.split(",");
Long[] fileIdArr = new Long[fileIdStrList.length];
for (int i = 0; i < fileIdStrList.length; i++) {
fileIdArr[i] = Long.valueOf(fileIdStrList[i].trim());
}
// 查询文件信息构建 fileId -> fileOriginName 映射
Map<Long, String> fileNameMap = new HashMap<>();
for (Long fileId : fileIdArr) {
SysFileInfo fileInfo = sysFileInfoMapper.selectSysFileInfoByFileId(fileId);
if (fileInfo != null) {
fileNameMap.put(fileId, fileInfo.getFileOriginName());
}
}
List<SysFileDownloadLog> downloadLogList = sysFileDownloadLogMapper.selectDownloadLogByFileIds(fileIdArr);
if (downloadLogList == null || downloadLogList.isEmpty()) {
return new ArrayList<>();
}
return downloadLogList.stream().map(log -> {
SysFileDownloadLogVo vo = new SysFileDownloadLogVo();
vo.setFileId(log.getFileId());
vo.setFileOriginName(fileNameMap.get(log.getFileId()));
vo.setUserId(log.getUserId());
vo.setUserName(log.getUserName());
vo.setIpAddr(log.getIpAddr());
vo.setDownloadTime(log.getDownloadTime());
return vo;
}).collect(Collectors.toList());
}
@Override
public SysFileInfo cloneFileInfo(SysFileInfo source) {
SysFileInfo clone = new SysFileInfo();

View File

@ -0,0 +1,48 @@
<?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.microservices.file.mapper.SysFileDownloadLogMapper">
<resultMap type="com.microservices.file.domain.SysFileDownloadLog" id="SysFileDownloadLogResult">
<result property="id" column="id"/>
<result property="fileId" column="file_id"/>
<result property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="downloadTime" column="download_time"/>
<result property="ipAddr" column="ip_addr"/>
</resultMap>
<sql id="selectSysFileDownloadLogVo">
select id, file_id, user_id, user_name, download_time, ip_addr
from sys_file_download_log
</sql>
<insert id="insertSysFileDownloadLog" parameterType="com.microservices.file.domain.SysFileDownloadLog" useGeneratedKeys="true" keyProperty="id">
insert into sys_file_download_log
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="fileId != null">file_id,</if>
<if test="userId != null">user_id,</if>
<if test="userName != null and userName != ''">user_name,</if>
<if test="downloadTime != null">download_time,</if>
<if test="ipAddr != null and ipAddr != ''">ip_addr,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="fileId != null">#{fileId},</if>
<if test="userId != null">#{userId},</if>
<if test="userName != null and userName != ''">#{userName},</if>
<if test="downloadTime != null">#{downloadTime},</if>
<if test="ipAddr != null and ipAddr != ''">#{ipAddr},</if>
</trim>
</insert>
<select id="selectDownloadLogByFileIds" resultMap="SysFileDownloadLogResult">
<include refid="selectSysFileDownloadLogVo"/>
where file_id in
<foreach item="fileId" collection="fileIds" open="(" separator="," close=")">
#{fileId}
</foreach>
order by download_time desc
</select>
</mapper>

View File

@ -3,6 +3,7 @@ package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.microservices.system.api.domain.SimpleSysUser;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.zone.resource.domain.ZoneResourceType;
import com.microservices.zone.resource.utils.ResourceExtendDataUtils;
import io.swagger.annotations.ApiModel;
@ -86,6 +87,9 @@ public class ZoneResourceDataVo {
@ApiModelProperty(value = "审核意见/驳回原因")
private String auditReason;
@ApiModelProperty(value = "下载记录列表所有附件的下载明细含用户名、IP、时间")
private List<SysFileDownloadLogVo> downloadUserSummary;
@ApiModelProperty(value = "浏览次数")
private Integer viewCount;

View File

@ -7,6 +7,8 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* @author otto
*/
@ -17,6 +19,9 @@ public class ZoneResourceFileVo extends SysFileInfo {
@ApiModelProperty(value = "资源类型列表")
private ZoneResourceType zoneResourceType;
@ApiModelProperty(value = "下载用户列表")
private List<String> downloadUserList;
public ZoneResourceFileVo(ZoneResourceType zoneResourceType, SysFileInfo sysFileInfo) {
this.zoneResourceType = zoneResourceType;
BeanUtils.copyProperties(sysFileInfo, this);

View File

@ -15,6 +15,7 @@ import com.microservices.common.security.auth.AuthUtil;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.system.api.RemoteFileService;
import com.microservices.system.api.RemoteUserService;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.domain.SysUser;
import com.microservices.system.api.utils.FeignUtils;
@ -44,7 +45,11 @@ import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
@ -275,7 +280,10 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
incrementViewCount(zoneResource.getId());
}
if (ZoneConstants.RESOURCE_AUDIT_PASS.equals(zoneResource.getAuditStatus())) {
return zoneResourceToData(zoneResource, false);
ZoneResourceDataVo dataVo = zoneResourceToData(zoneResource, false);
// 填充下载用户信息
populateDownloadUsers(dataVo);
return dataVo;
} else {
String gitLinkToken = AuthUtil.getGitLinkRequestToken(request);
if (StringUtils.isNotEmpty(gitLinkToken)) {
@ -283,7 +291,10 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
remoteUserService.getUserNameByGitLinkToken(gitLinkToken, SecurityConstants.INNER)
);
if (zoneResource.getCreateBy().equals(currentUsername)) {
return zoneResourceToData(zoneResource, false);
ZoneResourceDataVo dataVo = zoneResourceToData(zoneResource, false);
// 填充下载用户信息
populateDownloadUsers(dataVo);
return dataVo;
}
}
}
@ -291,6 +302,96 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
}
/**
* 填充文件下载用户列表
*/
private void populateDownloadUsers(ZoneResourceDataVo dataVo) {
if (dataVo == null || dataVo.getFileList() == null || dataVo.getFileList().isEmpty()) {
return;
}
try {
// 收集所有文件ID
String fileIds = dataVo.getFileList().stream()
.map(x -> String.valueOf(x.getFileId()))
.collect(Collectors.joining(","));
if (StringUtils.isEmpty(fileIds)) {
return;
}
// 通过Feign调用文件微服务获取下载记录
List<SysFileDownloadLogVo> downloadLogList = FeignUtils.getReturnData(
remoteFileService.getDownloadUserList(fileIds, SecurityConstants.INNER)
);
if (downloadLogList == null) {
downloadLogList = new ArrayList<>();
}
// 补全用户详情昵称手机号邮箱
enrichDownloadLogUserInfo(downloadLogList);
// 按文件ID分组填充到各附件VO中
Map<Long, List<String>> fileUserMap = downloadLogList.stream()
.collect(Collectors.groupingBy(
SysFileDownloadLogVo::getFileId,
Collectors.mapping(SysFileDownloadLogVo::getUserName,
Collectors.toCollection(LinkedHashSet::new))
))
.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> new ArrayList<>(entry.getValue())
));
for (ZoneResourceFileVo fileVo : dataVo.getFileList()) {
List<String> userList = fileUserMap.get(fileVo.getFileId());
fileVo.setDownloadUserList(userList != null ? userList : new ArrayList<>());
}
// 外层直接用原始下载记录列表
dataVo.setDownloadUserSummary(downloadLogList);
} catch (Exception e) {
logger.error("填充下载用户信息失败:{}", e.getMessage());
}
}
/**
* 补全下载记录中的用户详情昵称手机号邮箱
*/
private void enrichDownloadLogUserInfo(List<SysFileDownloadLogVo> downloadLogList) {
if (downloadLogList == null || downloadLogList.isEmpty()) {
return;
}
// 收集所有非空的用户名去重
Set<String> userNames = downloadLogList.stream()
.map(SysFileDownloadLogVo::getUserName)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toSet());
if (userNames.isEmpty()) {
return;
}
// 逐个查询用户信息并构建映射
Map<String, SysUser> userMap = new HashMap<>();
for (String userName : userNames) {
try {
SysUser sysUser = FeignUtils.getReturnData(
remoteUserService.getSysUserByUserName(userName, SecurityConstants.INNER)
);
if (sysUser != null) {
userMap.put(userName, sysUser);
}
} catch (Exception e) {
logger.warn("查询用户[{}]信息失败:{}", userName, e.getMessage());
}
}
// 填充用户详情到下载记录
for (SysFileDownloadLogVo logVo : downloadLogList) {
if (StringUtils.isEmpty(logVo.getUserName())) {
continue;
}
SysUser sysUser = userMap.get(logVo.getUserName());
if (sysUser != null) {
logVo.setNickName(sysUser.getNickName());
logVo.setPhonenumber(sysUser.getPhonenumber());
logVo.setEmail(sysUser.getEmail());
}
}
}
@Override
public GenericsTableDataInfo<ZoneResourceDataVo> selectMyZoneResourceList(ZoneResource zoneResourceSearch) {
if (zoneResourceSearch == null || zoneResourceSearch.getZoneId() == null) {