如何实现“Angular + Spring Boot 联动,触发发送 Report 邮件的完整流程与代码(详细介绍)
前端Angular和后端使用 Spring Boot相结合
前端使用的是 Angular,后端使用 Spring Boot,并希望它们协作发送邮件(用后端模板渲染),如何实现“Angular + Spring Boot 联动,触发发送 Eligibility Report 邮件”的完整流程与代码。
项目目标
用户在 Angular 页面上传/提交 Eligibility 文件 ➜
Spring Boot 接收请求 ➜ 保存数据 ➜ 生成 HTML 报告 ➜ 发送邮件
系统结构
📦 项目分为两个独立模块:├── frontend/ (Angular 项目)└── backend/ (Spring Boot 项目)
后端:Spring Boot 示例(email + Thymeleaf)项目结构
eligibility-report-mailer/├── src/│ ├── main/│ │ ├── java/com/example/mailer/│ │ │ ├── controller/│ │ │ │ └── ReportController.java│ │ │ ├── model/│ │ │ │ ├── EligibilityReport.java│ │ │ │ └── RejectedRecord.java│ │ │ ├── repository/│ │ │ │ └── EligibilityReportRepository.java│ │ │ ├── service/│ │ │ │ └── MailService.java│ │ │ └── EligibilityReportMailerApplication.java│ │ └── resources/│ │ ├── application.properties│ │ └── templates/│ │ └── report-template.html└── pom.xml
1. 创建 API 接口 /api/reports/send
EligibilityReport.java(实体)
package com.amy.twilio_demo_test.entity;import jakarta.persistence.*;import java.util.List;@Entitypublic class EligibilityReport { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String fileName; private String healthPlanName; private String submissionDateTime; private int totalRecords; private int successfullyIngested; private int rejectedRecords; private String recipientEmail; @OneToMany(mappedBy = \"report\", cascade = CascadeType.ALL) private List<RejectedRecord> rejectedDetails; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public String getHealthPlanName() { return healthPlanName; } public void setHealthPlanName(String healthPlanName) { this.healthPlanName = healthPlanName; } public String getSubmissionDateTime() { return submissionDateTime; } public void setSubmissionDateTime(String submissionDateTime) { this.submissionDateTime = submissionDateTime; } public int getTotalRecords() { return totalRecords; } public void setTotalRecords(int totalRecords) { this.totalRecords = totalRecords; } public int getSuccessfullyIngested() { return successfullyIngested; } public void setSuccessfullyIngested(int successfullyIngested) { this.successfullyIngested = successfullyIngested; } public int getRejectedRecords() { return rejectedRecords; } public void setRejectedRecords(int rejectedRecords) { this.rejectedRecords = rejectedRecords; } public String getRecipientEmail() { return recipientEmail; } public void setRecipientEmail(String recipientEmail) { this.recipientEmail = recipientEmail; } public List<RejectedRecord> getRejectedDetails() { return rejectedDetails; } public void setRejectedDetails(List<RejectedRecord> rejectedDetails) { this.rejectedDetails = rejectedDetails; }}
RejectedRecord.java
package com.amy.twilio_demo_test.entity;import jakarta.persistence.*;@Entitypublic class RejectedRecord { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private int recordNumber; private String memberId; private String errorReason; @ManyToOne @JoinColumn(name = \"report_id\") private EligibilityReport report; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public int getRecordNumber() { return recordNumber; } public void setRecordNumber(int recordNumber) { this.recordNumber = recordNumber; } public String getMemberId() { return memberId; } public void setMemberId(String memberId) { this.memberId = memberId; } public String getErrorReason() { return errorReason; } public void setErrorReason(String errorReason) { this.errorReason = errorReason; } public EligibilityReport getReport() { return report; } public void setReport(EligibilityReport report) { this.report = report; }}
2. 控制器 ReportController.java
package com.amy.twilio_demo_test.controller;import com.amy.twilio_demo_test.entity.EligibilityReport;import com.amy.twilio_demo_test.repository.EligibilityReportRepository;import com.amy.twilio_demo_test.service.MailService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;@CrossOrigin(origins = \"http://localhost:4200\")@RestController@RequestMapping(\"/api/reports\")public class ReportController { @Autowired private EligibilityReportRepository reportRepository; @Autowired private MailService mailService; @PostMapping(\"/send\") public ResponseEntity<String> sendReport(@RequestBody EligibilityReport report) { report.getRejectedDetails().forEach(r -> r.setReport(report)); reportRepository.save(report); mailService.sendEligibilityReport(report); return ResponseEntity.ok(\"Email sent successfully.\"); }}
3. 邮件服务 MailService.java
package com.amy.twilio_demo_test.service;import com.amy.twilio_demo_test.entity.EligibilityReport;import jakarta.mail.MessagingException;import jakarta.mail.internet.MimeMessage;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.mail.javamail.JavaMailSender;import org.springframework.mail.javamail.MimeMessageHelper;import org.springframework.stereotype.Service;import org.thymeleaf.TemplateEngine;import org.thymeleaf.context.Context;@Servicepublic class MailService { @Autowired private JavaMailSender mailSender; @Autowired private TemplateEngine templateEngine; public void sendEligibilityReport(EligibilityReport report) { Context context = new Context(); context.setVariable(\"report\", report); context.setVariable(\"rejectedRecords\", report.getRejectedDetails()); String body = templateEngine.process(\"report-template\", context); MimeMessage mimeMessage = mailSender.createMimeMessage(); try { MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); helper.setTo(report.getRecipientEmail()); helper.setSubject(\"Eligibility File Processing Report - \" + report.getFileName()); helper.setText(body, true); mailSender.send(mimeMessage); } catch (MessagingException e) { throw new RuntimeException(\"Failed to send email\", e); } }}
4. application.properties
spring.mail.host=localhostspring.mail.port=1025spring.mail.username=spring.mail.password=spring.mail.properties.mail.smtp.auth=falsespring.mail.properties.mail.smtp.starttls.enable=falsespring.thymeleaf.prefix=classpath:/templates/spring.thymeleaf.suffix=.html
spring.application.name=twilio_demo_testserver.port=8080# databasespring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false#spring.datasource.url=jdbc:mysql://localhost:3306/sms_schedulerspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.username=rootspring.datasource.password=rootspring.jpa.hibernate.ddl-auto=updatespring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialectspring.datasource.hikari.maximum-pool-size=10spring.datasource.hikari.minimum-idle=2spring.datasource.hikari.idle-timeout=30000spring.datasource.hikari.max-lifetime=60000spring.datasource.hikari.connection-timeout=30000spring.main.allow-bean-definition-overriding=true## send mailspring.mail.host=localhostspring.mail.port=1025spring.mail.username=spring.mail.password=spring.mail.properties.mail.smtp.auth=falsespring.mail.properties.mail.smtp.starttls.enable=falsespring.thymeleaf.prefix=classpath:/templates/spring.thymeleaf.suffix=.htmlspring.thymeleaf.mode=HTMLspring.thymeleaf.encoding=UTF-8spring.thymeleaf.cache=false# JPAspring.jpa.show-sql=truespring.jpa.open-in-view=false# Twiliotwilio.account.sid=ACca13ddbb9244ade1bb167022dd5e8b15twilio.auth.token=635f85819844a00ea52786815ac7aa7btwilio.phone.number=+12524604215
5. Repository:EligibilityReportRepository.java
import com.amy.twilio_demo_test.entity.EligibilityReport;import org.springframework.data.jpa.repository.JpaRepository;public interface EligibilityReportRepository extends JpaRepository<EligibilityReport, Long> {}
6. report-template.html
<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"UTF-8\"> <title>Eligibility File Processing Report</title> <style> body { font-family: Arial, sans-serif; background-color: #f9f9f9; color: #333; margin: 0; padding: 20px; } .container { background-color: #ffffff; border-radius: 8px; padding: 20px; max-width: 800px; margin: auto; box-shadow: 0 2px 8px rgba(0,0,0,0.05); } h2 { color: #2a6496; } .summary, .rejection { margin-top: 20px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { border: 1px solid #dddddd; padding: 10px; text-align: left; } th { background-color: #f2f2f2; } .footer { margin-top: 30px; font-size: 14px; color: #666; } .support { margin-top: 10px; font-weight: bold; } </style></head><body><div class=\"container\"> <h2>Eligibility File Processing Report – <span th:text=\"${report.fileName}\"></span> | <span th:text=\"${report.submissionDateTime}\"></span></h2> <p>Dear <strong th:text=\"${report.healthPlanName}\">[Health Plan\'s Name]</strong>,</p> <p> Thank you for submitting your eligibility file (<strong th:text=\"${report.fileName}\">[File Name]</strong>) on <strong th:text=\"${report.submissionDateTime}\">[Date/Time]</strong>. </p> <div class=\"summary\"> <h3>Processing Summary:</h3> <ul> <li><strong>Total Records Received:</strong> <span th:text=\"${report.totalRecords}\">[XXX]</span></li> <li><strong>Successfully Ingested:</strong> <span th:text=\"${report.successfullyIngested}\">[XXX]</span></li> <li><strong>Rejected Records:</strong> <span th:text=\"${report.rejectedRecords}\">[XXX]</span></li> </ul> </div> <tbody> <tr th:each=\"record : ${report.rejectedDetails}\"> <td th:text=\"${record.recordNumber}\">1</td> <td th:text=\"${record.memberId}\">A1B2C3</td> <td th:text=\"${record.errorReason}\">Invalid Member ID Format</td> </tr> <tbody> <tr th:each=\"record : ${report.rejectedDetails}\"> <td th:text=\"${record.recordNumber}\">1</td> <td th:text=\"${record.memberId}\">A1B2C3</td> <td th:text=\"${record.errorReason}\">Invalid Member ID Format</td> </tr> </tbody></div></body></html>
7.pom.xml
<?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 https://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>3.4.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.amy</groupId> <artifactId>twilio_demo_test</artifactId> <version>0.0.1-SNAPSHOT</version> <name>twilio_demo_test</name> <description>twilio_demo_test</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <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> <!-- 加入依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>com.twilio.sdk</groupId> <artifactId>twilio</artifactId> <version>10.6.8</version> </dependency> <!-- https://mvnrepository.com/artifact/com.sendgrid/sendgrid-java --> <dependency> <groupId>com.sendgrid</groupId> <artifactId>sendgrid-java</artifactId> <version>4.10.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.18.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.18.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </path> </annotationProcessorPaths> </configuration> </plugin> <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>
8.本地测试邮件发送服务
建议使用本地邮件服务器进行调试:
启动 MailHog(Docker):
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
访问 http://localhost:8025 查看邮件
Postman 测试接口(也可以测试 Angular 后端接口)
POST http://localhost:8080/api/reports/sendContent-Type: application/json
{ \"fileName\": \"eligibility_aug_05.csv\", \"healthPlanName\": \"HealthPlus\", \"submissionDateTime\": \"2025-08-05T14:00\", \"totalRecords\": 100, \"successfullyIngested\": 95, \"rejectedRecords\": 5, \"recipientEmail\": \"test@example.com\", \"rejectedDetails\": [ { \"recordNumber\": 1, \"memberId\": \"A123\", \"errorReason\": \"Missing DOB\" }, { \"recordNumber\": 2, \"memberId\": \"B456\", \"errorReason\": \"Invalid ID\" } ]}
前端:Angular 示例
src/ └── app/ └── report/ ├── report.component.ts ✅ ├── report.component.html ✅ └── report.component.css (可选)
新建一个组件:
ng generate component report# 或简写:ng g c report
src/└── app/ └── report/ ├── report.component.ts // 组件逻辑代码 ├── report.component.html // 组件模板(HTML) ├── report.component.css // 组件样式 └── report.component.spec.ts // 单元测试文件
工作流程说明
-
用户在浏览器中访问 Angular 应用(默认端口是 http://localhost:4200)。
-
Angular 页面渲染并展示一个按钮:Send Email。
-
用户点击按钮,触发 sendEmail() 方法。
-
该方法使用 Angular 的 HttpClient 发出一个 POST 请求到 http://localhost:8080/api/reports/send。
-
后端 Spring Boot 接收到请求,执行邮件发送逻辑。
-
请求完成后,前端可通过 .subscribe() 显示提示信息(如 alert 成功/失败)。
1. report.component.ts
import { Component } from \'@angular/core\';import { HttpClient } from \'@angular/common/http\';@Component({ selector: \'app-report\', templateUrl: \'./report.component.html\',})export class ReportComponent { constructor(private http: HttpClient) {} sendEmail() { const report = { fileName: \'test_aug_05.csv\', recipientEmail: \'recipient@example.com\', healthPlanName:\'test\', totalRecords: 100, successfullyIngested: 98, rejectedRecords: 2, submissionDateTime:\'2025-08-05 14:22\', rejectedDetails: [ { recordNumber: 1, memberId: \'A1B2C3\', errorReason: \'Invalid Member ID Format\' }, { recordNumber: 2, memberId: \'XYZ456\', errorReason: \'Missing Date of Birth (DOB)\' } ]}; // 调用后端发送邮件 API this.http.post(\'http://localhost:8080/api/reports/send\', report, { responseType: \'text\' }) .subscribe({ next: (res) => alert(\'✅ 邮件发送成功: \' + res), error: (err) => alert(\'❌ 发送失败: \' + err.message) }); }}
2. report.component.html 有点击按钮
<div class=\"container\"> <h2>发送 Eligibility Report</h2> <button (click)=\"sendEmail()\">点击发送邮件</button></div>
3. app.module.ts 引入了 HttpClientModule
import { NgModule } from \'@angular/core\';import { BrowserModule } from \'@angular/platform-browser\';import { AppComponent } from \'./app.component\';import { HttpClientModule } from \'@angular/common/http\';import { ReportComponent } from \'./report/report.component\';@NgModule({ declarations: [AppComponent, ReportComponent], imports: [BrowserModule, HttpClientModule], bootstrap: [AppComponent]})export class AppModule {}
4. Angular 的页面中显示了 report 组件,app.component.html
<app-report></app-report>
本地模拟邮件服务(MailHog)
本地启动一个 SMTP 模拟服务收邮件(方便调试):
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
打开发送结果:http://localhost:8025
CORS 设置说明
Spring Boot Controller 添加:
@CrossOrigin(origins = \"http://localhost:4200\")
或 application.properties:
# 全局允许 CORS(不推荐生产)spring.web.cors.allowed-origins=http://localhost:4200
http://localhost:4200
http://localhost:8080