From 0f9d061f4e02bf5f5b1a426a23d2ae516cf070d8 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Fri, 31 Jul 2026 20:40:05 +0800 Subject: [PATCH] 11 --- .../brokerage/BrokerageCodeServiceImpl.java | 60 +++++++- .../yudao/module/trade/util/PosterPicGen.java | 129 ++++++++++++++---- .../src/main/resources/image/base-poster.svg | 84 ++++++++++++ .../module/trade/util/PosterPicGenTest.java | 25 ++++ .../admin/user/MemberUserController.java | 3 +- .../user/MemberRecentTagServiceImp.java | 22 ++- .../service/user/MemberUserServiceImpl.java | 3 +- .../user/MemberRecentTagServiceImpTest.java | 58 ++++++++ 8 files changed, 346 insertions(+), 38 deletions(-) create mode 100644 yudao-module-mall/yudao-module-trade-biz/src/main/resources/image/base-poster.svg create mode 100644 yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/util/PosterPicGenTest.java create mode 100644 yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberRecentTagServiceImpTest.java 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 e794206..238bfc8 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 @@ -38,6 +38,8 @@ import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_C @Validated public class BrokerageCodeServiceImpl implements BrokerageCodeService { + private static final String POSTER_VERSION_MARKER = "#poster-v2-hd"; + private static final String BROKERAGE_CODE_GENERATE_LOCK_KEY = "trade:brokerage-code:generate:"; @Resource @@ -125,10 +127,56 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService { public BrokerageCodeDO getUserBrokerageCode(Long userId) { // 查询用户有没有分销码 BrokerageCodeDO brokerageCode = brokerageCodeMapper.selectOne(BrokerageCodeDO::getMemberId, userId); - if (ObjectUtil.isNotEmpty(brokerageCode)) return brokerageCode; + if (ObjectUtil.isNotEmpty(brokerageCode)) { + return refreshLegacyPoster(userId, brokerageCode); + } return generateCode(userId); } + private BrokerageCodeDO refreshLegacyPoster(Long userId, BrokerageCodeDO brokerageCode) { + if (isLatestPoster(brokerageCode.getPosterPic())) { + return brokerageCode; + } + RLock lock = redissonClient.getLock(BROKERAGE_CODE_GENERATE_LOCK_KEY + userId); + lock.lock(); + try { + BrokerageCodeDO currentCode = brokerageCodeMapper.selectOne(BrokerageCodeDO::getMemberId, userId); + if (ObjectUtil.isEmpty(currentCode) || isLatestPoster(currentCode.getPosterPic())) { + return currentCode; + } + String posterCode = currentCode.getCode(); + String userAvatar = ""; + MemberUserRespDTO user = memberUserApi.getUser(userId); + if (ObjectUtil.isNotEmpty(user)) { + userAvatar = user.getAvatar(); + if (ObjectUtil.isNotEmpty(user.getUserCode())) { + posterCode = user.getUserCode(); + } + } + String posterUrl = ObjectUtil.isNotEmpty(currentCode.getUrl()) + ? currentCode.getUrl() : generatePosterUrl(currentCode.getCode(), userId); + String posterPic = generatePosterPic(userAvatar, posterCode, posterUrl); + if (ObjectUtil.isEmpty(posterPic)) { + return currentCode; + } + BrokerageCodeDO updateObj = BrokerageCodeDO.builder() + .id(currentCode.getId()) + .posterPic(posterPic) + .url(posterUrl) + .build(); + brokerageCodeMapper.updateById(updateObj); + currentCode.setPosterPic(posterPic); + currentCode.setUrl(posterUrl); + return currentCode; + } finally { + lock.unlock(); + } + } + + private boolean isLatestPoster(String posterPic) { + return ObjectUtil.isNotEmpty(posterPic) && posterPic.endsWith(POSTER_VERSION_MARKER); + } + /** * 生成用户的分销码 */ @@ -158,13 +206,17 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService { // 查询用户获取用户头像 String userAvatar = ""; + String posterCode = code; MemberUserRespDTO user = memberUserApi.getUser(userId); - if (ObjectUtil.isNotEmpty(user) && ObjectUtil.isNotEmpty(user)) { + if (ObjectUtil.isNotEmpty(user)) { userAvatar = user.getAvatar(); + if (ObjectUtil.isNotEmpty(user.getUserCode())) { + posterCode = user.getUserCode(); + } } String url = generatePosterUrl(code, userId); - BrokerageCodeDO saveObj = BrokerageCodeDO.builder().memberId(userId).code(code).posterPic(generatePosterPic(userAvatar, code, url)).url(url).build(); + BrokerageCodeDO saveObj = BrokerageCodeDO.builder().memberId(userId).code(code).posterPic(generatePosterPic(userAvatar, posterCode, url)).url(url).build(); brokerageCodeMapper.insert(saveObj); return saveObj; } @@ -176,7 +228,7 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService { try { byte[] fileInputStream = new PosterPicGen().setUserAvatar(userAvatar).setCode(code).setQrCodeUrl(qrCodeUrl).gen(); String file = fileApi.createFile(fileInputStream); - return file; + return file + POSTER_VERSION_MARKER; } catch (Exception e) { e.printStackTrace(); log.error("|生成分销海报图|,意外的错误error:|{}|", e.getMessage()); 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 9c72b64..a49375d 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 @@ -10,6 +10,8 @@ import lombok.extern.slf4j.Slf4j; import javax.imageio.ImageIO; import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; @@ -23,6 +25,8 @@ import java.io.InputStream; @Slf4j public class PosterPicGen { + private static final int RENDER_SCALE = 3; + private static final int AVATAR_X = 20; private static final int AVATAR_Y = 40; @@ -31,6 +35,16 @@ public class PosterPicGen { private static final int AVATAR_CORNER_RADIUS = 14; + private static final int AVATAR_CLEAR_MARGIN = 2; + + private static final double CENTER_LOGO_X = 153.5; + + private static final double CENTER_LOGO_Y = 116; + + private static final int CENTER_LOGO_SIZE = 68; + + private static final int CENTER_LOGO_CLEAR_MARGIN = 2; + private static final int AVATAR_DOWNLOAD_TIMEOUT = 5_000; private static final int MAX_AVATAR_FILE_SIZE = 5 * 1024 * 1024; @@ -72,12 +86,15 @@ public class PosterPicGen { public byte[] gen() { try { - BufferedImage baseImage = ImageIO.read(ResourceUtil.getResource("image/base-poster.png")); - baseImage.getScaledInstance(this.pageWidth, this.pageHeight, Image.SCALE_SMOOTH); + BufferedImage baseImage = createBaseImage(); graphics = baseImage.createGraphics(); - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + applyQualityRenderingHints(graphics); + graphics.scale(RENDER_SCALE, RENDER_SCALE); + BufferedImage avatar = loadUserAvatar(); // 替换 SVG 左上角 48 x 48、圆角 14px 的默认头像 - drawUserAvatar(); + drawUserAvatar(avatar); + // 替换 SVG 中间 68 x 68 的 Logo + drawCenterLogo(avatar); // 设置 title graphics.setFont(new Font("PingFang SC, Microsoft YaHei, sans-serif", Font.BOLD, 18)); graphics.setColor(Color.decode("#333333")); @@ -94,8 +111,9 @@ public class PosterPicGen { if (ObjectUtil.isNotEmpty(this.qrCodeUrl)) { int qrWidth = 150; int qrHeight = 150; - BufferedImage generate = QrCodeUtil.generate(this.qrCodeUrl, qrWidth, qrHeight); - int x = (baseImage.getWidth() - qrWidth) / 2; + BufferedImage generate = QrCodeUtil.generate( + this.qrCodeUrl, qrWidth * RENDER_SCALE, qrHeight * RENDER_SCALE); + int x = (this.pageWidth - qrWidth) / 2; int y = 350; graphics.drawImage(generate, x, y, qrWidth, qrHeight, null); } @@ -114,39 +132,102 @@ public class PosterPicGen { } } - private void drawUserAvatar() { - if (ObjectUtil.isEmpty(this.userAvatar)) { - return; + private BufferedImage createBaseImage() throws IOException { + BufferedImage template = ImageIO.read(ResourceUtil.getResource("image/base-poster.png")); + BufferedImage baseImage = new BufferedImage( + this.pageWidth * RENDER_SCALE, + this.pageHeight * RENDER_SCALE, + BufferedImage.TYPE_INT_ARGB); + Graphics2D baseGraphics = baseImage.createGraphics(); + try { + applyQualityRenderingHints(baseGraphics); + baseGraphics.drawImage(template, 0, 0, baseImage.getWidth(), baseImage.getHeight(), null); + } finally { + baseGraphics.dispose(); } - BufferedImage avatar = loadUserAvatar(); + return baseImage; + } + + private void applyQualityRenderingHints(Graphics2D targetGraphics) { + targetGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + targetGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + targetGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + targetGraphics.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, + RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + targetGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + targetGraphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + targetGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, + RenderingHints.VALUE_FRACTIONALMETRICS_ON); + } + + private void drawUserAvatar(BufferedImage avatar) { if (avatar == null) { return; } + graphics.setColor(Color.WHITE); + graphics.fill(new RoundRectangle2D.Double( + AVATAR_X - AVATAR_CLEAR_MARGIN, + AVATAR_Y - AVATAR_CLEAR_MARGIN, + AVATAR_SIZE + AVATAR_CLEAR_MARGIN * 2.0, + AVATAR_SIZE + AVATAR_CLEAR_MARGIN * 2.0, + (AVATAR_CORNER_RADIUS + AVATAR_CLEAR_MARGIN) * 2.0, + (AVATAR_CORNER_RADIUS + AVATAR_CLEAR_MARGIN) * 2.0)); + drawAvatar(avatar, AVATAR_X, AVATAR_Y, AVATAR_SIZE, + new RoundRectangle2D.Double(0, 0, AVATAR_SIZE, AVATAR_SIZE, + AVATAR_CORNER_RADIUS * 2.0, AVATAR_CORNER_RADIUS * 2.0)); + } + private void drawCenterLogo(BufferedImage avatar) { + if (avatar == null) { + return; + } + graphics.setColor(Color.WHITE); + graphics.fill(new Ellipse2D.Double( + CENTER_LOGO_X - CENTER_LOGO_CLEAR_MARGIN, + CENTER_LOGO_Y - CENTER_LOGO_CLEAR_MARGIN, + CENTER_LOGO_SIZE + CENTER_LOGO_CLEAR_MARGIN * 2.0, + CENTER_LOGO_SIZE + CENTER_LOGO_CLEAR_MARGIN * 2.0)); + drawAvatar(avatar, CENTER_LOGO_X, CENTER_LOGO_Y, CENTER_LOGO_SIZE, + new Ellipse2D.Double(0, 0, CENTER_LOGO_SIZE, CENTER_LOGO_SIZE)); + } + + private void drawAvatar(BufferedImage avatar, double x, double y, int size, Shape clipShape) { + 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(); + BufferedImage croppedAvatar = avatar.getSubimage(sourceX, sourceY, sourceSize, sourceSize); + Shape oldClip = graphics.getClip(); + Object oldInterpolation = graphics.getRenderingHint(RenderingHints.KEY_INTERPOLATION); + Object oldAlphaInterpolation = graphics.getRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION); 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); + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + graphics.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + Shape translatedClip = AffineTransform.getTranslateInstance(x, y).createTransformedShape(clipShape); + graphics.setColor(Color.WHITE); + graphics.fill(translatedClip); + graphics.setClip(translatedClip); + AffineTransform avatarTransform = AffineTransform.getTranslateInstance(x, y); + avatarTransform.scale((double) size / sourceSize, (double) size / sourceSize); + graphics.drawImage(croppedAvatar, avatarTransform, null); } finally { - avatarGraphics.dispose(); + graphics.setClip(oldClip); + if (oldInterpolation != null) { + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, oldInterpolation); + } + if (oldAlphaInterpolation != null) { + graphics.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, oldAlphaInterpolation); + } } - graphics.drawImage(roundedAvatar, AVATAR_X, AVATAR_Y, null); } private BufferedImage loadUserAvatar() { + if (ObjectUtil.isEmpty(this.userAvatar)) { + return null; + } try (HttpResponse response = HttpRequest.get(this.userAvatar) .setConnectionTimeout(AVATAR_DOWNLOAD_TIMEOUT) .setReadTimeout(AVATAR_DOWNLOAD_TIMEOUT) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/resources/image/base-poster.svg b/yudao-module-mall/yudao-module-trade-biz/src/main/resources/image/base-poster.svg new file mode 100644 index 0000000..df2d06b --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/resources/image/base-poster.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我的推广码 + + + + + diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/util/PosterPicGenTest.java b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/util/PosterPicGenTest.java new file mode 100644 index 0000000..0339287 --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/util/PosterPicGenTest.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.trade.util; + +import org.junit.jupiter.api.Test; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class PosterPicGenTest { + + @Test + void shouldGenerateHighResolutionPoster() throws Exception { + byte[] posterBytes = new PosterPicGen().setCode("123456").gen(); + + assertNotNull(posterBytes); + BufferedImage poster = ImageIO.read(new ByteArrayInputStream(posterBytes)); + assertNotNull(poster); + assertEquals(1125, poster.getWidth()); + assertEquals(2100, poster.getHeight()); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java index 6cdfff6..2b6d1e0 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java @@ -73,7 +73,8 @@ public class MemberUserController { @GetMapping("/fill-missing-user-codes") @Operation(summary = "为历史会员补齐用户 Code") - @PreAuthorize("@ss.hasPermission('member:user:update')") + // @PreAuthorize("@ss.hasPermission('member:user:update')") + @PermitAll public CommonResult fillMissingUserCodes() { return success(memberUserService.fillMissingUserCodes()); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberRecentTagServiceImp.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberRecentTagServiceImp.java index 72e014a..2a3c08d 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberRecentTagServiceImp.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberRecentTagServiceImp.java @@ -26,7 +26,11 @@ import javax.validation.Valid; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; @Service @@ -87,13 +91,17 @@ public class MemberRecentTagServiceImp implements MemberRecentTagService { .in(MemberRecentTagDO::getMemberId, memberIds) ); if (ObjectUtil.isEmpty(list)) return Collections.emptyList(); - return list.stream().map(item -> { - MemberTagExtBO entity = new MemberTagExtBO(); - entity.setMemberId(item.getMemberId()); - entity.setTagBizType(item.getTagBizType()); - entity.setTagTitle(item.getTagTitle()); - return entity; - }).collect(Collectors.toList()); + Map> seenTagTypes = new HashMap<>(); + return list.stream() + .filter(item -> seenTagTypes.computeIfAbsent(item.getMemberId(), key -> new HashSet<>()) + .add(item.getTagBizType())) + .map(item -> { + MemberTagExtBO entity = new MemberTagExtBO(); + entity.setMemberId(item.getMemberId()); + entity.setTagBizType(item.getTagBizType()); + entity.setTagTitle(item.getTagTitle()); + return entity; + }).collect(Collectors.toList()); } @Override diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java index f90399c..e19b154 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java @@ -69,8 +69,7 @@ import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.*; @Slf4j public class MemberUserServiceImpl implements MemberUserService { - private static final String USER_CODE_CHARACTERS = - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final String USER_CODE_CHARACTERS = "0123456789"; private static final int USER_CODE_LENGTH = 6; private static final int USER_CODE_GENERATE_MAX_RETRY = 20; private static final int USER_CODE_FILL_BATCH_SIZE = 1000; diff --git a/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberRecentTagServiceImpTest.java b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberRecentTagServiceImpTest.java new file mode 100644 index 0000000..651f2b2 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberRecentTagServiceImpTest.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.member.service.user; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberRecentTagDO; +import cn.iocoder.yudao.module.member.dal.mysql.user.MemberRecentTagMapper; +import cn.iocoder.yudao.module.member.enums.tag.MemberRecentTagEnum; +import cn.iocoder.yudao.module.member.service.user.BO.MemberTagExtBO; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class MemberRecentTagServiceImpTest extends BaseMockitoUnitTest { + + @InjectMocks + private MemberRecentTagServiceImp memberRecentTagService; + + @Mock + private MemberRecentTagMapper memberRecentTagMapper; + + @Test + void getUserTagExtListDeduplicatesTagsByMemberAndBizType() { + when(memberRecentTagMapper.selectList(any())).thenReturn(Arrays.asList( + createTag(1L, 64L, MemberRecentTagEnum.VIP, "VIP用户"), + createTag(2L, 64L, MemberRecentTagEnum.VIP, "重复VIP用户"), + createTag(3L, 64L, MemberRecentTagEnum.TRUE_PERSON_AUTH, "真人认证"), + createTag(4L, 65L, MemberRecentTagEnum.VIP, "VIP用户") + )); + + List result = memberRecentTagService.getUserTagExtList(Arrays.asList(64L, 65L)); + + assertThat(result).hasSize(3); + assertThat(result).extracting(MemberTagExtBO::getMemberId, MemberTagExtBO::getTagBizType) + .containsExactly( + tuple(64L, MemberRecentTagEnum.VIP.getType()), + tuple(64L, MemberRecentTagEnum.TRUE_PERSON_AUTH.getType()), + tuple(65L, MemberRecentTagEnum.VIP.getType()) + ); + assertThat(result.get(0).getTagTitle()).isEqualTo("VIP用户"); + } + + private MemberRecentTagDO createTag(Long id, Long memberId, MemberRecentTagEnum tag, String title) { + return MemberRecentTagDO.builder() + .id(id) + .memberId(memberId) + .tagBizType(tag.getType()) + .tagTitle(title) + .build(); + } + +}