feat(system): 增强用户与租户操作的审计日志能力
- AdminUserServiceImpl:为 updateUserWithClientRole 添加 @LogRecord,自动补全空字段并生成变更明细 - LogRecordConstants:补充租户及用户客户端权限相关操作日志模板 - TenantSaveReqVO:增加 @DiffLogField 以便记录字段级变更 - TenantServiceImpl:为 createTenant、updateTenant 添加 @LogRecord 及变更上下文 - UserClientSaveReqVO:补充 @DiffLogField 与布尔值解析函数,完善日志展示
This commit is contained in:
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -194,7 +194,6 @@ public class UserController {
|
||||
}
|
||||
|
||||
|
||||
@TenantIgnore
|
||||
@PutMapping("update-client-role")
|
||||
@Operation(summary = "修改用户客户端使用权限")
|
||||
@PreAuthorize("@ss.hasPermission('system:user:update-client')")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}}";
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user