This commit is contained in:
dzc committed 2026-07-31 20:40:05 +08:00
1 parent aa9963bfe1
commit 0f9d061f4e
8 files changed
+346 -38

No files matched your search

@@ -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());
@@ -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)
@@ -0,0 +1,84 @@
<svg width="375" height="700" viewBox="0 0 375 700" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 阴影滤镜 -->
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="4" stdDeviation="10" flood-color="#000000" flood-opacity="0.1"/>
</filter>
<!-- 背景渐变 -->
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#EBF4FF" />
<stop offset="100%" stop-color="#F2F4FF" />
</linearGradient>
<!-- 柔光球 -->
<filter id="blurFilter">
<feGaussianBlur in="SourceGraphic" stdDeviation="40" />
</filter>
<!-- 心形路径 (复用) -->
<path id="heart" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
<!-- 圆形剪切路径 (用于Logo) -->
<clipPath id="logoClip">
<circle cx="187.5" cy="150" r="34" />
</clipPath>
</defs>
<!-- 1. 背景 -->
<rect width="100%" height="100%" fill="url(#bgGradient)" />
<!-- 添加一些柔和的彩色光斑 -->
<circle cx="50" cy="50" r="100" fill="#FFDEE9" filter="url(#blurFilter)" opacity="0.5" />
<circle cx="375" cy="700" r="150" fill="#B5FFFC" filter="url(#blurFilter)" opacity="0.3" />
<!-- 2. 左上角图标 -->
<g transform="translate(20, 40)">
<defs>
<clipPath id="smallLogoClip">
<rect width="48" height="48" rx="14" />
</clipPath>
</defs>
<image href="data:image/png;base64,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 truncated
</g>
<!-- 3. 中间卡片区域 -->
<!-- 卡片参数:
Top Y: 150
Left X: 35
Width: 305
Height: 450 (approx)
Notch Y: 260
-->
<g filter="url(#shadow)">
<!-- 卡片主体路径 -->
<path fill="#FFFFFF" d="
M 65 150
L 147.5 150
A 40 40 0 0 1 227.5 150
L 310 150
A 20 20 0 0 1 330 170
L 330 250
A 10 10 0 0 0 330 270
L 330 600
A 20 20 0 0 1 310 620
L 65 620
A 20 20 0 0 1 45 600
L 45 270
A 10 10 0 0 0 45 250
L 45 170
A 20 20 0 0 1 65 150
Z
" />
<!-- Logo 图片 -->
<image href="data:image/png;base64,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 truncated
</g>
<!-- 4. 卡片上的文字和虚线 -->
<!-- 文字: 我的推广码 -->
<text x="187.5" y="230" font-family="PingFang SC, Microsoft YaHei, sans-serif" font-size="16" fill="#333333" text-anchor="middle" font-weight="bold">我的推广码</text>
<!-- 虚线 -->
<line x1="55" y1="260" x2="320" y2="260" stroke="#EEEEEE" stroke-width="2" stroke-dasharray="6,6" />
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

@@ -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());
}
}
@@ -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<Integer> fillMissingUserCodes() {
return success(memberUserService.fillMissingUserCodes());
}
@@ -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<Long, Set<String>> 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
@@ -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;
@@ -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<MemberTagExtBO> 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();
}
}