黑马苍穹外卖——基本数据管理

456 阅读22分钟

以套餐管理为例,记录项目管理端——基本数据管理内容。

一、数据库设计

序号数据表名中文名称
1employee员工表
2category分类表
3dish菜品表
4dish_flavor菜品口味表
5setmeal套餐表
6setmeal_dish套餐菜品关系表
7user用户表
8address_book地址表
9shopping_cart购物车表
10orders订单表
11order_detail订单明细表

数据管理涉及到前六个表:

1. employee

employee表为员工表,用于存储商家内部的员工信息。具体表结构如下:

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)姓名
usernamevarchar(32)用户名唯一
passwordvarchar(64)密码
phonevarchar(11)手机号
sexvarchar(2)性别
id_numbervarchar(18)身份证号
statusint账号状态1正常 0锁定
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人id
update_userbigint最后修改人id

2. category

category表为分类表,用于存储商品的分类信息。具体表结构如下:

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)分类名称唯一
typeint分类类型1菜品分类 2套餐分类
sortint排序字段用于分类数据的排序
statusint状态1启用 0禁用
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人id
update_userbigint最后修改人id

3. dish

dish表为菜品表,用于存储菜品的信息。具体表结构如下:

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)菜品名称唯一
category_idbigint分类id逻辑外键
pricedecimal(10,2)菜品价格
imagevarchar(255)图片路径
descriptionvarchar(255)菜品描述
statusint售卖状态1起售 0停售
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人id
update_userbigint最后修改人id

4. dish_flavor

dish_flavor表为菜品口味表,用于存储菜品的口味信息。具体表结构如下:

字段名数据类型说明备注
idbigint主键自增
dish_idbigint菜品id逻辑外键
namevarchar(32)口味名称
valuevarchar(255)口味值

5. setmeal

setmeal表为套餐表,用于存储套餐的信息。具体表结构如下:

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)套餐名称唯一
category_idbigint分类id逻辑外键
pricedecimal(10,2)套餐价格
imagevarchar(255)图片路径
descriptionvarchar(255)套餐描述
statusint售卖状态1起售 0停售
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人id
update_userbigint最后修改人id

6. setmeal_dish

setmeal_dish表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:

字段名数据类型说明备注
idbigint主键自增
setmeal_idbigint套餐id逻辑外键
dish_idbigint菜品id逻辑外键
namevarchar(32)菜品名称冗余字段
pricedecimal(10,2)菜品单价冗余字段
copiesint菜品份数

以套餐管理为例,实现四个功能:

1、新增

2、分页查询

3、删除

4、修改

5、起售停售


二、套餐管理

资料中day12含最终代码

1、新增套餐

1.1 分析

<1>需求

image-20231102210347040

setmeal 表里有一栏 category_id,意思是你这个套餐要有所属的 分类表项。

套餐菜品——添加菜品:

image-20231102210425142

是在当前页面添加一个框。。如上图

看页面原型: 可看出加的菜显示了name price copied,这都是setmeal-dish表的内容。它需要依赖setmeal_id外键和dish_id外键。

image-20231102210828972

<2>规则分析

需要DTO中属性:套餐名、套餐的所属类(要下拉菜单显示,category中/list已实现根据type列出来)、套餐价格、图片、描述、

还有嵌套的添加菜品(插入的是setmeal-dish表的内容)


业务规则:

  • 套餐名称唯一
  • 套餐必须属于某个分类(category_id)⭐
  • 套餐必须包含菜品
  • 名称、分类、价格、图片为必填项
  • 添加菜品窗口需要根据分类类型来展示菜品(看图,它有个侧边菜品导航)⭐
  • 新增的套餐默认为停售状态⭐

接口设计(共涉及到4个接口):

  • 根据类型查询分类(已完成,category/list,根据type得到List,选套餐属于啥类时,下拉菜单展示)
  • 根据分类id查询菜品(在菜品controller写,根据categoryId得到List,这是那个添加菜品时需要展示)
  • 图片上传(已完成)
  • 新增套餐(存到setmeal-dish表)

1.2 实现查询菜品接口

分析:

根据分类id查询菜品,即小窗口里的功能。看图的侧栏,是按类别列出来 + 按名字搜索查询

另外:

必须保证 在售出的菜才可以添加到套餐中

image-20221018141521068

在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

image-20231106104812290

image-20231106104903001转存失败,建议直接上传图片文件

请求的参数是json

里面是 需求分析中可看到的,最终保存的所有东西。

image-20231106105000566

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去查询。

image-20221018152429246.png

看下面项:

查询出来的要有 setmeal的内容(setmeal表)、套餐分类的name(setmeal表联合category表通过categoryId)、setmeal_dish表(虽然查出来没显示这个,但是我后面编辑修改用的也是SetmealVO,这个东西要回显的,所以VO里要有List<SetmealDish>属性)

封装在SetmealVO

接口设计:

image-20221018152731141

查询是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 分析

业务规则:

  • 可以一次删除一个套餐,也可以批量删除套餐
  • 起售中的套餐不能删除⭐⭐

接口:

image-20221018154541067

要根据套餐的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 分析

image-20221018160214225

接口设计(共涉及到5个接口):

  • 根据id查询套餐信息(回显数据,返回的封装到setmealVO)
  • 根据类型查询分类(已完成)
  • 根据分类id查询菜品(已完成)
  • 图片上传(已完成)
  • 修改套餐

回显是 查 setmeal 和 List

修改update功能是 直接更新setmeal表、更新setmeal_dish表(先把套餐id原本对应的全删了,再添加进去)

其中类似新增套餐,需要得到套餐id,再设置setmeal_dish表,去先全删再添加。只不过不同于新增套餐,那时候套餐id还没完全存在表里,需要useGeneratedKeys="true" keyProperty="id"

现在的套餐id,从传来的参数里直接get即可得到。

image-20221018160915177

image-20221018160949864

image-20221018161046352

image-20221018161117780

image-20221018161139861

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}")

接口设计:

image-20221018165055208

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

image-20231024171920967

第一步:自定义一个注解annotation

第二步:自定义切面类,写好切入点,对属性应用反射来进行赋值。

第三步:在需要拦截的方法上,加上注解

回顾一下AOP

黄牛买票,中间商动态代理,对目标对象生成一个代理对象(这个代理对象中就有一些功能的增强,比如计算耗时。。),以后调用这个目标对象的时候,其实调用的是代理对象,返回功能增强的结果。

<1>自定义注解

第一步,自定义注解,

创annotation包,创AutoFill注解

image-20231024172213543

/**
 * 自定义的自动填充注解,去标识某些方法需要进行一些公共字段的填充
 */
@Target(ElementType.METHOD)     //表示注解加在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //数据库操作类型,insert update
    OperationType value();
}

//OperationType是在common包下的枚举包下写的,里面只是声明了两个枚举
public enum OperationType {
    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT

}

<2>⭐自定义切面类

自定义切面类:

image-20231024173234385

实现 公共字段自动填充的 处理逻辑

对于切面上面的注解:

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打不开

image-20231026171906112

后面授权策略也可以改一下:

代码第一步:

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里具体的

image-20231026170517535

(若在公司里,再搞个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)

image-20231026160000132

用于创建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可以

下面直接前后端联调:

上传一个图片

image-20231026172129232

会有显示。。

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 ,即前者能作用的地方后者一定也能作用。