目录
[使用Spring Task定时处理异常订单](#使用Spring Task定时处理异常订单)
[Apache Echarts制表](#Apache Echarts制表)
关于Swagger使用
Swagger能生成接口文档,方便后端调试
使用方式
导入maven坐标
html
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
创建配置类
java
@Bean
public Docket docket() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
图中扫描了controller包,之后会自动生成接口文档
添加静态资源映射
java
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
完成后访问页面即可,按图中操作既是访问/doc.html
注解
注解的作用是在swagger生成的接口文档中添加相应的说明,随手添加养成好习惯
添加新员工
创建类往线程中添加键值对存储当前操作者的id,前端的每一次请求都会开启一个线程
java
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
在员工类中对dto和其他属性进行封装,BeanUtils.copyProperties(a,b)能将a对象中成员值传递到b对象的同名成员中
java
public static Employee create(EmployeeDTO employeeDTO,Long creator)
{
Employee employee=new Employee();
BeanUtils.copyProperties(employeeDTO,employee);
employee.setStatus(StatusConstant.ENABLE);
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
employee.setCreateUser(creator);
employee.setUpdateUser(creator);
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
return employee;
}
用户名在数据库里设置为unique,当重复时抛出SQLIntegrityConstraintViolationException异常,对前端返回
java
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
String message=ex.getMessage();
if(message.contains("Duplicate entry")) return Result.error("用户名已存在");
return Result.error("未知错误");
}
员工分页查询
在service层使用PageHelper插件,该插件类似于拦截器,在sql语句之后自动添加limit,故不要加分号,同时,在此之前会进行对数据库的一次count()询问,得到total
PageHelper依赖
java
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
此时,返回的值中日期时间会以数组形式返回给前端
在WebMvcConfiguration中重写实现json转换器对传回前端的日期等数据进行格式转化
java
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters){
MappingJackson2HttpMessageConverter converter=new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(new JacksonObjectMapper());
converters.add(0,converter);//往转换器集合中添加自己的转换器
}
重启服务器后得到正确格式
启用禁用员工
为员工设置状态,限制其登录,本质上就是修改操作update
将update使用<set>标签,传递一个实体类,若不为空就修改,这样使得sql语句可以复用,以后就不用写update了
javascript
<update id="update">
update sky_take_out.employee
<set>
<if test="name!=null and name!='' ">name=#{name},</if>
<if test="username!=null and username!='' ">username=#{username},</if>
<if test="password!=null and password!='' ">passord=#{password},</if>
<if test="phone!=null and phone!='' ">phone=#{phone},</if>
<if test="sex!=null and sex!='' ">sex=#{sex},</if>
<if test="idNumber!=null and idNumber!='' ">id_number=#{idNumber},</if>
<if test="status!=null">status=#{status},</if>
<if test="createTime!=null">create_time=#{createTime},</if>
<if test="updateTime!=null">update_time=#{updateTime},</if>
<if test="createUser!=null">create_user=#{createUser},</if>
<if test="updateUser!=null">update_user=#{updateUser},</if>
</set>
where id=#{id}
</update>
编辑员工信息
前端需要数据回显,先进行id查询员工的操作,然后在修改
我们利用上面提到的update更新即可,记得更新修改者和修改时间,同上单线程内的当前id已经放入context了,直接取出来就行了
java
@Override
public void updateEmployee(EmployeeDTO employeeDTO) {
Employee employee=new Employee();
BeanUtils.copyProperties(employeeDTO,employee);
employee.setUpdateUser(BaseContext.getCurrentId());
employee.setUpdateTime(LocalDateTime.now());
employeeMapper.update(employee);
}
分类接口
分类接口和员工接口操作类似,不再赘述
注解AOP自动填充创建修改信息
发现随着项目的复杂化,在创建修改数据是会产生大量冗余的代码来设置创建修改的信息,所以考虑采用AOP和注解的方式实现自动填充信息
自定义注解AutoFill
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//设置操作类型,update 和 insert
OperationType value();
}
这里我们在需要填充信息的方法上添加注解,在切面设置Before方法时通过下面的代码就能获得添加的注解的value从而执行不同的填充操作,然后利用反射对mapper的参数填充信息
java
//获得加了注解的方法对象
MethodSignature signature=(MethodSignature) joinPoint.getSignature();
//通过方法对象获得该方法上的注解对象
AutoFill autoFill=signature.getMethod().getAnnotation(AutoFill.class);
//获得注解的枚举值
OperationType operationType=autoFill.value();
阿里云上传文件
我们可以把项目中所上传保存的图片上传到阿里云保存,先开通阿里云的对象存储oss服务
创建bucket并获取自己的AccessKey的id和secret,在meaven中导入依赖,创建上传工具类后即可,创建controller以接受前端传来的请求
安装方式参考官方文档
工具类代码如下
java
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
新增菜品
菜品除了自身单一的属性之外,还有和口味的一对多的关系,这里将口味抽离出来作为单独的对象,自建一个数据库表,所以在添加菜品时应先将口味除外的属性添加到Dish表,并拿到回显的主键,然后将设置好对应关系的口味插入DishFlavor表中
java
@Override
public Result save(DishDTO dishDTO) {
Dish dish=new Dish();
BeanUtils.copyProperties(dishDTO,dish);
dish.setStatus(StatusConstant.ENABLE);
List<DishFlavor> flavors=dishDTO.getFlavors();
if(dishMapper.check(dish.getName())!=0)
{return Result.error("菜品已存在");}
dishMapper.save(dish);
Long id=dish.getId();
if(flavors!=null && !flavors.isEmpty()){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(id);
});
}
dishFlavorMapper.saveBatch(flavors);
return Result.success();
}
菜品分页展示
dish表中只含有category_id,没有菜品分类的名字,这里使用多表查询,为c的项目起别名来映射到对象
java
<select id="pageQuery" resultMap="dishVoMapper">
select d.*,c.name as categoryName from dish d left outer join category c on d.category_id=c.id
<where>
<if test="categoryId != null">and d.category_id=#{categoryId}</if>
<if test="name !=null and name!='' ">and d.name like concat('%',#{name},'%')</if>
<if test="status !=null ">and d.status = #{status}</if>
</where>
order by d.create_time desc
</select>
删除菜品
删除单个和多个菜品共用一个方法即可,删除菜品时得删两个表,菜品表和口味表,在售的菜品不能删,还包含在套餐里的菜品也不能删
java
@Override
public Result deleteBatch(List<Long> ids) {
//在售不能删
Long countSell=dishMapper.numberOfSell(ids);
//套餐关联不能删
Long countSetMeal=setMealDishMapper.numberOfDishId(ids);
if(countSell!=0||countSetMeal!=0)
{
return Result.error("有菜品在售或被套餐绑定!");
}
dishMapper.deleteBatch(ids);
dishFlavorMapper.deleteByDishId(ids);
return Result.success("删除成功");
}
修改菜品
修改菜品时口味表也许会变,得先删除原先的口味表,然后新设置口味表插入
java
@Override
@Transactional
public void update(DishDTO dishDTO) {
Dish dish=new Dish();
BeanUtils.copyProperties(dishDTO,dish);
List<Long> dishId=new ArrayList<>();//复用方法删除口味表
dishId.add(dishDTO.getId());
dishFlavorMapper.deleteByDishId(dishId);
List<DishFlavor> flavors=dishDTO.getFlavors();//为dish设置口味表
Long id=dishDTO.getId();
if(flavors!=null && !flavors.isEmpty()){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(id);
});
}
dishFlavorMapper.saveBatch(flavors);
System.out.println("OK");
dishMapper.update(dish);
}
店铺营业状态开发
店铺状态查询需要频繁查询,内容又简单,适合使用redis来实现
Redis在spring-boot中的使用方法
1.导入maven坐标
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.在配置文件中配置redis
java
redis:
password: 123456 //密码
host: localhost //地址
port: 6379 //端口
database: 0 //使用几号库
3.添加配置类,导入序列化器
java
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
log.info("创建redis模板对象");
RedisTemplate redisTemplate=new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
然后在代码里通过redisTemplate对象得到的如ValueOperations的方法就可使用redis
营业端代码
java
private final String KEY="SHOP_STATUS";
@PutMapping("/{status}")
@ApiOperation("设置营业状态")
public Result setShopStatus(@PathVariable Integer status)
{
log.info("设置营业状态:{}",status==1?"营业中":"打样中");
ValueOperations valueOperations=redisTemplate.opsForValue();
valueOperations.set(KEY,status);
return Result.success();
}
@GetMapping("/status")
@ApiOperation("查询营业状态")
public Result<Integer> getShopStatus()
{
ValueOperations valueOperations= redisTemplate.opsForValue();
Integer status= (Integer) valueOperations.get(KEY);
log.info("营业状态:{}",status==1?"营业中":"打样中");
return Result.success(status);
}
微信小程序登录
微信小程序登录的官方流程图如下
小程序先发出携带js_code 的登录请求,服务端接收后将小程序的id 和密钥 连同用户的js_code 发送给微信接口服务,微信接口服务返回用户的openid给服务端,服务端此时可以通过此id来注册登录用户
向微信接口服务发送请求时可以使用Httpclient,此处用工具类包装
java
private String WX_LOGIN="https://api.weixin.qq.com/sns/jscode2session";
private String getOpenid(String code){
Map<String,String> mp=new HashMap<>();
mp.put("appid", weChatProperties.getAppid());
mp.put("secret",weChatProperties.getSecret());
mp.put("js_code",code);
mp.put("grant_type","authorization_code");
String json=HttpClientUtil.doGet(WX_LOGIN,mp);
JSONObject jsonObject= JSON.parseObject(json);
return jsonObject.getString("openid");
}
同时注意要再设置一个拦截器拦截来自用户端的请求
用户端浏览菜品
后端代码与管理端大同小异,不再赘述,根据需求文档编写代码
但是,在用户端访问时,可能出现频繁请求访问的情况,使用redis缓存来减轻数据库压力,再通过分类查询菜品后,将该分类id作为key,将得到的数据放入redis缓存中,下次用户查询若redis中已经存在该数据,就不用查询数据库了
java
String key="dish_"+categoryId;
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
if(list!=null && !list.isEmpty())
{
return Result.success(list);
}
为了保持数据的一致性,管理端在更新修改菜品时,应该清除redis中相关的缓存
在修改操作返回前执行清除缓存操作
java
private void cleanCache(String pattern)
{
Set keys=redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
使用spring_cache管理套餐缓存
补全套餐接口,注意没写根据id获得套餐信息(数据回显)之前无法正常测试修改套餐接口,(明明前端已经有套餐id了还要使用回显的id),代码重复度高,略
spring_cache使用方法
导入坐标
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
在启动类上加上@EnableCaching注解开启缓存代理,会自动判断你使用的缓存来进行管理
在需要管理的方法上加上对应注解即可
@Cacheable(cacheNames="xxx",key="#yyy")
将在缓存中添加key为xxx::yyy,value为方法返回值的键值对(#代表取值而不是字符串)
@CacheEvict(cacheNames="xxx",allentries=true)
删除所有xxx::的缓存,也可将后面替换为key来精确删除
购物车业务
单独开表实现购物车功能,购物车表里每一项都代表着某人的购物车里的某个菜,在添加时先查询是否存在,如果存在就修改数量,不存在就从其他表里拿出必要信息补全并添加,删除同理,删除单个时要先判断菜品的数量来决定修改还是删除
添加代码如下
java
@Override
public void add(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart=new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
shoppingCart.setUserId(BaseContext.getCurrentId());
List<ShoppingCart> list=shoppingCartMapper.list(shoppingCart);
if(list!=null && !list.isEmpty())
{
ShoppingCart res=list.get(0);
res.setNumber(res.getNumber()+1);
shoppingCartMapper.update(res);
return ;
}
if(shoppingCart.getDishId()!=null)
{
Long dishId=shoppingCart.getDishId();
Dish dish= new Dish();
dish.setId(dishId);
List<Dish> res=dishMapper.list(dish);
dish=res.get(0);
shoppingCart.setImage(dish.getImage());
shoppingCart.setName(dish.getName());
shoppingCart.setAmount(dish.getPrice());
}
else
{
Long setmealId=shoppingCart.getSetmealId();
Setmeal setmeal=setmealMapper.selectById(setmealId);
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setName(setmeal.getName());
shoppingCart.setAmount(setmeal.getPrice());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.add(shoppingCart);
}
删除代码如下
java
@Override
public void delete(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart=new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
shoppingCart.setUserId(BaseContext.getCurrentId());
List<ShoppingCart> list=shoppingCartMapper.list(shoppingCart);
ShoppingCart res=list.get(0);
if(res.getNumber()>1)
{
res.setNumber(res.getNumber()-1);
shoppingCartMapper.update(res);
}
else
{
shoppingCartMapper.delete(shoppingCart);
}
}
订单提交
订单提交到后端时,先处理下数据,处理没有地址,购物车为空的情况(前端已经完成过,以防万一),然后将前端传过来的数据补全,添加到订单表(order)和订单详情表(order_detail),最后注意清空用户的购物车表(shoppingcart)
订单支付
个人认证的小程序没法使用支付功能,这里跳过支付,只要用户点击支付就算支付成功,代码如下
java
@Override
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) {
Long userId=BaseContext.getCurrentId();
User user=userMapper.getByid(userId);
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", "ORDERPAID");
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));//返回前端的对象
Orders orders=new Orders();
orders.setNumber(ordersPaymentDTO.getOrderNumber());
orders.setUserId(userId);
Orders ordersDB = orderMapper.select(orders);//根据userid和订单号查询指定订单
orders.setId(ordersDB.getId());
orders.setStatus(Orders.TO_BE_CONFIRMED);
orders.setPayStatus(Orders.PAID);
orders.setPayMethod(ordersPaymentDTO.getPayMethod());
orders.setCheckoutTime(LocalDateTime.now());
orderMapper.update(orders);//更新为支付状态
return vo;
}
微信小程序支付官方流程图
使用Spring Task定时处理异常订单
使用方法
导入坐标(在整合包中已经包含)
在启动类上添加@EnableScheduling开启定时任务
创建定时任务类,在方法上添加@Scheduled(cron = "参数")注解
在项目启动后,就会按照cron所表达的内容定时执行该方法
java
@Scheduled(cron = "0 * * * * ?")
public void timeOutOrder()
{
log.info("处理超时未支付订单:{}",LocalDateTime.now());
LocalDateTime time=LocalDateTime.now().plusMinutes(-15);
List<Orders> list=orderMapper.wrongOrder(Orders.PENDING_PAYMENT,time);
if(list==null||list.isEmpty()) return;
for(Orders orders:list)
{
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("支付超时");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
@Select("select * from orders where status=#{status} and order_time<#{orderTime}")
List<Orders> wrongOrder(Integer status, LocalDateTime orderTime);
使用websocket实现订单提醒
与http短连接协议不同,ws协议是持续链接协议,建立了全双工通道,可以实时相互发送消息,项目里包装使用,代码如下
java
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
由于无法实现支付,新订单提醒放在了暂定的实现方法后面,正常应放在支付成功,微信服务端返回调用的函数中
催单提醒时,先判断订单号和订单状态,存在相应的订单才向客户端发送催单
Apache Echarts制表
此为前端技术,引入js文件后,按照指定格式填写数据就能简单地在页面生成一个表格
因此,后端的任务根据接口文档就知道返回数据列表即可
先将前端转过来的日期范围落实成一个日期的列表,然后根据列表的内容去查询每天的营业额,再把每天的营业额放入另一个列表,最后将两个列表封装返回给前端
java
@Override
public TurnoverReportVO turnoverStatistics(LocalDate begin, LocalDate end) {
List<LocalDate> datelist=new ArrayList<>();
datelist.add(begin);
while(!begin.equals(end))
{
begin=begin.plusDays(1);
datelist.add(begin);
}
List<Double> incomelist=new ArrayList<>();
for(LocalDate date:datelist)
{
LocalDateTime beginTime=LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime=LocalDateTime.of(date, LocalTime.MAX);
Map mp=new HashMap<>();
mp.put("begin",beginTime);
mp.put("end",endTime);
mp.put("status",Orders.COMPLETED);
Double income=orderMapper.sumByMap(mp);
incomelist.add(income==null?0.0:income);
}
return TurnoverReportVO.builder()
.dateList(org.apache.commons.lang.StringUtils.join(datelist,","))
.turnoverList(StringUtils.join(incomelist,","))
.build();
}
管理端数据分析功能
通过上述制表技术,前端要求给出日期始末,返回相应的数据
将拿到的日期先得到一个日期的List,再根据每一个日期在数据库中相应的数据
将得到的数据List转化为字符串(此处可用stream流),封装之后返回给前端
订单管理
按着计划写下来最后就差订单管理板块,根据接口文档写即可,注意在put的请求时,这里接收参数都是用dto或path,单独接收id会出错(传过来的就是对象)
至此项目基本完成