软件开发流程
需求分析:说明书和原型
设计:UI,数据库,接口设计
编码:项目代码,单元测试
测试:测试用例,测试报告
上线运维:软件环境安装,配置
软件环境
开发环境:本地电脑环境,外部用户无法访问
测试环境:测试人员测试项目,测试服务器
生产环境:正式对外提供服务的环境
苍穹外卖项目介绍
技术选型
项目结构
为什么直接给出来而不是从零开始写呢,因为在公司里也不可能让你造轮子的
数据库
前后端联调
登录过程:
执行启动项以后,进入EmployeeController,执行login方法,接收前端传进来的数据employeeLoginDTO(数据传输对象),打印一个员工登录日志,此时调用employeeService的login函数,传入刚才的DTO。
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
通过实体类,调用employeServiceImpl的login函数,接收DTO,通过@AutoWired注入employeeMapper的bean。利用DTO的get和set方法得到输入的用户名和密码,调用employee的getByUsername来根据用户名查询员工。
@Service 写在实现类里
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
/**
* 员工登录
*
* @param employeeLoginDTO
* @return
*/
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();
//1、根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);
从mysql数据库中寻找这个用户名信息的数据,以Employee的形式返回给Service
@Select("select * from employee where username = #{username}")
Employee getByUsername(String username);
接下来返回到Service层里接收employee,处理各种异常情况,如过Employee为空,说明没有从sql里找到数据,返回异常。接着比对密码,如果输入的密码不等于从数据库里拿出来的密码,也返回异常,如果账号的状态是锁定,也返回异常,都不是的话,说明账号是对的,返回实体对象,回到Controller中。
if (employee == null) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
//密码比对
// TODO 后期需要进行md5加密,然后再进行比对
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (employee.getStatus() == StatusConstant.DISABLE) {
//账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
//3、返回实体对象
return employee;
接着生成jwt令牌,在里面传入想传入的数据如empid,利用JwtUtil(已封装好)方法,传入秘钥,过期时间,以及刚才生成的claims(利用@ConfigurationProperties生成一个配置属性类,与yml文件相连接,得到对应的参数,令牌生成成功)
@Component
@ConfigurationProperties(prefix = "sky.jwt") //配置属性类,封装配置项,把yml里的数据传进来
@Data
public class JwtProperties {
/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
令牌生成以后,生成一个视图对象VO返回给前端,利用@Builder来创建出一个employeeLoginVO,以result形式返回给前端
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
为什么要通过nginx连接前端和后端呢,前后url一样不好吗?
密码加密
md5加密处理,如果数据库被偷也问题不大了
password = DigestUtils.md5DigestAsHex(password.getBytes());
项目接口文档
Yapi是设计阶段使用的工具,管理和维护接口
Swagger用来代替postman,在开发阶段使用的框架,帮助后端开发人员做后端的接口测试
@Bean
public Docket docket() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
Api后面加tags=,ApiModel后面加description=
新增员工
代码开发
当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据,调用业务层,传入DTO即可,一般新增员工用post方法,同时由于前端传递过来的是json对象,所以要加一个@RequsetBody注解才能将其转换为DTO类
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO){
log.info("新增员工:{}",employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}
接下来是业务层的逻辑,重写接口的sava方法,注意由于控制层传入的是前端发送过来的DTO对象,但是要给Mapper传入的最好是实体类对象,所以最好进行一下转换,这里需要new一个对象,如果一个一个的把DTO传入到实体类里,可能会比较麻烦,所以这里我们使用对象属性拷贝BeanUtils.copyProperties(employeeDTO,employee); 剩下还有一些数据再单独加入(这里创建人和修改人的id逻辑后面再处理,先todo)
@Override
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//对象属性拷贝 , 前提属性名一致
BeanUtils.copyProperties(employeeDTO,employee);
//设置账号状态 默认正常 1正常 0锁定
employee.setStatus(StatusConstant.ENABLE); //用常量类,不要硬编码
//设置密码,默认密码123456
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//设置当前记录创建人id和修改人id
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.insert(employee);
}
Mapper层里由于逻辑比较简单,所以直接插入即可
@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status)" +
"values" +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
void insert(Employee employee);
代码优化
重复员工异常
如果增加的两个username相同,按照sql里的设定,就一定会报错,我们不想让他直接报错,而是给出一定的响应,这就需要在全局异常处理器里面进行设定。在server目录下的handler包里设置一个全局异常处理器,加入@RestController注解。
@RestControllerAdvice 是 Spring Framework 为我们提供的一个复合注解,它是 @ControllerAdvice 和 @ResponseBody 的结合体。
@ControllerAdvice:该注解标志着一个类可以为所有的 @RequestMapping 处理方法提供通用的异常处理和数据绑定等增强功能。当应用到一个类上时,该类中定义的方法将在所有控制器类的请求处理链中生效。
@ResponseBody:表示方法的返回值将被直接写入 HTTP 响应体中,通常配合 Jackson 或 Gson 等 JSON 库将对象转换为 JSON 格式的响应。
因此,@RestControllerAdvice 就是专门为 RESTful 控制器设计的全局异常处理器,它的方法返回值将自动转换为响应体。
同时在每个异常上面加@ExceptionHandler注解,进行函数重载接收异常,对于上面的sql异常,可以如下处理:
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
// Duplicate entry 'zhangsan' for key 'employee.idx_username'
String message = ex.getMessage();
if (message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
需要注意的是尽量用常量来表示字符串,不要硬编码。通过以上处理,就可以在接受异常时返回一个Result,里面传入的就是异常信息msg。
创建修改人ID处理
上面没有处理创建人和修改人的id,那该如何获取呢?
这是前后端进行交互的大致流程,可以看到在拦截请求验证时,我们就可以读到jwt令牌中我们当时传入过的id了(之前在控制层实现的)
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
我们会在JwtTokenAdminInterceptor(注意要加上component才行)里根据获取的jwt进行拦截操作,显然可以在这里得到token里的id信息,但是要如何传入到业务层里呢,我们可以调用threadLocal方法,一次操作中的线程是同一个,里面的数据是连通的,为了方便起见,把threadLocal封装在common的context里,需要时进行调用:
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
所以在校验以后获得empId,调用里面的set方法即可将id放入其中,同时在业务层里用get方法取出id即可。
员工分页查询
代码开发
根据接口文档,可以看出要接受的是Query参数,并不是json,所以不需要加@RequestBody,而因为传过来的只有那三个参数,所以我们特意封装出来一个类EmployeePageQueryDTO用来解决这个问题,看接口文档里要返回的数据里的data项,我们又设计一个pageResult类来封装:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
最后将这个对象封装到success中返回即可:
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
log.info("员工分页查询,参数为:{}",employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}
接下来,我们引入PageHelper依赖,进行分页查询的操作,传入页数和页面大小,调用mapper层的分页查询函数(已自动优化,返回的是一个Page<Employee>对象page),利用getTotal函数得到页数,getResult函数得到其他所有的信息(是一个list),最后利用PageResult的有参构造封装成能传入给success的对象。
/**
* 分页查询
*
* @param employeePageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total, records);
}
接下来是Mapper层的逻辑,只需要模糊匹配且按照创建时间排序即可,无需自己计算页数之类的东西,以及limit方法,PageHelper会自动调整好
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
</where>
order by create_time desc
</select>
时间优化
在进行测试时,我们肯能发现显示的时间并不是想要的那种格式(可能是Page的原因),在这里有两种处理方法,这里比较推荐第二种。
1. 设置@JsonFormat注解,可控制该属性在序列化为json时的字符串表示形式,缺点是每一个想要加的元素都需要一个这种注解。
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
2. 在配置类里面扩展SpringMVC框架的消息转换器,创建消息转换器对象,然后设置一个对象转换器(参数已经定义好了,在common里),最后将自己的消息加入到容器中,前面加0表示最优先。
/**
* 扩展SpringMVC框架的消息转化器
*
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,可以将java对象序列号为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入的容器中
converters.add(0, converter);
}
启用禁用员工账号
代码开发
根据接口的信息,我们要传入一个路径参数status,在前面加入@PathVariable注解,和一个id参数用来作为判断判断员工的条件,因为是修改所以用Post提交:
/**
* 启用禁用员工账号
* @param id
* @param status
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result startOrStop( @PathVariable Integer status,Long id) {
log.info("启用禁用员工账号,{},{}",status,id);
employeeService.startOrStop(status,id);
return Result.success();
}
在Service层中,直接将id和status传给Mapper其实不太好,因为完全可以制作一个修改所有属性的动态sql,所以最好传入一个emp对象,可以用get/set方法,但是由于在emp上面加了一个@builder注解以可以用build方法
/**
* 启用禁用员工账号
* @param status
* @param id
*/
@Override
public void startOrStop(Integer status, Long id) {
//update employee set status = ? where id = ?
// Employee employee = new Employee();
// employee.setStatus(status);
// employee.setId(id);
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();
employeeMapper.update(employee);
}
在Mapper层中,动态sql如下,set可以用<set>忽略逗号的错误;
<update id="update" parameterType="com.sky.entity.Employee">
update employee
<set>
<if test="name != null">name = #{name},</if>
<if test="username != null">username = #{username},</if>
<if test="password != null">password = #{password},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="sex != null">sex = #{sex},</if>
<if test="idNumber != null">id_Number = #{idNumber},</if>
<if test="updateTime != null">update_Time = #{updateTime},</if>
<if test="updateUser != null">update_User = #{updateUser},</if>
<if test="status != null">status = #{status},</if>
</set>
where id = #{id}
</update>
编辑员工
根据id查询员工信息
代码开发
这里主要是为了编辑员工时的信息回显,传入的是路径参数,记得加入path注解,返回的信息很多,所以用employee来接收
/**
* 根据id查询员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<Employee> getById(@PathVariable Long id){
Employee employee = employeeService.getById(id);
return Result.success(employee);
}
业务层接受id传入Mapper返回employee对象,但是要注意这里最好把密码给抹掉,否则可以通过f12来查看造成密码泄露,后面的Mapper层比较简单,select即可
/**
* 根据id查询员工信息
* @param id
* @return
*/
@Override
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
employee.setPassword("****");
return employee;
}
编辑员工信息
这里要更新参数选择PutMapping,同时传入的是一个实体DTO,由于前端传过来的是一个json,所以要加入@RequsetBody注解
/**
* 编辑员工信息
* @param employeeDTO
* @return
*/
@PutMapping
@ApiOperation("编辑员工信息")
public Result update(@RequestBody EmployeeDTO employeeDTO){
log.info("编辑员工信息,{}",employeeDTO);
employeeService.update(employeeDTO);
return Result.success();
}
业务层接受一个DTO,需要传递给Mapper的update函数,但是它只能接受employee对象,所以要转换一下,这里还是用那个拷贝方法,同时加入更新时间,和更新人id(这个用之前的方法,不做解释),最后调用上面创建的updat。
@Override
public void update(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO,employee);
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.update(employee);
}
分类管理功能
这里和上面的逻辑基本差不多,直接从文件夹里导入即可
公共字段自动填充
自定义注解AutoFill,用于标识需要进行公共字段自动填充的方法
自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
在Mapper的方法上加入AutoFill注解
首先自定义注解,注解二件套加上,同时注解里面有属性value,分别用枚举类update和insert表示,到时候用来区分注解。
/**
* 自定义注解,用于标识某个方法需要自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
自定义切面类,切面类上面要有@Aspect注解,@Component注解,为了写日志可以加一个Slf4j注解。定义一个切入点@PointCut,里面利用execution和annotation来找到要扫描的方法。因为要在sql之前加入时间和id之类的信息,所以用前置通知@Before,,传入joinpoint,分别得到方法签名对象,注解对象,注解参数对象,最后通过joinPoint.getArgs得到被拦截方法的参数,也就是emp对象,取出里面的第一个(虽然只有一个)。之后准备赋值的数据,根据不同的操作类型(update和insert分别选择方法的调用,也就是emp的get/set方法),分别选出对应的方法即可。
/**
* 自定义切面,实现公共字段自动填充
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
//切入点
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
//前置通知,在通知中进行公共字段的赋值
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段自动填充...");
//获取当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //方法签名对象 EmployeeMapper.update
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); //获得方法上的注解对象 AutoFill(value = UPDATE)
OperationType operationType = autoFill.value(); //获取数据库操作类型 UPDATE
//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs(); //返回一个长度为1的数组
if (args == null || args.length == 0){
return;
}
Object entity = args[0];
//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据不同操作类型,为对应的属性赋值
if (operationType == OperationType.INSERT) {
//为四个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
} else if(operationType == OperationType.UPDATE) {
//为两个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
最后在每个想要调用切面的方法加入AutoFill注解即可!
新增菜品
根据类型查询分类
/**
* 根据类型查询分类
* @param type
* @return
*/
@GetMapping("/list")
@ApiOperation("根据类型查询分类")
public Result<List<Category>> list(Integer type){
List<Category> list = categoryService.list(type);
return Result.success(list);
}
<select id="list" resultType="com.sky.entity.Category">
select * from category
where status = 1
<if test="type != null">
and type = #{type}
</if>
order by sort asc,create_time desc
</select>
文件上传
首先要在yml里面配置阿里云oss的相关参数,这里不要直接在主yml里面赋值,而是要在dev里面加入,方便到时候换用户时将sping.profiles.active.dev改掉即可:
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
紧接着要配置属性类,类似于jwt令牌,要有@Data注解(get/set方法),@Component注解(要变成bean),以及@ConfigurationProperties(prefix = "sky.alioss")
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
定义一个文件上传的工具类AliOssUtil,里面的属性就是上面这四个,同时定义一个upload方法,能够返回一个地址,点击这个网址就能够看到上传的文件了
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
但是我们这是一个springboot项目,必须要让这个工具类自动启动才好,所以这时候再定义一个配置类,用于创建AliOssUtil对象,配置类都要加入@Configuration注解来保证是个配置类,里面定义一个返回值为AliOssUtil的方法,传入的就是刚才定义的那个aliOssProperties(已经加了Component),然后利用有参构造函数返回一个对象即可,注意上面要加入@Bean注解,这样项目启动的时候就能将参数注入创建一个工具类对象,这里最好加一个@ConditionalOnMissingBean,保证整个spring容器最多只有一个util对象。
/**
* 配置类,用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
最后,就可以定义上传文件的控制器了,根据接口文档,需要返回一个String里面记录了文件的请求路径。控制层传入的参数为文件的固定类型MultipartFile 制作一个新的文件名避免重复,利用util里的upload函数,传入文件数组和新的文件名,得到请求路径返回即可。
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@ApiOperation("文件上传")
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原神文件名的后缀 fsdf.png
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
//构造新文件名称
String objectName = UUID.randomUUID().toString() + extension;
//文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}",e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
新增菜品
这里面有要处理两张表的数据:菜品表和口味表,两张表通过逻辑外键进行连接
首先编写控制层,。传入的事dishDTO数据(包含原有的dish参数外加了一个口味列表flavors),因为这里是改变数据所以不需要Result的泛型。
/**
* 菜品管理
*/
@RestController
@RequestMapping("/admin/dish")
@Slf4j
@Api(tags = "菜品相关接口")
public class DishController {
@Autowired
private DishService dishService;
/**
* 新增菜品
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}",dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}
在业务层里,我们分两块来处理,一部分是向菜品表插入一个数据,还有就是向口味表插入n条数据,这两项必须同时提交,所以形成了一个事物,方法上面加入@Transaction注解。
1 向菜品表插入一条数据
因为控制层传入的是DTO,我们不需要flavor参数,所以创建一个dish对象传到DIshMapper层中,因为是插入,所以加入前面的@AutoFill注解,在xml映射文件里面进行insert操作即可,在这里要进行一下逐渐返回,将主键的值传回给id,后面会用到。
<insert id="insert" parameterType="com.sky.entity.Dish" useGeneratedKeys="true" keyProperty="id">
insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
VALUES
(#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})
</insert>
2 向口味表插入n条数据
取出前端传过来的dishDTO,取出里面的flavor属性,因为dishId并不是自增而是和菜品表的id逻辑外键,所以需要自行加入,刚才通过主键返回取出了dish的id赋值给dishId,之后进行判断前端传入的flavors是否为空,如果不空,就为List<DishFlavor> flavors里的每一个id进行赋值,接下来批量注入剩余的flavor信息,通过<foreach>,依次为每一个DishFlavor进行插入赋值,collection为list名,item为形参对象,separator为分割符,这样就插入了所有的数据。
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) VALUES
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
总体新增菜品的代码如下:
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
/**
* 新增菜品和对应的口味数据
* @param dishDTO
*/
@Transactional
@Override
public void saveWithFlavor(DishDTO dishDTO) {
//DTO里面还有口味,没必要,所以传入一个Dish对象
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//1 向菜品表插入一条数据
dishMapper.insert(dish);
//获取insert语句生成的主键值
Long dishId = dish.getId();
//2 向口味表插入n条数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors!=null && !flavors.isEmpty()){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
dishFlavorMapper.insertBatch(flavors);
}
}
}
菜品分页查询
代码开发
在控制层中,传入的是一个DTO,里面包含了前端传入的数据,因为是Query,也就是地址栏问号传参,所以传过来的并不是json格式,所以不需要加body注解,返回的类型是一个PageResult格式。
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
@ApiOperation("菜品分页查询")
@GetMapping("/page")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品分页查询:{}",dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
业务层还是之前的分页查询逻辑,注意Page的泛型(也就是要返回前端的类型)是VO类型,因为还要显示菜品的分类,而普通的dish里面并没有。
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
在XML映射文件里面书写动态sql,进行多表查询,因为每个表都有name,避免重复将category里的name重命名一下,之后进行匹配即可。
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.*, c.name as categoryName from dish d left outer join category c on d.category_id = c.id
<where>
<if test="name != null">and d.name like concat('%', #{name}, '%')</if>
<if test="categoryId != null">and d.category_id = #{categoryId}</if>
<if test="status != null">and d.status = #{status}</if>
</where>
order by d.create_time desc
</select>
删除菜品
需求分析和设计
起售中的菜品不能删除,被套餐关联也不能删除,删除菜品后关联的口味数据也删除
代码开发
可以传入一个Long类型的列表,到时候springMVC会自动进行处理里面的元素(要加入@RequsetParam)
/**
* 菜品批量删除
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids){
log.info("菜品批量删除:{}",ids);
dishService.deleteBatch(ids);
return Result.success();
}
在业务层中,先要判断菜品是否能够删除,首先,如果起售,那么就不可以删除,遍历传入的菜品id列表,调用Mapper层中的方法,返回菜品,如果菜品的状态是起售,那么就抛出异常
//判断当前菜品是否能够删除--是否存在起售中的菜品??
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if (Objects.equals(dish.getStatus(), StatusConstant.ENABLE)) {
//当前菜品处于起售中,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
/**
* 根据主键查询菜品
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
再判断一下菜品是否绑定了套餐,这里用setmealDishMapper.getSetmealIdsByDishids(ids)返回一个列表了里面装的都是setmeal_id,如果这些菜品里面找到了setmeal_id就说明有关联,抛出异常。
//断当前菜品是否能够删除--是否被套餐关联??
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishids(ids);
if (setmealIds != null && !setmealIds.isEmpty()) {
//当前菜品被套餐关联了
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
排除了以上的情况,就可以安全的删除菜品数据了,可以遍历取出所有的id,然后进行删除,同时也要把口味给删除
//删除菜品表中的菜品数据
// for (Long id : ids) {
// dishMapper.deleteById(id);
// //删除菜品关联的口味数据
// dishFlavorMapper.deleteByDishId(id);
// }
/**
* 根据主键删除id
* @param id
*/
@Delete("delete from dish where id = #{id}")
void deleteById(Long id);
/**
* 根据菜品id来删除对应的口味数据
* @param dishId
*/
@Delete("delete from dish_flavor where dish_id = #{dishID}")
void deleteByDishId(Long dishId);
代码优化
最后删除菜品数据时,要进行遍历取出菜品,进行一次sql删除,如果数量过多,一定会对性能产生影响,所以我们直接每次用一条sql语句,传入的是ids。
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" open="(" close=")" item="id">
#{id}
</foreach>
</delete>
<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" item="dishId" open="(" close=")">
#{dishId}
</foreach>
</delete>
修改菜品
需求分析和设计
根据Id查询菜品
传入的是路径参数所以使用path注解,返回的是VO对象
/**
* 根据id查询菜品
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询菜品")
public Result<DishVO> getById(@PathVariable Long id){
log.info("根据id查询菜品:{}",id);
DishVO dishVO = dishService.getByIdWithFlavor(id);
return Result.success(dishVO);
}
分别根据id取出dish的信息以及dishFlavors的信息,将所有的信息封装到dishVO对象中,注意此时其实并没有类别信息对象,但是有类别id,这一点由前端实现了。
@Override
public DishVO getByIdWithFlavor(Long id) {
//根据id查询菜品数据
Dish dish = dishMapper.getById(id);
//根据菜品id查询口味数据
List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);
//将查询到的数据封装到dishVO
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish, dishVO);
dishVO.setFlavors(dishFlavors);
return dishVO;
}
/**
* 根据主键查询菜品
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
/**
* 根据菜品id查询对应的口味数据
* @param dishId
* @return
*/
@Select("select * from dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishId(Long dishId);
修改菜品
传入的是JSON数据,所以要加body注解,由于是要修改所以Result并不需要泛型。
/**
* 修改菜品
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品;{}",dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
由于传入的是DTO,但是我们并不需要这些信息,所以将他转换成dish会更好,修改菜品分为两步,一个是修改基本信息,一个是修改口味,基本信息比较简单,修改口味分为两步,删除之前所有口味之后再重新插入口味。
/**
* 根据id修改菜品和口味信息
* @param dishDTO
*/
@Override
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//修改菜品表基本信息
dishMapper.update(dish);
//删除原有的口味数据
dishFlavorMapper.deleteByDishId(dishDTO.getId());
//重新插入口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors!=null && !flavors.isEmpty()){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishDTO.getId());
});
dishFlavorMapper.insertBatch(flavors);
}
<update id="update">
update dish
<set>
<if test="name != null">name = #{name},</if>
<if test="categoryId != null">category_id = #{categoryId},</if>
<if test="price != null">price = #{price},</if>
<if test="image != null">image = #{image},</if>
<if test="description != null">description = #{description},</if>
<if test="status != null">status = #{status},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateUser != null">update_user = #{updateUser},</if>
</set>
where id = #{id}
</update>
/**
* 根据菜品id来删除对应的口味数据
* @param dishId
*/
@Delete("delete from dish_flavor where dish_id = #{dishID}")
void deleteByDishId(Long dishId);
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) VALUES
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
启用禁用菜品
和之前类似,不过多赘述:
/**
* 启用、禁用菜品
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用分类")
public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
dishService.startOrStop(status,id);
return Result.success();
}
/**
* 启用、禁用菜品
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
Dish dish = Dish.builder()
.status(status)
.id(id)
.build();
dishMapper.update(dish);
}
新增套餐
需求分析和设计
接口设计(共涉及到4个接口):
-
根据类型查询分类(已完成)
-
根据分类id查询菜品
-
图片上传(已完成)
-
新增套餐
根据分类id查询菜品
这里是要在新增套餐的时候,通过选择分类,在里面显示出能够添加的菜品,返回结果是一个菜品列表,效果如下:
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId) {
log.info("根据分类id:{} 查询菜品",categoryId);
List<Dish> list = dishService.list(categoryId);
return Result.success(list);
}
业务层接收的是分类id,但是最好把他封装成菜品对象,传入套餐id和状态信息交给数据层,这样后面也可以根据菜品名来进行查询了。
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
@Override
public List<Dish> list(Long categoryId) {
Dish dish = Dish.builder()
.categoryId(categoryId)
.status(StatusConstant.ENABLE)
.build();
return dishMapper.list(dish);
}
数据层通过动态sql在dish表里面查找相应的菜品:
<select id="list" resultType="com.sky.entity.Dish">
select * from dish
<where>
<if test="name != null">
and name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
order by create_time desc
</select>
新增套餐
创建一套新的控制器,传入的是setmealJson数据,加入body注解
/*
套餐管理
*/
@Slf4j
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@RestController
public class SetmealController {
@Autowired
private SetmealService setmealService;
/**
* 新增套餐
* @param setmealDTO
* @return
*/
@PostMapping
@ApiOperation("新增套餐")
public Result save(@RequestBody SetmealDTO setmealDTO) {
log.info("新增套餐");
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
}
在业务层里,将setmealDTO里的数据传入到setmeal里(DTO里面多了一List<SetmealDish> setmealDishes 用来表示套餐和菜品之间的联系),之后向套餐表里插入数据,加入AutoFill注解
<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
insert into setmeal
(category_id, name, price, description, image, create_time, update_time, create_user, update_user)
VALUES
(#{categoryId},#{name},#{price},#{description},#{image},#{createTime},#{updateTime},#{createUser},#{updateUser})
</insert>
通过主键返回获取生成的套餐id传入给套餐菜品关联属性的套餐id,这样套餐和菜品的id就能够对应上,最后保存套餐和菜品之间的关联关系
<insert id="insertBatch">
insert into setmeal_dish
(setmeal_id, dish_id, name, price, copies)
VALUES
<foreach collection="setmealDishes" item="sd" separator=",">
(#{sd.setmealId}, #{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
</foreach>
</insert>
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {
@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private DishMapper dishMapper;
/**
* 新增套餐同时需要保存套餐和菜品的关联关系
* @param setmealDTO
*/
@Override
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO,setmeal);
//向套餐表插入数据
setmealMapper.insert(setmeal);
//获取生成的套餐id
Long setmealId = setmeal.getId();
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//保存套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);
}
}
套餐分页查询
需求分析和设计
代码开发
与前面的分页查询其实类似,这里不过多赘述
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("分页查询")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
log.info("分页查询:{}",setmealPageQueryDTO);
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
PageHelper.startPage(setmealPageQueryDTO.getPage(),setmealPageQueryDTO.getPageSize());
Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select s.*, c.name categoryName
from setmeal s left join category c on s.category_id = c.id
<where>
<if test="name != null">and s.name like concat('%', #{name}, '%')</if>
<if test="status != null">and s.status = #{status}</if>
<if test="categoryId != null">and s.category_id = #{categoryId}</if>
</where>
order by s.create_time desc
</select>
删除套餐
需求和业务分析
控制层里需要加入@RequestParam注解,以确保spring能够正确的解析传入的id列表
/**
* 批量删除套餐
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids) {
setmealService.deleteBatch(ids);
return Result.success();
}
业务层负责删除套餐,如果起售,则不能删除,遍历套餐表(之前用的for循环,这里用的foreach,其实差不多),根据id找到每一个套餐,根据状态来判断是否能删除,之后就可以分别删除套餐表和套餐菜品关系表中的数据了。
/**
* 批量删除套餐
* @param ids
* @return
*/
public void deleteBatch(List<Long> ids) {
ids.forEach(id -> {
Setmeal setmeal = setmealMapper.getById(id);
if (setmeal.getStatus().equals(StatusConstant.ENABLE)) {
//起售中的菜品不能删除
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}
});
ids.forEach(setmealId -> {
//删除套餐表中的数据
setmealMapper.deleteById(setmealId);
//删除套餐菜品关系表中的数据
setmealDishMapper.deleteBySetmealId(setmealId);
});
}
//SetmealMapper
/**
* 根据id查询套餐
* @param id
* @return
*/
@Select("select * from setmeal where id = #{id}")
Setmeal getById(Long id);
/**
* 根据id删除套餐
* @param id
*/
@Delete("delete from setmeal where id = #{id}")
void deleteById(Long id);
//SetmealDishMapper
/**
* 根据套餐id删除套菜和菜品的关联关系
* @param setmealId
*/
@Delete("delete from setmeal_dish where setmeal_id = #{setmealId}")
void deleteBySetmealId(Long setmealId);
修改套餐
需求分析和设计
-
根据id查询套餐
-
根据类型查询分类(已完成)
-
根据分类id查询菜品(已完成)
-
图片上传(已完成)
-
修改套餐
根据Id查询套餐
点击修改套餐后,会什么都没有,要在页面回显出以下效果:
/**
* 根据id查询套餐
* @param id
* @return
*/
@ApiOperation("根据id查询套餐")
@GetMapping("/{id}")
public Result<SetmealVO> getById(@PathVariable Long id) {
SetmealVO setmealVO = setmealService.getByIdWithDish(id);
return Result.success(setmealVO);
}
业务层里,首先根据id得到对应的套餐,之后根据id得到套餐菜品关系表里面的数据,建立一个要返回的VO对象,分别吧套餐数据和关系表的数据传入进去再返回即可。
/**
* 根据id查询套餐和套餐菜品关系
* @param id
* @return
*/
public SetmealVO getByIdWithDish(Long id) {
Setmeal setmeal = setmealMapper.getById(id);
List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(id);
SetmealVO setmealVO = new SetmealVO();
BeanUtils.copyProperties(setmeal,setmealVO);
setmealVO.setSetmealDishes(setmealDishes);
return setmealVO;
}
修改套餐
控制层中传入body对象:
/**
* 修改套餐
* @param setmealDTO
* @return
*/
@PutMapping
@ApiOperation("修改套餐")
public Result update(@RequestBody SetmealDTO setmealDTO) {
setmealService.update(setmealDTO);
return Result.success();
}
业务层里逻辑比较多,首先要将传入的DTO变回setmeal,利用update传入setmeal的基本数据,之后删除套餐和菜品的关联关系,再将新的关联信息一个一个的存入到setmealdisher里,最后进行批量的插入即可。
/**
* 修改套餐
* @param setmealDTO
* @return
*/
public void update(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO,setmeal);
//修改套餐表,执行update,插入基本数据
setmealMapper.update(setmeal);
//删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
setmealDishMapper.deleteBySetmealId(setmealDTO.getId());
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealDTO.getId());
});
//重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
setmealDishMapper.insertBatch(setmealDishes);
}
起售停售套餐
需求分析和设计
代码开发
与之前的起售停售相比,多了一个包含禁售菜品不能启用套餐的规定,,从套餐中拿出所有的菜品,如果菜品的状态是0,那么就得抛异常了
/**
* 启用、禁用套餐
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用套餐")
public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
setmealService.startOrStop(status,id);
return Result.success();
}
/**
* 起售禁售套餐
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
//起售套餐时如果里面有停售菜品,就要抛出异常
if (status.equals(StatusConstant.ENABLE)) {
List<Dish> dishList = dishMapper.getBySetmealId(id);
if (dishList != null && dishList.size() > 0) {
dishList.forEach(dish -> {
if (dish.getStatus().equals(StatusConstant.DISABLE)) {
throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
}
});
}
}
Setmeal setmeal = Setmeal.builder()
.id(id)
.status(status)
.build();
setmealMapper.update(setmeal);
}
/**
* 根据套餐id查询菜品
* @param setmealId
* @return
*/
@Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
List<Dish> getBySetmealId(Long setmealId);
Redis
Spring Date Redis使用方式
1 导入sdr的maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2 配置redis数据源
spring:
redis:
host: localhost
port: 6379
password: 123456
database: 0
3 编写配置类,创建RedisTemplate对象
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建redis模版对象");
RedisTemplate redisTemplate = new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
4 通过RedisTemplate对象操作Redis
@SpringBootTest
public class SpringDataRedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testRedisTemplate() {
System.out.println(redisTemplate);
}
}
店铺营业状态设置
需求分析和设计
代码开发
由于店铺的营业状态只有营业中和打样中,没有必要创建mysql表格,这里利用redis缓存来实现,直接注入RedisTemplate即可
@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {
public static final String KEY = "SHOP_STATUS";
@Autowired
private RedisTemplate redisTemplate;
/**
* 设置店铺的营业状态
* @param status
* @return
*/
@PutMapping("/{status}")
@ApiOperation("设置店铺的营业状态")
public Result setStatus(@PathVariable Integer status) {
log.info("设置店铺的营业状态为:{}",status == 1 ? "营业中":"打样中");
redisTemplate.opsForValue().set(KEY,status);
return Result.success();
}
/**
* 获取店铺的营业状态
* @return
*/
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus() {
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("获取到店铺的营业状态为:{}",status == 1 ? "营业中":"打样中");
return Result.success(status);
}
}
用户端的代码和第二段代码基本一样,唯一需要注意的就是两个Controller的名字最好不要一样,否则bean会重复,这里重新命名。
@RestController("userShopController")
@RequestMapping("/user/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {
public static final String KEY = "SHOP_STATUS";
@Autowired
private RedisTemplate redisTemplate;
/**
* 获取店铺的营业状态
* @return
*/
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus() {
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("获取到店铺的营业状态为:{}",status == 1 ? "营业中":"打样中");
return Result.success(status);
}
}
接口文档优化
现在的管理层和用户层的接口文档放在了一起不好区分,所以在配置时要去分开,主要就是两个url里进行了区分,同时加了一个groupName建立名字。
@Bean
public Docket docket1() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("管理端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
.paths(PathSelectors.any())
.build();
return docket;
}
@Bean
public Docket docket2() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("用户端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
.paths(PathSelectors.any())
.build();
return docket;
}
HTTPClient
@SpringBootTest
public class HttpClientTest {
/**
* 通过Httpsclient发送get请求
*/
@Test
public void testGet() throws IOException {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
//发送请求 接受响应结果
CloseableHttpResponse response = httpClient.execute(httpGet);
//获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("服务端返回的状态码为:"+statusCode);
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("服务端返回的数据为:"+body);
//关闭资源
response.close();
httpClient.close();
}
/**
* 通过Httpsclient发送post请求
*/
@Test
public void testPOST() throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");
StringEntity entity = new StringEntity(jsonObject.toString());
//指定编码方式
entity.setContentEncoding("utf-8");
//数据格式
entity.setContentType("application/json");
httpPost.setEntity(entity);
//发送请求
CloseableHttpResponse response = httpClient.execute(httpPost);
//解析返回结果
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应码为:"+statusCode);
HttpEntity entity1 = response.getEntity();
String body = EntityUtils.toString(entity1);
System.out.println("响应数据为:"+body);
//关闭资源
response.close();
httpClient.close();
}
微信小程序
总得来说,小程序通过wx.login获取code,并发送给后端,后端将四个数据发送给微信接口服务,返回一些数据,其中最重要的就是openid,后端将token之类的数据返回给小程序,这时两端就可以进行连通了。
登录功能
需求分析和设计
代码开发
配置文件:
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
user-secret-key: itheima
user-ttl: 7200000
user-token-name: authentication
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
wechat:
appid: ${sky.wechat.appid}
secret: ${sky.wechat.secret}
表现层接收小程序端传过来的DTO数据(其实里面只有一个code),调用业务层的login返回一个user对象,之后为这个微信用户生成一个jwt令牌,传入这个用户在user数据库里的id,封装成一个token,把所有信息封装成一个userVO对象,返回给小程序端。
@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;
/**
* 微信登录
* @param userLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
log.info("微信用户登录:{}",userLoginDTO.getCode());
//微信登录
User user = userService.wxLogin(userLoginDTO);
//为微信用户生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID,user.getId());
String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);
UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.openid(user.getOpenid())
.token(token)
.build();
return Result.success(userLoginVO);
}
}
在业务层里,调用微信接口服务获取当前用户的openid,通过httpclientutil来发送请求,传入四个数据,得到一个json里面包含着openid,解析出来。如果openid为空则抛出异常,之后判断是否为新用户,如果是新用户,自动完成注册。
@Service
public class UserServiceImpl implements UserService {
public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";
@Autowired
private WeChatProperties weChatProperties;
@Autowired
private UserMapper userMapper;
/**
* 微信登录
* @param userLoginDTO
* @return
*/
public User wxLogin(UserLoginDTO userLoginDTO) {
String openid = getOpenid(userLoginDTO.getCode());
//判断openid是否为空,如果为空登录失败抛出业务异常
if (openid == null) {
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}
//判断当前用户是否为新用户
User user = userMapper.getByOpenid(openid);
//如果是新用户,自动完成注册
if (user == null) {
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}
//返回用户对象
return user;
}
/**
* 调用微信接口服务,获取微信用户的openid
* @param code
* @return
*/
private String getOpenid(String code) {
//调用微信接口服务获得当前用户的openid
//通过httpclient向微信地址发送请求
Map<String, String> map = new HashMap<>();
map.put("appid",weChatProperties.getAppid());
map.put("secret",weChatProperties.getSecret());
map.put("js_code",code);
map.put("grant_type","authorization_code");
String json = HttpClientUtil.doGet(WX_LOGIN, map);
JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");
return openid;
}
}
数据层里比较简单,但是注意要进行一下主键返回,代码如下:
@Mapper
public interface UserMapper {
/**
* 根据openid查询用户
* @param openid
* @return
*/
@Select("select * from user where openid = #{openid}")
User getByOpenid(String openid);
/**
* 插入数据
* @param user
*/
void insert(User user);
}
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into user
(openid, name, phone, sex, id_number, avatar, create_time)
VALUES
(#{openid},#{name},#{phone},#{sex},#{idNumber},#{avatar},#{createTime})
</insert>
拦截器更新
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getUserTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("当前用户id:{}", userId);
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}
商品浏览
需求分析和设计
代码导入
这里和前面基本类似,导入这些代码即可。
缓存菜品
实现思路
每个分类下的菜品保存一份缓存数据:key:分类id value:菜品集合字符串
数据库中菜品数据有变更时及时清理缓存数据
因为加入到了缓存中,所以更新操作要保持同步,包括新增菜品,修改菜品,批量删除菜品,起售停售菜品
首先是user的表现层里,在查询sql之前先查询缓存,空则继续sql,之后再存进去,不空则查询缓存
/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
//构造redis中的key dish_id
String key = "dish_"+categoryId;
//查询redis中是否存在菜品数据
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
//如果存在,直接返回,无序查询数据库
if (list != null && !list.isEmpty()) {
return Result.success(list);
}
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
//如果不存在,查询数据库,将查询到的数据放入到redis中
list = dishService.listWithFlavor(dish);
//将数据重新放到redis中
redisTemplate.opsForValue().set(key,list);
return Result.success(list);
}
之后是admin的表现层,每次进行crud之前都要进行相应的缓存处理:
private void cleanCache(String pattern) {
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
缓存套餐
Spring Cache
添加购物车
需求分析和设计
代码开发
表现层里传入shoppingcartDTO,调用业务层的addShoppingCart。
@RestController
@Slf4j
@RequestMapping("/user/shoppingCart")
@Api(tags = "C端购物车相关接口")
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
/**
* 添加购物车
* @param shoppingCartDTO
* @return
*/
@PostMapping("/add")
@ApiOperation("添加购物车")
public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO) {
log.info("添加购物车,商品信息为:{}",shoppingCartDTO);
shoppingCartService.addShoppingCart(shoppingCartDTO);
return Result.success();
}
}
业务层里面主要有三个比较重要的逻辑:1判断当前加入购物车的商品是否已经存在,2如果已存在,数量加一,3不存在插入一条购物车数据。
将DTO对象转换成Shoppingcart对象以后,再去数据库里面找看是否存在,注意要额外注入userID。如果存在,取出数据加以后更新。如果不存在,就要看加入的是菜品还是套餐数据,主要看能不能取到对应的id,之后进行添加即可。
@Service
@Slf4j
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;
/**
* 添加购物车
* @param shoppingCartDTO
*/
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
//判断当前加入购物车的商品是否已经存在
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
shoppingCart.setUserId(BaseContext.getCurrentId());
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
//如果已存在,数量加一
if (list != null && !list.isEmpty()) {
ShoppingCart cart = list.get(0);
cart.setNumber(cart.getNumber() + 1);
shoppingCartMapper.updateNumberById(cart);
} else {
//不存在,插入一条购物车数据
//判断本次添加到购物车的是菜品还是套餐
Long dishId = shoppingCartDTO.getDishId();
if (dishId != null) {
//本次添加到购物车的是菜品
Dish dish = dishMapper.getById(dishId);
shoppingCart.setName(dish.getName());
shoppingCart.setImage(dish.getImage());
shoppingCart.setAmount(dish.getPrice());
} else {
//本次添加到购物车的是菜品
Long setmealId = shoppingCartDTO.getSetmealId();
Setmeal setmeal = setmealMapper.getById(setmealId);
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}
}
}
数据层代码如下
@Mapper
public interface ShoppingCartMapper {
/**
* 动态条件查询
* @param shoppingCart
* @return
*/
List<ShoppingCart> list(ShoppingCart shoppingCart);
/**
* 根据id修改商品数量
* @param shoppingCart
*/
@Update("update shopping_cart set number = #{number} where id = #{id}")
void updateNumberById(ShoppingCart shoppingCart);
/**
* 插入购物车数据
* @param shoppingCart
*/
@Insert("insert into shopping_cart (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time) " +
"values (#{name},#{image},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{createTime})")
void insert(ShoppingCart shoppingCart);
}
<mapper namespace="com.sky.mapper.ShoppingCartMapper">
<select id="list" resultType="com.sky.entity.ShoppingCart">
select * from shopping_cart
<where>
<if test="userId != null">and user_id = #{userId}</if>
<if test="setmealId != null">and setmeal_id = #{setmealId}</if>
<if test="dishId != null">and dish_id = #{dishId}</if>
<if test="dishFlavor != null">and dish_flavor = #{dishFlavor}</if>
</where>
</select>
</mapper>
查看购物车
需求分析和设计
代码开发
表现层如下
/**
* 查看购物车
* @return
*/
@ApiOperation("查看购物车")
@GetMapping("/list")
public Result<List<ShoppingCart>> list(){
List<ShoppingCart> list = shoppingCartService.showShoppingCart();
return Result.success(list);
}
业务层里,主要是根据userId来封装一个购物车对象传给Mapper的list中
/**
* 查看购物车
* @return
*/
public List<ShoppingCart> showShoppingCart() {
//获取到当前微信用户的id
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = ShoppingCart.builder()
.userId(userId)
.build();
return shoppingCartMapper.list(shoppingCart);
}
清空购物车
需求分析和设计
代码开发
只要把关于这个userId的购物车数据全部清空即可:
/**
* 清空购物车
* @return
*/
@DeleteMapping("/clean")
@ApiOperation("清空购物车")
public Result clean() {
shoppingCartService.cleanShoppingCart();
return Result.success();
}
/**
* 清空购物车
*/
public void cleanShoppingCart() {
Long userId = BaseContext.getCurrentId();
shoppingCartMapper.deleteByUserId(userId);
}
/**
* 根据userid删除购物车数据
*/
@Delete("delete from shopping_cart where user_id = #{userId}")
void deleteByUserId(Long userId);
删除购物车中的一件商品
代码和添加购物车十分类似,只不过如果数量为1则删除,数量不是1则改数。如下所示:
/**
* 删除购物车中的一个商品
* @param shoppingCartDTO
* @return
*/
@PostMapping("/sub")
@ApiOperation("删除购物车中的一个商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO) {
log.info("删除购物车中的一个商品,商品信息为:{}",shoppingCartDTO);
shoppingCartService.subShoppingCart(shoppingCartDTO);
return Result.success();
}
/**
* 删除购物车中的一个商品
* @param shoppingCartDTO
* @return
*/
public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
shoppingCart.setUserId(BaseContext.getCurrentId());
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
if (list != null && !list.isEmpty()) {
shoppingCart = list.get(0);
if (shoppingCart.getNumber() == 1) {
//数量为1直接删除
shoppingCartMapper.deleteById(shoppingCart.getId());
} else {
//不唯一修改份数
shoppingCart.setNumber(shoppingCart.getNumber()-1);
shoppingCartMapper.updateNumberById(shoppingCart);
}
}
}
/**
* 根据id删除购物车数据
* @param id
*/
@Delete("delete from shopping_cart where id = #{id}")
void deleteById(Long id);
}
地址簿模块开发
需求分析和设计
代码导入
用户下单
需求分析和设计
代码开发
表现层比较简单,只需要传入DTO返回VO即可
@RestController("userOrderController")
@RequestMapping("/user/order")
@Api(tags = "用户端订单相关接口")
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 用户下单
* @param ordersSubmitDTO
* @return
*/
@PostMapping("/submit")
@ApiOperation("用户下单")
public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO){
log.info("用户下单:参数为:{}",ordersSubmitDTO);
OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);
return Result.success(orderSubmitVO);
}
}
业务层里,首先要处理各种异常(其实在小程序里面并不会出现,但是在调试过程中可能会出现问题),首先是地址簿为空,之后是购物车数据是否为空。如果都不为空,就可以分别往订单表和订单明细表里面插数据了,前者copy了DTO的数据以后还要额外加入一些,后者遍历出购物车的每一条数据加进去。最后通过VO返回结果
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private AddressBookMapper addressBookMapper;
@Autowired
private ShoppingCartMapper shoppingCartMapper;
/**
* 用户下单
* @param ordersSubmitDTO
* @return
*/
@Transactional
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
//处理各种业务异常(地址簿为空,购物车数据为空)
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if (addressBook == null) {
//抛出业务异常
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
//查询当前用户的购物车数据
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);
List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
if (shoppingCartList == null || shoppingCartList.isEmpty()) {
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
//向订单表插入一条数据
Orders orders = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO,orders);
orders.setOrderTime(LocalDateTime.now());
orders.setPayStatus(Orders.UN_PAID);
orders.setStatus(Orders.PENDING_PAYMENT);
orders.setNumber(String.valueOf(System.currentTimeMillis()));
orders.setPhone(addressBook.getPhone());
orders.setConsignee(addressBook.getConsignee());
orders.setUserId(userId);
orderMapper.insert(orders);
List<OrderDetail> orderDetailList = new ArrayList<>();
//向订单明细表插入n条数据
for (ShoppingCart cart : shoppingCartList) {
OrderDetail orderDetail = new OrderDetail(); //订单明细
BeanUtils.copyProperties(cart,orderDetail);
orderDetail.setOrderId(orders.getId()); //设置当前订单明细关联的订单id
orderDetailList.add(orderDetail);
}
orderDetailMapper.insertBatch(orderDetailList);
//清空用户的购物车数据
shoppingCartMapper.deleteByUserId(userId);
//封装VO返回结果
OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
.id(orders.getId())
.orderTime(orders.getOrderTime())
.orderNumber(orders.getNumber())
.orderAmount(orders.getAmount())
.build();
return orderSubmitVO;
}
}
数据层代码如下;
<mapper namespace="com.sky.mapper.OrderDetailMapper">
<insert id="insertBatch">
insert into order_detail (name, image, order_id, dish_id, setmeal_id, dish_flavor, amount,number)
values
<foreach collection="orderDetailList" item="od" separator=",">
(#{od.name},#{od.image},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},#{od.amount},#{od.number})
</foreach>
</insert>
</mapper>
<mapper namespace="com.sky.mapper.OrderMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into orders (number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status,
amount, remark, phone, address, user_name, consignee, cancel_reason, rejection_reason,
cancel_time, estimated_delivery_time, delivery_status, delivery_time, pack_amount,
tableware_number, tableware_status)
values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
#{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{userName}, #{consignee}, #{cancelReason},
#{rejectionReason},
#{cancelTime}, #{estimatedDeliveryTime}, #{deliveryStatus}, #{deliveryTime}, #{packAmount},
#{tablewareNumber}, #{tablewareStatus})
</insert>
</mapper>
微信支付
微信支付介绍
代码导入
代码不需要写,导入即可。
但是由于自己不是商户,所以并不能支付订单,所以把代码改变一下:表现层不变:
/**
* 订单支付
*
* @param ordersPaymentDTO
* @return
*/
@PutMapping("/payment")
@ApiOperation("订单支付")
public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
log.info("订单支付:{}", ordersPaymentDTO);
OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
log.info("生成预支付交易单:{}", orderPaymentVO);
return Result.success(orderPaymentVO);
}
业务层代码进行修改:首先要定义一个全局变量order来获取order的id,否则很困难,在如下页面点击去支付后就会调用submitOrder方法,将订单数据写入数据库,所以可以在submitOrder方法中获取订单的id。json那几行是为了能够得到一个返回的数据来欺骗微信支付,后面是将orders表中的数据直接变成支付完以后的样子。
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(userId);
/* //调用微信支付接口,生成预支付交易单
JSONObject jsonObject = weChatPayUtil.pay(
ordersPaymentDTO.getOrderNumber(), //商户订单号
new BigDecimal(0.01), //支付金额,单位 元
"苍穹外卖订单", //商品描述
user.getOpenid() //微信用户的openid
);
if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
throw new OrderBusinessException("该订单已支付");
}
*/
JSONObject jsonObject = new JSONObject();
jsonObject.put("code","ORDERPAID");
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));
Integer OrderPaidStatus = Orders.PAID;//支付状态,已支付
Integer OrderStatus = Orders.TO_BE_CONFIRMED; //订单状态,待接单
LocalDateTime check_out_time = LocalDateTime.now();//更新支付时间
orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time, this.orders.getId());
return vo;
}
查询历史订单
分页查询历史订单,根据订单状态查询,展示订单数据时,需要展示的数据包括:下单时间,订单状态,订单金额,订单明细。
表现层,传入pageNum,pagesize和状态信息,调用pageQueryUser
/**
* 历史订单查询
*
* @param page
* @param pageSize
* @param status 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
* @return
*/
@GetMapping("/historyOrders")
@ApiOperation("历史订单查询")
public Result<PageResult> page(int page, int pageSize, Integer status){
PageResult pageResult = orderService.pageQuery4User(page,pageSize,status);
return Result.success(pageResult);
}
业务层首先进行分页的基本操作,之后将用户的id和状态封装在OrderPageQueryDTO里,利用分页条件查询得到Page<Orders>,从里面的每一个orders得到订单id,查询订单明细,封装在orderVO里面最后按照格式返回即可
/**
* 用户订单分页查询
* @param pageNum
* @param pageSize
* @param status
* @return
*/
public PageResult pageQueryUser(int pageNum, int pageSize, Integer status) {
//设置分页
PageHelper.startPage(pageNum,pageSize);
OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO();
ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());
ordersPageQueryDTO.setStatus(status);
//分页条件查询
Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);
List<OrderVO> list = new ArrayList<>();
//查询出订单明细,并封装入OrderVO进行响应
if (page != null && page.getTotal() > 0) {
for (Orders orders : page) {
Long orderId = orders.getId(); //订单id
//查询订单明细
List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId);
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders,orderVO);
orderVO.setOrderDetailList(orderDetails);
list.add(orderVO);
}
}
return new PageResult(page.getTotal(),list);
}
数据层代码如下:
/**
* 分页条件查询并按下单时间排序
* @param ordersPageQueryDTO
*/
Page<Orders> pageQuery(OrdersPageQueryDTO ordersPageQueryDTO);
<select id="pageQuery" resultType="Orders">
select * from orders
<where>
<if test="number != null and number!=''">
and number like concat('%',#{number},'%')
</if>
<if test="phone != null and phone!=''">
and phone like concat('%',#{phone},'%')
</if>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="beginTime != null">
and order_time >= #{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){
//查询当前用户id
Long userId = BaseContext.getCurrentId();
//根据订单id查询当前订单详情
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);
//将订单详情对象转换为购物车对象
List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {
ShoppingCart shoppingCart = new ShoppingCart();
//将原订单详情里面的菜品信息重新复制到购物车对象中
BeanUtils.copyProperties(x, shoppingCart, "id");
shoppingCart.setUserId(userId);
shoppingCart.setCreateTime(LocalDateTime.now());
return shoppingCart;
}).collect(Collectors.toList());
//将购物车对象批量添加到数据库
shoppingCartMapper.insertBatch(shoppingCartList);
}
<insert id="insertBatch" parameterType="list">
insert into shopping_cart
(name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)
values
<foreach collection="shoppingCartList" item="sc" separator=",">
(#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime})
</foreach>
</insert>
商家端订单管理模块
订单搜索
业务规则
-
输入订单号/手机号进行搜索,支持模糊搜索
-
根据订单状态进行筛选
-
下单时间进行时间筛选
-
搜索内容为空,提示未找到相关订单
-
搜索结果页,展示包含搜索关键词的内容
-
分页展示搜索到的订单数据
代码开发
/**
* 订单管理
*/
@RestController("adminOrderController")
@RequestMapping("/admin/order")
@Api("订单管理接口")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/conditionSearch")
@ApiOperation("订单搜索")
public Result<PageResult> conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO){
PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO);
return Result.success(pageResult);
}
}
业务层里的逻辑主要是要向PageResult里传入一个orderVOList才行,但是我们只能得到orders,所以这里需要进行一下转换,将每一个orders里的数据封装到orderVO里(注意里面的菜品格式要进行一次啊转换再塞进去方便观看)
/**
* 订单搜索
*
* @param ordersPageQueryDTO
* @return
*/
public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());
Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);
// 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO
List<OrderVO> orderVOList = getOrderVOList(page);
return new PageResult(page.getTotal(), orderVOList);
}
private List<OrderVO> getOrderVOList(Page<Orders> page) {
// 需要返回订单菜品信息,自定义OrderVO响应结果
List<OrderVO> orderVOList = new ArrayList<>();
List<Orders> ordersList = page.getResult();
if (!CollectionUtils.isEmpty(ordersList)) {
for (Orders orders : ordersList) {
// 将共同字段复制到OrderVO
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
String orderDishes = getOrderDishesStr(orders);
// 将订单菜品信息封装到orderVO中,并添加到orderVOList
orderVO.setOrderDishes(orderDishes);
orderVOList.add(orderVO);
}
}
return orderVOList;
}
/**
* 根据订单id获取菜品信息字符串
*
* @param orders
* @return
*/
private String getOrderDishesStr(Orders orders) {
// 查询订单菜品详情信息(订单中的菜品和数量)
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());
// 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
List<String> orderDishList = orderDetailList.stream().map(x -> {
String orderDish = x.getName() + "*" + x.getNumber() + ";";
return orderDish;
}).collect(Collectors.toList());
// 将该订单对应的所有菜品信息拼接在一起
return String.join("", orderDishList);
}
各个状态的订单数量统计
/**
* 各个状态的订单数据统计
* @return
*/
@GetMapping("/statistics")
@ApiOperation("各个状态的订单数据统计")
public Result<OrderStatisticsVO> statistics(){
OrderStatisticsVO orderStatisticsVO = orderService.statistics();
return Result.success(orderStatisticsVO);
}
/**
* 各个状态的订单数量统计
*
* @return
*/
public OrderStatisticsVO statistics(){
//根据状态,分别查询出待接单,待派送,派送中的订单数量
Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED);
Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED);
Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS);
//将查询出的数据封装到orderStatisticsVO响应
OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO();
orderStatisticsVO.setToBeConfirmed(toBeConfirmed);
orderStatisticsVO.setConfirmed(confirmed);
orderStatisticsVO.setDeliveryInProgress(deliveryInProgress);
return orderStatisticsVO;
}
/**
* 根据状态统计订单数量
* @param toBeConfirmed
*/
@Select("select count(id) from orders where status = #{status}")
Integer countStatus(Integer status);
查询订单详情
需求和业务分析
业务规则:
-
订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)
-
订单详情页面需要展示订单明细数据(商品名称、数量、单价)
代码开发
/**
* 订单详情
*
* @param id
* @return
*/
@GetMapping("/details/{id}")
@ApiOperation("查询订单详情")
public Result<OrderVO> details(@PathVariable("id") Long id) {
OrderVO orderVO = orderService.details(id);
return Result.success(orderVO);
}
接单
/**
* 接单
*
* @return
*/
@PutMapping("/confirm")
@ApiOperation("接单")
public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) {
orderService.confirm(ordersConfirmDTO);
return Result.success();
}
/**
* 接单
*
* @param ordersConfirmDTO
*/
public void confirm(OrdersConfirmDTO ordersConfirmDTO) {
Orders orders = Orders.builder()
.id(ordersConfirmDTO.getId())
.status(Orders.CONFIRMED)
.build();
orderMapper.update(orders);
}
拒单
业务规则:
-
商家拒单其实就是将订单状态修改为“已取消”
-
只有订单处于“待接单”状态时可以执行拒单操作
-
商家拒单时需要指定拒单原因
-
商家拒单时,如果用户已经完成了支付,需要为用户退款
/**
* 拒单
*
* @return
*/
@PutMapping("/rejection")
@ApiOperation("拒单")
public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception {
orderService.rejection(ordersRejectionDTO);
return Result.success();
}
/**
* 拒单
*
* @param ordersRejectionDTO
*/
public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId());
// 订单只有存在且状态为2(待接单)才可以拒单
if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
//支付状态
Integer payStatus = ordersDB.getPayStatus();
if (payStatus == Orders.PAID) {
//用户已支付,需要退款
String refund = weChatPayUtil.refund(
ordersDB.getNumber(),
ordersDB.getNumber(),
new BigDecimal(0.01),
new BigDecimal(0.01));
log.info("申请退款:{}", refund);
}
// 拒单需要退款,根据订单id更新订单状态、拒单原因、取消时间
Orders orders = new Orders();
orders.setId(ordersDB.getId());
orders.setStatus(Orders.CANCELLED);
orders.setRejectionReason(ordersRejectionDTO.getRejectionReason());
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
取消订单
业务规则:
-
取消订单其实就是将订单状态修改为“已取消”
-
商家取消订单时需要指定取消原因
-
商家取消订单时,如果用户已经完成了支付,需要为用户退款
/**
* 取消订单
*
* @return
*/
@PutMapping("/cancel")
@ApiOperation("取消订单")
public Result cancel(@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception {
orderService.cancel(ordersCancelDTO);
return Result.success();
}
/**
* 取消订单
*
* @param ordersCancelDTO
*/
public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId());
//支付状态
Integer payStatus = ordersDB.getPayStatus();
if (payStatus == 1) {
//用户已支付,需要退款
String refund = weChatPayUtil.refund(
ordersDB.getNumber(),
ordersDB.getNumber(),
new BigDecimal(0.01),
new BigDecimal(0.01));
log.info("申请退款:{}", refund);
}
// 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间
Orders orders = new Orders();
orders.setId(ordersCancelDTO.getId());
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason(ordersCancelDTO.getCancelReason());
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
派送订单
业务规则:
-
派送订单其实就是将订单状态修改为“派送中”
-
只有状态为“待派送”的订单可以执行派送订单操作
/**
* 派送订单
*
* @return
*/
@PutMapping("/delivery/{id}")
@ApiOperation("派送订单")
public Result delivery(@PathVariable("id") Long id) {
orderService.delivery(id);
return Result.success();
}
/**
* 派送订单
*
* @param id
*/
public void delivery(Long id) {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);
// 校验订单是否存在,并且状态为3
if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
Orders orders = new Orders();
orders.setId(ordersDB.getId());
// 更新订单状态,状态转为派送中
orders.setStatus(Orders.DELIVERY_IN_PROGRESS);
orderMapper.update(orders);
}
完成订单
业务规则:
-
完成订单其实就是将订单状态修改为“已完成”
-
只有状态为“派送中”的订单可以执行订单完成操作
/**
* 完成订单
*
* @return
*/
@PutMapping("/complete/{id}")
@ApiOperation("完成订单")
public Result complete(@PathVariable("id") Long id) {
orderService.complete(id);
return Result.success();
}
/**
* 完成订单
*
* @param id
*/
public void complete(Long id) {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);
// 校验订单是否存在,并且状态为4
if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
Orders orders = new Orders();
orders.setId(ordersDB.getId());
// 更新订单状态,状态转为完成
orders.setStatus(Orders.COMPLETED);
orders.setDeliveryTime(LocalDateTime.now());
orderMapper.update(orders);
}
校验收货地址是否超出配送范围
这里上传比较麻烦,就不做这个功能了,代码贴一下:
订单状态定时处理
Spring Task
这里只需要调用Mapper中的函数,找到所有符合条件的orders,之后遍历这些orders改变状态即可。
/**
* 定时任务类
*/
@Slf4j
@Component
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 处理超时订单的方法
*/
@Scheduled(cron = "0 * * * * ?") //每分钟触发一次
public void processTimeoutOrder(){
log.info("定时处理超时订单:{}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
if (ordersList != null && !ordersList.isEmpty()){
for (Orders orders : ordersList) {
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("订单超时,自动取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
}
/**
* 处理一直处于派送中的订单
*/
@Scheduled(cron = "0 0 1 * * ?") //每天凌晨一点触发一次
public void processDeliveryOrder(){
log.info("定时处理处于派送中的订单:{}",LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);
if (ordersList != null && !ordersList.isEmpty()){
for (Orders orders : ordersList) {
orders.setStatus(Orders.COMPLETED);
orderMapper.update(orders);
}
}
}
}
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);
WebSocket协议
来单提醒
语音播报+弹出提示框
这里我在OrderServiceImpl里面的订单支付页面加入如下代码,让服务端向客户端发送消息,首先是m
ap,之后转换为json,最后调用websocketServer的群发方法即可。
//通过websocket向客户端浏览器推送消息 type orderId content
Map map = new HashMap<>();
map.put("type",1); //1表示来单提醒 2表示客户催单
map.put("orderId",this.orders.getId());
map.put("content","订单号:"+ ordersPaymentDTO.getOrderNumber());
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
客户催单
这里设计一个催单接口,比较简单,和前面类似:
/**
* 客户催单
* @param id
* @return
*/
@ApiOperation("客户催单")
@GetMapping("/reminder/{id}")
public Result reminder(@PathVariable("id") Long id){
orderService.reminder(id);
return Result.success();
}
/**
* 客户催单
* @param id
*/
public void reminder(Long id) {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);
// 校验订单是否存在,并且状态为4
if (ordersDB == null) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
Map map = new HashMap<>();
map.put("type",2); //1提醒 2催单
map.put("orderId",id);
map.put("content","订单号:"+ordersDB.getNumber());
String json = JSON.toJSONString(map);
//通过websocket向客户端推送消息
webSocketServer.sendToAllClient(json);
}
营业额统计
这里要传入起始和结束日期,记得要加@DateFormat注解
/**
* 数据统计相关接口
*/
@Api(tags = "数据统计相关接口")
@Slf4j
@RestController
@RequestMapping("/admin/report")
public class ReportController {
@Autowired
private ReportService reportService;
/**
* 营业额统计
* @param begin
* @param end
* @return
*/
@GetMapping("/turnoverStatistics")
@ApiOperation("营业额统计")
public Result<TurnoverReportVO> turnoverStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
log.info("营业额数据统计:{},{}",begin,end);
TurnoverReportVO turnoverStatistics = reportService.getTurnoverStatistics(begin, end);
return Result.success(turnoverStatistics);
}
}
业务层的逻辑主要是将传入的日期变成一个日期数组,再把数组里日期对应的营业额计算出来,最后将两个数据都变成字符串
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
@Autowired
private OrderMapper orderMapper;
/**
* 统计指定区间营业额数据
* @param begin
* @param end
* @return
*/
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
//当前集合存放begin到end的所有日期
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while (!begin.equals(end)) {
//日期计算,计算指定日期的后一天对应的日期
begin = begin.plusDays(1);
dateList.add(begin);
}
List<Double> turnoverList = new ArrayList<>();
for (LocalDate date : dateList) {
//查询date日期对应的营业额 状态为已完成的订单金额合计
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
Map map = new HashMap<>();
map.put("begin",beginTime);
map.put("end",endTime);
map.put("status", Orders.COMPLETED);
//select sum(amount) from orders where order_time > beginTime and order_time < endTime end and status = 5
Double turnover = orderMapper.sumByMap(map);
turnover = turnover == null ? 0.0 : turnover;
turnoverList.add(turnover);
}
return TurnoverReportVO
.builder()
.dateList(StringUtils.join(dateList, ",")) //封装成字符串
.turnoverList(StringUtils.join(turnoverList,","))
.build();
}
}
数据层条件查询当日总金额,记得比较大小的时候要用转义字符:
<select id="sumByMap" resultType="java.lang.Double">
select sum(amount) from orders
<where>
<if test="begin != null">and order_time > #{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 < endTime
Integer orderCount = getOrderCount(beginTime, endTime, null);
//查询每天有效订单数 select count(id) from orders where order_time > beginTime and order_time < endTime and status = 5
Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);
orderCountList.add(orderCount);
validOrderCountList.add(validOrderCount);
}
//计算时间区间内的订单总数量
Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();
//计算时间区间内的有效订单数量
Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();
//计算订单完成率
Double orderCompletionRate = 0.0;
if (totalOrderCount != 0) {
orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
}
return OrderReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.orderCountList(StringUtils.join(orderCountList, ","))
.validOrderCountList(StringUtils.join(validOrderCountList, ","))
.totalOrderCount(totalOrderCount)
.validOrderCount(validOrderCount)
.orderCompletionRate(orderCompletionRate)
.build();
}
/**
* 根据条件统计订单数量
* @param begin
* @param end
* @param status
* @return
*/
private Integer getOrderCount(LocalDateTime begin, LocalDateTime end,Integer status){
Map map = new HashMap<>();
map.put("begin",begin);
map.put("end",end);
map.put("status",status);
return orderMapper.countByMap(map);
}
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from orders
<where>
<if test="begin != null">and order_time > #{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) number
from order_detail od, orders o
where od.order_id = o.id and o.status = 5
<if test="begin != null">and o.order_time > #{begin}</if>
<if test="end != null">and o.order_time < #{end}</if>
group by od.name
order by number desc
limit 0,10
</select>
工作台
代码重复,直接导入!
Apache POI
具体操作实例
/**
* poi操作Excel文件
*/
public class POITest {
//通过POI创建Excel文件并且写入文件内容
public static void Write() throws Exception{
//在内存中创建一个Excel文件
XSSFWorkbook excel = new XSSFWorkbook();
//在Excel文件中创建一个sheet页
XSSFSheet sheet = excel.createSheet("info");
//在sheet页中创建行对象,rownum从0开始
XSSFRow row = sheet.createRow(1);
//创建单元格,写入文件内容
row.createCell(1).setCellValue("姓名");
row.createCell(2).setCellValue("城市");
//创建一个新行
row = sheet.createRow(2);
row.createCell(1).setCellValue("张三");
row.createCell(2).setCellValue("北京");
//创建一个新行
row = sheet.createRow(3);
row.createCell(1).setCellValue("李四");
row.createCell(2).setCellValue("南京");
//通过输出流将内存中的excel写入磁盘
FileOutputStream out = new FileOutputStream(new File("D:\\info.xlsx"));
excel.write(out);
//关闭资源
out.close();
excel.close();
}
//通过POI读取Excel文件中的内容
public static void read() throws Exception{
FileInputStream fileInputStream = new FileInputStream(new File("D:\\info.xlsx"));
XSSFWorkbook excel = new XSSFWorkbook(fileInputStream);
//读取第一个sheet页
XSSFSheet sheet = excel.getSheetAt(0);
int lastRowNum = sheet.getLastRowNum(); //有文字的最后一行行号
for (int i = 1; i <= lastRowNum; i++){
//获得某一行
XSSFRow row = sheet.getRow(i);
//获得单元格对象
String cellValue1 = row.getCell(1).getStringCellValue();
String cellValue2 = row.getCell(2).getStringCellValue();
System.out.println(cellValue1+" "+cellValue2);
//关闭资源
excel.close();
fileInputStream.close();
}
}
public static void main(String[] args) throws Exception{
//Write();
read();
}
}
导出运营数据Excel报表
表现层中传入参数是为了能下载到浏览器里:
/**
* 导出运营数据报表
* @param response
*/
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
reportService.exportBusinessDate(response);
}
业务层比较新颖的一点就是自动注入了workspaceService,因为里面有我们想要的数据,这里的input流采用了从类路径中读取资源。
/**
* 导出运营数据报表
* @param response
*/
public void exportBusinessDate(HttpServletResponse response) {
//1 查询数据库获取营业数据 查询最近三十天运营数据
LocalDate dateBegin = LocalDate.now().minusDays(30);
LocalDate dateEnd = LocalDate.now().minusDays(1);
//查询概览数据
BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX));
//2 通过POI将数据写入Excel
InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/excelTemplate.xlsx");
try {
//基于模板文件创建一个新的excel
XSSFWorkbook excel = new XSSFWorkbook(in);
//获取表格文件sheet标签页
XSSFSheet sheet = excel.getSheet("Sheet1");
//填充数据 -- 时间
sheet.getRow(1).getCell(1).setCellValue("时间:"+dateBegin+"至"+dateEnd);
//获得第四行
XSSFRow row = sheet.getRow(3);
row.getCell(2).setCellValue(businessDataVO.getTurnover());
row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
row.getCell(6).setCellValue(businessDataVO.getNewUsers());
//获得第五行
row = sheet.getRow(4);
row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());
row.getCell(4).setCellValue(businessDataVO.getUnitPrice());
//填充明细数据
for (int i = 0; i < 30; i++) {
LocalDate date = dateBegin.plusDays(i);
//查询某一天的营业数据
workspaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN),LocalDateTime.of(date,LocalTime.MAX));
// 获得某一行
row = sheet.getRow(7 + i);
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(businessDataVO.getTurnover());
row.getCell(3).setCellValue(businessDataVO.getValidOrderCount());
row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
row.getCell(5).setCellValue(businessDataVO.getUnitPrice());
row.getCell(6).setCellValue(businessDataVO.getNewUsers());
}
//3 通过输出流,将Excel文件下载到客户端浏览器
ServletOutputStream out = response.getOutputStream();
excel.write(out);
//关闭资源
out.close();
excel.close();
} catch (Exception e) {
throw new RuntimeException(e);
}