完成h5前端微信分享功能

This commit is contained in:
xpecya 2024-12-09 17:28:14 +08:00
parent 802885b73a
commit bcc17e7bd7
12 changed files with 594 additions and 0 deletions

View File

@ -79,4 +79,26 @@ public class TagController {
return tagService.queryTag(parent, needChildren, keyword, exclude, page, size, orderBy, direction);
}
@GetMapping("/source")
public PageObject<TagVO> 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<TagVO> 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);
}
}

View File

@ -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

View File

@ -17,4 +17,33 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.jinrui</groupId>
<artifactId>core</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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();
}
}

View File

@ -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<WechatVO> 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<WechatVO> 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("微信没有返回accessTokenerrorCode: {}, 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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&timestamp=%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();
}
}
}

View File

@ -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> T getForObject(URI uri, Class<T> responseClass) {
ResponseEntity<T> 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<String, List<String>> entry : httpHeaders.entrySet()) {
String key = entry.getKey();
if (!StringUtils.hasText(key)) {
continue;
}
List<String> 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);
}
}
}

View File

@ -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

View File

@ -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