From 8dc5994ece6edc88a58e63510368f1af179dfa72 Mon Sep 17 00:00:00 2001 From: dzq Date: Wed, 3 Dec 2025 11:36:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BC=81=E4=B8=9A=E5=BE=AE=E4=BF=A1):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B0=8F=E7=A8=8B=E5=BA=8F=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E6=9F=A5=E8=AF=A2=E5=92=8C=E7=94=9F=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加根据企业ID和名称查询二维码的接口 实现自动生成小程序二维码并保存到本地 新增二维码类型枚举和控制器接口 --- .../controller/qywx/MpQrcodeController.java | 101 +++++++++++ .../mpQrcode/MpQrcodeApplicationService.java | 167 ++++++++++++++++++ .../qywx/mpQrcode/db/QyMpQrcodeMapper.java | 9 + .../qywx/mpQrcode/db/QyMpQrcodeService.java | 5 + .../mpQrcode/db/QyMpQrcodeServiceImpl.java | 5 + .../qywx/mpQrcode/enums/MpQrcodeTypeEnum.java | 73 ++++++++ 6 files changed, 360 insertions(+) create mode 100644 agileboot-admin/src/main/java/com/agileboot/admin/controller/qywx/MpQrcodeController.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/enums/MpQrcodeTypeEnum.java diff --git a/agileboot-admin/src/main/java/com/agileboot/admin/controller/qywx/MpQrcodeController.java b/agileboot-admin/src/main/java/com/agileboot/admin/controller/qywx/MpQrcodeController.java new file mode 100644 index 0000000..3090202 --- /dev/null +++ b/agileboot-admin/src/main/java/com/agileboot/admin/controller/qywx/MpQrcodeController.java @@ -0,0 +1,101 @@ +package com.agileboot.admin.controller.qywx; + +import com.agileboot.admin.customize.aop.accessLog.AccessLog; +import com.agileboot.common.core.base.BaseController; +import com.agileboot.common.core.dto.ResponseDTO; +import com.agileboot.common.core.page.PageDTO; +import com.agileboot.common.enums.common.BusinessTypeEnum; +import com.agileboot.domain.common.command.BulkOperationCommand; +import com.agileboot.domain.qywx.mpQrcode.MpQrcodeApplicationService; +import com.agileboot.domain.qywx.mpQrcode.command.AddMpQrcodeCommand; +import com.agileboot.domain.qywx.mpQrcode.command.UpdateMpQrcodeCommand; +import com.agileboot.domain.qywx.mpQrcode.db.QyMpQrcodeEntity; +import com.agileboot.domain.qywx.mpQrcode.dto.QyMpQrcodeDTO; +import com.agileboot.domain.qywx.mpQrcode.query.SearchMpQrcodeQuery; +import io.swagger.v3.oas.annotations.Operation; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + *

+ * 企业小程序二维码表 前端控制器 + *

+ * + * @author AgileBoot + * @since 2025-12-02 + */ +@RestController +@RequestMapping("/qywx/mp-qrcodes") +@RequiredArgsConstructor +@Validated +public class MpQrcodeController extends BaseController { + + private final MpQrcodeApplicationService mpQrcodeApplicationService; + + @Operation(summary = "二维码列表") + @GetMapping + public ResponseDTO> list(SearchMpQrcodeQuery query) { + PageDTO page = mpQrcodeApplicationService.getQrcodeList(query); + return ResponseDTO.ok(page); + } + + @Operation(summary = "二维码详情") + @GetMapping("/{id}") + public ResponseDTO detail(@PathVariable Long id) { + // 通过查询接口获取详情 + SearchMpQrcodeQuery query = new SearchMpQrcodeQuery<>(); + query.setPageNum(1); + query.setPageSize(1); + PageDTO page = mpQrcodeApplicationService.getQrcodeList(query); + if (page.getRows() != null && !page.getRows().isEmpty()) { + return ResponseDTO.ok(page.getRows().get(0)); + } + return ResponseDTO.ok(null); + } + + @Operation(summary = "新增二维码") + @AccessLog(title = "二维码管理", businessType = BusinessTypeEnum.ADD) + @PostMapping + public ResponseDTO add(@Validated @RequestBody AddMpQrcodeCommand command) { + mpQrcodeApplicationService.addQrcode(command); + return ResponseDTO.ok(); + } + + @Operation(summary = "修改二维码") + @AccessLog(title = "二维码管理", businessType = BusinessTypeEnum.MODIFY) + @PutMapping("/{id}") + public ResponseDTO edit(@PathVariable Long id, @Validated @RequestBody UpdateMpQrcodeCommand command) { + command.setQrcodeId(id); + mpQrcodeApplicationService.updateQrcode(command); + return ResponseDTO.ok(); + } + + @Operation(summary = "删除二维码") + @AccessLog(title = "二维码管理", businessType = BusinessTypeEnum.DELETE) + @DeleteMapping("/{ids}") + public ResponseDTO remove(@PathVariable @NotNull List ids) { + mpQrcodeApplicationService.deleteQrcode(new BulkOperationCommand<>(ids)); + return ResponseDTO.ok(); + } + + @Operation(summary = "根据企业ID查询二维码") + @GetMapping("/by-corpid/{corpid}") + public ResponseDTO> getByCorpid(@PathVariable String corpid) { + List list = mpQrcodeApplicationService.getQrcodeListByCorpid(corpid); + return ResponseDTO.ok(list); + } + + @Operation(summary = "根据企业ID和名称查询或创建二维码") + @GetMapping("/get-or-create") + public ResponseDTO getOrCreateQrcode( + @RequestParam String corpid, + @RequestParam @Pattern(regexp = "^[A-Z_]+$", message = "名称必须为大写字母或下划线") String name) { + + QyMpQrcodeEntity qrcode = mpQrcodeApplicationService.getOrCreateQrcode(corpid, name); + return ResponseDTO.ok(qrcode); + } +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/MpQrcodeApplicationService.java b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/MpQrcodeApplicationService.java index d6997fc..1681f46 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/MpQrcodeApplicationService.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/MpQrcodeApplicationService.java @@ -1,19 +1,37 @@ package com.agileboot.domain.qywx.mpQrcode; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; import com.agileboot.common.core.page.PageDTO; +import com.agileboot.common.exception.ApiException; +import com.agileboot.common.exception.error.ErrorCode; +import com.agileboot.common.utils.file.FileUploadUtils; +import com.agileboot.domain.common.cache.CacheCenter; import com.agileboot.domain.common.command.BulkOperationCommand; +import com.agileboot.domain.qywx.authCorpInfo.db.QyAuthCorpInfoEntity; +import com.agileboot.domain.qywx.authCorpInfo.db.QyAuthCorpInfoService; import com.agileboot.domain.qywx.mpQrcode.command.AddMpQrcodeCommand; import com.agileboot.domain.qywx.mpQrcode.command.UpdateMpQrcodeCommand; import com.agileboot.domain.qywx.mpQrcode.db.QyMpQrcodeEntity; import com.agileboot.domain.qywx.mpQrcode.db.QyMpQrcodeService; import com.agileboot.domain.qywx.mpQrcode.dto.QyMpQrcodeDTO; +import com.agileboot.domain.qywx.mpQrcode.enums.MpQrcodeTypeEnum; import com.agileboot.domain.qywx.mpQrcode.model.MpQrcodeModel; import com.agileboot.domain.qywx.mpQrcode.model.MpQrcodeModelFactory; import com.agileboot.domain.qywx.mpQrcode.query.SearchMpQrcodeQuery; +import com.agileboot.domain.wx.UnlimitedQRCodeRequest; +import com.agileboot.domain.wx.WxAccessToken; +import com.agileboot.domain.wx.WxConstant; +import com.agileboot.domain.wx.WxService; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; /** @@ -24,12 +42,17 @@ import org.springframework.stereotype.Service; * @author AgileBoot * @since 2025-12-02 */ +@Slf4j @Service @RequiredArgsConstructor public class MpQrcodeApplicationService { + private static final String QRCODE_SUB_DIR = "upload"; + private final QyMpQrcodeService qrcodeService; private final MpQrcodeModelFactory qrcodeModelFactory; + private final WxService wxService; + private final QyAuthCorpInfoService authCorpInfoService; /** * 分页查询二维码列表 @@ -76,4 +99,148 @@ public class MpQrcodeApplicationService { public List getQrcodeListByCorpid(String corpid) { return qrcodeService.selectByCorpid(corpid); } + + /** + * 根据企业ID和名称查询或创建二维码 + */ + public QyMpQrcodeEntity getOrCreateQrcode(String corpid, String name) { + // 先查询是否已存在 + QyMpQrcodeEntity existingQrcode = qrcodeService.selectByCorpidAndName(corpid, name); + if (existingQrcode != null) { + return existingQrcode; + } + + MpQrcodeTypeEnum typeEnum = MpQrcodeTypeEnum.getByName(name); + if (typeEnum == null) { + throw new ApiException(ErrorCode.Internal.INVALID_PARAMETER, "name"); + } + + // 从数据库中查询企业ID对应的cid + QyAuthCorpInfoEntity authCorpInfo = authCorpInfoService.selectByCorpid(corpid); + if (authCorpInfo == null) { + throw new ApiException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, corpid, "企业认证信息"); + } + Integer cid = authCorpInfo.getId(); + + // 构建 scene 和 page + String scene = MpQrcodeTypeEnum.buildScene(typeEnum, cid); + String page = typeEnum.getPage(); + + // 生成小程序码 + String qrcodeUrl = generateQrcode(scene, page); + + // 创建记录 + AddMpQrcodeCommand command = new AddMpQrcodeCommand(); + command.setCorpid(corpid); + command.setName(name); + command.setScene(scene); + command.setPage(page); + command.setQrcodeUrl(qrcodeUrl); + + addQrcode(command); + + // 重新查询刚创建的记录 + return qrcodeService.selectByCorpidAndName(corpid, name); + } + + /** + * 生成小程序码 + * @param scene 场景参数 + * @param page 页面路径 + * @return 小程序码的相对URL路径 + */ + private String generateQrcode(String scene, String page) { + try { + // 1. 获取 access_token + String accessToken = getAccessToken(); + if (StrUtil.isEmpty(accessToken)) { + log.error("获取access_token失败"); + return ""; + } + + // 2. 构建请求参数 + UnlimitedQRCodeRequest request = new UnlimitedQRCodeRequest(); + request.setScene(scene); + request.setPage(page); + request.setWidth(430); + request.setAutoColor(false); + request.setIsHyaline(false); + + // 3. 调用微信接口获取小程序码 + byte[] qrCodeBytes = wxService.getUnlimitedQRCode(accessToken, request); + + if (qrCodeBytes != null && qrCodeBytes.length > 0) { + // 4. 生成文件名并保存到本地 + String fileName = generateQrcodeFilename(scene); + saveQrcodeToLocal(qrCodeBytes, QRCODE_SUB_DIR, fileName); + + String relativeUrl = FileUploadUtils.getRelativeFileUrl(QRCODE_SUB_DIR, fileName); + log.info("成功生成小程序码,文件路径:{}", relativeUrl); + return relativeUrl; + } else { + log.error("获取小程序码失败,返回数据为空"); + return ""; + } + } catch (Exception e) { + log.error("生成小程序码时发生错误", e); + return ""; + } + } + + /** + * 获取 access_token,优先从缓存获取 + */ + private String getAccessToken() { + // 1. 先从缓存获取 + WxAccessToken cachedToken = CacheCenter.accessTokenCache.get(WxConstant.AccessTokenCacheName); + if (cachedToken != null && cachedToken.getAccessToken() != null) { + log.debug("从缓存获取access_token"); + return cachedToken.getAccessToken(); + } + + // 2. 缓存中没有,调用微信接口获取 + log.info("缓存中没有access_token,开始调用微信接口获取..."); + WxAccessToken newToken = wxService.getAccessToken(); + + if (newToken != null && newToken.getAccessToken() != null) { + // 将 access_token 存入缓存 + CacheCenter.accessTokenCache.put(WxConstant.AccessTokenCacheName, newToken); + log.info("成功获取access_token并存入缓存,有效期:{}秒", newToken.getExpiresIn()); + return newToken.getAccessToken(); + } + + return null; + } + + /** + * 生成小程序码文件名 + */ + private String generateQrcodeFilename(String scene) { + return StrUtil.format("qrcode_{}_{}.jpg", + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_PATTERN), + scene.replace("=", "_")); + } + + /** + * 保存小程序码到本地 + */ + private void saveQrcodeToLocal(byte[] qrCodeBytes, String subDir, String fileName) throws IOException { + if (StrUtil.isEmpty(subDir) || StrUtil.isEmpty(fileName)) { + throw new ApiException(ErrorCode.Internal.INVALID_PARAMETER, "subDir or fileName"); + } + + String filePath = FileUploadUtils.getFileAbsolutePath(subDir, fileName); + File destination = new File(filePath); + + if (!destination.getParentFile().exists()) { + destination.getParentFile().mkdirs(); + } + + try (FileOutputStream fos = new FileOutputStream(destination)) { + fos.write(qrCodeBytes); + fos.flush(); + } + + log.info("小程序码已保存到:{}", filePath); + } } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeMapper.java b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeMapper.java index 6a73f4d..91ac167 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeMapper.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeMapper.java @@ -46,4 +46,13 @@ public interface QyMpQrcodeMapper extends BaseMapper { "WHERE corpid = #{corpid} AND deleted = 0 " + "ORDER BY create_time DESC") List selectByCorpid(@Param("corpid") String corpid); + + /** + * 根据企业ID和名称查询二维码 + */ + @Select("SELECT * " + + "FROM qy_mp_qrcode " + + "WHERE corpid = #{corpid} AND name = #{name} AND deleted = 0 " + + "LIMIT 1") + QyMpQrcodeEntity selectByCorpidAndName(@Param("corpid") String corpid, @Param("name") String name); } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeService.java b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeService.java index b1e587a..a7654f5 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeService.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeService.java @@ -29,4 +29,9 @@ public interface QyMpQrcodeService extends IService { * 根据企业ID查询二维码列表 */ List selectByCorpid(String corpid); + + /** + * 根据企业ID和名称查询二维码 + */ + QyMpQrcodeEntity selectByCorpidAndName(String corpid, String name); } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeServiceImpl.java b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeServiceImpl.java index 0e2464e..abb1e52 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeServiceImpl.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/db/QyMpQrcodeServiceImpl.java @@ -33,4 +33,9 @@ public class QyMpQrcodeServiceImpl extends ServiceImpl selectByCorpid(String corpid) { return baseMapper.selectByCorpid(corpid); } + + @Override + public QyMpQrcodeEntity selectByCorpidAndName(String corpid, String name) { + return baseMapper.selectByCorpidAndName(corpid, name); + } } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/enums/MpQrcodeTypeEnum.java b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/enums/MpQrcodeTypeEnum.java new file mode 100644 index 0000000..bd2048e --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/mpQrcode/enums/MpQrcodeTypeEnum.java @@ -0,0 +1,73 @@ +package com.agileboot.domain.qywx.mpQrcode.enums; + +import org.apache.commons.lang3.StringUtils; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + *

+ * 小程序二维码类型枚举 + *

+ * + * @author AgileBoot + * @since 2025-12-03 + */ +@Getter +@AllArgsConstructor +public enum MpQrcodeTypeEnum { + + /** + * 首页 + */ + HOME("HOME", "cid", "pages/index/index") + + ; + + /** + * 名称 + */ + private final String name; + + /** + * 场景参数 + */ + private final String scene; + + /** + * 页面路径 + */ + private final String page; + + /** + * 根据名称获取枚举 + * + * @param name 名称 + * @return 对应的枚举 + */ + public static MpQrcodeTypeEnum getByName(String name) { + for (MpQrcodeTypeEnum typeEnum : values()) { + if (typeEnum.getName().equals(name)) { + return typeEnum; + } + } + return null; + } + + /** + * 构建场景参数 + * + * @param typeEnum 枚举 + * @param corpid 企业ID + * @return 场景参数 + */ + public static String buildScene(MpQrcodeTypeEnum typeEnum, Integer cid) { + if (typeEnum == null) { + return null; + } + if (typeEnum.getName().equals("HOME")) { + return StringUtils.join(typeEnum.getScene(), "=", cid); + } + return typeEnum.getScene(); + } +}