【JavaEE33-博客系统案例实战】从零开始撸一个博客系统(一):项目搭建 + 博客列表,让文章“晒”出来

老铁,前面咱们学了 Spring Boot、MyBatis、事务、AOP...... 理论知识堆了一箩筐,也该动手练练了。

别急,从本期开始,咱就手把手从 0 到 1 撸一个完整的博客系统

这个系统麻雀虽小五脏俱全:登录、发表、编辑、删除、详情、列表...... 能让你把之前学的 CRUD、事务、拦截器、JWT 令牌、统一异常处理等等统统用上。 案例里面藏着很多细节,以及很多干货,老铁们,咋们会分4篇左右文章介绍这个案例,不急不躁,咱们慢慢来。

这一篇咱先搞定 项目搭建 + 博客列表 ,让文章先"晒"出来。

放心,代码一行一行敲,道理一句一句讲,咱开始吧。🚀


写在前面:项目技术栈以及源码地址

  • 后端:Spring Boot + MyBatis-Plus + JWT + 拦截器 + 统一异常处理
  • 数据库:MySQL
  • 前端:HTML + CSS + jQuery + editor.md(Markdown编辑器)
  • 前端页面由 AI 辅助生成(AI前端更美观,开发效率更高)

项目源码已经放在 Gitee 上 ,老铁们可以边看边对照,或者直接拿过去改:

👉 博客系统完整源码仓库地址


一、项目长啥样?先看效果

咱们要做的博客系统一共 5 个页面:

页面 功能
blog_login.html 用户登录
blog_list.html 博客列表(所有人可见)
blog_detail.html 博客详情(支持 markdown 渲染)
blog_edit.html 发布新博客 / 编辑博客
blog_update.html 修改博客(实际和编辑页共用)

1、项目演示视频

博客系统演示

2、登录页面

3、未登录不允许访问其他页面

4、博客列表页(显示所有作者的博客)

5、博客详情页

6、博客发布页


二、准备工作:数据库 + 项目初始化

那咱们数据库设计的话,主要设计就是实体表和关系表,就是看这两种表。

你看我们登录页面它涉及到是一个用户表,那么用户表中它要有用户名和密码,那他还应该有哪些相应的字段呢?

我们点击提交完之后,进入我们的首页,在右侧它会有我们的头像和昵称以及Gitee地址。然后你可能还会说,右侧的列表中,我们还有文章和分类,对吧?那这两个你觉得应该放在用户表中吗?其实我们不应该放在用户表中,当你发布一篇文章,那么文章数以及分类,这两个数目会更新。而如果你放在用户表中,它的耦合度就会增高,也就是说这两个属性是由文章表中文章的个数和分类去影响的,所以我们应该进行一个隔离,进行解耦,不要把它放在用户表中。如果你把文章数和文章分类数放在用户表中,那么当你的文发布一篇文章,那么此时你就得去实时更新用户表,耦合度就增高了。

所以我们就看文章表,看文章表中有哪些字段:我们这个前端的页面可以知道,文章表应该有文章的标题,内容、日期,以及作者。

但是我们文章的个数和分类的个数存在哪儿呢?唉,我们不会存在文章表中,那怎么做呢?我们一般会单独的放一个表,不要跟用户表放在一起,要么就是你直接不存,然后到时候去查询数据库的时候去统计。

也就是说第一种实现方式:你就用单独存储在MySQL中,也可以存在Redis这些组件中。

第二种方式的话:你去实时的计算,每次你去统计文章表的时候,就去统计出它的数目,显示页面你就去查询一下,然后统计出来就行。

但是我们本案例中,我们不去具体的去实现它,那如果你实现,到时候也可以进行扩展。

那我们的文章表和用户表肯定是有关联关系的,也就是说我们的用户和文章表中作者肯定是有一个关系,而且他们是一对多的关系,我们把一的一方id放在多的一方,也就是说是隶属关系隶。一个作者可以发布多个文章,这些文章隶属于这个作者,然后我们把这个外键呢添在我们的多的一方的。


那基于上述的一个分析,我们的 sql数据库的编写基本可以完成了。
🐢提示:对于头像我们本案件,不去具体的实现,但是呢它有这个字段,用于后续的扩展,如果本案例实现的话,那就得是到时候上传一个头像,同时注意我们存储的时候是存储照片的url,而不是我们照片的实际内容。同时我们用户表和文章表中还存储一个delete flag,就是我们删除的话采用逻辑删除而不用物理删除。


2.1 建数据库和表

咱先搞一个数据库 java_blog_spring,建两张表:用户表 user_info 和博客表 blog_info

sql 复制代码
create database if not exists javaee_blog_spring charset utf8mb4;
use javaee_blog_spring;

-- 用户表
drop tables if exists user_info;
create table `user_info` (
  `id` int not null auto_increment comment '用户主键id',
  `user_name` varchar(128) not null comment '用户名',
  `password` varchar(128) not null comment '密码(加密存储)',
  `avatar_url` varchar(256) default null comment '头像url',
  `github_url` varchar(128) default null comment 'github地址',
  `delete_flag` tinyint(4) default 0 comment '逻辑删除标记:0-未删除,1-已删除',
  `create_time` datetime default current_timestamp comment '创建时间',
  `update_time` datetime default current_timestamp on update current_timestamp comment '更新时间',
  primary key (`id`),
  unique key `idx_user_name` (`user_name`)
) engine=innodb default charset=utf8mb4 comment='用户表';

-- 博客表
drop tables if exists blog_info;
create table `blog_info` (
  `id` int not null auto_increment comment '博客主键id',
  `title` varchar(200) not null comment '博客标题',
  `content` text not null comment '博客正文(markdown格式)',
  `user_id` int not null comment '作者id,关联user_info.id',
  `delete_flag` tinyint(4) default 0 comment '逻辑删除标记:0-未删除,1-已删除',
  `create_time` datetime default current_timestamp comment '创建时间',
  `update_time` datetime default current_timestamp on update current_timestamp comment '更新时间',
  primary key (`id`),
  key `idx_user_id` (`user_id`)
) engine=innodb default charset=utf8mb4 comment='博客文章表';

插几条测试数据,方便后面展示:

sql 复制代码
insert into user_info (user_name, password, github_url) values ('张三', '123456', 'https://gitee.com/xxx');
insert into user_info (user_name, password, github_url) values ('李四', '123456', 'https://gitee.com/xxx');

insert into `blog_info` (`title`, `content`, `user_id`) values
('第一篇博客', '这是用markdown写的正文内容,**支持加粗**,[链接](https://example.com)', 1),
('第二篇博客', '学习Spring Boot的总结笔记...', 2);

注意:这里密码先明文存,后面咱会用 MD5 + 盐 加密,别怕。


执行sql代码 创建出库和表




2.2 创建 Spring Boot 项目

用 IDEA 或者 Spring Initializr 创建一个项目,依赖选:


如果想要让lombok生效的话,那么需要删除下述插件:


然后手动在 pom.xml 里加入 MyBatis-Plus 依赖:

SpringBoot版本适配的mybatis-plus适配的版本

依赖:

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
    <version>3.5.15</version>
</dependency>

为啥用 MyBatis-Plus?因为它自带很多好用的 CRUD 方法,少写一堆 XML,咱把精力花在业务上。

2.3 配置 application.yml

yaml 复制代码
spring:
  application:
    name: spring-blog
  datasource:
    url: jdbc:mysql://localhost:3306/javaee_blog_spring?useSSL=false&characterEncoding=utf8
    username: root       # 你的数据库用户名
    password: 123456     # 你的数据库密码
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true   # 数据库下划线自动转驼峰
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl   # 打印 SQL
    
logging:
  file:
    name: logger/spring-blog.log # 持久化日志logging:
  pattern:
    console: "%clr(%d{HH:mm:ss}){yellow} %clr(%-5level) --- %msg%n" # 自定义日志格式

2.4 把前端静态页面拷进来

本案例我们核心目标还是去实现后端,那对于前端的话,我们让ai来帮我们完成,也就是我们通过ai把前端给实现了,而且前端的样式通过ai来润色之后呢,会更加的好看。

那前端的相关代码呢,老铁们可参考我的Gitee仓库并将其拷贝进来即可。

然后启动项目,访问 http://localhost:8080/blog_login.html,看到页面就说明环境好了。


2.5 项目的架构


三、公共模块:统一返回 + 统一异常处理(老套路)

3.1 统一返回结果的格式 Result

java 复制代码
@Data
public class Result {
    //统一返回结果三大常见信息:状态码+错误信息+具体数据
    private ResultCodeEnum code; //业务状态码(不是http的) 可以给他定义枚举类
    private String errMsg;//错误类型
    private Object data;//具体数据

    /**
     * 成功
     * @param data 成功的时候的数据
     * @return 返回统一封装之后的数据
     */
    public static Result  success(Object data) {
        Result result = new Result();
        result.setCode(ResultCodeEnum.SUCCESS);
        result.setData(data);
        return result;
    }

    /**
     * 失败
     * @param errMsg 失败的原因
     * @return 返回统一封装之后的数据
     */
    public static Result fail(String errMsg) {
        Result result = new Result();
        result.setCode(ResultCodeEnum.FAIL);
        result.setErrMsg(errMsg);
        return result;
    }
}

//业务状态码封装为枚举
@AllArgsConstructor //生成构造方法
public enum ResultCodeEnum {
    //成功
    SUCCESS(200),
    FAIL(-1);//失败

    @Setter @Getter //给这个属性生成set和get方法
    private int code;

}

3.2 统一结果的返回增强版(ResponseAdvice

我们这个类放在这个包中,如图:

为了让所有 Controller 返回的数据自动包上 Result 结构,咱还写一个 @ControllerAdvice,和图书系统一样的:目的就是做一层补充兜底,对你没有手动包上Result的,我Spring帮你自动包上,此时还有一个最大的好处就是,你不需要在我们的Controller 层去自己包装了,我们在此处统一的包装,如下图:

此处我们在之前的统一返回格式中介绍过。

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    //用于序列化
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;//是否支持统一结果:是
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {
        //对统一结果进行封装
        if (body instanceof String) {
            objectMapper.writeValueAsString(Result.success(body));//将Result.success(body)进行转变为JSON字符串
        } else if (body instanceof Result) {
            //如果你已经直接是Result的话 那么就直接返回这个Result对象即可
            return body;
        }
        //其他类型就使用统一返回格式类进行一个包装
        return Result.success(body);
    }
}

再次强调逻辑 如图:


3.3 自定义异常 + 统一异常处理

自定义异常 :我们将他写咋exception包下

java 复制代码
@Data
public class BlogException extends RuntimeException{//继承运行时异常就行了
    //一个异常我们可能需要 状态码+异常信息
    private int code;
    private String errMsg;

    //构造方法

    public BlogException(String message, int code, String errMsg) {
        super(message);
        this.code = code;
        this.errMsg = errMsg;
    }
}

自定义完异常之后,我们进行统一的异常处理:也是使用@ControllerAdvice注解,我们将他写在如下包下:

java 复制代码
@Slf4j//使用日志 方便观察
@ResponseBody//返回的是数据 不是页面
@ControllerAdvice//告诉Spring让他为我们做统一异常处理
public class ExceptionAdvice  {

    /**
     * 处理异常的时候
     * @param exception 异常对象
     * @return 处理完异常之后 我们返回给前端的也是需要使用统一返回类型Result的
     */
    @ExceptionHandler//异常处理类型 默认捕获的就是参数中Exception这个异常
    public Result exceptionHandler(Exception exception) {
        log.error("发生异常:,e: ", exception);//先打印一个日志看一下

        //将处理之后的异常信息返回前端 告诉前端出错了{至于前端怎么展示给用户 就是另一码事情了}
        return Result.fail(exception.getMessage());
    }

    //统一处理我们的自定义异常
    @ExceptionHandler
    public Result exceptionHandler(BlogException exception) {
        log.error("发生异常:,e: ", exception);//先打印一个日志看一下

        //将处理之后的异常信息返回前端 告诉前端出错了{至于前端怎么展示给用户 就是另一码事情了}
        return Result.fail(exception.getMessage());
    }
}

这样,不管业务成功还是失败,前端拿到的永远是 {code, errMsg, data} 格式,如此甚好也。

当然,如果老铁们看到这里,对统一返回格式、统一返回结果、统一异常处理这几个概念还有点模糊,别慌,咱之前专门写过一篇博客讲这个。点下面的链接去回顾一下,看完就门清了👇
统一结果返回+统一异常处理


四、持久层:用 MyBatis-Plus 少写代码

4.1 实体类


代码:

java 复制代码
@Data//自动生成get和set方法
public class BlogInfo {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String title;
    private String content;
    private Integer userId;
    private Integer deleteFlag;
    // 时间格式化:从数据库读取后,对数据库中的时间进行格式化
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    
    // 时间格式化:从数据库读取后,对数据库中的时间进行格式化
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
}

@Data//自动生成get和set方法
public class UserInfo {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;//用户编号
    private String userName;//用户名
    private String password;//密码
    private String githubUrl;//码云地址
    private Byte deleteFlag;//删除标记

    // 时间格式化:从数据库读取后,对数据库中的时间进行格式化
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;//创建时间

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;//修改时间
}

4.2 Mapper 接口

java 复制代码
@Mapper
public interface BlogInfoMapper extends BaseMapper<BlogInfo> {
}

@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}

就这两行,MyBatis-Plus 自动提供了 insertselectListselectByIdupdateById 等方法,咱连 SQL 都不用写。


五、实现博客列表(让文章晒出来)

5.1 约定接口

前端给后端发一个 GET /blog/getList,后端返回所有未删除的博客列表,格式如下:

json 复制代码
{
  "code": 200,
  "errMsg": null,
  "data": [
    {
      "id": 1,
      "title": "第一篇博客",
      "content": "这是用markdown写的正文内容,**支持加粗**,[链接](https://example.com)",
      "userId": 1,
      "createTime": "2024-08-22 11:27:03"
    }
  ]
}

5.2 Service 层

代码初步版本:

java 复制代码
//接口
public interface BlogService {
    List<BlogInfo> getList();
}


//实现类
@Service
public class BlogServiceImpl implements BlogService {
    //调用持久层
    @Autowired
    private BlogInfoMapper blogInfoMapper;

    @Override
    public List<BlogInfo> getList() {
        /**
         * 查询的时候 由于数据库中有一个字段:就是我们的删除标记delete_flag
         * 那么0表示没有删除 如果是1就表示删除
         * 所以我们是有条件的我们只能查询出非1的数据 所以此时就需要条件构造器了
         */
        //创建条件构造器
        QueryWrapper<BlogInfo> queryWrapper = new QueryWrapper<>();
        //我们使用拉姆达的方式
        queryWrapper.lambda().eq(BlogInfo::getDeleteFlag, 0);

        List<BlogInfo> blogInfos = blogInfoMapper.selectList(queryWrapper);

        return blogInfos;
    }
}

服务层接口和服务层实现如下所示:

为什么需要这样设计?为了解耦,让控制层调用服务层接口,面向接口编程。


服务层中

@Service注解不加载接口上 要加载实现类上



5.3 Controller 层

初步代码:

java 复制代码
@Slf4j//方便看日志
@RequestMapping("/blog")
@RestController
public class BlogController {

    @Resource(name = "blogServiceImpl")//指定你要注入哪一个
    private BlogService blogService;

    /**
     * 获取博客列表
     * @return 博客列表-集合
     */
    @RequestMapping("/getList")
    public List<BlogInfo> getList() {
        log.info("获取博客列表>>>");
        return blogService.getList();
    }
}

控制层写在如下包下:


解释补充:

因为咱有 ResponseAdvice,返回的 List 会被自动包装成 Result,前端拿到的就是带 code 的标准结构。


注入服务层解释:


5.4 测试并思考

于是我们就需要vo将前端需要的返回,所以请继续往下阅读


5.5 后端:定义返回数据的 VO

vo我们弄在下图包中:


VO:是视图用到的对象

咱不用直接把 BlogInfo 整个吐出去,可以定义一个 BlogInfoResponse,只挑需要的字段,顺便格式化日期。

java 复制代码
@Data
public class BlogInfoResponse {
    private Integer id;//文章id
    private String title;//文章标题
    private String content;//文章内容
    private Integer userId;//作者
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;//创建时间(发布时间)
}

5.6 最终版本控制层和服务层代码


🤔服务层持久层给我们的对象如何变为VO?


代码:

控制层

java 复制代码
@Slf4j//方便看日志
@RequestMapping("/blog")
@RestController
public class BlogController {

    @Resource(name = "blogServiceImpl")//指定你要注入哪一个
    private BlogService blogService;

    /**
     * 获取博客列表
     * @return 博客列表-集合
     */
    @RequestMapping("/getList")
    public List<BlogInfoResponse> getList() {
        log.info("获取博客列表>>>");
        return blogService.getList();
    }
}

服务层

java 复制代码
//接口
public interface BlogService {
    List<BlogInfoResponse> getList();
}
//实现类
@Service
public class BlogServiceImpl implements BlogService {
    //调用持久层
    @Autowired
    private BlogInfoMapper blogInfoMapper;

    @Override
    public List<BlogInfoResponse> getList() {
        /**
         * 查询的时候 由于数据库中有一个字段:就是我们的删除标记delete_flag
         * 那么0表示没有删除 如果是1就表示删除
         * 所以我们是有条件的我们只能查询出非1的数据 所以此时就需要条件构造器了
         */
        //创建条件构造器
        QueryWrapper<BlogInfo> queryWrapper = new QueryWrapper<>();
        //我们使用拉姆达的方式
        queryWrapper.lambda().eq(BlogInfo::getDeleteFlag, 0);

        List<BlogInfo> blogInfos = blogInfoMapper.selectList(queryWrapper);

        List<BlogInfoResponse> blogInfoResponses = new ArrayList<>();
        //直接遍历 blogInfos 将需要的属性搞到我们的 VO中 即可
        for (BlogInfo blogInfo : blogInfos) {
            BlogInfoResponse blogInfoResponse = new BlogInfoResponse();
            blogInfoResponse.setId(blogInfo.getId());
            blogInfoResponse.setTitle(blogInfo.getTitle());
            blogInfoResponse.setContent(blogInfo.getContent());
            blogInfoResponse.setCreateTime(blogInfo.getCreateTime());
            blogInfoResponse.setUserId(blogInfo.getUserId());

            //加入到集合中
            blogInfoResponses.add(blogInfoResponse);
        }

        //最后返回VO组成的集合

        return blogInfoResponses;
    }
}

此时再次测试前端:符合我们的需求


只不过还有一个细节就是:我们前端展示的是年月日 没有时分秒

那么 此时 我们有几个处理方法

1、改变类型

2、改变日期格式化(我使用的是这种)


3、直接使用一个工具类来转换

代码:

java 复制代码
//工具类
public class DateUtils {
    /**
     * 日期格式化工具
     * @param date 时间
     * @return 返回格式化之后的日期
     */
    public static String dateFormat(Date date) {
        //日期格式处理类
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyy-MM-dd");
        return dateFormat.format(date);
    }
}

//使用
@Data
public class BlogInfoResponse {
    private Integer id;//文章id
    private String title;//文章标题
    private String content;//文章内容
    private Integer userId;//作者

//    @JsonFormat(pattern = "yyyy-MM-dd") 不使用这种方式
    private Date createTime;//创建时间(发布时间) 注意要使用Date类型才可以

    //重写get方法 因为返回的时候其实执行的是get方法

    public String getCreateTime() {
        return DateUtils.dateFormat(createTime);
    }
}

此时运行结果:


5.7 前端:动态渲染博客列表

打开 blog_list.html,咱用 AJAX 动态生成。

javascript 复制代码
$(function () {
    $.ajax({
        type: "get",
        url: "/blog/getList",
        success: function (result) {
            if (result.code == 200 && result.data != null && result.data.length > 0) {
                var finalHtml = "";
                for (var blog of result.data) {
                    finalHtml += '<div class="blog">';
                    finalHtml += '<div class="title">' + blog.title + '</div>';
                    finalHtml += '<div class="date">' + blog.updateTime + '</div>';
                    finalHtml += '<div class="desc">' + blog.content + '</div>';
                    finalHtml += '<a class="detail" href="blog_detail.html?blogId=' + blog.id + '">查看全文&gt;&gt;</a>';
                    finalHtml += '</div>';
                }
                $(".right").html(finalHtml);
            }
        }
    });
});



处理博客内容:

我们一篇博客的内容是很多的,你不可能全部去展示出来,而是要去截取它。那怎么截断呢?

首先我们这个content文本内容是从数据库中读取出来的,从数据库中读取出来的话,我们用的是set的方法,把数据set到我们的Java内存对象中,然后我们把Java内存对象中的数据拿到前端使用的是get方法,所以我们这里只需要重写一个get方法就行了:如下

当然我们还可以通过前端去获取,就是后端直接返回全部内容,然后前端自己去写逻辑截取,不过这个啊我们使用的是上面这种方式,这个方式老铁们可自行选择。

Ok,然后此时的话我们就来运行一下结果分析会报错:

后端错误日志

所以我们需要注意,截取的时候,你不是所有的文本你都截取,因为有些文章内容字数小于50,那此时的话你去截取就会报错,所以呢我们就取一个最小值。你字数小于50我就只全部返回,如果你字数大于50我就返回50个字数。


现在访问 http://localhost:8080/blog_list.html,就能看到从数据库动态加载的博客列表了。


5.8 控制层和服务层的调用关系



六、小结 + 下期预告

今天咱干了这几件事:

  1. 建库建表,初始化数据。
  2. 创建 Spring Boot 项目,集成 MyBatis-Plus。
  3. 搭建公共模块(Result、统一返回、统一异常)。
  4. 编写实体、Mapper,用 MyBatis-Plus 少写代码。
  5. 实现博客列表接口,前端动态渲染。

现在博客能"晒"出来了,但还有好多功能没做:用户登录、JWT 令牌、博客详情、发布、编辑、删除、拦截器、加密......

整理这篇我花了不少心思,排版、代码、截图、解释......生怕哪个地方没说清楚。

老铁如果你觉得有用,点赞、收藏、关注 走一波,这是对我最大的鼓励,咱下期见!🚀


下期干货预警 !咱们重点搞定 登录 + JWT 令牌 ,再顺手加上强制登录拦截器,给系统配个严实的"门卫"。内容满到要溢出来,老铁们,戳这里 👉 进入下一篇:登录+JWT令牌~

相关推荐
弹简特4 小时前
【JavaEE34-博客系统案例实战】从零开始撸一个博客系统(二):登录 + JWT令牌 + 强制登录,让系统先有“门卫”
jwt·博客系统·java实战
独断万古他化2 个月前
【SSM开发实战:博客系统】(三)核心业务功能开发与安全加密实现
spring boot·spring·mybatis·博客系统·加密
独断万古他化2 个月前
【SSM开发实战:博客系统】(二)JWT 登录流程、拦截器实现和用户信息接口落地
spring boot·spring·mybatis·博客系统·项目
独断万古他化2 个月前
【SSM开发实战:博客系统】(一)项目初始化与基础功能实现
spring boot·spring·mybatis·博客系统·项目
时光追逐者3 个月前
一个基于 .NET 8 开源免费、高性能、低占用的博客系统
c#·.net·博客系统
梅花145 个月前
基于Django的博客系统
后端·python·django·毕业设计·博客·博客系统·毕设
方才coding1 年前
2024最新的开源博客系统:vue3.x+SpringBoot 3.x 前后端分离
spring boot·后端·开源·博客系统·前后端分离·个人博客·vue 3.x
皮不卡球秋1 年前
Servlet实现博客系统
java·servlet·博客系统·javase·javaee
IT学长编程1 年前
计算机毕业设计 基于Flask+vue的博客系统的设计与实现 Python毕业设计 Python毕业设计选题 Flask框架 Vue【附源码+安装调试】
后端·python·flask·毕业设计·博客系统·毕业论文·计算机毕业设计选题