我不再使用 Excel:如何使用 Spring Boot 3 与 Angular 17 构建企业级工作跟踪器
Source: Dev.to
Note: 这不仅仅是一个简单的 CRUD 应用。我把它当作一次 系统设计挑战 来处理,重点关注安全性、并发性和云原生存储模式。
🔗 资源
- 在线演示 – https://thughari.github.io/JobTrackPro-UI
- 后端仓库 – https://github.com/thughari/JobTrackPro
- 前端仓库 – https://github.com/thughari/JobTrackPro-UI
🛠️ 技术栈
| 层 | 技术 |
|---|---|
| 后端 | Java 21 + Spring Boot 3.2 |
| 安全 | Spring Security 6 (OAuth2 + JWT) |
| 数据库 | PostgreSQL(生产)/ MySQL(开发) |
| 存储 | Cloudflare R2(兼容 AWS S3) |
| 邮件 | Spring Mail(SMTP)+ Brevo |
| 前端 | Angular 17(Signals)+ TailwindCSS + D3.js |
| DevOps | Docker + Google Cloud Run + GitHub Actions |
Source: …
🧠 架构深度解析
以下是我在构建 JobTrackPro 时解决的四个最有趣的工程难题。
1️⃣ 混合认证(Google/GitHub + 本地)
目标 – 让用户可以使用 Google 或 GitHub 登录 并且 后续设置密码以脱离外部账号。
流程

实现了自定义的 OAuth2SuccessHandler,它:
- 检查邮箱是否已存在。
- 如有必要,实时创建用户。
- 同步头像 URL。
- 为无状态前端颁发 JWT。
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
// Extract provider‑specific attributes
OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) authentication;
OAuth2User oAuth2User = authToken.getPrincipal();
String registrationId = authToken.getAuthorizedClientRegistrationId();
UserInfo userInfo = extractUserInfo(registrationId, oAuth2User.getAttributes());
// Upsert user (create if absent, update otherwise)
User user = userService.upsertUser(userInfo);
// Generate JWT
String token = jwtService.createToken(user);
// Redirect back to Angular with the token
String redirectUrl = uiUrl + "/login-success?token=" + token;
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
}
}
2️⃣ 使用 CompletableFuture 的多线程分析
仪表盘展示四个关键指标。顺序执行查询成为瓶颈,于是我将它们并行化:
// Run aggregations in parallel
CompletableFuture<Long> activeFuture = CompletableFuture.supplyAsync(
() -> jobRepository.countActiveJobs(email), executor);
CompletableFuture<Long> interviewFuture = CompletableFuture.supplyAsync(
() -> jobRepository.countInterviews(email), executor);
CompletableFuture<Long> offerFuture = CompletableFuture.supplyAsync(
() -> jobRepository.countOffers(email), executor);
// Wait for all to finish
CompletableFuture.allOf(activeFuture, interviewFuture, offerFuture).join();
Long activeCount = activeFuture.join();
Long interviewCount = interviewFuture.join();
Long offerCount = offerFuture.join();
3️⃣ 原子化的个人资料更新 & Cloudflare R2
将图片存为 BLOB 会导致备份膨胀,而本地磁盘在无服务器平台上不可用。解决方案是使用 Cloudflare R2,并通过单一原子事务完成:
@Transactional
public void updateProfile(ProfileDto dto, MultipartFile avatar) {
// 1. Upload new avatar to R2 (if present)
String newAvatarUrl = null;
if (avatar != null && !avatar.isEmpty()) {
newAvatarUrl = r2Service.upload(avatar);
}
// 2. Update profile fields
User user = userRepository.findById(dto.getUserId())
.orElseThrow(() -> new NotFoundException("User not found"));
user.setFullName(dto.getFullName());
user.setBio(dto.getBio());
// 3. Replace avatar URL atomically
if (newAvatarUrl != null) {
String oldAvatarUrl = user.getAvatarUrl();
user.setAvatarUrl(newAvatarUrl);
// Delete old file after successful DB commit
if (oldAvatarUrl != null) {
r2Service.delete(oldAvatarUrl);
}
}
userRepository.save(user);
}
4️⃣ 异步邮件系统
“忘记密码”流程现在会立即返回,而邮件则在后台发送:
@Service
public class EmailService {
@Async
public void sendPasswordReset(String to, String resetLink) {
SimpleMailMessage message = new SimpleMailMessage();
message
.setTo(to);
message.setSubject("Password Reset Request");
message.setText("Click the link to reset your password: " + resetLink);
mailSender.send(message);
}
}
⚡ Angular Signals & Optimistic UI
前端完全依赖 Angular 17 Signals。状态(工作列表、统计、个人资料)是响应式的,支持乐观 UI 更新。
“魔法”时刻 – 当添加工作时,UI 会立即更新 Signal 列表,同时在后台执行 POST 请求。仪表盘图表会即时重新渲染。
// Angular service: optimistic update
async addJob(job: Job) {
// 1️⃣ Optimistically update the local signal
const current = this.jobsSignal();
this.jobsSignal.set([...current, job]);
// 2️⃣ Send request to backend
try {
const saved = await firstValueFrom(this.http.post(`${this.apiUrl}/jobs`, job));
// Replace the optimistic entry with the server‑generated one (e.g., ID)
this.jobsSignal.update(list => list.map(j => (j.tempId === job.tempId ? saved : j)));
} catch (err) {
// Rollback on error
this.jobsSignal.update(list => list.filter(j => j.tempId !== job.tempId));
throw err;
}
}
🎉 最后思考
JobTrackPro 展示了 精心构建的云原生堆栈 如何将个人生产力工具转变为企业级应用。关键要点:
- 混合 OAuth2/JWT 认证
- 使用
CompletableFuture进行并行分析 - 在 Cloudflare R2 上进行原子媒体处理
- 异步邮件发送
- 响应式 Angular Signals
这种组合提供了快速、安全且令人愉悦的用户体验——正是现代求职工作流所应得的。欢迎随意探索演示、给仓库加星,或在有建议时提交 issue! 🚀
🏁 结论
构建 JobTrackPro 让我明白,“全栈”不仅仅是把数据库连接到前端。它还涉及处理边缘情况——例如图片上传失败、保护 OAuth2 重定向,以及使分析可扩展。该项目保持 开源,以帮助开发者构建类似的仪表盘。
- Live App: https://thughari.github.io/JobTrackPro-UI
- Backend Source: https://github.com/thughari/JobTrackPro
- Frontend Source: https://github.com/thughari/JobTrackPro-UI
💬 Discussion
你尝试过使用 Cloudflare R2 来存储图片吗?我发现它比 S3 便宜很多。请在评论中告诉我你对该架构的看法!
感谢阅读!祝编码愉快!🚀