赞
踩
本文参考自
本文是仿12306项目实战第(二)章——项目实现 的第五篇,本篇讲解该项目的核心功能——余票查询、车票预定功能的基础版开发,以及讲解项目与Nacos的集成
business.sql
drop table if exists `daily_train_ticket`;
create table `daily_train_ticket` (
`id` bigint not null comment 'id',
`date` date not null comment '日期',
`train_code` varchar(20) not null comment '车次编号',
`start` varchar(20) not null comment '出发站',
`start_pinyin` varchar(50) not null comment '出发站拼音',
`start_time` time not null comment '出发时间',
`start_index` tinyint not null comment '出发站序|本站是整个车次的第几站',
`end` varchar(20) not null comment '到达站',
`end_pinyin` varchar(50) not null comment '到达站拼音',
`end_time` time not null comment '到站时间',
`end_index` tinyint not null comment '到站站序|本站是整个车次的第几站',
`ydz` int not null comment '一等座余票',
`ydz_price` decimal(8, 2) not null comment '一等座票价',
`edz` int not null comment '二等座余票',
`edz_price` decimal(8, 2) not null comment '二等座票价',
`rw` int not null comment '软卧余票',
`rw_price` decimal(8, 2) not null comment '软卧票价',
`yw` int not null comment '硬卧余票',
`yw_price` decimal(8, 2) not null comment '硬卧票价',
`create_time` datetime(3) comment '新增时间',
`update_time` datetime(3) comment '修改时间',
primary key (`id`),
unique key `date_train_code_start_end_unique` (`date`, `train_code`, `start`, `end`)
) engine=innodb default charset=utf8mb4 comment='余票信息';
实际12306用的是商业缓存软件来做的余票信息缓存,本项目用mysql新建临时表来代替缓存演示业务实现
修改generator-config-business.xml,生成持久层、前后端代码
<table tableName="daily_train_ticket" domainObjectName="DailyTrainTicket"/>
操作同之前
修改admin/package.json,加规则去除ESLint报错
"rules": {
"vue/multi-word-component-names": 0,
"no-undef": 0,
"vue/no-unused-vars": 0
}
修改路由、侧边栏
操作同之前
测试
数据后续由定时任务填充
给批量方法都加上@Transactional
虽然由于事务的传递性,外层有事务,内层方法也会是事务,但是规范点还是都加上事务注解
DailyTrainTicketService.java
逻辑:比如车站1——车站2——车站3,站站组合情况就有 1、2;1、3;2、3 三种,需要分别生成每种组合的余票信息
这里第一版 先解决站站组合逻辑,具体余票信息后面完善
package com.neilxu.train.business.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.neilxu.train.business.domain.DailyTrainTicket;
import com.neilxu.train.business.domain.DailyTrainTicketExample;
import com.neilxu.train.business.domain.TrainStation;
import com.neilxu.train.business.mapper.DailyTrainTicketMapper;
import com.neilxu.train.business.req.DailyTrainTicketQueryReq;
import com.neilxu.train.business.req.DailyTrainTicketSaveReq;
import com.neilxu.train.business.resp.DailyTrainTicketQueryResp;
import com.neilxu.train.common.resp.PageResp;
import com.neilxu.train.common.util.SnowUtil;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Service
public class DailyTrainTicketService {
private static final Logger LOG = LoggerFactory.getLogger(DailyTrainTicketService.class);
@Resource
private DailyTrainTicketMapper dailyTrainTicketMapper;
@Resource
private TrainStationService trainStationService;
public void save(DailyTrainTicketSaveReq req) {
DateTime now = DateTime.now();
DailyTrainTicket dailyTrainTicket = BeanUtil.copyProperties(req, DailyTrainTicket.class);
if (ObjectUtil.isNull(dailyTrainTicket.getId())) {
dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());
dailyTrainTicket.setCreateTime(now);
dailyTrainTicket.setUpdateTime(now);
dailyTrainTicketMapper.insert(dailyTrainTicket);
} else {
dailyTrainTicket.setUpdateTime(now);
dailyTrainTicketMapper.updateByPrimaryKey(dailyTrainTicket);
}
}
public PageResp<DailyTrainTicketQueryResp> queryList(DailyTrainTicketQueryReq req) {
DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();
dailyTrainTicketExample.setOrderByClause("id desc");
DailyTrainTicketExample.Criteria criteria = dailyTrainTicketExample.createCriteria();
LOG.info("查询页码:{}", req.getPage());
LOG.info("每页条数:{}", req.getSize());
PageHelper.startPage(req.getPage(), req.getSize());
List<DailyTrainTicket> dailyTrainTicketList = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);
PageInfo<DailyTrainTicket> pageInfo = new PageInfo<>(dailyTrainTicketList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
List<DailyTrainTicketQueryResp> list = BeanUtil.copyToList(dailyTrainTicketList, DailyTrainTicketQueryResp.class);
PageResp<DailyTrainTicketQueryResp> pageResp = new PageResp<>();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
public void delete(Long id) {
dailyTrainTicketMapper.deleteByPrimaryKey(id);
}
@Transactional
public void genDaily(Date date, String trainCode) {
LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);
// 删除某日某车次的余票信息
DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();
dailyTrainTicketExample.createCriteria()
.andDateEqualTo(date)
.andTrainCodeEqualTo(trainCode);
dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample);
// 查出某车次的所有的车站信息
List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);
if (CollUtil.isEmpty(stationList)) {
LOG.info("该车次没有车站基础数据,生成该车次的余票信息结束");
return;
}
DateTime now = DateTime.now();
for (int i = 0; i < stationList.size(); i++) {
// 得到出发站
TrainStation trainStationStart = stationList.get(i);
for (int j = (i + 1); j < stationList.size(); j++) {
TrainStation trainStationEnd = stationList.get(j);
DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();
dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());
dailyTrainTicket.setDate(date);
dailyTrainTicket.setTrainCode(trainCode);
dailyTrainTicket.setStart(trainStationStart.getName());
dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin());
dailyTrainTicket.setStartTime(trainStationStart.getOutTime());
dailyTrainTicket.setStartIndex(trainStationStart.getIndex());
dailyTrainTicket.setEnd(trainStationEnd.getName());
dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin());
dailyTrainTicket.setEndTime(trainStationEnd.getInTime());
dailyTrainTicket.setEndIndex(trainStationEnd.getIndex());
dailyTrainTicket.setYdz(0);
dailyTrainTicket.setYdzPrice(BigDecimal.ZERO);
dailyTrainTicket.setEdz(0);
dailyTrainTicket.setEdzPrice(BigDecimal.ZERO);
dailyTrainTicket.setRw(0);
dailyTrainTicket.setRwPrice(BigDecimal.ZERO);
dailyTrainTicket.setYw(0);
dailyTrainTicket.setYwPrice(BigDecimal.ZERO);
dailyTrainTicket.setCreateTime(now);
dailyTrainTicket.setUpdateTime(now);
dailyTrainTicketMapper.insert(dailyTrainTicket);
}
}
LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);
}
}
DailyTrainService.java
package com.neilxu.train.business.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.neilxu.train.business.domain.DailyTrain;
import com.neilxu.train.business.domain.DailyTrainExample;
import com.neilxu.train.business.domain.Train;
import com.neilxu.train.business.mapper.DailyTrainMapper;
import com.neilxu.train.business.req.DailyTrainQueryReq;
import com.neilxu.train.business.req.DailyTrainSaveReq;
import com.neilxu.train.business.resp.DailyTrainQueryResp;
import com.neilxu.train.common.resp.PageResp;
import com.neilxu.train.common.util.SnowUtil;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
@Service
public class DailyTrainService {
private static final Logger LOG = LoggerFactory.getLogger(DailyTrainService.class);
@Resource
private DailyTrainMapper dailyTrainMapper;
@Resource
private TrainService trainService;
@Resource
private DailyTrainStationService dailyTrainStationService;
@Resource
private DailyTrainCarriageService dailyTrainCarriageService;
@Resource
private DailyTrainSeatService dailyTrainSeatService;
@Resource
private DailyTrainTicketService dailyTrainTicketService;
public void save(DailyTrainSaveReq req) {
DateTime now = DateTime.now();
DailyTrain dailyTrain = BeanUtil.copyProperties(req, DailyTrain.class);
if (ObjectUtil.isNull(dailyTrain.getId())) {
dailyTrain.setId(SnowUtil.getSnowflakeNextId());
dailyTrain.setCreateTime(now);
dailyTrain.setUpdateTime(now);
dailyTrainMapper.insert(dailyTrain);
} else {
dailyTrain.setUpdateTime(now);
dailyTrainMapper.updateByPrimaryKey(dailyTrain);
}
}
public PageResp<DailyTrainQueryResp> queryList(DailyTrainQueryReq req) {
DailyTrainExample dailyTrainExample = new DailyTrainExample();
dailyTrainExample.setOrderByClause("date desc, code asc");
DailyTrainExample.Criteria criteria = dailyTrainExample.createCriteria();
if (ObjectUtil.isNotNull(req.getDate())) {
criteria.andDateEqualTo(req.getDate());
}
if (ObjectUtil.isNotEmpty(req.getCode())) {
criteria.andCodeEqualTo(req.getCode());
}
LOG.info("查询页码:{}", req.getPage());
LOG.info("每页条数:{}", req.getSize());
PageHelper.startPage(req.getPage(), req.getSize());
List<DailyTrain> dailyTrainList = dailyTrainMapper.selectByExample(dailyTrainExample);
PageInfo<DailyTrain> pageInfo = new PageInfo<>(dailyTrainList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
List<DailyTrainQueryResp> list = BeanUtil.copyToList(dailyTrainList, DailyTrainQueryResp.class);
PageResp<DailyTrainQueryResp> pageResp = new PageResp<>();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
public void delete(Long id) {
dailyTrainMapper.deleteByPrimaryKey(id);
}
/**
* 生成某日所有车次信息,包括车次、车站、车厢、座位
* @param date
*/
public void genDaily(Date date) {
List<Train> trainList = trainService.selectAll();
if (CollUtil.isEmpty(trainList)) {
LOG.info("没有车次基础数据,任务结束");
return;
}
for (Train train : trainList) {
genDailyTrain(date, train);
}
}
@Transactional
public void genDailyTrain(Date date, Train train) {
LOG.info("生成日期【{}】车次【{}】的信息开始", DateUtil.formatDate(date), train.getCode());
// 删除该车次已有的数据
DailyTrainExample dailyTrainExample = new DailyTrainExample();
dailyTrainExample.createCriteria()
.andDateEqualTo(date)
.andCodeEqualTo(train.getCode());
dailyTrainMapper.deleteByExample(dailyTrainExample);
// 生成该车次的数据
DateTime now = DateTime.now();
DailyTrain dailyTrain = BeanUtil.copyProperties(train, DailyTrain.class);
dailyTrain.setId(SnowUtil.getSnowflakeNextId());
dailyTrain.setCreateTime(now);
dailyTrain.setUpdateTime(now);
dailyTrain.setDate(date);
dailyTrainMapper.insert(dailyTrain);
// 生成该车次的车站数据
dailyTrainStationService.genDaily(date, train.getCode());
// 生成该车次的车厢数据
dailyTrainCarriageService.genDaily(date, train.getCode());
// 生成该车次的座位数据
dailyTrainSeatService.genDaily(date, train.getCode());
// 生成该车次的余票数据
dailyTrainTicketService.genDaily(date, train.getCode());
LOG.info("生成日期【{}】车次【{}】的信息结束", DateUtil.formatDate(date), train.getCode());
}
}
测试
DailyTrainSeatService.java
public int countSeat(Date date, String trainCode, String seatType) {
DailyTrainSeatExample example = new DailyTrainSeatExample();
example.createCriteria()
.andDateEqualTo(date)
.andTrainCodeEqualTo(trainCode)
.andSeatTypeEqualTo(seatType);
long l = dailyTrainSeatMapper.countByExample(example);
if (l == 0L) {
return -1;
}
return (int) l;
}
DailyTrainService.java
// 生成该车次的余票数据
dailyTrainTicketService.genDaily(dailyTrain, date, train.getCode());
DailyTrainTicketService.java
package com.neilxu.train.business.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.neilxu.train.business.domain.DailyTrain;
import com.neilxu.train.business.domain.DailyTrainTicket;
import com.neilxu.train.business.domain.DailyTrainTicketExample;
import com.neilxu.train.business.domain.TrainStation;
import com.neilxu.train.business.enums.SeatTypeEnum;
import com.neilxu.train.business.enums.TrainTypeEnum;
import com.neilxu.train.business.mapper.DailyTrainTicketMapper;
import com.neilxu.train.business.req.DailyTrainTicketQueryReq;
import com.neilxu.train.business.req.DailyTrainTicketSaveReq;
import com.neilxu.train.business.resp.DailyTrainTicketQueryResp;
import com.neilxu.train.common.resp.PageResp;
import com.neilxu.train.common.util.SnowUtil;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.List;
@Service
public class DailyTrainTicketService {
private static final Logger LOG = LoggerFactory.getLogger(DailyTrainTicketService.class);
@Resource
private DailyTrainTicketMapper dailyTrainTicketMapper;
@Resource
private TrainStationService trainStationService;
@Resource
private DailyTrainSeatService dailyTrainSeatService;
public void save(DailyTrainTicketSaveReq req) {
DateTime now = DateTime.now();
DailyTrainTicket dailyTrainTicket = BeanUtil.copyProperties(req, DailyTrainTicket.class);
if (ObjectUtil.isNull(dailyTrainTicket.getId())) {
dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());
dailyTrainTicket.setCreateTime(now);
dailyTrainTicket.setUpdateTime(now);
dailyTrainTicketMapper.insert(dailyTrainTicket);
} else {
dailyTrainTicket.setUpdateTime(now);
dailyTrainTicketMapper.updateByPrimaryKey(dailyTrainTicket);
}
}
public PageResp<DailyTrainTicketQueryResp> queryList(DailyTrainTicketQueryReq req) {
DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();
dailyTrainTicketExample.setOrderByClause("id desc");
DailyTrainTicketExample.Criteria criteria = dailyTrainTicketExample.createCriteria();
LOG.info("查询页码:{}", req.getPage());
LOG.info("每页条数:{}", req.getSize());
PageHelper.startPage(req.getPage(), req.getSize());
List<DailyTrainTicket> dailyTrainTicketList = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);
PageInfo<DailyTrainTicket> pageInfo = new PageInfo<>(dailyTrainTicketList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
List<DailyTrainTicketQueryResp> list = BeanUtil.copyToList(dailyTrainTicketList, DailyTrainTicketQueryResp.class);
PageResp<DailyTrainTicketQueryResp> pageResp = new PageResp<>();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
public void delete(Long id) {
dailyTrainTicketMapper.deleteByPrimaryKey(id);
}
@Transactional
public void genDaily(DailyTrain dailyTrain, Date date, String trainCode) {
LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);
// 删除某日某车次的余票信息
DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();
dailyTrainTicketExample.createCriteria()
.andDateEqualTo(date)
.andTrainCodeEqualTo(trainCode);
dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample);
// 查出某车次的所有的车站信息
List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);
if (CollUtil.isEmpty(stationList)) {
LOG.info("该车次没有车站基础数据,生成该车次的余票信息结束");
return;
}
DateTime now = DateTime.now();
int ydz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YDZ.getCode());
int edz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.EDZ.getCode());
int rw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.RW.getCode());
int yw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YW.getCode());
for (int i = 0; i < stationList.size(); i++) {
// 得到出发站
TrainStation trainStationStart = stationList.get(i);
BigDecimal sumKM = BigDecimal.ZERO;
for (int j = (i + 1); j < stationList.size(); j++) {
TrainStation trainStationEnd = stationList.get(j);
sumKM = sumKM.add(trainStationEnd.getKm());
DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();
dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());
dailyTrainTicket.setDate(date);
dailyTrainTicket.setTrainCode(trainCode);
dailyTrainTicket.setStart(trainStationStart.getName());
dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin());
dailyTrainTicket.setStartTime(trainStationStart.getOutTime());
dailyTrainTicket.setStartIndex(trainStationStart.getIndex());
dailyTrainTicket.setEnd(trainStationEnd.getName());
dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin());
dailyTrainTicket.setEndTime(trainStationEnd.getInTime());
dailyTrainTicket.setEndIndex(trainStationEnd.getIndex());
// 票价 = 里程之和 * 座位单价 * 车次类型系数
String trainType = dailyTrain.getType();
// 计算票价系数:TrainTypeEnum.priceRate
BigDecimal priceRate = EnumUtil.getFieldBy(TrainTypeEnum::getPriceRate, TrainTypeEnum::getCode, trainType);
BigDecimal ydzPrice = sumKM.multiply(SeatTypeEnum.YDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);
BigDecimal edzPrice = sumKM.multiply(SeatTypeEnum.EDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);
BigDecimal rwPrice = sumKM.multiply(SeatTypeEnum.RW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);
BigDecimal ywPrice = sumKM.multiply(SeatTypeEnum.YW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);
dailyTrainTicket.setYdz(ydz);
dailyTrainTicket.setYdzPrice(ydzPrice);
dailyTrainTicket.setEdz(edz);
dailyTrainTicket.setEdzPrice(edzPrice);
dailyTrainTicket.setRw(rw);
dailyTrainTicket.setRwPrice(rwPrice);
dailyTrainTicket.setYw(yw);
dailyTrainTicket.setYwPrice(ywPrice);
dailyTrainTicket.setCreateTime(now);
dailyTrainTicket.setUpdateTime(now);
dailyTrainTicketMapper.insert(dailyTrainTicket);
}
}
LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);
}
}
测试
重新生成车次
DailyTrainTicketQueryReq.java
package com.neilxu.train.business.req;
import com.neilxu.train.common.req.PageReq;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Data
public class DailyTrainTicketQueryReq extends PageReq {
/**
* 日期
*/
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date date;
/**
* 车次编号
*/
private String trainCode;
/**
* 出发站
*/
private String start;
/**
* 到达站
*/
private String end;
}
DailyTrainTicketService.java
package com.neilxu.train.business.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.neilxu.train.business.domain.DailyTrain;
import com.neilxu.train.business.domain.DailyTrainTicket;
import com.neilxu.train.business.domain.DailyTrainTicketExample;
import com.neilxu.train.business.domain.TrainStation;
import com.neilxu.train.business.enums.SeatTypeEnum;
import com.neilxu.train.business.enums.TrainTypeEnum;
import com.neilxu.train.business.mapper.DailyTrainTicketMapper;
import com.neilxu.train.business.req.DailyTrainTicketQueryReq;
import com.neilxu.train.business.req.DailyTrainTicketSaveReq;
import com.neilxu.train.business.resp.DailyTrainTicketQueryResp;
import com.neilxu.train.common.resp.PageResp;
import com.neilxu.train.common.util.SnowUtil;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.List;
@Service
public class DailyTrainTicketService {
private static final Logger LOG = LoggerFactory.getLogger(DailyTrainTicketService.class);
@Resource
private DailyTrainTicketMapper dailyTrainTicketMapper;
@Resource
private TrainStationService trainStationService;
@Resource
private DailyTrainSeatService dailyTrainSeatService;
public void save(DailyTrainTicketSaveReq req) {
DateTime now = DateTime.now();
DailyTrainTicket dailyTrainTicket = BeanUtil.copyProperties(req, DailyTrainTicket.class);
if (ObjectUtil.isNull(dailyTrainTicket.getId())) {
dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());
dailyTrainTicket.setCreateTime(now);
dailyTrainTicket.setUpdateTime(now);
dailyTrainTicketMapper.insert(dailyTrainTicket);
} else {
dailyTrainTicket.setUpdateTime(now);
dailyTrainTicketMapper.updateByPrimaryKey(dailyTrainTicket);
}
}
public PageResp<DailyTrainTicketQueryResp> queryList(DailyTrainTicketQueryReq req) {
DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();
dailyTrainTicketExample.setOrderByClause("id desc");
DailyTrainTicketExample.Criteria criteria = dailyTrainTicketExample.createCriteria();
if (ObjUtil.isNotNull(req.getDate())) {
criteria.andDateEqualTo(req.getDate());
}
if (ObjUtil.isNotEmpty(req.getTrainCode())) {
criteria.andTrainCodeEqualTo(req.getTrainCode());
}
if (ObjUtil.isNotEmpty(req.getStart())) {
criteria.andStartEqualTo(req.getStart());
}
if (ObjUtil.isNotEmpty(req.getEnd())) {
criteria.andEndEqualTo(req.getEnd());
}
LOG.info("查询页码:{}", req.getPage());
LOG.info("每页条数:{}", req.getSize());
PageHelper.startPage(req.getPage(), req.getSize());
List<DailyTrainTicket> dailyTrainTicketList = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);
PageInfo<DailyTrainTicket> pageInfo = new PageInfo<>(dailyTrainTicketList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
List<DailyTrainTicketQueryResp> list = BeanUtil.copyToList(dailyTrainTicketList, DailyTrainTicketQueryResp.class);
PageResp<DailyTrainTicketQueryResp> pageResp = new PageResp<>();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
public void delete(Long id) {
dailyTrainTicketMapper.deleteByPrimaryKey(id);
}
@Transactional
public void genDaily(DailyTrain dailyTrain, Date date, String trainCode) {
LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);
// 删除某日某车次的余票信息
DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();
dailyTrainTicketExample.createCriteria()
.andDateEqualTo(date)
.andTrainCodeEqualTo(trainCode);
dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample);
// 查出某车次的所有的车站信息
List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);
if (CollUtil.isEmpty(stationList)) {
LOG.info("该车次没有车站基础数据,生成该车次的余票信息结束");
return;
}
DateTime now = DateTime.now();
for (int i = 0; i < stationList.size(); i++) {
// 得到出发站
TrainStation trainStationStart = stationList.get(i);
BigDecimal sumKM = BigDecimal.ZERO;
for (int j = (i + 1); j < stationList.size(); j++) {
TrainStation trainStationEnd = stationList.get(j);
sumKM = sumKM.add(trainStationEnd.getKm());
DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();
dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());
dailyTrainTicket.setDate(date);
dailyTrainTicket.setTrainCode(trainCode);
dailyTrainTicket.setStart(trainStationStart.getName());
dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin());
dailyTrainTicket.setStartTime(trainStationStart.getOutTime());
dailyTrainTicket.setStartIndex(trainStationStart.getIndex());
dailyTrainTicket.setEnd(trainStationEnd.getName());
dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin());
dailyTrainTicket.setEndTime(trainStationEnd.getInTime());
dailyTrainTicket.setEndIndex(trainStationEnd.getIndex());
int ydz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YDZ.getCode());
int edz = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.EDZ.getCode());
int rw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.RW.getCode());
int yw = dailyTrainSeatService.countSeat(date, trainCode, SeatTypeEnum.YW.getCode());
// 票价 = 里程之和 * 座位单价 * 车次类型系数
String trainType = dailyTrain.getType();
// 计算票价系数:TrainTypeEnum.priceRate
BigDecimal priceRate = EnumUtil.getFieldBy(TrainTypeEnum::getPriceRate, TrainTypeEnum::getCode, trainType);
BigDecimal ydzPrice = sumKM.multiply(SeatTypeEnum.YDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);
BigDecimal edzPrice = sumKM.multiply(SeatTypeEnum.EDZ.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);
BigDecimal rwPrice = sumKM.multiply(SeatTypeEnum.RW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);
BigDecimal ywPrice = sumKM.multiply(SeatTypeEnum.YW.getPrice()).multiply(priceRate).setScale(2, RoundingMode.HALF_UP);
dailyTrainTicket.setYdz(ydz);
dailyTrainTicket.setYdzPrice(ydzPrice);
dailyTrainTicket.setEdz(edz);
dailyTrainTicket.setEdzPrice(edzPrice);
dailyTrainTicket.setRw(rw);
dailyTrainTicket.setRwPrice(rwPrice);
dailyTrainTicket.setYw(yw);
dailyTrainTicket.setYwPrice(ywPrice);
dailyTrainTicket.setCreateTime(now);
dailyTrainTicket.setUpdateTime(now);
dailyTrainTicketMapper.insert(dailyTrainTicket);
}
}
LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);
}
}
daily-train-ticket.vue
<template>
<p>
<a-space>
<train-select-view v-model="params.trainCode" width="200px"></train-select-view>
<a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期"></a-date-picker>
<station-select-view v-model="params.start" width="200px"></station-select-view>
<station-select-view v-model="params.end" width="200px"></station-select-view>
<a-button type="primary" @click="handleQuery()">查找</a-button>
</a-space>
</p>
<a-table :dataSource="dailyTrainTickets"
:columns="columns"
:pagination="pagination"
@change="handleTableChange"
:loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operation'">
</template>
</template>
</a-table>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue';
import {notification} from "ant-design-vue";
import axios from "axios";
import TrainSelectView from "@/components/train-select";
import StationSelectView from "@/components/station-select";
export default defineComponent({
name: "daily-train-ticket-view",
components: {StationSelectView, TrainSelectView},
setup() {
const visible = ref(false);
let dailyTrainTicket = ref({
id: undefined,
date: undefined,
trainCode: undefined,
start: undefined,
startPinyin: undefined,
startTime: undefined,
startIndex: undefined,
end: undefined,
endPinyin: undefined,
endTime: undefined,
endIndex: undefined,
ydz: undefined,
ydzPrice: undefined,
edz: undefined,
edzPrice: undefined,
rw: undefined,
rwPrice: undefined,
yw: undefined,
ywPrice: undefined,
createTime: undefined,
updateTime: undefined,
});
const dailyTrainTickets = ref([]);
// 分页的三个属性名是固定的
const pagination = ref({
total: 0,
current: 1,
pageSize: 10,
});
let loading = ref(false);
const params = ref({});
const columns = [
{
title: '日期',
dataIndex: 'date',
key: 'date',
},
{
title: '车次编号',
dataIndex: 'trainCode',
key: 'trainCode',
},
{
title: '出发站',
dataIndex: 'start',
key: 'start',
},
{
title: '出发站拼音',
dataIndex: 'startPinyin',
key: 'startPinyin',
},
{
title: '出发时间',
dataIndex: 'startTime',
key: 'startTime',
},
{
title: '出发站序',
dataIndex: 'startIndex',
key: 'startIndex',
},
{
title: '到达站',
dataIndex: 'end',
key: 'end',
},
{
title: '到达站拼音',
dataIndex: 'endPinyin',
key: 'endPinyin',
},
{
title: '到站时间',
dataIndex: 'endTime',
key: 'endTime',
},
{
title: '到站站序',
dataIndex: 'endIndex',
key: 'endIndex',
},
{
title: '一等座余票',
dataIndex: 'ydz',
key: 'ydz',
},
{
title: '一等座票价',
dataIndex: 'ydzPrice',
key: 'ydzPrice',
},
{
title: '二等座余票',
dataIndex: 'edz',
key: 'edz',
},
{
title: '二等座票价',
dataIndex: 'edzPrice',
key: 'edzPrice',
},
{
title: '软卧余票',
dataIndex: 'rw',
key: 'rw',
},
{
title: '软卧票价',
dataIndex: 'rwPrice',
key: 'rwPrice',
},
{
title: '硬卧余票',
dataIndex: 'yw',
key: 'yw',
},
{
title: '硬卧票价',
dataIndex: 'ywPrice',
key: 'ywPrice',
},
];
const handleQuery = (param) => {
if (!param) {
param = {
page: 1,
size: pagination.value.pageSize
};
}
loading.value = true;
axios.get("/business/admin/daily-train-ticket/query-list", {
params: {
page: param.page,
size: param.size,
trainCode: params.value.trainCode,
date: params.value.date,
start: params.value.start,
end: params.value.end
}
}).then((response) => {
loading.value = false;
let data = response.data;
if (data.success) {
dailyTrainTickets.value = data.content.list;
// 设置分页控件的值
pagination.value.current = param.page;
pagination.value.total = data.content.total;
} else {
notification.error({description: data.message});
}
});
};
const handleTableChange = (page) => {
// console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
pagination.value.pageSize = page.pageSize;
handleQuery({
page: page.current,
size: page.pageSize
});
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize
});
});
return {
dailyTrainTicket,
visible,
dailyTrainTickets,
pagination,
columns,
handleTableChange,
handleQuery,
loading,
params
};
},
});
</script>
测试
问题:当前页面显示信息太乱,且有多余的列,需要优化
优化余票信息页面
daily-train-ticket.vue
<template>
<p>
<a-space>
<train-select-view v-model="params.trainCode" width="200px"></train-select-view>
<a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期"></a-date-picker>
<station-select-view v-model="params.start" width="200px"></station-select-view>
<station-select-view v-model="params.end" width="200px"></station-select-view>
<a-button type="primary" @click="handleQuery()">查找</a-button>
</a-space>
</p>
<a-table :dataSource="dailyTrainTickets"
:columns="columns"
:pagination="pagination"
@change="handleTableChange"
:loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operation'">
</template>
<template v-else-if="column.dataIndex === 'station'">
{{record.start}}<br/>
{{record.end}}
</template>
<template v-else-if="column.dataIndex === 'time'">
{{record.startTime}}<br/>
{{record.endTime}}
</template>
<template v-else-if="column.dataIndex === 'duration'">
{{calDuration(record.startTime, record.endTime)}}<br/>
<div v-if="record.startTime.replaceAll(':', '') >= record.endTime.replaceAll(':', '')">
次日到达
</div>
<div v-else>
当日到达
</div>
</template>
<template v-else-if="column.dataIndex === 'ydz'">
<div v-if="record.ydz >= 0">
{{record.ydz}}<br/>
{{record.ydzPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'edz'">
<div v-if="record.edz >= 0">
{{record.edz}}<br/>
{{record.edzPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'rw'">
<div v-if="record.rw >= 0">
{{record.rw}}<br/>
{{record.rwPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'yw'">
<div v-if="record.yw >= 0">
{{record.yw}}<br/>
{{record.ywPrice}}¥
</div>
<div v-else>
--
</div>
</template>
</template>
</a-table>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue';
import {notification} from "ant-design-vue";
import axios from "axios";
import TrainSelectView from "@/components/train-select";
import StationSelectView from "@/components/station-select";
import dayjs from "dayjs";
export default defineComponent({
name: "daily-train-ticket-view",
components: {StationSelectView, TrainSelectView},
setup() {
const visible = ref(false);
let dailyTrainTicket = ref({
id: undefined,
date: undefined,
trainCode: undefined,
start: undefined,
startPinyin: undefined,
startTime: undefined,
startIndex: undefined,
end: undefined,
endPinyin: undefined,
endTime: undefined,
endIndex: undefined,
ydz: undefined,
ydzPrice: undefined,
edz: undefined,
edzPrice: undefined,
rw: undefined,
rwPrice: undefined,
yw: undefined,
ywPrice: undefined,
createTime: undefined,
updateTime: undefined,
});
const dailyTrainTickets = ref([]);
// 分页的三个属性名是固定的
const pagination = ref({
total: 0,
current: 1,
pageSize: 10,
});
let loading = ref(false);
const params = ref({});
const columns = [
{
title: '日期',
dataIndex: 'date',
key: 'date',
},
{
title: '车次编号',
dataIndex: 'trainCode',
key: 'trainCode',
},
{
title: '车站',
dataIndex: 'station',
},
{
title: '时间',
dataIndex: 'time',
},
{
title: '历时',
dataIndex: 'duration',
},
// {
// title: '出发站',
// dataIndex: 'start',
// key: 'start',
// },
// {
// title: '出发站拼音',
// dataIndex: 'startPinyin',
// key: 'startPinyin',
// },
// {
// title: '出发时间',
// dataIndex: 'startTime',
// key: 'startTime',
// },
// {
// title: '出发站序',
// dataIndex: 'startIndex',
// key: 'startIndex',
// },
// {
// title: '到达站',
// dataIndex: 'end',
// key: 'end',
// },
// {
// title: '到达站拼音',
// dataIndex: 'endPinyin',
// key: 'endPinyin',
// },
// {
// title: '到站时间',
// dataIndex: 'endTime',
// key: 'endTime',
// },
// {
// title: '到站站序',
// dataIndex: 'endIndex',
// key: 'endIndex',
// },
{
title: '一等座',
dataIndex: 'ydz',
key: 'ydz',
},
// {
// title: '一等座票价',
// dataIndex: 'ydzPrice',
// key: 'ydzPrice',
// },
{
title: '二等座',
dataIndex: 'edz',
key: 'edz',
},
// {
// title: '二等座票价',
// dataIndex: 'edzPrice',
// key: 'edzPrice',
// },
{
title: '软卧',
dataIndex: 'rw',
key: 'rw',
},
// {
// title: '软卧票价',
// dataIndex: 'rwPrice',
// key: 'rwPrice',
// },
{
title: '硬卧',
dataIndex: 'yw',
key: 'yw',
},
// {
// title: '硬卧票价',
// dataIndex: 'ywPrice',
// key: 'ywPrice',
// },
];
const handleQuery = (param) => {
if (!param) {
param = {
page: 1,
size: pagination.value.pageSize
};
}
loading.value = true;
axios.get("/business/admin/daily-train-ticket/query-list", {
params: {
page: param.page,
size: param.size,
trainCode: params.value.trainCode,
date: params.value.date,
start: params.value.start,
end: params.value.end
}
}).then((response) => {
loading.value = false;
let data = response.data;
if (data.success) {
dailyTrainTickets.value = data.content.list;
// 设置分页控件的值
pagination.value.current = param.page;
pagination.value.total = data.content.total;
} else {
notification.error({description: data.message});
}
});
};
const handleTableChange = (page) => {
// console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
pagination.value.pageSize = page.pageSize;
handleQuery({
page: page.current,
size: page.pageSize
});
};
const calDuration = (startTime, endTime) => {
let diff = dayjs(endTime, 'HH:mm:ss').diff(dayjs(startTime, 'HH:mm:ss'), 'seconds');
return dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize
});
});
return {
dailyTrainTicket,
visible,
dailyTrainTickets,
pagination,
columns,
handleTableChange,
handleQuery,
loading,
params,
calDuration
};
},
});
</script>
测试
修改乘客管理页面,正常显示页面;修改前端模块的启动指令名称
passenger.vue
web/src/views/main/passenger.vue
<template>
<p>
<a-space>
<a-button type="primary" @click="handleQuery()">刷新</a-button>
<a-button type="primary" @click="onAdd">新增</a-button>
</a-space>
</p>
<a-table :dataSource="passengers"
:columns="columns"
:pagination="pagination"
@change="handleTableChange"
:loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operation'">
<a-space>
<a-popconfirm
title="删除后不可恢复,确认删除?"
@confirm="onDelete(record)"
ok-text="确认" cancel-text="取消">
<a style="color: red">删除</a>
</a-popconfirm>
<a @click="onEdit(record)">编辑</a>
</a-space>
</template>
<template v-else-if="column.dataIndex === 'type'">
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
<span v-if="item.code === record.type">
{{item.desc}}
</span>
</span>
</template>
</template>
</a-table>
<a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
ok-text="确认" cancel-text="取消">
<a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
<a-form-item label="姓名">
<a-input v-model:value="passenger.name" />
</a-form-item>
<a-form-item label="身份证">
<a-input v-model:value="passenger.idCard" />
</a-form-item>
<a-form-item label="旅客类型">
<a-select v-model:value="passenger.type">
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue';
import {notification} from "ant-design-vue";
import axios from "axios";
export default defineComponent({
name: "passenger-view",
setup() {
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
const visible = ref(false);
let passenger = ref({
id: undefined,
memberId: undefined,
name: undefined,
idCard: undefined,
type: undefined,
createTime: undefined,
updateTime: undefined,
});
const passengers = ref([]);
// 分页的三个属性名是固定的
const pagination = ref({
total: 0,
current: 1,
pageSize: 10,
});
let loading = ref(false);
const columns = [
{
title: '会员id',
dataIndex: 'memberId',
key: 'memberId',
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '身份证',
dataIndex: 'idCard',
key: 'idCard',
},
{
title: '旅客类型',
dataIndex: 'type',
key: 'type',
},
{
title: '操作',
dataIndex: 'operation'
}
];
const onAdd = () => {
passenger.value = {};
visible.value = true;
};
const onEdit = (record) => {
passenger.value = window.Tool.copy(record);
visible.value = true;
};
const onDelete = (record) => {
axios.delete("/member/passenger/delete/" + record.id).then((response) => {
const data = response.data;
if (data.success) {
notification.success({description: "删除成功!"});
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
} else {
notification.error({description: data.message});
}
});
};
const handleOk = () => {
axios.post("/member/passenger/save", passenger.value).then((response) => {
let data = response.data;
if (data.success) {
notification.success({description: "保存成功!"});
visible.value = false;
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize
});
} else {
notification.error({description: data.message});
}
});
};
const handleQuery = (param) => {
if (!param) {
param = {
page: 1,
size: pagination.value.pageSize
};
}
loading.value = true;
axios.get("/member/passenger/query-list", {
params: {
page: param.page,
size: param.size
}
}).then((response) => {
loading.value = false;
let data = response.data;
if (data.success) {
passengers.value = data.content.list;
// 设置分页控件的值
pagination.value.current = param.page;
pagination.value.total = data.content.total;
} else {
notification.error({description: data.message});
}
});
};
const handleTableChange = (pagination) => {
// console.log("看看自带的分页参数都有啥:" + pagination);
handleQuery({
page: pagination.current,
size: pagination.pageSize
});
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize
});
});
return {
PASSENGER_TYPE_ARRAY,
passenger,
visible,
passengers,
pagination,
columns,
handleTableChange,
handleQuery,
loading,
onAdd,
handleOk,
onEdit,
onDelete
};
},
});
</script>
admin/package.json
"scripts": {
"admin-dev": "vue-cli-service serve --mode dev --port 9001",
"admin-prod": "vue-cli-service serve --mode prod --port 9001",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
web/package.json
"private": true,
"scripts": {
"web-dev": "vue-cli-service serve --mode dev --port 9000",
"web-prod": "vue-cli-service serve --mode prod --port 9000",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
测试效果
会员端增加余票查询页面,功能和控台端完全一致
操作:将前面控台端的代码复制到用户端
DailyTrainTicketController.java
package com.neilxu.train.business.controller;
import com.neilxu.train.business.req.DailyTrainTicketQueryReq;
import com.neilxu.train.business.resp.DailyTrainTicketQueryResp;
import com.neilxu.train.business.service.DailyTrainTicketService;
import com.neilxu.train.common.resp.CommonResp;
import com.neilxu.train.common.resp.PageResp;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/daily-train-ticket")
public class DailyTrainTicketController {
@Resource
private DailyTrainTicketService dailyTrainTicketService;
@GetMapping("/query-list")
public CommonResp<PageResp<DailyTrainTicketQueryResp>> queryList(@Valid DailyTrainTicketQueryReq req) {
PageResp<DailyTrainTicketQueryResp> list = dailyTrainTicketService.queryList(req);
return new CommonResp<>(list);
}
}
StationController.java
package com.neilxu.train.business.controller;
import com.neilxu.train.business.resp.StationQueryResp;
import com.neilxu.train.business.service.StationService;
import com.neilxu.train.common.resp.CommonResp;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/station")
public class StationController {
@Resource
private StationService stationService;
@GetMapping("/query-all")
public CommonResp<List<StationQueryResp>> queryList() {
List<StationQueryResp> list = stationService.queryAll();
return new CommonResp<>(list);
}
}
TrainController.java
package com.neilxu.train.business.controller;
import com.neilxu.train.business.resp.TrainQueryResp;
import com.neilxu.train.business.service.TrainService;
import com.neilxu.train.common.resp.CommonResp;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/train")
public class TrainController {
@Resource
private TrainService trainService;
@GetMapping("/query-all")
public CommonResp<List<TrainQueryResp>> queryList() {
List<TrainQueryResp> list = trainService.queryAll();
return new CommonResp<>(list);
}
}
station-select.vue
<template>
<a-select v-model:value="name" show-search allowClear
:filterOption="filterNameOption"
@change="onChange" placeholder="请选择车站"
:style="'width: ' + localWidth">
<a-select-option v-for="item in stations" :key="item.name" :value="item.name" :label="item.name + item.namePinyin + item.namePy">
{{item.name}} {{item.namePinyin}} ~ {{item.namePy}}
</a-select-option>
</a-select>
</template>
<script>
import {defineComponent, onMounted, ref, watch} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "station-select-view",
props: ["modelValue", "width"],
emits: ['update:modelValue', 'change'],
setup(props, {emit}) {
const name = ref();
const stations = ref([]);
const localWidth = ref(props.width);
if (Tool.isEmpty(props.width)) {
localWidth.value = "100%";
}
// 利用watch,动态获取父组件的值,如果放在onMounted或其它方法里,则只有第一次有效
watch(() => props.modelValue, ()=>{
console.log("props.modelValue", props.modelValue);
name.value = props.modelValue;
}, {immediate: true});
/**
* 查询所有的车站,用于车站下拉框
*/
const queryAllStation = () => {
axios.get("/business/station/query-all").then((response) => {
let data = response.data;
if (data.success) {
stations.value = data.content;
} else {
notification.error({description: data.message});
}
});
};
/**
* 车站下拉框筛选
*/
const filterNameOption = (input, option) => {
console.log(input, option);
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
/**
* 将当前组件的值响应给父组件
* @param value
*/
const onChange = (value) => {
emit('update:modelValue', value);
let station = stations.value.filter(item => item.code === value)[0];
if (Tool.isEmpty(station)) {
station = {};
}
emit('change', station);
};
onMounted(() => {
queryAllStation();
});
return {
name,
stations,
filterNameOption,
onChange,
localWidth
};
},
});
</script>
train-select.vue
<template>
<a-select v-model:value="trainCode" show-search allowClear
:filterOption="filterTrainCodeOption"
@change="onChange" placeholder="请选择车次"
:style="'width: ' + localWidth">
<a-select-option v-for="item in trains" :key="item.code" :value="item.code" :label="item.code + item.start + item.end">
{{item.code}} {{item.start}} ~ {{item.end}}
</a-select-option>
</a-select>
</template>
<script>
import {defineComponent, onMounted, ref, watch} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "train-select-view",
props: ["modelValue", "width"],
emits: ['update:modelValue', 'change'],
setup(props, {emit}) {
const trainCode = ref();
const trains = ref([]);
const localWidth = ref(props.width);
if (Tool.isEmpty(props.width)) {
localWidth.value = "100%";
}
// 利用watch,动态获取父组件的值,如果放在onMounted或其它方法里,则只有第一次有效
watch(() => props.modelValue, ()=>{
console.log("props.modelValue", props.modelValue);
trainCode.value = props.modelValue;
}, {immediate: true});
/**
* 查询所有的车次,用于车次下拉框
*/
const queryAllTrain = () => {
axios.get("/business/train/query-all").then((response) => {
let data = response.data;
if (data.success) {
trains.value = data.content;
} else {
notification.error({description: data.message});
}
});
};
/**
* 车次下拉框筛选
*/
const filterTrainCodeOption = (input, option) => {
console.log(input, option);
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
/**
* 将当前组件的值响应给父组件
* @param value
*/
const onChange = (value) => {
emit('update:modelValue', value);
let train = trains.value.filter(item => item.code === value)[0];
if (Tool.isEmpty(train)) {
train = {};
}
emit('change', train);
};
onMounted(() => {
queryAllTrain();
});
return {
trainCode,
trains,
filterTrainCodeOption,
onChange,
localWidth
};
},
});
</script>
the-header.vue和the-sider.vue
<a-menu-item key="/ticket">
<router-link to="/ticket">
<user-outlined /> 余票查询
</router-link>
</a-menu-item>
web/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import store from "@/store";
import {notification} from "ant-design-vue";
const routes = [{
path: '/login',
component: () => import('../views/login.vue')
}, {
path: '/',
component: () => import('../views/main.vue'),
meta: {
loginRequire: true
},
children: [{
path: 'welcome',
component: () => import('../views/main/welcome.vue'),
}, {
path: 'passenger',
component: () => import('../views/main/passenger.vue'),
}, {
path: 'ticket',
component: () => import('../views/main/ticket.vue'),
}]
}, {
path: '',
redirect: '/welcome'
}];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
// 路由登录拦截
router.beforeEach((to, from, next) => {
// 要不要对meta.loginRequire属性做监控拦截
if (to.matched.some(function (item) {
console.log(item, "是否需要登录校验:", item.meta.loginRequire || false);
return item.meta.loginRequire
})) {
const _member = store.state.member;
console.log("页面登录校验开始:", _member);
if (!_member.token) {
console.log("用户未登录或登录超时!");
notification.error({ description: "未登录或登录超时" });
next('/login');
} else {
next();
}
} else {
next();
}
});
export default router
ticket.vue
<template>
<p>
<a-space>
<train-select-view v-model="params.trainCode" width="200px"></train-select-view>
<a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期"></a-date-picker>
<station-select-view v-model="params.start" width="200px"></station-select-view>
<station-select-view v-model="params.end" width="200px"></station-select-view>
<a-button type="primary" @click="handleQuery()">查找</a-button>
</a-space>
</p>
<a-table :dataSource="dailyTrainTickets"
:columns="columns"
:pagination="pagination"
@change="handleTableChange"
:loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operation'">
</template>
<template v-else-if="column.dataIndex === 'station'">
{{record.start}}<br/>
{{record.end}}
</template>
<template v-else-if="column.dataIndex === 'time'">
{{record.startTime}}<br/>
{{record.endTime}}
</template>
<template v-else-if="column.dataIndex === 'duration'">
{{calDuration(record.startTime, record.endTime)}}<br/>
<div v-if="record.startTime.replaceAll(':', '') >= record.endTime.replaceAll(':', '')">
次日到达
</div>
<div v-else>
当日到达
</div>
</template>
<template v-else-if="column.dataIndex === 'ydz'">
<div v-if="record.ydz >= 0">
{{record.ydz}}<br/>
{{record.ydzPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'edz'">
<div v-if="record.edz >= 0">
{{record.edz}}<br/>
{{record.edzPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'rw'">
<div v-if="record.rw >= 0">
{{record.rw}}<br/>
{{record.rwPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'yw'">
<div v-if="record.yw >= 0">
{{record.yw}}<br/>
{{record.ywPrice}}¥
</div>
<div v-else>
--
</div>
</template>
</template>
</a-table>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue';
import {notification} from "ant-design-vue";
import axios from "axios";
import TrainSelectView from "@/components/train-select";
import StationSelectView from "@/components/station-select";
import dayjs from "dayjs";
export default defineComponent({
name: "ticket-view",
components: {StationSelectView, TrainSelectView},
setup() {
const visible = ref(false);
let dailyTrainTicket = ref({
id: undefined,
date: undefined,
trainCode: undefined,
start: undefined,
startPinyin: undefined,
startTime: undefined,
startIndex: undefined,
end: undefined,
endPinyin: undefined,
endTime: undefined,
endIndex: undefined,
ydz: undefined,
ydzPrice: undefined,
edz: undefined,
edzPrice: undefined,
rw: undefined,
rwPrice: undefined,
yw: undefined,
ywPrice: undefined,
createTime: undefined,
updateTime: undefined,
});
const dailyTrainTickets = ref([]);
// 分页的三个属性名是固定的
const pagination = ref({
total: 0,
current: 1,
pageSize: 10,
});
let loading = ref(false);
const params = ref({});
const columns = [
{
title: '日期',
dataIndex: 'date',
key: 'date',
},
{
title: '车次编号',
dataIndex: 'trainCode',
key: 'trainCode',
},
{
title: '车站',
dataIndex: 'station',
},
{
title: '时间',
dataIndex: 'time',
},
{
title: '历时',
dataIndex: 'duration',
},
// {
// title: '出发站',
// dataIndex: 'start',
// key: 'start',
// },
// {
// title: '出发站拼音',
// dataIndex: 'startPinyin',
// key: 'startPinyin',
// },
// {
// title: '出发时间',
// dataIndex: 'startTime',
// key: 'startTime',
// },
// {
// title: '出发站序',
// dataIndex: 'startIndex',
// key: 'startIndex',
// },
// {
// title: '到达站',
// dataIndex: 'end',
// key: 'end',
// },
// {
// title: '到达站拼音',
// dataIndex: 'endPinyin',
// key: 'endPinyin',
// },
// {
// title: '到站时间',
// dataIndex: 'endTime',
// key: 'endTime',
// },
// {
// title: '到站站序',
// dataIndex: 'endIndex',
// key: 'endIndex',
// },
{
title: '一等座',
dataIndex: 'ydz',
key: 'ydz',
},
// {
// title: '一等座票价',
// dataIndex: 'ydzPrice',
// key: 'ydzPrice',
// },
{
title: '二等座',
dataIndex: 'edz',
key: 'edz',
},
// {
// title: '二等座票价',
// dataIndex: 'edzPrice',
// key: 'edzPrice',
// },
{
title: '软卧',
dataIndex: 'rw',
key: 'rw',
},
// {
// title: '软卧票价',
// dataIndex: 'rwPrice',
// key: 'rwPrice',
// },
{
title: '硬卧',
dataIndex: 'yw',
key: 'yw',
},
// {
// title: '硬卧票价',
// dataIndex: 'ywPrice',
// key: 'ywPrice',
// },
];
const handleQuery = (param) => {
if (!param) {
param = {
page: 1,
size: pagination.value.pageSize
};
}
loading.value = true;
axios.get("/business/daily-train-ticket/query-list", {
params: {
page: param.page,
size: param.size,
trainCode: params.value.trainCode,
date: params.value.date,
start: params.value.start,
end: params.value.end
}
}).then((response) => {
loading.value = false;
let data = response.data;
if (data.success) {
dailyTrainTickets.value = data.content.list;
// 设置分页控件的值
pagination.value.current = param.page;
pagination.value.total = data.content.total;
} else {
notification.error({description: data.message});
}
});
};
const handleTableChange = (page) => {
// console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
pagination.value.pageSize = page.pageSize;
handleQuery({
page: page.current,
size: page.pageSize
});
};
const calDuration = (startTime, endTime) => {
let diff = dayjs(endTime, 'HH:mm:ss').diff(dayjs(startTime, 'HH:mm:ss'), 'seconds');
return dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize
});
});
return {
dailyTrainTicket,
visible,
dailyTrainTickets,
pagination,
columns,
handleTableChange,
handleQuery,
loading,
params,
calDuration
};
},
});
</script>
测试
问题:用户端不需要根据车次查询,另外三个参数则必须输入
修改余票查询页面,查询的三个参数必输,去掉车次查询条件
ticket.vue
<template>
<p>
<a-space>
<a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期"></a-date-picker>
<station-select-view v-model="params.start" width="200px"></station-select-view>
<station-select-view v-model="params.end" width="200px"></station-select-view>
<a-button type="primary" @click="handleQuery()">查找</a-button>
</a-space>
</p>
<a-table :dataSource="dailyTrainTickets"
:columns="columns"
:pagination="pagination"
@change="handleTableChange"
:loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operation'">
</template>
<template v-else-if="column.dataIndex === 'station'">
{{record.start}}<br/>
{{record.end}}
</template>
<template v-else-if="column.dataIndex === 'time'">
{{record.startTime}}<br/>
{{record.endTime}}
</template>
<template v-else-if="column.dataIndex === 'duration'">
{{calDuration(record.startTime, record.endTime)}}<br/>
<div v-if="record.startTime.replaceAll(':', '') >= record.endTime.replaceAll(':', '')">
次日到达
</div>
<div v-else>
当日到达
</div>
</template>
<template v-else-if="column.dataIndex === 'ydz'">
<div v-if="record.ydz >= 0">
{{record.ydz}}<br/>
{{record.ydzPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'edz'">
<div v-if="record.edz >= 0">
{{record.edz}}<br/>
{{record.edzPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'rw'">
<div v-if="record.rw >= 0">
{{record.rw}}<br/>
{{record.rwPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'yw'">
<div v-if="record.yw >= 0">
{{record.yw}}<br/>
{{record.ywPrice}}¥
</div>
<div v-else>
--
</div>
</template>
</template>
</a-table>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue';
import {notification} from "ant-design-vue";
import axios from "axios";
import StationSelectView from "@/components/station-select";
import dayjs from "dayjs";
export default defineComponent({
name: "ticket-view",
components: {StationSelectView},
setup() {
const visible = ref(false);
let dailyTrainTicket = ref({
id: undefined,
date: undefined,
trainCode: undefined,
start: undefined,
startPinyin: undefined,
startTime: undefined,
startIndex: undefined,
end: undefined,
endPinyin: undefined,
endTime: undefined,
endIndex: undefined,
ydz: undefined,
ydzPrice: undefined,
edz: undefined,
edzPrice: undefined,
rw: undefined,
rwPrice: undefined,
yw: undefined,
ywPrice: undefined,
createTime: undefined,
updateTime: undefined,
});
const dailyTrainTickets = ref([]);
// 分页的三个属性名是固定的
const pagination = ref({
total: 0,
current: 1,
pageSize: 10,
});
let loading = ref(false);
const params = ref({});
const columns = [
{
title: '车次编号',
dataIndex: 'trainCode',
key: 'trainCode',
},
{
title: '车站',
dataIndex: 'station',
},
{
title: '时间',
dataIndex: 'time',
},
{
title: '历时',
dataIndex: 'duration',
},
{
title: '一等座',
dataIndex: 'ydz',
key: 'ydz',
},
{
title: '二等座',
dataIndex: 'edz',
key: 'edz',
},
{
title: '软卧',
dataIndex: 'rw',
key: 'rw',
},
{
title: '硬卧',
dataIndex: 'yw',
key: 'yw',
},
];
const handleQuery = (param) => {
if (Tool.isEmpty(params.value.date)) {
notification.error({description: "请输入日期"});
return;
}
if (Tool.isEmpty(params.value.start)) {
notification.error({description: "请输入出发地"});
return;
}
if (Tool.isEmpty(params.value.end)) {
notification.error({description: "请输入目的地"});
return;
}
if (!param) {
param = {
page: 1,
size: pagination.value.pageSize
};
}
loading.value = true;
axios.get("/business/daily-train-ticket/query-list", {
params: {
page: param.page,
size: param.size,
trainCode: params.value.trainCode,
date: params.value.date,
start: params.value.start,
end: params.value.end
}
}).then((response) => {
loading.value = false;
let data = response.data;
if (data.success) {
dailyTrainTickets.value = data.content.list;
// 设置分页控件的值
pagination.value.current = param.page;
pagination.value.total = data.content.total;
} else {
notification.error({description: data.message});
}
});
};
const handleTableChange = (page) => {
// console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
pagination.value.pageSize = page.pageSize;
handleQuery({
page: page.current,
size: page.pageSize
});
};
const calDuration = (startTime, endTime) => {
let diff = dayjs(endTime, 'HH:mm:ss').diff(dayjs(startTime, 'HH:mm:ss'), 'seconds');
return dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
};
onMounted(() => {
// handleQuery({
// page: 1,
// size: pagination.value.pageSize
// });
});
return {
dailyTrainTicket,
visible,
dailyTrainTickets,
pagination,
columns,
handleTableChange,
handleQuery,
loading,
params,
calDuration
};
},
});
</script>
效果
order.vue
下单页面
<template>
<div>{{ dailyTrainTicket }}</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: "order-view",
setup() {
const dailyTrainTicket = SessionStorage.get("dailyTrainTicket") || {};
console.log("下单的车次信息", dailyTrainTicket);
return {
dailyTrainTicket
};
},
});
</script>
ticket.vue
<template>
<p>
<a-space>
<a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期"></a-date-picker>
<station-select-view v-model="params.start" width="200px"></station-select-view>
<station-select-view v-model="params.end" width="200px"></station-select-view>
<a-button type="primary" @click="handleQuery()">查找</a-button>
</a-space>
</p>
<a-table :dataSource="dailyTrainTickets"
:columns="columns"
:pagination="pagination"
@change="handleTableChange"
:loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operation'">
<a-button type="primary" @click="toOrder(record)">预订</a-button>
</template>
<template v-else-if="column.dataIndex === 'station'">
{{record.start}}<br/>
{{record.end}}
</template>
<template v-else-if="column.dataIndex === 'time'">
{{record.startTime}}<br/>
{{record.endTime}}
</template>
<template v-else-if="column.dataIndex === 'duration'">
{{calDuration(record.startTime, record.endTime)}}<br/>
<div v-if="record.startTime.replaceAll(':', '') >= record.endTime.replaceAll(':', '')">
次日到达
</div>
<div v-else>
当日到达
</div>
</template>
<template v-else-if="column.dataIndex === 'ydz'">
<div v-if="record.ydz >= 0">
{{record.ydz}}<br/>
{{record.ydzPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'edz'">
<div v-if="record.edz >= 0">
{{record.edz}}<br/>
{{record.edzPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'rw'">
<div v-if="record.rw >= 0">
{{record.rw}}<br/>
{{record.rwPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'yw'">
<div v-if="record.yw >= 0">
{{record.yw}}<br/>
{{record.ywPrice}}¥
</div>
<div v-else>
--
</div>
</template>
</template>
</a-table>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue';
import {notification} from "ant-design-vue";
import axios from "axios";
import StationSelectView from "@/components/station-select";
import dayjs from "dayjs";
import router from "@/router";
export default defineComponent({
name: "ticket-view",
components: {StationSelectView},
setup() {
const visible = ref(false);
let dailyTrainTicket = ref({
id: undefined,
date: undefined,
trainCode: undefined,
start: undefined,
startPinyin: undefined,
startTime: undefined,
startIndex: undefined,
end: undefined,
endPinyin: undefined,
endTime: undefined,
endIndex: undefined,
ydz: undefined,
ydzPrice: undefined,
edz: undefined,
edzPrice: undefined,
rw: undefined,
rwPrice: undefined,
yw: undefined,
ywPrice: undefined,
createTime: undefined,
updateTime: undefined,
});
const dailyTrainTickets = ref([]);
// 分页的三个属性名是固定的
const pagination = ref({
total: 0,
current: 1,
pageSize: 10,
});
let loading = ref(false);
const params = ref({});
const columns = [
{
title: '车次编号',
dataIndex: 'trainCode',
key: 'trainCode',
},
{
title: '车站',
dataIndex: 'station',
},
{
title: '时间',
dataIndex: 'time',
},
{
title: '历时',
dataIndex: 'duration',
},
{
title: '一等座',
dataIndex: 'ydz',
key: 'ydz',
},
{
title: '二等座',
dataIndex: 'edz',
key: 'edz',
},
{
title: '软卧',
dataIndex: 'rw',
key: 'rw',
},
{
title: '硬卧',
dataIndex: 'yw',
key: 'yw',
},
{
title: '操作',
dataIndex: 'operation',
},
];
const handleQuery = (param) => {
if (Tool.isEmpty(params.value.date)) {
notification.error({description: "请输入日期"});
return;
}
if (Tool.isEmpty(params.value.start)) {
notification.error({description: "请输入出发地"});
return;
}
if (Tool.isEmpty(params.value.end)) {
notification.error({description: "请输入目的地"});
return;
}
if (!param) {
param = {
page: 1,
size: pagination.value.pageSize
};
}
loading.value = true;
axios.get("/business/daily-train-ticket/query-list", {
params: {
page: param.page,
size: param.size,
trainCode: params.value.trainCode,
date: params.value.date,
start: params.value.start,
end: params.value.end
}
}).then((response) => {
loading.value = false;
let data = response.data;
if (data.success) {
dailyTrainTickets.value = data.content.list;
// 设置分页控件的值
pagination.value.current = param.page;
pagination.value.total = data.content.total;
} else {
notification.error({description: data.message});
}
});
};
const handleTableChange = (page) => {
// console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
pagination.value.pageSize = page.pageSize;
handleQuery({
page: page.current,
size: page.pageSize
});
};
const calDuration = (startTime, endTime) => {
let diff = dayjs(endTime, 'HH:mm:ss').diff(dayjs(startTime, 'HH:mm:ss'), 'seconds');
return dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
};
const toOrder = (record) => {
dailyTrainTicket.value = Tool.copy(record);
SessionStorage.set("dailyTrainTicket", dailyTrainTicket.value);
router.push("/order")
};
onMounted(() => {
// handleQuery({
// page: 1,
// size: pagination.value.pageSize
// });
});
return {
dailyTrainTicket,
visible,
dailyTrainTickets,
pagination,
columns,
handleTableChange,
handleQuery,
loading,
params,
calDuration,
toOrder
};
},
});
</script>
路由
import { createRouter, createWebHistory } from 'vue-router'
import store from "@/store";
import {notification} from "ant-design-vue";
const routes = [{
path: '/login',
component: () => import('../views/login.vue')
}, {
path: '/',
component: () => import('../views/main.vue'),
meta: {
loginRequire: true
},
children: [{
path: 'welcome',
component: () => import('../views/main/welcome.vue'),
}, {
path: 'passenger',
component: () => import('../views/main/passenger.vue'),
}, {
path: 'ticket',
component: () => import('../views/main/ticket.vue'),
}, {
path: 'order',
component: () => import('../views/main/order.vue'),
}]
}, {
path: '',
redirect: '/welcome'
}];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
// 路由登录拦截
router.beforeEach((to, from, next) => {
// 要不要对meta.loginRequire属性做监控拦截
if (to.matched.some(function (item) {
console.log(item, "是否需要登录校验:", item.meta.loginRequire || false);
return item.meta.loginRequire
})) {
const _member = store.state.member;
console.log("页面登录校验开始:", _member);
if (!_member.token) {
console.log("用户未登录或登录超时!");
notification.error({ description: "未登录或登录超时" });
next('/login');
} else {
next();
}
} else {
next();
}
});
export default router
效果
点击预定
web/public/js/session-storage.js
// 所有的session key都在这里统一定义,可以避免多个功能使用同一个key
SESSION_ORDER = "SESSION_ORDER";
SESSION_TICKET_PARAMS = "SESSION_TICKET_PARAMS";
web/src/views/main/order.vue
export default defineComponent({
name: "order-view",
setup() {
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
return {
dailyTrainTicket
};
},
});
web/src/views/main/ticket.vue
<template>
<p>
<a-space>
<a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期"></a-date-picker>
<station-select-view v-model="params.start" width="200px"></station-select-view>
<station-select-view v-model="params.end" width="200px"></station-select-view>
<a-button type="primary" @click="handleQuery()">查找</a-button>
</a-space>
</p>
<a-table :dataSource="dailyTrainTickets"
:columns="columns"
:pagination="pagination"
@change="handleTableChange"
:loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operation'">
<a-button type="primary" @click="toOrder(record)">预订</a-button>
</template>
<template v-else-if="column.dataIndex === 'station'">
{{record.start}}<br/>
{{record.end}}
</template>
<template v-else-if="column.dataIndex === 'time'">
{{record.startTime}}<br/>
{{record.endTime}}
</template>
<template v-else-if="column.dataIndex === 'duration'">
{{calDuration(record.startTime, record.endTime)}}<br/>
<div v-if="record.startTime.replaceAll(':', '') >= record.endTime.replaceAll(':', '')">
次日到达
</div>
<div v-else>
当日到达
</div>
</template>
<template v-else-if="column.dataIndex === 'ydz'">
<div v-if="record.ydz >= 0">
{{record.ydz}}<br/>
{{record.ydzPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'edz'">
<div v-if="record.edz >= 0">
{{record.edz}}<br/>
{{record.edzPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'rw'">
<div v-if="record.rw >= 0">
{{record.rw}}<br/>
{{record.rwPrice}}¥
</div>
<div v-else>
--
</div>
</template>
<template v-else-if="column.dataIndex === 'yw'">
<div v-if="record.yw >= 0">
{{record.yw}}<br/>
{{record.ywPrice}}¥
</div>
<div v-else>
--
</div>
</template>
</template>
</a-table>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue';
import {notification} from "ant-design-vue";
import axios from "axios";
import StationSelectView from "@/components/station-select";
import dayjs from "dayjs";
import router from "@/router";
export default defineComponent({
name: "ticket-view",
components: {StationSelectView},
setup() {
const visible = ref(false);
let dailyTrainTicket = ref({
id: undefined,
date: undefined,
trainCode: undefined,
start: undefined,
startPinyin: undefined,
startTime: undefined,
startIndex: undefined,
end: undefined,
endPinyin: undefined,
endTime: undefined,
endIndex: undefined,
ydz: undefined,
ydzPrice: undefined,
edz: undefined,
edzPrice: undefined,
rw: undefined,
rwPrice: undefined,
yw: undefined,
ywPrice: undefined,
createTime: undefined,
updateTime: undefined,
});
const dailyTrainTickets = ref([]);
// 分页的三个属性名是固定的
const pagination = ref({
total: 0,
current: 1,
pageSize: 10,
});
let loading = ref(false);
const params = ref({});
const columns = [
{
title: '车次编号',
dataIndex: 'trainCode',
key: 'trainCode',
},
{
title: '车站',
dataIndex: 'station',
},
{
title: '时间',
dataIndex: 'time',
},
{
title: '历时',
dataIndex: 'duration',
},
{
title: '一等座',
dataIndex: 'ydz',
key: 'ydz',
},
{
title: '二等座',
dataIndex: 'edz',
key: 'edz',
},
{
title: '软卧',
dataIndex: 'rw',
key: 'rw',
},
{
title: '硬卧',
dataIndex: 'yw',
key: 'yw',
},
{
title: '操作',
dataIndex: 'operation',
},
];
const handleQuery = (param) => {
if (Tool.isEmpty(params.value.date)) {
notification.error({description: "请输入日期"});
return;
}
if (Tool.isEmpty(params.value.start)) {
notification.error({description: "请输入出发地"});
return;
}
if (Tool.isEmpty(params.value.end)) {
notification.error({description: "请输入目的地"});
return;
}
if (!param) {
param = {
page: 1,
size: pagination.value.pageSize
};
}
// 保存查询参数
SessionStorage.set(SESSION_TICKET_PARAMS, params.value);
loading.value = true;
axios.get("/business/daily-train-ticket/query-list", {
params: {
page: param.page,
size: param.size,
trainCode: params.value.trainCode,
date: params.value.date,
start: params.value.start,
end: params.value.end
}
}).then((response) => {
loading.value = false;
let data = response.data;
if (data.success) {
dailyTrainTickets.value = data.content.list;
// 设置分页控件的值
pagination.value.current = param.page;
pagination.value.total = data.content.total;
} else {
notification.error({description: data.message});
}
});
};
const handleTableChange = (page) => {
// console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
pagination.value.pageSize = page.pageSize;
handleQuery({
page: page.current,
size: page.pageSize
});
};
const calDuration = (startTime, endTime) => {
let diff = dayjs(endTime, 'HH:mm:ss').diff(dayjs(startTime, 'HH:mm:ss'), 'seconds');
return dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
};
const toOrder = (record) => {
dailyTrainTicket.value = Tool.copy(record);
SessionStorage.set(SESSION_ORDER, dailyTrainTicket.value);
router.push("/order")
};
onMounted(() => {
// "|| {}"是常用技巧,可以避免空指针异常
params.value = SessionStorage.get(SESSION_TICKET_PARAMS) || {};
if (Tool.isNotEmpty(params.value)) {
handleQuery({
page: 1,
size: pagination.value.pageSize
});
}
});
return {
dailyTrainTicket,
visible,
dailyTrainTickets,
pagination,
columns,
handleTableChange,
handleQuery,
loading,
params,
calDuration,
toOrder
};
},
});
</script>
效果
只要不关闭页面,可以缓存查询条件
web/src/views/main/order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: "order-view",
setup() {
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
return {
dailyTrainTicket
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
</style>
效果
重新生成下枚举文件
修改EnumGenerator.java,生成web/src/assets/js/enums.js
web/src/views/main/order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: "order-view",
setup() {
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
return {
dailyTrainTicket,
seatTypes
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
</style>
效果
PassengerService.java
/**
* 查询我的所有乘客
*/
public List<PassengerQueryResp> queryMine() {
PassengerExample passengerExample = new PassengerExample();
passengerExample.setOrderByClause("name asc");
PassengerExample.Criteria criteria = passengerExample.createCriteria();
criteria.andMemberIdEqualTo(LoginMemberContext.getId());
List<Passenger> list = passengerMapper.selectByExample(passengerExample);
return BeanUtil.copyToList(list, PassengerQueryResp.class);
}
PassengerController.java
@GetMapping("/query-mine")
public CommonResp<List<PassengerQueryResp>> queryMine() {
List<PassengerQueryResp> list = passengerService.queryMine();
return new CommonResp<>(list);
}
web/src/views/main/order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
{{passengers}}
</template>
<script>
import {defineComponent, ref, onMounted} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
} else {
notification.error({description: data.message});
}
});
};
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
</style>
效果
web/src/views/main/order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
<b>勾选要购票的乘客:</b>
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
<br/>
选中的乘客:{{passengerChecks}}
</template>
<script>
import {defineComponent, ref, onMounted} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const passengerOptions = ref([]);
const passengerChecks = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
passengers.value.forEach((item) => passengerOptions.value.push({
label: item.name,
value: item.id
}))
} else {
notification.error({description: data.message});
}
});
};
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes,
passengerOptions,
passengerChecks
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
</style>
效果
web/src/views/main/order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
<b>勾选要购票的乘客:</b>
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
<br/>
选中的乘客:{{passengerChecks}}
<br/>
购票列表:{{tickets}}
</template>
<script>
import {defineComponent, ref, onMounted, watch} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const passengerOptions = ref([]);
const passengerChecks = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
// {
// passengerId: 123,
// passengerType: "1",
// passengerName: "张三",
// passengerIdCard: "12323132132",
// seatTypeCode: "1"
// }
const tickets = ref([]);
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
watch(() => passengerChecks.value, (newVal, oldVal)=>{
console.log("勾选乘客发生变化", newVal, oldVal)
// 每次有变化时,把购票列表清空,重新构造列表
tickets.value = [];
passengerChecks.value.forEach((item) => tickets.value.push({
passengerId: item.id,
passengerType: item.type,
seatTypeCode: seatTypes[0].code,
passengerName: item.name,
passengerIdCard: item.idCard
}))
}, {immediate: true});
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
passengers.value.forEach((item) => passengerOptions.value.push({
label: item.name,
value: item
}))
} else {
notification.error({description: data.message});
}
});
};
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes,
passengerOptions,
passengerChecks,
tickets
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
</style>
效果
web/src/views/main/order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
<b>勾选要购票的乘客:</b>
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
<br/>
选中的乘客:{{passengerChecks}}
<br/>
购票列表:{{tickets}}
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="2">乘客</a-col>
<a-col :span="6">身份证</a-col>
<a-col :span="4">票种</a-col>
<a-col :span="4">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="2">{{ticket.passengerName}}</a-col>
<a-col :span="6">{{ticket.passengerIdCard}}</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.passengerType" style="width: 100%">
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
<a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
</a-row>
</div>
</template>
<script>
import {defineComponent, ref, onMounted, watch} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const passengerOptions = ref([]);
const passengerChecks = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
// {
// passengerId: 123,
// passengerType: "1",
// passengerName: "张三",
// passengerIdCard: "12323132132",
// seatTypeCode: "1"
// }
const tickets = ref([]);
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
watch(() => passengerChecks.value, (newVal, oldVal)=>{
console.log("勾选乘客发生变化", newVal, oldVal)
// 每次有变化时,把购票列表清空,重新构造列表
tickets.value = [];
passengerChecks.value.forEach((item) => tickets.value.push({
passengerId: item.id,
passengerType: item.type,
seatTypeCode: seatTypes[0].code,
passengerName: item.name,
passengerIdCard: item.idCard
}))
}, {immediate: true});
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
passengers.value.forEach((item) => passengerOptions.value.push({
label: item.name,
value: item
}))
} else {
notification.error({description: data.message});
}
});
};
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes,
passengerOptions,
passengerChecks,
tickets,
PASSENGER_TYPE_ARRAY
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
.order-tickets {
margin: 10px 0;
}
.order-tickets .ant-col {
padding: 5px 10px;
}
.order-tickets .order-tickets-header {
background-color: cornflowerblue;
border: solid 1px cornflowerblue;
color: white;
font-size: 16px;
padding: 5px 0;
}
.order-tickets .order-tickets-row {
border: solid 1px cornflowerblue;
border-top: none;
vertical-align: middle;
line-height: 30px;
}
</style>
效果
web/src/views/main/order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
<b>勾选要购票的乘客:</b>
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="2">乘客</a-col>
<a-col :span="6">身份证</a-col>
<a-col :span="4">票种</a-col>
<a-col :span="4">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="2">{{ticket.passengerName}}</a-col>
<a-col :span="6">{{ticket.passengerIdCard}}</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.passengerType" style="width: 100%">
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
<a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
</a-row>
</div>
<div v-if="tickets.length > 0">
<a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
</div>
<a-modal v-model:visible="visible" title="请核对以下信息"
style="top: 50px; width: 800px"
ok-text="确认" cancel-text="取消">
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="3">乘客</a-col>
<a-col :span="15">身份证</a-col>
<a-col :span="3">票种</a-col>
<a-col :span="3">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="3">{{ticket.passengerName}}</a-col>
<a-col :span="15">{{ticket.passengerIdCard}}</a-col>
<a-col :span="3">
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
<span v-if="item.code === ticket.passengerType">
{{item.desc}}
</span>
</span>
</a-col>
<a-col :span="3">
<span v-for="item in seatTypes" :key="item.code">
<span v-if="item.code === ticket.seatTypeCode">
{{item.desc}}
</span>
</span>
</a-col>
</a-row>
</div>
</a-modal>
</template>
<script>
import {defineComponent, ref, onMounted, watch} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const passengerOptions = ref([]);
const passengerChecks = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
// {
// passengerId: 123,
// passengerType: "1",
// passengerName: "张三",
// passengerIdCard: "12323132132",
// seatTypeCode: "1"
// }
const tickets = ref([]);
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
const visible = ref(false);
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
watch(() => passengerChecks.value, (newVal, oldVal)=>{
console.log("勾选乘客发生变化", newVal, oldVal)
// 每次有变化时,把购票列表清空,重新构造列表
tickets.value = [];
passengerChecks.value.forEach((item) => tickets.value.push({
passengerId: item.id,
passengerType: item.type,
seatTypeCode: seatTypes[0].code,
passengerName: item.name,
passengerIdCard: item.idCard
}))
}, {immediate: true});
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
passengers.value.forEach((item) => passengerOptions.value.push({
label: item.name,
value: item
}))
} else {
notification.error({description: data.message});
}
});
};
const finishCheckPassenger = () => {
console.log("购票列表:", tickets.value);
if (tickets.value.length > 5) {
notification.error({description: '最多只能购买5张车票'});
return;
}
// 弹出确认界面
visible.value = true;
};
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes,
passengerOptions,
passengerChecks,
tickets,
PASSENGER_TYPE_ARRAY,
visible,
finishCheckPassenger
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
.order-tickets {
margin: 10px 0;
}
.order-tickets .ant-col {
padding: 5px 10px;
}
.order-tickets .order-tickets-header {
background-color: cornflowerblue;
border: solid 1px cornflowerblue;
color: white;
font-size: 16px;
padding: 5px 0;
}
.order-tickets .order-tickets-row {
border: solid 1px cornflowerblue;
border-top: none;
vertical-align: middle;
line-height: 30px;
}
</style>
效果
12306规则:
- 只有全部是一等座或全部是二等座才支持选座
- 余票小于一定数量时,不允许选座(本项目以20为例)
构造两个重要的响应式变量:
// 0:不支持选座;1:选一等座;2:选二等座 const chooseSeatType = ref(0); // 选择的座位 // { // A1: false, C1: true,D1: false, F1: false, // A2: false, C2: false,D2: true, F2: false // } const chooseSeatObj = ref({});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
最终购票tickets:
// seat可选,当无选座时,seat为空 [{ passengerId: 123, passengerType: "1", seatTypeCode: "1", passengerName: "张三", passengerIdCard: "12323132132", seat: "C1" }, { passengerId: 123, passengerType: "1", seatTypeCode: "1", passengerName: "李四", passengerIdCard: "12323132132", seat: "D2" }]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
座位售卖详情,比如有ABCDE五个站,sell=0110,则AB未被购买,AC已被购买
后端购票逻辑,分成选座和不选座
不选座,以购买一等座为例:遍历一等座车厢,每个车厢从1号座位开始找,未被购买的,就选中它
选座,以购买两张一等座AB为例:遍历一等座车厢,每个车厢从1号座位开始找A列座位,未被购买的,就预选中它;再挑它旁边的B,如果也未被购买,则最终选中这两个座位,如果B已被购买,则回到第一步,继续找未被购买的A座。从第二个座位开始,需要计算和第一个座位的偏移值,可以减少循环,提高选座效率
举例:当选择A1和C2座位,遍历找到A1座位,索引为0,则C2不需要再遍历,直接计算出偏移值是5,即索引是5,看可以不可选就行了
这节是前端的选座逻辑的实现
order.vue
const finishCheckPassenger = () => {
console.log("购票列表:", tickets.value);
if (tickets.value.length > 5) {
notification.error({description: '最多只能购买5张车票'});
return;
}
// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
// 前端校验不一定准,但前端校验可以减轻后端很多压力
// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
let seatTypesTemp = Tool.copy(seatTypes);
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
for (let j = 0; j < seatTypesTemp.length; j++) {
let seatType = seatTypesTemp[j];
// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
if (ticket.seatTypeCode === seatType.code) {
seatType.count--;
if (seatType.count < 0) {
notification.error({description: seatType.desc + '余票不足'});
return;
}
}
}
}
console.log("前端余票校验通过");
// 弹出确认界面
visible.value = true;
};
只有都选择一等座或都选二等座,才可以支持选座
获取到选座类型后,如果是一等座得到一等座的选座对象,二等座得到二等座的选座对象,如果是不可选,选座对象为空
order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
<b>勾选要购票的乘客:</b>
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="2">乘客</a-col>
<a-col :span="6">身份证</a-col>
<a-col :span="4">票种</a-col>
<a-col :span="4">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="2">{{ticket.passengerName}}</a-col>
<a-col :span="6">{{ticket.passengerIdCard}}</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.passengerType" style="width: 100%">
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
<a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
</a-row>
</div>
<div v-if="tickets.length > 0">
<a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
</div>
<a-modal v-model:visible="visible" title="请核对以下信息"
style="top: 50px; width: 800px"
ok-text="确认" cancel-text="取消">
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="3">乘客</a-col>
<a-col :span="15">身份证</a-col>
<a-col :span="3">票种</a-col>
<a-col :span="3">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="3">{{ticket.passengerName}}</a-col>
<a-col :span="15">{{ticket.passengerIdCard}}</a-col>
<a-col :span="3">
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
<span v-if="item.code === ticket.passengerType">
{{item.desc}}
</span>
</span>
</a-col>
<a-col :span="3">
<span v-for="item in seatTypes" :key="item.code">
<span v-if="item.code === ticket.seatTypeCode">
{{item.desc}}
</span>
</span>
</a-col>
</a-row>
<br/>
选座类型chooseSeatType:{{chooseSeatType}}
<br/>
选座对象chooseSeatType:{{chooseSeatObj}}
<br/>
座位类型SEAT_COL_ARRAY:{{SEAT_COL_ARRAY}}
</div>
</a-modal>
</template>
<script>
import {defineComponent, ref, onMounted, watch, computed} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const passengerOptions = ref([]);
const passengerChecks = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
// {
// passengerId: 123,
// passengerType: "1",
// passengerName: "张三",
// passengerIdCard: "12323132132",
// seatTypeCode: "1"
// }
const tickets = ref([]);
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
const visible = ref(false);
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
watch(() => passengerChecks.value, (newVal, oldVal)=>{
console.log("勾选乘客发生变化", newVal, oldVal)
// 每次有变化时,把购票列表清空,重新构造列表
tickets.value = [];
passengerChecks.value.forEach((item) => tickets.value.push({
passengerId: item.id,
passengerType: item.type,
seatTypeCode: seatTypes[0].code,
passengerName: item.name,
passengerIdCard: item.idCard
}))
}, {immediate: true});
// 0:不支持选座;1:选一等座;2:选二等座
const chooseSeatType = ref(0);
// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
const SEAT_COL_ARRAY = computed(() => {
return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
});
// 选择的座位
// {
// A1: false, C1: true,D1: false, F1: false,
// A2: false, C2: false,D2: true, F2: false
// }
const chooseSeatObj = ref({});
watch(() => SEAT_COL_ARRAY.value, () => {
for (let i = 1; i <= 2; i++) {
SEAT_COL_ARRAY.value.forEach((item) => {
chooseSeatObj.value[item.code + i] = false;
})
}
console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
}, {immediate: true});
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
passengers.value.forEach((item) => passengerOptions.value.push({
label: item.name,
value: item
}))
} else {
notification.error({description: data.message});
}
});
};
const finishCheckPassenger = () => {
console.log("购票列表:", tickets.value);
if (tickets.value.length > 5) {
notification.error({description: '最多只能购买5张车票'});
return;
}
// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
// 前端校验不一定准,但前端校验可以减轻后端很多压力
// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
let seatTypesTemp = Tool.copy(seatTypes);
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
for (let j = 0; j < seatTypesTemp.length; j++) {
let seatType = seatTypesTemp[j];
// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
if (ticket.seatTypeCode === seatType.code) {
seatType.count--;
if (seatType.count < 0) {
notification.error({description: seatType.desc + '余票不足'});
return;
}
}
}
}
console.log("前端余票校验通过");
// 判断是否支持选座,只有纯一等座和纯二等座支持选座
// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
let ticketSeatTypeCodes = [];
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
ticketSeatTypeCodes.push(ticket.seatTypeCode);
}
// 为购票列表中的所有座位类型去重:[1, 2]
const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
console.log("选好的座位类型:", ticketSeatTypeCodesSet);
if (ticketSeatTypeCodesSet.length !== 1) {
console.log("选了多种座位,不支持选座");
chooseSeatType.value = 0;
} else {
// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
console.log("一等座选座");
chooseSeatType.value = SEAT_TYPE.YDZ.code;
} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
console.log("二等座选座");
chooseSeatType.value = SEAT_TYPE.EDZ.code;
} else {
console.log("不是一等座或二等座,不支持选座");
chooseSeatType.value = 0;
}
}
// 弹出确认界面
visible.value = true;
};
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes,
passengerOptions,
passengerChecks,
tickets,
PASSENGER_TYPE_ARRAY,
visible,
finishCheckPassenger,
chooseSeatType,
chooseSeatObj,
SEAT_COL_ARRAY,
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
.order-tickets {
margin: 10px 0;
}
.order-tickets .ant-col {
padding: 5px 10px;
}
.order-tickets .order-tickets-header {
background-color: cornflowerblue;
border: solid 1px cornflowerblue;
color: white;
font-size: 16px;
padding: 5px 0;
}
.order-tickets .order-tickets-row {
border: solid 1px cornflowerblue;
border-top: none;
vertical-align: middle;
line-height: 30px;
}
</style>
效果
order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
<b>勾选要购票的乘客:</b>
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="2">乘客</a-col>
<a-col :span="6">身份证</a-col>
<a-col :span="4">票种</a-col>
<a-col :span="4">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="2">{{ticket.passengerName}}</a-col>
<a-col :span="6">{{ticket.passengerIdCard}}</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.passengerType" style="width: 100%">
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
<a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
</a-row>
</div>
<div v-if="tickets.length > 0">
<a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
</div>
<a-modal v-model:visible="visible" title="请核对以下信息"
style="top: 50px; width: 800px"
ok-text="确认" cancel-text="取消">
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="3">乘客</a-col>
<a-col :span="15">身份证</a-col>
<a-col :span="3">票种</a-col>
<a-col :span="3">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="3">{{ticket.passengerName}}</a-col>
<a-col :span="15">{{ticket.passengerIdCard}}</a-col>
<a-col :span="3">
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
<span v-if="item.code === ticket.passengerType">
{{item.desc}}
</span>
</span>
</a-col>
<a-col :span="3">
<span v-for="item in seatTypes" :key="item.code">
<span v-if="item.code === ticket.seatTypeCode">
{{item.desc}}
</span>
</span>
</a-col>
</a-row>
<br/>
选座对象chooseSeatType:{{chooseSeatObj}}
<br/>
<div v-if="chooseSeatType === 0" style="color: red;">
您购买的车票不支持选座
<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div>
<div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div>
</div>
<div v-else style="text-align: center">
<a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" />
<div v-if="tickets.length > 1">
<a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" />
</div>
<div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div>
</div>
</div>
</a-modal>
</template>
<script>
import {defineComponent, ref, onMounted, watch, computed} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const passengerOptions = ref([]);
const passengerChecks = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
// {
// passengerId: 123,
// passengerType: "1",
// passengerName: "张三",
// passengerIdCard: "12323132132",
// seatTypeCode: "1"
// }
const tickets = ref([]);
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
const visible = ref(false);
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
watch(() => passengerChecks.value, (newVal, oldVal)=>{
console.log("勾选乘客发生变化", newVal, oldVal)
// 每次有变化时,把购票列表清空,重新构造列表
tickets.value = [];
passengerChecks.value.forEach((item) => tickets.value.push({
passengerId: item.id,
passengerType: item.type,
seatTypeCode: seatTypes[0].code,
passengerName: item.name,
passengerIdCard: item.idCard
}))
}, {immediate: true});
// 0:不支持选座;1:选一等座;2:选二等座
const chooseSeatType = ref(0);
// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
const SEAT_COL_ARRAY = computed(() => {
return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
});
// 选择的座位
// {
// A1: false, C1: true,D1: false, F1: false,
// A2: false, C2: false,D2: true, F2: false
// }
const chooseSeatObj = ref({});
watch(() => SEAT_COL_ARRAY.value, () => {
for (let i = 1; i <= 2; i++) {
SEAT_COL_ARRAY.value.forEach((item) => {
chooseSeatObj.value[item.code + i] = false;
})
}
console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
}, {immediate: true});
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
passengers.value.forEach((item) => passengerOptions.value.push({
label: item.name,
value: item
}))
} else {
notification.error({description: data.message});
}
});
};
const finishCheckPassenger = () => {
console.log("购票列表:", tickets.value);
if (tickets.value.length > 5) {
notification.error({description: '最多只能购买5张车票'});
return;
}
// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
// 前端校验不一定准,但前端校验可以减轻后端很多压力
// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
let seatTypesTemp = Tool.copy(seatTypes);
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
for (let j = 0; j < seatTypesTemp.length; j++) {
let seatType = seatTypesTemp[j];
// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
if (ticket.seatTypeCode === seatType.code) {
seatType.count--;
if (seatType.count < 0) {
notification.error({description: seatType.desc + '余票不足'});
return;
}
}
}
}
console.log("前端余票校验通过");
// 判断是否支持选座,只有纯一等座和纯二等座支持选座
// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
let ticketSeatTypeCodes = [];
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
ticketSeatTypeCodes.push(ticket.seatTypeCode);
}
// 为购票列表中的所有座位类型去重:[1, 2]
const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
console.log("选好的座位类型:", ticketSeatTypeCodesSet);
if (ticketSeatTypeCodesSet.length !== 1) {
console.log("选了多种座位,不支持选座");
chooseSeatType.value = 0;
} else {
// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
console.log("一等座选座");
chooseSeatType.value = SEAT_TYPE.YDZ.code;
} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
console.log("二等座选座");
chooseSeatType.value = SEAT_TYPE.EDZ.code;
} else {
console.log("不是一等座或二等座,不支持选座");
chooseSeatType.value = 0;
}
}
// 弹出确认界面
visible.value = true;
};
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes,
passengerOptions,
passengerChecks,
tickets,
PASSENGER_TYPE_ARRAY,
visible,
finishCheckPassenger,
chooseSeatType,
chooseSeatObj,
SEAT_COL_ARRAY,
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
.order-tickets {
margin: 10px 0;
}
.order-tickets .ant-col {
padding: 5px 10px;
}
.order-tickets .order-tickets-header {
background-color: cornflowerblue;
border: solid 1px cornflowerblue;
color: white;
font-size: 16px;
padding: 5px 0;
}
.order-tickets .order-tickets-row {
border: solid 1px cornflowerblue;
border-top: none;
vertical-align: middle;
line-height: 30px;
}
.order-tickets .choose-seat-item {
margin: 5px 5px;
}
</style>
效果
order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
<b>勾选要购票的乘客:</b>
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="2">乘客</a-col>
<a-col :span="6">身份证</a-col>
<a-col :span="4">票种</a-col>
<a-col :span="4">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="2">{{ticket.passengerName}}</a-col>
<a-col :span="6">{{ticket.passengerIdCard}}</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.passengerType" style="width: 100%">
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
<a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
</a-row>
</div>
<div v-if="tickets.length > 0">
<a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
</div>
<a-modal v-model:visible="visible" title="请核对以下信息"
style="top: 50px; width: 800px"
ok-text="确认" cancel-text="取消">
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="3">乘客</a-col>
<a-col :span="15">身份证</a-col>
<a-col :span="3">票种</a-col>
<a-col :span="3">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="3">{{ticket.passengerName}}</a-col>
<a-col :span="15">{{ticket.passengerIdCard}}</a-col>
<a-col :span="3">
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
<span v-if="item.code === ticket.passengerType">
{{item.desc}}
</span>
</span>
</a-col>
<a-col :span="3">
<span v-for="item in seatTypes" :key="item.code">
<span v-if="item.code === ticket.seatTypeCode">
{{item.desc}}
</span>
</span>
</a-col>
</a-row>
<br/>
<div v-if="chooseSeatType === 0" style="color: red;">
您购买的车票不支持选座
<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div>
<div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div>
</div>
<div v-else style="text-align: center">
<a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" />
<div v-if="tickets.length > 1">
<a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" />
</div>
<div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div>
</div>
</div>
</a-modal>
</template>
<script>
import {defineComponent, ref, onMounted, watch, computed} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const passengerOptions = ref([]);
const passengerChecks = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
// {
// passengerId: 123,
// passengerType: "1",
// passengerName: "张三",
// passengerIdCard: "12323132132",
// seatTypeCode: "1"
// }
const tickets = ref([]);
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
const visible = ref(false);
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
watch(() => passengerChecks.value, (newVal, oldVal)=>{
console.log("勾选乘客发生变化", newVal, oldVal)
// 每次有变化时,把购票列表清空,重新构造列表
tickets.value = [];
passengerChecks.value.forEach((item) => tickets.value.push({
passengerId: item.id,
passengerType: item.type,
seatTypeCode: seatTypes[0].code,
passengerName: item.name,
passengerIdCard: item.idCard
}))
}, {immediate: true});
// 0:不支持选座;1:选一等座;2:选二等座
const chooseSeatType = ref(0);
// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
const SEAT_COL_ARRAY = computed(() => {
return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
});
// 选择的座位
// {
// A1: false, C1: true,D1: false, F1: false,
// A2: false, C2: false,D2: true, F2: false
// }
const chooseSeatObj = ref({});
watch(() => SEAT_COL_ARRAY.value, () => {
for (let i = 1; i <= 2; i++) {
SEAT_COL_ARRAY.value.forEach((item) => {
chooseSeatObj.value[item.code + i] = false;
})
}
console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
}, {immediate: true});
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
passengers.value.forEach((item) => passengerOptions.value.push({
label: item.name,
value: item
}))
} else {
notification.error({description: data.message});
}
});
};
const finishCheckPassenger = () => {
console.log("购票列表:", tickets.value);
if (tickets.value.length > 5) {
notification.error({description: '最多只能购买5张车票'});
return;
}
// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
// 前端校验不一定准,但前端校验可以减轻后端很多压力
// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
let seatTypesTemp = Tool.copy(seatTypes);
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
for (let j = 0; j < seatTypesTemp.length; j++) {
let seatType = seatTypesTemp[j];
// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
if (ticket.seatTypeCode === seatType.code) {
seatType.count--;
if (seatType.count < 0) {
notification.error({description: seatType.desc + '余票不足'});
return;
}
}
}
}
console.log("前端余票校验通过");
// 判断是否支持选座,只有纯一等座和纯二等座支持选座
// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
let ticketSeatTypeCodes = [];
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
ticketSeatTypeCodes.push(ticket.seatTypeCode);
}
// 为购票列表中的所有座位类型去重:[1, 2]
const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
console.log("选好的座位类型:", ticketSeatTypeCodesSet);
if (ticketSeatTypeCodesSet.length !== 1) {
console.log("选了多种座位,不支持选座");
chooseSeatType.value = 0;
} else {
// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
console.log("一等座选座");
chooseSeatType.value = SEAT_TYPE.YDZ.code;
} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
console.log("二等座选座");
chooseSeatType.value = SEAT_TYPE.EDZ.code;
} else {
console.log("不是一等座或二等座,不支持选座");
chooseSeatType.value = 0;
}
// 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
if (chooseSeatType.value !== 0) {
for (let i = 0; i < seatTypes.length; i++) {
let seatType = seatTypes[i];
// 找到同类型座位
if (ticketSeatTypeCodesSet[0] === seatType.code) {
// 判断余票,小于20张就不支持选座
if (seatType.count < 20) {
console.log("余票小于20张就不支持选座")
chooseSeatType.value = 0;
break;
}
}
}
}
}
// 弹出确认界面
visible.value = true;
};
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes,
passengerOptions,
passengerChecks,
tickets,
PASSENGER_TYPE_ARRAY,
visible,
finishCheckPassenger,
chooseSeatType,
chooseSeatObj,
SEAT_COL_ARRAY,
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
.order-tickets {
margin: 10px 0;
}
.order-tickets .ant-col {
padding: 5px 10px;
}
.order-tickets .order-tickets-header {
background-color: cornflowerblue;
border: solid 1px cornflowerblue;
color: white;
font-size: 16px;
padding: 5px 0;
}
.order-tickets .order-tickets-row {
border: solid 1px cornflowerblue;
border-top: none;
vertical-align: middle;
line-height: 30px;
}
.order-tickets .choose-seat-item {
margin: 5px 5px;
}
</style>
order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
<b>勾选要购票的乘客:</b>
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="2">乘客</a-col>
<a-col :span="6">身份证</a-col>
<a-col :span="4">票种</a-col>
<a-col :span="4">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="2">{{ticket.passengerName}}</a-col>
<a-col :span="6">{{ticket.passengerIdCard}}</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.passengerType" style="width: 100%">
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
<a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
</a-row>
</div>
<div v-if="tickets.length > 0">
<a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
</div>
<a-modal v-model:visible="visible" title="请核对以下信息"
style="top: 50px; width: 800px"
ok-text="确认" cancel-text="取消"
@ok="handleOk">
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="3">乘客</a-col>
<a-col :span="15">身份证</a-col>
<a-col :span="3">票种</a-col>
<a-col :span="3">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="3">{{ticket.passengerName}}</a-col>
<a-col :span="15">{{ticket.passengerIdCard}}</a-col>
<a-col :span="3">
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
<span v-if="item.code === ticket.passengerType">
{{item.desc}}
</span>
</span>
</a-col>
<a-col :span="3">
<span v-for="item in seatTypes" :key="item.code">
<span v-if="item.code === ticket.seatTypeCode">
{{item.desc}}
</span>
</span>
</a-col>
</a-row>
<br/>
<div v-if="chooseSeatType === 0" style="color: red;">
您购买的车票不支持选座
<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div>
<div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div>
</div>
<div v-else style="text-align: center">
<a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" />
<div v-if="tickets.length > 1">
<a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" />
</div>
<div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div>
</div>
<br/>
最终购票:{{tickets}}
</div>
</a-modal>
</template>
<script>
import {defineComponent, ref, onMounted, watch, computed} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const passengerOptions = ref([]);
const passengerChecks = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
// {
// passengerId: 123,
// passengerType: "1",
// passengerName: "张三",
// passengerIdCard: "12323132132",
// seatTypeCode: "1"
// }
const tickets = ref([]);
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
const visible = ref(false);
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
watch(() => passengerChecks.value, (newVal, oldVal)=>{
console.log("勾选乘客发生变化", newVal, oldVal)
// 每次有变化时,把购票列表清空,重新构造列表
tickets.value = [];
passengerChecks.value.forEach((item) => tickets.value.push({
passengerId: item.id,
passengerType: item.type,
seatTypeCode: seatTypes[0].code,
passengerName: item.name,
passengerIdCard: item.idCard
}))
}, {immediate: true});
// 0:不支持选座;1:选一等座;2:选二等座
const chooseSeatType = ref(0);
// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
const SEAT_COL_ARRAY = computed(() => {
return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
});
// 选择的座位
// {
// A1: false, C1: true,D1: false, F1: false,
// A2: false, C2: false,D2: true, F2: false
// }
const chooseSeatObj = ref({});
watch(() => SEAT_COL_ARRAY.value, () => {
for (let i = 1; i <= 2; i++) {
SEAT_COL_ARRAY.value.forEach((item) => {
chooseSeatObj.value[item.code + i] = false;
})
}
console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
}, {immediate: true});
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
passengers.value.forEach((item) => passengerOptions.value.push({
label: item.name,
value: item
}))
} else {
notification.error({description: data.message});
}
});
};
const finishCheckPassenger = () => {
console.log("购票列表:", tickets.value);
if (tickets.value.length > 5) {
notification.error({description: '最多只能购买5张车票'});
return;
}
// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
// 前端校验不一定准,但前端校验可以减轻后端很多压力
// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
let seatTypesTemp = Tool.copy(seatTypes);
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
for (let j = 0; j < seatTypesTemp.length; j++) {
let seatType = seatTypesTemp[j];
// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
if (ticket.seatTypeCode === seatType.code) {
seatType.count--;
if (seatType.count < 0) {
notification.error({description: seatType.desc + '余票不足'});
return;
}
}
}
}
console.log("前端余票校验通过");
// 判断是否支持选座,只有纯一等座和纯二等座支持选座
// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
let ticketSeatTypeCodes = [];
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
ticketSeatTypeCodes.push(ticket.seatTypeCode);
}
// 为购票列表中的所有座位类型去重:[1, 2]
const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
console.log("选好的座位类型:", ticketSeatTypeCodesSet);
if (ticketSeatTypeCodesSet.length !== 1) {
console.log("选了多种座位,不支持选座");
chooseSeatType.value = 0;
} else {
// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
console.log("一等座选座");
chooseSeatType.value = SEAT_TYPE.YDZ.code;
} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
console.log("二等座选座");
chooseSeatType.value = SEAT_TYPE.EDZ.code;
} else {
console.log("不是一等座或二等座,不支持选座");
chooseSeatType.value = 0;
}
// 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
if (chooseSeatType.value !== 0) {
for (let i = 0; i < seatTypes.length; i++) {
let seatType = seatTypes[i];
// 找到同类型座位
if (ticketSeatTypeCodesSet[0] === seatType.code) {
// 判断余票,小于20张就不支持选座
if (seatType.count < 20) {
console.log("余票小于20张就不支持选座")
chooseSeatType.value = 0;
break;
}
}
}
}
}
// 弹出确认界面
visible.value = true;
};
const handleOk = () => {
console.log("选好的座位:", chooseSeatObj.value);
// 设置每张票的座位
// 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍
for (let i = 0; i < tickets.value.length; i++) {
tickets.value[i].seat = null;
}
let i = -1;
// 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)
for (let key in chooseSeatObj.value) {
if (chooseSeatObj.value[key]) {
i++;
if (i > tickets.value.length - 1) {
notification.error({description: '所选座位数大于购票数'});
return;
}
tickets.value[i].seat = key;
}
}
if (i > -1 && i < (tickets.value.length - 1)) {
notification.error({description: '所选座位数小于购票数'});
return;
}
console.log("最终购票:", tickets.value);
}
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes,
passengerOptions,
passengerChecks,
tickets,
PASSENGER_TYPE_ARRAY,
visible,
finishCheckPassenger,
chooseSeatType,
chooseSeatObj,
SEAT_COL_ARRAY,
handleOk,
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
.order-tickets {
margin: 10px 0;
}
.order-tickets .ant-col {
padding: 5px 10px;
}
.order-tickets .order-tickets-header {
background-color: cornflowerblue;
border: solid 1px cornflowerblue;
color: white;
font-size: 16px;
padding: 5px 0;
}
.order-tickets .order-tickets-row {
border: solid 1px cornflowerblue;
border-top: none;
vertical-align: middle;
line-height: 30px;
}
.order-tickets .choose-seat-item {
margin: 5px 5px;
}
</style>
效果
order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
<b>勾选要购票的乘客:</b>
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="2">乘客</a-col>
<a-col :span="6">身份证</a-col>
<a-col :span="4">票种</a-col>
<a-col :span="4">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="2">{{ticket.passengerName}}</a-col>
<a-col :span="6">{{ticket.passengerIdCard}}</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.passengerType" style="width: 100%">
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
<a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
</a-row>
</div>
<div v-if="tickets.length > 0">
<a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
</div>
<a-modal v-model:visible="visible" title="请核对以下信息"
style="top: 50px; width: 800px"
ok-text="确认" cancel-text="取消"
@ok="handleOk">
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="3">乘客</a-col>
<a-col :span="15">身份证</a-col>
<a-col :span="3">票种</a-col>
<a-col :span="3">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="3">{{ticket.passengerName}}</a-col>
<a-col :span="15">{{ticket.passengerIdCard}}</a-col>
<a-col :span="3">
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
<span v-if="item.code === ticket.passengerType">
{{item.desc}}
</span>
</span>
</a-col>
<a-col :span="3">
<span v-for="item in seatTypes" :key="item.code">
<span v-if="item.code === ticket.seatTypeCode">
{{item.desc}}
</span>
</span>
</a-col>
</a-row>
<br/>
<div v-if="chooseSeatType === 0" style="color: red;">
您购买的车票不支持选座
<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div>
<div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div>
</div>
<div v-else style="text-align: center">
<a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" />
<div v-if="tickets.length > 1">
<a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" />
</div>
<div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div>
</div>
<br/>
最终购票:{{tickets}}
最终选座:{{chooseSeatObj}}
</div>
</a-modal>
</template>
<script>
import {defineComponent, ref, onMounted, watch, computed} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const passengerOptions = ref([]);
const passengerChecks = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
// {
// passengerId: 123,
// passengerType: "1",
// passengerName: "张三",
// passengerIdCard: "12323132132",
// seatTypeCode: "1",
// seat: "C1"
// }
const tickets = ref([]);
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
const visible = ref(false);
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
watch(() => passengerChecks.value, (newVal, oldVal)=>{
console.log("勾选乘客发生变化", newVal, oldVal)
// 每次有变化时,把购票列表清空,重新构造列表
tickets.value = [];
passengerChecks.value.forEach((item) => tickets.value.push({
passengerId: item.id,
passengerType: item.type,
seatTypeCode: seatTypes[0].code,
passengerName: item.name,
passengerIdCard: item.idCard
}))
}, {immediate: true});
// 0:不支持选座;1:选一等座;2:选二等座
const chooseSeatType = ref(0);
// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
const SEAT_COL_ARRAY = computed(() => {
return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
});
// 选择的座位
// {
// A1: false, C1: true,D1: false, F1: false,
// A2: false, C2: false,D2: true, F2: false
// }
const chooseSeatObj = ref({});
watch(() => SEAT_COL_ARRAY.value, () => {
chooseSeatObj.value = {};
for (let i = 1; i <= 2; i++) {
SEAT_COL_ARRAY.value.forEach((item) => {
chooseSeatObj.value[item.code + i] = false;
})
}
console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
}, {immediate: true});
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
passengers.value.forEach((item) => passengerOptions.value.push({
label: item.name,
value: item
}))
} else {
notification.error({description: data.message});
}
});
};
const finishCheckPassenger = () => {
console.log("购票列表:", tickets.value);
if (tickets.value.length > 5) {
notification.error({description: '最多只能购买5张车票'});
return;
}
// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
// 前端校验不一定准,但前端校验可以减轻后端很多压力
// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
let seatTypesTemp = Tool.copy(seatTypes);
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
for (let j = 0; j < seatTypesTemp.length; j++) {
let seatType = seatTypesTemp[j];
// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
if (ticket.seatTypeCode === seatType.code) {
seatType.count--;
if (seatType.count < 0) {
notification.error({description: seatType.desc + '余票不足'});
return;
}
}
}
}
console.log("前端余票校验通过");
// 判断是否支持选座,只有纯一等座和纯二等座支持选座
// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
let ticketSeatTypeCodes = [];
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
ticketSeatTypeCodes.push(ticket.seatTypeCode);
}
// 为购票列表中的所有座位类型去重:[1, 2]
const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
console.log("选好的座位类型:", ticketSeatTypeCodesSet);
if (ticketSeatTypeCodesSet.length !== 1) {
console.log("选了多种座位,不支持选座");
chooseSeatType.value = 0;
} else {
// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
console.log("一等座选座");
chooseSeatType.value = SEAT_TYPE.YDZ.code;
} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
console.log("二等座选座");
chooseSeatType.value = SEAT_TYPE.EDZ.code;
} else {
console.log("不是一等座或二等座,不支持选座");
chooseSeatType.value = 0;
}
// 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
if (chooseSeatType.value !== 0) {
for (let i = 0; i < seatTypes.length; i++) {
let seatType = seatTypes[i];
// 找到同类型座位
if (ticketSeatTypeCodesSet[0] === seatType.code) {
// 判断余票,小于20张就不支持选座
if (seatType.count < 20) {
console.log("余票小于20张就不支持选座")
chooseSeatType.value = 0;
break;
}
}
}
}
}
// 弹出确认界面
visible.value = true;
};
const handleOk = () => {
console.log("选好的座位:", chooseSeatObj.value);
// 设置每张票的座位
// 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍
for (let i = 0; i < tickets.value.length; i++) {
tickets.value[i].seat = null;
}
let i = -1;
// 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)
for (let key in chooseSeatObj.value) {
if (chooseSeatObj.value[key]) {
i++;
if (i > tickets.value.length - 1) {
notification.error({description: '所选座位数大于购票数'});
return;
}
tickets.value[i].seat = key;
}
}
if (i > -1 && i < (tickets.value.length - 1)) {
notification.error({description: '所选座位数小于购票数'});
return;
}
console.log("最终购票:", tickets.value);
}
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes,
passengerOptions,
passengerChecks,
tickets,
PASSENGER_TYPE_ARRAY,
visible,
finishCheckPassenger,
chooseSeatType,
chooseSeatObj,
SEAT_COL_ARRAY,
handleOk,
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
.order-tickets {
margin: 10px 0;
}
.order-tickets .ant-col {
padding: 5px 10px;
}
.order-tickets .order-tickets-header {
background-color: cornflowerblue;
border: solid 1px cornflowerblue;
color: white;
font-size: 16px;
padding: 5px 0;
}
.order-tickets .order-tickets-row {
border: solid 1px cornflowerblue;
border-top: none;
vertical-align: middle;
line-height: 30px;
}
.order-tickets .choose-seat-item {
margin: 5px 5px;
}
</style>
效果
新增表
sql/business.sql
这里的车票使用json类型,实际上也可以使用子表来做
drop table if exists `confirm_order`;
create table `confirm_order` (
`id` bigint not null comment 'id',
`member_id` bigint not null comment '会员id',
`date` date not null comment '日期',
`train_code` varchar(20) not null comment '车次编号',
`start` varchar(20) not null comment '出发站',
`end` varchar(20) not null comment '到达站',
`daily_train_ticket_id` bigint not null comment '余票ID',
`tickets` json not null comment '车票',
`status` char(1) not null comment '订单状态|枚举[ConfirmOrderStatusEnum]',
`create_time` datetime(3) comment '新增时间',
`update_time` datetime(3) comment '修改时间',
primary key (`id`),
index `date_train_code_index` (`date`, `train_code`)
) engine=innodb default charset=utf8mb4 comment='确认订单';
ConfirmOrderStatusEnum
enum也是可以用lombok注解的
package com.neilxu.train.business.enums;
public enum ConfirmOrderStatusEnum {
INIT("I", "初始"),
PENDING("P", "处理中"),
SUCCESS("S", "成功"),
FAILURE("F", "失败"),
EMPTY("E", "无票"),
CANCEL("C", "取消");
private String code;
private String desc;
ConfirmOrderStatusEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
@Override public String toString() {
return "ConfirmOrderStatusEnum{" +
"code='" + code + '\'' +
", desc='" + desc + '\'' +
"} " + super.toString();
}
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;
}
}
修改generator-config-business.xml、ServerGenerator.java、EnumGenerator.java生成代码
操作同之前,注意是admin
修改路由、侧边栏
操作同之前,注意是admin
效果
这节整理下后端确认下单购票接口的逻辑
com.neilxu.train.business.req.ConfirmOrderTicketReq
package com.neilxu.train.business.req;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class ConfirmOrderTicketReq {
/**
* 乘客ID
*/
@NotNull(message = "【乘客ID】不能为空")
private Long passengerId;
/**
* 乘客票种
*/
@NotBlank(message = "【乘客票种】不能为空")
private String passengerType;
/**
* 乘客名称
*/
@NotBlank(message = "【乘客名称】不能为空")
private String passengerName;
/**
* 乘客身份证
*/
@NotBlank(message = "【乘客身份证】不能为空")
private String passengerIdCard;
/**
* 座位类型code
*/
@NotBlank(message = "【座位类型code】不能为空")
private String seatTypeCode;
/**
* 选座,可空,值示例:A1
*/
private String seat;
}
ConfirmOrderSaveReq.java ——> ConfirmOrderDoReq.java
package com.neilxu.train.business.req;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
public class ConfirmOrderDoReq {
/**
* 会员id
*/
@NotNull(message = "【会员id】不能为空")
private Long memberId;
/**
* 日期
*/
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
@NotNull(message = "【日期】不能为空")
private Date date;
/**
* 车次编号
*/
@NotBlank(message = "【车次编号】不能为空")
private String trainCode;
/**
* 出发站
*/
@NotBlank(message = "【出发站】不能为空")
private String start;
/**
* 到达站
*/
@NotBlank(message = "【到达站】不能为空")
private String end;
/**
* 余票ID
*/
@NotNull(message = "【余票ID】不能为空")
private Long dailyTrainTicketId;
/**
* 车票
*/
@NotBlank(message = "【车票】不能为空")
private List<ConfirmOrderTicketReq> tickets;
@Override
public String toString() {
return "ConfirmOrderDoReq{" +
"memberId=" + memberId +
", date=" + date +
", trainCode='" + trainCode + '\'' +
", start='" + start + '\'' +
", end='" + end + '\'' +
", dailyTrainTicketId=" + dailyTrainTicketId +
", tickets=" + tickets +
'}';
}
}
ConfirmOrderAdminController.java
package com.neilxu.train.business.controller.admin;
import com.neilxu.train.business.req.ConfirmOrderDoReq;
import com.neilxu.train.business.service.ConfirmOrderService;
import com.neilxu.train.common.resp.CommonResp;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
;
@RestController
@RequestMapping("/admin/confirm-order")
public class ConfirmOrderAdminController {
@Resource
private ConfirmOrderService confirmOrderService;
@PostMapping("/save")
public CommonResp<Object> save(@Valid @RequestBody ConfirmOrderDoReq req) {
confirmOrderService.save(req);
return new CommonResp<>();
}
}
ConfirmOrderService.java
package com.neilxu.train.business.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.ObjectUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.neilxu.train.common.resp.PageResp;
import com.neilxu.train.common.util.SnowUtil;
import com.neilxu.train.business.domain.ConfirmOrder;
import com.neilxu.train.business.domain.ConfirmOrderExample;
import com.neilxu.train.business.mapper.ConfirmOrderMapper;
import com.neilxu.train.business.req.ConfirmOrderQueryReq;
import com.neilxu.train.business.req.ConfirmOrderDoReq;
import com.neilxu.train.business.resp.ConfirmOrderQueryResp;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ConfirmOrderService {
private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);
@Resource
private ConfirmOrderMapper confirmOrderMapper;
public void save(ConfirmOrderDoReq req) {
DateTime now = DateTime.now();
ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);
if (ObjectUtil.isNull(confirmOrder.getId())) {
confirmOrder.setId(SnowUtil.getSnowflakeNextId());
confirmOrder.setCreateTime(now);
confirmOrder.setUpdateTime(now);
confirmOrderMapper.insert(confirmOrder);
} else {
confirmOrder.setUpdateTime(now);
confirmOrderMapper.updateByPrimaryKey(confirmOrder);
}
}
public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {
ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
confirmOrderExample.setOrderByClause("id desc");
ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
LOG.info("查询页码:{}", req.getPage());
LOG.info("每页条数:{}", req.getSize());
PageHelper.startPage(req.getPage(), req.getSize());
List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);
PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);
PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
public void delete(Long id) {
confirmOrderMapper.deleteByPrimaryKey(id);
}
public void doConfirm(ConfirmOrderDoReq req) {
//省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,ticket条数>0,同乘客同车次是否已经买过
//保存确认订单表,状态初始
//查出余票记录,需要得到真实的库存
//扣减余票数量,并判断余票是否足够
//选座
//一个车厢一个车厢的获取座位数据
//挑选符合条件的座位,如果这个车厢不满足,则进入下一个车厢(多个选座应该在同一车厢)
//选中座位后事务处理
//座位表修改售卖情况sell
//余票详情表修改余票
//为会员增加购票记录
//更新确认订单为成功
}
}
ConfirmOrderController.java
package com.neilxu.train.business.controller;
import com.neilxu.train.business.req.ConfirmOrderDoReq;
import com.neilxu.train.business.service.ConfirmOrderService;
import com.neilxu.train.common.resp.CommonResp;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/confirm-order")
public class ConfirmOrderController {
@Resource
private ConfirmOrderService confirmOrderService;
@PostMapping("/do")
public CommonResp<Object> doConfirm(@Valid @RequestBody ConfirmOrderDoReq req) {
confirmOrderService.doConfirm(req);
return new CommonResp<>();
}
}
order.vue
<template>
<div class="order-train">
<span class="order-train-main">{{dailyTrainTicket.date}}</span>
<span class="order-train-main">{{dailyTrainTicket.trainCode}}</span>次
<span class="order-train-main">{{dailyTrainTicket.start}}</span>站
<span class="order-train-main">({{dailyTrainTicket.startTime}})</span>
<span class="order-train-main">——</span>
<span class="order-train-main">{{dailyTrainTicket.end}}</span>站
<span class="order-train-main">({{dailyTrainTicket.endTime}})</span>
<div class="order-train-ticket">
<span v-for="item in seatTypes" :key="item.type">
<span>{{item.desc}}</span>:
<span class="order-train-ticket-main">{{item.price}}¥</span>
<span class="order-train-ticket-main">{{item.count}}</span> 张票
</span>
</div>
</div>
<a-divider></a-divider>
<b>勾选要购票的乘客:</b>
<a-checkbox-group v-model:value="passengerChecks" :options="passengerOptions" />
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="2">乘客</a-col>
<a-col :span="6">身份证</a-col>
<a-col :span="4">票种</a-col>
<a-col :span="4">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="2">{{ticket.passengerName}}</a-col>
<a-col :span="6">{{ticket.passengerIdCard}}</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.passengerType" style="width: 100%">
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select v-model:value="ticket.seatTypeCode" style="width: 100%">
<a-select-option v-for="item in seatTypes" :key="item.code" :value="item.code">
{{item.desc}}
</a-select-option>
</a-select>
</a-col>
</a-row>
</div>
<div v-if="tickets.length > 0">
<a-button type="primary" size="large" @click="finishCheckPassenger">提交订单</a-button>
</div>
<a-modal v-model:visible="visible" title="请核对以下信息"
style="top: 50px; width: 800px"
ok-text="确认" cancel-text="取消"
@ok="handleOk">
<div class="order-tickets">
<a-row class="order-tickets-header" v-if="tickets.length > 0">
<a-col :span="3">乘客</a-col>
<a-col :span="15">身份证</a-col>
<a-col :span="3">票种</a-col>
<a-col :span="3">座位类型</a-col>
</a-row>
<a-row class="order-tickets-row" v-for="ticket in tickets" :key="ticket.passengerId">
<a-col :span="3">{{ticket.passengerName}}</a-col>
<a-col :span="15">{{ticket.passengerIdCard}}</a-col>
<a-col :span="3">
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
<span v-if="item.code === ticket.passengerType">
{{item.desc}}
</span>
</span>
</a-col>
<a-col :span="3">
<span v-for="item in seatTypes" :key="item.code">
<span v-if="item.code === ticket.seatTypeCode">
{{item.desc}}
</span>
</span>
</a-col>
</a-row>
<br/>
<div v-if="chooseSeatType === 0" style="color: red;">
您购买的车票不支持选座
<div>12306规则:只有全部是一等座或全部是二等座才支持选座</div>
<div>12306规则:余票小于一定数量时,不允许选座(本项目以20为例)</div>
</div>
<div v-else style="text-align: center">
<a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
v-model:checked="chooseSeatObj[item.code + '1']" :checked-children="item.desc" :un-checked-children="item.desc" />
<div v-if="tickets.length > 1">
<a-switch class="choose-seat-item" v-for="item in SEAT_COL_ARRAY" :key="item.code"
v-model:checked="chooseSeatObj[item.code + '2']" :checked-children="item.desc" :un-checked-children="item.desc" />
</div>
<div style="color: #999999">提示:您可以选择{{tickets.length}}个座位</div>
</div>
<br/>
最终购票:{{tickets}}
最终选座:{{chooseSeatObj}}
</div>
</a-modal>
</template>
<script>
import {defineComponent, ref, onMounted, watch, computed} from 'vue';
import axios from "axios";
import {notification} from "ant-design-vue";
export default defineComponent({
name: "order-view",
setup() {
const passengers = ref([]);
const passengerOptions = ref([]);
const passengerChecks = ref([]);
const dailyTrainTicket = SessionStorage.get(SESSION_ORDER) || {};
console.log("下单的车次信息", dailyTrainTicket);
const SEAT_TYPE = window.SEAT_TYPE;
console.log(SEAT_TYPE)
// 本车次提供的座位类型seatTypes,含票价,余票等信息,例:
// {
// type: "YDZ",
// code: "1",
// desc: "一等座",
// count: "100",
// price: "50",
// }
// 关于SEAT_TYPE[KEY]:当知道某个具体的属性xxx时,可以用obj.xxx,当属性名是个变量时,可以使用obj[xxx]
const seatTypes = [];
for (let KEY in SEAT_TYPE) {
let key = KEY.toLowerCase();
if (dailyTrainTicket[key] >= 0) {
seatTypes.push({
type: KEY,
code: SEAT_TYPE[KEY]["code"],
desc: SEAT_TYPE[KEY]["desc"],
count: dailyTrainTicket[key],
price: dailyTrainTicket[key + 'Price'],
})
}
}
console.log("本车次提供的座位:", seatTypes)
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
// {
// passengerId: 123,
// passengerType: "1",
// passengerName: "张三",
// passengerIdCard: "12323132132",
// seatTypeCode: "1",
// seat: "C1"
// }
const tickets = ref([]);
const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
const visible = ref(false);
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
watch(() => passengerChecks.value, (newVal, oldVal)=>{
console.log("勾选乘客发生变化", newVal, oldVal)
// 每次有变化时,把购票列表清空,重新构造列表
tickets.value = [];
passengerChecks.value.forEach((item) => tickets.value.push({
passengerId: item.id,
passengerType: item.type,
seatTypeCode: seatTypes[0].code,
passengerName: item.name,
passengerIdCard: item.idCard
}))
}, {immediate: true});
// 0:不支持选座;1:选一等座;2:选二等座
const chooseSeatType = ref(0);
// 根据选择的座位类型,计算出对应的列,比如要选的是一等座,就筛选出ACDF,要选的是二等座,就筛选出ABCDF
const SEAT_COL_ARRAY = computed(() => {
return window.SEAT_COL_ARRAY.filter(item => item.type === chooseSeatType.value);
});
// 选择的座位
// {
// A1: false, C1: true,D1: false, F1: false,
// A2: false, C2: false,D2: true, F2: false
// }
const chooseSeatObj = ref({});
watch(() => SEAT_COL_ARRAY.value, () => {
chooseSeatObj.value = {};
for (let i = 1; i <= 2; i++) {
SEAT_COL_ARRAY.value.forEach((item) => {
chooseSeatObj.value[item.code + i] = false;
})
}
console.log("初始化两排座位,都是未选中:", chooseSeatObj.value);
}, {immediate: true});
const handleQueryPassenger = () => {
axios.get("/member/passenger/query-mine").then((response) => {
let data = response.data;
if (data.success) {
passengers.value = data.content;
passengers.value.forEach((item) => passengerOptions.value.push({
label: item.name,
value: item
}))
} else {
notification.error({description: data.message});
}
});
};
const finishCheckPassenger = () => {
console.log("购票列表:", tickets.value);
if (tickets.value.length > 5) {
notification.error({description: '最多只能购买5张车票'});
return;
}
// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
// 前端校验不一定准,但前端校验可以减轻后端很多压力
// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
let seatTypesTemp = Tool.copy(seatTypes);
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
for (let j = 0; j < seatTypesTemp.length; j++) {
let seatType = seatTypesTemp[j];
// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
if (ticket.seatTypeCode === seatType.code) {
seatType.count--;
if (seatType.count < 0) {
notification.error({description: seatType.desc + '余票不足'});
return;
}
}
}
}
console.log("前端余票校验通过");
// 判断是否支持选座,只有纯一等座和纯二等座支持选座
// 先筛选出购票列表中的所有座位类型,比如四张表:[1, 1, 2, 2]
let ticketSeatTypeCodes = [];
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
ticketSeatTypeCodes.push(ticket.seatTypeCode);
}
// 为购票列表中的所有座位类型去重:[1, 2]
const ticketSeatTypeCodesSet = Array.from(new Set(ticketSeatTypeCodes));
console.log("选好的座位类型:", ticketSeatTypeCodesSet);
if (ticketSeatTypeCodesSet.length !== 1) {
console.log("选了多种座位,不支持选座");
chooseSeatType.value = 0;
} else {
// ticketSeatTypeCodesSet.length === 1,即只选择了一种座位(不是一个座位,是一种座位)
if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.YDZ.code) {
console.log("一等座选座");
chooseSeatType.value = SEAT_TYPE.YDZ.code;
} else if (ticketSeatTypeCodesSet[0] === SEAT_TYPE.EDZ.code) {
console.log("二等座选座");
chooseSeatType.value = SEAT_TYPE.EDZ.code;
} else {
console.log("不是一等座或二等座,不支持选座");
chooseSeatType.value = 0;
}
// 余票小于20张时,不允许选座,否则选座成功率不高,影响出票
if (chooseSeatType.value !== 0) {
for (let i = 0; i < seatTypes.length; i++) {
let seatType = seatTypes[i];
// 找到同类型座位
if (ticketSeatTypeCodesSet[0] === seatType.code) {
// 判断余票,小于20张就不支持选座
if (seatType.count < 20) {
console.log("余票小于20张就不支持选座")
chooseSeatType.value = 0;
break;
}
}
}
}
}
// 弹出确认界面
visible.value = true;
};
const handleOk = () => {
console.log("选好的座位:", chooseSeatObj.value);
// 设置每张票的座位
// 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍
for (let i = 0; i < tickets.value.length; i++) {
tickets.value[i].seat = null;
}
let i = -1;
// 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)
for (let key in chooseSeatObj.value) {
if (chooseSeatObj.value[key]) {
i++;
if (i > tickets.value.length - 1) {
notification.error({description: '所选座位数大于购票数'});
return;
}
tickets.value[i].seat = key;
}
}
if (i > -1 && i < (tickets.value.length - 1)) {
notification.error({description: '所选座位数小于购票数'});
return;
}
console.log("最终购票:", tickets.value);
axios.post("/business/confirm-order/do", {
dailyTrainTicketId: dailyTrainTicket.id,
date: dailyTrainTicket.date,
trainCode: dailyTrainTicket.trainCode,
start: dailyTrainTicket.start,
end: dailyTrainTicket.end,
tickets: tickets.value
}).then((response) => {
let data = response.data;
if (data.success) {
notification.success({description: "下单成功!"});
} else {
notification.error({description: data.message});
}
});
}
onMounted(() => {
handleQueryPassenger();
});
return {
passengers,
dailyTrainTicket,
seatTypes,
passengerOptions,
passengerChecks,
tickets,
PASSENGER_TYPE_ARRAY,
visible,
finishCheckPassenger,
chooseSeatType,
chooseSeatObj,
SEAT_COL_ARRAY,
handleOk,
};
},
});
</script>
<style>
.order-train .order-train-main {
font-size: 18px;
font-weight: bold;
}
.order-train .order-train-ticket {
margin-top: 15px;
}
.order-train .order-train-ticket .order-train-ticket-main {
color: red;
font-size: 18px;
}
.order-tickets {
margin: 10px 0;
}
.order-tickets .ant-col {
padding: 5px 10px;
}
.order-tickets .order-tickets-header {
background-color: cornflowerblue;
border: solid 1px cornflowerblue;
color: white;
font-size: 16px;
padding: 5px 0;
}
.order-tickets .order-tickets-row {
border: solid 1px cornflowerblue;
border-top: none;
vertical-align: middle;
line-height: 30px;
}
.order-tickets .choose-seat-item {
margin: 5px 5px;
}
</style>
小tips:
ctrl + alt + v 可以快速提取变量
DailyTrainTicketService.java
public DailyTrainTicket selectByUnique(Date date, String trainCode, String start, String end) {
DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();
dailyTrainTicketExample.createCriteria()
.andDateEqualTo(date)
.andTrainCodeEqualTo(trainCode)
.andStartEqualTo(start)
.andEndEqualTo(end);
List<DailyTrainTicket> list = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample);
if (CollUtil.isNotEmpty(list)) {
return list.get(0);
} else {
return null;
}
}
ConfirmOrderService.java
package com.neilxu.train.business.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.neilxu.train.business.domain.DailyTrainTicket;
import com.neilxu.train.business.enums.ConfirmOrderStatusEnum;
import com.neilxu.train.common.context.LoginMemberContext;
import com.neilxu.train.common.resp.PageResp;
import com.neilxu.train.common.util.SnowUtil;
import com.neilxu.train.business.domain.ConfirmOrder;
import com.neilxu.train.business.domain.ConfirmOrderExample;
import com.neilxu.train.business.mapper.ConfirmOrderMapper;
import com.neilxu.train.business.req.ConfirmOrderQueryReq;
import com.neilxu.train.business.req.ConfirmOrderDoReq;
import com.neilxu.train.business.resp.ConfirmOrderQueryResp;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service
public class ConfirmOrderService {
private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);
@Resource
private ConfirmOrderMapper confirmOrderMapper;
@Resource
private DailyTrainTicketService dailyTrainTicketService;
public void save(ConfirmOrderDoReq req) {
DateTime now = DateTime.now();
ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);
if (ObjectUtil.isNull(confirmOrder.getId())) {
confirmOrder.setId(SnowUtil.getSnowflakeNextId());
confirmOrder.setCreateTime(now);
confirmOrder.setUpdateTime(now);
confirmOrderMapper.insert(confirmOrder);
} else {
confirmOrder.setUpdateTime(now);
confirmOrderMapper.updateByPrimaryKey(confirmOrder);
}
}
public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {
ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
confirmOrderExample.setOrderByClause("id desc");
ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
LOG.info("查询页码:{}", req.getPage());
LOG.info("每页条数:{}", req.getSize());
PageHelper.startPage(req.getPage(), req.getSize());
List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);
PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);
PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
public void delete(Long id) {
confirmOrderMapper.deleteByPrimaryKey(id);
}
public void doConfirm(ConfirmOrderDoReq req) {
// 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过
Date date = req.getDate();
String trainCode = req.getTrainCode();
String start = req.getStart();
String end = req.getEnd();
// 保存确认订单表,状态初始
DateTime now = DateTime.now();
ConfirmOrder confirmOrder = new ConfirmOrder();
confirmOrder.setId(SnowUtil.getSnowflakeNextId());
confirmOrder.setCreateTime(now);
confirmOrder.setUpdateTime(now);
confirmOrder.setMemberId(LoginMemberContext.getId());
confirmOrder.setDate(date);
confirmOrder.setTrainCode(trainCode);
confirmOrder.setStart(start);
confirmOrder.setEnd(end);
confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
confirmOrder.setTickets(JSON.toJSONString(req.getTickets()));
confirmOrderMapper.insert(confirmOrder);
// 查出余票记录,需要得到真实的库存
DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);
LOG.info("查出余票记录:{}", dailyTrainTicket);
// 扣减余票数量,并判断余票是否足够
// 选座
// 一个车厢一个车厢的获取座位数据
// 挑选符合条件的座位,如果这个车厢不满足,则进入下个车厢(多个选座应该在同一个车厢)
// 选中座位后事务处理:
// 座位表修改售卖情况sell;
// 余票详情表修改余票;
// 为会员增加购票记录
// 更新确认订单为成功
}
}
BusinessExceptionEnum.java
CONFIRM_ORDER_TICKET_COUNT_ERROR("余票不足"),
ConfirmOrderService.java
package com.neilxu.train.business.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.neilxu.train.business.domain.ConfirmOrder;
import com.neilxu.train.business.domain.ConfirmOrderExample;
import com.neilxu.train.business.domain.DailyTrainTicket;
import com.neilxu.train.business.enums.ConfirmOrderStatusEnum;
import com.neilxu.train.business.enums.SeatTypeEnum;
import com.neilxu.train.business.mapper.ConfirmOrderMapper;
import com.neilxu.train.business.req.ConfirmOrderDoReq;
import com.neilxu.train.business.req.ConfirmOrderQueryReq;
import com.neilxu.train.business.req.ConfirmOrderTicketReq;
import com.neilxu.train.business.resp.ConfirmOrderQueryResp;
import com.neilxu.train.common.context.LoginMemberContext;
import com.neilxu.train.common.exception.BusinessException;
import com.neilxu.train.common.exception.BusinessExceptionEnum;
import com.neilxu.train.common.resp.PageResp;
import com.neilxu.train.common.util.SnowUtil;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service
public class ConfirmOrderService {
private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);
@Resource
private ConfirmOrderMapper confirmOrderMapper;
@Resource
private DailyTrainTicketService dailyTrainTicketService;
public void save(ConfirmOrderDoReq req) {
DateTime now = DateTime.now();
ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);
if (ObjectUtil.isNull(confirmOrder.getId())) {
confirmOrder.setId(SnowUtil.getSnowflakeNextId());
confirmOrder.setCreateTime(now);
confirmOrder.setUpdateTime(now);
confirmOrderMapper.insert(confirmOrder);
} else {
confirmOrder.setUpdateTime(now);
confirmOrderMapper.updateByPrimaryKey(confirmOrder);
}
}
public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {
ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
confirmOrderExample.setOrderByClause("id desc");
ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
LOG.info("查询页码:{}", req.getPage());
LOG.info("每页条数:{}", req.getSize());
PageHelper.startPage(req.getPage(), req.getSize());
List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);
PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);
PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
public void delete(Long id) {
confirmOrderMapper.deleteByPrimaryKey(id);
}
public void doConfirm(ConfirmOrderDoReq req) {
// 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过
Date date = req.getDate();
String trainCode = req.getTrainCode();
String start = req.getStart();
String end = req.getEnd();
// 保存确认订单表,状态初始
DateTime now = DateTime.now();
ConfirmOrder confirmOrder = new ConfirmOrder();
confirmOrder.setId(SnowUtil.getSnowflakeNextId());
confirmOrder.setCreateTime(now);
confirmOrder.setUpdateTime(now);
confirmOrder.setMemberId(LoginMemberContext.getId());
confirmOrder.setDate(date);
confirmOrder.setTrainCode(trainCode);
confirmOrder.setStart(start);
confirmOrder.setEnd(end);
confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
confirmOrder.setTickets(JSON.toJSONString(req.getTickets()));
confirmOrderMapper.insert(confirmOrder);
// 查出余票记录,需要得到真实的库存
DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);
LOG.info("查出余票记录:{}", dailyTrainTicket);
// 预扣减余票数量,并判断余票是否足够
reduceTickets(req, dailyTrainTicket);
// 选座
// 一个车箱一个车箱的获取座位数据
// 挑选符合条件的座位,如果这个车箱不满足,则进入下个车箱(多个选座应该在同一个车厢)
// 选中座位后事务处理:
// 座位表修改售卖情况sell;
// 余票详情表修改余票;
// 为会员增加购票记录
// 更新确认订单为成功
}
private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) {
for (ConfirmOrderTicketReq ticketReq : req.getTickets()) {
String seatTypeCode = ticketReq.getSeatTypeCode();
SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode);
switch (seatTypeEnum) {
case YDZ -> {
int countLeft = dailyTrainTicket.getYdz() - 1;
if (countLeft < 0) {
throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
}
dailyTrainTicket.setYdz(countLeft);
}
case EDZ -> {
int countLeft = dailyTrainTicket.getEdz() - 1;
if (countLeft < 0) {
throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
}
dailyTrainTicket.setEdz(countLeft);
}
case RW -> {
int countLeft = dailyTrainTicket.getRw() - 1;
if (countLeft < 0) {
throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
}
dailyTrainTicket.setRw(countLeft);
}
case YW -> {
int countLeft = dailyTrainTicket.getYw() - 1;
if (countLeft < 0) {
throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
}
dailyTrainTicket.setYw(countLeft);
}
}
}
}
}
ConfirmOrderService.java
package com.neilxu.train.business.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.neilxu.train.business.domain.ConfirmOrder;
import com.neilxu.train.business.domain.ConfirmOrderExample;
import com.neilxu.train.business.domain.DailyTrainTicket;
import com.neilxu.train.business.enums.ConfirmOrderStatusEnum;
import com.neilxu.train.business.enums.SeatColEnum;
import com.neilxu.train.business.enums.SeatTypeEnum;
import com.neilxu.train.business.mapper.ConfirmOrderMapper;
import com.neilxu.train.business.req.ConfirmOrderDoReq;
import com.neilxu.train.business.req.ConfirmOrderQueryReq;
import com.neilxu.train.business.req.ConfirmOrderTicketReq;
import com.neilxu.train.business.resp.ConfirmOrderQueryResp;
import com.neilxu.train.common.context.LoginMemberContext;
import com.neilxu.train.common.exception.BusinessException;
import com.neilxu.train.common.exception.BusinessExceptionEnum;
import com.neilxu.train.common.resp.PageResp;
import com.neilxu.train.common.util.SnowUtil;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class ConfirmOrderService {
public static final ArrayList<Object> OBJECT = new ArrayList<>();
private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);
@Resource
private ConfirmOrderMapper confirmOrderMapper;
@Resource
private DailyTrainTicketService dailyTrainTicketService;
public void save(ConfirmOrderDoReq req) {
DateTime now = DateTime.now();
ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);
if (ObjectUtil.isNull(confirmOrder.getId())) {
confirmOrder.setId(SnowUtil.getSnowflakeNextId());
confirmOrder.setCreateTime(now);
confirmOrder.setUpdateTime(now);
confirmOrderMapper.insert(confirmOrder);
} else {
confirmOrder.setUpdateTime(now);
confirmOrderMapper.updateByPrimaryKey(confirmOrder);
}
}
public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req) {
ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
confirmOrderExample.setOrderByClause("id desc");
ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
LOG.info("查询页码:{}", req.getPage());
LOG.info("每页条数:{}", req.getSize());
PageHelper.startPage(req.getPage(), req.getSize());
List<ConfirmOrder> confirmOrderList = confirmOrderMapper.selectByExample(confirmOrderExample);
PageInfo<ConfirmOrder> pageInfo = new PageInfo<>(confirmOrderList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
List<ConfirmOrderQueryResp> list = BeanUtil.copyToList(confirmOrderList, ConfirmOrderQueryResp.class);
PageResp<ConfirmOrderQueryResp> pageResp = new PageResp<>();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
public void delete(Long id) {
confirmOrderMapper.deleteByPrimaryKey(id);
}
public void doConfirm(ConfirmOrderDoReq req) {
// 省略业务数据校验,如:车次是否存在,余票是否存在,车次是否在有效期内,tickets条数>0,同乘客同车次是否已买过
Date date = req.getDate();
String trainCode = req.getTrainCode();
String start = req.getStart();
String end = req.getEnd();
List<ConfirmOrderTicketReq> tickets = req.getTickets();
// 保存确认订单表,状态初始
DateTime now = DateTime.now();
ConfirmOrder confirmOrder = new ConfirmOrder();
confirmOrder.setId(SnowUtil.getSnowflakeNextId());
confirmOrder.setCreateTime(now);
confirmOrder.setUpdateTime(now);
confirmOrder.setMemberId(LoginMemberContext.getId());
confirmOrder.setDate(date);
confirmOrder.setTrainCode(trainCode);
confirmOrder.setStart(start);
confirmOrder.setEnd(end);
confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
confirmOrder.setTickets(JSON.toJSONString(tickets));
confirmOrderMapper.insert(confirmOrder);
// 查出余票记录,需要得到真实的库存
DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);
LOG.info("查出余票记录:{}", dailyTrainTicket);
// 预扣减余票数量,并判断余票是否足够
reduceTickets(req, dailyTrainTicket);
// 计算相对第一个座位的偏移值
// 比如选择的是C1,D2,则偏移值是:[0,5]
// 比如选择的是A1,B1,C1,则偏移值是:[0,1,2]
ConfirmOrderTicketReq ticketReq0 = tickets.get(0);
if(StrUtil.isNotBlank(ticketReq0.getSeat())) {
LOG.info("本次购票有选座");
// 查出本次选座的座位类型都有哪些列,用于计算所选座位与第一个座位的偏离值
List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(ticketReq0.getSeatTypeCode());
LOG.info("本次选座的座位类型包含的列:{}", colEnumList);
// 组成和前端两排选座一样的列表,用于作参照的座位列表,例:referSeatList = {A1, C1, D1, F1, A2, C2, D2, F2}
List<String> referSeatList = new ArrayList<>();
for (int i = 1; i <= 2; i++) {
for (SeatColEnum seatColEnum : colEnumList) {
referSeatList.add(seatColEnum.getCode() + i);
}
}
LOG.info("用于作参照的两排座位:{}", referSeatList);
List<Integer> offsetList = new ArrayList<>();
// 绝对偏移值,即:在参照座位列表中的位置
List<Integer> aboluteOffsetList = new ArrayList<>();
for (ConfirmOrderTicketReq ticketReq : tickets) {
int index = referSeatList.indexOf(ticketReq.getSeat());
aboluteOffsetList.add(index);
}
LOG.info("计算得到所有座位的绝对偏移值:{}", aboluteOffsetList);
for (Integer index : aboluteOffsetList) {
int offset = index - aboluteOffsetList.get(0);
offsetList.add(offset);
}
LOG.info("计算得到所有座位的相对第一个座位的偏移值:{}", offsetList);
} else {
LOG.info("本次购票没有选座");
}
// 选座
// 一个车箱一个车箱的获取座位数据
// 挑选符合条件的座位,如果这个车箱不满足,则进入下个车箱(多个选座应该在同一个车厢)
// 选中座位后事务处理:
// 座位表修改售卖情况sell;
// 余票详情表修改余票;
// 为会员增加购票记录
// 更新确认订单为成功
}
private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) {
for (ConfirmOrderTicketReq ticketReq : req.getTickets()) {
String seatTypeCode = ticketReq.getSeatTypeCode();
SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode);
switch (seatTypeEnum) {
case YDZ -> {
int countLeft = dailyTrainTicket.getYdz() - 1;
if (countLeft < 0) {
throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
}
dailyTrainTicket.setYdz(countLeft);
}
case EDZ -> {
int countLeft = dailyTrainTicket.getEdz() - 1;
if (countLeft < 0) {
throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
}
dailyTrainTicket.setEdz(countLeft);
}
case RW -> {
int countLeft = dailyTrainTicket.getRw() - 1;
if (countLeft < 0) {
throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
}
dailyTrainTicket.setRw(countLeft);
}
case YW -> {
int countLeft = dailyTrainTicket.getYw() - 1;
if (countLeft < 0) {
throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
}
dailyTrainTicket.setYw(countLeft);
}
}
}
}
}
测试
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。