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

  本篇文章为你整理了SpringCloud微服务实战——搭建企业级开发框架(四十三):多租户可配置的电子邮件发送系统设计与实现()的详细内容,包含有 SpringCloud微服务实战——搭建企业级开发框架(四十三):多租户可配置的电子邮件发送系统设计与实现,希望能帮助你了解 SpringCloud微服务实战——搭建企业级开发框架(四十三):多租户可配置的电子邮件发送系统设计与实现。

    在日常生活中,邮件已经被聊天软件、短信等更便捷的信息传送方式代替。但在日常工作中,我们的重要的信息通知等非常有必要去归档追溯,那么邮件就是不可或缺的信息传送渠道。对于我们工作中经常用到的系统,里面也基本都集成了邮件发送功能。
 

    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微服务实战——搭建企业级开发框架(四十三):多租户可配置的电子邮件发送系统设计与实现()的详细内容,想要了解更多 SpringCloud微服务实战——搭建企业级开发框架(四十三):多租户可配置的电子邮件发送系统设计与实现的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: