软件开发流程
需求分析:需求规格说明书、产品原型
设计:ui设计、数据库设计、接口设计
编码:项目代码、单元测试
测试:测试用例、测试报告
上线运维:软件环境安装、配置
角色分工
项目经理:对整个项目负责,任务分配、把控进度
产品经理:进行需求调研,输出需求调研文档、产品原型等
ui设计师:根据产品原型输出界面效果图
架构师:项目整体架构设计、技术选型等
开发工程师:代码实现
测试工程师:编写测试用例,输出测试报告
运维工程师:软件环境搭建、项目上线
软件环境
开发环境:开发人员在开发阶段使用的环境,一般外部用户无法访问
测试环境:专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
生产环境:就是线上环境,正式提供对外服务的环境
苍穹外卖项目介绍
定位:专门为餐饮企业定制的一款软件产品
管理端:外卖商家使用
员工管理 分类管理、菜品管理、套餐管理、订单管理、工作台、数据统计、来单提醒
用户端:点餐用户使用
微信登录、商品浏览、购物车、用户下单、微信支付、历史订单、地址管理、用户催单
功能架构:体现项目中的业务功能模块
产品原型:用于展示项目的业务功能
技术选型:展示项目中使用到的技术框架和中间件等
用户层:node.js、VUE.js、ElementUI、微信小程序、apache echart
网关层:nginx
应用层:Spring Boot、Spring MVC、Spring Task、httpclient、Spring Cache、JWT、阿里云OSS、Swagger、POI、WebSocket
数据层:MySQL、Redis、mybatis、pagehelper、spring data redis
工具:Git、maven、Junit、postman
完善登录功能-密码加密
修改数据库中明文密码,改为MD5加密后的密文
修改Java代码,前端提交的密码进行MD5加密后再跟数据库中密码比对
Swagger
使用Swagger只需要按照他的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口测试页面
Swagger在开发阶段使用的框架,帮助后端开发人员做后端的接口测试
使用方式:
- 导入knife4j的maven坐标
- 在配置类中加入knife4j相关配置
- 设置静态资源映射,否则接口文档页面无法访问
功能测试
1.通过接口文档测试
2.通过前后端联调测试
由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主。
新增员工部分代码
@PostMapping()
@ApiOperation("新增员工") //添加swagger的接口描述
public Result save(@RequestBody EmployeeDTO employeeDTO){ //如果是json格式的数据,需要加上@RequestBody注解
log.info("新增员工:{}",employeeDTO); //{}是占位符
employeeService.save(employeeDTO);
return Result.success();
}
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//通过set方法设置属性过于繁琐
// employee.setName(employeeDTO.getName());
//对象属性拷贝
BeanUtils.copyProperties(employeeDTO,employee);
//设置账号状态,默认正常状态,1表示正常,0表示锁定
employee.setStatus(StatusConstant.ENABLE);
//设置密码,对密码进行md5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置创建时间、更新时间
// employee.setCreateTime(LocalDateTime.now());
// employee.setUpdateTime(LocalDateTime.now());
//设置创建人、更新人
// employee.setCreateUser(BaseContext.getCurrentId());
// employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.insert(employee);
}
员工分页查询部分代码
@PostMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
log.info("员工分页查询,参数:{}",employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO); //调用emloyeeService,返回PageResult对象
return Result.success(pageResult); //返回结果并封装到Result中
}
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//select * from employee limit 0,10 底层是基于limit关键字来查询
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO); //底层基于MyBatis的分页插件进行分页查询
//Page对象包含了分页查询的所有数据
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total,records);
}
启用禁用员工账号部分代码
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result startOrStop(@PathVariable Integer status, Long id){
log.info("启用或禁用员工账号{},{}",status,id);
employeeService.startOrstop(status,id);
return Result.success();
}
@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();
//将update语句设置为动态的,根据传进来的参数的不同,可以修改多个字段
employeeMapper.update(employee);
}
根据id查询员工信息
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<Employee> getById(@PathVariable Long id){
Employee employee = employeeService.getById(id);
return Result.success(employee);
}
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
employee.setPassword("****");
return employee;
}
编辑员工信息
@PutMapping
@ApiOperation("编辑员工信息")
public Result update(@RequestBody EmployeeDTO employeeDTO){
log.info("编辑员工信息:{}",employeeDTO);
employeeService.update(employeeDTO);
return Result.success();A
}
public void update(EmployeeDTO employeeDTO) {
Employee employee = new Employee(); // 接受到的是emploeeDTO,我们要的是employee,所以要对数据进行转换
BeanUtils.copyProperties(employeeDTO,employee); //属性拷贝
//DTO中没有修改时间及修改者,所以要单独设置
// employee.setUpdateTime(LocalDateTime.now());
// employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.update(employee);
}
自定义注解
用于标识某个方法需要进行功能字段自动填充处理
AutoFill
@Target(ElementType.METHOD) //指定当前注解会加在什么位置,指定我们的方法只能加在方法上面
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:update insert两种,查询操作不需要设置
OperationType value();
}
AutoFillAspect
@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(); //方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); //获取方法上的注解对象
OperationType operationType = autoFill.value(); //获取数据库操作类型
//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0){
return;
}
Object entity = args[0];
//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前不同的操作类型,为对应的属性通过反射来赋值
if (operationType == OperationType.INSERT){
//为4个公共字段赋值
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){
//为2个公共字段赋值
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();
}
}
}
}
新增菜品
DishController.java
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
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();
}
}
DishService.java
public void saveWithFlavor(DishDTO dishDTO); //除了菜品信息外,还要保存菜品口味数据
DishServiceImpl.java
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
/**
* 新增菜品及对应的口味
* @param dishDTO
*/
@Override
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
//向菜品表插入1跳数据
dishMapper.insert(dish);
//获取insert语句生成的主键值
Long dishId = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishId)); // 设置菜品id
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors); // 批量插入
}
}
}
DishDao.java
@Data
public class DishDTO implements Serializable {
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//口味
private List<DishFlavor> flavors = new ArrayList<>();
}
DishMapper.java
@Mapper
public interface DishMapper {
/**
* 根据分类id查询菜品数量
* @param categoryId
* @return
*/
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
/**
* 插入菜品数据
* @param dish
*/
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);
}
DishFlavorMapper.java
@Mapper
public interface DishFlavorMapper {
/**
* 批量插入口味数据
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
}
DishFlavorMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorMapper">
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
/* useGeneratedKeys 添加后,mybatis 会自动填充主键
keyProperty 添加后,mybatis 会将主键填充到实体类中*/
insert into dish_flavor(dish_id, name, value) values
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
</mapper>
DishMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
<insert id="insert">
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>
</mapper>
菜品分页查询部分代码
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){ //菜品查询需要返回值
log.info("菜品分页查询:{}", dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
@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());
}
删除菜品部分代码
@DeleteMapping()
@ApiOperation("批量删除菜品")
public Result delete(@RequestParam List<Long> ids){ //@RequestParam是告诉SpringMVC,这个参数的值是请求参数中的ids
log.info("批量删除菜品:{}",ids);
dishService.deleteBatch(ids); //批量删除
return Result.success();
}
@Transactional //涉及多个表操作,要使用事务注解
public void deleteBatch(List<Long> ids) {
//判断当前菜品是否能够删除--是否存在起售中的菜品
for (Long id : ids){
Dish dish = dishMapper.getById(id);
if (dish.getStatus() == StatusConstant.ENABLE){
//当前菜品处于起售中,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//判断当前菜品是否能够删除--当前菜品是否有关联的订单
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if (setmealIds != null && setmealIds.size() > 0){
//当前菜品有关联的套餐,不能删除,抛出业务异常
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品数据
for (Long id : ids){
dishMapper.deleteById(id);
//删除菜品关联的口味数据
dishFlavorMapper.deleteByDishId(id);
}
}
修改菜品部分代码
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
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.size() > 0){
flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishDTO.getId())); // 设置菜品id
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors); // 批量插入
}
}