Skip to content

系统配置模块开发文档

迭代编号: sprint-2
模块: 系统配置模块
状态: ✅ 已完成


开发任务

  • [√] 系统参数配置页面
  • [√] 密码策略配置
  • [√] 登录策略配置
  • [√] 参数热加载机制
  • [√] 参数导入导出
  • [√] 操作日志增强

技术方案

架构设计

┌─────────────────────────────────────────────────────────────┐
│                      系统配置模块                            │
├─────────────────────────────────────────────────────────────┤
│  前端层                                                       │
│  ├── ConfigList.vue            系统参数列表                  │
│  ├── ConfigForm.vue            系统参数表单                  │
│  ├── PasswordPolicy.vue        密码策略配置                  │
│  ├── LoginPolicy.vue           登录策略配置                  │
│  └── OperationLog.vue          操作日志管理                  │
├─────────────────────────────────────────────────────────────┤
│  API层                                                        │
│  ├── ConfigController          系统参数接口                  │
│  ├── PasswordPolicyController  密码策略接口                  │
│  ├── LoginPolicyController     登录策略接口                  │
│  ├── OperationLogController    操作日志接口                  │
│  └── ConfigRefreshService      配置刷新服务                  │
├─────────────────────────────────────────────────────────────┤
│  数据层                                                       │
│  ├── sys_config                系统参数表                    │
│  ├── sys_password_policy       密码策略表                    │
│  ├── sys_login_policy          登录策略表                    │
│  └── sys_operation_log         操作日志表                    │
└─────────────────────────────────────────────────────────────┘

数据库设计

系统参数表 (sys_config)

字段名类型长度必填说明
idbigint20主键ID
config_keyvarchar100参数键
config_valuevarchar500参数值
config_groupvarchar50参数分组
config_typetinyint1类型:1-字符串 2-数字 3-布尔 4-JSON
descriptionvarchar500描述
is_systemtinyint1是否系统参数:0-否 1-是
statustinyint1状态:0-禁用 1-启用
create_timedatetime-创建时间
update_timedatetime-更新时间
create_bybigint20创建人
update_bybigint20更新人
deletedtinyint1删除标记

密码策略表 (sys_password_policy)

字段名类型长度必填说明
idbigint20主键ID
min_lengthint11最小长度
max_lengthint11最大长度
require_uppercasetinyint1需要大写字母
require_lowercasetinyint1需要小写字母
require_digittinyint1需要数字
require_specialtinyint1需要特殊字符
special_charsvarchar50特殊字符集
expiry_daysint11过期天数(0-永不过期)
history_countint11历史密码限制数量
statustinyint1状态
create_timedatetime-创建时间
update_timedatetime-更新时间

登录策略表 (sys_login_policy)

字段名类型长度必填说明
idbigint20主键ID
max_fail_attemptsint11最大失败次数
lock_durationint11锁定时间(分钟)
enable_ip_whitelisttinyint1启用IP白名单
enable_ip_blacklisttinyint1启用IP黑名单
ip_whitelisttext-IP白名单
ip_blacklisttext-IP黑名单
statustinyint1状态
create_timedatetime-创建时间
update_timedatetime-更新时间

参数热加载机制

java
@Configuration
@EnableConfigurationProperties
public class ConfigRefreshConfig {
    
    @Bean
    public ConfigRefreshListener configRefreshListener() {
        return new ConfigRefreshListener();
    }
}

@Component
public class ConfigRefreshListener {
    
    @Autowired
    private ApplicationContext applicationContext;
    
    @Autowired
    private SysConfigMapper configMapper;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 刷新配置
     */
    @EventListener
    public void handleConfigChange(ConfigChangeEvent event) {
        String configKey = event.getConfigKey();
        String configValue = event.getConfigValue();
        
        // 更新缓存
        redisTemplate.opsForValue().set("config:" + configKey, configValue);
        
        // 刷新Spring Environment
        refreshEnvironment(configKey, configValue);
        
        // 发布刷新事件
        applicationContext.publishEvent(new EnvironmentChangeEvent(
            Collections.singleton(configKey)
        ));
    }
    
    /**
     * 获取配置值(带缓存)
     */
    public String getConfigValue(String key, String defaultValue) {
        String cacheKey = "config:" + key;
        String value = (String) redisTemplate.opsForValue().get(cacheKey);
        
        if (value == null) {
            SysConfig config = configMapper.selectByKey(key);
            if (config != null && config.getStatus() == 1) {
                value = config.getConfigValue();
                redisTemplate.opsForValue().set(cacheKey, value, 30, TimeUnit.MINUTES);
            }
        }
        
        return value != null ? value : defaultValue;
    }
}

密码策略校验器

java
@Component
public class PasswordPolicyValidator {
    
    @Autowired
    private SysPasswordPolicyMapper passwordPolicyMapper;
    
    @Autowired
    private SysUserPasswordHistoryMapper passwordHistoryMapper;
    
    /**
     * 校验密码复杂度
     */
    public void validatePassword(String password) {
        SysPasswordPolicy policy = passwordPolicyMapper.selectActivePolicy();
        
        if (policy == null) {
            return; // 无策略,跳过校验
        }
        
        List<String> errors = new ArrayList<>();
        
        // 长度校验
        if (password.length() < policy.getMinLength()) {
            errors.add("密码长度不能少于" + policy.getMinLength() + "位");
        }
        if (password.length() > policy.getMaxLength()) {
            errors.add("密码长度不能超过" + policy.getMaxLength() + "位");
        }
        
        // 字符类型校验
        if (policy.getRequireUppercase() == 1 && !containsUppercase(password)) {
            errors.add("密码必须包含大写字母");
        }
        if (policy.getRequireLowercase() == 1 && !containsLowercase(password)) {
            errors.add("密码必须包含小写字母");
        }
        if (policy.getRequireDigit() == 1 && !containsDigit(password)) {
            errors.add("密码必须包含数字");
        }
        if (policy.getRequireSpecial() == 1 && !containsSpecial(password, policy.getSpecialChars())) {
            errors.add("密码必须包含特殊字符");
        }
        
        if (!errors.isEmpty()) {
            throw new BusinessException("密码不符合策略:" + String.join(";", errors));
        }
    }
    
    /**
     * 检查密码是否过期
     */
    public boolean isPasswordExpired(Long userId) {
        SysPasswordPolicy policy = passwordPolicyMapper.selectActivePolicy();
        
        if (policy == null || policy.getExpiryDays() == 0) {
            return false; // 无过期策略
        }
        
        SysUser user = userMapper.selectById(userId);
        if (user.getPasswordUpdateTime() == null) {
            return true; // 从未修改过密码
        }
        
        LocalDateTime expiryTime = user.getPasswordUpdateTime()
            .plusDays(policy.getExpiryDays());
        
        return LocalDateTime.now().isAfter(expiryTime);
    }
    
    /**
     * 检查历史密码
     */
    public void checkHistoryPassword(Long userId, String newPassword) {
        SysPasswordPolicy policy = passwordPolicyMapper.selectActivePolicy();
        
        if (policy == null || policy.getHistoryCount() == 0) {
            return;
        }
        
        List<String> historyPasswords = passwordHistoryMapper
            .selectRecentPasswords(userId, policy.getHistoryCount());
        
        for (String historyPassword : historyPasswords) {
            if (passwordEncoder.matches(newPassword, historyPassword)) {
                throw new BusinessException("新密码不能与最近" + 
                    policy.getHistoryCount() + "次密码相同");
            }
        }
    }
}

接口定义

系统参数接口

1. 获取参数列表

yaml
接口: GET /api/v1/sys/config/list
描述: 分页查询系统参数
参数:
  - pageNum: 页码
  - pageSize: 每页条数
  - configKey: 参数键
  - configGroup: 参数分组
响应:
  code: 200
  data:
    list: 参数列表
    total: 总条数

2. 创建参数

yaml
接口: POST /api/v1/sys/config
描述: 创建系统参数
请求体:
  configKey: 参数键
  configValue: 参数值
  configGroup: 参数分组
  configType: 参数类型
  description: 描述
响应:
  code: 200
  data: 创建的参数信息

3. 更新参数

yaml
接口: PUT /api/v1/sys/config/{id}
描述: 更新系统参数
请求体: 同创建
响应:
  code: 200
  data: 更新后的参数信息

4. 刷新参数

yaml
接口: POST /api/v1/sys/config/refresh
描述: 刷新参数缓存
请求体:
  configKey: 参数键(不传则刷新全部)
响应:
  code: 200
  message: 刷新成功

5. 导出参数

yaml
接口: GET /api/v1/sys/config/export
描述: 导出系统参数
响应:
  content-type: application/json
  body: JSON文件

6. 导入参数

yaml
接口: POST /api/v1/sys/config/import
描述: 导入系统参数
请求体:
  content-type: multipart/form-data
  file: JSON文件
响应:
  code: 200
  data:
    success: 成功数量
    failed: 失败数量
    errors: 错误列表

密码策略接口

7. 获取密码策略

yaml
接口: GET /api/v1/sys/password-policy
描述: 获取当前密码策略
响应:
  code: 200
  data: 密码策略信息

8. 更新密码策略

yaml
接口: PUT /api/v1/sys/password-policy
描述: 更新密码策略
请求体:
  minLength: 最小长度
  maxLength: 最大长度
  requireUppercase: 需要大写字母
  requireLowercase: 需要小写字母
  requireDigit: 需要数字
  requireSpecial: 需要特殊字符
  expiryDays: 过期天数
  historyCount: 历史密码限制
响应:
  code: 200
  message: 更新成功

登录策略接口

9. 获取登录策略

yaml
接口: GET /api/v1/sys/login-policy
描述: 获取当前登录策略
响应:
  code: 200
  data: 登录策略信息

10. 更新登录策略

yaml
接口: PUT /api/v1/sys/login-policy
描述: 更新登录策略
请求体:
  maxFailAttempts: 最大失败次数
  lockDuration: 锁定时间
  enableIpWhitelist: 启用IP白名单
  enableIpBlacklist: 启用IP黑名单
  ipWhitelist: IP白名单
  ipBlacklist: IP黑名单
响应:
  code: 200
  message: 更新成功

测试用例

系统参数测试用例

用例编号用例名称前置条件测试步骤预期结果状态
TC-CONFIG-001创建参数已登录管理员1. 进入系统参数
2. 点击新增
3. 填写信息
4. 保存
参数创建成功[√] 通过
TC-CONFIG-002修改参数存在参数1. 选择参数
2. 修改值
3. 保存
参数更新成功,热加载生效[√] 通过
TC-CONFIG-003导出参数存在参数1. 点击导出导出JSON文件成功[√] 通过
TC-CONFIG-004导入参数有参数文件1. 点击导入
2. 选择文件
参数导入成功[√] 通过
TC-CONFIG-005参数热加载已修改参数1. 修改参数值
2. 验证功能
新值立即生效[√] 通过

密码策略测试用例

用例编号用例名称测试场景预期结果状态
TC-PWD-001密码长度校验密码长度小于最小值校验失败[√] 通过
TC-PWD-002密码复杂度校验密码缺少必需字符类型校验失败[√] 通过
TC-PWD-003密码过期检查密码超过过期天数提示密码过期[√] 通过
TC-PWD-004历史密码检查使用最近使用过的密码校验失败[√] 通过
TC-PWD-005密码强度检测输入不同复杂度密码正确显示强度等级[√] 通过

登录策略测试用例

用例编号用例名称测试场景预期结果状态
TC-LOGIN-001登录失败锁定连续失败达到上限账户锁定[√] 通过
TC-LOGIN-002自动解锁锁定时间到达自动解锁[√] 通过
TC-LOGIN-003IP白名单非白名单IP访问拒绝访问[√] 通过
TC-LOGIN-004IP黑名单黑名单IP访问拒绝访问[√] 通过

开发记录

日期工作内容完成状态负责人
2026-04-20系统参数表设计[√] 已完成钱七
2026-04-20系统参数CRUD API开发[√] 已完成钱七
2026-04-21参数热加载机制实现[√] 已完成钱七
2026-04-21参数缓存管理[√] 已完成钱七
2026-04-21系统参数页面开发[√] 已完成赵六
2026-04-22参数分组展示优化[√] 已完成赵六
2026-04-22参数导入导出功能[√] 已完成钱七
2026-04-22参数导入导出页面[√] 已完成赵六
2026-04-22密码策略表设计[√] 已完成钱七
2026-04-22密码策略API开发[√] 已完成钱七
2026-04-23密码复杂度校验器[√] 已完成钱七
2026-04-23密码过期检查机制[√] 已完成钱七
2026-04-23密码策略页面开发[√] 已完成赵六
2026-04-23密码强度检测组件[√] 已完成赵六
2026-04-24登录策略表设计[√] 已完成钱七
2026-04-24登录策略API开发[√] 已完成钱七
2026-04-24登录失败锁定机制[√] 已完成钱七
2026-04-24登录策略页面开发[√] 已完成赵六
2026-04-24系统参数单元测试[√] 已完成孙八
2026-04-24密码策略单元测试[√] 已完成孙八

代码片段

登录失败锁定服务

java
@Service
public class LoginLockService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private SysLoginPolicyMapper loginPolicyMapper;
    
    private static final String LOGIN_FAIL_KEY = "login:fail:";
    private static final String LOGIN_LOCK_KEY = "login:lock:";
    
    /**
     * 记录登录失败
     */
    public void recordLoginFail(String username, String ip) {
        SysLoginPolicy policy = loginPolicyMapper.selectActivePolicy();
        if (policy == null || policy.getMaxFailAttempts() == 0) {
            return;
        }
        
        String failKey = LOGIN_FAIL_KEY + username;
        String lockKey = LOGIN_LOCK_KEY + username;
        
        // 增加失败次数
        Long failCount = redisTemplate.opsForValue().increment(failKey);
        redisTemplate.expire(failKey, 30, TimeUnit.MINUTES);
        
        // 检查是否达到锁定阈值
        if (failCount >= policy.getMaxFailAttempts()) {
            // 锁定账户
            redisTemplate.opsForValue().set(lockKey, "1", 
                policy.getLockDuration(), TimeUnit.MINUTES);
            
            // 发送锁定通知
            sendLockNotification(username, ip);
        }
    }
    
    /**
     * 检查账户是否被锁定
     */
    public boolean isLocked(String username) {
        String lockKey = LOGIN_LOCK_KEY + username;
        return Boolean.TRUE.equals(redisTemplate.hasKey(lockKey));
    }
    
    /**
     * 清除登录失败记录
     */
    public void clearLoginFail(String username) {
        String failKey = LOGIN_FAIL_KEY + username;
        redisTemplate.delete(failKey);
    }
    
    /**
     * 手动解锁
     */
    public void unlock(String username) {
        String lockKey = LOGIN_LOCK_KEY + username;
        String failKey = LOGIN_FAIL_KEY + username;
        redisTemplate.delete(lockKey);
        redisTemplate.delete(failKey);
    }
}

前端密码强度检测组件

vue
<template>
  <div class="password-strength">
    <el-input
      v-model="password"
      type="password"
      placeholder="请输入密码"
      @input="checkStrength"
    />
    <div class="strength-bar">
      <div
        class="strength-item"
        :class="getStrengthClass(index)"
        v-for="index in 4"
        :key="index"
      />
    </div>
    <div class="strength-text">{{ strengthText }}</div>
    <ul class="requirement-list">
      <li :class="{ met: hasMinLength }">至少{{ policy.minLength }}位字符</li>
      <li :class="{ met: hasUppercase }">包含大写字母</li>
      <li :class="{ met: hasLowercase }">包含小写字母</li>
      <li :class="{ met: hasDigit }">包含数字</li>
      <li :class="{ met: hasSpecial }">包含特殊字符</li>
    </ul>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const props = defineProps({
  policy: Object
});

const password = ref('');
const strength = ref(0);

const hasMinLength = computed(() => password.value.length >= props.policy.minLength);
const hasUppercase = computed(() => /[A-Z]/.test(password.value));
const hasLowercase = computed(() => /[a-z]/.test(password.value));
const hasDigit = computed(() => /\d/.test(password.value));
const hasSpecial = computed(() => new RegExp(`[${props.policy.specialChars}]`).test(password.value));

const strengthText = computed(() => {
  const texts = ['太短', '弱', '中', '强', '非常强'];
  return texts[strength.value] || '';
});

const checkStrength = () => {
  let score = 0;
  if (hasMinLength.value) score++;
  if (hasUppercase.value && hasLowercase.value) score++;
  if (hasDigit.value) score++;
  if (hasSpecial.value) score++;
  strength.value = score;
};

const getStrengthClass = (index) => {
  if (index <= strength.value) {
    const classes = ['weak', 'fair', 'good', 'strong'];
    return classes[strength.value - 1];
  }
  return '';
};
</script>

<style scoped>
.strength-bar {
  display: flex;
  gap: 4px;
  margin-top: 8px;
}
.strength-item {
  flex: 1;
  height: 4px;
  background: #e0e0e0;
  border-radius: 2px;
}
.strength-item.weak { background: #ff4d4f; }
.strength-item.fair { background: #faad14; }
.strength-item.good { background: #52c41a; }
.strength-item.strong { background: #1890ff; }
.requirement-list li { color: #999; }
.requirement-list li.met { color: #52c41a; }
</style>

问题记录

问题编号问题描述解决方案状态
ISSUE-001参数热加载与Spring配置冲突使用@RefreshScope注解,配合配置中心[√] 已解决
ISSUE-002密码策略实时生效问题策略变更时清除校验器缓存[√] 已解决
ISSUE-003登录锁定分布式一致性问题使用Redis分布式锁,统一锁定状态[√] 已解决

文档创建: 2026-04-20
最后更新: 2026-04-24
负责人: 钱七、赵六

Released under the MIT License.