diff --git a/src/main/java/com/yunhai/bschclassmanage/Repository/SyncLogRepository.java b/src/main/java/com/yunhai/bschclassmanage/Repository/SyncLogRepository.java new file mode 100644 index 0000000..1773698 --- /dev/null +++ b/src/main/java/com/yunhai/bschclassmanage/Repository/SyncLogRepository.java @@ -0,0 +1,9 @@ +package com.yunhai.bschclassmanage.Repository; + +import com.yunhai.bschclassmanage.entity.SyncLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SyncLogRepository extends JpaRepository { +} diff --git a/src/main/java/com/yunhai/bschclassmanage/annotation/SyncOpLog.java b/src/main/java/com/yunhai/bschclassmanage/annotation/SyncOpLog.java new file mode 100644 index 0000000..2c22d26 --- /dev/null +++ b/src/main/java/com/yunhai/bschclassmanage/annotation/SyncOpLog.java @@ -0,0 +1,19 @@ +package com.yunhai.bschclassmanage.annotation; + +import com.yunhai.bschclassmanage.enums.SyncDataType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 标记一个方法需要记录"用户同步调用日志"。 + * 由 SyncLogAspect 拦截,按 dataType 写入 sync_log 表。 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SyncOpLog { + /** 该操作对应的同步数据类型枚举(作为默认的 value 元素,可用 @SyncOpLog(SyncDataType.X) 简写) */ + SyncDataType value(); +} diff --git a/src/main/java/com/yunhai/bschclassmanage/aspect/SyncLogAspect.java b/src/main/java/com/yunhai/bschclassmanage/aspect/SyncLogAspect.java new file mode 100644 index 0000000..d66e69a --- /dev/null +++ b/src/main/java/com/yunhai/bschclassmanage/aspect/SyncLogAspect.java @@ -0,0 +1,99 @@ +package com.yunhai.bschclassmanage.aspect; + +import com.alibaba.fastjson.JSON; +import com.yunhai.bschclassmanage.annotation.SyncOpLog; +import com.yunhai.bschclassmanage.entity.SyncLog; +import com.yunhai.bschclassmanage.enums.SyncDataType; +import com.yunhai.bschclassmanage.service.ISyncLogService; +import com.yunhai.bschclassmanage.utils.OtherUtil; +import com.yunhai.bschclassmanage.utils.SensitiveDataUtil; +import com.yunhai.bschclassmanage.utils.UserContext; +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.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 同步调用日志切面。 + * 拦截所有标注 @SyncLog 的方法(当前仅 SyncUserBaseService), + * 统一采集:枚举类型 / 大类(Sync-Send) / 类名.方法名 / 操作人 / 学校 / IP / 脱敏入参 / 异常, + * 并在 finally 中异步落库(不影响主流程、异常照常抛出)。 + */ +@Slf4j +@Aspect +@Component +@Order(10) +public class SyncLogAspect { + + private static final int MAX_ERROR_MSG = 2000; + + @Autowired + private ISyncLogService syncLogService; + + @Around("@annotation(syncLog)") + public Object around(ProceedingJoinPoint pjp,SyncOpLog syncLog) throws Throwable { + SyncDataType dataType = syncLog.value(); + String classMethod = pjp.getTarget().getClass().getName() + "." + pjp.getSignature().getName(); + String jsonData = buildJsonData(pjp); + + SyncLog entity = new SyncLog(); + // 枚举类型 key/name + entity.setSyncDataTypeKey(dataType.getKey()); + entity.setSyncDataTypeName(dataType.getName()); + // 大类:Send / Sync(由 key 前缀推导) + boolean isSend = dataType.getKey() != null && dataType.getKey().startsWith("Send"); + entity.setSyncType(isSend ? "Send" : "Sync"); + entity.setSyncTypeKey(isSend ? "Send" : "Sync"); + entity.setSyncTypeName(isSend ? "推送" : "同步"); + + entity.setClassMethod(classMethod); + entity.setUserId(UserContext.getUserId()); + entity.setUserName(UserContext.getUserName()); + entity.setSchId(UserContext.getSchId()); + entity.setRequestIp(StringUtils.hasText(UserContext.getIp()) + ? UserContext.getIp() : OtherUtil.getClientIp()); + entity.setJsonData(jsonData); + + Object result; + try { + result = pjp.proceed(); + } catch (Throwable t) { + entity.setErrorMsg(truncate(t.getMessage(), MAX_ERROR_MSG)); + syncLogService.saveAsync(entity); + throw t; + } + syncLogService.saveAsync(entity); + return result; + } + + /** + * 将方法参数按"参数名->值"序列化为 JSON,并用 SensitiveDataUtil 脱敏 pwd/idCard/phone 等。 + */ + private String buildJsonData(ProceedingJoinPoint pjp) { + try { + MethodSignature ms = (MethodSignature) pjp.getSignature(); + String[] names = ms.getParameterNames(); + Object[] args = pjp.getArgs(); + if (names == null || args == null) return ""; + Map map = new LinkedHashMap<>(); + for (int i = 0; i < args.length && i < names.length; i++) { + map.put(names[i], args[i]); + } + return SensitiveDataUtil.mask(JSON.toJSONString(map)); + } catch (Exception e) { + return ""; + } + } + + private String truncate(String s, int max) { + return s != null && s.length() > max ? s.substring(0, max) : s; + } +} diff --git a/src/main/java/com/yunhai/bschclassmanage/entity/SyncLog.java b/src/main/java/com/yunhai/bschclassmanage/entity/SyncLog.java new file mode 100644 index 0000000..062e3a8 --- /dev/null +++ b/src/main/java/com/yunhai/bschclassmanage/entity/SyncLog.java @@ -0,0 +1,80 @@ +package com.yunhai.bschclassmanage.entity; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import javax.persistence.*; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 用户同步调用日志实体。 + * 专用于记录 SyncUserBaseService 对基础平台 OpenAPI 的同步/推送调用, + * 与 operation_log(业务日志)、sys_operation_log(审计日志)物理隔离。 + * 索引由 sqlscript/sync_log.sql 负责创建,本实体不含 @Table(indexes), + * 避免 ddl-auto=update 启动时重复建索引告警。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sync_log") +public class SyncLog implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** SyncDataType.key,如 Sync-AddGroup */ + @Column(name = "sync_data_type_key", length = 64) + private String syncDataTypeKey; + + /** SyncDataType.name,如 添加组 */ + @Column(name = "sync_data_type_name", length = 64) + private String syncDataTypeName; + + /** 大类:Sync / Send */ + @Column(name = "sync_type", nullable = false, length = 16) + private String syncType; + + /** 大类 key:Sync / Send */ + @Column(name = "sync_type_key", nullable = false, length = 16) + private String syncTypeKey; + + /** 大类名称:同步 / 推送 */ + @Column(name = "sync_type_name", length = 32) + private String syncTypeName; + + /** 类名.方法名 */ + @Column(name = "class_method", nullable = false, length = 512) + private String classMethod; + + /** 操作人ID(来自 UserContext) */ + @Column(name = "user_id") + private Integer userId; + + /** 操作人姓名(来自 UserContext) */ + @Column(name = "user_name", length = 64) + private String userName; + + /** 学校ID(多校隔离,来自 UserContext) */ + @Column(name = "sch_id") + private Integer schId; + + /** 客户端IP */ + @Column(name = "request_ip", length = 64) + private String requestIp; + + /** 请求入参(参数名->值,已脱敏),TEXT */ + @Column(name = "json_data", columnDefinition = "TEXT") + private String jsonData; + + /** 异常信息,成功为 NULL */ + @Column(name = "error_msg", columnDefinition = "TEXT") + private String errorMsg; + + @CreationTimestamp + @Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)") + private LocalDateTime createTime; +} diff --git a/src/main/java/com/yunhai/bschclassmanage/service/ISyncLogService.java b/src/main/java/com/yunhai/bschclassmanage/service/ISyncLogService.java new file mode 100644 index 0000000..ba4b0c0 --- /dev/null +++ b/src/main/java/com/yunhai/bschclassmanage/service/ISyncLogService.java @@ -0,0 +1,11 @@ +package com.yunhai.bschclassmanage.service; + +import com.yunhai.bschclassmanage.entity.SyncLog; + +public interface ISyncLogService { + /** + * 异步保存一条同步调用日志。 + * 由 SyncLogAspect 在 finally 中调用,失败仅记日志、不抛异常、不阻塞主流程。 + */ + void saveAsync(SyncLog entity); +} diff --git a/src/main/java/com/yunhai/bschclassmanage/service/impl/SyncLogServiceImpl.java b/src/main/java/com/yunhai/bschclassmanage/service/impl/SyncLogServiceImpl.java new file mode 100644 index 0000000..e9d6d60 --- /dev/null +++ b/src/main/java/com/yunhai/bschclassmanage/service/impl/SyncLogServiceImpl.java @@ -0,0 +1,27 @@ +package com.yunhai.bschclassmanage.service.impl; + +import com.yunhai.bschclassmanage.Repository.SyncLogRepository; +import com.yunhai.bschclassmanage.entity.SyncLog; +import com.yunhai.bschclassmanage.service.ISyncLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class SyncLogServiceImpl implements ISyncLogService { + + @Autowired + private SyncLogRepository repository; + + @Async("logExecutor") + @Override + public void saveAsync(SyncLog entity) { + try { + repository.save(entity); + } catch (Exception e) { + log.error("同步日志异步落库失败, classMethod={}", entity.getClassMethod(), e); + } + } +}