From 8e0569ee544b26cc6944c7eab6bb92d82bca57f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E5=A4=A9?= <2982176321@qq.com> Date: Wed, 17 Nov 2021 10:15:22 +0800 Subject: [PATCH 01/40] =?UTF-8?q?update=20=E6=96=B0=E5=A2=9E=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=9A=E4=BF=AE=E6=94=B9=E6=89=8B=E6=9C=BA=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81=EF=BC=8C=E5=BF=98=E8=AE=B0?= =?UTF-8?q?=E5=AF=86=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-user-server/pom.xml | 7 + .../user/SysUserProfileController.java | 20 +++ .../user/vo/MbrUserUpdateMobileReqVO.java | 35 +++++ .../member/service/user/MbrUserService.java | 8 ++ .../service/user/impl/MbrUserServiceImpl.java | 16 +++ .../controller/auth/SysAuthController.java | 39 ++++- .../auth/vo/MbrAuthResetPasswordReqVO.java | 23 ++- .../system/enums/SysErrorCodeConstants.java | 3 + .../system/enums/sms/SysSmsSceneEnum.java | 4 +- .../system/service/auth/SysAuthService.java | 19 +++ .../service/auth/impl/SysAuthServiceImpl.java | 79 ++++++++++- .../system/service/sms/SysSmsCodeService.java | 12 ++ .../sms/impl/SysSmsCodeServiceImpl.java | 43 ++++++ .../userserver/BaseDbAndRedisUnitTest.java | 48 +++++++ .../config/RedisTestConfiguration.java | 30 ++++ .../SysUserProfileControllerTest.java | 59 ++++++++ .../service/MbrUserServiceImplTest.java | 60 ++++++-- .../controller/SysAuthControllerTest.java | 75 ++++++++++ .../system/service/SysAuthServiceTest.java | 134 ++++++++++++++++++ 19 files changed, 696 insertions(+), 18 deletions(-) create mode 100644 yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/vo/MbrUserUpdateMobileReqVO.java create mode 100644 yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/BaseDbAndRedisUnitTest.java create mode 100644 yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/config/RedisTestConfiguration.java create mode 100644 yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/controller/SysUserProfileControllerTest.java create mode 100644 yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/controller/SysAuthControllerTest.java create mode 100644 yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java diff --git a/yudao-user-server/pom.xml b/yudao-user-server/pom.xml index e7b1cd82d..b4c2cf8a5 100644 --- a/yudao-user-server/pom.xml +++ b/yudao-user-server/pom.xml @@ -91,6 +91,13 @@ test + + + junit + junit + test + + diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/SysUserProfileController.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/SysUserProfileController.java index 870d33718..39c3d88f3 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/SysUserProfileController.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/SysUserProfileController.java @@ -5,6 +5,9 @@ import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService; +import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO; +import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum; +import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @@ -13,10 +16,12 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; +import javax.validation.Valid; import java.io.IOException; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static cn.iocoder.yudao.userserver.modules.member.enums.MbrErrorCodeConstants.FILE_IS_EMPTY; @@ -30,6 +35,9 @@ public class SysUserProfileController { @Resource private MbrUserService userService; + @Resource + private SysSmsCodeService smsCodeService; + @PutMapping("/update-nickname") @ApiOperation("修改用户昵称") @PreAuthenticated @@ -56,5 +64,17 @@ public class SysUserProfileController { return success(userService.getUserInfo(getLoginUserId())); } + + @PostMapping("/update-mobile") + @ApiOperation(value = "修改用户手机") + @PreAuthenticated + public CommonResult updateMobile(@RequestBody @Valid MbrUserUpdateMobileReqVO reqVO) { + // 校验验证码 + smsCodeService.useSmsCode(reqVO.getMobile(),SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(), reqVO.getCode(),getClientIP()); + + userService.updateMobile(getLoginUserId(), reqVO); + return success(true); + } + } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/vo/MbrUserUpdateMobileReqVO.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/vo/MbrUserUpdateMobileReqVO.java new file mode 100644 index 000000000..0e5499bc7 --- /dev/null +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/vo/MbrUserUpdateMobileReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.userserver.modules.member.controller.user.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@ApiModel("修改手机 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MbrUserUpdateMobileReqVO { + + @ApiModelProperty(value = "手机验证码", required = true, example = "1024") + @NotEmpty(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + + + @ApiModelProperty(value = "手机号",required = true,example = "15823654487") + @NotBlank(message = "手机号不能为空") + @Length(min = 8, max = 11, message = "手机号码长度为 8-11 位") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误") + private String mobile; + +} diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/MbrUserService.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/MbrUserService.java index 6b6a36a8e..e45763dd3 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/MbrUserService.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/MbrUserService.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.userserver.modules.member.service.user; import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO; import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO; import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO; import java.io.InputStream; @@ -69,4 +70,11 @@ public interface MbrUserService { */ MbrUserInfoRespVO getUserInfo(Long userId); + /** + * 修改手机 + * @param userId 用户id + * @param reqVO 请求实体 + */ + void updateMobile(Long userId, MbrUserUpdateMobileReqVO reqVO); + } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/impl/MbrUserServiceImpl.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/impl/MbrUserServiceImpl.java index f340c5fe9..76b4501eb 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/impl/MbrUserServiceImpl.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/impl/MbrUserServiceImpl.java @@ -6,8 +6,10 @@ import cn.iocoder.yudao.coreservice.modules.infra.service.file.InfFileCoreServic import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO; import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO; import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper; import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService; +import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; @@ -40,6 +42,9 @@ public class MbrUserServiceImpl implements MbrUserService { @Resource private PasswordEncoder passwordEncoder; + @Resource + private SysAuthService sysAuthService; + @Override public MbrUserDO getUserByMobile(String mobile) { return userMapper.selectByMobile(mobile); @@ -116,6 +121,17 @@ public class MbrUserServiceImpl implements MbrUserService { return userResp; } + @Override + public void updateMobile(Long userId, MbrUserUpdateMobileReqVO reqVO) { + // 检测用户是否存在 + MbrUserDO userDO = checkUserExists(userId); + // 检测手机与验证码是否匹配 + sysAuthService.checkIfMobileMatchCodeAndDeleteCode(userDO.getMobile(),reqVO.getCode()); + // 更新用户手机 + userDO.setMobile(reqVO.getMobile()); + userMapper.updateById(userDO); + } + @VisibleForTesting public MbrUserDO checkUserExists(Long id) { if (id == null) { diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java index 6d18d53be..a4a4efb87 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java @@ -3,7 +3,9 @@ package cn.iocoder.yudao.userserver.modules.system.controller.auth; import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialService; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.*; +import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum; import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService; import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; import com.alibaba.fastjson.JSON; @@ -56,16 +58,47 @@ public class SysAuthController { } @PostMapping("/send-sms-code") - @ApiOperation("发送手机验证码") + @ApiOperation(value = "发送手机验证码",notes = "不检测该手机号是否已被注册") public CommonResult sendSmsCode(@RequestBody @Valid SysAuthSendSmsReqVO reqVO) { smsCodeService.sendSmsCode(reqVO.getMobile(), reqVO.getScene(), getClientIP()); return success(true); } + @PostMapping("/send-sms-new-code") + @ApiOperation(value = "发送手机验证码",notes = "检测该手机号是否已被注册,用于修改手机时使用") + public CommonResult sendSmsNewCode(@RequestBody @Valid SysAuthSendSmsReqVO reqVO) { + smsCodeService.sendSmsNewCode(reqVO); + return success(true); + } + + @GetMapping("/send-sms-code-login") + @ApiOperation(value = "向已登录用户发送验证码",notes = "修改手机时验证原手机号使用") + public CommonResult sendSmsCodeLogin() { + smsCodeService.sendSmsCodeLogin(getLoginUserId()); + return success(true); + } + @PostMapping("/reset-password") @ApiOperation(value = "重置密码", notes = "用户忘记密码时使用") - public CommonResult resetPassword(@RequestBody @Valid MbrAuthResetPasswordReqVO reqVO) { - return null; + public CommonResult resetPassword(@RequestBody @Validated(MbrAuthResetPasswordReqVO.resetPasswordValidView.class) MbrAuthResetPasswordReqVO reqVO) { + authService.resetPassword(reqVO); + return success(true); + } + + @PostMapping("/update-password") + @ApiOperation(value = "修改用户密码",notes = "用户修改密码时使用") + @PreAuthenticated + public CommonResult updatePassword(@RequestBody @Validated(MbrAuthResetPasswordReqVO.updatePasswordValidView.class) MbrAuthResetPasswordReqVO reqVO) { + authService.updatePassword(getLoginUserId(), reqVO); + return success(true); + } + + @PostMapping("/check-sms-code") + @ApiOperation(value = "校验验证码是否正确") + @PreAuthenticated + public CommonResult checkSmsCode(@RequestBody @Valid SysAuthSmsLoginReqVO reqVO) { + smsCodeService.useSmsCode(reqVO.getMobile(),SysSmsSceneEnum.CHECK_CODE_BY_SMS.getScene(),reqVO.getCode(),getClientIP()); + return success(true); } // ========== 社交登录相关 ========== diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java index 3b3556fdb..f092eaf48 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java @@ -8,6 +8,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; +import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; @@ -18,15 +19,31 @@ import javax.validation.constraints.Pattern; @Builder public class MbrAuthResetPasswordReqVO { + /** + * 修改密码校验规则 + */ + public interface updatePasswordValidView { + } + + /** + * 忘记密码校验规则 + */ + public interface resetPasswordValidView { + } + + @ApiModelProperty(value = "用户旧密码", required = true, example = "123456") + @NotBlank(message = "旧密码不能为空",groups = updatePasswordValidView.class) + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String oldPassword; + @ApiModelProperty(value = "新密码", required = true, example = "buzhidao") - @NotEmpty(message = "新密码不能为空") + @NotEmpty(message = "新密码不能为空",groups = {updatePasswordValidView.class,resetPasswordValidView.class}) @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; @ApiModelProperty(value = "手机验证码", required = true, example = "1024") - @NotEmpty(message = "手机验证码不能为空") + @NotEmpty(message = "手机验证码不能为空",groups = resetPasswordValidView.class) @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") private String code; - } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/SysErrorCodeConstants.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/SysErrorCodeConstants.java index f24be9cc6..e7c104afb 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/SysErrorCodeConstants.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/SysErrorCodeConstants.java @@ -23,7 +23,10 @@ public interface SysErrorCodeConstants { 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, "短信发送过于频率"); + ErrorCode USER_SMS_CODE_IS_EXISTS = new ErrorCode(1005001006, "手机号已被使用"); // ========== 用户模块 1005002000 ========== ErrorCode USER_NOT_EXISTS = new ErrorCode(1005002001, "用户不存在"); + ErrorCode USER_CODE_FAILED = new ErrorCode(1005002002, "验证码不匹配"); + ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1005002003, "密码校验失败"); } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/sms/SysSmsSceneEnum.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/sms/SysSmsSceneEnum.java index 6f5ce3daa..c2156d218 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/sms/SysSmsSceneEnum.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/sms/SysSmsSceneEnum.java @@ -17,7 +17,9 @@ public enum SysSmsSceneEnum implements IntArrayValuable { LOGIN_BY_SMS(1, "手机号登陆"), CHANGE_MOBILE_BY_SMS(2, "更换手机号"), - ; + FORGET_MOBILE_BY_SMS(3, "忘记密码"), + CHECK_CODE_BY_SMS(4, "审核验证码"), + ; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SysSmsSceneEnum::getScene).toArray(); diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/SysAuthService.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/SysAuthService.java index 628f95c80..d84d17558 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/SysAuthService.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/SysAuthService.java @@ -63,4 +63,23 @@ public interface SysAuthService extends SecurityAuthFrameworkService { */ void socialBind(Long userId, @Valid MbrAuthSocialBindReqVO reqVO); + /** + * 修改用户密码 + * @param userId 用户id + * @param userReqVO 用户请求实体类 + */ + void updatePassword(Long userId, MbrAuthResetPasswordReqVO userReqVO); + + /** + * 忘记密码 + * @param userReqVO 用户请求实体类 + */ + void resetPassword(MbrAuthResetPasswordReqVO userReqVO); + + /** + * 检测手机与验证码是否匹配 + * @param phone 手机号 + * @param code 验证码 + */ + void checkIfMobileMatchCodeAndDeleteCode(String phone,String code); } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java index 2394cd03a..bd8b57198 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java @@ -15,15 +15,19 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper; import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService; import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.*; import cn.iocoder.yudao.userserver.modules.system.convert.auth.SysAuthConvert; import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum; import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService; import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; +import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import me.zhyd.oauth.model.AuthUser; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; @@ -32,6 +36,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -40,6 +45,7 @@ import java.util.List; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConstants.*; /** @@ -65,6 +71,13 @@ public class SysAuthServiceImpl implements SysAuthService { private SysUserSessionCoreService userSessionCoreService; @Resource private SysSocialService socialService; + @Resource + private StringRedisTemplate stringRedisTemplate; + @Resource + private PasswordEncoder passwordEncoder; + @Resource + private MbrUserMapper userMapper; + private static final UserTypeEnum userTypeEnum = UserTypeEnum.MEMBER; @Override @@ -200,12 +213,12 @@ public class SysAuthServiceImpl implements SysAuthService { } reqDTO.setUsername(mobile); reqDTO.setUserAgent(ServletUtils.getUserAgent()); - reqDTO.setUserIp(ServletUtils.getClientIP()); + reqDTO.setUserIp(getClientIP()); reqDTO.setResult(loginResult.getResult()); loginLogCoreService.createLoginLog(reqDTO); // 更新最后登录时间 if (user != null && Objects.equals(SysLoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { - userService.updateUserLogin(user.getId(), ServletUtils.getClientIP()); + userService.updateUserLogin(user.getId(), getClientIP()); } } @@ -266,6 +279,66 @@ public class SysAuthServiceImpl implements SysAuthService { this.createLogoutLog(loginUser.getId(), loginUser.getUsername()); } + @Override + public void updatePassword(Long userId, MbrAuthResetPasswordReqVO reqVO) { + // 检验旧密码 + MbrUserDO userDO = checkOldPassword(userId, reqVO.getOldPassword()); + + // 更新用户密码 + userDO.setPassword(passwordEncoder.encode(reqVO.getPassword())); + userMapper.updateById(userDO); + } + + @Override + public void resetPassword(MbrAuthResetPasswordReqVO reqVO) { + // 根据验证码取出手机号,并查询用户 + String mobile = stringRedisTemplate.opsForValue().get(reqVO.getCode()); + MbrUserDO userDO = userMapper.selectByMobile(mobile); + if (userDO == null){ + throw exception(USER_NOT_EXISTS); + } + // TODO @芋艿 这一步没必要检验验证码与手机是否匹配,因为是根据验证码去redis中查找手机号,然后根据手机号查询用户 + // 也就是说 即便黑客以其他方式将验证码发送到自己手机上,最终还是会根据手机号查询用户然后进行重置密码的操作,不存在安全问题 + + // 校验验证码 + smsCodeService.useSmsCode(userDO.getMobile(), SysSmsSceneEnum.FORGET_MOBILE_BY_SMS.getScene(), reqVO.getCode(),getClientIP()); + + // 更新密码 + userDO.setPassword(passwordEncoder.encode(reqVO.getPassword())); + userMapper.updateById(userDO); + } + + @Override + public void checkIfMobileMatchCodeAndDeleteCode(String phone, String code) { + // 检验用户手机与验证码是否匹配 + String mobile = stringRedisTemplate.opsForValue().get(code); + if (!phone.equals(mobile)){ + throw exception(USER_CODE_FAILED); + } + // 销毁redis中此验证码 + stringRedisTemplate.delete(code); + } + + /** + * 校验旧密码 + * + * @param id 用户 id + * @param oldPassword 旧密码 + * @return MbrUserDO 用户实体 + */ + @VisibleForTesting + public MbrUserDO checkOldPassword(Long id, String oldPassword) { + MbrUserDO user = userMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + // 参数:未加密密码,编码后的密码 + if (!passwordEncoder.matches(oldPassword,user.getPassword())) { + throw exception(USER_PASSWORD_FAILED); + } + return user; + } + private void createLogoutLog(Long userId, String username) { SysLoginLogCreateReqDTO reqDTO = new SysLoginLogCreateReqDTO(); reqDTO.setLogType(SysLoginLogTypeEnum.LOGOUT_SELF.getType()); @@ -274,7 +347,7 @@ public class SysAuthServiceImpl implements SysAuthService { reqDTO.setUserType(userTypeEnum.getValue()); reqDTO.setUsername(username); reqDTO.setUserAgent(ServletUtils.getUserAgent()); - reqDTO.setUserIp(ServletUtils.getClientIP()); + reqDTO.setUserIp(getClientIP()); reqDTO.setResult(SysLoginResultEnum.SUCCESS.getResult()); loginLogCoreService.createLoginLog(reqDTO); } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/SysSmsCodeService.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/SysSmsCodeService.java index 5ee81b67c..6e9c3c7b3 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/SysSmsCodeService.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/SysSmsCodeService.java @@ -2,6 +2,7 @@ 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.controller.auth.vo.SysAuthSendSmsReqVO; import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum; /** @@ -20,6 +21,12 @@ public interface SysSmsCodeService { */ void sendSmsCode(@Mobile String mobile, Integer scene, String createIp); + /** + * 发送短信验证码,并检测手机号是否已被注册 + * @param reqVO 请求实体 + */ + void sendSmsNewCode(SysAuthSendSmsReqVO reqVO); + /** * 验证短信验证码,并进行使用 * 如果正确,则将验证码标记成已使用 @@ -32,4 +39,9 @@ public interface SysSmsCodeService { */ void useSmsCode(@Mobile String mobile, Integer scene, String code, String usedIp); + /** + * 根据用户id发送验证码 + * @param userId 用户id + */ + void sendSmsCodeLogin(Long userId); } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java index 6ad132aa5..d9a3d68f1 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java @@ -1,20 +1,27 @@ package cn.iocoder.yudao.userserver.modules.system.service.sms.impl; import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO; import cn.iocoder.yudao.coreservice.modules.system.service.sms.SysSmsCoreService; +import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService; +import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSendSmsReqVO; 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.enums.sms.SysSmsSceneEnum; import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsTemplateCodeConstants; import cn.iocoder.yudao.userserver.modules.system.framework.sms.SmsCodeProperties; import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; import java.util.Date; +import java.util.concurrent.TimeUnit; import static cn.hutool.core.util.RandomUtil.randomInt; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConstants.*; /** @@ -26,15 +33,26 @@ import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConst @Validated public class SysSmsCodeServiceImpl implements SysSmsCodeService { + /** + * 验证码 + 手机 在redis中存储的有效时间,单位:分钟 + */ + private static final Long CODE_TIME = 10L; + @Resource private SmsCodeProperties smsCodeProperties; @Resource private SysSmsCodeMapper smsCodeMapper; + @Resource + private MbrUserService mbrUserService; + @Resource private SysSmsCoreService smsCoreService; + @Resource + private StringRedisTemplate stringRedisTemplate; + @Override public void sendSmsCode(String mobile, Integer scene, String createIp) { // 创建验证码 @@ -42,6 +60,21 @@ public class SysSmsCodeServiceImpl implements SysSmsCodeService { // 发送验证码 smsCoreService.sendSingleSmsToMember(mobile, null, SysSmsTemplateCodeConstants.USER_SMS_LOGIN, MapUtil.of("code", code)); + + // 存储手机号与验证码到redis,用于标记 + stringRedisTemplate.opsForValue().set(code,mobile,CODE_TIME, TimeUnit.MINUTES); + } + + @Override + public void sendSmsNewCode(SysAuthSendSmsReqVO reqVO) { + // 检测手机号是否已被使用 + MbrUserDO userByMobile = mbrUserService.getUserByMobile(reqVO.getMobile()); + if (userByMobile != null){ + throw exception(USER_SMS_CODE_IS_EXISTS); + } + + // 发送短信 + this.sendSmsCode(reqVO.getMobile(),reqVO.getScene(),getClientIP()); } private String createSmsCode(String mobile, Integer scene, String ip) { @@ -91,4 +124,14 @@ public class SysSmsCodeServiceImpl implements SysSmsCodeService { .used(true).usedTime(new Date()).usedIp(usedIp).build()); } + @Override + public void sendSmsCodeLogin(Long userId) { + MbrUserDO user = mbrUserService.getUser(userId); + if (user == null){ + throw exception(USER_NOT_EXISTS); + } + // 发送验证码 + this.sendSmsCode(user.getMobile(),SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(), getClientIP()); + } + } diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/BaseDbAndRedisUnitTest.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/BaseDbAndRedisUnitTest.java new file mode 100644 index 000000000..2669ef49c --- /dev/null +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/BaseDbAndRedisUnitTest.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.userserver; + +import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; +import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import cn.iocoder.yudao.userserver.config.RedisTestConfiguration; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +/** + * 依赖内存 DB + Redis 的单元测试 + * + * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis + * + * @author 芋道源码 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 +@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB +public class BaseDbAndRedisUnitTest { + + @Import({ + // DB 配置类 + YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + DruidDataSourceAutoConfigure.class, // Druid 自动配置类 + // MyBatis 配置类 + YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + // Redis 配置类 + RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/config/RedisTestConfiguration.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/config/RedisTestConfiguration.java new file mode 100644 index 000000000..7164efd87 --- /dev/null +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/config/RedisTestConfiguration.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.userserver.config; + +import com.github.fppt.jedismock.RedisServer; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import java.io.IOException; + +@Configuration(proxyBeanMethods = false) +@Lazy(false) // 禁止延迟加载 +@EnableConfigurationProperties(RedisProperties.class) +public class RedisTestConfiguration { + + /** + * 创建模拟的 Redis Server 服务器 + */ + @Bean + public RedisServer redisServer(RedisProperties properties) throws IOException { + RedisServer redisServer = new RedisServer(properties.getPort()); + // TODO 芋艿:一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。 + try { + redisServer.start(); + } catch (Exception ignore) {} + return redisServer; + } + +} diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/controller/SysUserProfileControllerTest.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/controller/SysUserProfileControllerTest.java new file mode 100644 index 000000000..d8b8e7f71 --- /dev/null +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/controller/SysUserProfileControllerTest.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.userserver.modules.member.controller; + +import cn.iocoder.yudao.userserver.modules.member.controller.user.SysUserProfileController; +import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService; +import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * {@link SysUserProfileController} 的单元测试类 + * + * @author 宋天 + */ +public class SysUserProfileControllerTest { + + private MockMvc mockMvc; + + @InjectMocks + private SysUserProfileController sysUserProfileController; + + @Mock + private MbrUserService userService; + + @Mock + private SysSmsCodeService smsCodeService; + + @Before + public void setup() { + // 初始化 + MockitoAnnotations.openMocks(this); + + // 构建mvc环境 + mockMvc = MockMvcBuilders.standaloneSetup(sysUserProfileController).build(); + } + + @Test + public void testUpdateMobile_success() throws Exception { + //模拟接口调用 + this.mockMvc.perform(post("/system/user/profile/update-mobile") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{\"mobile\":\"15819844280\",\"code\":\"123456\"}}")) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()); + + } + + +} diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java index 3639c25db..dadb27f7d 100644 --- a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java @@ -2,28 +2,32 @@ package cn.iocoder.yudao.userserver.modules.member.service; import cn.iocoder.yudao.coreservice.modules.infra.service.file.InfFileCoreService; import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO; -import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import cn.iocoder.yudao.userserver.BaseDbAndRedisUnitTest; import cn.iocoder.yudao.userserver.BaseDbUnitTest; import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO; +import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO; import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper; import cn.iocoder.yudao.userserver.modules.member.service.user.impl.MbrUserServiceImpl; +import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSendSmsReqVO; +import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum; +import cn.iocoder.yudao.userserver.modules.system.service.auth.impl.SysAuthServiceImpl; +import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; +import cn.iocoder.yudao.userserver.modules.system.service.sms.impl.SysSmsCodeServiceImpl; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.util.Assert; import javax.annotation.Resource; import java.io.*; import java.util.function.Consumer; -import static cn.hutool.core.util.RandomUtil.randomBytes; +import static cn.hutool.core.util.RandomUtil.*; import static org.junit.jupiter.api.Assertions.assertEquals; -import static cn.hutool.core.util.RandomUtil.randomEle; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; import static org.mockito.Mockito.*; @@ -32,21 +36,30 @@ import static org.mockito.Mockito.*; * * @author 宋天 */ -@Import(MbrUserServiceImpl.class) -public class MbrUserServiceImplTest extends BaseDbUnitTest { +@Import({MbrUserServiceImpl.class, YudaoRedisAutoConfiguration.class}) +public class MbrUserServiceImplTest extends BaseDbAndRedisUnitTest { @Resource private MbrUserServiceImpl mbrUserService; + @Resource + private StringRedisTemplate stringRedisTemplate; + @Resource private MbrUserMapper userMapper; + @MockBean + private SysAuthServiceImpl authService; + @MockBean private InfFileCoreService fileCoreService; @MockBean private PasswordEncoder passwordEncoder; + @MockBean + private SysSmsCodeService sysSmsCodeService; + @Test public void testUpdateNickName_success(){ // mock 数据 @@ -94,6 +107,37 @@ public class MbrUserServiceImplTest extends BaseDbUnitTest { assertEquals(avatar, str); } + @Test + public void updateMobile_success(){ + // mock数据 + String oldMobile = randomNumbers(11); + MbrUserDO userDO = randomMbrUserDO(); + userDO.setMobile(oldMobile); + userMapper.insert(userDO); + + // 验证旧手机 + sysSmsCodeService.sendSmsCodeLogin(userDO.getId()); + + // 验证旧手机验证码是否正确 + sysSmsCodeService.useSmsCode(oldMobile,SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(),"123","1.1.1.1"); + + // 验证新手机 + SysAuthSendSmsReqVO smsReqVO = new SysAuthSendSmsReqVO(); + smsReqVO.setMobile(oldMobile); + smsReqVO.setScene(SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene()); + sysSmsCodeService.sendSmsNewCode(smsReqVO); + + // 更新手机号 + String newMobile = randomNumbers(11); + String code = randomNumbers(4); + MbrUserUpdateMobileReqVO reqVO = new MbrUserUpdateMobileReqVO(); + reqVO.setMobile(newMobile); + reqVO.setCode(code); + mbrUserService.updateMobile(userDO.getId(),reqVO); + + assertEquals(mbrUserService.getUser(userDO.getId()).getMobile(),newMobile); + } + // ========== 随机对象 ========== @SafeVarargs diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/controller/SysAuthControllerTest.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/controller/SysAuthControllerTest.java new file mode 100644 index 000000000..599ebaab6 --- /dev/null +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/controller/SysAuthControllerTest.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.userserver.modules.system.controller; + +import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialService; +import cn.iocoder.yudao.userserver.modules.system.controller.auth.SysAuthController; +import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService; +import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * {@link SysAuthController} 的单元测试类 + * + * @author 宋天 + */ +public class SysAuthControllerTest { + + private MockMvc mockMvc; + + @InjectMocks + private SysAuthController sysAuthController; + + @Mock + private SysAuthService authService; + @Mock + private SysSmsCodeService smsCodeService; + @Mock + private SysSocialService socialService; + + + @Before + public void setup() { + // 初始化 + MockitoAnnotations.openMocks(this); + + // 构建mvc环境 + mockMvc = MockMvcBuilders.standaloneSetup(sysAuthController).build(); + } + + @Test + public void testResetPassword_success() throws Exception { + //模拟接口调用 + this.mockMvc.perform(post("/reset-password") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"password\":\"1123\",\"code\":\"123456\"}}")) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()); + + } + + @Test + public void testUpdatePassword_success() throws Exception { + //模拟接口调用 + this.mockMvc.perform(post("/update-password") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"password\":\"1123\",\"code\":\"123456\",\"oldPassword\":\"1123\"}}")) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()); + + } + + + +} diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java new file mode 100644 index 000000000..2c2d73c76 --- /dev/null +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.userserver.modules.system.service; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO; +import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO; +import cn.iocoder.yudao.coreservice.modules.system.service.auth.SysUserSessionCoreService; +import cn.iocoder.yudao.coreservice.modules.system.service.logger.SysLoginLogCoreService; +import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialService; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import cn.iocoder.yudao.userserver.BaseDbAndRedisUnitTest; +import cn.iocoder.yudao.userserver.BaseDbUnitTest; +import cn.iocoder.yudao.userserver.config.RedisTestConfiguration; +import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper; +import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService; +import cn.iocoder.yudao.userserver.modules.member.service.user.impl.MbrUserServiceImpl; +import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.MbrAuthResetPasswordReqVO; +import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService; +import cn.iocoder.yudao.userserver.modules.system.service.auth.impl.SysAuthServiceImpl; +import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.annotation.Resource; + +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.hutool.core.util.RandomUtil.randomNumbers; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + + +/** + * {@link SysAuthService} 的单元测试类 + * + * @author 宋天 + */ +@Import({SysAuthServiceImpl.class, YudaoRedisAutoConfiguration.class}) +public class SysAuthServiceTest extends BaseDbAndRedisUnitTest { + + @MockBean + private AuthenticationManager authenticationManager; + @MockBean + private MbrUserService userService; + @MockBean + private SysSmsCodeService smsCodeService; + @MockBean + private SysLoginLogCoreService loginLogCoreService; + @MockBean + private SysUserSessionCoreService userSessionCoreService; + @MockBean + private SysSocialService socialService; + @Resource + private StringRedisTemplate stringRedisTemplate; + @MockBean + private PasswordEncoder passwordEncoder; + @Resource + private MbrUserMapper mbrUserMapper; + @Resource + private SysAuthServiceImpl authService; + + + @Test + public void testUpdatePassword_success(){ + // 准备参数 + MbrUserDO userDO = randomMbrUserDO(); + mbrUserMapper.insert(userDO); + + // 新密码 + String newPassword = randomString(); + + // 请求实体 + MbrAuthResetPasswordReqVO reqVO = new MbrAuthResetPasswordReqVO(); + reqVO.setOldPassword(userDO.getPassword()); + reqVO.setPassword(newPassword); + + // 测试桩 + // 这两个相等是为了返回ture这个结果 + when(passwordEncoder.matches(reqVO.getOldPassword(),reqVO.getOldPassword())).thenReturn(true); + when(passwordEncoder.encode(newPassword)).thenReturn(newPassword); + + // 更新用户密码 + authService.updatePassword(userDO.getId(),reqVO); + assertEquals(mbrUserMapper.selectById(userDO.getId()).getPassword(),newPassword); + } + + @Test + public void testResetPassword_success(){ + // 准备参数 + MbrUserDO userDO = randomMbrUserDO(); + mbrUserMapper.insert(userDO); + + // 随机密码 + String password = randomNumbers(11); + // 随机验证码 + String code = randomNumbers(4); + + MbrAuthResetPasswordReqVO reqVO = new MbrAuthResetPasswordReqVO(); + reqVO.setPassword(password); + reqVO.setCode(code); + + // 放入code+手机号 + stringRedisTemplate.opsForValue().set(code,userDO.getMobile(),10, TimeUnit.MINUTES); + + // mock + when(passwordEncoder.encode(password)).thenReturn(password); + + // 更新用户密码 + authService.resetPassword(reqVO); + assertEquals(mbrUserMapper.selectById(userDO.getId()).getPassword(),password); + } + + + // ========== 随机对象 ========== + + @SafeVarargs + private static MbrUserDO randomMbrUserDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setPassword(randomString()); + }; + return randomPojo(MbrUserDO.class, ArrayUtils.append(consumer, consumers)); + } + + +} From 6a7761313e21182d91096fc1fbe7953c976644a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E5=A4=A9?= <2982176321@qq.com> Date: Wed, 17 Nov 2021 15:12:59 +0800 Subject: [PATCH 02/40] =?UTF-8?q?[updaate]=20=E6=8B=86=E5=88=86=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=AF=86=E7=A0=81=E4=B8=8E=E9=87=8D=E7=BD=AE=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E8=AF=B7=E6=B1=82=E5=AE=9E=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/auth/SysAuthController.java | 5 ++-- .../auth/vo/MbrAuthResetPasswordReqVO.java | 21 ++----------- .../auth/vo/MbrAuthUpdatePasswordReqVO.java | 30 +++++++++++++++++++ .../system/service/auth/SysAuthService.java | 2 +- .../service/auth/impl/SysAuthServiceImpl.java | 4 +-- .../service/MbrUserServiceImplTest.java | 1 - .../system/service/SysAuthServiceTest.java | 16 +++++----- 7 files changed, 47 insertions(+), 32 deletions(-) create mode 100644 yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthUpdatePasswordReqVO.java diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java index a4a4efb87..eb6e142ee 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java @@ -80,7 +80,8 @@ public class SysAuthController { @PostMapping("/reset-password") @ApiOperation(value = "重置密码", notes = "用户忘记密码时使用") - public CommonResult resetPassword(@RequestBody @Validated(MbrAuthResetPasswordReqVO.resetPasswordValidView.class) MbrAuthResetPasswordReqVO reqVO) { + @PreAuthenticated + public CommonResult resetPassword(@RequestBody @Valid MbrAuthResetPasswordReqVO reqVO) { authService.resetPassword(reqVO); return success(true); } @@ -88,7 +89,7 @@ public class SysAuthController { @PostMapping("/update-password") @ApiOperation(value = "修改用户密码",notes = "用户修改密码时使用") @PreAuthenticated - public CommonResult updatePassword(@RequestBody @Validated(MbrAuthResetPasswordReqVO.updatePasswordValidView.class) MbrAuthResetPasswordReqVO reqVO) { + public CommonResult updatePassword(@RequestBody @Valid MbrAuthUpdatePasswordReqVO reqVO) { authService.updatePassword(getLoginUserId(), reqVO); return success(true); } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java index f092eaf48..92fd8445f 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java @@ -19,30 +19,13 @@ import javax.validation.constraints.Pattern; @Builder public class MbrAuthResetPasswordReqVO { - /** - * 修改密码校验规则 - */ - public interface updatePasswordValidView { - } - - /** - * 忘记密码校验规则 - */ - public interface resetPasswordValidView { - } - - @ApiModelProperty(value = "用户旧密码", required = true, example = "123456") - @NotBlank(message = "旧密码不能为空",groups = updatePasswordValidView.class) - @Length(min = 4, max = 16, message = "密码长度为 4-16 位") - private String oldPassword; - @ApiModelProperty(value = "新密码", required = true, example = "buzhidao") - @NotEmpty(message = "新密码不能为空",groups = {updatePasswordValidView.class,resetPasswordValidView.class}) + @NotEmpty(message = "新密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; @ApiModelProperty(value = "手机验证码", required = true, example = "1024") - @NotEmpty(message = "手机验证码不能为空",groups = resetPasswordValidView.class) + @NotEmpty(message = "手机验证码不能为空") @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") private String code; diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthUpdatePasswordReqVO.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthUpdatePasswordReqVO.java new file mode 100644 index 000000000..b5cc0c785 --- /dev/null +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthUpdatePasswordReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.userserver.modules.system.controller.auth.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; + +@ApiModel("修改密码 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MbrAuthUpdatePasswordReqVO { + + @ApiModelProperty(value = "用户旧密码", required = true, example = "123456") + @NotBlank(message = "旧密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String oldPassword; + + @ApiModelProperty(value = "新密码", required = true, example = "buzhidao") + @NotEmpty(message = "新密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; +} diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/SysAuthService.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/SysAuthService.java index d84d17558..33664d351 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/SysAuthService.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/SysAuthService.java @@ -68,7 +68,7 @@ public interface SysAuthService extends SecurityAuthFrameworkService { * @param userId 用户id * @param userReqVO 用户请求实体类 */ - void updatePassword(Long userId, MbrAuthResetPasswordReqVO userReqVO); + void updatePassword(Long userId, @Valid MbrAuthUpdatePasswordReqVO userReqVO); /** * 忘记密码 diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java index bd8b57198..eb05f7086 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java @@ -25,7 +25,6 @@ import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import me.zhyd.oauth.model.AuthUser; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.AuthenticationManager; @@ -41,6 +40,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; +import javax.validation.Valid; import java.util.List; import java.util.Objects; @@ -280,7 +280,7 @@ public class SysAuthServiceImpl implements SysAuthService { } @Override - public void updatePassword(Long userId, MbrAuthResetPasswordReqVO reqVO) { + public void updatePassword(Long userId, @Valid MbrAuthUpdatePasswordReqVO reqVO) { // 检验旧密码 MbrUserDO userDO = checkOldPassword(userId, reqVO.getOldPassword()); diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java index dadb27f7d..0ed1e77e6 100644 --- a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java @@ -120,7 +120,6 @@ public class MbrUserServiceImplTest extends BaseDbAndRedisUnitTest { // 验证旧手机验证码是否正确 sysSmsCodeService.useSmsCode(oldMobile,SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(),"123","1.1.1.1"); - // 验证新手机 SysAuthSendSmsReqVO smsReqVO = new SysAuthSendSmsReqVO(); smsReqVO.setMobile(oldMobile); diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java index 2c2d73c76..65fe3031c 100644 --- a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java @@ -16,6 +16,7 @@ import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper; import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService; import cn.iocoder.yudao.userserver.modules.member.service.user.impl.MbrUserServiceImpl; import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.MbrAuthResetPasswordReqVO; +import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.MbrAuthUpdatePasswordReqVO; import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService; import cn.iocoder.yudao.userserver.modules.system.service.auth.impl.SysAuthServiceImpl; import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; @@ -78,9 +79,10 @@ public class SysAuthServiceTest extends BaseDbAndRedisUnitTest { String newPassword = randomString(); // 请求实体 - MbrAuthResetPasswordReqVO reqVO = new MbrAuthResetPasswordReqVO(); - reqVO.setOldPassword(userDO.getPassword()); - reqVO.setPassword(newPassword); + MbrAuthUpdatePasswordReqVO reqVO = MbrAuthUpdatePasswordReqVO.builder() + .oldPassword(userDO.getPassword()) + .password(newPassword) + .build(); // 测试桩 // 这两个相等是为了返回ture这个结果 @@ -103,10 +105,10 @@ public class SysAuthServiceTest extends BaseDbAndRedisUnitTest { // 随机验证码 String code = randomNumbers(4); - MbrAuthResetPasswordReqVO reqVO = new MbrAuthResetPasswordReqVO(); - reqVO.setPassword(password); - reqVO.setCode(code); - + MbrAuthResetPasswordReqVO reqVO = MbrAuthResetPasswordReqVO.builder() + .password(password) + .code(code) + .build(); // 放入code+手机号 stringRedisTemplate.opsForValue().set(code,userDO.getMobile(),10, TimeUnit.MINUTES); From e2b76ee0e5f4d9f5edc57c5f22d9dc736daf1fd0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 21 Nov 2021 12:05:34 +0800 Subject: [PATCH 03/40] =?UTF-8?q?code=20review=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E7=AD=89=E7=9A=84=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-admin-server/pom.xml | 2 -- yudao-user-server/pom.xml | 1 + .../controller/user/SysUserProfileController.java | 1 + .../user/vo/MbrUserUpdateMobileReqVO.java | 2 +- .../service/user/impl/MbrUserServiceImpl.java | 2 ++ .../system/controller/auth/SysAuthController.java | 1 + .../auth/vo/MbrAuthResetPasswordReqVO.java | 2 +- .../service/auth/impl/SysAuthServiceImpl.java | 3 +++ .../service/sms/impl/SysSmsCodeServiceImpl.java | 4 ++++ .../controller/SysUserProfileControllerTest.java | 5 +++-- .../member/service/MbrUserServiceImplTest.java | 8 ++++---- .../modules/system/service/SysAuthServiceTest.java | 14 ++++---------- 12 files changed, 25 insertions(+), 20 deletions(-) diff --git a/yudao-admin-server/pom.xml b/yudao-admin-server/pom.xml index 5cdf85712..4314e0fed 100644 --- a/yudao-admin-server/pom.xml +++ b/yudao-admin-server/pom.xml @@ -117,8 +117,6 @@ yudao-spring-boot-starter-excel - - org.apache.velocity velocity-engine-core diff --git a/yudao-user-server/pom.xml b/yudao-user-server/pom.xml index b4c2cf8a5..03dbfd699 100644 --- a/yudao-user-server/pom.xml +++ b/yudao-user-server/pom.xml @@ -91,6 +91,7 @@ test + junit diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/SysUserProfileController.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/SysUserProfileController.java index 39c3d88f3..b2afbc78b 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/SysUserProfileController.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/SysUserProfileController.java @@ -70,6 +70,7 @@ public class SysUserProfileController { @PreAuthenticated public CommonResult updateMobile(@RequestBody @Valid MbrUserUpdateMobileReqVO reqVO) { // 校验验证码 + // TODO @宋天:统一到 userService.updateMobile 方法里 smsCodeService.useSmsCode(reqVO.getMobile(),SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(), reqVO.getCode(),getClientIP()); userService.updateMobile(getLoginUserId(), reqVO); diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/vo/MbrUserUpdateMobileReqVO.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/vo/MbrUserUpdateMobileReqVO.java index 0e5499bc7..df1980b89 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/vo/MbrUserUpdateMobileReqVO.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/user/vo/MbrUserUpdateMobileReqVO.java @@ -25,9 +25,9 @@ public class MbrUserUpdateMobileReqVO { @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") private String code; - @ApiModelProperty(value = "手机号",required = true,example = "15823654487") @NotBlank(message = "手机号不能为空") + // TODO @宋天:手机校验,直接使用 @Mobile 哈 @Length(min = 8, max = 11, message = "手机号码长度为 8-11 位") @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误") private String mobile; diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/impl/MbrUserServiceImpl.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/impl/MbrUserServiceImpl.java index 76b4501eb..f45ad257b 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/impl/MbrUserServiceImpl.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/service/user/impl/MbrUserServiceImpl.java @@ -126,8 +126,10 @@ public class MbrUserServiceImpl implements MbrUserService { // 检测用户是否存在 MbrUserDO userDO = checkUserExists(userId); // 检测手机与验证码是否匹配 + // TODO @宋天:修改手机的时候。应该要校验,老手机 + 老手机 code;新手机 + 新手机 code sysAuthService.checkIfMobileMatchCodeAndDeleteCode(userDO.getMobile(),reqVO.getCode()); // 更新用户手机 + // TODO @宋天:更新的时候,单独创建对象。直接全量更新,会可能导致属性覆盖。可以看看打印出来的 SQL 哈 userDO.setMobile(reqVO.getMobile()); userMapper.updateById(userDO); } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java index eb6e142ee..4dfdb7a8c 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java @@ -98,6 +98,7 @@ public class SysAuthController { @ApiOperation(value = "校验验证码是否正确") @PreAuthenticated public CommonResult checkSmsCode(@RequestBody @Valid SysAuthSmsLoginReqVO reqVO) { + // TODO @宋天:check 的时候,不应该使用 useSmsCode 哈,这样验证码就直接被使用了。另外,check 开头的方法,更多是校验的逻辑,不会有 update 数据的动作。这点,在方法命名上,也是要注意的 smsCodeService.useSmsCode(reqVO.getMobile(),SysSmsSceneEnum.CHECK_CODE_BY_SMS.getScene(),reqVO.getCode(),getClientIP()); return success(true); } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java index 92fd8445f..3b3556fdb 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthResetPasswordReqVO.java @@ -8,7 +8,6 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; -import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; @@ -29,4 +28,5 @@ public class MbrAuthResetPasswordReqVO { @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") private String code; + } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java index eb05f7086..f4193b35b 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/auth/impl/SysAuthServiceImpl.java @@ -285,6 +285,7 @@ public class SysAuthServiceImpl implements SysAuthService { MbrUserDO userDO = checkOldPassword(userId, reqVO.getOldPassword()); // 更新用户密码 + // TODO @宋天:不要更新整个对象哈 userDO.setPassword(passwordEncoder.encode(reqVO.getPassword())); userMapper.updateById(userDO); } @@ -300,6 +301,8 @@ public class SysAuthServiceImpl implements SysAuthService { // TODO @芋艿 这一步没必要检验验证码与手机是否匹配,因为是根据验证码去redis中查找手机号,然后根据手机号查询用户 // 也就是说 即便黑客以其他方式将验证码发送到自己手机上,最终还是会根据手机号查询用户然后进行重置密码的操作,不存在安全问题 + // TODO @宋天:这块微信在讨论下哈~~~ + // 校验验证码 smsCodeService.useSmsCode(userDO.getMobile(), SysSmsSceneEnum.FORGET_MOBILE_BY_SMS.getScene(), reqVO.getCode(),getClientIP()); diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java index d9a3d68f1..a5796c6fc 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java @@ -58,10 +58,14 @@ public class SysSmsCodeServiceImpl implements SysSmsCodeService { // 创建验证码 String code = this.createSmsCode(mobile, scene, createIp); // 发送验证码 + // TODO @宋天:这里可以拓展下 SysSmsSceneEnum,支持设置对应的短信模板编号(不同场景的短信文案是不同的)、是否要校验手机号已经注册。这样 Controller 就可以收口成一个接口了。相当于说,不同场景,不同策略 smsCoreService.sendSingleSmsToMember(mobile, null, SysSmsTemplateCodeConstants.USER_SMS_LOGIN, MapUtil.of("code", code)); // 存储手机号与验证码到redis,用于标记 + // TODO @宋天:SysSmsCodeDO 表应该足够,无需增加额外的 redis 存储哇 + // TODO @宋天:Redis 相关的操作,不要散落到业务层,而是写一个它对应的 RedisDAO。这样,实现业务与技术的解耦 + // TODO @宋天:直接使用 code 作为 key,会存在 2 个问题:1)code 可能会冲突,多个手机号之间;2)缺少前缀。例如说 sms_code_${code} stringRedisTemplate.opsForValue().set(code,mobile,CODE_TIME, TimeUnit.MINUTES); } diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/controller/SysUserProfileControllerTest.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/controller/SysUserProfileControllerTest.java index d8b8e7f71..3f32cc82c 100644 --- a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/controller/SysUserProfileControllerTest.java +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/controller/SysUserProfileControllerTest.java @@ -22,6 +22,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * * @author 宋天 */ +// TODO @宋天:controller 的单测可以不写哈,因为收益太低了。未来我们做 qa 自动化测试 public class SysUserProfileControllerTest { private MockMvc mockMvc; @@ -35,7 +36,7 @@ public class SysUserProfileControllerTest { @Mock private SysSmsCodeService smsCodeService; - @Before + @Before // TODO @宋天:使用 junit5 哈 public void setup() { // 初始化 MockitoAnnotations.openMocks(this); @@ -52,7 +53,7 @@ public class SysUserProfileControllerTest { .content("{\"mobile\":\"15819844280\",\"code\":\"123456\"}}")) .andExpect(status().isOk()) .andDo(MockMvcResultHandlers.print()); - +// TODO @宋天:方法的结尾,不用空行哈 } diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java index 0ed1e77e6..05571ba2e 100644 --- a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/member/service/MbrUserServiceImplTest.java @@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; import cn.iocoder.yudao.userserver.BaseDbAndRedisUnitTest; -import cn.iocoder.yudao.userserver.BaseDbUnitTest; import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO; import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO; import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper; @@ -15,7 +14,6 @@ import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSend import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum; import cn.iocoder.yudao.userserver.modules.system.service.auth.impl.SysAuthServiceImpl; import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService; -import cn.iocoder.yudao.userserver.modules.system.service.sms.impl.SysSmsCodeServiceImpl; import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; @@ -23,14 +21,16 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.Resource; -import java.io.*; +import java.io.ByteArrayInputStream; import java.util.function.Consumer; import static cn.hutool.core.util.RandomUtil.*; -import static org.junit.jupiter.api.Assertions.assertEquals; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; + +// TODO @芋艿:单测的 review,等逻辑都达成一致后 /** * {@link MbrUserServiceImpl} 的单元测试类 * diff --git a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java index 65fe3031c..c96425bda 100644 --- a/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java +++ b/yudao-user-server/src/test/java/cn/iocoder/yudao/userserver/modules/system/service/SysAuthServiceTest.java @@ -1,8 +1,6 @@ package cn.iocoder.yudao.userserver.modules.system.service; -import cn.hutool.core.util.ArrayUtil; import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO; -import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO; import cn.iocoder.yudao.coreservice.modules.system.service.auth.SysUserSessionCoreService; import cn.iocoder.yudao.coreservice.modules.system.service.logger.SysLoginLogCoreService; import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialService; @@ -10,11 +8,8 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; import cn.iocoder.yudao.userserver.BaseDbAndRedisUnitTest; -import cn.iocoder.yudao.userserver.BaseDbUnitTest; -import cn.iocoder.yudao.userserver.config.RedisTestConfiguration; import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper; import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService; -import cn.iocoder.yudao.userserver.modules.member.service.user.impl.MbrUserServiceImpl; import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.MbrAuthResetPasswordReqVO; import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.MbrAuthUpdatePasswordReqVO; import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService; @@ -28,17 +23,17 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.Resource; - import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static cn.hutool.core.util.RandomUtil.randomEle; import static cn.hutool.core.util.RandomUtil.randomNumbers; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - +import static org.mockito.Mockito.when; +// TODO @芋艿:单测的 review,等逻辑都达成一致后 /** * {@link SysAuthService} 的单元测试类 * @@ -68,7 +63,6 @@ public class SysAuthServiceTest extends BaseDbAndRedisUnitTest { @Resource private SysAuthServiceImpl authService; - @Test public void testUpdatePassword_success(){ // 准备参数 From 6c4908e70e1cd5dd2afaae57ea17de8844f5957f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 24 Nov 2021 23:33:06 +0800 Subject: [PATCH 04/40] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=20uniapp=20?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-vue-ui/.hbuilderx/launch.json | 16 + yudao-vue-ui/App.vue | 19 + yudao-vue-ui/common/css/common.css | 182 ++ yudao-vue-ui/common/css/icon.css | 271 +++ .../components/jyf-parser/jyf-parser.vue | 630 ++++++ .../components/jyf-parser/libs/CssHandler.js | 97 + .../jyf-parser/libs/MpHtmlParser.js | 534 +++++ .../components/jyf-parser/libs/config.js | 93 + .../components/jyf-parser/libs/handler.wxs | 22 + .../components/jyf-parser/libs/trees.vue | 500 +++++ .../mescroll-uni/components/mescroll-down.css | 55 + .../mescroll-uni/components/mescroll-down.vue | 47 + .../components/mescroll-empty.vue | 27 + .../components/mescroll-empty1.vue | 95 + .../mescroll-uni/components/mescroll-top.vue | 83 + .../mescroll-uni/components/mescroll-up.css | 47 + .../mescroll-uni/components/mescroll-up.vue | 39 + .../components/mescroll-uni/mescroll-body.css | 14 + .../components/mescroll-uni/mescroll-body.vue | 344 ++++ .../mescroll-uni/mescroll-mixins.js | 65 + .../mescroll-uni/mescroll-uni-option.js | 33 + .../components/mescroll-uni/mescroll-uni.css | 36 + .../components/mescroll-uni/mescroll-uni.js | 788 +++++++ .../components/mescroll-uni/mescroll-uni.vue | 408 ++++ .../mescroll-uni/mixins/mescroll-comp.js | 23 + .../mescroll-uni/mixins/mescroll-more-item.js | 51 + .../mescroll-uni/mixins/mescroll-more.js | 56 + .../components/mescroll-uni/wxs/bounce.js | 23 + .../components/mescroll-uni/wxs/mixins.js | 102 + .../components/mescroll-uni/wxs/renderjs.js | 92 + .../components/mescroll-uni/wxs/wxs.wxs | 267 +++ .../mix-action-sheet/mix-action-sheet.vue | 82 + .../components/mix-button/mix-button.vue | 154 ++ yudao-vue-ui/components/mix-code/mix-code.vue | 113 + .../components/mix-empty/mix-empty.vue | 209 ++ .../mix-icon-loading/mix-icon-loading.vue | 66 + .../mix-list-cell/mix-list-cell.vue | 117 ++ .../mix-load-more/mix-load-more.vue | 60 + .../components/mix-loading/mix-loading.vue | 114 ++ .../components/mix-modal/mix-modal.vue | 105 + .../components/mix-nav-bar/mix-nav-bar.vue | 139 ++ .../mix-number-box/mix-number-box.vue | 180 ++ .../mix-price-view/mix-price-view.vue | 53 + .../components/mix-timeline/mix-timeline.vue | 137 ++ .../mix-upload-image/mix-upload-image.vue | 200 ++ .../number-keyboard/number-keyboard.vue | 186 ++ .../pay-password-keyboard.vue | 97 + yudao-vue-ui/components/uni-popup/popup.js | 25 + .../components/uni-popup/uni-popup.vue | 302 +++ .../uni-swipe-action-item/bindingx.js | 245 +++ .../uni-swipe-action-item/index.wxs | 204 ++ .../uni-swipe-action-item/mpalipay.js | 160 ++ .../uni-swipe-action-item/mpother.js | 158 ++ .../components/uni-swipe-action-item/mpwxs.js | 97 + .../uni-swipe-action-item.vue | 270 +++ .../uni-swipe-action/uni-swipe-action.vue | 58 + .../uni-transition/uni-transition.vue | 290 +++ .../version-update/base-cloud-mobile.scss | 1250 ++++++++++++ .../version-update/static/airship.png | Bin 0 -> 8640 bytes .../version-update/static/cloudLeft.png | Bin 0 -> 14663 bytes .../version-update/static/cloudRight.png | Bin 0 -> 11364 bytes .../version-update/static/login-wave.png | Bin 0 -> 4419 bytes .../version-update/static/shipAir.png | Bin 0 -> 1420 bytes .../version-update/static/shipGas.png | Bin 0 -> 3601 bytes .../version-update/static/smallCloud.png | Bin 0 -> 1249 bytes .../components/version-update/static/star.png | Bin 0 -> 1324 bytes .../version-update/version-update.vue | 1811 +++++++++++++++++ yudao-vue-ui/index.html | 14 + yudao-vue-ui/main.js | 21 + yudao-vue-ui/manifest.json | 72 + yudao-vue-ui/pages.json | 43 + yudao-vue-ui/pages/index/index.vue | 52 + yudao-vue-ui/pages/tabbar/user.vue | 257 +++ yudao-vue-ui/static/backgroud/user.jpg | Bin 0 -> 8756 bytes yudao-vue-ui/static/icon/arc.png | Bin 0 -> 8014 bytes yudao-vue-ui/static/icon/default-avatar.png | Bin 0 -> 7974 bytes yudao-vue-ui/static/logo.png | Bin 0 -> 4023 bytes yudao-vue-ui/static/tarbar/index-active.png | Bin 0 -> 2415 bytes yudao-vue-ui/static/tarbar/index.png | Bin 0 -> 1867 bytes yudao-vue-ui/static/tarbar/logo.png | Bin 0 -> 4023 bytes yudao-vue-ui/static/tarbar/product-active.png | Bin 0 -> 1562 bytes yudao-vue-ui/static/tarbar/product.png | Bin 0 -> 1981 bytes yudao-vue-ui/static/tarbar/ucenter-active.png | Bin 0 -> 2073 bytes yudao-vue-ui/static/tarbar/ucenter.png | Bin 0 -> 1672 bytes yudao-vue-ui/uni.scss | 78 + 85 files changed, 12478 insertions(+) create mode 100644 yudao-vue-ui/.hbuilderx/launch.json create mode 100644 yudao-vue-ui/App.vue create mode 100644 yudao-vue-ui/common/css/common.css create mode 100644 yudao-vue-ui/common/css/icon.css create mode 100644 yudao-vue-ui/components/jyf-parser/jyf-parser.vue create mode 100644 yudao-vue-ui/components/jyf-parser/libs/CssHandler.js create mode 100644 yudao-vue-ui/components/jyf-parser/libs/MpHtmlParser.js create mode 100644 yudao-vue-ui/components/jyf-parser/libs/config.js create mode 100644 yudao-vue-ui/components/jyf-parser/libs/handler.wxs create mode 100644 yudao-vue-ui/components/jyf-parser/libs/trees.vue create mode 100644 yudao-vue-ui/components/mescroll-uni/components/mescroll-down.css create mode 100644 yudao-vue-ui/components/mescroll-uni/components/mescroll-down.vue create mode 100644 yudao-vue-ui/components/mescroll-uni/components/mescroll-empty.vue create mode 100644 yudao-vue-ui/components/mescroll-uni/components/mescroll-empty1.vue create mode 100644 yudao-vue-ui/components/mescroll-uni/components/mescroll-top.vue create mode 100644 yudao-vue-ui/components/mescroll-uni/components/mescroll-up.css create mode 100644 yudao-vue-ui/components/mescroll-uni/components/mescroll-up.vue create mode 100644 yudao-vue-ui/components/mescroll-uni/mescroll-body.css create mode 100644 yudao-vue-ui/components/mescroll-uni/mescroll-body.vue create mode 100644 yudao-vue-ui/components/mescroll-uni/mescroll-mixins.js create mode 100644 yudao-vue-ui/components/mescroll-uni/mescroll-uni-option.js create mode 100644 yudao-vue-ui/components/mescroll-uni/mescroll-uni.css create mode 100644 yudao-vue-ui/components/mescroll-uni/mescroll-uni.js create mode 100644 yudao-vue-ui/components/mescroll-uni/mescroll-uni.vue create mode 100644 yudao-vue-ui/components/mescroll-uni/mixins/mescroll-comp.js create mode 100644 yudao-vue-ui/components/mescroll-uni/mixins/mescroll-more-item.js create mode 100644 yudao-vue-ui/components/mescroll-uni/mixins/mescroll-more.js create mode 100644 yudao-vue-ui/components/mescroll-uni/wxs/bounce.js create mode 100644 yudao-vue-ui/components/mescroll-uni/wxs/mixins.js create mode 100644 yudao-vue-ui/components/mescroll-uni/wxs/renderjs.js create mode 100644 yudao-vue-ui/components/mescroll-uni/wxs/wxs.wxs create mode 100644 yudao-vue-ui/components/mix-action-sheet/mix-action-sheet.vue create mode 100644 yudao-vue-ui/components/mix-button/mix-button.vue create mode 100644 yudao-vue-ui/components/mix-code/mix-code.vue create mode 100644 yudao-vue-ui/components/mix-empty/mix-empty.vue create mode 100644 yudao-vue-ui/components/mix-icon-loading/mix-icon-loading.vue create mode 100644 yudao-vue-ui/components/mix-list-cell/mix-list-cell.vue create mode 100644 yudao-vue-ui/components/mix-load-more/mix-load-more.vue create mode 100644 yudao-vue-ui/components/mix-loading/mix-loading.vue create mode 100644 yudao-vue-ui/components/mix-modal/mix-modal.vue create mode 100644 yudao-vue-ui/components/mix-nav-bar/mix-nav-bar.vue create mode 100644 yudao-vue-ui/components/mix-number-box/mix-number-box.vue create mode 100644 yudao-vue-ui/components/mix-price-view/mix-price-view.vue create mode 100644 yudao-vue-ui/components/mix-timeline/mix-timeline.vue create mode 100644 yudao-vue-ui/components/mix-upload-image/mix-upload-image.vue create mode 100644 yudao-vue-ui/components/number-keyboard/number-keyboard.vue create mode 100644 yudao-vue-ui/components/pay-password-keyboard/pay-password-keyboard.vue create mode 100644 yudao-vue-ui/components/uni-popup/popup.js create mode 100644 yudao-vue-ui/components/uni-popup/uni-popup.vue create mode 100644 yudao-vue-ui/components/uni-swipe-action-item/bindingx.js create mode 100644 yudao-vue-ui/components/uni-swipe-action-item/index.wxs create mode 100644 yudao-vue-ui/components/uni-swipe-action-item/mpalipay.js create mode 100644 yudao-vue-ui/components/uni-swipe-action-item/mpother.js create mode 100644 yudao-vue-ui/components/uni-swipe-action-item/mpwxs.js create mode 100644 yudao-vue-ui/components/uni-swipe-action-item/uni-swipe-action-item.vue create mode 100644 yudao-vue-ui/components/uni-swipe-action/uni-swipe-action.vue create mode 100644 yudao-vue-ui/components/uni-transition/uni-transition.vue create mode 100644 yudao-vue-ui/components/version-update/base-cloud-mobile.scss create mode 100644 yudao-vue-ui/components/version-update/static/airship.png create mode 100644 yudao-vue-ui/components/version-update/static/cloudLeft.png create mode 100644 yudao-vue-ui/components/version-update/static/cloudRight.png create mode 100644 yudao-vue-ui/components/version-update/static/login-wave.png create mode 100644 yudao-vue-ui/components/version-update/static/shipAir.png create mode 100644 yudao-vue-ui/components/version-update/static/shipGas.png create mode 100644 yudao-vue-ui/components/version-update/static/smallCloud.png create mode 100644 yudao-vue-ui/components/version-update/static/star.png create mode 100644 yudao-vue-ui/components/version-update/version-update.vue create mode 100644 yudao-vue-ui/index.html create mode 100644 yudao-vue-ui/main.js create mode 100644 yudao-vue-ui/manifest.json create mode 100644 yudao-vue-ui/pages.json create mode 100644 yudao-vue-ui/pages/index/index.vue create mode 100644 yudao-vue-ui/pages/tabbar/user.vue create mode 100644 yudao-vue-ui/static/backgroud/user.jpg create mode 100644 yudao-vue-ui/static/icon/arc.png create mode 100644 yudao-vue-ui/static/icon/default-avatar.png create mode 100644 yudao-vue-ui/static/logo.png create mode 100644 yudao-vue-ui/static/tarbar/index-active.png create mode 100644 yudao-vue-ui/static/tarbar/index.png create mode 100644 yudao-vue-ui/static/tarbar/logo.png create mode 100644 yudao-vue-ui/static/tarbar/product-active.png create mode 100644 yudao-vue-ui/static/tarbar/product.png create mode 100644 yudao-vue-ui/static/tarbar/ucenter-active.png create mode 100644 yudao-vue-ui/static/tarbar/ucenter.png create mode 100644 yudao-vue-ui/uni.scss diff --git a/yudao-vue-ui/.hbuilderx/launch.json b/yudao-vue-ui/.hbuilderx/launch.json new file mode 100644 index 000000000..07c1d5fa5 --- /dev/null +++ b/yudao-vue-ui/.hbuilderx/launch.json @@ -0,0 +1,16 @@ +{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/ + // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数 + "version": "0.0", + "configurations": [{ + "default" : + { + "launchtype" : "local" + }, + "h5" : + { + "launchtype" : "local" + }, + "type" : "uniCloud" + } + ] +} diff --git a/yudao-vue-ui/App.vue b/yudao-vue-ui/App.vue new file mode 100644 index 000000000..6b658ecf3 --- /dev/null +++ b/yudao-vue-ui/App.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/yudao-vue-ui/common/css/common.css b/yudao-vue-ui/common/css/common.css new file mode 100644 index 000000000..2f22c237e --- /dev/null +++ b/yudao-vue-ui/common/css/common.css @@ -0,0 +1,182 @@ +/* #ifndef APP-PLUS-NVUE */ +view, +scroll-view, +swiper, +swiper-item, +cover-view, +cover-image, +icon, +text, +rich-text, +progress, +button, +checkbox, +form, +input, +label, +radio, +slider, +switch, +textarea, +navigator, +audio, +camera, +image, +video { + box-sizing: border-box; +} +image{ + display: block; +} +text{ + line-height: 1; + /* font-family: Helvetica Neue, Helvetica, sans-serif; */ +} +button{ + padding: 0; + margin: 0; + background-color: rgba(0,0,0,0) !important; +} +button:after{ + border: 0; +} +.bottom-fill{ + height: constant(safe-area-inset-bottom); + height: env(safe-area-inset-bottom); +} +.fix-bot{ + box-sizing: content-box; + padding-bottom: constant(safe-area-inset-bottom); + padding-bottom: env(safe-area-inset-bottom); +} + +/* 边框 */ +.round{ + position: relative; + border-radius: 100rpx; +} +.round:after{ + content: ''; + position: absolute; + left: 0; + top: 0; + width: 200%; + height: 200%; + transform: scale(.5) translate(-50%,-50%); + border: 1px solid #878787; + border-radius: 100rpx; + box-sizing: border-box; +} +.b-b:after{ + position: absolute; + z-index: 3; + left: 0; + top: auto; + bottom: 0; + right: 0; + height: 0; + content: ''; + transform: scaleY(.5); + border-bottom: 1px solid #e0e0e0; +} +.b-t:before{ + position: absolute; + z-index: 3; + left: 0; + top: 0; + right: 0; + height: 0; + content: ''; + transform: scaleY(.5); + border-bottom: 1px solid #e5e5e5; +} +.b-r:after{ + position: absolute; + z-index: 3; + right: 0; + top: 0; + bottom: 0; + width: 0; + content: ''; + transform: scaleX(.5); + border-right: 1px solid #e5e5e5; +} +.b-l:before{ + position: absolute; + z-index: 3; + left: 0; + top: 0; + bottom: 0; + width: 0; + content: ''; + transform: scaleX(.5); + border-left: 1px solid #e5e5e5; +} +.b-b, .b-t, .b-l, .b-r{ + position: relative; +} +/* 点击态 */ +.hover-gray { + background: #fafafa !important; +} +.hover-dark { + background: #f0f0f0 !important; +} + +.hover-opacity { + opacity: 0.7; +} + +/* #endif */ + +.clamp { + /* #ifdef APP-PLUS-NVUE */ + lines: 1; + /* #endif */ + /* #ifndef APP-PLUS-NVUE */ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + /* #endif */ +} +.clamp2 { + /* #ifdef APP-PLUS-NVUE */ + lines: 2; + /* #endif */ + /* #ifndef APP-PLUS-NVUE */ + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + /* #endif */ +} + +/* 布局 */ +.row{ + /* #ifndef APP-PLUS-NVUE */ + display:flex; + /* #endif */ + flex-direction:row; + align-items: center; +} +.column{ + /* #ifndef APP-PLUS-NVUE */ + display:flex; + /* #endif */ + flex-direction: column; +} +.center{ + /* #ifndef APP-PLUS-NVUE */ + display:flex; + /* #endif */ + align-items: center; + justify-content: center; +} +.fill{ + flex: 1; +} +/* input */ +.placeholder{ + color: #999 !important; +} \ No newline at end of file diff --git a/yudao-vue-ui/common/css/icon.css b/yudao-vue-ui/common/css/icon.css new file mode 100644 index 000000000..15a177608 --- /dev/null +++ b/yudao-vue-ui/common/css/icon.css @@ -0,0 +1,271 @@ +@font-face { + font-family: "mix-icon"; + font-weight: normal; + font-style: normal; + src: url('https://at.alicdn.com/t/font_1913318_2ui3nitf38x.ttf') format('truetype'); +} + +.mix-icon { + font-family: "mix-icon" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-fanhui:before { + content: "\e7d5"; +} + +.icon-shoujihaoma:before { + content: "\e7ec"; +} + +.icon-close:before { + content: "\e60f"; +} + +.icon-xingbie-nv:before { + content: "\e60e"; +} + +.icon-wuliuyunshu:before { + content: "\e7ed"; +} + +.icon-jingpin:before { + content: "\e608"; +} + +.icon-zhangdanmingxi01:before { + content: "\e637"; +} + +.icon-tixian1:before { + content: "\e625"; +} + +.icon-chongzhi:before { + content: "\e605"; +} + +.icon-wodezhanghu_zijinjilu:before { + content: "\e615"; +} + +.icon-tixian:before { + content: "\e6ab"; +} + +.icon-qianbao:before { + content: "\e6c4"; +} + +.icon-guanbi1:before { + content: "\e61a"; +} + +.icon-daipingjia:before { + content: "\e604"; +} + +.icon-daifahuo:before { + content: "\e6bd"; +} + +.icon-yue:before { + content: "\e600"; +} + +.icon-wxpay:before { + content: "\e602"; +} + +.icon-alipay:before { + content: "\e603"; +} + +.icon-tishi:before { + content: "\e662"; +} + +.icon-shoucang-1:before { + content: "\e607"; +} + +.icon-gouwuche:before { + content: "\e657"; +} + +.icon-shoucang:before { + content: "\e645"; +} + +.icon-home:before { + content: "\e60c"; +} + +.icon-bangzhu1:before { + content: "\e63d"; +} + +.icon-xingxing:before { + content: "\e70b"; +} + +.icon-shuxiangliebiao:before { + content: "\e635"; +} + +.icon-hengxiangliebiao:before { + content: "\e636"; +} + +.icon-guanbi2:before { + content: "\e7be"; +} + +.icon-down:before { + content: "\e65c"; +} + +.icon-arrow-top:before { + content: "\e63e"; +} + +.icon-xiaoxi:before { + content: "\e634"; +} + +.icon-saoma:before { + content: "\e655"; +} + +.icon-dizhi1:before { + content: "\e618"; +} + +.icon-ditu-copy:before { + content: "\e609"; +} + +.icon-lajitong:before { + content: "\e682"; +} + +.icon-bianji:before { + content: "\e60d"; +} + +.icon-yanzhengma1:before { + content: "\e613"; +} + +.icon-yanjing:before { + content: "\e65b"; +} + +.icon-mima:before { + content: "\e628"; +} + +.icon-biyan:before { + content: "\e633"; +} + +.icon-iconfontweixin:before { + content: "\e611"; +} + +.icon-shouye:before { + content: "\e626"; +} + +.icon-daifukuan:before { + content: "\e68f"; +} + +.icon-pinglun-copy:before { + content: "\e612"; +} + +.icon-lishijilu:before { + content: "\e6b9"; +} + +.icon-shoucang_xuanzhongzhuangtai:before { + content: "\e6a9"; +} + +.icon-share:before { + content: "\e656"; +} + +.icon-shezhi1:before { + content: "\e61d"; +} + +.icon-shouhoutuikuan:before { + content: "\e631"; +} + +.icon-dizhi:before { + content: "\e614"; +} + +.icon-yishouhuo:before { + content: "\e71a"; +} + +.icon-xuanzhong:before { + content: "\e632"; +} + +.icon-xiangzuo:before { + content: "\e653"; +} + +.icon-iconfontxingxing:before { + content: "\e6b0"; +} + +.icon-jia2:before { + content: "\e60a"; +} + +.icon-sousuo:before { + content: "\e7ce"; +} + +.icon-xiala:before { + content: "\e644"; +} + +.icon-xia:before { + content: "\e62d"; +} + +.icon--jianhao:before { + content: "\e60b"; +} + +.icon-you:before { + content: "\e606"; +} + +.icon-yk_yuanquan:before { + content: "\e601"; +} + +.icon-xing:before { + content: "\e627"; +} + +.icon-guanbi:before { + content: "\e71d"; +} + +.icon-loading:before { + content: "\e646"; +} + diff --git a/yudao-vue-ui/components/jyf-parser/jyf-parser.vue b/yudao-vue-ui/components/jyf-parser/jyf-parser.vue new file mode 100644 index 000000000..01484f9d2 --- /dev/null +++ b/yudao-vue-ui/components/jyf-parser/jyf-parser.vue @@ -0,0 +1,630 @@ + + + + + diff --git a/yudao-vue-ui/components/jyf-parser/libs/CssHandler.js b/yudao-vue-ui/components/jyf-parser/libs/CssHandler.js new file mode 100644 index 000000000..8000377d1 --- /dev/null +++ b/yudao-vue-ui/components/jyf-parser/libs/CssHandler.js @@ -0,0 +1,97 @@ +const cfg = require('./config.js'), + isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + +function CssHandler(tagStyle) { + var styles = Object.assign(Object.create(null), cfg.userAgentStyles); + for (var item in tagStyle) + styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item]; + this.styles = styles; +} +CssHandler.prototype.getStyle = function(data) { + this.styles = new parser(data, this.styles).parse(); +} +CssHandler.prototype.match = function(name, attrs) { + var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : ''; + if (attrs.class) { + var items = attrs.class.split(' '); + for (var i = 0, item; item = items[i]; i++) + if (tmp = this.styles['.' + item]) + matched += tmp + ';'; + } + if (tmp = this.styles['#' + attrs.id]) + matched += tmp + ';'; + return matched; +} +module.exports = CssHandler; + +function parser(data, init) { + this.data = data; + this.floor = 0; + this.i = 0; + this.list = []; + this.res = init; + this.state = this.Space; +} +parser.prototype.parse = function() { + for (var c; c = this.data[this.i]; this.i++) + this.state(c); + return this.res; +} +parser.prototype.section = function() { + return this.data.substring(this.start, this.i); +} +// 状态机 +parser.prototype.Space = function(c) { + if (c == '.' || c == '#' || isLetter(c)) { + this.start = this.i; + this.state = this.Name; + } else if (c == '/' && this.data[this.i + 1] == '*') + this.Comment(); + else if (!cfg.blankChar[c] && c != ';') + this.state = this.Ignore; +} +parser.prototype.Comment = function() { + this.i = this.data.indexOf('*/', this.i) + 1; + if (!this.i) this.i = this.data.length; + this.state = this.Space; +} +parser.prototype.Ignore = function(c) { + if (c == '{') this.floor++; + else if (c == '}' && !--this.floor) this.state = this.Space; +} +parser.prototype.Name = function(c) { + if (cfg.blankChar[c]) { + this.list.push(this.section()); + this.state = this.NameSpace; + } else if (c == '{') { + this.list.push(this.section()); + this.Content(); + } else if (c == ',') { + this.list.push(this.section()); + this.Comma(); + } else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_') + this.state = this.Ignore; +} +parser.prototype.NameSpace = function(c) { + if (c == '{') this.Content(); + else if (c == ',') this.Comma(); + else if (!cfg.blankChar[c]) this.state = this.Ignore; +} +parser.prototype.Comma = function() { + while (cfg.blankChar[this.data[++this.i]]); + if (this.data[this.i] == '{') this.Content(); + else { + this.start = this.i--; + this.state = this.Name; + } +} +parser.prototype.Content = function() { + this.start = ++this.i; + if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length; + var content = this.section(); + for (var i = 0, item; item = this.list[i++];) + if (this.res[item]) this.res[item] += ';' + content; + else this.res[item] = content; + this.list = []; + this.state = this.Space; +} diff --git a/yudao-vue-ui/components/jyf-parser/libs/MpHtmlParser.js b/yudao-vue-ui/components/jyf-parser/libs/MpHtmlParser.js new file mode 100644 index 000000000..8911e36d3 --- /dev/null +++ b/yudao-vue-ui/components/jyf-parser/libs/MpHtmlParser.js @@ -0,0 +1,534 @@ +/** + * html 解析器 + * @tutorial https://github.com/jin-yufeng/Parser + * @version 20200719 + * @author JinYufeng + * @listens MIT + */ +const cfg = require('./config.js'), + blankChar = cfg.blankChar, + CssHandler = require('./CssHandler.js'), + windowWidth = uni.getSystemInfoSync().windowWidth; +var emoji; + +function MpHtmlParser(data, options = {}) { + this.attrs = {}; + this.CssHandler = new CssHandler(options.tagStyle, windowWidth); + this.data = data; + this.domain = options.domain; + this.DOM = []; + this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0; + options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http'; + this.options = options; + this.state = this.Text; + this.STACK = []; + // 工具函数 + this.bubble = () => { + for (var i = this.STACK.length, item; item = this.STACK[--i];) { + if (cfg.richOnlyTags[item.name]) { + if (item.name == 'table' && !Object.hasOwnProperty.call(item, 'c')) item.c = 1; + return false; + } + item.c = 1; + } + return true; + } + this.decode = (val, amp) => { + var i = -1, + j, en; + while (1) { + if ((i = val.indexOf('&', i + 1)) == -1) break; + if ((j = val.indexOf(';', i + 2)) == -1) break; + if (val[i + 1] == '#') { + en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j)); + if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1); + } else { + en = val.substring(i + 1, j); + if (cfg.entities[en] || en == amp) + val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1); + } + } + return val; + } + this.getUrl = url => { + if (url[0] == '/') { + if (url[1] == '/') url = this.options.prot + ':' + url; + else if (this.domain) url = this.domain + url; + } else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://')) + url = this.domain + '/' + url; + return url; + } + this.isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>'); + this.section = () => this.data.substring(this.start, this.i); + this.parent = () => this.STACK[this.STACK.length - 1]; + this.siblings = () => this.STACK.length ? this.parent().children : this.DOM; +} +MpHtmlParser.prototype.parse = function() { + if (emoji) this.data = emoji.parseEmoji(this.data); + for (var c; c = this.data[this.i]; this.i++) + this.state(c); + if (this.state == this.Text) this.setText(); + while (this.STACK.length) this.popNode(this.STACK.pop()); + return this.DOM; +} +// 设置属性 +MpHtmlParser.prototype.setAttr = function() { + var name = this.attrName.toLowerCase(), + val = this.attrVal; + if (cfg.boolAttrs[name]) this.attrs[name] = 'T'; + else if (val) { + if (name == 'src' || (name == 'data-src' && !this.attrs.src)) this.attrs.src = this.getUrl(this.decode(val, 'amp')); + else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp'); + else if (name.substr(0, 5) != 'data-') this.attrs[name] = val; + } + this.attrVal = ''; + while (blankChar[this.data[this.i]]) this.i++; + if (this.isClose()) this.setNode(); + else { + this.start = this.i; + this.state = this.AttrName; + } +} +// 设置文本节点 +MpHtmlParser.prototype.setText = function() { + var back, text = this.section(); + if (!text) return; + text = (cfg.onText && cfg.onText(text, () => back = true)) || text; + if (back) { + this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i); + let j = this.start + text.length; + for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]); + return; + } + if (!this.pre) { + // 合并空白符 + var tmp = []; + for (let i = text.length, c; c = text[--i];) + if (!blankChar[c] || (!blankChar[tmp[0]] && (c = ' '))) tmp.unshift(c); + text = tmp.join(''); + } + this.siblings().push({ + type: 'text', + text: this.decode(text) + }); +} +// 设置元素节点 +MpHtmlParser.prototype.setNode = function() { + var node = { + name: this.tagName.toLowerCase(), + attrs: this.attrs + }, + close = cfg.selfClosingTags[node.name]; + this.attrs = {}; + if (!cfg.ignoreTags[node.name]) { + // 处理属性 + var attrs = node.attrs, + style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''), + styleObj = {}; + if (attrs.id) { + if (this.options.compress & 1) attrs.id = void 0; + else if (this.options.useAnchor) this.bubble(); + } + if ((this.options.compress & 2) && attrs.class) attrs.class = void 0; + switch (node.name) { + case 'a': + case 'ad': // #ifdef APP-PLUS + case 'iframe': + // #endif + this.bubble(); + break; + case 'font': + if (attrs.color) { + styleObj['color'] = attrs.color; + attrs.color = void 0; + } + if (attrs.face) { + styleObj['font-family'] = attrs.face; + attrs.face = void 0; + } + if (attrs.size) { + var size = parseInt(attrs.size); + if (size < 1) size = 1; + else if (size > 7) size = 7; + var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large']; + styleObj['font-size'] = map[size - 1]; + attrs.size = void 0; + } + break; + case 'embed': + // #ifndef APP-PLUS + var src = node.attrs.src || '', + type = node.attrs.type || ''; + if (type.includes('video') || src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8')) + node.name = 'video'; + else if (type.includes('audio') || src.includes('.m4a') || src.includes('.wav') || src.includes('.mp3') || src.includes( + '.aac')) + node.name = 'audio'; + else break; + if (node.attrs.autostart) + node.attrs.autoplay = 'T'; + node.attrs.controls = 'T'; + // #endif + // #ifdef APP-PLUS + this.bubble(); + break; + // #endif + case 'video': + case 'audio': + if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]); + else this[`${node.name}Num`]++; + if (node.name == 'video') { + if (this.videoNum > 3) + node.lazyLoad = 1; + if (attrs.width) { + styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px'); + attrs.width = void 0; + } + if (attrs.height) { + styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px'); + attrs.height = void 0; + } + } + attrs.source = []; + if (attrs.src) { + attrs.source.push(attrs.src); + attrs.src = void 0; + } + this.bubble(); + break; + case 'td': + case 'th': + if (attrs.colspan || attrs.rowspan) + for (var k = this.STACK.length, item; item = this.STACK[--k];) + if (item.name == 'table') { + item.c = void 0; + break; + } + } + if (attrs.align) { + styleObj['text-align'] = attrs.align; + attrs.align = void 0; + } + // 压缩 style + var styles = style.split(';'); + style = ''; + for (var i = 0, len = styles.length; i < len; i++) { + var info = styles[i].split(':'); + if (info.length < 2) continue; + let key = info[0].trim().toLowerCase(), + value = info.slice(1).join(':').trim(); + if (value.includes('-webkit') || value.includes('-moz') || value.includes('-ms') || value.includes('-o') || value.includes( + 'safe')) + style += `;${key}:${value}`; + else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import')) + styleObj[key] = value; + } + if (node.name == 'img') { + if (attrs.src && !attrs.ignore) { + if (this.bubble()) + attrs.i = (this.imgNum++).toString(); + else attrs.ignore = 'T'; + } + if (attrs.ignore) { + style += ';-webkit-touch-callout:none'; + styleObj['max-width'] = '100%'; + } + var width; + if (styleObj.width) width = styleObj.width; + else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : attrs.width + 'px'; + if (width) { + styleObj.width = width; + attrs.width = '100%'; + if (parseInt(width) > windowWidth) { + styleObj.height = ''; + if (attrs.height) attrs.height = void 0; + } + } + if (styleObj.height) { + attrs.height = styleObj.height; + styleObj.height = ''; + } else if (attrs.height && !attrs.height.includes('%')) + attrs.height += 'px'; + } + for (var key in styleObj) { + var value = styleObj[key]; + if (!value) continue; + if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1; + // 填充链接 + if (value.includes('url')) { + var j = value.indexOf('('); + if (j++ != -1) { + while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++; + value = value.substr(0, j) + this.getUrl(value.substr(j)); + } + } + // 转换 rpx + else if (value.includes('rpx')) + value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px'); + else if (key == 'white-space' && value.includes('pre') && !close) + this.pre = node.pre = true; + style += `;${key}:${value}`; + } + style = style.substr(1); + if (style) attrs.style = style; + if (!close) { + node.children = []; + if (node.name == 'pre' && cfg.highlight) { + this.remove(node); + this.pre = node.pre = true; + } + this.siblings().push(node); + this.STACK.push(node); + } else if (!cfg.filter || cfg.filter(node, this) != false) + this.siblings().push(node); + } else { + if (!close) this.remove(node); + else if (node.name == 'source') { + var parent = this.parent(); + if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src) + parent.attrs.source.push(node.attrs.src); + } else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href; + } + if (this.data[this.i] == '/') this.i++; + this.start = this.i + 1; + this.state = this.Text; +} +// 移除标签 +MpHtmlParser.prototype.remove = function(node) { + var name = node.name, + j = this.i; + // 处理 svg + var handleSvg = () => { + var src = this.data.substring(j, this.i + 1); + if (!node.attrs.xmlns) src = ' xmlns="http://www.w3.org/2000/svg"' + src; + var i = j; + while (this.data[j] != '<') j--; + src = this.data.substring(j, i).replace("viewbox", "viewBox") + src; + var parent = this.parent(); + if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline')) + parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style; + this.siblings().push({ + name: 'img', + attrs: { + src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'), + style: (/vertical[^;]+/.exec(node.attrs.style) || []).shift(), + ignore: 'T' + } + }) + } + if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++); + while (1) { + if ((this.i = this.data.indexOf('', this.i)) == -1) this.i = this.data.length; + if (name == 'svg') handleSvg(); + return; + } + } +} +// 节点出栈处理 +MpHtmlParser.prototype.popNode = function(node) { + // 空白符处理 + if (node.pre) { + node.pre = this.pre = void 0; + for (let i = this.STACK.length; i--;) + if (this.STACK[i].pre) + this.pre = true; + } + var siblings = this.siblings(), + len = siblings.length, + childs = node.children; + if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false)) + return siblings.pop(); + var attrs = node.attrs; + // 替换一些标签名 + if (cfg.blockTags[node.name]) node.name = 'div'; + else if (!cfg.trustTags[node.name]) node.name = 'span'; + // 去除块标签前后空串 + if (node.name == 'div' || node.name == 'p' || node.name[0] == 't') { + if (len > 1 && siblings[len - 2].text == ' ') + siblings.splice(--len - 1, 1); + if (childs.length && childs[childs.length - 1].text == ' ') + childs.pop(); + } + // 处理列表 + if (node.c && (node.name == 'ul' || node.name == 'ol')) { + if ((node.attrs.style || '').includes('list-style:none')) { + for (let i = 0, child; child = childs[i++];) + if (child.name == 'li') + child.name = 'div'; + } else if (node.name == 'ul') { + var floor = 1; + for (let i = this.STACK.length; i--;) + if (this.STACK[i].name == 'ul') floor++; + if (floor != 1) + for (let i = childs.length; i--;) + childs[i].floor = floor; + } else { + for (let i = 0, num = 1, child; child = childs[i++];) + if (child.name == 'li') { + child.type = 'ol'; + child.num = ((num, type) => { + if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26); + if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26); + if (type == 'i' || type == 'I') { + num = (num - 1) % 99 + 1; + var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'], + ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'], + res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || ''); + if (type == 'i') return res.toLowerCase(); + return res; + } + return num; + })(num++, attrs.type) + '.'; + } + } + } + // 处理表格的边框 + if (node.name == 'table') { + var padding = attrs.cellpadding, + spacing = attrs.cellspacing, + border = attrs.border; + if (node.c) { + this.bubble(); + attrs.style = (attrs.style || '') + ';display:table'; + if (!padding) padding = 2; + if (!spacing) spacing = 2; + } + if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`; + if (spacing) attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`; + if (border || padding || node.c) + (function f(ns) { + for (var i = 0, n; n = ns[i]; i++) { + if (n.type == 'text') continue; + var style = n.attrs.style || ''; + if (node.c && n.name[0] == 't') { + n.c = 1; + style += ';display:table-' + (n.name == 'th' || n.name == 'td' ? 'cell' : (n.name == 'tr' ? 'row' : 'row-group')); + } + if (n.name == 'th' || n.name == 'td') { + if (border) style = `border:${border}px solid gray;${style}`; + if (padding) style = `padding:${padding}px;${style}`; + } else f(n.children || []); + if (style) n.attrs.style = style; + } + })(childs) + if (this.options.autoscroll) { + var table = Object.assign({}, node); + node.name = 'div'; + node.attrs = { + style: 'overflow:scroll' + } + node.children = [table]; + } + } + this.CssHandler.pop && this.CssHandler.pop(node); + // 自动压缩 + if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div') + siblings[len - 1] = childs[0]; +} +// 状态机 +MpHtmlParser.prototype.Text = function(c) { + if (c == '<') { + var next = this.data[this.i + 1], + isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + if (isLetter(next)) { + this.setText(); + this.start = this.i + 1; + this.state = this.TagName; + } else if (next == '/') { + this.setText(); + if (isLetter(this.data[++this.i + 1])) { + this.start = this.i + 1; + this.state = this.EndTag; + } else this.Comment(); + } else if (next == '!' || next == '?') { + this.setText(); + this.Comment(); + } + } +} +MpHtmlParser.prototype.Comment = function() { + var key; + if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->'; + else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>'; + else key = '>'; + if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length; + else this.i += key.length - 1; + this.start = this.i + 1; + this.state = this.Text; +} +MpHtmlParser.prototype.TagName = function(c) { + if (blankChar[c]) { + this.tagName = this.section(); + while (blankChar[this.data[this.i]]) this.i++; + if (this.isClose()) this.setNode(); + else { + this.start = this.i; + this.state = this.AttrName; + } + } else if (this.isClose()) { + this.tagName = this.section(); + this.setNode(); + } +} +MpHtmlParser.prototype.AttrName = function(c) { + if (c == '=' || blankChar[c] || this.isClose()) { + this.attrName = this.section(); + if (blankChar[c]) + while (blankChar[this.data[++this.i]]); + if (this.data[this.i] == '=') { + while (blankChar[this.data[++this.i]]); + this.start = this.i--; + this.state = this.AttrValue; + } else this.setAttr(); + } +} +MpHtmlParser.prototype.AttrValue = function(c) { + if (c == '"' || c == "'") { + this.start++; + if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length; + this.attrVal = this.section(); + this.i++; + } else { + for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++); + this.attrVal = this.section(); + } + this.setAttr(); +} +MpHtmlParser.prototype.EndTag = function(c) { + if (blankChar[c] || c == '>' || c == '/') { + var name = this.section().toLowerCase(); + for (var i = this.STACK.length; i--;) + if (this.STACK[i].name == name) break; + if (i != -1) { + var node; + while ((node = this.STACK.pop()).name != name) this.popNode(node); + this.popNode(node); + } else if (name == 'p' || name == 'br') + this.siblings().push({ + name, + attrs: {} + }); + this.i = this.data.indexOf('>', this.i); + this.start = this.i + 1; + if (this.i == -1) this.i = this.data.length; + else this.state = this.Text; + } +} +module.exports = MpHtmlParser; diff --git a/yudao-vue-ui/components/jyf-parser/libs/config.js b/yudao-vue-ui/components/jyf-parser/libs/config.js new file mode 100644 index 000000000..1cfc111b5 --- /dev/null +++ b/yudao-vue-ui/components/jyf-parser/libs/config.js @@ -0,0 +1,93 @@ +/* 配置文件 */ +// #ifdef MP-WEIXIN +const canIUse = wx.canIUse('editor'); // 高基础库标识,用于兼容 +// #endif +module.exports = { + // 出错占位图 + errorImg: null, + // 过滤器函数 + filter: null, + // 代码高亮函数 + highlight: null, + // 文本处理函数 + onText: null, + // 实体编码列表 + entities: { + quot: '"', + apos: "'", + semi: ';', + nbsp: '\xA0', + ensp: '\u2002', + emsp: '\u2003', + ndash: '–', + mdash: '—', + middot: '·', + lsquo: '‘', + rsquo: '’', + ldquo: '“', + rdquo: '”', + bull: '•', + hellip: '…' + }, + blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'), + boolAttrs: makeMap('allowfullscreen,autoplay,autostart,controls,ignore,loop,muted'), + // 块级标签,将被转为 div + blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,section' + ( + // #ifdef MP-WEIXIN + canIUse ? '' : + // #endif + ',pre')), + // 将被移除的标签 + ignoreTags: makeMap( + 'area,base,canvas,frame,input,link,map,meta,param,script,source,style,svg,textarea,title,track,wbr' + // #ifdef MP-WEIXIN + + (canIUse ? ',rp' : '') + // #endif + // #ifndef APP-PLUS + + ',iframe' + // #endif + ), + // 只能被 rich-text 显示的标签 + richOnlyTags: makeMap('a,colgroup,fieldset,legend,table' + // #ifdef MP-WEIXIN + + (canIUse ? ',bdi,bdo,caption,rt,ruby' : '') + // #endif + ), + // 自闭合的标签 + selfClosingTags: makeMap( + 'area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr' + ), + // 信任的标签 + trustTags: makeMap( + 'a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video' + // #ifdef MP-WEIXIN + + (canIUse ? ',bdi,bdo,caption,pre,rt,ruby' : '') + // #endif + // #ifdef APP-PLUS + + ',embed,iframe' + // #endif + ), + // 默认的标签样式 + userAgentStyles: { + address: 'font-style:italic', + big: 'display:inline;font-size:1.2em', + blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px', + caption: 'display:table-caption;text-align:center', + center: 'text-align:center', + cite: 'font-style:italic', + dd: 'margin-left:40px', + mark: 'background-color:yellow', + pre: 'font-family:monospace;white-space:pre;overflow:scroll', + s: 'text-decoration:line-through', + small: 'display:inline;font-size:0.8em', + u: 'text-decoration:underline' + } +} + +function makeMap(str) { + var map = Object.create(null), + list = str.split(','); + for (var i = list.length; i--;) + map[list[i]] = true; + return map; +} diff --git a/yudao-vue-ui/components/jyf-parser/libs/handler.wxs b/yudao-vue-ui/components/jyf-parser/libs/handler.wxs new file mode 100644 index 000000000..d3b1aaabe --- /dev/null +++ b/yudao-vue-ui/components/jyf-parser/libs/handler.wxs @@ -0,0 +1,22 @@ +var inline = { + abbr: 1, + b: 1, + big: 1, + code: 1, + del: 1, + em: 1, + i: 1, + ins: 1, + label: 1, + q: 1, + small: 1, + span: 1, + strong: 1, + sub: 1, + sup: 1 +} +module.exports = { + use: function(item) { + return !item.c && !inline[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1 + } +} diff --git a/yudao-vue-ui/components/jyf-parser/libs/trees.vue b/yudao-vue-ui/components/jyf-parser/libs/trees.vue new file mode 100644 index 000000000..8232aac14 --- /dev/null +++ b/yudao-vue-ui/components/jyf-parser/libs/trees.vue @@ -0,0 +1,500 @@ +