苍穹外卖项目笔记

软件开发流程

需求分析:说明书和原型 

设计: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 {@Autowiredprivate 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,在开发阶段使用的框架,帮助后端开发人员做后端的接口测试

    @Beanpublic 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)

@Overridepublic void save(EmployeeDTO employeeDTO) {Employee employee = new Employee();//对象属性拷贝 , 前提属性名一致BeanUtils.copyProperties(employeeDTO,employee);//设置账号状态 默认正常 1正常 0锁定employee.setStatus(StatusConstant.ENABLE); //用常量类,不要硬编码//设置密码,默认密码123456employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));//设置创建时间和修改时间employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());//设置当前记录创建人id和修改人idemployee.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异常,可以如下处理:

@ExceptionHandlerpublic 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*/@Overridepublic 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*/@Overrideprotected 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*/@Overridepublic 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*/@Overridepublic 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。

 @Overridepublic 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 INSERTOperationType 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.updateAutoFill 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 categorywhere 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/ObjectNameStringBuilder 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@ConditionalOnMissingBeanpublic 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 {@Autowiredprivate AliOssUtil aliOssUtil;/*** 文件上传* @param file* @return*/@ApiOperation("文件上传")@PostMapping("/upload")public Result<String> upload(MultipartFile file){log.info("文件上传:{}",file);try {//原始文件名String originalFilename = file.getOriginalFilename();//截取原神文件名的后缀 fsdf.pngString 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 {@Autowiredprivate 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 {@Autowiredprivate DishMapper dishMapper;@Autowiredprivate DishFlavorMapper dishFlavorMapper;/*** 新增菜品和对应的口味数据* @param dishDTO*/@Transactional@Overridepublic 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*/@Overridepublic 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,这一点由前端实现了。

@Overridepublic DishVO getByIdWithFlavor(Long id) {//根据id查询菜品数据Dish dish = dishMapper.getById(id);//根据菜品id查询口味数据List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);//将查询到的数据封装到dishVODishVO 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*/@Overridepublic 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*/@Overridepublic 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 {@Autowiredprivate 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 {@Autowiredprivate SetmealMapper setmealMapper;@Autowiredprivate SetmealDishMapper setmealDishMapper;@Autowiredprivate DishMapper dishMapper;/*** 新增套餐同时需要保存套餐和菜品的关联关系* @param setmealDTO*/@Overridepublic void saveWithDish(SetmealDTO setmealDTO) {Setmeal setmeal = new Setmeal();BeanUtils.copyProperties(setmealDTO,setmeal);//向套餐表插入数据setmealMapper.insert(setmeal);//获取生成的套餐idLong 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 categoryNamefrom 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表,执行deletesetmealDishMapper.deleteBySetmealId(setmealDTO.getId());List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();setmealDishes.forEach(setmealDish -> {setmealDish.setSetmealId(setmealDTO.getId());});//重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insertsetmealDishMapper.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: localhostport: 6379password: 123456database: 0

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

@Configuration
@Slf4j
public class RedisConfiguration {@Beanpublic 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 {@Autowiredprivate RedisTemplate redisTemplate;@Testpublic 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";@Autowiredprivate 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";@Autowiredprivate 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建立名字。

@Beanpublic 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;}@Beanpublic 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请求*/@Testpublic 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请求*/@Testpublic 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: tokenuser-secret-key: itheimauser-ttl: 7200000user-token-name: authenticationalioss: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 {@Autowiredprivate UserService userService;@Autowiredprivate 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";@Autowiredprivate WeChatProperties weChatProperties;@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate JwtTokenAdminInterceptor jwtTokenAdminInterceptor;@Autowiredprivate 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_idString 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 {@Autowiredprivate 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 {@Autowiredprivate ShoppingCartMapper shoppingCartMapper;@Autowiredprivate DishMapper dishMapper;@Autowiredprivate 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() {//获取到当前微信用户的idLong 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 {@Autowiredprivate 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 {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate OrderDetailMapper orderDetailMapper;@Autowiredprivate AddressBookMapper addressBookMapper;@Autowiredprivate ShoppingCartMapper shoppingCartMapper;/*** 用户下单* @param ordersSubmitDTO* @return*/@Transactionalpublic 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()); //设置当前订单明细关联的订单idorderDetailList.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 {// 当前登录用户idLong 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){//查询当前用户idLong 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 {@Autowiredprivate 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转化为OrderVOList<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) {// 将共同字段复制到OrderVOOrderVO orderVO = new OrderVO();BeanUtils.copyProperties(orders, orderVO);String orderDishes = getOrderDishesStr(orders);// 将订单菜品信息封装到orderVO中,并添加到orderVOListorderVO.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);// 校验订单是否存在,并且状态为3if (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);// 校验订单是否存在,并且状态为4if (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 {@Autowiredprivate 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 contentMap 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);// 校验订单是否存在,并且状态为4if (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 {@Autowiredprivate 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 {@Autowiredprivate 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 = 5Double 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 < endTimeInteger orderCount = getOrderCount(beginTime, endTime, null);//查询每天有效订单数  select count(id) from orders where order_time > beginTime and order_time < endTime and status = 5Integer 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) numberfrom order_detail od, orders owhere 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.nameorder by number desclimit 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将数据写入ExcelInputStream in = this.getClass().getClassLoader().getResourceAsStream("template/excelTemplate.xlsx");try {//基于模板文件创建一个新的excelXSSFWorkbook 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);}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/287083.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IPhone让用户升级?网友你咋不降点!

最近一个热搜刷屏了我的朋友圈&#xff0c;我点开一看是苹果上架了全新“换代有来”页面&#xff0c;其主要表达了苹果用户可以将旧的iphone升级到全新的iphoe15上。并告诉贴心的给网友对比了一下换代的好处。 并且还详细了说了一些iPhone 11、11 Pro、11 Pro Max、12、12 mini…

【鸿蒙HarmonyOS开发笔记】使用@Preview装饰器预览组件

概述 ArkTS应用/服务支持组件预览&#xff0c;要求compileSdkVersion为8或以上。组件预览支持实时预览&#xff0c;不支持动态图和动态预览。组件预览通过在组件前添加注解Preview实现&#xff0c;在单个源文件中&#xff0c;最多可以使用10个Preview装饰自定义组件。 Preview…

EI级!高创新原创未发表!VMD-TCN-BiGRU-MATT变分模态分解卷积神经网络双向门控循环单元融合多头注意力机制多变量时间序列预测(Matlab)

EI级&#xff01;高创新原创未发表&#xff01;VMD-TCN-BiGRU-MATT变分模态分解卷积神经网络双向门控循环单元融合多头注意力机制多变量时间序列预测&#xff08;Matlab&#xff09; 目录 EI级&#xff01;高创新原创未发表&#xff01;VMD-TCN-BiGRU-MATT变分模态分解卷积神经…

Elastic 线下 Meetup 将于 2024 年 3 月 30 号在武汉举办

2024 Elastic Meetup 武汉站活动&#xff0c;由 Elastic、腾讯、新智锦绣联合举办&#xff0c;现诚邀广大技术爱好者及开发者参加。 活动时间 2024年3月30日 13:30-18:00 活动地点 中国武汉 武汉市江夏区腾讯大道1号腾讯武汉研发中心一楼多功能厅 13:30-14:00 入场 活动流程…

在基于Android相机预览的CV应用程序中使用 OpenCL

查看&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV4.9.0在Android 开发简介 下一篇&#xff1a;在 MacOS 中安装 本指南旨在帮助您在基于 Android 相机预览的 CV 应用程序中使用 OpenCL ™。教程是为 Android Studio 20…

javaWeb健康管理系统

一、引言 1.1 设计背景 紧张的工作节奏、教学和科研的压力、个人不良的工作生活习惯、以及伴随工作压力而来的家庭关系、人际关系紧张等因素使得高校群体成为慢性病的高发群体[1]。学生入学的定期体检&#xff0c;教职工人入职体检&#xff0c;以及所有学生和教职工的定期体检…

UI自动化_id 元素定位

## 导包selenium from selenium import webdriver import time1、创建浏览器驱动对象 driver webdriver.Chrome() 2、打开测试网站 driver.get("你公司的平台地址") 3、使浏览器窗口最大化 driver.maximize_window() 4、在用户名输入框中输入admin driver.find_ele…

去中心化的 AI 数据供应:认识Grass,参与Grass

去中心化的 AI 数据供应&#xff1a;认识Grass&#xff0c;参与Grass &#x1f44b;&#xff1a;邀请链接☘️&#xff1a;Intro❓&#xff1a;看好Grass和即将推出的L2的原因有哪些&#xff1f;&#x1f4a1;&#xff1a;展望&#x1f50d;&#xff1a;总结 &#x1f44b;&…

【Python】Data Science with Python 数据科学(1)环境搭建

一、操作系统 使用运行在Windows11主机上的Ubuntu 22.04虚拟机&#xff0c;虚拟化平台为Oracle VM VirtualBox。 二、PyCharm安装 有关PyCharm的安装和快捷方式创建&#xff0c;可分别参考我的博客 Ubuntu安装PyCharm、Ubuntu创建桌面快捷方式 &#xff0c;以及Ubuntu创建桌…

开源博客项目Blog .NET Core源码学习(11:App.Core项目结构分析)

开源博客项目Blog的App.Core项目主要定义数据库表对应的数据类&#xff0c;同时定义配置文件读取、日志记录、辅助缓存等辅助类。App.Core项目安装的Nuget包不多&#xff0c;仅包括SqlSugarCore和Microsoft.Extensions.DependencyInjectio两类。   App.Core项目的顶层文件夹如…

C++模版(基础)

目录 C泛型编程思想 C模版 模版介绍 模版使用 函数模版 函数模版基础语法 函数模版原理 函数模版实例化 模版参数匹配规则 类模版 类模版基础语法 C泛型编程思想 泛型编程&#xff1a;编写与类型无关的通用代码&#xff0c;是代码复用的一种手段。 模板是泛型编程…

解決flask-restful提示Did not attempt to load JSON data 问题

在使用flask-restfull进行API开发的时候。一旦我使用类似下面的代码从url或者form中获得参数就会出现报错&#xff1a;Did not attempt to load JSON data because the request Content-Type was not ‘application/json’。 代码如下&#xff1a; # Flask_RESTFUl数据解析 f…

【微服务】接口幂等性常用解决方案

一、前言 在微服务开发中&#xff0c;接口幂等性问题是一个常见却容易被忽视的问题&#xff0c;同时对于微服务架构设计来讲&#xff0c;好的幂等性设计方案可以让程序更好的应对一些高并发场景下的数据一致性问题。 二、幂等性介绍 2.1 什么是幂等性 通常我们说的幂等性&…

Springboot+vue的企业质量管理系统(有报告)。Javaee项目,springboot vue前后端分离项目。

演示视频&#xff1a; Springbootvue的企业质量管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09…

【JavaSE】数据类型和运算符

前言 从这一篇我们开始Java的学习~ 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 前言 Java第一个程序 字面常量 字面常量的分类 结合代码理解 类型转换 类型提升 byte与byte的运算 正确写法 字符串类型St…

Matplotlib中英文使用不同字体的最优解

中英文使用不同字体&#xff0c;我们需要解决两个需求&#xff1a; 第一个需求&#xff1a;要用中文字体显示中文&#xff0c;不能全部都是框框。第二个需求&#xff1a;横纵坐标的数字用英文字体显示&#xff0c;英文用英语字体显示。 方法很简单&#xff0c;只需要添加一行…

【Entity Framework】 EF三种开发模式

【Entity Framework】 EF三种开发模式 文章目录 【Entity Framework】 EF三种开发模式一、概述二、DataBase First2.1 DataBase First简介2.2 DataBase First应用步骤2.3 DataBase First总结 三、Model First3.1 Model First简介3.2 Model First实现步骤 四、Code First4.1 Cod…

c++初阶------c++代码模块

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

背包DP模板

01背包 01背包-1 #include <bits/stdc.h> using namespace std;const int N 1e5 10; int n, m, f[N][N], v[N], w[N];int main() {cin >> n >> m;for (int i 1; i < n; i) {cin >> v[i] >> w[i];}for (int i 1; i < n; i) {for (int…

Python提取本体文件的数据

运行结果&#xff1a; 使用replace函数去除前缀。 查找OWL的对象属性&#xff1a; 输出结果&#xff1a; 出现最后这个的原因&#xff1a; 修改程序&#xff1a; 最后的输出结果&#xff1a; 这个解析之后是这个样子的&#xff1a;