fix: 优化签到时间规则、报名状态判断和评价查询逻辑

This commit is contained in:
2026-01-13 23:29:31 +08:00
parent ae2c359101
commit ce4346c35a
7 changed files with 240 additions and 69 deletions

View File

@@ -40,6 +40,9 @@ public class SecurityConfig {
config.addAllowedMethod("*"); config.addAllowedMethod("*");
config.addAllowedHeader("*"); config.addAllowedHeader("*");
config.addExposedHeader("*"); config.addExposedHeader("*");
config.addExposedHeader("Content-Disposition");
config.addExposedHeader("Content-Length");
config.addExposedHeader("Content-Type");
config.setMaxAge(3600L); config.setMaxAge(3600L);
return config; return config;
})) }))

View File

@@ -4,13 +4,14 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.entity.Review; import com.campus.activity.entity.Review;
import com.campus.activity.vo.ReviewVO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@Mapper @Mapper
public interface ReviewMapper extends BaseMapper<Review> { public interface ReviewMapper extends BaseMapper<Review> {
IPage<Review> selectActivityReviews(Page<Review> page, @Param("activityId") Long activityId); IPage<ReviewVO> selectActivityReviews(Page<Review> page, @Param("activityId") Long activityId);
IPage<Review> selectMyReviews(Page<Review> page, @Param("userId") Long userId); IPage<ReviewVO> selectMyReviews(Page<Review> page, @Param("userId") Long userId);
} }

View File

@@ -64,7 +64,7 @@ public class ActivityServiceImpl implements ActivityService {
new LambdaQueryWrapper<Registration>() new LambdaQueryWrapper<Registration>()
.eq(Registration::getUserId, currentUser.getId()) .eq(Registration::getUserId, currentUser.getId())
.eq(Registration::getActivityId, id) .eq(Registration::getActivityId, id)
.eq(Registration::getStatus, 1) .in(Registration::getStatus, 1, 2)
); );
vo.setIsRegistered(registration != null); vo.setIsRegistered(registration != null);
} catch (Exception e) { } catch (Exception e) {

View File

@@ -76,7 +76,8 @@ public class CheckInServiceImpl implements CheckInService {
} }
LocalDateTime now = LocalDateTime.now(); 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); throw new BusinessException(ResultCode.CHECKIN_TIME_EXPIRED);
} }
@@ -121,7 +122,8 @@ public class CheckInServiceImpl implements CheckInService {
} }
LocalDateTime now = LocalDateTime.now(); 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); throw new BusinessException(ResultCode.CHECKIN_TIME_EXPIRED);
} }

View File

@@ -73,23 +73,13 @@ public class ReviewServiceImpl implements ReviewService {
@Override @Override
public IPage<ReviewVO> getActivityReviews(Page<Review> page, Long activityId) { public IPage<ReviewVO> getActivityReviews(Page<Review> page, Long activityId) {
return reviewMapper.selectActivityReviews(page, activityId) return reviewMapper.selectActivityReviews(page, activityId);
.convert(review -> {
ReviewVO vo = new ReviewVO();
BeanUtils.copyProperties(review, vo);
return vo;
});
} }
@Override @Override
public IPage<ReviewVO> getMyReviews(Page<Review> page) { public IPage<ReviewVO> getMyReviews(Page<Review> page) {
User currentUser = authService.getCurrentUser(); User currentUser = authService.getCurrentUser();
return reviewMapper.selectMyReviews(page, currentUser.getId()) return reviewMapper.selectMyReviews(page, currentUser.getId());
.convert(review -> {
ReviewVO vo = new ReviewVO();
BeanUtils.copyProperties(review, vo);
return vo;
});
} }
private ReviewVO convertToVO(Review review, User user, Activity activity) { private ReviewVO convertToVO(Review review, User user, Activity activity) {

View File

@@ -8,72 +8,46 @@ import com.google.zxing.EncodeHintType;
import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix; import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.QRCodeWriter;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.image.ImageDataFactory; import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.kernel.colors.ColorConstants; 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.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.draw.SolidLine;
import com.itextpdf.layout.Document; import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image; import com.itextpdf.layout.element.*;
import com.itextpdf.layout.element.Paragraph; import com.itextpdf.layout.properties.HorizontalAlignment;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.properties.TextAlignment; import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.layout.properties.UnitValue; import com.itextpdf.layout.properties.UnitValue;
import com.itextpdf.layout.properties.VerticalAlignment;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class PdfUtil { 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) { public static byte[] generateTicketPdf(User user, Activity activity, Registration registration) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
PdfWriter writer = new PdfWriter(outputStream); PdfWriter writer = new PdfWriter(outputStream);
PdfDocument pdf = new PdfDocument(writer); PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf); Document document = new Document(pdf);
document.add(new Paragraph("活动电子票") PdfFont chineseFont = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
.setFontSize(24) PdfFont boldChineseFont = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
.setBold()
.setTextAlignment(TextAlignment.CENTER)
.setMarginBottom(20));
Table table = new Table(UnitValue.createPercentArray(new float[]{1, 2})) createHeader(document, chineseFont, boldChineseFont);
.setMarginBottom(20); createDivider(document);
createActivityInfo(document, activity, chineseFont, boldChineseFont);
table.addCell(createCell("活动名称:")); createParticipantAndQrCode(document, user, registration, chineseFont, boldChineseFont);
table.addCell(createCell(activity.getTitle())); createFooter(document, chineseFont);
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));
document.close(); document.close();
@@ -83,10 +57,201 @@ public class PdfUtil {
} }
} }
private static Paragraph createCell(String text) { private static void createHeader(Document document, PdfFont font, PdfFont boldFont) {
return new Paragraph(text) 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) .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 { private static byte[] generateQrCode(String content) throws IOException {
@@ -94,6 +259,7 @@ public class PdfUtil {
QRCodeWriter qrCodeWriter = new QRCodeWriter(); QRCodeWriter qrCodeWriter = new QRCodeWriter();
Map<EncodeHintType, Object> hints = new HashMap<>(); Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.MARGIN, 2);
BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, 300, 300, hints); BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, 300, 300, hints);

View File

@@ -2,9 +2,13 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.campus.activity.mapper.ReviewMapper"> <mapper namespace="com.campus.activity.mapper.ReviewMapper">
<select id="selectActivityReviews" resultType="com.campus.activity.entity.Review"> <select id="selectActivityReviews" resultType="com.campus.activity.vo.ReviewVO">
SELECT r.*, SELECT r.id,
u.username, r.user_id as userId,
r.activity_id as activityId,
r.rating,
r.content,
r.created_at as createdAt,
u.name as userName, u.name as userName,
u.avatar as userAvatar u.avatar as userAvatar
FROM review r FROM review r
@@ -13,8 +17,13 @@
ORDER BY r.created_at DESC ORDER BY r.created_at DESC
</select> </select>
<select id="selectMyReviews" resultType="com.campus.activity.entity.Review"> <select id="selectMyReviews" resultType="com.campus.activity.vo.ReviewVO">
SELECT r.*, 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 a.title as activityTitle
FROM review r FROM review r
LEFT JOIN activity a ON r.activity_id = a.id LEFT JOIN activity a ON r.activity_id = a.id