feat(企业微信): 新增小程序二维码查询和生成功能

添加根据企业ID和名称查询二维码的接口
实现自动生成小程序二维码并保存到本地
新增二维码类型枚举和控制器接口
This commit is contained in:
dzq 2025-12-03 11:36:36 +08:00
parent 61b53dfb67
commit 8dc5994ece
6 changed files with 360 additions and 0 deletions

View File

@ -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.*;
/**
* <p>
* 企业小程序二维码表 前端控制器
* </p>
*
* @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<PageDTO<QyMpQrcodeDTO>> list(SearchMpQrcodeQuery<QyMpQrcodeEntity> query) {
PageDTO<QyMpQrcodeDTO> page = mpQrcodeApplicationService.getQrcodeList(query);
return ResponseDTO.ok(page);
}
@Operation(summary = "二维码详情")
@GetMapping("/{id}")
public ResponseDTO<QyMpQrcodeDTO> detail(@PathVariable Long id) {
// 通过查询接口获取详情
SearchMpQrcodeQuery<QyMpQrcodeEntity> query = new SearchMpQrcodeQuery<>();
query.setPageNum(1);
query.setPageSize(1);
PageDTO<QyMpQrcodeDTO> 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<Void> add(@Validated @RequestBody AddMpQrcodeCommand command) {
mpQrcodeApplicationService.addQrcode(command);
return ResponseDTO.ok();
}
@Operation(summary = "修改二维码")
@AccessLog(title = "二维码管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping("/{id}")
public ResponseDTO<Void> 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<Void> remove(@PathVariable @NotNull List<Long> ids) {
mpQrcodeApplicationService.deleteQrcode(new BulkOperationCommand<>(ids));
return ResponseDTO.ok();
}
@Operation(summary = "根据企业ID查询二维码")
@GetMapping("/by-corpid/{corpid}")
public ResponseDTO<List<QyMpQrcodeEntity>> getByCorpid(@PathVariable String corpid) {
List<QyMpQrcodeEntity> list = mpQrcodeApplicationService.getQrcodeListByCorpid(corpid);
return ResponseDTO.ok(list);
}
@Operation(summary = "根据企业ID和名称查询或创建二维码")
@GetMapping("/get-or-create")
public ResponseDTO<QyMpQrcodeEntity> getOrCreateQrcode(
@RequestParam String corpid,
@RequestParam @Pattern(regexp = "^[A-Z_]+$", message = "名称必须为大写字母或下划线") String name) {
QyMpQrcodeEntity qrcode = mpQrcodeApplicationService.getOrCreateQrcode(corpid, name);
return ResponseDTO.ok(qrcode);
}
}

View File

@ -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<QyMpQrcodeEntity> 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);
}
}

View File

@ -46,4 +46,13 @@ public interface QyMpQrcodeMapper extends BaseMapper<QyMpQrcodeEntity> {
"WHERE corpid = #{corpid} AND deleted = 0 " +
"ORDER BY create_time DESC")
List<QyMpQrcodeEntity> 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);
}

View File

@ -29,4 +29,9 @@ public interface QyMpQrcodeService extends IService<QyMpQrcodeEntity> {
* 根据企业ID查询二维码列表
*/
List<QyMpQrcodeEntity> selectByCorpid(String corpid);
/**
* 根据企业ID和名称查询二维码
*/
QyMpQrcodeEntity selectByCorpidAndName(String corpid, String name);
}

View File

@ -33,4 +33,9 @@ public class QyMpQrcodeServiceImpl extends ServiceImpl<QyMpQrcodeMapper, QyMpQrc
public List<QyMpQrcodeEntity> selectByCorpid(String corpid) {
return baseMapper.selectByCorpid(corpid);
}
@Override
public QyMpQrcodeEntity selectByCorpidAndName(String corpid, String name) {
return baseMapper.selectByCorpidAndName(corpid, name);
}
}

View File

@ -0,0 +1,73 @@
package com.agileboot.domain.qywx.mpQrcode.enums;
import org.apache.commons.lang3.StringUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* <p>
* 小程序二维码类型枚举
* </p>
*
* @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();
}
}