|
@@ -1,5 +1,6 @@
|
|
|
package com.train.controller;
|
|
package com.train.controller;
|
|
|
|
|
|
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.train.cfc.entity.CfcUser;
|
|
import com.train.cfc.entity.CfcUser;
|
|
|
import com.train.cfc.mapper.CfcUserMapper;
|
|
import com.train.cfc.mapper.CfcUserMapper;
|
|
@@ -8,15 +9,25 @@ import com.train.entity.TrainInvite;
|
|
|
import com.train.entity.TrainUser;
|
|
import com.train.entity.TrainUser;
|
|
|
import com.train.mapper.TrainInviteMapper;
|
|
import com.train.mapper.TrainInviteMapper;
|
|
|
import com.train.mapper.TrainUserMapper;
|
|
import com.train.mapper.TrainUserMapper;
|
|
|
|
|
+import com.train.service.WechatService;
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
|
|
+import org.springframework.http.MediaType;
|
|
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
import org.springframework.util.StringUtils;
|
|
import org.springframework.util.StringUtils;
|
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
|
import org.springframework.web.bind.annotation.RequestAttribute;
|
|
import org.springframework.web.bind.annotation.RequestAttribute;
|
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
|
|
+import java.io.File;
|
|
|
|
|
+import java.io.FileOutputStream;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
import java.util.UUID;
|
|
import java.util.UUID;
|
|
@@ -24,12 +35,13 @@ import java.util.UUID;
|
|
|
/**
|
|
/**
|
|
|
* 裂变分享:专属海报/邀请码、我的转介绍统计。
|
|
* 裂变分享:专属海报/邀请码、我的转介绍统计。
|
|
|
* <p>邀请码复用 train_user.invite_code;未设置时按需生成(折校:可进校友校验证写入)。
|
|
* <p>邀请码复用 train_user.invite_code;未设置时按需生成(折校:可进校友校验证写入)。
|
|
|
- * 海报 URL 为占位(test-mode 返回空,前端用邀请码本地渲染海报);
|
|
|
|
|
- * 生产环境接入微信 getwxacodeunlimit 生成小程序码后回填。
|
|
|
|
|
|
|
+ * 海报小程序码:test-mode 返回空(前端用邀请码本地渲染海报);
|
|
|
|
|
+ * 生产环境调用微信 getwxacodeunlimit 生成带参小程序码,缓存到本地上传目录并回填 posterUrl。
|
|
|
* 同时返回 sharePath(小程序带参路径)/ shareText(分享文案),便于前端拼装分享卡。
|
|
* 同时返回 sharePath(小程序带参路径)/ shareText(分享文案),便于前端拼装分享卡。
|
|
|
*/
|
|
*/
|
|
|
@Tag(name = "裂变分享", description = "专属海报/邀请码、转介绍统计")
|
|
@Tag(name = "裂变分享", description = "专属海报/邀请码、转介绍统计")
|
|
|
@RestController
|
|
@RestController
|
|
|
|
|
+@Slf4j
|
|
|
@RequestMapping("/api/invite")
|
|
@RequestMapping("/api/invite")
|
|
|
public class InviteController {
|
|
public class InviteController {
|
|
|
|
|
|
|
@@ -39,6 +51,19 @@ public class InviteController {
|
|
|
private TrainInviteMapper trainInviteMapper;
|
|
private TrainInviteMapper trainInviteMapper;
|
|
|
@Resource
|
|
@Resource
|
|
|
private CfcUserMapper cfcUserMapper;
|
|
private CfcUserMapper cfcUserMapper;
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private WechatService wechatService;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.test-mode}")
|
|
|
|
|
+ private boolean testMode;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${upload.base-dir}")
|
|
|
|
|
+ private String baseDir;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.qrcode-url:https://api.weixin.qq.com/wxa/getwxacodeunlimit}")
|
|
|
|
|
+ private String qrcodeUrl;
|
|
|
|
|
+
|
|
|
|
|
+ private final RestTemplate restTemplate = new RestTemplate();
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 我的专属海报 + 邀请码 + 分享文案。
|
|
* 我的专属海报 + 邀请码 + 分享文案。
|
|
@@ -75,12 +100,57 @@ public class InviteController {
|
|
|
sharePath;
|
|
sharePath;
|
|
|
Map<String, Object> row = new HashMap<>();
|
|
Map<String, Object> row = new HashMap<>();
|
|
|
row.put("inviteCode", inviteCode);
|
|
row.put("inviteCode", inviteCode);
|
|
|
- row.put("posterUrl", ""); // 占位:test-mode 不生成海报图;生产接入 getwxacodeunlimit
|
|
|
|
|
|
|
+ row.put("posterUrl", generatePosterCode(inviteCode)); // test-mode 返回空串(前端本地渲染)
|
|
|
row.put("sharePath", sharePath);
|
|
row.put("sharePath", sharePath);
|
|
|
row.put("shareText", shareText);
|
|
row.put("shareText", shareText);
|
|
|
return Result.success(row);
|
|
return Result.success(row);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 生成带参小程序码(getwxacodeunlimit)。
|
|
|
|
|
+ * scene 传邀请码(最长 32 字符),page 固定报名列表页;
|
|
|
|
|
+ * 图片字节缓存到本地上传目录,返回可访问 URL。test-mode 或失败返回空串(前端降级本地海报)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private String generatePosterCode(String inviteCode) {
|
|
|
|
|
+ if (testMode) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ String accessToken = wechatService.getAccessToken();
|
|
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
|
|
+ body.put("scene", inviteCode);
|
|
|
|
|
+ body.put("page", "pages/enroll/list");
|
|
|
|
|
+ body.put("check_path", false);
|
|
|
|
|
+ body.put("width", 430);
|
|
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
+ headers.setContentType(MediaType.APPLICATION_JSON);
|
|
|
|
|
+ ResponseEntity<byte[]> response = restTemplate.postForEntity(
|
|
|
|
|
+ qrcodeUrl + "?access_token=" + accessToken,
|
|
|
|
|
+ new HttpEntity<>(body.toJSONString(), headers),
|
|
|
|
|
+ byte[].class);
|
|
|
|
|
+ byte[] bytes = response.getBody();
|
|
|
|
|
+ if (bytes == null || bytes.length == 0) {
|
|
|
|
|
+ log.warn("getwxacodeunlimit 返回空字节,inviteCode={}", inviteCode);
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ File dir = new File(baseDir);
|
|
|
|
|
+ if (!dir.exists() && !dir.mkdirs()) {
|
|
|
|
|
+ log.error("海报目录创建失败: {}", baseDir);
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ String filename = "invite_" + inviteCode + "_" + UUID.randomUUID().toString().substring(0, 8) + ".png";
|
|
|
|
|
+ File dest = new File(dir, filename);
|
|
|
|
|
+ try (FileOutputStream fos = new FileOutputStream(dest)) {
|
|
|
|
|
+ fos.write(bytes);
|
|
|
|
|
+ }
|
|
|
|
|
+ return "/uploads/" + filename;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ // 微信接口失败/网络异常:降级为前端本地海报,不阻塞分享
|
|
|
|
|
+ log.error("生成小程序码失败,降级本地海报 inviteCode={}", inviteCode, e);
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 我的转介绍统计。
|
|
* 我的转介绍统计。
|
|
|
* @return {invited, paid, rewards}
|
|
* @return {invited, paid, rewards}
|