Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/1.8.0-uniapp

 Conflicts:
	sql/ruoyi-vue-pro.sql
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/AppAddressController.http
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/AppAddressController.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/package-info.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/vo/AppAddressBaseVO.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/vo/AppAddressCreateReqVO.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/vo/AppAddressRespVO.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/vo/AppAddressUpdateReqVO.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/address/AddressConvert.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/address/AddressDO.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/address/package-info.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/address/AddressMapper.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/enums/AddressTypeEnum.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/address/AddressService.java
	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/address/AddressServiceImpl.java
	yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/address/AddressServiceImplTest.java
This commit is contained in:
YunaiV
2022-04-22 20:18:50 +08:00
1021 changed files with 12497 additions and 9311 deletions

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-member</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-member-biz</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
member 模块,我们放会员业务。
例如说:会员中心等等
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-member-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-system-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-infra-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-redis</artifactId>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mq</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 工具类相关 -->
</dependencies>
</project>

View File

@@ -0,0 +1 @@
package cn.iocoder.yudao.module.member.api;

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.member.api.user;
import cn.iocoder.yudao.module.member.api.user.dto.UserRespDTO;
import cn.iocoder.yudao.module.member.convert.user.UserConvert;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
/**
* 会员用户的 API 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class MemberUserApiImpl implements MemberUserApi {
@Resource
private MemberUserService userService;
@Override
public UserRespDTO getUser(Long id) {
MemberUserDO user = userService.getUser(id);
return UserConvert.INSTANCE.convert2(user);
}
}

View File

@@ -0,0 +1 @@
package cn.iocoder.yudao.module.member.controller.admin.address;

View File

@@ -0,0 +1,4 @@
package cn.iocoder.yudao.module.member.controller.admin.user;
public class UserController {
}

View File

@@ -0,0 +1 @@
package cn.iocoder.yudao.module.member.controller.admin.user;

View File

@@ -0,0 +1,54 @@
### 请求 /create 接口 => 成功
POST {{appApi}}//member/address/create
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer 2510e2e4287346eb8e36353a55e27fd6
{
"userId": "245",
"name": "yunai",
"mobile": "15601691300",
"areaCode": "610632",
"detailAddress": "芋道源码 233 号 666 室",
"type": "1"
}
### 请求 /update 接口 => 成功
PUT {{appApi}}//member/address/update
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer 2510e2e4287346eb8e36353a55e27fd6
{
"id": "1",
"userId": "245",
"name": "yunai888",
"mobile": "15601691300",
"areaCode": "610632",
"detailAddress": "芋道源码 233 号 666 室",
"type": "1"
}
### 请求 /delete 接口 => 成功
DELETE {{appApi}}//member/address/delete?id=2
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer fa4848b001de4eae9faf516c0c8520f8
### 请求 /get 接口 => 成功
GET {{appApi}}//member/address/get?id=1
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer fa4848b001de4eae9faf516c0c8520f8
### 请求 /get-default 接口 => 成功
GET {{appApi}}//member/address/get-default
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer fa4848b001de4eae9faf516c0c8520f8
### 请求 /list 接口 => 成功
GET {{appApi}}//member/address/list
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer fa4848b001de4eae9faf516c0c8520f8

View File

@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.member.controller.app.address;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.member.controller.app.address.vo.AppAddressCreateReqVO;
import cn.iocoder.yudao.module.member.controller.app.address.vo.AppAddressRespVO;
import cn.iocoder.yudao.module.member.controller.app.address.vo.AppAddressUpdateReqVO;
import cn.iocoder.yudao.module.member.convert.address.AddressConvert;
import cn.iocoder.yudao.module.member.dal.dataobject.address.AddressDO;
import cn.iocoder.yudao.module.member.service.address.AddressService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Api(tags = "用户 APP - 用户收件地址")
@RestController
@RequestMapping("/member/address")
@Validated
public class AppAddressController {
@Resource
private AddressService addressService;
@PostMapping("/create")
@ApiOperation("创建用户收件地址")
public CommonResult<Long> createAddress(@Valid @RequestBody AppAddressCreateReqVO createReqVO) {
return success(addressService.createAddress(getLoginUserId(), createReqVO));
}
@PutMapping("/update")
@ApiOperation("更新用户收件地址")
public CommonResult<Boolean> updateAddress(@Valid @RequestBody AppAddressUpdateReqVO updateReqVO) {
addressService.updateAddress(getLoginUserId(), updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除用户收件地址")
@ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
public CommonResult<Boolean> deleteAddress(@RequestParam("id") Long id) {
addressService.deleteAddress(getLoginUserId(), id);
return success(true);
}
@GetMapping("/get")
@ApiOperation("获得用户收件地址")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
public CommonResult<AppAddressRespVO> getAddress(@RequestParam("id") Long id) {
AddressDO address = addressService.getAddress(getLoginUserId(), id);
return success(AddressConvert.INSTANCE.convert(address));
}
@GetMapping("/get-default")
@ApiOperation("获得默认的用户收件地址")
public CommonResult<AppAddressRespVO> getDefaultUserAddress() {
AddressDO address = addressService.getDefaultUserAddress(getLoginUserId());
return success(AddressConvert.INSTANCE.convert(address));
}
@GetMapping("/list")
@ApiOperation("获得用户收件地址列表")
public CommonResult<List<AppAddressRespVO>> getAddressList() {
List<AddressDO> list = addressService.getAddressList(getLoginUserId());
return success(AddressConvert.INSTANCE.convertList(list));
}
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.member.controller.app.address.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.member.enums.AddressTypeEnum;
import lombok.*;
import io.swagger.annotations.*;
import javax.validation.constraints.*;
/**
* 用户收件地址 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class AppAddressBaseVO {
@ApiModelProperty(value = "收件人名称", required = true)
@NotNull(message = "收件人名称不能为空")
private String name;
@ApiModelProperty(value = "手机号", required = true)
@NotNull(message = "手机号不能为空")
private String mobile;
@ApiModelProperty(value = "地区编码", required = true)
@NotNull(message = "地区编码不能为空")
private Integer areaCode;
@ApiModelProperty(value = "收件详细地址", required = true)
@NotNull(message = "收件详细地址不能为空")
private String detailAddress;
@ApiModelProperty(value = "地址类型", required = true)
@NotNull(message = "地址类型不能为空")
@InEnum(AddressTypeEnum.class)
private Integer type;
}

View File

@@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.member.controller.app.address.vo;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
import javax.validation.constraints.*;
@ApiModel("用户 APP - 用户收件地址创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppAddressCreateReqVO extends AppAddressBaseVO {
}

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.member.controller.app.address.vo;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
@ApiModel("用户 APP - 用户收件地址 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppAddressRespVO extends AppAddressBaseVO {
@ApiModelProperty(value = "编号", required = true)
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.member.controller.app.address.vo;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
import javax.validation.constraints.*;
@ApiModel("用户 APP - 用户收件地址更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppAddressUpdateReqVO extends AppAddressBaseVO {
@ApiModelProperty(value = "编号", required = true)
@NotNull(message = "编号不能为空")
private Long id;
}

View File

@@ -0,0 +1,35 @@
### 请求 /login 接口 => 成功
POST {{appApi}}/member/login
Content-Type: application/json
tenant-id: {{appTenentId}}
{
"mobile": "15601691300",
"password": "admin123"
}
### 请求 /send-sms-code 接口 => 成功
POST {{appApi}}/member/send-sms-code
Content-Type: application/json
tenant-id: {{appTenentId}}
{
"mobile": "15601691399",
"scene": 1
}
### 请求 /sms-login 接口 => 成功
POST {{appApi}}/member/sms-login
Content-Type: application/json
tenant-id: {{appTenentId}}
{
"mobile": "15601691301",
"code": 9999
}
### 请求 /logout 接口 => 成功
POST {{appApi}}/member/logout
Content-Type: application/json
Authorization: Bearer c1b76bdaf2c146c581caa4d7fd81ee66
tenant-id: {{appTenentId}}

View File

@@ -0,0 +1,121 @@
package cn.iocoder.yudao.module.member.controller.app.auth;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
import cn.iocoder.yudao.module.member.service.auth.MemberAuthService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
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.common.util.servlet.ServletUtils.getUserAgent;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Api(tags = "用户 APP - 认证")
@RestController
@RequestMapping("/member/")
@Validated
@Slf4j
public class AppAuthController {
@Resource
private MemberAuthService authService;
@PostMapping("/login")
@ApiOperation("使用手机 + 密码登录")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) {
String token = authService.login(reqVO, getClientIP(), getUserAgent());
// 返回结果
return success(AppAuthLoginRespVO.builder().token(token).build());
}
@PostMapping("/sms-login")
@ApiOperation("使用手机 + 验证码登录")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AppAuthLoginRespVO> smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) {
String token = authService.smsLogin(reqVO, getClientIP(), getUserAgent());
// 返回结果
return success(AppAuthLoginRespVO.builder().token(token).build());
}
@PostMapping("/send-sms-code")
@ApiOperation(value = "发送手机验证码")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid AppAuthSendSmsReqVO reqVO) {
authService.sendSmsCode(getLoginUserId(), reqVO);
return success(true);
}
@PostMapping("/reset-password")
@ApiOperation(value = "重置密码", notes = "用户忘记密码时使用")
@PreAuthenticated
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<Boolean> resetPassword(@RequestBody @Valid AppAuthResetPasswordReqVO reqVO) {
authService.resetPassword(reqVO);
return success(true);
}
@PostMapping("/update-password")
@ApiOperation(value = "修改用户密码",notes = "用户修改密码时使用")
@PreAuthenticated
public CommonResult<Boolean> updatePassword(@RequestBody @Valid AppAuthUpdatePasswordReqVO reqVO) {
authService.updatePassword(getLoginUserId(), reqVO);
return success(true);
}
// ========== 社交登录相关 ==========
@GetMapping("/social-auth-redirect")
@ApiOperation("社交授权的跳转")
@ApiImplicitParams({
@ApiImplicitParam(name = "type", value = "社交类型", required = true, dataTypeClass = Integer.class),
@ApiImplicitParam(name = "redirectUri", value = "回调路径", dataTypeClass = String.class)
})
public CommonResult<String> socialAuthRedirect(@RequestParam("type") Integer type,
@RequestParam("redirectUri") String redirectUri) {
return CommonResult.success(authService.getSocialAuthorizeUrl(type, redirectUri));
}
@PostMapping("/social-login")
@ApiOperation(value = "社交登录,使用 code 授权码", notes = "适合未登录的用户,但是社交账号已绑定用户")
public CommonResult<AppAuthLoginRespVO> socialLogin(@RequestBody @Valid AppAuthSocialLoginReqVO reqVO) {
String token = authService.socialLogin(reqVO, getClientIP(), getUserAgent());
return success(AppAuthLoginRespVO.builder().token(token).build());
}
@PostMapping("/social-login2")
@ApiOperation(value = "社交登录,使用 手机号 + 手机验证码", notes = "适合未登录的用户,进行登录 + 绑定")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AppAuthLoginRespVO> socialLogin2(@RequestBody @Valid AppAuthSocialLogin2ReqVO reqVO) {
String token = authService.socialLogin2(reqVO, getClientIP(), getUserAgent());
return success(AppAuthLoginRespVO.builder().token(token).build());
}
@PostMapping("/social-bind")
@ApiOperation(value = "社交绑定,使用 code 授权码", notes = "使用在用户已经登录的情况下")
@PreAuthenticated
public CommonResult<Boolean> socialBind(@RequestBody @Valid AppAuthSocialBindReqVO reqVO) {
authService.socialBind(getLoginUserId(), reqVO);
return CommonResult.success(true);
}
@DeleteMapping("/social-unbind")
@ApiOperation("取消社交绑定")
@PreAuthenticated
public CommonResult<Boolean> socialUnbind(@RequestBody AppAuthSocialUnbindReqVO reqVO) {
authService.unbindSocialUser(getLoginUserId(), reqVO);
return CommonResult.success(true);
}
}

View File

@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.framework.common.validation.Mobile;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
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.NotNull;
import javax.validation.constraints.Pattern;
// TODO 芋艿code review 相关逻辑
@ApiModel("用户 APP - 校验验证码 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthCheckCodeReqVO {
@ApiModelProperty(value = "手机号", example = "15601691234")
@NotBlank(message = "手机号不能为空")
@Mobile
private String mobile;
@ApiModelProperty(value = "手机验证码", required = true, example = "1024")
@NotBlank(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
@ApiModelProperty(value = "发送场景", example = "1", notes = "对应 SmsSceneEnum 枚举")
@NotNull(message = "发送场景不能为空")
@InEnum(SmsSceneEnum.class)
private Integer scene;
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import cn.iocoder.yudao.framework.common.validation.Mobile;
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.NotEmpty;
@ApiModel("用户 APP - 手机 + 密码登录 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthLoginReqVO {
@ApiModelProperty(value = "手机号", required = true, example = "15601691300")
@NotEmpty(message = "手机号不能为空")
@Mobile
private String mobile;
@ApiModelProperty(value = "密码", required = true, example = "buzhidao")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
}

View File

@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@ApiModel("用户 APP - 手机密码登录 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthLoginRespVO {
@ApiModelProperty(value = "token", required = true, example = "yudaoyuanma")
private String token;
}

View File

@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import cn.iocoder.yudao.framework.common.validation.Mobile;
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;
// TODO 芋艿code review 相关逻辑
@ApiModel("用户 APP - 重置密码 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthResetPasswordReqVO {
@ApiModelProperty(value = "新密码", required = true, example = "buzhidao")
@NotEmpty(message = "新密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
@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 = "15878962356")
@NotBlank(message = "手机号不能为空")
@Mobile
private String mobile;
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.framework.common.validation.Mobile;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@ApiModel("用户 APP - 发送手机验证码 Response VO")
@Data
@Accessors(chain = true)
public class AppAuthSendSmsReqVO {
@ApiModelProperty(value = "手机号", example = "15601691234")
@Mobile
private String mobile;
@ApiModelProperty(value = "发送场景", example = "1", notes = "对应 SmsSceneEnum 枚举")
@NotNull(message = "发送场景不能为空")
@InEnum(SmsSceneEnum.class)
private Integer scene;
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import cn.iocoder.yudao.framework.common.validation.Mobile;
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.NotEmpty;
import javax.validation.constraints.Pattern;
@ApiModel("用户 APP - 手机 + 验证码登录 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthSmsLoginReqVO {
@ApiModelProperty(value = "手机号", required = true, example = "15601691300")
@NotEmpty(message = "手机号不能为空")
@Mobile
private String mobile;
@ApiModelProperty(value = "手机验证码", required = true, example = "1024")
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@ApiModel("用户 APP - 社交绑定 Request VO使用 code 授权码")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthSocialBindReqVO {
@ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SysUserSocialTypeEnum 枚举值")
@InEnum(SocialTypeEnum.class)
@NotNull(message = "社交平台的类型不能为空")
private Integer type;
@ApiModelProperty(value = "授权码", required = true, example = "1024")
@NotEmpty(message = "授权码不能为空")
private String code;
@ApiModelProperty(value = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
@NotEmpty(message = "state 不能为空")
private String state;
}

View File

@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
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.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@ApiModel("用户 APP - 社交登录 Request VO使用 code 授权码 + 账号密码")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthSocialLogin2ReqVO {
@ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SysUserSocialTypeEnum 枚举值")
@InEnum(SocialTypeEnum.class)
@NotNull(message = "社交平台的类型不能为空")
private Integer type;
@ApiModelProperty(value = "授权码", required = true, example = "1024")
@NotEmpty(message = "授权码不能为空")
private String code;
@ApiModelProperty(value = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
@NotEmpty(message = "state 不能为空")
private String state;
@ApiModelProperty(value = "手机号", required = true, example = "15119100000")
@NotEmpty(message = "手机号不能为空")
@Length(min = 11, max = 11, message = "手机号是11位数字")
private String mobile;
@ApiModelProperty(value = "手机验证码", required = true, example = "1024")
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String smsCode;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@ApiModel("用户 APP - 社交登录 Request VO使用 code 授权码")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthSocialLoginReqVO {
@ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SysUserSocialTypeEnum 枚举值")
@InEnum(SocialTypeEnum.class)
@NotNull(message = "社交平台的类型不能为空")
private Integer type;
@ApiModelProperty(value = "授权码", required = true, example = "1024")
@NotEmpty(message = "授权码不能为空")
private String code;
@ApiModelProperty(value = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
@NotEmpty(message = "state 不能为空")
private String state;
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@ApiModel("用户 APP - 取消社交绑定 Request VO使用 code 授权码")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthSocialUnbindReqVO {
@ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SysUserSocialTypeEnum 枚举值")
@InEnum(SocialTypeEnum.class)
@NotNull(message = "社交平台的类型不能为空")
private Integer type;
@ApiModelProperty(value = "社交的全局编号", required = true, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE")
@NotEmpty(message = "社交的全局编号不能为空")
private String unionId;
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.member.controller.app.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;
// TODO 芋艿code review 相关逻辑
@ApiModel("用户 APP - 修改密码 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthUpdatePasswordReqVO {
@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;
}

View File

@@ -0,0 +1,14 @@
### 请求 /member/user/profile/get 接口 => 没有权限
GET {{appApi}}/member/user/get
Authorization: Bearer test245
tenant-id: {{appTenentId}}
### 请求 /member/user/profile/revise-nickname 接口 成功
PUT {{appApi}}/member/user/update-nickname?nickname=yunai222
Authorization: Bearer test245
tenant-id: {{appTenentId}}
### 请求 /member/user/get-user-info 接口 成功
GET {{appApi}}/member/user/get-user-info?id=245
Authorization: Bearer test245
tenant-id: {{appTenentId}}

View File

@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.member.controller.app.user;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.module.member.controller.app.user.vo.AppUserInfoRespVO;
import cn.iocoder.yudao.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO;
import cn.iocoder.yudao.module.member.convert.user.UserConvert;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.validation.Valid;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;
@Api(tags = "用户 APP - 用户个人中心")
@RestController
@RequestMapping("/member/user")
@Validated
@Slf4j
public class AppUserController {
@Resource
private MemberUserService userService;
@PutMapping("/update-nickname")
@ApiOperation("修改用户昵称")
@PreAuthenticated
public CommonResult<Boolean> updateUserNickname(@RequestParam("nickname") String nickname) {
userService.updateUserNickname(getLoginUserId(), nickname);
return success(true);
}
@PostMapping("/update-avatar")
@ApiOperation("修改用户头像")
@PreAuthenticated
public CommonResult<String> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception {
if (file.isEmpty()) {
throw exception(FILE_IS_EMPTY);
}
String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream());
return success(avatar);
}
@GetMapping("/get")
@ApiOperation("获得基本信息")
@PreAuthenticated
public CommonResult<AppUserInfoRespVO> getUserInfo() {
MemberUserDO user = userService.getUser(getLoginUserId());
return success(UserConvert.INSTANCE.convert(user));
}
@PostMapping("/update-mobile")
@ApiOperation(value = "修改用户手机")
@PreAuthenticated
public CommonResult<Boolean> updateMobile(@RequestBody @Valid AppUserUpdateMobileReqVO reqVO) {
userService.updateUserMobile(getLoginUserId(), reqVO);
return success(true);
}
}

View File

@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.member.controller.app.user.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@ApiModel("用户 APP - 用户个人信息 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppUserInfoRespVO {
@ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
private String nickname;
@ApiModelProperty(value = "用户头像", required = true, example = "/infra/file/get/35a12e57-4297-4faa-bf7d-7ed2f211c952")
private String avatar;
}

View File

@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.member.controller.app.user.vo;
import cn.iocoder.yudao.framework.common.validation.Mobile;
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("用户 APP - 修改手机 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppUserUpdateMobileReqVO {
@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 位")
@Mobile
private String mobile;
@ApiModelProperty(value = "原手机验证码", required = true, example = "1024")
@NotEmpty(message = "原手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String oldCode;
// TODO @芋艿oldMobile 应该不用传递
@ApiModelProperty(value = "原手机号",required = true,example = "15823654487")
@NotBlank(message = "手机号不能为空")
@Length(min = 8, max = 11, message = "手机号码长度为 8-11 位")
@Mobile
private String oldMobile;
}

View File

@@ -0,0 +1,2 @@
### 请求 /login 接口 => 成功
GET {{userServerUrl}}/wx/mp/get-jsapi-ticket

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.member.controller.app.weixin;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Api(tags = "微信公众号")
@RestController
@RequestMapping("/member/wx-mp")
@Validated
@Slf4j
public class AppWxMpController {
@Resource
private WxMpService mpService;
@PostMapping("/create-jsapi-signature")
@ApiOperation(value = "创建微信 JS SDK 初始化所需的签名",
notes = "参考 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 文档")
public CommonResult<WxJsapiSignature> createJsapiSignature(@RequestParam("url") String url) throws WxErrorException {
return success(mpService.createJsapiSignature(url));
}
}

View File

@@ -0,0 +1,6 @@
/**
* 提供 RESTful API 给前端:
* 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目
* 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分
*/
package cn.iocoder.yudao.module.member.controller;

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.member.convert.address;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import cn.iocoder.yudao.module.member.controller.app.address.vo.*;
import cn.iocoder.yudao.module.member.dal.dataobject.address.AddressDO;
/**
* 用户收件地址 Convert
*
* @author 芋道源码
*/
@Mapper
public interface AddressConvert {
AddressConvert INSTANCE = Mappers.getMapper(AddressConvert.class);
AddressDO convert(AppAddressCreateReqVO bean);
AddressDO convert(AppAddressUpdateReqVO bean);
AppAddressRespVO convert(AddressDO bean);
List<AppAddressRespVO> convertList(List<AddressDO> list);
PageResult<AppAddressRespVO> convertPage(PageResult<AddressDO> page);
}

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.member.convert.auth;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface AuthConvert {
AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class);
@Mapping(source = "mobile", target = "username")
LoginUser convert0(MemberUserDO bean);
default LoginUser convert(MemberUserDO bean) {
// 目的,为了设置 UserTypeEnum.MEMBER.getValue()
return convert0(bean).setUserType(UserTypeEnum.MEMBER.getValue());
}
SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialBindReqVO reqVO);
SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialLogin2ReqVO reqVO);
SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialLoginReqVO reqVO);
SocialUserUnbindReqDTO convert(Long userId, Integer userType, AppAuthSocialUnbindReqVO reqVO);
SmsCodeSendReqDTO convert(AppAuthSendSmsReqVO reqVO);
SmsCodeUseReqDTO convert(AppAuthResetPasswordReqVO reqVO, SmsSceneEnum scene, String usedIp);
SmsCodeUseReqDTO convert(AppAuthSmsLoginReqVO reqVO, Integer scene, String usedIp);
}

View File

@@ -0,0 +1,6 @@
/**
* 提供 POJO 类的实体转换
*
* 目前使用 MapStruct 框架
*/
package cn.iocoder.yudao.module.member.convert;

View File

@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.member.convert.user;
import cn.iocoder.yudao.module.member.api.user.dto.UserRespDTO;
import cn.iocoder.yudao.module.member.controller.app.user.vo.AppUserInfoRespVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface UserConvert {
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
AppUserInfoRespVO convert(MemberUserDO bean);
UserRespDTO convert2(MemberUserDO bean);
}

View File

@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.member.dal.dataobject.address;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.member.enums.AddressTypeEnum;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 用户收件地址 DO
*
* @author 芋道源码
*/
@TableName("member_address")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AddressDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 用户编号
*/
private Long userId;
/**
* 收件人名称
*/
private String name;
/**
* 手机号
*/
private String mobile;
/**
* 地区编码
*/
private Integer areaCode;
/**
* 收件详细地址
*/
private String detailAddress;
/**
* 地址类型
*
* 枚举 {@link AddressTypeEnum}
*/
private Integer type;
}

View File

@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.member.dal.dataobject.user;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.Date;
/**
* 会员用户 DO
*
* uk_mobile 索引:基于 {@link #mobile} 字段
*
* @author 芋道源码
*/
@TableName(value = "member_user", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberUserDO extends TenantBaseDO {
/**
* 用户ID
*/
@TableId
private Long id;
/**
* 用户昵称
*/
private String nickname;
/**
* 用户头像
*/
private String avatar;
/**
* 帐号状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 手机
*/
private String mobile;
/**
* 加密后的密码
*
* 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐
*/
private String password;
/**
* 注册 IP
*/
private String registerIp;
/**
* 最后登录IP
*/
private String loginIp;
/**
* 最后登录时间
*/
private Date loginDate;
}

View File

@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.member.dal.mysql.address;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.member.dal.dataobject.address.AddressDO;
import cn.iocoder.yudao.module.member.enums.AddressTypeEnum;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 用户收件地址 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface AddressMapper extends BaseMapperX<AddressDO> {
/**
* 获取当前地址 根据id和userId
* @param userId
* @param id
* @return
*/
default AddressDO getAddressByIdAndUserId(Long userId, Long id) {
QueryWrapperX<AddressDO> queryWrapperX = new QueryWrapperX<>();
queryWrapperX.eq("user_id", userId).eq("id", id);
return selectList(queryWrapperX).stream().findFirst().orElse(null);
}
/**
* 获取地址列表
* @param userId
* @param type
* @return
*/
default List<AddressDO> selectListByUserIdAndType(Long userId, Integer type) {
QueryWrapperX<AddressDO> queryWrapperX = new QueryWrapperX<AddressDO>().eq("user_id", userId)
.eqIfPresent("type", type);
return selectList(queryWrapperX);
}
/**
* 获取默认地址
* @param userId
* @return
*/
default AddressDO getDefaultUserAddress(Long userId) {
List<AddressDO> addressDOList = selectListByUserIdAndType(userId, AddressTypeEnum.DEFAULT.getType());
return addressDOList.stream().findFirst().orElse(null);
}
/**
* 获取默认地址
* @param id
* @param addressTypeEnum
* @return
*/
default int updateTypeById(Long id, AddressTypeEnum addressTypeEnum) {
return updateById(new AddressDO().setId(id).setType(addressTypeEnum.getType()));
}
}

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.member.dal.mysql.user;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 会员 User Mapper
*
* @author 芋道源码
*/
@Mapper
public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
default MemberUserDO selectByMobile(String mobile) {
return selectOne(MemberUserDO::getMobile, mobile);
}
}

View File

@@ -0,0 +1,9 @@
/**
* DAL = Data Access Layer 数据访问层
* 1. data object数据对象
* 2. redisRedis 的 CRUD 操作
* 3. mysqlMySQL 的 CRUD 操作
*
* 其中MySQL 的表以 member_ 作为前缀
*/
package cn.iocoder.yudao.module.member.dal;

View File

@@ -0,0 +1,4 @@
/**
* 占位,后续有类后,可以删除,避免 package 无法提交到 Git 上
*/
package cn.iocoder.yudao.module.member.dal.redis;

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.member.enums;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 用户收件地址的类型枚举
*/
@Getter
@AllArgsConstructor
public enum AddressTypeEnum implements IntArrayValuable {
DEFAULT(1, "默认收件地址"),
NORMAL(2, "普通收件地址"), // 即非默认收件地址
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AddressTypeEnum::getType).toArray();
private final Integer type;
private final String desc;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@@ -0,0 +1,4 @@
/**
* 占位
*/
package cn.iocoder.yudao.module.member.enums;

View File

@@ -0,0 +1,8 @@
/**
* member 模块,我们放会员业务。
* 例如说:会员中心等等
*
* 1. Controller URL以 /member/ 开头,避免和其它 Module 冲突
* 2. DataObject 表名:以 member_ 开头,方便在数据库中区分
*/
package cn.iocoder.yudao.module.member;

View File

@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.member.service.address;
import java.util.*;
import javax.validation.*;
import cn.iocoder.yudao.module.member.controller.app.address.vo.*;
import cn.iocoder.yudao.module.member.dal.dataobject.address.AddressDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
/**
* 用户收件地址 Service 接口
*
* @author 芋道源码
*/
public interface AddressService {
/**
* 创建用户收件地址
*
*
* @param userId 用户编号
* @param createReqVO 创建信息
* @return 编号
*/
Long createAddress(Long userId, @Valid AppAddressCreateReqVO createReqVO);
/**
* 更新用户收件地址
*
* @param userId 用户编号
* @param updateReqVO 更新信息
*/
void updateAddress(Long userId, @Valid AppAddressUpdateReqVO updateReqVO);
/**
* 删除用户收件地址
*
* @param userId 用户编号
* @param id 编号
*/
void deleteAddress(Long userId, Long id);
/**
* 获得用户收件地址
*
* @param id 编号
* @return 用户收件地址
*/
AddressDO getAddress(Long userId, Long id);
/**
* 获得用户收件地址列表
*
* @param userId 用户编号
* @return 用户收件地址列表
*/
List<AddressDO> getAddressList(Long userId);
AddressDO getDefaultUserAddress(Long userId);
}

View File

@@ -0,0 +1,133 @@
package cn.iocoder.yudao.module.member.service.address;
import cn.iocoder.yudao.module.member.enums.AddressTypeEnum;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import cn.iocoder.yudao.module.member.controller.app.address.vo.*;
import cn.iocoder.yudao.module.member.dal.dataobject.address.AddressDO;
import cn.iocoder.yudao.module.member.convert.address.AddressConvert;
import cn.iocoder.yudao.module.member.dal.mysql.address.AddressMapper;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.*;
/**
* 用户收件地址 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class AddressServiceImpl implements AddressService {
@Resource
private AddressMapper addressMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createAddress(Long userId, AppAddressCreateReqVO createReqVO) {
// 如果添加的是默认收件地址,则将原默认地址修改为非默认
if (AddressTypeEnum.DEFAULT.getType().equals(createReqVO.getType())) {
//查询到一个,然后进行 update
List<AddressDO> addressDOs = selectListByUserIdAndType(userId, AddressTypeEnum.DEFAULT.getType());
AddressDO defaultUserAddress = addressMapper.getDefaultUserAddress(userId);
if (!CollectionUtils.isEmpty(addressDOs)) {
addressDOs.forEach(userAddressDO -> addressMapper.updateById(new AddressDO()
.setId(userAddressDO.getId()).setType(AddressTypeEnum.NORMAL.getType())));
}
Optional.ofNullable(defaultUserAddress)
//更新为非默认
.ifPresent( u -> addressMapper.updateTypeById(u.getId(), AddressTypeEnum.NORMAL));
}
// 插入
AddressDO address = AddressConvert.INSTANCE.convert(createReqVO);
address.setUserId(userId);
addressMapper.insert(address);
// 返回
return address.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAddress(Long userId, AppAddressUpdateReqVO updateReqVO) {
// 校验存在,校验是否能够操作
check(userId, updateReqVO.getId());
// 如果修改的是默认收件地址,则将原默认地址修改为非默认
if (AddressTypeEnum.DEFAULT.getType().equals(updateReqVO.getType())) {
//获取默认地址
AddressDO defaultUserAddress = addressMapper.getDefaultUserAddress(userId);
Optional.ofNullable(defaultUserAddress)
//排除当前地址
.filter(u -> !u.getId().equals(updateReqVO.getId()))
//更新为非默认
.ifPresent( u -> addressMapper.updateTypeById(u.getId(), AddressTypeEnum.NORMAL));
}
// 更新
AddressDO updateObj = AddressConvert.INSTANCE.convert(updateReqVO);
addressMapper.updateById(updateObj);
}
@Override
public void deleteAddress(Long userId, Long id) {
// 校验存在,校验是否能够操作
check(userId, id);
// 删除
addressMapper.deleteById(id);
}
/**
* 校验用户收件地址是不是属于该用户
*
* @param userId 用户编号
* @param userAddressId 用户收件地址
*/
private void check(Long userId, Long userAddressId) {
AddressDO addressDO = getAddress(userId, userAddressId);
if(null == addressDO){
throw exception(ADDRESS_NOT_EXISTS);
}
if (!addressDO.getUserId().equals(userId)) {
throw exception(ADDRESS_FORBIDDEN);
}
}
@Override
public AddressDO getAddress(Long userId, Long id) {
return addressMapper.getAddressByIdAndUserId(userId, id);
}
@Override
public List<AddressDO> getAddressList(Long userId) {
return selectListByUserIdAndType(userId, null);
}
/**
* 获取默认地址
* @param userId
* @return
*/
@Override
public AddressDO getDefaultUserAddress(Long userId) {
return addressMapper.getDefaultUserAddress(userId);
}
/**
* 根据类型获取地址列表
* @param userId
* @param type null则查询全部
* @return
*/
public List<AddressDO> selectListByUserIdAndType(Long userId, Integer type) {
return addressMapper.selectListByUserIdAndType(userId, type);
}
}

View File

@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.member.service.auth;
import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
import javax.validation.Valid;
/**
* 会员的认证 Service 接口
*
* 提供用户的账号密码登录、token 的校验等认证相关的功能
*
* @author 芋道源码
*/
public interface MemberAuthService extends SecurityAuthFrameworkService {
/**
* 手机 + 密码登录
*
* @param reqVO 登录信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
*/
String login(@Valid AppAuthLoginReqVO reqVO, String userIp, String userAgent);
/**
* 手机 + 验证码登陆
*
* @param reqVO 登陆信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
*/
String smsLogin(@Valid AppAuthSmsLoginReqVO reqVO, String userIp, String userAgent);
/**
* 社交登录,使用 code 授权码
*
* @param reqVO 登录信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
*/
String socialLogin(@Valid AppAuthSocialLoginReqVO reqVO, String userIp, String userAgent);
/**
* 社交登录,使用 手机号 + 手机验证码
*
* @param reqVO 登录信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
*/
String socialLogin2(@Valid AppAuthSocialLogin2ReqVO reqVO, String userIp, String userAgent);
/**
* 社交绑定,使用 code 授权码
*
* @param userId 用户编号
* @param reqVO 绑定信息
*/
void socialBind(Long userId, @Valid AppAuthSocialBindReqVO reqVO);
/**
* 取消社交绑定
*
* @param userId 用户编号
* @param reqVO 解绑信息
*/
void unbindSocialUser(Long userId, @Valid AppAuthSocialUnbindReqVO reqVO);
/**
* 获得社交认证 URL
*
* @param type 社交平台类型
* @param redirectUri 跳转地址
* @return 认证 URL
*/
String getSocialAuthorizeUrl(Integer type, String redirectUri);
/**
* 修改用户密码
* @param userId 用户id
* @param userReqVO 用户请求实体类
*/
void updatePassword(Long userId, AppAuthUpdatePasswordReqVO userReqVO);
/**
* 忘记密码
* @param userReqVO 用户请求实体类
*/
void resetPassword(AppAuthResetPasswordReqVO userReqVO);
/**
* 给用户发送短信验证码
*
* @param userId 用户编号
* @param reqVO 发送信息
*/
void sendSmsCode(Long userId, AppAuthSendSmsReqVO reqVO);
}

View File

@@ -0,0 +1,354 @@
package cn.iocoder.yudao.module.member.service.auth;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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.framework.security.core.authentication.MultiUsernamePasswordAuthenticationToken;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
import cn.iocoder.yudao.module.member.convert.auth.AuthConvert;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import cn.iocoder.yudao.module.system.api.auth.UserSessionApi;
import cn.iocoder.yudao.module.system.api.logger.LoginLogApi;
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
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;
import javax.annotation.Resource;
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.module.member.enums.ErrorCodeConstants.*;
/**
* 会员的认证 Service 接口
*
* @author 芋道源码
*/
@Service
@Slf4j
public class MemberAuthServiceImpl implements MemberAuthService {
@Resource
@Lazy // 延迟加载,因为存在相互依赖的问题
private AuthenticationManager authenticationManager;
@Resource
private MemberUserService userService;
@Resource
private SmsCodeApi smsCodeApi;
@Resource
private LoginLogApi loginLogApi;
@Resource
private UserSessionApi userSessionApi;
@Resource
private SocialUserApi socialUserApi;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private MemberUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
// 获取 username 对应的 SysUserDO
MemberUserDO user = userService.getUserByMobile(mobile);
if (user == null) {
throw new UsernameNotFoundException(mobile);
}
// 创建 LoginUser 对象
return AuthConvert.INSTANCE.convert(user);
}
@Override
public String login(AppAuthLoginReqVO reqVO, String userIp, String userAgent) {
// 使用手机 + 密码,进行登录。
LoginUser loginUser = this.login0(reqVO.getMobile(), reqVO.getPassword());
// 缓存登录用户到 Redis 中,返回 sessionId 编号
return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_USERNAME, userIp, userAgent);
}
@Override
@Transactional
public String smsLogin(AppAuthSmsLoginReqVO reqVO, String userIp, String userAgent) {
// 校验验证码
smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp));
// 获得获得注册用户
MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), userIp);
Assert.notNull(user, "获取用户失败,结果为空");
// 执行登陆
LoginUser loginUser = AuthConvert.INSTANCE.convert(user);
// 缓存登录用户到 Redis 中,返回 sessionId 编号
return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SMS, userIp, userAgent);
}
@Override
public String socialLogin(AppAuthSocialLoginReqVO reqVO, String userIp, String userAgent) {
// 使用 code 授权码,进行登录。然后,获得到绑定的用户编号
Long userId = socialUserApi.getBindUserId(UserTypeEnum.MEMBER.getValue(), reqVO.getType(),
reqVO.getCode(), reqVO.getState());
if (userId == null) {
throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
}
// 自动登录
MemberUserDO user = userService.getUser(userId);
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
// 创建 LoginUser 对象
LoginUser loginUser = AuthConvert.INSTANCE.convert(user);
// 绑定社交用户(更新)
socialUserApi.bindSocialUser(AuthConvert.INSTANCE.convert(loginUser.getId(), getUserType().getValue(), reqVO));
// 缓存登录用户到 Redis 中,返回 sessionId 编号
return createUserSessionAfterLoginSuccess(loginUser, LoginLogTypeEnum.LOGIN_SOCIAL, userIp, userAgent);
}
@Override
public String socialLogin2(AppAuthSocialLogin2ReqVO reqVO, String userIp, String userAgent) {
// 校验社交平台的认证信息是否正确
socialUserApi.checkSocialUser(reqVO.getType(), reqVO.getCode(), reqVO.getState());
// 使用手机号、手机验证码登录
AppAuthSmsLoginReqVO loginReqVO = AppAuthSmsLoginReqVO.builder()
.mobile(reqVO.getMobile()).code(reqVO.getSmsCode()).build();
String sessionId = this.smsLogin(loginReqVO, userIp, userAgent);
LoginUser loginUser = userSessionApi.getLoginUser(sessionId);
// 绑定社交用户(新增)
socialUserApi.bindSocialUser(AuthConvert.INSTANCE.convert(loginUser.getId(), getUserType().getValue(), reqVO));
return sessionId;
}
private String createUserSessionAfterLoginSuccess(LoginUser loginUser, LoginLogTypeEnum logType, String userIp, String userAgent) {
// 插入登陆日志
createLoginLog(loginUser.getUsername(), logType, LoginResultEnum.SUCCESS);
// 缓存登录用户到 Redis 中,返回 sessionId 编号
return userSessionApi.createUserSession(loginUser, userIp, userAgent);
}
@Override
public void socialBind(Long userId, AppAuthSocialBindReqVO reqVO) {
// 绑定社交用户(新增)
socialUserApi.bindSocialUser(AuthConvert.INSTANCE.convert(userId, getUserType().getValue(), reqVO));
}
@Override
public void unbindSocialUser(Long userId, AppAuthSocialUnbindReqVO reqVO) {
socialUserApi.unbindSocialUser(AuthConvert.INSTANCE.convert(userId, getUserType().getValue(), reqVO));
}
@Override
public String getSocialAuthorizeUrl(Integer type, String redirectUri) {
return socialUserApi.getAuthorizeUrl(type, redirectUri);
}
private LoginUser login0(String username, String password) {
final LoginLogTypeEnum logType = LoginLogTypeEnum.LOGIN_USERNAME;
// 用户验证
Authentication authentication;
try {
// 调用 Spring Security 的 AuthenticationManager#authenticate(...) 方法,使用账号密码进行认证
// 在其内部,会调用到 loadUserByUsername 方法,获取 User 信息
authentication = authenticationManager.authenticate(new MultiUsernamePasswordAuthenticationToken(
username, password, getUserType()));
} catch (BadCredentialsException badCredentialsException) {
this.createLoginLog(username, logType, LoginResultEnum.BAD_CREDENTIALS);
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
} catch (DisabledException disabledException) {
this.createLoginLog(username, logType, LoginResultEnum.USER_DISABLED);
throw exception(AUTH_LOGIN_USER_DISABLED);
} catch (AuthenticationException authenticationException) {
log.error("[login0][username({}) 发生未知异常]", username, authenticationException);
this.createLoginLog(username, logType, LoginResultEnum.UNKNOWN_ERROR);
throw exception(AUTH_LOGIN_FAIL_UNKNOWN);
}
Assert.notNull(authentication.getPrincipal(), "Principal 不会为空");
return (LoginUser) authentication.getPrincipal();
}
private void createLoginLog(String mobile, LoginLogTypeEnum logType, LoginResultEnum loginResult) {
// 获得用户
MemberUserDO user = userService.getUserByMobile(mobile);
// 插入登录日志
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(logType.getType());
reqDTO.setTraceId(TracerUtils.getTraceId());
if (user != null) {
reqDTO.setUserId(user.getId());
}
reqDTO.setUserType(getUserType().getValue());
reqDTO.setUsername(mobile);
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(getClientIP());
reqDTO.setResult(loginResult.getResult());
loginLogApi.createLoginLog(reqDTO);
// 更新最后登录时间
if (user != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) {
userService.updateUserLogin(user.getId(), getClientIP());
}
}
@Override
public LoginUser verifyTokenAndRefresh(String token) {
// 获得 LoginUser
LoginUser loginUser = userSessionApi.getLoginUser(token);
if (loginUser == null) {
return null;
}
// 刷新 LoginUser 缓存
this.refreshLoginUserCache(token, loginUser);
return loginUser;
}
private void refreshLoginUserCache(String token, LoginUser loginUser) {
// 每 1/3 的 Session 超时时间,刷新 LoginUser 缓存
if (System.currentTimeMillis() - loginUser.getUpdateTime().getTime() <
userSessionApi.getSessionTimeoutMillis() / 3) {
return;
}
// 重新加载 UserDO 信息
MemberUserDO user = userService.getUser(loginUser.getId());
if (user == null || CommonStatusEnum.DISABLE.getStatus().equals(user.getStatus())) {
// 校验 token 时,用户被禁用的情况下,也认为 token 过期,方便前端跳转到登录界面
throw exception(AUTH_TOKEN_EXPIRED);
}
// 刷新 LoginUser 缓存
userSessionApi.refreshUserSession(token, loginUser);
}
@Override
public LoginUser mockLogin(Long userId) {
// 获取用户编号对应的 UserDO
MemberUserDO user = userService.getUser(userId);
if (user == null) {
throw new UsernameNotFoundException(String.valueOf(userId));
}
// 执行登陆
this.createLoginLog(user.getMobile(), LoginLogTypeEnum.LOGIN_MOCK, LoginResultEnum.SUCCESS);
// 创建 LoginUser 对象
return AuthConvert.INSTANCE.convert(user);
}
@Override
public void logout(String token) {
// 查询用户信息
LoginUser loginUser = userSessionApi.getLoginUser(token);
if (loginUser == null) {
return;
}
// 删除 session
userSessionApi.deleteUserSession(token);
// 记录登出日志
this.createLogoutLog(loginUser.getId(), loginUser.getUsername());
}
@Override
public UserTypeEnum getUserType() {
return UserTypeEnum.MEMBER;
}
@Override
public void updatePassword(Long userId, AppAuthUpdatePasswordReqVO reqVO) {
// 检验旧密码
MemberUserDO userDO = checkOldPassword(userId, reqVO.getOldPassword());
// 更新用户密码
userMapper.updateById(MemberUserDO.builder().id(userDO.getId())
.password(passwordEncoder.encode(reqVO.getPassword())).build());
}
@Override
public void resetPassword(AppAuthResetPasswordReqVO reqVO) {
// 检验用户是否存在
MemberUserDO userDO = checkUserIfExists(reqVO.getMobile());
// 使用验证码
smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_FORGET_PASSWORD,
getClientIP()));
// 更新密码
userMapper.updateById(MemberUserDO.builder().id(userDO.getId())
.password(passwordEncoder.encode(reqVO.getPassword())).build());
}
@Override
public void sendSmsCode(Long userId, AppAuthSendSmsReqVO reqVO) {
// TODO 要根据不同的场景,校验是否有用户
smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP()));
}
/**
* 校验旧密码
*
* @param id 用户 id
* @param oldPassword 旧密码
* @return MemberUserDO 用户实体
*/
@VisibleForTesting
public MemberUserDO checkOldPassword(Long id, String oldPassword) {
MemberUserDO 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;
}
public MemberUserDO checkUserIfExists(String mobile) {
MemberUserDO user = userMapper.selectByMobile(mobile);
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
return user;
}
private void createLogoutLog(Long userId, String username) {
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_SELF.getType());
reqDTO.setTraceId(TracerUtils.getTraceId());
reqDTO.setUserId(userId);
reqDTO.setUserType(getUserType().getValue());
reqDTO.setUsername(username);
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(getClientIP());
reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
loginLogApi.createLoginLog(reqDTO);
}
}

View File

@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.member.service.user;
import cn.iocoder.yudao.framework.common.validation.Mobile;
import cn.iocoder.yudao.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import java.io.InputStream;
/**
* 会员用户 Service 接口
*
* @author 芋道源码
*/
public interface MemberUserService {
/**
* 通过手机查询用户
*
* @param mobile 手机
* @return 用户对象
*/
MemberUserDO getUserByMobile(String mobile);
/**
* 基于手机号创建用户。
* 如果用户已经存在,则直接进行返回
*
* @param mobile 手机号
* @param registerIp 注册 IP
* @return 用户对象
*/
MemberUserDO createUserIfAbsent(@Mobile String mobile, String registerIp);
/**
* 更新用户的最后登陆信息
*
* @param id 用户编号
* @param loginIp 登陆 IP
*/
void updateUserLogin(Long id, String loginIp);
/**
* 通过用户 ID 查询用户
*
* @param id 用户ID
* @return 用户对象信息
*/
MemberUserDO getUser(Long id);
/**
* 修改用户昵称
* @param userId 用户id
* @param nickname 用户新昵称
*/
void updateUserNickname(Long userId, String nickname);
/**
* 修改用户头像
* @param userId 用户id
* @param inputStream 头像文件
* @return 头像url
*/
String updateUserAvatar(Long userId, InputStream inputStream) throws Exception;
/**
* 修改手机
* @param userId 用户id
* @param reqVO 请求实体
*/
void updateUserMobile(Long userId, AppUserUpdateMobileReqVO reqVO);
}

View File

@@ -0,0 +1,142 @@
package cn.iocoder.yudao.module.member.service.user;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import cn.iocoder.yudao.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.io.InputStream;
import java.util.Date;
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.module.member.enums.ErrorCodeConstants.USER_NOT_EXISTS;
/**
* 会员 User Service 实现类
*
* @author 芋道源码
*/
@Service
@Valid
@Slf4j
public class MemberUserServiceImpl implements MemberUserService {
@Resource
private MemberUserMapper memberUserMapper;
@Resource
private FileApi fileApi;
@Resource
private SmsCodeApi smsCodeApi;
@Resource
private PasswordEncoder passwordEncoder;
@Override
public MemberUserDO getUserByMobile(String mobile) {
return memberUserMapper.selectByMobile(mobile);
}
@Override
public MemberUserDO createUserIfAbsent(String mobile, String registerIp) {
// 用户已经存在
MemberUserDO user = memberUserMapper.selectByMobile(mobile);
if (user != null) {
return user;
}
// 用户不存在,则进行创建
return this.createUser(mobile, registerIp);
}
private MemberUserDO createUser(String mobile, String registerIp) {
// 生成密码
String password = IdUtil.fastSimpleUUID();
// 插入用户
MemberUserDO user = new MemberUserDO();
user.setMobile(mobile);
user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启
user.setPassword(passwordEncoder.encode(password)); // 加密密码
user.setRegisterIp(registerIp);
memberUserMapper.insert(user);
return user;
}
@Override
public void updateUserLogin(Long id, String loginIp) {
memberUserMapper.updateById(new MemberUserDO().setId(id)
.setLoginIp(loginIp).setLoginDate(new Date()));
}
@Override
public MemberUserDO getUser(Long id) {
return memberUserMapper.selectById(id);
}
@Override
public void updateUserNickname(Long userId, String nickname) {
MemberUserDO user = this.checkUserExists(userId);
// 仅当新昵称不等于旧昵称时进行修改
if (nickname.equals(user.getNickname())){
return;
}
MemberUserDO userDO = new MemberUserDO();
userDO.setId(user.getId());
userDO.setNickname(nickname);
memberUserMapper.updateById(userDO);
}
@Override
public String updateUserAvatar(Long userId, InputStream avatarFile) throws Exception {
this.checkUserExists(userId);
// 创建文件
String avatar = fileApi.createFile(IoUtil.readBytes(avatarFile));
// 更新头像路径
memberUserMapper.updateById(MemberUserDO.builder().id(userId).avatar(avatar).build());
return avatar;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUserMobile(Long userId, AppUserUpdateMobileReqVO reqVO) {
// 检测用户是否存在
checkUserExists(userId);
// TODO 芋艿oldMobile 应该不用传递
// 校验旧手机和旧验证码
smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(reqVO.getOldMobile()).setCode(reqVO.getOldCode())
.setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP()));
// 使用新验证码
smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(reqVO.getMobile()).setCode(reqVO.getCode())
.setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP()));
// 更新用户手机
memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(reqVO.getMobile()).build());
}
@VisibleForTesting
public MemberUserDO checkUserExists(Long id) {
if (id == null) {
return null;
}
MemberUserDO user = memberUserMapper.selectById(id);
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
return user;
}
}

View File

@@ -0,0 +1,160 @@
package cn.iocoder.yudao.module.member.service.address;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.member.controller.app.address.vo.AppAddressCreateReqVO;
import cn.iocoder.yudao.module.member.controller.app.address.vo.AppAddressUpdateReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.address.AddressDO;
import cn.iocoder.yudao.module.member.dal.mysql.address.AddressMapper;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.ADDRESS_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link AddressServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(AddressServiceImpl.class)
public class AddressServiceImplTest extends BaseDbUnitTest {
@Resource
private AddressServiceImpl addressService;
@Resource
private AddressMapper addressMapper;
@Test
public void testCreateAddress_success() {
// 准备参数
AppAddressCreateReqVO reqVO = randomPojo(AppAddressCreateReqVO.class);
// 调用
Long addressId = addressService.createAddress(getLoginUserId(), reqVO);
// 断言
assertNotNull(addressId);
// 校验记录的属性是否正确
AddressDO address = addressMapper.selectById(addressId);
assertPojoEquals(reqVO, address);
}
@Test
public void testUpdateAddress_success() {
// mock 数据
AddressDO dbAddress = randomPojo(AddressDO.class);
addressMapper.insert(dbAddress);// @Sql: 先插入出一条存在的数据
// 准备参数
AppAddressUpdateReqVO reqVO = randomPojo(AppAddressUpdateReqVO.class, o -> {
o.setId(dbAddress.getId()); // 设置更新的 ID
});
// 调用
addressService.updateAddress(getLoginUserId(), reqVO);
// 校验是否更新正确
AddressDO address = addressMapper.selectById(reqVO.getId()); // 获取最新的
assertPojoEquals(reqVO, address);
}
@Test
public void testUpdateAddress_notExists() {
// 准备参数
AppAddressUpdateReqVO reqVO = randomPojo(AppAddressUpdateReqVO.class);
// 调用, 并断言异常
assertServiceException(() -> addressService.updateAddress(getLoginUserId(), reqVO), ADDRESS_NOT_EXISTS);
}
@Test
public void testDeleteAddress_success() {
// mock 数据
AddressDO dbAddress = randomPojo(AddressDO.class);
addressMapper.insert(dbAddress);// @Sql: 先插入出一条存在的数据
// 准备参数
Long id = dbAddress.getId();
// 调用
addressService.deleteAddress(getLoginUserId(), id);
// 校验数据不存在了
assertNull(addressMapper.selectById(id));
}
@Test
public void testDeleteAddress_notExists() {
// 准备参数
Long id = randomLongId();
// 调用, 并断言异常
assertServiceException(() -> addressService.deleteAddress(getLoginUserId(), id), ADDRESS_NOT_EXISTS);
}
@Test
@Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
public void ins() {
// mock 数据
AddressDO dbAddress = randomPojo(AddressDO.class, o -> { // 等会查询到
o.setUserId(null);
o.setName(null);
o.setMobile(null);
o.setAreaCode(null);
o.setDetailAddress(null);
o.setType(null);
o.setCreateTime(null);
});
addressMapper.insert(dbAddress);
// 测试 userId 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setUserId(null)));
// 测试 name 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setName(null)));
// 测试 mobile 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setMobile(null)));
// 测试 areaCode 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setAreaCode(null)));
// 测试 detailAddress 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setDetailAddress(null)));
// 测试 type 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setType(null)));
// 测试 createTime 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setCreateTime(null)));
}
@Test
@Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
public void testGetAddressList() {
// mock 数据
AddressDO dbAddress = randomPojo(AddressDO.class, o -> { // 等会查询到
o.setUserId(null);
o.setName(null);
o.setMobile(null);
o.setAreaCode(null);
o.setDetailAddress(null);
o.setType(null);
o.setCreateTime(null);
});
addressMapper.insert(dbAddress);
// 测试 userId 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setUserId(null)));
// 测试 name 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setName(null)));
// 测试 mobile 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setMobile(null)));
// 测试 areaCode 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setAreaCode(null)));
// 测试 detailAddress 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setDetailAddress(null)));
// 测试 type 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setType(null)));
// 测试 createTime 不匹配
addressMapper.insert(cloneIgnoreId(dbAddress, o -> o.setCreateTime(null)));
}
}

View File

@@ -0,0 +1,122 @@
package cn.iocoder.yudao.module.member.service.auth;
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.framework.test.core.ut.BaseDbAndRedisUnitTest;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.AppAuthResetPasswordReqVO;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.AppAuthUpdatePasswordReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import cn.iocoder.yudao.module.system.api.auth.UserSessionApi;
import cn.iocoder.yudao.module.system.api.logger.LoginLogApi;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
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.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.when;
// TODO @芋艿:单测的 review等逻辑都达成一致后
/**
* {@link MemberAuthService} 的单元测试类
*
* @author 宋天
*/
@Import({MemberAuthServiceImpl.class, YudaoRedisAutoConfiguration.class})
public class MemberAuthServiceTest extends BaseDbAndRedisUnitTest {
@MockBean
private AuthenticationManager authenticationManager;
@MockBean
private MemberUserService userService;
@MockBean
private SmsCodeApi smsCodeApi;
@MockBean
private LoginLogApi loginLogApi;
@MockBean
private UserSessionApi userSessionApi;
@MockBean
private SocialUserApi socialUserApi;
@MockBean
private PasswordEncoder passwordEncoder;
@Resource
private MemberUserMapper memberUserMapper;
@Resource
private MemberAuthServiceImpl authService;
@Test
public void testUpdatePassword_success(){
// 准备参数
MemberUserDO userDO = randomUserDO();
memberUserMapper.insert(userDO);
// 新密码
String newPassword = randomString();
// 请求实体
AppAuthUpdatePasswordReqVO reqVO = AppAuthUpdatePasswordReqVO.builder()
.oldPassword(userDO.getPassword())
.password(newPassword)
.build();
// 测试桩
// 这两个相等是为了返回ture这个结果
when(passwordEncoder.matches(reqVO.getOldPassword(),reqVO.getOldPassword())).thenReturn(true);
when(passwordEncoder.encode(newPassword)).thenReturn(newPassword);
// 更新用户密码
authService.updatePassword(userDO.getId(), reqVO);
assertEquals(memberUserMapper.selectById(userDO.getId()).getPassword(),newPassword);
}
@Test
public void testResetPassword_success(){
// 准备参数
MemberUserDO userDO = randomUserDO();
memberUserMapper.insert(userDO);
// 随机密码
String password = randomNumbers(11);
// 随机验证码
String code = randomNumbers(4);
// mock
when(passwordEncoder.encode(password)).thenReturn(password);
// 更新用户密码
AppAuthResetPasswordReqVO reqVO = new AppAuthResetPasswordReqVO();
reqVO.setMobile(userDO.getMobile());
reqVO.setPassword(password);
reqVO.setCode(code);
authService.resetPassword(reqVO);
assertEquals(memberUserMapper.selectById(userDO.getId()).getPassword(),password);
}
// ========== 随机对象 ==========
@SafeVarargs
private static MemberUserDO randomUserDO(Consumer<MemberUserDO>... consumers) {
Consumer<MemberUserDO> consumer = (o) -> {
o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
o.setPassword(randomString());
};
return randomPojo(MemberUserDO.class, ArrayUtils.append(consumer, consumers));
}
}

View File

@@ -0,0 +1,137 @@
package cn.iocoder.yudao.module.member.service.user;
import cn.hutool.core.util.RandomUtil;
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.framework.test.core.ut.BaseDbAndRedisUnitTest;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import cn.iocoder.yudao.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
import cn.iocoder.yudao.module.member.service.auth.MemberAuthServiceImpl;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
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.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.util.function.Consumer;
import static cn.hutool.core.util.RandomUtil.*;
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.eq;
import static org.mockito.Mockito.when;
// TODO @芋艿:单测的 review等逻辑都达成一致后
/**
* {@link MemberUserServiceImpl} 的单元测试类
*
* @author 宋天
*/
@Import({MemberUserServiceImpl.class, YudaoRedisAutoConfiguration.class})
public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest {
@Resource
private MemberUserServiceImpl memberUserService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private MemberUserMapper userMapper;
@MockBean
private MemberAuthServiceImpl authService;
@MockBean
private PasswordEncoder passwordEncoder;
@MockBean
private SmsCodeApi smsCodeApi;
@MockBean
private FileApi fileApi;
@Test
public void testUpdateNickName_success(){
// mock 数据
MemberUserDO userDO = randomUserDO();
userMapper.insert(userDO);
// 随机昵称
String newNickName = randomString();
// 调用接口修改昵称
memberUserService.updateUserNickname(userDO.getId(),newNickName);
// 查询新修改后的昵称
String nickname = memberUserService.getUser(userDO.getId()).getNickname();
// 断言
assertEquals(newNickName,nickname);
}
@Test
public void testUpdateAvatar_success() throws Exception {
// mock 数据
MemberUserDO dbUser = randomUserDO();
userMapper.insert(dbUser);
// 准备参数
Long userId = dbUser.getId();
byte[] avatarFileBytes = randomBytes(10);
ByteArrayInputStream avatarFile = new ByteArrayInputStream(avatarFileBytes);
// mock 方法
String avatar = randomString();
when(fileApi.createFile(eq(avatarFileBytes))).thenReturn(avatar);
// 调用
String str = memberUserService.updateUserAvatar(userId, avatarFile);
// 断言
assertEquals(avatar, str);
}
@Test
public void updateMobile_success(){
// mock数据
String oldMobile = randomNumbers(11);
MemberUserDO userDO = randomUserDO();
userDO.setMobile(oldMobile);
userMapper.insert(userDO);
// TODO 芋艿:需要修复该单元测试,重构多模块带来的
// 旧手机和旧验证码
// SmsCodeDO codeDO = new SmsCodeDO();
String oldCode = RandomUtil.randomString(4);
// codeDO.setMobile(userDO.getMobile());
// codeDO.setCode(oldCode);
// codeDO.setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene());
// codeDO.setUsed(Boolean.FALSE);
// when(smsCodeService.checkCodeIsExpired(codeDO.getMobile(),codeDO.getCode(),codeDO.getScene())).thenReturn(codeDO);
// 更新手机号
String newMobile = randomNumbers(11);
String newCode = randomNumbers(4);
AppUserUpdateMobileReqVO reqVO = new AppUserUpdateMobileReqVO();
reqVO.setMobile(newMobile);
reqVO.setCode(newCode);
reqVO.setOldMobile(oldMobile);
reqVO.setOldCode(oldCode);
memberUserService.updateUserMobile(userDO.getId(),reqVO);
assertEquals(memberUserService.getUser(userDO.getId()).getMobile(),newMobile);
}
// ========== 随机对象 ==========
@SafeVarargs
private static MemberUserDO randomUserDO(Consumer<MemberUserDO>... consumers) {
Consumer<MemberUserDO> consumer = (o) -> {
o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
};
return randomPojo(MemberUserDO.class, ArrayUtils.append(consumer, consumers));
}
}

View File

@@ -0,0 +1,49 @@
spring:
main:
lazy-initialization: true # 开启懒加载,加快速度
banner-mode: off # 单元测试,禁用 Banner
--- #################### 数据库相关配置 ####################
spring:
# 数据源配置项
datasource:
name: ruoyi-vue-pro
url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式DATABASE_TO_UPPER 配置表和字段使用小写
driver-class-name: org.h2.Driver
username: sa
password:
druid:
async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
initial-size: 1 # 单元测试,配置为 1提升启动速度
sql:
init:
schema-locations: classpath:/sql/create_tables.sql
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
redis:
host: 127.0.0.1 # 地址
port: 16379 # 端口(单元测试,使用 16379 端口)
database: 0 # 数据库索引
mybatis:
lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
--- #################### 定时任务相关配置 ####################
--- #################### 配置中心相关配置 ####################
--- #################### 服务保障相关配置 ####################
# Lock4j 配置项(单元测试,禁用 Lock4j
# Resilience4j 配置项
--- #################### 监控相关配置 ####################
--- #################### 芋道相关配置 ####################
# 芋道配置项,设置当前项目所有自定义的配置
yudao:
info:
base-package: cn.iocoder.yudao.module

View File

@@ -0,0 +1,4 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
</configuration>

View File

@@ -0,0 +1,2 @@
DELETE FROM "member_user";
DELETE FROM "member_address";

View File

@@ -0,0 +1,49 @@
CREATE TABLE IF NOT EXISTS "member_user" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
"nickname" varchar(30) NOT NULL DEFAULT '' COMMENT '用户昵称',
"avatar" varchar(255) NOT NULL DEFAULT '' COMMENT '头像',
"status" tinyint NOT NULL COMMENT '状态',
"mobile" varchar(11) NOT NULL COMMENT '手机号',
"password" varchar(100) NOT NULL DEFAULT '' COMMENT '密码',
"register_ip" varchar(32) NOT NULL COMMENT '注册 IP',
"login_ip" varchar(50) NULL DEFAULT '' COMMENT '最后登录IP',
"login_date" datetime NULL DEFAULT NULL COMMENT '最后登录时间',
"creator" varchar(64) NULL DEFAULT '' COMMENT '创建者',
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
"updater" varchar(64) NULL DEFAULT '' COMMENT '更新者',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
"deleted" bit(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
"tenant_id" bigint not null default '0',
PRIMARY KEY ("id")
) COMMENT '会员表';
-- inf 开头的 DB
CREATE TABLE IF NOT EXISTS "inf_file" (
"id" varchar(188) NOT NULL,
"type" varchar(63) DEFAULT NULL,
"content" blob NOT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
PRIMARY KEY ("id")
) COMMENT '文件表';
CREATE TABLE IF NOT EXISTS "member_address" (
"id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"user_id" bigint(20) NOT NULL,
"name" varchar(10) NOT NULL,
"mobile" varchar(20) NOT NULL,
"area_code" int(11) NOT NULL,
"detail_address" varchar(250) NOT NULL,
"type" tinyint(4) NOT NULL,
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
"creator" varchar(64) DEFAULT '',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"updater" varchar(64) DEFAULT '',
"tenant_id" bigint(20) NOT NULL,
PRIMARY KEY ("id")
) COMMENT '用户收件地址';