yshop-pro init

This commit is contained in:
hupeng
2023-05-19 18:29:26 +08:00
commit 6ff21a3799
1846 changed files with 114288 additions and 0 deletions

View File

@ -0,0 +1,24 @@
<?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>co.yixiang.boot</groupId>
<artifactId>yshop</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<modules>
<module>yshop-module-member-api</module>
<module>yshop-module-member-biz</module>
</modules>
<artifactId>yshop-module-member</artifactId>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>
member 模块,我们放会员业务。
例如说:会员中心等等
</description>
</project>

View File

@ -0,0 +1,26 @@
<?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>co.yixiang.boot</groupId>
<artifactId>yshop-module-member</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yshop-module-member-api</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
member 模块 API暴露给其它模块调用
</description>
<dependencies>
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,21 @@
package co.yixiang.yshop.module.member.api.address;
import co.yixiang.yshop.module.member.api.address.dto.AddressRespDTO;
/**
* 用户收件地址 API 接口
*
* @author yshop
*/
public interface AddressApi {
/**
* 获得用户收件地址
*
* @param id 收件地址编号
* @param userId 用户编号
* @return 用户收件地址
*/
AddressRespDTO getAddress(Long id, Long userId);
}

View File

@ -0,0 +1,48 @@
package co.yixiang.yshop.module.member.api.address.dto;
import lombok.Data;
/**
* 用户收件地址 Response DTO
*
* @author yshop
*/
@Data
public class AddressRespDTO {
/**
* 编号
*/
private Long id;
/**
* 用户编号
*/
private Long userId;
/**
* 收件人名称
*/
private String name;
/**
* 手机号
*/
private String mobile;
/**
* 地区编号
*/
private Long areaId;
/**
* 邮编
*/
private String postCode;
/**
* 收件详细地址
*/
private String detailAddress;
/**
* 是否默认
*
* true - 默认收件地址
*/
private Boolean defaulted;
}

View File

@ -0,0 +1,4 @@
/**
* member API 包,定义暴露给其它模块的 API
*/
package co.yixiang.yshop.module.member.api;

View File

@ -0,0 +1,60 @@
package co.yixiang.yshop.module.member.api.user;
import co.yixiang.yshop.module.member.api.user.dto.MemberUserRespDTO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static co.yixiang.yshop.framework.common.util.collection.CollectionUtils.convertMap;
/**
* 会员用户的 API 接口
*
* @author yshop
*/
public interface MemberUserApi {
/**
* 获得会员用户信息
*
* @param id 用户编号
* @return 用户信息
*/
MemberUserRespDTO getUser(Long id);
/**
* 获得会员用户信息们
*
* @param ids 用户编号的数组
* @return 用户信息们
*/
List<MemberUserRespDTO> getUsers(Collection<Long> ids);
/**
* 获得会员用户 Map
*
* @param ids 用户编号的数组
* @return 会员用户 Map
*/
default Map<Long, MemberUserRespDTO> getUserMap(Collection<Long> ids) {
return convertMap(getUsers(ids), MemberUserRespDTO::getId);
}
/**
* 基于用户昵称,模糊匹配用户列表
*
* @param nickname 用户昵称,模糊匹配
* @return 用户信息的列表
*/
List<MemberUserRespDTO> getUserListByNickname(String nickname);
/**
* 基于手机号,精准匹配用户
*
* @param mobile 手机号
* @return 用户信息
*/
MemberUserRespDTO getUserByMobile(String mobile);
}

View File

@ -0,0 +1,34 @@
package co.yixiang.yshop.module.member.api.user.dto;
import co.yixiang.yshop.framework.common.enums.CommonStatusEnum;
import lombok.Data;
/**
* 用户信息 Response DTO
*
* @author yshop
*/
@Data
public class MemberUserRespDTO {
/**
* 用户ID
*/
private Long id;
/**
* 用户昵称
*/
private String nickname;
/**
* 帐号状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 手机
*/
private String mobile;
}

View File

@ -0,0 +1,26 @@
package co.yixiang.yshop.module.member.enums;
import co.yixiang.yshop.framework.common.exception.ErrorCode;
/**
* Member 错误码枚举类
*
* member 系统,使用 1-004-000-000 段
*/
public interface ErrorCodeConstants {
// ========== 用户相关 1004001000============
ErrorCode USER_NOT_EXISTS = new ErrorCode(1004001000, "用户不存在");
ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1004001001, "密码校验失败");
// ========== AUTH 模块 1004003000 ==========
ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1004003000, "登录失败,账号密码不正确");
ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1004003001, "登录失败,账号被禁用");
ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1004003004, "Token 已经过期");
ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1004003005, "未绑定账号,需要进行绑定");
ErrorCode AUTH_WEIXIN_MINI_APP_PHONE_CODE_ERROR = new ErrorCode(1004003006, "获得手机号失败");
// ========== 用户收件地址 1004004000 ==========
ErrorCode ADDRESS_NOT_EXISTS = new ErrorCode(1004004000, "用户收件地址不存在");
}

View File

@ -0,0 +1,89 @@
<?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>co.yixiang.boot</groupId>
<artifactId>yshop-module-member</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yshop-module-member-biz</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
member 模块,我们放会员业务。
例如说:会员中心等等
</description>
<dependencies>
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-module-member-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-module-system-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-module-infra-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-spring-boot-starter-biz-operatelog</artifactId>
</dependency>
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-spring-boot-starter-biz-weixin</artifactId>
</dependency>
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-spring-boot-starter-redis</artifactId>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-spring-boot-starter-mq</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>co.yixiang.boot</groupId>
<artifactId>yshop-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 工具类相关 -->
</dependencies>
</project>

View File

@ -0,0 +1,28 @@
package co.yixiang.yshop.module.member.api.address;
import co.yixiang.yshop.module.member.api.address.dto.AddressRespDTO;
import co.yixiang.yshop.module.member.convert.address.AddressConvert;
import co.yixiang.yshop.module.member.service.address.AddressService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
/**
* 用户收件地址 API 实现类
*
* @author yshop
*/
@Service
@Validated
public class AddressApiImpl implements AddressApi {
@Resource
private AddressService addressService;
@Override
public AddressRespDTO getAddress(Long id, Long userId) {
return AddressConvert.INSTANCE.convert02(addressService.getAddress(userId, id));
}
}

View File

@ -0,0 +1 @@
package co.yixiang.yshop.module.member.api;

View File

@ -0,0 +1,47 @@
package co.yixiang.yshop.module.member.api.user;
import co.yixiang.yshop.module.member.api.user.dto.MemberUserRespDTO;
import co.yixiang.yshop.module.member.convert.user.UserConvert;
import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO;
import co.yixiang.yshop.module.member.service.user.MemberUserService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
/**
* 会员用户的 API 实现类
*
* @author yshop
*/
@Service
@Validated
public class MemberUserApiImpl implements MemberUserApi {
@Resource
private MemberUserService userService;
@Override
public MemberUserRespDTO getUser(Long id) {
MemberUserDO user = userService.getUser(id);
return UserConvert.INSTANCE.convert2(user);
}
@Override
public List<MemberUserRespDTO> getUsers(Collection<Long> ids) {
return UserConvert.INSTANCE.convertList2(userService.getUserList(ids));
}
@Override
public List<MemberUserRespDTO> getUserListByNickname(String nickname) {
return UserConvert.INSTANCE.convertList2(userService.getUserListByNickname(nickname));
}
@Override
public MemberUserRespDTO getUserByMobile(String mobile) {
return UserConvert.INSTANCE.convert2(userService.getUserByMobile(mobile));
}
}

View File

@ -0,0 +1 @@
package co.yixiang.yshop.module.member.controller.admin.address;

View File

@ -0,0 +1 @@
package co.yixiang.yshop.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 {{appToken}}
{
"name": "yunai",
"mobile": "15601691300",
"areaId": "610632",
"postCode": "200000",
"detailAddress": "yshop 233 号 666 室",
"defaulted": true
}
### 请求 /update 接口 => 成功
PUT {{appApi}}//member/address/update
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}
{
"id": "1",
"name": "yunai888",
"mobile": "15601691300",
"areaId": "610632",
"postCode": "200000",
"detailAddress": "yshop 233 号 666 室",
"defaulted": false
}
### 请求 /delete 接口 => 成功
DELETE {{appApi}}//member/address/delete?id=2
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}
### 请求 /get 接口 => 成功
GET {{appApi}}//member/address/get?id=1
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}
### 请求 /get-default 接口 => 成功
GET {{appApi}}//member/address/get-default
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}
### 请求 /list 接口 => 成功
GET {{appApi}}//member/address/list
Content-Type: application/json
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}

View File

@ -0,0 +1,75 @@
package co.yixiang.yshop.module.member.controller.app.address;
import co.yixiang.yshop.framework.common.pojo.CommonResult;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressCreateReqVO;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressRespVO;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressUpdateReqVO;
import co.yixiang.yshop.module.member.convert.address.AddressConvert;
import co.yixiang.yshop.module.member.dal.dataobject.address.AddressDO;
import co.yixiang.yshop.module.member.service.address.AddressService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
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 co.yixiang.yshop.framework.common.pojo.CommonResult.success;
import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "用户 APP - 用户收件地址")
@RestController
@RequestMapping("/member/address")
@Validated
public class AppAddressController {
@Resource
private AddressService addressService;
@PostMapping("/create")
@Operation(summary = "创建用户收件地址")
public CommonResult<Long> createAddress(@Valid @RequestBody AppAddressCreateReqVO createReqVO) {
return success(addressService.createAddress(getLoginUserId(), createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新用户收件地址")
public CommonResult<Boolean> updateAddress(@Valid @RequestBody AppAddressUpdateReqVO updateReqVO) {
addressService.updateAddress(getLoginUserId(), updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除用户收件地址")
@Parameter(name = "id", description = "编号", required = true)
public CommonResult<Boolean> deleteAddress(@RequestParam("id") Long id) {
addressService.deleteAddress(getLoginUserId(), id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得用户收件地址")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<AppAddressRespVO> getAddress(@RequestParam("id") Long id) {
AddressDO address = addressService.getAddress(getLoginUserId(), id);
return success(AddressConvert.INSTANCE.convert(address));
}
@GetMapping("/get-default")
@Operation(summary = "获得默认的用户收件地址")
public CommonResult<AppAddressRespVO> getDefaultUserAddress() {
AddressDO address = addressService.getDefaultUserAddress(getLoginUserId());
return success(AddressConvert.INSTANCE.convert(address));
}
@GetMapping("/list")
@Operation(summary = "获得用户收件地址列表")
public CommonResult<List<AppAddressRespVO>> getAddressList() {
List<AddressDO> list = addressService.getAddressList(getLoginUserId());
return success(AddressConvert.INSTANCE.convertList(list));
}
}

View File

@ -0,0 +1,39 @@
package co.yixiang.yshop.module.member.controller.app.address.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 用户收件地址 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class AppAddressBaseVO {
@Schema(description = "收件人名称", required = true)
@NotNull(message = "收件人名称不能为空")
private String name;
@Schema(description = "手机号", required = true)
@NotNull(message = "手机号不能为空")
private String mobile;
@Schema(description = "地区编号", required = true)
@NotNull(message = "地区编号不能为空")
private Long areaId;
@Schema(description = "邮编", required = true)
@NotEmpty(message = "邮编不能为空")
private String postCode;
@Schema(description = "收件详细地址", required = true)
@NotNull(message = "收件详细地址不能为空")
private String detailAddress;
@Schema(description = "是否默认地址", required = true)
@NotNull(message = "是否默认地址不能为空")
private Boolean defaulted;
}

View File

@ -0,0 +1,11 @@
package co.yixiang.yshop.module.member.controller.app.address.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
@Schema(description = "用户 APP - 用户收件地址创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppAddressCreateReqVO extends AppAddressBaseVO {
}

View File

@ -0,0 +1,18 @@
package co.yixiang.yshop.module.member.controller.app.address.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.time.LocalDateTime;
@Schema(description = "用户 APP - 用户收件地址 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppAddressRespVO extends AppAddressBaseVO {
@Schema(description = "编号", required = true)
private Long id;
@Schema(description = "创建时间", required = true)
private LocalDateTime createTime;
}

View File

@ -0,0 +1,16 @@
package co.yixiang.yshop.module.member.controller.app.address.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import javax.validation.constraints.*;
@Schema(description = "用户 APP - 用户收件地址更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppAddressUpdateReqVO extends AppAddressBaseVO {
@Schema(description = "编号", required = true)
@NotNull(message = "编号不能为空")
private Long id;
}

View File

@ -0,0 +1,51 @@
### 请求 /login 接口 => 成功
POST {{appApi}}/member/auth/login
Content-Type: application/json
tenant-id: {{appTenentId}}
{
"mobile": "15601691300",
"password": "admin123"
}
### 请求 /send-sms-code 接口 => 成功
POST {{appApi}}/member/auth/send-sms-code
Content-Type: application/json
tenant-id: {{appTenentId}}
{
"mobile": "15601691399",
"scene": 1
}
### 请求 /sms-login 接口 => 成功
POST {{appApi}}/member/auth/sms-login
Content-Type: application/json
tenant-id: {{appTenentId}}
{
"mobile": "15601691301",
"code": 9999
}
### 请求 /weixin-mini-app-login 接口 => 成功
POST {{appApi}}/member/auth/weixin-mini-app-login
Content-Type: application/json
tenant-id: {{appTenentId}}
{
"phoneCode": "618e6412e0c728f5b8fc7164497463d0158a923c9e7fd86af8bba393b9decbc5",
"loginCode": "001frTkl21JUf94VGxol2hSlff1frTkR"
}
### 请求 /logout 接口 => 成功
POST {{appApi}}/member/auth/logout
Content-Type: application/json
Authorization: Bearer c1b76bdaf2c146c581caa4d7fd81ee66
tenant-id: {{appTenentId}}
### 请求 /auth/refresh-token 接口 => 成功
POST {{appApi}}/member/auth/refresh-token?refreshToken=bc43d929094849a28b3a69f6e6940d70
Content-Type: application/json
tenant-id: {{appTenentId}}

View File

@ -0,0 +1,121 @@
package co.yixiang.yshop.module.member.controller.app.auth;
import cn.hutool.core.util.StrUtil;
import co.yixiang.yshop.framework.common.pojo.CommonResult;
import co.yixiang.yshop.framework.operatelog.core.annotations.OperateLog;
import co.yixiang.yshop.framework.security.config.SecurityProperties;
import co.yixiang.yshop.framework.security.core.annotations.PreAuthenticated;
import co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils;
import co.yixiang.yshop.module.member.controller.app.auth.vo.*;
import co.yixiang.yshop.module.member.service.auth.MemberAuthService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import static co.yixiang.yshop.framework.common.pojo.CommonResult.success;
import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "用户 APP - 认证")
@RestController
@RequestMapping("/member/auth")
@Validated
@Slf4j
public class AppAuthController {
@Resource
private MemberAuthService authService;
@Resource
private SecurityProperties securityProperties;
@PostMapping("/login")
@Operation(summary = "使用手机 + 密码登录")
public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) {
return success(authService.login(reqVO));
}
@PostMapping("/logout")
@PermitAll
@Operation(summary = "登出系统")
public CommonResult<Boolean> logout(HttpServletRequest request) {
String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
if (StrUtil.isNotBlank(token)) {
authService.logout(token);
}
return success(true);
}
@PostMapping("/refresh-token")
@Operation(summary = "刷新令牌")
@Parameter(name = "refreshToken", description = "刷新令牌", required = true)
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AppAuthLoginRespVO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
return success(authService.refreshToken(refreshToken));
}
// ========== 短信登录相关 ==========
@PostMapping("/sms-login")
@Operation(summary = "使用手机 + 验证码登录")
public CommonResult<AppAuthLoginRespVO> smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) {
return success(authService.smsLogin(reqVO));
}
@PostMapping("/send-sms-code")
@Operation(summary = "发送手机验证码")
public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid AppAuthSmsSendReqVO reqVO) {
authService.sendSmsCode(getLoginUserId(), reqVO);
return success(true);
}
@PostMapping("/reset-password")
@Operation(summary = "重置密码", description = "用户忘记密码时使用")
@PreAuthenticated
public CommonResult<Boolean> resetPassword(@RequestBody @Valid AppAuthResetPasswordReqVO reqVO) {
authService.resetPassword(reqVO);
return success(true);
}
@PostMapping("/update-password")
@Operation(summary = "修改用户密码", description = "用户修改密码时使用")
@PreAuthenticated
public CommonResult<Boolean> updatePassword(@RequestBody @Valid AppAuthUpdatePasswordReqVO reqVO) {
authService.updatePassword(getLoginUserId(), reqVO);
return success(true);
}
// ========== 社交登录相关 ==========
@GetMapping("/social-auth-redirect")
@Operation(summary = "社交授权的跳转")
@Parameters({
@Parameter(name = "type", description = "社交类型", required = true),
@Parameter(name = "redirectUri", description = "回调路径")
})
public CommonResult<String> socialAuthRedirect(@RequestParam("type") Integer type,
@RequestParam("redirectUri") String redirectUri) {
return CommonResult.success(authService.getSocialAuthorizeUrl(type, redirectUri));
}
@PostMapping("/social-login")
@Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户")
public CommonResult<AppAuthLoginRespVO> socialLogin(@RequestBody @Valid AppAuthSocialLoginReqVO reqVO) {
return success(authService.socialLogin(reqVO));
}
@PostMapping("/weixin-mini-app-login")
@Operation(summary = "微信小程序的一键登录")
public CommonResult<AppAuthLoginRespVO> weixinMiniAppLogin(@RequestBody @Valid AppAuthWeixinMiniAppLoginReqVO reqVO) {
return success(authService.weixinMiniAppLogin(reqVO));
}
}

View File

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

View File

@ -0,0 +1,56 @@
package co.yixiang.yshop.module.member.controller.app.auth.vo;
import cn.hutool.core.util.StrUtil;
import co.yixiang.yshop.framework.common.validation.InEnum;
import co.yixiang.yshop.framework.common.validation.Mobile;
import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
@Schema(description = "用户 APP - 手机 + 密码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthLoginReqVO {
@Schema(description = "手机号", required = true, example = "15601691300")
@NotEmpty(message = "手机号不能为空")
@Mobile
private String mobile;
@Schema(description = "密码", required = true, example = "buzhidao")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
// ========== 绑定社交登录时,需要传递如下参数 ==========
@Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", required = true, example = "10")
@InEnum(SocialTypeEnum.class)
private Integer socialType;
@Schema(description = "授权码", required = true, example = "1024")
private String socialCode;
@Schema(description = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
private String socialState;
@AssertTrue(message = "授权码不能为空")
public boolean isSocialCodeValid() {
return socialType == null || StrUtil.isNotEmpty(socialCode);
}
@AssertTrue(message = "授权 state 不能为空")
public boolean isSocialState() {
return socialType == null || StrUtil.isNotEmpty(socialState);
}
}

View File

@ -0,0 +1,30 @@
package co.yixiang.yshop.module.member.controller.app.auth.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Schema(description = "用户 APP - 登录 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthLoginRespVO {
@Schema(description = "用户编号", required = true, example = "1024")
private Long userId;
@Schema(description = "访问令牌", required = true, example = "happy")
private String accessToken;
@Schema(description = "刷新令牌", required = true, example = "nice")
private String refreshToken;
@Schema(description = "过期时间", required = true)
private LocalDateTime expiresTime;
}

View File

@ -0,0 +1,39 @@
package co.yixiang.yshop.module.member.controller.app.auth.vo;
import co.yixiang.yshop.framework.common.validation.Mobile;
import io.swagger.v3.oas.annotations.media.Schema;
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 yshopcode review 相关逻辑
@Schema(description = "用户 APP - 重置密码 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthResetPasswordReqVO {
@Schema(description = "新密码", required = true, example = "buzhidao")
@NotEmpty(message = "新密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
@Schema(description = "手机验证码", required = true, example = "1024")
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
@Schema(description = "手机号",required = true,example = "15878962356")
@NotBlank(message = "手机号不能为空")
@Mobile
private String mobile;
}

View File

@ -0,0 +1,58 @@
package co.yixiang.yshop.module.member.controller.app.auth.vo;
import cn.hutool.core.util.StrUtil;
import co.yixiang.yshop.framework.common.validation.InEnum;
import co.yixiang.yshop.framework.common.validation.Mobile;
import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Schema(description = "用户 APP - 手机 + 验证码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthSmsLoginReqVO {
@Schema(description = "手机号", required = true, example = "15601691300")
@NotEmpty(message = "手机号不能为空")
@Mobile
private String mobile;
@Schema(description = "手机验证码", required = true, example = "1024")
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
// ========== 绑定社交登录时,需要传递如下参数 ==========
@Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", required = true, example = "10")
@InEnum(SocialTypeEnum.class)
private Integer socialType;
@Schema(description = "授权码", required = true, example = "1024")
private String socialCode;
@Schema(description = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
private String socialState;
@AssertTrue(message = "授权码不能为空")
public boolean isSocialCodeValid() {
return socialType == null || StrUtil.isNotEmpty(socialCode);
}
@AssertTrue(message = "授权 state 不能为空")
public boolean isSocialState() {
return socialType == null || StrUtil.isNotEmpty(socialState);
}
}

View File

@ -0,0 +1,26 @@
package co.yixiang.yshop.module.member.controller.app.auth.vo;
import co.yixiang.yshop.framework.common.validation.InEnum;
import co.yixiang.yshop.framework.common.validation.Mobile;
import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Schema(description = "用户 APP - 发送手机验证码 Request VO")
@Data
@Accessors(chain = true)
public class AppAuthSmsSendReqVO {
@Schema(description = "手机号", example = "15601691234")
@Mobile
private String mobile;
@Schema(description = "发送场景,对应 SmsSceneEnum 枚举", example = "1")
@NotNull(message = "发送场景不能为空")
@InEnum(SmsSceneEnum.class)
private Integer scene;
}

View File

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

View File

@ -0,0 +1,30 @@
package co.yixiang.yshop.module.member.controller.app.auth.vo;
import io.swagger.v3.oas.annotations.media.Schema;
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 yshopcode review 相关逻辑
@Schema(description = "用户 APP - 修改密码 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthUpdatePasswordReqVO {
@Schema(description = "用户旧密码", required = true, example = "123456")
@NotBlank(message = "旧密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String oldPassword;
@Schema(description = "新密码", required = true, example = "buzhidao")
@NotEmpty(message = "新密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
}

View File

@ -0,0 +1,26 @@
package co.yixiang.yshop.module.member.controller.app.auth.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
@Schema(description = "用户 APP - 微信小程序手机登录 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthWeixinMiniAppLoginReqVO {
@Schema(description = "手机 code,小程序通过 wx.getPhoneNumber 方法获得", required = true, example = "hello")
@NotEmpty(message = "手机 code 不能为空")
private String phoneCode;
@Schema(description = "登录 code,小程序通过 wx.login 方法获得", required = true, example = "word")
@NotEmpty(message = "登录 code 不能为空")
private String loginCode;
}

View File

@ -0,0 +1,42 @@
package co.yixiang.yshop.module.member.controller.app.social;
import co.yixiang.yshop.framework.common.enums.UserTypeEnum;
import co.yixiang.yshop.framework.common.pojo.CommonResult;
import co.yixiang.yshop.module.member.controller.app.social.vo.AppSocialUserBindReqVO;
import co.yixiang.yshop.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO;
import co.yixiang.yshop.module.member.convert.social.SocialUserConvert;
import co.yixiang.yshop.module.system.api.social.SocialUserApi;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "用户 App - 社交用户")
@RestController
@RequestMapping("/system/social-user")
@Validated
public class AppSocialUserController {
@Resource
private SocialUserApi socialUserApi;
@PostMapping("/bind")
@Operation(summary = "社交绑定,使用 code 授权码")
public CommonResult<Boolean> socialBind(@RequestBody @Valid AppSocialUserBindReqVO reqVO) {
socialUserApi.bindSocialUser(SocialUserConvert.INSTANCE.convert(getLoginUserId(), UserTypeEnum.MEMBER.getValue(), reqVO));
return CommonResult.success(true);
}
@DeleteMapping("/unbind")
@Operation(summary = "取消社交绑定")
public CommonResult<Boolean> socialUnbind(@RequestBody AppSocialUserUnbindReqVO reqVO) {
socialUserApi.unbindSocialUser(SocialUserConvert.INSTANCE.convert(getLoginUserId(), UserTypeEnum.MEMBER.getValue(), reqVO));
return CommonResult.success(true);
}
}

View File

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

View File

@ -0,0 +1,30 @@
package co.yixiang.yshop.module.member.controller.app.social.vo;
import co.yixiang.yshop.framework.common.validation.InEnum;
import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Schema(description = "用户 APP - 取消社交绑定 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppSocialUserUnbindReqVO {
@Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", required = true, example = "10")
@InEnum(SocialTypeEnum.class)
@NotNull(message = "社交平台的类型不能为空")
private Integer type;
@Schema(description = "社交用户的 openid", required = true, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE")
@NotEmpty(message = "社交用户的 openid 不能为空")
private String openid;
}

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=yshop
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 co.yixiang.yshop.module.member.controller.app.user;
import co.yixiang.yshop.framework.common.pojo.CommonResult;
import co.yixiang.yshop.framework.security.core.annotations.PreAuthenticated;
import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserInfoRespVO;
import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO;
import co.yixiang.yshop.module.member.convert.user.UserConvert;
import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO;
import co.yixiang.yshop.module.member.service.user.MemberUserService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
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 co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception;
import static co.yixiang.yshop.framework.common.pojo.CommonResult.success;
import static co.yixiang.yshop.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static co.yixiang.yshop.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;
@Tag(name = "用户 APP - 用户个人中心")
@RestController
@RequestMapping("/member/user")
@Validated
@Slf4j
public class AppUserController {
@Resource
private MemberUserService userService;
@PutMapping("/update-nickname")
@Operation(summary = "修改用户昵称")
@PreAuthenticated
public CommonResult<Boolean> updateUserNickname(@RequestParam("nickname") String nickname) {
userService.updateUserNickname(getLoginUserId(), nickname);
return success(true);
}
@PostMapping("/update-avatar")
@Operation(summary = "修改用户头像")
@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")
@Operation(summary = "获得基本信息")
@PreAuthenticated
public CommonResult<AppUserInfoRespVO> getUserInfo() {
MemberUserDO user = userService.getUser(getLoginUserId());
return success(UserConvert.INSTANCE.convert(user));
}
@PostMapping("/update-mobile")
@Operation(summary = "修改用户手机")
@PreAuthenticated
public CommonResult<Boolean> updateMobile(@RequestBody @Valid AppUserUpdateMobileReqVO reqVO) {
userService.updateUserMobile(getLoginUserId(), reqVO);
return success(true);
}
}

View File

@ -0,0 +1,22 @@
package co.yixiang.yshop.module.member.controller.app.user.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Schema(description = "用户 APP - 用户个人信息 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppUserInfoRespVO {
@Schema(description = "用户昵称", required = true, example = "yshop")
private String nickname;
@Schema(description = "用户头像", required = true, example = "/infra/file/get/35a12e57-4297-4faa-bf7d-7ed2f211c952")
private String avatar;
@Schema(description = "用户手机号", required = true, example = "15601691300")
private String mobile;
}

View File

@ -0,0 +1,48 @@
package co.yixiang.yshop.module.member.controller.app.user.vo;
import co.yixiang.yshop.framework.common.validation.Mobile;
import io.swagger.v3.oas.annotations.media.Schema;
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;
@Schema(description = "用户 APP - 修改手机 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppUserUpdateMobileReqVO {
@Schema(description = "手机验证码", required = true, example = "1024")
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
@Schema(description = "手机号",required = true,example = "15823654487")
@NotBlank(message = "手机号不能为空")
@Length(min = 8, max = 11, message = "手机号码长度为 8-11 位")
@Mobile
private String mobile;
@Schema(description = "原手机验证码", required = true, example = "1024")
@NotEmpty(message = "原手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String oldCode;
// TODO @yshopoldMobile 应该不用传递
@Schema(description = "原手机号",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 co.yixiang.yshop.module.member.controller.app.weixin;
import co.yixiang.yshop.framework.common.pojo.CommonResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
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 co.yixiang.yshop.framework.common.pojo.CommonResult.success;
@Tag(name = "微信公众号")
@RestController
@RequestMapping("/member/wx-mp")
@Validated
@Slf4j
public class AppWxMpController {
@Resource
private WxMpService mpService;
@PostMapping("/create-jsapi-signature")
@Operation(summary = "创建微信 JS SDK 初始化所需的签名",
description = "参考 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 包:提供给管理后台 yshop-ui-admin 前端项目
* 2. app 包:提供给用户 APP yshop-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分
*/
package co.yixiang.yshop.module.member.controller;

View File

@ -0,0 +1,36 @@
package co.yixiang.yshop.module.member.convert.address;
import co.yixiang.yshop.framework.common.pojo.PageResult;
import co.yixiang.yshop.module.member.api.address.dto.AddressRespDTO;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressCreateReqVO;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressRespVO;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressUpdateReqVO;
import co.yixiang.yshop.module.member.dal.dataobject.address.AddressDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 用户收件地址 Convert
*
* @author yshop
*/
@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);
AddressRespDTO convert02(AddressDO bean);
}

View File

@ -0,0 +1,28 @@
package co.yixiang.yshop.module.member.convert.auth;
import co.yixiang.yshop.module.member.controller.app.auth.vo.*;
import co.yixiang.yshop.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO;
import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO;
import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO;
import co.yixiang.yshop.module.system.api.social.dto.SocialUserUnbindReqDTO;
import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface AuthConvert {
AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class);
SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialLoginReqVO reqVO);
SocialUserUnbindReqDTO convert(Long userId, Integer userType, AppSocialUserUnbindReqVO reqVO);
SmsCodeSendReqDTO convert(AppAuthSmsSendReqVO reqVO);
SmsCodeUseReqDTO convert(AppAuthResetPasswordReqVO reqVO, SmsSceneEnum scene, String usedIp);
SmsCodeUseReqDTO convert(AppAuthSmsLoginReqVO reqVO, Integer scene, String usedIp);
AppAuthLoginRespVO convert(OAuth2AccessTokenRespDTO bean);
}

View File

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

View File

@ -0,0 +1,19 @@
package co.yixiang.yshop.module.member.convert.social;
import co.yixiang.yshop.module.member.controller.app.social.vo.AppSocialUserBindReqVO;
import co.yixiang.yshop.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO;
import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO;
import co.yixiang.yshop.module.system.api.social.dto.SocialUserUnbindReqDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface SocialUserConvert {
SocialUserConvert INSTANCE = Mappers.getMapper(SocialUserConvert.class);
SocialUserBindReqDTO convert(Long userId, Integer userType, AppSocialUserBindReqVO reqVO);
SocialUserUnbindReqDTO convert(Long userId, Integer userType, AppSocialUserUnbindReqVO reqVO);
}

View File

@ -0,0 +1,22 @@
package co.yixiang.yshop.module.member.convert.user;
import co.yixiang.yshop.module.member.api.user.dto.MemberUserRespDTO;
import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserInfoRespVO;
import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface UserConvert {
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
AppUserInfoRespVO convert(MemberUserDO bean);
MemberUserRespDTO convert2(MemberUserDO bean);
List<MemberUserRespDTO> convertList2(List<MemberUserDO> list);
}

View File

@ -0,0 +1,58 @@
package co.yixiang.yshop.module.member.dal.dataobject.address;
import co.yixiang.yshop.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 用户收件地址 DO
*
* @author yshop
*/
@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 Long areaId;
/**
* 邮编
*/
private String postCode;
/**
* 收件详细地址
*/
private String detailAddress;
/**
* 是否默认
*
* true - 默认收件地址
*/
private Boolean defaulted;
}

View File

@ -0,0 +1,78 @@
package co.yixiang.yshop.module.member.dal.dataobject.user;
import co.yixiang.yshop.framework.common.enums.CommonStatusEnum;
import co.yixiang.yshop.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.time.LocalDateTime;
/**
* 会员用户 DO
*
* uk_mobile 索引:基于 {@link #mobile} 字段
*
* @author yshop
*/
@TableName(value = "member_user", autoResultMap = true)
@KeySequence("member_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@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 LocalDateTime loginDate;
// TODO yshopname 真实名字;
// TODO yshopemail 邮箱;
// TODO yshopgender 性别;
// TODO yshopscore 积分;
// TODO yshoppayPassword 支付密码;
}

View File

@ -0,0 +1,22 @@
package co.yixiang.yshop.module.member.dal.mysql.address;
import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX;
import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX;
import co.yixiang.yshop.module.member.dal.dataobject.address.AddressDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface AddressMapper extends BaseMapperX<AddressDO> {
default AddressDO selectByIdAndUserId(Long id, Long userId) {
return selectOne(AddressDO::getId, id, AddressDO::getUserId, userId);
}
default List<AddressDO> selectListByUserIdAndDefaulted(Long userId, Boolean defaulted) {
return selectList(new LambdaQueryWrapperX<AddressDO>().eq(AddressDO::getUserId, userId)
.eqIfPresent(AddressDO::getDefaulted, defaulted));
}
}

View File

@ -0,0 +1,27 @@
package co.yixiang.yshop.module.member.dal.mysql.user;
import co.yixiang.yshop.framework.mybatis.core.mapper.BaseMapperX;
import co.yixiang.yshop.framework.mybatis.core.query.LambdaQueryWrapperX;
import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 会员 User Mapper
*
* @author yshop
*/
@Mapper
public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
default MemberUserDO selectByMobile(String mobile) {
return selectOne(MemberUserDO::getMobile, mobile);
}
default List<MemberUserDO> selectListByNicknameLike(String nickname) {
return selectList(new LambdaQueryWrapperX<MemberUserDO>()
.likeIfPresent(MemberUserDO::getNickname, nickname));
}
}

View File

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

View File

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

View File

@ -0,0 +1,6 @@
/**
* 属于 member 模块的 framework 封装
*
* @author yshop
*/
package co.yixiang.yshop.module.member.framework;

View File

@ -0,0 +1,24 @@
package co.yixiang.yshop.module.member.framework.web.config;
import co.yixiang.yshop.framework.swagger.config.YshopSwaggerAutoConfiguration;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* member 模块的 web 组件的 Configuration
*
* @author yshop
*/
@Configuration(proxyBeanMethods = false)
public class MemberWebConfiguration {
/**
* member 模块的 API 分组
*/
@Bean
public GroupedOpenApi memberGroupedOpenApi() {
return YshopSwaggerAutoConfiguration.buildGroupedOpenApi("member");
}
}

View File

@ -0,0 +1,4 @@
/**
* member 模块的 web 配置
*/
package co.yixiang.yshop.module.member.framework.web;

View File

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

View File

@ -0,0 +1,67 @@
package co.yixiang.yshop.module.member.service.address;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressCreateReqVO;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressUpdateReqVO;
import co.yixiang.yshop.module.member.dal.dataobject.address.AddressDO;
import javax.validation.Valid;
import java.util.List;
/**
* 用户收件地址 Service 接口
*
* @author yshop
*/
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);
/**
* 获得用户默认的收件地址
*
* @param userId 用户编号
* @return 用户收件地址
*/
AddressDO getDefaultUserAddress(Long userId);
}

View File

@ -0,0 +1,97 @@
package co.yixiang.yshop.module.member.service.address;
import cn.hutool.core.collection.CollUtil;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressCreateReqVO;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressUpdateReqVO;
import co.yixiang.yshop.module.member.convert.address.AddressConvert;
import co.yixiang.yshop.module.member.dal.dataobject.address.AddressDO;
import co.yixiang.yshop.module.member.dal.mysql.address.AddressMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.List;
import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception;
import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.ADDRESS_NOT_EXISTS;
/**
* 用户收件地址 Service 实现类
*
* @author yshop
*/
@Service
@Validated
public class AddressServiceImpl implements AddressService {
@Resource
private AddressMapper addressMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createAddress(Long userId, AppAddressCreateReqVO createReqVO) {
// 如果添加的是默认收件地址,则将原默认地址修改为非默认
if (Boolean.TRUE.equals(createReqVO.getDefaulted())) {
List<AddressDO> addresses = addressMapper.selectListByUserIdAndDefaulted(userId, true);
addresses.forEach(address -> addressMapper.updateById(new AddressDO().setId(address.getId()).setDefaulted(false)));
}
// 插入
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) {
// 校验存在,校验是否能够操作
validAddressExists(userId, updateReqVO.getId());
// 如果修改的是默认收件地址,则将原默认地址修改为非默认
if (Boolean.TRUE.equals(updateReqVO.getDefaulted())) {
List<AddressDO> addresses = addressMapper.selectListByUserIdAndDefaulted(userId, true);
addresses.stream().filter(u -> !u.getId().equals(updateReqVO.getId())) // 排除自己
.forEach(address -> addressMapper.updateById(new AddressDO().setId(address.getId()).setDefaulted(false)));
}
// 更新
AddressDO updateObj = AddressConvert.INSTANCE.convert(updateReqVO);
addressMapper.updateById(updateObj);
}
@Override
public void deleteAddress(Long userId, Long id) {
// 校验存在,校验是否能够操作
validAddressExists(userId, id);
// 删除
addressMapper.deleteById(id);
}
private void validAddressExists(Long userId, Long id) {
AddressDO addressDO = getAddress(userId, id);
if (addressDO == null) {
throw exception(ADDRESS_NOT_EXISTS);
}
}
@Override
public AddressDO getAddress(Long userId, Long id) {
return addressMapper.selectByIdAndUserId(id, userId);
}
@Override
public List<AddressDO> getAddressList(Long userId) {
return addressMapper.selectListByUserIdAndDefaulted(userId, null);
}
@Override
public AddressDO getDefaultUserAddress(Long userId) {
List<AddressDO> addresses = addressMapper.selectListByUserIdAndDefaulted(userId, true);
return CollUtil.getFirst(addresses);
}
}

View File

@ -0,0 +1,93 @@
package co.yixiang.yshop.module.member.service.auth;
import co.yixiang.yshop.module.member.controller.app.auth.vo.*;
import javax.validation.Valid;
/**
* 会员的认证 Service 接口
*
* 提供用户的账号密码登录、token 的校验等认证相关的功能
*
* @author yshop
*/
public interface MemberAuthService {
/**
* 手机 + 密码登录
*
* @param reqVO 登录信息
* @return 登录结果
*/
AppAuthLoginRespVO login(@Valid AppAuthLoginReqVO reqVO);
/**
* 基于 token 退出登录
*
* @param token token
*/
void logout(String token);
/**
* 手机 + 验证码登陆
*
* @param reqVO 登陆信息
* @return 登录结果
*/
AppAuthLoginRespVO smsLogin(@Valid AppAuthSmsLoginReqVO reqVO);
/**
* 社交登录,使用 code 授权码
*
* @param reqVO 登录信息
* @return 登录结果
*/
AppAuthLoginRespVO socialLogin(@Valid AppAuthSocialLoginReqVO reqVO);
/**
* 微信小程序的一键登录
*
* @param reqVO 登录信息
* @return 登录结果
*/
AppAuthLoginRespVO weixinMiniAppLogin(AppAuthWeixinMiniAppLoginReqVO 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, AppAuthSmsSendReqVO reqVO);
/**
* 刷新访问令牌
*
* @param refreshToken 刷新令牌
* @return 登录结果
*/
AppAuthLoginRespVO refreshToken(String refreshToken);
}

View File

@ -0,0 +1,301 @@
package co.yixiang.yshop.module.member.service.auth;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import co.yixiang.yshop.framework.common.enums.CommonStatusEnum;
import co.yixiang.yshop.framework.common.enums.UserTypeEnum;
import co.yixiang.yshop.framework.common.util.monitor.TracerUtils;
import co.yixiang.yshop.framework.common.util.servlet.ServletUtils;
import co.yixiang.yshop.module.member.controller.app.auth.vo.*;
import co.yixiang.yshop.module.member.convert.auth.AuthConvert;
import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO;
import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper;
import co.yixiang.yshop.module.member.service.user.MemberUserService;
import co.yixiang.yshop.module.system.api.logger.LoginLogApi;
import co.yixiang.yshop.module.system.api.logger.dto.LoginLogCreateReqDTO;
import co.yixiang.yshop.module.system.api.oauth2.OAuth2TokenApi;
import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
import co.yixiang.yshop.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO;
import co.yixiang.yshop.module.system.api.sms.SmsCodeApi;
import co.yixiang.yshop.module.system.api.social.SocialUserApi;
import co.yixiang.yshop.module.system.api.social.dto.SocialUserBindReqDTO;
import co.yixiang.yshop.module.system.enums.logger.LoginLogTypeEnum;
import co.yixiang.yshop.module.system.enums.logger.LoginResultEnum;
import co.yixiang.yshop.module.system.enums.oauth2.OAuth2ClientConstants;
import co.yixiang.yshop.module.system.enums.sms.SmsSceneEnum;
import co.yixiang.yshop.module.system.enums.social.SocialTypeEnum;
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 java.util.Objects;
import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception;
import static co.yixiang.yshop.framework.common.util.servlet.ServletUtils.getClientIP;
import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.*;
/**
* 会员的认证 Service 接口
*
* @author yshop
*/
@Service
@Slf4j
public class MemberAuthServiceImpl implements MemberAuthService {
@Resource
private MemberUserService userService;
@Resource
private SmsCodeApi smsCodeApi;
@Resource
private LoginLogApi loginLogApi;
@Resource
private SocialUserApi socialUserApi;
@Resource
private OAuth2TokenApi oauth2TokenApi;
@Resource
private WxMaService wxMaService;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private MemberUserMapper userMapper;
@Override
public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) {
// 使用手机 + 密码,进行登录。
MemberUserDO user = login0(reqVO.getMobile(), reqVO.getPassword());
// 如果 socialType 非空,说明需要绑定社交用户
if (reqVO.getSocialType() != null) {
socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
}
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE);
}
@Override
@Transactional
public AppAuthLoginRespVO smsLogin(AppAuthSmsLoginReqVO reqVO) {
// 校验验证码
String userIp = getClientIP();
smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp));
// 获得获得注册用户
MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), userIp);
Assert.notNull(user, "获取用户失败,结果为空");
// 如果 socialType 非空,说明需要绑定社交用户
if (reqVO.getSocialType() != null) {
socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
}
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS);
}
@Override
public AppAuthLoginRespVO socialLogin(AppAuthSocialLoginReqVO reqVO) {
// 使用 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);
}
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL);
}
@Override
public AppAuthLoginRespVO weixinMiniAppLogin(AppAuthWeixinMiniAppLoginReqVO reqVO) {
// 获得对应的手机号信息
WxMaPhoneNumberInfo phoneNumberInfo;
try {
phoneNumberInfo = wxMaService.getUserService().getNewPhoneNoInfo(reqVO.getPhoneCode());
} catch (Exception exception) {
throw exception(AUTH_WEIXIN_MINI_APP_PHONE_CODE_ERROR);
}
// 获得获得注册用户
MemberUserDO user = userService.createUserIfAbsent(phoneNumberInfo.getPurePhoneNumber(), getClientIP());
Assert.notNull(user, "获取用户失败,结果为空");
// 绑定社交用户
socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
SocialTypeEnum.WECHAT_MINI_APP.getType(), reqVO.getLoginCode(), ""));
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL);
}
private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile, LoginLogTypeEnum logType) {
// 插入登陆日志
createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS);
// 创建 Token 令牌
OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO()
.setUserId(user.getId()).setUserType(getUserType().getValue())
.setClientId(OAuth2ClientConstants.CLIENT_ID_DEFAULT));
// 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenRespDTO);
}
@Override
public String getSocialAuthorizeUrl(Integer type, String redirectUri) {
return socialUserApi.getAuthorizeUrl(type, redirectUri);
}
private MemberUserDO login0(String mobile, String password) {
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_MOBILE;
// 校验账号是否存在
MemberUserDO user = userService.getUserByMobile(mobile);
if (user == null) {
createLoginLog(null, mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
if (!userService.isPasswordMatch(password, user.getPassword())) {
createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
// 校验是否禁用
if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.USER_DISABLED);
throw exception(AUTH_LOGIN_USER_DISABLED);
}
return user;
}
private void createLoginLog(Long userId, String mobile, LoginLogTypeEnum logType, LoginResultEnum loginResult) {
// 插入登录日志
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(logType.getType());
reqDTO.setTraceId(TracerUtils.getTraceId());
reqDTO.setUserId(userId);
reqDTO.setUserType(getUserType().getValue());
reqDTO.setUsername(mobile);
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(getClientIP());
reqDTO.setResult(loginResult.getResult());
loginLogApi.createLoginLog(reqDTO);
// 更新最后登录时间
if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) {
userService.updateUserLogin(userId, getClientIP());
}
}
@Override
public void logout(String token) {
// 删除访问令牌
OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.removeAccessToken(token);
if (accessTokenRespDTO == null) {
return;
}
// 删除成功,则记录登出日志
createLogoutLog(accessTokenRespDTO.getUserId());
}
@Override
public void updatePassword(Long userId, AppAuthUpdatePasswordReqVO reqVO) {
// 检验旧密码
MemberUserDO userDO = checkOldPassword(userId, reqVO.getOldPassword());
// 更新用户密码
// TODO yshop需要重构到用户模块
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, AppAuthSmsSendReqVO reqVO) {
// TODO 要根据不同的场景,校验是否有用户
smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP()));
}
@Override
public AppAuthLoginRespVO refreshToken(String refreshToken) {
OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
return AuthConvert.INSTANCE.convert(accessTokenDO);
}
/**
* 校验旧密码
*
* @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) {
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_SELF.getType());
reqDTO.setTraceId(TracerUtils.getTraceId());
reqDTO.setUserId(userId);
reqDTO.setUserType(getUserType().getValue());
reqDTO.setUsername(getMobile(userId));
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(getClientIP());
reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
loginLogApi.createLoginLog(reqDTO);
}
private String getMobile(Long userId) {
if (userId == null) {
return null;
}
MemberUserDO user = userService.getUser(userId);
return user != null ? user.getMobile() : null;
}
private UserTypeEnum getUserType() {
return UserTypeEnum.MEMBER;
}
}

View File

@ -0,0 +1,99 @@
package co.yixiang.yshop.module.member.service.user;
import co.yixiang.yshop.framework.common.validation.Mobile;
import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO;
import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
/**
* 会员用户 Service 接口
*
* @author yshop
*/
public interface MemberUserService {
/**
* 通过手机查询用户
*
* @param mobile 手机
* @return 用户对象
*/
MemberUserDO getUserByMobile(String mobile);
/**
* 基于用户昵称,模糊匹配用户列表
*
* @param nickname 用户昵称,模糊匹配
* @return 用户信息的列表
*/
List<MemberUserDO> getUserListByNickname(String nickname);
/**
* 基于手机号创建用户。
* 如果用户已经存在,则直接进行返回
*
* @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);
/**
* 通过用户 ID 查询用户们
*
* @param ids 用户 ID
* @return 用户对象信息数组
*/
List<MemberUserDO> getUserList(Collection<Long> ids);
/**
* 修改用户昵称
* @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);
/**
* 判断密码是否匹配
*
* @param rawPassword 未加密的密码
* @param encodedPassword 加密后的密码
* @return 是否匹配
*/
boolean isPasswordMatch(String rawPassword, String encodedPassword);
}

View File

@ -0,0 +1,170 @@
package co.yixiang.yshop.module.member.service.user;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import co.yixiang.yshop.framework.common.enums.CommonStatusEnum;
import co.yixiang.yshop.module.infra.api.file.FileApi;
import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO;
import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO;
import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper;
import co.yixiang.yshop.module.system.api.sms.SmsCodeApi;
import co.yixiang.yshop.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import co.yixiang.yshop.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.Collection;
import java.util.Date;
import java.util.List;
import java.time.LocalDateTime;
import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception;
import static co.yixiang.yshop.framework.common.util.servlet.ServletUtils.getClientIP;
import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.USER_NOT_EXISTS;
/**
* 会员 User Service 实现类
*
* @author yshop
*/
@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 List<MemberUserDO> getUserListByNickname(String nickname) {
return memberUserMapper.selectListByNicknameLike(nickname);
}
@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(encodePassword(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(LocalDateTime.now()));
}
@Override
public MemberUserDO getUser(Long id) {
return memberUserMapper.selectById(id);
}
@Override
public List<MemberUserDO> getUserList(Collection<Long> ids) {
return memberUserMapper.selectBatchIds(ids);
}
@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 yshopoldMobile 应该不用传递
// 校验旧手机和旧验证码
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());
}
@Override
public boolean isPasswordMatch(String rawPassword, String encodedPassword) {
return passwordEncoder.matches(rawPassword, encodedPassword);
}
/**
* 对密码进行加密
*
* @param password 密码
* @return 加密后的密码
*/
private String encodePassword(String password) {
return passwordEncoder.encode(password);
}
@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,98 @@
package co.yixiang.yshop.module.member.service.address;
import co.yixiang.yshop.framework.test.core.ut.BaseDbUnitTest;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressCreateReqVO;
import co.yixiang.yshop.module.member.controller.app.address.vo.AppAddressUpdateReqVO;
import co.yixiang.yshop.module.member.dal.dataobject.address.AddressDO;
import co.yixiang.yshop.module.member.dal.mysql.address.AddressMapper;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertPojoEquals;
import static co.yixiang.yshop.framework.test.core.util.AssertUtils.assertServiceException;
import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomLongId;
import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo;
import static co.yixiang.yshop.module.member.enums.ErrorCodeConstants.ADDRESS_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* {@link AddressServiceImpl} 的单元测试类
*
* @author yshop
*/
@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(randomLongId(), 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(dbAddress.getUserId(), reqVO);
// 校验是否更新正确
AddressDO address = addressMapper.selectById(reqVO.getId()); // 获取最新的
assertPojoEquals(reqVO, address);
}
@Test
public void testUpdateAddress_notExists() {
// 准备参数
AppAddressUpdateReqVO reqVO = randomPojo(AppAddressUpdateReqVO.class);
// 调用, 并断言异常
assertServiceException(() -> addressService.updateAddress(randomLongId(), 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(dbAddress.getUserId(), id);
// 校验数据不存在了
assertNull(addressMapper.selectById(id));
}
@Test
public void testDeleteAddress_notExists() {
// 准备参数
Long id = randomLongId();
// 调用, 并断言异常
assertServiceException(() -> addressService.deleteAddress(randomLongId(), id), ADDRESS_NOT_EXISTS);
}
}

View File

@ -0,0 +1,126 @@
package co.yixiang.yshop.module.member.service.auth;
import cn.binarywang.wx.miniapp.api.WxMaService;
import co.yixiang.yshop.framework.common.enums.CommonStatusEnum;
import co.yixiang.yshop.framework.common.util.collection.ArrayUtils;
import co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration;
import co.yixiang.yshop.framework.test.core.ut.BaseDbAndRedisUnitTest;
import co.yixiang.yshop.module.member.controller.app.auth.vo.AppAuthResetPasswordReqVO;
import co.yixiang.yshop.module.member.controller.app.auth.vo.AppAuthUpdatePasswordReqVO;
import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO;
import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper;
import co.yixiang.yshop.module.member.service.user.MemberUserService;
import co.yixiang.yshop.module.system.api.oauth2.OAuth2TokenApi;
import co.yixiang.yshop.module.system.api.logger.LoginLogApi;
import co.yixiang.yshop.module.system.api.sms.SmsCodeApi;
import co.yixiang.yshop.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.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 co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo;
import static co.yixiang.yshop.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
// TODO @yshop单测的 review等逻辑都达成一致后
/**
* {@link MemberAuthService} 的单元测试类
*
* @author 宋天
*/
@Import({MemberAuthServiceImpl.class, YshopRedisAutoConfiguration.class})
public class MemberAuthServiceTest extends BaseDbAndRedisUnitTest {
// TODO @yshop登录相关的单测待补全
@Resource
private MemberAuthServiceImpl authService;
@MockBean
private MemberUserService userService;
@MockBean
private SmsCodeApi smsCodeApi;
@MockBean
private LoginLogApi loginLogApi;
@MockBean
private OAuth2TokenApi oauth2TokenApi;
@MockBean
private SocialUserApi socialUserApi;
@MockBean
private WxMaService wxMaService;
@MockBean
private PasswordEncoder passwordEncoder;
@Resource
private MemberUserMapper memberUserMapper;
@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 co.yixiang.yshop.module.member.service.user;
import cn.hutool.core.util.RandomUtil;
import co.yixiang.yshop.framework.common.enums.CommonStatusEnum;
import co.yixiang.yshop.framework.common.util.collection.ArrayUtils;
import co.yixiang.yshop.framework.redis.config.YshopRedisAutoConfiguration;
import co.yixiang.yshop.framework.test.core.ut.BaseDbAndRedisUnitTest;
import co.yixiang.yshop.module.infra.api.file.FileApi;
import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO;
import co.yixiang.yshop.module.member.dal.dataobject.user.MemberUserDO;
import co.yixiang.yshop.module.member.dal.mysql.user.MemberUserMapper;
import co.yixiang.yshop.module.member.service.auth.MemberAuthServiceImpl;
import co.yixiang.yshop.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 co.yixiang.yshop.framework.test.core.util.RandomUtils.randomPojo;
import static co.yixiang.yshop.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 @yshop单测的 review等逻辑都达成一致后
/**
* {@link MemberUserServiceImpl} 的单元测试类
*
* @author 宋天
*/
@Import({MemberUserServiceImpl.class, YshopRedisAutoConfiguration.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 yshop需要修复该单元测试重构多模块带来的
// 旧手机和旧验证码
// 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;NON_KEYWORDS=value; # 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 配置项
--- #################### 监控相关配置 ####################
--- #################### yshop相关配置 ####################
# yshop配置项设置当前项目所有自定义的配置
yshop:
info:
base-package: co.yixiang.yshop.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,36 @@
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 '会员表';
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_id" bigint(20) NOT NULL,
"post_code" varchar(16) NOT NULL,
"detail_address" varchar(250) NOT NULL,
"defaulted" bit 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 '',
PRIMARY KEY ("id")
) COMMENT '用户收件地址';