46 changed files with 864 additions and 190 deletions
@ -0,0 +1,68 @@ |
|||
package com.project.ding.auth; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.project.ding.domain.dto.UserDTO; |
|||
import com.project.ding.domain.entity.AdminWhiteListEntity; |
|||
import com.project.ding.domain.entity.UserEntity; |
|||
import com.project.ding.mapper.AdminWhiteListMapper; |
|||
import com.project.ding.mapper.UserMapper; |
|||
import com.project.ding.utils.DingUserSyncUtil; |
|||
import com.project.ding.utils.DingUtil; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.security.authentication.AuthenticationProvider; |
|||
import org.springframework.security.authentication.BadCredentialsException; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.*; |
|||
|
|||
@Component |
|||
public class DingTalkAuthenticationProvider implements AuthenticationProvider { |
|||
@Autowired |
|||
private DingUtil dingUtil; // 封装获取 userId 接口
|
|||
@Autowired |
|||
private UserMapper userMapper; |
|||
@Autowired |
|||
private AdminWhiteListMapper whitelistMapper; |
|||
|
|||
@Autowired |
|||
private DingUserSyncUtil dingUserSyncUtil; |
|||
|
|||
@Override |
|||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
|||
String authCode = (String) authentication.getPrincipal(); |
|||
// 1. 换取钉钉 UserId
|
|||
String dingId = dingUtil.getUserIdByCode(authCode); |
|||
if (StrUtil.isBlank(dingId)) { |
|||
throw new BadCredentialsException("钉钉授权失败"); |
|||
} |
|||
|
|||
UserEntity user = userMapper.selectById(Long.valueOf(dingId)); |
|||
if (Objects.isNull(user)) { |
|||
// 直接插入这个用户并且触发一次异步的全量用户同步
|
|||
UserDTO userDTO = dingUtil.getUserById(dingId); |
|||
if (Objects.isNull(userDTO)) { |
|||
throw new BadCredentialsException("获取用户信息异常,请联系管理员"); |
|||
} |
|||
userMapper.batchUpsert(Collections.singletonList(userDTO.toEntity(UserEntity::new))); |
|||
user = userMapper.selectById(Long.valueOf(dingId)); |
|||
dingUserSyncUtil.triggerSync(false); |
|||
} |
|||
|
|||
// 3. 分配角色:默认考生,白名单为管理员
|
|||
List<SimpleGrantedAuthority> authorities = new ArrayList<>(); |
|||
authorities.add(new SimpleGrantedAuthority("ROLE_CANDIDATE")); |
|||
LambdaQueryWrapper<AdminWhiteListEntity> queryWrapper = new LambdaQueryWrapper<>(); |
|||
queryWrapper.eq(AdminWhiteListEntity::getUserId , user.getId()); |
|||
AdminWhiteListEntity existOne = whitelistMapper.selectOne(queryWrapper); |
|||
if (Objects.nonNull(existOne)) { |
|||
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); |
|||
} |
|||
return new DingTalkAuthenticationToken(user, authorities); |
|||
} |
|||
|
|||
@Override public boolean supports(Class<?> a) { return DingTalkAuthenticationToken.class.isAssignableFrom(a); } |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
package com.project.ding.auth; |
|||
|
|||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
|||
import org.springframework.security.core.GrantedAuthority; |
|||
|
|||
import java.util.Collection; |
|||
|
|||
public class DingTalkAuthenticationToken extends AbstractAuthenticationToken { |
|||
private final Object principal; // 登录后存 UserEntity,登录前存 authCode
|
|||
|
|||
public DingTalkAuthenticationToken(Object principal) { |
|||
super(null); |
|||
this.principal = principal; |
|||
setAuthenticated(false); |
|||
} |
|||
|
|||
public DingTalkAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { |
|||
super(authorities); |
|||
this.principal = principal; |
|||
setAuthenticated(true); |
|||
} |
|||
|
|||
@Override public Object getCredentials() { return null; } |
|||
@Override public Object getPrincipal() { return principal; } |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
package com.project.ding.auth; |
|||
|
|||
import com.project.ding.utils.JwtUtils; |
|||
import io.jsonwebtoken.Claims; |
|||
import jakarta.servlet.FilterChain; |
|||
import jakarta.servlet.ServletException; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
|||
import org.springframework.security.core.authority.SimpleGrantedAuthority; // 确保有这行导入
|
|||
import org.springframework.security.core.context.SecurityContextHolder; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.StringUtils; |
|||
import org.springframework.web.filter.OncePerRequestFilter; |
|||
|
|||
import java.io.IOException; |
|||
import java.util.List; |
|||
|
|||
@Component |
|||
public class JwtAuthenticationFilter extends OncePerRequestFilter { |
|||
|
|||
@Autowired |
|||
private JwtUtils jwtUtils; |
|||
|
|||
@Override |
|||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
|||
throws ServletException, IOException { |
|||
|
|||
String header = request.getHeader("Authorization"); |
|||
|
|||
if (StringUtils.hasText(header) && header.startsWith("Bearer ")) { |
|||
String token = header.substring(7); |
|||
try { |
|||
Claims claims = jwtUtils.parseToken(token); |
|||
String userId = claims.getSubject(); |
|||
|
|||
// 1. 获取原始的 roles 列表
|
|||
Object rolesObj = claims.get("roles"); |
|||
|
|||
if (userId != null && rolesObj instanceof List<?>) { |
|||
// 2. 显式转换并映射,增加 Object::toString 保证类型安全
|
|||
List<SimpleGrantedAuthority> authorities = ((List<?>) rolesObj).stream() |
|||
.map(Object::toString) // 强制转为 String,解决构造器匹配问题
|
|||
.map(SimpleGrantedAuthority::new) // 现在这里不会报红了
|
|||
.toList(); |
|||
|
|||
// 3. 构建并设置认证信息
|
|||
UsernamePasswordAuthenticationToken authentication = |
|||
new UsernamePasswordAuthenticationToken(userId, null, authorities); |
|||
SecurityContextHolder.getContext().setAuthentication(authentication); |
|||
} |
|||
} catch (Exception e) { |
|||
// Token解析失败不处理,后续 Security 拦截器会返回 403
|
|||
logger.error("JWT authentication failed: " + e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
filterChain.doFilter(request, response); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
package com.project.ding.config; |
|||
|
|||
import lombok.Data; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
@Data |
|||
@Component |
|||
@ConfigurationProperties(prefix = "ding") |
|||
public class DingProperties { |
|||
private String appKey; |
|||
private String appSecret; |
|||
private Long agentId; |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
package com.project.ding.config; |
|||
|
|||
import com.project.ding.auth.DingTalkAuthenticationProvider; |
|||
import com.project.ding.auth.JwtAuthenticationFilter; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.security.authentication.AuthenticationManager; |
|||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; |
|||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; |
|||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
|||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
|||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; |
|||
import org.springframework.security.config.http.SessionCreationPolicy; |
|||
import org.springframework.security.web.SecurityFilterChain; |
|||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
|||
|
|||
@Configuration |
|||
@EnableWebSecurity |
|||
@EnableMethodSecurity |
|||
public class SecurityConfig { |
|||
|
|||
@Bean |
|||
public SecurityFilterChain filterChain(HttpSecurity http, |
|||
DingTalkAuthenticationProvider provider, |
|||
JwtAuthenticationFilter jwtFilter) throws Exception { |
|||
return http |
|||
.csrf(AbstractHttpConfigurer::disable) |
|||
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) |
|||
.authorizeHttpRequests(auth -> auth |
|||
// 登录放行
|
|||
.requestMatchers("/api/login/**").permitAll() |
|||
// 管理端锁定
|
|||
.requestMatchers("/api/admin/**").hasRole("ADMIN") |
|||
// 其余(考生端)需登录
|
|||
.anyRequest().authenticated() |
|||
) |
|||
.authenticationProvider(provider) |
|||
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) |
|||
.build(); |
|||
} |
|||
|
|||
@Bean |
|||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { |
|||
return config.getAuthenticationManager(); |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
package com.project.ding.controller; |
|||
|
|||
|
|||
import com.github.tingyugetc520.ali.dingtalk.api.DtService; |
|||
import com.project.ding.domain.dto.DepartmentDTO; |
|||
import com.project.ding.domain.dto.DingUserDTO; |
|||
import com.project.ding.utils.DingUtil; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
|
|||
@RestController |
|||
@Slf4j |
|||
public class DepartmentController { |
|||
@Autowired |
|||
private DingUtil dingUtil; |
|||
@Autowired |
|||
private DtService dtService; |
|||
@GetMapping("/test") |
|||
public void test(HttpServletResponse response) throws Exception { |
|||
List<DepartmentDTO> list = dingUtil.getAllDepartment(); |
|||
|
|||
List<DingUserDTO> userList = new ArrayList<>(); |
|||
for (int i = 0; i < list.size(); i++) { |
|||
List<DingUserDTO> userInDepartment = dingUtil.getUserIdInDepartment(list.get(i).getId()); |
|||
userList.addAll(userInDepartment); |
|||
} |
|||
for (int i = 0; i < 10; i++) { |
|||
System.out.println(111); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package com.project.ding.controller; |
|||
|
|||
import com.project.base.domain.result.Result; |
|||
import com.project.ding.domain.dto.LoginDTO; |
|||
import com.project.ding.domain.service.AuthDomainService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
@RestController |
|||
@RequestMapping("/api/login") |
|||
public class LoginController { |
|||
|
|||
@Autowired |
|||
private AuthDomainService authDomainService; |
|||
|
|||
/** |
|||
* 钉钉免登 |
|||
*/ |
|||
@PostMapping("/ding") |
|||
public Result<LoginDTO> login(@RequestParam("authCode") String authCode) { |
|||
// 逻辑全部下沉到 Service
|
|||
return Result.success(authDomainService.loginByDing(authCode)); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
package com.project.ding.domain.dto; |
|||
|
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Data |
|||
@Builder |
|||
public class LoginDTO { |
|||
private String token; // JWT 令牌
|
|||
private String name; // 用户姓名
|
|||
private List<String> roles; // 用户角色列表
|
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
package com.project.ding.domain.entity; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.IdType; |
|||
import com.baomidou.mybatisplus.annotation.TableId; |
|||
import com.baomidou.mybatisplus.annotation.TableName; |
|||
import com.project.base.domain.entity.BaseEntity; |
|||
import jakarta.persistence.Column; |
|||
import jakarta.persistence.Entity; |
|||
import jakarta.persistence.Id; |
|||
import jakarta.persistence.Table; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.hibernate.annotations.Comment; |
|||
|
|||
/** |
|||
* 管理员白名单表 |
|||
*/ |
|||
@Data |
|||
@Entity |
|||
@Table(name = "evaluator_admin_white_list") |
|||
@TableName("evaluator_admin_white_list") |
|||
@EqualsAndHashCode(callSuper = true) |
|||
public class AdminWhiteListEntity extends BaseEntity { |
|||
|
|||
@TableId(value = "id" , type = IdType.ASSIGN_ID) |
|||
@Id |
|||
private Long id; |
|||
|
|||
@Column(name = "user_id", nullable = false) |
|||
@Comment("用户ID (关联evaluator_user的id)") |
|||
private Long userId; |
|||
|
|||
@Column(name = "user_name", length = 100) |
|||
@Comment("用户姓名 (冗余字段,方便管理后台直接查看)") |
|||
private String userName; |
|||
|
|||
@Column(name = "admin_type") |
|||
@Comment("管理员类型") |
|||
private Integer adminType = 0; |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
package com.project.ding.domain.service; |
|||
|
|||
import com.project.ding.domain.dto.LoginDTO; |
|||
|
|||
public interface AuthDomainService { |
|||
/** |
|||
* 钉钉免登逻辑 |
|||
*/ |
|||
LoginDTO loginByDing(String authCode); |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
package com.project.ding.domain.service.impl; |
|||
|
|||
import com.project.base.domain.exception.BusinessErrorException; |
|||
import com.project.ding.auth.DingTalkAuthenticationToken; |
|||
import com.project.ding.domain.dto.LoginDTO; |
|||
import com.project.ding.domain.entity.UserEntity; |
|||
import com.project.ding.domain.service.AuthDomainService; |
|||
import com.project.ding.utils.JwtUtils; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.security.authentication.AuthenticationManager; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.security.core.GrantedAuthority; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Service |
|||
@Slf4j |
|||
public class AuthDomainServiceImpl implements AuthDomainService { |
|||
|
|||
@Autowired |
|||
private AuthenticationManager authenticationManager; |
|||
|
|||
@Autowired |
|||
private JwtUtils jwtUtils; |
|||
|
|||
@Override |
|||
public LoginDTO loginByDing(String authCode) { |
|||
// 1. 构建未认证的内部 Token
|
|||
DingTalkAuthenticationToken authToken = new DingTalkAuthenticationToken(authCode); |
|||
|
|||
// 2. 委托 AuthenticationManager 进行认证
|
|||
// 这一步会进入你之前写的 DingTalkAuthenticationProvider
|
|||
Authentication authentication; |
|||
try { |
|||
authentication = authenticationManager.authenticate(authToken); |
|||
} catch (AuthenticationException e) { |
|||
log.warn("钉钉认证失败: {}", e.getMessage()); |
|||
throw new BusinessErrorException("认证失败:" + e.getMessage()); // 抛出自定义业务异常
|
|||
} |
|||
|
|||
// 3. 认证成功后,从安全上下文中提取用户信息和权限
|
|||
UserEntity user = (UserEntity) authentication.getPrincipal(); |
|||
List<String> roles = authentication.getAuthorities().stream() |
|||
.map(GrantedAuthority::getAuthority) |
|||
.toList(); |
|||
|
|||
// 4. 生成 JWT
|
|||
String token = jwtUtils.createToken(user.getId(), roles); |
|||
|
|||
// 5. 组装并返回结果
|
|||
return LoginDTO.builder() |
|||
.token(token) |
|||
.name(user.getName()) |
|||
.roles(roles) |
|||
.build(); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
package com.project.ding.mapper; |
|||
|
|||
|
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
import com.project.ding.domain.entity.AdminWhiteListEntity; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
@Mapper |
|||
public interface AdminWhiteListMapper extends BaseMapper<AdminWhiteListEntity> { |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
package com.project.ding.utils; |
|||
|
|||
import io.jsonwebtoken.Claims; |
|||
import io.jsonwebtoken.Jwts; |
|||
import io.jsonwebtoken.SignatureAlgorithm; |
|||
import io.jsonwebtoken.security.Keys; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.security.Key; |
|||
import java.util.Date; |
|||
import java.util.List; |
|||
|
|||
@Component |
|||
public class JwtUtils { |
|||
private static final Key KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256); |
|||
private static final long EXPIRE = 86400000; // 24小时
|
|||
|
|||
public String createToken(String userId, List<String> roles) { |
|||
return Jwts.builder() |
|||
.setSubject(userId) |
|||
.claim("roles", roles) |
|||
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) |
|||
.signWith(KEY).compact(); |
|||
} |
|||
|
|||
public Claims parseToken(String token) { |
|||
return Jwts.parserBuilder().setSigningKey(KEY).build().parseClaimsJws(token).getBody(); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
package com.project.information.application; |
|||
|
|||
import com.project.base.domain.result.Result; |
|||
import com.project.information.domain.param.FileCheckItem; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.util.List; |
|||
|
|||
public interface InformationApplicationService { |
|||
Result<String> checkDuplicates(Long subLineId, List<FileCheckItem> files) throws Exception; |
|||
|
|||
|
|||
Result<String> batchUploadAndOverwrite(MultipartFile[] files, Long subLineId) throws Exception; |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
package com.project.information.application.impl; |
|||
|
|||
import com.project.base.domain.result.Result; |
|||
import com.project.information.application.InformationApplicationService; |
|||
import com.project.information.domain.param.FileCheckItem; |
|||
import com.project.information.domain.service.UploadInformationDomainService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Service |
|||
public class InformationApplicationServiceImpl implements InformationApplicationService { |
|||
|
|||
@Autowired |
|||
private UploadInformationDomainService uploadInformationDomainService; |
|||
|
|||
@Override |
|||
public Result<String> checkDuplicates(Long subLineId, List<FileCheckItem> files) throws Exception { |
|||
return uploadInformationDomainService.checkDuplicates(subLineId , files); |
|||
} |
|||
|
|||
@Override |
|||
public Result<String> batchUploadAndOverwrite(MultipartFile[] files, Long subLineId) throws Exception { |
|||
return uploadInformationDomainService.batchUploadAndOverwrite(files , subLineId); |
|||
} |
|||
} |
|||
@ -1,13 +1,34 @@ |
|||
package com.project.information.controller; |
|||
|
|||
|
|||
import com.project.base.domain.result.Result; |
|||
import com.project.information.application.InformationApplicationService; |
|||
import com.project.information.application.ProductLineApplicationService; |
|||
import com.project.information.domain.dto.ProductLineDTO; |
|||
import com.project.information.domain.param.CheckDuplicatesParam; |
|||
import com.project.information.domain.param.FileCheckItem; |
|||
import com.project.information.domain.param.ProductLineParam; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.web.bind.annotation.*; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.util.List; |
|||
|
|||
@RestController |
|||
@Slf4j |
|||
@RequestMapping("/information") |
|||
@RequestMapping("/api/admin/information") |
|||
public class InformationController { |
|||
@Autowired |
|||
private InformationApplicationService informationApplicationService; |
|||
|
|||
@PostMapping("/checkDuplicates") |
|||
public Result<String> checkDuplicates(CheckDuplicatesParam param) throws Exception { |
|||
return informationApplicationService.checkDuplicates(param.getSubLineId(), param.getFileList()); |
|||
} |
|||
|
|||
@PostMapping("/batchUploadAndOverwrite") |
|||
public Result<String> batchUploadAndOverwrite(MultipartFile[] files, Long subLineId) throws Exception { |
|||
return informationApplicationService.batchUploadAndOverwrite(files , subLineId); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,15 @@ |
|||
package com.project.information.domain.enums; |
|||
|
|||
import com.project.base.domain.enums.HasValueEnum; |
|||
import lombok.Getter; |
|||
import lombok.RequiredArgsConstructor; |
|||
|
|||
@RequiredArgsConstructor |
|||
public enum InformationParseStatusEnum implements HasValueEnum<Integer> { |
|||
NotStart(0) , |
|||
InProgress(1) , |
|||
Success(2) , |
|||
Fail(-1); |
|||
@Getter |
|||
private final Integer value; |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
package com.project.information.domain.param; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Data |
|||
public class BatchCheckParam { |
|||
private Long productLineId; |
|||
private List<FileCheckItem> files; |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
package com.project.information.domain.param; |
|||
|
|||
|
|||
import com.fasterxml.jackson.core.JsonProcessingException; |
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import com.project.base.domain.exception.BusinessErrorException; |
|||
import lombok.Data; |
|||
import org.springframework.util.StringUtils; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
@Data |
|||
public class CheckDuplicatesParam { |
|||
private Long subLineId; |
|||
private String files; // 先接收为字符串
|
|||
|
|||
// 手动解析为List的方法
|
|||
public List<FileCheckItem> getFileList() { |
|||
if (StringUtils.isEmpty(files)) { |
|||
return Collections.emptyList(); |
|||
} |
|||
try { |
|||
return new ObjectMapper().readValue( |
|||
files, |
|||
new TypeReference<List<FileCheckItem>>() {}); |
|||
} catch (JsonProcessingException e) { |
|||
// 处理解析异常
|
|||
throw new BusinessErrorException("Json解析失败"); |
|||
} |
|||
}} |
|||
@ -0,0 +1,9 @@ |
|||
package com.project.information.domain.param; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class FileCheckItem { |
|||
private String name; |
|||
private Long size; // 单位:Byte
|
|||
} |
|||
@ -1,11 +1,14 @@ |
|||
package com.project.information.domain.service; |
|||
|
|||
import com.project.base.domain.result.Result; |
|||
import com.project.information.domain.dto.FileDTO; |
|||
import com.project.information.domain.dto.InformationDTO; |
|||
import com.project.information.domain.dto.InitMultipartDTO; |
|||
import com.project.information.domain.param.FileCheckItem; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.util.List; |
|||
|
|||
public interface UploadInformationDomainService { |
|||
Result<String> checkDuplicates(Long subLineId, List<FileCheckItem> files) throws Exception; |
|||
|
|||
|
|||
Result<InitMultipartDTO> initMultiPartUpload(InformationDTO dto) throws Exception; |
|||
Result<String> batchUploadAndOverwrite(MultipartFile[] files, Long subLineId) throws Exception; |
|||
} |
|||
|
|||
@ -1,28 +0,0 @@ |
|||
package com.project.information.domain.service; |
|||
|
|||
import com.project.information.domain.dto.StreamInfoDTO; |
|||
|
|||
import java.util.Map; |
|||
|
|||
public interface UploadService { |
|||
|
|||
/** |
|||
* 分片上传初始化 |
|||
* |
|||
* @param path 路径 |
|||
* @param filename 文件名 |
|||
* @param partCount 分片数量 |
|||
* @param contentType / |
|||
* @return / |
|||
*/ |
|||
Map<String, Object> initMultiPartUpload(String path, String filename, Integer partCount, String contentType); |
|||
|
|||
/** |
|||
* 完成分片上传 |
|||
* |
|||
* @param objectName 文件名 |
|||
* @param uploadId 标识 |
|||
* @return / |
|||
*/ |
|||
StreamInfoDTO mergeMultipartUpload(String objectName, String uploadId); |
|||
} |
|||
@ -1,56 +1,117 @@ |
|||
package com.project.information.domain.service.impl; |
|||
|
|||
import cn.hutool.core.bean.BeanUtil; |
|||
import cn.hutool.core.bean.copier.CopyOptions; |
|||
import cn.hutool.core.util.RandomUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.date.DateUtil; |
|||
import cn.hutool.core.io.file.FileNameUtil; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.project.base.domain.exception.MissingParameterException; |
|||
import com.project.base.domain.exception.BusinessErrorException; |
|||
import com.project.base.domain.result.Result; |
|||
import com.project.base.domain.result.ResultCodeEnum; |
|||
import com.project.information.domain.dto.FileDTO; |
|||
import com.project.information.domain.dto.InformationDTO; |
|||
import com.project.information.domain.dto.InitMultipartDTO; |
|||
import com.project.information.domain.entity.InformationEntity; |
|||
import com.project.information.domain.entity.ProductLineEntity; |
|||
import com.project.information.domain.enums.InformationParseStatusEnum; |
|||
import com.project.information.domain.param.FileCheckItem; |
|||
import com.project.information.domain.service.InformationBaseService; |
|||
import com.project.information.domain.service.ProductLineBaseService; |
|||
import com.project.information.domain.service.UploadInformationDomainService; |
|||
import com.project.information.domain.service.UploadService; |
|||
import com.project.information.utils.MinIoUtils; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.util.Map; |
|||
import java.util.ArrayList; |
|||
import java.util.Date; |
|||
import java.util.List; |
|||
import java.util.Objects; |
|||
|
|||
|
|||
@Service |
|||
public class UploadInformationDomainServiceImpl implements UploadInformationDomainService { |
|||
|
|||
@Autowired |
|||
private UploadService uploadService; |
|||
|
|||
@Autowired |
|||
private InformationBaseService informationBaseService; |
|||
@Autowired |
|||
private ProductLineBaseService productLineBaseService; |
|||
@Autowired |
|||
private MinIoUtils minIoUtils; |
|||
|
|||
|
|||
private static final long MAX_SIZE = 50 * 1024 * 1024; // 50MB 限制
|
|||
|
|||
@Override |
|||
public Result<InitMultipartDTO> initMultiPartUpload(InformationDTO dto) throws Exception { |
|||
if (StrUtil.isBlank(dto.getName()) || Objects.isNull(dto.getPartCount()) || Objects.isNull(dto.getSubLineId())) { |
|||
throw new MissingParameterException(ResultCodeEnum.MISSING_PARAMETER.getMessage()); |
|||
public Result<String> checkDuplicates(Long subLineId, List<FileCheckItem> files) throws Exception { |
|||
if (Objects.isNull(subLineId)) { |
|||
throw new BusinessErrorException("所属子产品线id不能为空"); |
|||
} |
|||
if (CollUtil.isEmpty(files)) { |
|||
throw new BusinessErrorException("上传文件集合不能为空"); |
|||
} |
|||
ProductLineEntity productLine = productLineBaseService.getById(subLineId); |
|||
if (Objects.isNull(productLine) || !productLine.getLeaf()) { |
|||
throw new BusinessErrorException("不存在的子产品线"); |
|||
} |
|||
String path = dto.getSubLineId().toString(); |
|||
List<String> overSizeList = files.stream() |
|||
.filter(f -> f.getSize() > MAX_SIZE) |
|||
.map(FileCheckItem::getName) |
|||
.toList(); |
|||
|
|||
LambdaQueryWrapper<InformationEntity> queryWrapper = new LambdaQueryWrapper<>(); |
|||
queryWrapper.eq(InformationEntity::getSubLineId , dto.getSubLineId()); |
|||
queryWrapper.eq(InformationEntity::getName , dto.getName()); |
|||
InformationEntity existOne = informationBaseService.getOne(queryWrapper); |
|||
if (Objects.nonNull(existOne)) { |
|||
if (CollUtil.isNotEmpty(overSizeList)) { |
|||
// 发现超大文件,直接中断,不查数据库
|
|||
throw new BusinessErrorException(String.format( |
|||
"以下文件超过50MB限制,禁止上传:【%s】", |
|||
String.join("、", overSizeList))); |
|||
} |
|||
List<String> fileNames = files.stream().map(FileCheckItem::getName).toList(); |
|||
|
|||
List<String> duplicatesList = informationBaseService.list(new LambdaQueryWrapper<InformationEntity>() |
|||
.select(InformationEntity::getName) |
|||
.eq(InformationEntity::getSubLineId, subLineId) |
|||
.in(InformationEntity::getName, fileNames)) |
|||
.stream() |
|||
.map(InformationEntity::getName) |
|||
.toList(); |
|||
if (CollUtil.isNotEmpty(duplicatesList)) { |
|||
throw new BusinessErrorException(String.format("你上传的文件集合中【%s】已存在,会进行覆盖操作,是否继续?" , String.join("," , duplicatesList))); |
|||
} |
|||
return Result.success("校验成功"); |
|||
} |
|||
|
|||
Map<String, Object> stringObjectMap = uploadService.initMultiPartUpload(path, dto.getName() , dto.getPartCount() , "application/octet-stream"); |
|||
InitMultipartDTO initMultipartDTO = BeanUtil.mapToBean(stringObjectMap, InitMultipartDTO.class, true, CopyOptions.create()); |
|||
initMultipartDTO.setObjectName(path + "/" + dto.getName()); |
|||
return Result.success(initMultipartDTO); |
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public Result<String> batchUploadAndOverwrite(MultipartFile[] files, Long subLineId) throws Exception { |
|||
if (Objects.isNull(subLineId)) { |
|||
throw new BusinessErrorException("所属子产品线id不能为空"); |
|||
} |
|||
if (Objects.isNull(files) || files.length == 0) { |
|||
throw new BusinessErrorException("上传文件集合不能为空"); |
|||
} |
|||
ProductLineEntity productLine = productLineBaseService.getById(subLineId); |
|||
if (Objects.isNull(productLine) || !productLine.getLeaf()) { |
|||
throw new BusinessErrorException("不存在的子产品线"); |
|||
} |
|||
List<String> successFiles = new ArrayList<>(); |
|||
for (MultipartFile file : files) { |
|||
String fileName = file.getOriginalFilename(); |
|||
// 删掉原来的
|
|||
informationBaseService.remove(new LambdaQueryWrapper<InformationEntity>() |
|||
.eq(InformationEntity::getSubLineId, subLineId) |
|||
.eq(InformationEntity::getName, fileName)); |
|||
String filePath = String.format("%s/%s/%s.%s", |
|||
subLineId, |
|||
DateUtil.format(new Date(), "yyyyMMdd"), |
|||
IdUtil.fastSimpleUUID(), |
|||
FileNameUtil.extName(fileName)); |
|||
minIoUtils.uploadFile(file.getInputStream() , filePath); |
|||
successFiles.add(fileName); |
|||
InformationDTO informationDTO = new InformationDTO(); |
|||
informationDTO.setSubLineId(subLineId); |
|||
informationDTO.setSubLineName(productLine.getName()); |
|||
informationDTO.setName(fileName); |
|||
informationDTO.setFilePath(filePath); |
|||
informationDTO.setParseStatus(InformationParseStatusEnum.NotStart.getValue()); |
|||
informationBaseService.save(informationDTO.toEntity(InformationEntity::new)); |
|||
} |
|||
return Result.success(String.format("上传成功:【%s】" , String.join("," , successFiles))); |
|||
} |
|||
} |
|||
|
|||
@ -1,43 +0,0 @@ |
|||
package com.project.information.domain.service.impl; |
|||
|
|||
import com.google.common.collect.ImmutableList; |
|||
import com.google.common.collect.ImmutableMap; |
|||
import com.project.information.domain.dto.StreamInfoDTO; |
|||
import com.project.information.domain.service.UploadService; |
|||
import com.project.information.utils.MinIoUtils; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.Map; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class UploadServiceImpl implements UploadService { |
|||
|
|||
private final MinIoUtils minIoUtils; |
|||
|
|||
@Override |
|||
public Map<String, Object> initMultiPartUpload(String path, String filename, Integer partCount, String contentType) { |
|||
path = path.replaceAll("/+", "/"); |
|||
if (path.indexOf("/") == 0) { |
|||
path = path.substring(1); |
|||
} |
|||
String filePath = path + "/" + filename; |
|||
|
|||
Map<String, Object> result; |
|||
// 单文件,直接上传
|
|||
if (partCount == 1) { |
|||
String uploadObjectUrl = minIoUtils.getUploadObjectUrl(filePath); |
|||
result = ImmutableMap.of("uploadUrls", ImmutableList.of(uploadObjectUrl)); |
|||
} else {//多文件,分片上传
|
|||
result = minIoUtils.initMultiPartUpload(filePath, partCount, contentType); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
@Override |
|||
public StreamInfoDTO mergeMultipartUpload(String objectName, String uploadId) { |
|||
return minIoUtils.mergeMultipartUpload(objectName, uploadId); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue