苍穹外卖项目笔记

软件开发流程

需求分析:说明书和原型 

设计: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.mzph.cn/news/772550.shtml

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

相关文章

Mybatis中条件传入多个参数时,如何处理

entity&#xff1a; Data AllArgsConstructor NoArgsConstructor public class User {private Integer id;private String username;private String password;private String phone;private String address; }dao接口&#xff1a; public interface UserDAO {List<User>…

基于vue+element+springboot+uniapp开发的智慧城管源码,java智慧城市管理综合执法系统源码

智慧城管源码&#xff0c;智慧执法&#xff0c;数字化城市管理综合执法系统源码 智慧城管系统充分利用物联网、云计算、信息融合、网络通讯、数据分析与挖掘等技术&#xff0c;对城市管理进行全方位覆盖。它通过建立城市综合管理平台&#xff0c;将城市的信息和管理资源有机结合…

错误 LNK1104 无法打开文件“mfc140.lib”

如图&#xff0c;编译一个别人已有的项目&#xff0c;我的编译报错为&#xff1a; 但是我所有文件夹全局搜索了一下&#xff0c;这个文件是存在的。但是当前项目访问不到。 更改方法&#xff1a;项目->属性->配置属性->VC目录->库目录 全局搜索找到mfc140.lib的…

【LeetCode 算法专题突破】定长滑动窗口

文章目录 前言[1456. 定长子串中元音的最大数目](https://leetcode.cn/problems/maximum-number-of-vowels-in-a-substring-of-given-length/)题目描述&#xff1a;难度分1263代码与解题思路代码复盘 [2269. 找到一个数字的 K 美丽值](https://leetcode.cn/problems/find-the-k…

NAT---网络地址转换技术

Network Address Translation 1、起源&#xff1a;ip地址不够用 2、作用&#xff1a;让私网地址映射成公网地址&#xff0c;进而访问网络。 3、私网Ip地址的范围&#xff1a; A类&#xff1a;10.0.0.0-10.255.255.255 B类&#xff1a;172.16.0.0-172.31.255.255 C类&…

码垛机与人工搬运:效率与安全性的比较分析

在现代包装行业中&#xff0c;泡沫箱因其轻便和保温特性被广泛用于商品的包装与运输。随着自动化技术的不断发展&#xff0c;码垛机成为提升泡沫箱生产效率、降低劳动强度的关键技术。本文旨在比较码垛机与人工码垛在泡沫箱生产中的优势&#xff0c;并探讨自动化码垛的未来发展…

Springboot快速整合bootstrap-table使用,接口对接

这个表格加持还是不错了&#xff0c;自带了全局搜索&#xff0c;分页&#xff0c;数据导出&#xff0c;卡片视图&#xff0c;等&#xff0c;本次整合添加了数据添加弹窗和编辑数据回显弹窗&#xff0c;附完整页面代码&#xff0c;只需要拿过来替换自己实际的接口即可。 效果图 …

鸿蒙实战开发-如何通过拖动滑块调节应用内字体大小

介绍 本篇Codelab将介绍如何使用基础组件Slider&#xff0c;通过拖动滑块调节应用内字体大小。要求完成以下功能&#xff1a; 实现两个页面的UX&#xff1a;主页面和字体大小调节页面。拖动滑块改变字体大小系数&#xff0c;列表页和调节页面字体大小同步变化。往右拖动滑块字…

ppp验证实验

实际操作图 1&#xff0c;IP划分分配 [r1]interface Serial 4/0/0 [r1-Serial4/0/0]ip add 192.168.1.1 24 [r2]interface Serial 4/0/0 [r2-Serial4/0/0]ip address 192.168.1.2 24 [r2]int Mp-group 0/0/0 [r2-Mp-group0/0/0]ip add 192.168.2.1 24 [r3]int Mp-group 0/…

Xcode Launching “XXX“ is taking longer than expected

文章目录 1.问题2.如何进入iOS DeviceSupport目录3.解决方法4.参考博客 1.问题 LLDB is likely reading from device memory to resolve symbols 2.如何进入iOS DeviceSupport目录 3.解决方法 进入iOS DeviceSupport目录&#xff0c;删除该真机对应的架构文件&#xff08;比如…

QT作业day3

1、使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#xff0c;密码是…

JMeter 如何并发执行 Python 脚本

要在JMeter中并发执行Python脚本&#xff0c;可以使用Jython脚本或通过调用外部Python脚本的方式实现。 使用Jython脚本并发执行Python脚本的步骤&#xff1a; 1、创建一个线程组&#xff1a;在JMeter界面中&#xff0c;右键点击测试计划&#xff0c;选择 “添加” -> “线…

c语言文件操作(下)

目录 1.文件的随机读写1.1 fseek1.2 ftell1.3 rewind 2. 文件结束的判定2.1 文本文件读取结束的判断2.2 二进制文件读取结束的判断 3. 文件缓冲区 1.文件的随机读写 1.1 fseek 根据⽂件指针的位置和偏移量来定位⽂件指针。 函数原型&#xff1a; int fseek (FILE * stream,…

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…

黑马头条day5总结

1、surefire-reports for the individual test results. 借鉴&#xff1a;【已解决】surefire-reports for the individual test results.-CSDN博客 Please refer to D:\javashizhan01\heima-leadnews\heima-leadnews-service\heima-leadnews-article\target\surefire-report…

HTML(二)---【常见的标签使用】

零.前言 本文只介绍常见的标签使用&#xff0c;其中使用的一些HTML专业术语可以在作者的第一篇文章&#xff1a; HTML&#xff08;一&#xff09;---【基础】-CSDN博客中找到。 一.<b>粗体、<i>或<em>斜体 1.定义 粗体、斜体的实现可以在CSS中实现&…

【MySQL】事务日志

事务的隔离性由锁机制实现&#xff0c;事务的原子性、一致性和持久性由redo日志和undo日志实现。 一、redo日志 1.1、为什么需要redo日志 一方面&#xff0c;由于数据从内存写回磁盘需要一定的时间&#xff0c;假如在事务提交后&#xff0c;还没有写回磁盘&#xff0c;数据库…

web前端面试题---->HTML、CSS

一.居中方法 block元素如何居中 margin&#xff1a;0 auto&#xff1b;position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);flex布局&#xff1a; 对父元素操作 &#xff1a; justify-content:center; al…

商城小程序项目实现监控的可观测性最佳实践

前言 微信小程序是一种轻量级的应用程序&#xff0c;用户可以在微信内直接使用&#xff0c;无需下载安装。它具有独立的开发框架和生态系统&#xff0c;支持丰富的功能和交互&#xff0c;包括社交、购物、服务等。 观测云对微信小程序的监控能够实时收集性能指标、错误日志和…

分布式系统CAP理论

1、什么是CAP理论 C是Consistency(强一致性)、A是Availability(可用性)、P是Partition Tolerance(分区容错性)&#xff0c;一个分布式系统不可能同时很好的满足—致性、可用性和分区容错性这三个需求&#xff0c;不能同时成立&#xff0c;最多只能同时满足其中的两项&#xff…