023-从零搭建微服务-推送服务(三)

原【短信服务】更名【推送服务】

写在最前

如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。

源码地址(后端):gitee.com/csps/mingyu...

源码地址(前端):gitee.com/csps/mingyu...

文档地址:gitee.com/csps/mingyu...

常用邮箱客户端

封装邮件工具

新建模块 mingyue-common-email 邮件模块

新建 Jakarta Mail 配置属性

arduino 复制代码
@Data
@ConfigurationProperties(prefix = "email")
public class EmailProperties {
​
    /**
     * 过滤开关
     */
    private Boolean enabled;
​
    /**
     * SMTP 服务器域名
     */
    private String host;
​
    /**
     * SMTP 服务端口
     */
    private Integer port;
​
    /**
     * 是否需要用户名密码验证
     */
    private Boolean auth;
​
    /**
     * 用户名
     */
    private String user;
​
    /**
     * 密码
     */
    private String pass;
​
    /**
     * 发送方,遵循 RFC-822 标准
     */
    private String from;
​
    /**
     * 使用 STARTTLS 安全连接,STARTTLS 是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
     */
    private Boolean starttlsEnable;
​
    /**
     * 使用 SSL 安全连接
     */
    private Boolean sslEnable;
​
    /**
     * SMTP 超时时长,单位毫秒,缺省值不超时
     */
    private Long timeout;
​
    /**
     * Socket 连接超时值,单位毫秒,缺省值不超时
     */
    private Long connectionTimeout;
​
}

新建 Java Mail 配置类

less 复制代码
@AutoConfiguration
@EnableConfigurationProperties(EmailProperties.class)
public class EmailConfiguration {
​
    @Bean
    @ConditionalOnProperty(value = "email.enabled", havingValue = "true")
    public MailAccount mailAccount(EmailProperties emailProperties) {
        MailAccount account = new MailAccount();
        account.setHost(emailProperties.getHost());
        account.setPort(emailProperties.getPort());
        account.setAuth(emailProperties.getAuth());
        account.setFrom(emailProperties.getFrom());
        account.setUser(emailProperties.getUser());
        account.setPass(emailProperties.getPass());
        account.setSocketFactoryPort(emailProperties.getPort());
        account.setStarttlsEnable(emailProperties.getStarttlsEnable());
        account.setSslEnable(emailProperties.getSslEnable());
        account.setTimeout(emailProperties.getTimeout());
        account.setConnectionTimeout(emailProperties.getConnectionTimeout());
        return account;
    }
​
}

新建邮件工具类

org.springframework.boot.autoconfigure.AutoConfiguration.imports:com.csp.mingyue.common.email.config.EmailConfiguration

typescript 复制代码
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class EmailUtils {
​
    private static final MailAccount ACCOUNT = SpringUtils.getBean(MailAccount.class);
​
    /**
     * 获取邮件发送实例
     */
    public static MailAccount getMailAccount() {
        return ACCOUNT;
    }
​
    /**
     * 获取邮件发送实例 (自定义发送人以及授权码)
     *
     * @param user 发送人
     * @param pass 授权码
     */
    public static MailAccount getMailAccount(String from, String user, String pass) {
        ACCOUNT.setFrom(StrUtil.blankToDefault(from, ACCOUNT.getFrom()));
        ACCOUNT.setUser(StrUtil.blankToDefault(user, ACCOUNT.getUser()));
        ACCOUNT.setPass(StrUtil.blankToDefault(pass, ACCOUNT.getPass()));
        return ACCOUNT;
    }
​
    /**
     * 使用配置文件中设置的账户发送文本邮件,发送给单个或多个收件人<br>
     * 多个收件人可以使用逗号","分隔,也可以通过分号";"分隔
     *
     * @param to      收件人
     * @param subject 标题
     * @param content 正文
     * @param files   附件列表
     * @return message-id
     * @since 3.2.0
     */
    public static String sendText(String to, String subject, String content, File... files) {
        return send(to, subject, content, false, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送HTML邮件,发送给单个或多个收件人<br>
     * 多个收件人可以使用逗号","分隔,也可以通过分号";"分隔
     *
     * @param to      收件人
     * @param subject 标题
     * @param content 正文
     * @param files   附件列表
     * @return message-id
     * @since 3.2.0
     */
    public static String sendHtml(String to, String subject, String content, File... files) {
        return send(to, subject, content, true, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人<br>
     * 多个收件人可以使用逗号","分隔,也可以通过分号";"分隔
     *
     * @param to      收件人
     * @param subject 标题
     * @param content 正文
     * @param isHtml  是否为HTML
     * @param files   附件列表
     * @return message-id
     */
    public static String send(String to, String subject, String content, boolean isHtml, File... files) {
        return send(splitAddress(to), subject, content, isHtml, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人<br>
     * 多个收件人、抄送人、密送人可以使用逗号","分隔,也可以通过分号";"分隔
     *
     * @param to      收件人,可以使用逗号","分隔,也可以通过分号";"分隔
     * @param cc      抄送人,可以使用逗号","分隔,也可以通过分号";"分隔
     * @param bcc     密送人,可以使用逗号","分隔,也可以通过分号";"分隔
     * @param subject 标题
     * @param content 正文
     * @param isHtml  是否为HTML
     * @param files   附件列表
     * @return message-id
     * @since 4.0.3
     */
    public static String send(String to, String cc, String bcc, String subject, String content, boolean isHtml, File... files) {
        return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, isHtml, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送文本邮件,发送给多人
     *
     * @param tos     收件人列表
     * @param subject 标题
     * @param content 正文
     * @param files   附件列表
     * @return message-id
     */
    public static String sendText(Collection<String> tos, String subject, String content, File... files) {
        return send(tos, subject, content, false, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送HTML邮件,发送给多人
     *
     * @param tos     收件人列表
     * @param subject 标题
     * @param content 正文
     * @param files   附件列表
     * @return message-id
     * @since 3.2.0
     */
    public static String sendHtml(Collection<String> tos, String subject, String content, File... files) {
        return send(tos, subject, content, true, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送邮件,发送给多人
     *
     * @param tos     收件人列表
     * @param subject 标题
     * @param content 正文
     * @param isHtml  是否为HTML
     * @param files   附件列表
     * @return message-id
     */
    public static String send(Collection<String> tos, String subject, String content, boolean isHtml, File... files) {
        return send(tos, null, null, subject, content, isHtml, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送邮件,发送给多人
     *
     * @param tos     收件人列表
     * @param ccs     抄送人列表,可以为null或空
     * @param bccs    密送人列表,可以为null或空
     * @param subject 标题
     * @param content 正文
     * @param isHtml  是否为HTML
     * @param files   附件列表
     * @return message-id
     * @since 4.0.3
     */
    public static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) {
        return send(getMailAccount(), true, tos, ccs, bccs, subject, content, null, isHtml, files);
    }
​
    // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount
​
    /**
     * 发送邮件给多人
     *
     * @param mailAccount 邮件认证对象
     * @param to          收件人,多个收件人逗号或者分号隔开
     * @param subject     标题
     * @param content     正文
     * @param isHtml      是否为HTML格式
     * @param files       附件列表
     * @return message-id
     * @since 3.2.0
     */
    public static String send(MailAccount mailAccount, String to, String subject, String content, boolean isHtml, File... files) {
        return send(mailAccount, splitAddress(to), subject, content, isHtml, files);
    }
​
    /**
     * 发送邮件给多人
     *
     * @param mailAccount 邮件帐户信息
     * @param tos         收件人列表
     * @param subject     标题
     * @param content     正文
     * @param isHtml      是否为HTML格式
     * @param files       附件列表
     * @return message-id
     */
    public static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, boolean isHtml, File... files) {
        return send(mailAccount, tos, null, null, subject, content, isHtml, files);
    }
​
    /**
     * 发送邮件给多人
     *
     * @param mailAccount 邮件帐户信息
     * @param tos         收件人列表
     * @param ccs         抄送人列表,可以为null或空
     * @param bccs        密送人列表,可以为null或空
     * @param subject     标题
     * @param content     正文
     * @param isHtml      是否为HTML格式
     * @param files       附件列表
     * @return message-id
     * @since 4.0.3
     */
    public static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, boolean isHtml, File... files) {
        return send(mailAccount, false, tos, ccs, bccs, subject, content, null, isHtml, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送HTML邮件,发送给单个或多个收件人<br>
     * 多个收件人可以使用逗号","分隔,也可以通过分号";"分隔
     *
     * @param to       收件人
     * @param subject  标题
     * @param content  正文
     * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
     * @param files    附件列表
     * @return message-id
     * @since 3.2.0
     */
    public static String sendHtml(String to, String subject, String content, Map<String, InputStream> imageMap, File... files) {
        return send(to, subject, content, imageMap, true, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人<br>
     * 多个收件人可以使用逗号","分隔,也可以通过分号";"分隔
     *
     * @param to       收件人
     * @param subject  标题
     * @param content  正文
     * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
     * @param isHtml   是否为HTML
     * @param files    附件列表
     * @return message-id
     */
    public static String send(String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
        return send(splitAddress(to), subject, content, imageMap, isHtml, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人<br>
     * 多个收件人、抄送人、密送人可以使用逗号","分隔,也可以通过分号";"分隔
     *
     * @param to       收件人,可以使用逗号","分隔,也可以通过分号";"分隔
     * @param cc       抄送人,可以使用逗号","分隔,也可以通过分号";"分隔
     * @param bcc      密送人,可以使用逗号","分隔,也可以通过分号";"分隔
     * @param subject  标题
     * @param content  正文
     * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
     * @param isHtml   是否为HTML
     * @param files    附件列表
     * @return message-id
     * @since 4.0.3
     */
    public static String send(String to, String cc, String bcc, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
        return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, imageMap, isHtml, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送HTML邮件,发送给多人
     *
     * @param tos      收件人列表
     * @param subject  标题
     * @param content  正文
     * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
     * @param files    附件列表
     * @return message-id
     * @since 3.2.0
     */
    public static String sendHtml(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, File... files) {
        return send(tos, subject, content, imageMap, true, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送邮件,发送给多人
     *
     * @param tos      收件人列表
     * @param subject  标题
     * @param content  正文
     * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
     * @param isHtml   是否为HTML
     * @param files    附件列表
     * @return message-id
     */
    public static String send(Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
        return send(tos, null, null, subject, content, imageMap, isHtml, files);
    }
​
    /**
     * 使用配置文件中设置的账户发送邮件,发送给多人
     *
     * @param tos      收件人列表
     * @param ccs      抄送人列表,可以为null或空
     * @param bccs     密送人列表,可以为null或空
     * @param subject  标题
     * @param content  正文
     * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
     * @param isHtml   是否为HTML
     * @param files    附件列表
     * @return message-id
     * @since 4.0.3
     */
    public static String send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
        return send(getMailAccount(), true, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
    }
​
    // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount
​
    /**
     * 发送邮件给多人
     *
     * @param mailAccount 邮件认证对象
     * @param to          收件人,多个收件人逗号或者分号隔开
     * @param subject     标题
     * @param content     正文
     * @param imageMap    图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
     * @param isHtml      是否为HTML格式
     * @param files       附件列表
     * @return message-id
     * @since 3.2.0
     */
    public static String send(MailAccount mailAccount, String to, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
        return send(mailAccount, splitAddress(to), subject, content, imageMap, isHtml, files);
    }
​
    /**
     * 发送邮件给多人
     *
     * @param mailAccount 邮件帐户信息
     * @param tos         收件人列表
     * @param subject     标题
     * @param content     正文
     * @param imageMap    图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
     * @param isHtml      是否为HTML格式
     * @param files       附件列表
     * @return message-id
     * @since 4.6.3
     */
    public static String send(MailAccount mailAccount, Collection<String> tos, String subject, String content, Map<String, InputStream> imageMap, boolean isHtml, File... files) {
        return send(mailAccount, tos, null, null, subject, content, imageMap, isHtml, files);
    }
​
    /**
     * 发送邮件给多人
     *
     * @param mailAccount 邮件帐户信息
     * @param tos         收件人列表
     * @param ccs         抄送人列表,可以为null或空
     * @param bccs        密送人列表,可以为null或空
     * @param subject     标题
     * @param content     正文
     * @param imageMap    图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
     * @param isHtml      是否为HTML格式
     * @param files       附件列表
     * @return message-id
     * @since 4.6.3
     */
    public static String send(MailAccount mailAccount, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content, Map<String, InputStream> imageMap,
                              boolean isHtml, File... files) {
        return send(mailAccount, false, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
    }
​
    /**
     * 根据配置文件,获取邮件客户端会话
     *
     * @param mailAccount 邮件账户配置
     * @param isSingleton 是否单例(全局共享会话)
     * @return {@link Session}
     * @since 5.5.7
     */
    public static Session getSession(MailAccount mailAccount, boolean isSingleton) {
        Authenticator authenticator = null;
        if (mailAccount.isAuth()) {
            authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
        }
​
        return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //
                : Session.getInstance(mailAccount.getSmtpProps(), authenticator);
    }
​
    /**
     * 发送邮件给多人
     *
     * @param mailAccount      邮件帐户信息
     * @param useGlobalSession 是否全局共享Session
     * @param tos              收件人列表
     * @param ccs              抄送人列表,可以为null或空
     * @param bccs             密送人列表,可以为null或空
     * @param subject          标题
     * @param content          正文
     * @param imageMap         图片与占位符,占位符格式为cid:${cid}
     * @param isHtml           是否为HTML格式
     * @param files            附件列表
     * @return message-id
     * @since 4.6.3
     */
    private static String send(MailAccount mailAccount, boolean useGlobalSession, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content,
                               Map<String, InputStream> imageMap, boolean isHtml, File... files) {
        final Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession);
​
        // 可选抄送人
        if (CollUtil.isNotEmpty(ccs)) {
            mail.setCcs(ccs.toArray(new String[0]));
        }
        // 可选密送人
        if (CollUtil.isNotEmpty(bccs)) {
            mail.setBccs(bccs.toArray(new String[0]));
        }
​
        mail.setTos(tos.toArray(new String[0]));
        mail.setTitle(subject);
        mail.setContent(content);
        mail.setHtml(isHtml);
        mail.setFiles(files);
​
        // 图片
        if (MapUtil.isNotEmpty(imageMap)) {
            for (Map.Entry<String, InputStream> entry : imageMap.entrySet()) {
                mail.addImage(entry.getKey(), entry.getValue());
                // 关闭流
                IoUtil.close(entry.getValue());
            }
        }
​
        return mail.send();
    }
​
    /**
     * 将多个联系人转为列表,分隔符为逗号或者分号
     *
     * @param addresses 多个联系人,如果为空返回null
     * @return 联系人列表
     */
    private static List<String> splitAddress(String addresses) {
        if (StrUtil.isBlank(addresses)) {
            return null;
        }
​
        List<String> result;
        if (StrUtil.contains(addresses, CharUtil.COMMA)) {
            result = StrUtil.splitTrim(addresses, CharUtil.COMMA);
        } else if (StrUtil.contains(addresses, ';')) {
            result = StrUtil.splitTrim(addresses, ';');
        } else {
            result = CollUtil.newArrayList(addresses);
        }
        return result;
    }
}

邮件登录

引入依赖

mingyue-push 引入 mingyue-common-email

xml 复制代码
<!-- 邮件工具 -->
<dependency>
    <groupId>com.csp.mingyue</groupId>
    <artifactId>mingyue-common-email</artifactId>
</dependency>

新建邮箱验证码接口

less 复制代码
@Slf4j
@Tag(name = "邮箱服务模块")
@Validated
@RestController
@RequestMapping("email")
@RequiredArgsConstructor
public class EmailController {
​
    private final EmailProperties emailProperties;
​
    /**
     * 邮箱验证码
     *
     * @param email 邮箱
     */
    @GetMapping("/code")
    @Operation(summary = "邮箱验证码", parameters = { @Parameter(name = "email", description = "邮箱", required = true) })
    public R<Void> emailCode(@Valid @NotBlank(message = "邮箱不能为空") String email) {
        if (!emailProperties.getEnabled()) {
            return R.fail("当前系统没有开启邮箱功能!");
        }
        String key = CacheConstants.CAPTCHA_CODE_KEY + email;
        String code = RandomUtil.randomNumbers(4);
        RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
        try {
            EmailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
        } catch (Exception e) {
            log.error("验证码邮箱发送异常 => {}", e.getMessage());
            return R.fail(e.getMessage());
        }
        return R.ok();
    }
​
}

Nacos 放行接口

yaml 复制代码
# 安全配置
security:
  # 不校验白名单
  ignore:
    whites:
      # 放行邮箱验证码
      - /push/email/code

mingyue-push-biz.yml

yaml 复制代码
email:
  enabled: false
  host: smtp.qq.com
  port: 465
  # 是否需要用户名密码验证
  auth: true
  # 发送方,遵循RFC-822标准
  from: xxxxxx@qq.com
  # 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
  user: xxxxxx@qq.com
  # 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
  pass: xxxxxx
  # 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
  starttlsEnable: true
  # 使用SSL安全连接
  sslEnable: true
  # SMTP超时时长,单位毫秒,缺省值不超时
  timeout: 0
  # Socket连接超时值,单位毫秒,缺省值不超时
  connectionTimeout: 0

启动项目发送测试

rust 复制代码
curl -X 'GET' \
  'http://mingyue-gateway:9100/push/email/code?email=xxx@qq.com' \
  -H 'accept: */*'

推送邮箱收到以下信息即可

您本次验证码为:6508,有效性为2分钟,请尽快填写。

短信登录

邮箱登录接口

less 复制代码
/**
 * 邮箱登录
 */
@PostMapping("/emailLogin")
@Operation(summary = "邮箱登录")
public R<String> emailLogin(@RequestBody @Valid EmailLoginDto dto) {
  log.info("------- 进入【邮箱登录】请求: " + SaHolder.getRequest().getUrl());
  // 用户登录
  SaTokenInfo login = sysLoginService.emailLogin(dto);
​
  if (Objects.isNull(login)) {
    return R.fail("登录失败");
  }
​
  return R.ok("登录成功", login.getTokenValue());
}

短信登录逻辑处理

scss 复制代码
public SaTokenInfo emailLogin(EmailLoginDto dto) {
    // 远程调用用户服务
    R<LoginUser> userInfoResp = remoteUserService.userInfoByEmail(dto.getEmail());
    if (userInfoResp.getCode() == Constants.FAIL) {
      throw new UserException(userInfoResp.getMsg());
    }
​
    // 校验验证码是否正确
    if (!checkSmsOrEmailCode(dto.getEmail(), dto.getEmailCode())) {
      throw new UserException("验证码错误");
    }
​
    LoginUser userInfo = userInfoResp.getData();
​
    if (dto.getEmail().equals(userInfo.getEmail())) {
      // 第1步,先登录上
      LoginHelper.login(userInfo);
      // 第2步,获取 Token 相关参数
      SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
​
      return tokenInfo;
    }
​
    return null;
}

mingyue-ui 添加 email 模块

ini 复制代码
<el-tab-pane :label="$t('message.label.two3')" name="email">
  <Email @signInSuccess="signInSuccess"/>
</el-tab-pane>

启动测试

邮件登录需要启动 MingYueGatewayApplication 网关服务MingYueAuthApplication 认证服务MingYueSystemApplication 系统服务MingYuePushApplication 推送服务 以及 mingyue-ui

小结

邮箱登录的功能也加上了,当然不止可以通过邮件发送验证码呦,自己去拓展一下吧,具体看一下 EmailUtils 邮件工具类。

接下来想给 mingyue-ui 写一个增删改查的前后端交互示例 ,就编写用户管理吧!

相关推荐
wn53142 分钟前
【Go - 类型断言】
服务器·开发语言·后端·golang
bjzhang751 小时前
SpringBoot开发——集成Tess4j实现OCR图像文字识别
spring boot·ocr·tess4j
flying jiang1 小时前
Spring Boot 入门面试五道题
spring boot
小菜yh1 小时前
关于Redis
java·数据库·spring boot·redis·spring·缓存
希冀1231 小时前
【操作系统】1.2操作系统的发展与分类
后端
GoppViper2 小时前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
爱上语文3 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
荆州克莱3 小时前
springcloud整合nacos、sentinal、springcloud-gateway,springboot security、oauth2总结
spring boot·spring·spring cloud·css3·技术
serve the people3 小时前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
罗政8 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端