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="`"/>
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" /> <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&characterEncoding=utf8&useSSL=false" userId="train_member" password="wu123456"> </jdbcConnection>
<javaModelGenerator targetProject="../member/src/main/java" targetPackage="com.bang.train.member.domain"/>
<sqlMapGenerator targetProject="../member/src/main/resources" targetPackage="mapper"/>
<javaClientGenerator targetProject="../member/src/main/java" targetPackage="com.bang.train.member.mapper" type="XMLMAPPER"/>
<table tableName="passenger" domainObjectName="Passenger"/>
</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; @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) { Passenger savePassenger = BeanUtil.copyProperties(passenger, Passenger.class); DateTime now = DateTime.now(); savePassenger.setId(SnowUtil.getSnowflakeId()); savePassenger.setCreateTime(now); savePassenger.setUpdateTime(now); 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); } 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
|
@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开始"); 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) {
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) { Passenger savePassenger = BeanUtil.copyProperties(passenger, Passenger.class); savePassenger.setMemberId(LoginMemberContext.getId()); DateTime now = DateTime.now(); savePassenger.setId(SnowUtil.getSnowflakeId()); savePassenger.setCreateTime(now); savePassenger.setUpdateTime(now); 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) { PassengerExample passengerExample = new PassengerExample(); PassengerExample.Criteria criteria = passengerExample.createCriteria(); if(ObjectUtil.isNotNull(req.getMemberId())){ criteria.andMemberIdEqualTo(req.getMemberId()); } List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample); 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.startPage(页码,分页大小);
|
PassengerService代码变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override public List<PassengerQueryResp> queryList(PassengerQueryReq req) { PassengerExample passengerExample = new PassengerExample(); PassengerExample.Criteria criteria = passengerExample.createCriteria(); if(ObjectUtil.isNotNull(req.getMemberId())){ criteria.andMemberIdEqualTo(req.getMemberId()); } PageHelper.startPage(1,2); List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample); 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) { PassengerExample passengerExample = new PassengerExample(); PassengerExample.Criteria criteria = passengerExample.createCriteria(); if(ObjectUtil.isNotNull(req.getMemberId())){ criteria.andMemberIdEqualTo(req.getMemberId()); } PageHelper.startPage(req.getPage(),req.getSize()); List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample); 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) { PassengerExample passengerExample = new PassengerExample(); PassengerExample.Criteria criteria = passengerExample.createCriteria(); if(ObjectUtil.isNotNull(req.getMemberId())){ criteria.andMemberIdEqualTo(req.getMemberId()); } PageHelper.startPage(req.getPage(),req.getSize()); List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample); PageInfo<Passenger> pageInfo = new PageInfo<>(passengerList); LOG.info("总行数:{}",pageInfo.getTotal()); LOG.info("总页数:{}",pageInfo.getPages());
List<PassengerQueryResp> passengerQueryRespList = BeanUtil.copyToList(passengerList, PassengerQueryResp.class); 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();
Passenger savePassenger = BeanUtil.copyProperties(passenger, Passenger.class); savePassenger.setMemberId(LoginMemberContext.getId()); if(ObjectUtil.isNull(savePassenger.getId())){ 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}}
|