Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions OpenBlog-business/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>com.yqz</groupId>
<artifactId>OpenBlog-common</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Dubbo + Nacos (consumer) -->
<dependency>
<groupId>org.apache.dubbo</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,22 @@
import com.yqz.openblog.seo.SeoProperties;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
* 显式声明组件扫描(覆盖 @SpringBootApplication 的默认扫描):
* - basePackages 与默认一致;额外排除 com.yqz.openblog.common 包——
* 该包的异常处理器改由 OpenBlog-common 的自动装配(CommonAutoConfiguration)注册。
* - 显式 @ComponentScan 不会自动带上 @SpringBootApplication 默认的两个过滤,
* 因此手动补回 TypeExcludeFilter / AutoConfigurationExcludeFilter,保证
* framework 模块下的 @AutoConfiguration 类不被组件扫描重复注册。
*/
@SpringBootApplication
@EnableDubbo
@EnableScheduling
Expand All @@ -19,6 +31,13 @@
AuthSecurityProperties.class,
SeoProperties.class
})
@ComponentScan(
basePackages = "com.yqz.openblog",
excludeFilters = {
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.yqz\\.openblog\\.common\\..*")
})
public class OpenBlogApplication {

public static void main(String[] args) {
Expand Down
34 changes: 34 additions & 0 deletions OpenBlog-common/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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>com.yqz</groupId>
<artifactId>OpenBlog</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>

<artifactId>OpenBlog-common</artifactId>
<name>OpenBlog-common</name>
<description>OpenBlog common module — unified REST response, exceptions, and utilities</description>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationDeniedException;

/**
* 统一异常处理(MVP)
* 统一异常处理(OpenBlog-common)。通过 CommonAutoConfiguration 自动装配注册
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
Expand All @@ -38,15 +36,6 @@ public ResponseEntity<ApiResponse<Object>> onValidation(MethodArgumentNotValidEx
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.fail(4001, msg));
}

/**
* 方法级鉴权(@PreAuthorize)失败会抛出 AuthorizationDeniedException;
* 以前会落到兜底 Exception -> 5001,导致前端误判为“服务器异常”。
*/
@ExceptionHandler({AuthorizationDeniedException.class, AccessDeniedException.class})
public ResponseEntity<ApiResponse<Object>> onAccessDenied(Exception ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.fail(4030, "无权限"));
}

/**
* IO 异常(MinIO 读写、文件读写等)直接返回原始错误信息,便于排查。
*/
Expand All @@ -64,4 +53,3 @@ public ResponseEntity<ApiResponse<Object>> onOther(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResponse.fail(5001, msg));
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.yqz.openblog.common;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
* 安全相关异常处理,仅在 classpath 存在 spring-security 时由 CommonAutoConfiguration 装配。
* (email 等无 security 依赖的服务自动跳过。)
*/
@RestControllerAdvice
public class SecurityExceptionHandler {

/**
* 方法级鉴权(@PreAuthorize)失败会抛出 AuthorizationDeniedException;
* 以前会落到兜底 Exception -> 5001,导致前端误判为“服务器异常”。
*/
@ExceptionHandler({AuthorizationDeniedException.class, AccessDeniedException.class})
public ResponseEntity<ApiResponse<Object>> onAccessDenied(Exception ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.fail(4030, "无权限"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.yqz.openblog.common.config;

import com.yqz.openblog.common.GlobalExceptionHandler;
import com.yqz.openblog.common.SecurityExceptionHandler;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;

/**
* OpenBlog-common 自动装配:注册统一异常处理器。
* 注意:调用方组件扫描应排除 com.yqz.openblog.common.*,避免与自动装配重复注册。
*/
@AutoConfiguration
@ConditionalOnWebApplication
public class CommonAutoConfiguration {

@Bean
@ConditionalOnMissingBean(GlobalExceptionHandler.class)
public GlobalExceptionHandler globalExceptionHandler() {
return new GlobalExceptionHandler();
}

@Bean
@ConditionalOnClass(name = "org.springframework.security.access.AccessDeniedException")
@ConditionalOnMissingBean(SecurityExceptionHandler.class)
public SecurityExceptionHandler securityExceptionHandler() {
return new SecurityExceptionHandler();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.yqz.openblog.common.config.CommonAutoConfiguration
7 changes: 7 additions & 0 deletions OpenBlog-email/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
<version>${project.version}</version>
</dependency>

<!-- Common (统一响应结构 + 全局异常处理器) -->
<dependency>
<groupId>com.yqz</groupId>
<artifactId>OpenBlog-common</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.yqz.openblog.email.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.yqz.openblog.common.ApiResponse;
import com.yqz.openblog.common.PageResult;
import com.yqz.openblog.email.api.EmailSendRequest;
import com.yqz.openblog.email.api.EmailSendResult;
import com.yqz.openblog.email.dto.EmailRecordResponse;
import com.yqz.openblog.email.service.EmailService;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/v1/email")
@CrossOrigin(origins = "*")
Expand All @@ -24,25 +24,21 @@ public EmailAdminController(EmailService emailService) {
* 快速测试发送邮件:POST /api/v1/email/test?recipient=xxx&subject=xxx&body=xxx
*/
@PostMapping("/test")
public EmailSendResult testSend(
public ApiResponse<EmailSendResult> testSend(
@RequestParam String recipient,
@RequestParam String subject,
@RequestParam String body) {
EmailSendRequest req = new EmailSendRequest(recipient, subject, body);
return emailService.send(req);
return ApiResponse.ok(emailService.send(req));
}

@GetMapping("/records")
public Map<String, Object> listRecords(
public ApiResponse<PageResult<EmailRecordResponse>> listRecords(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String status) {
IPage<EmailRecordResponse> p = emailService.listRecords(page, size, status);
return Map.of(
"items", p.getRecords(),
"total", p.getTotal(),
"page", page,
"size", size
);
PageResult<EmailRecordResponse> pr = new PageResult<>(p.getRecords(), page, size, p.getTotal());
return ApiResponse.ok(pr);
}
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

| 模块 | 说明 |
|------|------|
| `OpenBlog-common` | 公共模块:统一响应 `ApiResponse`、`PageResult`、`BizException`、异常处理自动装配 |
| `OpenBlog-business` | 主业务服务(端口 8082):文章、评论、论坛、用户、认证、SEO、媒体管理 |
| `OpenBlog-email` | 独立邮件服务(端口 8083):阿里云 DirectMail + Dubbo RPC + Nacos 注册 |
| `OpenBlog-api` | 共享 API 聚合模块 |
Expand Down
124 changes: 124 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# OpenBlog 项目规划

> 最近更新:2026-08-18
> 目的:梳理近期开发任务优先级与范围,作为后续 spec/plan 的输入。
> 每个任务开工前,建议用 brainstorming 细化设计 → writing-plans 出实现计划 → subagent-driven 执行(与 OpenBlog-common 抽取同一流程)。

---

## 任务一览(按优先级)

| # | 任务 | 状态 | 目标 |
|---|------|------|------|
| 1 | 完善 email 服务,真正实现邮箱注册验证 | 🔲 待排期 | 注册流程真实发送验证邮件 |
| 2 | 首页前端动态化 | 🔲 待排期 | 设计动态首页 |
| 3 | 部署 Docker 化 + 启动加速 | 🔲 待排期 | 后端 docker 启动,加快启动 |

---

## 任务 1:邮箱验证注册(最高优先级)

### 现状(已核实)

- **注册流程不发邮件**:`AuthService.register()`(`OpenBlog-business/.../user/service/AuthService.java:69`)流程为
滑块验证 → 邮箱格式白名单(`AllowedMailbox`:QQ/网易/谷歌系)→ 用户名/邮箱唯一性 → **直接创建 ACTIVE READER → 返回 token**(注册即登录)。
- **email 基础设施已就绪,只差接线**:
- RPC 契约 `EmailRpcService.send(EmailSendRequest)`(`OpenBlog-api/OpenBlog-email-api/`)
- Provider:`EmailRpcServiceImpl` `@DubboService`(email 模块),委托 `EmailService.send()`,经阿里云 DirectMail 真实发送,落 `email_record` 表
- **business 侧目前没有 `@DubboReference` 消费调用**(全库检索确认)——链路已通但未接入
- Redis 封装可用(`OpenBlog-framework-redis`,`RedisOps`),适合存验证码。

### 建议范围

1. **business 接入 Dubbo 消费**:新增 `@DubboReference` 注入 `EmailRpcService`(consumer 配置 `application.yaml` 已就绪)。
2. **验证码发送接口**:`POST /api/v1/auth/email-code`(邮箱 + 滑块 challenge)
- 生成 6 位数字验证码;Redis 存储(`email:code:{email}`),TTL 5 分钟;发送冷却 60 秒。
- 调 `EmailRpcService.send()` 发"OpenBlog 注册验证码"模板邮件。
3. **注册接口校验验证码**:`RegisterRequest` 增加 `code` 字段;校验通过才创建用户。
4. **email 模块**:新增注册验证码邮件模板(HTML),可配置站点名/验证码。
5. **前端注册页**(`vue/src/views/`,注册组件):邮箱输入 → "获取验证码"按钮(60s 倒计时)→ 提交注册。

### 推荐决策:验证码前置

先验证邮箱(发码 → 校验)再建号,而非"建号后激活"。理由:避免邮箱占用、符合"注册即登录"现状,改动集中在 auth 流程。

### 验收标准

- [ ] 注册流程真实发出验证邮件(本地可观察 email_record 记录)
- [ ] 验证码错误 / 过期 / 重发冷却均有明确提示
- [ ] business → email 的 Dubbo 调用打通(日志可见)

### 开放问题(开工前确认)

- 验证码**前置** vs **建号后激活**(现有 `PendingUser` 后台审核是管理员审核概念,是否要与此结合?)
- email 模块现有 HTTP 管理接口(`/api/v1/email/*`)是否保留?
- 验证码邮件是否需要图形验证码之外的防刷(滑块已有,是否足够)?

---

## 任务 2:首页前端动态化

### 现状(已核实)

- 首页组件:`vue/src/views/HomeView.vue`。
- 此前相关设计:`2026-07-21-home-hero-image`(Hero 图)、`2026-07-22-project-recommendation`(项目推荐)、`2026-06-23` 系列飞书主题重构。
- business 已提供文章/论坛/项目/互动/站点配置等 API,首页可接真实数据。

### 建议范围

1. **信息架构**:Hero(动态化)→ 最新文章流 → 推荐项目墙 → 站点数据(文章数/评论数/访问) → 最新动态/评论。
2. **动态效果**:滚动渐入、鼠标视差、动态渐变背景、骨架屏;Live2D 看板娘已有,保留。
3. **真实数据接入**:文章列表、项目推荐、站点配置(`/api/v1/...`)替代静态占位。
4. **响应式 + 性能**:懒加载、图片优化、首屏指标。

### 验收标准

- [ ] 首页为动态渲染(数据来自接口,非写死)
- [ ] 动效流畅,无首屏卡顿
- [ ] 桌面/移动端响应式可用

### 开放问题(开工前确认)

- "动态前端页面"侧重**视觉动效**还是**内容动态加载**,还是两者?
- 在现有飞书主题上迭代,还是重新设计风格?

---

## 任务 3:部署 Docker 化 + 启动加速

### 现状(已核实)

- **前端**:已 Docker 化 —— Nginx 容器(`vue/Dockerfile`、`vue/docker.sh`),宿主端口 `18088 → 80`,CI 在 Runner 构建镜像 → 上传 → `docker.sh` 加载更新。
- **后端**:宿主机 **systemd** `java -jar`(`OpenBlog-business-1.0.0-SNAPSHOT.jar`,`-Xmx1024M -Xms256M`),由 self-hosted Runner(`openblog-backend`)部署(`.github/workflows/ci.yml`)。
- **基础设施**:MySQL / Redis / MinIO / Nacos 位于 `10.21.76.221`(局域网,宿主或独立机器)。

### 建议范围

1. **后端 Dockerfile**(`OpenBlog-business`):基于 JRE 17 镜像;分层构建(依赖层缓存);非 root 用户;healthcheck。
2. **docker-compose 编排**:business + email 服务;MySQL/Redis/MinIO/Nacos 作为外部依赖通过环境变量注入,保持容器外。
3. **启动加速**(Spring Boot + JVM 层面):
- `spring.main.lazy-initialization=true`(或按 bean 选择性懒加载)
- JVM 调优:`-XX:+UseG1GC`(或 ZGC)、合理 `-Xms/-Xmx`、显式 dump 参数
- 可选:AppCDS / 镜像分层缓存加速部署
- 依赖瘦身:排查未用 starter
4. **CI 改造**:构建后端镜像 → 上传服务器 → `docker compose up -d`(替换 systemd);保留回滚(镜像 tag)。

### 验收标准

- [ ] `docker compose up -d` 一键启动后端服务
- [ ] 记录优化前后启动耗时对比
- [ ] CI 部署走 Docker,且失败可回滚

### 开放问题(开工前确认)

- 是否也容器化 **email** 服务(建议一起,同机编排)?
- MySQL/Redis/MinIO/Nacos 留在容器外还是纳入编排?
- "加快启动速度"目标值(如:冷启动 < 15s / 部署总耗时减半)?

---

## 通用待办(非当前优先级)

- 单元测试补充(README 待办项)。
- 版本号收敛到根 pom `dependencyManagement`。
- `MybatisPlusMetaObjectHandler` 迁移至 framework 模块(common 抽取时遗留)。
Loading