|
|
|
@ -11,6 +11,8 @@ import org.jeecg.common.api.vo.Result; |
|
|
|
import org.jeecg.common.exception.JeecgBootException; |
|
|
|
import org.jeecg.common.system.util.AppletUserUtil; |
|
|
|
import org.jeecg.common.system.vo.AppletUser; |
|
|
|
import org.jeecg.common.util.RedisUtil; |
|
|
|
import org.jeecg.common.util.oss.OssBootUtil; |
|
|
|
import org.jeecg.modules.applet.entity.StatisticsVo; |
|
|
|
import org.jeecg.modules.applet.service.AppletApiWaterService; |
|
|
|
import org.jeecg.modules.common.IdUtils; |
|
|
|
@ -27,6 +29,7 @@ import org.springframework.beans.factory.annotation.Value; |
|
|
|
import org.springframework.http.HttpEntity; |
|
|
|
import org.springframework.http.HttpMethod; |
|
|
|
import org.springframework.http.ResponseEntity; |
|
|
|
import org.springframework.scheduling.annotation.Async; |
|
|
|
import org.springframework.stereotype.Service; |
|
|
|
import org.springframework.util.LinkedMultiValueMap; |
|
|
|
import org.springframework.util.MultiValueMap; |
|
|
|
@ -37,12 +40,16 @@ import java.awt.*; |
|
|
|
import java.awt.image.BufferedImage; |
|
|
|
import java.io.ByteArrayInputStream; |
|
|
|
import java.io.File; |
|
|
|
import java.io.InputStream; |
|
|
|
import java.math.BigDecimal; |
|
|
|
import java.net.URL; |
|
|
|
import java.nio.file.Files; |
|
|
|
import java.util.ArrayList; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.concurrent.CompletableFuture; |
|
|
|
import java.util.concurrent.ExecutorService; |
|
|
|
import java.util.concurrent.Executors; |
|
|
|
|
|
|
|
@Log4j2 |
|
|
|
@Service |
|
|
|
@ -59,6 +66,11 @@ public class AppletApiWaterServiceImpl implements AppletApiWaterService { |
|
|
|
@Autowired |
|
|
|
private WxHttpUtils wxHttpUtils; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private RedisUtil redisUtil; |
|
|
|
|
|
|
|
// 创建线程池用于异步处理 |
|
|
|
private final ExecutorService asyncExecutor = Executors.newFixedThreadPool(5); |
|
|
|
|
|
|
|
@Override |
|
|
|
public byte[] getInviteCode(){ |
|
|
|
@ -79,62 +91,173 @@ public class AppletApiWaterServiceImpl implements AppletApiWaterService { |
|
|
|
String xcxSharePage = appletConfigService.getContentByCode("xcxSharePage"); |
|
|
|
String backgroundImageUrl = appletConfigService.getContentByCode("qr_code_bg"); |
|
|
|
|
|
|
|
// 优化缓存策略:使用更精确的缓存key,包含所有影响因素(包括背景图片URL) |
|
|
|
String cacheKey = String.format("inviteCode:final:%s:%s:%s:%s", user.getId(), trial, xcxSharePage.hashCode(), backgroundImageUrl.hashCode()); |
|
|
|
String imageCacheKey = String.format("inviteCode:image:%s:%s:%s:%s", user.getId(), trial, xcxSharePage.hashCode(), backgroundImageUrl.hashCode()); |
|
|
|
|
|
|
|
// 首先检查是否有最终合成图片的缓存 |
|
|
|
byte[] cachedImage = (byte[]) redisUtil.get(imageCacheKey); |
|
|
|
if (cachedImage != null) { |
|
|
|
log.info("从缓存返回邀请码图片,用户ID: {}", user.getId()); |
|
|
|
return cachedImage; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查OSS中是否已存在最终图片 |
|
|
|
String finalPath = "invite/final/" + user.getId() + "_" + xcxSharePage + "_" + trial + ".jpg"; |
|
|
|
try { |
|
|
|
InputStream ossFile = OssBootUtil.getOssFile(finalPath, null); |
|
|
|
if (ossFile != null) { |
|
|
|
try { |
|
|
|
// 使用ByteArrayOutputStream读取InputStream中的所有字节 |
|
|
|
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream(); |
|
|
|
int nRead; |
|
|
|
byte[] data = new byte[1024]; |
|
|
|
while ((nRead = ossFile.read(data, 0, data.length)) != -1) { |
|
|
|
buffer.write(data, 0, nRead); |
|
|
|
} |
|
|
|
buffer.flush(); |
|
|
|
byte[] ossImageBytes = buffer.toByteArray(); |
|
|
|
|
|
|
|
// 将OSS中的图片缓存到Redis,设置24小时过期 |
|
|
|
redisUtil.set(imageCacheKey, ossImageBytes, 24 * 60 * 60); |
|
|
|
log.info("从OSS返回邀请码图片,用户ID: {}", user.getId()); |
|
|
|
return ossImageBytes; |
|
|
|
} finally { |
|
|
|
ossFile.close(); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.debug("OSS中未找到最终图片,需要重新生成,用户ID: {}", user.getId()); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// 检查是否有小程序码的缓存 |
|
|
|
String qrCodeCacheKey = String.format("inviteCode:qrcode:%s:%s:%s", user.getId(), trial, xcxSharePage.hashCode()); |
|
|
|
byte[] cachedQrCode = (byte[]) redisUtil.get(qrCodeCacheKey); |
|
|
|
|
|
|
|
byte[] qrCodeBytes; |
|
|
|
if (cachedQrCode != null) { |
|
|
|
log.info("使用缓存的小程序码,用户ID: {}", user.getId()); |
|
|
|
qrCodeBytes = cachedQrCode; |
|
|
|
} else { |
|
|
|
log.info("生成新的小程序码,用户ID: {}", user.getId()); |
|
|
|
qrCodeBytes = generateWxQrCode(user, xcxSharePage, trial); |
|
|
|
// 缓存小程序码,设置7天过期(小程序码相对稳定) |
|
|
|
redisUtil.set(qrCodeCacheKey, qrCodeBytes, 7 * 24 * 60 * 60); |
|
|
|
} |
|
|
|
|
|
|
|
// 准备微信API请求参数 |
|
|
|
String key = "shareId=" + user.getId(); |
|
|
|
Map<String, Object> param = new HashMap<>(); |
|
|
|
param.put("path", xcxSharePage + "?" + key); |
|
|
|
param.put("scene", user.getId()); |
|
|
|
param.put("width", 150); |
|
|
|
param.put("auto_color", false); |
|
|
|
param.put("env_version", trial); |
|
|
|
|
|
|
|
Map<String, Object> line_color = new HashMap<>(); |
|
|
|
line_color.put("r", 0); |
|
|
|
line_color.put("g", 0); |
|
|
|
line_color.put("b", 0); |
|
|
|
param.put("line_color", line_color); |
|
|
|
|
|
|
|
|
|
|
|
param.put("is_hyaline", true); |
|
|
|
|
|
|
|
// 获取微信小程序码 |
|
|
|
String accessToken = wxHttpUtils.getAccessToken(); |
|
|
|
String url = "https://api.weixin.qq.com/wxa/getwxacode?access_token=" + accessToken; |
|
|
|
|
|
|
|
// 请求微信API获取二维码图片数据 |
|
|
|
RestTemplate rest = new RestTemplate(); |
|
|
|
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>(); |
|
|
|
HttpEntity requestEntity = new HttpEntity(JSON.toJSONString(param), headers); |
|
|
|
ResponseEntity<byte[]> entity = rest.exchange(url, HttpMethod.POST, requestEntity, byte[].class, new Object[0]); |
|
|
|
byte[] qrCodeBytes = entity.getBody(); |
|
|
|
|
|
|
|
// return qrCodeBytes; |
|
|
|
return this.generateAndCombineImagesFromUrl2(qrCodeBytes, backgroundImageUrl); |
|
|
|
// 生成最终合成图片 |
|
|
|
byte[] finalImage = this.generateAndCombineImagesFromUrl2(qrCodeBytes, backgroundImageUrl); |
|
|
|
|
|
|
|
// 异步上传到OSS并缓存 |
|
|
|
uploadAndCacheAsync(finalImage, finalPath, imageCacheKey); |
|
|
|
|
|
|
|
return finalImage; |
|
|
|
} catch (Exception e) { |
|
|
|
e.printStackTrace(); |
|
|
|
log.error("生成邀请码失败,用户ID: {}", user.getId(), e); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 生成微信小程序码 |
|
|
|
*/ |
|
|
|
private byte[] generateWxQrCode(AppletUser user, String xcxSharePage, String trial) throws Exception { |
|
|
|
// 准备微信API请求参数 |
|
|
|
String key = "inviter=" + user.getId(); |
|
|
|
Map<String, Object> param = new HashMap<>(); |
|
|
|
param.put("path", xcxSharePage + "?" + key); |
|
|
|
param.put("scene", user.getId()); |
|
|
|
param.put("width", 150); |
|
|
|
param.put("auto_color", false); |
|
|
|
param.put("env_version", trial); |
|
|
|
|
|
|
|
Map<String, Object> line_color = new HashMap<>(); |
|
|
|
line_color.put("r", 0); |
|
|
|
line_color.put("g", 0); |
|
|
|
line_color.put("b", 0); |
|
|
|
param.put("line_color", line_color); |
|
|
|
param.put("is_hyaline", true); |
|
|
|
|
|
|
|
// 获取微信小程序码 |
|
|
|
String accessToken = wxHttpUtils.getAccessToken(); |
|
|
|
String url = "https://api.weixin.qq.com/wxa/getwxacode?access_token=" + accessToken; |
|
|
|
|
|
|
|
// 请求微信API获取二维码图片数据 |
|
|
|
RestTemplate rest = new RestTemplate(); |
|
|
|
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>(); |
|
|
|
HttpEntity requestEntity = new HttpEntity(JSON.toJSONString(param), headers); |
|
|
|
ResponseEntity<byte[]> entity = rest.exchange(url, HttpMethod.POST, requestEntity, byte[].class, new Object[0]); |
|
|
|
|
|
|
|
return entity.getBody(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 异步上传到OSS并缓存到Redis |
|
|
|
*/ |
|
|
|
private void uploadAndCacheAsync(byte[] imageBytes, String ossPath, String cacheKey) { |
|
|
|
// 使用线程池异步执行上传和缓存操作 |
|
|
|
CompletableFuture.runAsync(() -> { |
|
|
|
try { |
|
|
|
// 上传到OSS |
|
|
|
OssBootUtil.upload(new ByteArrayInputStream(imageBytes), ossPath); |
|
|
|
log.info("异步上传OSS完成,路径: {}", ossPath); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("异步上传OSS失败,路径: {}", ossPath, e); |
|
|
|
} |
|
|
|
}, asyncExecutor).thenRunAsync(() -> { |
|
|
|
try { |
|
|
|
// 缓存到Redis,设置24小时过期 |
|
|
|
redisUtil.set(cacheKey, imageBytes, 24 * 60 * 60); |
|
|
|
log.info("异步缓存完成,key: {}", cacheKey); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("异步缓存失败,key: {}", cacheKey, e); |
|
|
|
} |
|
|
|
}, asyncExecutor); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public byte[] generateAndCombineImagesFromUrl2(byte[] qrCodeImageByte, String backgroundUrl) { |
|
|
|
File file = null; |
|
|
|
try { |
|
|
|
|
|
|
|
int qr_code_y = appletConfigService.getContentByCodeAsInt("qr_code_y"); |
|
|
|
int qr_code_x = appletConfigService.getContentByCodeAsInt("qr_code_x"); |
|
|
|
// 缓存背景图片,避免重复下载 |
|
|
|
String bgCacheKey = "background_image:" + backgroundUrl.hashCode(); |
|
|
|
BufferedImage backgroundImage = (BufferedImage) redisUtil.get(bgCacheKey); |
|
|
|
|
|
|
|
if (backgroundImage == null) { |
|
|
|
// 从URL加载背景图像 |
|
|
|
URL backgroundImageUrl = new URL(backgroundUrl); |
|
|
|
backgroundImage = ImageIO.read(backgroundImageUrl); |
|
|
|
// 缓存背景图片,设置1小时过期 |
|
|
|
redisUtil.set(bgCacheKey, backgroundImage, 60 * 60); |
|
|
|
log.debug("背景图片已缓存"); |
|
|
|
} else { |
|
|
|
log.debug("使用缓存的背景图片"); |
|
|
|
} |
|
|
|
|
|
|
|
// 从字节数组加载小程序码图像 |
|
|
|
BufferedImage qrCodeImage = ImageIO.read(new ByteArrayInputStream(qrCodeImageByte)); |
|
|
|
|
|
|
|
// 从URL加载背景图像 |
|
|
|
URL backgroundImageUrl = new URL(backgroundUrl); |
|
|
|
BufferedImage backgroundImage = ImageIO.read(backgroundImageUrl); |
|
|
|
// 获取配置参数 |
|
|
|
int qr_code_y = appletConfigService.getContentByCodeAsInt("qr_code_y"); |
|
|
|
int qr_code_x = appletConfigService.getContentByCodeAsInt("qr_code_x"); |
|
|
|
|
|
|
|
// 创建一个新的BufferedImage来保存合并后的图像 |
|
|
|
Graphics2D g2d = backgroundImage.createGraphics(); |
|
|
|
// 创建一个新的BufferedImage来保存合并后的图像(复制背景图片) |
|
|
|
BufferedImage combinedImage = new BufferedImage( |
|
|
|
backgroundImage.getWidth(), |
|
|
|
backgroundImage.getHeight(), |
|
|
|
BufferedImage.TYPE_INT_RGB |
|
|
|
); |
|
|
|
|
|
|
|
Graphics2D g2d = combinedImage.createGraphics(); |
|
|
|
|
|
|
|
// 设置高质量渲染 |
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); |
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); |
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
|
|
|
|
|
|
|
// 绘制背景图像 |
|
|
|
g2d.drawImage(backgroundImage, 0, 0, null); |
|
|
|
|
|
|
|
int wh = backgroundImage.getWidth() / 3; |
|
|
|
|
|
|
|
@ -150,10 +273,9 @@ public class AppletApiWaterServiceImpl implements AppletApiWaterService { |
|
|
|
|
|
|
|
// 将合并后的图像保存到临时文件 |
|
|
|
file = File.createTempFile("combined_", ".png"); |
|
|
|
ImageIO.write(backgroundImage, "png", file); |
|
|
|
ImageIO.write(combinedImage, "png", file); |
|
|
|
|
|
|
|
// 上传到阿里云OSS |
|
|
|
// return this.uploadAliYunOss(Files.readAllBytes(file.toPath()), file.getName()); |
|
|
|
// 读取文件字节并返回 |
|
|
|
return Files.readAllBytes(file.toPath()); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("生成合并图片失败", e); |
|
|
|
@ -256,6 +378,16 @@ public class AppletApiWaterServiceImpl implements AppletApiWaterService { |
|
|
|
return appletWithdrawal; |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void withdrawSuccess(String id) { |
|
|
|
AppletWithdrawal appletWithdrawal = appletWithdrawalService.getById(id); |
|
|
|
if (appletWithdrawal == null) { |
|
|
|
throw new JeecgBootException("提现记录不存在"); |
|
|
|
} |
|
|
|
appletWithdrawal.setWithdrawStatus("1"); |
|
|
|
appletWithdrawalService.updateById(appletWithdrawal); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public StatisticsVo statistics() { |
|
|
|
|
|
|
|
|