first commit

This commit is contained in:
2026-06-08 09:24:41 +08:00
commit 223230e9bc
34 changed files with 1223 additions and 0 deletions

96
pom.xml Normal file
View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/>
</parent>
<groupId>com.discipline</groupId>
<artifactId>report</artifactId>
<version>1.0.0</version>
<name>discipline-report</name>
<dependencies>
<!-- Spring Boot核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- MyBatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 二维码生成ZXing -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,18 @@
package com.discipline.report;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import com.discipline.report.config.CustomConfigProperties;
import com.discipline.report.config.FileUploadProperties;
@SpringBootApplication
@MapperScan("com.discipline.report.mapper")
@EnableConfigurationProperties({CustomConfigProperties.class, FileUploadProperties.class})
public class DisciplineReportApplication {
public static void main(String[] args) {
SpringApplication.run(DisciplineReportApplication.class, args);
}
}

View File

@@ -0,0 +1,22 @@
package com.discipline.report.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 自定义配置属性
*/
@Data
@ConfigurationProperties(prefix = "custom")
public class CustomConfigProperties {
private String disciplineEmail; // 纪委接收邮箱
private String h5IndexUrl; // H5首页URL
private String tipContent; // 填报提示文案
private String qrCodeFixedUrl; // 二维码固定URL
private String emailFrom; // 发件人邮箱
private String emailSubject;
private String emailContent;
}

View File

@@ -0,0 +1,29 @@
package com.discipline.report.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* 文件上传配置属性
*/
@Slf4j
@Data
@ConfigurationProperties(prefix = "discipline.file.upload")
public class FileUploadProperties {
private String path; // 文件上传路径
@PostConstruct
public void init() {
try {
Files.createDirectories(Paths.get(path));
} catch (Exception e) {
log.error("文件目录创建失败:{}", path, e);
}
}
}

View File

@@ -0,0 +1,21 @@
package com.discipline.report.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC配置
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

View File

@@ -0,0 +1,35 @@
package com.discipline.report.controller;
import com.discipline.report.config.CustomConfigProperties;
import com.discipline.report.result.Result;
import com.discipline.report.result.ResultUtil;
import com.discipline.report.util.QrCodeUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 二维码生成控制器
*/
@RestController
@RequestMapping("/api/discipline/qrcode")
public class QrCodeController {
@Resource
private CustomConfigProperties config;
@GetMapping
public Result<?> getQrCode() {
try {
String fixedUrl = config.getQrCodeFixedUrl();
if (fixedUrl == null || fixedUrl.trim().isEmpty()) {
return ResultUtil.error(500, "配置文件未配置二维码固定链接");
}
String qrCode = QrCodeUtil.createQrCode(fixedUrl);
return ResultUtil.success(qrCode);
} catch (Exception e) {
return ResultUtil.error(500, "生成二维码失败:" + e.getMessage());
}
}
}

View File

@@ -0,0 +1,33 @@
package com.discipline.report.controller;
import com.discipline.report.dto.ReportSubmitDTO;
import com.discipline.report.result.Result;
import com.discipline.report.result.ResultUtil;
import com.discipline.report.service.ReportService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
/**
* 举报提交控制器
*/
@RestController
@RequestMapping("/api/discipline/report")
public class ReportController {
@Resource
private ReportService reportService;
/**
* 提交举报表单+附件
*/
@PostMapping("/submit")
public Result submit(@Valid ReportSubmitDTO dto) {
try {
reportService.saveReportAndSendEmail(dto);
return ResultUtil.success("举报提交成功");
} catch (Exception e) {
// 移除e.printStackTrace(),全局异常统一打印日志
return ResultUtil.error(500, "系统异常,提交失败");
}
}
}

View File

@@ -0,0 +1,29 @@
package com.discipline.report.dto;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 举报提交数据传输对象
*/
@Data
public class ReportSubmitDTO {
@NotNull(message = "是否匿名不能为空")
private Boolean isAnonymous; // 是否匿名
private String reporterName; // 举报人姓名
@NotBlank(message = "举报内容不能为空")
private String reportContent; // 举报内容
private String contactInfo; // 联系方式
private MultipartFile[] file; // 上传文件数组
public MultipartFile[] getFile() {
return file;
}
public void setFile(MultipartFile[] file) {
this.file = file;
}
}

View File

@@ -0,0 +1,50 @@
package com.discipline.report.entity;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 举报邮件接收人配置实体
*/
@Data
public class ReportEmailReceiver {
/**
* 主键ID
*/
private Long id;
/**
* 接收人姓名
*/
private String receiverName;
/**
* 接收人邮箱地址
*/
private String receiverEmail;
/**
* 接收类型1-主送(TO) 2-抄送(CC)
*/
private Integer receiveType;
/**
* 排序号
*/
private Integer sortNum;
/**
* 状态0-禁用 1-启用
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,18 @@
package com.discipline.report.entity;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 举报文件实体
*/
@Data
public class ReportFile {
private Long id; // 主键ID
private Long reportId; // 举报记录ID
private String fileName; // 文件名
private String filePath; // 文件路径
private Long fileSize; // 文件大小
private LocalDateTime createTime;// 创建时间
}

View File

@@ -0,0 +1,19 @@
package com.discipline.report.entity;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 举报记录实体
*/
@Data
public class ReportRecord {
private Long id; // 主键ID
private Boolean isAnonymous; // 是否匿名
private String reporterName; // 举报人姓名
private String reportContent; // 举报内容
private String contactInfo; // 联系方式
private String emailStatus; // 邮件发送状态
private LocalDateTime createTime;// 创建时间
}

View File

@@ -0,0 +1,16 @@
package com.discipline.report.exception;
import lombok.Getter;
/**
* 业务异常类
*/
@Getter
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
}

View File

@@ -0,0 +1,38 @@
package com.discipline.report.exception;
import com.discipline.report.result.Result;
import com.discipline.report.result.ResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result<?> businessException(BusinessException e) {
log.error("业务异常:{}", e.getMessage());
return ResultUtil.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public Result<?> validException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldError().getDefaultMessage();
log.error("参数校验异常:{}", msg);
return ResultUtil.error(400, msg);
}
@ExceptionHandler(Exception.class)
@ResponseBody
public Result<?> exception(Exception e) {
log.error("系统异常", e);
return ResultUtil.error(500, "系统异常,请联系管理员");
}
}

View File

@@ -0,0 +1,23 @@
package com.discipline.report.mapper;
import com.discipline.report.entity.ReportEmailReceiver;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 举报邮件接收人Mapper
*/
public interface ReportEmailReceiverMapper {
/**
* 根据接收类型查询启用的接收人列表
* @param receiveType 接收类型 1-主送 2-抄送
* @return 接收人列表
*/
List<ReportEmailReceiver> selectByReceiveType(@Param("receiveType") Integer receiveType);
/**
* 查询所有启用的接收人
* @return 接收人列表
*/
List<ReportEmailReceiver> selectAllEnabled();
}

View File

@@ -0,0 +1,16 @@
package com.discipline.report.mapper;
import com.discipline.report.entity.ReportFile;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 举报文件数据访问接口
*/
@Mapper
public interface ReportFileMapper {
void insertBatch(@Param("files") List<ReportFile> files);
List<ReportFile> selectByReportId(@Param("reportId") Long reportId);
}

View File

@@ -0,0 +1,14 @@
package com.discipline.report.mapper;
import com.discipline.report.entity.ReportRecord;
import org.apache.ibatis.annotations.Mapper;
/**
* 举报记录数据访问接口
*/
@Mapper
public interface ReportRecordMapper {
void insert(ReportRecord record);
// 新增:根据主键更新邮件状态
int updateById(ReportRecord record);
}

View File

@@ -0,0 +1,13 @@
package com.discipline.report.result;
import lombok.Data;
/**
* 统一返回结果类
*/
@Data
public class Result<T> {
private int code; // 响应码
private String msg; // 响应消息
private T data; // 响应数据
}

View File

@@ -0,0 +1,37 @@
package com.discipline.report.result;
/**
* 统一返回结果工具类
*/
public class ResultUtil {
/**
* 成功返回(带数据)
*/
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("成功");
result.setData(data);
return result;
}
/**
* 成功返回(无数据)
*/
public static <T> Result<T> success() {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("成功");
return result;
}
/**
* 失败返回
*/
public static <T> Result<T> error(int code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
return result;
}
}

View File

@@ -0,0 +1,17 @@
package com.discipline.report.service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/**
* 文件上传服务接口
*/
public interface FileUploadService {
/**
* 上传单个文件,返回本地全路径
* @param file 前端上传文件
* @return 本地磁盘全路径
* @throws IOException IO异常
*/
String uploadFile(MultipartFile file) throws IOException;
}

View File

@@ -0,0 +1,27 @@
package com.discipline.report.service;
import com.discipline.report.entity.ReportEmailReceiver;
import java.util.List;
/**
* 举报邮件接收人服务接口
*/
public interface ReportEmailReceiverService {
/**
* 获取主送接收人邮箱列表
* @return 邮箱数组
*/
String[] getToEmailArray();
/**
* 获取抄送接收人邮箱列表
* @return 邮箱数组
*/
String[] getCcEmailArray();
/**
* 查询所有启用的接收人
* @return 接收人列表
*/
List<ReportEmailReceiver> getAllEnabledReceivers();
}

View File

@@ -0,0 +1,11 @@
package com.discipline.report.service;
import com.discipline.report.dto.ReportSubmitDTO;
import java.util.List;
/**
* 举报服务接口
*/
public interface ReportService {
void saveReportAndSendEmail(ReportSubmitDTO dto) throws Exception;
}

View File

@@ -0,0 +1,73 @@
package com.discipline.report.service.impl;
import com.discipline.report.config.FileUploadProperties;
import com.discipline.report.exception.BusinessException;
import com.discipline.report.service.FileUploadService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
/**
* 文件上传服务实现
*/
@Slf4j
@Service
public class FileUploadServiceImpl implements FileUploadService {
// 修正注释原注释写的10MB但值是50MB保持值不变仅修正注释可选
private static final long MAX_SIZE = 50*1024*1024; // 50MB
@Resource
private FileUploadProperties uploadProperties;
@Override
public String uploadFile(MultipartFile file) throws IOException {
// 1. 校验文件是否为空
if (file.isEmpty()) {
throw new BusinessException(400, "上传文件不能为空");
}
// 2. 校验文件大小
if (file.getSize() > MAX_SIZE) {
throw new BusinessException(400, "文件大小不能超过50MB");
}
// 3. 校验文件名是否为空(仅校验名称,不再校验后缀)
String originalName = file.getOriginalFilename();
if (originalName == null || originalName.trim().isEmpty()) {
throw new BusinessException(400, "上传文件名称不能为空");
}
// 4. 生成新文件名(兼容无后缀文件)
String newFileName;
int dotIndex = originalName.lastIndexOf(".");
if (dotIndex == -1) {
// 无后缀文件直接用UUID作为文件名
newFileName = UUID.randomUUID().toString();
} else {
// 有后缀文件UUID + 原后缀
String suffix = originalName.substring(dotIndex).toLowerCase();
newFileName = UUID.randomUUID().toString() + suffix;
}
// 5. 保存文件
String uploadPath = uploadProperties.getPath();
File dest = new File(uploadPath + File.separator + newFileName);
// 确保上传目录存在(新增:防止目录不存在导致报错)
if (!dest.getParentFile().exists()) {
boolean mkdirs = dest.getParentFile().mkdirs();
if (!mkdirs) {
log.error("创建上传目录失败:{}", dest.getParentFile().getAbsolutePath());
throw new BusinessException(500, "文件上传失败,创建目录失败");
}
}
file.transferTo(dest);
log.info("文件上传成功:{}", dest.getAbsolutePath());
return dest.getAbsolutePath();
}
}

View File

@@ -0,0 +1,42 @@
package com.discipline.report.service.impl;
import com.discipline.report.entity.ReportEmailReceiver;
import com.discipline.report.mapper.ReportEmailReceiverMapper;
import com.discipline.report.service.ReportEmailReceiverService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
/**
* 举报邮件接收人服务实现
*/
@Slf4j
@Service
public class ReportEmailReceiverServiceImpl implements ReportEmailReceiverService {
@Resource
private ReportEmailReceiverMapper receiverMapper;
@Override
public String[] getToEmailArray() {
List<ReportEmailReceiver> toList = receiverMapper.selectByReceiveType(1);
return toList.stream()
.map(ReportEmailReceiver::getReceiverEmail)
.toArray(String[]::new);
}
@Override
public String[] getCcEmailArray() {
List<ReportEmailReceiver> ccList = receiverMapper.selectByReceiveType(2);
return ccList.stream()
.map(ReportEmailReceiver::getReceiverEmail)
.toArray(String[]::new);
}
@Override
public List<ReportEmailReceiver> getAllEnabledReceivers() {
return receiverMapper.selectAllEnabled();
}
}

View File

@@ -0,0 +1,209 @@
package com.discipline.report.service.impl;
import com.discipline.report.config.CustomConfigProperties;
import com.discipline.report.dto.ReportSubmitDTO;
import com.discipline.report.entity.ReportFile;
import com.discipline.report.entity.ReportRecord;
import com.discipline.report.mapper.ReportFileMapper;
import com.discipline.report.mapper.ReportRecordMapper;
import com.discipline.report.service.FileUploadService;
import com.discipline.report.service.ReportEmailReceiverService;
import com.discipline.report.service.ReportService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 举报服务实现类
* 负责举报记录保存、文件上传、举报通知邮件发送
*/
@Slf4j
@Service
public class ReportServiceImpl implements ReportService {
/**
* 邮件发送状态枚举
*/
private enum EmailStatus {
PENDING("PENDING"), // 待发送
SUCCESS("SUCCESS"), // 发送成功
FAIL("FAIL"); // 发送失败
private final String value;
EmailStatus(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
@Resource
private ReportRecordMapper reportRecordMapper;
@Resource
private ReportFileMapper reportFileMapper;
@Resource
private FileUploadService fileUploadService;
@Resource
private JavaMailSender mailSender;
@Resource
private ReportEmailReceiverService receiverService;
@Resource
private CustomConfigProperties config;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveReportAndSendEmail(ReportSubmitDTO dto) throws Exception {
// 1. 保存举报主记录
ReportRecord reportRecord = saveReportRecord(dto);
// 2. 处理举报附件并保存
handleReportFiles(reportRecord.getId(), dto.getFile());
// 3. 发送举报通知邮件
sendReportNotificationEmail(reportRecord);
}
/**
* 保存举报主记录
*/
private ReportRecord saveReportRecord(ReportSubmitDTO dto) {
ReportRecord record = new ReportRecord();
record.setIsAnonymous(dto.getIsAnonymous());
// 优化:匿名自动清空姓名和联系方式
if(Boolean.TRUE.equals(dto.getIsAnonymous())){
record.setReporterName(null);
record.setContactInfo(null);
}else{
record.setReporterName(dto.getReporterName());
record.setContactInfo(dto.getContactInfo());
}
record.setReportContent(dto.getReportContent());
record.setEmailStatus(EmailStatus.PENDING.getValue());
record.setCreateTime(LocalDateTime.now());
reportRecordMapper.insert(record);
log.info("举报记录保存成功举报ID{}", record.getId());
return record;
}
/**
* 处理举报附件上传并保存
*/
private void handleReportFiles(Long reportId, MultipartFile[] files) {
if (files == null || files.length == 0) {
log.info("举报ID{} 无附件需要上传", reportId);
return;
}
List<ReportFile> reportFiles = new ArrayList<>();
for (MultipartFile file : files) {
if (file.isEmpty()) {
log.warn("举报ID{} 存在空文件,跳过处理", reportId);
continue;
}
try {
String filePath = fileUploadService.uploadFile(file);
ReportFile reportFile = new ReportFile();
reportFile.setReportId(reportId);
reportFile.setFileName(file.getOriginalFilename());
reportFile.setFilePath(filePath);
reportFile.setFileSize(file.getSize());
reportFile.setCreateTime(LocalDateTime.now());
reportFiles.add(reportFile);
} catch (Exception e) {
log.error("举报ID{} 附件上传失败,文件名:{}", reportId, file.getOriginalFilename(), e);
}
}
if (!reportFiles.isEmpty()) {
reportFileMapper.insertBatch(reportFiles);
log.info("举报ID{} 成功保存{}个附件记录", reportId, reportFiles.size());
}
}
/**
* 发送举报通知邮件(优化:添加附件 + 拼接举报详情正文)
*/
private void sendReportNotificationEmail(ReportRecord reportRecord) {
Long reportId = reportRecord.getId();
try {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
// 设置邮件基础信息
helper.setFrom(config.getEmailFrom());
String subject = config.getEmailSubject() != null ? config.getEmailSubject() : "新的举报通知";
helper.setSubject(subject);
// ========== 核心:拼接举报详情到邮件正文 ==========
String baseContent = config.getEmailContent() != null ? config.getEmailContent() : "<p>收到一条新的举报信息,请及时处理!</p>";
StringBuilder sb = new StringBuilder(baseContent);
sb.append("<hr>");
sb.append("<p><strong>是否匿名:</strong>").append(reportRecord.getIsAnonymous() ? "" : "").append("</p>");
if (!reportRecord.getIsAnonymous()) {
sb.append("<p><strong>举报人:</strong>").append(reportRecord.getReporterName()).append("</p>");
sb.append("<p><strong>联系方式:</strong>").append(reportRecord.getContactInfo()).append("</p>");
}
sb.append("<p><strong>举报内容:</strong>").append(reportRecord.getReportContent()).append("</p>");
helper.setText(sb.toString(), true);
// ================================================
// 获取主送、抄送邮箱
String[] toEmails = receiverService.getToEmailArray();
String[] ccEmails = receiverService.getCcEmailArray();
// 主送为空直接失败
if (toEmails == null || toEmails.length == 0) {
log.warn("举报ID{} 邮件主送人为空,无法发送邮件", reportId);
updateEmailStatus(reportRecord, EmailStatus.FAIL);
return;
}
helper.setTo(toEmails);
if (ccEmails != null && ccEmails.length > 0) {
helper.setCc(ccEmails);
}
// 查询附件并添加到邮件
List<ReportFile> fileList = reportFileMapper.selectByReportId(reportId);
for(ReportFile rf : fileList){
File attachFile = new File(rf.getFilePath());
if(attachFile.exists()){
helper.addAttachment(rf.getFileName(), new FileSystemResource(attachFile));
}else{
log.warn("附件文件不存在,路径:{}",rf.getFilePath());
}
}
// 发送邮件
mailSender.send(mimeMessage);
updateEmailStatus(reportRecord, EmailStatus.SUCCESS);
log.info("举报邮件发送成功举报ID{},主送:{},抄送:{}",
reportId,
arrayToString(toEmails),
arrayToString(ccEmails));
} catch (Exception e) {
log.error("举报邮件发送失败举报ID{}", reportId, e);
updateEmailStatus(reportRecord, EmailStatus.FAIL);
}
}
/**
* 更新邮件状态
*/
private void updateEmailStatus(ReportRecord reportRecord, EmailStatus status) {
reportRecord.setEmailStatus(status.getValue());
reportRecordMapper.updateById(reportRecord);
}
/**
* 数组转字符串(空安全)
*/
private String arrayToString(String[] array) {
if (array == null || array.length == 0) {
return "";
}
return String.join(",", array);
}
}

View File

@@ -0,0 +1,62 @@
package com.discipline.report.util;
import com.discipline.report.config.CustomConfigProperties;
import com.discipline.report.entity.ReportFile;
import com.discipline.report.entity.ReportRecord;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.util.List;
/**
* 邮件发送工具类
*/
@Slf4j
@Component
public class EmailSendUtil {
@Resource
private JavaMailSender mailSender;
@Resource
private CustomConfigProperties config;
public void sendReportEmail(ReportRecord record, List<ReportFile> fileList) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(config.getEmailFrom());
helper.setTo(config.getDisciplineEmail());
helper.setSubject("【纪律举报】新举报信息提交");
// 构建邮件内容
StringBuilder content = new StringBuilder();
content.append("<h3>新举报信息</h3>");
content.append("<p><strong>举报时间:</strong>").append(record.getCreateTime()).append("</p>");
content.append("<p><strong>是否匿名:</strong>").append(record.getIsAnonymous() ? "" : "").append("</p>");
if (!record.getIsAnonymous() && record.getReporterName() != null) {
content.append("<p><strong>举报人:</strong>").append(record.getReporterName()).append("</p>");
}
if (record.getContactInfo() != null) {
content.append("<p><strong>联系方式:</strong>").append(record.getContactInfo()).append("</p>");
}
content.append("<p><strong>举报内容:</strong><br/>").append(record.getReportContent().replace("\n", "<br/>")).append("</p>");
// 添加附件
if (fileList != null && !fileList.isEmpty()) {
content.append("<p><strong>附件列表:</strong></p>");
for (ReportFile file : fileList) {
content.append("<p>").append(file.getFileName()).append(" (").append(file.getFileSize()/1024).append("KB)").append("</p>");
helper.addAttachment(file.getFileName(), new File(file.getFilePath()));
}
}
helper.setText(content.toString(), true);
mailSender.send(message);
log.info("举报邮件发送成功,收件人:{}", config.getDisciplineEmail());
}
}

View File

@@ -0,0 +1,67 @@
package com.discipline.report.util;
import com.google.zxing.*;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.springframework.util.Base64Utils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* 二维码生成工具类基于ZXing实现
* 用于生成固定尺寸、固定纠错级别的Base64格式二维码
*/
public class QrCodeUtil {
// 固定配置:确保每次生成的二维码完全一致
private static final int QR_CODE_SIZE = 300; // 二维码尺寸300x300像素
private static final ErrorCorrectionLevel ERROR_CORRECTION = ErrorCorrectionLevel.H; // 高纠错级别30%容错)
private static final String IMAGE_FORMAT = "png"; // 图片格式
private static final String CHARSET = "UTF-8"; // 字符编码
private static final int MARGIN = 1; // 二维码边距(最小)
/**
* 生成Base64格式的二维码图片
* @param content 二维码内容前端提供的固定URL
* @return Base64编码的图片字符串带data:image/png;base64前缀
* @throws Exception 生成过程中的异常
*/
public static String createQrCode(String content) throws Exception {
// 校验输入内容
if (content == null || content.trim().isEmpty()) {
throw new IllegalArgumentException("二维码链接不能为空");
}
QRCodeWriter writer = new QRCodeWriter();
// 配置二维码参数(固定配置,确保结果一致)
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.ERROR_CORRECTION, ERROR_CORRECTION);
hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
hints.put(EncodeHintType.MARGIN, MARGIN);
// 生成固定尺寸的二维码矩阵
BitMatrix matrix = writer.encode(
content,
BarcodeFormat.QR_CODE,
QR_CODE_SIZE,
QR_CODE_SIZE,
hints
);
// 转换为BufferedImage
BufferedImage image = MatrixToImageWriter.toBufferedImage(matrix);
// 写入字节流并转换为Base64
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(image, IMAGE_FORMAT, bos);
// 返回带前缀的Base64字符串前端可直接显示
return "data:image/png;base64," + Base64Utils.encodeToString(bos.toByteArray());
}
}

View File

@@ -0,0 +1,52 @@
server:
port: 8090
spring:
application:
name: discipline-report
# 数据库
datasource:
url: jdbc:mysql://10.73.199.164:1444/finedb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: bigdata
password: bigdata@2020
driver-class-name: com.mysql.cj.jdbc.Driver
# 邮箱配置
mail:
host: smtp.163.com
username: 18817503105@163.com
password: NScs2Ct8YTniyBNP
port: 465
protocol: smtps
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
# Mybatis
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.discipline.report.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 自定义配置【和CustomConfigProperties字段完全对齐】
custom:
h5-index-url: http://localhost
tip-content: 请如实填写举报信息,我们将严格保密
qr-code-fixed-url: http://10.15.33.107:3301/#/home
email-from: 18817503105@163.com
email-subject: 【系统提醒】新举报工单提交
email-content: <p>系统收到一条新举报,请及时处理!</p>
# 文件上传Windows开发路径
discipline:
file:
upload:
path: D:/home/upload/report

View File

@@ -0,0 +1,49 @@
server:
port: 8090
spring:
application:
name: discipline-report
datasource:
url: jdbc:mysql://10.73.199.144:3506/finedb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: bigdata
password: bigdata@2020
driver-class-name: com.mysql.cj.jdbc.Driver
mail:
host: smtp.163.com
username: 18817503105@163.com
password: NScs2Ct8YTniyBNP
port: 465
protocol: smtps
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.discipline.report.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 自定义配置
custom:
h5-index-url: https://isee.ahtrq.com
tip-content: 请如实填写举报信息,我们将严格保密
qr-code-fixed-url: https://isee.ahtrq.com:9081/gmo
email-from: 18817503105@163.com
email-subject: 【系统提醒】新举报工单提交
email-content: <p>系统收到一条新举报,请及时处理!</p>
# Linux生产路径
discipline:
file:
upload:
path: /opt/ahtrq/discipline-report/upload/report

View File

@@ -0,0 +1,3 @@
spring:
profiles:
active: prod

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.discipline.report.mapper.ReportEmailReceiverMapper">
<resultMap id="BaseResultMap" type="com.discipline.report.entity.ReportEmailReceiver">
<id column="id" property="id"/>
<result column="receiver_name" property="receiverName"/>
<result column="receiver_email" property="receiverEmail"/>
<result column="receive_type" property="receiveType"/>
<result column="sort_num" property="sortNum"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<sql id="Base_Column_List">
id, receiver_name, receiver_email, receive_type, sort_num, status, create_time, update_time
</sql>
<select id="selectByReceiveType" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM report_email_receiver
WHERE status = 1 AND receive_type = #{receiveType}
ORDER BY sort_num ASC, id ASC
</select>
<select id="selectAllEnabled" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM report_email_receiver
WHERE status = 1
ORDER BY receive_type ASC, sort_num ASC, id ASC
</select>
</mapper>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.discipline.report.mapper.ReportFileMapper">
<insert id="insertBatch">
INSERT INTO report_file (report_id, file_name, file_path, file_size, create_time)
VALUES
<foreach collection="files" item="file" separator=",">
(#{file.reportId}, #{file.fileName}, #{file.filePath}, #{file.fileSize}, #{file.createTime})
</foreach>
</insert>
<select id="selectByReportId" resultType="com.discipline.report.entity.ReportFile">
SELECT id, report_id, file_name, file_path, file_size, create_time
FROM report_file
WHERE report_id = #{reportId}
ORDER BY create_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.discipline.report.mapper.ReportRecordMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO report_record (is_anonymous, reporter_name, report_content, contact_info, email_status, create_time)
VALUES (#{isAnonymous}, #{reporterName}, #{reportContent}, #{contactInfo}, #{emailStatus}, #{createTime})
</insert>
<update id="updateById">
UPDATE report_record
SET email_status = #{emailStatus}
WHERE id = #{id}
</update>
</mapper>

View File

@@ -0,0 +1,6 @@
<html>
<body>
<h1>hello word!!!</h1>
<p>this is a html page</p>
</body>
</html>

View File

@@ -0,0 +1,13 @@
package com.discipline.report;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DisciplineReportApplicationTests {
@Test
void contextLoads() {
}
}