mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 10:57:13 +08:00 
			
		
		
		
	refactor: 优化密码策略处理
This commit is contained in:
		| @@ -66,6 +66,11 @@ public class CacheConstants { | |||||||
|      */ |      */ | ||||||
|     public static final String DASHBOARD_KEY_PREFIX = "DASHBOARD" + DELIMITER; |     public static final String DASHBOARD_KEY_PREFIX = "DASHBOARD" + DELIMITER; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户密码错误次数缓存键前缀 | ||||||
|  |      */ | ||||||
|  |     public static final String USER_PASSWORD_ERROR_KEY_PREFIX = USER_KEY_PREFIX + "PASSWORD_ERROR" + DELIMITER; | ||||||
|  |  | ||||||
|     private CacheConstants() { |     private CacheConstants() { | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -25,27 +25,32 @@ package top.continew.admin.common.constant; | |||||||
| public class RegexConstants { | public class RegexConstants { | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 用户名正则(长度为 4 到 64 位,可以包含字母、数字,下划线,以字母开头) |      * 用户名正则(用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头) | ||||||
|      */ |      */ | ||||||
|     public static final String USERNAME = "^[a-zA-Z][a-zA-Z0-9_]{3,64}$"; |     public static final String USERNAME = "^[a-zA-Z][a-zA-Z0-9_]{3,64}$"; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 密码正则(长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字) |      * 密码正则模板(密码长度为 min-max 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字) | ||||||
|      */ |      */ | ||||||
|     public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z]).{6,32}$"; |     public static final String PASSWORD_TEMPLATE = "^(?=.*\\d)(?=.*[a-z]).{%s,%s}$"; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 密码正则严格版(长度为 8 到 32 位,包含至少1个大写字母、1个小写字母、1个数字,1个特殊字符) |      * 密码正则(密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字) | ||||||
|      */ |      */ | ||||||
|     public static final String PASSWORD_STRICT = "^\\S*(?=\\S{8,32})(?=\\S*\\d)(?=\\S*[A-Z])(?=\\S*[a-z])(?=\\S*[!@#$%^&*? ])\\S*$"; |     public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z]).{8,32}$"; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 通用编码正则(长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头) |      * 特殊字符正则 | ||||||
|  |      */ | ||||||
|  |     public static final String SPECIAL_CHARACTER = "[-_`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]|\\n|\\r|\\t"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 通用编码正则(长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头) | ||||||
|      */ |      */ | ||||||
|     public static final String GENERAL_CODE = "^[a-zA-Z][a-zA-Z0-9_]{1,29}$"; |     public static final String GENERAL_CODE = "^[a-zA-Z][a-zA-Z0-9_]{1,29}$"; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 通用名称正则(长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线) |      * 通用名称正则(长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线) | ||||||
|      */ |      */ | ||||||
|     public static final String GENERAL_NAME = "^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{2,30}$"; |     public static final String GENERAL_NAME = "^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{2,30}$"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,6 +24,16 @@ package top.continew.admin.common.constant; | |||||||
|  */ |  */ | ||||||
| public class SysConstants { | public class SysConstants { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 否 | ||||||
|  |      */ | ||||||
|  |     public static final Integer NO = 0; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 是 | ||||||
|  |      */ | ||||||
|  |     public static final Integer YES = 1; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 管理员角色编码 |      * 管理员角色编码 | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ | |||||||
|  |  | ||||||
| package top.continew.admin.auth.service; | package top.continew.admin.auth.service; | ||||||
|  |  | ||||||
|  | import jakarta.servlet.http.HttpServletRequest; | ||||||
| import me.zhyd.oauth.model.AuthUser; | import me.zhyd.oauth.model.AuthUser; | ||||||
| import top.continew.admin.auth.model.resp.RouteResp; | import top.continew.admin.auth.model.resp.RouteResp; | ||||||
|  |  | ||||||
| @@ -34,9 +35,10 @@ public interface LoginService { | |||||||
|      * |      * | ||||||
|      * @param username 用户名 |      * @param username 用户名 | ||||||
|      * @param password 密码 |      * @param password 密码 | ||||||
|  |      * @param request  请求对象 | ||||||
|      * @return 令牌 |      * @return 令牌 | ||||||
|      */ |      */ | ||||||
|     String accountLogin(String username, String password); |     String accountLogin(String username, String password, HttpServletRequest request); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 手机号登录 |      * 手机号登录 | ||||||
|   | |||||||
| @@ -21,7 +21,9 @@ import cn.hutool.core.collection.CollUtil; | |||||||
| import cn.hutool.core.lang.tree.Tree; | import cn.hutool.core.lang.tree.Tree; | ||||||
| import cn.hutool.core.lang.tree.TreeNodeConfig; | import cn.hutool.core.lang.tree.TreeNodeConfig; | ||||||
| import cn.hutool.core.util.*; | import cn.hutool.core.util.*; | ||||||
|  | import cn.hutool.extra.servlet.JakartaServletUtil; | ||||||
| import cn.hutool.json.JSONUtil; | import cn.hutool.json.JSONUtil; | ||||||
|  | import jakarta.servlet.http.HttpServletRequest; | ||||||
| import lombok.RequiredArgsConstructor; | import lombok.RequiredArgsConstructor; | ||||||
| import me.zhyd.oauth.model.AuthUser; | import me.zhyd.oauth.model.AuthUser; | ||||||
| import org.springframework.security.crypto.password.PasswordEncoder; | import org.springframework.security.crypto.password.PasswordEncoder; | ||||||
| @@ -39,7 +41,7 @@ import top.continew.admin.common.enums.MessageTypeEnum; | |||||||
| import top.continew.admin.common.model.dto.LoginUser; | import top.continew.admin.common.model.dto.LoginUser; | ||||||
| import top.continew.admin.common.util.helper.LoginHelper; | import top.continew.admin.common.util.helper.LoginHelper; | ||||||
| import top.continew.admin.system.enums.MessageTemplateEnum; | import top.continew.admin.system.enums.MessageTemplateEnum; | ||||||
| import top.continew.admin.system.enums.OptionCodeEnum; | import top.continew.admin.system.enums.PasswordPolicyEnum; | ||||||
| import top.continew.admin.system.model.entity.DeptDO; | import top.continew.admin.system.model.entity.DeptDO; | ||||||
| import top.continew.admin.system.model.entity.RoleDO; | import top.continew.admin.system.model.entity.RoleDO; | ||||||
| import top.continew.admin.system.model.entity.UserDO; | import top.continew.admin.system.model.entity.UserDO; | ||||||
| @@ -80,36 +82,15 @@ public class LoginServiceImpl implements LoginService { | |||||||
|     private final OptionService optionService; |     private final OptionService optionService; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public String accountLogin(String username, String password) { |     public String accountLogin(String username, String password, HttpServletRequest request) { | ||||||
|         UserDO user = userService.getByUsername(username); |         UserDO user = userService.getByUsername(username); | ||||||
|         boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(password, user.getPassword()); |         boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(password, user.getPassword()); | ||||||
|         isPasswordLocked(username, isError); |         this.checkUserLocked(username, request, isError); | ||||||
|         CheckUtils.throwIf(isError, "用户名或密码错误"); |         CheckUtils.throwIf(isError, "用户名或密码错误"); | ||||||
|         this.checkUserStatus(user); |         this.checkUserStatus(user); | ||||||
|         return this.login(user); |         return this.login(user); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 检测用户是否被密码锁定 |  | ||||||
|      * |  | ||||||
|      * @param username 用户名 |  | ||||||
|      */ |  | ||||||
|     private void isPasswordLocked(String username, boolean isError) { |  | ||||||
|         // 不锁定账户 |  | ||||||
|         int maxErrorCount = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_ERROR_COUNT); |  | ||||||
|         if (maxErrorCount <= 0) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         int lockMinutes = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_LOCK_MINUTES); |  | ||||||
|         String key = CacheConstants.USER_KEY_PREFIX + "PASSWORD-ERROR:" + username; |  | ||||||
|         long currentErrorCount = 0; |  | ||||||
|         if (isError) { |  | ||||||
|             currentErrorCount = RedisUtils.incr(key); |  | ||||||
|             RedisUtils.expire(key, Duration.ofMinutes(lockMinutes)); |  | ||||||
|         } |  | ||||||
|         CheckUtils.throwIf(currentErrorCount > maxErrorCount, "密码错误已达 {} 次,账户锁定 {} 分钟", maxErrorCount, lockMinutes); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public String phoneLogin(String phone) { |     public String phoneLogin(String phone) { | ||||||
|         UserDO user = userService.getByPhone(phone); |         UserDO user = userService.getByPhone(phone); | ||||||
| @@ -229,6 +210,36 @@ public class LoginServiceImpl implements LoginService { | |||||||
|         CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, dept.getStatus(), "此账号所属部门已被禁用,如有疑问,请联系管理员"); |         CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, dept.getStatus(), "此账号所属部门已被禁用,如有疑问,请联系管理员"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 检测用户是否已被锁定 | ||||||
|  |      * | ||||||
|  |      * @param username 用户名 | ||||||
|  |      * @param request  请求对象 | ||||||
|  |      * @param isError  是否登录错误 | ||||||
|  |      */ | ||||||
|  |     private void checkUserLocked(String username, HttpServletRequest request, boolean isError) { | ||||||
|  |         // 不锁定 | ||||||
|  |         int maxErrorCount = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.name()); | ||||||
|  |         if (maxErrorCount <= SysConstants.NO) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         // 检测是否已被锁定 | ||||||
|  |         String key = CacheConstants.USER_PASSWORD_ERROR_KEY_PREFIX + RedisUtils.formatKey(username, JakartaServletUtil | ||||||
|  |             .getClientIP(request)); | ||||||
|  |         int lockMinutes = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.name()); | ||||||
|  |         Integer currentErrorCount = ObjectUtil.defaultIfNull(RedisUtils.get(key), 0); | ||||||
|  |         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "账号锁定 {} 分钟,请稍后再试", lockMinutes); | ||||||
|  |         // 登录成功清除计数 | ||||||
|  |         if (!isError) { | ||||||
|  |             RedisUtils.delete(key); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         // 登录失败递增计数 | ||||||
|  |         currentErrorCount++; | ||||||
|  |         RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes)); | ||||||
|  |         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账号锁定 {} 分钟", maxErrorCount, lockMinutes); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 发送系统消息 |      * 发送系统消息 | ||||||
|      * |      * | ||||||
| @@ -237,8 +248,8 @@ public class LoginServiceImpl implements LoginService { | |||||||
|     private void sendSystemMsg(UserDO user) { |     private void sendSystemMsg(UserDO user) { | ||||||
|         MessageReq req = new MessageReq(); |         MessageReq req = new MessageReq(); | ||||||
|         MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER; |         MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER; | ||||||
|         req.setTitle(StrUtil.format(socialRegister.getTitle(), projectProperties.getName())); |         req.setTitle(socialRegister.getTitle().formatted(projectProperties.getName())); | ||||||
|         req.setContent(StrUtil.format(socialRegister.getContent(), user.getNickname())); |         req.setContent(socialRegister.getContent().formatted(user.getNickname())); | ||||||
|         req.setType(MessageTypeEnum.SYSTEM); |         req.setType(MessageTypeEnum.SYSTEM); | ||||||
|         messageService.add(req, CollUtil.toList(user.getId())); |         messageService.add(req, CollUtil.toList(user.getId())); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ public enum MessageTemplateEnum { | |||||||
|     /** |     /** | ||||||
|      * 第三方登录 |      * 第三方登录 | ||||||
|      */ |      */ | ||||||
|     SOCIAL_REGISTER("欢迎注册 {}", "尊敬的 {},欢迎注册使用,请及时配置您的密码。"); |     SOCIAL_REGISTER("欢迎注册 %s", "尊敬的 %s,欢迎注册使用,请及时配置您的密码。"); | ||||||
|  |  | ||||||
|     private final String title; |     private final String title; | ||||||
|     private final String content; |     private final String content; | ||||||
|   | |||||||
| @@ -1,63 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. |  | ||||||
|  * |  | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
|  * you may not use this file except in compliance with the License. |  | ||||||
|  * You may obtain a copy of the License at |  | ||||||
|  * |  | ||||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 |  | ||||||
|  * |  | ||||||
|  * Unless required by applicable law or agreed to in writing, software |  | ||||||
|  * distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
|  * See the License for the specific language governing permissions and |  | ||||||
|  * limitations under the License. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| package top.continew.admin.system.enums; |  | ||||||
|  |  | ||||||
| import lombok.Getter; |  | ||||||
| import lombok.RequiredArgsConstructor; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 参数枚举 |  | ||||||
|  * |  | ||||||
|  * @author Kils |  | ||||||
|  * @since 2024/05/09 11:25 |  | ||||||
|  */ |  | ||||||
| @Getter |  | ||||||
| @RequiredArgsConstructor |  | ||||||
| public enum OptionCodeEnum { |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 密码是否允许包含正反序帐户名 |  | ||||||
|      */ |  | ||||||
|     PASSWORD_CONTAIN_NAME("password_contain_name", "密码不允许包含正反序帐户名"), |  | ||||||
|     /** |  | ||||||
|      * 密码错误锁定帐户次数 |  | ||||||
|      */ |  | ||||||
|     PASSWORD_ERROR_COUNT("password_error_count", "密码错误锁定帐户次数"), |  | ||||||
|     /** |  | ||||||
|      * 密码有效期 |  | ||||||
|      */ |  | ||||||
|     PASSWORD_EXPIRATION_DAYS("password_expiration_days", "密码有效期"), |  | ||||||
|     /** |  | ||||||
|      * 密码是否允许包含正反序帐户名 |  | ||||||
|      */ |  | ||||||
|     PASSWORD_LOCK_MINUTES("password_lock_minutes", "密码错误锁定帐户的时间"), |  | ||||||
|     /** |  | ||||||
|      * 密码最小长度 |  | ||||||
|      */ |  | ||||||
|     PASSWORD_MIN_LENGTH("password_min_length", "密码最小长度"), |  | ||||||
|     /** |  | ||||||
|      * 密码是否必须包含特殊字符 |  | ||||||
|      */ |  | ||||||
|     PASSWORD_SPECIAL_CHAR("password_special_char", "密码是否必须包含特殊字符"), |  | ||||||
|     /** |  | ||||||
|      * 修改密码最短间隔 |  | ||||||
|      */ |  | ||||||
|     PASSWORD_UPDATE_INTERVAL("password_update_interval", "修改密码最短间隔"); |  | ||||||
|  |  | ||||||
|     private final String value; |  | ||||||
|     private final String description; |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,77 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package top.continew.admin.system.enums; | ||||||
|  |  | ||||||
|  | import lombok.Getter; | ||||||
|  | import lombok.RequiredArgsConstructor; | ||||||
|  | import top.continew.admin.common.constant.SysConstants; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 密码策略枚举 | ||||||
|  |  * | ||||||
|  |  * @author Kils | ||||||
|  |  * @author Charles7c | ||||||
|  |  * @since 2024/5/9 11:25 | ||||||
|  |  */ | ||||||
|  | @Getter | ||||||
|  | @RequiredArgsConstructor | ||||||
|  | public enum PasswordPolicyEnum { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 登录密码错误锁定账号的次数 | ||||||
|  |      */ | ||||||
|  |     PASSWORD_ERROR_LOCK_COUNT(null, SysConstants.NO, 10), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 登录密码错误锁定账号的时间(min) | ||||||
|  |      */ | ||||||
|  |     PASSWORD_ERROR_LOCK_MINUTES(null, 1, 1440), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 密码到期提前提示(天) | ||||||
|  |      */ | ||||||
|  |     PASSWORD_EXPIRATION_WARNING_DAYS(null, SysConstants.NO, Integer.MAX_VALUE), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 密码有效期(天) | ||||||
|  |      */ | ||||||
|  |     PASSWORD_EXPIRATION_DAYS(null, SysConstants.NO, 999), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 密码重复使用规则 | ||||||
|  |      */ | ||||||
|  |     PASSWORD_REUSE_POLICY("不允许使用最近 %s 次的历史密码", 3, 32), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 密码最小长度 | ||||||
|  |      */ | ||||||
|  |     PASSWORD_MIN_LENGTH("密码最小长度为 %s 个字符", 8, 32), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 密码是否允许包含正反序账号名 | ||||||
|  |      */ | ||||||
|  |     PASSWORD_ALLOW_CONTAIN_USERNAME("密码不允许包含正反序账号名", SysConstants.NO, SysConstants.YES), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 密码是否必须包含特殊字符 | ||||||
|  |      */ | ||||||
|  |     PASSWORD_CONTAIN_SPECIAL_CHARACTERS("密码必须包含特殊字符", SysConstants.NO, SysConstants.YES),; | ||||||
|  |  | ||||||
|  |     private final String description; | ||||||
|  |     private final Integer min; | ||||||
|  |     private final Integer max; | ||||||
|  | } | ||||||
| @@ -54,7 +54,7 @@ public class DeptReq extends BaseReq { | |||||||
|      */ |      */ | ||||||
|     @Schema(description = "名称", example = "测试部") |     @Schema(description = "名称", example = "测试部") | ||||||
|     @NotBlank(message = "名称不能为空") |     @NotBlank(message = "名称不能为空") | ||||||
|     @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线") |     @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线") | ||||||
|     private String name; |     private String name; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ public class DictReq extends BaseReq { | |||||||
|      */ |      */ | ||||||
|     @Schema(description = "名称", example = "公告类型") |     @Schema(description = "名称", example = "公告类型") | ||||||
|     @NotBlank(message = "名称不能为空") |     @NotBlank(message = "名称不能为空") | ||||||
|     @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线") |     @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线") | ||||||
|     private String name; |     private String name; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -52,7 +52,7 @@ public class DictReq extends BaseReq { | |||||||
|      */ |      */ | ||||||
|     @Schema(description = "编码", example = "notice_type") |     @Schema(description = "编码", example = "notice_type") | ||||||
|     @NotBlank(message = "编码不能为空") |     @NotBlank(message = "编码不能为空") | ||||||
|     @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头") |     @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头") | ||||||
|     private String code; |     private String code; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ public class RoleReq extends BaseReq { | |||||||
|      */ |      */ | ||||||
|     @Schema(description = "名称", example = "测试人员") |     @Schema(description = "名称", example = "测试人员") | ||||||
|     @NotBlank(message = "名称不能为空") |     @NotBlank(message = "名称不能为空") | ||||||
|     @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线") |     @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线") | ||||||
|     private String name; |     private String name; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -56,7 +56,7 @@ public class RoleReq extends BaseReq { | |||||||
|      */ |      */ | ||||||
|     @Schema(description = "编码", example = "test") |     @Schema(description = "编码", example = "test") | ||||||
|     @NotBlank(message = "编码不能为空") |     @NotBlank(message = "编码不能为空") | ||||||
|     @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头") |     @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头") | ||||||
|     private String code; |     private String code; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -55,7 +55,7 @@ public class StorageReq extends BaseReq { | |||||||
|      */ |      */ | ||||||
|     @Schema(description = "编码", example = "local") |     @Schema(description = "编码", example = "local") | ||||||
|     @NotBlank(message = "编码不能为空") |     @NotBlank(message = "编码不能为空") | ||||||
|     @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头") |     @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头") | ||||||
|     private String code; |     private String code; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -45,7 +45,7 @@ public class UserBasicInfoUpdateReq implements Serializable { | |||||||
|      */ |      */ | ||||||
|     @Schema(description = "昵称", example = "张三") |     @Schema(description = "昵称", example = "张三") | ||||||
|     @NotBlank(message = "昵称不能为空") |     @NotBlank(message = "昵称不能为空") | ||||||
|     @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线") |     @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线") | ||||||
|     private String nickname; |     private String nickname; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -51,7 +51,7 @@ public class UserReq extends BaseReq { | |||||||
|      */ |      */ | ||||||
|     @Schema(description = "用户名", example = "zhangsan") |     @Schema(description = "用户名", example = "zhangsan") | ||||||
|     @NotBlank(message = "用户名不能为空") |     @NotBlank(message = "用户名不能为空") | ||||||
|     @Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4 到 64 位,可以包含字母、数字,下划线,以字母开头") |     @Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头") | ||||||
|     private String username; |     private String username; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -59,7 +59,7 @@ public class UserReq extends BaseReq { | |||||||
|      */ |      */ | ||||||
|     @Schema(description = "昵称", example = "张三") |     @Schema(description = "昵称", example = "张三") | ||||||
|     @NotBlank(message = "昵称不能为空") |     @NotBlank(message = "昵称不能为空") | ||||||
|     @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2 到 30 位,可以包含中文、字母、数字、下划线,短横线") |     @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线") | ||||||
|     private String nickname; |     private String nickname; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -16,15 +16,14 @@ | |||||||
|  |  | ||||||
| package top.continew.admin.system.service; | package top.continew.admin.system.service; | ||||||
|  |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.function.Function; |  | ||||||
|  |  | ||||||
| import top.continew.admin.system.enums.OptionCodeEnum; |  | ||||||
| import top.continew.admin.system.model.query.OptionQuery; | import top.continew.admin.system.model.query.OptionQuery; | ||||||
| import top.continew.admin.system.model.req.OptionReq; | import top.continew.admin.system.model.req.OptionReq; | ||||||
| import top.continew.admin.system.model.req.OptionResetValueReq; | import top.continew.admin.system.model.req.OptionResetValueReq; | ||||||
| import top.continew.admin.system.model.resp.OptionResp; | import top.continew.admin.system.model.resp.OptionResp; | ||||||
|  |  | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.function.Function; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 参数业务接口 |  * 参数业务接口 | ||||||
|  * |  * | ||||||
| @@ -56,19 +55,19 @@ public interface OptionService { | |||||||
|     void resetValue(OptionResetValueReq req); |     void resetValue(OptionResetValueReq req); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 根据code获取int参数值 |      * 根据编码查询参数值 | ||||||
|      *  |      * | ||||||
|      * @param code code |      * @param code 编码 | ||||||
|      * @return 参数值 |      * @return 参数值(自动转换为 int 类型) | ||||||
|      */ |      */ | ||||||
|     int getValueByCode2Int(OptionCodeEnum code); |     int getValueByCode2Int(String code); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 根据code获取参数值 |      * 根据编码查询参数值 | ||||||
|      *  |      * | ||||||
|      * @param code   code |      * @param code   编码 | ||||||
|      * @param mapper 类型转换 ex:value -> Integer.parseInt(value) |      * @param mapper 转换方法 e.g.:value -> Integer.parseInt(value) | ||||||
|      * @return 参数值 |      * @return 参数值 | ||||||
|      */ |      */ | ||||||
|     <T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper); |     <T> T getValueByCode(String code, Function<String, T> mapper); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,14 +17,10 @@ | |||||||
| package top.continew.admin.system.service.impl; | package top.continew.admin.system.service.impl; | ||||||
|  |  | ||||||
| import cn.hutool.core.bean.BeanUtil; | import cn.hutool.core.bean.BeanUtil; | ||||||
| import cn.hutool.core.util.ObjUtil; |  | ||||||
| import cn.hutool.core.util.StrUtil; | import cn.hutool.core.util.StrUtil; | ||||||
| import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |  | ||||||
| import com.baomidou.mybatisplus.core.toolkit.Wrappers; |  | ||||||
| import lombok.RequiredArgsConstructor; | import lombok.RequiredArgsConstructor; | ||||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||||
| import top.continew.admin.common.constant.CacheConstants; | import top.continew.admin.common.constant.CacheConstants; | ||||||
| import top.continew.admin.system.enums.OptionCodeEnum; |  | ||||||
| import top.continew.admin.system.mapper.OptionMapper; | import top.continew.admin.system.mapper.OptionMapper; | ||||||
| import top.continew.admin.system.model.entity.OptionDO; | import top.continew.admin.system.model.entity.OptionDO; | ||||||
| import top.continew.admin.system.model.query.OptionQuery; | import top.continew.admin.system.model.query.OptionQuery; | ||||||
| @@ -70,25 +66,24 @@ public class OptionServiceImpl implements OptionService { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public int getValueByCode2Int(OptionCodeEnum code) { |     public int getValueByCode2Int(String code) { | ||||||
|         return this.getValueByCode(code, Integer::parseInt); |         return this.getValueByCode(code, Integer::parseInt); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public <T> T getValueByCode(OptionCodeEnum code, Function<String, T> mapper) { |     public <T> T getValueByCode(String code, Function<String, T> mapper) { | ||||||
|         String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code.getValue()); |         String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code); | ||||||
|         if (StrUtil.isNotBlank(value)) { |         if (StrUtil.isNotBlank(value)) { | ||||||
|             return mapper.apply(value); |             return mapper.apply(value); | ||||||
|         } |         } | ||||||
|         LambdaQueryWrapper<OptionDO> queryWrapper = Wrappers.<OptionDO>lambdaQuery() |         OptionDO option = baseMapper.lambdaQuery() | ||||||
|             .eq(OptionDO::getCode, code.getValue()) |             .eq(OptionDO::getCode, code) | ||||||
|             .select(OptionDO::getValue, OptionDO::getDefaultValue); |             .select(OptionDO::getValue, OptionDO::getDefaultValue) | ||||||
|         OptionDO optionDO = baseMapper.selectOne(queryWrapper); |             .one(); | ||||||
|         CheckUtils.throwIf(ObjUtil.isEmpty(optionDO), "配置 [{}] 不存在", code); |         CheckUtils.throwIfNull(option, "参数 [{}] 不存在", code); | ||||||
|         value = StrUtil.nullToDefault(optionDO.getValue(), optionDO.getDefaultValue()); |         value = StrUtil.nullToDefault(option.getValue(), option.getDefaultValue()); | ||||||
|         CheckUtils.throwIf(StrUtil.isBlank(value), "配置 [{}] 不存在", code); |         CheckUtils.throwIfBlank(value, "参数 [{}] 数据错误", code); | ||||||
|         RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code.getValue(), value); |         RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code, value); | ||||||
|         return mapper.apply(value); |         return mapper.apply(value); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -18,7 +18,6 @@ package top.continew.admin.system.service.impl; | |||||||
|  |  | ||||||
| import cn.hutool.core.bean.BeanUtil; | import cn.hutool.core.bean.BeanUtil; | ||||||
| import cn.hutool.core.collection.CollUtil; | import cn.hutool.core.collection.CollUtil; | ||||||
| import cn.hutool.core.date.LocalDateTimeUtil; |  | ||||||
| import cn.hutool.core.io.file.FileNameUtil; | import cn.hutool.core.io.file.FileNameUtil; | ||||||
| import cn.hutool.core.util.ObjectUtil; | import cn.hutool.core.util.ObjectUtil; | ||||||
| import cn.hutool.core.util.ReUtil; | import cn.hutool.core.util.ReUtil; | ||||||
| @@ -41,6 +40,7 @@ import org.springframework.web.multipart.MultipartFile; | |||||||
| import top.continew.admin.auth.service.OnlineUserService; | import top.continew.admin.auth.service.OnlineUserService; | ||||||
| import top.continew.admin.common.constant.CacheConstants; | import top.continew.admin.common.constant.CacheConstants; | ||||||
| import top.continew.admin.common.constant.RegexConstants; | import top.continew.admin.common.constant.RegexConstants; | ||||||
|  | import top.continew.admin.common.constant.SysConstants; | ||||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||||
| import top.continew.admin.common.util.helper.LoginHelper; | import top.continew.admin.common.util.helper.LoginHelper; | ||||||
| import top.continew.admin.system.mapper.UserMapper; | import top.continew.admin.system.mapper.UserMapper; | ||||||
| @@ -63,10 +63,13 @@ import top.continew.starter.extension.crud.service.CommonUserService; | |||||||
| import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; | import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; | ||||||
|  |  | ||||||
| import java.time.LocalDateTime; | import java.time.LocalDateTime; | ||||||
| import java.util.*; | import java.util.Collection; | ||||||
|  | import java.util.Date; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Optional; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
| import static top.continew.admin.system.enums.OptionCodeEnum.*; | import static top.continew.admin.system.enums.PasswordPolicyEnum.*; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 用户业务实现 |  * 用户业务实现 | ||||||
| @@ -197,7 +200,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes | |||||||
|             CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, password), "当前密码错误"); |             CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, password), "当前密码错误"); | ||||||
|         } |         } | ||||||
|         // 校验密码合法性 |         // 校验密码合法性 | ||||||
|         checkPassword(newPassword, user); |         this.checkPassword(newPassword, user); | ||||||
|         // 更新密码和密码重置时间 |         // 更新密码和密码重置时间 | ||||||
|         user.setPassword(newPassword); |         user.setPassword(newPassword); | ||||||
|         user.setPwdResetTime(LocalDateTime.now()); |         user.setPwdResetTime(LocalDateTime.now()); | ||||||
| @@ -205,52 +208,11 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes | |||||||
|         onlineUserService.cleanByUserId(user.getId()); |         onlineUserService.cleanByUserId(user.getId()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 检测修改密码合法性 |  | ||||||
|      * |  | ||||||
|      * @param password 密码 |  | ||||||
|      * @param user     用户 |  | ||||||
|      */ |  | ||||||
|     private void checkPassword(String password, UserDO user) { |  | ||||||
|         // 密码最小长度 |  | ||||||
|         int passwordMinLength = optionService.getValueByCode2Int(PASSWORD_MIN_LENGTH); |  | ||||||
|         ValidationUtils.throwIf(StrUtil.length(password) < passwordMinLength, PASSWORD_MIN_LENGTH |  | ||||||
|             .getDescription() + "为 {}", passwordMinLength); |  | ||||||
|         // 密码是否允许包含正反序用户名 |  | ||||||
|         int passwordContainName = optionService.getValueByCode2Int(PASSWORD_CONTAIN_NAME); |  | ||||||
|         if (passwordContainName == 1) { |  | ||||||
|             String username = user.getUsername(); |  | ||||||
|             ValidationUtils.throwIf(StrUtil.containsIgnoreCase(password, username) || StrUtil |  | ||||||
|                 .containsIgnoreCase(password, StrUtil.reverse(username)), PASSWORD_CONTAIN_NAME.getDescription()); |  | ||||||
|         } |  | ||||||
|         // 密码是否必须包含特殊字符 |  | ||||||
|         int passwordSpecialChar = optionService.getValueByCode2Int(PASSWORD_SPECIAL_CHAR); |  | ||||||
|         String match = RegexConstants.PASSWORD; |  | ||||||
|         String desc = "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字"; |  | ||||||
|         if (passwordSpecialChar == 1) { |  | ||||||
|             match = RegexConstants.PASSWORD_STRICT; |  | ||||||
|             desc = "密码长度为 8 到 32 位,包含至少1个大写字母、1个小写字母、1个数字,1个特殊字符"; |  | ||||||
|         } |  | ||||||
|         ValidationUtils.throwIf(!ReUtil.isMatch(match, password), desc); |  | ||||||
|         // 密码修改间隔 |  | ||||||
|         if (ObjectUtil.isNull(user.getPwdResetTime())) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         int passwordUpdateInterval = optionService.getValueByCode2Int(PASSWORD_UPDATE_INTERVAL); |  | ||||||
|         if (passwordUpdateInterval <= 0) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         LocalDateTime lastResetTime = user.getPwdResetTime(); |  | ||||||
|         LocalDateTime limitUpdateTime = lastResetTime.plusMinutes(passwordUpdateInterval); |  | ||||||
|         ValidationUtils.throwIf(LocalDateTime.now().isBefore(limitUpdateTime), "上次修改于:{},下次可修改时间:{}", LocalDateTimeUtil |  | ||||||
|             .formatNormal(lastResetTime), LocalDateTimeUtil.formatNormal(limitUpdateTime)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public Boolean isPasswordExpired(LocalDateTime pwdResetTime) { |     public Boolean isPasswordExpired(LocalDateTime pwdResetTime) { | ||||||
|         // 永久有效 |         // 永久有效 | ||||||
|         int passwordExpirationDays = optionService.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS); |         int passwordExpirationDays = optionService.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name()); | ||||||
|         if (passwordExpirationDays <= 0) { |         if (passwordExpirationDays <= SysConstants.NO) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|         // 初始密码也提示修改 |         // 初始密码也提示修改 | ||||||
| @@ -375,6 +337,33 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes | |||||||
|         userRoleService.add(req.getRoleIds(), userId); |         userRoleService.add(req.getRoleIds(), userId); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 检测密码合法性 | ||||||
|  |      * | ||||||
|  |      * @param password 密码 | ||||||
|  |      * @param user     用户信息 | ||||||
|  |      */ | ||||||
|  |     private void checkPassword(String password, UserDO user) { | ||||||
|  |         // 密码最小长度 | ||||||
|  |         int passwordMinLength = optionService.getValueByCode2Int(PASSWORD_MIN_LENGTH.name()); | ||||||
|  |         ValidationUtils.throwIf(StrUtil.length(password) < passwordMinLength, PASSWORD_MIN_LENGTH.getDescription() | ||||||
|  |             .formatted(passwordMinLength)); | ||||||
|  |         // 密码是否允许包含正反序账号名 | ||||||
|  |         int passwordAllowContainUsername = optionService.getValueByCode2Int(PASSWORD_ALLOW_CONTAIN_USERNAME.name()); | ||||||
|  |         if (passwordAllowContainUsername == SysConstants.NO) { | ||||||
|  |             String username = user.getUsername(); | ||||||
|  |             ValidationUtils.throwIf(StrUtil.containsAnyIgnoreCase(password, username, StrUtil | ||||||
|  |                 .reverse(username)), PASSWORD_ALLOW_CONTAIN_USERNAME.getDescription()); | ||||||
|  |         } | ||||||
|  |         int passwordMaxLength = PASSWORD_MIN_LENGTH.getMax(); | ||||||
|  |         ValidationUtils.throwIf(!ReUtil.isMatch(RegexConstants.PASSWORD_TEMPLATE | ||||||
|  |             .formatted(passwordMinLength, passwordMaxLength), password), "密码长度为 {}-{} 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字", passwordMinLength, passwordMaxLength); | ||||||
|  |         // 密码是否必须包含特殊字符 | ||||||
|  |         int passwordContainSpecialChar = optionService.getValueByCode2Int(PASSWORD_CONTAIN_SPECIAL_CHARACTERS.name()); | ||||||
|  |         ValidationUtils.throwIf(passwordContainSpecialChar == SysConstants.YES && !ReUtil | ||||||
|  |             .isMatch(RegexConstants.SPECIAL_CHARACTER, password), PASSWORD_CONTAIN_SPECIAL_CHARACTERS.getDescription()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 名称是否存在 |      * 名称是否存在 | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ import io.swagger.v3.oas.annotations.Operation; | |||||||
| import io.swagger.v3.oas.annotations.Parameter; | import io.swagger.v3.oas.annotations.Parameter; | ||||||
| import io.swagger.v3.oas.annotations.enums.ParameterIn; | import io.swagger.v3.oas.annotations.enums.ParameterIn; | ||||||
| import io.swagger.v3.oas.annotations.tags.Tag; | import io.swagger.v3.oas.annotations.tags.Tag; | ||||||
|  | import jakarta.servlet.http.HttpServletRequest; | ||||||
| import lombok.RequiredArgsConstructor; | import lombok.RequiredArgsConstructor; | ||||||
| import org.springframework.validation.annotation.Validated; | import org.springframework.validation.annotation.Validated; | ||||||
| import org.springframework.web.bind.annotation.*; | import org.springframework.web.bind.annotation.*; | ||||||
| @@ -68,7 +69,7 @@ public class AuthController { | |||||||
|     @SaIgnore |     @SaIgnore | ||||||
|     @Operation(summary = "账号登录", description = "根据账号和密码进行登录认证") |     @Operation(summary = "账号登录", description = "根据账号和密码进行登录认证") | ||||||
|     @PostMapping("/account") |     @PostMapping("/account") | ||||||
|     public R<LoginResp> accountLogin(@Validated @RequestBody AccountLoginReq loginReq) { |     public R<LoginResp> accountLogin(@Validated @RequestBody AccountLoginReq loginReq, HttpServletRequest request) { | ||||||
|         String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + loginReq.getUuid(); |         String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + loginReq.getUuid(); | ||||||
|         String captcha = RedisUtils.get(captchaKey); |         String captcha = RedisUtils.get(captchaKey); | ||||||
|         ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); |         ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); | ||||||
| @@ -77,21 +78,7 @@ public class AuthController { | |||||||
|         // 用户登录 |         // 用户登录 | ||||||
|         String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(loginReq.getPassword())); |         String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(loginReq.getPassword())); | ||||||
|         ValidationUtils.throwIfBlank(rawPassword, "密码解密失败"); |         ValidationUtils.throwIfBlank(rawPassword, "密码解密失败"); | ||||||
|         String token = loginService.accountLogin(loginReq.getUsername(), rawPassword); |         String token = loginService.accountLogin(loginReq.getUsername(), rawPassword, request); | ||||||
|         return R.ok(LoginResp.builder().token(token).build()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @SaIgnore |  | ||||||
|     @Operation(summary = "邮箱登录", description = "根据邮箱和验证码进行登录认证") |  | ||||||
|     @PostMapping("/email") |  | ||||||
|     public R<LoginResp> emailLogin(@Validated @RequestBody EmailLoginReq loginReq) { |  | ||||||
|         String email = loginReq.getEmail(); |  | ||||||
|         String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email; |  | ||||||
|         String captcha = RedisUtils.get(captchaKey); |  | ||||||
|         ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); |  | ||||||
|         ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR); |  | ||||||
|         RedisUtils.delete(captchaKey); |  | ||||||
|         String token = loginService.emailLogin(email); |  | ||||||
|         return R.ok(LoginResp.builder().token(token).build()); |         return R.ok(LoginResp.builder().token(token).build()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -109,6 +96,20 @@ public class AuthController { | |||||||
|         return R.ok(LoginResp.builder().token(token).build()); |         return R.ok(LoginResp.builder().token(token).build()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @SaIgnore | ||||||
|  |     @Operation(summary = "邮箱登录", description = "根据邮箱和验证码进行登录认证") | ||||||
|  |     @PostMapping("/email") | ||||||
|  |     public R<LoginResp> emailLogin(@Validated @RequestBody EmailLoginReq loginReq) { | ||||||
|  |         String email = loginReq.getEmail(); | ||||||
|  |         String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email; | ||||||
|  |         String captcha = RedisUtils.get(captchaKey); | ||||||
|  |         ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); | ||||||
|  |         ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR); | ||||||
|  |         RedisUtils.delete(captchaKey); | ||||||
|  |         String token = loginService.emailLogin(email); | ||||||
|  |         return R.ok(LoginResp.builder().token(token).build()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Operation(summary = "用户退出", description = "注销用户的当前登录") |     @Operation(summary = "用户退出", description = "注销用户的当前登录") | ||||||
|     @Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", in = ParameterIn.HEADER) |     @Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", in = ParameterIn.HEADER) | ||||||
|     @PostMapping("/logout") |     @PostMapping("/logout") | ||||||
|   | |||||||
| @@ -61,7 +61,7 @@ public class UserController extends BaseController<UserService, UserResp, UserDe | |||||||
|         String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword())); |         String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword())); | ||||||
|         ValidationUtils.throwIfNull(rawPassword, "密码解密失败"); |         ValidationUtils.throwIfNull(rawPassword, "密码解密失败"); | ||||||
|         ValidationUtils.throwIf(!ReUtil |         ValidationUtils.throwIf(!ReUtil | ||||||
|             .isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字"); |             .isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字"); | ||||||
|         req.setPassword(rawPassword); |         req.setPassword(rawPassword); | ||||||
|         return super.add(req); |         return super.add(req); | ||||||
|     } |     } | ||||||
| @@ -74,7 +74,7 @@ public class UserController extends BaseController<UserService, UserResp, UserDe | |||||||
|         String rawNewPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getNewPassword())); |         String rawNewPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getNewPassword())); | ||||||
|         ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败"); |         ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败"); | ||||||
|         ValidationUtils.throwIf(!ReUtil |         ValidationUtils.throwIf(!ReUtil | ||||||
|             .isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字"); |             .isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字"); | ||||||
|         req.setNewPassword(rawNewPassword); |         req.setNewPassword(rawNewPassword); | ||||||
|         baseService.resetPassword(req, id); |         baseService.resetPassword(req, id); | ||||||
|         return R.ok("重置密码成功"); |         return R.ok("重置密码成功"); | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| -- liquibase formatted sql | -- liquibase formatted sql | ||||||
|  |  | ||||||
| -- changeset Charles7c:2.5.0 | -- changeset Charles7c:1 | ||||||
| -- comment 初始化表数据 | -- comment 初始化表数据 | ||||||
| -- 初始化默认菜单 | -- 初始化默认菜单 | ||||||
| INSERT INTO `sys_menu` | INSERT INTO `sys_menu` | ||||||
| @@ -109,19 +109,20 @@ VALUES | |||||||
| INSERT INTO `sys_option` | INSERT INTO `sys_option` | ||||||
| (`name`, `code`, `value`, `default_value`, `description`, `update_user`, `update_time`) | (`name`, `code`, `value`, `default_value`, `description`, `update_user`, `update_time`) | ||||||
| VALUES | VALUES | ||||||
| ('系统标题', 'site_title', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL), | ('系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL), | ||||||
| ('版权信息', 'site_copyright', NULL, | ('版权信息', 'SITE_COPYRIGHT', NULL, | ||||||
|  'Copyright © 2022-present <a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a> <span>⋅</span> <a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a> <span>⋅</span> <a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-2</a>', |  'Copyright © 2022-present <a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a> <span>⋅</span> <a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a> <span>⋅</span> <a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-3</a>', | ||||||
|  '用于显示登录页面的底部版权信息。', NULL, NULL), |  '用于显示登录页面的底部版权信息。', NULL, NULL), | ||||||
| ('系统LOGO(16*16)', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL), | ('系统LOGO(16*16)', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL), | ||||||
| ('系统LOGO(33*33)', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), | ('系统LOGO(33*33)', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), | ||||||
| ('密码是否允许包含正反序帐户名', 'password_contain_name', NULL, '0', '', NULL, NULL), | ('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-10(0 表示不锁定)。', NULL, NULL), | ||||||
| ('密码错误锁定帐户次数', 'password_error_count', NULL, '5', '0表示不限制。', NULL, NULL), | ('登录密码错误锁定账号的时间(min)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440(一天)。', NULL, NULL), | ||||||
| ('密码有效期', 'password_expiration_days', NULL, '0', '取值范围为0-999,0表示永久有效。', NULL, NULL), | ('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。', NULL, NULL), | ||||||
| ('密码错误锁定帐户的时间', 'password_lock_minutes', NULL, '5', '0表示不解锁。', NULL, NULL), | ('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-999(0 表示永久有效)。', NULL, NULL), | ||||||
| ('密码最小长度', 'password_min_length', NULL, '8', '取值范围为8-32。', NULL, NULL), | ('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL), | ||||||
| ('密码是否必须包含特殊字符', 'password_special_char', NULL, '0', '', NULL, NULL), | ('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL), | ||||||
| ('修改密码最短间隔', 'password_update_interval', NULL, '5', '取值范围为0-9999,0表示不限制。', NULL, NULL); | ('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '0', '', NULL, NULL), | ||||||
|  | ('密码是否必须包含特殊字符', 'PASSWORD_CONTAIN_SPECIAL_CHARACTERS', NULL, '0', '', NULL, NULL); | ||||||
|  |  | ||||||
| -- 初始化默认字典 | -- 初始化默认字典 | ||||||
| INSERT INTO `sys_dict` | INSERT INTO `sys_dict` | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| -- liquibase formatted sql | -- liquibase formatted sql | ||||||
|  |  | ||||||
| -- changeset Charles7c:2.5.0 | -- changeset Charles7c:1 | ||||||
| -- comment 初始化表结构 | -- comment 初始化表结构 | ||||||
| CREATE TABLE IF NOT EXISTS `sys_menu` ( | CREATE TABLE IF NOT EXISTS `sys_menu` ( | ||||||
|     `id`          bigint(20)   NOT NULL                    COMMENT 'ID', |     `id`          bigint(20)   NOT NULL                    COMMENT 'ID', | ||||||
| @@ -256,19 +256,19 @@ CREATE TABLE IF NOT EXISTS `sys_storage` ( | |||||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储表'; | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储表'; | ||||||
|  |  | ||||||
| CREATE TABLE IF NOT EXISTS `sys_file` ( | CREATE TABLE IF NOT EXISTS `sys_file` ( | ||||||
|     `id`            bigint(20)   NOT NULL                    COMMENT 'ID', |     `id`             bigint(20)   NOT NULL                    COMMENT 'ID', | ||||||
|     `name`          varchar(255) NOT NULL                    COMMENT '名称', |     `name`           varchar(255) NOT NULL                    COMMENT '名称', | ||||||
|     `size`          bigint(20)   NOT NULL                    COMMENT '大小(字节)', |     `size`           bigint(20)   NOT NULL                    COMMENT '大小(字节)', | ||||||
|     `url`           varchar(512) NOT NULL                    COMMENT 'URL', |     `url`            varchar(512) NOT NULL                    COMMENT 'URL', | ||||||
|     `extension`     varchar(100) DEFAULT NULL                COMMENT '扩展名', |     `extension`      varchar(100) DEFAULT NULL                COMMENT '扩展名', | ||||||
|     `type`          tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:其他;2:图片;3:文档;4:视频;5:音频)', |     `thumbnail_size` bigint(20)   DEFAULT NULL                COMMENT '缩略图大小(字节)', | ||||||
|     `storage_id`    bigint(20)   NOT NULL                    COMMENT '存储ID', |     `thumbnail_url`  varchar(512) DEFAULT NULL                COMMENT '缩略图URL', | ||||||
|     `create_user`   bigint(20)   NOT NULL                    COMMENT '创建人', |     `type`           tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:其他;2:图片;3:文档;4:视频;5:音频)', | ||||||
|     `create_time`   datetime     NOT NULL                    COMMENT '创建时间', |     `storage_id`     bigint(20)   NOT NULL                    COMMENT '存储ID', | ||||||
|     `update_user`   bigint(20)   NOT NULL                    COMMENT '修改人', |     `create_user`    bigint(20)   NOT NULL                    COMMENT '创建人', | ||||||
|     `update_time`   datetime     NOT NULL                    COMMENT '修改时间', |     `create_time`    datetime     NOT NULL                    COMMENT '创建时间', | ||||||
|     `thumbnail_size` bigint(20) NULL DEFAULT NULL COMMENT '缩略图大小(字节)', |     `update_user`    bigint(20)   NOT NULL                    COMMENT '修改人', | ||||||
|     `thumbnail_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '缩略图URL', |     `update_time`    datetime     NOT NULL                    COMMENT '修改时间', | ||||||
|     PRIMARY KEY (`id`) USING BTREE, |     PRIMARY KEY (`id`) USING BTREE, | ||||||
|     INDEX `idx_url`(`url`) USING BTREE, |     INDEX `idx_url`(`url`) USING BTREE, | ||||||
|     INDEX `idx_type`(`type`) USING BTREE, |     INDEX `idx_type`(`type`) USING BTREE, | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| -- liquibase formatted sql | -- liquibase formatted sql | ||||||
|  |  | ||||||
| -- changeset Charles7c:2.5.0 | -- changeset Charles7c:1 | ||||||
| -- comment 初始化表数据 | -- comment 初始化表数据 | ||||||
| -- 初始化默认菜单 | -- 初始化默认菜单 | ||||||
| INSERT INTO "sys_menu" | INSERT INTO "sys_menu" | ||||||
| @@ -109,19 +109,20 @@ VALUES | |||||||
| INSERT INTO "sys_option" | INSERT INTO "sys_option" | ||||||
| ("name", "code", "value", "default_value", "description", "update_user", "update_time") | ("name", "code", "value", "default_value", "description", "update_user", "update_time") | ||||||
| VALUES | VALUES | ||||||
| ('系统标题', 'site_title', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL), | ('系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。', NULL, NULL), | ||||||
| ('版权信息', 'site_copyright', NULL, | ('版权信息', 'SITE_COPYRIGHT', NULL, | ||||||
|  'Copyright © 2022-present <a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a> <span>⋅</span> <a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a> <span>⋅</span> <a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-2</a>', |  'Copyright © 2022-present <a href="https://blog.charles7c.top/about/me" target="_blank" rel="noopener">Charles7c</a> <span>⋅</span> <a href="https://github.com/Charles7c/continew-admin" target="_blank" rel="noopener">ContiNew Admin</a> <span>⋅</span> <a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">津ICP备2022005864号-3</a>', | ||||||
|  '用于显示登录页面的底部版权信息。', NULL, NULL), |  '用于显示登录页面的底部版权信息。', NULL, NULL), | ||||||
| ('系统LOGO(16*16)', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL), | ('系统LOGO(16*16)', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL), | ||||||
| ('系统LOGO(33*33)', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), | ('系统LOGO(33*33)', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), | ||||||
| ('密码是否允许包含正反序帐户名', 'password_contain_name', NULL, '0', '', NULL, NULL), | ('登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-10(0 表示不锁定)。', NULL, NULL), | ||||||
| ('密码错误锁定帐户次数', 'password_error_count', NULL, '5', '0表示不限制。', NULL, NULL), | ('登录密码错误锁定账号的时间(min)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440(一天)。', NULL, NULL), | ||||||
| ('密码有效期', 'password_expiration_days', NULL, '0', '取值范围为0-999,0表示永久有效。', NULL, NULL), | ('密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。', NULL, NULL), | ||||||
| ('密码错误锁定帐户的时间', 'password_lock_minutes', NULL, '5', '0表示不解锁。', NULL, NULL), | ('密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-999(0 表示永久有效)。', NULL, NULL), | ||||||
| ('密码最小长度', 'password_min_length', NULL, '8', '取值范围为8-32。', NULL, NULL), | ('密码重复使用规则', 'PASSWORD_REUSE_POLICY', NULL, '5', '不允许使用最近 N 次密码,取值范围为 3-32。', NULL, NULL), | ||||||
| ('密码是否必须包含特殊字符', 'password_special_char', NULL, '0', '', NULL, NULL), | ('密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。', NULL, NULL), | ||||||
| ('修改密码最短间隔', 'password_update_interval', NULL, '5', '取值范围为0-9999,0表示不限制。', NULL, NULL); | ('密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '0', '', NULL, NULL), | ||||||
|  | ('密码是否必须包含特殊字符', 'PASSWORD_CONTAIN_SPECIAL_CHARACTERS', NULL, '0', '', NULL, NULL); | ||||||
|  |  | ||||||
| -- 初始化默认字典 | -- 初始化默认字典 | ||||||
| INSERT INTO "sys_dict" | INSERT INTO "sys_dict" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| -- liquibase formatted sql | -- liquibase formatted sql | ||||||
|  |  | ||||||
| -- changeset Charles7c:2.5.0 | -- changeset Charles7c:1 | ||||||
| -- comment 初始化表结构 | -- comment 初始化表结构 | ||||||
| CREATE TABLE IF NOT EXISTS "sys_menu" ( | CREATE TABLE IF NOT EXISTS "sys_menu" ( | ||||||
|     "id"          int8         NOT NULL, |     "id"          int8         NOT NULL, | ||||||
| @@ -430,39 +430,39 @@ COMMENT ON COLUMN "sys_storage"."update_time" IS '修改时间'; | |||||||
| COMMENT ON TABLE  "sys_storage"               IS '存储表'; | COMMENT ON TABLE  "sys_storage"               IS '存储表'; | ||||||
|  |  | ||||||
| CREATE TABLE IF NOT EXISTS "sys_file" ( | CREATE TABLE IF NOT EXISTS "sys_file" ( | ||||||
|     "id"            int8         NOT NULL, |     "id"             int8         NOT NULL, | ||||||
|     "name"          varchar(255) NOT NULL, |     "name"           varchar(255) NOT NULL, | ||||||
|     "size"          int8         NOT NULL, |     "size"           int8         NOT NULL, | ||||||
|     "url"           varchar(512) NOT NULL, |     "url"            varchar(512) NOT NULL, | ||||||
|     "extension"     varchar(100) DEFAULT NULL, |     "extension"      varchar(100) DEFAULT NULL, | ||||||
|     "type"          int2         NOT NULL DEFAULT 1, |     "thumbnail_size" int8         DEFAULT NULL, | ||||||
|     "storage_id"    int8         NOT NULL, |     "thumbnail_url"  varchar(512) DEFAULT NULL, | ||||||
|     "create_user"   int8         NOT NULL, |     "type"           int2         NOT NULL DEFAULT 1, | ||||||
|     "create_time"   timestamp    NOT NULL, |     "storage_id"     int8         NOT NULL, | ||||||
|     "update_user"   int8         NOT NULL, |     "create_user"    int8         NOT NULL, | ||||||
|     "update_time"   timestamp    NOT NULL, |     "create_time"    timestamp    NOT NULL, | ||||||
|     "thumbnail_size"   int8    DEFAULT NULL, |     "update_user"    int8         NOT NULL, | ||||||
|     "thumbnail_url"   varchar(512)    DEFAULT NULL, |     "update_time"    timestamp    NOT NULL, | ||||||
|     PRIMARY KEY ("id") |     PRIMARY KEY ("id") | ||||||
| ); | ); | ||||||
| CREATE INDEX "idx_file_url"  ON "sys_file" ("url"); | CREATE INDEX "idx_file_url"  ON "sys_file" ("url"); | ||||||
| CREATE INDEX "idx_file_type" ON "sys_file" ("type"); | CREATE INDEX "idx_file_type" ON "sys_file" ("type"); | ||||||
| CREATE INDEX "idx_file_create_user" ON "sys_file" ("create_user"); | CREATE INDEX "idx_file_create_user" ON "sys_file" ("create_user"); | ||||||
| CREATE INDEX "idx_file_update_user" ON "sys_file" ("update_user"); | CREATE INDEX "idx_file_update_user" ON "sys_file" ("update_user"); | ||||||
| COMMENT ON COLUMN "sys_file"."id"          IS 'ID'; | COMMENT ON COLUMN "sys_file"."id"             IS 'ID'; | ||||||
| COMMENT ON COLUMN "sys_file"."name"        IS '名称'; | COMMENT ON COLUMN "sys_file"."name"           IS '名称'; | ||||||
| COMMENT ON COLUMN "sys_file"."size"        IS '大小(字节)'; | COMMENT ON COLUMN "sys_file"."size"           IS '大小(字节)'; | ||||||
| COMMENT ON COLUMN "sys_file"."url"         IS 'URL'; | COMMENT ON COLUMN "sys_file"."url"            IS 'URL'; | ||||||
| COMMENT ON COLUMN "sys_file"."extension"   IS '扩展名'; | COMMENT ON COLUMN "sys_file"."extension"      IS '扩展名'; | ||||||
| COMMENT ON COLUMN "sys_file"."type"        IS '类型(1:其他;2:图片;3:文档;4:视频;5:音频)'; |  | ||||||
| COMMENT ON COLUMN "sys_file"."storage_id"  IS '存储ID'; |  | ||||||
| COMMENT ON COLUMN "sys_file"."create_user" IS '创建人'; |  | ||||||
| COMMENT ON COLUMN "sys_file"."create_time" IS '创建时间'; |  | ||||||
| COMMENT ON COLUMN "sys_file"."update_user" IS '修改人'; |  | ||||||
| COMMENT ON COLUMN "sys_file"."update_time" IS '修改时间'; |  | ||||||
| COMMENT ON COLUMN "sys_file"."thumbnail_size" IS '缩略图大小(字节)'; | COMMENT ON COLUMN "sys_file"."thumbnail_size" IS '缩略图大小(字节)'; | ||||||
| COMMENT ON COLUMN "sys_file"."thumbnail_url" IS '缩略图URL'; | COMMENT ON COLUMN "sys_file"."thumbnail_url"  IS '缩略图URL'; | ||||||
| COMMENT ON TABLE  "sys_file"               IS '文件表'; | COMMENT ON COLUMN "sys_file"."type"           IS '类型(1:其他;2:图片;3:文档;4:视频;5:音频)'; | ||||||
|  | COMMENT ON COLUMN "sys_file"."storage_id"     IS '存储ID'; | ||||||
|  | COMMENT ON COLUMN "sys_file"."create_user"    IS '创建人'; | ||||||
|  | COMMENT ON COLUMN "sys_file"."create_time"    IS '创建时间'; | ||||||
|  | COMMENT ON COLUMN "sys_file"."update_user"    IS '修改人'; | ||||||
|  | COMMENT ON COLUMN "sys_file"."update_time"    IS '修改时间'; | ||||||
|  | COMMENT ON TABLE  "sys_file"                  IS '文件表'; | ||||||
|  |  | ||||||
| CREATE TABLE IF NOT EXISTS "gen_config" ( | CREATE TABLE IF NOT EXISTS "gen_config" ( | ||||||
|     "table_name"    varchar(64)  NOT NULL, |     "table_name"    varchar(64)  NOT NULL, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user