苍穹外卖项目笔记

软件开发流程

需求分析:说明书和原型 

设计:UI,数据库,接口设计

编码:项目代码,单元测试

测试:测试用例,测试报告

上线运维:软件环境安装,配置

软件环境

开发环境:本地电脑环境,外部用户无法访问
测试环境:测试人员测试项目,测试服务器
生产环境:正式对外提供服务的环境

苍穹外卖项目介绍

技术选型

 项目结构

 

 为什么直接给出来而不是从零开始写呢,因为在公司里也不可能让你造轮子的

 数据库

 前后端联调

 登录过程:

        执行启动项以后,进入EmployeeController,执行login方法,接收前端传进来的数据employeeLoginDTO(数据传输对象),打印一个员工登录日志,此时调用employeeService的login函数,传入刚才的DTO。

@PostMapping("/login")
    public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
        log.info("员工登录:{}", employeeLoginDTO);

        Employee employee = employeeService.login(employeeLoginDTO);

        通过实体类,调用employeServiceImpl的login函数,接收DTO,通过@AutoWired注入employeeMapper的bean。利用DTO的get和set方法得到输入的用户名和密码,调用employee的getByUsername来根据用户名查询员工。

@Service 写在实现类里
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    /**
     * 员工登录
     *
     * @param employeeLoginDTO
     * @return
     */
    public Employee login(EmployeeLoginDTO employeeLoginDTO) {
        String username = employeeLoginDTO.getUsername();
        String password = employeeLoginDTO.getPassword();

        //1、根据用户名查询数据库中的数据
        Employee employee = employeeMapper.getByUsername(username);

        从mysql数据库中寻找这个用户名信息的数据,以Employee的形式返回给Service

@Select("select * from employee where username = #{username}")
    Employee getByUsername(String username);

        接下来返回到Service层里接收employee,处理各种异常情况,如过Employee为空,说明没有从sql里找到数据,返回异常。接着比对密码,如果输入的密码不等于从数据库里拿出来的密码,也返回异常,如果账号的状态是锁定,也返回异常,都不是的话,说明账号是对的,返回实体对象,回到Controller中。

if (employee == null) {
            //账号不存在
            throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
        }

        //密码比对
        // TODO 后期需要进行md5加密,然后再进行比对
        if (!password.equals(employee.getPassword())) {
            //密码错误
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }

        if (employee.getStatus() == StatusConstant.DISABLE) {
            //账号被锁定
            throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
        }

        //3、返回实体对象
        return employee;

        接着生成jwt令牌,在里面传入想传入的数据如empid,利用JwtUtil(已封装好)方法,传入秘钥,过期时间,以及刚才生成的claims(利用@ConfigurationProperties生成一个配置属性类,与yml文件相连接,得到对应的参数,令牌生成成功)

@Component
@ConfigurationProperties(prefix = "sky.jwt") //配置属性类,封装配置项,把yml里的数据传进来
@Data
public class JwtProperties {

    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;
    private long adminTtl;
    private String adminTokenName;
sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token

        令牌生成以后,生成一个视图对象VO返回给前端,利用@Builder来创建出一个employeeLoginVO,以result形式返回给前端

        EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
                .id(employee.getId())
                .userName(employee.getUsername())
                .name(employee.getName())
                .token(token)
                .build();

        return Result.success(employeeLoginVO);

         为什么要通过nginx连接前端和后端呢,前后url一样不好吗?

 密码加密

md5加密处理,如果数据库被偷也问题不大了

password = DigestUtils.md5DigestAsHex(password.getBytes());

 项目接口文档

Yapi是设计阶段使用的工具,管理和维护接口

Swagger用来代替postman,在开发阶段使用的框架,帮助后端开发人员做后端的接口测试

    @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;
    }

    /**
     * 设置静态资源映射
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

 Api后面加tags=,ApiModel后面加description=

新增员工

 

代码开发

        当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据,调用业务层,传入DTO即可,一般新增员工用post方法,同时由于前端传递过来的是json对象,所以要加一个@RequsetBody注解才能将其转换为DTO类

@PostMapping
    @ApiOperation("新增员工")
    public Result save(@RequestBody EmployeeDTO employeeDTO){
        log.info("新增员工:{}",employeeDTO);
        employeeService.save(employeeDTO);
        return Result.success();
    }

        接下来是业务层的逻辑,重写接口的sava方法,注意由于控制层传入的是前端发送过来的DTO对象,但是要给Mapper传入的最好是实体类对象,所以最好进行一下转换,这里需要new一个对象,如果一个一个的把DTO传入到实体类里,可能会比较麻烦,所以这里我们使用对象属性拷贝BeanUtils.copyProperties(employeeDTO,employee); 剩下还有一些数据再单独加入(这里创建人和修改人的id逻辑后面再处理,先todo)

@Override
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();

        //对象属性拷贝 , 前提属性名一致
        BeanUtils.copyProperties(employeeDTO,employee);

        //设置账号状态 默认正常 1正常 0锁定
        employee.setStatus(StatusConstant.ENABLE); //用常量类,不要硬编码

        //设置密码,默认密码123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

        //设置创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());

        //设置当前记录创建人id和修改人id
        employee.setCreateUser(BaseContext.getCurrentId());
        employee.setUpdateUser(BaseContext.getCurrentId());


        employeeMapper.insert(employee);
    }

        Mapper层里由于逻辑比较简单,所以直接插入即可

 @Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status)" +
            "values" +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
    void insert(Employee employee);

代码优化

重复员工异常

        如果增加的两个username相同,按照sql里的设定,就一定会报错,我们不想让他直接报错,而是给出一定的响应,这就需要在全局异常处理器里面进行设定。在server目录下的handler包里设置一个全局异常处理器,加入@RestController注解。

@RestControllerAdvice 是 Spring Framework 为我们提供的一个复合注解,它是 @ControllerAdvice 和 @ResponseBody 的结合体。

@ControllerAdvice:该注解标志着一个类可以为所有的 @RequestMapping 处理方法提供通用的异常处理和数据绑定等增强功能。当应用到一个类上时,该类中定义的方法将在所有控制器类的请求处理链中生效。

@ResponseBody:表示方法的返回值将被直接写入 HTTP 响应体中,通常配合 Jackson 或 Gson 等 JSON 库将对象转换为 JSON 格式的响应。

因此,@RestControllerAdvice 就是专门为 RESTful 控制器设计的全局异常处理器,它的方法返回值将自动转换为响应体。

        同时在每个异常上面加@ExceptionHandler注解,进行函数重载接收异常,对于上面的sql异常,可以如下处理:

@ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        // Duplicate entry 'zhangsan' for key 'employee.idx_username'
        String message = ex.getMessage();
        if (message.contains("Duplicate entry")){
            String[] split = message.split(" ");
            String username = split[2];
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        }else {
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }

        需要注意的是尽量用常量来表示字符串,不要硬编码。通过以上处理,就可以在接受异常时返回一个Result,里面传入的就是异常信息msg。

创建修改人ID处理

        上面没有处理创建人和修改人的id,那该如何获取呢?

         这是前后端进行交互的大致流程,可以看到在拦截请求验证时,我们就可以读到jwt令牌中我们当时传入过的id了(之前在控制层实现的)

//登录成功后,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getAdminSecretKey(),
                jwtProperties.getAdminTtl(),
                claims);

        我们会在JwtTokenAdminInterceptor(注意要加上component才行)里根据获取的jwt进行拦截操作,显然可以在这里得到token里的id信息,但是要如何传入到业务层里呢,我们可以调用threadLocal方法,一次操作中的线程是同一个,里面的数据是连通的,为了方便起见,把threadLocal封装在common的context里,需要时进行调用:

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();
    }

}

        所以在校验以后获得empId,调用里面的set方法即可将id放入其中,同时在业务层里用get方法取出id即可。

员工分页查询

 

代码开发        

 根据接口文档,可以看出要接受的是Query参数,并不是json,所以不需要加@RequestBody,而因为传过来的只有那三个参数,所以我们特意封装出来一个类EmployeePageQueryDTO用来解决这个问题,看接口文档里要返回的数据里的data项,我们又设计一个pageResult类来封装:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

    private long total; //总记录数

    private List records; //当前页数据集合
}

        最后将这个对象封装到success中返回即可:

@GetMapping("/page")
    @ApiOperation("员工分页查询")
    public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
        log.info("员工分页查询,参数为:{}",employeePageQueryDTO);
        PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
        return Result.success(pageResult);
    }

             接下来,我们引入PageHelper依赖,进行分页查询的操作,传入页数和页面大小,调用mapper层的分页查询函数(已自动优化,返回的是一个Page<Employee>对象page),利用getTotal函数得到页数,getResult函数得到其他所有的信息(是一个list),最后利用PageResult的有参构造封装成能传入给success的对象。

/**
     * 分页查询
     *
     * @param employeePageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        //开始分页查询
        PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

        Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);

        long total = page.getTotal();
        List<Employee> records = page.getResult();

        return new PageResult(total, records);
    }

        接下来是Mapper层的逻辑,只需要模糊匹配且按照创建时间排序即可,无需自己计算页数之类的东西,以及limit方法,PageHelper会自动调整好

<select id="pageQuery" resultType="com.sky.entity.Employee">
        select * from employee
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
        </where>
        order by create_time desc
    </select>

 时间优化

        在进行测试时,我们肯能发现显示的时间并不是想要的那种格式(可能是Page的原因),在这里有两种处理方法,这里比较推荐第二种。

1. 设置@JsonFormat注解,可控制该属性在序列化为json时的字符串表示形式,缺点是每一个想要加的元素都需要一个这种注解。

//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;

2. 在配置类里面扩展SpringMVC框架的消息转换器,创建消息转换器对象,然后设置一个对象转换器(参数已经定义好了,在common里),最后将自己的消息加入到容器中,前面加0表示最优先。

/**
     * 扩展SpringMVC框架的消息转化器
     *
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,可以将java对象序列号为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转换器加入的容器中
        converters.add(0, converter);
    }

启用禁用员工账号

代码开发

        根据接口的信息,我们要传入一个路径参数status,在前面加入@PathVariable注解,和一个id参数用来作为判断判断员工的条件,因为是修改所以用Post提交:

/**
     * 启用禁用员工账号
     * @param id
     * @param status
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用员工账号")
    public Result startOrStop( @PathVariable Integer status,Long id) {
        log.info("启用禁用员工账号,{},{}",status,id);
        employeeService.startOrStop(status,id);
        return Result.success();
    }

        在Service层中,直接将id和status传给Mapper其实不太好,因为完全可以制作一个修改所有属性的动态sql,所以最好传入一个emp对象,可以用get/set方法,但是由于在emp上面加了一个@builder注解以可以用build方法

/**
     * 启用禁用员工账号
     * @param status
     * @param id
     */
    @Override
    public void startOrStop(Integer status, Long id) {
        //update employee set status = ? where id = ?

//        Employee employee = new Employee();
//        employee.setStatus(status);
//        employee.setId(id);

        Employee employee = Employee.builder()
                .status(status)
                .id(id)
                .build();
        employeeMapper.update(employee);
    }

         在Mapper层中,动态sql如下,set可以用<set>忽略逗号的错误;

<update id="update" parameterType="com.sky.entity.Employee">
        update employee
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="username != null">username = #{username},</if>
            <if test="password != null">password = #{password},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="sex != null">sex = #{sex},</if>
            <if test="idNumber != null">id_Number = #{idNumber},</if>
            <if test="updateTime != null">update_Time = #{updateTime},</if>
            <if test="updateUser != null">update_User = #{updateUser},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        where id = #{id}
    </update>

编辑员工

根据id查询员工信息

 

 代码开发

         这里主要是为了编辑员工时的信息回显,传入的是路径参数,记得加入path注解,返回的信息很多,所以用employee来接收

/**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询员工信息")
    public Result<Employee> getById(@PathVariable Long id){
        Employee employee = employeeService.getById(id);
        return Result.success(employee);
    }

        业务层接受id传入Mapper返回employee对象,但是要注意这里最好把密码给抹掉,否则可以通过f12来查看造成密码泄露,后面的Mapper层比较简单,select即可

/**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @Override
    public Employee getById(Long id) {
        Employee employee = employeeMapper.getById(id);
        employee.setPassword("****");
        return employee;
    }

编辑员工信息

         这里要更新参数选择PutMapping,同时传入的是一个实体DTO,由于前端传过来的是一个json,所以要加入@RequsetBody注解

 /**
     * 编辑员工信息
     * @param employeeDTO
     * @return
     */
    @PutMapping
    @ApiOperation("编辑员工信息")
    public Result update(@RequestBody EmployeeDTO employeeDTO){
        log.info("编辑员工信息,{}",employeeDTO);
        employeeService.update(employeeDTO);
        return Result.success();
    }

         业务层接受一个DTO,需要传递给Mapper的update函数,但是它只能接受employee对象,所以要转换一下,这里还是用那个拷贝方法,同时加入更新时间,和更新人id(这个用之前的方法,不做解释),最后调用上面创建的updat。

 @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);
    }

分类管理功能

        这里和上面的逻辑基本差不多,直接从文件夹里导入即可

公共字段自动填充

自定义注解AutoFill,用于标识需要进行公共字段自动填充的方法

自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值

在Mapper的方法上加入AutoFill注解

        首先自定义注解,注解二件套加上,同时注解里面有属性value,分别用枚举类update和insert表示,到时候用来区分注解。

/**
 * 自定义注解,用于标识某个方法需要自动填充处理
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //数据库操作类型:UPDATE INSERT
    OperationType value();
}

        自定义切面类,切面类上面要有@Aspect注解,@Component注解,为了写日志可以加一个Slf4j注解。定义一个切入点@PointCut,里面利用execution和annotation来找到要扫描的方法。因为要在sql之前加入时间和id之类的信息,所以用前置通知@Before,,传入joinpoint,分别得到方法签名对象,注解对象,注解参数对象,最后通过joinPoint.getArgs得到被拦截方法的参数,也就是emp对象,取出里面的第一个(虽然只有一个)。之后准备赋值的数据,根据不同的操作类型(update和insert分别选择方法的调用,也就是emp的get/set方法),分别选出对应的方法即可。

/**
 * 自定义切面,实现公共字段自动填充
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    //切入点
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    //前置通知,在通知中进行公共字段的赋值
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段自动填充...");

        //获取当前被拦截的方法上的数据库操作类型
        MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //方法签名对象 EmployeeMapper.update
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); //获得方法上的注解对象 AutoFill(value = UPDATE)
        OperationType operationType = autoFill.value(); //获取数据库操作类型 UPDATE

        //获取到当前被拦截的方法的参数--实体对象
        Object[] args = joinPoint.getArgs(); //返回一个长度为1的数组
        if (args == null || args.length == 0){
            return;
        }

        Object entity = args[0];

        //准备赋值的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        //根据不同操作类型,为对应的属性赋值
        if (operationType == OperationType.INSERT) {
            //为四个公共字段赋值
            try {
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值
                setCreateTime.invoke(entity,now);
                setCreateUser.invoke(entity,currentId);
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }

        } else if(operationType == OperationType.UPDATE) {
            //为两个公共字段赋值
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}

        最后在每个想要调用切面的方法加入AutoFill注解即可!

新增菜品

根据类型查询分类

 

/**
     * 根据类型查询分类
     * @param type
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据类型查询分类")
    public Result<List<Category>> list(Integer type){
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
<select id="list" resultType="com.sky.entity.Category">
        select * from category
        where status = 1
        <if test="type != null">
            and type = #{type}
        </if>
        order by sort asc,create_time desc
    </select>

文件上传

         首先要在yml里面配置阿里云oss的相关参数,这里不要直接在主yml里面赋值,而是要在dev里面加入,方便到时候换用户时将sping.profiles.active.dev改掉即可:

  alioss:
    endpoint: ${sky.alioss.endpoint}
    access-key-id: ${sky.alioss.access-key-id}
    access-key-secret: ${sky.alioss.access-key-secret}
    bucket-name: ${sky.alioss.bucket-name}

         紧接着要配置属性类,类似于jwt令牌,要有@Data注解(get/set方法),@Component注解(要变成bean),以及@ConfigurationProperties(prefix = "sky.alioss")

@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

}

         定义一个文件上传的工具类AliOssUtil,里面的属性就是上面这四个,同时定义一个upload方法,能够返回一个地址,点击这个网址就能够看到上传的文件了

@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();
    }
}

         但是我们这是一个springboot项目,必须要让这个工具类自动启动才好,所以这时候再定义一个配置类,用于创建AliOssUtil对象,配置类都要加入@Configuration注解来保证是个配置类,里面定义一个返回值为AliOssUtil的方法,传入的就是刚才定义的那个aliOssProperties(已经加了Component),然后利用有参构造函数返回一个对象即可,注意上面要加入@Bean注解,这样项目启动的时候就能将参数注入创建一个工具类对象,这里最好加一个@ConditionalOnMissingBean,保证整个spring容器最多只有一个util对象。

/**
 * 配置类,用于创建AliOssUtil对象
 */
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
        log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}

         最后,就可以定义上传文件的控制器了,根据接口文档,需要返回一个String里面记录了文件的请求路径。控制层传入的参数为文件的固定类型MultipartFile 制作一个新的文件名避免重复,利用util里的upload函数,传入文件数组和新的文件名,得到请求路径返回即可。

@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {

    @Autowired
    private AliOssUtil aliOssUtil;

    /**
     * 文件上传
     * @param file
     * @return
     */
    @ApiOperation("文件上传")
    @PostMapping("/upload")
    public Result<String> upload(MultipartFile file){
        log.info("文件上传:{}",file);

        try {
            //原始文件名
            String originalFilename = file.getOriginalFilename();
            //截取原神文件名的后缀 fsdf.png
            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.error("文件上传失败:{}",e);
        }
        return Result.error(MessageConstant.UPLOAD_FAILED);
    }
}

 新增菜品

         这里面有要处理两张表的数据:菜品表和口味表,两张表通过逻辑外键进行连接

         首先编写控制层,。传入的事dishDTO数据(包含原有的dish参数外加了一个口味列表flavors),因为这里是改变数据所以不需要Result的泛型。

/**
 * 菜品管理
 */
@RestController
@RequestMapping("/admin/dish")
@Slf4j
@Api(tags = "菜品相关接口")
public class DishController {

    @Autowired
    private DishService dishService;

    /**
     * 新增菜品
     * @param dishDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}",dishDTO);
        dishService.saveWithFlavor(dishDTO);

        return Result.success();
    }
}

        在业务层里,我们分两块来处理,一部分是向菜品表插入一个数据,还有就是向口味表插入n条数据,这两项必须同时提交,所以形成了一个事物,方法上面加入@Transaction注解。

1 向菜品表插入一条数据

        因为控制层传入的是DTO,我们不需要flavor参数,所以创建一个dish对象传到DIshMapper层中,因为是插入,所以加入前面的@AutoFill注解,在xml映射文件里面进行insert操作即可,在这里要进行一下逐渐返回,将主键的值传回给id,后面会用到。

<insert id="insert" parameterType="com.sky.entity.Dish" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
         VALUES
        (#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})

    </insert>

 2 向口味表插入n条数据

        取出前端传过来的dishDTO,取出里面的flavor属性,因为dishId并不是自增而是和菜品表的id逻辑外键,所以需要自行加入,刚才通过主键返回取出了dish的id赋值给dishId,之后进行判断前端传入的flavors是否为空,如果不空,就为List<DishFlavor> flavors里的每一个id进行赋值,接下来批量注入剩余的flavor信息,通过<foreach>,依次为每一个DishFlavor进行插入赋值,collection为list名,item为形参对象,separator为分割符,这样就插入了所有的数据。

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

总体新增菜品的代码如下:

@Service
@Slf4j
public class DishServiceImpl implements DishService {

    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;

    /**
     * 新增菜品和对应的口味数据
     * @param dishDTO
     */
    @Transactional
    @Override
    public void saveWithFlavor(DishDTO dishDTO) {

        //DTO里面还有口味,没必要,所以传入一个Dish对象
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);

        //1 向菜品表插入一条数据
        dishMapper.insert(dish);

        //获取insert语句生成的主键值
        Long dishId = dish.getId();

        //2 向口味表插入n条数据
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors!=null && !flavors.isEmpty()){
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            dishFlavorMapper.insertBatch(flavors);
        }
    }
}

菜品分页查询

 代码开发

        在控制层中,传入的是一个DTO,里面包含了前端传入的数据,因为是Query,也就是地址栏问号传参,所以传过来的并不是json格式,所以不需要加body注解,返回的类型是一个PageResult格式。

/**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    @ApiOperation("菜品分页查询")
    @GetMapping("/page")
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
        log.info("菜品分页查询:{}",dishPageQueryDTO);
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
        return Result.success(pageResult);
    }

        业务层还是之前的分页查询逻辑,注意Page的泛型(也就是要返回前端的类型)是VO类型,因为还要显示菜品的分类,而普通的dish里面并没有。

/**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
        PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
        return new PageResult(page.getTotal(),page.getResult());
    }

         在XML映射文件里面书写动态sql,进行多表查询,因为每个表都有name,避免重复将category里的name重命名一下,之后进行匹配即可。

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

删除菜品

需求分析和设计

 起售中的菜品不能删除,被套餐关联也不能删除,删除菜品后关联的口味数据也删除

代码开发

        可以传入一个Long类型的列表,到时候springMVC会自动进行处理里面的元素(要加入@RequsetParam)

 /**
     * 菜品批量删除
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("菜品批量删除")
    public Result delete(@RequestParam List<Long> ids){
        log.info("菜品批量删除:{}",ids);
        dishService.deleteBatch(ids);
        return Result.success();
    }

        在业务层中,先要判断菜品是否能够删除,首先,如果起售,那么就不可以删除,遍历传入的菜品id列表,调用Mapper层中的方法,返回菜品,如果菜品的状态是起售,那么就抛出异常

//判断当前菜品是否能够删除--是否存在起售中的菜品??
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);
            if (Objects.equals(dish.getStatus(), StatusConstant.ENABLE)) {
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }
/**
     * 根据主键查询菜品
     * @param id
     * @return
     */
    @Select("select * from dish where id = #{id}")
    Dish getById(Long id);

        再判断一下菜品是否绑定了套餐,这里用setmealDishMapper.getSetmealIdsByDishids(ids)返回一个列表了里面装的都是setmeal_id,如果这些菜品里面找到了setmeal_id就说明有关联,抛出异常。

//断当前菜品是否能够删除--是否被套餐关联??
        List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishids(ids);
        if (setmealIds != null && !setmealIds.isEmpty()) {
            //当前菜品被套餐关联了
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }

        排除了以上的情况,就可以安全的删除菜品数据了,可以遍历取出所有的id,然后进行删除,同时也要把口味给删除

//删除菜品表中的菜品数据
//        for (Long id : ids) {
//            dishMapper.deleteById(id);
//            //删除菜品关联的口味数据
//            dishFlavorMapper.deleteByDishId(id);
//        }
    /**
     * 根据主键删除id
     * @param id
     */
    @Delete("delete from dish where id = #{id}")
    void deleteById(Long id);
/**
     * 根据菜品id来删除对应的口味数据
     * @param dishId
     */
    @Delete("delete from dish_flavor where dish_id = #{dishID}")
    void deleteByDishId(Long dishId);

代码优化

        最后删除菜品数据时,要进行遍历取出菜品,进行一次sql删除,如果数量过多,一定会对性能产生影响,所以我们直接每次用一条sql语句,传入的是ids。

    <delete id="deleteByIds">
        delete from dish where id in
        <foreach collection="ids" open="(" close=")" item="id">
            #{id}
        </foreach>
    </delete>
 <delete id="deleteByDishIds">
        delete from dish_flavor where dish_id in
        <foreach collection="dishIds" item="dishId" open="(" close=")">
            #{dishId}
        </foreach>
    </delete>

修改菜品

需求分析和设计

 

根据Id查询菜品

        传入的是路径参数所以使用path注解,返回的是VO对象

    /**
     * 根据id查询菜品
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询菜品")
    public Result<DishVO> getById(@PathVariable Long id){
        log.info("根据id查询菜品:{}",id);
        DishVO dishVO = dishService.getByIdWithFlavor(id);
        return Result.success(dishVO);
    }

        分别根据id取出dish的信息以及dishFlavors的信息,将所有的信息封装到dishVO对象中,注意此时其实并没有类别信息对象,但是有类别id,这一点由前端实现了。

@Override
    public DishVO getByIdWithFlavor(Long id) {
        //根据id查询菜品数据
        Dish dish = dishMapper.getById(id);

        //根据菜品id查询口味数据
        List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);

        //将查询到的数据封装到dishVO
        DishVO dishVO = new DishVO();
        BeanUtils.copyProperties(dish, dishVO);
        dishVO.setFlavors(dishFlavors);
        return dishVO;
    }
/**
     * 根据主键查询菜品
     * @param id
     * @return
     */
    @Select("select * from dish where id = #{id}")
    Dish getById(Long id);
/**
     * 根据菜品id查询对应的口味数据
     * @param dishId
     * @return
     */
    @Select("select * from dish_flavor where dish_id = #{dishId}")
    List<DishFlavor> getByDishId(Long dishId);

 修改菜品

        传入的是JSON数据,所以要加body注解,由于是要修改所以Result并不需要泛型。

/**
     * 修改菜品
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO){
        log.info("修改菜品;{}",dishDTO);
        dishService.updateWithFlavor(dishDTO);
        return Result.success();
    }

         由于传入的是DTO,但是我们并不需要这些信息,所以将他转换成dish会更好,修改菜品分为两步,一个是修改基本信息,一个是修改口味,基本信息比较简单,修改口味分为两步,删除之前所有口味之后再重新插入口味。

/**
     * 根据id修改菜品和口味信息
     * @param dishDTO
     */
    @Override
    public void updateWithFlavor(DishDTO dishDTO) {
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);
        //修改菜品表基本信息
        dishMapper.update(dish);

        //删除原有的口味数据
        dishFlavorMapper.deleteByDishId(dishDTO.getId());

        //重新插入口味数据
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors!=null && !flavors.isEmpty()){
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishDTO.getId());
            });
            dishFlavorMapper.insertBatch(flavors);
        }
<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>
/**
     * 根据菜品id来删除对应的口味数据
     * @param dishId
     */
    @Delete("delete from dish_flavor where dish_id = #{dishID}")
    void deleteByDishId(Long dishId);
    <insert id="insertBatch">
        insert into dish_flavor (dish_id, name, value) VALUES
        <foreach collection="flavors" item="df" separator=",">
            (#{df.dishId},#{df.name},#{df.value})
        </foreach>
    </insert>

 启用禁用菜品

和之前类似,不过多赘述:

/**
     * 启用、禁用菜品
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用分类")
    public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
        dishService.startOrStop(status,id);
        return Result.success();
    }
/**
     * 启用、禁用菜品
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        Dish dish = Dish.builder()
                .status(status)
                .id(id)
                .build();
        dishMapper.update(dish);
    }

新增套餐

需求分析和设计

接口设计(共涉及到4个接口):

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

  • 根据分类id查询菜品

  • 图片上传(已完成)

  • 新增套餐

 根据分类id查询菜品

        这里是要在新增套餐的时候,通过选择分类,在里面显示出能够添加的菜品,返回结果是一个菜品列表,效果如下:

/**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<Dish>> list(Long categoryId) {
        log.info("根据分类id:{} 查询菜品",categoryId);
        List<Dish> list = dishService.list(categoryId);
        return Result.success(list);
    }

        业务层接收的是分类id,但是最好把他封装成菜品对象,传入套餐id和状态信息交给数据层,这样后面也可以根据菜品名来进行查询了。

/**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    @Override
    public List<Dish> list(Long categoryId) {
        Dish dish = Dish.builder()
                .categoryId(categoryId)
                .status(StatusConstant.ENABLE)
                .build();
        return dishMapper.list(dish);
    }

        数据层通过动态sql在dish表里面查找相应的菜品:

 <select id="list" resultType="com.sky.entity.Dish">
        select * from dish
        <where>
            <if test="name != null">
                and name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
        order by create_time desc
    </select>

新增套餐

        创建一套新的控制器,传入的是setmealJson数据,加入body注解

/*
套餐管理
 */
@Slf4j
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@RestController
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    /**
     * 新增套餐
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        log.info("新增套餐");
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }
}

        在业务层里,将setmealDTO里的数据传入到setmeal里(DTO里面多了一List<SetmealDish> setmealDishes 用来表示套餐和菜品之间的联系),之后向套餐表里插入数据,加入AutoFill注解

 <insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
        insert into setmeal
        (category_id, name, price, description, image, create_time, update_time, create_user, update_user)
        VALUES
        (#{categoryId},#{name},#{price},#{description},#{image},#{createTime},#{updateTime},#{createUser},#{updateUser})
    </insert>

        通过主键返回获取生成的套餐id传入给套餐菜品关联属性的套餐id,这样套餐和菜品的id就能够对应上,最后保存套餐和菜品之间的关联关系

<insert id="insertBatch">
        insert into setmeal_dish
        (setmeal_id, dish_id, name, price, copies)
        VALUES
        <foreach collection="setmealDishes" item="sd" separator=",">
            (#{sd.setmealId}, #{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
        </foreach>
    </insert>
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

    @Autowired
    private SetmealMapper setmealMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private DishMapper dishMapper;



    /**
     * 新增套餐同时需要保存套餐和菜品的关联关系
     * @param setmealDTO
     */
    @Override
    public void saveWithDish(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO,setmeal);

        //向套餐表插入数据
        setmealMapper.insert(setmeal);

        //获取生成的套餐id
        Long setmealId = setmeal.getId();

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });

        //保存套餐和菜品的关联关系
        setmealDishMapper.insertBatch(setmealDishes);
    }
}

套餐分页查询

需求分析和设计

 代码开发

与前面的分页查询其实类似,这里不过多赘述

/**
     * 分页查询
     * @param setmealPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分页查询")
    public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
        log.info("分页查询:{}",setmealPageQueryDTO);
        PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
        return Result.success(pageResult);
    }
/**
     * 分页查询
     * @param setmealPageQueryDTO
     * @return
     */
    public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
        PageHelper.startPage(setmealPageQueryDTO.getPage(),setmealPageQueryDTO.getPageSize());
        Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
        return new PageResult(page.getTotal(),page.getResult());
    }
<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>

删除套餐

需求和业务分析

 

         控制层里需要加入@RequestParam注解,以确保spring能够正确的解析传入的id列表

/**
     * 批量删除套餐
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除套餐")
    public Result delete(@RequestParam List<Long> ids) {
        setmealService.deleteBatch(ids);
        return Result.success();
    }

        业务层负责删除套餐,如果起售,则不能删除,遍历套餐表(之前用的for循环,这里用的foreach,其实差不多),根据id找到每一个套餐,根据状态来判断是否能删除,之后就可以分别删除套餐表和套餐菜品关系表中的数据了。

/**
     * 批量删除套餐
     * @param ids
     * @return
     */
    public void deleteBatch(List<Long> ids) {
        ids.forEach(id -> {
            Setmeal setmeal = setmealMapper.getById(id);
            if (setmeal.getStatus().equals(StatusConstant.ENABLE)) {
                //起售中的菜品不能删除
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }
        });

        ids.forEach(setmealId -> {
            //删除套餐表中的数据
            setmealMapper.deleteById(setmealId);

            //删除套餐菜品关系表中的数据
            setmealDishMapper.deleteBySetmealId(setmealId);
        });
    }
//SetmealMapper
/**
     * 根据id查询套餐
     * @param id
     * @return
     */
    @Select("select * from setmeal where id = #{id}")
    Setmeal getById(Long id);

    /**
     * 根据id删除套餐
     * @param id
     */
    @Delete("delete from setmeal where id = #{id}")
    void deleteById(Long id);
//SetmealDishMapper
/**
     * 根据套餐id删除套菜和菜品的关联关系
     * @param setmealId
     */
    @Delete("delete from setmeal_dish where setmeal_id = #{setmealId}")
    void deleteBySetmealId(Long setmealId);

修改套餐

需求分析和设计

  • 根据id查询套餐

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

  • 根据分类id查询菜品(已完成)

  • 图片上传(已完成)

  • 修改套餐

 根据Id查询套餐

点击修改套餐后,会什么都没有,要在页面回显出以下效果:

/**
     * 根据id查询套餐
     * @param id
     * @return
     */
    @ApiOperation("根据id查询套餐")
    @GetMapping("/{id}")
    public Result<SetmealVO> getById(@PathVariable Long id) {
        SetmealVO setmealVO = setmealService.getByIdWithDish(id);
        return Result.success(setmealVO);
    }

         业务层里,首先根据id得到对应的套餐,之后根据id得到套餐菜品关系表里面的数据,建立一个要返回的VO对象,分别吧套餐数据和关系表的数据传入进去再返回即可。

/**
     * 根据id查询套餐和套餐菜品关系
     * @param id
     * @return
     */
    public SetmealVO getByIdWithDish(Long id) {
        Setmeal setmeal = setmealMapper.getById(id);
        List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(id);

        SetmealVO setmealVO = new SetmealVO();
        BeanUtils.copyProperties(setmeal,setmealVO);
        setmealVO.setSetmealDishes(setmealDishes);

        return setmealVO;
    }

修改套餐

        控制层中传入body对象:

/**
     * 修改套餐
     * @param setmealDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改套餐")
    public Result update(@RequestBody SetmealDTO setmealDTO) {
        setmealService.update(setmealDTO);
        return Result.success();
    }

        业务层里逻辑比较多,首先要将传入的DTO变回setmeal,利用update传入setmeal的基本数据,之后删除套餐和菜品的关联关系,再将新的关联信息一个一个的存入到setmealdisher里,最后进行批量的插入即可。

/**
     * 修改套餐
     * @param setmealDTO
     * @return
     */
    public void update(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO,setmeal);

        //修改套餐表,执行update,插入基本数据
        setmealMapper.update(setmeal);

        //删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
        setmealDishMapper.deleteBySetmealId(setmealDTO.getId());

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealDTO.getId());
        });

        //重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
        setmealDishMapper.insertBatch(setmealDishes);


    }

起售停售套餐

需求分析和设计

 

 代码开发

        与之前的起售停售相比,多了一个包含禁售菜品不能启用套餐的规定,,从套餐中拿出所有的菜品,如果菜品的状态是0,那么就得抛异常了

/**
     * 启用、禁用套餐
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用套餐")
    public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
        setmealService.startOrStop(status,id);
        return Result.success();
    }
/**
     * 起售禁售套餐
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        //起售套餐时如果里面有停售菜品,就要抛出异常
        if (status.equals(StatusConstant.ENABLE)) {
            List<Dish> dishList = dishMapper.getBySetmealId(id);
            if (dishList != null && dishList.size() > 0) {
                dishList.forEach(dish -> {
                    if (dish.getStatus().equals(StatusConstant.DISABLE)) {
                        throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
                    }
                });
            }
        }

        Setmeal setmeal = Setmeal.builder()
                .id(id)
                .status(status)
                .build();
        setmealMapper.update(setmeal);
    }
/**
     * 根据套餐id查询菜品
     * @param setmealId
     * @return
     */
    @Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
    List<Dish> getBySetmealId(Long setmealId);

Redis

Spring Date Redis使用方式

1 导入sdr的maven坐标

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2 配置redis数据源

spring:  
    redis:
        host: localhost
        port: 6379
        password: 123456
        database: 0

3 编写配置类,创建RedisTemplate对象

@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        log.info("开始创建redis模版对象");
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

4 通过RedisTemplate对象操作Redis

@SpringBootTest
public class SpringDataRedisTest {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testRedisTemplate() {
        System.out.println(redisTemplate);
    }
}

店铺营业状态设置

需求分析和设计

 

 代码开发

        由于店铺的营业状态只有营业中和打样中,没有必要创建mysql表格,这里利用redis缓存来实现,直接注入RedisTemplate即可

@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {

    public static final String KEY = "SHOP_STATUS";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 设置店铺的营业状态
     * @param status
     * @return
     */
    @PutMapping("/{status}")
    @ApiOperation("设置店铺的营业状态")
    public Result setStatus(@PathVariable Integer status) {
        log.info("设置店铺的营业状态为:{}",status == 1 ? "营业中":"打样中");
        redisTemplate.opsForValue().set(KEY,status);
        return Result.success();
    }

    /**
     * 获取店铺的营业状态
     * @return
     */
    @GetMapping("/status")
    @ApiOperation("获取店铺的营业状态")
    public Result<Integer> getStatus() {
        Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("获取到店铺的营业状态为:{}",status == 1 ? "营业中":"打样中");
        return Result.success(status);
    }
}

         用户端的代码和第二段代码基本一样,唯一需要注意的就是两个Controller的名字最好不要一样,否则bean会重复,这里重新命名。

@RestController("userShopController")
@RequestMapping("/user/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {

    public static final String KEY = "SHOP_STATUS";

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 获取店铺的营业状态
     * @return
     */
    @GetMapping("/status")
    @ApiOperation("获取店铺的营业状态")
    public Result<Integer> getStatus() {
        Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("获取到店铺的营业状态为:{}",status == 1 ? "营业中":"打样中");
        return Result.success(status);
    }
}

接口文档优化

        现在的管理层和用户层的接口文档放在了一起不好区分,所以在配置时要去分开,主要就是两个url里进行了区分,同时加了一个groupName建立名字。

@Bean
    public Docket docket1() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("管理端接口")
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

    @Bean
    public Docket docket2() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户端接口")
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

HTTPClient

@SpringBootTest
public class HttpClientTest {
    /**
     * 通过Httpsclient发送get请求
     */
    
    @Test
    public void testGet() throws IOException {
        //创建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();
    }
/**
     * 通过Httpsclient发送post请求
     */
    @Test
    public void testPOST() throws IOException {
        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();
    }

微信小程序

         总得来说,小程序通过wx.login获取code,并发送给后端,后端将四个数据发送给微信接口服务,返回一些数据,其中最重要的就是openid,后端将token之类的数据返回给小程序,这时两端就可以进行连通了。

 登录功能

需求分析和设计

代码开发

配置文件:

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
    user-secret-key: itheima
    user-ttl: 7200000
    user-token-name: authentication
  alioss:
    endpoint: ${sky.alioss.endpoint}
    access-key-id: ${sky.alioss.access-key-id}
    access-key-secret: ${sky.alioss.access-key-secret}
    bucket-name: ${sky.alioss.bucket-name}
  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}

        表现层接收小程序端传过来的DTO数据(其实里面只有一个code),调用业务层的login返回一个user对象,之后为这个微信用户生成一个jwt令牌,传入这个用户在user数据库里的id,封装成一个token,把所有信息封装成一个userVO对象,返回给小程序端。

@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;
    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation("微信登录")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
        log.info("微信用户登录:{}",userLoginDTO.getCode());

        //微信登录
        User user = userService.wxLogin(userLoginDTO);

        //为微信用户生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID,user.getId());
        String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);

        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())
                .openid(user.getOpenid())
                .token(token)
                .build();

        return Result.success(userLoginVO);
    }

}

        在业务层里,调用微信接口服务获取当前用户的openid,通过httpclientutil来发送请求,传入四个数据,得到一个json里面包含着openid,解析出来。如果openid为空则抛出异常,之后判断是否为新用户,如果是新用户,自动完成注册。

@Service
public class UserServiceImpl implements UserService {

    public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";
    @Autowired
    private WeChatProperties weChatProperties;
    @Autowired
    private UserMapper userMapper;

    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    public User wxLogin(UserLoginDTO userLoginDTO) {

        String openid = getOpenid(userLoginDTO.getCode());

        //判断openid是否为空,如果为空登录失败抛出业务异常
        if (openid == null) {
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }

        //判断当前用户是否为新用户
        User user = userMapper.getByOpenid(openid);

        //如果是新用户,自动完成注册
        if (user == null) {
            user =  User.builder()
                    .openid(openid)
                    .createTime(LocalDateTime.now())
                    .build();
            userMapper.insert(user);
        }

        //返回用户对象
        return user;
    }

    /**
     * 调用微信接口服务,获取微信用户的openid
     * @param code
     * @return
     */
    private String getOpenid(String code) {
        //调用微信接口服务获得当前用户的openid
        //通过httpclient向微信地址发送请求
        Map<String, String> map = new HashMap<>();
        map.put("appid",weChatProperties.getAppid());
        map.put("secret",weChatProperties.getSecret());
        map.put("js_code",code);
        map.put("grant_type","authorization_code");

        String json = HttpClientUtil.doGet(WX_LOGIN, map);

        JSONObject jsonObject = JSON.parseObject(json);
        String openid = jsonObject.getString("openid");
        return openid;
    }

}

        数据层里比较简单,但是注意要进行一下主键返回,代码如下:

@Mapper
public interface UserMapper {

    /**
     * 根据openid查询用户
     * @param openid
     * @return
     */
    @Select("select * from user where openid = #{openid}")
    User getByOpenid(String openid);


    /**
     * 插入数据
     * @param user
     */
    void insert(User user);
}
<insert id="insert"  useGeneratedKeys="true" keyProperty="id">
        insert into user
        (openid, name, phone, sex, id_number, avatar, create_time)
        VALUES
            (#{openid},#{name},#{phone},#{sex},#{idNumber},#{avatar},#{createTime})
    </insert>

  拦截器更新

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getUserTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            log.info("当前用户id:{}", userId);
            BaseContext.setCurrentId(userId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}
@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");
    }

商品浏览

需求分析和设计

 

 代码导入

这里和前面基本类似,导入这些代码即可。

 缓存菜品

实现思路

         每个分类下的菜品保存一份缓存数据:key:分类id value:菜品集合字符串

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

         因为加入到了缓存中,所以更新操作要保持同步,包括新增菜品,修改菜品,批量删除菜品,起售停售菜品

        首先是user的表现层里,在查询sql之前先查询缓存,空则继续sql,之后再存进去,不空则查询缓存

/**
     * 根据分类id查询菜品
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {
        //构造redis中的key dish_id
        String key = "dish_"+categoryId;

        //查询redis中是否存在菜品数据
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);

        //如果存在,直接返回,无序查询数据库
        if (list != null && !list.isEmpty()) {
            return Result.success(list);
        }

        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

        //如果不存在,查询数据库,将查询到的数据放入到redis中
        list = dishService.listWithFlavor(dish);

        //将数据重新放到redis中
        redisTemplate.opsForValue().set(key,list);

        return Result.success(list);
    }

       之后是admin的表现层,每次进行crud之前都要进行相应的缓存处理:

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

缓存套餐

Spring Cache

添加购物车

需求分析和设计

代码开发         

表现层里传入shoppingcartDTO,调用业务层的addShoppingCart。

@RestController
@Slf4j
@RequestMapping("/user/shoppingCart")
@Api(tags = "C端购物车相关接口")
public class ShoppingCartController {

    @Autowired
    private ShoppingCartService shoppingCartService;


    /**
     * 添加购物车
     * @param shoppingCartDTO
     * @return
     */
    @PostMapping("/add")
    @ApiOperation("添加购物车")
    public Result  add(@RequestBody ShoppingCartDTO shoppingCartDTO) {
        log.info("添加购物车,商品信息为:{}",shoppingCartDTO);
        shoppingCartService.addShoppingCart(shoppingCartDTO);
        return Result.success();
    }
}

        业务层里面主要有三个比较重要的逻辑:1判断当前加入购物车的商品是否已经存在,2如果已存在,数量加一,3不存在插入一条购物车数据。

        将DTO对象转换成Shoppingcart对象以后,再去数据库里面找看是否存在,注意要额外注入userID。如果存在,取出数据加以后更新。如果不存在,就要看加入的是菜品还是套餐数据,主要看能不能取到对应的id,之后进行添加即可。

@Service
@Slf4j
public class ShoppingCartServiceImpl implements ShoppingCartService {

    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 添加购物车
     * @param shoppingCartDTO
     */
    public void addShoppingCart(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 cart = list.get(0);
            cart.setNumber(cart.getNumber() + 1);
            shoppingCartMapper.updateNumberById(cart);
        } else {
            //不存在,插入一条购物车数据
            //判断本次添加到购物车的是菜品还是套餐
            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 = shoppingCartDTO.getSetmealId();
                Setmeal setmeal = setmealMapper.getById(setmealId);

                shoppingCart.setName(setmeal.getName());
                shoppingCart.setImage(setmeal.getImage());
                shoppingCart.setAmount(setmeal.getPrice());
            }
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartMapper.insert(shoppingCart);
        }
    }
}

        数据层代码如下

@Mapper
public interface ShoppingCartMapper {

    /**
     * 动态条件查询
     * @param shoppingCart
     * @return
     */
    List<ShoppingCart> list(ShoppingCart shoppingCart);


    /**
     * 根据id修改商品数量
     * @param shoppingCart
     */
    @Update("update shopping_cart set number = #{number} where id = #{id}")
    void updateNumberById(ShoppingCart shoppingCart);

    /**
     * 插入购物车数据
     * @param shoppingCart
     */
    @Insert("insert into shopping_cart (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time) " +
            "values (#{name},#{image},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{createTime})")
    void insert(ShoppingCart shoppingCart);
}
<mapper namespace="com.sky.mapper.ShoppingCartMapper">

    <select id="list" resultType="com.sky.entity.ShoppingCart">
        select * from shopping_cart
        <where>
            <if test="userId != null">and user_id = #{userId}</if>
            <if test="setmealId != null">and setmeal_id = #{setmealId}</if>
            <if test="dishId != null">and dish_id = #{dishId}</if>
            <if test="dishFlavor != null">and dish_flavor = #{dishFlavor}</if>
        </where>
    </select>
</mapper>

查看购物车

需求分析和设计

 代码开发

        表现层如下

/**
     * 查看购物车
     * @return
     */
    @ApiOperation("查看购物车")
    @GetMapping("/list")
    public Result<List<ShoppingCart>> list(){
        List<ShoppingCart> list = shoppingCartService.showShoppingCart();
        return Result.success(list);
    }

        业务层里,主要是根据userId来封装一个购物车对象传给Mapper的list中

 /**
     * 查看购物车
     * @return
     */
    public List<ShoppingCart> showShoppingCart() {
        //获取到当前微信用户的id
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = ShoppingCart.builder()
                .userId(userId)
                .build();
        return shoppingCartMapper.list(shoppingCart);
    }

清空购物车

需求分析和设计

 代码开发

只要把关于这个userId的购物车数据全部清空即可:

/**
     * 清空购物车
     * @return
     */
    @DeleteMapping("/clean")
    @ApiOperation("清空购物车")
    public Result clean() {
        shoppingCartService.cleanShoppingCart();
        return Result.success();
    }
/**
     * 清空购物车
     */
    public void cleanShoppingCart() {
        Long userId = BaseContext.getCurrentId();
        shoppingCartMapper.deleteByUserId(userId);
    }
/**
     * 根据userid删除购物车数据
     */
    @Delete("delete from shopping_cart where user_id = #{userId}")
    void deleteByUserId(Long userId);

删除购物车中的一件商品

        代码和添加购物车十分类似,只不过如果数量为1则删除,数量不是1则改数。如下所示:

/**
 * 删除购物车中的一个商品
 * @param shoppingCartDTO
 * @return
 */
@PostMapping("/sub")
@ApiOperation("删除购物车中的一个商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO) {
    log.info("删除购物车中的一个商品,商品信息为:{}",shoppingCartDTO);
    shoppingCartService.subShoppingCart(shoppingCartDTO);
    return Result.success();
}
  /**
     * 删除购物车中的一个商品
     * @param shoppingCartDTO
     * @return
     */
    public void subShoppingCart(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 = list.get(0);
            if (shoppingCart.getNumber() == 1) {
                //数量为1直接删除
                shoppingCartMapper.deleteById(shoppingCart.getId());
            } else {
                //不唯一修改份数
                shoppingCart.setNumber(shoppingCart.getNumber()-1);
                shoppingCartMapper.updateNumberById(shoppingCart);
            }
        }
    }
/**
     * 根据id删除购物车数据
     * @param id
     */
    @Delete("delete from shopping_cart where id = #{id}")
    void deleteById(Long id);
}

地址簿模块开发

需求分析和设计

 

 

代码导入

用户下单

需求分析和设计

代码开发        

 表现层比较简单,只需要传入DTO返回VO即可

@RestController("userOrderController")
@RequestMapping("/user/order")
@Api(tags = "用户端订单相关接口")
@Slf4j
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 用户下单
     * @param ordersSubmitDTO
     * @return
     */
    @PostMapping("/submit")
    @ApiOperation("用户下单")
    public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO){
        log.info("用户下单:参数为:{}",ordersSubmitDTO);
        OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);
        return Result.success(orderSubmitVO);
    }
}

        业务层里,首先要处理各种异常(其实在小程序里面并不会出现,但是在调试过程中可能会出现问题),首先是地址簿为空,之后是购物车数据是否为空。如果都不为空,就可以分别往订单表和订单明细表里面插数据了,前者copy了DTO的数据以后还要额外加入一些,后者遍历出购物车的每一条数据加进去。最后通过VO返回结果

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderDetailMapper orderDetailMapper;
    @Autowired
    private AddressBookMapper addressBookMapper;
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;

    /**
     * 用户下单
     * @param ordersSubmitDTO
     * @return
     */
    @Transactional
    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.isEmpty()) {
            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);


        List<OrderDetail> orderDetailList = new ArrayList<>();
        //向订单明细表插入n条数据
        for (ShoppingCart cart : shoppingCartList) {
            OrderDetail orderDetail = new OrderDetail(); //订单明细
            BeanUtils.copyProperties(cart,orderDetail);
            orderDetail.setOrderId(orders.getId()); //设置当前订单明细关联的订单id
            orderDetailList.add(orderDetail);
        }

        orderDetailMapper.insertBatch(orderDetailList);
        //清空用户的购物车数据
        shoppingCartMapper.deleteByUserId(userId);

        //封装VO返回结果
        OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
                .id(orders.getId())
                .orderTime(orders.getOrderTime())
                .orderNumber(orders.getNumber())
                .orderAmount(orders.getAmount())
                .build();
        return orderSubmitVO;
    }
}

数据层代码如下;

<mapper namespace="com.sky.mapper.OrderDetailMapper">


    <insert id="insertBatch">
        insert into order_detail (name, image, order_id, dish_id, setmeal_id, dish_flavor, amount,number)
        values
        <foreach collection="orderDetailList" item="od" separator=",">
            (#{od.name},#{od.image},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},#{od.amount},#{od.number})
        </foreach>
    </insert>
</mapper>
<mapper namespace="com.sky.mapper.OrderMapper">

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into orders (number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status,
                            amount, remark, phone, address, user_name, consignee, cancel_reason, rejection_reason,
                            cancel_time, estimated_delivery_time, delivery_status, delivery_time, pack_amount,
                            tableware_number, tableware_status)
        values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
                #{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{userName}, #{consignee}, #{cancelReason},
                #{rejectionReason},
                #{cancelTime}, #{estimatedDeliveryTime}, #{deliveryStatus}, #{deliveryTime}, #{packAmount},
                #{tablewareNumber}, #{tablewareStatus})
    </insert>
</mapper>

微信支付

微信支付介绍

 

代码导入 

代码不需要写,导入即可。

         但是由于自己不是商户,所以并不能支付订单,所以把代码改变一下:表现层不变:

/**
     * 订单支付
     *
     * @param ordersPaymentDTO
     * @return
     */
    @PutMapping("/payment")
    @ApiOperation("订单支付")
    public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        log.info("订单支付:{}", ordersPaymentDTO);
        OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
        log.info("生成预支付交易单:{}", orderPaymentVO);
        return Result.success(orderPaymentVO);
    }

        业务层代码进行修改:首先要定义一个全局变量order来获取order的id,否则很困难,在如下页面点击去支付后就会调用submitOrder方法,将订单数据写入数据库,所以可以在submitOrder方法中获取订单的id。json那几行是为了能够得到一个返回的数据来欺骗微信支付,后面是将orders表中的数据直接变成支付完以后的样子。

 public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();
        User user = userMapper.getById(userId);
/*        //调用微信支付接口,生成预支付交易单
        JSONObject jsonObject = weChatPayUtil.pay(
                ordersPaymentDTO.getOrderNumber(), //商户订单号
                new BigDecimal(0.01), //支付金额,单位 元
                "苍穹外卖订单", //商品描述
                user.getOpenid() //微信用户的openid
        );
 
        if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
            throw new OrderBusinessException("该订单已支付");
        }
*/
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code","ORDERPAID");
        OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
        vo.setPackageStr(jsonObject.getString("package"));

        Integer OrderPaidStatus = Orders.PAID;//支付状态,已支付
        Integer OrderStatus = Orders.TO_BE_CONFIRMED;  //订单状态,待接单
        LocalDateTime check_out_time = LocalDateTime.now();//更新支付时间
        orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time, this.orders.getId());
        return vo;
    }

查询历史订单

        分页查询历史订单,根据订单状态查询,展示订单数据时,需要展示的数据包括:下单时间,订单状态,订单金额,订单明细。

        表现层,传入pageNum,pagesize和状态信息,调用pageQueryUser

/**
     * 历史订单查询
     *
     * @param page
     * @param pageSize
     * @param status   订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
     * @return
     */
    @GetMapping("/historyOrders")
    @ApiOperation("历史订单查询")
    public Result<PageResult> page(int page, int pageSize, Integer status){
        PageResult pageResult = orderService.pageQuery4User(page,pageSize,status);
        return Result.success(pageResult);
    }

        业务层首先进行分页的基本操作,之后将用户的id和状态封装在OrderPageQueryDTO里,利用分页条件查询得到Page<Orders>,从里面的每一个orders得到订单id,查询订单明细,封装在orderVO里面最后按照格式返回即可

/**
     * 用户订单分页查询
     * @param pageNum
     * @param pageSize
     * @param status
     * @return
     */
    public PageResult pageQueryUser(int pageNum, int pageSize, Integer status) {
        //设置分页
        PageHelper.startPage(pageNum,pageSize);

        OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO();
        ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());
        ordersPageQueryDTO.setStatus(status);

        //分页条件查询
        Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

        List<OrderVO> list = new ArrayList<>();

        //查询出订单明细,并封装入OrderVO进行响应
        if (page != null && page.getTotal() > 0) {
            for (Orders orders : page) {
                Long orderId = orders.getId(); //订单id

                //查询订单明细
                List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId);

                OrderVO orderVO = new OrderVO();
                BeanUtils.copyProperties(orders,orderVO);
                orderVO.setOrderDetailList(orderDetails);

                list.add(orderVO);
            }
        }
        return  new PageResult(page.getTotal(),list);
    }

       数据层代码如下:

/**
     * 分页条件查询并按下单时间排序
     * @param ordersPageQueryDTO
     */
    Page<Orders> pageQuery(OrdersPageQueryDTO ordersPageQueryDTO);
 <select id="pageQuery" resultType="Orders">
        select * from orders
        <where>
        <if test="number != null and number!=''">
            and number like concat('%',#{number},'%')
        </if>
        <if test="phone != null and phone!=''">
            and phone like concat('%',#{phone},'%')
        </if>
        <if test="userId != null">
            and user_id = #{userId}
        </if>
        <if test="status != null">
            and status = #{status}
        </if>
        <if test="beginTime != null">
            and order_time &gt;= #{beginTime}
        </if>
            <if test="endTime != null">
                and order_time &lt;= #{endTime}
            </if>
        </where>
        order by order_time desc
    </select>
/**
     * 根据订单id查询订单明细
     * @param orderId
     * @return
     */
    @Select("select * from order_detail where order_id = #{orderId}")
    List<OrderDetail> getByOrderId(Long orderId);

查询订单详情

代码如下:

/**
     * 查询订单详情
     * @param id
     * @return
     */
    @GetMapping("/orderDetail/{id}")
    @ApiOperation("查询订单详情")
    public Result<OrderVO> details(@PathVariable Long id){
        OrderVO orderVO = orderService.details(id);
        return Result.success(orderVO);
    }
/**
     * 查询订单详情
     *
     * @param id
     * @return
     */
    public OrderVO details(Long id) {
        //根据id查询订单
        Orders orders = orderMapper.getById(id);

        //查询该订单对应的菜品/套餐明细
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

        //将该订单及其详情封装到OrderVO并返回
        OrderVO orderVO = new OrderVO();
        BeanUtils.copyProperties(orders,orderVO);
        orderVO.setOrderDetailList(orderDetailList);

        return orderVO;
    }
/**
     * 根据id查询订单
     * @param id
     */
    @Select("select * from orders where id=#{id}")
    Orders getById(Long id);

取消订单

/**
     * 用户取消订单
     * @param id
     * @return
     */
    @ApiOperation("取消订单")
    @PutMapping("/cancel/{id}")
    public Result cancel(@PathVariable Long id) throws Exception{
        orderService.userCancelById(id);
        return Result.success();
    }
/**
     * 用户取消订单
     *
     * @param id
     */
    public void userCancelById(Long id) throws Exception {
        //根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        //检验订单是否存在
        if (ordersDB == null) {
            throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
        }

        //订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
        if (ordersDB.getStatus() > 2) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());

        //订单处于待接单的状态下取消,需要进行退款
        if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)){
            //调用微信支付退款接口
            //weChatPayUtil.refund(ordersDB.getNumber(),ordersDB.getNumber(),new BigDecimal("0.01"),new BigDecimal("0.01"));

            //支付状态修改为 退款
            orders.setPayStatus(Orders.REFUND);
        }

        //更新订单状态,取消原因,取消时间
        orders.setStatus(Orders.CANCELLED);
        orders.setCancelReason("用户取消");
        orders.setCancelTime(LocalDateTime.now());
        orderMapper.update(orders);
    }

再来一单

        因为此时的购物车已经消失,所以将订单详情对象转换为购物车对象,使用stream的方式将里面的每一个对象都塞回购物车里,最后将购物车对象批量添加到数据库。

/**
     * 再来一单
     *
     * @param id
     * @return
     */
    @PostMapping("/repetition/{id}")
    @ApiOperation("再来一单")
    public Result repetition(@PathVariable Long id) {
        orderService.repetition(id);
        return Result.success();
    }

/**
     * 再来一单
     *
     * @param id
     */
    public void repetition(Long id){
        //查询当前用户id
        Long userId = BaseContext.getCurrentId();

        //根据订单id查询当前订单详情
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);

        //将订单详情对象转换为购物车对象
        List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {
            ShoppingCart shoppingCart = new ShoppingCart();

            //将原订单详情里面的菜品信息重新复制到购物车对象中
            BeanUtils.copyProperties(x, shoppingCart, "id");
            shoppingCart.setUserId(userId);
            shoppingCart.setCreateTime(LocalDateTime.now());
            return shoppingCart;
        }).collect(Collectors.toList());

        //将购物车对象批量添加到数据库
        shoppingCartMapper.insertBatch(shoppingCartList);
    }
<insert id="insertBatch" parameterType="list">
        insert into shopping_cart
        (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)
        values
        <foreach collection="shoppingCartList" item="sc" separator=",">
            (#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime})
        </foreach>
    </insert>

商家端订单管理模块

订单搜索

业务规则

  • 输入订单号/手机号进行搜索,支持模糊搜索

  • 根据订单状态进行筛选

  • 下单时间进行时间筛选

  • 搜索内容为空,提示未找到相关订单

  • 搜索结果页,展示包含搜索关键词的内容

  • 分页展示搜索到的订单数据

代码开发

/**
 * 订单管理
 */
@RestController("adminOrderController")
@RequestMapping("/admin/order")
@Api("订单管理接口")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("/conditionSearch")
    @ApiOperation("订单搜索")
    public Result<PageResult> conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO){
        PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO);
        return Result.success(pageResult);
    }

}

        业务层里的逻辑主要是要向PageResult里传入一个orderVOList才行,但是我们只能得到orders,所以这里需要进行一下转换,将每一个orders里的数据封装到orderVO里(注意里面的菜品格式要进行一次啊转换再塞进去方便观看)

/**
     * 订单搜索
     *
     * @param ordersPageQueryDTO
     * @return
     */
    public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
        PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());

        Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

        // 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO
        List<OrderVO> orderVOList = getOrderVOList(page);

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

    private List<OrderVO> getOrderVOList(Page<Orders> page) {
        // 需要返回订单菜品信息,自定义OrderVO响应结果
        List<OrderVO> orderVOList = new ArrayList<>();

        List<Orders> ordersList = page.getResult();
        if (!CollectionUtils.isEmpty(ordersList)) {
            for (Orders orders : ordersList) {
                // 将共同字段复制到OrderVO
                OrderVO orderVO = new OrderVO();
                BeanUtils.copyProperties(orders, orderVO);
                String orderDishes = getOrderDishesStr(orders);

                // 将订单菜品信息封装到orderVO中,并添加到orderVOList
                orderVO.setOrderDishes(orderDishes);
                orderVOList.add(orderVO);
            }
        }
        return orderVOList;
    }

    /**
     * 根据订单id获取菜品信息字符串
     *
     * @param orders
     * @return
     */
    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);
    }

各个状态的订单数量统计

/**
     * 各个状态的订单数据统计
     * @return
     */
    @GetMapping("/statistics")
    @ApiOperation("各个状态的订单数据统计")
    public Result<OrderStatisticsVO> statistics(){
        OrderStatisticsVO orderStatisticsVO = orderService.statistics();
        return Result.success(orderStatisticsVO);
    }
/**
     * 各个状态的订单数量统计
     *
     * @return
     */
    public OrderStatisticsVO statistics(){
        //根据状态,分别查询出待接单,待派送,派送中的订单数量
        Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED);
        Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED);
        Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS);

        //将查询出的数据封装到orderStatisticsVO响应
        OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO();
        orderStatisticsVO.setToBeConfirmed(toBeConfirmed);
        orderStatisticsVO.setConfirmed(confirmed);
        orderStatisticsVO.setDeliveryInProgress(deliveryInProgress);
        return orderStatisticsVO;
    }
/**
     * 根据状态统计订单数量
     * @param toBeConfirmed
     */
    @Select("select count(id) from orders where status = #{status}")
    Integer countStatus(Integer status);

查询订单详情

需求和业务分析

业务规则:

  • 订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)

  • 订单详情页面需要展示订单明细数据(商品名称、数量、单价)

代码开发

/**
     * 订单详情
     *
     * @param id
     * @return
     */
    @GetMapping("/details/{id}")
    @ApiOperation("查询订单详情")
    public Result<OrderVO> details(@PathVariable("id") Long id) {
        OrderVO orderVO = orderService.details(id);
        return Result.success(orderVO);
    }

接单

    /**
     * 接单
     *
     * @return
     */
    @PutMapping("/confirm")
    @ApiOperation("接单")
    public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) {
        orderService.confirm(ordersConfirmDTO);
        return Result.success();
    }
/**
     * 接单
     *
     * @param ordersConfirmDTO
     */
    public void confirm(OrdersConfirmDTO ordersConfirmDTO) {
        Orders orders = Orders.builder()
                .id(ordersConfirmDTO.getId())
                .status(Orders.CONFIRMED)
                .build();

        orderMapper.update(orders);
    }

拒单

业务规则:

  • 商家拒单其实就是将订单状态修改为“已取消”

  • 只有订单处于“待接单”状态时可以执行拒单操作

  • 商家拒单时需要指定拒单原因

  • 商家拒单时,如果用户已经完成了支付,需要为用户退款

 /**
     * 拒单
     *
     * @return
     */
    @PutMapping("/rejection")
    @ApiOperation("拒单")
    public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception {
        orderService.rejection(ordersRejectionDTO);
        return Result.success();
    }
/**
     * 拒单
     *
     * @param ordersRejectionDTO
     */
    public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId());

        // 订单只有存在且状态为2(待接单)才可以拒单
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        //支付状态
        Integer payStatus = ordersDB.getPayStatus();
        if (payStatus == Orders.PAID) {
            //用户已支付,需要退款
            String refund = weChatPayUtil.refund(
                    ordersDB.getNumber(),
                    ordersDB.getNumber(),
                    new BigDecimal(0.01),
                    new BigDecimal(0.01));
            log.info("申请退款:{}", refund);
        }

        // 拒单需要退款,根据订单id更新订单状态、拒单原因、取消时间
        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        orders.setStatus(Orders.CANCELLED);
        orders.setRejectionReason(ordersRejectionDTO.getRejectionReason());
        orders.setCancelTime(LocalDateTime.now());

        orderMapper.update(orders);
    }

取消订单

业务规则:

  • 取消订单其实就是将订单状态修改为“已取消”

  • 商家取消订单时需要指定取消原因

  • 商家取消订单时,如果用户已经完成了支付,需要为用户退款

/**
     * 取消订单
     *
     * @return
     */
    @PutMapping("/cancel")
    @ApiOperation("取消订单")
    public Result cancel(@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception {
        orderService.cancel(ordersCancelDTO);
        return Result.success();
    }
/**
     * 取消订单
     *
     * @param ordersCancelDTO
     */
    public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId());

        //支付状态
        Integer payStatus = ordersDB.getPayStatus();
        if (payStatus == 1) {
            //用户已支付,需要退款
            String refund = weChatPayUtil.refund(
                    ordersDB.getNumber(),
                    ordersDB.getNumber(),
                    new BigDecimal(0.01),
                    new BigDecimal(0.01));
            log.info("申请退款:{}", refund);
        }

        // 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间
        Orders orders = new Orders();
        orders.setId(ordersCancelDTO.getId());
        orders.setStatus(Orders.CANCELLED);
        orders.setCancelReason(ordersCancelDTO.getCancelReason());
        orders.setCancelTime(LocalDateTime.now());
        orderMapper.update(orders);
    }

派送订单

业务规则:

  • 派送订单其实就是将订单状态修改为“派送中”

  • 只有状态为“待派送”的订单可以执行派送订单操作

/**
     * 派送订单
     *
     * @return
     */
    @PutMapping("/delivery/{id}")
    @ApiOperation("派送订单")
    public Result delivery(@PathVariable("id") Long id) {
        orderService.delivery(id);
        return Result.success();
    }
/**
     * 派送订单
     *
     * @param id
     */
    public void delivery(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在,并且状态为3
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        // 更新订单状态,状态转为派送中
        orders.setStatus(Orders.DELIVERY_IN_PROGRESS);

        orderMapper.update(orders);
    }

完成订单

业务规则:

  • 完成订单其实就是将订单状态修改为“已完成”

  • 只有状态为“派送中”的订单可以执行订单完成操作

/**
     * 完成订单
     *
     * @return
     */
    @PutMapping("/complete/{id}")
    @ApiOperation("完成订单")
    public Result complete(@PathVariable("id") Long id) {
        orderService.complete(id);
        return Result.success();
    }
/**
     * 完成订单
     *
     * @param id
     */
    public void complete(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在,并且状态为4
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        // 更新订单状态,状态转为完成
        orders.setStatus(Orders.COMPLETED);
        orders.setDeliveryTime(LocalDateTime.now());

        orderMapper.update(orders);
    }

校验收货地址是否超出配送范围

这里上传比较麻烦,就不做这个功能了,代码贴一下:

 

 

 订单状态定时处理

Spring Task

         这里只需要调用Mapper中的函数,找到所有符合条件的orders,之后遍历这些orders改变状态即可。

/**
 * 定时任务类
 */
@Slf4j
@Component
public class OrderTask {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 处理超时订单的方法
     */
    @Scheduled(cron = "0 * * * * ?") //每分钟触发一次
    public void processTimeoutOrder(){
        log.info("定时处理超时订单:{}", LocalDateTime.now());

        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);

        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);

        if (ordersList != null && !ordersList.isEmpty()){
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.CANCELLED);
                orders.setCancelReason("订单超时,自动取消");
                orders.setCancelTime(LocalDateTime.now());
                orderMapper.update(orders);
            }
        }
    }

    /**
     * 处理一直处于派送中的订单
     */
    @Scheduled(cron = "0 0 1 * * ?") //每天凌晨一点触发一次
    public void processDeliveryOrder(){
        log.info("定时处理处于派送中的订单:{}",LocalDateTime.now());

        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);

        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);

        if (ordersList != null && !ordersList.isEmpty()){
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
            }
        }
    }
}
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
    List<Orders> getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);

WebSocket协议

 来单提醒

语音播报+弹出提示框

         这里我在OrderServiceImpl里面的订单支付页面加入如下代码,让服务端向客户端发送消息,首先是m

ap,之后转换为json,最后调用websocketServer的群发方法即可。

 //通过websocket向客户端浏览器推送消息 type orderId content
        Map map = new HashMap<>();
        map.put("type",1); //1表示来单提醒 2表示客户催单
        map.put("orderId",this.orders.getId());
        map.put("content","订单号:"+ ordersPaymentDTO.getOrderNumber());

        String json = JSON.toJSONString(map);
        webSocketServer.sendToAllClient(json);

客户催单

         这里设计一个催单接口,比较简单,和前面类似:

/**
     * 客户催单
     * @param id
     * @return
     */
    @ApiOperation("客户催单")
    @GetMapping("/reminder/{id}")
    public Result reminder(@PathVariable("id") Long id){
        orderService.reminder(id);
        return Result.success();
    }
/**
     * 客户催单
     * @param id
     */
    public void reminder(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在,并且状态为4
        if (ordersDB == null) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Map map = new HashMap<>();
        map.put("type",2); //1提醒 2催单
        map.put("orderId",id);
        map.put("content","订单号:"+ordersDB.getNumber());

        String json = JSON.toJSONString(map);

        //通过websocket向客户端推送消息
        webSocketServer.sendToAllClient(json);

    }

营业额统计

     这里要传入起始和结束日期,记得要加@DateFormat注解

/**
 * 数据统计相关接口
 */
@Api(tags = "数据统计相关接口")
@Slf4j
@RestController
@RequestMapping("/admin/report")
public class ReportController {

    @Autowired
    private ReportService reportService;

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

        业务层的逻辑主要是将传入的日期变成一个日期数组,再把数组里日期对应的营业额计算出来,最后将两个数据都变成字符串

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 统计指定区间营业额数据
     * @param begin
     * @param end
     * @return
     */
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
        //当前集合存放begin到end的所有日期
        List<LocalDate> dateList = new ArrayList<>();

        dateList.add(begin);
        while (!begin.equals(end)) {
            //日期计算,计算指定日期的后一天对应的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        List<Double> turnoverList = new ArrayList<>();
        for (LocalDate date : dateList) {
            //查询date日期对应的营业额 状态为已完成的订单金额合计
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);

            Map map = new HashMap<>();
            map.put("begin",beginTime);
            map.put("end",endTime);
            map.put("status", Orders.COMPLETED);

            //select sum(amount) from orders where order_time > beginTime and order_time < endTime end and status = 5
            Double turnover = orderMapper.sumByMap(map);
            turnover = turnover == null ? 0.0 : turnover;
            turnoverList.add(turnover);
        }
        
        return TurnoverReportVO
                .builder()
                .dateList(StringUtils.join(dateList, ",")) //封装成字符串
                .turnoverList(StringUtils.join(turnoverList,","))
                .build();
    }
}

        数据层条件查询当日总金额,记得比较大小的时候要用转义字符:

<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">and status = #{status}</if>
        </where>
    </select>

用户统计 

用户数量统计与上面的营业额统计十分类似,唯一我觉得需要注意的一点就是,在插入新增用户和总用户时,先查总用户的,因为信息较少,再在map里加入新的限制,查询新增用户:

/**
     * 用户统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/userStatistics")
    @ApiOperation("用户统计")
    public Result<UserReportVO> userStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
        log.info("用户数据统计:{},{}",begin,end);
        return Result.success(reportService.getUserStatistics(begin, end));
    }
/**
     * 统计指定区间用户数据
     * @param begin
     * @param end
     * @return
     */
    public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
        //当前集合存放begin到end的所有日期
        List<LocalDate> dateList = new ArrayList<>();

        dateList.add(begin);
        while (!begin.equals(end)) {
            //日期计算,计算指定日期的后一天对应的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        //每天新增用户数量
        List<Integer> newUserList = new ArrayList<>(); //select count(id) from user where create_time < ? and create_time > ?
        //每天总用户数量
        List<Integer> totalUserList = new ArrayList<>(); //select count(id) from user where create_time < ?

        for (LocalDate date : dateList) {
            //查询date日期对应的营业额 状态为已完成的订单金额合计
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);

            Map map = new HashMap();
            map.put("end",endTime);

            //总用户数量
            Integer totalUser = userMapper.countByMap(map);

            map.put("begin",beginTime);
            //新增用户数量
            Integer newUser = userMapper.countByMap(map);

            totalUserList.add(totalUser);
            newUserList.add(newUser);
        }

        return UserReportVO.builder()
                .totalUserList(StringUtils.join(totalUserList,","))
                .newUserList(StringUtils.join(newUserList,","))
                .dateList(StringUtils.join(dateList,","))
                .build();
    }
<select id="countByMap" resultType="java.lang.Integer">
        select count(id) from user
        <where>
            <if test="begin != null">and create_time &gt; #{begin}</if>
            <if test="end != null">and create_time &lt; #{end}</if>
        </where>
    </select>

订单统计

这里代码与上面也十分类似,只需要注意要多返回一下具体的数量,同时,获取数量建议使用stream流的方法即可。

/**
     * 订单统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/ordersStatistics")
    @ApiOperation("订单统计")
    public Result<OrderReportVO> ordersStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
        log.info("用户数据统计:{},{}",begin,end);
        return Result.success(reportService.getOrdersStatistics(begin, end));
    }
/**
     * 统计指定区间订单数据
     * @param begin
     * @param end
     * @return
     */
    public OrderReportVO getOrdersStatistics(LocalDate begin, LocalDate end) {
        //当前集合存放begin到end的所有日期
        List<LocalDate> dateList = new ArrayList<>();

        dateList.add(begin);
        while (!begin.equals(end)) {
            //日期计算,计算指定日期的后一天对应的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        // 存放每天订单总数
        List<Integer> orderCountList = new ArrayList<>();
        // 存放每天有效订单数
        List<Integer> validOrderCountList = new ArrayList<>();

        //遍历dateList,查询每天有效订单数和订单总数
        for (LocalDate date : dateList) {
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);

            //查询每天订单总数 select count(id) from orders where order_time > beginTime and order_time < endTime
            Integer orderCount = getOrderCount(beginTime, endTime, null);

            //查询每天有效订单数  select count(id) from orders where order_time > beginTime and order_time < endTime and status = 5
            Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);

            orderCountList.add(orderCount);
            validOrderCountList.add(validOrderCount);
        }

        //计算时间区间内的订单总数量
        Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();

        //计算时间区间内的有效订单数量
        Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();

        //计算订单完成率
        Double orderCompletionRate = 0.0;
        if (totalOrderCount != 0) {
            orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
        }

        return OrderReportVO.builder()
                .dateList(StringUtils.join(dateList, ","))
                .orderCountList(StringUtils.join(orderCountList, ","))
                .validOrderCountList(StringUtils.join(validOrderCountList, ","))
                .totalOrderCount(totalOrderCount)
                .validOrderCount(validOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .build();
    }

    /**
     * 根据条件统计订单数量
     * @param begin
     * @param end
     * @param status
     * @return
     */
    private Integer getOrderCount(LocalDateTime begin, LocalDateTime end,Integer status){
        Map map = new HashMap<>();
        map.put("begin",begin);
        map.put("end",end);
        map.put("status",status);
        return orderMapper.countByMap(map);
    }
<select id="countByMap" resultType="java.lang.Integer">
        select count(id) 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">and status = #{status}</if>
        </where>
    </select>

订单销量排名

 这里要查询订单表和订单信息表,因为订单信息表里没有说明当前这个订单是否完成,其余类似:

/**
     * 销量排名top10
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/top10")
    @ApiOperation("销量排名top10")
    public Result<SalesTop10ReportVO> top10(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
        log.info("销量排名top10:{},{}",begin,end);
        return Result.success(reportService.getSalesTop10(begin, end));
    }
/**
     * 统计指定时间区间内的销量排名前十
     * @param begin
     * @param end
     * @return
     */
    public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
        LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
        LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);

        List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop(beginTime, endTime);

        List<String> names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
        String nameList = StringUtils.join(names, ",");
        List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
        String numberList = StringUtils.join(numbers, ",");

        //封装返回结果数据
        return SalesTop10ReportVO.builder()
                .nameList(nameList)
                .numberList(numberList)
                .build();
    }
<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

具体操作实例

/**
 * poi操作Excel文件
 */
public class POITest {

    //通过POI创建Excel文件并且写入文件内容
    public static void Write() throws Exception{
        //在内存中创建一个Excel文件
        XSSFWorkbook excel = new XSSFWorkbook();
        //在Excel文件中创建一个sheet页
        XSSFSheet sheet = excel.createSheet("info");
        //在sheet页中创建行对象,rownum从0开始
        XSSFRow row = sheet.createRow(1);
        //创建单元格,写入文件内容
        row.createCell(1).setCellValue("姓名");
        row.createCell(2).setCellValue("城市");

        //创建一个新行
        row = sheet.createRow(2);
        row.createCell(1).setCellValue("张三");
        row.createCell(2).setCellValue("北京");

        //创建一个新行
        row = sheet.createRow(3);
        row.createCell(1).setCellValue("李四");
        row.createCell(2).setCellValue("南京");

        //通过输出流将内存中的excel写入磁盘
        FileOutputStream out = new FileOutputStream(new File("D:\\info.xlsx"));
        excel.write(out);
        //关闭资源
        out.close();
        excel.close();
    }

    //通过POI读取Excel文件中的内容
    public static void read() throws Exception{
        FileInputStream fileInputStream = new FileInputStream(new File("D:\\info.xlsx"));
        XSSFWorkbook excel = new XSSFWorkbook(fileInputStream);
        //读取第一个sheet页
        XSSFSheet sheet = excel.getSheetAt(0);

        int lastRowNum = sheet.getLastRowNum(); //有文字的最后一行行号
        for (int i = 1; i <= lastRowNum; i++){
            //获得某一行
            XSSFRow row = sheet.getRow(i);
            //获得单元格对象
            String cellValue1 = row.getCell(1).getStringCellValue();
            String cellValue2 = row.getCell(2).getStringCellValue();
            System.out.println(cellValue1+" "+cellValue2);

            //关闭资源
            excel.close();
            fileInputStream.close();
        }


    }

    public static void main(String[] args) throws Exception{
         //Write();
         read();
    }

}

 导出运营数据Excel报表

 表现层中传入参数是为了能下载到浏览器里:

/**
     * 导出运营数据报表
     * @param response
     */
    @GetMapping("/export")
    @ApiOperation("导出运营数据报表")
    public void export(HttpServletResponse response){
        reportService.exportBusinessDate(response);
    }

业务层比较新颖的一点就是自动注入了workspaceService,因为里面有我们想要的数据,这里的input流采用了从类路径中读取资源。

    /**
     * 导出运营数据报表
     * @param response
     */
    public void exportBusinessDate(HttpServletResponse response) {
        //1 查询数据库获取营业数据 查询最近三十天运营数据
        LocalDate dateBegin = LocalDate.now().minusDays(30);
        LocalDate dateEnd = LocalDate.now().minusDays(1);

        //查询概览数据
        BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX));

        //2 通过POI将数据写入Excel
        InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/excelTemplate.xlsx");

        try {
            //基于模板文件创建一个新的excel
            XSSFWorkbook excel = new XSSFWorkbook(in);

            //获取表格文件sheet标签页
            XSSFSheet sheet = excel.getSheet("Sheet1");
            
            //填充数据 -- 时间
            sheet.getRow(1).getCell(1).setCellValue("时间:"+dateBegin+"至"+dateEnd);

            //获得第四行
            XSSFRow row = sheet.getRow(3);
            row.getCell(2).setCellValue(businessDataVO.getTurnover());
            row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
            row.getCell(6).setCellValue(businessDataVO.getNewUsers());

            //获得第五行
            row = sheet.getRow(4);
            row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());
            row.getCell(4).setCellValue(businessDataVO.getUnitPrice());

            //填充明细数据
            for (int i = 0; i < 30; i++) {
                LocalDate date = dateBegin.plusDays(i);
                //查询某一天的营业数据
                workspaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN),LocalDateTime.of(date,LocalTime.MAX));

                // 获得某一行
                row = sheet.getRow(7 + i);
                row.getCell(1).setCellValue(date.toString());
                row.getCell(2).setCellValue(businessDataVO.getTurnover());
                row.getCell(3).setCellValue(businessDataVO.getValidOrderCount());
                row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
                row.getCell(5).setCellValue(businessDataVO.getUnitPrice());
                row.getCell(6).setCellValue(businessDataVO.getNewUsers());
            }

            //3 通过输出流,将Excel文件下载到客户端浏览器
            ServletOutputStream out = response.getOutputStream();
            excel.write(out);

            //关闭资源
            out.close();
            excel.close();

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
苍穹外卖项目可以使用Postman进行API接口的测试和调试。Postman是一款常用的API开发工具,它可以帮助开发人员发送HTTP请求并查看响应结果,方便进行接口的测试和调试。 在苍穹外卖项目,可以使用Postman发送各种类型的HTTP请求,比如GET、POST、PUT、DELETE等,来模拟用户操作和测试接口功能。通过Postman,可以验证接口的正确性、查看接口返回的数据、调试接口的参数等。 为了使用Postman进行苍穹外卖项目的接口测试,您需要以下步骤: 1. 下载并安装Postman:您可以从Postman官网(https://www.postman.com/)上下载并安装适合您的操作系统的版本。 2. 打开Postman并创建一个新的请求:打开Postman应用,在界面上选择"New"来创建一个新的请求。 3. 输入接口URL和选择请求方法:在新建请求的界面,输入苍穹外卖项目的接口URL,并选择适当的请求方法,比如GET或POST。 4. 添加请求参数和请求头:根据需要,您可以添加请求参数和请求头,以便于模拟不同的请求情况。 5. 发送请求并查看响应:点击发送按钮,Postman会向服务器发送请求,并在界面上显示响应结果。您可以查看接口返回的数据、响应状态码等信息。 通过以上步骤,您可以使用Postman进行苍穹外卖项目的接口测试。这样可以帮助您确保接口的正确性和稳定性,提高项目的质量和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值