diff --git a/backend/src/main/java/com/qqchen/deploy/backend/api/TenantApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/api/TenantApiController.java index a1b75141..31efceba 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/api/TenantApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/api/TenantApiController.java @@ -1,33 +1,36 @@ -//package com.qqchen.deploy.backend.api; -// -//import com.qqchen.deploy.backend.framework.api.Response; -//import com.qqchen.deploy.backend.framework.controller.BaseController; -//import com.qqchen.deploy.backend.converter.TenantConverter; -//import com.qqchen.deploy.backend.dto.query.TenantQuery; -//import com.qqchen.deploy.backend.dto.request.TenantRequest; -//import com.qqchen.deploy.backend.dto.response.TenantResponse; -//import com.qqchen.deploy.backend.entity.Tenant; -//import com.qqchen.deploy.backend.service.ITenantService; -//import org.springframework.web.bind.annotation.GetMapping; -//import org.springframework.web.bind.annotation.PathVariable; -//import org.springframework.web.bind.annotation.RequestMapping; -//import org.springframework.web.bind.annotation.RestController; -// -//@RestController -//@RequestMapping("/api/v1/tenant") -//public class TenantApiController extends BaseController { -// -// protected final ITenantService service; -// -// public TenantApiController(ITenantService service, TenantConverter converter) { -// super(service, converter); -// this.service = service; -// } -// +package com.qqchen.deploy.backend.api; + +import com.qqchen.deploy.backend.dto.TenantDTO; +import com.qqchen.deploy.backend.framework.controller.BaseController; +import com.qqchen.deploy.backend.dto.query.TenantQuery; +import com.qqchen.deploy.backend.entity.Tenant; +import com.qqchen.deploy.backend.service.ITenantService; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/tenant") +public class TenantApiController extends BaseController { + + protected final ITenantService service; + + public TenantApiController(ITenantService service) { + super(service); + this.service = service; + } + + @Override + protected void exportData(HttpServletResponse response, List data) { + + } + // @GetMapping("/code/{code}") // public Response findByCode(@PathVariable String code) { // Tenant tenant = service.findByCode(code); // return Response.success(converter.toResponse(tenant)); // } -// -//} \ No newline at end of file + +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/dto/response/TenantResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/dto/response/TenantResponse.java new file mode 100644 index 00000000..e7aed94d --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/dto/response/TenantResponse.java @@ -0,0 +1,27 @@ +package com.qqchen.deploy.backend.dto.response; + +import com.qqchen.deploy.backend.framework.dto.BaseResponse; +import lombok.Data; +import lombok.EqualsAndHashCode; + + +@Data +@EqualsAndHashCode(callSuper = true) +public class TenantResponse extends BaseResponse { + + private Long id; + + private String name; + + private String code; + + private String contactName; + + private String contactPhone; + + private String email; + + private String address; + + private Boolean enabled = true; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/AuditMetadata.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/AuditMetadata.java new file mode 100644 index 00000000..64f559ce --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/AuditMetadata.java @@ -0,0 +1,16 @@ +package com.qqchen.deploy.backend.framework.audit; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class AuditMetadata { + private String operator; // 操作人 + private String action; // 操作类型(CREATE/UPDATE/DELETE) + private String entityType; // 实体类型 + private String entityId; // 实体ID + private String detail; // 操作详情 + private LocalDateTime timestamp; // 操作时间 + private String ip; // 操作IP + private String tenantId; // 租户ID +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/annotation/Audited.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/annotation/Audited.java new file mode 100644 index 00000000..04963393 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/annotation/Audited.java @@ -0,0 +1,11 @@ +package com.qqchen.deploy.backend.framework.audit.annotation; + +import java.lang.annotation.*; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Audited { + String action() default ""; + String detail() default ""; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/aspect/AuditAspect.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/aspect/AuditAspect.java new file mode 100644 index 00000000..356e7eed --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/aspect/AuditAspect.java @@ -0,0 +1,90 @@ +package com.qqchen.deploy.backend.framework.audit.aspect; + +import com.qqchen.deploy.backend.framework.audit.AuditMetadata; +import com.qqchen.deploy.backend.framework.audit.annotation.Audited; +import com.qqchen.deploy.backend.framework.audit.event.AuditEvent; +import com.qqchen.deploy.backend.framework.security.SecurityUtils; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.time.LocalDateTime; + +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class AuditAspect { + + private final ApplicationEventPublisher eventPublisher; + + @Around("@annotation(com.qqchen.deploy.backend.framework.audit.annotation.Audited)") + public Object auditMethod(ProceedingJoinPoint joinPoint) throws Throwable { + // 获取审计元数据 + AuditMetadata metadata = extractAuditMetadata(joinPoint); + + try { + // 执行原方法 + Object result = joinPoint.proceed(); + + // 发布审计事件 + eventPublisher.publishEvent(new AuditEvent(this, metadata)); + + return result; + } catch (Exception e) { + // 记录异常信息 + metadata.setDetail(metadata.getDetail() + " [Error: " + e.getMessage() + "]"); + eventPublisher.publishEvent(new AuditEvent(this, metadata)); + throw e; + } + } + + private AuditMetadata extractAuditMetadata(ProceedingJoinPoint joinPoint) { + AuditMetadata metadata = new AuditMetadata(); + + // 设置基本信息 + metadata.setOperator(SecurityUtils.getCurrentUsername()); + metadata.setTimestamp(LocalDateTime.now()); +// metadata.setTenantId(SecurityUtils.getCurrentTenantId()); + + // 设置IP地址 + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder + .getRequestAttributes()).getRequest(); + metadata.setIp(getClientIp(request)); + + // 获取方法上的注解信息 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Audited audited = signature.getMethod().getAnnotation(Audited.class); + + metadata.setAction(audited.action()); + metadata.setDetail(audited.detail()); + + // 设置实体信息 + String className = joinPoint.getTarget().getClass().getSimpleName(); + metadata.setEntityType(className.replace("ServiceImpl", "")); + + return metadata; + } + + private String getClientIp(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/event/AuditEvent.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/event/AuditEvent.java new file mode 100644 index 00000000..28a8fe5a --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/event/AuditEvent.java @@ -0,0 +1,15 @@ +package com.qqchen.deploy.backend.framework.audit.event; + +import com.qqchen.deploy.backend.framework.audit.AuditMetadata; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class AuditEvent extends ApplicationEvent { + private final AuditMetadata metadata; + + public AuditEvent(Object source, AuditMetadata metadata) { + super(source); + this.metadata = metadata; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/listener/AuditEventListener.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/listener/AuditEventListener.java new file mode 100644 index 00000000..21c4b1c1 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/audit/listener/AuditEventListener.java @@ -0,0 +1,26 @@ +package com.qqchen.deploy.backend.framework.audit.listener; + +import com.qqchen.deploy.backend.framework.audit.event.AuditEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class AuditEventListener { + + @Async + @EventListener + public void handleAuditEvent(AuditEvent event) { + // 这里可以将审计信息保存到数据库或发送到日志系统 + log.info("Audit: {} performed {} on {} (ID: {}) at {}, detail: {}", + event.getMetadata().getOperator(), + event.getMetadata().getAction(), + event.getMetadata().getEntityType(), + event.getMetadata().getEntityId(), + event.getMetadata().getTimestamp(), + event.getMetadata().getDetail() + ); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/repository/IUserRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/repository/IUserRepository.java index 725ee28e..d82b2126 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/repository/IUserRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/repository/IUserRepository.java @@ -10,8 +10,4 @@ import java.util.Optional; public interface IUserRepository extends IBaseRepository { Optional findByUsernameAndDeletedFalse(String username); - - boolean existsByUsername(String username); - - boolean existsByEmail(String email); -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java b/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java index a37fe5e7..c5c37655 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java @@ -1,12 +1,14 @@ package com.qqchen.deploy.backend.service; import com.qqchen.deploy.backend.dto.UserDTO; +import com.qqchen.deploy.backend.framework.audit.annotation.Audited; import com.qqchen.deploy.backend.framework.service.IBaseService; import com.qqchen.deploy.backend.dto.request.LoginRequest; import com.qqchen.deploy.backend.dto.response.LoginResponse; import com.qqchen.deploy.backend.entity.User; import com.qqchen.deploy.backend.dto.request.UserRequest; import com.qqchen.deploy.backend.dto.RoleDTO; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -17,4 +19,8 @@ public interface IUserService extends IBaseService { boolean checkEmailExists(String email); List getUserRoles(Long userId); List getCurrentUserRoles(); + + @Transactional(readOnly = false) + @Audited(action = "CHANGE_PASSWORD", detail = "修改密码") + void changePassword(Long userId, String oldPassword, String newPassword); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/TenantServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/TenantServiceImpl.java index 35ba8177..71754882 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/TenantServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/TenantServiceImpl.java @@ -18,8 +18,8 @@ public class TenantServiceImpl extends BaseServiceImpl super(repository, converter); } - @Override - public List findAll(BaseQuery query) { - return null; - } +// @Override +// public List findAll(BaseQuery query) { +// return null; +// } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java index 54272dd9..9b6d24c2 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java @@ -1,9 +1,8 @@ package com.qqchen.deploy.backend.service.impl; +import com.qqchen.deploy.backend.framework.audit.annotation.Audited; import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.exception.BusinessException; -import com.qqchen.deploy.backend.framework.query.BaseQuery; -import com.qqchen.deploy.backend.framework.security.SecurityUtils; import com.qqchen.deploy.backend.framework.security.util.JwtTokenUtil; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.dto.request.LoginRequest; @@ -38,6 +37,9 @@ public class UserServiceImpl extends BaseServiceImpl implem @Resource private AuthenticationManager authenticationManager; + + @Resource + private IUserRepository userRepository; @Resource private JwtTokenUtil jwtTokenUtil; @@ -50,6 +52,7 @@ public class UserServiceImpl extends BaseServiceImpl implem @Override @Transactional(readOnly = false) + @Audited(action = "REGISTER", detail = "用户注册") public UserDTO register(UserRequest request) { if (checkUsernameExists(request.getUsername())) { throw new BusinessException(ResponseCode.USERNAME_EXISTS); @@ -63,7 +66,6 @@ public class UserServiceImpl extends BaseServiceImpl implem userDTO.setEmail(request.getEmail()); userDTO.setNickname(request.getNickname()); userDTO.setPhone(request.getPhone()); - return create(userDTO); } @@ -79,28 +81,28 @@ public class UserServiceImpl extends BaseServiceImpl implem @Override @Transactional(readOnly = true) + @Audited(action = "USER_LOGIN", detail = "登录") public LoginResponse login(LoginRequest request) { -// Authentication authentication = authenticationManager.authenticate( -// new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) -// ); -// -// UserDetails userDetails = (UserDetails) authentication.getPrincipal(); -// String token = jwtTokenUtil.generateToken(userDetails); -// -// User user = repository.findByUsernameAndDeletedFalse(userDetails.getUsername()) -// .orElseThrow(() -> new BusinessException(ResponseCode.USER_NOT_FOUND)); -// -// LoginResponse response = new LoginResponse(); -// response.setId(user.getId()); -// response.setUsername(user.getUsername()); -// response.setNickname(user.getNickname()); -// response.setEmail(user.getEmail()); -// response.setPhone(user.getPhone()); -// response.setToken(token); -// -// log.info("用户 {} ({}) 登录成功", user.getUsername(), user.getNickname()); -// return response; - return null; + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) + ); + + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + String token = jwtTokenUtil.generateToken(userDetails); + + User user = userRepository.findByUsernameAndDeletedFalse(userDetails.getUsername()) + .orElseThrow(() -> new BusinessException(ResponseCode.USER_NOT_FOUND)); + + LoginResponse response = new LoginResponse(); + response.setId(user.getId()); + response.setUsername(user.getUsername()); + response.setNickname(user.getNickname()); + response.setEmail(user.getEmail()); + response.setPhone(user.getPhone()); + response.setToken(token); + + log.info("用户 {} ({}) 登录成功", user.getUsername(), user.getNickname()); + return response; } @Override @@ -126,4 +128,11 @@ public class UserServiceImpl extends BaseServiceImpl implem // public List findAll(BaseQuery query) { // return null; // } + + @Override + @Transactional(readOnly = false) + @Audited(action = "CHANGE_PASSWORD", detail = "修改密码") + public void changePassword(Long userId, String oldPassword, String newPassword) { + // ... 密码修改逻辑 ... + } } \ No newline at end of file