|
|
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollectionUtil;
|
|
|
import cn.hutool.core.img.FontUtil;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.google.zxing.BarcodeFormat;
|
|
|
@@ -44,12 +45,17 @@ import org.dromara.system.mapper.GameEventMapper;
|
|
|
import org.dromara.system.config.FileUploadConfig;
|
|
|
import org.dromara.system.service.*;
|
|
|
import org.dromara.system.domain.vo.SysOssVo;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.context.annotation.Lazy;
|
|
|
import org.springframework.scheduling.annotation.Async;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.util.CollectionUtils;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.net.HttpURLConnection;
|
|
|
+import java.net.URL;
|
|
|
|
|
|
import javax.imageio.ImageIO;
|
|
|
import java.awt.*;
|
|
|
@@ -59,11 +65,13 @@ import java.awt.image.BufferedImage;
|
|
|
import java.io.*;
|
|
|
import java.nio.file.Files;
|
|
|
import java.nio.file.Paths;
|
|
|
+import java.time.Duration;
|
|
|
import java.util.*;
|
|
|
import java.util.List;
|
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
import java.util.concurrent.Executors;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
|
import java.util.stream.Collectors;
|
|
|
import java.util.zip.ZipEntry;
|
|
|
@@ -108,6 +116,33 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
"424D" // BMP
|
|
|
};
|
|
|
|
|
|
+ /**
|
|
|
+ * 更新赛事小程序码
|
|
|
+ * 提供给前端更新按钮调用,重新生成赛事的小程序码
|
|
|
+ * @param eventId 赛事ID
|
|
|
+ * @return 更新结果
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public Boolean updateEventQrCode(Long eventId) {
|
|
|
+ try {
|
|
|
+ // 查询赛事信息
|
|
|
+ GameEvent gameEvent = baseMapper.selectById(eventId);
|
|
|
+ if (gameEvent == null) {
|
|
|
+ log.error("赛事不存在,eventId: {}", eventId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重新生成二维码
|
|
|
+ generateEventQrCode(gameEvent);
|
|
|
+
|
|
|
+ log.info("成功更新赛事[{}]的微信小程序二维码", gameEvent.getEventName());
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("更新赛事微信小程序二维码失败: {}, eventId: {}", e.getMessage(), eventId, e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 预览框尺寸(固定)
|
|
|
private static final int previewWidth = 600;
|
|
|
private static final int previewHeight = 400;
|
|
|
@@ -133,8 +168,9 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
@Override
|
|
|
public TableDataInfo<GameEventVo> queryPageList(GameEventBo bo, PageQuery pageQuery) {
|
|
|
LambdaQueryWrapper<GameEvent> lqw = buildQueryWrapper(bo);
|
|
|
+ Page<GameEventVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
|
|
// 使用带数据权限注解的方法
|
|
|
- Page<GameEventVo> result = baseMapper.selectPageEventList(pageQuery.build(), lqw);
|
|
|
+// Page<GameEventVo> result = baseMapper.selectPageEventList(pageQuery.build(), lqw);
|
|
|
return TableDataInfo.build(result);
|
|
|
}
|
|
|
|
|
|
@@ -148,7 +184,8 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
public List<GameEventVo> queryList(GameEventBo bo) {
|
|
|
LambdaQueryWrapper<GameEvent> lqw = buildQueryWrapper(bo);
|
|
|
// 使用带数据权限注解的方法
|
|
|
- return baseMapper.selectEventList(lqw);
|
|
|
+// return baseMapper.selectEventList(lqw);
|
|
|
+ return baseMapper.selectVoList(lqw);
|
|
|
}
|
|
|
|
|
|
private LambdaQueryWrapper<GameEvent> buildQueryWrapper(GameEventBo bo) {
|
|
|
@@ -177,11 +214,202 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
validEntityBeforeSave(add);
|
|
|
boolean flag = baseMapper.insert(add) > 0;
|
|
|
if (flag) {
|
|
|
+ // 生成微信小程序二维码并设置到赛事链接
|
|
|
+ generateEventQrCode(add);
|
|
|
return add.getEventId();
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+ // 微信相关常量
|
|
|
+ private static final String ACCESS_TOKEN_KEY = "wechat:access_token";
|
|
|
+ @Value("${wechat.miniapp.access-token-expires:}")
|
|
|
+ private long ACCESS_TOKEN_EXPIRE; // access_token过期时间,7000秒(比微信的7200秒少200秒)
|
|
|
+ @Value("${wechat.miniapp.appid:}")
|
|
|
+ private String APP_ID; // 实际使用时替换为真实AppID
|
|
|
+ @Value("${wechat.miniapp.secret:}")
|
|
|
+ private String APP_SECRET; // 实际使用时替换为真实AppSecret
|
|
|
+ @Value("${wechat.miniapp.page:}")
|
|
|
+ private String page;
|
|
|
+ private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
|
|
|
+ private static final String GET_WXACODE_UNLIMIT_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成赛事微信小程序二维码
|
|
|
+ * @param gameEvent 赛事信息
|
|
|
+ */
|
|
|
+ private void generateEventQrCode(GameEvent gameEvent) {
|
|
|
+ try {
|
|
|
+ // 获取微信access_token
|
|
|
+ String accessToken = getAccessToken();
|
|
|
+ if (accessToken == null) {
|
|
|
+ log.error("获取微信access_token失败,无法生成小程序码");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建scene参数,包含赛事ID
|
|
|
+ String scene = "eventId=" + gameEvent.getEventId();
|
|
|
+
|
|
|
+ // 页面路径,实际使用时需要替换为真实路径
|
|
|
+// String page = "pages/index/index";
|
|
|
+
|
|
|
+ // 调用微信接口生成小程序码
|
|
|
+ byte[] qrCodeBytes = getWxaCodeUnlimit(accessToken, scene, page);
|
|
|
+ if (qrCodeBytes == null) {
|
|
|
+ log.error("调用微信接口生成小程序码失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 直接使用自定义的ByteArrayMultipartFile包装二维码字节数组
|
|
|
+ String fileName = "event_" + gameEvent.getEventId() + "_" + System.currentTimeMillis() + ".png";
|
|
|
+ MultipartFile multipartFile = new ByteArrayMultipartFile("file", fileName, "image/png", qrCodeBytes);
|
|
|
+
|
|
|
+ // 使用sysOssService上传文件并获取URL
|
|
|
+ SysOssVo ossVo = sysOssService.upload(multipartFile);
|
|
|
+ String qrCodeUrl = ossVo.getUrl();
|
|
|
+
|
|
|
+ // 更新赛事链接
|
|
|
+ gameEvent.setEventUrl(qrCodeUrl);
|
|
|
+ baseMapper.updateById(gameEvent);
|
|
|
+
|
|
|
+ log.info("成功为赛事[{}]生成微信小程序二维码,URL: {}", gameEvent.getEventName(), qrCodeUrl);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("生成赛事微信小程序二维码失败: {}", e.getMessage(), e);
|
|
|
+ // 不抛出异常,避免影响赛事创建
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取微信access_token(带缓存)
|
|
|
+ * 注意:access_token是调用微信接口的凭证,与生成的小程序码有效期无关
|
|
|
+ * 微信小程序码默认是永久有效的,除非手动失效
|
|
|
+ * @return access_token
|
|
|
+ */
|
|
|
+ private String getAccessToken() {
|
|
|
+ try {
|
|
|
+ // 先尝试从Redis获取缓存的access_token
|
|
|
+ String accessToken = RedisUtils.getCacheObject(ACCESS_TOKEN_KEY);
|
|
|
+ if (StringUtils.isNotBlank(accessToken)) {
|
|
|
+ log.info("从缓存获取access_token成功");
|
|
|
+ return accessToken;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 缓存未命中,调用微信接口获取
|
|
|
+ String url = String.format(ACCESS_TOKEN_URL, APP_ID, APP_SECRET);
|
|
|
+ String response = sendGetRequest(url);
|
|
|
+
|
|
|
+ // 解析响应
|
|
|
+ Map<String, Object> result = JsonUtils.parseMap(response);
|
|
|
+ if (result.containsKey("access_token")) {
|
|
|
+ accessToken = (String) result.get("access_token");
|
|
|
+ // 缓存access_token,设置过期时间
|
|
|
+ long expireInSeconds = TimeUnit.SECONDS.convert(ACCESS_TOKEN_EXPIRE, TimeUnit.MILLISECONDS);
|
|
|
+ RedisUtils.setCacheObject(ACCESS_TOKEN_KEY, accessToken, Duration.ofSeconds(expireInSeconds));
|
|
|
+ log.info("获取并缓存access_token成功");
|
|
|
+ return accessToken;
|
|
|
+ } else {
|
|
|
+ log.error("获取access_token失败: {}", response);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取access_token异常: {}", e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用微信接口生成小程序码
|
|
|
+ * @param accessToken access_token
|
|
|
+ * @param scene 场景值
|
|
|
+ * @param page 页面路径
|
|
|
+ * @return 小程序码图片字节数组
|
|
|
+ */
|
|
|
+ private byte[] getWxaCodeUnlimit(String accessToken, String scene, String page) {
|
|
|
+ try {
|
|
|
+ String url = String.format(GET_WXACODE_UNLIMIT_URL, accessToken);
|
|
|
+
|
|
|
+ // 构建请求参数
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
+ params.put("scene", scene);
|
|
|
+ params.put("page", page);
|
|
|
+ params.put("width", 430);
|
|
|
+ params.put("auto_color", false);
|
|
|
+ Map<String, Object> lineColor = new HashMap<>();
|
|
|
+ lineColor.put("r", 0);
|
|
|
+ lineColor.put("g", 0);
|
|
|
+ lineColor.put("b", 0);
|
|
|
+ params.put("line_color", lineColor);
|
|
|
+ params.put("is_hyaline", false); // 是否透明底色
|
|
|
+ params.put("env_version", "release"); // 默认使用正式版
|
|
|
+
|
|
|
+ // 发送POST请求
|
|
|
+ return sendPostRequest(url, JsonUtils.toJsonString(params));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("调用微信接口生成小程序码异常: {}", e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送HTTP GET请求
|
|
|
+ * @param url 请求URL
|
|
|
+ * @return 响应结果
|
|
|
+ */
|
|
|
+ private String sendGetRequest(String url) throws Exception {
|
|
|
+ HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
|
|
+ connection.setRequestMethod("GET");
|
|
|
+ connection.setConnectTimeout(5000);
|
|
|
+ connection.setReadTimeout(5000);
|
|
|
+ connection.setDoInput(true);
|
|
|
+
|
|
|
+ try (InputStream inputStream = connection.getInputStream();
|
|
|
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
|
|
+ byte[] buffer = new byte[1024];
|
|
|
+ int len;
|
|
|
+ while ((len = inputStream.read(buffer)) != -1) {
|
|
|
+ outputStream.write(buffer, 0, len);
|
|
|
+ }
|
|
|
+ return outputStream.toString("UTF-8");
|
|
|
+ } finally {
|
|
|
+ connection.disconnect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送HTTP POST请求
|
|
|
+ * @param url 请求URL
|
|
|
+ * @param jsonData JSON数据
|
|
|
+ * @return 响应字节数组
|
|
|
+ */
|
|
|
+ private byte[] sendPostRequest(String url, String jsonData) throws Exception {
|
|
|
+ HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
|
|
+ connection.setRequestMethod("POST");
|
|
|
+ connection.setConnectTimeout(5000);
|
|
|
+ connection.setReadTimeout(5000);
|
|
|
+ connection.setDoOutput(true);
|
|
|
+ connection.setDoInput(true);
|
|
|
+ connection.setRequestProperty("Content-Type", "application/json");
|
|
|
+
|
|
|
+ // 发送请求体
|
|
|
+ try (OutputStream outputStream = connection.getOutputStream()) {
|
|
|
+ outputStream.write(jsonData.getBytes("UTF-8"));
|
|
|
+ outputStream.flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取响应
|
|
|
+ try (InputStream inputStream = connection.getInputStream();
|
|
|
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
|
|
+ byte[] buffer = new byte[1024];
|
|
|
+ int len;
|
|
|
+ while ((len = inputStream.read(buffer)) != -1) {
|
|
|
+ outputStream.write(buffer, 0, len);
|
|
|
+ }
|
|
|
+ return outputStream.toByteArray();
|
|
|
+ } finally {
|
|
|
+ connection.disconnect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 修改赛事基本信息
|
|
|
*
|
|
|
@@ -231,7 +459,7 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
if (defaultEvent != null) {
|
|
|
RedisUtils.deleteObject(GameEventConstant.DEFAULT_EVENT);
|
|
|
}
|
|
|
- return baseMapper.deleteByIds(ids) > 0;
|
|
|
+ return result;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -1650,14 +1878,14 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
if (zipFile.exists()) {
|
|
|
long fileSize = zipFile.length();
|
|
|
log.info("ZIP文件大小: {} bytes ({} MB)", fileSize, fileSize / 1024 / 1024);
|
|
|
-
|
|
|
+
|
|
|
// 使用智能上传(自动选择直接上传或分片上传)
|
|
|
SysOssVo ossVo = sysOssService.smartUpload(zipFile);
|
|
|
log.info("ZIP文件已上传到OSS,ossId: {}, url: {}", ossVo.getOssId(), ossVo.getUrl());
|
|
|
-
|
|
|
+
|
|
|
// 使用OSS版本完成任务
|
|
|
gameBibTaskService.completeTaskWithOss(taskId, zipFilePath, ossVo.getOssId());
|
|
|
-
|
|
|
+
|
|
|
// 删除本地ZIP文件以节省空间
|
|
|
if (zipFile.delete()) {
|
|
|
log.info("本地ZIP文件已删除: {}", zipFilePath);
|
|
|
@@ -1817,11 +2045,11 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
private String generateQRCodeData(GameAthleteVo athlete) {
|
|
|
StringBuilder joinProject = new StringBuilder();
|
|
|
StringJoiner joiner = new StringJoiner(",");
|
|
|
-
|
|
|
+
|
|
|
// 添加调试日志
|
|
|
- log.debug("生成二维码数据 - 运动员: {}, projectValue: {}, projectList: {}",
|
|
|
+ log.debug("生成二维码数据 - 运动员: {}, projectValue: {}, projectList: {}",
|
|
|
athlete.getName(), athlete.getProjectValue(), athlete.getProjectList());
|
|
|
-
|
|
|
+
|
|
|
// 检查projectList是否为null或空
|
|
|
if (athlete.getProjectList() == null || athlete.getProjectList().isEmpty()) {
|
|
|
log.warn("运动员 {} 的参与项目列表为空,projectValue: {}", athlete.getName(), athlete.getProjectValue());
|
|
|
@@ -1844,24 +2072,24 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
athlete.getAge(),
|
|
|
athlete.getTeamName() != null ? athlete.getTeamName() : "未知队伍");
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
Map<Long, String> projectMap = gameEventProjectService.queryNameByEventIdAndProjectIds(athlete.getEventId(), athlete.getProjectList());
|
|
|
log.debug("项目映射结果: {}", projectMap);
|
|
|
-
|
|
|
+
|
|
|
// 添加额外的null检查
|
|
|
if (projectMap == null) {
|
|
|
log.warn("项目映射结果为空,返回空字符串,逻辑上不可能为空,如果有空项目,应该在运动员表中删除");
|
|
|
projectMap = new HashMap<>();
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
for (Long projectId : athlete.getProjectList()) {
|
|
|
String projectName = projectMap.get(projectId) != null ? projectMap.get(projectId).toString() : "未知项目";
|
|
|
joiner.add(projectName);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
String projectNames = joiner.toString();
|
|
|
log.debug("最终项目名称: {}", projectNames);
|
|
|
-
|
|
|
+
|
|
|
return String.format(
|
|
|
"""
|
|
|
{
|
|
|
@@ -1886,13 +2114,13 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
/**
|
|
|
* 从内存中创建ZIP文件(不保存单个图片文件)
|
|
|
*/
|
|
|
- private void createZipFileFromMemory(byte[] templateImage, List<GameAthleteVo> athleteVoList,
|
|
|
+ private void createZipFileFromMemory(byte[] templateImage, List<GameAthleteVo> athleteVoList,
|
|
|
GenerateBibBo bibParam, String zipFilePath, Long taskId) {
|
|
|
try (FileOutputStream fos = new FileOutputStream(zipFilePath);
|
|
|
ZipOutputStream zos = new ZipOutputStream(fos)) {
|
|
|
|
|
|
log.info("开始从内存创建ZIP文件 - ZIP文件: {}", zipFilePath);
|
|
|
-
|
|
|
+
|
|
|
int totalCount = athleteVoList.size();
|
|
|
int completedCount = 0;
|
|
|
|
|
|
@@ -1912,7 +2140,7 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
completedCount++;
|
|
|
int progress = (completedCount * 100) / totalCount;
|
|
|
gameBibTaskService.updateTaskProgress(taskId, progress, completedCount);
|
|
|
-
|
|
|
+
|
|
|
log.debug("添加图片到ZIP: {}", fileName);
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
@@ -2004,4 +2232,63 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 自定义MultipartFile实现,用于处理内存中的字节数组
|
|
|
+ */
|
|
|
+ private static class ByteArrayMultipartFile implements MultipartFile {
|
|
|
+ private final String name;
|
|
|
+ private final String originalFilename;
|
|
|
+ private final String contentType;
|
|
|
+ private final byte[] content;
|
|
|
+
|
|
|
+ public ByteArrayMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
|
|
|
+ this.name = name;
|
|
|
+ this.originalFilename = originalFilename;
|
|
|
+ this.contentType = contentType;
|
|
|
+ this.content = content != null ? content : new byte[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getName() {
|
|
|
+ return name;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getOriginalFilename() {
|
|
|
+ return originalFilename;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getContentType() {
|
|
|
+ return contentType;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean isEmpty() {
|
|
|
+ return content.length == 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public long getSize() {
|
|
|
+ return content.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public byte[] getBytes() throws IOException {
|
|
|
+ return content.clone();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public InputStream getInputStream() throws IOException {
|
|
|
+ return new ByteArrayInputStream(content);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void transferTo(File dest) throws IOException, IllegalStateException {
|
|
|
+ try (FileOutputStream fos = new FileOutputStream(dest)) {
|
|
|
+ fos.write(content);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
}
|