Commit 71b9104a13fddcd03d37e49c76b9e42f5c3db379
1 parent
d784b113
增加短信验证码的功能
Showing
17 changed files
with
255 additions
and
16 deletions
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.http
| ... | ... | @@ -6,3 +6,12 @@ Content-Type: application/json |
| 6 | 6 | "mobile": "15601691300", |
| 7 | 7 | "password": "admin123" |
| 8 | 8 | } |
| 9 | + | |
| 10 | +### 请求 /send-sms-code 接口 => 成功 | |
| 11 | +POST {{userServerUrl}}/send-sms-code | |
| 12 | +Content-Type: application/json | |
| 13 | + | |
| 14 | +{ | |
| 15 | + "mobile": "15601691300", | |
| 16 | + "scene": 1 | |
| 17 | +} | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java
| ... | ... | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.userserver.modules.system.controller.auth; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | 4 | import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.*; |
| 5 | 5 | import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService; |
| 6 | +import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; | |
| 6 | 7 | import io.swagger.annotations.Api; |
| 7 | 8 | import io.swagger.annotations.ApiImplicitParam; |
| 8 | 9 | import io.swagger.annotations.ApiImplicitParams; |
| ... | ... | @@ -27,6 +28,8 @@ public class SysAuthController { |
| 27 | 28 | |
| 28 | 29 | @Resource |
| 29 | 30 | private SysAuthService authService; |
| 31 | + @Resource | |
| 32 | + private SysSmsCodeService smsCodeService; | |
| 30 | 33 | |
| 31 | 34 | @PostMapping("/login") |
| 32 | 35 | @ApiOperation("使用手机 + 密码登录") |
| ... | ... | @@ -45,10 +48,8 @@ public class SysAuthController { |
| 45 | 48 | @PostMapping("/send-sms-code") |
| 46 | 49 | @ApiOperation("发送手机验证码") |
| 47 | 50 | public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid MbrAuthSendSmsReqVO reqVO) { |
| 48 | -// passportManager.sendSmsCode(sendSmsCodeDTO, HttpUtil.getIp(request)); | |
| 49 | -// // 返回成功 | |
| 50 | -// return success(true); | |
| 51 | - return null; | |
| 51 | + smsCodeService.sendSmsCode(reqVO.getMobile(), reqVO.getScene(), getClientIP()); | |
| 52 | + return success(true); | |
| 52 | 53 | } |
| 53 | 54 | |
| 54 | 55 | @PostMapping("/reset-password") | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthSendSmsReqVO.java
| 1 | 1 | package cn.iocoder.yudao.userserver.modules.system.controller.auth.vo; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.validation.InEnum; | |
| 3 | 4 | import cn.iocoder.yudao.framework.common.validation.Mobile; |
| 5 | +import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum; | |
| 4 | 6 | import io.swagger.annotations.ApiModel; |
| 5 | 7 | import io.swagger.annotations.ApiModelProperty; |
| 6 | 8 | import lombok.Data; |
| ... | ... | @@ -19,6 +21,7 @@ public class MbrAuthSendSmsReqVO { |
| 19 | 21 | |
| 20 | 22 | @ApiModelProperty(value = "发送场景", example = "1", notes = "对应 MbrSmsSceneEnum 枚举") |
| 21 | 23 | @NotNull(message = "发送场景不能为空") |
| 24 | + @InEnum(SysSmsSceneEnum.class) | |
| 22 | 25 | private Integer scene; |
| 23 | 26 | |
| 24 | 27 | } | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/dal/dataobject/package-info.java
0 → 100644
| 1 | +package cn.iocoder.yudao.userserver.modules.system.dal.dataobject; | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/dal/mysql/sms/MbrSmsCodeDO.java renamed to yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/dal/dataobject/sms/SysSmsCodeDO.java
| 1 | -package cn.iocoder.yudao.userserver.modules.member.dal.mysql.sms; | |
| 1 | +package cn.iocoder.yudao.userserver.modules.system.dal.dataobject.sms; | |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 4 | 4 | import com.baomidou.mybatisplus.annotation.TableName; |
| 5 | -import lombok.Data; | |
| 6 | -import lombok.EqualsAndHashCode; | |
| 5 | +import lombok.*; | |
| 7 | 6 | import lombok.experimental.Accessors; |
| 8 | 7 | |
| 9 | 8 | import java.util.Date; |
| ... | ... | @@ -15,11 +14,13 @@ import java.util.Date; |
| 15 | 14 | * |
| 16 | 15 | * @author 芋道源码 |
| 17 | 16 | */ |
| 18 | -@TableName("mbr_sms_code") | |
| 17 | +@TableName("sys_sms_code") | |
| 19 | 18 | @Data |
| 20 | 19 | @EqualsAndHashCode(callSuper = true) |
| 21 | -@Accessors(chain = true) | |
| 22 | -public class MbrSmsCodeDO extends BaseDO { | |
| 20 | +@Builder | |
| 21 | +@NoArgsConstructor | |
| 22 | +@AllArgsConstructor | |
| 23 | +public class SysSmsCodeDO extends BaseDO { | |
| 23 | 24 | |
| 24 | 25 | /** |
| 25 | 26 | * 编号 |
| ... | ... | @@ -36,7 +37,7 @@ public class MbrSmsCodeDO extends BaseDO { |
| 36 | 37 | /** |
| 37 | 38 | * 发送场景 |
| 38 | 39 | * |
| 39 | - * 枚举 {@link MbrSmsCodeDO} | |
| 40 | + * 枚举 {@link SysSmsCodeDO} | |
| 40 | 41 | */ |
| 41 | 42 | private Integer scene; |
| 42 | 43 | /** | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/dal/mysql/package-info.java
0 → 100644
| 1 | +package cn.iocoder.yudao.userserver.modules.system.dal.mysql; | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/dal/mysql/sms/SysSmsCodeMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.userserver.modules.system.dal.mysql.sms; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | |
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 5 | +import cn.iocoder.yudao.userserver.modules.system.dal.dataobject.sms.SysSmsCodeDO; | |
| 6 | +import org.apache.ibatis.annotations.Mapper; | |
| 7 | + | |
| 8 | +@Mapper | |
| 9 | +public interface SysSmsCodeMapper extends BaseMapperX<SysSmsCodeDO> { | |
| 10 | + | |
| 11 | + /** | |
| 12 | + * 获得手机号的最后一个手机验证码 | |
| 13 | + * | |
| 14 | + * @param mobile 手机号 | |
| 15 | + * @param scene 发送场景,选填 | |
| 16 | + * @return 手机验证码 | |
| 17 | + */ | |
| 18 | + default SysSmsCodeDO selectLastByMobile(String mobile, Integer scene) { | |
| 19 | + return selectOne(new QueryWrapperX<SysSmsCodeDO>() | |
| 20 | + .eq("mobile", mobile) | |
| 21 | + .eqIfPresent("scene", scene) | |
| 22 | + .orderByDesc("id") | |
| 23 | + .last("LIMIT 1")); | |
| 24 | + } | |
| 25 | + | |
| 26 | +} | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/dal/redis/package-info.java
0 → 100644
| 1 | +package cn.iocoder.yudao.userserver.modules.system.dal.redis; | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/SysErrorCodeConstants.java
| ... | ... | @@ -14,4 +14,11 @@ public interface SysErrorCodeConstants { |
| 14 | 14 | ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1005000001, "登录失败,账号被禁用"); |
| 15 | 15 | ErrorCode AUTH_LOGIN_FAIL_UNKNOWN = new ErrorCode(1005000002, "登录失败"); // 登录失败的兜底,未知原因 |
| 16 | 16 | |
| 17 | + // ========== SMS CODE 模块 1005001000 ========== | |
| 18 | + ErrorCode USER_SMS_CODE_NOT_FOUND = new ErrorCode(1005001000, "验证码不存在"); | |
| 19 | + ErrorCode USER_SMS_CODE_EXPIRED = new ErrorCode(1005001001, "验证码已过期"); | |
| 20 | + ErrorCode USER_SMS_CODE_USED = new ErrorCode(1005001002, "验证码已使用"); | |
| 21 | + ErrorCode USER_SMS_CODE_NOT_CORRECT = new ErrorCode(1005001003, "验证码不正确"); | |
| 22 | + ErrorCode USER_SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1005001004, "超过每日短信发送数量"); | |
| 23 | + ErrorCode USER_SMS_CODE_SEND_TOO_FAST = new ErrorCode(1005001005, "短信发送过于频率"); | |
| 17 | 24 | } | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/enums/sms/MbrSmsSceneEnum.java renamed to yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/sms/SysSmsSceneEnum.java
| 1 | -package cn.iocoder.yudao.userserver.modules.member.enums.sms; | |
| 1 | +package cn.iocoder.yudao.userserver.modules.system.enums.sms; | |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.core.IntArrayValuable; |
| 4 | 4 | import lombok.AllArgsConstructor; |
| ... | ... | @@ -13,13 +13,13 @@ import java.util.Arrays; |
| 13 | 13 | */ |
| 14 | 14 | @Getter |
| 15 | 15 | @AllArgsConstructor |
| 16 | -public enum MbrSmsSceneEnum implements IntArrayValuable { | |
| 16 | +public enum SysSmsSceneEnum implements IntArrayValuable { | |
| 17 | 17 | |
| 18 | 18 | LOGIN_BY_SMS(1, "手机号登陆"), |
| 19 | 19 | CHANGE_MOBILE_BY_SMS(2, "更换手机号"), |
| 20 | 20 | ; |
| 21 | 21 | |
| 22 | - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(MbrSmsSceneEnum::getScene).toArray(); | |
| 22 | + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SysSmsSceneEnum::getScene).toArray(); | |
| 23 | 23 | |
| 24 | 24 | private final Integer scene; |
| 25 | 25 | private final String name; | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/framework/package-info.java
0 → 100644
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/framework/sms/SmsCodeConfiguration.java
0 → 100644
| 1 | +package cn.iocoder.yudao.userserver.modules.system.framework.sms; | |
| 2 | + | |
| 3 | +import org.springframework.boot.context.properties.EnableConfigurationProperties; | |
| 4 | +import org.springframework.context.annotation.Configuration; | |
| 5 | + | |
| 6 | +@Configuration | |
| 7 | +@EnableConfigurationProperties(SmsCodeProperties.class) | |
| 8 | +public class SmsCodeConfiguration { | |
| 9 | +} | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/framework/sms/SmsCodeProperties.java
0 → 100644
| 1 | +package cn.iocoder.yudao.userserver.modules.system.framework.sms; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | +import org.springframework.boot.context.properties.ConfigurationProperties; | |
| 5 | +import org.springframework.validation.annotation.Validated; | |
| 6 | + | |
| 7 | +import javax.validation.constraints.NotEmpty; | |
| 8 | +import javax.validation.constraints.NotNull; | |
| 9 | +import java.time.Duration; | |
| 10 | +import java.util.Collection; | |
| 11 | + | |
| 12 | +@ConfigurationProperties(prefix = "yudao.sms-code") | |
| 13 | +@Validated | |
| 14 | +@Data | |
| 15 | +public class SmsCodeProperties { | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * 过期时间 | |
| 19 | + */ | |
| 20 | + @NotNull(message = "过期时间不能为空") | |
| 21 | + private Duration expireTimes; | |
| 22 | + /** | |
| 23 | + * 短信发送频率 | |
| 24 | + */ | |
| 25 | + @NotNull(message = "短信发送频率不能为空") | |
| 26 | + private Duration sendFrequency; | |
| 27 | + /** | |
| 28 | + * 每日发送最大数量 | |
| 29 | + */ | |
| 30 | + @NotNull(message = "每日发送最大数量不能为空") | |
| 31 | + private Integer sendMaximumQuantityPerDay; | |
| 32 | + /** | |
| 33 | + * 验证码最小值 | |
| 34 | + */ | |
| 35 | + @NotNull(message = "验证码最小值不能为空") | |
| 36 | + private Integer beginCode; | |
| 37 | + /** | |
| 38 | + * 验证码最大值 | |
| 39 | + */ | |
| 40 | + @NotNull(message = "验证码最大值不能为空") | |
| 41 | + private Integer endCode; | |
| 42 | + | |
| 43 | +} | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/SysSmsCodeService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.userserver.modules.system.service.sms; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 4 | +import cn.iocoder.yudao.framework.common.validation.Mobile; | |
| 5 | +import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * 短信验证码 Service 接口 | |
| 9 | + * | |
| 10 | + * @author 芋道源码 | |
| 11 | + */ | |
| 12 | +public interface SysSmsCodeService { | |
| 13 | + | |
| 14 | + /** | |
| 15 | + * 创建短信验证码,并进行发送 | |
| 16 | + * | |
| 17 | + * @param mobile 手机号 | |
| 18 | + * @param scene 发送场景 {@link SysSmsSceneEnum} | |
| 19 | + * @param createIp 发送 IP | |
| 20 | + */ | |
| 21 | + void sendSmsCode(@Mobile String mobile, Integer scene, String createIp); | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * 验证短信验证码,并进行使用 | |
| 25 | + * 如果正确,则将验证码标记成已使用 | |
| 26 | + * 如果错误,则抛出 {@link ServiceException} 异常 | |
| 27 | + * | |
| 28 | + * @param mobile 手机号 | |
| 29 | + * @param scene 发送场景 | |
| 30 | + * @param code 验证码 | |
| 31 | + * @param usedIp 使用 IP | |
| 32 | + */ | |
| 33 | + void useSmsCode(@Mobile String mobile, Integer scene, String code, String usedIp); | |
| 34 | + | |
| 35 | +} | ... | ... |
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.userserver.modules.system.service.sms.impl; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.userserver.modules.system.dal.dataobject.sms.SysSmsCodeDO; | |
| 4 | +import cn.iocoder.yudao.userserver.modules.system.dal.mysql.sms.SysSmsCodeMapper; | |
| 5 | +import cn.iocoder.yudao.userserver.modules.system.framework.sms.SmsCodeProperties; | |
| 6 | +import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; | |
| 7 | +import org.springframework.stereotype.Service; | |
| 8 | +import org.springframework.validation.annotation.Validated; | |
| 9 | + | |
| 10 | +import javax.annotation.Resource; | |
| 11 | +import java.util.Date; | |
| 12 | + | |
| 13 | +import static cn.hutool.core.util.RandomUtil.randomInt; | |
| 14 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 15 | +import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConstants.*; | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * 短信验证码 Service 实现类 | |
| 19 | + * | |
| 20 | + * @author 芋道源码 | |
| 21 | + */ | |
| 22 | +@Service | |
| 23 | +@Validated | |
| 24 | +public class SysSmsCodeServiceImpl implements SysSmsCodeService { | |
| 25 | + | |
| 26 | + @Resource | |
| 27 | + private SmsCodeProperties smsCodeProperties; | |
| 28 | + | |
| 29 | + @Resource | |
| 30 | + private SysSmsCodeMapper smsCodeMapper; | |
| 31 | + | |
| 32 | + @Override | |
| 33 | + public void sendSmsCode(String mobile, Integer scene, String createIp) { | |
| 34 | + // 创建验证码 | |
| 35 | + String code = this.createSmsCode(mobile, scene, createIp); | |
| 36 | + // 发送验证码 | |
| 37 | + // TODO 芋艿:重要,发送短信验证码 | |
| 38 | + } | |
| 39 | + | |
| 40 | + private String createSmsCode(String mobile, Integer scene, String ip) { | |
| 41 | + // 校验是否可以发送验证码,不用筛选场景 | |
| 42 | + SysSmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, null); | |
| 43 | + if (lastSmsCode != null) { | |
| 44 | + if (lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。 | |
| 45 | + throw exception(USER_SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY); | |
| 46 | + } | |
| 47 | + if (System.currentTimeMillis() - lastSmsCode.getCreateTime().getTime() | |
| 48 | + < smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁 | |
| 49 | + throw exception(USER_SMS_CODE_SEND_TOO_FAST); | |
| 50 | + } | |
| 51 | + // TODO 芋艿:提升,每个 IP 每天可发送数量 | |
| 52 | + // TODO 芋艿:提升,每个 IP 每小时可发送数量 | |
| 53 | + } | |
| 54 | + | |
| 55 | + // 创建验证码记录 | |
| 56 | + String code = String.valueOf(randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1)); | |
| 57 | + SysSmsCodeDO newSmsCode = SysSmsCodeDO.builder().mobile(mobile).code(code) | |
| 58 | + .scene(scene).todayIndex(lastSmsCode != null ? lastSmsCode.getTodayIndex() + 1 : 1) | |
| 59 | + .createIp(ip).used(false).build(); | |
| 60 | + smsCodeMapper.insert(newSmsCode); | |
| 61 | + return code; | |
| 62 | + } | |
| 63 | + | |
| 64 | + @Override | |
| 65 | + public void useSmsCode(String mobile, Integer scene, String code, String usedIp) { | |
| 66 | + // 校验验证码 | |
| 67 | + SysSmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, scene); | |
| 68 | + if (lastSmsCode == null) { // 若验证码不存在,抛出异常 | |
| 69 | + throw exception(USER_SMS_CODE_NOT_FOUND); | |
| 70 | + } | |
| 71 | + if (System.currentTimeMillis() - lastSmsCode.getCreateTime().getTime() | |
| 72 | + >= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期 | |
| 73 | + throw exception(USER_SMS_CODE_EXPIRED); | |
| 74 | + } | |
| 75 | + if (lastSmsCode.getUsed()) { // 验证码已使用 | |
| 76 | + throw exception(USER_SMS_CODE_USED); | |
| 77 | + } | |
| 78 | + if (!lastSmsCode.getCode().equals(code)) { | |
| 79 | + throw exception(USER_SMS_CODE_NOT_CORRECT); | |
| 80 | + } | |
| 81 | + | |
| 82 | + // 使用验证码 | |
| 83 | + smsCodeMapper.updateById(SysSmsCodeDO.builder().id(lastSmsCode.getId()) | |
| 84 | + .used(true).usedTime(new Date()).usedIp(usedIp).build()); | |
| 85 | + } | |
| 86 | + | |
| 87 | +} | ... | ... |
yudao-user-server/src/main/resources/application.yaml
| ... | ... | @@ -58,5 +58,13 @@ yudao: |
| 58 | 58 | db-schemas: ${spring.datasource.dynamic.datasource.master.name} |
| 59 | 59 | error-code: # 错误码相关配置项 |
| 60 | 60 | constants-class-list: |
| 61 | + - cn.iocoder.yudao.userserver.modules.member.enums.MbrErrorCodeConstants | |
| 62 | + - cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConstants | |
| 63 | + sms-code: # 短信验证码相关的配置项 | |
| 64 | + expire-times: 10m | |
| 65 | + send-frequency: 1m | |
| 66 | + send-maximum-quantity-per-day: 10 | |
| 67 | + begin-code: 9999 # 这里配置 9999 的原因是,测试方便。 | |
| 68 | + end-code: 9999 # 这里配置 9999 的原因是,测试方便。 | |
| 61 | 69 | |
| 62 | 70 | debug: false | ... | ... |
更新日志.md
| ... | ... | @@ -10,13 +10,14 @@ |
| 10 | 10 | ## [v1.1.1] 待定 |
| 11 | 11 | |
| 12 | 12 | * 支付 |
| 13 | +* 用户前台的社交登陆 | |
| 13 | 14 | |
| 14 | -## [v1.1.0] 待定 | |
| 15 | +## [v1.1.0] 进行中 | |
| 15 | 16 | |
| 16 | 17 | * 新增管理后台的企业微信、钉钉等社交登录 |
| 17 | 18 | * 新增用户前台(例如说,用户使用的小程序)的后端项目 `yudao-user-server` |
| 18 | 19 | * 新增公共服务 `yudao-core-service` 项目,通过 Jar 包的方式,提供 `yudao-user-server` 和 `yudao-admin-server` 的共享逻辑的复用。 |
| 19 | -* 新增用户前台的手机登录、验证码登录、TODO | |
| 20 | +* 新增用户前台的手机登录、验证码登录 | |
| 20 | 21 | * 修复管理后台的用户头像上传 404 的问题 |
| 21 | 22 | |
| 22 | 23 | ## [v1.0.0] 2021.05.03 | ... | ... |