当前位置: 首页 > news >正文

怎么在网站中搜索关键字企业网页页面设计

怎么在网站中搜索关键字,企业网页页面设计,iis怎么创建网站,东莞市住房和城乡建设局门户网站软件开发流程 需求分析#xff1a;说明书和原型 设计#xff1a;UI#xff0c;数据库#xff0c;接口设计 编码#xff1a;项目代码#xff0c;单元测试 测试#xff1a;测试用例#xff0c;测试报告 上线运维#xff1a;软件环境安装#xff0c;配置 软件环境…软件开发流程 需求分析说明书和原型  设计UI数据库接口设计 编码项目代码单元测试 测试测试用例测试报告 上线运维软件环境安装配置 软件环境 开发环境本地电脑环境外部用户无法访问 测试环境测试人员测试项目测试服务器 生产环境正式对外提供服务的环境 苍穹外卖项目介绍 技术选型 项目结构 为什么直接给出来而不是从零开始写呢因为在公司里也不可能让你造轮子的 数据库 前后端联调 登录过程 执行启动项以后进入EmployeeController执行login方法接收前端传进来的数据employeeLoginDTO数据传输对象打印一个员工登录日志此时调用employeeService的login函数传入刚才的DTO。 PostMapping(/login)public ResultEmployeeLoginVO 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后面加tagsApiModel后面加description 新增员工 代码开发 当前端提交的数据和实体类中对应的属性差别比较大时建议使用DTO来封装数据调用业务层传入DTO即可一般新增员工用post方法同时由于前端传递过来的是json对象所以要加一个RequsetBody注解才能将其转换为DTO类 PostMappingApiOperation(新增员工)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_usernameString 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令牌MapString, 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 ThreadLocalLong 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 ResultPageResult page(EmployeePageQueryDTO employeePageQueryDTO){log.info(员工分页查询参数为{},employeePageQueryDTO);PageResult pageResult employeeService.pageQuery(employeePageQueryDTO);return Result.success(pageResult);} 接下来我们引入PageHelper依赖进行分页查询的操作传入页数和页面大小调用mapper层的分页查询函数已自动优化返回的是一个PageEmployee对象page利用getTotal函数得到页数getResult函数得到其他所有的信息是一个list最后利用PageResult的有参构造封装成能传入给success的对象。 /*** 分页查询** param employeePageQueryDTO* return*/Overridepublic PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {//开始分页查询PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());PageEmployee page employeeMapper.pageQuery(employeePageQueryDTO);long total page.getTotal();ListEmployee records page.getResult();return new PageResult(total, records);} 接下来是Mapper层的逻辑只需要模糊匹配且按照创建时间排序即可无需自己计算页数之类的东西以及limit方法PageHelper会自动调整好 select idpageQuery resultTypecom.sky.entity.Employeeselect * from employeewhereif testname ! null and name ! and name like concat(%,#{name},%)/if/whereorder 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(ListHttpMessageConverter? 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 idupdate parameterTypecom.sky.entity.Employeeupdate employeesetif testname ! nullname #{name},/ifif testusername ! nullusername #{username},/ifif testpassword ! nullpassword #{password},/ifif testphone ! nullphone #{phone},/ifif testsex ! nullsex #{sex},/ifif testidNumber ! nullid_Number #{idNumber},/ifif testupdateTime ! nullupdate_Time #{updateTime},/ifif testupdateUser ! nullupdate_User #{updateUser},/ifif teststatus ! nullstatus #{status},/if/setwhere id #{id}/update 编辑员工 根据id查询员工信息 代码开发 这里主要是为了编辑员工时的信息回显传入的是路径参数记得加入path注解返回的信息很多所以用employee来接收 /*** 根据id查询员工信息* param id* return*/GetMapping(/{id})ApiOperation(根据id查询员工信息)public ResultEmployee 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*/PutMappingApiOperation(编辑员工信息)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 ResultListCategory list(Integer type){ListCategory list categoryService.list(type);return Result.success(list);} select idlist resultTypecom.sky.entity.Categoryselect * from categorywhere status 1if testtype ! nulland type #{type}/iforder 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 {BeanConditionalOnMissingBeanpublic 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 ResultString 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*/PostMappingApiOperation(新增菜品)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 idinsert parameterTypecom.sky.entity.Dish useGeneratedKeystrue keyPropertyidinsert 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是否为空如果不空就为ListDishFlavor flavors里的每一个id进行赋值接下来批量注入剩余的flavor信息通过foreach依次为每一个DishFlavor进行插入赋值collection为list名item为形参对象separator为分割符这样就插入了所有的数据。 insert idinsertBatchinsert into dish_flavor (dish_id, name, value) VALUESforeach collectionflavors itemdf separator,(#{df.dishId},#{df.name},#{df.value})/foreach/insert 总体新增菜品的代码如下 Service Slf4j public class DishServiceImpl implements DishService {Autowiredprivate DishMapper dishMapper;Autowiredprivate DishFlavorMapper dishFlavorMapper;/*** 新增菜品和对应的口味数据* param dishDTO*/TransactionalOverridepublic 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条数据ListDishFlavor 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 ResultPageResult 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());PageDishVO page dishMapper.pageQuery(dishPageQueryDTO);return new PageResult(page.getTotal(),page.getResult());} 在XML映射文件里面书写动态sql进行多表查询因为每个表都有name避免重复将category里的name重命名一下之后进行匹配即可。 select idpageQuery resultTypecom.sky.vo.DishVOselect d.*, c.name as categoryName from dish d left outer join category c on d.category_id c.idwhereif testname ! nulland d.name like concat(%, #{name}, %)/ifif testcategoryId ! nulland d.category_id #{categoryId}/ifif teststatus ! nulland d.status #{status}/if/whereorder by d.create_time desc/select 删除菜品 需求分析和设计 起售中的菜品不能删除被套餐关联也不能删除删除菜品后关联的口味数据也删除 代码开发 可以传入一个Long类型的列表到时候springMVC会自动进行处理里面的元素要加入RequsetParam /*** 菜品批量删除* param ids* return*/DeleteMappingApiOperation(菜品批量删除)public Result delete(RequestParam ListLong 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就说明有关联抛出异常。 //断当前菜品是否能够删除--是否被套餐关联ListLong 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 iddeleteByIdsdelete from dish where id inforeach collectionids open( close) itemid#{id}/foreach/delete delete iddeleteByDishIdsdelete from dish_flavor where dish_id inforeach collectiondishIds itemdishId open( close)#{dishId}/foreach/delete 修改菜品 需求分析和设计 根据Id查询菜品 传入的是路径参数所以使用path注解返回的是VO对象 /*** 根据id查询菜品* param id* return*/GetMapping(/{id})ApiOperation(根据id查询菜品)public ResultDishVO 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查询口味数据ListDishFlavor 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})ListDishFlavor getByDishId(Long dishId); 修改菜品 传入的是JSON数据所以要加body注解由于是要修改所以Result并不需要泛型。 /*** 修改菜品* param dishDTO* return*/PutMappingApiOperation(修改菜品)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());//重新插入口味数据ListDishFlavor flavors dishDTO.getFlavors();if (flavors!null !flavors.isEmpty()){flavors.forEach(dishFlavor - {dishFlavor.setDishId(dishDTO.getId());});dishFlavorMapper.insertBatch(flavors);} update idupdateupdate dishsetif testname ! nullname #{name},/ifif testcategoryId ! nullcategory_id #{categoryId},/ifif testprice ! nullprice #{price},/ifif testimage ! nullimage #{image},/ifif testdescription ! nulldescription #{description},/ifif teststatus ! nullstatus #{status},/ifif testupdateTime ! nullupdate_time #{updateTime},/ifif testupdateUser ! nullupdate_user #{updateUser},/if/setwhere id #{id}/update /*** 根据菜品id来删除对应的口味数据* param dishId*/Delete(delete from dish_flavor where dish_id #{dishID})void deleteByDishId(Long dishId); insert idinsertBatchinsert into dish_flavor (dish_id, name, value) VALUESforeach collectionflavors itemdf separator,(#{df.dishId},#{df.name},#{df.value})/foreach/insert 启用禁用菜品 和之前类似不过多赘述 /*** 启用、禁用菜品* param status* param id* return*/PostMapping(/status/{status})ApiOperation(启用禁用分类)public ResultString 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 ResultListDish list(Long categoryId) {log.info(根据分类id{} 查询菜品,categoryId);ListDish list dishService.list(categoryId);return Result.success(list);} 业务层接收的是分类id但是最好把他封装成菜品对象传入套餐id和状态信息交给数据层这样后面也可以根据菜品名来进行查询了。 /*** 根据分类id查询菜品* param categoryId* return*/Overridepublic ListDish list(Long categoryId) {Dish dish Dish.builder().categoryId(categoryId).status(StatusConstant.ENABLE).build();return dishMapper.list(dish);} 数据层通过动态sql在dish表里面查找相应的菜品 select idlist resultTypecom.sky.entity.Dishselect * from dishwhereif testname ! nulland name like concat(%,#{name},%)/ifif testcategoryId ! nulland category_id #{categoryId}/ifif teststatus ! nulland status #{status}/if/whereorder by create_time desc/select 新增套餐 创建一套新的控制器传入的是setmealJson数据加入body注解 /* 套餐管理*/ Slf4j RequestMapping(/admin/setmeal) Api(tags 套餐相关接口) RestController public class SetmealController {Autowiredprivate SetmealService setmealService;/*** 新增套餐* param setmealDTO* return*/PostMappingApiOperation(新增套餐)public Result save(RequestBody SetmealDTO setmealDTO) {log.info(新增套餐);setmealService.saveWithDish(setmealDTO);return Result.success();} } 在业务层里将setmealDTO里的数据传入到setmeal里DTO里面多了一ListSetmealDish setmealDishes 用来表示套餐和菜品之间的联系之后向套餐表里插入数据加入AutoFill注解 insert idinsert parameterTypeSetmeal useGeneratedKeystrue keyPropertyidinsert 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 idinsertBatchinsert into setmeal_dish(setmeal_id, dish_id, name, price, copies)VALUESforeach collectionsetmealDishes itemsd 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();ListSetmealDish setmealDishes setmealDTO.getSetmealDishes();setmealDishes.forEach(setmealDish - {setmealDish.setSetmealId(setmealId);});//保存套餐和菜品的关联关系setmealDishMapper.insertBatch(setmealDishes);} }套餐分页查询 需求分析和设计 代码开发 与前面的分页查询其实类似这里不过多赘述 /*** 分页查询* param setmealPageQueryDTO* return*/GetMapping(/page)ApiOperation(分页查询)public ResultPageResult 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());PageSetmealVO page setmealMapper.pageQuery(setmealPageQueryDTO);return new PageResult(page.getTotal(),page.getResult());} select idpageQuery resultTypecom.sky.vo.SetmealVOselect s.*, c.name categoryNamefrom setmeal s left join category c on s.category_id c.idwhereif testname ! nulland s.name like concat(%, #{name}, %)/ifif teststatus ! nulland s.status #{status}/ifif testcategoryId ! nulland s.category_id #{categoryId}/if/whereorder by s.create_time desc/select 删除套餐 需求和业务分析 控制层里需要加入RequestParam注解以确保spring能够正确的解析传入的id列表 /*** 批量删除套餐* param ids* return*/DeleteMappingApiOperation(批量删除套餐)public Result delete(RequestParam ListLong ids) {setmealService.deleteBatch(ids);return Result.success();} 业务层负责删除套餐如果起售则不能删除遍历套餐表之前用的for循环这里用的foreach其实差不多根据id找到每一个套餐根据状态来判断是否能删除之后就可以分别删除套餐表和套餐菜品关系表中的数据了。 /*** 批量删除套餐* param ids* return*/public void deleteBatch(ListLong 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 ResultSetmealVO 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);ListSetmealDish setmealDishes setmealDishMapper.getBySetmealId(id);SetmealVO setmealVO new SetmealVO();BeanUtils.copyProperties(setmeal,setmealVO);setmealVO.setSetmealDishes(setmealDishes);return setmealVO;} 修改套餐 控制层中传入body对象 /*** 修改套餐* param setmealDTO* return*/PutMappingApiOperation(修改套餐)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());ListSetmealDish 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 ResultString 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)) {ListDish 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})ListDish getBySetmealId(Long setmealId); Redis Spring Date Redis使用方式 1 导入sdr的maven坐标 dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-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 ResultInteger 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 ResultInteger 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 ResultUserLoginVO login(RequestBody UserLoginDTO userLoginDTO){log.info(微信用户登录{},userLoginDTO.getCode());//微信登录User user userService.wxLogin(userLoginDTO);//为微信用户生成jwt令牌MapString, 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向微信地址发送请求MapString, 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 idinsert useGeneratedKeystrue keyPropertyidinsert 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 ResultListDishVO list(Long categoryId) {//构造redis中的key dish_idString key dish_categoryId;//查询redis中是否存在菜品数据ListDishVO list (ListDishVO) 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());ListShoppingCart 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*/ListShoppingCart 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 namespacecom.sky.mapper.ShoppingCartMapperselect idlist resultTypecom.sky.entity.ShoppingCartselect * from shopping_cartwhereif testuserId ! nulland user_id #{userId}/ifif testsetmealId ! nulland setmeal_id #{setmealId}/ifif testdishId ! nulland dish_id #{dishId}/ifif testdishFlavor ! nulland dish_flavor #{dishFlavor}/if/where/select /mapper 查看购物车 需求分析和设计 代码开发 表现层如下 /*** 查看购物车* return*/ApiOperation(查看购物车)GetMapping(/list)public ResultListShoppingCart list(){ListShoppingCart list shoppingCartService.showShoppingCart();return Result.success(list);} 业务层里主要是根据userId来封装一个购物车对象传给Mapper的list中 /*** 查看购物车* return*/public ListShoppingCart 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());ListShoppingCart 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 ResultOrderSubmitVO 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);ListShoppingCart 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);ListOrderDetail 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 namespacecom.sky.mapper.OrderDetailMapperinsert idinsertBatchinsert into order_detail (name, image, order_id, dish_id, setmeal_id, dish_flavor, amount,number)valuesforeach collectionorderDetailList itemod separator,(#{od.name},#{od.image},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},#{od.amount},#{od.number})/foreach/insert /mapper mapper namespacecom.sky.mapper.OrderMapperinsert idinsert useGeneratedKeystrue keyPropertyidinsert 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 ResultOrderPaymentVO 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;} 查询历史订单 分页查询历史订单根据订单状态查询展示订单数据时需要展示的数据包括下单时间订单状态订单金额订单明细。 表现层传入pageNumpagesize和状态信息调用pageQueryUser /*** 历史订单查询** param page* param pageSize* param status 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消* return*/GetMapping(/historyOrders)ApiOperation(历史订单查询)public ResultPageResult page(int page, int pageSize, Integer status){PageResult pageResult orderService.pageQuery4User(page,pageSize,status);return Result.success(pageResult);} 业务层首先进行分页的基本操作之后将用户的id和状态封装在OrderPageQueryDTO里利用分页条件查询得到PageOrders从里面的每一个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);//分页条件查询PageOrders page orderMapper.pageQuery(ordersPageQueryDTO);ListOrderVO list new ArrayList();//查询出订单明细并封装入OrderVO进行响应if (page ! null page.getTotal() 0) {for (Orders orders : page) {Long orderId orders.getId(); //订单id//查询订单明细ListOrderDetail 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*/PageOrders pageQuery(OrdersPageQueryDTO ordersPageQueryDTO); select idpageQuery resultTypeOrdersselect * from orderswhereif testnumber ! null and number!and number like concat(%,#{number},%)/ifif testphone ! null and phone!and phone like concat(%,#{phone},%)/ifif testuserId ! nulland user_id #{userId}/ifif teststatus ! nulland status #{status}/ifif testbeginTime ! nulland order_time gt; #{beginTime}/ifif testendTime ! nulland order_time lt; #{endTime}/if/whereorder by order_time desc/select /*** 根据订单id查询订单明细* param orderId* return*/Select(select * from order_detail where order_id #{orderId})ListOrderDetail getByOrderId(Long orderId); 查询订单详情 代码如下 /*** 查询订单详情* param id* return*/GetMapping(/orderDetail/{id})ApiOperation(查询订单详情)public ResultOrderVO 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);//查询该订单对应的菜品/套餐明细ListOrderDetail 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查询当前订单详情ListOrderDetail orderDetailList orderDetailMapper.getByOrderId(id);//将订单详情对象转换为购物车对象ListShoppingCart 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 idinsertBatch parameterTypelistinsert into shopping_cart(name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)valuesforeach collectionshoppingCartList itemsc 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 ResultPageResult 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());PageOrders page orderMapper.pageQuery(ordersPageQueryDTO);// 部分订单状态需要额外返回订单菜品信息将Orders转化为OrderVOListOrderVO orderVOList getOrderVOList(page);return new PageResult(page.getTotal(), orderVOList);}private ListOrderVO getOrderVOList(PageOrders page) {// 需要返回订单菜品信息自定义OrderVO响应结果ListOrderVO orderVOList new ArrayList();ListOrders 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) {// 查询订单菜品详情信息订单中的菜品和数量ListOrderDetail orderDetailList orderDetailMapper.getByOrderId(orders.getId());// 将每一条订单菜品信息拼接为字符串格式宫保鸡丁*3ListString 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 ResultOrderStatisticsVO 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 ResultOrderVO 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);ListOrders 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);ListOrders 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})ListOrders 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 ResultTurnoverReportVO 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的所有日期ListLocalDate dateList new ArrayList();dateList.add(begin);while (!begin.equals(end)) {//日期计算计算指定日期的后一天对应的日期begin begin.plusDays(1);dateList.add(begin);}ListDouble 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 idsumByMap resultTypejava.lang.Doubleselect sum(amount) from orderswhereif testbegin ! nulland order_time gt; #{begin}/ifif testend ! nulland order_time lt; #{end}/ifif teststatus ! nulland status #{status}/if/where/select 用户统计  用户数量统计与上面的营业额统计十分类似唯一我觉得需要注意的一点就是在插入新增用户和总用户时先查总用户的因为信息较少再在map里加入新的限制查询新增用户 /*** 用户统计* param begin* param end* return*/GetMapping(/userStatistics)ApiOperation(用户统计)public ResultUserReportVO 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的所有日期ListLocalDate dateList new ArrayList();dateList.add(begin);while (!begin.equals(end)) {//日期计算计算指定日期的后一天对应的日期begin begin.plusDays(1);dateList.add(begin);}//每天新增用户数量ListInteger newUserList new ArrayList(); //select count(id) from user where create_time ? and create_time ?//每天总用户数量ListInteger 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 idcountByMap resultTypejava.lang.Integerselect count(id) from userwhereif testbegin ! nulland create_time gt; #{begin}/ifif testend ! nulland create_time lt; #{end}/if/where/select 订单统计 这里代码与上面也十分类似只需要注意要多返回一下具体的数量同时获取数量建议使用stream流的方法即可。 /*** 订单统计* param begin* param end* return*/GetMapping(/ordersStatistics)ApiOperation(订单统计)public ResultOrderReportVO 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的所有日期ListLocalDate dateList new ArrayList();dateList.add(begin);while (!begin.equals(end)) {//日期计算计算指定日期的后一天对应的日期begin begin.plusDays(1);dateList.add(begin);}// 存放每天订单总数ListInteger orderCountList new ArrayList();// 存放每天有效订单数ListInteger 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 idcountByMap resultTypejava.lang.Integerselect count(id) from orderswhereif testbegin ! nulland order_time gt; #{begin}/ifif testend ! nulland order_time lt; #{end}/ifif teststatus ! nulland status #{status}/if/where/select 订单销量排名 这里要查询订单表和订单信息表因为订单信息表里没有说明当前这个订单是否完成其余类似 /*** 销量排名top10* param begin* param end* return*/GetMapping(/top10)ApiOperation(销量排名top10)public ResultSalesTop10ReportVO 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);ListGoodsSalesDTO salesTop10 orderMapper.getSalesTop(beginTime, endTime);ListString names salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());String nameList StringUtils.join(names, ,);ListInteger numbers salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());String numberList StringUtils.join(numbers, ,);//封装返回结果数据return SalesTop10ReportVO.builder().nameList(nameList).numberList(numberList).build();} select idgetSalesTop resultTypecom.sky.dto.GoodsSalesDTOselect od.name, sum(od.number) numberfrom order_detail od, orders owhere od.order_id o.id and o.status 5if testbegin ! nulland o.order_time gt; #{begin}/ifif testend ! nulland o.order_time lt; #{end}/ifgroup 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.pierceye.com/news/428688/

相关文章:

  • 主机屋免费网站空间网站建设安全标准
  • .net cms网站管理系统生态养殖网站模板
  • 国外网站一般公司网站是什么设计师做
  • 备案停止网站网站建设研究课题
  • 个人网站开发是学什么语言网站建设 总体思路
  • 网站怎么留住用户在什么地方可以接到做网站的活
  • jsp页面如何做网站pv统计wordpress4.7.10
  • 澄海网站建设用asp做网站怎么美观
  • 未满18岁能申请网站备案吗网站做qq链接
  • 网络公司网站开发互联网技术专业学什么
  • 校园网站建设详细的设计方案php网站开发实战
  • 网站设计广州量计价格网站可信度建设
  • 门业网站 源码做企业网站必须要座机吗
  • 广东网站开发推荐wordpress快速加载
  • php网站开发外包北京网站建设迈程网络
  • 微信开发者平台取消授权seo资源网站排名
  • 将网站源码下载下来如何使用网站seo链接购买
  • 可信网站认证必须做苏州那里可以建网站
  • 手机网站底部代码有名的网站建设
  • 做一个网站需要多长时间网站制作有哪些种类
  • 做微信推送的网站小刘网站建设
  • 18款禁用软件app网站入口台州网站推广排名
  • 网站的服务内容济南网站制作方案
  • 微网站模板 phpwordpress 支付宝
  • wordpress dux主题破解安装全屏网站 图片优化
  • 一键建站公司做网站 怎么做留言
  • 制作的网站西安网页设计培训哪里有
  • 株洲市住房和城乡建设局门户网站中国建设银行官网站大同
  • 北京响应式网站制作公司wordpress邀请码注册
  • 衡阳网站开发有哪些公司怎么建设淘客自己的网站、