【网站开发-java】

网站开发-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. 访问系统

前端地址:http://localhost:3000

后端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

后续会有实操保姆级教程,敬请期待!

相关推荐
我要成为嵌入式大佬1 小时前
正点原子MP157--问题详解--四(关于根文件系统驱动模块指令的注意事项)
linux·运维·服务器
武藤一雄1 小时前
C#:nameof 运算符全指南
开发语言·microsoft·c#·.net·.netcore
feng68_1 小时前
Redis架构实践
linux·运维·redis·架构·bootstrap
欧云服务器1 小时前
宝塔计划任务怎么自动删除多少个以外的文件?
linux·运维·服务器
wertyuytrewm1 小时前
Java面试——Java基础
java·jvm·面试
czlczl200209251 小时前
RAG实现思路流程
java·jvm
带娃的IT创业者1 小时前
WeClaw_40_系统监控与日志体系:多层次日志架构与Trace追踪
java·开发语言·python·架构·系统监控·日志系统·链路追踪
Y001112361 小时前
JDBC原理
java·开发语言·数据库·jdbc
XXOOXRT1 小时前
零基础掌握Linux常用命令
linux·运维·服务器
程序员侠客行2 小时前
Tomcat 从陌生到熟悉
java·tomcat·web