初始化order和pay模块

This commit is contained in:
2025-08-27 10:02:53 +08:00
parent ea780302e3
commit 1555739fcd
536 changed files with 6640 additions and 17174 deletions

View File

@@ -0,0 +1,16 @@
package com.tashow.cloud.member;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 项目的启动类
*/
@SpringBootApplication
public class MemberServerApplication {
public static void main(String[] args) {
SpringApplication.run(MemberServerApplication.class, args);
}
}

View File

@@ -0,0 +1,35 @@
package com.tashow.cloud.member.address;
import com.tashow.cloud.common.pojo.CommonResult;
import com.tashow.cloud.memberapi.api.address.MemberAddressApi;
import com.tashow.cloud.memberapi.api.address.dto.MemberAddressRespDTO;
import com.tashow.cloud.member.convert.address.AddressConvert;
import com.tashow.cloud.member.service.address.AddressService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import static com.tashow.cloud.common.pojo.CommonResult.success;
/**
* 用户收件地址 API 实现类
*/
@RestController // 提供 RESTful API 接口,给 Feign 调用
@Validated
public class MemberAddressApiImpl implements MemberAddressApi {
@Resource
private AddressService addressService;
@Override
public CommonResult<MemberAddressRespDTO> getAddress(Long id, Long userId) {
return success(AddressConvert.INSTANCE.convert02(addressService.getAddress(userId, id)));
}
@Override
public CommonResult<MemberAddressRespDTO> getDefaultAddress(Long userId) {
return success(AddressConvert.INSTANCE.convert02(addressService.getDefaultUserAddress(userId)));
}
}

View File

@@ -0,0 +1,39 @@
package com.tashow.cloud.member.controller.admin.address;
import com.tashow.cloud.common.pojo.CommonResult;
import com.tashow.cloud.member.controller.admin.address.vo.AddressRespVO;
import com.tashow.cloud.member.convert.address.AddressConvert;
import com.tashow.cloud.member.dal.dataobject.address.MemberAddressDO;
import com.tashow.cloud.member.service.address.AddressService;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.tashow.cloud.common.pojo.CommonResult.success;
// 管理后台 - 用户收件地址
@RestController
@RequestMapping("/member/address")
@Validated
public class AddressController {
@Resource
private AddressService addressService;
@GetMapping("/list")
// 获得用户收件地址列表
// userId: 用户编号,必填
@PreAuthorize("@ss.hasPermission('member:user:query')")
public CommonResult<List<AddressRespVO>> getAddressList(@RequestParam("userId") Long userId) {
List<MemberAddressDO> list = addressService.getAddressList(userId);
return success(AddressConvert.INSTANCE.convertList2(list));
}
}

View File

@@ -0,0 +1 @@
package com.tashow.cloud.member.controller.admin.address;

View File

@@ -0,0 +1,36 @@
package com.tashow.cloud.member.controller.admin.address.vo;
import lombok.*;
import java.time.LocalDateTime;
import java.util.*;
import jakarta.validation.constraints.*;
/**
* 用户收件地址 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class AddressBaseVO {
// 收件人名称,必填,示例:张三
@NotNull(message = "收件人名称不能为空")
private String name;
// 手机号,必填
@NotNull(message = "手机号不能为空")
private String mobile;
// 地区编码必填示例15716
@NotNull(message = "地区编码不能为空")
private Long areaId;
// 收件详细地址,必填
@NotNull(message = "收件详细地址不能为空")
private String detailAddress;
// 是否默认必填示例2
@NotNull(message = "是否默认不能为空")
private Boolean defaultStatus;
}

View File

@@ -0,0 +1,18 @@
package com.tashow.cloud.member.controller.admin.address.vo;
import lombok.*;
import java.time.LocalDateTime;
// 管理后台 - 用户收件地址 Response VO
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AddressRespVO extends AddressBaseVO {
// 收件地址编号必填示例7380
private Long id;
// 创建时间,必填
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,72 @@
package com.tashow.cloud.member.controller.admin.user;
import cn.hutool.core.collection.CollUtil;
import com.tashow.cloud.common.pojo.CommonResult;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.member.controller.admin.user.vo.MemberUserPageReqVO;
import com.tashow.cloud.member.controller.admin.user.vo.MemberUserRespVO;
import com.tashow.cloud.member.controller.admin.user.vo.MemberUserUpdateReqVO;
import com.tashow.cloud.member.convert.user.MemberUserConvert;
import com.tashow.cloud.member.dal.dataobject.user.MemberUserDO;
import com.tashow.cloud.member.service.user.MemberUserService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static com.tashow.cloud.common.pojo.CommonResult.success;
// 管理后台 - 会员用户
@RestController
@RequestMapping("/member/user")
@Validated
public class MemberUserController {
@Resource
private MemberUserService memberUserService;
@Resource
@PutMapping("/update")
// 更新会员用户
@PreAuthorize("@ss.hasPermission('member:user:update')")
public CommonResult<Boolean> updateUser(@Valid @RequestBody MemberUserUpdateReqVO updateReqVO) {
memberUserService.updateUser(updateReqVO);
return success(true);
}
@GetMapping("/get")
// 获得会员用户
// id: 编号必填示例1024
@PreAuthorize("@ss.hasPermission('member:user:query')")
public CommonResult<MemberUserRespVO> getUser(@RequestParam("id") Long id) {
MemberUserDO user = memberUserService.getUser(id);
return success(MemberUserConvert.INSTANCE.convert03(user));
}
@GetMapping("/page")
// 获得会员用户分页
@PreAuthorize("@ss.hasPermission('member:user:query')")
public CommonResult<PageResult<MemberUserRespVO>> getUserPage(@Valid MemberUserPageReqVO pageVO) {
PageResult<MemberUserDO> pageResult = memberUserService.getUserPage(pageVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty());
}
// 处理用户标签返显
Set<Long> tagIds = pageResult.getList().stream()
.map(MemberUserDO::getTagIds)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.collect(Collectors.toSet());
return success(MemberUserConvert.INSTANCE.convertPage(pageResult));
}
}

View File

@@ -0,0 +1,65 @@
package com.tashow.cloud.member.controller.admin.user.vo;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import org.springframework.format.annotation.DateTimeFormat;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.List;
import static com.tashow.cloud.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
/**
* 会员用户 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class MemberUserBaseVO {
// 手机号必填示例15601691300
@NotNull(message = "手机号不能为空")
private String mobile;
// 状态必填示例2
@NotNull(message = "状态不能为空")
private Byte status;
// 用户昵称,必填,示例:李四
@NotNull(message = "用户昵称不能为空")
private String nickname;
// 头像必填示例https://www.iocoder.cn/x.png
@URL(message = "头像必须是 URL 格式")
private String avatar;
// 用户昵称,示例:李四
private String name;
// 用户性别示例1
private Integer sex;
// 所在地编号示例4371
private Long areaId;
// 所在地全程,示例:上海上海市普陀区
private String areaName;
// 出生日期示例2023-03-12
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDateTime birthday;
// 会员备注,示例:我是小备注
private String mark;
// 会员标签,示例:[1, 2]
private List<Long> tagIds;
// 会员等级编号示例1
private Long levelId;
// 用户分组编号示例1
private Long groupId;
}

View File

@@ -0,0 +1,48 @@
package com.tashow.cloud.member.controller.admin.user.vo;
import com.tashow.cloud.common.pojo.PageParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static com.tashow.cloud.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
// 管理后台 - 会员用户分页 Request VO
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MemberUserPageReqVO extends PageParam {
// 手机号示例15601691300
private String mobile;
// 用户昵称,示例:李四
private String nickname;
// 最后登录时间
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] loginDate;
// 创建时间
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
// 会员标签编号列表,示例:[1, 2]
private List<Long> tagIds;
// 会员等级编号示例1
private Long levelId;
// 用户分组编号示例1
private Long groupId;
// TODO 芋艿:注册用户类型;
// TODO 芋艿:登录用户类型;
}

View File

@@ -0,0 +1,51 @@
package com.tashow.cloud.member.controller.admin.user.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.List;
// 管理后台 - 会员用户 Response VO
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MemberUserRespVO extends MemberUserBaseVO {
// 编号必填示例23788
private Long id;
// 注册 IP必填示例127.0.0.1
private String registerIp;
// 最后登录IP必填示例127.0.0.1
private String loginIp;
// 最后登录时间,必填
private LocalDateTime loginDate;
// 创建时间,必填
private LocalDateTime createTime;
// ========== 其它信息 ==========
// 积分必填示例100
private Integer point;
// 总积分必填示例2000
private Integer totalPoint;
// 会员标签,示例:[红色, 快乐]
private List<String> tagNames;
// 会员等级,示例:黄金会员
private String levelName;
// 用户分组,示例:购物达人
private String groupName;
// 用户经验值必填示例200
private Integer experience;
}

View File

@@ -0,0 +1,28 @@
package com.tashow.cloud.member.controller.admin.user.vo;
import lombok.Data;
import lombok.ToString;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
// 管理后台 - 用户修改等级 Request VO
@Data
@ToString(callSuper = true)
public class MemberUserUpdateLevelReqVO {
// 用户编号必填示例23788
@NotNull(message = "用户编号不能为空")
private Long id;
/**
* 取消用户等级时,值为空
*/
// 用户等级编号示例1
private Long levelId;
// 修改原因,必填,示例:推广需要
@NotBlank(message = "修改原因不能为空")
private String reason;
}

View File

@@ -0,0 +1,21 @@
package com.tashow.cloud.member.controller.admin.user.vo;
import lombok.Data;
import lombok.ToString;
import jakarta.validation.constraints.NotNull;
// 管理后台 - 用户修改积分 Request VO
@Data
@ToString(callSuper = true)
public class MemberUserUpdatePointReqVO {
// 用户编号必填示例23788
@NotNull(message = "用户编号不能为空")
private Long id;
// 变动积分正数为增加负数为减少必填示例100
@NotNull(message = "变动积分不能为空")
private Integer point;
}

View File

@@ -0,0 +1,19 @@
package com.tashow.cloud.member.controller.admin.user.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import jakarta.validation.constraints.NotNull;
// 管理后台 - 会员用户更新 Request VO
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MemberUserUpdateReqVO extends MemberUserBaseVO {
// 编号必填示例23788
@NotNull(message = "编号不能为空")
private Long id;
}

View File

@@ -0,0 +1,54 @@
### 请求 /create 接口 => 成功
POST {{appApi}}//member/address/create
Content-Type: application/json
tenant-id: {{appTenantId}}
Authorization: Bearer {{appToken}}
{
"name": "yunai",
"mobile": "15601691300",
"areaId": "610632",
"postCode": "200000",
"detailAddress": "芋道源码 233 号 666 室",
"defaulted": true
}
### 请求 /update 接口 => 成功
PUT {{appApi}}//member/address/update
Content-Type: application/json
tenant-id: {{appTenantId}}
Authorization: Bearer {{appToken}}
{
"id": "1",
"name": "yunai888",
"mobile": "15601691300",
"areaId": "610632",
"postCode": "200000",
"detailAddress": "芋道源码 233 号 666 室",
"defaulted": false
}
### 请求 /delete 接口 => 成功
DELETE {{appApi}}//member/address/delete?id=2
Content-Type: application/json
tenant-id: {{appTenantId}}
Authorization: Bearer {{appToken}}
### 请求 /get 接口 => 成功
GET {{appApi}}//member/address/get?id=1
Content-Type: application/json
tenant-id: {{appTenantId}}
Authorization: Bearer {{appToken}}
### 请求 /get-default 接口 => 成功
GET {{appApi}}//member/address/get-default
Content-Type: application/json
tenant-id: {{appTenantId}}
Authorization: Bearer {{appToken}}
### 请求 /list 接口 => 成功
GET {{appApi}}//member/address/list
Content-Type: application/json
tenant-id: {{appTenantId}}
Authorization: Bearer {{appToken}}

View File

@@ -0,0 +1,95 @@
package com.tashow.cloud.member.controller.app.address;
import com.tashow.cloud.common.pojo.CommonResult;
import com.tashow.cloud.member.controller.app.address.vo.AppAddressCreateReqVO;
import com.tashow.cloud.member.controller.app.address.vo.AppAddressRespVO;
import com.tashow.cloud.member.controller.app.address.vo.AppAddressUpdateReqVO;
import com.tashow.cloud.member.convert.address.AddressConvert;
import com.tashow.cloud.member.dal.dataobject.address.MemberAddressDO;
import com.tashow.cloud.member.service.address.AddressService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.tashow.cloud.common.pojo.CommonResult.success;
import static com.tashow.cloud.security.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
* 用户 APP - 用户收件地址
*/
@RestController
@RequestMapping("/member/address")
@Validated
public class AppAddressController {
@Resource
private AddressService addressService;
/**
* 创建用户收件地址
* @param createReqVO
* @return
*/
@PostMapping("/create")
public CommonResult<Long> createAddress(@Valid @RequestBody AppAddressCreateReqVO createReqVO) {
return success(addressService.createAddress(getLoginUserId(), createReqVO));
}
/**
* 更新用户收件地址
* @param updateReqVO
* @return
*/
@PutMapping("/update")
public CommonResult<Boolean> updateAddress(@Valid @RequestBody AppAddressUpdateReqVO updateReqVO) {
addressService.updateAddress(getLoginUserId(), updateReqVO);
return success(true);
}
/**
* 删除用户收件地址
* @param id 编号
* @return
*/
@DeleteMapping("/delete")
public CommonResult<Boolean> deleteAddress(@RequestParam("id") Long id) {
addressService.deleteAddress(getLoginUserId(), id);
return success(true);
}
/**
* 获得用户收件地址
* @param id 编号
* @return
*/
@GetMapping("/get")
public CommonResult<AppAddressRespVO> getAddress(@RequestParam("id") Long id) {
MemberAddressDO address = addressService.getAddress(getLoginUserId(), id);
return success(AddressConvert.INSTANCE.convert(address));
}
/**
* 获得默认的用户收件地址
* @return
*/
@GetMapping("/get-default")
public CommonResult<AppAddressRespVO> getDefaultUserAddress() {
MemberAddressDO address = addressService.getDefaultUserAddress(getLoginUserId());
return success(AddressConvert.INSTANCE.convert(address));
}
/**
* 获得用户收件地址列表
* @return
*/
@GetMapping("/list")
public CommonResult<List<AppAddressRespVO>> getAddressList() {
List<MemberAddressDO> list = addressService.getAddressList(getLoginUserId());
return success(AddressConvert.INSTANCE.convertList(list));
}
}

View File

@@ -0,0 +1,32 @@
package com.tashow.cloud.member.controller.app.address.vo;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
/**
* 用户收件地址 Base VO提供给添加、修改、详细的子 VO 使用
*/
@Data
public class AppAddressBaseVO {
//收件人名称
@NotNull(message = "收件人名称不能为空")
private String name;
//手机号
@NotNull(message = "手机号不能为空")
private String mobile;
//地区编号
@NotNull(message = "地区编号不能为空")
private Long areaId;
//收件详细地址
@NotNull(message = "收件详细地址不能为空")
private String detailAddress;
//是否默认地址
@NotNull(message = "是否默认地址不能为空")
private Boolean defaultStatus;
}

View File

@@ -0,0 +1,15 @@
package com.tashow.cloud.member.controller.app.address.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 用户 APP - 用户收件地址创建 Request VO
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppAddressCreateReqVO extends AppAddressBaseVO {
}

View File

@@ -0,0 +1,21 @@
package com.tashow.cloud.member.controller.app.address.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 用户 APP - 用户收件地址 Response VO
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppAddressRespVO extends AppAddressBaseVO {
//编号
private Long id;
//地区名字
private String areaName;
}

View File

@@ -0,0 +1,20 @@
package com.tashow.cloud.member.controller.app.address.vo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 用户 APP - 用户收件地址更新 Request VO
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppAddressUpdateReqVO extends AppAddressBaseVO {
//编号
@NotNull(message = "编号不能为空")
private Long id;
}

View File

@@ -0,0 +1,67 @@
### 请求 /login 接口 => 成功
POST {{appApi}}/member/auth/login
Content-Type: application/json
tenant-id: {{appTenantId}}
{
"mobile": "15601691388",
"password": "admin123"
}
### 请求 /send-sms-code 接口 => 成功
POST {{appApi}}/member/auth/send-sms-code
Content-Type: application/json
tenant-id: {{appTenantId}}
{
"mobile": "15601691388",
"scene": 1
}
### 请求 /sms-login 接口 => 成功
POST {{appApi}}/member/auth/sms-login
Content-Type: application/json
tenant-id: {{appTenantId}}
terminal: 30
{
"mobile": "15601691388",
"code": 9999
}
### 请求 /social-login 接口 => 成功
POST {{appApi}}/member/auth/social-login
Content-Type: application/json
tenant-id: {{appTenantId}}
{
"type": 34,
"code": "0e1oc9000CTjFQ1oim200bhtb61oc90g",
"state": "default"
}
### 请求 /weixin-mini-app-login 接口 => 成功
POST {{appApi}}/member/auth/weixin-mini-app-login
Content-Type: application/json
tenant-id: {{appTenantId}}
{
"phoneCode": "618e6412e0c728f5b8fc7164497463d0158a923c9e7fd86af8bba393b9decbc5",
"loginCode": "001frTkl21JUf94VGxol2hSlff1frTkR"
}
### 请求 /logout 接口 => 成功
POST {{appApi}}/member/auth/logout
Content-Type: application/json
Authorization: Bearer c1b76bdaf2c146c581caa4d7fd81ee66
tenant-id: {{appTenantId}}
### 请求 /auth/refresh-token 接口 => 成功
POST {{appApi}}/member/auth/refresh-token?refreshToken=bc43d929094849a28b3a69f6e6940d70
Content-Type: application/json
tenant-id: {{appTenantId}}
### 请求 /auth/create-weixin-jsapi-signature 接口 => 成功
POST {{appApi}}/member/auth/create-weixin-jsapi-signature?url=http://www.iocoder.cn
Authorization: Bearer {{appToken}}
tenant-id: {{appTenantId}}

View File

@@ -0,0 +1,169 @@
package com.tashow.cloud.member.controller.app.auth;
import cn.hutool.core.util.StrUtil;
import com.tashow.cloud.common.enums.UserTypeEnum;
import com.tashow.cloud.common.pojo.CommonResult;
import com.tashow.cloud.member.controller.app.auth.vo.*;
import com.tashow.cloud.member.convert.auth.AuthConvert;
import com.tashow.cloud.member.service.auth.MemberAuthService;
import com.tashow.cloud.security.security.config.SecurityProperties;
import com.tashow.cloud.security.security.core.util.SecurityFrameworkUtils;
import com.tashow.cloud.systemapi.api.social.SocialClientApi;
import com.tashow.cloud.systemapi.api.social.dto.SocialWxJsapiSignatureRespDTO;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static com.tashow.cloud.common.pojo.CommonResult.success;
import static com.tashow.cloud.security.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
* 用户 APP - 认证
*/
@RestController
@RequestMapping("/member/auth")
@Validated
@Slf4j
public class AppAuthController {
@Resource
private MemberAuthService authService;
@Resource
private SocialClientApi socialClientApi;
@Resource
private SecurityProperties securityProperties;
/**
* 使用手机 + 密码登录
* @param reqVO
* @return
*/
@PostMapping("/login")
@PermitAll
public CommonResult<AppAuthLoginRespVO> login(@RequestBody @Valid AppAuthLoginReqVO reqVO) {
return success(authService.login(reqVO));
}
/**
* 登出系统
* @param request
* @return
*/
@PostMapping("/logout")
@PermitAll
public CommonResult<Boolean> logout(HttpServletRequest request) {
String token = SecurityFrameworkUtils.obtainAuthorization(request,
securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
if (StrUtil.isNotBlank(token)) {
authService.logout(token);
}
return success(true);
}
/**
* 刷新令牌
* @param refreshToken 刷新令牌
* @return
*/
@PostMapping("/refresh-token")
@PermitAll
public CommonResult<AppAuthLoginRespVO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
return success(authService.refreshToken(refreshToken));
}
// ========== 短信登录相关 ==========
/**
* 使用手机 + 验证码登录
* @param reqVO
* @return
*/
@PostMapping("/sms-login")
@PermitAll
public CommonResult<AppAuthLoginRespVO> smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) {
return success(authService.smsLogin(reqVO));
}
/**
* 发送手机验证码
* @param reqVO
* @return
*/
@PostMapping("/send-sms-code")
@PermitAll
public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid AppAuthSmsSendReqVO reqVO) {
authService.sendSmsCode(getLoginUserId(), reqVO);
return success(true);
}
/**
* 校验手机验证码
* @param reqVO
* @return
*/
@PostMapping("/validate-sms-code")
@PermitAll
public CommonResult<Boolean> validateSmsCode(@RequestBody @Valid AppAuthSmsValidateReqVO reqVO) {
authService.validateSmsCode(getLoginUserId(), reqVO);
return success(true);
}
// ========== 社交登录相关 ==========
/**
* 社交授权的跳转
* @param type 社交类型
* @param redirectUri 回调路径
* @return
*/
@GetMapping("/social-auth-redirect")
@PermitAll
public CommonResult<String> socialAuthRedirect(@RequestParam("type") Integer type,
@RequestParam("redirectUri") String redirectUri) {
return success(authService.getSocialAuthorizeUrl(type, redirectUri));
}
/**
* 社交快捷登录,使用 code 授权码
* @param reqVO
* @return
*/
@PostMapping("/social-login")
@PermitAll
public CommonResult<AppAuthLoginRespVO> socialLogin(@RequestBody @Valid AppAuthSocialLoginReqVO reqVO) {
return success(authService.socialLogin(reqVO));
}
/**
* 微信小程序的一键登录
* @param reqVO
* @return
*/
@PostMapping("/weixin-mini-app-login")
@PermitAll
public CommonResult<AppAuthLoginRespVO> weixinMiniAppLogin(@RequestBody @Valid AppAuthWeixinMiniAppLoginReqVO reqVO) {
return success(authService.weixinMiniAppLogin(reqVO));
}
/**
* 创建微信 JS SDK 初始化所需的签名
* @param url
* @return
*/
@PostMapping("/create-weixin-jsapi-signature")
@PermitAll
public CommonResult<SocialWxJsapiSignatureRespDTO> createWeixinMpJsapiSignature(@RequestParam("url") String url) {
SocialWxJsapiSignatureRespDTO signature = socialClientApi.createWxMpJsapiSignature(
UserTypeEnum.MEMBER.getValue(), url).getCheckedData();
return success(AuthConvert.INSTANCE.convert(signature));
}
}

View File

@@ -0,0 +1,40 @@
package com.tashow.cloud.member.controller.app.auth.vo;
import com.tashow.cloud.common.validation.InEnum;
import com.tashow.cloud.common.validation.Mobile;
import com.tashow.cloud.systemapi.enums.sms.SmsSceneEnum;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
/**
* 用户 APP - 校验验证码 Request VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthCheckCodeReqVO {
//手机号
@NotBlank(message = "手机号不能为空")
@Mobile
private String mobile;
//手机验证码
@NotBlank(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
//发送场景,对应 SmsSceneEnum 枚举
@NotNull(message = "发送场景不能为空")
@InEnum(SmsSceneEnum.class)
private Integer scene;
}

View File

@@ -0,0 +1,56 @@
package com.tashow.cloud.member.controller.app.auth.vo;
import cn.hutool.core.util.StrUtil;
import com.tashow.cloud.common.validation.InEnum;
import com.tashow.cloud.common.validation.Mobile;
import com.tashow.cloud.systemapi.enums.social.SocialTypeEnum;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
/**
* 用户 APP - 手机 + 密码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthLoginReqVO {
//手机号
@NotEmpty(message = "手机号不能为空")
@Mobile
private String mobile;
//密码
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
// ========== 绑定社交登录时,需要传递如下参数 ==========
//社交平台的类型,参见 SocialTypeEnum 枚举值
@InEnum(SocialTypeEnum.class)
private Integer socialType;
//授权码
private String socialCode;
//state
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,38 @@
package com.tashow.cloud.member.controller.app.auth.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 用户 APP - 登录 Response VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthLoginRespVO {
//用户编号
private Long userId;
//访问令牌
private String accessToken;
//刷新令牌
private String refreshToken;
//过期时间
private LocalDateTime expiresTime;
/**
* 社交用户
* 仅社交登录、社交绑定时会返回
* 为什么需要返回?微信公众号、微信小程序支付需要传递 openid 给支付接口
*/
private String openid;
}

View File

@@ -0,0 +1,58 @@
package com.tashow.cloud.member.controller.app.auth.vo;
import cn.hutool.core.util.StrUtil;
import com.tashow.cloud.common.validation.InEnum;
import com.tashow.cloud.common.validation.Mobile;
import com.tashow.cloud.systemapi.enums.social.SocialTypeEnum;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
/**
* 用户 APP - 手机 + 验证码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthSmsLoginReqVO {
//手机号
@NotEmpty(message = "手机号不能为空")
@Mobile
private String mobile;
//手机验证码
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
// ========== 绑定社交登录时,需要传递如下参数 ==========
//社交平台的类型,参见 SocialTypeEnum 枚举值
@InEnum(SocialTypeEnum.class)
private Integer socialType;
//授权码
private String socialCode;
//state
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 com.tashow.cloud.member.controller.app.auth.vo;
import com.tashow.cloud.common.validation.InEnum;
import com.tashow.cloud.common.validation.Mobile;
import com.tashow.cloud.systemapi.enums.sms.SmsSceneEnum;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 用户 APP - 发送手机验证码 Request VO
*/
@Data
@Accessors(chain = true)
public class AppAuthSmsSendReqVO {
//手机号
@Mobile
private String mobile;
//发送场景,对应 SmsSceneEnum 枚举
@NotNull(message = "发送场景不能为空")
@InEnum(SmsSceneEnum.class)
private Integer scene;
}

View File

@@ -0,0 +1,33 @@
package com.tashow.cloud.member.controller.app.auth.vo;
import com.tashow.cloud.common.validation.InEnum;
import com.tashow.cloud.common.validation.Mobile;
import com.tashow.cloud.systemapi.enums.sms.SmsSceneEnum;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.Length;
//用户 APP - 校验手机验证码 Request VO
@Data
@Accessors(chain = true)
public class AppAuthSmsValidateReqVO {
//手机号
@Mobile
private String mobile;
//发送场景,对应 SmsSceneEnum 枚举
@NotNull(message = "发送场景不能为空")
@InEnum(SmsSceneEnum.class)
private Integer scene;
//手机验证码
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
}

View File

@@ -0,0 +1,34 @@
package com.tashow.cloud.member.controller.app.auth.vo;
import com.tashow.cloud.common.validation.InEnum;
import com.tashow.cloud.systemapi.enums.social.SocialTypeEnum;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户 APP - 社交快捷登录 Request VO使用 code 授权码
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthSocialLoginReqVO {
//社交平台的类型,参见 SocialTypeEnum 枚举值
@InEnum(SocialTypeEnum.class)
@NotNull(message = "社交平台的类型不能为空")
private Integer type;
//授权码
@NotEmpty(message = "授权码不能为空")
private String code;
//state
@NotEmpty(message = "state 不能为空")
private String state;
}

View File

@@ -0,0 +1,36 @@
package com.tashow.cloud.member.controller.app.auth.vo;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户 APP - 微信小程序手机登录 Request VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppAuthWeixinMiniAppLoginReqVO {
/**
* 手机 code小程序通过 wx.getPhoneNumber 方法获得
*/
@NotEmpty(message = "手机 code 不能为空")
private String phoneCode;
/**
* 登录 code小程序通过 wx.login 方法获得
*/
@NotEmpty(message = "登录 code 不能为空")
private String loginCode;
/**
* state
*/
@NotEmpty(message = "state 不能为空")
private String state;
}

View File

@@ -0,0 +1,32 @@
package com.tashow.cloud.member.controller.app.auth.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户 APP - 微信公众号 JSAPI 签名 Response VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthWeixinJsapiSignatureRespVO {
//微信公众号的 appId
private String appId;
//匿名串
private String nonceStr;
//时间戳
private Long timestamp;
//URL
private String url;
//签名
private String signature;
}

View File

@@ -0,0 +1,4 @@
### 请求 /member/user/profile/get 接口 => 没有权限
GET {{appApi}}/member/user/get
Authorization: Bearer test245
tenant-id: {{appTenantId}}

View File

@@ -0,0 +1,74 @@
package com.tashow.cloud.member.controller.app.user;
import com.tashow.cloud.common.pojo.CommonResult;
import com.tashow.cloud.member.controller.app.user.vo.*;
import com.tashow.cloud.member.convert.user.MemberUserConvert;
import com.tashow.cloud.member.dal.dataobject.user.MemberUserDO;
import com.tashow.cloud.member.service.user.MemberUserService;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static com.tashow.cloud.common.pojo.CommonResult.success;
import static com.tashow.cloud.security.security.core.util.SecurityFrameworkUtils.getLoginUserId;
// 用户 APP - 用户个人中心
@RestController
@RequestMapping("/member/user")
@Validated
@Slf4j
public class AppMemberUserController {
@Resource
private MemberUserService userService;
@GetMapping("/get")
// 获得基本信息
public CommonResult<AppMemberUserInfoRespVO> getUserInfo() {
MemberUserDO user = userService.getUser(getLoginUserId());
return success(MemberUserConvert.INSTANCE.convert(user));
}
@PutMapping("/update")
// 修改基本信息
public CommonResult<Boolean> updateUser(@RequestBody @Valid AppMemberUserUpdateReqVO reqVO) {
userService.updateUser(getLoginUserId(), reqVO);
return success(true);
}
@PutMapping("/update-mobile")
// 修改用户手机
public CommonResult<Boolean> updateUserMobile(@RequestBody @Valid AppMemberUserUpdateMobileReqVO reqVO) {
userService.updateUserMobile(getLoginUserId(), reqVO);
return success(true);
}
@PutMapping("/update-mobile-by-weixin")
// 基于微信小程序的授权码,修改用户手机
public CommonResult<Boolean> updateUserMobileByWeixin(@RequestBody @Valid AppMemberUserUpdateMobileByWeixinReqVO reqVO) {
userService.updateUserMobileByWeixin(getLoginUserId(), reqVO);
return success(true);
}
@PutMapping("/update-password")
// 修改用户密码
// 用户修改密码时使用
public CommonResult<Boolean> updateUserPassword(@RequestBody @Valid AppMemberUserUpdatePasswordReqVO reqVO) {
userService.updateUserPassword(getLoginUserId(), reqVO);
return success(true);
}
@PutMapping("/reset-password")
// 重置密码
// 用户忘记密码时使用
@PermitAll
public CommonResult<Boolean> resetUserPassword(@RequestBody @Valid AppMemberUserResetPasswordReqVO reqVO) {
userService.resetUserPassword(reqVO);
return success(true);
}
}

View File

@@ -0,0 +1,51 @@
package com.tashow.cloud.member.controller.app.user.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户 APP - 用户个人信息 Response VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppMemberUserInfoRespVO {
//用户编号
private Long id;
//用户昵称
private String nickname;
//用户头像
private String avatar;
//用户手机号
private String mobile;
//用户性别
private Integer sex;
//积分
private Integer point;
//经验值
private Integer experience;
/// 用户等级
private Level level;
//是否成为推广员
private Boolean brokerageEnabled;
/**
* 用户 App - 会员等级
*/
@Data
public static class Level {
//等级编号
private Long id;
//等级名称
private String name;
// 等级
private Integer level;
//等级图标
private String icon;
}
}

View File

@@ -0,0 +1,37 @@
package com.tashow.cloud.member.controller.app.user.vo;
import com.tashow.cloud.common.validation.Mobile;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
// 用户 APP - 重置密码 Request VO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppMemberUserResetPasswordReqVO {
// 新密码, 必需, 示例: buzhidao
@NotEmpty(message = "新密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
// 手机验证码, 必需, 示例: 1024
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
// 手机号, 必需, 示例: 15878962356
@NotBlank(message = "手机号不能为空")
@Mobile
private String mobile;
}

View File

@@ -0,0 +1,14 @@
package com.tashow.cloud.member.controller.app.user.vo;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
// 用户 APP - 基于微信小程序的授权码,修改手机 Request VO
@Data
public class AppMemberUserUpdateMobileByWeixinReqVO {
// 手机 code小程序通过 wx.getPhoneNumber 方法获得, 必需, 示例: hello
@NotEmpty(message = "手机 code 不能为空")
private String code;
}

View File

@@ -0,0 +1,31 @@
package com.tashow.cloud.member.controller.app.user.vo;
import com.tashow.cloud.common.validation.Mobile;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
// 用户 APP - 修改手机 Request VO
@Data
public class AppMemberUserUpdateMobileReqVO {
// 手机验证码, 必需, 示例: 1024
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
// 手机号, 必需, 示例: 15823654487
@NotBlank(message = "手机号不能为空")
@Length(min = 8, max = 11, message = "手机号码长度为 8-11 位")
@Mobile
private String mobile;
// 原手机验证码, 示例: 1024
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String oldCode;
}

View File

@@ -0,0 +1,30 @@
package com.tashow.cloud.member.controller.app.user.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
// 用户 APP - 修改密码 Request VO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AppMemberUserUpdatePasswordReqVO {
// 新密码, 必需, 示例: buzhidao
@NotEmpty(message = "新密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
// 手机验证码, 必需, 示例: 1024
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
}

View File

@@ -0,0 +1,20 @@
package com.tashow.cloud.member.controller.app.user.vo;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
// 用户 App - 会员用户更新 Request VO
@Data
public class AppMemberUserUpdateReqVO {
// 用户昵称, 必需, 示例: 李四
private String nickname;
// 头像, 必需, 示例: https://www.iocoder.cn/x.png
@URL(message = "头像必须是 URL 格式")
private String avatar;
// 性别, 必需, 示例: 1
private Integer sex;
}

View File

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

View File

@@ -0,0 +1,45 @@
package com.tashow.cloud.member.convert.address;
import com.tashow.cloud.common.util.ip.AreaUtils;
import com.tashow.cloud.memberapi.api.address.dto.MemberAddressRespDTO;
import com.tashow.cloud.member.controller.admin.address.vo.AddressRespVO;
import com.tashow.cloud.member.controller.app.address.vo.AppAddressCreateReqVO;
import com.tashow.cloud.member.controller.app.address.vo.AppAddressRespVO;
import com.tashow.cloud.member.controller.app.address.vo.AppAddressUpdateReqVO;
import com.tashow.cloud.member.dal.dataobject.address.MemberAddressDO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 用户收件地址 Convert
*
* @author 芋道源码
*/
@Mapper
public interface AddressConvert {
AddressConvert INSTANCE = Mappers.getMapper(AddressConvert.class);
MemberAddressDO convert(AppAddressCreateReqVO bean);
MemberAddressDO convert(AppAddressUpdateReqVO bean);
@Mapping(source = "areaId", target = "areaName", qualifiedByName = "convertAreaIdToAreaName")
AppAddressRespVO convert(MemberAddressDO bean);
List<AppAddressRespVO> convertList(List<MemberAddressDO> list);
MemberAddressRespDTO convert02(MemberAddressDO bean);
@Named("convertAreaIdToAreaName")
default String convertAreaIdToAreaName(Integer areaId) {
return AreaUtils.format(areaId);
}
List<AddressRespVO> convertList2(List<MemberAddressDO> list);
}

View File

@@ -0,0 +1,34 @@
package com.tashow.cloud.member.convert.auth;
import com.tashow.cloud.member.controller.app.auth.vo.*;
import com.tashow.cloud.member.controller.app.user.vo.AppMemberUserResetPasswordReqVO;
import com.tashow.cloud.systemapi.api.oauth2.dto.OAuth2AccessTokenRespDTO;
import com.tashow.cloud.systemapi.api.sms.dto.code.SmsCodeSendReqDTO;
import com.tashow.cloud.systemapi.api.sms.dto.code.SmsCodeUseReqDTO;
import com.tashow.cloud.systemapi.api.sms.dto.code.SmsCodeValidateReqDTO;
import com.tashow.cloud.systemapi.api.social.dto.SocialUserBindReqDTO;
import com.tashow.cloud.systemapi.api.social.dto.SocialUserUnbindReqDTO;
import com.tashow.cloud.systemapi.api.social.dto.SocialWxJsapiSignatureRespDTO;
import com.tashow.cloud.systemapi.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);
SmsCodeSendReqDTO convert(AppAuthSmsSendReqVO reqVO);
SmsCodeUseReqDTO convert(AppMemberUserResetPasswordReqVO reqVO, SmsSceneEnum scene, String usedIp);
SmsCodeUseReqDTO convert(AppAuthSmsLoginReqVO reqVO, Integer scene, String usedIp);
AppAuthLoginRespVO convert(OAuth2AccessTokenRespDTO bean, String openid);
SmsCodeValidateReqDTO convert(AppAuthSmsValidateReqVO bean);
SocialWxJsapiSignatureRespDTO convert(SocialWxJsapiSignatureRespDTO bean);
}

View File

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

View File

@@ -0,0 +1,41 @@
package com.tashow.cloud.member.convert.user;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.memberapi.api.user.dto.MemberUserRespDTO;
import com.tashow.cloud.member.controller.admin.user.vo.MemberUserRespVO;
import com.tashow.cloud.member.controller.admin.user.vo.MemberUserUpdateReqVO;
import com.tashow.cloud.member.controller.app.user.vo.AppMemberUserInfoRespVO;
import com.tashow.cloud.member.convert.address.AddressConvert;
import com.tashow.cloud.member.dal.dataobject.user.MemberUserDO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper(uses = {AddressConvert.class})
public interface MemberUserConvert {
MemberUserConvert INSTANCE = Mappers.getMapper(MemberUserConvert.class);
AppMemberUserInfoRespVO convert(MemberUserDO bean);
@Mappings({
@Mapping(source = "bean.id", target = "id"),
})
MemberUserRespDTO convert2(MemberUserDO bean);
List<MemberUserRespDTO> convertList2(List<MemberUserDO> list);
MemberUserDO convert(MemberUserUpdateReqVO bean);
PageResult<MemberUserRespVO> convertPage(PageResult<MemberUserDO> page);
@Mapping(source = "areaId", target = "areaName", qualifiedByName = "convertAreaIdToAreaName")
MemberUserRespVO convert03(MemberUserDO bean);
}

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao>

View File

@@ -0,0 +1,55 @@
package com.tashow.cloud.member.dal.dataobject.address;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tashow.cloud.mybatis.mybatis.core.dataobject.BaseDO;
import lombok.*;
/**
* 用户收件地址 DO
*
*/
@TableName("member_address")
@KeySequence("member_address_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberAddressDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 用户编号
*/
private Long userId;
/**
* 收件人名称
*/
private String name;
/**
* 手机号
*/
private String mobile;
/**
* 地区编号
*/
private Long areaId;
/**
* 收件详细地址
*/
private String detailAddress;
/**
* 是否默认
*
* true - 默认收件地址
*/
private Boolean defaultStatus;
}

View File

@@ -0,0 +1,139 @@
package com.tashow.cloud.member.dal.dataobject.user;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.tashow.cloud.common.enums.CommonStatusEnum;
import com.tashow.cloud.common.enums.TerminalEnum;
import com.tashow.cloud.mybatis.mybatis.core.type.LongListTypeHandler;
import com.tashow.cloud.systemapi.enums.common.SexEnum;
import com.tashow.cloud.tenant.core.db.TenantBaseDO;
import lombok.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.time.LocalDateTime;
import java.util.List;
/**
* 会员用户 DO
*
* uk_mobile 索引:基于 {@link #mobile} 字段
*
*/
@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 mobile;
/**
* 加密后的密码
*
* 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐
*/
private String password;
/**
* 帐号状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 注册 IP
*/
private String registerIp;
/**
* 注册终端
* 枚举 {@link TerminalEnum}
*/
private Integer registerTerminal;
/**
* 最后登录IP
*/
private String loginIp;
/**
* 最后登录时间
*/
private LocalDateTime loginDate;
// ========== 基础信息 ==========
/**
* 用户昵称
*/
private String nickname;
/**
* 用户头像
*/
private String avatar;
/**
* 真实名字
*/
private String name;
/**
* 性别
*
* 枚举 {@link SexEnum}
*/
private Integer sex;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 所在地
*
* 关联 {@link Area#getId()} 字段
*/
private Integer areaId;
/**
* 用户备注
*/
private String mark;
// ========== 其它信息 ==========
/**
* 积分
*/
private Integer point;
// TODO 疯狂:增加一个 totalPoint个人信息接口要返回
/**
* 会员标签列表,以逗号分隔
*/
@TableField(typeHandler = LongListTypeHandler.class)
private List<Long> tagIds;
/**
* 会员级别编号
*
* 关联 {@link MemberLevelDO#getId()} 字段
*/
private Long levelId;
/**
* 会员经验
*/
private Integer experience;
/**
* 用户分组编号
*
* 关联 {@link MemberGroupDO#getId()} 字段
*/
private Long groupId;
}

View File

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

View File

@@ -0,0 +1,96 @@
package com.tashow.cloud.member.dal.mysql.user;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.member.controller.admin.user.vo.MemberUserPageReqVO;
import com.tashow.cloud.member.dal.dataobject.user.MemberUserDO;
import com.tashow.cloud.mybatis.mybatis.core.mapper.BaseMapperX;
import com.tashow.cloud.mybatis.mybatis.core.query.LambdaQueryWrapperX;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.stream.Collectors;
/**
* 会员 User Mapper
*
* @author 芋道源码
*/
@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));
}
default PageResult<MemberUserDO> selectPage(MemberUserPageReqVO reqVO) {
// 处理 tagIds 过滤条件
String tagIdSql = "";
if (CollUtil.isNotEmpty(reqVO.getTagIds())) {
tagIdSql = reqVO.getTagIds().stream()
.map(tagId -> "FIND_IN_SET(" + tagId + ", tag_ids)")
.collect(Collectors.joining(" OR "));
}
// 分页查询
return selectPage(reqVO, new LambdaQueryWrapperX<MemberUserDO>()
.likeIfPresent(MemberUserDO::getMobile, reqVO.getMobile())
.betweenIfPresent(MemberUserDO::getLoginDate, reqVO.getLoginDate())
.likeIfPresent(MemberUserDO::getNickname, reqVO.getNickname())
.betweenIfPresent(MemberUserDO::getCreateTime, reqVO.getCreateTime())
.eqIfPresent(MemberUserDO::getLevelId, reqVO.getLevelId())
.eqIfPresent(MemberUserDO::getGroupId, reqVO.getGroupId())
.apply(StrUtil.isNotEmpty(tagIdSql), tagIdSql)
.orderByDesc(MemberUserDO::getId));
}
default Long selectCountByGroupId(Long groupId) {
return selectCount(MemberUserDO::getGroupId, groupId);
}
default Long selectCountByLevelId(Long levelId) {
return selectCount(MemberUserDO::getLevelId, levelId);
}
default Long selectCountByTagId(Long tagId) {
return selectCount(new LambdaQueryWrapperX<MemberUserDO>()
.apply("FIND_IN_SET({0}, tag_ids)", tagId));
}
/**
* 更新用户积分(增加)
*
* @param id 用户编号
* @param incrCount 增加积分(正数)
*/
default void updatePointIncr(Long id, Integer incrCount) {
Assert.isTrue(incrCount > 0);
LambdaUpdateWrapper<MemberUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<MemberUserDO>()
.setSql(" point = point + " + incrCount)
.eq(MemberUserDO::getId, id);
update(null, lambdaUpdateWrapper);
}
/**
* 更新用户积分(减少)
*
* @param id 用户编号
* @param incrCount 增加积分(负数)
* @return 更新行数
*/
default int updatePointDecr(Long id, Integer incrCount) {
Assert.isTrue(incrCount < 0);
LambdaUpdateWrapper<MemberUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<MemberUserDO>()
.setSql(" point = point + " + incrCount) // 负数,所以使用 + 号
.eq(MemberUserDO::getId, id);
return update(null, lambdaUpdateWrapper);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
/**
* 属于 member 模块的 framework 封装
*
* @author 芋道源码
*/
package com.tashow.cloud.member.framework;

View File

@@ -0,0 +1,13 @@
package com.tashow.cloud.member.framework.rpc.config;
import com.tashow.cloud.systemapi.api.logger.LoginLogApi;
import com.tashow.cloud.systemapi.api.sms.SmsCodeApi;
import com.tashow.cloud.systemapi.api.social.SocialClientApi;
import com.tashow.cloud.systemapi.api.social.SocialUserApi;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@EnableFeignClients(clients = {SmsCodeApi.class, LoginLogApi.class, SocialUserApi.class, SocialClientApi.class})
public class RpcConfiguration {
}

View File

@@ -0,0 +1,4 @@
/**
* 占位
*/
package com.tashow.cloud.member.framework.rpc;

View File

@@ -0,0 +1,39 @@
package com.tashow.cloud.member.framework.security.config;
import com.tashow.cloud.memberapi.enums.ApiConstants;
import com.tashow.cloud.security.security.config.AuthorizeRequestsCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
/**
* Member 模块的 Security 配置
*/
@Configuration("memberSecurityConfiguration")
public class SecurityConfiguration {
@Bean("memberAuthorizeRequestsCustomizer")
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
return new AuthorizeRequestsCustomizer() {
@Override
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
// Swagger 接口文档
registry.requestMatchers("/v3/api-docs/**").permitAll()
.requestMatchers("/webjars/**").permitAll()
.requestMatchers("/swagger-ui").permitAll()
.requestMatchers("/swagger-ui/**").permitAll();
// Spring Boot Actuator 的安全配置
registry.requestMatchers("/actuator").permitAll()
.requestMatchers("/actuator/**").permitAll();
// Druid 监控
registry.requestMatchers("/druid/**").permitAll();
// RPC 服务的安全配置
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
}
};
}
}

View File

@@ -0,0 +1,4 @@
/**
* 占位
*/
package com.tashow.cloud.member.framework.security.core;

View File

@@ -0,0 +1,4 @@
/**
* 消息队列的消费者
*/
package com.tashow.cloud.member.mq.consumer;

View File

@@ -0,0 +1,4 @@
/**
* 消息队列的消息
*/
package com.tashow.cloud.member.mq.message;

View File

@@ -0,0 +1,4 @@
/**
* 消息队列的生产者
*/
package com.tashow.cloud.member.mq.producer;

View File

@@ -0,0 +1,31 @@
package com.tashow.cloud.member.mq.producer.user;
import com.tashow.cloud.memberapi.message.user.MemberUserCreateMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
/**
* 会员用户 Producer
*
* @author owen
*/
@Slf4j
@Component
public class MemberUserProducer {
@Resource
private ApplicationContext applicationContext;
/**
* 发送 {@link MemberUserCreateMessage} 消息
*
* @param userId 用户编号
*/
public void sendUserCreateMessage(Long userId) {
applicationContext.publishEvent(new MemberUserCreateMessage().setUserId(userId));
}
}

View File

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

View File

@@ -0,0 +1,67 @@
package com.tashow.cloud.member.service.address;
import com.tashow.cloud.member.controller.app.address.vo.AppAddressCreateReqVO;
import com.tashow.cloud.member.controller.app.address.vo.AppAddressUpdateReqVO;
import com.tashow.cloud.member.dal.dataobject.address.MemberAddressDO;
import jakarta.validation.Valid;
import java.util.List;
/**
* 用户收件地址 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 用户收件地址
*/
MemberAddressDO getAddress(Long userId, Long id);
/**
* 获得用户收件地址列表
*
* @param userId 用户编号
* @return 用户收件地址列表
*/
List<MemberAddressDO> getAddressList(Long userId);
/**
* 获得用户默认的收件地址
*
* @param userId 用户编号
* @return 用户收件地址
*/
MemberAddressDO getDefaultUserAddress(Long userId);
}

View File

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

View File

@@ -0,0 +1,88 @@
package com.tashow.cloud.member.service.auth;
import com.tashow.cloud.member.controller.app.auth.vo.*;
import jakarta.validation.Valid;
/**
* 会员的认证 Service 接口
*
* 提供用户的账号密码登录、token 的校验等认证相关的功能
*
* @author 芋道源码
*/
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 用户编号
* @param reqVO 发送信息
*/
void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO);
/**
* 校验短信验证码是否正确
*
* @param userId 用户编号
* @param reqVO 校验信息
*/
void validateSmsCode(Long userId, AppAuthSmsValidateReqVO reqVO);
/**
* 刷新访问令牌
*
* @param refreshToken 刷新令牌
* @return 登录结果
*/
AppAuthLoginRespVO refreshToken(String refreshToken);
}

View File

@@ -0,0 +1,287 @@
package com.tashow.cloud.member.service.auth;
import cn.hutool.core.lang.Assert;
import com.tashow.cloud.common.enums.CommonStatusEnum;
import com.tashow.cloud.common.enums.TerminalEnum;
import com.tashow.cloud.common.enums.UserTypeEnum;
import com.tashow.cloud.common.util.monitor.TracerUtils;
import com.tashow.cloud.common.util.servlet.ServletUtils;
import com.tashow.cloud.member.controller.app.auth.vo.*;
import com.tashow.cloud.member.convert.auth.AuthConvert;
import com.tashow.cloud.member.dal.dataobject.user.MemberUserDO;
import com.tashow.cloud.member.service.user.MemberUserService;
import com.tashow.cloud.systemapi.api.logger.LoginLogApi;
import com.tashow.cloud.systemapi.api.logger.dto.LoginLogCreateReqDTO;
import com.tashow.cloud.systemapi.api.oauth2.OAuth2TokenApi;
import com.tashow.cloud.systemapi.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
import com.tashow.cloud.systemapi.api.oauth2.dto.OAuth2AccessTokenRespDTO;
import com.tashow.cloud.systemapi.api.sms.SmsCodeApi;
import com.tashow.cloud.systemapi.api.social.SocialClientApi;
import com.tashow.cloud.systemapi.api.social.SocialUserApi;
import com.tashow.cloud.systemapi.api.social.dto.SocialUserBindReqDTO;
import com.tashow.cloud.systemapi.api.social.dto.SocialUserRespDTO;
import com.tashow.cloud.systemapi.api.social.dto.SocialWxPhoneNumberInfoRespDTO;
import com.tashow.cloud.systemapi.enums.logger.LoginLogTypeEnum;
import com.tashow.cloud.systemapi.enums.logger.LoginResultEnum;
import com.tashow.cloud.systemapi.enums.oauth2.OAuth2ClientConstants;
import com.tashow.cloud.systemapi.enums.sms.SmsSceneEnum;
import com.tashow.cloud.systemapi.enums.social.SocialTypeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
import static com.tashow.cloud.common.exception.util.ServiceExceptionUtil.exception;
import static com.tashow.cloud.common.util.servlet.ServletUtils.getClientIP;
import static com.tashow.cloud.memberapi.enums.ErrorCodeConstants.AUTH_MOBILE_USED;
import static com.tashow.cloud.memberapi.enums.ErrorCodeConstants.AUTH_SOCIAL_USER_NOT_FOUND;
import static com.tashow.cloud.systemapi.enums.ErrorCodeConstants.*;
import static com.tashow.cloud.web.web.core.util.WebFrameworkUtils.getTerminal;
/**
* 会员的认证 Service 接口
*
* @author 芋道源码
*/
@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 SocialClientApi socialClientApi;
@Resource
private OAuth2TokenApi oauth2TokenApi;
@Override
public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) {
// 使用手机 + 密码,进行登录。
MemberUserDO user = login0(reqVO.getMobile(), reqVO.getPassword());
// 如果 socialType 非空,说明需要绑定社交用户
String openid = null;
if (reqVO.getSocialType() != null) {
openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())).getCheckedData();
}
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE, openid);
}
@Override
@Transactional
public AppAuthLoginRespVO smsLogin(AppAuthSmsLoginReqVO reqVO) {
// 校验验证码
String userIp = getClientIP();
smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp)).checkError();
// 获得获得注册用户
MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), userIp, getTerminal());
Assert.notNull(user, "获取用户失败,结果为空");
// 校验是否禁用
if (CommonStatusEnum.isDisable(user.getStatus())) {
createLoginLog(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS, LoginResultEnum.USER_DISABLED);
throw exception(AUTH_LOGIN_USER_DISABLED);
}
// 如果 socialType 非空,说明需要绑定社交用户
String openid = null;
if (reqVO.getSocialType() != null) {
openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())).getCheckedData();
}
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS, openid);
}
@Override
@Transactional
public AppAuthLoginRespVO socialLogin(AppAuthSocialLoginReqVO reqVO) {
// 使用 code 授权码,进行登录。然后,获得到绑定的用户编号
SocialUserRespDTO socialUser = socialUserApi.getSocialUserByCode(UserTypeEnum.MEMBER.getValue(), reqVO.getType(),
reqVO.getCode(), reqVO.getState()).getCheckedData();
if (socialUser == null) {
throw exception(AUTH_SOCIAL_USER_NOT_FOUND);
}
// 情况一:已绑定,直接读取用户信息
MemberUserDO user;
if (socialUser.getUserId() != null) {
user = userService.getUser(socialUser.getUserId());
// 情况二:未绑定,注册用户 + 绑定用户
} else {
user = userService.createUser(socialUser.getNickname(), socialUser.getAvatar(), getClientIP(), getTerminal());
socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
reqVO.getType(), reqVO.getCode(), reqVO.getState())).checkError();
}
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, socialUser.getOpenid());
}
@Override
public AppAuthLoginRespVO weixinMiniAppLogin(AppAuthWeixinMiniAppLoginReqVO reqVO) {
// 获得对应的手机号信息
SocialWxPhoneNumberInfoRespDTO phoneNumberInfo = socialClientApi.getWxMaPhoneNumberInfo(
UserTypeEnum.MEMBER.getValue(), reqVO.getPhoneCode()).getCheckedData();
Assert.notNull(phoneNumberInfo, "获得手机信息失败,结果为空");
// 获得获得注册用户
MemberUserDO user = userService.createUserIfAbsent(phoneNumberInfo.getPurePhoneNumber(),
getClientIP(), TerminalEnum.WECHAT_MINI_PROGRAM.getTerminal());
Assert.notNull(user, "获取用户失败,结果为空");
// 绑定社交用户
String openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
SocialTypeEnum.WECHAT_MINI_APP.getType(), reqVO.getLoginCode(), reqVO.getState())).getCheckedData();
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, openid);
}
private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile,
LoginLogTypeEnum logType, String openid) {
// 插入登陆日志
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)).getCheckedData();
// 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenRespDTO, openid);
}
@Override
public String getSocialAuthorizeUrl(Integer type, String redirectUri) {
return socialClientApi.getAuthorizeUrl(type, UserTypeEnum.MEMBER.getValue(), redirectUri).getCheckedData();
}
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 (CommonStatusEnum.isDisable(user.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).checkError();
// 更新最后登录时间
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).getCheckedData();
if (accessTokenRespDTO == null) {
return;
}
// 删除成功,则记录登出日志
createLogoutLog(accessTokenRespDTO.getUserId());
}
@Override
public void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO) {
// 情况 1如果是修改手机场景需要校验新手机号是否已经注册说明不能使用该手机了
if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene())) {
MemberUserDO user = userService.getUserByMobile(reqVO.getMobile());
if (user != null && !Objects.equals(user.getId(), userId)) {
throw exception(AUTH_MOBILE_USED);
}
}
// 情况 2如果是重置密码场景需要校验手机号是存在的
if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_RESET_PASSWORD.getScene())) {
MemberUserDO user = userService.getUserByMobile(reqVO.getMobile());
if (user == null) {
throw exception(USER_MOBILE_NOT_EXISTS);
}
}
// 情况 3如果是修改密码场景需要查询手机号无需前端传递
if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_UPDATE_PASSWORD.getScene())) {
MemberUserDO user = userService.getUser(userId);
// TODO 芋艿:后续 member user 手机非强绑定,这块需要做下调整;
reqVO.setMobile(user.getMobile());
}
// 执行发送
smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())).checkError();
}
@Override
public void validateSmsCode(Long userId, AppAuthSmsValidateReqVO reqVO) {
smsCodeApi.validateSmsCode(AuthConvert.INSTANCE.convert(reqVO));
}
@Override
public AppAuthLoginRespVO refreshToken(String refreshToken) {
OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken,
OAuth2ClientConstants.CLIENT_ID_DEFAULT).getCheckedData();
return AuthConvert.INSTANCE.convert(accessTokenDO, null);
}
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).checkError();
}
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,190 @@
package com.tashow.cloud.member.service.user;
import com.tashow.cloud.common.enums.TerminalEnum;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.validation.Mobile;
import com.tashow.cloud.member.controller.admin.user.vo.MemberUserPageReqVO;
import com.tashow.cloud.member.controller.admin.user.vo.MemberUserUpdateReqVO;
import com.tashow.cloud.member.controller.app.user.vo.*;
import com.tashow.cloud.member.dal.dataobject.user.MemberUserDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* 会员用户 Service 接口
*
* @author 芋道源码
*/
public interface MemberUserService {
/**
* 通过手机查询用户
*
* @param mobile 手机
* @return 用户对象
*/
MemberUserDO getUserByMobile(String mobile);
/**
* 基于用户昵称,模糊匹配用户列表
*
* @param nickname 用户昵称,模糊匹配
* @return 用户信息的列表
*/
List<MemberUserDO> getUserListByNickname(String nickname);
/**
* 基于手机号创建用户。
* 如果用户已经存在,则直接进行返回
*
* @param mobile 手机号
* @param registerIp 注册 IP
* @param terminal 终端 {@link TerminalEnum}
* @return 用户对象
*/
MemberUserDO createUserIfAbsent(@Mobile String mobile, String registerIp, Integer terminal);
/**
* 创建用户
* 目的:三方登录时,如果未绑定用户时,自动创建对应用户
*
* @param nickname 昵称
* @param avtar 头像
* @param registerIp 注册 IP
* @param terminal 终端 {@link TerminalEnum}
* @return 用户对象
*/
MemberUserDO createUser(String nickname, String avtar, String registerIp, Integer terminal);
/**
* 更新用户的最后登陆信息
*
* @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 用户编号
* @param reqVO 基本信息
*/
void updateUser(Long userId, AppMemberUserUpdateReqVO reqVO);
/**
* 【会员】修改手机,基于手机验证码
*
* @param userId 用户编号
* @param reqVO 请求信息
*/
void updateUserMobile(Long userId, AppMemberUserUpdateMobileReqVO reqVO);
/**
* 【会员】修改手机,基于微信小程序的授权码
*
* @param userId 用户编号
* @param reqVO 请求信息
*/
void updateUserMobileByWeixin(Long userId, AppMemberUserUpdateMobileByWeixinReqVO reqVO);
/**
* 【会员】修改密码
*
* @param userId 用户编号
* @param reqVO 请求信息
*/
void updateUserPassword(Long userId, AppMemberUserUpdatePasswordReqVO reqVO);
/**
* 【会员】忘记密码
*
* @param reqVO 请求信息
*/
void resetUserPassword(AppMemberUserResetPasswordReqVO reqVO);
/**
* 判断密码是否匹配
*
* @param rawPassword 未加密的密码
* @param encodedPassword 加密后的密码
* @return 是否匹配
*/
boolean isPasswordMatch(String rawPassword, String encodedPassword);
/**
* 【管理员】更新会员用户
*
* @param updateReqVO 更新信息
*/
void updateUser(@Valid MemberUserUpdateReqVO updateReqVO);
/**
* 【管理员】获得会员用户分页
*
* @param pageReqVO 分页查询
* @return 会员用户分页
*/
PageResult<MemberUserDO> getUserPage(MemberUserPageReqVO pageReqVO);
/**
* 更新用户的等级和经验
*
* @param id 用户编号
* @param levelId 用户等级
* @param experience 用户经验
*/
void updateUserLevel(Long id, Long levelId, Integer experience);
/**
* 获得指定用户分组下的用户数量
*
* @param groupId 用户分组编号
* @return 用户数量
*/
Long getUserCountByGroupId(Long groupId);
/**
* 获得指定用户等级下的用户数量
*
* @param levelId 用户等级编号
* @return 用户数量
*/
Long getUserCountByLevelId(Long levelId);
/**
* 获得指定会员标签下的用户数量
*
* @param tagId 用户标签编号
* @return 用户数量
*/
Long getUserCountByTagId(Long tagId);
/**
* 更新用户的积分
*
* @param userId 用户编号
* @param point 积分数量
* @return 更新结果
*/
boolean updateUserPoint(Long userId, Integer point);
}

View File

@@ -0,0 +1,323 @@
package com.tashow.cloud.member.service.user;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.annotations.VisibleForTesting;
import com.tashow.cloud.common.enums.CommonStatusEnum;
import com.tashow.cloud.common.enums.UserTypeEnum;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.common.util.object.BeanUtils;
import com.tashow.cloud.member.controller.admin.user.vo.MemberUserPageReqVO;
import com.tashow.cloud.member.controller.admin.user.vo.MemberUserUpdateReqVO;
import com.tashow.cloud.member.controller.app.user.vo.*;
import com.tashow.cloud.member.convert.auth.AuthConvert;
import com.tashow.cloud.member.convert.user.MemberUserConvert;
import com.tashow.cloud.member.dal.dataobject.user.MemberUserDO;
import com.tashow.cloud.member.dal.mysql.user.MemberUserMapper;
import com.tashow.cloud.member.mq.producer.user.MemberUserProducer;
import com.tashow.cloud.systemapi.api.sms.SmsCodeApi;
import com.tashow.cloud.systemapi.api.sms.dto.code.SmsCodeUseReqDTO;
import com.tashow.cloud.systemapi.api.social.SocialClientApi;
import com.tashow.cloud.systemapi.api.social.dto.SocialWxPhoneNumberInfoRespDTO;
import com.tashow.cloud.systemapi.enums.sms.SmsSceneEnum;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import static com.tashow.cloud.common.exception.util.ServiceExceptionUtil.exception;
import static com.tashow.cloud.common.util.servlet.ServletUtils.getClientIP;
import static com.tashow.cloud.memberapi.enums.ErrorCodeConstants.USER_MOBILE_USED;
import static com.tashow.cloud.systemapi.enums.ErrorCodeConstants.USER_MOBILE_NOT_EXISTS;
import static com.tashow.cloud.systemapi.enums.ErrorCodeConstants.USER_NOT_EXISTS;
/**
* 会员 User Service 实现类
*
* @author 芋道源码
*/
@Service
@Valid
@Slf4j
public class MemberUserServiceImpl implements MemberUserService {
@Resource
private MemberUserMapper memberUserMapper;
@Resource
private SmsCodeApi smsCodeApi;
@Resource
private SocialClientApi socialClientApi;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private MemberUserProducer memberUserProducer;
@Override
public MemberUserDO getUserByMobile(String mobile) {
return memberUserMapper.selectByMobile(mobile);
}
@Override
public List<MemberUserDO> getUserListByNickname(String nickname) {
return memberUserMapper.selectListByNicknameLike(nickname);
}
@Override
@Transactional(rollbackFor = Exception.class)
public MemberUserDO createUserIfAbsent(String mobile, String registerIp, Integer terminal) {
// 用户已经存在
MemberUserDO user = memberUserMapper.selectByMobile(mobile);
if (user != null) {
return user;
}
// 用户不存在,则进行创建
return createUser(mobile, null, null, registerIp, terminal);
}
@Override
@Transactional(rollbackFor = Exception.class)
public MemberUserDO createUser(String nickname, String avtar, String registerIp, Integer terminal) {
return createUser(null, nickname, avtar, registerIp, terminal);
}
private MemberUserDO createUser(String mobile, String nickname, String avtar,
String registerIp, Integer terminal) {
// 生成密码
String password = IdUtil.fastSimpleUUID();
// 插入用户
MemberUserDO user = new MemberUserDO();
user.setMobile(mobile);
user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启
user.setPassword(encodePassword(password)); // 加密密码
user.setRegisterIp(registerIp).setRegisterTerminal(terminal);
user.setNickname(nickname).setAvatar(avtar); // 基础信息
if (StrUtil.isEmpty(nickname)) {
// 昵称为空时,随机一个名字,避免一些依赖 nickname 的逻辑报错,或者有点丑。例如说,短信发送有昵称时~
user.setNickname("用户" + RandomUtil.randomNumbers(6));
}
memberUserMapper.insert(user);
// 发送 MQ 消息:用户创建
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
memberUserProducer.sendUserCreateMessage(user.getId());
}
});
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) {
if (CollUtil.isEmpty(ids)) {
return ListUtil.empty();
}
return memberUserMapper.selectBatchIds(ids);
}
@Override
public void updateUser(Long userId, AppMemberUserUpdateReqVO reqVO) {
MemberUserDO updateObj = BeanUtils.toBean(reqVO, MemberUserDO.class).setId(userId);
memberUserMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUserMobile(Long userId, AppMemberUserUpdateMobileReqVO reqVO) {
// 1.1 检测用户是否存在
MemberUserDO user = validateUserExists(userId);
// 1.2 校验新手机是否已经被绑定
validateMobileUnique(null, reqVO.getMobile());
// 2.1 校验旧手机和旧验证码
// 补充说明:从安全性来说,老手机也校验 oldCode 验证码会更安全。但是由于 uni-app 商城界面暂时没做,所以这里不强制校验
if (StrUtil.isNotEmpty(reqVO.getOldCode())) {
smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(user.getMobile()).setCode(reqVO.getOldCode())
.setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())).checkError();
}
// 2.2 使用新验证码
smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(reqVO.getMobile()).setCode(reqVO.getCode())
.setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())).checkError();
// 3. 更新用户手机
memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(reqVO.getMobile()).build());
}
@Override
public void updateUserMobileByWeixin(Long userId, AppMemberUserUpdateMobileByWeixinReqVO reqVO) {
// 1.1 获得对应的手机号信息
SocialWxPhoneNumberInfoRespDTO phoneNumberInfo = socialClientApi.getWxMaPhoneNumberInfo(
UserTypeEnum.MEMBER.getValue(), reqVO.getCode()).getCheckedData();
Assert.notNull(phoneNumberInfo, "获得手机信息失败,结果为空");
// 1.2 校验新手机是否已经被绑定
validateMobileUnique(userId, phoneNumberInfo.getPhoneNumber());
// 2. 更新用户手机
memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(phoneNumberInfo.getPhoneNumber()).build());
}
@Override
public void updateUserPassword(Long userId, AppMemberUserUpdatePasswordReqVO reqVO) {
// 检测用户是否存在
MemberUserDO user = validateUserExists(userId);
// 校验验证码
smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(user.getMobile()).setCode(reqVO.getCode())
.setScene(SmsSceneEnum.MEMBER_UPDATE_PASSWORD.getScene()).setUsedIp(getClientIP())).checkError();
// 更新用户密码
memberUserMapper.updateById(MemberUserDO.builder().id(userId)
.password(passwordEncoder.encode(reqVO.getPassword())).build());
}
@Override
public void resetUserPassword(AppMemberUserResetPasswordReqVO reqVO) {
// 检验用户是否存在
MemberUserDO user = validateUserExists(reqVO.getMobile());
// 使用验证码
smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_RESET_PASSWORD,
getClientIP())).checkError();
// 更新密码
memberUserMapper.updateById(MemberUserDO.builder().id(user.getId())
.password(passwordEncoder.encode(reqVO.getPassword())).build());
}
private MemberUserDO validateUserExists(String mobile) {
MemberUserDO user = memberUserMapper.selectByMobile(mobile);
if (user == null) {
throw exception(USER_MOBILE_NOT_EXISTS);
}
return user;
}
@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);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUser(MemberUserUpdateReqVO updateReqVO) {
// 校验存在
validateUserExists(updateReqVO.getId());
// 校验手机唯一
validateMobileUnique(updateReqVO.getId(), updateReqVO.getMobile());
// 更新
MemberUserDO updateObj = MemberUserConvert.INSTANCE.convert(updateReqVO);
memberUserMapper.updateById(updateObj);
}
@VisibleForTesting
MemberUserDO validateUserExists(Long id) {
if (id == null) {
return null;
}
MemberUserDO user = memberUserMapper.selectById(id);
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
return user;
}
@VisibleForTesting
void validateMobileUnique(Long id, String mobile) {
if (StrUtil.isBlank(mobile)) {
return;
}
MemberUserDO user = memberUserMapper.selectByMobile(mobile);
if (user == null) {
return;
}
// 如果 id 为空,说明不用比较是否为相同 id 的用户
if (id == null) {
throw exception(USER_MOBILE_USED, mobile);
}
if (!user.getId().equals(id)) {
throw exception(USER_MOBILE_USED, mobile);
}
}
@Override
public PageResult<MemberUserDO> getUserPage(MemberUserPageReqVO pageReqVO) {
return memberUserMapper.selectPage(pageReqVO);
}
@Override
public void updateUserLevel(Long id, Long levelId, Integer experience) {
// 0 代表无等级防止UpdateById时会被过滤掉的问题
levelId = ObjectUtil.defaultIfNull(levelId, 0L);
memberUserMapper.updateById(new MemberUserDO()
.setId(id)
.setLevelId(levelId).setExperience(experience)
);
}
@Override
public Long getUserCountByGroupId(Long groupId) {
return memberUserMapper.selectCountByGroupId(groupId);
}
@Override
public Long getUserCountByLevelId(Long levelId) {
return memberUserMapper.selectCountByLevelId(levelId);
}
@Override
public Long getUserCountByTagId(Long tagId) {
return memberUserMapper.selectCountByTagId(tagId);
}
@Override
public boolean updateUserPoint(Long id, Integer point) {
if (point > 0) {
memberUserMapper.updatePointIncr(id, point);
} else if (point < 0) {
return memberUserMapper.updatePointDecr(id, point) > 0;
}
return true;
}
}

View File

@@ -0,0 +1,62 @@
package com.tashow.cloud.member.user;
import com.tashow.cloud.common.pojo.CommonResult;
import com.tashow.cloud.memberapi.api.user.MemberUserApi;
import com.tashow.cloud.memberapi.api.user.dto.MemberUserRespDTO;
import com.tashow.cloud.member.convert.user.MemberUserConvert;
import com.tashow.cloud.member.dal.dataobject.user.MemberUserDO;
import com.tashow.cloud.member.service.user.MemberUserService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.List;
import static com.tashow.cloud.common.exception.util.ServiceExceptionUtil.exception;
import static com.tashow.cloud.common.pojo.CommonResult.success;
import static com.tashow.cloud.memberapi.enums.ErrorCodeConstants.USER_MOBILE_NOT_EXISTS;
/**
* 会员用户的 API 实现类
*
* @author 芋道源码
*/
@RestController // 提供 RESTful API 接口,给 Feign 调用
@Validated
public class MemberUserApiImpl implements MemberUserApi {
@Resource
private MemberUserService userService;
@Override
public CommonResult<MemberUserRespDTO> getUser(Long id) {
MemberUserDO user = userService.getUser(id);
return success(MemberUserConvert.INSTANCE.convert2(user));
}
@Override
public CommonResult<List<MemberUserRespDTO>> getUserList(Collection<Long> ids) {
return success(MemberUserConvert.INSTANCE.convertList2(userService.getUserList(ids)));
}
@Override
public CommonResult<List<MemberUserRespDTO>> getUserListByNickname(String nickname) {
return success(MemberUserConvert.INSTANCE.convertList2(userService.getUserListByNickname(nickname)));
}
@Override
public CommonResult<MemberUserRespDTO> getUserByMobile(String mobile) {
return success(MemberUserConvert.INSTANCE.convert2(userService.getUserByMobile(mobile)));
}
@Override
public CommonResult<Boolean> validateUser(Long id) {
MemberUserDO user = userService.getUser(id);
if (user == null) {
throw exception(USER_MOBILE_NOT_EXISTS);
}
return success(true);
}
}

View File

@@ -0,0 +1,19 @@
--- #################### 注册中心 + 配置中心相关配置 ####################
spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
username: # Nacos 账号
password: # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP

View File

@@ -0,0 +1,16 @@
spring:
application:
name: member-server
profiles:
active: local
main:
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务
config:
import:
- optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置
- optional:nacos:application.yaml # 加载【Nacos】的配置

View File

@@ -0,0 +1,76 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!-- 变量 tashow.info.base-package基础业务包 -->
<springProperty scope="context" name="tashow.info.base-package" source="tashow.info.base-package"/>
<!-- 格式化输出:%d 表示日期,%X{tid} SkWalking 链路追踪编号,%thread 表示线程名,%-5level级别从左显示 5 个字符宽度,%msg日志消息%n是换行符 -->
<property name="PATTERN_DEFAULT" value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} | %highlight(${LOG_LEVEL_PATTERN:-%5p} ${PID:- }) | %boldYellow(%thread [%tid]) %boldGreen(%-40.40logger{39}) | %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!-- 控制台 Appender -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">     
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${PATTERN_DEFAULT}</pattern>
</layout>
</encoder>
</appender>
<!-- 文件 Appender -->
<!-- 参考 Spring Boot 的 file-appender.xml 编写 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${PATTERN_DEFAULT}</pattern>
</layout>
</encoder>
<!-- 日志文件名 -->
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 滚动后的日志文件名 -->
<fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
<!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<!-- 日志文件,到达多少容量,进行滚动 -->
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
<!-- 日志文件的保留天数 -->
<maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>
<!-- 异步写入日志,提升性能 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志。默认的,如果队列的 80% 已满,则会丢弃 TRACT、DEBUG、INFO 级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能。默认值为 256 -->
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- SkyWalking GRPC 日志收集实现日志中心。注意SkyWalking 8.4.0 版本开始支持 -->
<appender name="GRPC" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${PATTERN_DEFAULT}</pattern>
</layout>
</encoder>
</appender>
<!-- 本地环境 -->
<springProfile name="local">
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->
<appender-ref ref="ASYNC"/> <!-- 本地环境下,如果不想打印日志,可以注释掉本行 -->
</root>
</springProfile>
<!-- 其它环境 -->
<springProfile name="dev,test,stage,prod,default">
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ASYNC"/>
<appender-ref ref="GRPC"/>
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,48 @@
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 默认的配置足够使用,一般不需要进行调优
data:
redis:
host: 127.0.0.1 # 地址
port: 16379 # 端口(单元测试,使用 16379 端口)
database: 0 # 数据库索引
mybatis:
lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
--- #################### 定时任务相关配置 ####################
--- #################### 配置中心相关配置 ####################
--- #################### 服务保障相关配置 ####################
# Lock4j 配置项(单元测试,禁用 Lock4j
--- #################### 监控相关配置 ####################
--- #################### 芋道相关配置 ####################
# 芋道配置项,设置当前项目所有自定义的配置
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,5 @@
DELETE FROM "member_user";
DELETE FROM "member_address";
DELETE FROM "member_tag";
DELETE FROM "member_level";
DELETE FROM "member_group";

View File

@@ -0,0 +1,113 @@
CREATE TABLE IF NOT EXISTS "member_user"
(
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
"nickname" varchar(30) NOT NULL DEFAULT '' COMMENT '用户昵称',
"name" varchar(30) NULL COMMENT '真实名字',
sex tinyint null comment '性别',
birthday datetime null comment '出生日期',
area_id int null comment '所在地',
mark varchar(255) null comment '用户备注',
point int default 0 null 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 '最后登录时间',
"tag_ids" varchar(255) NULL DEFAULT NULL COMMENT '用户标签编号列表,以逗号分隔',
"level_id" bigint NULL DEFAULT NULL COMMENT '等级编号',
"experience" bigint NULL DEFAULT NULL COMMENT '经验',
"group_id" bigint 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,
"detail_address" varchar(250) NOT NULL,
"default_status" 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 '用户收件地址';
CREATE TABLE IF NOT EXISTS "member_tag"
(
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" varchar NOT NULL,
"creator" varchar DEFAULT '',
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar DEFAULT '',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL default '0',
PRIMARY KEY ("id")
) COMMENT '会员标签';
CREATE TABLE IF NOT EXISTS "member_level"
(
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" varchar NOT NULL,
"experience" int NOT NULL,
"level" int NOT NULL,
"discount_percent" int NOT NULL,
"icon" varchar NOT NULL,
"background_url" varchar NOT NULL,
"creator" varchar DEFAULT '',
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar DEFAULT '',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint not null default '0',
"status" tinyint NOT NULL DEFAULT '0',
PRIMARY KEY ("id")
) COMMENT '会员等级';
CREATE TABLE IF NOT EXISTS "member_group"
(
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" varchar NOT NULL,
"remark" varchar NOT NULL,
"status" tinyint NOT NULL DEFAULT '0',
"creator" varchar DEFAULT '',
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar DEFAULT '',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint not null default '0',
PRIMARY KEY ("id")
) COMMENT '用户分组';
CREATE TABLE IF NOT EXISTS "member_brokerage_record"
(
"id" int NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"user_id" bigint NOT NULL,
"biz_id" varchar NOT NULL,
"biz_type" varchar NOT NULL,
"title" varchar NOT NULL,
"price" int NOT NULL,
"total_price" int NOT NULL,
"description" varchar NOT NULL,
"status" varchar NOT NULL,
"frozen_days" int NOT NULL,
"unfreeze_time" varchar,
"creator" varchar DEFAULT '',
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar DEFAULT '',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint not null default '0',
PRIMARY KEY ("id")
) COMMENT '佣金记录';