项目整体介绍

开发环境搭建

1.前端环境搭建

2.后端环境搭建

nginx反向代理

管理端

员工管理

禁用员工

PathVariable路径参数的使用

builder注解的使用

员工分页查询

  • Controller层 返回值为 Result<PageResult> return Result.success(PageResult);

  • Service层 返回值为 PageResult return new PageResult(.getTotal(),list)

@Override  
public PageResult pageQuey(EmployeePageQueryDTO employeePageQueryDTO) {  
    //重点:使用pagehelper:本质是拦截器,用Treadlocal取出page参数在xml文件里加入limit关键字  
    PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());  
  
    //重点:这里的page是pagehelper里的,范性里填属性,用mapper返回  
    Page<Employee> page  = employeeMapper.pageQuery(employeePageQueryDTO);  
  
    //重点:从mapper获取到page(pagehelper里的)列表后,取出page里的数量和列表记录  
    long total = page.getTotal();  
    List<Employee> records = page.getResult();  
  
    return new PageResult(total,records);  
}

时间日期格式化 (Spring MVC消息转换器) (JsonFormat)

新增员工

分为根据id查询员工与更新员工(传入上个步骤的对象json格式) 这两个步骤之间是自动完成的(前端路径自动匹配)

@RequestBody当传入对象为json格式时使用

拷贝对象属性:(复制属性)

  • 有点像把一个对象属性转换为另一个对象属性
@Override  
public void update(EmployeeDTO employeeDTO) {  
    Employee employee = new Employee();  
  
    BeanUtils.copyProperties(employeeDTO,employee);  
    employee.setUpdateTime(LocalDateTime.now());  
    employee.setUpdateUser(BaseContext.getCurrentId());  
      
    employeeMapper.update(employee);  
}

通过拦截器Basecontext获取修改者(JwtTokenAdminInterceptor内)

分类管理

公共字段的自动填充

技术点:枚举,自定义注解,AOP,反射

菜品管理

新增菜品

接口设计

  • 根据类型查询分类(已完成)

  • 文件上传

  • 新增菜品

  • 数据库设计

文件上传

返回类型:Result<String> 传入参数:MultipartFile file 注释:PostMapping(“…”)

传入过程:

  • 用file.得出原始文件名
  • 然后用subString(原始文件名.lastIndexOf(“.”))得出文件名后缀
  • 用UUID拼接新的文件名
  • 在Utils.upload传入参数拼接后的文件名:返回fielPath
@Slf4j  
@RestController  
@RequestMapping("/admin/common")  
public class CommonController {  
  
    private AliOssUtil aliOssUtil;  
  
  
    @PostMapping("/upload")  
    public Result<String> upload(MultipartFile file){  
        log.info("文件上传:{}",file);  
        try {  
            String originalFilename = file.getOriginalFilename();  
            String extension = originalFilename.substring(originalFilename.lastIndexOf("."));  
            String objectName = UUID.randomUUID().toString()+extension;  
  
            String filePath = aliOssUtil.upload(file.getBytes(), objectName);  
            return Result.success(filePath);  
        } catch (IOException e) {  
            log.info("文件上传失败:{}",file);  
        }  
        return null;  
    }  
}

DTO

在Java编程中,DTO(Data Transfer Object,数据传输对象)是一种用于在不同层之间传输数据的对象。DTO通常用于将业务逻辑层(Service层)的数据传输给表示层(Presentation层)或持久化层(Persistence层)。DTO对象通常包含业务领域的数据,但不包含业务逻辑12

使用场景

DTO在以下场景中非常有用:

  1. 数据传输:在业务逻辑层和表示层之间传输数据时,使用DTO可以简化数据的传递过程。例如,在一个涉及多个表的业务操作中,可以使用DTO来封装不同表的数据2

  2. 数据封装:DTO可以将多个实体对象的数据封装在一个对象中,便于在一次操作中传递多个数据。例如,在新增、查询和更新菜品时,通过DTO对象进行数据库交互,包括保存、查询和更新菜品及关联的口味信息2

添加口味

获取insert的主键值:<insert id=“insert” useGeneratedKeys=“true” keyProperty=“id”>

在service层,通过DishDTO传入inset语句时,需new出一个dish给insert语句,因为DishDTO包含一个口味的集合 通过BeanUtils.copyPropertis复制属性

这里的insert语句上需加注解:@AutoFill(values = OperationType.INSERT)

public void saveWithFlavor(DishDTO dishDTO) {  
  
    Dish dish = new Dish();  
    BeanUtils.copyProperties(dishDTO,dish);  
  
    //像菜品表插入一条数据  
    dishMapper.insert(dish);  
  
    //获取insert语句生成的主键值(useGeneratedKeys="true" keyProperty="id)  
    Long dishId = dish.getId();  
  
    //像口味表插入n条数据  
    List<DishFlavor> flavors = dishDTO.getFlavors();  
    if(flavors!=null && flavors.size() > 0){  
        flavors.forEach(dishFlavor -> {  
            dishFlavor.setDishId(dishId);  
        });  
        dishFlavorMapper.insertBatch(flavors);  
    }  
  
}

insert 语句中的遍历插入(foreach标签的使用):

<insert id="insertBatch">  
    insert into dish_flavor (dish_id, name, value)  
    VALUES        <foreach collection="flavors" item="df">  
            (#{df.dishId},#{df.name},#{df.value})  
        </foreach>  
</insert>

分页查询

这里根据接口定义设计了DishPageQueryDTO 包含以上五个变量

这里根据接口定义设计了DishVO(vaule object)

DTO与VO,page工具的使用

DTO是前端传后端,VO是后传前(回显?)

//query方式:不需要像接受json数据一样加注解(直接在地址栏key=??)

这里的page范性内填<pageVO>(内容多一个分类名称),而不是像员工分页查询一样<Employee>

菜品删除

接口设计与数据库设计

传入参数

思路:传入参数为多个时(query方式), 第一种方法:接受形参为字符串,然后后续分割字符串处理. 第二张方式:SpringMVC处理,(@RequestParam List<Long> ids) 小总结: 传入为json格式:@RequestBody 传入为连续字符串:@RequestParam

多个参数的传入与sql书写

在查询setmeal_ids时,需使用动态sql到xml映射文件中,因为,传入的参数可能为多个,即 select setmeal_id from setmeal_dish in(1,2,3,4.....)

这里使用<foreach>标签

<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">  
    select setmeal_id from setmeal_dish where dish_id in  
    <foreach collection="dishIds" item="dishId" separator="," close=")" open="()">  
                                        #{dishId}  
    </foreach>  
</select>

<foreach>中的collection:返回值的名称,item:取出变量的名称,close与open:前后需要拼接的符号

传入多个参数时的快速遍历

如这里ids,.出for方法即:ids.for回车

事务

删除操作需有一体性,使用事务

异常

不满操作时,抛出自定义的异常

@Transactional  
@Override  
public void deleteBatch(List<Long> ids) {  
    //判断是否能删除,1.是否起售.2.呗套餐关联  
    for (Long id : ids) {  
        Dish dish = dishMapper.getById(ids);  
        if(Objects.equals(dish.getStatus(), StatusConstant.ENABLE)){  
            throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);  
        }  
    }  
  
    List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);  
    if(setmealIds !=null && !setmealIds.isEmpty()){  
        throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);  
    }  
  
    //删除菜品数据与口味数据  
    for (Long id : ids) {  
        dishMapper.deleteById(id);  
        dishFlavorMapper.deleteByDishId(id);  
    }  
}

菜品修改

路径参数

传入参数需称为路径参数时,使用@PathVariable注解

VO与DTO的选择

在修改菜品中的查询菜品id的操作时,返回需为DishVO,因为在项目中,VO除了包括基础的菜品数据时,还包括Flavor口味表 其实以上说法不对,VO相比于DTO多了更新时间等成员.主要是反馈给前端

dishDTO转换为普通的dish对象:使用BeanUitil方法赋值给新的dish对象

菜品回显

没什么nb的,就是在controller层返回了dishVO对象给前端(Result.success(dishVO))

具体口味的修改

技术层面来说,这里对口味的数据是先删除在添加.

<set>标签的使用

在更新菜品表时,使用sql语句 update dish set name = #{name},price = #{price}...... 改为动态sql语句xml文件

<update id="update">  
    update dish  
    <set>  
        <if test="name != null">  
            name = #{name},  
        </if>  
        <if test="categoryId != null">  
            category_id = #{categoryId},  
        </if>  
        <if test="price != null">  
            price = #{price},  
        </if>  
        <if test="image != null">  
            image = #{image},  
        </if>  
        <if test="description != null">  
            description = #{description},  
        </if>  
        <if test="status != null">  
            status = #{status},  
        </if>  
        <if test="updateTime != null">  
            update_time = #{updateTime},  
        </if>  
        <if test="updateUser != null">  
            update_user = #{updateUser},  
        </if>  
    </set>  
    where id = #{id}  
</update>

店铺营业状态(Redis)

Redis

Bean注解

  1. @Bean:Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中;

  2. SpringIOC 容器管理一个或者多个bean,这些bean都需要在@Configuration注解下进行创建,在一个方法上使用@Bean注解就表明这个方法需要交给Spring进行管理;

  3. @Bean是一个方法级别上的注解,主要用在@Configuration注解的类里,也可以用在@Component注解的类里。添加的bean的id为方法名;

  4. 使用Bean时,即是把已经在xml文件中配置好的Bean拿来用,完成属性、方法的组装;比如@Autowired , @Resource,可以通过byTYPE(@Autowired)、byNAME(@Resource)的方式获取Bean;

  5. 注册Bean时,@Component , @Repository , @ Controller , @Service , @Configration这些注解都是把你要实例化的对象转化成一个Bean,放在IoC容器中,等你要用的时候,它会和上面的@Autowired , @Resource配合到一起,把对象、属性、方法完美组装;

  6. @Configuration与@Bean结合使用:@Configuration可理解为用spring的时候xml里面的标签,@Bean可理解为用spring的时候xml里面的标签;

需求分析与设计

用户端吧路径中的admin改为user

营业状态存储方式:基于Redis的字符串进行存储

不同包下的同名类名处理

在RestController(“指定的不同类名”)

用户端

HttpClient

GET请求测试

@SpringBootTest  
@Slf4j  
public class HttpClientTest {  
  
    @Test  
    //GEt  
    public void testGET() throws Exception {  
        //创建httpclient对象  
        CloseableHttpClient httpClient = HttpClients.createDefault();  
        //创建请求对象  
        HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");  
        //发送请求  
        CloseableHttpResponse response = httpClient.execute((httpGet));  
        //获取服务端返回的状态吗  
        int statusCode = response.getStatusLine().getStatusCode();  
        System.out.println("状态码为:"+statusCode);  
        HttpEntity entity = response.getEntity();  
        String body = EntityUtils.toString(entity);  
        System.out.println("返回的数据为:"+body);  
        //关闭资源  
        response.close();  
        httpClient.close();  
    }  
  
}

POST请求测试

@Test  
public void testPOST() throws Exception {  
    CloseableHttpClient httpClient = HttpClients.createDefault();  
    HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");  
  
    JSONObject jsonObject = new JSONObject();  
    jsonObject.put("username","admin");  
    jsonObject.put("password","123456");  
    StringEntity entity = new StringEntity(jsonObject.toString());  
    //指定请求编码方式  
    entity.setContentEncoding("utf-8");  
    entity.setContentType("application/json");  
    httpPost.setEntity(entity);  
  
    CloseableHttpResponse response = httpClient.execute(httpPost);  
    int statusCode = response.getStatusLine().getStatusCode();  
    System.out.println("响应码为:"+statusCode);  
    HttpEntity entity1 = response.getEntity();  
    String body = EntityUtils.toString(entity1);  
    System.out.println("响应数据为"+body);  
  
    response.close();  
    httpClient.close();  
  
  
}

微信小程序开发

微信小程序

目录结构

微信登录接口

需配置微信小程序appid与secret在dev.yaml与yaml文件

用户登录代码开发

Controller层:

  • 接受DTO,返回VO
  • 调用sevice层返回user对象
  • 对user对象解析并加入jwttonken等
  • 返回VO

Service层

  • 接受DTO后,加入四大护法(两个在本地)进Map对象
  • 使用HttpClientUtil.doGet(微信官方api,map)得到json对象
  • 对json对象解析出openid
  • 判断openid
  • 然后通过openid查询Mapper层
  • (如果没有用户就插入用户表)
  • 返回user

为用户端配置全局拦截器

开发完后,添加用户的全局拦截器

  • JwtTokenUserInterceptor.java
  • 之后在配置类内注册此拦截器WebMvcConfiguration.java
@Configuration  
@Slf4j  
public class WebMvcConfiguration extends WebMvcConfigurationSupport {  
  
    @Autowired  
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;  
  
    @Autowired  
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;  
  
    /**  
     * 注册自定义拦截器  
     *  
     * @param registry  
     */  
    protected void addInterceptors(InterceptorRegistry registry) {  
        log.info("开始注册自定义拦截器...");  
        registry.addInterceptor(jwtTokenAdminInterceptor)  
                .addPathPatterns("/admin/**")  
                .excludePathPatterns("/admin/employee/login");  
  
        registry.addInterceptor(jwtTokenUserInterceptor)  
                .addPathPatterns("/user/**")  
                .excludePathPatterns("/user/user/login")  
                .excludePathPatterns("/user/shop/status");  
    }

商品浏览代码

缓存菜品

  • 数据库中的菜品数据变更时及时清理缓存数据

方案1:

private void cleanCache(String pattern){  
    Set keys = redisTemplate.keys(pattern);  
    redisTemplate.delete(keys);  
}

在每次更新,或删除菜品时调用redisTemplate方法

方案2:SpringCache

SpringCache

套餐代码(双表)

添加套餐

  • setmealDTO(传入参数)包含setmeal基础信息与setmealDish

在Service层:

  • 对于setmeal的一堆零碎参数 使用BeanUtils赋值给新创建的setmeal对象 插入对应的setmealMapper
  • 对于setmealDishs 从DTO中得出里面的DIsh表 遍历list<setmealDish>列表赋值相应的软连接(项目要求)
public void saveWithDish(SetmealDTO setmealDTO) {  
  
    Setmeal setmeal = new Setmeal();  
    BeanUtils.copyProperties(setmealDTO,setmeal);  
    setmealMapper.insert(setmeal);  
    Long setmeal_id = setmeal.getId();  
  
    List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();  
    setmealDishes.forEach(setmealDish -> {  
        setmealDish.setSetmealId(setmeal_id);  
    });  
    setmealDisheMapper.insert(setmealDishes);  
}

在Mapper层

  • 在xml文件中<parameterType=“返回的类型”>

  • <useGenerateKeys = “true”><KeyProperty=“id”> 通过这两个实现主键自增

  • 在插入多条数据时,使用forEach

  • collection=“setmealDishes”:指定要遍历的集合名称,这里是 setmealDishes 集合

  • item=“sd”:表示集合中每个元素的别名,在遍历过程中,每次迭代到的元素会被赋值给 sd 变量。

  • separator=”,“:指定每次迭代之间的分隔符,这里是逗号 ,,即在每次迭代生成的 SQL 片段之间用逗号隔开。

更新套餐

在service层:

  • 更新套餐时,对于零碎对象用BeanUtils
  • 对于setmealDish 先删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete 然后从DTO中获得dish.setmealId(forEach遍历赋值) 最后重新插入setmealDishes

更改套餐状态

在service层:

  • 不直接创建单独的status的mapper语句,用biuder容器将id的setmealStatus改为相应的状态,并将此setmeal装入update语句
  • 外连接查询 此处用的是dishMapper.从中间表Dish_Setmeal表查询
select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}

分表查询的动态sql

<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
    select
    	s.*,c.name categoryName
    from
    	setmeal s
    left join
    	category c
    on
    	s.category_id = c.id
    <where>
        <if test="name != null">
            and s.name like concat('%',#{name},'%')
        </if>
        <if test="status != null">
            and s.status = #{status}
        </if>
        <if test="categoryId != null">
            and s.category_id = #{categoryId}
        </if>
    </where>
    order by s.create_time desc
</select>

添加购物车

用户id的获取

在此项目中,通过拦截器可获取当前用户id BaseContext.getCurrentId()

购物车的添加逻辑

@Override  
public void addShoppingcart(ShoppingCartDTO shoppingCartDTO) {  
  
    ShoppingCart shoppingCart = new ShoppingCart();  
    BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);  
    Long id = BaseContext.getCurrentId();  
    shoppingCart.setUserId(id);  
    List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);  
    //如果已经存在,数量+1  
    if(list!=null && !list.isEmpty()){  
        ShoppingCart cart  = list.get(0);  
        cart.setNumber(cart.getNumber()+1);  
        shoppingCartMapper.updateNumberById(cart);  
    }else{  
        //如果不存在,插入新的一条购物车数据(判断条件:通过DTO里的dishId是否为空判断)  
        Long dishId = shoppingCartDTO.getDishId();  
        if(dishId != null){  
            Dish dish = dishMapper.getById(dishId);  
            
            shoppingCart.setName(dish.GetName())
            shoppingCart.setImage(dish.getImage());  
            shoppingCart.setAmount(dish.getPrice());  
  
        }else{  
            //查询的是套餐  
            Long setmealId = shoppingCart.getSetmealId();  
            Setmeal setmeal = setmealMapper.select(setmealId);  
  
            shoppingCart.setName(setmeal.getName());  
            shoppingCart.setImage(setmeal.getImage());  
            shoppingCart.setAmount(setmeal.getPrice());  
        }  
    }  
  
    shoppingCart.setNumber(1);  
    shoppingCart.setCreateTime(LocalDateTime.now());  
    shoppingCartMapper.insert(shoppingCart);  
  
}

缓存套餐

使用SpringCache

导入地址薄

需求分析

其中《修改地址需要先根据id查询在更新

用户下单

DTO设计

VO设计

@Transactional  
@Override  
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {  
  
    //各种业务异常(地址薄为空,购物车数据为空)  
    AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());  
    if(addressBook == null){  
        throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);  
    }  
    Long userId = BaseContext.getCurrentId();  
    ShoppingCart shoppingCart = new ShoppingCart();  
    shoppingCart.setUserId(userId);  
    List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);  
    if(shoppingCartList == null || shoppingCartList.size() == 0){  
        throw  new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);  
    }  
    //像订单表插入一条数据  
    Orders orders = new Orders();  
    BeanUtils.copyProperties(ordersSubmitDTO,orders);  
    orders.setOrderTime(LocalDateTime.now());  
    orders.setPayStatus(Orders.UN_PAID);  
    orders.setStatus(Orders.PENDING_PAYMENT);  
    orders.setNumber(String.valueOf(System.currentTimeMillis()));  
    orders.setPhone(addressBook.getPhone());//通过地址薄拿到用户手机号  
    orders.setConsignee(addressBook.getConsignee());  
    orders.setUserId(userId);  
  
    orderMapper.insert(orders);//这里需要返回orders的主键值key,在xml中添加相应参数  
  
    //向订单明细表插入n条数据  
  
    List<OrderDetail> orderDetails = new ArrayList<>();  
    for(ShoppingCart cart : shoppingCartList){  
        OrderDetail orderDetail = new OrderDetail();  
        BeanUtils.copyProperties(cart,orderDetail);  
        orderDetail.setOrderId(orders.getId());  
        orderDetails.add(orderDetail);  
    }  
  
    orderDetailMapper.insert(orderDetails);  
  
    //清空当前用户购物车数据  
    shoppingCartMapper.delete(userId);  
  
    //封装VO返回  
    OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()  
            .id(orders.getId())  
            .orderTime(orders.getOrderTime())  
            .orderNumber(orders.getNumber())  
            .orderAmount(orders.getAmount())  
            .build();  
  
  
    return orderSubmitVO;  
}

微信支付

Spring Task(订单状态定时处理)

cron表达式

需求分析

Websocket

来电提醒

Map map = new HashMap();  
map.put("type",1);  
map.put("orderId",ordersDB.getId());  
map.put("content","订单号: "+outTradeNo);  
  
String json = JSON.toJSONString(map);   
webSocketServer.sendToAllClient(json);

图形统计

Apache ECharts

实战

用户端

查询历史订单

查询订单详情

取消订单

已完成

再来一单

未完成,

商家端

订单搜索

分页难点

在service层

Page page = mapper层接口 List orderVOList = 从DTO加点东西到VO

返回是

return new PageResult(page.getTotal(),orderVOList)

另外一个难点就是从DTO到VO需要手动加入菜品信息(订单中的菜品和数量) “同时vo对象里面需要一个String类型的描述字段,所以我们指定格式为菜品数量的格式进行格式化字符串封装进orderVO对象里面”

  • 普通版本如下
private String getOrderDeshesStr(Orders orders) {  
  
    List<OrderDetail> orderDetailList = orderDetailMapper.getOrderDetailByOrdrId(orders.getId());  
      
    List<String> orderDishesList = new ArrayList<>();  
    for (OrderDetail orderDetail : orderDetailList) {  
        String orderDesh = orderDetail.getName()+"*"+orderDetail.getNumber()+";";  
        orderDishesList.add(orderDesh);  
    }  
    // 使用 StringBuilder 将所有菜品信息拼接在一起  
    StringBuilder result = new StringBuilder();  
    for (String dish : orderDishesList) {  
        result.append(dish);  
    }  
  
    // 返回最终拼接的字符串  
    return result.toString();  
}
  • stream流版本如下
private String getOrderDishesStr(Orders orders) {
        // 查询订单菜品详情信息(订单中的菜品和数量)
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());
 
        // 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
        List<String> orderDishList = orderDetailList.stream().map(x -> {
            String orderDish = x.getName() + "*" + x.getNumber() + ";";
            return orderDish;
        }).collect(Collectors.toList());
 
        // 将该订单对应的所有菜品信息拼接在一起
        return String.join("", orderDishList);
    }

各个状态的订单数量统计

查询订单详情

接单,拒单

  • 取消订单

  • 派送订单

  • 完成订单

当需要写理由时,传入DTO对象set理由 其他情况只需要传入id

营业额统计

当传入参数为时间时

@RequestMapping("/turnoverStatistics")  
public Result<TurnoverReportVO> turnoverStatistics(  
        @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,  
        @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){  
    log.info("营业额数据统计:{},{}",begin,end);  
    return Result.success(reportService.getTurnoverStatistics(begin,end));  
}

mapper传入参数为map时

使用动态xml ·

<select id="sumByMap" resultType="java.lang.Double">  
    select sum(amount) from orders  
    <where>  
        <if test="begin != null">  
            and order_time &gt; #{begin}  
        </if>  
        <if test="end != null">  
            and order_time &lt; #{end}  
        </if>  
        <if test="status != null">  
            ands status = #{status}  
        </if>  
    </where>  
</select>

新增用户统计

销量排名

Stream流的复杂使用

总结:

  • 先转换时间格式
  • 书写xml动态sql的出需要的数据列表(复杂,这里得到的是map)
  • 取出列表里的独立数据
  • 组装,返回VO数据
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {  
    LocalDateTime beginTime = LocalDateTime.of(begin,LocalTime.MIN);  
    LocalDateTime endTime = LocalDateTime.of(begin,LocalTime.MAX);  
  
    List<GoodsSalesDTO> salesTop = orderMapper.getSalesTop(beginTime, endTime);  
    List<String> names = salesTop.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());  
    String nameList = StringUtils.join(names, ",");  
    List<Integer> numbers = salesTop.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());  
    String numberList = StringUtils.join(numbers, ",");  
  
    return SalesTop10ReportVO.builder()  
            .nameList(nameList)  
            .numberList(numberList)  
            .build();  
    }

复杂sql的书写(多表查询)

<select id="getSalesTop" resultType="com.sky.dto.GoodsSalesDTO">  
    select od.name, sum(od.number) number  
    from order_detail od, orders o    
    where od.order_id = o.id and o.status = 5    
    <if test="begin != null">  
        and o.order_time &gt; #{begin}  
    </if>  
    <if test="end != null">  
        and o.order_time &lt; #{end}  
    </if>  
    group by od.name  
    order by number desc    
    limit 0,10</select>

工作台

APache POI