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; |
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 lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.web.bind.annotation.RestController; |
import org.springframework.web.bind.annotation.*; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
@RestController |
@RestController |
||||
@Slf4j |
@Slf4j |
||||
@RequestMapping("/information") |
@RequestMapping("/api/admin/information") |
||||
public class InformationController { |
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; |
package com.project.information.domain.service; |
||||
|
|
||||
import com.project.base.domain.result.Result; |
import com.project.base.domain.result.Result; |
||||
import com.project.information.domain.dto.FileDTO; |
import com.project.information.domain.param.FileCheckItem; |
||||
import com.project.information.domain.dto.InformationDTO; |
import org.springframework.web.multipart.MultipartFile; |
||||
import com.project.information.domain.dto.InitMultipartDTO; |
|
||||
|
import java.util.List; |
||||
|
|
||||
public interface UploadInformationDomainService { |
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; |
package com.project.information.domain.service.impl; |
||||
|
|
||||
import cn.hutool.core.bean.BeanUtil; |
import cn.hutool.core.collection.CollUtil; |
||||
import cn.hutool.core.bean.copier.CopyOptions; |
import cn.hutool.core.date.DateUtil; |
||||
import cn.hutool.core.util.RandomUtil; |
import cn.hutool.core.io.file.FileNameUtil; |
||||
import cn.hutool.core.util.StrUtil; |
import cn.hutool.core.util.IdUtil; |
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
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.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.InformationDTO; |
||||
import com.project.information.domain.dto.InitMultipartDTO; |
|
||||
import com.project.information.domain.entity.InformationEntity; |
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.InformationBaseService; |
||||
|
import com.project.information.domain.service.ProductLineBaseService; |
||||
import com.project.information.domain.service.UploadInformationDomainService; |
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.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
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; |
import java.util.Objects; |
||||
|
|
||||
|
|
||||
@Service |
@Service |
||||
public class UploadInformationDomainServiceImpl implements UploadInformationDomainService { |
public class UploadInformationDomainServiceImpl implements UploadInformationDomainService { |
||||
|
|
||||
@Autowired |
|
||||
private UploadService uploadService; |
|
||||
|
|
||||
@Autowired |
@Autowired |
||||
private InformationBaseService informationBaseService; |
private InformationBaseService informationBaseService; |
||||
|
@Autowired |
||||
|
private ProductLineBaseService productLineBaseService; |
||||
|
@Autowired |
||||
|
private MinIoUtils minIoUtils; |
||||
|
|
||||
|
private static final long MAX_SIZE = 50 * 1024 * 1024; // 50MB 限制
|
||||
|
|
||||
@Override |
@Override |
||||
public Result<InitMultipartDTO> initMultiPartUpload(InformationDTO dto) throws Exception { |
public Result<String> checkDuplicates(Long subLineId, List<FileCheckItem> files) throws Exception { |
||||
if (StrUtil.isBlank(dto.getName()) || Objects.isNull(dto.getPartCount()) || Objects.isNull(dto.getSubLineId())) { |
if (Objects.isNull(subLineId)) { |
||||
throw new MissingParameterException(ResultCodeEnum.MISSING_PARAMETER.getMessage()); |
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<>(); |
if (CollUtil.isNotEmpty(overSizeList)) { |
||||
queryWrapper.eq(InformationEntity::getSubLineId , dto.getSubLineId()); |
// 发现超大文件,直接中断,不查数据库
|
||||
queryWrapper.eq(InformationEntity::getName , dto.getName()); |
throw new BusinessErrorException(String.format( |
||||
InformationEntity existOne = informationBaseService.getOne(queryWrapper); |
"以下文件超过50MB限制,禁止上传:【%s】", |
||||
if (Objects.nonNull(existOne)) { |
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"); |
@Override |
||||
InitMultipartDTO initMultipartDTO = BeanUtil.mapToBean(stringObjectMap, InitMultipartDTO.class, true, CopyOptions.create()); |
@Transactional(rollbackFor = Exception.class) |
||||
initMultipartDTO.setObjectName(path + "/" + dto.getName()); |
public Result<String> batchUploadAndOverwrite(MultipartFile[] files, Long subLineId) throws Exception { |
||||
return Result.success(initMultipartDTO); |
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