网站开发-java
- 一、功能需求(论坛精简版)
- 二、技术选型
- 三、数据库设计(核心表)
-
- [1. 用户表 user](#1. 用户表 user)
- [2. 帖子表 post](#2. 帖子表 post)
- [3. 评论表 comment](#3. 评论表 comment)
- [四、后端开发(Spring Boot)](#四、后端开发(Spring Boot))
-
- [1. 项目结构与依赖](#1. 项目结构与依赖)
- [2. 配置文件 application.yml](#2. 配置文件 application.yml)
- [3. 实体类(使用 Lombok)](#3. 实体类(使用 Lombok))
- [4. JWT 工具类](#4. JWT 工具类)
- [5. 用户认证拦截器](#5. 用户认证拦截器)
- [6. 用户注册与登录](#6. 用户注册与登录)
- [7. 帖子模块(分页查询)](#7. 帖子模块(分页查询))
- [8. 统一返回与异常处理](#8. 统一返回与异常处理)
- [五、前端开发(Vue 3)](#五、前端开发(Vue 3))
-
- [1. 项目初始化](#1. 项目初始化)
- [2. 目录结构](#2. 目录结构)
- [3. axios 请求封装(request.js)](#3. axios 请求封装(request.js))
- [4. 用户状态管理(Pinia)](#4. 用户状态管理(Pinia))
- [5. 登录页面(Login.vue)](#5. 登录页面(Login.vue))
- [6. 帖子列表(PostList.vue)](#6. 帖子列表(PostList.vue))
- [7. 路由配置](#7. 路由配置)
- [六、部署方案(Docker Compose + Nginx)](#六、部署方案(Docker Compose + Nginx))
-
- [1. 项目根目录创建 docker-compose.yml](#1. 项目根目录创建 docker-compose.yml)
- [2. 后端 Dockerfile](#2. 后端 Dockerfile)
- [3. Nginx 配置(nginx.conf)](#3. Nginx 配置(nginx.conf))
- [4. 构建与启动](#4. 构建与启动)
- [5. 配置 HTTPS(可选)](#5. 配置 HTTPS(可选))
- 一、开源论坛系统快速选型对比
- 二、首选推荐:OpenIsle
-
- [1. 克隆项目](#1. 克隆项目)
- [2. 启动后端(Spring Boot)](#2. 启动后端(Spring Boot))
- [3. 启动前端(Vue3 + Nuxt)](#3. 启动前端(Vue3 + Nuxt))
- [4. 访问系统](#4. 访问系统)
- 三、备选方案:forum-java
- 四、选型建议与二次开发指南
第一次开发网站,以简单为主。以论坛系统为例,从零开始构建一个完整的网站
一、功能需求(论坛精简版)
| 模块 | 功能点 |
|---|---|
| 用户 | 注册、登录、个人资料(头像、签名) |
| 帖子 | 发布帖子(标题、内容、分类)、帖子列表(分页)、帖子详情 |
| 评论 | 对帖子进行评论、删除自己的评论 |
| 安全 | JWT 认证、密码加密、防 XSS、参数校验 |
二、技术选型
-
后端:Spring Boot 2.7.18 + MyBatis-Plus 3.5.3 + JWT (jjwt) + Lombok + Validation
-
前端:Vue 3 + Vite + Vue Router + Pinia + Axios + Element Plus
-
数据库:MySQL 8.0 + Redis(存储验证码/登录尝试,可选)
-
部署:Docker Compose + Nginx + JDK 8
三、数据库设计(核心表)
1. 用户表 user
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键(雪花ID或自增) |
| username | varchar(50) | 唯一,用户名 |
| password | varchar(100) | BCrypt 加密后的密码 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(255) | 头像 URL |
| signature | varchar(200) | 个人签名 |
| status | tinyint | 0-正常 1-禁用 |
| create_time | datetime | 注册时间 |
| update_time | datetime | 更新时间 |
2. 帖子表 post
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 发帖用户ID |
| title | varchar(100) | 标题 |
| content | text | 内容(富文本) |
| category | varchar(20) | 分类(技术/生活/问答) |
| view_count | int | 浏览数 |
| like_count | int | 点赞数 |
| status | tinyint | 0-正常 1-删除 |
| create_time | datetime | |
| update_time | datetime |
3. 评论表 comment
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| post_id | bigint | 关联的帖子ID |
| user_id | bigint | 评论用户ID |
| content | varchar(500) | 评论内容 |
| status | tinyint | 0-正常 1-删除 |
| create_time | datetime |
四、后端开发(Spring Boot)
1. 项目结构与依赖
text
src/main/java/com/example/forum/
├── controller
│ ├── AuthController.java
│ ├── PostController.java
│ └── CommentController.java
├── service
│ ├── UserService.java
│ ├── PostService.java
│ └── CommentService.java
├── mapper
│ ├── UserMapper.java
│ ├── PostMapper.java
│ └── CommentMapper.java
├── entity
│ ├── User.java
│ ├── Post.java
│ └── Comment.java
├── dto
│ ├── LoginDTO.java
│ ├── RegisterDTO.java
│ └── PostDTO.java
├── config
│ ├── JwtInterceptor.java
│ └── WebConfig.java
├── utils
│ └── JwtUtil.java
└── ForumApplication.java
pom.xml 核心依赖
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 配置文件 application.yml
java
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/forum?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto # 使用自增ID(也可使用雪花算法)
mapper-locations: classpath*:/mapper/**/*.xml
jwt:
secret: mySecretKeyForForumApp2024
expire: 604800000 # 7天
3. 实体类(使用 Lombok)
java
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String nickname;
private String avatar;
private String signature;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
4. JWT 工具类
java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire}")
private Long expire;
public String generateToken(Long userId, String username) {
return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("username", username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expire))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
public Long getUserIdFromToken(String token) {
return Long.parseLong(getClaimsFromToken(token).getSubject());
}
public boolean validateToken(String token) {
try {
getClaimsFromToken(token);
return true;
} catch (Exception e) {
return false;
}
}
}
5. 用户认证拦截器
java
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
throw new UnauthorizedException("缺少token");
}
token = token.substring(7);
if (!jwtUtil.validateToken(token)) {
throw new UnauthorizedException("token无效");
}
Long userId = jwtUtil.getUserIdFromToken(token);
request.setAttribute("userId", userId);
return true;
}
}
注册拦截器:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/**"); // 登录注册放行
}
}
6. 用户注册与登录
DTO
java
@Data
public class RegisterDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 4, max = 20)
private String username;
@NotBlank
@Size(min = 6, max = 20)
private String password;
}
Controller
java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(@Valid @RequestBody RegisterDTO dto) {
userService.register(dto);
return Result.success();
}
@PostMapping("/login")
public Result login(@Valid @RequestBody LoginDTO dto) {
String token = userService.login(dto);
return Result.success(token);
}
}
Service 关键实现
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private JwtUtil jwtUtil;
public void register(RegisterDTO dto) {
// 检查用户名是否已存在
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, dto.getUsername());
if (userMapper.selectCount(wrapper) > 0) {
throw new BusinessException("用户名已存在");
}
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt()));
user.setNickname(dto.getUsername()); // 默认昵称
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
userMapper.insert(user);
}
public String login(LoginDTO dto) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, dto.getUsername());
User user = userMapper.selectOne(wrapper);
if (user == null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) {
throw new BusinessException("用户名或密码错误");
}
if (user.getStatus() == 1) {
throw new BusinessException("账号已被禁用");
}
return jwtUtil.generateToken(user.getId(), user.getUsername());
}
}
7. 帖子模块(分页查询)
PostController
java
@RestController
@RequestMapping("/api/posts")
public class PostController {
@Autowired
private PostService postService;
@GetMapping
public Result getPosts(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String category) {
Page<Post> result = postService.listPosts(page, size, category);
return Result.success(result);
}
@PostMapping
public Result createPost(@RequestBody PostDTO postDTO, @RequestAttribute Long userId) {
Post post = postService.createPost(postDTO, userId);
return Result.success(post);
}
@GetMapping("/{id}")
public Result getPostDetail(@PathVariable Long id) {
PostDetailVO vo = postService.getPostDetail(id);
return Result.success(vo);
}
}
Service 实现
java
@Service
public class PostService {
@Autowired
private PostMapper postMapper;
@Autowired
private UserMapper userMapper;
public Page<Post> listPosts(Integer page, Integer size, String category) {
Page<Post> pageParam = new Page<>(page, size);
LambdaQueryWrapper<Post> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Post::getStatus, 0);
if (category != null && !category.isEmpty()) {
wrapper.eq(Post::getCategory, category);
}
wrapper.orderByDesc(Post::getCreateTime);
return postMapper.selectPage(pageParam, wrapper);
}
public Post createPost(PostDTO postDTO, Long userId) {
Post post = new Post();
post.setUserId(userId);
post.setTitle(postDTO.getTitle());
post.setContent(postDTO.getContent());
post.setCategory(postDTO.getCategory());
post.setCreateTime(LocalDateTime.now());
post.setUpdateTime(LocalDateTime.now());
post.setStatus(0);
postMapper.insert(post);
return post;
}
public PostDetailVO getPostDetail(Long id) {
Post post = postMapper.selectById(id);
if (post == null || post.getStatus() == 1) {
throw new BusinessException("帖子不存在");
}
// 增加浏览量(非精确计数,实际可使用Redis)
post.setViewCount(post.getViewCount() + 1);
postMapper.updateById(post);
User user = userMapper.selectById(post.getUserId());
PostDetailVO vo = new PostDetailVO();
BeanUtils.copyProperties(post, vo);
vo.setUsername(user.getUsername());
vo.setAvatar(user.getAvatar());
return vo;
}
}
8. 统一返回与异常处理
java
@Data
public class Result {
private Integer code;
private String msg;
private Object data;
public static Result success() {
return success(null);
}
public static Result success(Object data) {
Result result = new Result();
result.setCode(200);
result.setMsg("success");
result.setData(data);
return result;
}
public static Result error(String msg) {
Result result = new Result();
result.setCode(500);
result.setMsg(msg);
return result;
}
}
全局异常处理:
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
return Result.error(e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(","));
return Result.error(msg);
}
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
e.printStackTrace();
return Result.error("系统繁忙,请稍后再试");
}
}
五、前端开发(Vue 3)
1. 项目初始化
bash
npm create vite@latest forum-frontend -- --template vue
cd forum-frontend
npm install vue-router@4 pinia axios element-plus
2. 目录结构
text
src/
├── api/ # 接口封装
│ ├── auth.js
│ ├── post.js
│ └── request.js # axios 实例
├── router/
│ └── index.js
├── stores/ # Pinia
│ └── user.js
├── views/
│ ├── Login.vue
│ ├── Register.vue
│ ├── PostList.vue
│ ├── PostDetail.vue
│ └── CreatePost.vue
├── App.vue
└── main.js
3. axios 请求封装(request.js)
javascript
import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '../router';
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000
});
// 请求拦截器
request.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});
// 响应拦截器
request.interceptors.response.use(
response => {
const res = response.data;
if (res.code !== 200) {
ElMessage.error(res.msg || '请求失败');
if (res.code === 401) {
localStorage.removeItem('token');
router.push('/login');
}
return Promise.reject(res);
}
return res.data;
},
error => {
ElMessage.error(error.message || '网络错误');
return Promise.reject(error);
}
);
export default request;
4. 用户状态管理(Pinia)
javascript
// stores/user.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
import request from '@/api/request';
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '');
const userInfo = ref(null);
const login = async (username, password) => {
const res = await request.post('/auth/login', { username, password });
token.value = res;
localStorage.setItem('token', res);
await fetchUserInfo();
};
const logout = () => {
token.value = '';
userInfo.value = null;
localStorage.removeItem('token');
};
const fetchUserInfo = async () => {
// 可单独调用获取用户信息的接口
};
return { token, userInfo, login, logout, fetchUserInfo };
});
5. 登录页面(Login.vue)
javascript
<template>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="form.password" />
</el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
<router-link to="/register">没有账号?立即注册</router-link>
</el-form>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
const userStore = useUserStore();
const router = useRouter();
const formRef = ref();
const form = reactive({ username: '', password: '' });
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
};
const handleLogin = async () => {
await formRef.value.validate();
try {
await userStore.login(form.username, form.password);
ElMessage.success('登录成功');
router.push('/posts');
} catch (err) {
// 错误已在拦截器中处理
}
};
</script>
6. 帖子列表(PostList.vue)
javascript
<template>
<div>
<el-select v-model="category" placeholder="全部分类" @change="loadPosts">
<el-option label="技术" value="tech" />
<el-option label="生活" value="life" />
<el-option label="问答" value="qa" />
</el-select>
<el-table :data="posts" style="width: 100%">
<el-table-column prop="title" label="标题" />
<el-table-column prop="viewCount" label="浏览" width="80" />
<el-table-column prop="createTime" label="发布时间" width="180" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button link type="primary" @click="goDetail(row.id)">查看</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="page"
:page-size="size"
:total="total"
@current-change="loadPosts"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getPosts } from '@/api/post';
import { useRouter } from 'vue-router';
const router = useRouter();
const posts = ref([]);
const page = ref(1);
const size = ref(10);
const total = ref(0);
const category = ref('');
const loadPosts = async () => {
const res = await getPosts({ page: page.value, size: size.value, category: category.value });
posts.value = res.records;
total.value = res.total;
};
const goDetail = (id) => {
router.push(`/posts/${id}`);
};
onMounted(loadPosts);
</script>
7. 路由配置
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';
import PostList from '@/views/PostList.vue';
import PostDetail from '@/views/PostDetail.vue';
import CreatePost from '@/views/CreatePost.vue';
const routes = [
{ path: '/login', component: Login },
{ path: '/register', component: Register },
{ path: '/posts', component: PostList, meta: { requiresAuth: true } },
{ path: '/posts/:id', component: PostDetail, meta: { requiresAuth: true } },
{ path: '/create', component: CreatePost, meta: { requiresAuth: true } },
{ path: '/', redirect: '/posts' }
];
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.meta.requiresAuth && !token) {
next('/login');
} else {
next();
}
});
export default router;
六、部署方案(Docker Compose + Nginx)
1. 项目根目录创建 docker-compose.yml
java
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: forum-mysql
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: forum
volumes:
- ./mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化脚本
ports:
- "3306:3306"
networks:
- forum-net
backend:
build: ./backend
container_name: forum-backend
depends_on:
- mysql
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/forum?useSSL=false&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: 123456
ports:
- "8080:8080"
networks:
- forum-net
nginx:
image: nginx:latest
container_name: forum-nginx
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./frontend/dist:/usr/share/nginx/html
ports:
- "80:80"
- "443:443"
depends_on:
- backend
networks:
- forum-net
networks:
forum-net:
2. 后端 Dockerfile
java
FROM openjdk:17-jdk-slim
COPY target/forum-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
3. Nginx 配置(nginx.conf)
bash
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name your-domain.com;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
4. 构建与启动
bash
# 后端打包
cd backend
mvn clean package
# 前端构建
cd frontend
npm run build
# 回到项目根目录
cd ..
docker-compose up -d
5. 配置 HTTPS(可选)
-
在阿里云/腾讯云申请免费SSL证书,下载nginx格式。
-
将.pem和.key文件放到./ssl/目录。
-
修改nginx.conf,添加443端口监听。
想要快速搭建的同学也可以直接选择开源项目
一、开源论坛系统快速选型对比
| 项目名称 | 技术栈 | Star数 | 特点 | 推荐度 |
|---|---|---|---|---|
| OpenIsle | Spring Boot + Vue3 + JPA | 635 | 现代化设计,支持OAuth登录,功能完整,有详细的部署教程 | ⭐⭐⭐⭐⭐ |
| forum-java | Spring Boot + MyBatis + Thymeleaf | 1.7k+ | 现代化社区系统,功能完善,代码结构清晰 | ⭐⭐⭐⭐ |
| Symphony | Java + 自研框架 | 2.9k+ | 功能强大,但非Spring技术栈 | ⭐⭐⭐ |
| NiterForum | Spring Boot + MyBatis + Layui | 684 | 轻量级,适合学习,但前端较老 | ⭐⭐⭐ |
| jforum | Java + 自研MVC | 经典 | BSD协议,二次开发自由,但技术栈老旧 | ⭐⭐ |
二、首选推荐:OpenIsle
为什么推荐 OpenIsle?
-
技术栈完全匹配:Spring Boot + Vue3 + MySQL,
-
功能完整:用户管理、贴文发布、嵌套评论、点赞系统、关注功能、实时通知、全局搜索
-
登录方式丰富:JWT认证 + Google/GitHub/Discord/Twitter OAuth登录
-
文档齐全:有详细的中文部署教程和开发文档
-
开源协议友好:MIT协议,可商用可二次开发
功能模块一览
text
OpenIsle
├── 用户管理
│ ├── 注册/登录(JWT + OAuth)
│ ├── 个人资料编辑
│ ├── 成就与勋章体系
│ └── 关注/私信功能
├── 贴文管理
│ ├── 发布/编辑/删除
│ ├── 分类与标签
│ └── 草稿保存
├── 评论系统
│ ├── 嵌套评论
│ └── 点赞/抖弹系统
├── 通知系统
│ ├── 实时通知
│ └── 浏览器推送
├── 搜索功能
│ └── 全局搜索(用户+内容)
└── 其他
├── 图片上传(腾讯云COS)
└── Markdown编辑器
快速部署步骤
1. 克隆项目
bash
git clone https://github.com/nagisa77/OpenIsle.git
cd OpenIsle
2. 启动后端(Spring Boot)
bash
# 导入backend/文件夹到IDEA
# 创建MySQL数据库
CREATE DATABASE `openisle` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
# 修改配置文件 backend/src/main/resources/application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/openisle
spring.datasource.username=root
spring.datasource.password=你的密码
# 运行主启动类 OpenIsleApplication
3. 启动前端(Vue3 + Nuxt)
bash
cd frontend_nuxt/
# 配置环境变量
cp .env.example .env
# 修改 NUXT_PUBLIC_API_BASE_URL=http://localhost:8080
# 安装依赖并运行
npm install
npm run dev
4. 访问系统
后端API:http://localhost:8080
三、备选方案:forum-java
如果你更习惯 MyBatis 而非 JPA,这个项目可能更适合你。
- 项目特点
-
技术栈:Spring Boot + MyBatis + MySQL + Redis + Elasticsearch
-
功能:问答、论坛、社交网络、博客于一体
-
Star数:1.7k+,社区活跃
-
亮点:技术栈更接近你的经验,支持全文搜索
-
获取地址
GitHub搜索 Qbian61/forum-java 或 88250/symphony
四、选型建议与二次开发指南
如何选择?
| 如果你... | 推荐项目 |
|---|---|
| 想快速上手、技术栈完全匹配 | OpenIsle |
| 更熟悉 MyBatis,需要搜索功能 | forum-java |
| 只需要简单论坛功能 | NiterForum |
| 需要完全自由的二次开发(无框架限制) | jforum |
二次开发建议
拿到开源项目后,按以下步骤进行定制:
-
1.先跑起来:按照文档完成本地部署,确保前后端都能正常运行
-
2.理解核心流程:
-
调试登录注册流程,理解JWT的生成和验证
-
跟踪一个完整的发帖→评论流程
-
查看数据库表结构,理解数据流转
-
-
3.按需修改:
-
前端:修改主题色、Logo、页面布局
-
后端:添加你需要的业务字段和接口
-
权限:如果你需要管理员/普通用户的区分,可以参考我之前给出的权限设计,这两个项目都支持角色权限
-
-
4.部署上线:
-
参考之前给出的 Docker Compose 部署方案
-
配置 Nginx 反向代理和 HTTPS
-
后续会有实操保姆级教程,敬请期待!