diff --git a/docs/app_config.sql b/docs/app_config.sql new file mode 100644 index 0000000..dfe7f81 --- /dev/null +++ b/docs/app_config.sql @@ -0,0 +1,19 @@ +-- 应用配置表 +CREATE TABLE app_config ( + config_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '配置ID', + website_id INT NOT NULL COMMENT '应用ID', + config_key VARCHAR(100) NOT NULL COMMENT '配置键', + config_value TEXT NOT NULL COMMENT '配置值(JSON或字符串)', + config_type VARCHAR(50) NOT NULL DEFAULT 'general' COMMENT '配置类型:general/api/callback/wechat/payment/git等', + is_encrypted TINYINT DEFAULT 0 COMMENT '是否加密 0否 1是', + is_secret TINYINT DEFAULT 0 COMMENT '是否敏感信息 0否 1是', + description VARCHAR(500) DEFAULT '' COMMENT '配置说明', + sort_number INT DEFAULT 0 COMMENT '排序号', + tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户id', + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + deleted TINYINT DEFAULT 0 COMMENT '是否删除 0否 1是', + UNIQUE KEY uk_website_key (website_id, config_key, deleted), + INDEX idx_website_type (website_id, config_type), + INDEX idx_tenant (tenant_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用配置表'; diff --git a/src/main/java/com/gxwebsoft/app/controller/AppConfigController.java b/src/main/java/com/gxwebsoft/app/controller/AppConfigController.java new file mode 100644 index 0000000..39fab56 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/controller/AppConfigController.java @@ -0,0 +1,129 @@ +package com.gxwebsoft.app.controller; + +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.app.entity.AppConfig; +import com.gxwebsoft.app.param.AppConfigParam; +import com.gxwebsoft.app.service.AppConfigService; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.PageResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * 应用配置表 Controller + */ +@Slf4j +@Tag(name = "应用配置管理") +@RestController +@RequestMapping("/api/app/app-config") +public class AppConfigController extends BaseController { + + @Resource + private AppConfigService appConfigService; + + /** + * 分页查询应用配置 + */ + @Operation(summary = "分页查询应用配置") + @GetMapping("/page") + public ApiResult> page(AppConfigParam param) { + return success(new PageResult<>(appConfigService.page(param))); + } + + /** + * 获取应用配置列表 + */ + @Operation(summary = "获取应用配置列表") + @GetMapping() + public ApiResult> list(AppConfigParam param) { + return success(appConfigService.list(param)); + } + + /** + * 根据应用ID获取配置映射 + */ + @Operation(summary = "根据应用ID获取配置映射") + @GetMapping("/map/{websiteId}") + public ApiResult> getConfigsByWebsiteId(@PathVariable Integer websiteId) { + return success(appConfigService.getConfigsByWebsiteId(websiteId)); + } + + /** + * 获取单个配置值 + */ + @Operation(summary = "获取单个配置值") + @GetMapping("/value") + public ApiResult getConfigValue(@RequestParam Integer websiteId, @RequestParam String configKey) { + return success(appConfigService.getConfigValue(websiteId, configKey),null); + } + + /** + * 保存配置 + */ + @Operation(summary = "保存配置") + @PostMapping() + public ApiResult save(@RequestBody AppConfig config) { + appConfigService.saveConfig(config); + return success("保存成功"); + } + + /** + * 批量保存配置 + */ + @Operation(summary = "批量保存配置") + @PostMapping("/batch") + public ApiResult batchSave(@RequestBody BatchSaveRequest request) { + appConfigService.batchSaveConfig(request.getWebsiteId(), request.getConfigs()); + return success("保存成功"); + } + + /** + * 更新配置 + */ + @Operation(summary = "更新配置") + @PutMapping() + public ApiResult update(@RequestBody AppConfig config) { + appConfigService.updateConfig(config); + return success("更新成功"); + } + + /** + * 删除配置 + */ + @Operation(summary = "删除配置") + @DeleteMapping("/{configId}") + public ApiResult delete(@PathVariable Integer configId) { + appConfigService.deleteConfig(configId); + return success("删除成功"); + } + + /** + * 批量保存请求对象 + */ + public static class BatchSaveRequest { + private Integer websiteId; + private List configs; + + public Integer getWebsiteId() { + return websiteId; + } + + public void setWebsiteId(Integer websiteId) { + this.websiteId = websiteId; + } + + public List getConfigs() { + return configs; + } + + public void setConfigs(List configs) { + this.configs = configs; + } + } +} diff --git a/src/main/java/com/gxwebsoft/app/entity/AppConfig.java b/src/main/java/com/gxwebsoft/app/entity/AppConfig.java new file mode 100644 index 0000000..d8c77df --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/entity/AppConfig.java @@ -0,0 +1,87 @@ +package com.gxwebsoft.app.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * 应用配置表 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("app_config") +public class AppConfig implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 配置ID + */ + @TableId(value = "config_id", type = IdType.AUTO) + private Integer configId; + + /** + * 应用ID + */ + private Integer websiteId; + + /** + * 配置键 + */ + private String configKey; + + /** + * 配置值(JSON或字符串) + */ + private String configValue; + + /** + * 配置类型:general/api/callback/wechat/payment/git等 + */ + private String configType; + + /** + * 是否加密 0否 1是 + */ + private Integer isEncrypted; + + /** + * 是否敏感信息 0否 1是 + */ + private Integer isSecret; + + /** + * 配置说明 + */ + private String description; + + /** + * 排序号 + */ + private Integer sortNumber; + + /** + * 租户id + */ + private Long tenantId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private Long createdTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long updatedTime; + + /** + * 是否删除 0否 1是 + */ + @TableLogic + private Integer deleted; +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/AppConfigMapper.java b/src/main/java/com/gxwebsoft/app/mapper/AppConfigMapper.java new file mode 100644 index 0000000..1b428b4 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/AppConfigMapper.java @@ -0,0 +1,43 @@ +package com.gxwebsoft.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.app.entity.AppConfig; +import com.gxwebsoft.app.param.AppConfigParam; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 应用配置表 Mapper + * + * @author 科技小王子 + */ +@Mapper +public interface AppConfigMapper extends BaseMapper { + + /** + * 批量获取应用配置 + */ + List> selectConfigsByWebsiteId(Integer websiteId); + + /** + * 分页查询 + * + * @param page 分页对象 + * @param param 查询参数 + * @return List + */ + List selectPageRel(@Param("page") IPage page, + @Param("param") AppConfigParam param); + + /** + * 查询全部 + * + * @param param 查询参数 + * @return List + */ + List selectListRel(@Param("param") AppConfigParam param); +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppConfigMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppConfigMapper.xml new file mode 100644 index 0000000..c4679fa --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppConfigMapper.xml @@ -0,0 +1,66 @@ + + + + + + + SELECT a.* + FROM app_config a + + a.deleted = 0 + + AND a.config_id = #{param.configId} + + + AND a.website_id = #{param.websiteId} + + + AND a.config_key LIKE CONCAT('%', #{param.configKey}, '%') + + + AND a.config_type = #{param.configType} + + + AND a.is_secret = #{param.isSecret} + + + AND a.tenant_id = #{param.tenantId} + + + AND a.create_time >= #{param.createTimeStart} + + + AND a.create_time <= #{param.createTimeEnd} + + + AND a.config_key LIKE CONCAT('%', #{param.keywords}, '%') + + + + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/app/param/AppConfigParam.java b/src/main/java/com/gxwebsoft/app/param/AppConfigParam.java new file mode 100644 index 0000000..48dcfc7 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/param/AppConfigParam.java @@ -0,0 +1,38 @@ +package com.gxwebsoft.app.param; + +import com.gxwebsoft.common.core.web.BaseParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 应用配置表查询参数 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class AppConfigParam extends BaseParam { + + /** + * 配置ID + */ + private Integer configId; + + /** + * 应用ID + */ + private Integer websiteId; + + /** + * 配置键 + */ + private String configKey; + + /** + * 配置类型 + */ + private String configType; + + /** + * 是否敏感信息 + */ + private Integer isSecret; +} diff --git a/src/main/java/com/gxwebsoft/app/service/AppConfigService.java b/src/main/java/com/gxwebsoft/app/service/AppConfigService.java new file mode 100644 index 0000000..287d314 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/AppConfigService.java @@ -0,0 +1,58 @@ +package com.gxwebsoft.app.service; + +import com.gxwebsoft.app.entity.AppConfig; +import com.gxwebsoft.app.param.AppConfigParam; + +import java.util.List; +import java.util.Map; + +/** + * 应用配置表 Service + */ +public interface AppConfigService { + + /** + * 分页查询应用配置 + */ + List page(AppConfigParam param); + + /** + * 获取应用配置列表 + */ + List list(AppConfigParam param); + + /** + * 根据应用ID获取配置映射(自动解密) + */ + Map getConfigsByWebsiteId(Integer websiteId); + + /** + * 获取单个配置值 + */ + String getConfigValue(Integer websiteId, String configKey); + + /** + * 保存配置 + */ + void saveConfig(AppConfig config); + + /** + * 批量保存配置 + */ + void batchSaveConfig(Integer websiteId, List configs); + + /** + * 更新配置 + */ + void updateConfig(AppConfig config); + + /** + * 删除配置 + */ + void deleteConfig(Integer configId); + + /** + * 根据应用ID删除所有配置 + */ + void deleteByWebsiteId(Integer websiteId); +} diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppConfigServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppConfigServiceImpl.java new file mode 100644 index 0000000..fa8464e --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppConfigServiceImpl.java @@ -0,0 +1,270 @@ +package com.gxwebsoft.app.service.impl; + +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.symmetric.AES; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.app.entity.AppConfig; +import com.gxwebsoft.app.mapper.AppConfigMapper; +import com.gxwebsoft.app.param.AppConfigParam; +import com.gxwebsoft.app.service.AppConfigService; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.common.system.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 应用配置表 Service 实现类 + * + * @author 科技小王子 + */ +@Slf4j +@Service +public class AppConfigServiceImpl extends ServiceImpl implements AppConfigService { + + @Resource + private UserService userService; + + /** + * 配置加密密钥(从配置文件读取) + */ + @Value("${app.config.encrypt-key:GXWebsoft2024!@#$}") + private String encryptKey; + + /** + * 分页查询应用配置 + */ + @Override + public List page(AppConfigParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("sort_number asc, config_id asc"); + List list = baseMapper.selectPageRel(page, param); + return page.sortRecords(list); + } + + /** + * 分页查询应用配置(返回PageResult) + */ + public PageResult pageRel(AppConfigParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("sort_number asc, config_id asc"); + List list = baseMapper.selectPageRel(page, param); + return new PageResult<>(list, page.getTotal()); + } + + /** + * 获取应用配置列表 + */ + @Override + public List list(AppConfigParam param) { + List list = baseMapper.selectListRel(param); + PageParam page = new PageParam<>(); + page.setDefaultOrder("sort_number asc, config_id asc"); + return page.sortRecords(list); + } + + /** + * 根据应用ID获取配置映射(自动解密) + */ + @Override + public Map getConfigsByWebsiteId(Integer websiteId) { + List> configs = baseMapper.selectConfigsByWebsiteId(websiteId); + Map result = new HashMap<>(); + + for (Map config : configs) { + String configKey = (String) config.get("configKey"); + Object configValue = config.get("configValue"); + Integer isEncrypted = (Integer) config.get("isEncrypted"); + + // 解密 + if (isEncrypted != null && isEncrypted == 1 && configValue != null) { + try { + configValue = decrypt((String) configValue); + } catch (Exception e) { + log.error("配置解密失败: {}", configKey, e); + } + } + + result.put(configKey, configValue); + } + + return result; + } + + /** + * 获取单个配置值 + */ + @Override + public String getConfigValue(Integer websiteId, String configKey) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(AppConfig::getWebsiteId, websiteId); + wrapper.eq(AppConfig::getConfigKey, configKey); + AppConfig config = getOne(wrapper); + + if (config == null || config.getConfigValue() == null) { + return null; + } + + // 解密 + if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1) { + return decrypt(config.getConfigValue()); + } + + return config.getConfigValue(); + } + + /** + * 保存配置 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void saveConfig(AppConfig config) { + // 设置租户ID + Integer tenantId = getCurrentTenantId(); + if (tenantId != null) { + config.setTenantId(Long.valueOf(tenantId)); + } + + // 加密敏感信息 + if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1 && config.getConfigValue() != null) { + config.setConfigValue(encrypt(config.getConfigValue())); + } + + save(config); + } + + /** + * 批量保存配置 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void batchSaveConfig(Integer websiteId, List configs) { + // 设置租户ID + Integer tenantId = getCurrentTenantId(); + if (tenantId != null) { + for (AppConfig config : configs) { + config.setTenantId(Long.valueOf(tenantId)); + } + } + + // 先删除该应用的所有配置 + remove(new LambdaQueryWrapper() + .eq(AppConfig::getWebsiteId, websiteId)); + + // 批量插入新配置 + for (AppConfig config : configs) { + config.setWebsiteId(websiteId); + + // 加密敏感信息 + if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1 && config.getConfigValue() != null) { + config.setConfigValue(encrypt(config.getConfigValue())); + } + + save(config); + } + } + + /** + * 更新配置 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateConfig(AppConfig config) { + // 加密敏感信息 + if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1 && config.getConfigValue() != null) { + config.setConfigValue(encrypt(config.getConfigValue())); + } + + updateById(config); + } + + /** + * 删除配置 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteConfig(Integer configId) { + removeById(configId); + } + + /** + * 根据应用ID删除所有配置 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByWebsiteId(Integer websiteId) { + remove(new LambdaQueryWrapper() + .eq(AppConfig::getWebsiteId, websiteId)); + } + + /** + * 构建查询条件 + */ + private LambdaQueryWrapper buildQueryWrapper(AppConfigParam param) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (param.getConfigId() != null) { + wrapper.eq(AppConfig::getConfigId, param.getConfigId()); + } + if (param.getWebsiteId() != null) { + wrapper.eq(AppConfig::getWebsiteId, param.getWebsiteId()); + } + if (param.getConfigKey() != null) { + wrapper.like(AppConfig::getConfigKey, param.getConfigKey()); + } + if (param.getConfigType() != null) { + wrapper.eq(AppConfig::getConfigType, param.getConfigType()); + } + if (param.getIsSecret() != null) { + wrapper.eq(AppConfig::getIsSecret, param.getIsSecret()); + } + + wrapper.orderByAsc(AppConfig::getConfigType) + .orderByAsc(AppConfig::getSortNumber) + .orderByAsc(AppConfig::getConfigId); + + return wrapper; + } + + /** + * AES 加密 + */ + private String encrypt(String plainText) { + AES aes = SecureUtil.aes(encryptKey.getBytes()); + return aes.encryptBase64(plainText); + } + + /** + * AES 解密 + */ + private String decrypt(String cipherText) { + AES aes = SecureUtil.aes(encryptKey.getBytes()); + return aes.decryptStr(cipherText); + } + + /** + * 获取当前登录用户的租户ID + */ + private Integer getCurrentTenantId() { + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getPrincipal() instanceof User) { + return ((User) authentication.getPrincipal()).getTenantId(); + } + } catch (Exception e) { + log.error("获取当前用户租户ID失败", e); + } + return null; + } +}