前言:由于导师在我的毕设项目里加了消息系统(本来想水水就过的..),没办法…来稍微研究研究吧..简单简单…

需求分析

我的毕设是一个博客系统,类似于简书这样的,所以消息系统也类似,在用户的消息里包含了有:喜欢和赞、评论、关注、私信这样的一类东西,这样的一个系统应该包含以下的功能:

    1. 当用户评论/关注/点赞时能够通知到被评论/关注/点赞的用户,并生成像如下格式的提示信息(允许取消关注/点赞但不收到通知):

      我没有 关注了 你
      三颗 喜欢了你的文章 《Java消息系统简单设计与实现》
      心脏 评论了你的文章 《Java消息系统简单设计与实现》

    1. 用户之间能够发送/接受私信,不需要像QQ那样建立长连接实现实时通信,但刷新列表能看到新消息,并且界面类似QQ聊天界面一左一右,允许删除私信
    1. 管理员能发送通告,其实就像是用管理员的账号给每一个用户发送私信;
    1. 可以查看关注的用户最新发表的文章,得到类似推送的效果;
    1. 所有消息当然也要标注好消息已读or未读,登录就能得到消息提醒标识好有多少未读消息,像是QQ消息右上角的小红点那样类似;

OK,大致就是以上的功能,那么问题来了:这要怎么设计啊?

进一步分析

其实可以根据上面的需求分析,把上面的消息大致可以分为公告(Announcement)、提醒(Remind)、私信(Message)三类,我们可以大致抽象出一个 通知(Notify) 模型:

发送者 接受者 信息类型 动作类型 通知内容 是否已读 消息创建时间
粉丝1号 我没有三颗心脏 提醒 关注 粉丝1号 关注了 你 xx:xx:xx
粉丝1号 我没有三颗心脏 提醒 喜欢和赞 粉丝1号 喜欢了你的文章 《Java消息系统简单设计与实现》 xx:xx:xx
粉丝1号 我没有三颗心脏 提醒 评论 粉丝1号 评论了你的文章 《Java消息系统简单设计与实现》 xx:xx:xx
粉丝2号 我没有三颗心脏 私信 你收到了来自 粉丝2号 的 1 条私信 xx:xx:xx

上面加了一些数据以便理解,不过话说粉丝1号果然是真爱粉,又关注又喜欢又评论,嘻嘻嘻嘻…

emm.这样的模型能够胜任我们的工作吗?我也不知道..不过根据这个模型能够想出大概的这样的创建通知的逻辑:

似乎看上去也没有什么大问题..不过既然消息内容都可以根据动作类型自动生成的了,加上私信和公告的内容因为长度问题也肯定不保存在这张表里的好,所以我们在设计数据库时干脆把通知内容这条去掉不要,当信息类型是公告或者私信时可以根据这条通知的 id 在相应的表中找到相应的数据就可以了,emm..我觉得可以

顺下去想想其实脑中有了一个大概,这样的模型还容易设计和想到,其实主要的问题还是下面的那些

问题一:单表数据大了怎么办?

如果当用户量上去到一定量的时候,那么这张 通知表 势必会变得巨大,因为不管是我们的公告、提醒还是私信都会在这个通知表上创建一条数据,到时候就会面临查询慢的问题,问题的答案是:我也不知道..

所以我们的规定是:不考虑像简书这样超大用户量,能够应付毕设就好啦..简单设计,嘻嘻嘻..不过也不要太不相信MySQL的性能,还是有一定容纳能力的!

问题二:用户要怎样正确得到自己的未读消息呢?

暴力一点方法是,反正通知表里有用户所有的消息,直接读取完,然后通过是否已读字段就能够找到正确的所有未读消息了,这..这么简单吗?

其实有思考过使用时间或者另建一张保存有最新已读到哪条消息的表,但用户可以选择有一些读有一些不读,这两个似乎都很难达到目的…还是暴力吧

问题三:私信消息该怎么设计?

发送者 接受者 内容 发送时间
粉丝1号 我没有三颗心脏 我是你的真爱粉啊!我要给你生猴子! 2019年1月7日11:34:23
我没有三颗心脏 粉丝1号 已阅…下一个… 2019年1月7日11:34:53

就像 QQ消息 一样嘛,包含一个内容、时间、发送者和接受者,然后前端直接根据时间或者 id 排序生成一左一右的消息对话框,不过比较特殊的一点就是私信是一个双向交流的过程,在一个对话框中我可能既是接受者也是发送者,这也无所谓嘛,稍微分析分析场景:

  • 读取私信列表时:按照接受者和发送者一起查询的原则,也就是查询接受者是自己和发送者是自己的数据,然后根据时间和已读未读来建立私信列表;
  • 读取私信时:这时已经有了明确的接受者和发送者,那就查询所有 发送者是对方接受者是自己 Or 发送者是自己接受者是对方 的数据,然后在前端拼凑出一左一右的聊天框;
  • 发送私信时:先查询之前是否有记录,然后同上建立聊天框,点击发送之后把发送方设为自己接收方设为私信对象,然后在通知表中新建一条未读数据通知私信对象有私信来了;

这完全能满足要求,只不过感觉查询多了些..

数据库设计

简单弄了弄弄..看着挺难受的,不过能简单实现功能,并且为了演示,这里是做了一张user_follow表,表示用户之间的关联关系,点赞和评论与这个类似,就不多弄了..下面给一下建表语句吧:

user表:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(50) NOT NULL COMMENT '用户姓名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

user_follow表:

CREATE TABLE `user_follow` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `uid` bigint(20) NOT NULL COMMENT '用户ID',
  `follow_uid` bigint(20) NOT NULL COMMENT '关注的用户id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户关注表,记录了所有用户的关注信息';

notify表:

CREATE TABLE `notify` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `sender_id` bigint(20) NOT NULL COMMENT '发送者用户ID',
  `reciver_id` bigint(20) NOT NULL COMMENT '接受者用户ID',
  `type` varchar(50) NOT NULL COMMENT '消息类型:announcement公告/remind提醒/message私信',
  `is_read` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否已读,0未读,1已读',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间:按当前时间自动创建',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户通知表,包含了所有用户的消息';

message表:

CREATE TABLE `message` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `notify_id` bigint(20) NOT NULL COMMENT '对应通知消息的id',
  `sender_id` bigint(20) NOT NULL COMMENT '发送者用户ID',
  `reciver_id` bigint(20) NOT NULL COMMENT '接受者用户ID',
  `content` varchar(1000) NOT NULL COMMENT '消息内容,最长长度不允许超过1000',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间:按当前时间自动创建',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='私信信息表,包含了所有用户的私信信息';

根据《Java开发手册》5.3 第六条 没有使用任何级联和外键,bingo!

Spring Boot + MyBatis 实例

第一步:基础环境搭建

SpringBoot项目怎么搭就不说了吧,给一给几个关键的配置文件:

pom包依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>
<!-- SpringBoot - MyBatis 逆向工程 -->
<dependency>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-core</artifactId>
    <version>1.3.6</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>5.1.18</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

这里有一个巨坑,耗费了我好半天的时间,不知道为什么我明明引入的 5.1.18 版本的 mysql-connector-java,可 Maven 就是非要给我比较新版本的 8.0.13,这导致了在我使用 MyBatis 逆向工程生成 domain 和 mapper 的过程中出现了以下的问题:

  • 1、提示我数据库连接的驱动名称需要改成com.mysql.cj.jdbc.Driver而不是之前的com.mysql.jdbc.Driver,不然就报错:

Loading class com.mysql.jdbc.Driver'. This is deprecated. The new driver class iscom.mysql.cj.jdbc.Driver’. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.

  • 2、还需要设置 mysql 的时区,也就是需要将connectionURL属性写成"jdbc:mysql://localhost:3306/test?serverTimezone=UTC"。如果不指定serverTimezone=UTC(还必须大写),将报错:

java.sql.SQLException: The server time zone value ‘?й???????’ is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.
  at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:695)
  at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:663)

  • 3、逆向工程会去找 MySQL 其他库的相同表名的表,然后生成一堆乱七八糟的东西,还由于找不到主键 id 生成了只含 inser() 方法而不含删除、更新方法的 Mapper 文件;

解决方法就只有自己手动去调低 mysql-connector-java 的版本到 5.xx,还找到一个跟我情况类似:https://blog.csdn.net/angel_xiaa/article/details/52474022

application.properties:

## 数据库连接配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/message_system?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123456
## MyBatis相关配置
mybatis.type-aliases-package=com.wmyskxz.demo.messagesystem.domain
mybatis.mapper-locations=classpath:mapper/*.xml

在启动类上加上注解:

@EnableTransactionManagement  // 启注解事务管理,等同于xml配置方式的 <tx:annotation-driven />
@MapperScan("com.wmyskxz.demo.messagesystem.dao")
@SpringBootApplication
public class MessageSystemApplication {
        ....
}

第二步:MyBatis 逆向工程

新建【util】包,在下面新建两个类:

MybatisGenerator类:

public class MybatisGenerator {

    public static void main(String[] args) throws Exception {
        String today = "2019-1-7";

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date now = sdf.parse(today);
        Date d = new Date();

        if (d.getTime() > now.getTime() + 1000 * 60 * 60 * 24) {
            System.err.println("——————未成成功运行——————");
            System.err.println("——————未成成功运行——————");
            System.err.println("本程序具有破坏作用,应该只运行一次,如果必须要再运行,需要修改today变量为今天,如:" + sdf.format(new Date()));
            return;
        }

        if (false)
            return;
        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        InputStream is = MybatisGenerator.class.getClassLoader().getResource("generatorConfig.xml").openStream();
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(is);
        is.close();
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
        myBatisGenerator.generate(null);

        System.out.println("生成代码成功,只能执行一次,以后执行会覆盖掉mapper,pojo,xml 等文件上做的修改");

    }
}

OverIsMergeablePlugin类:

/**
 * 解決 MyBatis 逆向工程重复生成覆盖问题的工具类
 */
public class OverIsMergeablePlugin extends PluginAdapter {
    @Override
    public boolean validate(List<String> warnings) {
        return true;
    }

    @Override
    public boolean sqlMapGenerated(GeneratedXmlFile sqlMap, IntrospectedTable introspectedTable) {
        try {
            Field field = sqlMap.getClass().getDeclaredField("isMergeable");
            field.setAccessible(true);
            field.setBoolean(sqlMap, false);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }
}

在【resrouces】资源文件下新建逆向工程配置文件【generatorConfig.xml】:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <context id="DB2Tables" targetRuntime="MyBatis3">

        <!--避免生成重复代码的插件-->
        <plugin type="com.wmyskxz.demo.messagesystem.util.OverIsMergeablePlugin"/>

        <!-- 是否去除自动生成的代码中的注释 true:是 false:否-->
        <commentGenerator>
            <property name="suppressDate" value="true"/>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <!--数据库链接地址账号密码-->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/message_system?serverTimezone=UTC"
                        userId="root" password="123456">
        </jdbcConnection>
        <!-- 默认 false,把 JDBC DECIMAL 和 NUMERIC 类型解析为 Integer
             为 true 时解析为 java.math.BigDecimal -->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>
        <!--生成pojo类存放位置-->
        <javaModelGenerator targetPackage="com.wmyskxz.demo.messagesystem.domain" targetProject="src/main/java">
            <!-- enableSubPackages:是否让 schema 作为包的后缀-->
            <property name="enableSubPackages" value="true"/>
            <!-- trimStrings:从数据库返回的值被清理前后的空格 -->
            <property name="trimStrings" value="true"/>
            <!-- 是否对model添加 构造函数 -->
            <property name="constructorBased" value="true"/>
        </javaModelGenerator>
        <!--生成xml映射文件存放位置-->
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>
        <!--生成mapper类存放位置-->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.wmyskxz.demo.messagesystem.dao"
                             targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!--生成对应表及类名
            tableName:要生成的表名
            domainObjectName:生成后的实例名
            enableCountByExample:Count语句中加入where条件查询,默认为true开启
            enableUpdateByExample:Update语句中加入where条件查询,默认为true开启
            enableDeleteByExample:Delete语句中加入where条件查询,默认为true开启
            enableSelectByExample:Select多条语句中加入where条件查询,默认为true开启
            selectByExampleQueryId:Select单个对象语句中加入where条件查询,默认为true开启
        -->
        <table tableName="user" domainObjectName="User" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false" enableDeleteByPrimaryKey="true" enableUpdateByPrimaryKey="true">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="false"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table tableName="notify" domainObjectName="Notify" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false" enableDeleteByPrimaryKey="true" enableUpdateByPrimaryKey="true">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="false"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table tableName="user_follow" domainObjectName="UserFollow" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false" enableDeleteByPrimaryKey="true" enableUpdateByPrimaryKey="true">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="false"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table tableName="message" domainObjectName="Message" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false" enableDeleteByPrimaryKey="true" enableUpdateByPrimaryKey="true">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="false"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
    </context>
</generatorConfiguration>

运行我们的【MybatisGenerator】类中的 main 方法就能看到自动生成的实体、Xml文件以及 Mapper 类

第三步:Service 层

不给接口了,直接给实现吧,方法都很简单,而且没有做任何的安全限制,只是为了实现简单的消息系统,看效果

UserServiceImpl:

@Service
public class UserServiceImpl implements UserService {
    @Resource
    UserMapper userMapper;
    @Override
    public void addUserByUsername(String username) {
        userMapper.insert(new User(null, username));// 主键自增长.
    }
    @Override
    public User findUserById(Long id) {
        return userMapper.selectByPrimaryKey(id);
    }
}

UserFollowServiceImpl:

@Service
public class UserFollowServiceImpl implements UserFollowService {
    @Resource
    UserFollowMapper userFollowMapper;
    @Autowired
    NotifyService notifyService;
    @Override
    public void userAFollowUserBById(Long userAId, Long userBId) {
        // 先要创建一条提示消息
        notifyService.addNotify(userAId, userBId, "follow");// 关注信息
        UserFollow userFollow = new UserFollow();
        userFollow.setUid(userAId);
        userFollow.setFollowUid(userBId);
        userFollowMapper.insertSelective(userFollow);
    }
    @Override
    public void userAUnfollowUserBById(Long userAId, Long userBId) {
        // 首先查询到相关的记录
        UserFollowExample example = new UserFollowExample();
        example.or().andUidEqualTo(userAId).andFollowUidEqualTo(userBId);
        UserFollow userFollow = userFollowMapper.selectByExample(example).get(0);
        // 删除关注数据
        userFollowMapper.deleteByPrimaryKey(userFollow.getId());
    }
}

NotifyServiceImpl:

@Service
public class NotifyServiceImpl implements NotifyService {
    @Resource
    NotifyMapper notifyMapper;
    @Override
    public int addNotify(Long senderId, Long reciverId, String type) {
        Notify notify = new Notify(null, senderId, reciverId, type, false, null);
        return notifyMapper.insertSelective(notify);// id和creatTime自动生成.
    }
    @Override
    public void readNotifyById(Long id) {
        Notify notify = notifyMapper.selectByPrimaryKey(id);
        notify.setIsRead(true);
        notifyMapper.updateByPrimaryKey(notify);
    }
    @Override
    public List<Notify> findAllNotifyByReciverId(Long id) {
        List<Notify> notifies = new LinkedList<>();
        NotifyExample example = new NotifyExample();
        example.setOrderByClause("`id` DESC");// 按id倒叙,也就是第一个数据是最新的.
        example.or().andReciverIdEqualTo(id);
        notifies.addAll(notifyMapper.selectByExample(example));
        return notifies;
    }
    @Override
    public List<Notify> findAllUnReadNotifyByReciverId(Long id) {
        List<Notify> notifies = new LinkedList<>();
        NotifyExample example = new NotifyExample();
        example.setOrderByClause("`id` DESC");// 按id倒叙,也就是第一个数据是最新的.
        example.or().andReciverIdEqualTo(id).andIsReadEqualTo(false);
        notifies.addAll(notifyMapper.selectByExample(example));
        return notifies;
    }
}

MessageServiceImpl:

@Service
public class MessageServiceImpl implements MessageService {
    @Resource
    MessageMapper messageMapper;
    @Resource
    NotifyService notifyService;
    @Override
    public void addMessage(Long senderId, Long reciverId, String content) {
        // 先创建一条 notify 数据
        Long notifyId = (long) notifyService.addNotify(senderId, reciverId, "message");// message表示私信
        // 增加一条私信信心
        Message message = new Message(null, notifyId, senderId, reciverId, content, null);
        messageMapper.insertSelective(message);// 插入非空项,id/createTime数据库自动生成
    }
    @Override
    public void deleteMessageById(Long id) {
        messageMapper.deleteByPrimaryKey(id);
    }
    @Override
    public Message findMessageByNotifyId(Long id) {
        // 触发方法时应把消息置为已读
        notifyService.readNotifyById(id);
        MessageExample example = new MessageExample();
        example.or().andNotifyIdEqualTo(id);
        return messageMapper.selectByExample(example).get(0);
    }
}

第四步:Controller 层

也很简单,只是为了看效果

UserController:

@RestController
public class UserController {
    @Autowired
    UserService userService;
    @PostMapping("/addUser")
    public String addUser(@RequestParam String username) {
        userService.addUserByUsername(username);
        return "Success!";
    }
    @GetMapping("/findUser")
    public User findUser(@RequestParam Long id) {
        return userService.findUserById(id);
    }
}

UserFollowController :

@RestController
public class UserFollowController {
    @Autowired
    UserFollowService userFollowService;
    @PostMapping("/follow")
    public String follow(@RequestParam Long userAId,
                         @RequestParam Long userBId) {
        userFollowService.userAFollowUserBById(userAId, userBId);
        return "Success!";
    }
    @PostMapping("/unfollow")
    public String unfollow(@RequestParam Long userAId,
                           @RequestParam Long userBId) {
        userFollowService.userAUnfollowUserBById(userAId, userBId);
        return "Success!";
    }
}

NotifyController :

@RestController
public class NotifyController {
    @Autowired
    NotifyService notifyService;
    @PostMapping("/addNotify")
    public String addNotify(@RequestParam Long senderId,
                            @RequestParam Long reciverId,
                            @RequestParam String type) {
        notifyService.addNotify(senderId, reciverId, type);
        return "Success!";
    }
    @PostMapping("/readNotify")
    public String readNotify(@RequestParam Long id) {
        notifyService.readNotifyById(id);
        return "Success!";
    }
    @GetMapping("/listAllNotify")
    public List<Notify> listAllNotify(@RequestParam Long id) {
        return notifyService.findAllNotifyByReciverId(id);
    }
    @GetMapping("/listAllUnReadNotify")
    public List<Notify> listAllUnReadNotify(@RequestParam Long id) {
        return notifyService.findAllUnReadNotifyByReciverId(id);
    }
}

MessageController :

@RestController
public class MessageController {
    @Autowired
    MessageService messageService;
    @PostMapping("/addMessage")
    public String addMessage(@RequestParam Long senderId,
                             @RequestParam Long reciverId,
                             @RequestParam String content) {
        messageService.addMessage(senderId, reciverId, content);
        return "Success!";
    }
    @DeleteMapping("/deleteMessage")
    public String deleteMessage(@RequestParam Long id) {
        messageService.deleteMessageById(id);
        return "Success!";
    }
    @GetMapping("/findMessage")
    public Message findMessage(@RequestParam Long id) {
        return messageService.findMessageByNotifyId(id);
    }
}

第五步:测试

通过 REST 测试工具,可以看到正确的效果,这里就不给出所有的测试了。

总结

以上的项目简单而且没有任何的安全验证,不过能够基本完成我们的需求,还有一些功能没有实现,例如管理员发通告(上面只演示了私信和关注信息),按照上面的系统就直接暴力给每个用户都加一条通知消息,感觉有点自闭..我也不知道怎么设计好..希望有经验的大大能指条路啊!

其实关于这个简单的系统我查了好多好多资料..把自己都看自闭了,后来我干脆把所有网页都关掉,开始用 JPA 自己开始抽象实体,把各个实体写出来并把所有实体需要的数据啊相互之间的关联关系啊写清楚,然后再从自动生成的数据库中找思路…hhh…要不是我 JPA 不是很熟我觉得用 JPA 就能写出来了,不用 JPA 的原因在于一些数据的懒加载不知道怎么处理,还有就是查询语句太复杂,免不了要浪费一些资源…emmm..说到底还是不是特别懂 JPA,下面给一张复杂的用 JPA 建立的 User 实体吧(随手截的..hhh…很乱..):


按照惯例黏一个尾巴:

欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz
欢迎关注公众微信号:wmyskxz
分享自己的学习 & 学习资料 & 生活
想要交流的朋友也可以加qq群:3382693

前言:由于之前没有接触过Hibernate框架,但是最近看一些博客深深被它的“效率”所吸引,所以这就来跟大家一起就着一个简单的例子来尝尝Spring全家桶里自带的JPA的鲜

Spring-DATA-JPA 简介

JPA(Java Persistence API)是Sun官方提出的Java持久化规范。它为Java开发人员提供了一种对象/关联映射工具来管理Java应用中的关系数据。他的出现主要是为了简化现有的持久化开发工作和整合ORM技术,结束现在Hibernate,TopLink,JDO等ORM框架各自为营的局面。值得注意的是,JPA是在充分吸收了现有Hibernate,TopLink,JDO等ORM框架的基础上发展而来的,具有易于使用,伸缩性强等优点。从目前的开发社区的反应上看,JPA受到了极大的支持和赞扬,其中就包括了Spring与EJB3.0的开发团队。

注意:JPA是一套规范,不是一套产品,那么像Hibernate,TopLink,JDO他们是一套产品,如果说这些产品实现了这个JPA规范,那么我们就可以叫他们为JPA的实现产品。

Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套JPA应用框架,可使开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!学习并使用 Spring Data JPA 可以极大提高开发效率!(spring data jpa让我们解脱了DAO层的操作,基本上所有CRUD都可以依赖于它来实现

摘自:springboot(五):spring data jpa的使用——纯洁的微笑

Hibernate 和 MyBatis 简单对比

由于JPA底层干活的仍然是Hibernate框架,而我们之前学习的只有MyBatis相关的东西,所以在尝鲜之前还是有必要简单了解一下两者的区别:

Hibernate的优势:

  • Hibernate的DAO层开发比MyBatis简单,Mybatis需要维护SQL和结果映射。
  • Hibernate对对象的维护和缓存要比MyBatis好,对增删改查的对象的维护要方便。
  • Hibernate数据库移植性很好,MyBatis的数据库移植性不好,不同的数据库需要写不同SQL。
  • Hibernate有更好的二级缓存机制,可以使用第三方缓存。MyBatis本身提供的缓存机制不佳。

MyBatis的优势:

  • MyBatis可以进行更为细致的SQL优化,可以减少查询字段。
  • MyBatis容易掌握,而Hibernate门槛较高。

简单总结:

  • MyBatis:小巧、方便、高效、简单、直接、半自动化
  • Hibernate:强大、方便、高效、复杂、间接、全自动化

引用自:【持久化框架】Mybatis与Hibernate的详细对比——高亮

CRUD + 分页后台实例

下面我们来快速搭建一个使用Spring-DATA-JPA的CRUD+分页后台实例,并且我们会直接使用到RESTful API(不熟悉的同学戳这里

第一步:新建SpringBoot项目

打开IDEA新建一个SpringBoot项目,不熟悉SpringBoot的同学请右转:【传送门】,然后在pom.xml中添加以下依赖:

<!-- mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.21</version>
</dependency>

<!-- jpa-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

然后把application.properties弄成这个样子:

#数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/testdb?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
#显示SQL语句
spring.jpa.show-sql=true
#不加下面这句则默认创建MyISAM引擎的数据库
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#自己重写的配置类,默认使用utf8编码
spring.jpa.properties.hibernate.dialect=com.wmyskxz.demo.config.MySQLConfig

spring.jpa.properties.hibernate.hbm2ddl.auto是hibernate的配置属性,其主要作用是:自动创建、更新、验证数据库表结构。该参数的几种配置如下:

  • create:每次加载hibernate时都会删除上一次的生成的表,然后根据你的model类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。
  • create-drop:每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
  • update:最常用的属性,第一次加载hibernate时根据model类会自动建立起表的结构(前提是先建立好数据库),以后加载hibernate时根据model类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等应用第一次运行起来后才会。
  • validate:每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。

然后新建一个【config】包,创建一个【MySQLConfig】类(上面的spring.jpa.properties.hibernate.dialect属性就要配置这里的类全路径):

package com.wmyskxz.demo.config;

import org.hibernate.dialect.MySQL5InnoDBDialect;

public class MySQLConfig extends MySQL5InnoDBDialect {
    @Override
    public String getTableTypeString() {
        return "ENGINE=InnoDB DEFAULT CHARSET=utf8";
    }
}

第二步:创建好需要的数据库

打开SQL服务,建表语句也很简单啦:

create database testdb;

第三步:创建实体类

实体类映射的实际上是数据库表的结构,在适当的包目录下(例如【entity】)下创建好实体类:

package com.wmyskxz.demo.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity // 表明这是个实体类
public class User {

    @Id // 表明这个属性是主键
    @GeneratedValue // 自增长
    private long id;
    @Column(nullable = false, unique = true)    // 不允许为空,属性唯一
    private String username;
    @Column(nullable = false)   // 不允许为空
    private String password;

    // getter and setter
}

第四步:DAO层

新建一个【repository】包,然后新建一个【UserRepository】接口,并继承JpaRepository类:

package com.wmyskxz.demo.repository;

import com.wmyskxz.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

继承JpaRepository需要传入两个参数,一个是实体类User一个是主键的类型Long,而凡是继承了JpaRepository类的就会自动实现很多内置的方法,包括增删改查,以及使用默认支持的Pageable对象来进行分页,默认的方法大致如下:

public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    List<T> findAll();
    List<T> findAll(Sort var1);
    List<T> findAllById(Iterable<ID> var1);
    <S extends T> List<S> saveAll(Iterable<S> var1);
    void flush();
    <S extends T> S saveAndFlush(S var1);
    void deleteInBatch(Iterable<T> var1);
    void deleteAllInBatch();
    T getOne(ID var1);
    <S extends T> List<S> findAll(Example<S> var1);
    <S extends T> List<S> findAll(Example<S> var1, Sort var2);
}

第五步:Controller层

新建【controller】包,新建一个【UserController】类,编写简单的增删改查代码:

package com.wmyskxz.demo.controoler;

import com.wmyskxz.demo.entity.User;
import com.wmyskxz.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;

import java.util.Optional;

@RestController // 表明这是一个Controller并返回JSON格式
public class UserController {
    @Autowired
    private UserRepository userRepository;

    @GetMapping("/getOne")
    public Optional<User> getOneUserById(@RequestParam long id) {
        return userRepository.findById(id);
    }

    @GetMapping("/all")
    public Iterable<User> getAllUsers(@RequestParam(value = "page", defaultValue = "0") int page,
                                      @RequestParam(value = "size", defaultValue = "5") int size) {
        page = page < 0 ? 0 : page;// 如果page为负数则修改为0,防止在首页点击上一页发生错误
        Sort sort = new Sort(Sort.Direction.DESC, "id");// 按id倒叙排列
        return userRepository.findAll(new PageRequest(page, size, sort));
    }

    @PostMapping("/add")
    public String addUser(@RequestParam String username,
                          @RequestParam String password) {
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        userRepository.save(user);// 注意这里是save
        return "Saved";
    }

    @DeleteMapping("/delete")
    public String deleteUserById(@RequestParam long id) {
        userRepository.deleteById(id);
        return "Deleted";
    }

    @PutMapping("/update")
    public String updateUser(User user) {
//        User user = new User();
//        user.setId(id);
//        user.setUsername(username);
//        user.setPassword(password);
        userRepository.save(user);
        return "Updated";
    }
}

上面就直接使用@Autowired自动引入了继承了JpaRepository的UserRepository接口,我们使用它默认的方法已经足够完成我们的基础功能了,值得一提的是我们的getAllUsers(...)方法,它往findAll()方法里传入了一个Pageable对象,这是Spring Data库中定义的一个接口,是所有分页相关信息的一个抽象,通过该接口,我们可以得到和分页相关的所有信息(例如pageNumberpageSize等),这样Jpa就能够通过Pageable参数来得到一个带分页信息的Sql语句。

当然上面我们是通过自己创建了一个Pageable对象,Spring也支持直接获取Pageable对象,可以把上面的getAllUsers(...)方法改写成下面这样:

@GetMapping("/all")
public Iterable<User> getAllUsers(@PageableDefault(value = 5, sort = {"id"}, direction = Sort.Direction.DESC) 
                                              Pageable pageable) {
    return userRepository.findAll(pageable);
}

默认从第0页开始,也可以自己传入一个page参数,跟上面的是一样的。

第六步:运行项目

上面我们就快速搭建起来了一个基于Spring Boot和JPA的REST风格的后台增删改查实例,我们把项目跑起来,可以看到数据库自动创建了一些表:

JPA帮我们创建的user表的创建SQL如下:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL,
  `password` varchar(255) NOT NULL,
  `username` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_sb8bbouer5wak8vyiiy4pf2bx` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

使用REST测试工具测试

完全符合我们的要求,然后我们使用一些REST的测试工具,来测试上面的功能是否都能正确运行,比如我这里使用的【Restlet Client】,在Chrome商店就可以下载到。

/all地址测试:

首先先来测试一下http://localhost:8080/all地址,由于现在数据库还是空的,所以可以看到返回如下:

{
    "content": [
    ],
    "pageable": {
        "sort": {
            "sorted": true,
            "unsorted": false,
            "empty": false
        },
        "offset": 0,
        "pageNumber": 0,
        "pageSize": 5,
        "unpaged": false,
        "paged": true
    },
    "totalElements": 0,
    "last": true,
    "totalPages": 0,
    "number": 0,
    "size": 5,
    "sort": {
        "sorted": true,
        "unsorted": false,
        "empty": false
    },
    "numberOfElements": 0,
    "first": true,
    "empty": true
}

添加用户测试:

然后我们使用http://localhost:8080/add?username=wmyskxz&password=123地址,添加几个类似的用户信息:

可以看到返回正确的Saved信息:

/getOne地址测试:

我们就直接使用http://localhost:8080/getOne?id=1来获取刚才添加的用户,可以看到返回正确的数据:

{
    "id": 1,
    "username": "wmyskxz",
    "password": "123"
}

修改用户测试:

然后我们使用http://localhost:8080/update?id=1&username=wmyskxz&password=123456来模拟进行用户密码的修改:

可以看到正确的更新信息Updated,再次查询用户,也能看到正确的数据:

{
    "id": 1,
    "username": "wmyskxz",
    "password": "123456"
}

分页测试:

我们使用添加功能为数据库添加5条以上的数据,然后进行一次查询/all,可以看到能够按照id倒叙排列后返回5条数据:

数据库的情况

返回的JSON数据如下:

{
    "content": [
        {
            "id": 10,
            "username": "wmyskxz8",
            "password": "123"
        },
        {
            "id": 9,
            "username": "wmyskxz7",
            "password": "123"
        },
        {
            "id": 8,
            "username": "wmyskxz6",
            "password": "123"
        },
        {
            "id": 7,
            "username": "wmyskxz5",
            "password": "123"
        },
        {
            "id": 6,
            "username": "wmyskxz4",
            "password": "123"
        }
    ],
    "pageable": {
        "sort": {
            "sorted": true,
            "unsorted": false,
            "empty": false
        },
        "offset": 0,
        "pageNumber": 0,
        "pageSize": 5,
        "unpaged": false,
        "paged": true
    },
    "totalElements": 9,
    "last": false,
    "totalPages": 2,
    "number": 0,
    "size": 5,
    "sort": {
        "sorted": true,
        "unsorted": false,
        "empty": false
    },
    "numberOfElements": 5,
    "first": true,
    "empty": false
}

删除用户测试:

使用地址http://localhost:8080/delete?id=1来删除ID为1的用户:

能正确看到Deleted信息,并查看数据能够看到数据已经被删除了。


以上,我们就快速搭建好了一个CRUD+分页的后台实例,还用了比较流行的RESTful风格,粗略的感受了一下JPA的方便,还是挺爽的..没有复杂的Mapper文件,不用自动生成实体,甚至不用管SQL,只需要专注在逻辑上就行了,其实简单使用的话以上的东西也能应付一些常见的场景了,后期再深入了解了解吧!

参考资料:
springboot(五):spring data jpa的使用——纯洁的微笑
springboot(十五):springboot+jpa+thymeleaf增删改查示例——纯洁的微笑
Spring Boot中使用Spring-data-jpa让数据访问更简单、更优雅——程序猿DD

欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz
欢迎关注公众微信号:wmyskxz
分享自己的学习 & 学习资料 & 生活
想要交流的朋友也可以加qq群:3382693

前言:写前台真的是我不擅长的东西…所以学习和写了很久很久…前台页面大概开发了两天半就开发好了,采用的静态的html和bootstrap来写,写后台的时候纠结住了…怎么说呢,写页面真的是头疼…

Bootstrap or Vue?

先吐槽一句..写页面是真的不擅长,然后限于时间的问题,我开始考虑换用Bootstrap来完成页面的编写,因为时间有限(我得在实习完之前把所有页面开发完),学习Vue对于我来说还是存在一些学习成本的,虽然之前已经通过文档博客之类的学得七七八八(emm..三三四四吧…)的样子,但我还是想尽可能的写好…还是用自己熟悉的Bootstrap来完成吧…

前台页面分析

个人博客的前台页面相对比较简单,大概也就这么五个页面搞定,因为有了原型图,所以实现起来还是挺容易;

页面展示

首页

emm…还原度还算蛮高的啦…而且发现这一套模板对于题图的要求蛮高的…如果不好看或者图片本身太小就会变得奇怪以及模糊…目前还没有找到啥好的方法解决这个图片的问题,不过倒是找到一个有趣的css属性叫object-fit: cover;,超级方便的图片居中方法,之前找了很多JS都不是很满意,大家可以试试…

博客分类页

这个页面还有待商榷…因为后面可能会换成没有题图而是只是博文信息的版本…

博文页

Ps:突然很想吐槽一下自己写的文章都有点长..专门找了篇内容没那么多的,不然截图工具截不完页面…

保存在数据库中的是md源码,在前台请求的时候,我直接转成了html源码弄出来,然后CSS样式是自己找的,跟我的公众微信号是一个,大家需要的话存一下吧,感觉还是挺好看的:

p{font-size:15px; line-height:28px; color:#595959;font-family:微软雅黑}
pre, code{font-size:14px; font-family: Roboto, 'Courier New', Consolas, Inconsolata, Courier, monospace;}
code{margin:0 3px; padding:0 6px; white-space: pre-wrap; background-color:#F8F8F8; border-radius:2px; display: inline;}
pre{font-size:15px; line-height:20px;}
precode{white-space: pre; overflow:auto; border-radius:3px; padding:5px10px; display: block !important;}
strong, b{color:#BF360C;}
em, i{color:#009688;}
big{font-size:22px; color:#009688; font-weight: bold; vertical-align: middle; border-bottom:1px solid #eee;}
small{font-size:12px; line-height:22px;}
hr{border-bottom:0.05em dotted #eee; margin:10px auto;}
p{margin:15px 5px!important;}
table, pre, dl, blockquote, q, ul, ol{margin:10px 5px;}
ul, ol{padding-left:10px;}
li{margin:5px;}
lip{margin:5px 0!important;}
ulul, ulol, olul, olol{margin:0; padding-left:10px;}
olol, ulol{list-style-type: lower-roman;}
ululol, ulolol, olulol, ololol{list-style-type: lower-alpha;}
dl{padding:0;}
dldt{font-size:1em; font-weight: bold; font-style: italic;}
dldd{margin:0 0 10px; padding:0 10px;}
blockquote, q{border-left:3px solid #009688; padding:0 10px; color:#777; quotes: none;}
blockquote::before, blockquote::after, q::before, q::after{content: none;}
h1, h2, h3, h4, h5, h6{margin:20px 0 10px; padding:0; font-weight: bold; color:#009688;}
h1{font-size:24px;}
h2{font-size:22px;}
h3{font-size:18px;}
h4{font-size:18px;}
h5{font-size:16px;}
h6{font-size:16px; color:#777;}
table{padding:0; border-collapse: collapse; border-spacing:0; font-size:1em; font: inherit; border:0;}
tbody{margin:0; padding:0; border:0;}
tabletr{border:0; border-top:1px solid #CCC; background-color: white; margin:0; padding:0;}
tabletr:nth-child(2n){background-color:#F8F8F8;}
tabletrth, tabletrtd{font-size:16px; border:1px solid #CCC; margin:0; padding:5px10px;}
tabletrth{font-weight: bold; background-color:#F0F0F0;}
img{display:block;text-align:center;margin:0 auto;}

关于/留言页

emmm…一大堆测试用的留言信息请无视…

简历页

这里简历也仅仅是包含了一个头和一个尾巴,其实已经做好了…做成的PDF文件可以网上搜索工具直接转成html代码,但并不是很想粘出来…


后台页面分析

当时分析的时候是大致分成了这几个模块和这么多个页面,但是后来实现的时候,一个模块都整成了一个页面,懒得复制粘贴再改改改,直接一个页面搞定;

页面展示

数据统计模块

看过我之前博文的童鞋应该都会觉得眼熟(什么?这不就是之前写天猫找的模板吗?),反正能省事儿就省,而且这一套模板既有满足自适应的要求,又有增强表格的js插件,为啥不用呢?好了,链接在这里:http://www.cssmoban.com/cssthemes/7381.shtml

这一个模块就是一个数据统计,包括有:访问量、日志量、留言数还有一些详细的列表;

emmm..自适应后大概像下面这样:

分类管理模块

我把分类相关的功能都写在了一个页面上,这样操作也方便;

博文管理模块

点击编辑会弹出一个模态框,跟新增博文差不太多:

评论管理模块

这个模块我后来改了一下,只是用来显示文章的评论信息,通过监听Select来动态加载数据,删除之后虽然是置数据库isEffective字段为0,但仍然不会在后台显示…(因为我感觉删除的都是不想看到的,就不显示了…)


项目总结

上面就是项目的所有页面了,虽然现在看起来还是挺清晰简单的,但写的时候还是头大,头大,头大…不过通过这一次的开发,对写前台还是有了一定的熟悉,特别是对JS的编写以及Bootstrap响应式布局,而且在写前端的时候测出来还是一堆BUG,也犯了一些拼写错误这样的低级错误…而且感觉前端更多的是细节的东西,因为毕竟是直接与用户进行交互的,还是希望自己多写多积累吧,下面对项目中出现的一些问题进行一些总结:

HTML公有资源的提取

前端会面临着和写后端一样的问题,就是怎样抽取出一些中间件,来让大家共同使用,例如头部、尾部,我采用的是下面这样的方法解决:

<script language="javascript" type="text/javascript" src="js/head.js"></script>

先编写好单独的html文件,然后转成JS动态加载进来就好了,参考文章:https://blog.csdn.net/wabiaozia/article/details/75092623

跨域问题

在前后端结合的时候,出现的第一个问题就是跨域,参考文章:https://blog.csdn.net/saytime/article/details/74937204 ,我是使用的Filter方法,下面是代码:

/**
 * 实现跨域
 *
 * @author:wmyskxz
 * @create:2018-06-21-下午 22:48
 */
@Component
public class SimpleCORSFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS ,PUT, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Connection, User-Agent, Cookie");
        chain.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void destroy() {}
}

但其实这样配置了之后,前端发的请求确实的能提交到服务器上来,服务器也能够做相应的处理,但AJAX中,PUT和POST方法老是进到error方法中而不进入success方法,我也是没辙…而且之前没有给Allow-Headers添加上除X-Requested-with之外的东西,POST的时候它会先提交一个头里面不光有X-Requested-with还有Content-TypeOPTIONS方法…也不知道为啥,最后只能妥协搞成现在这样;

时间显示的BUG

从数据库取出来的数据给到前台显示的时候就变成了一串时间戳,解决方法就是在实体类的getCreateBy()这样的取时间的操作前加上一行注解:@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8"),就能成功解决了:

font-awesome库显示问题

单独编写前台的时候都能够正常的显示字体,但是给弄到服务器文件夹下的时候就加载不出来了,也不是404找不到一类的,但字体就是请求不到,但是我点击控制台的信息又能成功下载到字体不知道是为啥,所以只能妥协给换成了CDN上的CSS:

<link href="//cdn.bootcss.com/font-awesome/4.4.0/css/font-awesome.min.css" rel="stylesheet">

错误页面跳转的问题

因为不想让除了我以外的人登录进后台页面,我也是想了各种办法,其中之一就是把login.html页面给“隐藏”起来了,具体的做法就是把它放进一个奇怪的文件夹,然后使用一个奇怪的地址(以后再改)来绑定它:

所以这样就会出现一个问题,因为我写的拦截器的原因,所有不是以/admin开头的地址如果请求错误的话,地址栏不变,会自动跳转到error.html文件中,因为我把这个页面放到了static根目录下,这是SpringBoot默认覆盖的结果,但是如果我在后台拦截器中使用重定向访问error.html的话就会出现404图片请求不到的问题,因为它会默认访问/admin/img/404.png这个地址(此时我后台的页面放在/admin/目录下),所以我不得不把error.htmlimg标签中的src改成一个网络地址;


项目地址 & 后期计划

Github地址:https://github.com/wmyskxz/MyBlog

因为前端(不包括后台页面)没有加任何的响应式的东西,就只是用了百分比来显示,但测试的时候不知道为啥手机端啊,小屏幕啊都还可以,特别是手机端它不会缩放看到的还是页面本身的样子..还是挺好的,又可以骗自己节省了一大部分的工作,hhh

马上面临着暑假,同时也意味着秋招快要开始了,还是准备复习复习基础(数据结构和算法),emmm…冲刺冲刺大厂,自己加油吧…

欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz
欢迎关注公众微信号:wmyskxz_javaweb
分享自己的Java Web学习之路以及各种Java学习资料

前言:在之前,我们已经完成了项目的基本准备,那么就可以开始后台开发了,突然又想到一个问题,就是准备的时候只是设计了前台的RESTful APIs,但是后台管理我们同样也是需要API的,那么就在这一篇里面一起实现了吧…

一些设计上的调整

在查了一些资料和吸收了一些评论给出良好的建议之后,我觉得有必要对一些设计进行一些调整:

  • 1)数据库:命名应该更加规范,比如表示分类最好用category而不是sort,表示评论最好用comment而不是message;
  • 2)RESful APIs:在准备着手开始写后台的时候就已经发现,本来想的是凡是以/api开头的都是暴露出来给前端用的,凡是以/admin开头的都是给后台使用的地址,但是意外的没有设计后天的API也把一些删除命令暴露给了前端,这就不好了重新设计设计;
  • 3)命名规范的问题:因为使用MyBatis逆向工程自动生成的时候,配置了一个useActualColumnNames使用表真正名称的东西,所以整得来生成POJO类基础字段有下划线,看着着实有点不爽,把它给干掉干掉…;

数据库调整

把字段规范了一下,并且删除了分类下是否有效的字段(感觉这种不经常变换的字段留着也没啥用干脆干掉..),所以调整为了下面这个样子(调整字段已标红):

然后重新使用生成器自动生成对应的文件,注意记得修改generatorConfig.xml文件中对应的数据库名称;

创建和修改时间的字段设置

通过查资料发现其实我们可以通过直接设置数据库来自动更新我们的modified_by字段,并且可以像设置初始值那样给create_by和modified_by两个字段以当前时间戳设置默认值,这里具体以tbl_article_info这张表为例:

CREATE TABLE `tbl_article_info` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `title` varchar(50) NOT NULL DEFAULT '' COMMENT '文章标题',
  `summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章简介,默认100个汉字以内',
  `is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '文章是否置顶,0为否,1为是',
  `traffic` int(10) NOT NULL DEFAULT '0' COMMENT '文章访问量',
  `create_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `modified_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改日期',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我们通过设置DEFAULTCURRENT_TIMESTAMP,然后给modified_by字段多添加了一句ON UPDATE CURRENT_TIMESTAMP,这样它就会在更新的时候将该字段的值设置为更新时间,这样我们就不用在后台关心这两个值了,也少写了一些代码(其实是写代码的时候发现可以这样偷懒..hhh…);

RESTful APIs重新设计

我们需要把一些不能够暴露给前台的API收回,然后再设计一下后台的API,捣鼓了一下,最后大概是这个样子了:

后台Restful APIs:

前台开放RESful APIs:

这些API只是用来和前端交互的接口,另外一些关于日志啊之类的东西就直接在后台写就行了,OK,这样就爽多了,可以开始着手写代码了;

基本配置

随着配置内容的增多,我逐渐的想要放弃.yml的配置文件,主要的一点是这东西不好对内容进行分类(下图是简单配置了一些基本文件后的.yml和.properties文件的对比)..

最后还是用回.properties文件吧,不分类还是有点难受

编码设置

我们首先需要解决的是中文乱码的问题,对应GET请求,我们可以通过修改Tomcat的配置文件【server.xml】来把它默认的编码格式改为UTF-8,而对于POST请求,我们需要统一配置一个拦截器一样的东西把请求的编码统一改成UTF-8:

## ——————————编码设置——————————
spring.http.encoding.charset=UTF-8
spring.http.encoding.force=true
spring.http.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8

但是这样设置之后,在后面的使用当中还是会发生提交表单时中文乱码的问题,在网上搜索了一下找到了解决方法,新建一个【config】包创建下面这样一个配置类:

@Configuration
public class MyWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {
    @Bean
    public HttpMessageConverter<String> responseBodyConverter() {
        StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
        return converter;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        converters.add(responseBodyConverter());
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false);
    }
}

数据库及连接池配置

决定这一次试试Druid的监控功能,所以给一下数据库的配置:

## ——————————数据库访问配置——————————
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=UTF-8
spring.datasource.username = root
spring.datasource.password = 123456

# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
# 配置获取连接等待超时的时间
spring.datasource.druid.max-wait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.druid.filters=stat,wall,log4j

日志配置

在SpringBoot中其实已经使用了Logback来作为默认的日志框架,这是log4j作者推出的新一代日志框架,它效率更高、能够适应诸多的运行环境,同时天然支持SLF4J,在SpringBoot中我们无需再添加额外的依赖就能使用,这是因为在spring-boot-starter-web包中已经有了该依赖了,所以我们只需要进行配置使用就好了

第一步:创建logback-spring.xml

当项目跑起来的时候,我们不可能还去看控制台的输出信息吧,所以我们需要把日志写到文件里面,在网上找到一个例子(链接:http://tengj.top/2017/04/05/springboot7/)

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <contextName>logback</contextName>
    <!--自己定义一个log.path用于说明日志的输出目录-->
    <property name="log.path" value="/log/wmyskxz/"/>
    <!--输出到控制台-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
             <level>ERROR</level>
         </filter>-->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--输出到文件-->
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/logback.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </root>

    <!-- logback为java中的包 -->
    <logger name="cn.wmyskxz.blog.controller"/>
</configuration>

在Spring Boot中你只要按照规则组织文件名,就能够使得配置文件能够被正确加载,并且官方推荐优先使用带有-spring的文件名作为日志的配置(如上面使用的logback-spring.xml,而不是logback.xml),满足这样的命名规范并且保证文件在src/main/resources下就好了;

第二步:重启项目检查是否成功

我们定义的目录位置为/log/wmyskxz/,但是在项目的根目录下并没有发现这样的目录,反而是在当前盘符的根目录..不是很懂这个规则..总之是成功了的..

打开是密密麻麻一堆跟控制台一样的【info】级别的信息,因为这个系统本身就比较简单,所以就没有必要去搞什么文本切割之类的东西了,ok..日志算是配置完成;

实际测试了一下,上线之后肯定需要调整输出级别的,不然日志文件就会特别大…

拦截器配置

我们需要对地址进行拦截,对所有的/admin开头的地址请求进行拦截,因为这是后台管理的默认访问地址开头,这是必须进行验证之后才能访问的地址,正如上面的RESTful APIs,这里包含了一些增加/删除/更改/编辑一类的操作,而统统这些操作都是不能够开放给用户的操作,所以我们需要对这些地址进行拦截:

第一步:创建User实体类

做验证还是需要添加session,不然不好弄,所以我们还是得创建一个常规的实体:

public class User {
    private String username;
    private String password;

    /* getter and setter */
}

第二步:创建拦截器并继承HandlerInterceptor接口

在【interceptor】包下新建一个【BackInterceptor】类并继承HandlerInterceptor接口:

public class BackInterceptor implements HandlerInterceptor {

    private static String username = "wmyskxz";
    private static String password = "123456";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        boolean flag = true;
        User user = (User) request.getSession().getAttribute("user");
        if (null == user) {
            flag = false;
        } else {
            // 对用户账号进行验证,是否正确
            if (user.getUsername().equals(username) && user.getPassword().equals(password)) {
                flag = true;
            } else {
                flag = false;
            }
        }
        return flag;
    }
}

在拦截器中,我们从session中取出了user,并判断是否符合要求,这里我们直接写死了(并没有更改密码的需求,但需要加密),而且我们并没有做任何的跳转操作,原因很简单,根本就不需要跳转,因为访问后台的用户只有我一个人,所以只需要我知道正确的登录地址就可以了…

第三步:在配置类中复写addInterceptors方法

刚才我们在设置编码的时候自己创建了一个继承自WebMvcConfigurerAdapter的设置类,我们需要复写其中的addInterceptors方法来为我们的拦截器添加配置:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用于添加拦截规则
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
    super.addInterceptors(registry);
}
  • 说明:这个方法也很简单,通过在addPathPatterns中添加拦截规则(这里设置拦截/admin开头的所有地址),并通过excludePathPatterns来排除拦截的地址(这里为/toLogin,即登录地址,到时候我可以弄得复杂隐蔽一点儿)

第四步:配置登录页面

以前我们在写Spring MVC的时候,如果需要访问一个页面,必须要在Controller中添加一个方法跳转到相应的页面才可以,但是在SpringBoot中增加了更加方便快捷的方法:

/**
 * 以前要访问一个页面需要先创建个Controller控制类,在写方法跳转到页面
 * 在这里配置后就不需要那么麻烦了,直接访问http://localhost:8080/toLogin就跳转到login.html页面了
 *
 * @param registry
 */
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/admin/login").setViewName("login.html");
    super.addViewControllers(registry);
}
  • 注意:login.html记得要放在【templates】下才会生效哦…(我试过使用login绑定视图名不成功,只能写全了…)

访问日志记录

上面我们设置了访问限制的拦截器,对后台访问进行了限制,这是拦截器的好处,我们同样也使用拦截器对于访问数量进行一个统计

第一步:编写前台访问拦截器

对照着数据库的设计,我们需要保存的信息都从request对象中去获取,然后保存到数据库中即可,代码也很简单:

public class ForeInterceptor implements HandlerInterceptor {

    @Autowired
    SysService sysService;

    private SysLog sysLog = new SysLog();
    private SysView sysView = new SysView();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 访问者的IP
        String ip = request.getRemoteAddr();
        // 访问地址
        String url = request.getRequestURL().toString();
        //得到用户的浏览器名
        String userbrowser = BrowserUtil.getOsAndBrowserInfo(request);

        // 给SysLog增加字段
        sysLog.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip);
        sysLog.setOperateBy(StringUtils.isEmpty(userbrowser) ? "获取浏览器名失败" : userbrowser);
        sysLog.setOperateUrl(StringUtils.isEmpty(url) ? "获取URL失败" : url);

        // 增加访问量
        sysView.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip);
        sysService.addView(sysView);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        // 保存日志信息
        sysLog.setRemark(method.getName());
        sysService.addLog(sysLog);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
  • 注意:但是需要注意的是测试的时候别把拦截器开了(主要是postHandle方法中中无法强转handler),不然不方便测试…

BrowserUtil是找的网上的一段代码,直接黏贴复制放【util】包下就可以了:

/**
 * 用于从Request请求中获取到客户端的获取操作系统,浏览器及浏览器版本信息
 *
 * @author:wmyskxz
 * @create:2018-06-21-上午 8:40
 */
public class BrowserUtil {
    /**
     * 获取操作系统,浏览器及浏览器版本信息
     *
     * @param request
     * @return
     */
    public static String getOsAndBrowserInfo(HttpServletRequest request) {
        String browserDetails = request.getHeader("User-Agent");
        String userAgent = browserDetails;
        String user = userAgent.toLowerCase();

        String os = "";
        String browser = "";

        //=================OS Info=======================
        if (userAgent.toLowerCase().indexOf("windows") >= 0) {
            os = "Windows";
        } else if (userAgent.toLowerCase().indexOf("mac") >= 0) {
            os = "Mac";
        } else if (userAgent.toLowerCase().indexOf("x11") >= 0) {
            os = "Unix";
        } else if (userAgent.toLowerCase().indexOf("android") >= 0) {
            os = "Android";
        } else if (userAgent.toLowerCase().indexOf("iphone") >= 0) {
            os = "IPhone";
        } else {
            os = "UnKnown, More-Info: " + userAgent;
        }
        //===============Browser===========================
        if (user.contains("edge")) {
            browser = (userAgent.substring(userAgent.indexOf("Edge")).split(" ")[0]).replace("/", "-");
        } else if (user.contains("msie")) {
            String substring = userAgent.substring(userAgent.indexOf("MSIE")).split(";")[0];
            browser = substring.split(" ")[0].replace("MSIE", "IE") + "-" + substring.split(" ")[1];
        } else if (user.contains("safari") && user.contains("version")) {
            browser = (userAgent.substring(userAgent.indexOf("Safari")).split(" ")[0]).split("/")[0]
                    + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];
        } else if (user.contains("opr") || user.contains("opera")) {
            if (user.contains("opera")) {
                browser = (userAgent.substring(userAgent.indexOf("Opera")).split(" ")[0]).split("/")[0]
                        + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];
            } else if (user.contains("opr")) {
                browser = ((userAgent.substring(userAgent.indexOf("OPR")).split(" ")[0]).replace("/", "-"))
                        .replace("OPR", "Opera");
            }

        } else if (user.contains("chrome")) {
            browser = (userAgent.substring(userAgent.indexOf("Chrome")).split(" ")[0]).replace("/", "-");
        } else if ((user.indexOf("mozilla/7.0") > -1) || (user.indexOf("netscape6") != -1) ||
                (user.indexOf("mozilla/4.7") != -1) || (user.indexOf("mozilla/4.78") != -1) ||
                (user.indexOf("mozilla/4.08") != -1) || (user.indexOf("mozilla/3") != -1)) {
            browser = "Netscape-?";

        } else if (user.contains("firefox")) {
            browser = (userAgent.substring(userAgent.indexOf("Firefox")).split(" ")[0]).replace("/", "-");
        } else if (user.contains("rv")) {
            String IEVersion = (userAgent.substring(userAgent.indexOf("rv")).split(" ")[0]).replace("rv:", "-");
            browser = "IE" + IEVersion.substring(0, IEVersion.length() - 1);
        } else {
            browser = "UnKnown, More-Info: " + userAgent;
        }

        return os + "-" + browser;
    }
}

第二步:设置拦截地址

还是在刚才的配置类中,新增这么一条:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用于添加拦截规则
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
    registry.addInterceptor(getForeInterceptor()).addPathPatterns("/**").excludePathPatterns("/toLogin","/admin/**");
    super.addInterceptors(registry);
}

设置默认错误页面

在SpringBoot中,默认的错误页面比较丑(如下),所以我们可以自己改得稍微好看一点儿,具体的教程在这里:http://tengj.top/2018/05/16/springboot13/ ,我就搞前台的时候再去弄了…


Service 层开发

这是纠结最久应该怎么写的,一开始我还准备老老实实地利用MyBatis逆向工程生成的一堆东西去给每一个实体创建一个Service的,这样其实就只是对Dao层进行了一层不必要的封装而已,然后通过分析其实主要的业务也就分成几个:文章/评论/分类/日志浏览量这四个部分而已,所以创建这四个Service就好了;

比较神奇的事情是在网上找到一种通用Mapper的最佳实践方法,整个人都惊了,“wtf?还可以这样写哦?”,资料如下:http://tengj.top/2017/12/20/springboot11/

emmmm..我们通过MyBatis的逆向工程,已经很大程度上简化了我们的开发,因为在Dao层我们已经免去了自己写SQL语句,自己写实体,自己写XML映射文件的麻烦,但在Service层我们仍然无可避免的要写一些类似功能的代码,有没有什么方法能把这些比较通用的方法给提取出来呢? 答案就在上面的链接中,oh,简直太酷了…我决定在这里介绍一下

通用接口开发

在Spring4中,由于支持了泛型注解,再结合通用Mapper,我们的想法得到了一个最佳的实践方法,下面我们来讲解一下:

第一步:创建通用接口

我们把一些常见的,通用的方法统一使用泛型封装在一个通用接口之中:

/**
 * 通用接口
 *
 * @author: wmyskxz
 * @create: 2018年6月15日10:27:04
 */
public interface IService<T> {

    T selectByKey(Object key);

    int save(T entity);

    int delete(Object key);

    int updateAll(T entity);

    int updateNotNull(T entity);

    List<T> selectByExample(Object example);

}

第二步:实现通用接口类

/**
 * 通用Service
 *
 * @param <T>
 */
public abstract class BaseService<T> implements IService<T> {

    @Autowired
    protected Mapper<T> mapper;

    public Mapper<T> getMapper() {
        return mapper;
    }

    /**
     * 说明:根据主键字段进行查询,方法参数必须包含完整的主键属性,查询条件使用等号
     *
     * @param key
     * @return
     */
    @Override
    public T selectByKey(Object key) {
        return mapper.selectByPrimaryKey(key);
    }

    /**
     * 说明:保存一个实体,null的属性也会保存,不会使用数据库默认值
     *
     * @param entity
     * @return
     */
    @Override
    public int save(T entity) {
        return mapper.insert(entity);
    }

    /**
     * 说明:根据主键字段进行删除,方法参数必须包含完整的主键属性
     *
     * @param key
     * @return
     */
    @Override
    public int delete(Object key) {
        return mapper.deleteByPrimaryKey(key);
    }

    /**
     * 说明:根据主键更新实体全部字段,null值会被更新
     *
     * @param entity
     * @return
     */
    @Override
    public int updateAll(T entity) {
        return mapper.updateByPrimaryKey(entity);
    }

    /**
     * 根据主键更新属性不为null的值
     *
     * @param entity
     * @return
     */
    @Override
    public int updateNotNull(T entity) {
        return mapper.updateByPrimaryKeySelective(entity);
    }

    /**
     * 说明:根据Example条件进行查询
     * 重点:这个查询支持通过Example类指定查询列,通过selectProperties方法指定查询列
     *
     * @param example
     * @return
     */
    @Override
    public List<T> selectByExample(Object example) {
        return mapper.selectByExample(example);
    }
}

至此呢,我们的通用接口就开发完成了

第三步:使用通用接口

编写好我们的通用接口之后,使用就变得很方便了,只需要继承相应的通用接口或者通用接口实现类,然后进行简单的封装就行了,下面以SortInfo为例:

public interface SortInfoService extends IService<SortInfo> {
}
========================分割线========================
/**
 * 分类信息Service
 *
 * @author:wmyskxz
 * @create:2018-06-15-上午 11:14
 */
@Service
public class SortInfoServiceImpl extends BaseService<SortInfo> implements SortInfoService {
}

对应到SortInfo的RESTful API设计,这样简单的继承就能够很好的支持,但是我们还是使用最原始的方式来创建吧

Service接口申明

查了一些资料,问了一下实习公司的前辈老师,并且根据我们之前设计好的RESTful APIs,我们很有必要搞一个dto层用于前后端之间的数据交互,这一层主要是对数据库的数据进行一个封装整合,也方便前后端的数据交互,所以我们首先就需要分析在dto层中应该存在哪些数据:

DTO层开发

对应我们的业务逻辑和RESTful APIs,我大概弄了下面几个Dto:

① ArticleDto:

该Dto封装了文章的详细信息,对应RESTful API中的/api/article/{id}——通过文章ID获取文章信息

/**
 * 文章信息类
 * 说明:关联了tbl_article_info/tbl_article_content/tbl_article_category/tbl_category_info/
 * tbl_article_picture五张表的基础字段
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:13
 */
public class ArticleDto {

    // tbl_article_info基础字段
    private Long id;
    private String title;
    private String summary;
    private Boolean isTop;
    private Integer traffic;

    // tbl_article_content基础字段
    private Long articleContentId;
    private String content;

    // tbl_category_info基础字段
    private Long categoryId;
    private String categoryName;
    private Byte categoryNumber;

    // tbl_article_category基础字段
    private Long articleCategoryId;

    // tbl_article_picture基础字段
    private Long articlePictureId;
    private String pictureUrl;

    /* getter and setter */
}

②ArticleCommentDto:

该Dto封装的事文章的评论信息,对应/api/comment/article/{id}——通过文章ID获取某一篇文章的全部评论信息

/**
 * 文章评论信息
 * 说明:关联了tbl_comment和tbl_article_comment两张表的信息
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:09
 */
public class ArticleCommentDto {
    // tbl_comment基础字段
    private Long id;                // 评论id
    private String content;         // 评论内容
    private String name;            // 用户自定义的显示名称
    private String email;
    private String ip;

    // tbl_article_comment基础字段
    private Long articleCommentId;  // tbl_article_comment主键
    private Long articleId;         // 文章ID

    /* getter and setter */
}

③ArticleCategoryDto:

该Dto是封装了文章的一些分类信息,对应/admin/category/{id}——获取某一篇文章的分类信息

/**
 * 文章分类传输对象
 * 说明:关联了tbl_article_category和tbl_category_info两张表的数据
 *
 * @author:wmyskxz
 * @create:2018-06-20-上午 8:45
 */
public class ArticleCategoryDto {

    //  tbl_article_category表基础字段
    private Long id;            // tbl_article_category表主键
    private Long categoryId;    // 分类信息ID
    private Long articleId;     // 文章ID

    // tbl_category_info表基础字段
    private String name;        // 分类信息显示名称
    private Byte number;        // 该分类下对应的文章数量

    /* getter and setter */
}

④ArticleWithPictureDto:

该Dto封装了文章用于显示的基本信息,对应所有的获取文章集合的RESful APIs

/**
 * 带题图信息的文章基础信息分装类
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:53
 */
public class ArticleWithPictureDto {
    // tbl_article_info基础字段
    private Long id;
    private String title;
    private String summary;
    private Boolean isTop;
    private Integer traffic;

    // tbl_article_picture基础字段
    private Long articlePictureId;
    private String pictureUrl;

    /* getter and setter */
}

Service接口开发

Service层其实就是对我们业务的一个封装,所以有了RESTful APIs文档,我们可以很轻易的写出对应的业务模块:

文章Service

/**
 * 文章Service
 * 说明:ArticleInfo里面封装了picture/content/category等信息
 */
public interface ArticleService {

    void addArticle(ArticleDto articleDto);

    void deleteArticleById(Long id);

    void updateArticle(ArticleDto articleDto);

    void updateArticleCategory(Long articleId, Long categoryId);

    ArticleDto getOneById(Long id);

    ArticlePicture getPictureByArticleId(Long id);

    List<ArticleWithPictureDto> listAll();

    List<ArticleWithPictureDto> listByCategoryId(Long id);

    List<ArticleWithPictureDto> listLastest();
}

分类Service

/**
 * 分类Service
 */
public interface CategoryService {
    void addCategory(CategoryInfo categoryInfo);

    void deleteCategoryById(Long id);

    void updateCategory(CategoryInfo categoryInfo);

    void updateArticleCategory(ArticleCategory articleCategory);

    CategoryInfo getOneById(Long id);

    List<CategoryInfo> listAllCategory();

    ArticleCategoryDto getCategoryByArticleId(Long id);
}

留言Service

/**
 * 留言的Service
 */
public interface CommentService {
    void addComment(Comment comment);

    void addArticleComment(ArticleCommentDto articleCommentDto);

    void deleteCommentById(Long id);

    void deleteArticleCommentById(Long id);

    List<Comment> listAllComment();

    List<ArticleCommentDto> listAllArticleCommentById(Long id);
}

系统Service

/**
 * 日志/访问统计等系统相关Service
 */
public interface SysService {
    void addLog(SysLog sysLog);

    void addView(SysView sysView);

    int getLogCount();

    int getViewCount();

    List<SysLog> listAllLog();

    List<SysView> listAllView();
}

Controller 层开发

Controller层简单理解的话,就是用来获取数据的,所以只要Service层开发好了Controller层就很容易,就不多说了,只是我们可以把一些公用的东西放到一个BaseController中,比如引入Service:

/**
 * 基础控制器
 *
 * @author:wmyskxz
 * @create:2018-06-19-上午 11:25
 */
public class BaseController {
    @Autowired
    ArticleService articleService;
    @Autowired
    CommentService commentService;
    @Autowired
    CategoryService categoryService;
}

然后前后台的控制器只需要继承该类就行了,这样的方式非常值得借鉴的,只是因为这个系统比较简单,所以这个BaseController,我看过一些源码,可以在里面弄一个通用的用于返回数据的方法,比如分页数据/错误信息之类的;


记录坑

1)MyBatis中Text类型的坑

按照《阿里手册》(简称)上所规范的那样,我把文章的content单独弄成了一张表并且将这个“可能很长”的字段的类型设置成了text类型,但是MyBatis逆向工程自动生成的时候,却把这个text类型的字段单独给列了出去,即在生成的xml中多出了一个<resultMap>,标识id为ResultMapWithBLOBs,MyBatis这样做可能的原因还是怕这个字段太长影响前面的字段查询吧,但是操作这样的LONGVARCHAR类型的字段MyBatis好像并没有集成很好,所以想要很好的操作还是需要给它弄成VARCHAR类型才行;

在generatorConfig.xml中配置生成字段的时候加上这样一句话就好了:

<table domainObjectName="ArticleContent" tableName="tbl_article_content">  
    <columnOverride column="content" javaType="java.lang.String" jdbcType="VARCHAR" />  
</table>  

2)拦截器中Service注入为null的坑

在编写前台拦截器的时候,我使用@Autowired注解自动注入了SysService系统服务Service,但是却报nullpointer的错,发现是没有自动注入上,SysService为空..这是为什么呢?排除掉注解没有识别或者没有给Service添加上注解的可能性之后,我发现好像是拦截器拦截的时候Service并没有创建成功造成的,参考这篇文章:https://blog.csdn.net/slgxmh/article/details/51860278,成功解决问题:

@Bean
public HandlerInterceptor getForeInterceptor() {
    return new ForeInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用于添加拦截规则
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
   registry.addInterceptor(getForeInterceptor()).addPathPatterns("/**").excludePathPatterns("/toLogin", "/admin/**");
    super.addInterceptors(registry);
}

其实就是添加上@Bean注解让ForeInterceptor提前加载;

3)数据库sys_log表中operate_by字段的坑

当时设计表的时候,就只是单纯的想要保存一下用户使用的浏览器是什么,其实当时并不知道应该怎么获取获取到的东西又是什么,只是觉得保存浏览器20个字段够了,但后来发现这是很蠢萌的…所以不得不调整数据库的字段长度,好在只需要单方面调整数据库的字段长度就好了:

4)保存文章的方式的坑

因为我想要在数据库中保存的是md源码,而返回前台前端希望的是直接拿到html代码,这样就能很方便的输出了,所以这要怎么做呢?找到一篇参考文章:https://my.oschina.net/u/566591/blog/1535380

我们不要搞那么复杂的封装,只要简单弄一个工具类就可以了,在【util】包下新建一个【Markdown2HtmlUtil】:

/**
 * Markdown转Html工具类
 *
 * @author:wmyskxz
 * @create:2018-06-21-上午 10:09
 */
public class Markdown2HtmlUtil {
    /**
     * 将markdown源码转换成html返回
     *
     * @param markdown md源码
     * @return html代码
     */
    public static String markdown2html(String markdown) {
        MutableDataSet options = new MutableDataSet();
        options.setFrom(ParserEmulationProfile.MARKDOWN);
        options.set(Parser.EXTENSIONS, Arrays.asList(new Extension[]{TablesExtension.create()}));
        Parser parser = Parser.builder(options).build();
        HtmlRenderer renderer = HtmlRenderer.builder(options).build();

        Node document = parser.parse(markdown);
        return renderer.render(document);
    }
}

使用也很简单,只需要在获取一篇文章的时候把ArticleDto里面的md源码转成html代码再返回给前台就好了:

/**
 * 通过文章的ID获取对应的文章信息
 *
 * @param id
 * @return 自己封装好的文章信息类
 */
@ApiOperation("通过文章ID获取文章信息")
@GetMapping("article/{id}")
public ArticleDto getArticleById(@PathVariable Long id) {
    ArticleDto articleDto = articleService.getOneById(id);
    articleDto.setContent(Markdown2HtmlUtil.markdown2html(articleDto.getContent()));
    return articleDto;
}

样式之类的交给前台就好了,搞定…


简单总结

关于统计啊日志类的Controller还没有开发,RESful API也没有设计,这里就先发布文章了,因为好像时间有点紧,后台的页面暂时可能开发不完,准备直接开始前台页面显示的开发(主要是自己对前端不熟悉还要学习..),这里对后台进行一个简单的总结:

其实发现当数据库设计好了,RESful APIs设计好了之后,后台的任务变得非常明确,开发起来也就思路很清晰了,只是自己还是缺少一些必要的经验,如对一些通用方法的抽象/层与层之间数据交互的典型设计之类的东西,特别是一些安全方面的东西,网上的资料也比较少一些,也是自己需要学习的地方;

欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz
欢迎关注公众微信号:wmyskxz_javaweb
分享自己的Java Web学习之路以及各种Java学习资料

前言:很早之前就想要写一个自己的博客了,趁着现在学校安排的实习有很多的空档,决定把它给做出来,也顺便完成实习的任务(搞一个项目出来…)

需求分析

总体目标:设计一套自适应/简洁/美观/易于文章管理发布的一个属于我个人的博客,最后一页能展示我个人的简历,因为大三快结束了马上就该去找工作了…哦忘了,最重要的还是要支持Markdown才行,因为已经习惯了…

前端需求分析

首先,前端的页面要求是:

  • ①简洁/美观——个人很喜欢像Mac那样的简洁风,越简单越好,当然也得好看;
  • ②最好是单页面——单页面的目的一方面是为了简洁,另一方面也是为了实现起来比较简单;
  • ③自适应——至少能适配常见的手机分辨率吧,我可不希望自己的博客存在显示差异性的问题;

然后,思考了一下可能出现的页面

1)首页:

  • 最新的文章——我可以搞一个轮播之类的东西用来显示最新的几篇博文;
  • 顶部导航——导航栏可以要有,而且可以提出来搞成通用的;
  • 联系方式——首页最好再有能一眼找到我的联系方式,比如简书/博客园/微信公众号之类的;
  • 时间——摁,时间;

2)文章页:

  • 分类栏——左侧应该有文章的分类,记得要有一个全部文章;
  • 文章列表——分类栏的右边就应该是该分类下的所有文章;
  • 分页栏——考虑到现在我的Java Web分栏下的文章已经有那么多了,还是有必要搞个分页;

3)简历页:

这是预留的页面,到时候用来显示个人的简历;

4)关于页:

用来介绍项目的搭建编写过程,还有使用的技术栈啊之类的,然后留下个人的联系方式,Nice;

5)留言页:

因为是个人的博客,所以我并不想要限制看官们留言的权利,我希望他们能自己能定义用于显示的用户名,但是需要填写一个Email,不然搞得我不能回复,那搞个啥…当然也可以不留Email,也就是不希望得到回复呗(那可能有些留言会让我难受死吧..思考…)…

后台需求分析:

最初的思考是这样的:

后来一想,文章置顶这个都给忘了…然后发现其实有一个很关键的问题就是Markdown的文章应该怎样保存?一开始还是想要保存为.md文件保存在服务器的硬盘上的,但想想直接保存在数据库里也不错,省的麻烦,而且我很明确一点的是:我并不会直接在博客上写Markdown,因为有许许多多成熟的产品能让我写的舒心的多,我没必要去搞这么麻烦复杂繁琐,而且不一定好,所以我只需要用博客来展示我写的Markdown格式的博文就好了,Nice啊…又成功骗自己少写了好多代码hhhhh(没有啦..需求就这样的嘛…)

顺着这样的思路,我通常写文都是先在简书上写好的,并且简书有一个特点是所有的图片,不管是已经发布的文章还是没有发布的私人文章,都能通过地址取得,可以利用这一点让简书当个图床,诶又少弄了一部分代码,然后分析分析着就把需求搞成下面这样了:

1)博文管理:

这个比较常规,就不说了;

2)网站数据统计:

作为网站的拥有者和设计者,我当然希望能希望知道这些数据了,然后单独作为拥有者来说,最好再分为日访问量/月访问量/总访问量这样子显示出来,再搞搞样式,简直不要太爽;

3)缓存管理:

图片就没缓存了,因为保存文章内容我需要保存md源码,所以可能需要在Redis里缓存最近常访问的文章的md转HTML后渲染好的HTML源码;

4)系统设置:

网站标题可以改呀,然后导航栏的名字也可以弄弄呀,其实这个也可以不用去搞,只是以防有时候心情不好给一整捣鼓可能心情就好了,hhhhh….;

5)留言管理:

有一些流氓留言可以删掉,最近学习到的比较好的方法是让该条数据的状态为置为0,而不是直接删除该条数据,这个设计数据库的时候就需要多设计一个字段,也可以通过用户留下的Email地址进行回复,最好搞一个自动通知,完美;


表结构设计

通过需求分析,然后严格按照《阿里巴巴Java开发手册》(下面所说的规范均来自于此)反复分析了很多遍,最终确定了如下的几张表:

然后来具体说一下各个表:

1)日志表(sys_log):

CREATE TABLE `sys_log` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `ip` varchar(20) NOT NULL DEFAULT '' COMMENT '操作地址的IP',
  `create_by` datetime NOT NULL COMMENT '操作时间',
  `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '操作内容',
  `operate_url` varchar(50) NOT NULL DEFAULT '' COMMENT '操作的访问地址',
  `operate_by` varchar(20) DEFAULT '' COMMENT '操作的浏览器',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这张表就是拿来保存日志,用来记录每一个用户访问了哪些地址,使用了什么样的浏览器,操作内容可以作为一个保留字段,如果以后想要监控用户行为,那也是可以的~

这里首先遵守的规范是(下面雷同则不再重复赘述):

  • 第五章第一节第2条(强制)——表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑;
  • 第五章第一节第3条(强制)——表名不使用复数名词;
  • 第五章第一节第10条(推荐)——表的命名最好加上“业务名称_表的作用”

想要拿出来跟大家讨论的一则规范是:

  • 第五章第9条(强制)——表必备三个字段:id(unsigned bigint自增),gmt_create(date_time),gmt_modified(date_time)

像如上设计的日志表,它插入进去了就不会再更新了,而且对于我这个系统也很大概率不会有趣操作这个表的可能,那么对于这样不会更新和操作的表,gmt_modified这个字段还有必要存在吗?

emmm..事实上我问了孤尽大大本人,他回答的简洁有力:“要的,以备不时之需;”然而原谅我还是没有听话,hhhhh,另外一点我想说的是,我忘了是在哪里看到的了,但是像gmt_create这样的字段最好设计成create_by这样,字段本身就是很好的注释,摁,就喜欢这样满满的细节…

2)浏览量表(sys_view):

CREATE TABLE `sys_view` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT,
  `ip` varchar(20) NOT NULL COMMENT '访问IP',
  `create_by` datetime NOT NULL COMMENT '访问时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这张表用于保存每一次访问主页的记录,我想的是每访问主页就记录增加一条数据,简单同时也增加访问量嘛,hhhhh,也是不会更新的一张表,所以没modifield_by字段;

3)留言/评论表(tbl_message)

CREATE TABLE `tbl_message` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `content` varchar(200) NOT NULL DEFAULT '' COMMENT '留言/评论内容',
  `create_by` datetime NOT NULL COMMENT '创建日期',
  `email` varchar(20) NOT NULL DEFAULT '' COMMENT '邮箱,用于回复消息',
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户自己定义的名称',
  `ip` varchar(20) NOT NULL DEFAULT '' COMMENT '留言/评论IP',
  `is_effective` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,默认为1为有效,0为无效',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='因为message分为两种,一种是留言,一种是评论,这里搞成一张表是因为它们几乎是拥有相同的字段,我觉得没必要分成两张表来进行维护';

这是评论/留言表,因为考虑到留言和评论有几乎相同的字段,所以给弄成了一张表,这张表同样的不需要更新没有modifield_by字段,这里遵守的规范是:

  • 第五章第一节第1条(强制)——表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1表示是,0表示否)
  • 第五章第一节第15条(参考)——设置合适的字段存储长度,不但可以节约数据库表控件和索引存储,更重要的事能够提升检索速度;

4)分类信息表(tbl_sort_info):

CREATE TABLE `tbl_sort_info` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL COMMENT '分类名称',
  `number` tinyint(10) NOT NULL DEFAULT '0' COMMENT '该分类下的文章数量',
  `create_by` datetime NOT NULL COMMENT '分类创建时间',
  `modified_by` datetime NOT NULL COMMENT '分类修改时间',
  `is_effective` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,默认为1有效,为0无效',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这张表是文章的分类,一开始都忘记设计了….

5)文章信息表(tbl_article_info):

CREATE TABLE `tbl_article_info` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `title` varchar(50) NOT NULL DEFAULT '' COMMENT '文章标题',
  `summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章简介,默认100个汉字以内',
  `is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '文章是否置顶,0为否,1为是',
  `traffic` int(10) NOT NULL DEFAULT '0' COMMENT '文章访问量',
  `create_by` datetime NOT NULL COMMENT '创建时间',
  `modified_by` datetime NOT NULL COMMENT '修改日期',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这是文章信息表,都是一些基础常用的字段就不再多做解释了

6)文章内容表(tbl_article_content):

CREATE TABLE `tbl_article_content` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT,
  `content` text NOT NULL,
  `article_id` bigint(40) NOT NULL COMMENT '对应文章ID',
  `create_by` datetime NOT NULL COMMENT '创建时间',
  `modifield_by` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这是文章内容表,我们并没有直接把内容字段设计在文章信息表里,而是单独建了一个表用来保存文章的内容,然后使用主键来关联,我们这里遵守的规范是:

  • 第五章第一节第8条(强制)——varchar是可变长字符串,不预先分配存储空间,长度不要超过5000个字符。如果存储长度大于此值,则应定义字段类型为text,独立出来一张表,用主键来对应,避免影响其他字段的索引效率;
  • 第五章第三节第6条(强制)——不得使用外键与级联,一切外键概念必须在应用层解决;

我试过我现在最长的一篇文章长度大概能存储8W长度的varchar,所以我就给单独建一个表分离出来了,使用text类型来保存文章的md源码

7)文章评论表(tbl_article_message):

CREATE TABLE `tbl_article_message` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT,
  `article_id` bigint(40) NOT NULL COMMENT '文章ID',
  `message_id` bigint(40) NOT NULL COMMENT '对应的留言ID',
  `create_by` datetime NOT NULL COMMENT '创建时间',
  `is_effective` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,默认为1有效,置0无效',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这其实是一个关联表,关联了文章和tbl_message表,用于专门存储某个文章下的评论信息

8)文章分类表(tbl_article_sort):

CREATE TABLE `tbl_article_sort` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT,
  `sort_id` bigint(40) NOT NULL COMMENT '分类id',
  `article_id` bigint(40) NOT NULL COMMENT '文章id',
  `create_by` datetime NOT NULL COMMENT '创建时间',
  `modified_by` datetime NOT NULL COMMENT '更新时间',
  `is_effective` tinyint(1) DEFAULT '1' COMMENT '表示当前数据是否有效,默认为1有效,0则无效',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

同样是一张关联表,连接了文章和分类,并且同一篇文章能属于多个分类;

9)文章题图表(tbl_article_picture):

CREATE TABLE `tbl_article_picture` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT,
  `article_id` bigint(40) NOT NULL COMMENT '对应文章id',
  `picture_url` varchar(100) NOT NULL DEFAULT '' COMMENT '图片url',
  `create_by` datetime NOT NULL COMMENT '创建时间',
  `modified_by` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='这张表用来保存题图url,每一篇文章都应该有题图';

这个是保存每一篇文章的题图,每一篇文章都因该有题图;


原型设计

事实上,我是直接先去找的原型,去参考了一下大概我需要做成什么样子…

前端原型参考

在这里先给大家推荐一个设计网站吧,找素材啊之类的还挺方便的:

站酷:http://www.zcool.com.cn/

所以我在里面找到了我想要的前端原型,大概就像这个样子:

1)首页:

2)博客页:

3)博文详情页:

4)博文列表页:

不能再酷了..

后端原型参考

emmmm…大概就像这样了吧,具体的样式可以到时候再调…

总体是酷的就行!


项目搭建

先来介绍一下这次想要使用的一些技术:

  • SpringBoot / Spring 来编写后台
  • Vue 来写页面,准备抛弃一下JSP,虽然现在Vue还啥都不懂,学呗
  • MyBatis 用于ORM,喜欢这玩意儿的逆向工程
  • RESTful API / JSON 交互
  • Redis 可能还会使用这个来缓存一下md转换之后的html源码

SpringBoot 工程搭建

SpringBoot 项目搭建过程就不再赘述了,不熟悉的童鞋戳这边:https://www.jianshu.com/p/70963ab49f8c,这里就简单给一下配置信息:

后台肯定是需要加安全验证的,要简单点我可以搞一个拦截器来简单弄弄,也可以用现有的安全框架,这里暂时就不加入这方面的东西了,把基本的弄进来就OK,然后它默认加入的东西不能够支持我们的业务,所以还需要手动添加进一些包:



    4.0.0

    cn.wmyskxz
    blog
    0.0.1-SNAPSHOT
    jar

    blog
    Demo project for Spring Boot

    
        org.springframework.boot
        spring-boot-starter-parent
        2.0.2.RELEASE
         
    

    
        UTF-8
        UTF-8
        1.8
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            1.3.2
        
        
        
            org.mybatis.generator
            mybatis-generator-core
            1.3.6
        

        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
        
            mysql
            mysql-connector-java
            runtime
        

        
        
            org.springframework.boot
            spring-boot-devtools
            true 
        

    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


热部署还是要的呀,然后再在【resrouces】下新建一个【banner.txt】文件,修改一下SpringBoot启动的提示信息:

 __      __                                 __                        
/\ \  __/\ \                               /\ \                       
\ \ \/\ \ \ \    ___ ___    __  __     ____\ \ \/'\    __  _  ____    
 \ \ \ \ \ \ \ /' __` __`\ /\ \/\ \   /',__\\ \ , <   /\ \/'\/\_ ,`\  
  \ \ \_/ \_\ \/\ \/\ \/\ \\ \ \_\ \ /\__, `\\ \ \\`\ \/>  </\/_/  /_ 
   \ `\___x___/\ \_\ \_\ \_\\/`____ \\/\____/ \ \_\ \_\/\_/\_\ /\____\
    '\/__//__/  \/_/\/_/\/_/ `/___/> \\/___/   \/_/\/_/\//\/_/ \/____/
                                /\___/                                
                                \/__/                                 

弄弄结构,最后整个项目的目录看起来大概是这个样子:

下面对这些目录进行一些简要的说明:

  • controller:控制器
  • dao:实际上这个包可以改名叫mapper,因为里面放的应该是MyBatis逆向工程自动生成之后的mapper类,还是叫dao吧,传统…
  • entity:实体类,还会有一些MyBatis生成的example
  • generator:MyBatis逆向工程生成类
  • interceptor:SpringBoot 拦截器
  • service:Service层,里面还有一层impl目录
  • util:一些工具类可以放在里面
  • mapper:用于存放MyBatis逆向工程生成的.xml映射文件
  • static:这个目录存放一些静态文件,简单了解了一下Vue的前后端分离,前台文件以后也需要放在这个目录下面

然后我使用application.yml文件代替了application.properties,这个东西结构清晰一点儿,反正用哪个都无所谓,配置好就OK了:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=UTF-8
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    #Druid连接池配置相关
    druid:
      # 初始大小,最大,最小
      initial-size: 5
      min-idle: 5
      max-active: 20
      # 配置获取连接等待超时的时间
      max-wait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000

不需要检测数据库,不要整这么复杂,不过倒是需要给数据库密码加个密,明文的配置实在不安全,但是现在先不搞了;

MyBatis 逆向工程

使用过MyBatis逆向工程的朋友都应该知道,这东西有个BUG,就是重复生成的时候它并不会覆盖掉原来的内容(特指xml映射文件),而是会在后面重新生成一遍,这有点儿头疼,所以首先需要解决这个问题:

首先在【util】包下新建一个【OverIsMergeablePlugin】工具类:

package cn.wmyskxz.blog.util;

import org.mybatis.generator.api.GeneratedXmlFile;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;

import java.lang.reflect.Field;
import java.util.List;

/**
 * 避免MyBatiis重复生成的工具类
 *
 * @author:wmyskxz
 * @create:2018-06-14-上午 9:50
 */
public class OverIsMergeablePlugin extends PluginAdapter {
    @Override
    public boolean validate(List<String> warnings) {
        return true;
    }

    @Override
    public boolean sqlMapGenerated(GeneratedXmlFile sqlMap, IntrospectedTable introspectedTable) {
        try {
            Field field = sqlMap.getClass().getDeclaredField("isMergeable");
            field.setAccessible(true);
            field.setBoolean(sqlMap, false);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }
}

然后在【generatorConfig.xml】中配置上该工具类:

<plugin type="cn.wmyskxz.blog.util.OverIsMergeablePlugin"/>

好的这样就搞定了,我们就正式开始我们的逆向工程:

1)编写generatorConfig.xml逆向工程配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>

    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!--避免生成重复代码的插件-->
        <plugin type="cn.wmyskxz.blog.util.OverIsMergeablePlugin"/>

        <!--是否在代码中显示注释-->
        <commentGenerator>
            <property name="suppressDate" value="true"/>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <!--数据库链接地址账号密码-->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=UTF-8" userId="root"
                        password="123456">
        </jdbcConnection>

        <!--生成pojo类存放位置-->
        <javaModelGenerator targetPackage="cn.wmyskxz.blog.entity" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>
        <!--生成xml映射文件存放位置-->
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>
        <!--生成mapper类存放位置-->
        <javaClientGenerator type="XMLMAPPER" targetPackage="cn.wmyskxz.blog.dao" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!--生成对应表及类名-->
        <table tableName="sys_log" domainObjectName="SysLog" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false">
            <!--使用自增长键-->
            <property name="my.isgen.usekeys" value="true"/>
            <!--使用数据库中实际的字段名作为生成的实体类的属性-->
            <property name="useActualColumnNames" value="true"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table tableName="sys_view" domainObjectName="SysView" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="true"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table tableName="tbl_article_content" domainObjectName="ArticleContent" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="true"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table tableName="tbl_article_info" domainObjectName="ArticleInfo" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="true"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table tableName="tbl_article_message" domainObjectName="ArticleMessage" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="true"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table tableName="tbl_message" domainObjectName="Message" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="true"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table tableName="tbl_sort_info" domainObjectName="SortInfo" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="true"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table tableName="tbl_article_sort" domainObjectName="ArticleSort" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false">
            <property name="my.isgen.usekeys" value="true"/>
            <property name="useActualColumnNames" value="true"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>

    </context>
</generatorConfiguration>

注意表名/生成目标目录之类的有没有写错,表名最好就直接去复制数据库中的名称;

2)编写逆向工程生成类:

package cn.wmyskxz.blog.generator;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.exception.XMLParserException;
import org.mybatis.generator.internal.DefaultShellCallback;

import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * MyBatis逆向工程生成类
 *
 * @author:wmyskxz
 * @create:2018-06-14-上午 10:10
 */
public class MybatisGenerator {

    public static void main(String[] args) throws Exception {
        String today = "2018-6-14";

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date now = sdf.parse(today);
        Date d = new Date();

        if (d.getTime() > now.getTime() + 1000 * 60 * 60 * 24) {
            System.err.println("——————未成成功运行——————");
            System.err.println("——————未成成功运行——————");
            System.err.println("本程序具有破坏作用,应该只运行一次,如果必须要再运行,需要修改today变量为今天,如:" + sdf.format(new Date()));
            return;
        }

        if (false)
            return;
        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        InputStream is = MybatisGenerator.class.getClassLoader().getResource("generatorConfig.xml").openStream();
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(is);
        is.close();
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
        myBatisGenerator.generate(null);

        System.out.println("生成代码成功,只能执行一次,以后执行会覆盖掉mapper,pojo,xml 等文件上做的修改");
    }
}

这个是参考自how2j.cn的逆向工程,这个可以说是很成熟的模块了,写的很棒,考虑了安全方面的一些东西,链接在这里:http://how2j.cn/k/tmall_ssm/tmall_ssm-1547/1547.html

3)点击运行:

控制台看到成功的信息之后,就能看到项目中自动多了一堆文件了:

RESTful API 设计

为了实现前后端分离,好的RESTful API是离不开的,正好前一段时间学习了这方面的知识,所以决定先来设计一套RESTful API,之前学习的文章链接在这里:https://www.jianshu.com/p/91600da4df95

1)引入Swagger2来构造RESTful API:

既然想弄一下前后端分离,那就彻底一点儿,写后台完全不管前台,前后台的交互靠一套RESTful API和JSON数据来弄,所以需要一个文档来瞅瞅,首先在pox.xml添加相关依赖:

<!--Swagger2支持-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.2.2</version>
</dependency>

2)创建Swagger2配置类:

在SpringBoot启动类的同级目录下创建Swagger2的配置类【Swagger2】:

/**
 * Swagger2 配置类
 *
 * @author:wmyskxz
 * @create:2018-06-14-上午 10:40
 */
@Configuration
@EnableSwagger2
public class Swagger2 {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("cn.wmyskxz.blog"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Wmyskxz个人博客RESTful APIs")
                .description("原文地址链接:http://blog.didispace.com/springbootswagger2/")
                .termsOfServiceUrl("http://blog.didispace.com/")
                .contact("@我没有三颗心脏")
                .version("1.0")
                .build();
    }

}

这样,就可以在我们启动项目之后,访问http://localhost:8080/swagger-ui.html地址来查看当前项目中的RESTful风格的API:

3)设计RESTful API:

好的,捣鼓了半天,终于有了一些雏形:

但是这也只是设计了API,具体都还没有实现,这些就在写后台的时候来完善了,具体的这些内容怎么显示出来的,我给一个【SortController】的参考类:

/**
 * 分类信息控制器
 *
 * @author:wmyskxz
 * @create:2018-06-14-下午 13:25
 */
@RestController
@RequestMapping("/api/sort")
public class SortController {

    /**
     * 获取所有分类信息
     *
     * @return
     */
    @ApiOperation("获取所有分类信息")
    @GetMapping("/list")
    public List<SortInfo> listAllSortInfo() {
        return null;
    }

    /**
     * 通过id获取一条分类信息
     *
     * @param id
     * @return
     */
    @ApiOperation("获取某一条分类信息")
    @ApiImplicitParam(name = "id", value = "分类ID", required = true, dataType = "Long")
    @GetMapping("/{id}")
    public SortInfo getSortInfoById(@PathVariable Long id) {
        return null;
    }

    /**
     * 增加一条分类信息数据
     *
     * @return
     */
    @ApiOperation("增加分类信息")
    @ApiImplicitParam(name = "name", value = "分类名称", required = true, dataType = "String")
    @PostMapping("")
    public String addSortInfo() {
        return null;
    }

    /**
     * 更新/编辑一条数据
     *
     * @param id
     * @return
     */
    @ApiOperation("更新/编辑分类信息")
    @ApiImplicitParam(name = "id", value = "分类ID", required = true, dataType = "Long")
    @PutMapping("/{id}")
    public String updateSortInfo(@PathVariable Long id) {
        return null;
    }

    /**
     * 根据ID删除分类信息
     *
     * @param id
     * @return
     */
    @ApiOperation("删除分类信息")
    @ApiImplicitParam(name = "id", value = "分类ID", required = true, dataType = "Long")
    @DeleteMapping("/{id}")
    public String deleteSortInfo(@PathVariable Long id) {
        return null;
    }
}

简单介绍一下这些Swagger2的注解吧:

  • @ApiOperation:用于给API设置提示信息,就上图中右边显示的那些,默认不写的情况下是value属性,还可以多写一个notes属性,用于详细的描述API,这里就不需要了,都还比较简单;
  • @ApiImplicaitParam:用于说明API的参数信息,加了s的注解同理,写了这个之后呢,我们就可以利用Swagger2给我们的信息页面进行测试了,当然这里没有具体实现,也可以来看一下(下图);

这里没有具体实现所以就不足以完成测试,等到后台编写的时候再进行测试吧…


总结

至此呢,我们项目所需要的准备就差不多完成了,想要去做一个东西必须要清楚的知道要的是一个什么东西,这样才能更加好的完成我们的产品,这也是我喜欢和坚信的事情:方向永远比努力重要!(强行有联系..hhhh)

另外一个问题: 我在想文章信息和内容分成了两个表的问题,这样的设计我觉得是没有问题的,但是作为前端并不关心这些数据库的设计,他只要能拿到对象就可以了,在设计 API 的时候,就发现获得一篇文章,需要从三个表(文章信息/文章内容/评论)去获取信息并封装返回前端,这就需要自己在后台另外写一个实体类去封装这些信息,这无疑增加了我们的代码工作量,有没有什么好的方法解决呢?

欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz
欢迎关注公众微信号:wmyskxz_javaweb
分享自己的Java Web学习之路以及各种Java学习资料

第一篇文章链接:模仿天猫实战【SSM版】——项目起步
第二篇文章链接:模仿天猫实战【SSM版】——后台开发

总结:项目从4-27号开始写,到今天5-7号才算真正的完工,有许多粗糙的地方,但总算完成了,比想象中的开发周期要久的多,并且大部分的时间都花在了前端页面的编写上…仅以此文来总结一下

项目总结

功能一览表

大致理了一下功能列表,应该是齐全的,其中推荐链接暂时不支持修改。

项目页面一览表

  • 后端页面: 后台所需要用到的页面,从名字很好区分功能,其中 index.jsp 只有一行代码用于跳转
  • 公共页面: 都是前端页面,从对天猫页面的分析提取出一些复用比较高的页面用于动态的包含在其他前端页面中。
  • 前台页面:前台相较于后台页面 CSS 更加复杂,交互也更多,我把每一个页面的需要用到的 css 和 js 代码均保留在了当前 JSP 页面中,方便浏览学习。

项目主要逻辑类

  • 控制器(Controller): 用于控制页面的逻辑, 提取出一个 PageController 来专门控制页面的跳转,ForeController 用于前台所有的逻辑操作
  • 拦截器(Interceptor): LoginInteceptor 用于对登录进行判断,因为有一些页面需要登录之后才能访问的,例如:购物车;OtherInterceptor 用于向页面中添加一些其他的数据,例如:购物车数量。
  • 业务层(Service层): 业务处理层,其中封装了 Dao 层,用于完成主要的逻辑处理。

不需要登录就能访问的页面(以下为拦截器中的代码片段):

  • 其中包括:主页、搜索结果页、产品展示页、登录页、注册页。
  • 还包括一些其他的路径用于处理逻辑,test 为开发过程中用于测试的页面

前台总结

前台花费了大部分的时间,不仅仅是繁杂的样式和页面需要自己去编写,业务逻辑也比后台要复杂一些,因为是模仿,所以大部分的 CSS 我都是参照着天猫官网写的(利用FireFox来查看元素和元素样式):

另外一部分是参照了how2j.cn上模仿的前端教程:戳这里

首页

简要的首页大概就是这样,请别在意轮播下面的【女装/内衣】中的产品,因为在4月份的时候,第一个分类的名字还叫【女装 /男装 /内衣】(好像是这个),后来项目写着写着突然改了…

观察大部分的页面,其实都是包含了其中的三个部分:

即顶部导航栏、一个搜索框、还有底部,我们可以单独把他们写成一个 jsp ,并动态的包含在我们的页面中

  • 首页分类栏

因为一开始,我以为分类栏中保存的是一些直接的产品,但是分析前端的时候发现它们只是一些 hot-word 热词,所以为了和天猫的首页保持一致,我直接把分类栏写死了写成了一个单独的 JSP 文件并包含进了主页:

我还自己写了一个小程序,用来将这些 hor-word 转换成对应的 html 代码,不然这手写 2000 行可能真的够呛…

产品搜索页

并且支持按照【综合(销量*评价)】、【人气(评论量)】、【销量】、【价格】来排序产品,使用 Java 8 的新特性来完成该功能:

产品展示页

所有的产品展示图片均是来自how2j.com上的一张图,前面有链接,表示有参照这个教程来做。

购买页

在产品页中点击立即购买,或者在购物车点击结算都会跳转到该页面,创建订单。

付款页面

无耻的黏了一张自己的收款二维码…

付款成功页

当点击确认支付按钮之后,就会跳转到该页面来。

购物车页

该页面支持删除订单和对订单项进行相关的操作,点击结算页面跳转到购买页。

我的订单页

该页面用于对订单的管理,可以查看和操作订单。

评价页

当完成购买,即经过购买→支付→发货→确认收货的流程之后,即可对产品进行评论,评论完成后能看到其他用户的评价信息:

注册页

用户注册页,在前端判断两次密码是否相同,并提交给后台判断用户名是否唯一。

登录页

因为分辨率的问题有一点 BUG,不过不影响体验,登录之后顶部导航栏出现用户信息:


后台总结

前台因为有现成的原型可以参照和模仿,后台需要自己去设计和实现界面,所以我直接找了一个模板代码,很方便也很快的完成了开发,在我的第二篇文章:模仿天猫实战【SSM版】——后台开发 有介绍。

分类管理

其中的分页和搜索功能是我找来的模板中用 js 代码来实现的,分类管理中不仅提供了更改分类名称的功能,还能管理分类下的产品和属性。

产品管理页

产品图片管理页

产品的图片是默认放置在 img/product/产品的id号/ 目录下的,并且默认的五张图片分别为:1.jpg、2.jpg…..5.jpg,用于默认显示的图片均为 1.jpg

属性值管理页

产品属性值管理页,能增加的属性值只能为当前分类下拥有的属性。

用户管理

提供一个修改密码的功能,给申诉修改密码的用户留一条后路。

订单管理页

等待发货的订单有一个发货按钮,用于发货。

上面有一些产品管理的按钮乱入了..直接拷贝的之前的图片,左侧栏中的产品管理按钮是删除了的…

推荐链接管理

暂时不提供修改功能。


项目中遇到的一些问题

轮播失效

Bootstrap 的引入要在 JQuery 之后,不然不能正常使用…

为什么不在 PropertyValue 表中增加 property_name 字段?

在产品详情页明显感觉到显示产品的属性的时候,特别不方便。

PropertyValue

新增一个 Property 属性,来完成产品页的传递

后台属性值管理逻辑有点问题

之前的代码:

<c:forEach items="${propertyValues}" var="pv">
    <tr>
        <td>
            <c:forEach items="${properties}" var="p">
                <c:if test="${p.id==pv.property_id}">${p.name}</c:if>
            </c:forEach>
        </td>
        <td>${pv.value}</td>
        <td><a href="editPropertyValue?id=${pv.id}"><span
                class="glyphicon glyphicon-edit"></span></a></td>
        <td>
            <a href="deletePropertyValue?id=${pv.id}&category_id=${product.category_id}"><span
                    class="glyphicon glyphicon-trash"></span></a></td>
    </tr>
</c:forEach>

非常糟糕,逻辑就是错的。

利用上面为 PropertyValue 添加的 Property 来完成功能:

  • 下面的 select 标签也是错的

生成出来的代码是这样的:

我想要绑定一个隐藏的 input ,看来这样写是不行的,搜索了一下,可以通过为 <select> 标签写 onchange 属性来完成:

使用 Java 8 的新特性来排序

使用了 Java 8 的 Lambda 表达式来完成前端的排序工作:

注册页

天猫的注册搞得很高大上的样子,淘宝也弄成了一样的,不是很好模仿出效果:

所以照着改了一改,弄成了这个样子:

在前端通过 JS 来判断完成确认密码的功能,然后这是注册成功的页面:

发现 OrderItem 少设计了一个字段

这是最初设计的数据库表与表之间的关系:

当我按照流程一步一步完成着项目,在完成立即购买这个功能时,我需要按照user_id来返回订单项时,不容易实现,我们需要为 OrderItem 增加一个字段(user_id):

CREATE TABLE `order_item` (
    `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '唯一索引id',
    `product_id` INT(11) NOT NULL COMMENT '对应产品id',
    `order_id` INT(11) NOT NULL COMMENT '对应订单id',
    `user_id` INT(11) NOT NULL COMMENT '对应用户id',
    `number` INT(11) NULL DEFAULT NULL COMMENT '对应产品购买的数量',
    INDEX `fk_order_item_product` (`product_id`),
    INDEX `fk_order_item_order` (`order_id`),
    INDEX `fk_order_item_user` (`user_id`),
    PRIMARY KEY (`id`),
    CONSTRAINT `fk_order_item_order` FOREIGN KEY (`order_id`) REFERENCES `order_` (`id`),
    CONSTRAINT `fk_order_item_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
    CONSTRAINT `fk_order_item_product` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`)
)COLLATE='utf8_general_ci' ENGINE=InnoDB;

然后运行 TestMybatisGenerator 来重新生成相关的文件.

更改 OrderItem 表中的 order_id 字段默认为空

order_id 是用于判断当前的 OrderItem 是否存在于购物车中的依据,最开始我们将这个字段设计为不能为空,那么就只能在购物车中存在,当我们不需要经过购物车而要直接购买的时候,就不能得到满足…

修复购物车逻辑问题

之前给 cart.jsp 页面的 List 仅仅是通过 listByUserId 方法来获取,但其实真正的购物车是那些 order_id 为空的,所以我在 OrderItemService 中新增了一个方法:listForCart 来返回那些真正属于购物车的订单项:

@Override
public List<OrderItem> listForCart(Integer user_id) {
    OrderItemExample example = new OrderItemExample();
    example.or().andUser_idEqualTo(user_id).andOrder_idIsNull();
    List<OrderItem> result = orderItemMapper.selectByExample(example);
    setProduct(result);
    return result;
}

Github

完成之后的项目直接上传 Github,代码可能有些乱,可读性不是很高,但结构还是清晰的,还是值得参考:传送门

后期再对代码进行维护吧…菜鸟学习代码,勿喷….

关于 sql 语句

这里给一个连接提供建表语句以及一些数据:传送门

欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz
欢迎关注公众微信号:wmyskxz_javaweb
分享自己的Java Web学习之路以及各种Java学习资料