软件开发流程
需求分析:说明书和原型
设计: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 >= #{beginTime}</if><if test="endTime != null">and order_time <= #{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 > #{begin}</if><if test="end != null">and order_time < #{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 > #{begin}</if><if test="end != null">and create_time < #{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 > #{begin}</if><if test="end != null">and order_time < #{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 > #{begin}</if><if test="end != null">and o.order_time < #{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);}