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