diff --git a/src/main/java/com/ai/da/common/constant/CommonConstant.java b/src/main/java/com/ai/da/common/constant/CommonConstant.java index b167f4b6..ff539ec1 100644 --- a/src/main/java/com/ai/da/common/constant/CommonConstant.java +++ b/src/main/java/com/ai/da/common/constant/CommonConstant.java @@ -65,4 +65,7 @@ public class CommonConstant { public static final Long MAXIMUM_USER_ID = 704L; // public static final Long MAXIMUM_USER_ID = 225L; + // 激活更改邮箱 链接有效期 毫秒 3天 + public static final Long CHANGE_MAILBOX_LINK_VALIDITY = 259200000L; + } diff --git a/src/main/java/com/ai/da/common/enums/AuthenticationOperationTypeEnum.java b/src/main/java/com/ai/da/common/enums/AuthenticationOperationTypeEnum.java index e8ea46d1..a8300691 100644 --- a/src/main/java/com/ai/da/common/enums/AuthenticationOperationTypeEnum.java +++ b/src/main/java/com/ai/da/common/enums/AuthenticationOperationTypeEnum.java @@ -23,7 +23,11 @@ public enum AuthenticationOperationTypeEnum { /** * 忘记密码 */ - FORGET_PWD; + FORGET_PWD, + /** + * 更改邮箱 + */ + CHANGE_MAILBOX; public static AuthenticationOperationTypeEnum of(String name) { return Stream.of(AuthenticationOperationTypeEnum.values()).filter(v -> v.name().equals(name)).findFirst().orElse(null); diff --git a/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java b/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java index bc6071c8..bd1052be 100644 --- a/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java +++ b/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java @@ -51,7 +51,7 @@ public class AuthenticationFilter extends OncePerRequestFilter { "/api/python/flush","/api/account/healthy","/api/ali-pay/trade/notify","/api/paypal/ipn/back","/api/alipay-hk/trade/notify", "/api/portfolio/page", "/api/portfolio/detail", "/api/portfolio/commentPage", "/api/portfolio/viewsIncrease", "/api/account/designWorksRegister","/api/account/questionnaire","/api/stripe/trade/notify", - "/notification" + "/notification","/api/account/activateNewEmail" ); @Override diff --git a/src/main/java/com/ai/da/common/security/jwt/JWTTokenHelper.java b/src/main/java/com/ai/da/common/security/jwt/JWTTokenHelper.java index a43a21f0..d536ab29 100644 --- a/src/main/java/com/ai/da/common/security/jwt/JWTTokenHelper.java +++ b/src/main/java/com/ai/da/common/security/jwt/JWTTokenHelper.java @@ -2,6 +2,7 @@ package com.ai.da.common.security.jwt; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; +import com.ai.da.common.constant.CommonConstant; import com.ai.da.common.security.config.SecurityProperties; import com.ai.da.model.vo.AuthPrincipalVo; import com.alibaba.fastjson.JSON; @@ -30,6 +31,7 @@ public class JWTTokenHelper { private static final String ISSUER = "DWJ"; private static final String AUTHORITIES = "authorities"; + private static final String CHANGE_MAILBOX = "changeMailbox"; public String createToken(AuthPrincipalVo principal) { String token = Jwts.builder() @@ -65,4 +67,21 @@ public class JWTTokenHelper { token = token.replaceAll(securityProperties.getJwtTokenPrefix(), ""); return Jwts.parser().setSigningKey(securityProperties.getJwtSecret()).parseClaimsJws(token).getBody(); } + + public String createToken(Long userId, String userEmail){ + String token = Jwts.builder() + .setId(String.valueOf(userId)) + .setSubject(userEmail + "_" + userId) + .setIssuedAt(new Date()) + .setIssuer(ISSUER) + .claim(CHANGE_MAILBOX, JSON.toJSONString(new ArrayList<>()))//自定义属性 权限 + .setExpiration(new Date(System.currentTimeMillis() + CommonConstant.CHANGE_MAILBOX_LINK_VALIDITY)) + .signWith(SignatureAlgorithm.HS256, securityProperties.getJwtSecret()) + .compact(); + return token; + } + + public String parseToEmailAndId(String token) { + return parser(token).getSubject(); + } } diff --git a/src/main/java/com/ai/da/common/utils/RedisUtil.java b/src/main/java/com/ai/da/common/utils/RedisUtil.java index 66ba1ab3..eea3ce3f 100644 --- a/src/main/java/com/ai/da/common/utils/RedisUtil.java +++ b/src/main/java/com/ai/da/common/utils/RedisUtil.java @@ -1,6 +1,7 @@ package com.ai.da.common.utils; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Component; @@ -240,4 +241,17 @@ public class RedisUtil { return redisTemplate.opsForValue().increment(key, 0); } + public final static String NICKNAME_MODIFY_TIMES = "NicknameModifyTimes:"; + public void increaseCount(String key) { + redisTemplate.opsForValue().increment(key); + } + + public Long getIncrementCount(String key) { + return redisTemplate.opsForValue().increment(key, 0); + } + + public void setKeyExpire(String key, Long expire) { + redisTemplate.expire(key, expire, TimeUnit.DAYS); + } + } diff --git a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java index 16fcfe21..f01c6a34 100644 --- a/src/main/java/com/ai/da/common/utils/SendEmailUtil.java +++ b/src/main/java/com/ai/da/common/utils/SendEmailUtil.java @@ -53,6 +53,7 @@ public class SendEmailUtil { * 绑定邮箱 */ public static String BIND_MAILBOX_SUBJECT = "绑定邮箱"; + public static String CHANGE_MAILBOX_SUBJECT = "Change Mailbox"; /** * 登入模板id */ @@ -71,6 +72,8 @@ public class SendEmailUtil { */ public static Long BIND_MAILBOX_TEMPLATE_ID = 45619L; + public static Long CHANGE_MAILBOX_TEMPLATE_ID = 128210L; + public static Boolean send(String receiverAddress, String ip, Long templateId, String verifyCode) { try { @@ -92,7 +95,8 @@ public class SendEmailUtil { req.setDestination(new String[]{receiverAddress}); String subject = templateId == LOGIN_TEMPLATE_ID ? LOGIN_SUBJECT : templateId == UPDATE_PWD_TEMPLATE_ID ? FORGET_PWD_SUBJECT : - templateId == EXCEPTION_ID_TEMPLATE_ID ? EXCEPTION_ID_SUBJECT : BIND_MAILBOX_SUBJECT; + templateId == EXCEPTION_ID_TEMPLATE_ID ? EXCEPTION_ID_SUBJECT : + templateId == CHANGE_MAILBOX_TEMPLATE_ID ? CHANGE_MAILBOX_SUBJECT : BIND_MAILBOX_SUBJECT; req.setSubject(subject); req.setTemplate(contractTemplate(templateId, verifyCode, ip)); @@ -626,4 +630,49 @@ public class SendEmailUtil { throw new BusinessException("failed.to.send.mail"); } } + + private final static Long CHANGE_MAILBOX_CONFIRM_CN = 128278L; + private final static Long CHANGE_MAILBOX_CONFIRM_EN = 128277L; + + public static void changeMailboxConfirm(String receiverAddress, String language, String name, String link){ + try{ + // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 + // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 + // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 + Credential cred = new Credential(SECRET_ID, SECRET_KEy); + // 实例化一个http选项,可选的,没有特殊需求可以跳过 + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint("ses.tencentcloudapi.com"); + // 实例化一个client选项,可选的,没有特殊需求可以跳过 + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + // 实例化要请求产品的client对象,clientProfile是可选的 + SesClient client = new SesClient(cred, "ap-hongkong", clientProfile); + // 实例化一个请求对象,每个接口都会对应一个request对象 + SendEmailRequest req = new SendEmailRequest(); + req.setFromEmailAddress(SEND_ADDRESS); + req.setDestination(new String[]{receiverAddress}); + Template template = new Template(); + if (language.equals("ENGLISH")){ + req.setSubject("Change the email address bound to the AiDA account"); + template.setTemplateID(CHANGE_MAILBOX_CONFIRM_EN); + }else { + req.setSubject("更换AiDA账号绑定的邮箱地址"); + template.setTemplateID(CHANGE_MAILBOX_CONFIRM_CN); + } + JSONObject param = new JSONObject(); + param.put("userName", name); + param.put("link", link); + + template.setTemplateData(param.toJSONString()); + req.setTemplate(template); + + // 返回的resp是一个SendEmailResponse的实例,与请求对象对应 + SendEmailResponse resp = client.SendEmail(req); + log.info("短信发送结果res###{}", SendEmailResponse.toJsonString(resp)); + } catch (TencentCloudSDKException e) { + log.info("邮件发送失败###{}", e.toString()); + throw new BusinessException("failed.to.send.mail"); + } + } } diff --git a/src/main/java/com/ai/da/controller/AccountController.java b/src/main/java/com/ai/da/controller/AccountController.java index 8cf6a5a9..9c8e3e90 100644 --- a/src/main/java/com/ai/da/controller/AccountController.java +++ b/src/main/java/com/ai/da/controller/AccountController.java @@ -219,4 +219,40 @@ public class AccountController { public Response getPersonalHomepage(@RequestParam("id") Long id){ return Response.success(accountService.getPersonalHomepage(id)); } + + @ApiOperation(value = "getNicknameModifyTimes") + @GetMapping("/getNicknameModifyTimes") + public Response> getNicknameModifyTimes(){ + return Response.success(accountService.getNicknameModifyTimes()); + } + + @ApiOperation(value = "editUserName") + @GetMapping("/editUserName") + public Response editUserName(@RequestParam("newUserName") String newUserName){ + accountService.editUserName(newUserName); + return Response.success("success"); + } + + @ApiOperation(value = "verifyUserEmail") + @GetMapping("/verifyUserEmail") + public Response verifyUserEmail(@RequestParam("verifyCode") String verifyCode){ + accountService.verifyUserEmail(verifyCode); + return Response.success("success"); + } + + @ApiOperation(value = "changeUserEmail") + @GetMapping("/changeUserEmail") + public Response changeUserEmail(@RequestParam("newMailbox") String newMailbox){ + accountService.changeUserEmail(newMailbox); + return Response.success("success"); + } + + @ApiOperation(value = "activateNewEmail") + @GetMapping("/activateNewEmail") + public Response activateNewEmail(@RequestParam("token") String token){ + accountService.activateNewEmail(token); + return Response.success("success"); + } + + } diff --git a/src/main/java/com/ai/da/model/dto/EmailSendDTO.java b/src/main/java/com/ai/da/model/dto/EmailSendDTO.java index e79da073..eed614bb 100644 --- a/src/main/java/com/ai/da/model/dto/EmailSendDTO.java +++ b/src/main/java/com/ai/da/model/dto/EmailSendDTO.java @@ -15,7 +15,7 @@ public class EmailSendDTO { private String email; @NotBlank(message = "operationType.cannot.be.empty") - @ApiModelProperty("操作类型 LOGIN 注册 FORGET_PWD 忘记密码 BIND_MAILBOX 绑定邮箱") + @ApiModelProperty("操作类型 LOGIN 注册 FORGET_PWD 忘记密码 BIND_MAILBOX 绑定邮箱 CHANGE_MAILBOX 更改邮箱") private String operationType; @ApiModelProperty("异常ip") diff --git a/src/main/java/com/ai/da/model/enums/SketchStyle.java b/src/main/java/com/ai/da/model/enums/SketchStyle.java index 475b3a01..f4ca7894 100644 --- a/src/main/java/com/ai/da/model/enums/SketchStyle.java +++ b/src/main/java/com/ai/da/model/enums/SketchStyle.java @@ -6,7 +6,9 @@ public enum SketchStyle implements IEnumDisplay{ MEDIUM("2"), - THIN("3"); + THIN("3"), + + CUSTOM("Custom"); private String value; diff --git a/src/main/java/com/ai/da/service/AccountService.java b/src/main/java/com/ai/da/service/AccountService.java index 01b7d03e..166fb889 100644 --- a/src/main/java/com/ai/da/service/AccountService.java +++ b/src/main/java/com/ai/da/service/AccountService.java @@ -167,4 +167,14 @@ public interface AccountService extends IService { Boolean viewsIncrease(Long id); void registerUserToVisitor(); + + Map getNicknameModifyTimes(); + + void editUserName(String newUserName); + + void verifyUserEmail(String verifyCode); + + void changeUserEmail(String newMailbox); + + void activateNewEmail(String token); } diff --git a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java index 6a82b081..46245973 100644 --- a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java @@ -384,6 +384,10 @@ public class AccountServiceImpl extends ServiceImpl impl result = SendEmailUtil.send(emailSendDTO.getEmail(), null, SendEmailUtil.BIND_MAILBOX_TEMPLATE_ID, randomVerifyCode); break; + case CHANGE_MAILBOX: + result = SendEmailUtil.send(emailSendDTO.getEmail(), null, + SendEmailUtil.CHANGE_MAILBOX_TEMPLATE_ID, randomVerifyCode); + break; default: } if (!result) { @@ -1580,8 +1584,99 @@ public class AccountServiceImpl extends ServiceImpl impl } private Long viewPersonalHomepageCount(Long accountId) { - redisUtil.getPersonalHomepageViewCount(accountId); - return null; + return redisUtil.getPersonalHomepageViewCount(accountId); + } + + // 获取当前用户30天内 剩余昵称修改次数 + public Map getNicknameModifyTimes(){ + Long accountId = UserContext.getUserHolder().getId(); + String key = RedisUtil.NICKNAME_MODIFY_TIMES + accountId; + Long times = redisUtil.getIncrementCount(key); + HashMap resp = new HashMap<>(); + resp.put("remainingTimes", 5L - times); + resp.put("remainingDays", redisUtil.getExpire(key) == -1 ? 30L : (long) Math.ceil((double) redisUtil.getExpire(key) / (24 * 60 * 60))); + return resp; + } + + // 修改用户名 允许用户30天内修改5次 + @Transactional(rollbackFor = Exception.class) + public void editUserName(String newUserName){ + Long accountId = UserContext.getUserHolder().getId(); + // 判断当前用户是否还有修改昵称的次数 + Map remainTimes = getNicknameModifyTimes(); + Long remainingModifyTimes = remainTimes.get("remainingTimes"); + if (remainingModifyTimes > 0){ + Account account = new Account().setUserName(newUserName); + account.setId(accountId); + baseMapper.updateById(account); + + String key = RedisUtil.NICKNAME_MODIFY_TIMES + accountId; + // 先判断有没有这个key,若没有这个key, 需要为key添加有效期 + if (remainingModifyTimes == 5){ + redisUtil.setKeyExpire(key, 30L); + } + // 增加修改次数 + redisUtil.increaseCount(key); + }else { + throw new BusinessException("remaining.modifications", 1); + } + } + + // 验证是否是本人进行邮箱绑定更改 + public void verifyUserEmail(String verifyCode){ + // 向旧邮箱发送验证码,以保证是当前邮箱拥有者在进行更改 + String userEmail = baseMapper.selectById(UserContext.getUserHolder().getId()).getUserEmail(); + //校验邮箱验证码 + String verifyCodeCatch = LocalCacheUtils.getVerifyCodeCache(AuthenticationOperationTypeEnum.CHANGE_MAILBOX.name() + "_" + userEmail); + if (StringUtils.isBlank(verifyCodeCatch)) { + throw new BusinessException("the.verification.code.has.expired", ResultEnum.PROMPT.getCode()); + } + if (!verifyCode.equals(verifyCodeCatch)) { + throw new BusinessException("verification.code.error", ResultEnum.PROMPT.getCode()); + } + } + + // 修改邮箱地址 + public void changeUserEmail(String newMailbox){ + AuthPrincipalVo userHolder = UserContext.getUserHolder(); + Long accountId = userHolder.getId(); + // 将新邮箱信息存储到redis + String key = RedisUtil.CHANGE_MAILBOX + accountId; + redisUtil.addToString(key, newMailbox, CommonConstant.CHANGE_MAILBOX_LINK_VALIDITY / 1000); + + String username = userHolder.getUsername(); + String token = jwtTokenHelper.createToken(accountId, newMailbox); + // 准备激活链接,链接应该要有有效期 + String link = "?" + token; + // 向新邮箱发送邮件,邮件附带激活链接,点击链接进行验证 + SendEmailUtil.changeMailboxConfirm(newMailbox, userHolder.getLanguage(), username, link); + } + + // 验证激活链接 + public void activateNewEmail(String token){ + // 获取链接地址信息,更新指定用户邮箱 + + String emailAndId = jwtTokenHelper.parseToEmailAndId(token); + String newMailbox = emailAndId.substring(0, emailAndId.lastIndexOf("_")); + String accountId = emailAndId.substring(emailAndId.lastIndexOf("_") + 1); + + // 与redis的数据对比 + String key = RedisUtil.CHANGE_MAILBOX + accountId; + String preInfo = redisUtil.getFromString(key); + if (StringUtil.isNullOrEmpty(preInfo)){ + throw new BusinessException("申请已过期", 1); + }else if (!preInfo.equals(newMailbox)){ + throw new BusinessException("信息不匹配,请重新申请"); + } + + // 执行替换 + log.info("执行邮箱替换,用户id:{},新邮箱:{}", accountId, newMailbox); + + Account account = new Account(); + account.setUserEmail(newMailbox); + account.setId(Long.parseLong(accountId)); + baseMapper.updateById(account); + log.info("邮箱绑定更改完成,用户id:{},新邮箱:{}", accountId, newMailbox); } } diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 8985db45..dd1beb67 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -144,6 +144,7 @@ you.have.already.followed.this.user=You have already followed this user subscription.success=Subscription Success unsubscribe.success=Unsubscribe Success you.have.not.followed.the.current.user=You have not followed the current user +remaining.modifications=Remaining modifications are 0 # 可能会报异常 # Informative: @@ -213,6 +214,7 @@ POCKET=Pocket THICK=Thick Lines MEDIUM=Medium Lines THIN=Thin lines +CUSTOM=Custom GENERATE=Generate Sketch EXTRACT=Extract Sketch \ No newline at end of file diff --git a/src/main/resources/messages_zh.properties b/src/main/resources/messages_zh.properties index 4219585f..92d5e58a 100644 --- a/src/main/resources/messages_zh.properties +++ b/src/main/resources/messages_zh.properties @@ -139,6 +139,7 @@ you.have.already.followed.this.user=您已经关注当前用户 subscription.success=关注成功 unsubscribe.success=取消关注成功 you.have.not.followed.the.current.user=您还未关注当前用户 +remaining.modifications=剩余修改次数为0 # 可能会报异常 # Informative: @@ -206,6 +207,7 @@ POCKET=口袋 THICK=粗线条 MEDIUM=中线条 THIN=细线条 +CUSTOM=自定义 GENERATE=生成线稿 EXTRACT=提取线稿 \ No newline at end of file