Explorar o código

审计日志调整

liuchengsen hai 1 mes
pai
achega
5c06354ede

+ 1 - 1
zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteConfigService.java

@@ -101,7 +101,7 @@ public class InviteConfigService {
      */
     public Page<com.xuekairui.user.entity.OperationAuditLog> getConfigHistory(
             int pageNum, int pageSize) {
-        return auditLogService.getTargetOperationHistory("INVITE", null, pageNum, pageSize);
+        return auditLogService.getTargetOperationHistory("INVITE", null, null, pageNum, pageSize);
     }
 
     /**

+ 18 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/controller/OperationAuditLogController.java

@@ -22,6 +22,9 @@ public class OperationAuditLogController {
 
     private final OperationAuditLogService auditLogService;
 
+    /**
+     * 分页查询审计日志列表(不含大字段快照)
+     */
     @GetMapping
     public Result<Page<OperationAuditLog>> queryLogs(
             @RequestParam(required = false) String module,
@@ -38,6 +41,18 @@ public class OperationAuditLogController {
                 operationSource, page, size));
     }
 
+    /**
+     * 根据ID查询审计日志详情(含完整快照 beforeData/afterData)
+     */
+    @GetMapping("/{id}")
+    public Result<OperationAuditLog> getDetail(@PathVariable Long id) {
+        OperationAuditLog log = auditLogService.getAuditLogById(id);
+        if (log == null) {
+            return Result.error(404, "审计日志不存在");
+        }
+        return Result.success(log);
+    }
+
     @GetMapping("/sensitive")
     public Result<List<OperationAuditLog>> getSensitiveLogs(
             @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime) {
@@ -55,9 +70,11 @@ public class OperationAuditLogController {
     public Result<Page<OperationAuditLog>> getTargetHistory(
             @PathVariable String targetType,
             @PathVariable Long targetId,
+            @RequestParam(required = false) String operationSource,
             @RequestParam(defaultValue = "1") int page,
             @RequestParam(defaultValue = "20") int size) {
-        return Result.success(auditLogService.getTargetOperationHistory(targetType, targetId, page, size));
+        return Result.success(auditLogService.getTargetOperationHistory(
+                targetType, targetId, operationSource, page, size));
     }
 
     @GetMapping("/stats")

+ 180 - 64
zhijiayun-user/src/main/java/com/xuekairui/user/service/BusinessLicenseService.java

@@ -14,6 +14,7 @@ import com.xuekairui.user.enums.MembershipLevel;
 import com.xuekairui.user.enums.OperationSource;
 import com.xuekairui.user.enums.OperationType;
 import com.xuekairui.user.enums.OperatorRole;
+import com.xuekairui.user.mapper.AdminMapper;
 import com.xuekairui.user.mapper.BusinessLicenseMapper;
 import com.xuekairui.user.mapper.UserMapper;
 import lombok.RequiredArgsConstructor;
@@ -48,6 +49,7 @@ public class BusinessLicenseService {
     private final ActivityEventRedisService activityEventRedisService;
     private final OperationAuditLogService auditLogService;
     private final UserMapper userMapper;
+    private final AdminMapper adminMapper;
     private final ShopResourceSyncService shopResourceSyncService;
     private final com.xuekairui.user.mapper.LicenseSyncDiffMapper licenseSyncDiffMapper;
 
@@ -129,33 +131,49 @@ public class BusinessLicenseService {
             throw new BusinessException(ErrorCode.PARAM_ERROR, "入驻信息缺少提交用户ID,无法写入");
         }
         businessLicenseMapper.insert(license);
+
+        // 审计日志:系统同步新增入驻信息
+        String operatorName = OperatorRole.SYSTEM.getDescription();
+        Map<String, Object> afterData = buildSnapshot(license);
+        auditLogService.logSuccess(
+                0L,
+                operatorName,
+                OperatorRole.SYSTEM.getCode(),
+                OperationType.LICENSE_CREATE,
+                "LICENSE:" + license.getId(),
+                license.getId(),
+                null,
+                afterData,
+                String.format("系统同步新增入驻信息(药店:%s)",
+                        license.getStoreName() != null ? license.getStoreName() : "未知药店"),
+                OperationSource.SYSTEM_SYNC);
     }
 
     /**
      * 更新入驻信息(通用方法,供同步等场景使用)
      */
     public void updateById(BusinessLicense license) {
-        businessLicenseMapper.updateById(license);
-    }
+        // 变更前快照(在修改前查询旧数据)
+        BusinessLicense oldLicense = getById(license.getId());
+        Map<String, Object> beforeData = oldLicense != null ? buildSnapshot(oldLicense) : null;
 
-    /**
-     * 检查用户是否已有其他审核通过的入驻信息(避免重复赠送会员)
-     */
-    private boolean hasOtherApprovedLicense(Long userId, Long excludeLicenseId) {
-        Long count = businessLicenseMapper.selectCount(
-                new LambdaQueryWrapper<BusinessLicense>()
-                        .eq(BusinessLicense::getUserId, userId)
-                        .eq(BusinessLicense::getReviewStatus, LicenseStatus.APPROVED.name())
-                        .ne(BusinessLicense::getId, excludeLicenseId));
-        return count != null && count > 0;
-    }
+        businessLicenseMapper.updateById(license);
 
-    /**
-     * 检查用户是否已有审核通过的入驻信息(排除指定记录)
-     * <p>供 AuthService 在第三方同步时做幂等性检查,避免重复赠送高级会员导致时间累加。
-     */
-    public boolean hasOtherApprovedLicensePublic(Long userId, Long excludeLicenseId) {
-        return hasOtherApprovedLicense(userId, excludeLicenseId);
+        // 审计日志:系统同步更新入驻信息
+        String operatorName = OperatorRole.SYSTEM.getDescription();
+        Map<String, Object> afterData = buildSnapshot(license);
+        auditLogService.logSuccess(
+                0L,
+                operatorName,
+                OperatorRole.SYSTEM.getCode(),
+                OperationType.LICENSE_EDIT,
+                "LICENSE:" + license.getId(),
+                license.getId(),
+                beforeData,
+                afterData,
+                String.format("系统同步更新入驻信息(药店:%s)",
+                        license.getStoreName() != null ? license.getStoreName() : "未知药店"),
+                OperationSource.SYSTEM_SYNC);
     }
 
     /**
@@ -170,6 +188,27 @@ public class BusinessLicenseService {
         return businessLicenseMapper.markRewardGranted(licenseId);
     }
 
+    /** 获取管理员用户名(用于审计日志操作人) */
+    private String getAdminName(Long adminId) {
+        if (adminId == null) {
+            return "管理员";
+        }
+        com.xuekairui.user.entity.Admin admin = adminMapper.selectById(adminId);
+        return admin != null && admin.getUsername() != null ? admin.getUsername() : "管理员";
+    }
+
+    /** 获取用户展示名(优先昵称,兜底用户ID) */
+    private String getUserDisplayName(Long userId) {
+        if (userId == null) {
+            return "未知用户";
+        }
+        User user = userMapper.selectById(userId);
+        if (user != null && user.getNickname() != null && !user.getNickname().isBlank()) {
+            return user.getNickname();
+        }
+        return String.valueOf(userId);
+    }
+
     // ======================== 用户侧:提交入驻信息 ========================
 
     /**
@@ -179,7 +218,11 @@ public class BusinessLicenseService {
     public void saveOrUpdate(Long userId, BusinessLicenseSaveRequest request) {
         BusinessLicense existing = getByUserId(userId);
         final BusinessLicense targetLicense;
+        final Map<String, Object> beforeData;
+        final OperationType opType;
         if (existing == null) {
+            beforeData = null;
+            opType = OperationType.LICENSE_CREATE;
             BusinessLicense license = BusinessLicense.builder()
                     .userId(userId)
                     .storeName(request.getStoreName())
@@ -202,6 +245,8 @@ public class BusinessLicenseService {
             targetLicense = license;
             log.info("用户{}提交入驻信息:id={}", userId, license.getId());
         } else {
+            beforeData = buildSnapshot(existing);
+            opType = OperationType.LICENSE_EDIT;
             existing.setStoreName(request.getStoreName());
             existing.setTerminalType(request.getTerminalType());
             existing.setProvince(request.getProvince());
@@ -227,6 +272,22 @@ public class BusinessLicenseService {
             log.info("用户{}更新入驻信息:id={}", userId, existing.getId());
         }
 
+        // 审计日志:用户提交/更新入驻信息
+        String operatorName = getUserDisplayName(userId);
+        Map<String, Object> afterData = buildSnapshot(targetLicense);
+        auditLogService.logSuccess(
+                userId,
+                operatorName,
+                OperatorRole.USER.getCode(),
+                opType,
+                "LICENSE:" + targetLicense.getId(),
+                targetLicense.getId(),
+                beforeData,
+                afterData,
+                String.format("客户「%s」%s入驻信息",
+                        operatorName, opType == OperationType.LICENSE_CREATE ? "提交" : "更新"),
+                OperationSource.USER_SUBMIT);
+
         // 事务提交后同步客户入驻信息到第三方系统(智价云药店版),失败不阻塞主流程
         User submitter = userMapper.selectById(userId);
         if (submitter != null) {
@@ -304,19 +365,20 @@ public class BusinessLicenseService {
                 reviewerId, license.getId(), userId);
 
         // 审计日志:管理员新增,记录变更后快照
-        String userName = submitter.getNickname() != null
-                ? submitter.getNickname() : String.valueOf(userId);
+        String adminName = getAdminName(reviewerId);
         String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
+        Map<String, Object> afterData = buildSnapshot(license);
         auditLogService.logSuccess(
                 reviewerId != null ? reviewerId : 0L,
-                OperatorRole.ADMIN,
+                adminName,
+                OperatorRole.ADMIN.getCode(),
                 OperationType.LICENSE_CREATE,
                 "LICENSE:" + license.getId(),
                 license.getId(),
                 null,
-                Map.of("userId", userId, "userName", userName, "storeName", storeName,
-                        "reviewStatus", LicenseStatus.PENDING.name(), "syncSource", "MANUAL"),
-                String.format("管理员新增客户「%s」(药店:%s)的入驻信息,待审核确认", userName, storeName),
+                afterData,
+                String.format("管理员新增客户「%s」(药店:%s)的入驻信息,待审核确认",
+                        getUserDisplayName(userId), storeName),
                 OperationSource.ADMIN_CREATE);
 
         // 事务提交后同步到第三方系统(staus=PENDING→0),失败不阻塞主流程
@@ -441,18 +503,19 @@ public class BusinessLicenseService {
         });
 
         // 审计日志:管理员编辑,记录变更前/后快照
-        String userName = submitter.getNickname() != null
-                ? submitter.getNickname() : String.valueOf(userId);
+        String adminName = getAdminName(reviewerId);
         String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
         auditLogService.logSuccess(
                 reviewerId != null ? reviewerId : 0L,
-                OperatorRole.ADMIN,
+                adminName,
+                OperatorRole.ADMIN.getCode(),
                 OperationType.LICENSE_EDIT,
                 "LICENSE:" + license.getId(),
                 license.getId(),
                 beforeData,
                 afterData,
-                String.format("管理员编辑客户「%s」(药店:%s)的入驻信息", userName, storeName),
+                String.format("管理员编辑客户「%s」(药店:%s)的入驻信息",
+                        getUserDisplayName(userId), storeName),
                 OperationSource.ADMIN_CREATE);
     }
 
@@ -477,6 +540,9 @@ public class BusinessLicenseService {
         snapshot.put("medicalDeviceClass3Url", license.getMedicalDeviceClass3Url());
         snapshot.put("reviewStatus", license.getReviewStatus());
         snapshot.put("showVerifiedBadge", license.getShowVerifiedBadge());
+        snapshot.put("rejectReason", license.getRejectReason());
+        snapshot.put("status", license.getStatus());
+        snapshot.put("externalId", license.getExternalId());
         return snapshot;
     }
 
@@ -495,6 +561,9 @@ public class BusinessLicenseService {
     public void approve(Long licenseId, Long reviewerId) {
         BusinessLicense license = validateReviewable(licenseId);
 
+        // 变更前快照(在修改前捕获)
+        Map<String, Object> beforeData = buildSnapshot(license);
+
         // 提交用户必须存在,否则拒绝审核
         Long userId = license.getUserId();
         User submitter = userMapper.selectById(userId);
@@ -546,22 +615,24 @@ public class BusinessLicenseService {
             }
         });
 
-        // 审计日志:审核通过,记录客户信息
-        String userName = submitter.getNickname() != null
-                ? submitter.getNickname() : String.valueOf(userId);
+        // 审计日志:审核通过,记录变更前/后快照
+        String adminName = getAdminName(reviewerId);
         String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
+        Map<String, Object> afterData = buildSnapshot(license);
         auditLogService.logSuccess(
                 reviewerId != null ? reviewerId : 0L,
-                OperatorRole.ADMIN,
+                adminName,
+                OperatorRole.ADMIN.getCode(),
                 OperationType.LICENSE_APPROVE,
                 "LICENSE:" + license.getId(),
                 license.getId(),
-                null,
-                Map.of("userId", userId, "userName", userName, "storeName", storeName),
-                String.format("审核通过客户「%s」(药店:%s)的入驻申请", userName, storeName),
+                beforeData,
+                afterData,
+                String.format("审核通过客户「%s」(药店:%s)的入驻申请",
+                        getUserDisplayName(userId), storeName),
                 OperationSource.ADMIN_CREATE);
-        log.info("入驻审核通过: licenseId={}, userId={}, userName={}, reviewerId={}",
-                licenseId, userId, userName, reviewerId);
+        log.info("入驻审核通过: licenseId={}, userId={}, reviewerId={}",
+                licenseId, userId, reviewerId);
     }
 
     /**
@@ -586,6 +657,9 @@ public class BusinessLicenseService {
         String targetIdentifier = String.format("PHONE:%s|USER:%d|LICENSE:%d",
                 phone != null ? phone : "UNKNOWN", userId, license.getId());
 
+        // 同步前快照(捕获 externalId/status 变更前的状态)
+        Map<String, Object> licenseBefore = buildSnapshot(license);
+
         try {
             ShopResourceSyncService.SyncResult result =
                     shopResourceSyncService.syncShopResource(submitter, license);
@@ -609,11 +683,12 @@ public class BusinessLicenseService {
                         OperationType.LICENSE_SYNC_THIRD,
                         targetIdentifier,
                         license.getId(),
-                        null,
+                        licenseBefore,
                         Map.of("userId", userId, "userName", userName, "storeName", storeName,
                                 "phone", phone != null ? phone : "UNKNOWN",
                                 "trigger", trigger,
-                                "externalUserId", result.getUserId(), "newUser", result.getNewUser()),
+                                "externalUserId", result.getUserId(), "newUser", result.getNewUser(),
+                                "licenseAfter", buildSnapshot(license)),
                         String.format("同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方成功[%s],第三方用户ID:%s",
                                 userName, phone != null ? phone : "UNKNOWN", storeName, trigger, result.getUserId()),
                         OperationSource.SYSTEM_SYNC);
@@ -629,10 +704,11 @@ public class BusinessLicenseService {
                         OperationType.LICENSE_SYNC_THIRD,
                         targetIdentifier,
                         license.getId(),
-                        null,
+                        licenseBefore,
                         Map.of("userId", userId, "userName", userName, "storeName", storeName,
                                 "phone", phone != null ? phone : "UNKNOWN",
-                                "trigger", trigger, "result", "no_user_id"),
+                                "trigger", trigger, "result", "no_user_id",
+                                "licenseAfter", buildSnapshot(license)),
                         String.format("同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方未成功[%s]:第三方未返回用户ID",
                                 userName, phone != null ? phone : "UNKNOWN", storeName, trigger),
                         OperationSource.SYSTEM_SYNC);
@@ -649,10 +725,11 @@ public class BusinessLicenseService {
                     OperationType.LICENSE_SYNC_THIRD,
                     targetIdentifier,
                     license.getId(),
-                    null,
+                    licenseBefore,
                     Map.of("userId", userId, "userName", userName, "storeName", storeName,
                             "phone", phone != null ? phone : "UNKNOWN",
-                            "trigger", trigger, "error", e.getMessage()),
+                            "trigger", trigger, "error", e.getMessage(),
+                            "licenseAfter", buildSnapshot(license)),
                     String.format("同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方失败[%s]:%s",
                             userName, phone != null ? phone : "UNKNOWN", storeName, trigger, e.getMessage()),
                     OperationSource.SYSTEM_SYNC);
@@ -712,22 +789,22 @@ public class BusinessLicenseService {
         Map<String, Object> afterData = buildSnapshot(license);
 
         // 审计日志:审核驳回,记录变更前/后快照
-        String userName = submitter.getNickname() != null
-                ? submitter.getNickname() : String.valueOf(userId);
+        String adminName = getAdminName(reviewerId);
         String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
         auditLogService.logSuccess(
                 reviewerId != null ? reviewerId : 0L,
-                OperatorRole.ADMIN,
+                adminName,
+                OperatorRole.ADMIN.getCode(),
                 OperationType.LICENSE_REJECT,
                 "LICENSE:" + license.getId(),
                 license.getId(),
                 beforeData,
                 afterData,
                 String.format("驳回客户「%s」(药店:%s)的入驻申请,原因:%s",
-                        userName, storeName, rejectReason),
+                        getUserDisplayName(userId), storeName, rejectReason),
                 OperationSource.ADMIN_CREATE);
-        log.info("入驻审核驳回: licenseId={}, userId={}, userName={}, reviewerId={}, reason={}",
-                licenseId, userId, userName, reviewerId, rejectReason);
+        log.info("入驻审核驳回: licenseId={}, userId={}, reviewerId={}, reason={}",
+                licenseId, userId, reviewerId, rejectReason);
 
         // 事务提交后同步客户入驻信息到第三方系统(智价云药店版),失败不阻塞主流程
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@@ -766,6 +843,9 @@ public class BusinessLicenseService {
                 ? submitter.getNickname() : String.valueOf(userId);
         String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
 
+        // 同步前快照(捕获 externalId/status 变更前的状态)
+        Map<String, Object> licenseBefore = buildSnapshot(license);
+
         try {
             ShopResourceSyncService.SyncResult result =
                     shopResourceSyncService.syncShopResource(submitter, license);
@@ -778,13 +858,15 @@ public class BusinessLicenseService {
 
                 auditLogService.logSuccess(
                         reviewerId != null ? reviewerId : 0L,
-                        OperatorRole.ADMIN,
+                        getAdminName(reviewerId),
+                        OperatorRole.ADMIN.getCode(),
                         OperationType.LICENSE_SYNC_THIRD,
                         "LICENSE:" + license.getId(),
                         license.getId(),
-                        null,
+                        licenseBefore,
                         Map.of("userId", userId, "userName", userName, "storeName", storeName,
-                                "externalUserId", result.getUserId(), "newUser", result.getNewUser()),
+                                "externalUserId", result.getUserId(), "newUser", result.getNewUser(),
+                                "licenseAfter", buildSnapshot(license)),
                         String.format("手动同步客户「%s」(药店:%s)的入驻信息到第三方,第三方用户ID:%s",
                                 userName, storeName, result.getUserId()),
                         OperationSource.ADMIN_CREATE);
@@ -796,13 +878,15 @@ public class BusinessLicenseService {
 
                 auditLogService.logSuccess(
                         reviewerId != null ? reviewerId : 0L,
-                        OperatorRole.ADMIN,
+                        getAdminName(reviewerId),
+                        OperatorRole.ADMIN.getCode(),
                         OperationType.LICENSE_SYNC_THIRD,
                         "LICENSE:" + license.getId(),
                         license.getId(),
-                        null,
+                        licenseBefore,
                         Map.of("userId", userId, "userName", userName, "storeName", storeName,
-                                "result", "no_user_id"),
+                                "result", "no_user_id",
+                                "licenseAfter", buildSnapshot(license)),
                         String.format("手动同步客户「%s」(药店:%s)的入驻信息到第三方未成功:第三方未返回用户ID",
                                 userName, storeName),
                         OperationSource.ADMIN_CREATE);
@@ -815,13 +899,15 @@ public class BusinessLicenseService {
 
             auditLogService.logSuccess(
                     reviewerId != null ? reviewerId : 0L,
-                    OperatorRole.ADMIN,
+                    getAdminName(reviewerId),
+                    OperatorRole.ADMIN.getCode(),
                     OperationType.LICENSE_SYNC_THIRD,
                     "LICENSE:" + license.getId(),
                     license.getId(),
-                    null,
+                    licenseBefore,
                     Map.of("userId", userId, "userName", userName, "storeName", storeName,
-                            "error", e.getMessage()),
+                            "error", e.getMessage(),
+                            "licenseAfter", buildSnapshot(license)),
                     String.format("手动同步客户「%s」(药店:%s)的入驻信息到第三方失败:%s",
                             userName, storeName, e.getMessage()),
                     OperationSource.ADMIN_CREATE);
@@ -964,10 +1050,24 @@ public class BusinessLicenseService {
             throw new BusinessException(ErrorCode.BAD_REQUEST, "该差异记录已处理,无法重复操作");
         }
 
+        // ADOPT_THIRD 时捕获 License 变更前后快照
+        Map<String, Object> licenseBefore = null;
+        Map<String, Object> licenseAfter = null;
+
         int newStatus;
         if ("ADOPT_THIRD".equals(action)) {
+            // 采纳前捕获变更前快照
+            BusinessLicense license = getById(diff.getLicenseId());
+            if (license != null) {
+                licenseBefore = buildSnapshot(license);
+            }
             // 采纳第三方:将本地入驻信息字段更新为第三方值
             applyDiffToLicense(diff.getLicenseId(), diff.getFieldName(), diff.getThirdPartyValue());
+            // 采纳后捕获变更后快照
+            BusinessLicense updatedLicense = getById(diff.getLicenseId());
+            if (updatedLicense != null) {
+                licenseAfter = buildSnapshot(updatedLicense);
+            }
             newStatus = 1;
         } else if ("KEEP_LOCAL".equals(action)) {
             // 保留本地:不修改数据
@@ -989,16 +1089,32 @@ public class BusinessLicenseService {
         log.info("运营处理入驻信息差异: diffId={}, licenseId={}, action={}, reviewerId={}",
                 diffId, diff.getLicenseId(), action, reviewerId);
 
+        // 构建审计数据:差异信息 + License 快照(ADOPT_THIRD 时附带)
+        Map<String, Object> beforeData = new HashMap<>();
+        beforeData.put("fieldName", diff.getFieldName());
+        beforeData.put("localValue", diff.getLocalValue());
+        beforeData.put("thirdPartyValue", diff.getThirdPartyValue());
+        if (licenseBefore != null) {
+            beforeData.put("licenseBefore", licenseBefore);
+        }
+
+        Map<String, Object> afterData = new HashMap<>();
+        afterData.put("action", action);
+        afterData.put("status", newStatus);
+        afterData.put("remark", remark);
+        if (licenseAfter != null) {
+            afterData.put("licenseAfter", licenseAfter);
+        }
+
         auditLogService.logSuccess(
                 reviewerId != null ? reviewerId : 0L,
-                OperatorRole.ADMIN,
+                getAdminName(reviewerId),
+                OperatorRole.ADMIN.getCode(),
                 OperationType.LICENSE_THIRD_UPDATE,
                 "LICENSE_DIFF:" + diff.getId(),
                 diff.getLicenseId(),
-                Map.of("fieldName", diff.getFieldName(),
-                        "localValue", diff.getLocalValue(),
-                        "thirdPartyValue", diff.getThirdPartyValue()),
-                Map.of("action", action, "status", newStatus, "remark", remark),
+                beforeData,
+                afterData,
                 String.format("处理入驻信息差异(%s):字段=%s,动作=%s",
                         diff.getFieldName(), diff.getFieldName(), action),
                 OperationSource.ADMIN_CREATE);

+ 58 - 2
zhijiayun-user/src/main/java/com/xuekairui/user/service/OperationAuditLogService.java

@@ -198,6 +198,40 @@ public class OperationAuditLogService {
                   targetIdentifier, targetId, beforeData, afterData, remark);
     }
 
+    /**
+     * 便捷方法:记录成功操作-带数据快照 + 操作来源(String operatorName 版本,支持自定义操作人名)
+     */
+    public void logSuccess(Long operatorId, String operatorName, String operatorRole,
+                          OperationType operationType, String targetIdentifier, Long targetId,
+                          Object beforeData, Object afterData, String remark,
+                          OperationSource operationSource) {
+        AuditLogBuilder builder = AuditLogBuilder.builder()
+                .operatorId(operatorId)
+                .operatorName(operatorName)
+                .operatorRole(operatorRole)
+                .operationType(operationType.getCode())
+                .operationSource(operationSource != null ? operationSource.getCode() : null)
+                .module(operationType.getModule())
+                .description(operationType.getDescription())
+                .targetIdentifier(targetIdentifier)
+                .targetId(targetId)
+                .targetType(operationType.getTargetType())
+                .result(AuditResult.SUCCESS.getCode())
+                .isSensitive(operationType.isSensitive())
+                .remark(remark);
+
+        if (beforeData != null) {
+            builder.beforeData(toJson(beforeData));
+        }
+        if (afterData != null) {
+            builder.afterData(toJson(afterData));
+        }
+
+        OperationAuditLog log = builder.build();
+        fillRequestInfo(log);
+        logOperation(log);
+    }
+
 
     /**
      * 查询用户的操作日志
@@ -218,9 +252,10 @@ public class OperationAuditLogService {
     }
 
     /**
-     * 查询目标对象的操作历史
+     * 查询目标对象的操作历史(列表排除大字段,支持按来源筛选)
      */
     public Page<OperationAuditLog> getTargetOperationHistory(String targetType, Long targetId,
+                                                              String operationSource,
                                                               int pageNum, int pageSize) {
         Page<OperationAuditLog> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<OperationAuditLog> wrapper = new LambdaQueryWrapper<>();
@@ -230,12 +265,27 @@ public class OperationAuditLogService {
         if (targetId != null) {
             wrapper.eq(OperationAuditLog::getTargetId, targetId);
         }
+        if (operationSource != null && !operationSource.isBlank()) {
+            wrapper.eq(OperationAuditLog::getOperationSource, operationSource);
+        }
+        // 列表查询排除大字段(beforeData/afterData/requestParams),减少传输开销
+        wrapper.select(OperationAuditLog.class, info ->
+                !"before_data".equals(info.getColumn())
+                        && !"after_data".equals(info.getColumn())
+                        && !"request_params".equals(info.getColumn()));
         wrapper.orderByDesc(OperationAuditLog::getOperationTime);
         return auditLogMapper.selectPage(page, wrapper);
     }
 
     /**
-     * 分页查询操作日志(运营后台)
+     * 根据ID查询审计日志详情(含完整快照)
+     */
+    public OperationAuditLog getAuditLogById(Long id) {
+        return auditLogMapper.selectById(id);
+    }
+
+    /**
+     * 分页查询操作日志(运营后台,列表排除大字段)
      */
     public Page<OperationAuditLog> queryAuditLogs(String module, String operationType, 
                                                    Long operatorId, LocalDateTime startTime, 
@@ -268,6 +318,12 @@ public class OperationAuditLogService {
             wrapper.eq(OperationAuditLog::getOperationSource, operationSource);
         }
         
+        // 列表查询排除大字段(beforeData/afterData/requestParams),减少传输开销
+        wrapper.select(OperationAuditLog.class, info ->
+                !"before_data".equals(info.getColumn())
+                        && !"after_data".equals(info.getColumn())
+                        && !"request_params".equals(info.getColumn()));
+        
         wrapper.orderByDesc(OperationAuditLog::getOperationTime);
         
         return auditLogMapper.selectPage(page, wrapper);