12306会员基础功能实现

主要目的:完成前后端,单表增删改查功能的开发

乘车人 数据库表的设计

乘客表的设计

member表代表系统登录用户的信息,对于乘车系统,同一个用户不仅可以为自己,也可以为其他人购买车票

所以设计一张新表passenger表示对应乘客信息,与车票对应

1
2
3
4
5
6
7
8
9
10
11
create table `passenger` (
`id` bigint not null comment 'id',
`member_id` bigint not null comment '会员id',
`name` varchar(20) not null comment '姓名',
`id_card` varchar(18) not null comment '身份证',
`type` char(1) not null comment '旅客类型|枚举[PassengerTypeEnum]',
`create_time` datetime(3) comment '新增时间',
`update_time` datetime(3) comment '修改时间',
primary key (`id`),
index `member_id_index` (`member_id`)
) engine=innodb default charset=utf8mb4 comment='乘车人';

利用Mybatis生成器生成乘车人表对应持久层代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?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="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">

<!-- 自动检查关键字,为关键字增加反引号 -->
<property name="autoDelimitKeywords" value="true"/>
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>

<!--覆盖生成XML文件-->
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
<!-- 生成的实体类添加toString()方法 -->
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>

<!-- 不生成注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>

<!-- 配置数据源,需要根据自己的项目修改 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/train_member?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false"
userId="train_member"
password="wu123456">
</jdbcConnection>

<!-- domain类的位置 targetProject是相对pom.xml的路径-->
<javaModelGenerator targetProject="../member/src/main/java"
targetPackage="com.bang.train.member.domain"/>

<!-- mapper xml的位置 targetProject是相对pom.xml的路径 -->
<sqlMapGenerator targetProject="../member/src/main/resources"
targetPackage="mapper"/>

<!-- mapper类的位置 targetProject是相对pom.xml的路径 -->
<javaClientGenerator targetProject="../member/src/main/java"
targetPackage="com.bang.train.member.mapper"
type="XMLMAPPER"/>

<!-- <table tableName="member" domainObjectName="Member"/>-->
<table tableName="passenger" domainObjectName="Passenger"/>
<!-- <table tableName="ticket" domainObjectName="Ticket"/>-->
</context>
</generatorConfiguration>

乘客类型枚举类的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.bang.train.member.enums;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;

public enum PassengerTypeEnum {

ADULT("1", "成人"),
CHILD("2", "儿童"),
STUDENT("3", "学生");

private String code;

private String desc;

PassengerTypeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

public void setDesc(String desc) {
this.desc = desc;
}

public String getDesc() {
return desc;
}

public static List<HashMap<String,String>> getEnumList() {
List<HashMap<String, String>> list = new ArrayList<>();
for (PassengerTypeEnum anEnum : EnumSet.allOf(PassengerTypeEnum.class)) {
HashMap<String, String> map = new HashMap<>();
map.put("code",anEnum.code);
map.put("desc",anEnum.desc);
list.add(map);
}
return list;
}
}

新增乘车人接口设计

设计接口请求类

请求实体类与数据库对应的Po实体类一致,并利用Validation进行参数校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 新增乘车人接口对应请求实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PassengerSaveReq {
private Long id;
//@NotBlank不能用来修饰long类型数据
@NotNull(message = "【会员ID】不能为空")
private Long memberId;
@NotBlank(message = "【乘客姓名】不能为空")
private String name;
@NotBlank(message = "【乘客身份证号】不能为空")
private String idCard;
@NotBlank(message = "【乘客类型】不能为空")
private String type;

private Date createTime;

private Date updateTime;
}

服务层代码

新增IPassengerService接口

1
2
3
public interface IPassengerService {
void save(PassengerSaveReq passenger);
}

新增PassengerServiceImpl实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class PassengerServiceImpl implements IPassengerService {

@Resource
PassengerMapper passengerMapper;
@Override
public void save(PassengerSaveReq passenger) {
//1.将请求类转换为po对象
Passenger savePassenger = BeanUtil.copyProperties(passenger, Passenger.class);
//2.设置相关字段
//乘车人ID,以及记录创建时间和更新时间,ID利用雪花算法生成
DateTime now = DateTime.now();
savePassenger.setId(SnowUtil.getSnowflakeId());
savePassenger.setCreateTime(now);
savePassenger.setUpdateTime(now);
//3.存入数据库
passengerMapper.insert(savePassenger);
}
}

控制层代码

新增PassengerController

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/passenger")
public class PassengerController {
@Autowired
IPassengerService passengerService;

@PostMapping("/save")
public CommonResp<Void> save(@Valid @RequestBody PassengerSaveReq req){
passengerService.save(req);
return new CommonResp<>();
}
}

HttpClient测试

1
2
3
4
5
6
7
8
9
10
###新增乘车人
POST http://localhost:8001/member/passenger/save
Content-Type: application/json

{
"memberId": 1,
"name": "张飞",
"idCard": "33456789",
"type": "1"
}

使用HttpClient保存登录用户信息

在进行网关gateway请求时,由于有JWT登录校验过滤器,所以其他接口测试时,若请求头不带上token字段,则会被拦截

可以在HttpClient中,登录请求之后,为整个文件中所有其他请求的请求头加上token字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
###登录
POST http://localhost:8000/member/member/login
Content-Type: application/json

{
"mobile":"12345678908",
"code":"8888"
}
//以下语句将token数据进行全局缓存
> {%
client.log(JSON.stringify(response.body));
client.log(JSON.stringify(response.body.content.token));
client.global.set("token",response.body.content.token)
%}

在其他http请求中可以直接通过{{token}}引用缓存中的token`值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
###其他测试
GET http://localhost:8000/member/member/count
Content-Type: application/json
token: {{token}}

###新增乘车人
POST http://localhost:8000/member/passenger/save
Content-Type: application/json
token: {{token}}

{
"memberId": 1,
"name": "张飞",
"idCard": "33456789",
"type": "1"
}

使用线程本地变量存储会员信息

背景:新增乘车人记录时,实体类里面有个属性是当前登录的会员ID,如何将当前登录用户的信息保存在本地?

方案:在接口入口处获取会员信息,并放在线程本地变量,则在controller、service中都可以直接从线程本地变量获取会员信息

考点:此处可能面试会涉及到ThreadLocal线程本地变量的概念和理解

ThreadLocal存储登录会员信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LoginMemberContext {
private static final Logger LOG = LoggerFactory.getLogger(LoginMemberContext.class);

private static ThreadLocal<MemberLoginResp> member = new ThreadLocal<>();

public static MemberLoginResp getMember() {
return member.get();
}

public static void setMember(MemberLoginResp member) {
LoginMemberContext.member.set(member);
}
//memberId频繁使用,单独抽象成一个方法
public static Long getId() {
try {
return member.get().getId();
} catch (Exception e) {
LOG.error("获取登录会员信息异常", e);
throw e;
}
}
}

SpringMVC过滤器将会员信息存入线程本地变量

请求通过网关过滤器之后,经过网关路由,进入其他微服务时,其他微服务内部应该将JWT解析得到payloads,获取当前登录会员信息,并存储到对应的线程本地变量中,以便后续代码逻辑使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
*/
@Component
public class MemberInterceptor implements HandlerInterceptor {

private static final Logger LOG = LoggerFactory.getLogger(MemberInterceptor.class);

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
LOG.info("MemberInterceptor开始");
//获取header的token参数
String token = request.getHeader("token");
if (StrUtil.isNotBlank(token)) {
LOG.info("获取会员登录token:{}", token);
JSONObject loginMember = JwtUtil.getJSONObject(token);
LOG.info("当前登录会员:{}", loginMember);
MemberLoginResp member = JSONUtil.toBean(loginMember, MemberLoginResp.class);
//会员信息存储在线程本地变量
LoginMemberContext.setMember(member);
}
LOG.info("MemberInterceptor结束");
return true;
}

}

SpringMVC注册对应过滤器

member模块下编写SpringMVC的配置类,配置登录拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {


@Resource
MemberInterceptor memberInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {

// 路径不要包含context-path,添加白名单
registry.addInterceptor(memberInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/hello",
"/member/send-code",
"/member/login"
);
}
}

修改新增乘客服务层方法

memeberID此时无需从前端传入,直接从线程本地变量读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void save(PassengerSaveReq passenger) {
//1.将请求类转换为po对象
Passenger savePassenger = BeanUtil.copyProperties(passenger, Passenger.class);
//2.会员ID通过线程本地变量获取
savePassenger.setMemberId(LoginMemberContext.getId());
//3.设置相关字段
//乘车人ID,以及记录创建时间和更新时间,ID利用雪花算法生成
DateTime now = DateTime.now();
savePassenger.setId(SnowUtil.getSnowflakeId());
savePassenger.setCreateTime(now);
savePassenger.setUpdateTime(now);
//4.存入数据库
passengerMapper.insert(savePassenger);
}

乘车人列表查询后端接口

新增查询请求参数对应实体类

1
2
3
4
5
6
7
8
9
/**
* 查询指定会员对应的乘车人列表请求实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PassengerQueryReq {
private Long memberId;
}

新增响应结果对应实体类

对于规范而言,domain中的po对象一般最好只在持久层使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PassengerQueryResp {
private Long id;

private Long memberId;

private String name;

private String idCard;

private String type;

private Date createTime;

private Date updateTime;

}

查询乘车人列表服务层开发

IPassengerService中新增抽象方法queryList

1
List<PassengerQueryResp> queryList(PassengerQueryReq req);

PassengerServiceImpl中新增抽象方法重写方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public List<PassengerQueryResp> queryList(PassengerQueryReq req) {
//1.条件查询
PassengerExample passengerExample = new PassengerExample();
PassengerExample.Criteria criteria = passengerExample.createCriteria();
if(ObjectUtil.isNotNull(req.getMemberId())){
criteria.andMemberIdEqualTo(req.getMemberId());
}
//2.查询
List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);
//3.接口数据类型转换
return BeanUtil.copyToList(passengerList, PassengerQueryResp.class);
}

补充说明:

这个位置之所以写的比较复杂,是为了让服务层代码能够更加通用,对于用户界面而言,是查询当前登录用户对应的所有乘车人列表;但是,对于控台管理系统管理员而言,其需要查询所有的乘车人列表

控制层开发

前端无需传入任何参数,会员ID通过线程本地变量获取

1
2
3
4
5
6
7
8
@GetMapping("/query-list")
public CommonResp<List<PassengerQueryResp>> queryList(@Valid PassengerQueryReq req){
req.setMemberId(LoginMemberContext.getId());
List<PassengerQueryResp> passengerQueryRespList = passengerService.queryList(req);
CommonResp<List<PassengerQueryResp>> commonResp = new CommonResp<>();
commonResp.setContent(passengerQueryRespList);
return commonResp;
}

http测试

1
2
3
4
###乘车人列表查询
GET http://localhost:8000/member/passenger/query-list
Accept: application/json
token: {{token}}

Mybatis分页插件PageHelper的使用

引入PageHelper依赖

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>

PageHelper的用法

在SQL查询语句之前的上一行加上如下语句

1
2
// PageHelper分页
PageHelper.startPage(页码,分页大小);

PassengerService代码变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public List<PassengerQueryResp> queryList(PassengerQueryReq req) {
//1.条件查询
PassengerExample passengerExample = new PassengerExample();
PassengerExample.Criteria criteria = passengerExample.createCriteria();
if(ObjectUtil.isNotNull(req.getMemberId())){
criteria.andMemberIdEqualTo(req.getMemberId());
}
// PageHelper分页
PageHelper.startPage(1,2);
//2.查询
List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);
//3.接口数据类型转换
return BeanUtil.copyToList(passengerList, PassengerQueryResp.class);
}

集成PageHelper实现后端分页

分页查询请求实体类的创建

整个项目中,可能后续会有很多地方会用到分页查询,为了方便扩展,我们将分页参数单独抽象成一个实体类,让其他有分页查询需求的请求对应实体类继承自分页实体类,实现其他查询的分页功能

common模块下的com.bang.train.common.req下新建PageReq

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageReq {

@NotNull(message = "【页码】不能为空")
private Integer page;

@NotNull(message = "【每页条数】不能为空")
@Max(value = 100, message = "【每页条数】不能超过100")
private Integer size;

}

乘客请求实体类继承自分页实体类

1
2
3
4
5
6
7
8
9
/**
* 查询指定会员对应的乘车人列表请求实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PassengerQueryReq extends PageReq {
private Long memberId;
}

修改PassengerServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public List<PassengerQueryResp> queryList(PassengerQueryReq req) {
//1.条件查询
PassengerExample passengerExample = new PassengerExample();
PassengerExample.Criteria criteria = passengerExample.createCriteria();
if(ObjectUtil.isNotNull(req.getMemberId())){
criteria.andMemberIdEqualTo(req.getMemberId());
}
// PageHelper分页
PageHelper.startPage(req.getPage(),req.getSize());
//2.查询
List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);
//3.接口数据类型转换
return BeanUtil.copyToList(passengerList, PassengerQueryResp.class);
}

编写分页查询结果实体类

实体类应该包含总条数以及当前页数据列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResp<T> implements Serializable {

/**
* 总条数
*/
private Long total;

/**
* 当前页的列表
*/
private List<T> list;

}

PassengerServiceImpl修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public PageResp<PassengerQueryResp> queryList(PassengerQueryReq req) {
//1.条件查询
PassengerExample passengerExample = new PassengerExample();
PassengerExample.Criteria criteria = passengerExample.createCriteria();
if(ObjectUtil.isNotNull(req.getMemberId())){
criteria.andMemberIdEqualTo(req.getMemberId());
}
// 2.PageHelper分页
PageHelper.startPage(req.getPage(),req.getSize());
//3.查询
List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);
//4.获取总页数和总条数
PageInfo<Passenger> pageInfo = new PageInfo<>(passengerList);
LOG.info("总行数:{}",pageInfo.getTotal());
LOG.info("总页数:{}",pageInfo.getPages());

//3.接口数据类型转换
List<PassengerQueryResp> passengerQueryRespList = BeanUtil.copyToList(passengerList, PassengerQueryResp.class);
//4.封装成分页查询结果
PageResp<PassengerQueryResp> pageResp = new PageResp<>();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(passengerQueryRespList);
return pageResp;
}

PassengerController修改

1
2
3
4
5
6
7
8
@GetMapping("/query-list")
public CommonResp<PageResp<PassengerQueryResp>> queryList(@Valid PassengerQueryReq req){
req.setMemberId(LoginMemberContext.getId());
PageResp<PassengerQueryResp> pageResp = passengerService.queryList(req);
CommonResp<PageResp<PassengerQueryResp>> commonResp = new CommonResp<>();
commonResp.setContent(pageResp);
return commonResp;
}

http测试

1
2
3
4
###乘车人列表查询
GET http://localhost:8000/member/passenger/query-list?page=1&size=50
Accept: application/json
token: {{token}}

解决Long精度丢失的问题

不同的语言,虽然都有int long等类型,但他们的精度不太一样,在数据传递时需要特别注意精度丢失。

在本项目中乘车查询数据返回给前端时,乘客id和membertId字段的数据精度会丢失

解决方法:将long传成string

在相关的返回结果实体类的对应字段上加上如下注解

1
@JsonSerialize(using= ToStringSerializer.class)

乘客数据查询结果返回实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PassengerQueryResp {
//对应字段转换为字符串
@JsonSerialize(using= ToStringSerializer.class)
private Long id;
@JsonSerialize(using= ToStringSerializer.class)
private Long memberId;

private String name;

private String idCard;

private String type;
//日期格式转换
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date updateTime;

}

乘车人编辑接口开发

乘车人编辑和乘车人新增可以向后端同一个接口进行访问,后端可以共用同一套代码,只是对于乘车人新增而言,前端的请求数据中ID为空,对于编辑而言,前端的请求数据中ID不为空,在服务层可以依据此区别做不同的处理

PassengerServiceImpl中代码的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public void save(PassengerSaveReq passenger) {
DateTime now = DateTime.now();

//1.将请求类转换为po对象
Passenger savePassenger = BeanUtil.copyProperties(passenger, Passenger.class);
//2.会员ID通过线程本地变量获取
savePassenger.setMemberId(LoginMemberContext.getId());
//3.设置相关字段
//4.依据请求参数中ID是否为空,判断时乘车人新增业务还是乘车人编辑业务
if(ObjectUtil.isNull(savePassenger.getId())){//新增业务
//乘车人ID,以及记录创建时间和更新时间,ID利用雪花算法生成
savePassenger.setId(SnowUtil.getSnowflakeId());
savePassenger.setCreateTime(now);
savePassenger.setUpdateTime(now);
//存入数据库
passengerMapper.insert(savePassenger);
}else{ //编辑业务
//更新乘车人数据,依据主键进行更新
savePassenger.setUpdateTime(now);
passengerMapper.updateByPrimaryKey(savePassenger);
}
}

乘车人删除接口开发

服务层代码

IPassengerServicej接口新增抽象方法

1
void deleteById(Long id);

PassengerServiceImpl实现了实现对应抽象方法

1
2
3
4
@Override
public void deleteById(Long id) {
passengerMapper.deleteByPrimaryKey(id);
}

控制层代码

1
2
3
4
5
@DeleteMapping("/delete/{id}")
public CommonResp<Void> delete(@PathVariable Long id){
passengerService.deleteById(id);
return new CommonResp<>();
}

http测试

1
2
3
4
###乘车人删除
DELETE http://localhost:8000/member/passenger/delete/1734597882838913024
Accept: application/json
token: {{token}}