草庐IT

SpringCloud微服务实战——搭建企业级开发框架(四十三):多租户可配置的电子邮件发送系统设计与实现

全栈程序猿 2023-03-28 原文

  在日常生活中,邮件已经被聊天软件、短信等更便捷的信息传送方式代替。但在日常工作中,我们的重要的信息通知等非常有必要去归档追溯,那么邮件就是不可或缺的信息传送渠道。对于我们工作中经常用到的系统,里面也基本都集成了邮件发送功能。
  SpringBoot提供了基于JavaMail的starter,我们只要按照官方的说明配置邮件服务器信息,即可使我们的系统拥有发送电子邮件的功能。但是,在我们GitEgg开发框架的实际业务开发过程中,有两个问题需要解决:一个是SpringBoot邮箱服务器的配置是配置在配置文件中的,不支持灵活的界面配置。另外一个是我们的开发框架需要支持多租户,那么此时需要对SpringBoot提供的邮件发送功能进行扩展,以满足我们的需求。

那么,基于以上需求和问题,我们对GitEgg框架进行扩展,增加以下功能:

1、扩展系统配置:将邮箱服务器的配置信息持久化到数据库、Redis缓存,和配置文件一起使用,制定读取优先级。
2、扩展多租户配置:如果系统开启了多租户功能,那么在邮件发送时,首先读取租户的当前配置,如果没有配置,那么在读取系统配置。
3、自有选择服务器:用户可在系统界面上选择指定的邮箱服务器进行邮件发送。
4、提供邮件发送模板:用户可选择预先制定的邮件模板进行发送特定邮件。
5、增加发送数量、频率限制:增加配置,限制模板邮件的发送数量和频率。
6、保存邮件发送记录:不一定把所有附件都保存,只需保存邮件发送关键信息,如果需要保存所有附件等需要自己扩展。

  同一个租户可以配置多个电子邮件服务器,但只可以设置一个服务器为启用状态。默认情况下,系统通知类的功能只使用启用状态的服务器进行邮件发送。在有定制化需求的情况下,比如从页面直接指定某个服务器进行邮件发送,那么提供可以选择的接口,指定某个服务器进行邮件发送。

一、集成spring-boot-starter-mail扩展基础邮件发送功能

1、在基础框架gitegg-platform中新建gitegg-platform-mail子项目,引入邮件必需的相关依赖包。
    <dependencies>
        <!-- gitegg Spring Boot自定义及扩展 -->
        <dependency>
            <groupId>com.gitegg.platform</groupId>
            <artifactId>gitegg-platform-boot</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
            <!-- 去除springboot默认的logback配置-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
2、扩展邮件服务器配置类,增加租户等信息,方便从缓存读取到信息之后进行配置转换。
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class GitEggMailProperties extends MailProperties {

    /**
     * 配置id
     */
    private Long id;

    /**
     * 租户id
     */
    private Long tenantId;

    /**
     * 渠道id
     */
    private String channelCode;

    /**
     * 状态
     */
    private Integer channelStatus;

    /**
     * 配置的md5值
     */
    private String md5;
}
3、扩展邮件发送实现类JavaMailSenderImpl,添加多租户和邮箱服务器编码,便于多租户和渠道选择。
@Data
public class GitEggJavaMailSenderImpl extends JavaMailSenderImpl {

    /**
     * 配置id
     */
    private Long id;

    /**
     * 租户id
     */
    private Long tenantId;

    /**
     * 渠道编码
     */
    private String channelCode;

    /**
     * 配置的md5值
     */
    private String md5;

}
4、新建邮件发送实例工厂类JavaMailSenderFactory,在邮件发送时,根据需求生产需要的邮件发送实例。
@Slf4j
public class JavaMailSenderFactory {

    private RedisTemplate redisTemplate;

    private JavaMailSenderImpl javaMailSenderImpl;

    /**
     * 是否开启租户模式
     */
    private Boolean enable;

    /**
     * JavaMailSender 缓存
     * 尽管存在多个微服务,但是只需要在每个微服务初始化一次即可
     */
    private final static Map<String, GitEggJavaMailSenderImpl> javaMailSenderMap = new ConcurrentHashMap<>();

    public JavaMailSenderFactory(RedisTemplate redisTemplate, JavaMailSenderImpl javaMailSenderImpl, Boolean enable) {
        this.redisTemplate = redisTemplate;
        this.javaMailSenderImpl = javaMailSenderImpl;
        this.enable = enable;
    }

    /**
     * 指定邮件发送渠道
     * @return
     */
    public JavaMailSenderImpl getMailSender(String... channelCode){
        if (null == channelCode || channelCode.length == GitEggConstant.COUNT_ZERO
                || null == channelCode[GitEggConstant.Number.ZERO])
        {
            return this.getDefaultMailSender();
        }
        // 首先判断是否开启多租户
        String mailConfigKey = JavaMailConstant.MAIL_TENANT_CONFIG_KEY;

        if (enable) {
            mailConfigKey += GitEggAuthUtils.getTenantId();
        } else {
            mailConfigKey = JavaMailConstant.MAIL_CONFIG_KEY;
        }

        // 从缓存获取邮件配置信息
        // 根据channel code获取配置,用channel code时,不区分是否是默认配置
        String propertiesStr = (String) redisTemplate.opsForHash().get(mailConfigKey, channelCode[GitEggConstant.Number.ZERO]);
        if (StringUtils.isEmpty(propertiesStr))
        {
            throw new BusinessException("未获取到[" + channelCode[GitEggConstant.Number.ZERO] + "]的邮件配置信息");
        }
        GitEggMailProperties properties = null;
        try {
            properties = JsonUtils.jsonToPojo(propertiesStr, GitEggMailProperties.class);
        } catch (Exception e) {
            log.error("转换邮件配置信息异常:{}", e);
            throw new BusinessException("转换邮件配置信息异常:" + e);
        }
        return this.getMailSender(mailConfigKey, properties);
    }

    /**
     * 不指定邮件发送渠道,取默认配置
     * @return
     */
    public JavaMailSenderImpl getDefaultMailSender(){
        // 首先判断是否开启多租户
        String mailConfigKey = JavaMailConstant.MAIL_TENANT_CONFIG_KEY;

        if (enable) {
            mailConfigKey += GitEggAuthUtils.getTenantId();
        } else {
            mailConfigKey = JavaMailConstant.MAIL_CONFIG_KEY;
        }

        // 获取所有邮件配置列表
        Map<Object, Object> propertiesMap = redisTemplate.opsForHash().entries(mailConfigKey);
        Iterator<Map.Entry<Object, Object>> entries = propertiesMap.entrySet().iterator();
        // 如果没有设置取哪个配置,那么获取默认的配置
        GitEggMailProperties properties = null;
        try {
            while (entries.hasNext()) {
                Map.Entry<Object, Object> entry = entries.next();
                // 转为系统配置对象
                GitEggMailProperties propertiesEnable = JsonUtils.jsonToPojo((String) entry.getValue(), GitEggMailProperties.class);
                if (propertiesEnable.getChannelStatus().intValue() == GitEggConstant.ENABLE) {
                    properties = propertiesEnable;
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return this.getMailSender(mailConfigKey, properties);
    }

    private JavaMailSenderImpl getMailSender(String mailConfigKey, GitEggMailProperties properties) {
        // 根据最新配置信息判断是否从本地获取mailSender,在配置保存时,计算实体配置的md5值,然后进行比较,不要在每次对比的时候进行md5计算
        if (null != properties && !StringUtils.isEmpty(properties.getMd5()))
        {
            GitEggJavaMailSenderImpl javaMailSender = javaMailSenderMap.get(mailConfigKey);
            if (null == javaMailSender || !properties.getMd5().equals(javaMailSender.getMd5()))
            {
                // 如果没有配置信息,那么直接返回系统默认配置的mailSender
                javaMailSender = new GitEggJavaMailSenderImpl();
                this.applyProperties(properties, javaMailSender);
                javaMailSender.setMd5(properties.getMd5());
                javaMailSender.setId(properties.getId());
                // 将MailSender放入缓存
                javaMailSenderMap.put(mailConfigKey, javaMailSender);
            }
            return javaMailSender;
        }
        else
        {
            return this.javaMailSenderImpl;
        }
    }

    private void applyProperties(MailProperties properties, JavaMailSenderImpl sender) {
        sender.setHost(properties.getHost());
        if (properties.getPort() != null) {
            sender.setPort(properties.getPort());
        }

        sender.setUsername(properties.getUsername());
        sender.setPassword(properties.getPassword());
        sender.setProtocol(properties.getProtocol());
        if (properties.getDefaultEncoding() != null) {
            sender.setDefaultEncoding(properties.getDefaultEncoding().name());
        }

        if (!properties.getProperties().isEmpty()) {
            sender.setJavaMailProperties(this.asProperties(properties.getProperties()));
        }

    }

    private Properties asProperties(Map<String, String> source) {
        Properties properties = new Properties();
        properties.putAll(source);
        return properties;
    }
}
5、配置异步邮件发送的线程池,这里需注意异步线程池上下文变量共享问题,有两种方式解决,一个是使用装饰器TaskDecorator将父子线程变量进行复制,还有一种方式是transmittable-thread-local来共享线程上下文,这里不展开描述,后续会专门针对如何在微服务异步线程池中共享上线文进行说明。
@Configuration
public class MailThreadPoolConfig {

    @Value("${spring.mail-task.execution.pool.core-size}")
    private int corePoolSize;

    @Value("${spring.mail-task.execution.pool.max-size}")
    private int maxPoolSize;

    @Value("${spring.mail-task.execution.pool.queue-capacity}")
    private int queueCapacity;

    @Value("${spring.mail-task.execution.thread-name-prefix}")
    private String namePrefix;

    @Value("${spring.mail-task.execution.pool.keep-alive}")
    private int keepAliveSeconds;

    /**
     * 邮件发送的线程池
     * @return
     */
    @Bean("mailTaskExecutor")
    public Executor mailTaskExecutor(){

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        //核心线程数
        executor.setCorePoolSize(corePoolSize);
        //任务队列的大小
        executor.setQueueCapacity(queueCapacity);
        //线程前缀名
        executor.setThreadNamePrefix(namePrefix);
        //线程存活时间
        executor.setKeepAliveSeconds(keepAliveSeconds);

        // 设置装饰器,父子线程共享request header变量
        executor.setTaskDecorator(new RequestHeaderTaskDecorator());

        /**
         * 拒绝处理策略
         * CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
         * AbortPolicy():直接抛出异常。
         * DiscardPolicy():直接丢弃。
         * DiscardOldestPolicy():丢弃队列中最老的任务。
         */
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 线程初始化
        executor.initialize();
        return executor;
    }
}
6、增加邮件发送结果的枚举类MailResultCodeEnum
public enum MailResultCodeEnum {

    /**
     * 默认
     */
    SUCCESS("success", "邮件发送成功"),

    /**
     * 自定义
     */
    ERROR("error", "邮件发送失败");

    public String code;

    public String message;

    MailResultCodeEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

}
7、增加邮箱服务器相关默认配置的常量类JavaMailConstant.java
public class JavaMailConstant {
    /**
     * Redis JavaMail配置config key
     */
    public static final String MAIL_CONFIG_KEY = "mail:config";

    /**
     * 当开启多租户模式时,Redis JavaMail配置config key
     */
    public static final String MAIL_TENANT_CONFIG_KEY = "mail:tenant:config:";
}
8、增加GitEggJavaMail自动装配类,根据Nacos或者系统配置进行装配。
@Slf4j
@Configuration
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class GitEggJavaMailConfiguration {

    private final JavaMailSenderImpl javaMailSenderImpl;
    
    private final RedisTemplate redisTemplate;

    /**
     * 是否开启租户模式
     */
    @Value("${tenant.enable}")
    private Boolean enable;
    
    @Bean
    public JavaMailSenderFactory gitEggAuthRequestFactory() {
        return new JavaMailSenderFactory(redisTemplate, javaMailSenderImpl, enable);
    }
}

二、增加邮箱服务器配置界面

  邮箱服务器的配置,实际就是不同邮箱渠道的配置,这里我们将表和字段设计好,然后使用GitEgg自带代码生成器,生成业务的CRUD代码即可。

1、邮箱渠道配置表设计
CREATE TABLE `t_sys_mail_channel`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户id',
  `channel_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '渠道编码',
  `channel_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '渠道名称',
  `host` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'SMTP服务器地址',
  `port` int(11) NULL DEFAULT NULL COMMENT 'SMTP服务器端口',
  `username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账户名',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
  `protocol` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT '协议',
  `default_encoding` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '默认编码',
  `jndi_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '会话JNDI名称',
  `properties` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'JavaMail 配置',
  `channel_status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '渠道状态 1有效 0禁用',
  `md5` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'MD5',
  `comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
  `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '邮件渠道' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;
2、根据表设计,然后配置代码生成界面,生成前后端代码。

3、生成代码后,进行相关权限配置,前端界面展示:

三、以同样的方式增加邮箱模板配置界面和邮件发送日志记录

1、邮箱模板和邮件发送日志数据库表设计

邮件模板数据库表设计:

CREATE TABLE `t_sys_mail_template`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户id',
  `template_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模板编码',
  `template_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模板名称',
  `sign_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模板签名',
  `template_status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '模板状态',
  `template_type` tinyint(2) NULL DEFAULT NULL COMMENT '模板类型',
  `template_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '模板内容',
  `cache_code_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '缓存key',
  `cache_time_out` bigint(20) NULL DEFAULT 0 COMMENT '缓存有效期 值',
  `cache_time_out_unit` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '缓存有效期 单位',
  `send_times_limit` bigint(20) NULL DEFAULT 0 COMMENT '发送次数限制',
  `send_times_limit_period` bigint(20) NULL DEFAULT 0 COMMENT '限制时间间隔',
  `send_times_limit_period_unit` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '限制时间间隔 单位',
  `comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
  `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '邮件模板' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

邮件日志数据库表设计:

CREATE TABLE `t_sys_mail_log`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户id',
  `channel_id` bigint(20) NULL DEFAULT NULL COMMENT 'mail渠道id',
  `template_id` bigint(20) NULL DEFAULT NULL COMMENT 'mail模板id',
  `mail_subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮件主题',
  `mail_from` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送人',
  `mail_to` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '收件人',
  `mail_cc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '抄送',
  `mail_bcc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '密抄送',
  `mail_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '邮件内容',
  `attachment_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '附件名称',
  `attachment_size` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '附件大小',
  `send_time` datetime(0) NULL DEFAULT NULL COMMENT '发送时间',
  `send_result_code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '发送结果码',
  `send_result_msg` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送结果消息',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建日期',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新日期',
  `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `del_flag` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除 1:删除 0:不删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '邮件记录' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;
2、邮件模板和邮件发送日志界面

四、QQ邮箱配置和阿里云企业邮箱配置测试

  上面的基本功能开发完成之后,那么我们就需要进行测试,这里选择两种类型的邮箱进行测试,一种是QQ邮箱,还有一种是阿里云企业邮箱。

1、QQ邮箱配置

QQ邮箱在配置的时候不能使用QQ的登录密码,需要单独设置QQ邮箱的授权码,下面是操作步骤:

  • 开通qq邮箱的smtp功能



  • 经过一系列的验证之后,会获取到一个授权码:
  • 系统中配置QQ邮箱相关信息
2、 阿里云企业邮箱配置

阿里云企业邮箱的配置相比较而言就简单一些,配置的密码就是企业邮箱登录的密码。

  • 账户设置,开启POP3/SMTP和IMAP/SMTP服务
  • 系统中配置阿里云企业邮箱相关信息
3、Nacos中配置默认邮件服务器,同时增加邮件异步线程池配置
  mail:
    username: XXXXXXXXXXX
    password: XXXXXXXXXX
    default-encoding: UTF-8
    host: smtp.mxhichina.com
    port: 25
    protocol: smtp
    properties:
      mail:
        smtp:
          auth: true
          ssl:
            enable: false
  # 异步发送邮件,核心线程池数配置
  mail-task:
    execution:
      pool:
        core-size: 5
        max-size: 10
        queue-capacity: 5
        keep-alive: 60
      thread-name-prefix: mail-send-task-
4、在邮件渠道配置界面进行邮件发送测试,有两种测试方式,一种是选择指定渠道进行发送,另外一种是选择系统默认渠道进行邮件发送。发送完成后查看邮件日志模块,检查是否有邮件发送成功的记录。
  • 选择需要测试的邮箱服务器

  • 填写测试邮箱发送内容

  • 查看邮箱发送日志

源码地址:

Gitee: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg

有关SpringCloud微服务实战——搭建企业级开发框架(四十三):多租户可配置的电子邮件发送系统设计与实现的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  3. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

  4. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  5. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  6. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  7. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  8. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  9. ruby-on-rails - 设计注册确认 - 2

    我在我的项目中有一个用户和一个管理员角色。我使用Devise创建了身份验证。在我的管理员角色中,我没有任何确认。在我的用户模型中,我有以下内容:devise:database_authenticatable,:confirmable,:recoverable,:rememberable,:trackable,:validatable,:timeoutable,:registerable#Setupaccessible(orprotected)attributesforyourmodelattr_accessible:email,:username,:prename,:surname,:

  10. ruby - 在没有基准或时间的情况下用 Ruby 测量用户时间或系统时间 - 2

    因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实

随机推荐