feat: 新增验证码配置管理功能

- 新增 UserCaptchaConfigEntity 实体类,含 side(admin/app) 端标识
- 新增 Admin/App Controller、Service、Mapper
- BaseSysLoginServiceImpl 新增 isCaptchaEnabled() 查询数据库配置
- login/register 改用数据库配置替代 cool.captcha.enable 硬编码
- AdminBaseOpenController 新增 /captchaEnabled 接口
- menu.json 新增验证码管理菜单及权限
- base.json 新增 5 条预置数据(3条app+2条admin)
This commit is contained in:
OpenCode
2026-07-22 20:11:34 +08:00
parent f5205ef0e9
commit 7fc636a020
11 changed files with 334 additions and 5 deletions
@@ -125,4 +125,10 @@ public class AdminBaseOpenController {
}
return R.ok(Map.of("mode", "common"));
}
@Operation(summary = "管理端验证码是否启用")
@GetMapping("/captchaEnabled")
public R captchaEnabled() {
return R.ok(Map.of("enabled", baseSysLoginService.isCaptchaEnabled("admin", "adminLogin")));
}
}
@@ -23,6 +23,8 @@ public interface BaseSysLoginService {
*/
void captchaCheck(String captchaId, String code);
boolean isCaptchaEnabled(String side, String sceneKey);
/**
* 登录
*
@@ -23,6 +23,8 @@ import com.cool.modules.base.mapper.sys.BaseSysRoleMapper;
import com.cool.modules.base.mapper.sys.BaseSysUserMapper;
import com.cool.modules.base.service.sys.BaseSysLoginService;
import com.cool.modules.base.service.sys.BaseSysPermsService;
import com.cool.modules.user.entity.UserCaptchaConfigEntity;
import com.cool.modules.user.service.UserCaptchaConfigService;
import com.mybatisflex.core.query.QueryWrapper;
import java.awt.Color;
import java.util.HashMap;
@@ -52,6 +54,8 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
private final BaseSysPermsService baseSysPermsService;
private final UserCaptchaConfigService userCaptchaConfigService;
private final org.springframework.security.crypto.password.PasswordEncoder passwordEncoder;
@org.springframework.beans.factory.annotation.Value("${cool.captcha.enable:true}")
@@ -90,10 +94,21 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
}
}
@Override
public boolean isCaptchaEnabled(String side, String sceneKey) {
UserCaptchaConfigEntity config = userCaptchaConfigService.getOne(
QueryWrapper.create()
.eq(UserCaptchaConfigEntity::getSide, side)
.eq(UserCaptchaConfigEntity::getSceneKey, sceneKey));
if (config != null) {
return ObjUtil.equals(config.getEnabled(), 1);
}
return captchaEnable;
}
@Override
public Object login(BaseSysLoginDto baseSysLoginDto) {
// 1、检查验证码是否正确 2、执行登录操作
if (captchaEnable) {
if (isCaptchaEnabled("admin", "adminLogin")) {
captchaCheck(baseSysLoginDto.getCaptchaId(), baseSysLoginDto.getVerifyCode());
}
UsernamePasswordAuthenticationToken upToken =
@@ -109,7 +124,7 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
CoolPreconditions.check(
ObjectUtil.isEmpty(baseSysUserEntity) || baseSysUserEntity.getStatus() == 0, "用户已禁用");
Long[] roleIds = baseSysPermsService.getRoles(baseSysUserEntity);
if (captchaEnable) {
if (isCaptchaEnabled("admin", "adminLogin")) {
coolCache.del("verify:img:" + baseSysLoginDto.getCaptchaId());
}
return generateToken(roleIds, baseSysUserEntity, null);
@@ -134,7 +149,7 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
@Override
public Object register(BaseSysRegisterDto baseSysRegisterDto) {
if (captchaEnable) {
if (isCaptchaEnabled("admin", "adminRegister")) {
captchaCheck(baseSysRegisterDto.getCaptchaId(), baseSysRegisterDto.getVerifyCode());
}
BaseSysUserEntity existUser = baseSysUserMapper.selectOneByQuery(
@@ -160,7 +175,7 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
userEntity.setDepartmentId(defaultDepartment.getId());
baseSysUserMapper.insertSelective(userEntity);
baseSysPermsService.updateUserRole(userEntity.getId(), new Long[]{normalRole.getId()});
if (captchaEnable) {
if (isCaptchaEnabled("admin", "adminRegister")) {
coolCache.del("verify:img:" + baseSysRegisterDto.getCaptchaId());
}
return Dict.create().set("message", "注册成功");
@@ -0,0 +1,22 @@
package com.cool.modules.user.controller.admin;
import static com.cool.modules.user.entity.table.UserCaptchaConfigEntityTableDef.USER_CAPTCHA_CONFIG_ENTITY;
import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.base.BaseController;
import com.cool.modules.user.entity.UserCaptchaConfigEntity;
import com.cool.modules.user.service.UserCaptchaConfigService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
@Tag(name = "验证码配置", description = "管理验证码配置")
@CoolRestController(api = {"add", "delete", "update", "page", "list", "info"}, cname = "captchaConfig")
public class AdminUserCaptchaConfigController extends BaseController<UserCaptchaConfigService, UserCaptchaConfigEntity> {
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
setPageOption(createOp().fieldEq(USER_CAPTCHA_CONFIG_ENTITY.SIDE, USER_CAPTCHA_CONFIG_ENTITY.CAPTCHA_TYPE, USER_CAPTCHA_CONFIG_ENTITY.ENABLED)
.keyWordLikeFields(USER_CAPTCHA_CONFIG_ENTITY.LABEL));
}
}
@@ -0,0 +1,26 @@
package com.cool.modules.user.controller.app;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.annotation.TokenIgnore;
import com.cool.modules.user.entity.UserCaptchaConfigEntity;
import com.cool.modules.user.service.UserCaptchaConfigService;
import com.cool.core.request.R;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "App验证码配置", description = "App端获取验证码配置")
@RestController
@RequiredArgsConstructor
public class AppUserCaptchaConfigController {
private final UserCaptchaConfigService userCaptchaConfigService;
@TokenIgnore
@GetMapping("/app/user/captcha/config")
public R<List<UserCaptchaConfigEntity>> getConfig() {
return R.ok(userCaptchaConfigService.getEnabledConfigs("app"));
}
}
@@ -0,0 +1,52 @@
package com.cool.modules.user.entity;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Table;
import com.cool.core.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Table(value = "user_captcha_config", comment = "验证码配置")
public class UserCaptchaConfigEntity extends BaseEntity<UserCaptchaConfigEntity> {
@ColumnDefine(comment = "端标识 admin/app", notNull = true, length = 20)
private String side;
@ColumnDefine(comment = "场景标识 smsLogin/smsRegister/bindPhone/passwordLogin/adminLogin/adminRegister", notNull = true, length = 50)
private String sceneKey;
@ColumnDefine(comment = "场景名称", notNull = true, length = 50)
private String label;
@ColumnDefine(comment = "验证码类型 image/sms/slider/rotate/wordClick", notNull = true, length = 30)
private String captchaType;
@ColumnDefine(comment = "是否启用验证码 0-禁用 1-启用", defaultValue = "1")
private Integer enabled;
@ColumnDefine(comment = "验证码长度", defaultValue = "4")
private Integer codeLength;
@ColumnDefine(comment = "验证码过期时间(秒)", defaultValue = "1800")
private Integer expireSeconds;
@ColumnDefine(comment = "验证码宽度(图片验证码)", defaultValue = "150")
private Integer width;
@ColumnDefine(comment = "验证码高度(图片验证码)", defaultValue = "50")
private Integer height;
@ColumnDefine(comment = "短信发送间隔(秒,短信验证码)", defaultValue = "60")
private Integer smsInterval;
@ColumnDefine(comment = "每日发送上限(短信验证码)", defaultValue = "10")
private Integer smsDailyLimit;
@ColumnDefine(comment = "排序号,越小越靠前", defaultValue = "0")
private Integer orderNum;
@ColumnDefine(comment = "备注", length = 200)
private String remark;
}
@@ -0,0 +1,7 @@
package com.cool.modules.user.mapper;
import com.mybatisflex.core.BaseMapper;
import com.cool.modules.user.entity.UserCaptchaConfigEntity;
public interface UserCaptchaConfigMapper extends BaseMapper<UserCaptchaConfigEntity> {
}
@@ -0,0 +1,10 @@
package com.cool.modules.user.service;
import com.cool.core.base.BaseService;
import com.cool.modules.user.entity.UserCaptchaConfigEntity;
import java.util.List;
public interface UserCaptchaConfigService extends BaseService<UserCaptchaConfigEntity> {
List<UserCaptchaConfigEntity> getEnabledConfigs(String side);
}
@@ -0,0 +1,22 @@
package com.cool.modules.user.service.impl;
import com.cool.core.base.BaseServiceImpl;
import com.cool.modules.user.entity.UserCaptchaConfigEntity;
import com.cool.modules.user.mapper.UserCaptchaConfigMapper;
import com.cool.modules.user.service.UserCaptchaConfigService;
import com.mybatisflex.core.query.QueryWrapper;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class UserCaptchaConfigServiceImpl extends BaseServiceImpl<UserCaptchaConfigMapper, UserCaptchaConfigEntity>
implements UserCaptchaConfigService {
@Override
public List<UserCaptchaConfigEntity> getEnabledConfigs(String side) {
return list(QueryWrapper.create()
.eq(UserCaptchaConfigEntity::getSide, side)
.eq(UserCaptchaConfigEntity::getEnabled, 1)
.orderBy(UserCaptchaConfigEntity::getOrderNum, true));
}
}
+82
View File
@@ -209,5 +209,87 @@
"orderNum": 2,
"isDefault": 0
}
],
"user_captcha_config": [
{
"id": 1,
"side": "app",
"sceneKey": "smsLogin",
"label": "短信登录验证码",
"captchaType": "image",
"enabled": 1,
"codeLength": 4,
"expireSeconds": 1800,
"width": 150,
"height": 50,
"smsInterval": 60,
"smsDailyLimit": 10,
"orderNum": 1,
"remark": "手机验证码登录前的图片验证码"
},
{
"id": 2,
"side": "app",
"sceneKey": "smsRegister",
"label": "短信注册验证码",
"captchaType": "image",
"enabled": 1,
"codeLength": 4,
"expireSeconds": 1800,
"width": 150,
"height": 50,
"smsInterval": 60,
"smsDailyLimit": 10,
"orderNum": 2,
"remark": "注册时发送短信前的图片验证码"
},
{
"id": 3,
"side": "app",
"sceneKey": "bindPhone",
"label": "绑定手机验证码",
"captchaType": "image",
"enabled": 1,
"codeLength": 4,
"expireSeconds": 1800,
"width": 150,
"height": 50,
"smsInterval": 60,
"smsDailyLimit": 10,
"orderNum": 3,
"remark": "绑定手机号时的短信验证码"
},
{
"id": 4,
"side": "admin",
"sceneKey": "adminLogin",
"label": "管理员登录验证码",
"captchaType": "image",
"enabled": 1,
"codeLength": 4,
"expireSeconds": 1800,
"width": 150,
"height": 50,
"smsInterval": 60,
"smsDailyLimit": 10,
"orderNum": 1,
"remark": "管理端登录时的图片验证码"
},
{
"id": 5,
"side": "admin",
"sceneKey": "adminRegister",
"label": "管理员注册验证码",
"captchaType": "image",
"enabled": 1,
"codeLength": 4,
"expireSeconds": 1800,
"width": 150,
"height": 50,
"smsInterval": 60,
"smsDailyLimit": 10,
"orderNum": 2,
"remark": "管理端注册时的图片验证码"
}
]
}
@@ -992,6 +992,91 @@
"childMenus": []
}
]
},
{
"name": "验证码管理",
"router": "/user/captcha-config",
"perms": null,
"type": 1,
"icon": "icon-menu",
"orderNum": 3,
"viewPath": "modules/user/views/captcha-config.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "删除",
"router": null,
"perms": "user:captchaConfig:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "修改",
"router": null,
"perms": "user:captchaConfig:update,user:captchaConfig:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "单个信息",
"router": null,
"perms": "user:captchaConfig:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "列表查询",
"router": null,
"perms": "user:captchaConfig:list",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "分页查询",
"router": null,
"perms": "user:captchaConfig:page",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "新增",
"router": null,
"perms": "user:captchaConfig:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
}
]
}