1. 修改文件存储方式, 现支持阿里云OSS云存储

2. 修改二维码生成方式, 改成从微信官方获取二维码, 而不是第三方. 这样扫二维码就能直接打开页面
This commit is contained in:
mirage 2026-03-19 17:02:26 +08:00
parent 7b710b1875
commit af00c3cbc6
9 changed files with 293 additions and 138 deletions

View File

@ -25,12 +25,18 @@ import com.mdd.common.mapper.TeacherMapper;
import com.mdd.common.mapper.admin.AdminMapper;
import com.mdd.common.mapper.system.SystemRoleMapper;
import com.mdd.common.exception.OperateException;
import com.mdd.common.plugin.storage.engine.AliyunStorage;
import com.mdd.common.service.RegisterService;
import com.mdd.common.plugin.wechat.WxMnpDriver;
import com.mdd.common.util.*;
import com.google.zxing.WriterException;
import cn.binarywang.wx.miniapp.api.WxMaQrcodeService;
import me.chanjar.weixin.common.error.WxErrorException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -373,25 +379,29 @@ public class TeacherServiceImpl implements ITeacherService {
Assert.notNull(teacher, "教师不存在!");
// 生成唯一邀请码INV + 时间戳() + 6位随机字符串
String invitationCode;
do {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String randomStr = ToolUtils.randomString(6).toUpperCase();
invitationCode = "INV" + timestamp + randomStr;
} while (teacherMapper.selectCount(
// 生成固定邀请码INV + 教师ID + 校验码
String invitationCode = "INV" + teacherId + String.format("%06d", teacherId % 1000000);
// 检查邀请码是否已被其他教师使用
Teacher existingTeacher = teacherMapper.selectOne(
new QueryWrapper<Teacher>()
.eq("invitation_code", invitationCode)
.ne("teacher_id", teacherId)) > 0);
.ne("teacher_id", teacherId)
.last("limit 1"));
if (existingTeacher != null) {
// 如果冲突使用时间戳作为补充
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
invitationCode = "INV" + teacherId + timestamp;
}
// 生成二维码内容URL预报名页面地址
String qrCodeContent = buildQrCodeUrl(invitationCode);
// 生成微信小程序码页面路径预报名页面
String miniProgramPage = buildMiniProgramPagePath();
// 删除旧的二维码文件如果存在
deleteOldQrCodeFile(teacher.getQrcodeUrl());
// 生成二维码图片并保存
String qrcodeUrl = generateAndSaveQrCode(invitationCode, qrCodeContent, teacherId);
// 生成微信小程序码并保存
String qrcodeUrl = generateAndSaveQrCode(invitationCode, miniProgramPage, teacherId);
// 更新教师表的邀请码和二维码地址
teacher.setInvitationCode(invitationCode);
@ -403,81 +413,38 @@ public class TeacherServiceImpl implements ITeacherService {
}
/**
* 构建二维码内容URL预报名页面地址
* 构建微信小程序页面路径
*
* @param invitationCode 邀请码
* @return 完整的预报名页面URL
* @return 小程序页面路径
*/
private String buildQrCodeUrl(String invitationCode) {
// 优先从配置中读取预报名H5地址建议配置为 uniapp H5 https://xx.com/h5
String h5BaseUrl = ConfigUtils.get("pre_registration", "front_url", "");
if (h5BaseUrl == null || h5BaseUrl.isEmpty()) {
// 如果没有单独配置尝试从 yml 中读取 like.front-url
String frontUrl = YmlUtils.get("like.front-url");
if (frontUrl != null && !frontUrl.isEmpty()) {
h5BaseUrl = frontUrl;
} else {
// 再退一步使用当前请求域名
h5BaseUrl = UrlUtils.getRequestUrl();
}
}
if (h5BaseUrl == null) {
h5BaseUrl = "";
}
// 移除末尾的斜杠方便统一拼接
while (h5BaseUrl.endsWith("/")) {
h5BaseUrl = h5BaseUrl.substring(0, h5BaseUrl.length() - 1);
}
String fullUrl;
if (h5BaseUrl.contains("#/")) {
// 如果已经配置了完整的 hash 路径例如https://xx.com/h5/#/pages/pre_registration/pre_registration
// 这里只负责追加或合并 invitationCode 参数
if (h5BaseUrl.contains("?")) {
fullUrl = h5BaseUrl + "&invitationCode=" + invitationCode;
} else {
fullUrl = h5BaseUrl + "?invitationCode=" + invitationCode;
}
} else {
// 默认拼接为小程序路径去除H5的hash模式以便微信扫码直接进入小程序
// {h5BaseUrl}/pages/pre_registration/pre_registration?invitationCode=xxx
fullUrl = h5BaseUrl
/*+ "/pages/pre_registration/pre_registration.html"
+ "?invitationCode=" + invitationCode*/;
}
return fullUrl;
private String buildMiniProgramPagePath() {
return "pages/pre_registration/pre_registration";
}
/**
* 生成二维码图片并保存方案B本地存储预留方案AOSS上传
* 生成微信小程序码并保存方案B本地存储预留方案AOSS上传
*
* @param invitationCode 邀请码
* @param qrCodeContent 二维码内容
* @param teacherId 教师ID
* @return 二维码图片的相对路径URL
* @param invitationCode 邀请码
* @param miniProgramPage 小程序页面路径
* @param teacherId 教师ID
* @return 小程序码图片的相对路径URL
*/
private String generateAndSaveQrCode(String invitationCode, String qrCodeContent, Integer teacherId) {
private String generateAndSaveQrCode(String invitationCode, String miniProgramPage, Integer teacherId) {
// 获取存储引擎配置
String engine = ConfigUtils.get("storage", "default", "local");
engine = engine == null || engine.isEmpty() ? "local" : engine;
String qrcodeUrl;
String date = TimeUtils.timestampToDate(TimeUtils.timestamp(), "yyyyMMdd");
String fileName = "teacher_" + teacherId + "_qrcode.png"; // 使用固定文件名
WxMaQrcodeService qrcodeService = WxMnpDriver.mnp().getQrcodeService();
File wxCodeFile = null;
try {
// 二维码图片尺寸
int qrCodeWidth = 300;
int qrCodeHeight = 300;
// 获取存储引擎配置
String engine = ConfigUtils.get("storage", "default", "local");
engine = engine == null || engine.isEmpty() ? "local" : engine;
String qrcodeUrl;
String date = TimeUtils.timestampToDate(TimeUtils.timestamp(), "yyyyMMdd");
String fileName = "teacher_" + teacherId + "_" + invitationCode + ".png";
String fileKey = date + "/" + fileName;
wxCodeFile = qrcodeService.createWxaCodeUnlimit(invitationCode, miniProgramPage, false, "develop", 430, true, null, false);
if ("local".equals(engine)) {
// ========== 方案B本地存储 ==========
String folder = "qrcode/teacher";
String folder = "likeadmin/qrcode/teacher";
String savePath = (folder + "/" + date).replace("\\", "/");
File saveDir = new File(savePath);
if (!saveDir.exists()) {
@ -487,64 +454,54 @@ public class TeacherServiceImpl implements ITeacherService {
}
File qrCodeFile = new File(savePath, fileName);
QrCodeUtil.generateQrCodeToFile(qrCodeContent, qrCodeWidth, qrCodeHeight, qrCodeFile.getAbsolutePath());
Files.copy(wxCodeFile.toPath(), qrCodeFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
// 返回相对路径
qrcodeUrl = folder + "/" + fileKey;
qrcodeUrl = folder + "/" + fileName;
} else {
// ========== 方案AOSS上传预留代码 ==========
// 注意OSS上传需要MultipartFile这里需要将byte[]转换为临时文件再上传
// 或者直接使用OSS的putObject方法上传字节流
// 注意OSS上传需要结合具体存储引擎实现这里先保留文件生成结果
String folder = "likeadmin/qrcode/teacher";
String ossKey = folder + "/" + fileName;
// 方案A-1使用临时文件上传适用于所有OSS
File tempFile = File.createTempFile("qrcode_", ".png");
try {
QrCodeUtil.generateQrCodeToFile(qrCodeContent, qrCodeWidth, qrCodeHeight, tempFile.getAbsolutePath());
String folder = "qrcode/teacher";
String ossKey = folder + "/" + fileKey;
if ("aliyun".equals(engine)) {
switch (engine) {
case "aliyun":
// 阿里云OSS上传
/*
Map<String, String> config = ConfigUtils.getMap("storage", "aliyun");
AliyunStorage aliyunStorage = new AliyunStorage(config);
// 需要将File转换为MultipartFile或使用OSS的putObject方法
// 这里使用OSS直接上传字节流的方式
com.aliyun.oss.OSS ossClient = aliyunStorage.ossClient();
try {
ossClient.putObject(config.get("bucket"), ossKey, new FileInputStream(tempFile));
qrcodeUrl = ossKey;
ossClient.putObject(config.get("bucket"), ossKey, java.nio.file.Files.newInputStream(wxCodeFile.toPath()));
} finally {
ossClient.shutdown();
}
*/
// TODO: 实现阿里云OSS上传逻辑
qrcodeUrl = ossKey;
} else if ("qiniu".equals(engine)) {
break;
case "qiniu":
// 七牛云上传
// TODO: 实现七牛云上传逻辑
qrcodeUrl = ossKey;
} else if ("qcloud".equals(engine)) {
break;
case "qcloud":
// 腾讯云COS上传
// TODO: 实现腾讯云COS上传逻辑
qrcodeUrl = ossKey;
} else {
break;
default:
throw new OperateException("不支持的存储引擎: " + engine);
}
} finally {
// 删除临时文件
if (tempFile.exists()) {
tempFile.delete();
}
}
}
return qrcodeUrl;
} catch (WriterException | IOException e) {
throw new OperateException("生成二维码失败: " + e.getMessage());
} catch (WxErrorException | IOException e) {
throw new OperateException("生成微信小程序码失败: " + e.getMessage());
} finally {
if (wxCodeFile != null && wxCodeFile.exists()) {
wxCodeFile.delete();
}
}
}
@ -557,16 +514,53 @@ public class TeacherServiceImpl implements ITeacherService {
if (StringUtils.isBlank(oldQrcodeUrl)) {
return;
}
try {
String path = oldQrcodeUrl.startsWith("/") ? oldQrcodeUrl.substring(1) : oldQrcodeUrl;
File oldFile = new File(path);
if (oldFile.exists() && oldFile.isFile()) {
if (!oldFile.delete()) {
System.err.println("删除旧二维码文件失败: " + path);
// 根据存储引擎判断处理方式
String engine = ConfigUtils.get("storage", "default", "local");
if ("local".equals(engine)) {
// 仅对本地存储执行物理删除
try {
String path = oldQrcodeUrl.startsWith("/") ? oldQrcodeUrl.substring(1) : oldQrcodeUrl;
File oldFile = new File(path);
if (oldFile.exists() && oldFile.isFile()) {
if (!oldFile.delete()) {
System.err.println("删除旧二维码文件失败: " + path);
}
}
} catch (Exception e) {
System.err.println("删除旧二维码文件时发生异常: " + e.getMessage());
}
} else {
// 对于OSS存储可以通过配置决定是否删除旧文件
// 或者依赖OSS的同名文件覆盖机制无需显式删除
boolean shouldDeleteFromOss = Boolean.parseBoolean(
ConfigUtils.get("storage", "delete_old_on_update", "false"));
if (shouldDeleteFromOss) {
// 根据不同OSS类型执行删除操作
try {
switch (engine) {
case "aliyun":
// 阿里云OSS删除
Map<String, String> config = ConfigUtils.getMap("storage", "aliyun");
if (config != null) {
AliyunStorage aliyunStorage = new AliyunStorage(config);
aliyunStorage.delete(oldQrcodeUrl);
}
break;
case "qiniu":
// 七牛云删除预留实现
break;
case "qcloud":
// 腾讯云COS删除预留实现
break;
}
} catch (Exception e) {
System.err.println("删除旧OSS二维码文件时发生异常: " + e.getMessage());
}
}
} catch (Exception e) {
System.err.println("删除旧二维码文件时发生异常: " + e.getMessage());
// 如果不删除旧文件直接让新文件覆盖即可
}
}
}

View File

@ -69,4 +69,33 @@ public class AliyunStorage {
this.ossClient().deleteObject(this.config.get("bucket"), key);
}
/**
* 下载文件
*
* @param key 文件名称
* @return 文件字节数组
*/
public byte[] download(String key) {
try {
OSS ossClient = this.ossClient();
com.aliyun.oss.model.OSSObject ossObject = ossClient.getObject(this.config.get("bucket"), key);
if (ossObject != null) {
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
java.io.InputStream inputStream = ossObject.getObjectContent();
byte[] data = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, bytesRead);
}
inputStream.close();
ossObject.close();
ossClient.shutdown();
return buffer.toByteArray();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -36,12 +36,12 @@ public class QcloudStorage {
/**
* 鉴权客户端
*
* @author fzr
* @return String
* @author fzr
*/
public COSClient cosClient() {
String secretId = this.config.get("access_key");
String secretKey = this.config.get("secret_key");
String secretId = this.config.get("access_key");
String secretKey = this.config.get("secret_key");
COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
Region region = new Region(this.config.get("region"));
ClientConfig clientConfig = new ClientConfig(region);
@ -53,7 +53,7 @@ public class QcloudStorage {
* 上传文件
*
* @param multipartFile 文件对象
* @param key 文件名称 20220331/11.png
* @param key 文件名称 20220331/11.png
*/
public void upload(MultipartFile multipartFile, String key) {
try {
@ -75,4 +75,33 @@ public class QcloudStorage {
this.cosClient().deleteObject(this.config.get("bucket"), filePath);
}
// /**
// * 下载文件
// *
// * @param key 文件名称
// * @return 文件字节数组
// */
// public byte[] download(String key) {
// try {
// COSClient cosClient = this.cosClient();
// com.qcloud.cos.model.S3Object s3Object = cosClient.getObject(this.config.get("bucket"), key);
// if (s3Object != null) {
// java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
// java.io.InputStream inputStream = s3Object.getObjectContent();
// byte[] data = new byte[1024];
// int bytesRead;
// while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
// buffer.write(data, 0, bytesRead);
// }
// inputStream.close();
// s3Object.close();
// cosClient.shutdown();
// return buffer.toByteArray();
// }
// } catch (Exception e) {
// e.printStackTrace();
// }
// return null;
// }
}

View File

@ -79,4 +79,46 @@ public class QiniuStorage {
throw new RuntimeException(e);
}
}
/**
* 下载文件
*
* @param key 文件名称
* @return 文件字节数组
*/
public byte[] download(String key) {
try {
String accessKey = this.config.getOrDefault("access_key", "");
String secretKey = this.config.getOrDefault("secret_key", "");
String bucket = this.config.getOrDefault("bucket", "");
String domain = this.config.getOrDefault("domain", "");
Auth auth = Auth.create(accessKey, secretKey);
String downloadUrl = auth.privateDownloadUrl(domain + "/" + key);
java.net.URL url = new java.net.URL(downloadUrl);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
if (conn.getResponseCode() == 200) {
java.io.InputStream inputStream = conn.getInputStream();
byte[] buffer = new byte[1024];
java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream();
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
inputStream.close();
conn.disconnect();
return outputStream.toByteArray();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -54,20 +54,57 @@ public class QrCodeService {
if (teacher == null || teacher.getQrcodeUrl() == null || teacher.getQrcodeUrl().isEmpty()) {
return null;
}
String engine = ConfigUtils.get("storage", "default", "local");
engine = engine == null || engine.isEmpty() ? "local" : engine;
if (!"local".equals(engine)) {
return null;
}
String path = teacher.getQrcodeUrl().startsWith("/") ? teacher.getQrcodeUrl().substring(1) : teacher.getQrcodeUrl();
java.io.File file = new java.io.File(path);
if (!file.exists() || !file.isFile()) {
return null;
}
try {
return java.nio.file.Files.readAllBytes(file.toPath());
} catch (java.io.IOException e) {
return null;
if ("local".equals(engine)) {
// 本地存储处理
String path = teacher.getQrcodeUrl().startsWith("/") ? teacher.getQrcodeUrl().substring(1) : teacher.getQrcodeUrl();
java.io.File file = new java.io.File(path);
if (!file.exists() || !file.isFile()) {
return null;
}
try {
return java.nio.file.Files.readAllBytes(file.toPath());
} catch (java.io.IOException e) {
return null;
}
} else {
// OSS存储处理使用存储引擎的下载方法
try {
switch (engine) {
case "aliyun":
// 阿里云OSS下载
java.util.Map<String, String> aliyunConfig = ConfigUtils.getMap("storage", "aliyun");
if (aliyunConfig != null) {
com.mdd.common.plugin.storage.engine.AliyunStorage aliyunStorage = new com.mdd.common.plugin.storage.engine.AliyunStorage(aliyunConfig);
return aliyunStorage.download(teacher.getQrcodeUrl());
}
break;
case "qiniu":
// 七牛云下载
java.util.Map<String, String> qiniuConfig = ConfigUtils.getMap("storage", "qiniu");
if (qiniuConfig != null) {
com.mdd.common.plugin.storage.engine.QiniuStorage qiniuStorage = new com.mdd.common.plugin.storage.engine.QiniuStorage(qiniuConfig);
return qiniuStorage.download(teacher.getQrcodeUrl());
}
break;
// case "qcloud":
// // 腾讯云COS下载
// java.util.Map<String, String> cosConfig = ConfigUtils.getMap("storage", "qcloud");
// if (cosConfig != null) {
// com.mdd.common.plugin.storage.engine.QcloudStorage qcloudStorage = new com.mdd.common.plugin.storage.engine.QcloudStorage(cosConfig);
// return qcloudStorage.download(teacher.getQrcodeUrl());
// }
// break;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
return null;
}
}

View File

@ -37,7 +37,7 @@ public class TeacherController {
@PostMapping("/getTeacherInfo")
@ApiOperation(value = "根据查询条件获取教师信息")
public AjaxResult<TeacherListedVo> getTeacherInfo(@RequestBody TeacherSearchValidate searchValidate) {
public AjaxResult<Object> getTeacherInfo(@RequestBody TeacherSearchValidate searchValidate) {
return iTeacherService.getTeacherInfo(searchValidate);
}

View File

@ -25,5 +25,5 @@ public interface ITeacherService extends IService<Teacher> {
*/
PageResult<TeacherListedVo> list(PageValidate pageValidate, TeacherSearchValidate searchValidate);
AjaxResult<TeacherListedVo> getTeacherInfo(TeacherSearchValidate searchValidate);
AjaxResult<Object> getTeacherInfo(TeacherSearchValidate searchValidate);
}

View File

@ -87,9 +87,25 @@ public class TeacherServiceImpl extends ServiceImpl<TeacherMapper, Teacher> impl
}
@Override
public AjaxResult<TeacherListedVo> getTeacherInfo(TeacherSearchValidate searchValidate) {
public AjaxResult<Object> getTeacherInfo(TeacherSearchValidate searchValidate) {
if (searchValidate.getTeacherId() == null && StringUtils.isBlank(searchValidate.getInvitationCode())) {
return AjaxResult.failed("教师参数缺失!");
}
QueryWrapper<Teacher> queryWrapper = new QueryWrapper<>();
if (searchValidate.getTeacherId() != null) {
queryWrapper.eq("teacher_id", searchValidate.getTeacherId());
}
if (StringUtils.isNotBlank(searchValidate.getInvitationCode())) {
queryWrapper.eq("invitation_code", searchValidate.getInvitationCode());
}
Teacher teacher = teacherMapper.selectOne(queryWrapper.last("limit 1"));
if (teacher == null) {
return AjaxResult.failed("教师不存在!");
}
TeacherListedVo teacherListedVo = new TeacherListedVo();
Teacher teacher = teacherMapper.selectOne(new QueryWrapper<Teacher>().eq("teacher_id", searchValidate.getTeacherId()));
BeanUtils.copyProperties(teacher, teacherListedVo);
College college = collegeMapper.selectById(teacherListedVo.getCollegeId());
teacherListedVo.setCreateTime(TimeUtils.timestampToDate(teacher.getCreateTime()));

View File

@ -232,9 +232,9 @@ const submitBtnStyle = {
boxShadow: '0 8rpx 24rpx rgba(59, 130, 246, 0.3)'
}
const getTeacherData = async (teacherId: string) => {
const getTeacherData = async (params: { teacherId?: string; invitationCode?: string }) => {
try {
const res = await getTeacherInfo({ teacherId: Number(teacherId) })
const res = await getTeacherInfo(params)
if (res.code === 1 && res.data) {
formData.teacher = res.data.teacherName
formData.recruitmentTeacherId = res.data.teacherId
@ -430,8 +430,16 @@ onMounted(() => {
})
onLoad((options: any) => {
if (options.scene) {
getTeacherData({ invitationCode: decodeURIComponent(options.scene) })
return
}
if (options.invitationCode) {
getTeacherData({ invitationCode: options.invitationCode })
return
}
if (options.teacherId) {
getTeacherData(options.teacherId)
getTeacherData({ teacherId: options.teacherId })
}
})
</script>