【同步】BOOT 和 CLOUD 的功能

This commit is contained in:
YunaiV
2025-12-28 10:22:05 +08:00
parent f922d3fd55
commit 0f27c0aa72
41 changed files with 2851 additions and 1394 deletions

View File

@@ -36,6 +36,14 @@ public class LoginLogController {
@Resource
private LoginLogService loginLogService;
@GetMapping("/get")
@Operation(summary = "获得登录日志")
@PreAuthorize("@ss.hasPermission('system:login-log:query')")
public CommonResult<LoginLogRespVO> getLoginLog(Long id) {
LoginLogDO loginLog = loginLogService.getLoginLog(id);
return success(BeanUtils.toBean(loginLog, LoginLogRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得登录日志分页列表")
@PreAuthorize("@ss.hasPermission('system:login-log:query')")

View File

@@ -61,6 +61,7 @@ public class OperateLogController {
@Operation(summary = "导出操作日志")
@GetMapping("/export-excel")
@PreAuthorize("@ss.hasPermission('system:operate-log:export')")
@TransMethodResult
@ApiAccessLog(operateType = EXPORT)
public void exportOperateLog(HttpServletResponse response, @Valid OperateLogPageReqVO exportReqVO) throws IOException {
exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);

View File

@@ -1,8 +1,10 @@
package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
import com.fhs.core.trans.anno.Trans;
import com.fhs.core.trans.constant.TransType;
import com.fhs.core.trans.vo.VO;
@@ -31,6 +33,11 @@ public class OperateLogRespVO implements VO {
@ExcelProperty("操作人")
private String userName;
@Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1", implementation = Integer.class)
@ExcelProperty("用户类型")
@DictFormat(DictTypeConstants.USER_TYPE)
private Integer userType;
@Schema(description = "操作模块类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单")
@ExcelProperty("操作模块类型")
private String type;

View File

@@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogRespVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO;
import cn.iocoder.yudao.module.system.service.sms.SmsLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@@ -18,6 +19,7 @@ import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -44,6 +46,15 @@ public class SmsLogController {
return success(BeanUtils.toBean(pageResult, SmsLogRespVO.class));
}
@GetMapping("/get")
@Operation(summary = "获得短信日志")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:sms-log:query')")
public CommonResult<SmsLogRespVO> getSmsLog(@RequestParam("id") Long id) {
SmsLogDO smsLog = smsLogService.getSmsLog(id);
return success(BeanUtils.toBean(smsLog, SmsLogRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出短信日志 Excel")
@PreAuthorize("@ss.hasPermission('system:sms-log:export')")

View File

@@ -5,8 +5,10 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSmsSendReqVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
@@ -26,8 +28,6 @@ public interface AuthConvert {
AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class);
AuthLoginRespVO convert(OAuth2AccessTokenDO bean);
default AuthPermissionInfoRespVO convert(AdminUserDO user, List<RoleDO> roleList, List<MenuDO> menuList) {
return AuthPermissionInfoRespVO.builder()
.user(BeanUtils.toBean(user, AuthPermissionInfoRespVO.UserVO.class))

View File

@@ -91,10 +91,10 @@ public class AliyunSmsClient extends AbstractSmsClient {
@Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
// 1. 执行请求
// 参考链接 https://api.aliyun.com/document/Dysmsapi/2017-05-25/QuerySmsTemplate
// 参考链接 https://api.aliyun.com/document/Dysmsapi/2017-05-25/GetSmsTemplate
TreeMap<String, Object> queryParam = new TreeMap<>();
queryParam.put("TemplateCode", apiTemplateId);
JSONObject response = request("QuerySmsTemplate", queryParam);
JSONObject response = request("GetSmsTemplate", queryParam);
// 2.1 请求失败
String code = response.getStr("Code");

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
@@ -215,13 +216,13 @@ public class AdminAuthServiceImpl implements AdminAuthService {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
// 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenDO);
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
}
@Override
public AuthLoginRespVO refreshToken(String refreshToken) {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
return AuthConvert.INSTANCE.convert(accessTokenDO);
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
}
@Override

View File

@@ -12,6 +12,14 @@ import jakarta.validation.Valid;
*/
public interface LoginLogService {
/**
* 获得登录日志
*
* @param id 编号
* @return 登录日志
*/
LoginLogDO getLoginLog(Long id);
/**
* 获得登录日志分页
*

View File

@@ -21,6 +21,11 @@ public class LoginLogServiceImpl implements LoginLogService {
@Resource
private LoginLogMapper loginLogMapper;
@Override
public LoginLogDO getLoginLog(Long id) {
return loginLogMapper.selectById(id);
}
@Override
public PageResult<LoginLogDO> getLoginLogPage(LoginLogPageReqVO pageReqVO) {
return loginLogMapper.selectPage(pageReqVO);

View File

@@ -19,8 +19,10 @@ import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -53,7 +55,7 @@ public class MailTemplateServiceImpl implements MailTemplateService {
// 插入
MailTemplateDO template = BeanUtils.toBean(createReqVO, MailTemplateDO.class)
.setParams(parseTemplateContentParams(createReqVO.getContent()));
.setParams(parseTemplateTitleAndContentParams(createReqVO.getTitle(), createReqVO.getContent()));
mailTemplateMapper.insert(template);
return template.getId();
}
@@ -69,7 +71,7 @@ public class MailTemplateServiceImpl implements MailTemplateService {
// 更新
MailTemplateDO updateObj = BeanUtils.toBean(updateReqVO, MailTemplateDO.class)
.setParams(parseTemplateContentParams(updateReqVO.getContent()));
.setParams(parseTemplateTitleAndContentParams(updateReqVO.getTitle(), updateReqVO.getContent()));
mailTemplateMapper.updateById(updateObj);
}
@@ -129,7 +131,77 @@ public class MailTemplateServiceImpl implements MailTemplateService {
@Override
public String formatMailTemplateContent(String content, Map<String, Object> params) {
return StrUtil.format(content, params);
// 1. 先替换模板变量
String formattedContent = StrUtil.format(content, params);
// 关联 Pull Requesthttps://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1461 讨论
// 2.1 反转义HTML特殊字符
formattedContent = unescapeHtml(formattedContent);
// 2.2 处理代码块(确保<pre><code>标签格式正确)
formattedContent = formatHtmlCodeBlocks(formattedContent);
// 2.3 将最外层的 pre 标签替换为 div 标签
formattedContent = replaceOuterPreWithDiv(formattedContent);
return formattedContent;
}
private String replaceOuterPreWithDiv(String content) {
if (StrUtil.isEmpty(content)) {
return content;
}
// 使用正则表达式匹配所有的 <pre> 标签,包括嵌套的 <code> 标签
String regex = "(?s)<pre[^>]*>(.*?)</pre>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(content);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
// 提取 <pre> 标签内的内容
String innerContent = matcher.group(1);
// 返回 div 标签包裹的内容
matcher.appendReplacement(sb, "<div>" + innerContent + "</div>");
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 反转义 HTML 特殊字符
*
* @param input 输入字符串
* @return 反转义后的字符串
*/
private String unescapeHtml(String input) {
if (StrUtil.isEmpty(input)) {
return input;
}
return input
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("&#39;", "'")
.replace("&nbsp;", " ");
}
/**
* 格式化 HTML 中的代码块
*
* @param content 邮件内容
* @return 格式化后的邮件内容
*/
private String formatHtmlCodeBlocks(String content) {
// 匹配 <pre><code> 标签的代码块
Pattern codeBlockPattern = Pattern.compile("<pre\\s*.*?><code\\s*.*?>(.*?)</code></pre>", Pattern.DOTALL);
Matcher matcher = codeBlockPattern.matcher(content);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
// 获取代码块内容
String codeBlock = matcher.group(1);
// 为代码块添加样式
String replacement = "<pre style=\"background-color: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto;\"><code>" + codeBlock + "</code></pre>";
matcher.appendReplacement(sb, replacement);
}
matcher.appendTail(sb);
return sb.toString();
}
@Override
@@ -137,14 +209,31 @@ public class MailTemplateServiceImpl implements MailTemplateService {
return mailTemplateMapper.selectCountByAccountId(accountId);
}
/**
* 解析标题和内容中的参数
*/
@VisibleForTesting
public List<String> parseTemplateTitleAndContentParams(String title, String content) {
List<String> titleParams = ReUtil.findAllGroup1(PATTERN_PARAMS, title);
List<String> contentParams = ReUtil.findAllGroup1(PATTERN_PARAMS, content);
// 合并参数并去重
List<String> allParams = new ArrayList<>(titleParams);
for (String param : contentParams) {
if (!allParams.contains(param)) {
allParams.add(param);
}
}
return allParams;
}
/**
* 获得邮件模板中的参数,形如 {key}
*
* @param content 内容
* @return 参数列表
*/
private List<String> parseTemplateContentParams(String content) {
List<String> parseTemplateContentParams(String content) {
return ReUtil.findAllGroup1(PATTERN_PARAMS, content);
}
}
}

View File

@@ -180,7 +180,13 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
.setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
.setRefreshToken(refreshTokenDO.getRefreshToken())
.setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds()));
accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号
// 优先从 refreshToken 获取租户编号,避免 ThreadLocal 被污染时导致 tenantId 为 null
// 可能关联的 issuehttps://t.zsxq.com/JIi5G
Long tenantId = refreshTokenDO.getTenantId();
if (tenantId == null) {
tenantId = TenantContextHolder.getTenantId();
}
accessTokenDO.setTenantId(tenantId);
oauth2AccessTokenMapper.insert(accessTokenDO);
// 记录到 Redis 中
oauth2AccessTokenRedisDAO.set(accessTokenDO);

View File

@@ -255,6 +255,9 @@ public class MenuServiceImpl implements MenuService {
return;
}
// 如果 id 为空,说明不用比较是否为相同 id 的菜单
if (id == null) {
throw exception(MENU_NAME_DUPLICATE);
}
if (!menu.getId().equals(id)) {
throw exception(MENU_NAME_DUPLICATE);
}
@@ -277,7 +280,7 @@ public class MenuServiceImpl implements MenuService {
}
// 如果 id 为空,说明不用比较是否为相同 id 的菜单
if (id == null) {
return;
throw exception(MENU_COMPONENT_NAME_DUPLICATE);
}
if (!menu.getId().equals(id)) {
throw exception(MENU_COMPONENT_NAME_DUPLICATE);

View File

@@ -58,6 +58,14 @@ public interface SmsLogService {
void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success,
LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg);
/**
* 获得短信日志
*
* @param id 日志编号
* @return 短信日志
*/
SmsLogDO getSmsLog(Long id);
/**
* 获得短信日志分页
*

View File

@@ -79,6 +79,11 @@ public class SmsLogServiceImpl implements SmsLogService {
.receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build());
}
@Override
public SmsLogDO getSmsLog(Long id) {
return smsLogMapper.selectById(id);
}
@Override
public PageResult<SmsLogDO> getSmsLogPage(SmsLogPageReqVO pageReqVO) {
return smsLogMapper.selectPage(pageReqVO);