diff --git a/docs/superpowers/plans/2026-06-27-sms-login-config.md b/docs/superpowers/plans/2026-06-27-sms-login-config.md new file mode 100644 index 0000000..40ca4fc --- /dev/null +++ b/docs/superpowers/plans/2026-06-27-sms-login-config.md @@ -0,0 +1,1182 @@ +# 短信登录开关与通用系统配置 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 新建通用系统配置表和管理后台设置页面,通过「短信登录开关」控制小程序登录页是否显示手机号短信登录入口。 + +**Architecture:** 新建 `t_system_config` key-value 配置表,后端提供管理端 CRUD 接口和公开查询接口,管理后台新增「系统设置」页面动态渲染配置项,小程序登录页调用公开接口获取登录方式配置并条件渲染。 + +**Tech Stack:** Spring Boot 2.7 + MyBatis-Plus + Redis / Vue 3 + Element Plus / UniApp (Vue 3) + +**Spec:** `docs/superpowers/specs/2026-06-27-sms-login-config-design.md` + +--- + +### Task 1: 数据库建表和初始数据 + +**Files:** +- Execute: SQL on MySQL database + +- [ ] **Step 1: 执行建表 SQL** + +```sql +CREATE TABLE t_system_config ( + id VARCHAR(64) PRIMARY KEY, + config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置唯一标识', + config_value VARCHAR(500) NOT NULL DEFAULT '' COMMENT '配置值', + value_type VARCHAR(20) NOT NULL DEFAULT 'string' COMMENT '值类型: boolean/string/number/json', + config_group VARCHAR(50) NOT NULL DEFAULT 'system' COMMENT '配置分组: system/login/notification', + config_name VARCHAR(100) NOT NULL DEFAULT '' COMMENT '显示名称', + description VARCHAR(300) DEFAULT '' COMMENT '配置说明', + sort_order INT NOT NULL DEFAULT 0 COMMENT '排序号', + is_visible TINYINT NOT NULL DEFAULT 1 COMMENT '是否在管理后台可见: 0-隐藏, 1-显示', + create_by VARCHAR(64) DEFAULT NULL, + create_time DATETIME DEFAULT NULL, + update_by VARCHAR(64) DEFAULT NULL, + update_time DATETIME DEFAULT NULL, + is_deleted TINYINT NOT NULL DEFAULT 0 +) COMMENT = '系统配置表'; +``` + +- [ ] **Step 2: 插入初始数据(短信登录开关)** + +```sql +INSERT INTO t_system_config (id, config_key, config_value, value_type, config_group, config_name, description, sort_order, is_visible, create_time, update_time, is_deleted) +VALUES ( + REPLACE(UUID(), '-', ''), + 'sms_login_enabled', + 'false', + 'boolean', + 'login', + '短信登录', + '是否启用手机号+短信验证码登录方式', + 1, + 1, + NOW(), + NOW(), + 0 +); +``` + +- [ ] **Step 3: 验证数据** + +```sql +SELECT * FROM t_system_config WHERE is_deleted = 0; +``` + +Expected: 一行数据,`config_key = 'sms_login_enabled'`,`config_value = 'false'` + +--- + +### Task 2: 后端 Entity — SystemConfig.java + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/entity/SystemConfig.java` + +- [ ] **Step 1: 创建 SystemConfig 实体** + +```java +package com.emotion.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.emotion.common.BaseEntity; +import lombok.experimental.SuperBuilder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 系统配置实体 + * + * @author system + * @date 2026-06-27 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@TableName("t_system_config") +public class SystemConfig extends BaseEntity { + + /** + * 配置唯一标识 + */ + @TableField("config_key") + private String configKey; + + /** + * 配置值 + */ + @TableField("config_value") + private String configValue; + + /** + * 值类型: boolean/string/number/json + */ + @TableField("value_type") + private String valueType; + + /** + * 配置分组: system/login/notification + */ + @TableField("config_group") + private String configGroup; + + /** + * 显示名称 + */ + @TableField("config_name") + private String configName; + + /** + * 配置说明 + */ + @TableField("description") + private String description; + + /** + * 排序号 + */ + @TableField("sort_order") + private Integer sortOrder; + + /** + * 是否在管理后台可见: 0-隐藏, 1-显示 + */ + @TableField("is_visible") + private Integer isVisible; +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/entity/SystemConfig.java +git commit -m "feat:新增系统配置实体类" +``` + +--- + +### Task 3: 后端 Mapper — SystemConfigMapper.java + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/mapper/SystemConfigMapper.java` + +- [ ] **Step 1: 创建 SystemConfigMapper** + +```java +package com.emotion.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.emotion.entity.SystemConfig; +import org.apache.ibatis.annotations.Mapper; + +/** + * 系统配置Mapper接口 + * + * @author system + * @date 2026-06-27 + */ +@Mapper +public interface SystemConfigMapper extends BaseMapper { +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/mapper/SystemConfigMapper.java +git commit -m "feat:新增系统配置Mapper" +``` + +--- + +### Task 4: 后端 DTOs + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/dto/request/SystemConfigUpdateRequest.java` +- Create: `backend-single/src/main/java/com/emotion/dto/response/SystemConfigResponse.java` +- Create: `backend-single/src/main/java/com/emotion/dto/response/LoginConfigResponse.java` + +- [ ] **Step 1: 创建 SystemConfigUpdateRequest** + +```java +package com.emotion.dto.request; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 系统配置更新请求 + * + * @author system + * @date 2026-06-27 + */ +@Data +public class SystemConfigUpdateRequest { + + /** + * 配置唯一标识 + */ + @NotBlank(message = "配置标识不能为空") + private String configKey; + + /** + * 配置值 + */ + @NotBlank(message = "配置值不能为空") + private String configValue; +} +``` + +- [ ] **Step 2: 创建 SystemConfigResponse** + +```java +package com.emotion.dto.response; + +import lombok.Data; + +/** + * 系统配置响应 + * + * @author system + * @date 2026-06-27 + */ +@Data +public class SystemConfigResponse { + + /** + * 配置唯一标识 + */ + private String configKey; + + /** + * 配置值 + */ + private String configValue; + + /** + * 值类型: boolean/string/number/json + */ + private String valueType; + + /** + * 配置分组 + */ + private String configGroup; + + /** + * 显示名称 + */ + private String configName; + + /** + * 配置说明 + */ + private String description; + + /** + * 排序号 + */ + private Integer sortOrder; +} +``` + +- [ ] **Step 3: 创建 LoginConfigResponse** + +```java +package com.emotion.dto.response; + +import lombok.Data; + +/** + * 登录方式配置响应 + * 返回给小程序,告知当前启用的登录方式 + * + * @author system + * @date 2026-06-27 + */ +@Data +public class LoginConfigResponse { + + /** + * 是否启用微信登录 + */ + private Boolean wechatLoginEnabled; + + /** + * 是否启用短信登录 + */ + private Boolean smsLoginEnabled; +} +``` + +- [ ] **Step 4: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/dto/request/SystemConfigUpdateRequest.java +git add backend-single/src/main/java/com/emotion/dto/response/SystemConfigResponse.java +git add backend-single/src/main/java/com/emotion/dto/response/LoginConfigResponse.java +git commit -m "feat:新增系统配置相关DTO" +``` + +--- + +### Task 5: 后端 Service 接口 — SystemConfigService.java + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/service/SystemConfigService.java` + +- [ ] **Step 1: 创建 SystemConfigService 接口** + +```java +package com.emotion.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.emotion.dto.request.SystemConfigUpdateRequest; +import com.emotion.dto.response.LoginConfigResponse; +import com.emotion.dto.response.SystemConfigResponse; +import com.emotion.entity.SystemConfig; + +import java.util.List; + +/** + * 系统配置服务接口 + * + * @author system + * @date 2026-06-27 + */ +public interface SystemConfigService extends IService { + + /** + * 获取所有可见配置列表 + */ + List getVisibleConfigList(); + + /** + * 批量更新配置 + */ + void batchUpdateConfigs(List requests); + + /** + * 获取配置值(带 Redis 缓存) + * + * @param configKey 配置标识 + * @return 配置值字符串,不存在返回 null + */ + String getConfigValue(String configKey); + + /** + * 获取登录方式配置 + */ + LoginConfigResponse getLoginConfig(); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/service/SystemConfigService.java +git commit -m "feat:新增系统配置服务接口" +``` + +--- + +### Task 6: 后端 Service 实现 — SystemConfigServiceImpl.java + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/service/impl/SystemConfigServiceImpl.java` + +- [ ] **Step 1: 创建 SystemConfigServiceImpl** + +```java +package com.emotion.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.emotion.dto.request.SystemConfigUpdateRequest; +import com.emotion.dto.response.LoginConfigResponse; +import com.emotion.dto.response.SystemConfigResponse; +import com.emotion.entity.SystemConfig; +import com.emotion.mapper.SystemConfigMapper; +import com.emotion.service.SystemConfigService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 系统配置服务实现类 + * + * @author system + * @date 2026-06-27 + */ +@Slf4j +@Service +public class SystemConfigServiceImpl extends ServiceImpl implements SystemConfigService { + + @Autowired + private RedisTemplate redisTemplate; + + private static final String CACHE_PREFIX = "system_config:"; + private static final long CACHE_EXPIRE_HOURS = 24; + private static final String SMS_LOGIN_ENABLED_KEY = "sms_login_enabled"; + + @Override + public List getVisibleConfigList() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SystemConfig::getIsVisible, 1) + .orderByAsc(SystemConfig::getSortOrder); + List configs = list(wrapper); + return configs.stream().map(this::convertToResponse).collect(Collectors.toList()); + } + + @Override + public void batchUpdateConfigs(List requests) { + for (SystemConfigUpdateRequest request : requests) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(SystemConfig::getConfigKey, request.getConfigKey()) + .set(SystemConfig::getConfigValue, request.getConfigValue()); + update(updateWrapper); + + // 清除缓存 + redisTemplate.delete(CACHE_PREFIX + request.getConfigKey()); + log.info("更新系统配置: key={}, value={}", request.getConfigKey(), request.getConfigValue()); + } + } + + @Override + public String getConfigValue(String configKey) { + // 先查 Redis 缓存 + String cacheKey = CACHE_PREFIX + configKey; + String cached = (String) redisTemplate.opsForValue().get(cacheKey); + if (cached != null) { + return cached; + } + + // 查数据库 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SystemConfig::getConfigKey, configKey); + SystemConfig config = getOne(wrapper); + if (config == null) { + return null; + } + + // 写入缓存 + redisTemplate.opsForValue().set(cacheKey, config.getConfigValue(), CACHE_EXPIRE_HOURS, TimeUnit.HOURS); + return config.getConfigValue(); + } + + @Override + public LoginConfigResponse getLoginConfig() { + LoginConfigResponse response = new LoginConfigResponse(); + response.setWechatLoginEnabled(true); + + String smsEnabled = getConfigValue(SMS_LOGIN_ENABLED_KEY); + response.setSmsLoginEnabled("true".equalsIgnoreCase(smsEnabled)); + + return response; + } + + /** + * 实体转响应对象 + */ + private SystemConfigResponse convertToResponse(SystemConfig config) { + SystemConfigResponse response = new SystemConfigResponse(); + response.setConfigKey(config.getConfigKey()); + response.setConfigValue(config.getConfigValue()); + response.setValueType(config.getValueType()); + response.setConfigGroup(config.getConfigGroup()); + response.setConfigName(config.getConfigName()); + response.setDescription(config.getDescription()); + response.setSortOrder(config.getSortOrder()); + return response; + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/service/impl/SystemConfigServiceImpl.java +git commit -m "feat:新增系统配置服务实现(含Redis缓存)" +``` + +--- + +### Task 7: 后端管理端 Controller — SystemConfigController.java + +**Files:** +- Create: `backend-single/src/main/java/com/emotion/controller/SystemConfigController.java` + +- [ ] **Step 1: 创建 SystemConfigController** + +此 controller 的路径为 `/admin/systemConfig/**`,自动被 `AdminAuthInterceptor` 保护,不需要登录用户 JWT。 + +```java +package com.emotion.controller; + +import com.emotion.common.Result; +import com.emotion.dto.request.SystemConfigUpdateRequest; +import com.emotion.dto.response.SystemConfigResponse; +import com.emotion.service.SystemConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 系统配置管理控制器(管理端) + * + * @author system + * @date 2026-06-27 + */ +@RestController +@RequestMapping("/admin/systemConfig") +@Tag(name = "系统配置管理", description = "系统配置的查询和更新功能") +public class SystemConfigController { + + @Autowired + private SystemConfigService systemConfigService; + + /** + * 获取所有可见配置列表 + */ + @GetMapping("/list") + @Operation(summary = "获取配置列表", description = "获取所有可见的系统配置列表") + public Result> list() { + List configs = systemConfigService.getVisibleConfigList(); + return Result.success(configs); + } + + /** + * 批量更新配置 + */ + @PutMapping("/update") + @Operation(summary = "批量更新配置", description = "批量更新系统配置值") + public Result update(@RequestBody @Validated List requests) { + systemConfigService.batchUpdateConfigs(requests); + return Result.success("更新成功", null); + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/controller/SystemConfigController.java +git commit -m "feat:新增系统配置管理端Controller" +``` + +--- + +### Task 8: 后端公开接口 — AuthController 新增 loginConfig + +**Files:** +- Modify: `backend-single/src/main/java/com/emotion/controller/AuthController.java` + +- [ ] **Step 1: 在 AuthController 中添加 loginConfig 接口** + +在 `AuthController.java` 中添加 `SystemService` 注入和新接口方法。 + +添加 import: +```java +import com.emotion.dto.response.LoginConfigResponse; +``` + +添加 service 注入(在已有的 `@Autowired` 字段后面): +```java +@Autowired +private SystemConfigService systemConfigService; +``` + +添加新接口方法(放在 `wechatLogin` 方法后面): +```java +/** + * 获取登录方式配置 + */ +@GetMapping("/loginConfig") +@Operation(summary = "获取登录方式配置", description = "返回当前启用的登录方式,供小程序登录页动态渲染") +public Result getLoginConfig() { + LoginConfigResponse response = systemConfigService.getLoginConfig(); + return Result.success(response); +} +``` + +- [ ] **Step 2: 在 WebMvcConfig 白名单中放行** + +修改 `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java`,在 JWT 拦截器的 `excludePathPatterns` 中添加一行: + +```java +"/auth/loginConfig", // 登录方式配置接口(免登录) +``` + +添加位置:在 `"/auth/wechat/login"` 行的上方或下方。 + +- [ ] **Step 3: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/controller/AuthController.java +git add backend-single/src/main/java/com/emotion/config/WebMvcConfig.java +git commit -m "feat:新增公开接口 /auth/loginConfig 并放行白名单" +``` + +--- + +### Task 9: 后端编译验证 + +- [ ] **Step 1: 编译后端** + +```bash +cd backend-single +mvn clean install -DskipTests +``` + +Expected: `BUILD SUCCESS` + +- [ ] **Step 2: 如果有编译错误,修复后重新编译直到通过** + +- [ ] **Step 3: Commit(如果有修复)** + +```bash +git add -A +git commit -m "fix:修复系统配置编译错误" +``` + +--- + +### Task 10: 管理后台 — API 和路由配置 + +**Files:** +- Create: `web-admin/src/api/systemConfig.ts` +- Modify: `web-admin/src/router/index.ts` +- Modify: `web-admin/src/config/menu.ts` + +- [ ] **Step 1: 创建 systemConfig API 文件** + +`web-admin/src/api/systemConfig.ts`: + +```ts +import request from '@/utils/request' + +// 获取所有可见配置列表 +export function getSystemConfigList() { + return request({ + url: '/admin/systemConfig/list', + method: 'get' + }) +} + +// 批量更新配置 +export function updateSystemConfig(data: { configKey: string; configValue: string }[]) { + return request({ + url: '/admin/systemConfig/update', + method: 'put', + data + }) +} +``` + +- [ ] **Step 2: 添加路由配置** + +在 `web-admin/src/router/index.ts` 中,在 `dictionary` 路由块之前添加: + +```ts +{ + path: '/system', + component: Layout, + redirect: '/system/settings', + meta: { title: '系统设置', icon: 'Setting' }, + children: [ + { + path: 'settings', + name: 'SystemSettings', + component: () => import('@/views/system/SystemSettings.vue'), + meta: { title: '基础设置' } + } + ] +}, +``` + +- [ ] **Step 3: 添加菜单配置** + +在 `web-admin/src/config/menu.ts` 的 `menuConfig` 数组中,在 `AI配置管理` 项之前添加: + +```ts +{ + path: '/system', + title: '系统设置', + icon: 'Setting' +}, +``` + +- [ ] **Step 4: Commit** + +```bash +git add web-admin/src/api/systemConfig.ts +git add web-admin/src/router/index.ts +git add web-admin/src/config/menu.ts +git commit -m "feat:管理后台新增系统设置路由和菜单" +``` + +--- + +### Task 11: 管理后台 — SystemSettings.vue 页面 + +**Files:** +- Create: `web-admin/src/views/system/SystemSettings.vue` + +- [ ] **Step 1: 创建 SystemSettings 页面** + +```vue + + + + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add web-admin/src/views/system/SystemSettings.vue +git commit -m "feat:管理后台新增系统设置页面" +``` + +--- + +### Task 12: 小程序 — 新增 getLoginConfig 接口 + +**Files:** +- Modify: `mini-program/src/services/auth.js` + +- [ ] **Step 1: 在 auth.js 中添加 getLoginConfig 方法** + +在 `mini-program/src/services/auth.js` 的 `getSmsCode` 方法之前,添加: + +```js +/** + * 获取登录方式配置 + * @returns {Promise} 登录方式配置 + */ +export const getLoginConfig = async () => { + const response = await get('/auth/loginConfig') + return response +} +``` + +- [ ] **Step 2: 在底部的 default 导出对象中添加 `getLoginConfig`** + +```js +export default { + getLoginConfig, + getSmsCode, + login, + wechatLogin, + logout, + refreshToken, + validateToken, + validateTokenStrict, + getCurrentUserInfo, + checkPhone +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add mini-program/src/services/auth.js +git commit -m "feat:小程序新增获取登录方式配置接口" +``` + +--- + +### Task 13: 小程序 — 登录页条件渲染 + +**Files:** +- Modify: `mini-program/src/pages/login/index.vue` + +- [ ] **Step 1: 修改 template — 用 v-if 控制 SMS 相关元素** + +在 `mini-program/src/pages/login/index.vue` 中: + +1. 在「微信一键登录」按钮的 `` 之后,「分隔线」的 `