以套餐管理为例,记录项目管理端——基本数据管理内容。
一、数据库设计
| 序号 | 数据表名 | 中文名称 |
|---|---|---|
| 1 | employee | 员工表 |
| 2 | category | 分类表 |
| 3 | dish | 菜品表 |
| 4 | dish_flavor | 菜品口味表 |
| 5 | setmeal | 套餐表 |
| 6 | setmeal_dish | 套餐菜品关系表 |
| 7 | user | 用户表 |
| 8 | address_book | 地址表 |
| 9 | shopping_cart | 购物车表 |
| 10 | orders | 订单表 |
| 11 | order_detail | 订单明细表 |
数据管理涉及到前六个表:
1. employee
employee表为员工表,用于存储商家内部的员工信息。具体表结构如下:
| 字段名 | 数据类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键 | 自增 |
| name | varchar(32) | 姓名 | |
| username | varchar(32) | 用户名 | 唯一 |
| password | varchar(64) | 密码 | |
| phone | varchar(11) | 手机号 | |
| sex | varchar(2) | 性别 | |
| id_number | varchar(18) | 身份证号 | |
| status | int | 账号状态 | 1正常 0锁定 |
| create_time | datetime | 创建时间 | |
| update_time | datetime | 最后修改时间 | |
| create_user | bigint | 创建人id | |
| update_user | bigint | 最后修改人id |
2. category
category表为分类表,用于存储商品的分类信息。具体表结构如下:
| 字段名 | 数据类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键 | 自增 |
| name | varchar(32) | 分类名称 | 唯一 |
| type | int | 分类类型 | 1菜品分类 2套餐分类 |
| sort | int | 排序字段 | 用于分类数据的排序 |
| status | int | 状态 | 1启用 0禁用 |
| create_time | datetime | 创建时间 | |
| update_time | datetime | 最后修改时间 | |
| create_user | bigint | 创建人id | |
| update_user | bigint | 最后修改人id |
3. dish
dish表为菜品表,用于存储菜品的信息。具体表结构如下:
| 字段名 | 数据类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键 | 自增 |
| name | varchar(32) | 菜品名称 | 唯一 |
| category_id | bigint | 分类id | 逻辑外键 |
| price | decimal(10,2) | 菜品价格 | |
| image | varchar(255) | 图片路径 | |
| description | varchar(255) | 菜品描述 | |
| status | int | 售卖状态 | 1起售 0停售 |
| create_time | datetime | 创建时间 | |
| update_time | datetime | 最后修改时间 | |
| create_user | bigint | 创建人id | |
| update_user | bigint | 最后修改人id |
4. dish_flavor
dish_flavor表为菜品口味表,用于存储菜品的口味信息。具体表结构如下:
| 字段名 | 数据类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键 | 自增 |
| dish_id | bigint | 菜品id | 逻辑外键 |
| name | varchar(32) | 口味名称 | |
| value | varchar(255) | 口味值 |
5. setmeal
setmeal表为套餐表,用于存储套餐的信息。具体表结构如下:
| 字段名 | 数据类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键 | 自增 |
| name | varchar(32) | 套餐名称 | 唯一 |
| category_id | bigint | 分类id | 逻辑外键 |
| price | decimal(10,2) | 套餐价格 | |
| image | varchar(255) | 图片路径 | |
| description | varchar(255) | 套餐描述 | |
| status | int | 售卖状态 | 1起售 0停售 |
| create_time | datetime | 创建时间 | |
| update_time | datetime | 最后修改时间 | |
| create_user | bigint | 创建人id | |
| update_user | bigint | 最后修改人id |
6. setmeal_dish
setmeal_dish表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:
| 字段名 | 数据类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键 | 自增 |
| setmeal_id | bigint | 套餐id | 逻辑外键 |
| dish_id | bigint | 菜品id | 逻辑外键 |
| name | varchar(32) | 菜品名称 | 冗余字段 |
| price | decimal(10,2) | 菜品单价 | 冗余字段 |
| copies | int | 菜品份数 |
以套餐管理为例,实现四个功能:
1、新增
2、分页查询
3、删除
4、修改
5、起售停售
二、套餐管理
资料中day12含最终代码
1、新增套餐
1.1 分析
<1>需求
setmeal 表里有一栏 category_id,意思是你这个套餐要有所属的 分类表项。
套餐菜品——添加菜品:
是在当前页面添加一个框。。如上图
看页面原型: 可看出加的菜显示了name price copied,这都是setmeal-dish表的内容。它需要依赖setmeal_id外键和dish_id外键。
<2>规则分析
需要DTO中属性:套餐名、套餐的所属类(要下拉菜单显示,category中/list已实现根据type列出来)、套餐价格、图片、描述、
还有嵌套的添加菜品(插入的是setmeal-dish表的内容)
业务规则:
- 套餐名称唯一
- 套餐必须属于某个分类(category_id)⭐
- 套餐必须包含菜品
- 名称、分类、价格、图片为必填项
- 添加菜品窗口需要根据分类类型来展示菜品(看图,它有个侧边菜品导航)⭐
- 新增的套餐默认为停售状态⭐
接口设计(共涉及到4个接口):
- 根据类型查询分类(已完成,category/list,根据type得到List,选套餐属于啥类时,下拉菜单展示)
- 根据分类id查询菜品(
在菜品controller写,根据categoryId得到List,这是那个添加菜品时需要展示) - 图片上传(已完成)
- 新增套餐(存到setmeal-dish表)
1.2 实现查询菜品接口
分析:
根据分类id查询菜品,即小窗口里的功能。看图的侧栏,是按类别列出来 + 按名字搜索查询
另外:
必须保证 在售出的菜才可以添加到套餐中
在dishController里根据 菜的类别id项categoryId,把dish表项列出来,得到List来展示。
不需要口味啥的,返回的就是一个个的Dish。
还涉及到name模糊查询,以及status必须在售。
Controller:
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId){
List<Dish> list = dishService.list(categoryId);
return Result.success(list);
}
ServiceImp:
public List<Dish> list(Long categoryId) {
//需要封装成List
//dish需要categoryId,还有要在售,然后去mapper
Dish dish = Dish.builder()
.categoryId(categoryId)
.status(StatusConstant.ENABLE)
.build();
return dishMapper.list(dish);
}
Mapper.xml:
<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>
注意去mapper里查的时候,给mapper的是实体类Dish,已经赋值了categoryId 和status在售。
就是说把categoryId 传过去,现在某个categoryId 如酒水饮料类 且 status在售的 所有菜品。那为什么name你不给它传过来呢?
测试这个name搜索也是没实现的。
1.3 实现新增套餐接口
post /admin/setmeal
搞个新的SetmealController

请求的参数是json
里面是 需求分析中可看到的,最终保存的所有东西。
1、controller
接收请求参数,封装的有DTO,SetmealDTO,其属性有个List:
//套餐菜品关系
private List<SetmealDish> setmealDishes = new ArrayList<>();
SetmealDish是对应表的实体类。
@PostMapping
@ApiOperation("新增套餐")
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
2、serviecImp
任务:向套餐表、套餐菜品关系表中插入数据。
其中套餐菜品关系表中含setmeal_id,是套餐表的自增主键id,故需要在套餐表插入以后,生成了这个id以后,来get到,然后赋值一下,才能去套餐菜品关系表中插入数据。
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDTO
*/
@Transactional //多个表噢
public void saveWithDish(SetmealDTO setmealDTO) {
//前面都是setmeal表内容,后面是setmeal-dish表内容,需要向表中插入DTO数据
//前面表对应实体类Setmeal,后面表对应实体类SetmealDish
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO,setmeal);
//向套餐表中插入数据
setmealMapper.insert(setmeal);
//因为下面的部分需要得到生成的主键 套餐id,但这个功能函数还没跑完,还没出来呢,所以先得到一下。
//主键需回显,获取上面insert语句生成的主键⭐⭐⭐(需要在上面的mapper里加上useGeneratedKeys="true" keyProperty="id",就是给你传出来id)
Long setmealId = setmeal.getId();
//在下面直接用了List<SetmealDish>
// SetmealDish setmealDish = new SetmealDish();
//得到DTO中的list
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
//遍历list赋值给 表对应的实体类,但其实就还用他本身就可以了。。
//只需给他再set一个setmeal_id的值。
//foreach那里面就是一个变量的名字,可以变的。主要是用了setSetmealId他就会自动匹配变量是属于什么类型。
setmealDishes.forEach(setmealDish->{
setmealDish.setSetmealId(setmealId);
});
//保存setmeal-dish表,用的另一个mapper
setmealDishMapper.insertBatch(setmealDishes);
}
3、SetmealMapper
@AutoFill(value = OperationType.INSERT)
void insert(Setmeal setmeal);
xml:
<!--向套餐表setmeal表中插入数据-->
<!-- useGeneratedKeys="true" keyProperty="id" 就是说主键给你传出去,让你用-->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into setmeal(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>
4、SetmealDishMapper
/**
* 批量保存套餐和菜品的关联关系
* @param setmealDishes
*/
void insertBatch(List<SetmealDish> setmealDishes);
xml:
<!-- 批量保存套餐和菜品的关联关系 -->
<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>
List使用了for-each
2、套餐分页查询
2.1 分析
需要按照 套餐name、套餐分类category_id、售卖状态status去查询。
看下面项:
查询出来的要有 setmeal的内容(setmeal表)、套餐分类的name(setmeal表联合category表通过categoryId)、setmeal_dish表(虽然查出来没显示这个,但是我后面编辑修改用的也是SetmealVO,这个东西要回显的,所以VO里要有List<SetmealDish>属性)
封装在SetmealVO
接口设计:
查询是get方法
请求参数除了那几个查的字段,还有page 页码 和 pageSize每页的size,封装到DTO中。
返回数据是 PageResult 在common--result包下,PageResult 属性包括long total总记录数、List records结果
返回的数据是setmeal实体类加上一个categoryName,所以要新造接收实体类
在impl里可看到用的是新造的setmealVO,里面的属性跟上图一模一样,另外还多加了一个List<SetmealDish>(虽然页面原型图里没有显示,但我是包含着这个关系的,我后面修改啥的也要用它)
SetmealVO,在后面修改里回显 用的也是这个实体类。。所以这里VO里包括List属性。
2.2 实现
Controller:
请求参数是Query,不用写@RequestBody
@GetMapping("/page")
@ApiOperation("分页查询")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO){
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}
serviceImp:
固定写法
注意查出来的东西是VO接收的。。再注意VO的属性列都有啥
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
PageHelper.startPage(setmealPageQueryDTO.getPage(), setmealPageQueryDTO.getPageSize());
Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
mapper.xml:
<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>
3、删除套餐
3.1 分析
业务规则:
- 可以一次删除一个套餐,也可以批量删除套餐
- 起售中的套餐不能删除⭐⭐
接口:
要根据套餐的id,删除setmeal表项、setmeal_dish表项。
3.2.1 SetmealController
/**
* 批量删除套餐
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids){
setmealService.deleteBatch(ids);
return Result.success();
}
3.2.2 SetmealService
/**
* 批量删除套餐
* @param ids
*/
void deleteBatch(List<Long> ids);
3.2.3 SetmealServiceImpl
/**
* 批量删除套餐,遍历id删除
* 规则:在售的不能删,
* 所以要检查一下,在售的要删则抛出异常
* @param ids
*/
@Transactional
public void deleteBatch(List<Long> ids) {
ids.forEach(id -> {
Setmeal setmeal = setmealMapper.getById(id);
if(StatusConstant.ENABLE == setmeal.getStatus()){
//起售中的套餐不能删除
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}
});
ids.forEach(setmealId -> {
//删除套餐表中的数据
setmealMapper.deleteById(setmealId);
//删除套餐菜品关系表中的数据
setmealDishMapper.deleteBySetmealId(setmealId);
});
}
3.2.4 SetmealMapper
/**
* 根据id查询套餐
* @param id
* @return
*/
@Select("select * from setmeal where id = #{id}")
Setmeal getById(Long id);
/**
* 根据id删除套餐
* @param setmealId
*/
@Delete("delete from setmeal where id = #{id}")
void deleteById(Long setmealId);
3.2.5 SetmealDishMapper
/**
* 根据套餐id删除套餐和菜品的关联关系
* @param setmealId
*/
@Delete("delete from setmeal_dish where setmeal_id = #{setmealId}")
void deleteBySetmealId(Long setmealId);
4、修改套餐
4.1 分析
接口设计(共涉及到5个接口):
- 根据id查询套餐信息(回显数据,返回的封装到setmealVO)
- 根据类型查询分类(已完成)
- 根据分类id查询菜品(已完成)
- 图片上传(已完成)
- 修改套餐
回显是 查 setmeal 和 List
修改update功能是 直接更新setmeal表、更新setmeal_dish表(先把套餐id原本对应的全删了,再添加进去)
其中类似新增套餐,需要得到套餐id,再设置setmeal_dish表,去先全删再添加。只不过不同于新增套餐,那时候套餐id还没完全存在表里,需要useGeneratedKeys="true" keyProperty="id"
现在的套餐id,从传来的参数里直接get即可得到。
4.2 代码实现
4.2.1 SetmealController
/**
* 根据id查询套餐,用于修改页面回显数据
* 是路径参数,要用@PathVariable
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询套餐")
public Result<SetmealVO> getById(@PathVariable Long id) {
SetmealVO setmealVO = setmealService.getByIdWithDish(id);
return Result.success(setmealVO);
}
/**
* 修改套餐
*
* @param setmealDTO
* @return
*/
@PutMapping
@ApiOperation("修改套餐")
public Result update(@RequestBody SetmealDTO setmealDTO) {
setmealService.update(setmealDTO);
return Result.success();
}
4.2.2 SetmealServiceImpl
/**
* 根据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;
}
/**
* 修改套餐
*
* @param setmealDTO
*/
@Transactional
public void update(SetmealDTO setmealDTO) {
//先接收要改的setmeal表部分
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO,setmeal);
//1、改套餐表
setmealMapper.update(setmeal);
//记得要得到id,以便下面删除和修改
Long setmealId = setmealDTO.getId();
//2、改套餐--菜品表,先全删除再添加
setmealDishMapper.deleteBySetmealId(setmealId);
//接收List<SetmealDish>
//并添加上setmealId,以便构成SetmealDish表
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//添加进来修改的东西
setmealDishMapper.insertBatch(setmealDishes);
}
4.2.3 SetmealMapper
/**
* 根据id查询套餐,删除的时候检查要用;修改时回显也用
* @param id
* @return
*/
@Select("select * from setmeal where id=#{id}")
Setmeal getById(Long id);
/**
* 修改套餐表
* @param setmeal
*/
@AutoFill(OperationType.UPDATE) //省去在service里 对update_time user等公共字段的填充
void update(Setmeal setmeal);
4.2.4 SetmealMapper.xml
<!--修改套餐表-->
<update id="update">
update setmeal
<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="status != null">
status = #{status},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="image != null">
image = #{image},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser}
</if>
</set>
where id = #{id}
</update>
4.2.5 SetmealDishMapper
/**
* 根据套餐id查询套餐和菜品的关联关系
* 写具体点,参数写成setmealId
* @param setmealId
* @return
*/
@Select("select * from setmeal_dish where setmeal_id = #{setmealId}")
List<SetmealDish> getBySetmealId(Long setmealId);
/**
* 按照套餐id删除套餐时,先删除套餐--菜品关系表项 再添加进去新的。
* @param setmealId
*/
@Delete("delete from setmeal_dish where setmeal_id=#{setmealId}")
void deleteBySetmealId(Long setmealId);
/**
* 批量保存套餐和菜品的关联关系
* @param setmealDishes
*/
void insertBatch(List<SetmealDish> setmealDishes);
4.2.6 SetmealDishMapper.xml
<!-- 批量保存 套餐和菜品的关联关系,foreach遍历 -->
<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>
5、起售停售套餐
5.1 分析
业务规则:
- 可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作
- 起售的套餐可以展示在用户端,停售的套餐不能展示在用户端
- 起售套餐时,如果套餐内包含停售的菜品,则不能起售⭐⭐(起售套餐时,必须要菜品先是在售中的)⭐
- (禁售没有什么限制,直接更新status表就行了)
对于起售时的限制
需要查该setmealId 对应的菜品的status,必须是enable起售状态。
但dish表中status咋查,要连接dish表 和 setmeal_dish表,根据
dish_id,这样就可以根据setmealId 某套餐id 去定位到 其包含的菜品的status了。List dishList = dishMapper.getBySetmealId(id);
@Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
接口设计:
5.2 代码
5.2.1 SetmealController
/**
* 套餐起售停售
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
public Result startOrStop(@PathVariable Integer status, Long id) {
setmealService.startOrStop(status, id);
return Result.success();
}
5.2.2 SetmealServiceImpl
/**
* 套餐起售、停售
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
//起售套餐时,必须要菜品先是在售中的,,不然抛异常
if(status==StatusConstant.ENABLE){
List<Dish> dishList = dishMapper.getBySetmealId(id); //
if(dishList!=null && dishList.size()>0){
dishList.forEach(dish -> {
if(dish.getStatus()==StatusConstant.DISABLE){
throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
}
});
}
}
//改状态,正好复用上 上次的修改更新操作(之前写的是动态sql,给了啥字段改啥)
Setmeal setmeal = Setmeal.builder()
.id(id)
.status(status)
.build();
setmealMapper.update(setmeal);
}
5.2.4 DishMapper
/**
* 套餐起售功能中,需检查其中的菜品状态
* @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);
三、细节
1、公共字段自动填充
自定义@Autofill
SetmealMapper中改套餐表的时候,update 更新修改方法,用了@Autofill(...)
自动填充update_time,update_user,省去了在service里面再set
第一步:自定义一个注解annotation
第二步:自定义切面类,写好切入点,对属性应用反射来进行赋值。
第三步:在需要拦截的方法上,加上注解
回顾一下AOP
黄牛买票,中间商动态代理,对目标对象生成一个代理对象(这个代理对象中就有一些功能的增强,比如计算耗时。。),以后调用这个目标对象的时候,其实调用的是代理对象,返回功能增强的结果。
<1>自定义注解
第一步,自定义注解,
创annotation包,创AutoFill注解
/**
* 自定义的自动填充注解,去标识某些方法需要进行一些公共字段的填充
*/
@Target(ElementType.METHOD) //表示注解加在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型,insert update
OperationType value();
}
//OperationType是在common包下的枚举包下写的,里面只是声明了两个枚举
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
<2>⭐自定义切面类
自定义切面类:
实现 公共字段自动填充的 处理逻辑
对于切面上面的注解:
1、各种@Around @Before @After各种。。看在方法执行前还是后进行通知。。
2、@Pointcut用于对切入表达式重复了给它抽取:加注解@Pointcut去声明一个方法,如方法叫fun(),后面在别的方法的切入表达式的地方写这个方法即可,即写@Around("fun()")。
3、用切入点表达式execution不好描述的,不如用注解annotation,然后在aspect切面类中匹配的时候:
@Pointcut("@annotation(com.sky.annotation.AutoFill)") 这样匹配注解即可。
/**
* 自定义的切面类,实现 公共字段自动填充的 处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
// @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
@Pointcut("@annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){
}
/**
* 前置通知,在通知中进行公共字段的赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){ //⭐⭐joinpoint切入点
log.info("开始进行公共字段自动填充...");
//获取当前拦截的方法的操作类型是insert还是update
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]; //后面规范规定,mapper第一个参数是实体对象
//进行公共字段的赋值:
//1、准备数据
//2、根据不同操作类型,对属性通过反射来赋值
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
if(operationType==OperationType.INSERT){
//赋值四个字段
try {
// Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
//为了更规范,防止写错,name参数用自定义的常量代替
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) {
throw new RuntimeException(e);
}
}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) {
throw new RuntimeException(e);
}
}
}
}
getDeclaredMethod:返回Method方法对象;
invoke:根据传入的对象实例,通过配置的实参参数来调用方法
<3>使用注解
在mapper的 insert和update方法上,加注解
@AutoFill(value = OperationType.INSERT)
@AutoFill(value = OperationType.UPDATE)
2、文件上传功能
①阿里云oss配置(AliOSSProperties+yml)
阿里云oss,创建bucket
RAM用户,权限给了AccessfullOss:
AccessKey ID ××××××××××××
AccessKey Secret ××××××××××××××××
创建bucket,名字叫 skytakeout-work2
要有读写权限,不然发现后面生成的url打不开
后面授权策略也可以改一下:
代码第一步:
common类--properties包--AliOSSProperties.java:
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
上面写的是驼峰,后面yml里写的是 - 横线
第二步:
写配置的值,application.yml、application-dev.yml:
因为你具体的值,上线的时候可能要改,所以那些一定不会变的就在application.yml里,可能会变的在dev里,
我们在application.yml里写引用,引用dev里具体的
(若在公司里,再搞个application-prod.yml,上面值从dev改成 prod,它就当前表示是生产环境的配置。)
application.yml :
sky:
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}
application-dev.yml:
sky:
alioss:
endpoint: oss-cn-beijing.aliyuncs.com
access-key-id: ××××××××××××
access-key-secret: ××××××××××××
bucket-name: skytakeout-work2
②⭐写工具类AliOssUtil.java
sky-common/src/main/java/com/sky/utils/AliOssUtil.java
里面是upload函数,基本固定,主要是把文件,变到得到了文件的地址,string
@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();
}
}
③写配置类(OssConfiguration)
用于创建AliOssUtil对象(看②,这个对象是变file--存的地址,创出了这个对象,给它赋那些第一步配置好的值,然后调用这个对象的upload方法,即可得到我们要的阿里云存储地址了)
给它@Bean,它就会自动加载上 下面的方法,创出来一个AliOssUtil对象(那些值是第一步配置了的)
/**
* 配置类,用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean //保证只有一个AliOssUtil对象,miss没有的时候才创建
//注入AliOssProperties的bean,创建AliOssUtil对象
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建aliyun文件上传AliOssUtil工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
④开发controller
接口路径是 /admin/common/upload
那创建一个 CommonController,,,它是可复用的通用接口
以后公共的接口都放这个controller下面。
上传功能,返回的是 路径,故是Result,
请求参数 MultipartFile file,必须叫 file,要严格按照接口来,跟前端保持一致。
代码,
主要是 生成用uuid的带拼接后缀的新文件名,然后去调用AliOssUtil的upload方法,上传文件,得到其存储地址。
/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);
try {
//截取file后缀,.jpg .png...
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); //从.的位置开始截到最后
//新文件名
String objectName = UUID.randomUUID().toString() + extension;
//参数一个是bytes[](这里trycatch了),一个是name就是存在alioss的图片名(用uuid)
//upload函数搞来的 文件---> 存储地址
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
// throw new RuntimeException(e);
log.error("文件上传失败:{}",e);
}
return null;
}
}
⑤测试:
swagger对文件上传,不太支持去测试,没地方输图片。postman可以
下面直接前后端联调:
上传一个图片
会有显示。。
3、foreach
java serviceImp:
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
//foreach那里面就是一个变量的名字,可以变的。主要是用了setSetmealId他就会自动匹配变量是属于什么类型。
setmealDishes.forEach(setmealDish->{
setmealDish.setSetmealId(setmealId);
});
//保存setmeal-dish表,用的另一个mapper
setmealDishMapper.insertBatch(setmealDishes);
动态sql:
<!-- 批量保存套餐和菜品的关联关系 -->
<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>
4、常用注解
<1>http
@RequestMapping("/admin/employee") 类上面,下面的方法上面会直接往后拼接这个类上面的东西
@PostMapping("/login") 在方法上面,会拼接类上面的,http://localhost:8080/admin/employee/login @GetMapping @PutMapping @DeleteMapping
<2>修饰参数
传入路径参数时,要用@PathVariable,
如修改套餐,根据套餐id回显时,/{id},是路径参数
再如:
@PostMapping("/status/{status}") public ..... (@PathVariable("status") Integer status){}
对于json的输入:
(@RequestBody EmployeeDTO employeeDTO)
这个是@请求体,把传来的json格式,自动转为实体类,输入来的,是request
对于query格式:
(@RequestParam Integer id)
json的都是body参数,query的都是param参数,但这个可省略不写
<3>bean
@Autowired自动装配bean对象,在用的时候,即在声明了service和dao接口的地方加
controller -- service -- mapper
@RestContoller = @Controller + @ResponseBody,controller写@RestContoller即可,@ResponseBody作用就是把返回的自动转为 json去输出
在serviceImp上,加的是 @Service
在mapper上,加的是@Mapper
<4>管理
(因为引入了lombok):
@Data 自动搞get set方法
@AllArgsConstructor 所有参数的构造器
@NoArgsConstructor 无参构造
@Slf4j 日志输出 log
swagger接口描述相关:
(swagger是引入并配置了knife4j插件)
@Api 用在类上,例如Controller,表示对类的说明。如:@Api(tags="员工相关接口") @ApiModel 用在类上,例如entity、.DTO、VO 如:对于EmployeeLoginDTO实体类,
@ApiModel(description = "员工登录时传递的数据模型") @ApiModelProperty 用在属性上,描述属性信息,如上对其用户名和密码属性,@ApiModelProperty("密码")
@ApiOperation 用在方法上,例如ControllerE的方法,说明方法的用途、作用。
如:@ApiOperation(value="员工信息") @ApiOperation("员工退出")
加完后localhost:8080/doc.html里描述信息会变。
<5>其余
handle异常处理器
全局异常处理器,两个注解
@RestControllerAdvice = @ControllerAdvice + @ResponseBody,有responsebody,所以返回的error是Result类型的,可以自动转为 json
@ExceptionHandler 里面的value,写要捕获的异常类,Exception.class就是所有的异常。
@ConfigurationProperties(prefix = "sky.jwt") 表明是一个配置类(在common中),prefix的值是去sky-server即主项目的application.yml里找其对应的值。即通过这种注解方式,引入配置项的值。。
@Builder 在pojo--vo中,EmployeeLoginVO在controller中封装对象用的是EmployeeLoginVO.builder().id()。。。.build(),就是因为该实体类加了这个注解
自定义注解annotation:
在注解文件上添加的:
@Target(ElementType.METHOD) //表示注解加在方法上 @Retention(RetentionPolicy.RUNTIME) //retention保留,表示注解类的生命周期。
RetentionPolicy.SOURCE : 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
RetentionPolicy.CLASS : 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
RetentionPolicy.RUNTIME : 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。 生命周期长度 SOURCE < CLASS < RUNTIME ,即前者能作用的地方后者一定也能作用。