diff --git a/pom.xml b/pom.xml index f1f5a7b..07ac7a3 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,27 @@ 3.17.4 + + + com.aliyun + dysmsapi20170525 + 2.0.23 + + + + + com.aliyun + tea-openapi + 0.3.9 + + + + + com.aliyun + tea-util + 0.2.13 + + io.jsonwebtoken diff --git a/src/main/java/com/example/caseData/config/DySmsConfig.java b/src/main/java/com/example/caseData/config/DySmsConfig.java new file mode 100644 index 0000000..c8865ff --- /dev/null +++ b/src/main/java/com/example/caseData/config/DySmsConfig.java @@ -0,0 +1,16 @@ +package com.example.caseData.config; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Getter +public class DySmsConfig { + @Value("${dysms.access-key}") + private String accessKey; + + @Value("${dysms.access-secret}") + private String accessSecret; + +} diff --git a/src/main/java/com/example/caseData/controller/PublicController.java b/src/main/java/com/example/caseData/controller/PublicController.java index bf06aee..923bb9c 100644 --- a/src/main/java/com/example/caseData/controller/PublicController.java +++ b/src/main/java/com/example/caseData/controller/PublicController.java @@ -1,23 +1,33 @@ package com.example.caseData.controller; +import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import com.example.caseData.common.Response; import com.example.caseData.config.EnvConfig; +import com.example.caseData.dto.T; import com.example.caseData.dto.publicDto.GetOssSignDto; import com.example.caseData.dto.publicDto.LoginDto; import com.example.caseData.dto.user.UserDto; +import com.example.caseData.extend.aliyun.DySms; import com.example.caseData.extend.aliyun.Oss; -import com.example.caseData.request.publicRequest.GetOssSignRequest; -import com.example.caseData.request.publicRequest.LoginHcpRequest; -import com.example.caseData.request.publicRequest.LoginRequest; +import com.example.caseData.extend.weChat.WxMaServiceUtils; +import com.example.caseData.request.publicRequest.*; import com.example.caseData.request.UserRequest.UserRequest; import com.example.caseData.service.UserService; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.time.Duration; +import java.util.Map; import java.util.Objects; +import java.util.Random; +@RequiredArgsConstructor @RestController @RequestMapping("/api") public class PublicController { @@ -30,20 +40,49 @@ public class PublicController { @Resource private EnvConfig envConfig; + private final WxMaServiceUtils wxMaServiceUtils; + + @Resource + private RedisTemplate redisTemplate; + + @Resource + private DySms dySms; + // 登陆 @PostMapping("/login/wechat/mobile") public Response loginWechatMobile(@Validated({LoginRequest.Login.class}) @ModelAttribute LoginRequest request) { // 微信手机号授权登录 + try { // 获取手机号 + WxMaPhoneNumberInfo phoneInfo = wxMaServiceUtils.getPhoneNumber(request.getPhone_code()); + if (phoneInfo == null) { + return Response.error("微信授权失败"); + } + + if (phoneInfo.getPurePhoneNumber() == null) { + return Response.error("微信授权失败"); + } + // 获取用户openid + WxMaJscode2SessionResult wxInfoData = wxMaServiceUtils.getSessionInfo(request.getWx_code()); + if (wxInfoData == null) { + return Response.error("微信授权失败"); + } - // 临时测试使用 - String phone = "18221234167"; + if (wxInfoData.getOpenid() == null) { + return Response.error("微信授权失败"); + } - // 用户登陆 - LoginDto g = userService.UserLoginWithMobile(phone); + if (wxInfoData.getSessionKey() == null) { + return Response.error("微信授权失败"); + } - return Response.success(g); + // 用户登陆 + LoginDto g = userService.UserLoginWithMobile(phoneInfo.getPurePhoneNumber()); + return Response.success(g); + } catch (Exception e) { + return Response.error(e.getMessage()); + } } // 登陆 @@ -79,4 +118,96 @@ public class PublicController { GetOssSignDto g = Oss.getOssSign(ossPath); return Response.success(g); } + + // 获取验证码 + @PostMapping("/code/phone") + public Response GetPhoneCode(@Validated() @ModelAttribute GetPhoneCodeRequest request) { + String templateCode = ""; + String scene = "注册验证码"; + + + // 手机号登录 + if (request.getScene() == 1){ + templateCode = "SMS_215344868"; + scene = "注册验证码"; + } + + // 限流 key + String limitKey = "sms:limit:" + request.getPhone(); + + // 验证码缓存 key + String codeKey = "sms:code:" + request.getPhone(); + + // 获取当前请求次数 + String countStr = redisTemplate.opsForValue().get(limitKey); + int count = countStr != null ? Integer.parseInt(countStr) : 0; + + if (count > 3) { + return Response.error("验证码请求过于频繁,请稍后再试"); + } + + // 生成 4 位数字验证码 + String code = String.format("%04d", new Random().nextInt(10000)); + + try { + dySms.sendSms( + request.getPhone(), + templateCode, + scene, + Map.of("code", code) + ); + } catch (Exception e) { + return Response.error(e.getMessage()); + } + + // 缓存验证码(有效期 5 分钟) + redisTemplate.opsForValue().set(codeKey, code, Duration.ofMinutes(5)); + + // 缓存请求次数 + if (count == 0) { + // 初次请求,设置有效期为 5 分钟 + redisTemplate.opsForValue().set(limitKey, "1", Duration.ofMinutes(5)); + } else { + redisTemplate.opsForValue().increment(limitKey); + } + + return Response.success(); + } + + // 登陆 + @PostMapping("/login/phone") + public Response loginPhone(@Validated() @ModelAttribute LoginPhoneRequest request) { + // 验证码缓存 key + if (!Objects.equals(envConfig.getActive(), "dev")){ + String codeKey = "sms:code:" + request.getPhone(); + String code = redisTemplate.opsForValue().get(codeKey); + if (!Objects.equals(request.getCode(), code)){ + return Response.error("验证码错误"); + } + } + +// try { + // 获取用户openid + WxMaJscode2SessionResult wxInfoData = wxMaServiceUtils.getSessionInfo(request.getWx_code()); + if (wxInfoData == null) { + return Response.error("微信授权失败"); + } + + if (wxInfoData.getOpenid() == null) { + return Response.error("微信授权失败"); + } + + if (wxInfoData.getSessionKey() == null) { + return Response.error("微信授权失败"); + } + + // 用户登陆 + LoginDto g = userService.UserLoginWithMobile(request.getPhone()); + return Response.success(g); + +// } catch (Exception e) { +// return Response.error(e.getMessage()); +// } + } + } diff --git a/src/main/java/com/example/caseData/core/RedisConfiguration.java b/src/main/java/com/example/caseData/core/RedisConfiguration.java index 951be81..1289254 100644 --- a/src/main/java/com/example/caseData/core/RedisConfiguration.java +++ b/src/main/java/com/example/caseData/core/RedisConfiguration.java @@ -1,26 +1,25 @@ package com.example.caseData.core; import com.example.caseData.config.RedisConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.StringRedisSerializer; -import java.time.Duration; - @Configuration +@RequiredArgsConstructor public class RedisConfiguration { -// private final RedisConfig redisConfig; // 自动注入你的配置类 -// -// public RedisConfiguration(RedisConfig redisConfig) { -// this.redisConfig = redisConfig; -// } - + private final RedisConfig redisConfig; // 自动注入你的配置类 @Bean + @Qualifier("redisTemplate") public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); @@ -34,4 +33,31 @@ public class RedisConfiguration { return template; } + + @Bean + @Qualifier("prodRedisTemplate") + public RedisTemplate prodRedisTemplate(RedisConnectionFactory connectionFactory) { + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); + configuration.setHostName(redisConfig.getProdHost()); + configuration.setPort(redisConfig.getProdPort()); + configuration.setDatabase(Integer.parseInt(redisConfig.getProdDatabase())); + if (redisConfig.getProdPassword() != null && !redisConfig.getProdPassword().isEmpty()) { + configuration.setPassword(RedisPassword.of(redisConfig.getProdPassword())); + } + + LettuceConnectionFactory factory = new LettuceConnectionFactory(configuration); + factory.afterPropertiesSet(); + + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + + // 使用String序列化方式 + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + template.setKeySerializer(stringRedisSerializer); + template.setValueSerializer(stringRedisSerializer); + template.setHashKeySerializer(stringRedisSerializer); + template.setHashValueSerializer(stringRedisSerializer); + + return template; + } } diff --git a/src/main/java/com/example/caseData/extend/aliyun/DySms.java b/src/main/java/com/example/caseData/extend/aliyun/DySms.java new file mode 100644 index 0000000..27a750a --- /dev/null +++ b/src/main/java/com/example/caseData/extend/aliyun/DySms.java @@ -0,0 +1,77 @@ +package com.example.caseData.extend.aliyun; + +import com.aliyun.dysmsapi20170525.Client; +import com.aliyun.dysmsapi20170525.models.SendSmsRequest; +import com.aliyun.dysmsapi20170525.models.SendSmsResponse; +import com.aliyun.tea.TeaException; +import com.aliyun.teaopenapi.models.Config; +import com.aliyun.teautil.models.RuntimeOptions; +import com.example.caseData.config.DySmsConfig; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@Component +public class DySms { + @Resource + private DySmsConfig dySmsConfig; + + private static final String endpoint = "dysmsapi.aliyuncs.com"; + private static final String signName = "肝胆相照"; + + /** + * 创建短信客户端 + */ + private Client createClient() throws Exception { + Config config = new Config() + .setAccessKeyId(dySmsConfig.getAccessKey()) + .setAccessKeySecret(dySmsConfig.getAccessSecret()) + .setEndpoint(endpoint); + return new Client(config); + } + + /** + * 发送短信 + * + * @param phoneNumber 手机号 + * @param templateCode 模板CODE + * @param sceneDesc 场景说明,用于日志或调试 + * @param templateParam 模板参数 + */ + public void sendSms(String phoneNumber, String templateCode, String sceneDesc, Map templateParam) { + try { + Client client = createClient(); + ObjectMapper objectMapper = new ObjectMapper(); + String paramJson = objectMapper.writeValueAsString(templateParam); + + SendSmsRequest request = new SendSmsRequest() + .setPhoneNumbers(phoneNumber) + .setSignName(signName) + .setTemplateCode(templateCode) + .setTemplateParam(paramJson); + + // 只用标准的RuntimeOptions,不加任何自定义字段 + RuntimeOptions runtime = new RuntimeOptions(); + + SendSmsResponse response = client.sendSmsWithOptions(request, runtime); + + if (response.getBody() == null || !"OK".equals(response.getBody().getCode())) { + log.error("短信发送失败,手机号:{},场景:{},返回信息:{}", phoneNumber, sceneDesc, response.getBody() != null ? response.getBody().getMessage() : "无返回体"); + throw new RuntimeException("短信发送失败: " + (response.getBody() != null ? response.getBody().getMessage() : "无返回体")); + } else { + log.info("短信发送成功,手机号:{},场景:{},返回信息:{}", phoneNumber, sceneDesc, response.getBody().getMessage()); + } + + } catch (TeaException e) { + log.error("阿里云短信发送异常,手机号:{},场景:{},错误信息:{}", phoneNumber, sceneDesc, e.getMessage(), e); + throw new RuntimeException("阿里云短信发送异常", e); + } catch (Exception e) { + log.error("短信发送异常,手机号:{},场景:{},错误信息:{}", phoneNumber, sceneDesc, e.getMessage(), e); + throw new RuntimeException("短信发送异常", e); + } + } +} diff --git a/src/main/java/com/example/caseData/extend/weChat/WxMaRedisConfig.java b/src/main/java/com/example/caseData/extend/weChat/WxMaRedisConfig.java index 37410b1..c9a141a 100644 --- a/src/main/java/com/example/caseData/extend/weChat/WxMaRedisConfig.java +++ b/src/main/java/com/example/caseData/extend/weChat/WxMaRedisConfig.java @@ -2,35 +2,50 @@ package com.example.caseData.extend.weChat; import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.RedisTemplate; import java.util.concurrent.TimeUnit; @Slf4j public class WxMaRedisConfig extends WxMaDefaultConfigImpl { - private final StringRedisTemplate redisTemplate; + private final RedisTemplate redisTemplate; + private final RedisTemplate prodRedisTemplate; - public WxMaRedisConfig(StringRedisTemplate redisTemplate) { + public WxMaRedisConfig(RedisTemplate redisTemplate, RedisTemplate prodRedisTemplate) { this.redisTemplate = redisTemplate; + this.prodRedisTemplate = prodRedisTemplate; } @Override public String getAccessToken() { String redisKey = "wx:ma:access_token:" + this.getAppid(); - String token = redisTemplate.opsForValue().get(redisKey); + String token = (String) redisTemplate.opsForValue().get(redisKey); + if (token != null) { + return token; + } + + // 2. 再尝试从 prodRedis 获取 + token = (String) prodRedisTemplate.opsForValue().get(redisKey); if (token != null) { return token; } synchronized (this) { - token = redisTemplate.opsForValue().get(redisKey); + token = (String) redisTemplate.opsForValue().get(redisKey); + if (token != null) return token; + + token = (String) prodRedisTemplate.opsForValue().get(redisKey); if (token != null) return token; try { String newToken = super.getAccessToken(); // 强制刷新 - redisTemplate.opsForValue().set(redisKey, newToken, this.getExpiresTime() - 200L, TimeUnit.SECONDS); + long expireSeconds = this.getExpiresTime() - 200L; + + redisTemplate.opsForValue().set(redisKey, newToken, expireSeconds, TimeUnit.SECONDS); + prodRedisTemplate.opsForValue().set(redisKey, newToken, expireSeconds, TimeUnit.SECONDS); return newToken; } catch (Exception e) { log.error("获取access_token失败", e); @@ -42,6 +57,9 @@ public class WxMaRedisConfig extends WxMaDefaultConfigImpl { @Override public void updateAccessToken(String accessToken, int expiresInSeconds) { String redisKey = "wx:ma:access_token:" + this.getAppid(); - redisTemplate.opsForValue().set(redisKey, accessToken, expiresInSeconds - 200L, TimeUnit.SECONDS); + long expireSeconds = Math.max(expiresInSeconds - 200L, 1L); + + redisTemplate.opsForValue().set(redisKey, accessToken, expireSeconds, TimeUnit.SECONDS); + prodRedisTemplate.opsForValue().set(redisKey, accessToken, expireSeconds, TimeUnit.SECONDS); } } \ No newline at end of file diff --git a/src/main/java/com/example/caseData/extend/weChat/WxMaServiceConfig.java b/src/main/java/com/example/caseData/extend/weChat/WxMaServiceConfig.java index c2ffc40..6efcc26 100644 --- a/src/main/java/com/example/caseData/extend/weChat/WxMaServiceConfig.java +++ b/src/main/java/com/example/caseData/extend/weChat/WxMaServiceConfig.java @@ -3,20 +3,42 @@ package com.example.caseData.extend.weChat; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import com.example.caseData.config.WxMaConfig; +import com.example.caseData.service.RewardPointService; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.RedisTemplate; @Configuration public class WxMaServiceConfig { + private final RedisTemplate devRedis; + private final RedisTemplate prodRedis; + + @Resource + private WxMaConfig wxMaConfig; + + public WxMaServiceConfig( + @Qualifier("redisTemplate") RedisTemplate devRedis, + @Qualifier("prodRedisTemplate") RedisTemplate prodRedis + ) { + this.devRedis = devRedis; + this.prodRedis = prodRedis; + } + @Bean - public WxMaService wxMaService(WxMaConfig config, StringRedisTemplate redisTemplate) { - WxMaRedisConfig redisConfig = new WxMaRedisConfig(redisTemplate); - redisConfig.setAppid(config.getAppid()); - redisConfig.setSecret(config.getSecret()); + public WxMaService wxMaService() { + WxMaRedisConfig redisConfig = new WxMaRedisConfig(devRedis, prodRedis); + redisConfig.setAppid(wxMaConfig.getAppid()); + redisConfig.setSecret(wxMaConfig.getSecret()); WxMaServiceImpl service = new WxMaServiceImpl(); service.setWxMaConfig(redisConfig); return service; } + + + + + } diff --git a/src/main/java/com/example/caseData/request/publicRequest/GetPhoneCodeRequest.java b/src/main/java/com/example/caseData/request/publicRequest/GetPhoneCodeRequest.java new file mode 100644 index 0000000..24c890c --- /dev/null +++ b/src/main/java/com/example/caseData/request/publicRequest/GetPhoneCodeRequest.java @@ -0,0 +1,19 @@ +package com.example.caseData.request.publicRequest; + +import jakarta.validation.constraints.*; +import lombok.Data; + +@Data +public class GetPhoneCodeRequest { + @NotNull(message = "错误请求") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; + + /** + * 场景值:1登录 + */ + @NotNull(message = "错误请求") + @Min(value = 1, message = "错误请求") + @Max(value = 1, message = "错误请求") + private Integer scene; +} diff --git a/src/main/java/com/example/caseData/request/publicRequest/LoginPhoneRequest.java b/src/main/java/com/example/caseData/request/publicRequest/LoginPhoneRequest.java new file mode 100644 index 0000000..e2b6746 --- /dev/null +++ b/src/main/java/com/example/caseData/request/publicRequest/LoginPhoneRequest.java @@ -0,0 +1,16 @@ +package com.example.caseData.request.publicRequest; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Data +public class LoginPhoneRequest { + @NotEmpty(message = "错误请求") + private String phone; + + @NotEmpty(message = "错误请求") + private String code; + + @NotEmpty(message = "错误请求") + private String wx_code; +} diff --git a/src/main/java/com/example/caseData/service/UserService.java b/src/main/java/com/example/caseData/service/UserService.java index b335e99..d014533 100644 --- a/src/main/java/com/example/caseData/service/UserService.java +++ b/src/main/java/com/example/caseData/service/UserService.java @@ -106,7 +106,6 @@ public class UserService { return g; } - /** * 获取app用户数据 * @return UserModel @@ -231,4 +230,6 @@ public class UserService { return basicHospital; } + + } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bfee80e..71b3cbf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,7 +32,7 @@ spring: host: '139.155.127.177' port: 30002 password: gdxz2022&dj. - database: 9 + database: 11 # MyBatis-Plus 配置 @@ -50,6 +50,11 @@ oss: endpoint: oss-cn-beijing.aliyuncs.com custom-domain-name: https://caseplatform.oss-cn-beijing.aliyuncs.com +# [阿里大鱼短信] +dysms: + access-key: LTAI4GGygjsKhyBwvvC3CghV + access-secret: rcx7lO9kQxG10m8NqNPEfEtT9IS8EI + # jwt配置 jwt: sign-key: 123456899 # 私钥