From bcc17e7bd7d90636bab710de5a14264361ac46ed Mon Sep 17 00:00:00 2001 From: xpecya Date: Mon, 9 Dec 2024 17:28:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90h5=E5=89=8D=E7=AB=AF=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=88=86=E4=BA=AB=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/TagController.java | 22 +++ admin/src/main/resources/application-test.yml | 27 +++ mini/pom.xml | 29 ++++ .../reference/mini/MiniApplication.java | 42 +++++ .../mini/controller/WechatController.java | 155 ++++++++++++++++++ .../dto/wechat/WechatAccessTokenDTO.java | 53 ++++++ .../mini/model/dto/wechat/WechatShareDTO.java | 17 ++ .../model/dto/wechat/WechatTicketDTO.java | 49 ++++++ .../reference/mini/model/vo/WechatVO.java | 98 +++++++++++ .../reference/mini/service/HttpService.java | 67 ++++++++ mini/src/main/resources/application-test.yml | 20 +++ mini/src/main/resources/application.yml | 15 ++ 12 files changed, 594 insertions(+) create mode 100644 admin/src/main/resources/application-test.yml create mode 100644 mini/src/main/java/com/jinrui/reference/mini/MiniApplication.java create mode 100644 mini/src/main/java/com/jinrui/reference/mini/controller/WechatController.java create mode 100644 mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatAccessTokenDTO.java create mode 100644 mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatShareDTO.java create mode 100644 mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatTicketDTO.java create mode 100644 mini/src/main/java/com/jinrui/reference/mini/model/vo/WechatVO.java create mode 100644 mini/src/main/java/com/jinrui/reference/mini/service/HttpService.java create mode 100644 mini/src/main/resources/application-test.yml create mode 100644 mini/src/main/resources/application.yml diff --git a/admin/src/main/java/com/jinrui/reference/admin/controller/TagController.java b/admin/src/main/java/com/jinrui/reference/admin/controller/TagController.java index b2d8c33..5ac890d 100644 --- a/admin/src/main/java/com/jinrui/reference/admin/controller/TagController.java +++ b/admin/src/main/java/com/jinrui/reference/admin/controller/TagController.java @@ -79,4 +79,26 @@ public class TagController { return tagService.queryTag(parent, needChildren, keyword, exclude, page, size, orderBy, direction); } + + @GetMapping("/source") + public PageObject querySource(@RequestHeader("auth-token") String token, + @RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "page", required = false, defaultValue = "1") int page, + @RequestParam(value = "size", required = false, defaultValue = "10") int size, + @RequestParam(value = "orderBy", required = false, defaultValue = "id") String orderBy, + @RequestParam(value = "direction", required = false, defaultValue = "asc") String direction) { + return queryTag(token, 1L, false, keyword, null, page, size, orderBy, direction); + } + + @GetMapping("/diy") + public PageObject queryDiy(@RequestHeader("auth-token") String token, + @RequestParam(value = "parent", required = false) Long parent, + @RequestParam(value = "needChildren", required = false, defaultValue = "false") boolean needChildren, + @RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "page", required = false, defaultValue = "1") int page, + @RequestParam(value = "size", required = false, defaultValue = "10") int size, + @RequestParam(value = "orderBy", required = false, defaultValue = "id") String orderBy, + @RequestParam(value = "direction", required = false, defaultValue = "asc") String direction) { + return queryTag(token, 1L, false, keyword, null, page, size, orderBy, direction); + } } diff --git a/admin/src/main/resources/application-test.yml b/admin/src/main/resources/application-test.yml new file mode 100644 index 0000000..02f6cc4 --- /dev/null +++ b/admin/src/main/resources/application-test.yml @@ -0,0 +1,27 @@ +server: + port: 13579 + compression: + enabled: true + servlet: + context-path: /admin +oss: + ak: LTAI5t8z9QNdCG6b54mDgx8p + sk: F3M41hTgH8g99ZgVWyelj42825YbZM + roleArn: acs:ram::1647420045565932:role/ramoss + endPoint: oss-cn-hangzhou.aliyuncs.com + region: oss-cn-hangzhou + bucketName: lengfeng-test + baseUrl: https://lengfeng-test.oss-cn-hangzhou.aliyuncs.com + sts: + endPoint: sts.cn-hangzhou.aliyuncs.com +spring: + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://192.168.0.107:3306/reference?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true + username: financial + password: financial_8000 +redis: + host: 127.0.0.1 + port: 6379 + password: Xgf_redis diff --git a/mini/pom.xml b/mini/pom.xml index 870b413..b1a4945 100644 --- a/mini/pom.xml +++ b/mini/pom.xml @@ -17,4 +17,33 @@ UTF-8 + + + com.jinrui + core + 0.0.1-SNAPSHOT + + + redis.clients + jedis + ${jedis.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + diff --git a/mini/src/main/java/com/jinrui/reference/mini/MiniApplication.java b/mini/src/main/java/com/jinrui/reference/mini/MiniApplication.java new file mode 100644 index 0000000..d7ad2c7 --- /dev/null +++ b/mini/src/main/java/com/jinrui/reference/mini/MiniApplication.java @@ -0,0 +1,42 @@ +package com.jinrui.reference.mini; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +@SpringBootApplication(scanBasePackages = { + "com.jinrui.reference.mini", + "com.jinrui.reference.core" +}) +@MapperScan({ + "com.jinrui.reference.admin.mapper", + "com.jinrui.reference.core.mapper"}) +public class MiniApplication { + + public static void main(String[] args) { + SpringApplication.run(MiniApplication.class, args); + } + + @Bean + public JedisPool jedisPool(@Value("${redis.host}") String host, + @Value("${redis.port}") int port, + @Value("${redis.timeout:1000}") int timeout, + @Value("${redis.password:}") String password) { + if (StringUtils.hasText(password)) { + JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); + return new JedisPool(jedisPoolConfig, host, port, timeout, password); + } + return new JedisPool(host, port); + } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/mini/src/main/java/com/jinrui/reference/mini/controller/WechatController.java b/mini/src/main/java/com/jinrui/reference/mini/controller/WechatController.java new file mode 100644 index 0000000..eba9018 --- /dev/null +++ b/mini/src/main/java/com/jinrui/reference/mini/controller/WechatController.java @@ -0,0 +1,155 @@ +package com.jinrui.reference.mini.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jinrui.reference.core.model.vo.ResultObject; +import com.jinrui.reference.mini.model.dto.wechat.WechatAccessTokenDTO; +import com.jinrui.reference.mini.model.dto.wechat.WechatShareDTO; +import com.jinrui.reference.mini.model.dto.wechat.WechatTicketDTO; +import com.jinrui.reference.mini.model.vo.WechatVO; +import com.jinrui.reference.mini.service.HttpService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import javax.annotation.PostConstruct; +import java.net.URI; +import java.util.Map; + +@RestController +@RequestMapping("/wechat") +public class WechatController { + + private static final String ACCESS_TOKEN_URL_TEMPLATE = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"; + private static final String JS_API_URL_TEMPLATE = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"; + private static final String ACCESS_TOKEN_REDIS_KEY = "access-token"; + private static final Logger log = LoggerFactory.getLogger(WechatController.class); + + private final JedisPool jedisPool; + private final URI accessTokenURI; + private final HttpService httpService; + private final String appId; + private final ObjectMapper objectMapper; + + public WechatController(JedisPool jedisPool, + @Value("${wechat.appId}") String appId, + @Value("${wechat.secret}") String secret, + HttpService httpService, + ObjectMapper objectMapper) { + this.jedisPool = jedisPool; + this.httpService = httpService; + this.appId = appId; + this.objectMapper = objectMapper; + this.accessTokenURI = URI.create(String.format(ACCESS_TOKEN_URL_TEMPLATE, appId, secret)); + } + + @GetMapping("/share") + public ResultObject getAccessToken(@RequestParam("url") String url) { + if (!StringUtils.hasText(url)) { + return ResultObject.failed("分享链接为空!"); + } + WechatVO wechatVO = getWechatVO(url); + if (wechatVO != null) { + return ResultObject.success(wechatVO); + } + return ResultObject.failed(500, "服务器错误,请联系系统管理员!"); + } + + @PostMapping("/share") + public ResultObject share(@RequestBody WechatShareDTO wechatShareDTO) { + String url = wechatShareDTO.getUrl(); + return getAccessToken(url); + } + + @PostConstruct + private void refreshToken() { + new Thread(() -> { + doRefreshToken(); + try { + Thread.sleep(3_600_000); + } catch (InterruptedException e) { + log.error("refresh token sleep interrupted!", e); + } + refreshToken(); + }).start(); + } + + private void doRefreshToken() { + WechatAccessTokenDTO wechatAccessTokenDTO = httpService.getForObject(accessTokenURI, WechatAccessTokenDTO.class); + if (wechatAccessTokenDTO == null) { + log.error("微信获取AccessToken异常!请检查日志!"); + return; + } + String accessToken = wechatAccessTokenDTO.getAccessToken(); + if (!StringUtils.hasText(accessToken)) { + int errorCode = wechatAccessTokenDTO.getErrorCode(); + String errorMessage = wechatAccessTokenDTO.getErrorMessage(); + log.error("微信没有返回accessToken!errorCode: {}, errorMessage: {}", errorCode, errorMessage); + return; + } + Integer expiresIn = wechatAccessTokenDTO.getExpiresIn(); + if (expiresIn == null) { + log.info("微信AccessToken: {}, expiresIn: 没有返回,给予默认值7200", accessToken); + // 按照微信文档说明,默认给7200 + expiresIn = 7200; + } else { + log.info("微信AccessToken: {}, expiresIn: {}", accessToken, expiresIn); + } + + // 少1秒确保这个Token获取时一定有效 + long redisExpire = expiresIn - 1; + try (Jedis jedis = jedisPool.getResource()) { + jedis.set(ACCESS_TOKEN_REDIS_KEY, accessToken); + jedis.expire(ACCESS_TOKEN_REDIS_KEY, redisExpire); + } + } + + private String getToken() { + return getToken(true); + } + + private String getToken(boolean retry) { + try (Jedis jedis = jedisPool.getResource()) { + String token = jedis.get(ACCESS_TOKEN_REDIS_KEY); + if (StringUtils.hasText(token)) { + return token; + } + } + if (retry) { + doRefreshToken(); + return getToken(false); + } + log.error("重试后依然没有获取到微信AccessToken,请检查日志!"); + return null; + } + + private WechatVO getWechatVO(String url) { + String accessToken = getToken(); + if (!StringUtils.hasText(accessToken)) { + return null; + } + URI ticketURI = URI.create(String.format(JS_API_URL_TEMPLATE, accessToken)); + WechatTicketDTO wechatTicketDTO = httpService.getForObject(ticketURI, WechatTicketDTO.class); + if (wechatTicketDTO == null) { + log.error("获取微信Ticket异常!请检查日志!"); + return null; + } + String ticket = wechatTicketDTO.getTicket(); + if (!StringUtils.hasText(ticket)) { + log.error("获取微信Ticket异常!请检查日志!"); + return null; + } + WechatVO wechatVO = new WechatVO(); + wechatVO.setAppId(appId); + wechatVO.setSignature(ticket, url); + return wechatVO; + } +} diff --git a/mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatAccessTokenDTO.java b/mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatAccessTokenDTO.java new file mode 100644 index 0000000..7d6f9a2 --- /dev/null +++ b/mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatAccessTokenDTO.java @@ -0,0 +1,53 @@ +package com.jinrui.reference.mini.model.dto.wechat; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 微信access token返回值 + */ +public class WechatAccessTokenDTO { + + @JsonProperty("errcode") + private int errorCode; + + @JsonProperty("errmsg") + private String errorMessage; + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("expires_in") + private Integer expiresIn; + + public int getErrorCode() { + return errorCode; + } + + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public Integer getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } +} diff --git a/mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatShareDTO.java b/mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatShareDTO.java new file mode 100644 index 0000000..480113c --- /dev/null +++ b/mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatShareDTO.java @@ -0,0 +1,17 @@ +package com.jinrui.reference.mini.model.dto.wechat; + +/** + * 微信分享DTO + */ +public class WechatShareDTO { + + private String url; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatTicketDTO.java b/mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatTicketDTO.java new file mode 100644 index 0000000..513de59 --- /dev/null +++ b/mini/src/main/java/com/jinrui/reference/mini/model/dto/wechat/WechatTicketDTO.java @@ -0,0 +1,49 @@ +package com.jinrui.reference.mini.model.dto.wechat; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class WechatTicketDTO { + + @JsonProperty("errcode") + private int errorCode; + + @JsonProperty("errmsg") + private String errorMessage; + + private String ticket; + + @JsonProperty("expires_in") + private Integer expiresIn; + + public int getErrorCode() { + return errorCode; + } + + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getTicket() { + return ticket; + } + + public void setTicket(String ticket) { + this.ticket = ticket; + } + + public Integer getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } +} diff --git a/mini/src/main/java/com/jinrui/reference/mini/model/vo/WechatVO.java b/mini/src/main/java/com/jinrui/reference/mini/model/vo/WechatVO.java new file mode 100644 index 0000000..a7383c5 --- /dev/null +++ b/mini/src/main/java/com/jinrui/reference/mini/model/vo/WechatVO.java @@ -0,0 +1,98 @@ +package com.jinrui.reference.mini.model.vo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Formatter; +import java.util.UUID; + +/** + * 微信AccessToken返回值 + */ +public class WechatVO { + + private static final Logger log = LoggerFactory.getLogger(WechatVO.class); + + /** + * app唯一标识符 + */ + private String appId; + + /** + * 生成签名时间戳 + */ + private Long timestamp; + + /** + * 签名随机字符串 + */ + private String nonceStr; + + /** + * 签名 + */ + private String signature; + + public WechatVO() { + long now = System.currentTimeMillis(); + this.timestamp = now / 1000; + this.nonceStr = UUID.randomUUID().toString().replaceAll("-", ""); + } + + public String getAppId() { + return appId; + } + + public Long getTimestamp() { + return timestamp; + } + + public String getNonceStr() { + return nonceStr; + } + + public String getSignature() { + return signature; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public void setNonceStr(String nonceStr) { + this.nonceStr = nonceStr; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + public synchronized void setSignature(String ticket, String url) { + String signatureTemplate = "jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s"; + String rawSignature = String.format(signatureTemplate, ticket, nonceStr, timestamp, url); + byte[] rawBytes = rawSignature.getBytes(StandardCharsets.UTF_8); + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); + byte[] digest = messageDigest.digest(rawBytes); + this.signature = byteToHex(digest); + } catch (NoSuchAlgorithmException e) { + log.error("找不到SHA-1方法!", e); + } + } + + private String byteToHex(final byte[] hash) { + try (Formatter formatter = new Formatter()) { + for (byte b : hash) { + formatter.format("%02x", b); + } + return formatter.toString(); + } + } +} diff --git a/mini/src/main/java/com/jinrui/reference/mini/service/HttpService.java b/mini/src/main/java/com/jinrui/reference/mini/service/HttpService.java new file mode 100644 index 0000000..f82be3d --- /dev/null +++ b/mini/src/main/java/com/jinrui/reference/mini/service/HttpService.java @@ -0,0 +1,67 @@ +package com.jinrui.reference.mini.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class HttpService { + + private static final Logger log = LoggerFactory.getLogger(HttpService.class); + + private final RestTemplate restTemplate; + + public HttpService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public T getForObject(URI uri, Class responseClass) { + ResponseEntity responseEntity = restTemplate.getForEntity(uri, responseClass); + int statusCode = responseEntity.getStatusCodeValue(); + if (statusCode == 200) { + return responseEntity.getBody(); + } + logEntity(responseEntity); + if (responseEntity.hasBody()) { + return responseEntity.getBody(); + } + return null; + } + + private void logEntity(ResponseEntity responseEntity) { + HttpStatus httpStatus = responseEntity.getStatusCode(); + int statusCode = httpStatus.value(); + String statusPhrase = httpStatus.getReasonPhrase(); + log.error("{} {}", statusCode, statusPhrase); + HttpHeaders httpHeaders = responseEntity.getHeaders(); + for (Map.Entry> entry : httpHeaders.entrySet()) { + String key = entry.getKey(); + if (!StringUtils.hasText(key)) { + continue; + } + List values = entry.getValue(); + if (CollectionUtils.isEmpty(values)) { + continue; + } + int length = values.size(); + String valueString; + if (length == 1) { + valueString = values.get(0); + } else { + valueString = String.join(",", values); + } + log.error("{}: {}", key, valueString); + } + } +} diff --git a/mini/src/main/resources/application-test.yml b/mini/src/main/resources/application-test.yml new file mode 100644 index 0000000..3a4d47e --- /dev/null +++ b/mini/src/main/resources/application-test.yml @@ -0,0 +1,20 @@ +server: + port: 24680 + compression: + enabled: true + servlet: + context-path: /mini +spring: + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/reference?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true + username: root + password: Aa123456@ +redis: + host: 127.0.0.1 + port: 6379 + password: Xgf_redis +wechat: + appId: wxf76041de61c68a94 + secret: 33437d0005485222a44d6fbe73acd503 diff --git a/mini/src/main/resources/application.yml b/mini/src/main/resources/application.yml new file mode 100644 index 0000000..3d198d9 --- /dev/null +++ b/mini/src/main/resources/application.yml @@ -0,0 +1,15 @@ +server: + port: 24680 +spring: + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/reference?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true + username: root + password: Aa123456@ +redis: + host: 127.0.0.1 + port: 6379 +wechat: + appId: wxf76041de61c68a94 + secret: 33437d0005485222a44d6fbe73acd503