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();
+ }
+
+}