From ce4346c35ad759dd2c8372824e29c5bd2e449156 Mon Sep 17 00:00:00 2001 From: Shiro Date: Tue, 13 Jan 2026 23:29:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E7=AD=BE=E5=88=B0?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E8=A7=84=E5=88=99=E3=80=81=E6=8A=A5=E5=90=8D?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=88=A4=E6=96=AD=E5=92=8C=E8=AF=84=E4=BB=B7?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/config/SecurityConfig.java | 3 + .../campus/activity/mapper/ReviewMapper.java | 5 +- .../service/impl/ActivityServiceImpl.java | 2 +- .../service/impl/CheckInServiceImpl.java | 6 +- .../service/impl/ReviewServiceImpl.java | 14 +- .../com/campus/activity/util/PdfUtil.java | 260 ++++++++++++++---- .../main/resources/mapper/ReviewMapper.xml | 19 +- 7 files changed, 240 insertions(+), 69 deletions(-) diff --git a/server/src/main/java/com/campus/activity/config/SecurityConfig.java b/server/src/main/java/com/campus/activity/config/SecurityConfig.java index 21983e9..7859d31 100644 --- a/server/src/main/java/com/campus/activity/config/SecurityConfig.java +++ b/server/src/main/java/com/campus/activity/config/SecurityConfig.java @@ -40,6 +40,9 @@ public class SecurityConfig { config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.addExposedHeader("*"); + config.addExposedHeader("Content-Disposition"); + config.addExposedHeader("Content-Length"); + config.addExposedHeader("Content-Type"); config.setMaxAge(3600L); return config; })) diff --git a/server/src/main/java/com/campus/activity/mapper/ReviewMapper.java b/server/src/main/java/com/campus/activity/mapper/ReviewMapper.java index 80d7e18..f3d531d 100644 --- a/server/src/main/java/com/campus/activity/mapper/ReviewMapper.java +++ b/server/src/main/java/com/campus/activity/mapper/ReviewMapper.java @@ -4,13 +4,14 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.campus.activity.entity.Review; +import com.campus.activity.vo.ReviewVO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface ReviewMapper extends BaseMapper { - IPage selectActivityReviews(Page page, @Param("activityId") Long activityId); + IPage selectActivityReviews(Page page, @Param("activityId") Long activityId); - IPage selectMyReviews(Page page, @Param("userId") Long userId); + IPage selectMyReviews(Page page, @Param("userId") Long userId); } \ No newline at end of file diff --git a/server/src/main/java/com/campus/activity/service/impl/ActivityServiceImpl.java b/server/src/main/java/com/campus/activity/service/impl/ActivityServiceImpl.java index 326fec7..7c95b42 100644 --- a/server/src/main/java/com/campus/activity/service/impl/ActivityServiceImpl.java +++ b/server/src/main/java/com/campus/activity/service/impl/ActivityServiceImpl.java @@ -64,7 +64,7 @@ public class ActivityServiceImpl implements ActivityService { new LambdaQueryWrapper() .eq(Registration::getUserId, currentUser.getId()) .eq(Registration::getActivityId, id) - .eq(Registration::getStatus, 1) + .in(Registration::getStatus, 1, 2) ); vo.setIsRegistered(registration != null); } catch (Exception e) { diff --git a/server/src/main/java/com/campus/activity/service/impl/CheckInServiceImpl.java b/server/src/main/java/com/campus/activity/service/impl/CheckInServiceImpl.java index 3f4d5a0..db8dd57 100644 --- a/server/src/main/java/com/campus/activity/service/impl/CheckInServiceImpl.java +++ b/server/src/main/java/com/campus/activity/service/impl/CheckInServiceImpl.java @@ -76,7 +76,8 @@ public class CheckInServiceImpl implements CheckInService { } LocalDateTime now = LocalDateTime.now(); - if (now.isBefore(activity.getStartTime()) || now.isAfter(activity.getEndTime())) { + LocalDateTime earlyCheckInTime = activity.getStartTime().minusHours(1); + if (now.isBefore(earlyCheckInTime) || now.isAfter(activity.getEndTime())) { throw new BusinessException(ResultCode.CHECKIN_TIME_EXPIRED); } @@ -121,7 +122,8 @@ public class CheckInServiceImpl implements CheckInService { } LocalDateTime now = LocalDateTime.now(); - if (now.isBefore(activity.getStartTime()) || now.isAfter(activity.getEndTime())) { + LocalDateTime earlyCheckInTime = activity.getStartTime().minusHours(1); + if (now.isBefore(earlyCheckInTime) || now.isAfter(activity.getEndTime())) { throw new BusinessException(ResultCode.CHECKIN_TIME_EXPIRED); } diff --git a/server/src/main/java/com/campus/activity/service/impl/ReviewServiceImpl.java b/server/src/main/java/com/campus/activity/service/impl/ReviewServiceImpl.java index c7e157a..4ed8333 100644 --- a/server/src/main/java/com/campus/activity/service/impl/ReviewServiceImpl.java +++ b/server/src/main/java/com/campus/activity/service/impl/ReviewServiceImpl.java @@ -73,23 +73,13 @@ public class ReviewServiceImpl implements ReviewService { @Override public IPage getActivityReviews(Page page, Long activityId) { - return reviewMapper.selectActivityReviews(page, activityId) - .convert(review -> { - ReviewVO vo = new ReviewVO(); - BeanUtils.copyProperties(review, vo); - return vo; - }); + return reviewMapper.selectActivityReviews(page, activityId); } @Override public IPage getMyReviews(Page page) { User currentUser = authService.getCurrentUser(); - return reviewMapper.selectMyReviews(page, currentUser.getId()) - .convert(review -> { - ReviewVO vo = new ReviewVO(); - BeanUtils.copyProperties(review, vo); - return vo; - }); + return reviewMapper.selectMyReviews(page, currentUser.getId()); } private ReviewVO convertToVO(Review review, User user, Activity activity) { diff --git a/server/src/main/java/com/campus/activity/util/PdfUtil.java b/server/src/main/java/com/campus/activity/util/PdfUtil.java index 31d8073..ab48f63 100644 --- a/server/src/main/java/com/campus/activity/util/PdfUtil.java +++ b/server/src/main/java/com/campus/activity/util/PdfUtil.java @@ -8,72 +8,46 @@ import com.google.zxing.EncodeHintType; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; +import com.itextpdf.io.font.PdfEncodings; import com.itextpdf.io.image.ImageDataFactory; import com.itextpdf.kernel.colors.ColorConstants; +import com.itextpdf.kernel.font.PdfFont; +import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfWriter; +import com.itextpdf.kernel.pdf.canvas.draw.SolidLine; import com.itextpdf.layout.Document; -import com.itextpdf.layout.element.Image; -import com.itextpdf.layout.element.Paragraph; -import com.itextpdf.layout.element.Table; +import com.itextpdf.layout.element.*; +import com.itextpdf.layout.properties.HorizontalAlignment; import com.itextpdf.layout.properties.TextAlignment; import com.itextpdf.layout.properties.UnitValue; +import com.itextpdf.layout.properties.VerticalAlignment; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; public class PdfUtil { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm"); + public static byte[] generateTicketPdf(User user, Activity activity, Registration registration) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { PdfWriter writer = new PdfWriter(outputStream); PdfDocument pdf = new PdfDocument(writer); Document document = new Document(pdf); - document.add(new Paragraph("活动电子票") - .setFontSize(24) - .setBold() - .setTextAlignment(TextAlignment.CENTER) - .setMarginBottom(20)); + PdfFont chineseFont = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED); + PdfFont boldChineseFont = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED); - Table table = new Table(UnitValue.createPercentArray(new float[]{1, 2})) - .setMarginBottom(20); - - table.addCell(createCell("活动名称:")); - table.addCell(createCell(activity.getTitle())); - - table.addCell(createCell("学生姓名:")); - table.addCell(createCell(user.getName())); - - table.addCell(createCell("学号:")); - table.addCell(createCell(user.getStudentId())); - - table.addCell(createCell("活动时间:")); - table.addCell(createCell(activity.getStartTime() + " ~ " + activity.getEndTime())); - - table.addCell(createCell("活动地点:")); - table.addCell(createCell(activity.getLocation())); - - table.addCell(createCell("电子票号:")); - table.addCell(createCell(registration.getTicketCode())); - - document.add(table); - - byte[] qrCodeBytes = generateQrCode(registration.getTicketCode()); - Image qrCodeImage = new Image(ImageDataFactory.create(qrCodeBytes)) - .setWidth(150) - .setHeight(150) - .setMarginTop(20) - .setTextAlignment(TextAlignment.CENTER); - - document.add(qrCodeImage); - - document.add(new Paragraph("请妥善保管此电子票,活动当天凭此票签到") - .setFontSize(10) - .setTextAlignment(TextAlignment.CENTER) - .setMarginTop(20)); + createHeader(document, chineseFont, boldChineseFont); + createDivider(document); + createActivityInfo(document, activity, chineseFont, boldChineseFont); + createParticipantAndQrCode(document, user, registration, chineseFont, boldChineseFont); + createFooter(document, chineseFont); document.close(); @@ -83,10 +57,201 @@ public class PdfUtil { } } - private static Paragraph createCell(String text) { - return new Paragraph(text) + private static void createHeader(Document document, PdfFont font, PdfFont boldFont) { + Paragraph title = new Paragraph("校园活动电子票") + .setFont(boldFont) + .setFontSize(20) + .setBold() + .setTextAlignment(TextAlignment.CENTER) + .setMarginTop(15) + .setMarginBottom(5); + + Paragraph subtitle = new Paragraph("Campus Activity E-Ticket") + .setFont(font) + .setFontSize(9) + .setTextAlignment(TextAlignment.CENTER) + .setFontColor(ColorConstants.GRAY) + .setMarginBottom(10); + + document.add(title); + document.add(subtitle); + } + + private static void createDivider(Document document) { + SolidLine line = new SolidLine(1f); + line.setColor(ColorConstants.LIGHT_GRAY); + LineSeparator separator = new LineSeparator(line) + .setMarginBottom(15) + .setWidth(UnitValue.createPercentValue(100)); + document.add(separator); + } + + private static void createActivityInfo(Document document, Activity activity, PdfFont font, PdfFont boldFont) { + Paragraph sectionTitle = new Paragraph("活动信息") + .setFont(boldFont) .setFontSize(12) - .setPadding(5); + .setBold() + .setMarginBottom(8); + + document.add(sectionTitle); + + Table infoTable = new Table(UnitValue.createPercentArray(new float[]{25, 75})) + .setMarginBottom(15) + .setWidth(UnitValue.createPercentValue(90)) + .setHorizontalAlignment(HorizontalAlignment.CENTER); + + addTableRow(infoTable, "活动名称", activity.getTitle(), font, boldFont, true); + addTableRow(infoTable, "活动时间", formatDateTime(activity.getStartTime()) + " ~ " + formatDateTime(activity.getEndTime()), font, boldFont, false); + addTableRow(infoTable, "活动地点", activity.getLocation(), font, boldFont, false); + + document.add(infoTable); + } + + private static void createParticipantAndQrCode(Document document, User user, Registration registration, PdfFont font, PdfFont boldFont) { + Table mainTable = new Table(UnitValue.createPercentArray(new float[]{60, 40})) + .setWidth(UnitValue.createPercentValue(90)) + .setHorizontalAlignment(HorizontalAlignment.CENTER) + .setMarginBottom(15); + + Cell leftCell = new Cell() + .setBorder(com.itextpdf.layout.borders.Border.NO_BORDER) + .setPadding(0); + + Cell rightCell = new Cell() + .setBorder(com.itextpdf.layout.borders.Border.NO_BORDER) + .setPadding(0) + .setVerticalAlignment(VerticalAlignment.MIDDLE); + + Paragraph participantTitle = new Paragraph("参与者信息") + .setFont(boldFont) + .setFontSize(12) + .setBold() + .setMarginBottom(8); + + Table participantTable = new Table(UnitValue.createPercentArray(new float[]{25, 75})) + .setMarginBottom(0); + + addTableRow(participantTable, "学生姓名", user.getName(), font, boldFont, true); + addTableRow(participantTable, "学号", user.getStudentId(), font, boldFont, false); + addTableRow(participantTable, "电子票号", registration.getTicketCode(), font, boldFont, false); + addTableRow(participantTable, "报名时间", formatDateTime(registration.getCreatedAt()), font, boldFont, false); + + leftCell.add(participantTitle); + leftCell.add(participantTable); + + try { + byte[] qrCodeBytes = generateQrCode(registration.getTicketCode()); + Image qrCodeImage = new Image(ImageDataFactory.create(qrCodeBytes)) + .setWidth(120) + .setHeight(120) + .setHorizontalAlignment(HorizontalAlignment.CENTER) + .setMarginBottom(5); + + Paragraph qrTitle = new Paragraph("签到二维码") + .setFont(font) + .setFontSize(12) + .setBold() + .setTextAlignment(TextAlignment.CENTER) + .setMarginBottom(8); + + Paragraph ticketCode = new Paragraph("票号:" + registration.getTicketCode()) + .setFont(font) + .setFontSize(9) + .setTextAlignment(TextAlignment.CENTER) + .setFontColor(ColorConstants.GRAY) + .setMarginBottom(0); + + rightCell.add(qrTitle); + rightCell.add(qrCodeImage); + rightCell.add(ticketCode); + } catch (Exception e) { + throw new RuntimeException("生成二维码失败", e); + } + + mainTable.addCell(leftCell); + mainTable.addCell(rightCell); + + document.add(mainTable); + } + + private static void createFooter(Document document, PdfFont font) { + SolidLine line = new SolidLine(1f); + line.setColor(ColorConstants.LIGHT_GRAY); + LineSeparator separator = new LineSeparator(line) + .setMarginTop(15) + .setMarginBottom(10) + .setWidth(UnitValue.createPercentValue(100)); + document.add(separator); + + Paragraph notice = new Paragraph("温馨提示:请妥善保管此电子票,活动当天凭此票签到入场。请提前15分钟到达活动现场,出示电子票二维码签到。") + .setFont(font) + .setFontSize(9) + .setTextAlignment(TextAlignment.CENTER) + .setMarginBottom(10); + + document.add(notice); + + Paragraph footer = new Paragraph("本电子票由校园活动系统自动生成 | 生成时间:" + LocalDateTime.now().format(DATE_TIME_FORMATTER)) + .setFont(font) + .setFontSize(7) + .setTextAlignment(TextAlignment.CENTER) + .setFontColor(ColorConstants.GRAY) + .setMarginBottom(15); + + document.add(footer); + } + + private static void addTableRow(Table table, String label, String value, PdfFont font, PdfFont boldFont, boolean isFirstRow) { + Cell labelCell = new Cell() + .add(new Paragraph(label).setFont(boldFont).setFontSize(10)) + .setPadding(4) + .setBorder(com.itextpdf.layout.borders.Border.NO_BORDER); + + Cell valueCell = new Cell() + .add(new Paragraph(value != null ? value : "").setFont(font).setFontSize(10)) + .setPadding(4) + .setBorder(com.itextpdf.layout.borders.Border.NO_BORDER); + + if (isFirstRow) { + labelCell.setBackgroundColor(new com.itextpdf.kernel.colors.DeviceRgb(240, 240, 240)); + valueCell.setBackgroundColor(new com.itextpdf.kernel.colors.DeviceRgb(240, 240, 240)); + } + + table.addCell(labelCell); + table.addCell(valueCell); + } + + private static String formatDateTime(LocalDateTime dateTime) { + if (dateTime == null) { + return "待定"; + } + return dateTime.format(DATE_TIME_FORMATTER); + } + + private static String formatDescription(String description) { + if (description == null || description.isEmpty()) { + return "暂无描述"; + } + if (description.length() > 100) { + return description.substring(0, 100) + "..."; + } + return description; + } + + private static String formatRegistrationStatus(Integer status) { + if (status == null) { + return "未知"; + } + switch (status) { + case 0: + return "已取消"; + case 1: + return "已报名"; + case 2: + return "已签到"; + default: + return "未知"; + } } private static byte[] generateQrCode(String content) throws IOException { @@ -94,6 +259,7 @@ public class PdfUtil { QRCodeWriter qrCodeWriter = new QRCodeWriter(); Map hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); + hints.put(EncodeHintType.MARGIN, 2); BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, 300, 300, hints); diff --git a/server/src/main/resources/mapper/ReviewMapper.xml b/server/src/main/resources/mapper/ReviewMapper.xml index 471075b..dac2aea 100644 --- a/server/src/main/resources/mapper/ReviewMapper.xml +++ b/server/src/main/resources/mapper/ReviewMapper.xml @@ -2,9 +2,13 @@ - + SELECT r.id, + r.user_id as userId, + r.activity_id as activityId, + r.rating, + r.content, + r.created_at as createdAt, u.name as userName, u.avatar as userAvatar FROM review r @@ -13,8 +17,13 @@ ORDER BY r.created_at DESC - + SELECT r.id, + r.user_id as userId, + r.activity_id as activityId, + r.rating, + r.content, + r.created_at as createdAt, a.title as activityTitle FROM review r LEFT JOIN activity a ON r.activity_id = a.id