图片上传管理
由于公寓、房间等实体均包含图片信息,所以在新增或修改公寓、房间信息时,需要上传图片,因此我们需要实现一个上传图片的接口。
**1. 图片上传流程**
下图展示了新增房间或公寓时,上传图片的流程。
可以看出图片上传接口接收的是图片文件,返回的Minio对象的URL。
*2. 图片上传接口开发**
下面为该接口的具体实现
- **配置Minio Client**
- 引入Minio Maven依赖
在**common模块**的`pom.xml`文件增加如下内容:
spring框架专门用来接收文件的一个类 他里面包含文件 的内容和文件的各种信息 名称或者大小
前端接收到图片之后会保存到minio minio中每一个文件中都有一个url返回的是一个url
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
</dependency>
- 配置Minio相关参数
在`application.yml`中配置Minio的`endpoint`、`accessKey`、`secretKey`、`bucketName`等参数
minio:
endpoint: http://<hostname>:<port>
access-key: <access-key>
secret-key: <secret-key>
bucket-name: <bucket-name>
**注意**:上述`<hostname>`、`<port>`等信息需根据实际情况进行修改。
- 在**common模块**中创建`com.atguigu.lease.common.minio.MinioConfiguration`,内容如下
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class MinioConfiguration {
@Autowired
private MinioProperties properties;
@Bean
public MinioClient minioClient() {
return MinioClient.builder().endpoint(properties.getEndpoint()).credentials(properties.getAccessKey(), properties.getSecretKey()).build();
}
}
第一种方式:使用@value进行映射
第二种方式: - 在**common模块**中创建`com.atguigu.lease.common.minio.MinioProperties`,内容如下
@ConfigurationProperties(prefix = "minio")
@Data
public class MinioProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucketName;
}
configurationproperties就是把minio开头的类进行绑定
但是没有注册
只需要
第二种方式
第一种方式只能逐个配置 第二种方式可以统一扫描
- **开发图片上传接口**
- 编写Controller层逻辑
在`FileUploadController`中增加如下内容
@Tag(name = "文件管理")
@RequestMapping("/admin/file")
@RestController
public class FileUploadController {
@Autowired
private FileService service;
@Operation(summary = "上传文件")
@PostMapping("upload")
public Result<String> upload(@RequestParam MultipartFile file) {
String url = service.upload(file);
return Result.ok(url);
}
}
**说明:**`MultipartFile`是Spring框架中用于处理文件上传的类,它包含了上传文件的信息(如文件名、文件内容等)。
- 编写Service层逻辑
- 在`FileService`中增加如下内容
String upload(MultipartFile file);
- 在`FileServiceImpl`中增加如下内容
@Autowired
private MinioProperties properties;
@Autowired
private MinioClient client;
@Override
public String upload(MultipartFile file) {
try {
boolean bucketExists = client.bucketExists(BucketExistsArgs.builder().bucket(properties.getBucketName()).build());
if (!bucketExists) {
client.makeBucket(MakeBucketArgs.builder().bucket(properties.getBucketName()).build());
client.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(properties.getBucketName()).config(createBucketPolicyConfig(properties.getBucketName())).build());
}
String filename = new SimpleDateFormat("yyyyMMdd").format(new Date()) + "/" + UUID.randomUUID() + "-" + file.getOriginalFilename();
client.putObject(PutObjectArgs.builder().
bucket(properties.getBucketName()).
object(filename).
stream(file.getInputStream(), file.getSize(), -1).
contentType(file.getContentType()).build());
return String.join("/", properties.getEndpoint(), properties.getBucketName(), filename);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private String createBucketPolicyConfig(String bucketName) {
return """
{
"Statement" : [ {
"Action" : "s3:GetObject",
"Effect" : "Allow",
"Principal" : "*",
"Resource" : "arn:aws:s3:::%s/*"
} ],
"Version" : "2012-10-17"
}
""".formatted(bucketName);
}
使用minio访问图片的时候 他会直接下载 是因为是以stream的方式
要想直接在网页里面展示的话只需要这行代码
**注意**:
上述`createBucketPolicyConfig`方法的作用是生成用于描述指定bucket访问权限的JSON字符串。最终生成的字符串格式如下,其表示,允许(`Allow`)所有人(`*`)获取(`s3:GetObject`)指定桶(`<bucket-name>`)的内容。
```json
{
"Statement" : [ {
"Action" : "s3:GetObject",
"Effect" : "Allow",
"Principal" : "*",
"Resource" : "arn:aws:s3:::<bucket-name>/*"
} ],
"Version" : "2012-10-17"
}
```
由于公寓、房间的图片为公开信息,所以将其设置为所有人可访问。
- **异常处理**
- **问题说明**
上述代码只是对`MinioClient`方法抛出的各种异常进行了捕获,然后打印了异常信息,目前这种处理逻辑,无论Minio是否发生异常,前端在上传文件时,总是会受到成功的响应信息。可按照以下步骤进行操作,查看具体现象
关闭虚拟机中的Minio服务
```bash
systemctl stop minio
```
启动项目,并上传文件,观察接收的响应信息
- **问题解决思路**
为保证前端能够接收到正常的错误提示信息,应该将Service方法的异常抛出到Controller方法中,然后在Controller方法中对异常进行捕获并处理。具体操作如下
**Service层代码**
@Override
public String upload(MultipartFile file) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException{
boolean bucketExists = minioClient.bucketExists(
BucketExistsArgs.builder()
.bucket(properties.getBucketName())
.build());
if (!bucketExists) {
minioClient.makeBucket(
MakeBucketArgs.builder()
.bucket(properties.getBucketName())
.build());
minioClient.setBucketPolicy(
SetBucketPolicyArgs.builder()
.bucket(properties.getBucketName())
.config(createBucketPolicyConfig(properties.getBucketName()))
.build());
}
String filename = new SimpleDateFormat("yyyyMMdd").format(new Date()) +
"/" + UUID.randomUUID() + "-" + file.getOriginalFilename();
minioClient.putObject(
PutObjectArgs.builder()
.bucket(properties.getBucketName())
.stream(file.getInputStream(), file.getSize(), -1)
.object(filename)
.contentType(file.getContentType())
.build());
return String.join("/",properties.getEndpoint(),properties.getBucketName(),filename);
}
**Controller层代码**
public Result<String> upload(@RequestParam MultipartFile file) {
try {
String url = service.upload(file);
return Result.ok(url);
} catch (Exception e) {
e.printStackTrace();
return Result.fail();
}
}
改动就是将service里面的异常 挪到了controller中
这样关闭mino之后前端就会正常反应失败
- **全局异常处理**
按照上述写法,所有的Controller层方法均需要增加`try-catch`逻辑,使用Spring MVC提供的**全局异常处理**功能,可以将所有处理异常的逻辑集中起来,进而统一处理所有异常,使代码更容易维护。
具体用法如下,详细信息可参考[官方文档](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-exceptionhandler.html):
在**common模块**中创建`com.atguigu.lease.common.exception.GlobalExceptionHandler`类,内容如下
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result error(Exception e){
e.printStackTrace();
return Result.fail();
}
}
上述代码中的关键注解的作用如下
`@ControllerAdvice`用于声明处理全局Controller方法异常的类
`@ExceptionHandler`用于声明处理异常的方法,`value`属性用于声明该方法处理的异常类型
`@ResponseBody`表示将方法的返回值作为HTTP的响应体
**注意:**
全局异常处理功能由SpringMVC提供,因此需要在**common模块**的`pom.xml`中引入如下依赖
<!--spring-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- **修改Controller层代码**
由于前文的`GlobalExceptionHandler`会处理所有Controller方法抛出的异常,因此Controller层就无序关注异常的处理逻辑了,因此Controller层代码可做出如下调整。
public Result<String> upload(@RequestParam MultipartFile file) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
String url = service.upload(file);
return Result.ok(url);
}
#### 7.2.2.9 公寓管理
公寓管理共有六个接口,下面逐一实现。
首先在`ApartmentController`中注入`ApartmentInfoService`,如下
@Tag(name = "公寓信息管理")
@RestController
@RequestMapping("/admin/apartment")
public class ApartmentController {
@Autowired
private ApartmentInfoService service;
}
##### 1. 保存或更新公寓信息
- **查看请求的数据结构**
查看**web-admin模块**中的`com.atguigu.lease.web.admin.vo.apartment.ApartmentSubmitVo`类,内容如下:
@Schema(description = "公寓信息")
@Data
public class ApartmentSubmitVo extends ApartmentInfo {
@Schema(description="公寓配套id")
private List<Long> facilityInfoIds;
@Schema(description="公寓标签id")
private List<Long> labelIds;
@Schema(description="公寓杂费值id")
private List<Long> feeValueIds;
@Schema(description="公寓图片id")
private List<GraphVo> graphVoList;
}
- **编写Controller层逻辑**
在`ApartmentController`中增加如下内容
@Operation(summary = "保存或更新公寓信息")
@PostMapping("saveOrUpdate")
public Result saveOrUpdate(@RequestBody ApartmentSubmitVo apartmentSubmitVo) {
service.saveOrUpdateApartment(apartmentSubmitVo);
return Result.ok();
}
- **编写Service层逻辑**
- 在`ApartmentInfoService`中增加如下内容
void saveOrUpdateApartment(ApartmentSubmitVo apartmentSubmitVo);
- 在`ApartmentInfoServiceImpl`中增加如下内容
**注意**:所需`Service`和`Mapper`的注入语句省略未写。
@Override
public void saveOrUpdateApartment(ApartmentSubmitVo apartmentSubmitVo) {
boolean isUpdate = apartmentSubmitVo.getId()!=null;
super.saveOrUpdate(apartmentSubmitVo);
if (isUpdate){
//1.删除图片列表
LambdaQueryWrapper<GraphInfo> graphQueryWrapper = new LambdaQueryWrapper<>();
graphQueryWrapper.eq(GraphInfo::getItemType, ItemType.APARTMENT);
graphQueryWrapper.eq(GraphInfo::getItemId,apartmentSubmitVo.getId());
graphInfoService.remove(graphQueryWrapper);
//2.删除配套列表
LambdaQueryWrapper<ApartmentFacility> facilityQueryWrapper = new LambdaQueryWrapper<>();
facilityQueryWrapper.eq(ApartmentFacility::getApartmentId,apartmentSubmitVo.getId());
apartmentFacilityService.remove(facilityQueryWrapper);
//3.删除标签列表
LambdaQueryWrapper<ApartmentLabel> labelQueryWrapper = new LambdaQueryWrapper<>();
labelQueryWrapper.eq(ApartmentLabel::getApartmentId,apartmentSubmitVo.getId());
apartmentLabelService.remove(labelQueryWrapper);
//4.删除杂费列表
LambdaQueryWrapper<ApartmentFeeValue> feeQueryWrapper = new LambdaQueryWrapper<>();
feeQueryWrapper.eq(ApartmentFeeValue::getApartmentId,apartmentSubmitVo.getId());
apartmentFeeValueService.remove(feeQueryWrapper);
}
//1.插入图片列表
List<GraphVo> graphVoList = apartmentSubmitVo.getGraphVoList();
if (!CollectionUtils.isEmpty(graphVoList)){
ArrayList<GraphInfo> graphInfoList = new ArrayList<>();
for (GraphVo graphVo : graphVoList) {
GraphInfo graphInfo = new GraphInfo();
graphInfo.setItemType(ItemType.APARTMENT);
graphInfo.setItemId(apartmentSubmitVo.getId());
graphInfo.setName(graphVo.getName());
graphInfo.setUrl(graphVo.getUrl());
graphInfoList.add(graphInfo);
}
graphInfoService.saveBatch(graphInfoList);
}
//2.插入配套列表
List<Long> facilityInfoIdList = apartmentSubmitVo.getFacilityInfoIds();
if (!CollectionUtils.isEmpty(facilityInfoIdList)){
ArrayList<ApartmentFacility> facilityList = new ArrayList<>();
for (Long facilityId : facilityInfoIdList) {
ApartmentFacility apartmentFacility = new ApartmentFacility();
apartmentFacility.setApartmentId(apartmentSubmitVo.getId());
apartmentFacility.setFacilityId(facilityId);
facilityList.add(apartmentFacility);
}
apartmentFacilityService.saveBatch(facilityList);
}
//3.插入标签列表
List<Long> labelIds = apartmentSubmitVo.getLabelIds();
if (!CollectionUtils.isEmpty(labelIds)) {
List<ApartmentLabel> apartmentLabelList = new ArrayList<>();
for (Long labelId : labelIds) {
ApartmentLabel apartmentLabel = new ApartmentLabel();
apartmentLabel.setApartmentId(apartmentSubmitVo.getId());
apartmentLabel.setLabelId(labelId);
apartmentLabelList.add(apartmentLabel);
}
apartmentLabelService.saveBatch(apartmentLabelList);
}
//4.插入杂费列表
List<Long> feeValueIds = apartmentSubmitVo.getFeeValueIds();
if (!CollectionUtils.isEmpty(feeValueIds)) {
ArrayList<ApartmentFeeValue> apartmentFeeValueList = new ArrayList<>();
for (Long feeValueId : feeValueIds) {
ApartmentFeeValue apartmentFeeValue = new ApartmentFeeValue();
apartmentFeeValue.setApartmentId(apartmentSubmitVo.getId());
apartmentFeeValue.setFeeValueId(feeValueId);
apartmentFeeValueList.add(apartmentFeeValue);
}
apartmentFeeValueService.saveBatch(apartmentFeeValueList);
}
}
##### 2. 根据条件分页查询公寓列表
- **查看请求和响应的数据结构**
- **请求数据结构**
- `current`和`size`为分页相关参数,分别表示**当前所处页面**和**每个页面的记录数**。
- `ApartmentQueryVo`为公寓的查询条件,详细结构如下:
@Data
@Schema(description = "公寓查询实体")
public class ApartmentQueryVo {
@Schema(description = "省份id")
private Long provinceId;
@Schema(description = "城市id")
private Long cityId;
@Schema(description = "区域id")
private Long districtId;
}
- **响应数据结构**
单个公寓信息记录可查看`com.atguigu.lease.web.admin.vo.apartment.ApartmentItemVo`,内容如下:
@Data
@Schema(description = "后台管理系统公寓列表实体")
public class ApartmentItemVo extends ApartmentInfo {
@Schema(description = "房间总数")
private Long totalRoomCount;
@Schema(description = "空闲房间数")
private Long freeRoomCount;
}
**配置Mybatis-Plus分页插件**
在**common模块**中的`com.atguigu.lease.common.mybatisplus.MybatisPlusConfiguration`中增加如下内容:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
- **接口实现**
- **编写Controller层逻辑**
在`ApartmentController`中增加如下内容:
@Operation(summary = "根据条件分页查询公寓列表")
@GetMapping("pageItem")
public Result<IPage<ApartmentItemVo>> pageItem(@RequestParam long current, @RequestParam long size, ApartmentQueryVo queryVo) {
IPage<ApartmentItemVo> page = new Page<>(current, size);
IPage<ApartmentItemVo> list = service.pageApartmentItemByQuery(page, queryVo);
return Result.ok(list);
}
- **编写Service层逻辑**
- 在`ApartmentInfoService`中增加如下内容
IPage<ApartmentItemVo> pageApartmentItemByQuery(IPage<ApartmentItemVo> page, ApartmentQueryVo queryVo);
- 在`ApartmentInfoServiceImpl`中增加如下内容
@Override
public IPage<ApartmentItemVo> pageApartmentItemByQuery(IPage<ApartmentItemVo> page, ApartmentQueryVo queryVo) {
return apartmentInfoMapper.pageApartmentItemByQuery(page, queryVo);
}
- **编写Mapper层逻辑**
- 在`ApartmentInfoMapper`中增加如下内容
IPage<ApartmentItemVo> pageApartmentItemByQuery(IPage<ApartmentItemVo> page, ApartmentQueryVo queryVo);
- 在`ApartmentInfoMapper.xml`中增加如下内容
<select id="pageItem" resultType="com.atguigu.lease.web.admin.vo.apartment.ApartmentItemVo">
select ai.id,
ai.name,
ai.introduction,
ai.district_id,
ai.district_name,
ai.city_id,
ai.city_name,
ai.province_id,
ai.province_name,
ai.address_detail,
ai.latitude,
ai.longitude,
ai.phone,
ai.is_release,
ifnull(tc.cnt,0) total_room_count,
ifnull(tc.cnt,0) - ifnull(cc.cnt,0) free_room_count
from (select id,
name,
introduction,
district_id,
district_name,
city_id,
city_name,
province_id,
province_name,
address_detail,
latitude,
longitude,
phone,
is_release
from apartment_info
<where>
is_deleted=0
<if test="queryVo.provinceId != null">
and province_id=#{queryVo.provinceId}
</if>
<if test="queryVo.cityId != null">
and city_id=#{queryVo.cityId}
</if>
<if test="queryVo.districtId != null">
and district_id=#{queryVo.districtId}
</if>
</where>
) ai
left join
(select apartment_id,
count(*) cnt
from room_info
where is_deleted = 0
and is_release = 1
group by apartment_id) tc
on ai.id = tc.apartment_id
left join
(select apartment_id,
count(*) cnt
from lease_agreement
where is_deleted = 0
and status in (2, 5)
group by apartment_id) cc
on ai.id = cc.apartment_id
</select>
**注意:**
默认情况下Knife4j为该接口生成的接口文档如下图所示,其中的queryVo参数不方便调试
<img src="images/flat-param-false.png" style="zoom:50%; zoom:50%;border: 1px solid #000;" />
可在application.yml文件中增加如下配置,将queryVo做打平处理
springdoc:
default-flat-param-object: true
将`spring.default-flat-param-object`参数设置为`true`后,效果如下。
<img src="images/flat-param-true.png" style="zoom:50%; zoom:50%;border: 1px solid #000;" />
116(没看懂)