博客系统(前后端分离版)

news2024/11/15 8:59:09

博客系统的具体实现

文章目录

  • 博客系统的具体实现
    • 软件开发的基本流程
    • 具体实现的八大功能
    • 数据库设计
    • 创建数据库
    • 操作数据库
      • 引入依赖
      • 封装DataSource
      • 创建实体类
      • 将JDBC增删改查封装起来
    • 实现博客列表页
      • web.xml的配置文件
    • 实现博客系统的展示功能
    • 登录功能
    • 强制要求用户登录
    • 显示用户信息
    • 退出登录状态
    • 发布博客
    • 删除博客
  • 在Linux上部署博客系统
    • 安装jdk
    • 安装tomcat
    • 安装mysql
      • 使用yum安装必要的包
      • 启动
    • 正式部署

在正式写后端程序之前,我已经将博客系统的前端最基本的页面写好,详情可以见我的gitee

https://gitee.com/dengchuanfei/vscode_demo/tree/master/blog_system

软件开发的基本流程

  • 可行性分析
  • 需求分析
  • 概要设计
  • 详细设计
  • 编码
  • 测试
  • 发布

具体实现的八大功能

  • 实现博客列表的展示功能
  • 实现博客详情的展示功能
  • 登录功能
  • 强制用户登录
  • 显示用户的信息
  • 实现注销
  • 发布博客
  • 删除博客

数据库设计

写之前首先要进行规划,要做到“谋定而后动”

首先进行“数据库设计”,也就是想清楚需要几个库,几张表,每个表长啥样(属性是干什么的,是什么类型)

需要找到实体,然后分析实体之间的关联关系,再思考表的属性

这里业务比较简单,所以只需要两张表

博客表blog (blogId, tittle, content, postTime, userId)

用户表user (userId username password)

创建数据库

先建一个数据库,往里面添加数据

-- 拿到的数据库很可能并不干净,所以创建之前要确保之前没有同名的,还要删除同名的,注意:这是十分危险的操作,所以务必谨慎使用!!!
create database if not exists java_blog_system;
use java_blog_system;
drop table if exists blog;
-- 注意这里添加属性是使用()
create table blog(
   blogId int primary key auto_increment,
    title varchar(256),
    content text,
    postTime datetime,
    --userId是文章作者的ID
    userId int
 );

drop table if exists user;
create table  user
(
    userId   int primary key auto_increment,
    username varchar(50),
    password varchar(50)
);
-- 添加几个数据,测试一下
insert into blog values(null,"这是第一篇博客","从今天开始我要好好写代码,好好上课",now(),1);
insert into blog values(null,"这是第二篇博客","我要好好写代码,好好上课",now(),1);

insert into user values(null, "zhangsan","123");
insert into user values(null, "lisi","123");

这里设计的content是text类型的,text能放64KB的内容,一般是够博客使用的了,博客中的截图和博客文字不是存储在一起的,所以不用担心截图放不下

注意: 在SQL中的注释是 “-- ”,在–后面还有一个空格!

操作数据库

引入依赖

首先要先引入maven依赖,在.xml的中引入依赖

去中央仓库中,搜索servlet API(3.1.0),mysql connect Java(5.1.49), jackson Databind(2.13.4.2)(将JSON格式进行转换)

封装DataSource

由于DataSource只有一份,所以使用单例模式来实现会比较好

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import javax.xml.crypto.Data;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

//使用这个类来封装DataSource,使用单例模式(懒汉模式 + 多线程判断)
//实现数据库的连接和断开
public class DBUtil {
    private static volatile  DataSource dataSource = null;
    public static DataSource getDataSource() {
        //第一次判断是否需要加锁
        if (dataSource == null) {
            synchronized (DBUtil.class) { //针对类对象加锁
                //第二次判断是会否需要new对象
                if (dataSource == null) {
                    dataSource  = new MysqlDataSource();
                    ((MysqlDataSource)dataSource).setURL("jdbc:mysql://127.0.0.1:3306/java_blog_system?characterEncoding=utf8&&useSSL=false");
                    ((MysqlDataSource)dataSource).setUser("root");
                    ((MysqlDataSource)dataSource).setPassword("1111");
                }
            }
        }
        return dataSource;
    }

    //建立连接
    private  static Connection getConnection() throws SQLException {
        return getDataSource().getConnection();
    }

    //关闭连接
    //建立连接的顺序是connection statement resultSet,所以关闭的顺序是反着的
    private static void close(Connection connection, Statement statement, ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

}

image-20230219120213497

在关闭资源的时候, 要是像这样直接使用依次try catch就会导致一旦上面的抛出异常,下面的就不会被执行到了,此时就会导致资源泄漏,很严重

要是使用的是throws相当于上面的情况,后面的代码还是不会执行,导致资源泄漏

所以还是应该 细分一下,多用几个try catch,来保证3个对象都关闭了

创建实体类

一个实体类对象就对应表中的一条记录

表中的属性怎么写,实体类就这么写

这里需要创建2个实体类 User 和 Blog

里面需要有属性和getter和setter方法, IDEA快捷键是alt + fn + insert

import java.security.Timestamp;

public class Blog {
    private int blogId;
    private String tittle;
    private String content;
    //mysql中的datetime和timestamp类型在java中都是使用Timestamp表示的
    private Timestamp postTime;
    private int userId;

    public int getBlogId() {
        return blogId;
    }

    public void setBlogId(int blogId) {
        this.blogId = blogId;
    }

    public String getTittle() { 
        return tittle;
    }

    public void setTittle(String tittle) {
        this.tittle = tittle;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Timestamp getPostTime() {
        return postTime;
    }

    public void setPostTime(Timestamp postTime) {
        this.postTime = postTime;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }
}
public class User {
    private int userId;
    private String username;
    private String password;

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

将JDBC增删改查封装起来

​ 这里创建的是BlogDao和UserDao类,这里的DAO是 Data Acess Object 数据访问对象

也就是说访问数据库的操作就可以使用这几个DAO对象来进行

import java.sql.*;
import java.util.ArrayList;
import java.util.List;
//封装关于博客的相关操作
public class BlogDao {
    //插入博客--发布博客
    public void insert(Blog blog) {
        Connection connection = null;
        PreparedStatement statement = null;
        //此处只要判断改变的行数是不是1就行了,所以没有resultSet
        try {
            //1.建立连接
            connection = DBUtil.getConnection();
            //2.构造SQL
            //sql对应着blog的属性
            //blogId tittle content postTime userId
            String sql = "insert into blog values (null, ? , ?, now(), ?)";
            statement = connection.prepareStatement(sql);
            statement.setString(1, blog.getTittle());
            statement.setString(2, blog.getContent());
            statement.setInt(3, blog.getUserId());
            //3.执行sql
            int ret = statement.executeUpdate();//executeUpdate的返回值是修改的行数
            if (ret != 1) {
                System.out.println("博客插入失败!");
            } else {
                System.out.println("博客插入成功!");
            }
            //4.释放相关的资源--但是这里还是不适合,要是上面代码抛异常了,这里就会导致资源没有释放,资源泄露
        } catch (SQLException e) {
            e.printStackTrace();
        } finally{
            //方法哦finally就一定会执行到了,但是connection和statement是局部变量,所以就将这两个放到最外面,先置为null
            DBUtil.close(connection, statement, null);//这里没有涉及到resultSet,所以填null
        }
    }

    //查询一个博客--博客详情页
    public Blog selectOne(int blogId) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            //1.建立连接
            connection = DBUtil.getConnection();
            //2.构造SQL
            String sql = "select * from blog where blogId = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, blogId);
            //3.执行SQL
            resultSet = statement.executeQuery();
            //遍历结果集合
            if (resultSet.next()) {
                Blog blog = new Blog();
                blog.setBlogId(resultSet.getInt("blogId"));
                blog.setTittle(resultSet.getString("tittle"));
                blog.setContent(resultSet.getString("content"));
                blog.setPostTime(resultSet.getTimestamp("postTime"));
                blog.setUserId(resultSet.getInt("userId"));
                return blog;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            //关闭资源
            DBUtil.close(connection,statement,resultSet);
        }
        return null;//要是没有找到直接返回null就行
    }

    //查询所有博客--博客展示页
    public List<Blog> selectAll() {
        List<Blog> blogs = new ArrayList<>();
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            //1.建立连接
            connection = DBUtil.getConnection();
            //2.构造SQL
            String sql = "select * from blog";
            statement = connection.prepareStatement(sql);
            //3.执行SQL
            resultSet = statement.executeQuery();
            //遍历结果集合,这里使用的是while循环来寻找
            while(resultSet.next()) {
                Blog blog = new Blog();
                blog.setBlogId(resultSet.getInt("blogId"));
                blog.setTittle(resultSet.getString("tittle"));
                blog.setContent(resultSet.getString("content"));
                blog.setPostTime(resultSet.getTimestamp("postTime"));
                blog.setUserId(resultSet.getInt("userId"));
                blogs.add(blog);//将所有搜到的blog都添加到blogs中
            } 
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            //4.关闭资源
            DBUtil.close(connection,statement,resultSet);
        }
        return blogs;
    }

    //删除博客
    public void delete(int blogId) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            //1.建立连接
            connection = DBUtil.getConnection();
            //2.构造SQL
            String sql = "delete from blog where blogId = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, blogId);
            //3.执行sql
            int ret = statement.executeUpdate();//executeUpdate的返回值是修改的行数
            if (ret != 1) {
                System.out.println("博客删除失败!");
            } else {
                System.out.println("博客删除成功!");
            }
            //4.释放相关的资源--但是这里还是不适合,要是上面代码抛异常了,这里就会导致资源没有释放,资源泄露
        } catch (SQLException e) {
            e.printStackTrace();
        } finally{
            //方法哦finally就一定会执行到了,但是connection和statement是局部变量,所以就将这两个放到最外面,先置为null
            DBUtil.close(connection, statement, null);//这里没有涉及到resultSet,所以填null
        }
    }
}
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UserDao {
    //根据用户名来查询用户--登录模块
	//隐含条件:用户名必须要是唯一的
    public User selectByName(String username) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            //1.建立连接
            connection = DBUtil.getConnection();
            //2.构造SQL
            String sql = "select * from blog where username = ?";
            statement = connection.prepareStatement(sql);
            statement.setString(1, username);
            //3.执行SQL
            resultSet = statement.executeQuery();
            //遍历结果集合
            if (resultSet.next()) {
                User user = new User();
                user.setUserId(resultSet.getInt("userId"));
                user.setUsername(resultSet.getString("username"));
                user.setPassword(resultSet.getString("password"));
                return user;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            //关闭资源
            DBUtil.close(connection,statement,resultSet);
        }
        return null;
    }


    //根据用户ID来查询用户--在获取用户信息的时候会用到
    public User selectById(int userId) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            //1.建立连接
            connection = DBUtil.getConnection();
            //2.构造SQL
            String sql = "select * from blog where userId = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, userId);
            //3.执行
            resultSet =  statement.executeQuery();
            if (resultSet.next()) {
                User user = new User();
                user.setUserId(resultSet.getInt("userId"));
                user.setUsername(resultSet.getString("username"));
                user.setPassword(resultSet.getString("password"));
                return user;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            //4.关闭资源
            DBUtil.close(connection,statement,resultSet);
        }
        return null;
    }
}

将上面的几个函数的实现都看明白,其实JDBC的操作都是差不多的

下面主要是服务端和客户端的代码实现,由于服务端的代码比较长,就只有贴出核心功能的客户端代码

详细的客户端 服务端代码将会在文章最后给出

实现博客列表页

将之前写的前端代码复制到webapp目录下

image-20230219161534473

web.xml的配置文件

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
    <display-name>Archetype Created Web Application</display-name>
</web-app>

实现博客展示,是从数据库中读取数据,然后写到前端网页上

在博客列表页,需要做一个很重要的事情,页面在加载的时候通过ajax发起HTTP请求,从服务端获取到博客列表的数据

所以需要实现想好发什么样的请求,返回什么样的响---->约定前后端接口

[请求]
GET /blog

[响应]
[
    {
        blogId: 1,
        title: "第一篇博客",
        content: "博客正文",
        userId: 1,
        postTime: "2021-07-07 12:00:00"
    },
    {
        blogId: 2,
        title: "第二篇博客",
        content: "博客正文",
        userId: 1,
        postTime: "2021-07-07 12:10:00"
    },
    ...
]

由页面发起请求,后服务端进行响应

import com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@WebServlet("/blog")
public class BlogServlet extends HttpServlet {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        BlogDao blogDao = new BlogDao();
        List<Blog> blogs = blogDao.selectAll();
        resp.setContentType("application/json;charset=utf8");
        resp.getWriter().write(objectMapper.writeValueAsString(blogs));
    }
}

在之前已经写过blog_list.html页面上进行修改


<!-- 发送ajax从服务端上获取数据 -->
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script>
    //在页面加载的时候,通过ajax给服务端发送消息,获取到博客列表信息,并且显示在页面上
    function getBlog(){
        $.ajax({
            type:'get',
            url:'blog',
            success:function(body){
                //获取到的body就是一个js对象数组,每个元素就是一个js对象,根据这个对象来构造一个div
                //1.把原来.right里面原有的内容清空,替换成从数据库服务端拿到的
                let rightDiv=document.querySelector('.container-right');
                rightDiv.innerHTML='';//清空原有数据
                //2.遍历body,构造出一个个blogDiv
                for(let blog of body){
                    let blogDiv=document.createElement('div');
                    //针对blogDiv设置一个属性,类名设为blog
                    blogDiv.className='blog';
                    //构造内部元素
                    //构造标题
                    let titleDiv=document.createElement('div');
                    titleDiv.className='title';
                    titleDiv.innerHTML=blog.title;
                    //作为子元素添加进去
                    blogDiv.appendChild(titleDiv);
                    //构造发布时间
                    let dateDiv=document.createElement('div');
                    dateDiv.className='date';
                    dateDiv.innerHTML=blog.postTime;
                    blogDiv.appendChild(dateDiv);
                    //构造摘要
                    let descDiv=document.createElement('div');
                    descDiv.className='desc';
                    descDiv.innerHTML=blog.content;
                    blogDiv.appendChild(descDiv);
                    //链接 查看全文(这里用的是a标签)
                    let a=document.createElement('a');
                    a.innerHTML='查看全文 &gt;&gt;';
                    //希望点击后能跳转到博客详情页
                    //跳转要告知哪个博客的详情页
                    a.href='blog_detail.html?blogId='+blog.blogId;
                    blogDiv.appendChild(a);
                    //把blogDiv挂到dom树上
                    rightDiv.appendChild(blogDiv);
                }
            },
            error:function(){
                alert("获取博客列表失败");
            }
        });
    }
    getBlog();
</script>

image-20230220165053186

这里的时间很明显就是一个时间戳,并不直观,所以还是要改的,通过fiddler可以看到返回的响应就是时间戳的格式,所以也就是要将get方法的返回值变成String类型的时间格式

此处就要使用SimpleDateFormat类了

public String getPostTime() {
        //使用IDEA提供的原生的,返回的是时间戳,所以需要改一下返回值
        //使用SimpleDateFormat来将时间戳转换成指定的时间格式
        //这里的参数标准格式建议查一下,因为在不同的语言中表示放方式是不一样的
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return simpleDateFormat.format(postTime);
    }

image-20230220170449431

时间的格式确实改好了,但是发现最上面的博客确实比较老的博客,不符合常规的博客展示思路

此时只要在BlogDao中将selectAll的搜索改成select * from blog order by postTime desc 按时间降序排列就行了

要是正文很长,在博客展示页就应该显示一部分文章,此时就对内容进行截断

在BlogDao 中进行content的判断和截断:

String content = resultSet.getString("content");
if(content.length() > 100){
    content = content.substring(0,100) + "......";
}
blog.setContent(content);

实现博客系统的展示功能

在展示的时候,发送ajax请求来访问服务端,获取到服务端返回的响应之后,填充到博客的详情页面中

@WebServlet("/blog")
public class BlogServlet extends HttpServlet {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf8");
        BlogDao blogDao = new BlogDao();
        String  blogId = req.getParameter("blogId");
        if(blogId == null){
            
            //说明是博客列表页发起的请求
            List<Blog> blogs = blogDao.selectAll();
            resp.getWriter().write(objectMapper.writeValueAsString(blogs));
        }else{
            //说明是博客详情页发起的请求
            Blog blog = blogDao.selectOne(Integer.parseInt(blogId));
            resp.getWriter().write(objectMapper.writeValueAsString(blog));
        }
    }
}

客户端核心代码:

function getBlog(){
            $.ajax({
                type:'get',
                url:'blog' + location.search,
                success : function(body){
                    let h3 =  document.querySelector('.blog-detail>h3');
                    h3.innerHTML = body.title;

                    let dateDiv = document.querySelector('.blog-detail>.blog-date');
                    dateDiv.innerHTML = body.postTime; 
                     
                    //方法1:
                     //let contentDiv=  document.querySelector('#content');
                     //contentDiv.innerHTML = body.content;
					
                    //方法2:
                    //此处使用editor.md来进行渲染,主要是后面实现博客编辑的时候使用
                    editormd.markdownToHTML('content', {markdown: body.content}); 
                }
            });
        }
        getBlog();

关于前端代码:

  1. 要是想显示具体的哪篇博客,就要知道具体的博客id,所以这里写URL的时候将location.search添加上去了
  2. 为什么这里要使用方法2,而不是方法1,主要是因为博客是以markdown的形式来写的,所以渲染的时候也要以markdown格式来渲染

登录功能

所谓的登录功能就是在登录之后跳转到博客列表页

服务端代码:

其实登录操作就是先根据请求获取到用户输入的账号密码,之后进行判断,看看用户输入的账号密码是不是符合要求,要是符合的话,再查询数据库,要是账号密码都是正确的,就创建出一个会话,重定向到博客列表页

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.获取用户名和密码
        req.setCharacterEncoding("utf-8");
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        if (username == null || username.equals("") || password == null || password.equals("")) {
            //用户名或者密码不存在(为空)
            resp.setContentType("text/html;charset= utf8");
            resp.getWriter().write("用户名或者密码错为空,登录失败!");
            return;
        }

        //2.查询数据库,查看用户名或者密码是否正确
        UserDao userDao = new UserDao();
        User user = userDao.selectByName(username);
        if (user == null || !user.getPassword().equals(password)) {
            //用户不存在或者密码不正确
            resp.setContentType("text/html;charset= utf8");
            resp.getWriter().write("用户名或者密码错误,登录失败!");
            return;
        }
        //3.要是登录成功,就创建一个会话
        HttpSession session = req.getSession(true);
        //在会话中保存user,方便后面知道当前的user是谁
        session.setAttribute("user", user);

        //4.构造302响应报文(重定向)
        resp.sendRedirect("blog_list.html");
    }
}

客户端核心代码:

 <div class="login-container">
        <form action="login" method="post">
            <!-- 实现登录对话框 -->
        <div class="dialog">
            <h3>登录</h3>
            <div class="row">
                <span>用户名</span>
                <input type="text"id="username" name = "username">
            </div>
            <div class="row">
                <span>密码</span>
                <input type="password" id ="password" name = "password">    
            </div>
            <div class="row">
                <input class="login-btn"  value="登录" type = "submit"> 
            </div>
        </div>
        </form>

    </div>

强制要求用户登录

在博客系统中要求要先登录之后才能查看博客和编辑博客,所以要检查用户的登录状况,要是没有登录的话就自动跳转到登录页面

服务端核心代码:

首先要判断当前有没有创建出会话,要是当前存在session并且user也存在,说明已经登录了,就不进行操作,返回一个状态码200就行了,要是没有session或者user就说明当前没有登录,重定向到登录页面

@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.首先先获取一下会话
        //getSession的参数是false,就是说要是没有会话,也不用创建会话
        HttpSession session =req.getSession(false);
        if(session == null){
            //当前没有会话,说明没有登录
            resp.setStatus(403);
            return;
        }
        //为什么要判断user是否存在,其实还是会存在"有session但是没有user对象"这种情况的,这种情况会在后面退出登录的时候出现,所以此处一定要判断user对象是否存在
        User user = (User) session.getAttribute("user");
        if (user == null) {
            //说明此时虽然有会话,但是没有user对象,还是不行
            resp.setStatus(403);
            return;
        }
        //当前已经是登录状态,其实这行代码不写也行,但是写上更清楚
        resp.setStatus(200);
    }

客户端核心代码 :

function checkLogin() {
    $.ajax({
        type:'get',
        url:'login',
        success: function(body) {
            // 成功不做处理
        },
        error: function() {
            // 失败就会强行跳转到登录页面
            location.assign('login.html');
        }
    })
}

显示用户信息

当李四登录进去之后看到张三的文章,之后点了进去,此时就要修改一下用户的信息

这里的服务端逻辑要分成两种情况:

  1. 要是当前用户是在博客列表页,登录的信息就在session中
  2. 要是当前用户是在博客详情页中,就要求数据库中查询文章作者的信息
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebServlet("/userInfo")
public class UserInfoServlet extends HttpServlet {
   private  ObjectMapper objectMapper = new ObjectMapper();
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //首先获取用户信息
        String blogId = req.getParameter("blogId");//getParameter是用来获取表单中的值的,返回值是String,所以此时的blogId就变成了String类型
        if (blogId == null) {
            //博客ID为0说明当前是在列表页,登录的用户信息在session里面,所以从session中拿
                getUserInfoFromSession(req,resp);
        }else{
            //存在博客ID说明当前是在博客展示页,要获取作者信息,就要查询数据库,不能从session中拿,因为文章作者可能不是自己,所以要从数据库中拿
            getUserInfoFromDB(req,resp, Integer.parseInt(blogId));//将blogId转换成int类型
        }
    }

    private void getUserInfoFromDB(HttpServletRequest req, HttpServletResponse resp, int blogId) throws IOException {
        //先根据blogId查询blog对象,获取到userId(作者是谁)
        // 根据user查询对应的User对象即可
        BlogDao blogDao = new BlogDao();
        Blog blog = blogDao.selectOne(blogId);
        if (blog == null) {
            //这种情况是: blogId是随便写的,数据库中查不到,是不符合要求的
            resp.setStatus(404);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("blogId不存在");
            return;
        }
        UserDao userDao = new UserDao();
        User user = userDao.selectById(blog.getUserId());
        if (user == null) {
            resp.setStatus(404);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("user不存在");
            return;
        }
        //排除了上面两种错误情况,接下来只有将user以JSON的形式返回就行了
        user.setPassword("");
        resp.setContentType("application/json;charset=utf8");
        resp.getWriter().write(objectMapper.writeValueAsString(user));
    }

    private void getUserInfoFromSession(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        //首先先获取会话
        HttpSession session = req.getSession(false);
        if (session == null) {
            resp.setStatus(403);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("当前未登录!");
            return;
        }
        //获取user对象
        User user = (User) session.getAttribute("user");
        if (user == null) {
            resp.setStatus(403);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("当前未登录!");
            return;
        }
        //在获取到user对象之后为了安全起见,将密码设置为空
        //以JSON的形式返回响应,所以要提前创建objectMapper
        user.setPassword("");
        resp.setContentType("application/json;charset=utf8");
        resp.getWriter().write(objectMapper.writeValueAsString(user));
    }
}

服务端核心代码:

//针对博客列表页,获取到当前用户的信息
    function getUserInfo(){
        $.ajax({
            type:'get',
            url:'userInfo',
            success : function(body){
                //修改头像
                let h3 = document.querySelector('.container-left>.card>h3')
                h3.innerHTML = body.username;
            }
        })

    }
    getUserInfo();

退出登录状态

在页面上一个注销按钮,要求实现点击一下按钮就能退出当前的账号

服务端代码:

后端服务端在收到请求之后,将当前会话的信息删除,然后重定向到登录页面即可

//实现注销功能(退出登录状态)
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession(false);
        if (session == null) {
            resp.setStatus(403);
            return;
        }
        //删除user对象,并重定向到登录页面
        session.removeAttribute("user");
        resp.sendRedirect("login.html");
    }
}

这里需要注意的是: 在登录状态下,既有session又有user属性,要是想要删除用户的信息,只要删除其中一个就行了,但是这里选择的是删除user属性,因为没有提供删除session的api,并不是很好删除session

服务端代码:

<a href="logout">注销</a>

只要将注销按钮变成一个链接形式即可

发布博客

发布博客的意思是在用户写完博客之后,点击提交按钮,服务端收到请求之后,在数据库中添加一条记录,并且在最后还会跳转到博客列表页

有一个前提: 用户要写上了标题和正文之后点击提交按钮才能提交

所以还要判断一下是否用户写了标题和正文

服务端代码:

接着在原本的BlogServlet类中写

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //使用这个post方法来实现创建新的博客
    //1.检查一下用户的登录状态和用户信息,要是未登录就不能创建新的博客
    HttpSession session = req.getSession(false);
    if (session == null) {
        resp.setStatus(403);
        resp.setContentType("text/html;charset=utf8");
        resp.getWriter().write("当前未登录,不能创建博客!");
        return;
    }
    //检查是否存在user对象
    User user = (User) session.getAttribute("user");
    if (user == null) {
        resp.setStatus(403);
        resp.setContentType("text/html;charset=utf8");
        resp.getWriter().write("当前未登录,不能创建博客!");
        return;
    }
    //2.当前已经登录,获取请求的参数(博客的标题和内容)
    req.setCharacterEncoding("utf8");//将请求变成utf8的格式,防止乱码
    String title = req.getParameter("title");
    String content = req.getParameter("content");
    //3.构造Blog对象,并插入到数据库中
    Blog blog = new Blog();
    blog.setTitle(title);
    blog.setContent(content);
    //博客的blogId是自增主键,所以不用专门指定设置,postTime是now()函数,所以也不用指定
    blog.setUserId(user.getUserId());
    BlogDao blogDao = new BlogDao();
    blogDao.insert(blog);

    //4.插入新的博客之后重定向到博客列表页
    resp.sendRedirect("blog_list.html");

    //有一个疑问:前面已经实现了未登录状态下是不能进入博客编辑页的,那么此时为什么还要先判断一下是否登录呢?
    //1.要是有人使用postman直接构造post请求,直接绕开了登录,插入数据怎么办?
    //2.在上面的代码中,要想给新的博客设置userId需要使用user.getUserId(),所以前面要保证存在user对象
    //结合上面的两点理由,加上登录判断是很有必要的
}

客户端核心代码:

使用form表单来提交请求

<!-- 这个编辑页的版心 -->
    <div class="blog-edit-container" style="height: 100%">
        <!-- 套上一个form标签 -->
        <form action="blog" method="post" style="height: 100%">
            <!-- 标题的编辑区 -->
            <div class="title">
                <input type="text" id = 'blog-title' placeholder="请在这里输入文章的标题" name = "title">
                <input id = "submit" value="发布文章" type="submit">    
            </div>
            <!-- 正文的编辑区 -->
        <div id = "editor">
            <!-- editor.md规定,要想使用form表单来提交数据,就要按照下面的标准来写 -->
            <textarea name="content" style="display: none"></textarea>
        </div>

        </form>
    </div>

删除博客

要是想要删除博客,在不考虑管理员的情况下,只能由博客的作者自己来删除

所以就要判断当前登录的用户是不是博客的作者,要是不是的话,他是没有权限删除别人的博客的

在删除之后还要重定向到博客列表页

服务端代码:

主要的思路就是看看博客作者id和登陆者id是不是一样的,要是一样的,就有权限去删除博客

但是在此之前,还有很多要考虑的

  1. 当前用户是否已经登录?(这个问题是要考虑的,前面已经解释过)
  2. 客户端传过来的blogId是否可能是null(前端没有传过来blogId)
  3. 传过来的blogId是否可能不存在(客户端传了blogId,但是在服务端这边根本就没有)

在经历了上面的错误情况之后,就是客户端传来正确的blogId,服务端也有对应的博客作者的id,此时只要对比一下登录用户id和博客作者id即可

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/blog_delete")
public class BlogDeleteServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.先判定用户的登录状态
        HttpSession session = req.getSession(false);
        if (session == null) {
            resp.setStatus(403);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("您当前未登录,不能删除博客!");
            return;
        }
        User user = (User) session.getAttribute("user");
        if (user == null) {
            resp.setStatus(403);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("您当前未登录,不能删除博客 !");
            return;
        }
        //能走到这一步,说明已经是登录状态了
        //2.获取到blogId
        String blogId = req.getParameter("blogId");
        if (blogId == null) {
            //说明这个blogId为空,这种情况是前端没传blogId
            resp.setStatus(403);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("您当前要删除的blogId有误!");
            return;
        }
        //3.查询这个blogId对应的blog作者
        BlogDao blogDao = new BlogDao();
        Blog blog = blogDao.selectOne(Integer.parseInt(blogId));
        if (blog == null) {
            //此时是存在一个blogId,但是它并没有对应的文章,这种情况是前端传了blogId,但是后端没有对应的数据
            resp.setStatus(404);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("您当前要删除的博客不存在! blogId = " + blogId);
            return;
        }
        //观察上面的四个if可以感受到,这就是在排错,先将错误的情况都写出来,起到过滤的作用.
        //4.判断登录用户是不是文章作者
        if (blog.getUserId() != user.getUserId()) {
            //blog.getUserId是当前文章的作者
            //user.getUserId是从session中拿的登录用户信息
            //要是不一样,直接返回403,并且提示
            resp.setStatus(403);
            resp.setContentType("text/html;charset=utf8");
            resp.getWriter().write("您不能删除别人的博客!");
            return;
        }
        //5.真正执行删除操作
        //将String类型的blogId转换成int类型的,准备删除
        blogDao.delete(Integer.parseInt(blogId));
        //6.返回302,重定向到博客列表页,要是失败就提示失败
        resp.sendRedirect("blog_list.html");
    }
}

客户端核心代码:

 function upDateDeleteURL(){
    // 这里的location.search就是'?blogId=?'
    //可以在网页开发者工具的console中输入查看
    let deleteBtn = document.querySelector('#delete-btn');
    deleteBtn.href = 'blog_delete' + location.search;
 }
 upDateDeleteURL();

在Linux上部署博客系统

在完成了博客系统之后,只是可以在自己的电脑上查看博客系统, 别人还是看不到的,所以要借助公网ip来让所有人都能访问博客系统

要是想要部署java web程序,首先要配置环境

jdk tomcat mysql

安装jdk

推荐的方法是使用yum直接安装openjdk(开源的,与官方的jdk功能差不多),目前使用的最多的就是jdk8系列

yum list | grep jdk

在源上搜索所有关于jdk的文件

image-20230224200710035

devel表示development的意思,就是软件开发包

后面的x86_64的意思是支持64位的系统

选中之后按ctrl + insert来复制,之后使用yum install 粘贴(shift + insert ) 就下载好了

安装tomcat

当前的程序使用的是tomcat8.5的版本,但是使用yum list | grep tomcat 并没有8.5版本(当前的centos7系统有点老)

此时有一个好方法,找到之前的tomcat压缩包,直接拖到Linux上就能安装,主要是因为tomcat是java写的,能跨平台

注意: 在拖压缩包之前,要确认压缩包是不是zip格式的,因为Linux默认不支持rar格式的压缩包,只支持zip格式

可能会出现拖不过去的情况,此时就要安装一个lrzsz, yum insatll lrzsz

之后就能将tomcat的压缩包拖过去了

image-20230224203550512

之后进行解压,需要使用unzip ,所以要先安装unzip, yum insatll unzip

之后进行解压 unzip apache apache-tomcat-8.5.85.zip(后面的文件可以用TAB来补全)

进入到tomcat的bin目录下,就可以看到启动的脚本

image-20230224204609258

在Windows上使用的是startup.bat,但是在Linux上使用的是startup.sh

要想启动这里的程序,首先要赋予他们可执行权限,也就是chmod + x *.sh, 之后后面的文件就会变成绿色,表示可以执行

image-20230224204902904

image-20230224205155923

启动tomcat的过程:

  1. 下载安装包(要是zip格式的压缩包)
  2. 上传压缩包到Linux上(要使用到 lszrz命令)
  3. 解压缩(要使用到unzip 命令)
  4. 进入Tomcat目录的bin目录
  5. 给启动脚本增加可执行权限(chmod +x *.sh) --此时.sh的文件就会变成绿色
  6. 使用 sh startup.sh来启动 tomcat

验证tomcat是否启动成功

  • 通过ps aux | grep tomcat来查看进程

  • 通过nststat -anp | grep 8080 查看端口号是否被绑定

image-20230224210104965

image-20230224210244989

tomcat也是用java写的,所以此时8080已经被tomcat绑定了

还有一种验证tomcat是否启动的方式,就是直接访问tomcat的欢迎页面 公网IP地址:8080

但是大概率是不能访问的,这是因为云服务器的防火墙或者安全组没有对8080允许访问,所以要去云服务器那边手动添加一个8080端口号的规则

image-20230224212411896

添加完8080端口号的规则之后就能正常访问tomcat的欢迎页面

image-20230224212505240

但是在平时的时候还是不要开启端口号规则,防止服务器被攻击

安装mysql

linux安装mysql有很多的方式,最简单的一种安装方式是使用yum 安装mariadb(开源的,是mysql的孪生兄弟)

使用yum安装必要的包

直接执行这几个命令就行了

yum install -y mariadb-server
yum install -y mariadb
yum install -y mariadb-libs
yum install -y mariadb-devel

启动

启动mariadb服务

systemctl start mariadb

设置服务自启动

systemctl enable mariadb

查看服务的状态

systemctl status mariadb

验证是否连接上

mysql -uroot

image-20230224214033912

这样子就是连接成功了

创建数据库的时候为了支持中文,这里统一使用utf8mb4字符集

create database demo_db charset utf8mb4;

此时就将mariadb安装连接好了

正式部署

在安装好了jdk tomcat mysql之后,就可以开始将博客系统部署到云服务器上

  1. 首先要理清楚 博客系统的依赖,先将依赖的内容打通

  2. 将博客系统的程序打包, 将war包上传到云服务器的webapps目录下即可

    在我的博客系统中的依赖就是mysql,必须要知道的是,本地的电脑上的数据库与云服务器上的数据库是两台不一样的电脑,所以本地的数据库有数据,但是云服务器上的数据库还没有数据

    所以此时要将SQL指令在云服务器上输入(粘贴)一下

进入mariadb 的命令: mysql -uroot,退出mariadb: ctrl + c

在开始打war包之前要先调整一下DBUtil的代码,主要就是要调整一下连接数据库的代码

image-20230225102637117

需要将这里的密码设置成云服务器的数据库的密码,要是没有单独设置过云服务器上的密码,那么就是空字符串

image-20230225102959779

这里的setURL也基本上不用改,因为云服务器也是在我主机上的,所以还是可以使用环回IP, 后面的3306端口号也是不用改的

打war包的时候

image-20230225101928966

改好之后双击maven的package就好了

image-20230225103327293

打好war包之后找到war包

在Linux上进入到apache tomcat目录中,在进入webapps目录中

image-20230225103709894

将之前打好的war包拖到webapp下面

image-20230225104002387

之后就会自动进行解压缩和部署,变成蓝色的了

要是之后修改了代码,务必要重新打包,重新上传

部署(也叫上线)是一个很重要,很有仪式感的事情,所以一定要小心谨慎!

此时就已经是部署完毕了!

注意: 要是发现网页打不卡,先检测一下tomcat的欢迎页面能不能打开,要是不能打开说明是tomcat8080端口被阻止了,就要去云服务器那边将防火墙新增8080端口的规则

要知道本地电脑上能运行,不能说明部署到云服务器上就能成功

此时我出现了一个问题,那就是mariadb上的中文乱码,但是我在建库的时候已经指定了utf8mb4字符集,最后我发现还要在建表的时候也要指定字符集

image-20230225114035918

要是修改了任何代码都要重新使用maven双击package重新打war包

再把之前Linux上的war删除,再把新的war包拖进去,就能重新部署

此时,就正式完成了博客系统的简单功能实现和部署

博客系统的源码(服务端+客户端)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/399690.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

求职复盘:干了四年外包出来,面试5次全挂

我的情况 大概介绍一下个人情况&#xff0c;男&#xff0c;毕业于普通二本院校非计算机专业&#xff0c;18年跨专业入行测试&#xff0c;第一份工作在湖南某软件公司&#xff0c;做了接近4年的外包测试工程师&#xff0c;今年年初&#xff0c;感觉自己不能够再这样下去了&…

为什么做知识管理,就想选择Baklib呢?

随着科技的不断发展&#xff0c;知识管理已经成为现代企业不可或缺的一个重要组成部分。由于信息化快速发展&#xff0c;企业每天都会产生大量的数据和信息&#xff0c;如何高效地获取、整理和利用这些信息已经成为了企业成功的关键因素之一。为了更好地管理企业知识&#xff0…

利用Iptables构建虚拟路由器

利用Iptables构建虚拟路由器 &#xff08;1&#xff09;修改网络类型 在VMware Workstation软件中选择“编辑→虚拟网络编辑器”菜单命令&#xff0c;在虚拟网络列表中选中VMnet1&#xff0c;将其配置为“仅主机模式&#xff08;在专用网络内连接虚拟机&#xff09;”&#x…

模板进阶(仿函数,特化等介绍)

非类型模板参数 模板参数有类型形参和非类型形参&#xff1b; 类型形参&#xff1a;使用typename或者class修饰的参数类型名称 非类型形参&#xff1a;一个普通常量作为模板参数形参&#xff0c;不能为浮点数&#xff0c;字符类型以及类对象&#xff1b; #include<iostrea…

虹科新品| HK-TrueNAS企业存储

一、HK-TrueNAS概述HK-TrueNAS 是一种统一存储阵列&#xff0c;提供混合和全闪存配置&#xff0c;以前所未有的价格提供全面的功能集和高达 10.5PB 的容量。TrueNAS 全闪存存储阵列为以闪存为中心的数据中心提供了理想的统一数据存储。每个混合和全闪存 TrueNAS 系统都使用 Tru…

VSCode 开发配置,一文搞定(持续更新中...)

一、快速生成页面骨架 文件 > 首选项 > 配置用户代码片段 选择需要的代码片段或者创建一个新的&#xff0c;这里以 vue.json 举例&#xff1a; 下面为我配置的代码片段&#xff0c;仅供参考&#xff1a; {"Print to console": {"prefix": "…

Mac系统配置java、Android_sdk、gradle、maven、ndk、flutter、tomcat环境变量

搞了三天&#xff0c;终于搞定MAC系统下的各种环境变量了…… 旧版本10.13.6或者更老的MAC系统&#xff0c;只用在.bash_profile文件编辑就行了&#xff1b;新版本10.14.2、10.15.7或者更高的&#xff0c;还要去.zshrc文件加一句source ~/.bash_profile&#xff0c;才能使所有…

java明文数据加密、脱敏方法总结

前言 在一些安全性要求比较高的项目里&#xff0c;避免不了要对敏感信息进行加解密&#xff0c;比如配置文件中的敏感信息。 第一种方法&#xff08;自定义加解密&#xff09; 加解密工具类&#xff1a; public class SecurityTools {public static final String ALGORITHM…

最新!Windows 11 更新将整合 AI 技术

微软MVP实验室研究员张雅琪&#xff08;阿法兔&#xff09;微软最有价值专家&#xff08;MVP&#xff09;&#xff0c;毕业于外交学院和香港大学&#xff0c;IT 技术社区创始人&#xff0c;中关村互联网金融研究院兼职研究员&#xff0c;多次受邀在微软 Reactor 进行公开演讲&a…

JS的BroadcastChannel与MessageChannel

BroadcastChannel与MessageChannel BroadcastChannel BroadcastChannel以广播的形式进行通信 BroadcastChannel用于创建浏览器标签页之间的通信 使用BroadcastChannel的浏览器标签页面必须要遵循同源策略 页面1使用BroadcastChannel创建一个频道&#xff0c;页面2使用Broadc…

latex入门指南:插入图片、表格、公式方法一览

省事链接&#xff1a; 生成表格latex代码&#xff1a;www.tablesgenerator.com 生成公式latex代码&#xff1a;www.latexlive.com 目录1 插入图片1.1 移动标题位置1.2 双栏文章中图片横跨双栏2 插入表格2.1 常规表格2.2 设置单元格宽度2.3 合并单元格2.4 三线表2.5 移动标题位置…

脑机接口科普0018——前额叶切除手术

本文禁止转载&#xff01;&#xff01;&#xff01; 首先说明一下&#xff0c;前额叶切除手术&#xff0c;现在已经不允许做了。 其次&#xff0c;前额叶切除手术&#xff0c;发明这个手术的人居然还获得了诺贝尔奖。太过于讽刺。1949年的那次诺贝尔医学奖&#xff08;就是我…

打怪升级之发送单个UDP包升级版

目标 1.message的输入由edit_control进行&#xff0c;需要捕获输入。 2.用户的主机地址和发送地址不一样&#xff0c;需要分别设置并绑定。 设计RC外观 必备组件&#xff1a;主机IP与端口&#xff0c;从机IP与端口&#xff0c;消息框&#xff0c;发送&#xff0c;连接按钮。…

打卡小达人之路:Spring Boot与Redis GEO实现商户附近查询

在当今社会&#xff0c;定位服务已经成为了各种应用的重要组成部分&#xff0c;比如地图、打车、美食等应用。如何在应用中实现高效的附近商户搜索功能呢&#xff1f;传统的做法是将商户的经纬度信息存储在关系型数据库中&#xff0c;然后使用SQL查询语句实现附近商户搜索功能。…

Anaconda环境配置Python数据分析库Pandas的方法

本文介绍在Anaconda环境中&#xff0c;安装Python语言pandas模块的方法。 pandas模块是一个基于NumPy的开源数据分析库&#xff0c;提供了快速、灵活、易用的数据结构和数据分析工具。它的主要数据结构是Series和DataFrame&#xff0c;可以处理各种数据格式&#xff0c;如CSV、…

听客户说|东台农商银行:建立健全数据安全管理制度的探索与实践

夯实银行数据安全&#xff0c;需“规划先行、谋定后动”&#xff0c;首要工作是确立管理工作的行动纲要&#xff0c;并据此建立制度保障体系以贯彻纲要&#xff0c;而后才是具体的行动措施和日常检查、监测。从银行数据安全建设实践路径来说&#xff0c;我认为可以用“盘现状、…

markdown(.md)常用语法

markdown&#xff08;.md&#xff09;常用语法markdown常用语法常用目录标题分割线格式空格换行无序列表有序列表列表嵌套文字引用行内代码代码块字体转义斜体加粗删除线下划线功能链接todo listtypora插入图片并保存在本地包含了一些常用的MD语法和操作&#xff0c;语法不是很…

ECS 图解

旧系统执行逻辑&#xff1a;程序开发中有一个很经典的设计模式/框架 &#xff1a;MVCvc 部分是系统&#xff0c; m 是组件。 我通过名为 index 的索引访问此数据&#xff1a;现在该组件具有用于读取和写入该数据的数据。 看看源代码中的Entity 。 struct 结构体。 64位索引 &am…

STM32查看内存占用的map文件解析

STM32查看内存占用的map文件解析STM32查看内存占用的map文件解析程序内存分析在stm32中&#xff0c;通常堆向高地址增长&#xff08;向上增长&#xff09;&#xff0c;栈向低地址增长&#xff08;向下增长&#xff09;&#xff1b;1.2 flash、ROM、RAM的区别1.3 通常stm32在kei…

跨过社科院与杜兰大学金融管理硕士项目入学门槛,在金融世界里追逐成为更好的自己

没有人不想自己变得更优秀&#xff0c;在职的我们也是一样。当我们摸爬滚打在职场闯出一条路时&#xff0c;庆幸的是我们没有沉浸在当下&#xff0c;而是继续攻读硕士学位&#xff0c;在社科院与杜兰大学金融管理硕士项目汲取能量&#xff0c;在金融世界里追逐成为更好的自己。…