feat(system): 增强用户与租户操作的审计日志能力

- AdminUserServiceImpl:为 updateUserWithClientRole 添加 @LogRecord,自动补全空字段并生成变更明细
- LogRecordConstants:补充租户及用户客户端权限相关操作日志模板
- TenantSaveReqVO:增加 @DiffLogField 以便记录字段级变更
- TenantServiceImpl:为 createTenant、updateTenant 添加 @LogRecord 及变更上下文
- UserClientSaveReqVO:补充 @DiffLogField 与布尔值解析函数,完善日志展示
This commit is contained in:
2026-02-12 15:28:08 +08:00
parent 6d6c5f93df
commit 9a4faa65d6
6 changed files with 120 additions and 12 deletions

View File

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.mzt.logapi.starter.annotation.DiffLogField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
@@ -24,41 +25,52 @@ public class TenantSaveReqVO {
@Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
@NotNull(message = "租户名不能为空")
@DiffLogField(name = "租户名")
private String name;
@Schema(description = "联系人", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
@NotNull(message = "联系人不能为空")
@DiffLogField(name = "联系人")
private String contactName;
@Schema(description = "联系手机", example = "15601691300")
@DiffLogField(name = "联系手机")
private String contactMobile;
@Schema(description = "租户状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "租户状态")
@DiffLogField(name = "租户状态")
private Integer status;
@Schema(description = "绑定域名", example = "https://www.iocoder.cn")
@DiffLogField(name = "绑定域名")
private String website;
@Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "租户套餐编号不能为空")
@DiffLogField(name = "租户套餐编号")
private Long packageId;
@Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "过期时间不能为空")
@DiffLogField(name = "过期时间")
private LocalDateTime expireTime;
@Schema(description = "ai过期时间")
@DiffLogField(name = "AI 过期时间")
private LocalDateTime aiExpireTime;
@Schema(description = "大哥过期时间")
@DiffLogField(name = "大哥过期时间")
private LocalDateTime brotherExpireTime;
@Schema(description = "爬主播到期时间")
@DiffLogField(name = "爬主播到期时间")
private LocalDateTime crawlExpireTime;
@Schema(description = "账号数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账号数量不能为空")
@DiffLogField(name = "账号数量")
private Integer accountCount;
// ========== 仅【创建】时,需要传递的字段 ==========
@@ -66,6 +78,7 @@ public class TenantSaveReqVO {
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
@Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成")
@Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符")
@DiffLogField(name = "管理员账号")
private String username;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@@ -80,19 +93,24 @@ public class TenantSaveReqVO {
}
@Schema(description = "是否允许登录爬虫客户端", example = "0不允许1允许")
@DiffLogField(name = "允许登录爬虫客户端")
private Byte crawl;
@Schema(description = "是否允许登录爬虫客户端", example = "0不允许1允许")
@DiffLogField(name = "允许登录大哥客户端")
private Byte bigBrother;
@Schema(description = "能否登录 AI 聊天工具", example = "0不允许1允许")
@DiffLogField(name = "允许登录 AI 聊天工具")
private Byte aiChat;
@Schema(description = "备注", example = "备注")
@DiffLogField(name = "备注")
private String remark;
@Schema(description = "租户类型", example = "代理/客户")
@NotNull(message = "租户类型不能为空")
@DiffLogField(name = "租户类型")
private String tenantType;
}

View File

@@ -194,7 +194,6 @@ public class UserController {
}
@TenantIgnore
@PutMapping("update-client-role")
@Operation(summary = "修改用户客户端使用权限")
@PreAuthorize("@ss.hasPermission('system:user:update-client')")

View File

@@ -1,42 +1,42 @@
package cn.iocoder.yudao.module.system.controller.admin.user.vo.user;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.validation.Mobile;
import cn.iocoder.yudao.module.system.framework.operatelog.core.DeptParseFunction;
import cn.iocoder.yudao.module.system.framework.operatelog.core.PostParseFunction;
import cn.iocoder.yudao.module.system.framework.operatelog.core.SexParseFunction;
import com.fasterxml.jackson.annotation.JsonIgnore;
import cn.iocoder.yudao.module.system.framework.operatelog.core.BooleanParseFunction;
import com.mzt.logapi.starter.annotation.DiffLogField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
import java.util.Set;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 用户创建客户端用户/修改 Request VO")
@Data
public class UserClientSaveReqVO {
@Schema(description = "用户编号", example = "1024")
@NotNull(message = "用户编号不能为空")
private Long id;
@Schema(description = "是否允许登录主播爬虫客户端", example = "0不允许1允许")
@DiffLogField(name = "允许登录主播爬虫客户端", function = BooleanParseFunction.NAME)
private Byte crawl;
@Schema(description = "是否允许登录大哥爬虫客户端", example = "0不允许1允许")
@DiffLogField(name = "允许登录大哥爬虫客户端", function = BooleanParseFunction.NAME)
private Byte bigBrother;
@Schema(description = "租户 Id", example = "1")
@DiffLogField(name = "租户编号")
private Long tenantId;
@Schema(description = "能否登录 AI 聊天工具", example = "0不允许1允许")
@DiffLogField(name = "允许登录 AI 聊天工具", function = BooleanParseFunction.NAME)
private Byte aiChat;
@Schema(description = "是否允许使用 AI 回复", example = "0不允许1允许")
@DiffLogField(name = "允许使用 AI 回复", function = BooleanParseFunction.NAME)
private Byte aiReplay;
@Schema(description = "是否允许登录 Web AI 客户端", example = "0不允许1允许")
@DiffLogField(name = "允许登录 Web AI 客户端", function = BooleanParseFunction.NAME)
private Byte webAi;
}

View File

@@ -19,6 +19,8 @@ public interface LogRecordConstants {
String SYSTEM_USER_DELETE_SUCCESS = "删除了用户【{{#user.nickname}}】";
String SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE = "重置用户密码";
String SYSTEM_USER_UPDATE_PASSWORD_SUCCESS = "将用户【{{#user.nickname}}】的密码从【{{#user.password}}】重置为【{{#newPassword}}】";
String SYSTEM_USER_UPDATE_CLIENT_ROLE_SUB_TYPE = "更新用户客户端权限";
String SYSTEM_USER_UPDATE_CLIENT_ROLE_SUCCESS = "更新了用户【{{#user.username}}】客户端权限{{#clientRoleChangeDetail}}";
// ======================= SYSTEM_ROLE 角色 =======================
@@ -30,4 +32,12 @@ public interface LogRecordConstants {
String SYSTEM_ROLE_DELETE_SUB_TYPE = "删除角色";
String SYSTEM_ROLE_DELETE_SUCCESS = "删除了角色【{{#role.name}}】";
// ======================= SYSTEM_TENANT 租户 =======================
String SYSTEM_TENANT_TYPE = "SYSTEM 租户";
String SYSTEM_TENANT_CREATE_SUB_TYPE = "创建租户";
String SYSTEM_TENANT_CREATE_SUCCESS = "创建了租户【{{#tenant.name}}】: {_DIFF{#createReqVO}}";
String SYSTEM_TENANT_UPDATE_SUB_TYPE = "更新租户";
String SYSTEM_TENANT_UPDATE_SUCCESS = "更新了租户【{{#tenant.name}}】: {_DIFF{#updateReqVO}} {{#tenantChangeDetail}}";
}

View File

@@ -52,6 +52,9 @@ import cn.iocoder.yudao.module.system.service.tenantagencypackage.TenantAgencyPa
import cn.iocoder.yudao.module.system.service.tenantbalance.TenantBalanceService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -64,6 +67,7 @@ import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -73,6 +77,7 @@ import java.util.concurrent.atomic.AtomicReference;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.system.enums.LogRecordConstants.*;
import static java.util.Collections.singleton;
/**
@@ -299,6 +304,8 @@ public class TenantServiceImpl implements TenantService {
@Override
@DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
@DataPermission(enable = false) // 参见 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1154 说明
@LogRecord(type = SYSTEM_TENANT_TYPE, subType = SYSTEM_TENANT_CREATE_SUB_TYPE, bizNo = "{{#tenant.id}}",
success = SYSTEM_TENANT_CREATE_SUCCESS)
public Long createTenant(TenantSaveReqVO createReqVO) {
// 获取当前操作租户的ID即创建者的租户ID
@@ -410,6 +417,8 @@ public class TenantServiceImpl implements TenantService {
tenantBalanceMapper.insert(tenantBalance); // 插入钱包记录
}
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, new TenantSaveReqVO());
LogRecordContext.putVariable("tenant", tenant);
// 返回新创建的租户ID
return tenant.getId();
}
@@ -466,6 +475,8 @@ public class TenantServiceImpl implements TenantService {
tenantBalanceMapper.insert(tenantBalance); // 插入钱包记录
}
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, new TenantSaveReqVO());
LogRecordContext.putVariable("tenant", tenant);
// 返回新创建的租户ID
return tenant.getId();
}
@@ -502,6 +513,8 @@ public class TenantServiceImpl implements TenantService {
@Override
@DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
@LogRecord(type = SYSTEM_TENANT_TYPE, subType = SYSTEM_TENANT_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
success = SYSTEM_TENANT_UPDATE_SUCCESS)
public void updateTenant(TenantSaveReqVO updateReqVO) {
// 校验存在
TenantDO tenant = validateUpdateTenant(updateReqVO.getId());
@@ -531,8 +544,12 @@ public class TenantServiceImpl implements TenantService {
}
}
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(tenant, TenantSaveReqVO.class));
LogRecordContext.putVariable("tenant", tenant);
}
private void validTenantNameDuplicate(String name, Long id) {
TenantDO tenant = tenantMapper.selectByName(name);
if (tenant == null) {

View File

@@ -13,7 +13,7 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.datapermission.core.util.DataPermissionUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthRegisterReqVO;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
@@ -547,10 +547,74 @@ public class AdminUserServiceImpl implements AdminUserService {
}
@Override
@LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_CLIENT_ROLE_SUB_TYPE, bizNo = "{{#user.id}}",
success = SYSTEM_USER_UPDATE_CLIENT_ROLE_SUCCESS)
public void updateUserWithClientRole(UserClientSaveReqVO reqVO) {
// 1. 校验用户存在
AdminUserDO oldUser = TenantUtils.executeIgnore(() -> validateUserExists(reqVO.getId()));
fillUserClientRoleReqIfAbsent(reqVO, oldUser);
LogRecordContext.putVariable("user", oldUser);
LogRecordContext.putVariable("clientRoleChangeDetail", buildClientRoleChangeDetail(oldUser, reqVO));
// 2.1 更新用户
AdminUserDO updateObj = BeanUtils.toBean(reqVO, AdminUserDO.class);
userMapper.updateById(updateObj);
TenantUtils.executeIgnore(() -> userMapper.updateById(updateObj));
// 3. 记录操作日志上下文
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldUser, UserClientSaveReqVO.class));
}
private void fillUserClientRoleReqIfAbsent(UserClientSaveReqVO reqVO, AdminUserDO oldUser) {
if (reqVO.getTenantId() == null) {
reqVO.setTenantId(oldUser.getTenantId());
}
if (reqVO.getCrawl() == null) {
reqVO.setCrawl(oldUser.getCrawl());
}
if (reqVO.getBigBrother() == null) {
reqVO.setBigBrother(oldUser.getBigBrother());
}
if (reqVO.getAiChat() == null) {
reqVO.setAiChat(oldUser.getAiChat());
}
if (reqVO.getAiReplay() == null) {
reqVO.setAiReplay(oldUser.getAiReplay());
}
if (reqVO.getWebAi() == null) {
reqVO.setWebAi(oldUser.getWebAi());
}
}
private String buildClientRoleChangeDetail(AdminUserDO oldUser, UserClientSaveReqVO reqVO) {
List<String> changes = new ArrayList<>();
appendClientRoleChange(changes, "允许登录主播爬虫客户端", oldUser.getCrawl(), reqVO.getCrawl());
appendClientRoleChange(changes, "允许登录大哥爬虫客户端", oldUser.getBigBrother(), reqVO.getBigBrother());
appendClientRoleChange(changes, "允许登录 AI 聊天工具", oldUser.getAiChat(), reqVO.getAiChat());
appendClientRoleChange(changes, "允许使用 AI 回复", oldUser.getAiReplay(), reqVO.getAiReplay());
appendClientRoleChange(changes, "允许登录 Web AI 客户端", oldUser.getWebAi(), reqVO.getWebAi());
if (changes.isEmpty()) {
return "(未发现字段变更)";
}
return "(变更明细:" + String.join("", changes) + "";
}
private void appendClientRoleChange(List<String> changes, String fieldName, Byte oldValue, Byte newValue) {
if (ObjUtil.notEqual(oldValue, newValue)) {
changes.add(fieldName + ":【" + formatClientRoleValue(oldValue) + "】->【" + formatClientRoleValue(newValue) + "");
}
}
private String formatClientRoleValue(Byte value) {
if (value == null) {
return "-";
}
if (value == 1) {
return "允许";
}
if (value == 0) {
return "不允许";
}
return String.valueOf(value);
}
@Override