Merge remote-tracking branch 'origin/feature_教代会代表证' into release_20260831

This commit is contained in:
2026-08-30 14:40:14 +08:00
9 changed files with 675 additions and 236 deletions
@@ -0,0 +1,311 @@
package com.budwk.app.zhgh.democratic.teachercongress.delegate.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.result.Result;
import com.budwk.app.sys.models.Sys_config;
import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.teachercongress.delegate.models.Teacher_congress_delegate;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation;
import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_session;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
/**
* 教代会代表证
*/
@IocBean
@At("/platform/teacherCongress/delegate/card")
@Ok("json:full")
public class TeacherCongressDelegateCardController {
private static final String CARD_BACK_IMG_CONFIG = "TeacherCongressCardBackImg";
private static final String APP_LOGO_CONFIG = "AppLogo";
@Inject
private Dao dao;
@Inject
private SysFileService sysFileService;
@Inject
private SysRoleService sysRoleService;
@At("/h5")
@Ok("void")
@SaCheckLogin
public void h5Index(HttpServletResponse response) throws IOException, WriterException {
showCard(response);
}
@At
@SaCheckLogin
public Object isDb() {
Teacher_congress_session session = getCurrentSession();
if (session == null) {
return Result.error("当前没有开启的教代会");
}
Teacher_congress_delegate delegate = getCurrentDelegate(session.getId());
if (delegate == null) {
return Result.error("抱歉," + getSessionTitle(session) + "您不是代表");
}
return Result.success();
}
@At
@Ok("void")
@SaCheckLogin
public void showCard(HttpServletResponse response) throws IOException, WriterException {
NutMap cardInfo = getCardInfo();
BufferedImage backgroundImage = readBackgroundImage();
BufferedImage resultImage = new BufferedImage(backgroundImage.getWidth(), backgroundImage.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = resultImage.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
graphics.drawImage(backgroundImage, 0, 0, null);
BufferedImage logoImage = tintLogo(readConfigImage(APP_LOGO_CONFIG, "系统Logo读取失败"), Color.decode("#1476DF"));
int logoMaxWidth = resultImage.getWidth() * 55 / 100;
int logoMaxHeight = resultImage.getHeight() * 11 / 100;
double logoScale = Math.min((double) logoMaxWidth / logoImage.getWidth(), (double) logoMaxHeight / logoImage.getHeight());
int logoWidth = (int) (logoImage.getWidth() * logoScale);
int logoHeight = (int) (logoImage.getHeight() * logoScale);
int logoX = (resultImage.getWidth() - logoWidth) / 2;
int logoY = resultImage.getHeight() * 15 / 100;
graphics.drawImage(logoImage, logoX, logoY, logoWidth, logoHeight, null);
String title = StrUtil.blankToDefault(cardInfo.getString("title"), "");
int titleY = resultImage.getHeight() * 32 / 100;
Font titleFont = fitFontToWidth(graphics, new Font("微软雅黑", Font.BOLD, 70), title,
resultImage.getWidth() * 82 / 100, 36);
commonSpacedFont(resultImage, graphics, title, titleY, titleFont, Color.decode("#1476DF"), 6);
Map<EncodeHintType, Object> hintMap = new EnumMap<>(EncodeHintType.class);
hintMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hintMap.put(EncodeHintType.MARGIN, 0);
NutMap qrData = new NutMap();
qrData.setv("userId", SecurityUtil.getUserId());
int qrCodeSize = Math.min(330, resultImage.getWidth() * 31 / 100);
BitMatrix bitMatrix = new MultiFormatWriter().encode(Json.toJson(qrData), BarcodeFormat.QR_CODE,
qrCodeSize, qrCodeSize, hintMap);
int qrCodeX = (resultImage.getWidth() - qrCodeSize) / 2;
int qrCodeY = resultImage.getHeight() * 37 / 100;
int qrPanelPadding = 8;
graphics.setColor(Color.WHITE);
graphics.fillRoundRect(qrCodeX - qrPanelPadding, qrCodeY - qrPanelPadding,
qrCodeSize + qrPanelPadding * 2, qrCodeSize + qrPanelPadding * 2, 22, 22);
for (int x = 0; x < qrCodeSize; x++) {
for (int y = 0; y < qrCodeSize; y++) {
if (bitMatrix.get(x, y)) {
resultImage.setRGB(qrCodeX + x, qrCodeY + y, Color.BLACK.getRGB());
}
}
}
int dividerY = qrCodeY + qrCodeSize + 60;
int dividerWidth = resultImage.getWidth() * 38 / 100;
graphics.setColor(Color.decode("#1476DF"));
graphics.fillRect((resultImage.getWidth() - dividerWidth) / 2, dividerY, dividerWidth, 2);
graphics.fillRect(resultImage.getWidth() / 2 - 5, dividerY - 4, 10, 10);
String nameText = StrUtil.blankToDefault(cardInfo.getString("nameText"), SecurityUtil.getUserUsername());
int nameTextY = dividerY + 115;
Font nameFont = fitFontToWidth(graphics, new Font("微软雅黑", Font.BOLD, 68), nameText,
resultImage.getWidth() * 70 / 100, 34);
commonSpacedFont(resultImage, graphics, nameText, nameTextY, nameFont, Color.decode("#1476DF"), 30);
String dbtText = StrUtil.blankToDefault(cardInfo.getString("dbtText"), "");
int dbtTextY = nameTextY + 82;
Font dbtFont = fitFontToWidth(graphics, new Font("微软雅黑", Font.PLAIN, 36), dbtText,
resultImage.getWidth() * 75 / 100, 22);
commonSpacedFont(resultImage, graphics, dbtText, dbtTextY, dbtFont, Color.decode("#333333"), 8);
response.setContentType("image/png");
ImageIO.write(resultImage, "png", response.getOutputStream());
graphics.dispose();
}
private NutMap getCardInfo() throws IOException {
Teacher_congress_session session = getCurrentSession();
if (session == null) {
throw new IOException("当前没有开启的教代会");
}
Teacher_congress_delegate delegate = getCurrentDelegate(session.getId());
if (delegate == null) {
throw new IOException("抱歉," + getSessionTitle(session) + "您不是代表");
}
Teacher_congress_delegation delegation = null;
if (StrUtil.isNotBlank(delegate.getDelegationId())) {
delegation = dao.fetch(Teacher_congress_delegation.class, delegate.getDelegationId());
}
NutMap nutMap = NutMap.NEW();
nutMap.put("title", getSessionTitle(session));
nutMap.put("nameText", StrUtil.blankToDefault(delegate.getUserName(), SecurityUtil.getUserUsername()));
nutMap.put("dbtText", delegation == null ? "" : StrUtil.blankToDefault(delegation.getName(), ""));
return nutMap;
}
private Teacher_congress_session getCurrentSession() {
List<Teacher_congress_session> sessions = dao.query(Teacher_congress_session.class, Cnd.where("enable", "=", true)
.desc("startDate"));
if (Lang.isEmpty(sessions)) {
return null;
}
return sessions.get(0);
}
private Teacher_congress_delegate getCurrentDelegate(String sessionId) {
List<String> roleIds = sysRoleService.getRoleIdsByCode(List.of(
RoleConstant.TEACHER_CONGRESS_DELEGATE_FORMAL,
RoleConstant.TEACHER_CONGRESS_DELEGATE_ATTENDANCE,
RoleConstant.TEACHER_CONGRESS_DELEGATE_SPECIALLY_INVITE
));
return dao.fetch(Teacher_congress_delegate.class, Cnd.where("sessionId", "=", sessionId)
.and("userId", "=", SecurityUtil.getUserId())
.and("roleId", "in", roleIds));
}
private String getSessionTitle(Teacher_congress_session session) {
String fullName = StrUtil.blankToDefault(session.getFullName(), "");
if (fullName.contains("教代会")) {
return fullName;
}
return fullName + "教代会";
}
private BufferedImage readBackgroundImage() throws IOException {
String fileId = getCardBackImgFileId();
if (StrUtil.isBlank(fileId)) {
throw new IOException("请先配置教代会代表证背景图片");
}
byte[] bytes = sysFileService.download(fileId);
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
if (image == null) {
throw new IOException("教代会代表证背景图片读取失败");
}
return image;
}
private BufferedImage readConfigImage(String configKey, String errorMessage) throws IOException {
String fileId = getFileIdFromConfigValue(getConfigValue(configKey));
if (StrUtil.isBlank(fileId)) {
throw new IOException("请先配置" + configKey);
}
BufferedImage image = ImageIO.read(new ByteArrayInputStream(sysFileService.download(fileId)));
if (image == null) {
throw new IOException(errorMessage);
}
return image;
}
private BufferedImage tintLogo(BufferedImage source, Color color) {
BufferedImage tinted = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int x = 0; x < source.getWidth(); x++) {
for (int y = 0; y < source.getHeight(); y++) {
Color sourceColor = new Color(source.getRGB(x, y), true);
int luminance = (sourceColor.getRed() * 299 + sourceColor.getGreen() * 587
+ sourceColor.getBlue() * 114) / 1000;
int maskAlpha = Math.max(0, Math.min(255, (250 - luminance) * 25));
int alpha = sourceColor.getAlpha() * maskAlpha / 255;
if (alpha > 0) {
tinted.setRGB(x, y, new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha).getRGB());
}
}
}
return tinted;
}
private String getCardBackImgFileId() {
String configValue = getConfigValue(CARD_BACK_IMG_CONFIG);
if (StrUtil.isBlank(configValue)) {
configValue = getConfigValue("cardBackImg");
}
if (StrUtil.isBlank(configValue)) {
return "";
}
return getFileIdFromConfigValue(configValue);
}
private String getFileIdFromConfigValue(String configValue) {
String firstValue = StrUtil.subBefore(StrUtil.blankToDefault(configValue, ""), ",", false);
if (firstValue.contains("id=")) {
firstValue = StrUtil.subAfter(firstValue, "id=", true);
firstValue = StrUtil.subBefore(firstValue, "&", false);
}
return URLDecoder.decode(firstValue, StandardCharsets.UTF_8);
}
private String getConfigValue(String configKey) {
Sys_config config = dao.fetch(Sys_config.class, configKey);
return config == null ? "" : StrUtil.blankToDefault(config.getConfigValue(), "");
}
private void commonFont(BufferedImage resultImage, Graphics2D graphics, String text, int textY, Font font, Color color) {
graphics.setFont(font);
graphics.setColor(color);
FontMetrics fontMetrics = graphics.getFontMetrics();
int textWidth = fontMetrics.stringWidth(text);
int textX = (resultImage.getWidth() - textWidth) / 2;
graphics.drawString(text, textX, textY);
}
private void commonSpacedFont(BufferedImage resultImage, Graphics2D graphics, String text, int textY,
Font font, Color color, int letterSpacing) {
graphics.setFont(font);
graphics.setColor(color);
FontMetrics fontMetrics = graphics.getFontMetrics();
int textWidth = 0;
for (int i = 0; i < text.length(); i++) {
textWidth += fontMetrics.charWidth(text.charAt(i));
}
textWidth += Math.max(0, text.length() - 1) * letterSpacing;
int textX = (resultImage.getWidth() - textWidth) / 2;
for (int i = 0; i < text.length(); i++) {
char character = text.charAt(i);
graphics.drawString(String.valueOf(character), textX, textY);
textX += fontMetrics.charWidth(character) + letterSpacing;
}
}
private Font fitFontToWidth(Graphics2D graphics, Font font, String text, int maxWidth, int minSize) {
Font result = font;
while (result.getSize() > minSize) {
graphics.setFont(result);
if (graphics.getFontMetrics().stringWidth(text) <= maxWidth) {
break;
}
result = result.deriveFont((float) (result.getSize() - 2));
}
return result;
}
}