From befc75593d0af5f2e9241f09dc4ab7551f846287 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Wed, 15 Jul 2026 20:40:50 +0800 Subject: [PATCH] 111 --- .../brokerage/BrokerageCodeServiceImpl.java | 41 +++++- .../brokerage/BrokerageRecordServiceImpl.java | 2 +- .../yudao/module/trade/util/PosterPicGen.java | 121 +++++++++++++++- .../module/member/enums/MemberErrorCode.java | 4 +- .../member/client/constant/ChatConstant.java | 4 +- .../client/service/ThirdAuthService.java | 5 + .../client/service/ThirdAuthServiceImpl.java | 137 +++++++++++++++++- .../admin/recent/vo/RecentSaveReqVO.java | 1 - .../app/recent/AppRecentController.java | 4 + .../app/recent/vo/AppRecentSaveReqVO.java | 1 - .../service/photo/PhotoServiceImpl.java | 23 ++- .../service/recent/RecentServiceImpl.java | 34 ++++- .../main/resources/mapper/OperationMapper.xml | 29 ++-- .../src/main/resources/application-dev.yaml | 18 ++- .../src/main/resources/application-local.yaml | 2 +- 15 files changed, 381 insertions(+), 45 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageCodeServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageCodeServiceImpl.java index 5d466d8..e794206 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageCodeServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageCodeServiceImpl.java @@ -4,6 +4,8 @@ import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.RandomUtil; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.infra.api.file.FileApi; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.code.BrokerageCodePageReqVO; import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.code.BrokerageCodeSaveReqVO; import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageCodeDO; @@ -11,6 +13,8 @@ import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageCodeMapper; import cn.iocoder.yudao.module.trade.framework.web.config.TradeBrokerageConfiguration; import cn.iocoder.yudao.module.trade.util.PosterPicGen; import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -34,6 +38,8 @@ import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_C @Validated public class BrokerageCodeServiceImpl implements BrokerageCodeService { + private static final String BROKERAGE_CODE_GENERATE_LOCK_KEY = "trade:brokerage-code:generate:"; + @Resource private TradeBrokerageConfiguration brokerageConfiguration; @@ -42,6 +48,10 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService { @Resource private FileApi fileApi; + @Resource + private MemberUserApi memberUserApi; + @Resource + private RedissonClient redissonClient; @Override public Long createBrokerageCode(BrokerageCodeSaveReqVO createReqVO) { @@ -123,6 +133,21 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService { * 生成用户的分销码 */ private BrokerageCodeDO generateCode(Long userId) { + RLock lock = redissonClient.getLock(BROKERAGE_CODE_GENERATE_LOCK_KEY + userId); + lock.lock(); + try { + // 加锁期间其他请求可能已生成,避免重复生成海报 + BrokerageCodeDO existingCode = brokerageCodeMapper.selectOne(BrokerageCodeDO::getMemberId, userId); + if (ObjectUtil.isNotEmpty(existingCode)) { + return existingCode; + } + return doGenerateCode(userId); + } finally { + lock.unlock(); + } + } + + private BrokerageCodeDO doGenerateCode(Long userId) { boolean isRepet = true; String code = null; while (isRepet) { @@ -130,8 +155,16 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService { BrokerageCodeDO brokerageCode = brokerageCodeMapper.selectOne(BrokerageCodeDO::getCode, code); isRepet = ObjectUtil.isNotEmpty(brokerageCode); } + + // 查询用户获取用户头像 + String userAvatar = ""; + MemberUserRespDTO user = memberUserApi.getUser(userId); + if (ObjectUtil.isNotEmpty(user) && ObjectUtil.isNotEmpty(user)) { + userAvatar = user.getAvatar(); + } + String url = generatePosterUrl(code, userId); - BrokerageCodeDO saveObj = BrokerageCodeDO.builder().memberId(userId).code(code).posterPic(generatePosterPic(code, url)).url(url).build(); + BrokerageCodeDO saveObj = BrokerageCodeDO.builder().memberId(userId).code(code).posterPic(generatePosterPic(userAvatar, code, url)).url(url).build(); brokerageCodeMapper.insert(saveObj); return saveObj; } @@ -139,9 +172,9 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService { /** * 生成分销海报图 */ - private String generatePosterPic(String code, String qrCodeUrl) { + private String generatePosterPic(String userAvatar, String code, String qrCodeUrl) { try { - byte[] fileInputStream = new PosterPicGen().setCode(code).setQrCodeUrl(qrCodeUrl).gen(); + byte[] fileInputStream = new PosterPicGen().setUserAvatar(userAvatar).setCode(code).setQrCodeUrl(qrCodeUrl).gen(); String file = fileApi.createFile(fileInputStream); return file; } catch (Exception e) { @@ -155,4 +188,4 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService { return brokerageConfiguration.getDownloadUrl() + "?code=" + code + "&userId=" + userId; } -} \ No newline at end of file +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java index 878f332..19cd16e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java @@ -120,7 +120,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService { TradeConfigDO memberConfig = tradeConfigService.getTradeConfig(); // 0 未启用分销功能 if (memberConfig == null || !BooleanUtil.isTrue(memberConfig.getBrokerageEnabled())) { - log.error("[addBrokerage][增加佣金失败:brokerageEnabled 未配置,userId({}) bizType({}) list({})", userId, bizType, list); + log.error("[addRecentBrokerage][增加佣金失败:brokerageEnabled 未配置,userId({}) bizType({}) list({})", userId, bizType, list); return; } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/util/PosterPicGen.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/util/PosterPicGen.java index d8d19a0..9c72b64 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/util/PosterPicGen.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/util/PosterPicGen.java @@ -4,12 +4,18 @@ package cn.iocoder.yudao.module.trade.util; import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.extra.qrcode.QrCodeUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; import lombok.extern.slf4j.Slf4j; import javax.imageio.ImageIO; import java.awt.*; +import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; /** * 生成海报 @@ -17,6 +23,18 @@ import java.io.ByteArrayOutputStream; @Slf4j public class PosterPicGen { + private static final int AVATAR_X = 20; + + private static final int AVATAR_Y = 40; + + private static final int AVATAR_SIZE = 48; + + private static final int AVATAR_CORNER_RADIUS = 14; + + private static final int AVATAR_DOWNLOAD_TIMEOUT = 5_000; + + private static final int MAX_AVATAR_FILE_SIZE = 5 * 1024 * 1024; + private Graphics2D graphics; private int pageWidth = 375; @@ -31,6 +49,8 @@ public class PosterPicGen { private String qrCodeUrl = ""; + private String userAvatar = ""; + private String bottomDescription = "微信无法打开的用户请更换为其他方式扫码"; public PosterPicGen setCode(String code) { @@ -38,6 +58,11 @@ public class PosterPicGen { return this; } + public PosterPicGen setUserAvatar(String userAvatar) { + this.userAvatar = userAvatar; + return this; + } + public PosterPicGen setQrCodeUrl(String qrCodeUrl) { @@ -51,6 +76,8 @@ public class PosterPicGen { baseImage.getScaledInstance(this.pageWidth, this.pageHeight, Image.SCALE_SMOOTH); graphics = baseImage.createGraphics(); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + // 替换 SVG 左上角 48 x 48、圆角 14px 的默认头像 + drawUserAvatar(); // 设置 title graphics.setFont(new Font("PingFang SC, Microsoft YaHei, sans-serif", Font.BOLD, 18)); graphics.setColor(Color.decode("#333333")); @@ -72,9 +99,6 @@ public class PosterPicGen { int y = 350; graphics.drawImage(generate, x, y, qrWidth, qrHeight, null); } - - graphics.dispose(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(baseImage, "png", baos); return baos.toByteArray(); @@ -82,6 +106,97 @@ public class PosterPicGen { e.printStackTrace(); log.error("|生成海报|,意外的错误error:|{}|", e.getMessage()); return null; + } finally { + if (graphics != null) { + graphics.dispose(); + graphics = null; + } + } + } + + private void drawUserAvatar() { + if (ObjectUtil.isEmpty(this.userAvatar)) { + return; + } + BufferedImage avatar = loadUserAvatar(); + if (avatar == null) { + return; + } + + // 对应 SVG 的 preserveAspectRatio="xMidYMid slice",居中裁剪后再缩放,避免头像变形 + int sourceSize = Math.min(avatar.getWidth(), avatar.getHeight()); + int sourceX = (avatar.getWidth() - sourceSize) / 2; + int sourceY = (avatar.getHeight() - sourceSize) / 2; + BufferedImage roundedAvatar = new BufferedImage(AVATAR_SIZE, AVATAR_SIZE, BufferedImage.TYPE_INT_ARGB); + Graphics2D avatarGraphics = roundedAvatar.createGraphics(); + try { + avatarGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + avatarGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + avatarGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + avatarGraphics.setColor(Color.WHITE); + avatarGraphics.fill(new RoundRectangle2D.Double(0, 0, AVATAR_SIZE, AVATAR_SIZE, + AVATAR_CORNER_RADIUS * 2.0, AVATAR_CORNER_RADIUS * 2.0)); + avatarGraphics.setComposite(AlphaComposite.SrcAtop); + avatarGraphics.drawImage(avatar, + 0, 0, AVATAR_SIZE, AVATAR_SIZE, + sourceX, sourceY, sourceX + sourceSize, sourceY + sourceSize, null); + } finally { + avatarGraphics.dispose(); + } + graphics.drawImage(roundedAvatar, AVATAR_X, AVATAR_Y, null); + } + + private BufferedImage loadUserAvatar() { + try (HttpResponse response = HttpRequest.get(this.userAvatar) + .setConnectionTimeout(AVATAR_DOWNLOAD_TIMEOUT) + .setReadTimeout(AVATAR_DOWNLOAD_TIMEOUT) + .setMaxRedirectCount(3) + .executeAsync()) { + if (!response.isOk()) { + log.warn("|生成海报-加载用户头像失败|, HTTP status:|{}|", response.getStatus()); + return null; + } + byte[] avatarBytes = readAvatarBytes(response); + if (avatarBytes == null) { + return null; + } + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(avatarBytes)) { + BufferedImage avatar = ImageIO.read(inputStream); + if (avatar == null) { + log.warn("|生成海报-加载用户头像失败|, 图片格式无法识别"); + } + return avatar; + } + } catch (Exception e) { + log.warn("|生成海报-加载用户头像失败|, error:|{}|", e.getMessage()); + return null; + } + } + + private byte[] readAvatarBytes(HttpResponse response) throws IOException { + long contentLength = response.contentLength(); + if (contentLength > MAX_AVATAR_FILE_SIZE) { + log.warn("|生成海报-加载用户头像失败|, 图片文件超过 {} 字节", MAX_AVATAR_FILE_SIZE); + return null; + } + int initialSize = contentLength > 0 ? (int) contentLength : 8192; + try (InputStream inputStream = response.bodyStream(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(initialSize)) { + if (inputStream == null) { + return null; + } + byte[] buffer = new byte[8192]; + int totalSize = 0; + int readSize; + while ((readSize = inputStream.read(buffer)) != -1) { + totalSize += readSize; + if (totalSize > MAX_AVATAR_FILE_SIZE) { + log.warn("|生成海报-加载用户头像失败|, 图片文件超过 {} 字节", MAX_AVATAR_FILE_SIZE); + return null; + } + outputStream.write(buffer, 0, readSize); + } + return outputStream.toByteArray(); } } diff --git a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java index 31e75f1..6a5dfef 100644 --- a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java +++ b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java @@ -74,8 +74,8 @@ public interface MemberErrorCode { ErrorCode PHOTO_TYPE_NOT_SUPPORT = new ErrorCode(1_004_013_001, "只有[{}]能删除"); //========== 审核管理 1-004-014-000 ========== - ErrorCode CHECK_TEXT_NOT_PASS = new ErrorCode(1_004_014_001, "文字审核不通过,[{}]"); - ErrorCode CHECK_IMAGE_NOT_PASS = new ErrorCode(1_004_014_002, "图片审核不通过,[{}]"); + ErrorCode CHECK_TEXT_NOT_PASS = new ErrorCode(1_004_014_001, "文字审核不通过,{}"); + ErrorCode CHECK_IMAGE_NOT_PASS = new ErrorCode(1_004_014_002, "图片审核不通过,{}"); //========== 用户位置管理 1-004-015-000 ========== ErrorCode LOCATION_NOT_EXISTS = new ErrorCode(1_004_015_000, "用户位置不存在"); diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/constant/ChatConstant.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/constant/ChatConstant.java index 6512f71..7eefa83 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/constant/ChatConstant.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/constant/ChatConstant.java @@ -11,14 +11,14 @@ public final class ChatConstant { */ public static final Long COM_MESSAGE_USER_ID = -100L; public static final String COM_MESSAGE_USER_NAME = "消息通知"; - public static final String COM_MESSAGE_USER_AVATAR = "https://chatapi.zianxin.com/admin-api/infra/file/4/get/3dab7a0921d4d2ea6f7daee399114e9241145e754f7a1e4e1a955fada4542116.png"; + public static final String COM_MESSAGE_USER_AVATAR = "https://api.htysapp.com/admin-api/infra/file/4/get/d0c93df86b65174bc58513f1653bef14e6639ad64d7ee2fdc1e856e6aa2974f0.png"; /** * 客服用户id */ public static final Long COM_SERVICE_USER_ID = -200L; public static final String COM_SERVICE_USER_NAME = "荷塘月色客服"; - public static final String COM_SERVICE_USER_AVATAR = "https://chatapi.zianxin.com/admin-api/infra/file/4/get/b85e0bc2e4338c9919ac13e1f12cb3ac40dc7ba807a0e65ef83c822ef7d2f1a2.png"; + public static final String COM_SERVICE_USER_AVATAR = "https://api.htysapp.com/admin-api/infra/file/4/get/affa4cac924cb13fe5706100828685015d512bca7dfa0e26ef82422b31877abc.png"; public static final List IGNORE_USER_IDS = Arrays.asList(0L, COM_MESSAGE_USER_ID, COM_SERVICE_USER_ID); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthService.java index 9f2ff1b..3f39314 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthService.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthService.java @@ -18,4 +18,9 @@ public interface ThirdAuthService { */ ThirdAuthVo imageAuditSync(String imageUrl); + /** + * 视频审核-同步 + */ + ThirdAuthVo videoAuditSync(String videoUrl); + } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthServiceImpl.java index 27b8c17..61edd7f 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthServiceImpl.java @@ -25,6 +25,11 @@ import java.util.*; @Slf4j public class ThirdAuthServiceImpl implements ThirdAuthService { + private static final int VIDEO_MODERATION_SUCCESS_CODE = 200; + private static final int VIDEO_MODERATION_PROCESSING_CODE = 280; + private static final int VIDEO_AUDIT_MAX_QUERY_COUNT = 20; + private static final long VIDEO_AUDIT_QUERY_INTERVAL_MILLIS = 30_000L; + @Resource private ThirdAlConfig thirdAlConfig; @@ -153,7 +158,7 @@ public class ThirdAuthServiceImpl implements ThirdAuthService { imageBatchModerationRequest.setServiceParameters(JSON.toJSONString(serviceParameters)); ImageModerationResponse response = client.imageModerationWithOptions(imageBatchModerationRequest, runtime); if (response.getStatusCode() != 200) { - throw new RuntimeException("文本审核失败code:" + response.getStatusCode()); + throw new RuntimeException("图片审核核失败code:" + response.getStatusCode()); } ImageModerationResponseBody body = response.getBody(); if (ObjectUtil.isEmpty(body)) { @@ -167,6 +172,25 @@ public class ThirdAuthServiceImpl implements ThirdAuthService { if (ObjectUtil.isEmpty(list)) { throw new RuntimeException("result为null"); } + StringBuilder errorMsg = new StringBuilder(); + String firstErrorMsg = "你上传的图片"; + for (ImageModerationResponseBody.ImageModerationResponseBodyDataResult v : list) { + // 检测结果 + String description = v.getDescription(); + // 置信分值 + Float confidence = v.getConfidence(); + if (confidence >= 80) { + errorMsg.append(String.format("疑似%s,", description)); + } + } + // confidence最大的数据 + ImageModerationResponseBody.ImageModerationResponseBodyDataResult max = list.stream().max((pren, next) -> next.getConfidence().compareTo(pren.getConfidence())).orElse(null); + if (ObjectUtil.isNotEmpty(max)) { + firstErrorMsg = String.format("你发送的内容疑似%s,请重新上传", max.getDescription()); + } + resultVo.setIsSuccess(false); + resultVo.setErrorMsg(errorMsg.toString()); + resultVo.setFirstErrorMsg(firstErrorMsg); return resultVo; } catch (Exception e) { e.printStackTrace(); @@ -176,4 +200,115 @@ public class ThirdAuthServiceImpl implements ThirdAuthService { } } + /** + * 视频审核-同步 + */ + @Override + public ThirdAuthVo videoAuditSync(String videoUrl) { + ThirdAuthVo resultVo = new ThirdAuthVo(); + try { + Client client = createClient(); + if (ObjectUtil.isEmpty(client)) return null; + RuntimeOptions runtime = new RuntimeOptions(); + + Map serviceParameters = new HashMap<>(); + serviceParameters.put("url", videoUrl); + serviceParameters.put("dataId", String.valueOf(UUID.randomUUID())); + serviceParameters.put("infoType", "customImage,textInImage,publicFigure"); + VideoModerationRequest request = new VideoModerationRequest(); + request.setService("videoDetection"); + request.setServiceParameters(JSON.toJSONString(serviceParameters)); + + VideoModerationResponse response = client.videoModerationWithOptions(request, runtime); + if (response.getStatusCode() != VIDEO_MODERATION_SUCCESS_CODE) { + throw new RuntimeException("视频审核任务提交失败code:" + response.getStatusCode()); + } + VideoModerationResponseBody body = response.getBody(); + if (ObjectUtil.isEmpty(body)) { + throw new RuntimeException("body为null"); + } + if (!Objects.equals(body.getCode(), VIDEO_MODERATION_SUCCESS_CODE)) { + throw new RuntimeException("视频审核任务提交失败code:" + body.getCode() + ",message:" + body.getMessage()); + } + VideoModerationResponseBody.VideoModerationResponseBodyData data = body.getData(); + if (ObjectUtil.isEmpty(data) || ObjectUtil.isEmpty(data.getTaskId())) { + throw new RuntimeException("taskId为null"); + } + + Map resultParameters = new HashMap<>(); + resultParameters.put("taskId", data.getTaskId()); + VideoModerationResultRequest resultRequest = new VideoModerationResultRequest(); + resultRequest.setService("videoDetection"); + resultRequest.setServiceParameters(JSON.toJSONString(resultParameters)); + + // 视频审核为异步任务,按官方建议每 30 秒轮询一次处理结果。 + for (int i = 0; i < VIDEO_AUDIT_MAX_QUERY_COUNT; i++) { + VideoModerationResultResponse resultResponse = client.videoModerationResultWithOptions(resultRequest, runtime); + if (resultResponse.getStatusCode() != VIDEO_MODERATION_SUCCESS_CODE) { + throw new RuntimeException("视频审核结果查询失败code:" + resultResponse.getStatusCode()); + } + VideoModerationResultResponseBody resultBody = resultResponse.getBody(); + if (ObjectUtil.isEmpty(resultBody)) { + throw new RuntimeException("resultBody为null"); + } + if (Objects.equals(resultBody.getCode(), VIDEO_MODERATION_PROCESSING_CODE)) { + if (i < VIDEO_AUDIT_MAX_QUERY_COUNT - 1) { + Thread.sleep(VIDEO_AUDIT_QUERY_INTERVAL_MILLIS); + } + continue; + } + if (!Objects.equals(resultBody.getCode(), VIDEO_MODERATION_SUCCESS_CODE)) { + throw new RuntimeException("视频审核结果查询失败code:" + resultBody.getCode() + + ",message:" + resultBody.getMessage()); + } + + VideoModerationResultResponseBody.VideoModerationResultResponseBodyData resultData = resultBody.getData(); + if (ObjectUtil.isEmpty(resultData)) { + throw new RuntimeException("resultData为null"); + } + if (successRiskLevelList.contains(resultData.getRiskLevel())) { + resultVo.setIsSuccess(true); + return resultVo; + } + + LinkedHashSet descriptions = getVideoRiskDescriptions(resultData); + String riskDescription = descriptions.isEmpty() ? "存在风险" : String.join(",", descriptions); + resultVo.setIsSuccess(false); + resultVo.setErrorMsg(String.format("疑似%s,请重新上传", riskDescription)); + resultVo.setFirstErrorMsg(String.format("你上传的视频疑似%s,请重新上传", riskDescription)); + return resultVo; + } + throw new RuntimeException("视频审核结果查询超时"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("|视频审核-同步|,线程被中断videoUrl:{}error:{}", videoUrl, e.getMessage(), e); + resultVo.setIsSuccess(true); + return resultVo; + } catch (Exception e) { + e.printStackTrace(); + log.error("|视频审核-同步|,意外的错误videoUrl:{}error:{}", videoUrl, e.getMessage()); + resultVo.setIsSuccess(true); + return resultVo; + } + } + + private LinkedHashSet getVideoRiskDescriptions( + VideoModerationResultResponseBody.VideoModerationResultResponseBodyData data) { + LinkedHashSet descriptions = new LinkedHashSet<>(); + if (ObjectUtil.isNotEmpty(data.getFrameResult()) + && ObjectUtil.isNotEmpty(data.getFrameResult().getFrameSummarys())) { + data.getFrameResult().getFrameSummarys().forEach(summary -> + descriptions.add(ObjectUtil.isNotEmpty(summary.getDescription()) + ? summary.getDescription() : summary.getLabel())); + } + if (ObjectUtil.isNotEmpty(data.getAudioResult()) + && ObjectUtil.isNotEmpty(data.getAudioResult().getAudioSummarys())) { + data.getAudioResult().getAudioSummarys().forEach(summary -> + descriptions.add(ObjectUtil.isNotEmpty(summary.getDescription()) + ? summary.getDescription() : summary.getLabel())); + } + descriptions.removeIf(ObjectUtil::isEmpty); + return descriptions; + } + } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentSaveReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentSaveReqVO.java index 7b15d10..07a5559 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentSaveReqVO.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentSaveReqVO.java @@ -19,7 +19,6 @@ public class RecentSaveReqVO { private Long memberId; @Schema(description = "主图", requiredMode = Schema.RequiredMode.REQUIRED) - @NotEmpty(message = "主图不能为空") private String coverPic; @Schema(description = "视频") diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/AppRecentController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/AppRecentController.java index f43eb71..8e97750 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/AppRecentController.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/AppRecentController.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.member.controller.app.recent; +import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -53,6 +54,9 @@ public class AppRecentController { @PostMapping("/create") @Operation(summary = "创建朋友圈") public CommonResult createRecent(@Valid @RequestBody AppRecentSaveReqVO dto) { + if (ObjectUtil.isAllEmpty(dto.getCoverVideo(), dto.getCoverPic())) { + throw new RuntimeException("图片、视频必须上传其中之一"); + } dto.setMemberId(getLoginUserId()); return success(recentService.createRecent(BeanUtils.toBean(dto, RecentSaveReqVO.class))); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentSaveReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentSaveReqVO.java index 2269d6c..5a1d6cb 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentSaveReqVO.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentSaveReqVO.java @@ -14,7 +14,6 @@ public class AppRecentSaveReqVO { private Long memberId; @Schema(description = "主图url") - @NotEmpty(message = "主图不能为空") private String coverPic; @Schema(description = "视频url") diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/photo/PhotoServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/photo/PhotoServiceImpl.java index 310c2e5..55bf7a2 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/photo/PhotoServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/photo/PhotoServiceImpl.java @@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.member.enums.photo.PhotoResourceTypeEnum; import cn.iocoder.yudao.module.member.client.service.ThirdAuthService; import cn.iocoder.yudao.module.member.service.photo.BO.MemberPhotoTotalExtBO; import cn.iocoder.yudao.module.member.service.user.MemberUserService; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -26,6 +27,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.member.dal.mysql.photo.PhotoMapper; import java.util.List; +import java.util.function.Function; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -60,7 +62,7 @@ public class PhotoServiceImpl implements PhotoService { public Long appUpload(AppUploadReqVo dto) { // 校验图片 if (ObjectUtil.equals(dto.getResourceType(), PhotoResourceTypeEnum.IMAGE)) { - ThirdAuthVo textRes = thirdAuthService.imageAuditSync(dto.getAddress()); + ThirdAuthVo textRes = auditCommaSeparatedContent(dto.getAddress(), thirdAuthService::imageAuditSync); // 处理文字 if (ObjectUtil.isEmpty(textRes)) { throw exception(MemberErrorCode.CHECK_TEXT_NOT_PASS, "未知错误"); @@ -74,6 +76,23 @@ public class PhotoServiceImpl implements PhotoService { return photo.getId(); } + private ThirdAuthVo auditCommaSeparatedContent(String content, Function auditFunction) { + if (StringUtils.isBlank(content)) { + return new ThirdAuthVo(); + } + for (String item : StringUtils.split(content, ',')) { + String auditContent = StringUtils.trim(item); + if (StringUtils.isBlank(auditContent)) { + continue; + } + ThirdAuthVo result = auditFunction.apply(auditContent); + if (ObjectUtil.isEmpty(result) || result.isError()) { + return result; + } + } + return new ThirdAuthVo(); + } + @Override public void updatePhoto(PhotoSaveReqVO updateReqVO) { // 校验存在 @@ -164,4 +183,4 @@ public class PhotoServiceImpl implements PhotoService { public List getMemberPhotoTotalExtList(List memberId) { return photoMapper.getMemberPhotoExtList(memberId); } -} \ No newline at end of file +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImpl.java index fdb27f8..c366c65 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImpl.java @@ -64,6 +64,7 @@ import java.math.RoundingMode; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -137,8 +138,12 @@ public class RecentServiceImpl implements RecentService { recentMapper.insert(recent); // 文字和图片违规处理 - CompletableFuture textFuture = CompletableFuture.supplyAsync(() -> thirdAuthService.textAuditSync(vo.getDescription())); - CompletableFuture imageFuture = CompletableFuture.supplyAsync(() -> thirdAuthService.imageAuditSync(vo.getCoverPic())); + CompletableFuture textFuture = CompletableFuture.supplyAsync( + () -> auditCommaSeparatedContent(vo.getDescription(), thirdAuthService::textAuditSync) + ); + CompletableFuture imageFuture = CompletableFuture.supplyAsync( + () -> auditCommaSeparatedContent(vo.getCoverPic(), thirdAuthService::imageAuditSync) + ); try { ThirdAuthVo textRes = textFuture.get(); @@ -155,10 +160,10 @@ public class RecentServiceImpl implements RecentService { throw exception(MemberErrorCode.CHECK_IMAGE_NOT_PASS, "未知错误"); } if (imageRes.isError() && ObjectUtil.isNotEmpty(imageRes.getFirstErrorMsg())) { - throw exception(MemberErrorCode.CHECK_TEXT_NOT_PASS, imageRes.getFirstErrorMsg()); + throw exception(MemberErrorCode.CHECK_IMAGE_NOT_PASS, imageRes.getFirstErrorMsg()); } } catch (Exception e) { - log.error("|创建朋友圈|,以外的错误error:{}", e.getMessage()); + log.error("|创建朋友圈|,意外的错误error:{}", e.getMessage()); throw exception(MemberErrorCode.EMPTY, e.getMessage()); } @@ -166,6 +171,23 @@ public class RecentServiceImpl implements RecentService { return recent.getId(); } + private ThirdAuthVo auditCommaSeparatedContent(String content, Function auditFunction) { + if (StringUtils.isBlank(content)) { + return new ThirdAuthVo(); + } + for (String item : StringUtils.split(content, ',')) { + String auditContent = StringUtils.trim(item); + if (StringUtils.isBlank(auditContent)) { + continue; + } + ThirdAuthVo result = auditFunction.apply(auditContent); + if (ObjectUtil.isEmpty(result) || result.isError()) { + return result; + } + } + return new ThirdAuthVo(); + } + /** * 朋友圈数量统计 */ @@ -642,7 +664,7 @@ public class RecentServiceImpl implements RecentService { return total; } // 当前用户`operateUserId`是否在3个小时前查看过`userid`的微信 - LocalDateTime time = LocalDateTime.now().minusHours(3); + LocalDateTime time = LocalDateTime.now().plusHours(3); String timeStr = LocalDateTimeUtil.format(time, DatePattern.NORM_DATETIME_PATTERN); Long count = operationMapper.selectCount(new LambdaQueryWrapperX() .eq(OperationDO::getOperatorMemberId, operateUserId) @@ -949,4 +971,4 @@ public class RecentServiceImpl implements RecentService { resultQuery.setExcludeUserIdList(excludeUserIdList.stream().collect(Collectors.toList())); } -} \ No newline at end of file +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/resources/mapper/OperationMapper.xml b/yudao-module-member/yudao-module-member-biz/src/main/resources/mapper/OperationMapper.xml index 9bd5da6..901e265 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/resources/mapper/OperationMapper.xml +++ b/yudao-module-member/yudao-module-member-biz/src/main/resources/mapper/OperationMapper.xml @@ -1,7 +1,7 @@ - + a.deleted = 0 and a.member_recent_id = #{dto.memberRecentId} @@ -26,23 +26,24 @@