调整用户接口

This commit is contained in:
2025-10-29 15:07:49 +08:00
parent 4ebe4f9ac6
commit 483abcfd83
109 changed files with 935 additions and 1539 deletions

View File

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

View File

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

View File

@@ -0,0 +1,60 @@
package com.tashow.cloud.user.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.user.controller.admin.user.vo.UserLoginPageReqVO;
import com.tashow.cloud.user.controller.admin.user.vo.UserLoginRespVO;
import com.tashow.cloud.user.controller.admin.user.vo.MemberUserUpdateReqVO;
import com.tashow.cloud.user.convert.user.MemberUserConvert;
import com.tashow.cloud.user.dal.dataobject.user.UserLoginDO;
import com.tashow.cloud.user.service.user.UserLoginService;
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 static com.tashow.cloud.common.pojo.CommonResult.success;
// 管理后台 - 会员用户
@RestController
@RequestMapping("/member/user")
@Validated
public class UserLoginController {
@Resource
private UserLoginService loginUserService;
// 更新会员用户
@PutMapping("/update")
@PreAuthorize("@ss.hasPermission('member:user:update')")
public CommonResult<Boolean> updateUser(@Valid @RequestBody MemberUserUpdateReqVO updateReqVO) {
loginUserService.updateUser(updateReqVO);
return success(true);
}
// 获得会员用户
// id: 编号必填示例1024
@GetMapping("/get")
@PreAuthorize("@ss.hasPermission('member:user:query')")
public CommonResult<UserLoginRespVO> getUser(@RequestParam("id") Long id) {
UserLoginDO user = loginUserService.getUser(id);
return success(MemberUserConvert.INSTANCE.convert03(user));
}
// 获得会员用户分页
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('member:user:query')")
public CommonResult<PageResult<UserLoginRespVO>> getUserPage(@Valid UserLoginPageReqVO pageVO) {
PageResult<UserLoginDO> pageResult = loginUserService.getUserPage(pageVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty());
}
return success(MemberUserConvert.INSTANCE.convertPage(pageResult));
}
}

View File

@@ -0,0 +1,28 @@
package com.tashow.cloud.user.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.user.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.user.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 UserBaseVO {
// 编号必填示例23788
@NotNull(message = "编号不能为空")
private Long id;
}

View File

@@ -0,0 +1,34 @@
package com.tashow.cloud.user.controller.admin.user.vo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
/**
* 会员用户 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class UserBaseVO {
// 手机号必填示例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 remark;
}

View File

@@ -0,0 +1,33 @@
package com.tashow.cloud.user.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 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 UserLoginPageReqVO extends PageParam {
// 搜索条件 手机号,昵称,姓名
private String searchField;
// 会员等级编号示例1
private Long levelId;
// 注册渠道
private Integer registerTerminal;
// 注册时间
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] registDate;
}

View File

@@ -0,0 +1,29 @@
package com.tashow.cloud.user.controller.admin.user.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
// 管理后台 - 会员用户 Response VO
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class UserLoginRespVO extends UserBaseVO {
// 编号必填示例23788
private Long id;
// 会员类型
private Integer memberType;
// 注册渠道
private Integer registerTerminal;
// 注册渠道
private LocalDateTime registerTime;
// 最后登录时间,必填
private LocalDateTime loginDate;
}

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.user.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.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 com.tashow.cloud.user.controller.app.auth.vo.*;
import com.tashow.cloud.user.convert.auth.AuthConvert;
import com.tashow.cloud.user.service.auth.MemberAuthService;
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.user.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.user.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.user.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.user.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.user.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.user.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.user.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.user.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.user.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.user.controller.app.user;
import com.tashow.cloud.common.pojo.CommonResult;
import com.tashow.cloud.user.controller.app.user.vo.*;
import com.tashow.cloud.user.convert.user.MemberUserConvert;
import com.tashow.cloud.user.dal.dataobject.user.UserLoginDO;
import com.tashow.cloud.user.service.user.UserLoginService;
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 UserLoginService userService;
@GetMapping("/get")
// 获得基本信息
public CommonResult<AppMemberUserInfoRespVO> getUserInfo() {
UserLoginDO 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.user.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.user.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.user.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.user.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.user.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.user.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.user.controller;

View File

@@ -0,0 +1,34 @@
package com.tashow.cloud.user.convert.auth;
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 com.tashow.cloud.user.controller.app.auth.vo.*;
import com.tashow.cloud.user.controller.app.user.vo.AppMemberUserResetPasswordReqVO;
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.user.convert;

View File

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

View File

@@ -0,0 +1 @@
package com.tashow.cloud.user.dal.dataobject;

View File

@@ -0,0 +1,80 @@
package com.tashow.cloud.user.dal.dataobject.user;
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.*;
import java.time.LocalDateTime;
/**
* 登录用户 DO
*
* @author 芋道源码
*/
@TableName("tz_user_login")
@KeySequence("tz_user_login_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginDO extends BaseDO {
/**
* 用户ID
*/
@TableId
private Long id;
/**
* 手机
*/
private String mobile;
/**
* 加密后的密码
*/
private String password;
/**
* 帐号状态 (枚举 CommonStatusEnum)
*/
private Integer status;
/**
* 注册 IP
*/
private String registerIp;
/**
* 注册渠道 (枚举 TerminalEnum)
*/
private Integer registerTerminal;
/**
* 注册时间
*/
private LocalDateTime registerDate;
/**
* 最后登录IP
*/
private String loginIp;
/**
* 最后登录时间
*/
private LocalDateTime loginDate;
/**
* 最后登录设备
*/
private String loginTerminal;
/**
* 用户昵称
*/
private String nickname;
/**
* 用户头像
*/
private String avatar;
/**
* 用户备注
*/
private String remark;
}

View File

@@ -0,0 +1,50 @@
package com.tashow.cloud.user.dal.dataobject.user;
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
*
* @author 芋道源码
*/
@TableName("tz_user_member_address")
@KeySequence("tz_user_member_address_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserMemberAddressDO extends BaseDO {
/**
* 用户编号
*/
@TableId
private Long id;
/**
* 收件人名称
*/
private String name;
/**
* 手机号
*/
private String mobile;
/**
* 地区编号
*/
private Long areaId;
/**
* 收件详细地址
*/
private String detailAddress;
/**
* 是否默认
*/
private Integer defaultStatus;
}

View File

@@ -0,0 +1,68 @@
package com.tashow.cloud.user.dal.dataobject.user;
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.*;
import java.time.LocalDateTime;
/**
* 会员信息 DO
*
* @author 芋道源码
*/
@TableName("tz_user_member")
@KeySequence("tz_user_member_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserMemberDO extends BaseDO {
/**
* 用户ID
*/
@TableId
private Long id;
/**
* 真实名字
*/
private String name;
/**
* 性别 (枚举 SexEnum)
*/
private Integer sex;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 所在地 (关联 Area.id 字段)
*/
private Integer areaId;
/**
* 积分
*/
private Integer point;
/**
* 会员标签列表,以逗号分隔
*/
private String tagIds;
/**
* 会员级别编号 (关联 MemberLevelDO.id 字段)
*/
private Long levelId;
/**
* 会员经验
*/
private Integer experience;
/**
* 用户分组编号 (关联 MemberGroupDO.id 字段)
*/
private Long groupId;
}

View File

@@ -0,0 +1,58 @@
package com.tashow.cloud.user.dal.dataobject.user;
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
*
* @author 芋道源码
*/
@TableName("tz_user_member_level")
@KeySequence("tz_user_member_level_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserMemberLevelDO extends BaseDO {
/**
* ID
*/
@TableId
private Long id;
/**
* 等级名称
*/
private String levelName;
/**
* 性别 (枚举 SexEnum)
*/
private Integer level;
/**
* 会员经验
*/
private Integer experience;
/**
* 享受折扣
*/
private Integer discountPercent;
/**
* 等级图标
*/
private String icon;
/**
* 等级背景图
*/
private String backgroundUrl;
/**
* 状态
*/
private Integer status;
}

View File

@@ -0,0 +1 @@
package com.tashow.cloud.user.dal.mysql;

View File

@@ -0,0 +1,71 @@
package com.tashow.cloud.user.dal.mysql.user;
import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.tashow.cloud.common.pojo.PageResult;
import com.tashow.cloud.mybatis.mybatis.core.mapper.BaseMapperX;
import com.tashow.cloud.mybatis.mybatis.core.query.LambdaQueryWrapperX;
import com.tashow.cloud.user.controller.admin.user.vo.UserLoginPageReqVO;
import com.tashow.cloud.user.dal.dataobject.user.UserLoginDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 会员 User Mapper
*
* @author 芋道源码
*/
@Mapper
public interface UserLoginMapper extends BaseMapperX<UserLoginDO> {
default UserLoginDO selectByMobile(String mobile) {
return selectOne(UserLoginDO::getMobile, mobile);
}
default List<UserLoginDO> selectListByNicknameLike(String nickname) {
return selectList(new LambdaQueryWrapperX<UserLoginDO>()
.likeIfPresent(UserLoginDO::getNickname, nickname));
}
default PageResult<UserLoginDO> selectPage(UserLoginPageReqVO reqVO) {
// 分页查询
return selectPage(reqVO, new LambdaQueryWrapperX<UserLoginDO>()
.orderByDesc(UserLoginDO::getId));
}
default Long selectCountByTagId(Long tagId) {
return selectCount(new LambdaQueryWrapperX<UserLoginDO>()
.apply("FIND_IN_SET({0}, tag_ids)", tagId));
}
/**
* 更新用户积分(增加)
*
* @param id 用户编号
* @param incrCount 增加积分(正数)
*/
default void updatePointIncr(Long id, Integer incrCount) {
Assert.isTrue(incrCount > 0);
LambdaUpdateWrapper<UserLoginDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<UserLoginDO>()
.setSql(" point = point + " + incrCount)
.eq(UserLoginDO::getId, id);
update(null, lambdaUpdateWrapper);
}
/**
* 更新用户积分(减少)
*
* @param id 用户编号
* @param incrCount 增加积分(负数)
* @return 更新行数
*/
default int updatePointDecr(Long id, Integer incrCount) {
Assert.isTrue(incrCount < 0);
LambdaUpdateWrapper<UserLoginDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<UserLoginDO>()
.setSql(" point = point + " + incrCount) // 负数,所以使用 + 号
.eq(UserLoginDO::getId, id);
return update(null, lambdaUpdateWrapper);
}
}

View File

@@ -0,0 +1,15 @@
package com.tashow.cloud.user.dal.mysql.user;
import com.tashow.cloud.mybatis.mybatis.core.mapper.BaseMapperX;
import com.tashow.cloud.user.dal.dataobject.user.UserMemberAddressDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 会员地址 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface UserMemberAddressMapper extends BaseMapperX<UserMemberAddressDO> {
}

View File

@@ -0,0 +1,16 @@
package com.tashow.cloud.user.dal.mysql.user;
import com.tashow.cloud.mybatis.mybatis.core.mapper.BaseMapperX;
import com.tashow.cloud.user.dal.dataobject.user.UserMemberLevelDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 会员等级 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface UserMemberLevelMapper extends BaseMapperX<UserMemberLevelDO> {
}

View File

@@ -0,0 +1,16 @@
package com.tashow.cloud.user.dal.mysql.user;
import com.tashow.cloud.mybatis.mybatis.core.mapper.BaseMapperX;
import com.tashow.cloud.user.dal.dataobject.user.UserMemberDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 会员信息 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface UserMemberMapper extends BaseMapperX<UserMemberDO> {
}

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.user.dal;

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
package com.tashow.cloud.user.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.user.framework.rpc;

View File

@@ -0,0 +1,39 @@
package com.tashow.cloud.user.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.user.framework.security.core;

View File

@@ -0,0 +1,88 @@
package com.tashow.cloud.user.service.auth;
import com.tashow.cloud.user.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.user.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.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 com.tashow.cloud.user.controller.app.auth.vo.*;
import com.tashow.cloud.user.convert.auth.AuthConvert;
import com.tashow.cloud.user.dal.dataobject.user.UserLoginDO;
import com.tashow.cloud.user.service.user.UserLoginService;
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 UserLoginService 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) {
// 使用手机 + 密码,进行登录。
UserLoginDO 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();
// 获得获得注册用户
UserLoginDO 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);
}
// 情况一:已绑定,直接读取用户信息
UserLoginDO 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, "获得手机信息失败,结果为空");
// 获得获得注册用户
UserLoginDO 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(UserLoginDO 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 UserLoginDO login0(String mobile, String password) {
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_MOBILE;
// 校验账号是否存在
UserLoginDO 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())) {
UserLoginDO 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())) {
UserLoginDO 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())) {
UserLoginDO 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;
}
UserLoginDO user = userService.getUser(userId);
return user != null ? user.getMobile() : null;
}
private UserTypeEnum getUserType() {
return UserTypeEnum.MEMBER;
}
}

View File

@@ -0,0 +1,174 @@
package com.tashow.cloud.user.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.user.controller.admin.user.vo.UserLoginPageReqVO;
import com.tashow.cloud.user.controller.admin.user.vo.MemberUserUpdateReqVO;
import com.tashow.cloud.user.controller.app.user.vo.*;
import com.tashow.cloud.user.dal.dataobject.user.UserLoginDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* 会员用户 Service 接口
*
* @author 芋道源码
*/
public interface UserLoginService {
/**
* 通过手机查询用户
*
* @param mobile 手机
* @return 用户对象
*/
UserLoginDO getUserByMobile(String mobile);
/**
* 基于用户昵称,模糊匹配用户列表
*
* @param nickname 用户昵称,模糊匹配
* @return 用户信息的列表
*/
List<UserLoginDO> getUserListByNickname(String nickname);
/**
* 基于手机号创建用户。
* 如果用户已经存在,则直接进行返回
*
* @param mobile 手机号
* @param registerIp 注册 IP
* @param terminal 终端 {@link TerminalEnum}
* @return 用户对象
*/
UserLoginDO createUserIfAbsent(@Mobile String mobile, String registerIp, Integer terminal);
/**
* 创建用户
* 目的:三方登录时,如果未绑定用户时,自动创建对应用户
*
* @param nickname 昵称
* @param avtar 头像
* @param registerIp 注册 IP
* @param terminal 终端 {@link TerminalEnum}
* @return 用户对象
*/
UserLoginDO 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 用户对象信息
*/
UserLoginDO getUser(Long id);
/**
* 通过用户 ID 查询用户们
*
* @param ids 用户 ID
* @return 用户对象信息数组
*/
List<UserLoginDO> 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<UserLoginDO> getUserPage(UserLoginPageReqVO pageReqVO);
/**
* 更新用户的等级和经验
*
* @param id 用户编号
* @param levelId 用户等级
* @param experience 用户经验
*/
void updateUserLevel(Long id, Long levelId, Integer experience);
/**
* 获得指定会员标签下的用户数量
*
* @param tagId 用户标签编号
* @return 用户数量
*/
Long getUserCountByTagId(Long tagId);
/**
* 更新用户的积分
*
* @param userId 用户编号
* @param point 积分数量
* @return 更新结果
*/
boolean updateUserPoint(Long userId, Integer point);
}

View File

@@ -0,0 +1,308 @@
package com.tashow.cloud.user.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.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 com.tashow.cloud.user.controller.admin.user.vo.UserLoginPageReqVO;
import com.tashow.cloud.user.controller.admin.user.vo.MemberUserUpdateReqVO;
import com.tashow.cloud.user.controller.app.user.vo.*;
import com.tashow.cloud.user.convert.auth.AuthConvert;
import com.tashow.cloud.user.convert.user.MemberUserConvert;
import com.tashow.cloud.user.dal.dataobject.user.UserLoginDO;
import com.tashow.cloud.user.dal.mysql.user.UserLoginMapper;
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 UserLoginServiceImpl implements UserLoginService {
@Resource
private UserLoginMapper memberUserMapper;
@Resource
private SmsCodeApi smsCodeApi;
@Resource
private SocialClientApi socialClientApi;
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserLoginDO getUserByMobile(String mobile) {
return memberUserMapper.selectByMobile(mobile);
}
@Override
public List<UserLoginDO> getUserListByNickname(String nickname) {
return memberUserMapper.selectListByNicknameLike(nickname);
}
@Override
@Transactional(rollbackFor = Exception.class)
public UserLoginDO createUserIfAbsent(String mobile, String registerIp, Integer terminal) {
// 用户已经存在
UserLoginDO user = memberUserMapper.selectByMobile(mobile);
if (user != null) {
return user;
}
// 用户不存在,则进行创建
return createUser(mobile, null, null, registerIp, terminal);
}
@Override
@Transactional(rollbackFor = Exception.class)
public UserLoginDO createUser(String nickname, String avtar, String registerIp, Integer terminal) {
return createUser(null, nickname, avtar, registerIp, terminal);
}
private UserLoginDO createUser(String mobile, String nickname, String avtar,
String registerIp, Integer terminal) {
// 生成密码
String password = IdUtil.fastSimpleUUID();
// 插入用户
UserLoginDO user = new UserLoginDO();
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() {
}
});
return user;
}
@Override
public void updateUserLogin(Long id, String loginIp) {
memberUserMapper.updateById(new UserLoginDO().setId(id)
.setLoginIp(loginIp).setLoginDate(LocalDateTime.now()));
}
@Override
public UserLoginDO getUser(Long id) {
return memberUserMapper.selectById(id);
}
@Override
public List<UserLoginDO> getUserList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return ListUtil.empty();
}
return memberUserMapper.selectBatchIds(ids);
}
@Override
public void updateUser(Long userId, AppMemberUserUpdateReqVO reqVO) {
UserLoginDO updateObj = BeanUtils.toBean(reqVO, UserLoginDO.class).setId(userId);
memberUserMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUserMobile(Long userId, AppMemberUserUpdateMobileReqVO reqVO) {
// 1.1 检测用户是否存在
UserLoginDO 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(UserLoginDO.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(UserLoginDO.builder().id(userId).mobile(phoneNumberInfo.getPhoneNumber()).build());
}
@Override
public void updateUserPassword(Long userId, AppMemberUserUpdatePasswordReqVO reqVO) {
// 检测用户是否存在
UserLoginDO user = validateUserExists(userId);
// 校验验证码
smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(user.getMobile()).setCode(reqVO.getCode())
.setScene(SmsSceneEnum.MEMBER_UPDATE_PASSWORD.getScene()).setUsedIp(getClientIP())).checkError();
// 更新用户密码
memberUserMapper.updateById(UserLoginDO.builder().id(userId)
.password(passwordEncoder.encode(reqVO.getPassword())).build());
}
@Override
public void resetUserPassword(AppMemberUserResetPasswordReqVO reqVO) {
// 检验用户是否存在
UserLoginDO user = validateUserExists(reqVO.getMobile());
// 使用验证码
smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_RESET_PASSWORD,
getClientIP())).checkError();
// 更新密码
memberUserMapper.updateById(UserLoginDO.builder().id(user.getId())
.password(passwordEncoder.encode(reqVO.getPassword())).build());
}
private UserLoginDO validateUserExists(String mobile) {
UserLoginDO 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());
// 更新
UserLoginDO updateObj = MemberUserConvert.INSTANCE.convert(updateReqVO);
memberUserMapper.updateById(updateObj);
}
@VisibleForTesting
UserLoginDO validateUserExists(Long id) {
if (id == null) {
return null;
}
UserLoginDO 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;
}
UserLoginDO 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<UserLoginDO> getUserPage(UserLoginPageReqVO 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 UserLoginDO()
.setId(id)
);
}
@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,28 @@
package com.tashow.cloud.user.service.user;
import com.tashow.cloud.user.dal.dataobject.user.UserMemberAddressDO;
/**
* 会员地址 Service 接口
*
* @author 芋道源码
*/
public interface UserMemberAddressService {
/**
* 删除会员地址
*
* @param id 编号
*/
void deleteMemberAddress(Long id);
/**
* 获得会员地址
*
* @param id 编号
* @return 会员地址
*/
UserMemberAddressDO getMemberAddress(Long id);
}

View File

@@ -0,0 +1,41 @@
package com.tashow.cloud.user.service.user;
import com.tashow.cloud.user.dal.dataobject.user.UserMemberAddressDO;
import com.tashow.cloud.user.dal.mysql.user.UserMemberAddressMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
/**
* 会员地址 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class UserMemberAddressServiceImpl implements UserMemberAddressService {
@Resource
private UserMemberAddressMapper memberAddressMapper;
@Override
public void deleteMemberAddress(Long id) {
// 校验存在
validateMemberAddressExists(id);
// 删除
memberAddressMapper.deleteById(id);
}
private void validateMemberAddressExists(Long id) {
if (memberAddressMapper.selectById(id) == null) {
// throw exception(MEMBER_ADDRESS_NOT_EXISTS);
}
}
@Override
public UserMemberAddressDO getMemberAddress(Long id) {
return memberAddressMapper.selectById(id);
}
}

View File

@@ -0,0 +1,28 @@
package com.tashow.cloud.user.service.user;
import com.tashow.cloud.user.dal.dataobject.user.UserMemberLevelDO;
/**
* 会员等级 Service 接口
*
* @author 芋道源码
*/
public interface UserMemberLevelService {
/**
* 删除会员等级
*
* @param id 编号
*/
void deleteMemberLevel(Long id);
/**
* 获得会员等级
*
* @param id 编号
* @return 会员等级
*/
UserMemberLevelDO getMemberLevel(Long id);
}

View File

@@ -0,0 +1,41 @@
package com.tashow.cloud.user.service.user;
import com.tashow.cloud.user.dal.dataobject.user.UserMemberLevelDO;
import com.tashow.cloud.user.dal.mysql.user.UserMemberLevelMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
/**
* 会员等级 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class UserMemberLevelServiceImpl implements UserMemberLevelService {
@Resource
private UserMemberLevelMapper memberLevelMapper;
@Override
public void deleteMemberLevel(Long id) {
// 校验存在
validateMemberLevelExists(id);
// 删除
memberLevelMapper.deleteById(id);
}
private void validateMemberLevelExists(Long id) {
if (memberLevelMapper.selectById(id) == null) {
// throw exception(MEMBER_LEVEL_NOT_EXISTS);
}
}
@Override
public UserMemberLevelDO getMemberLevel(Long id) {
return memberLevelMapper.selectById(id);
}
}

View File

@@ -0,0 +1,29 @@
package com.tashow.cloud.user.service.user;
import com.tashow.cloud.user.dal.dataobject.user.UserMemberDO;
/**
* 会员信息 Service 接口
*
* @author 芋道源码
*/
public interface UserMemberService {
/**
* 删除会员信息
*
* @param id 编号
*/
void deleteMember(Long id);
/**
* 获得会员信息
*
* @param id 编号
* @return 会员信息
*/
UserMemberDO getMember(Long id);
}

View File

@@ -0,0 +1,41 @@
package com.tashow.cloud.user.service.user;
import com.tashow.cloud.user.dal.dataobject.user.UserMemberDO;
import com.tashow.cloud.user.dal.mysql.user.UserMemberMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
/**
* 会员信息 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class UserMemberServiceImpl implements UserMemberService {
@Resource
private UserMemberMapper memberMapper;
@Override
public void deleteMember(Long id) {
// 校验存在
validateMemberExists(id);
// 删除
memberMapper.deleteById(id);
}
private void validateMemberExists(Long id) {
if (memberMapper.selectById(id) == null) {
// throw exception(MEMBER_NOT_EXISTS);
}
}
@Override
public UserMemberDO getMember(Long id) {
return memberMapper.selectById(id);
}
}

View File

@@ -0,0 +1,62 @@
package com.tashow.cloud.user.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.user.convert.user.MemberUserConvert;
import com.tashow.cloud.user.dal.dataobject.user.UserLoginDO;
import com.tashow.cloud.user.service.user.UserLoginService;
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 UserLoginService userService;
@Override
public CommonResult<MemberUserRespDTO> getUser(Long id) {
UserLoginDO 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) {
UserLoginDO 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: 43.139.42.137:8848 # Nacos 服务器地址
username: nacos # Nacos 账号
password: nacos # 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,18 @@
server:
port: 48084
spring:
application:
name: user-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>