7.2.2 公寓信息管理
7.2.2.1 房间支付方式管理
房间支付方式管理共有三个接口,分别是查询全部支付方式列表、保存或更新支付方式和根据ID删除支付方式,下面逐一实现。
首先在PaymentTypeController
中注入PaymentTypeService
依赖,如下
@Tag(name = "支付方式管理")
@RequestMapping("/admin/payment")
@RestController
public class PaymentTypeController {
@Autowired
private PaymentTypeService service;
}
1. 查询全部支付方式列表
在PaymentTypeController
中增加如下内容
@Operation(summary = "查询全部支付方式列表")
@GetMapping("list")
public Result<List<PaymentType>> listPaymentType() {
List<PaymentType> list = service.list();
return Result.ok(list);
}
知识点:
-
逻辑删除功能
由于数据库中所有表均采用逻辑删除策略,所以查询数据时均需要增加过滤条件
is_deleted=0
。上述操作虽不难实现,但是每个查询接口都要考虑到,也显得有些繁琐。为简化上述操作,可以使用Mybatis-Plus提供的逻辑删除功能,它可以自动为查询操作增加
is_deleted=0
过滤条件,并将删除操作转为更新语句。具体配置如下,详细信息可参考官方文档。-
步骤一:在
application.yml
中增加如下内容mybatis-plus: global-config: db-config: logic-delete-field: flag # 全局逻辑删除的实体字段名(配置后可以忽略不配置步骤二) logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
-
步骤二:在实体类中的删除标识字段上增加
@TableLogic
注解(代码同下) -
忽略特定字段
通常情况下接口响应的Json对象中并不需要
create_time
、update_time
、is_deleted
等字段,这时只需在实体类中的相应字段添加@JsonIgnore
注解,该字段就会在序列化时被忽略。具体配置如下,详细信息可参考Jackson官方文档。
@Data public class BaseEntity implements Serializable { @Schema(description = "主键") @TableId(value = "id", type = IdType.AUTO) private Long id; @JsonIgnore @Schema(description = "创建时间") @TableField(value = "create_time", fill = FieldFill.INSERT) private Date createTime; @JsonIgnore @Schema(description = "更新时间") @TableField(value = "update_time", fill = FieldFill.UPDATE) private Date updateTime; @TableLogic @JsonIgnore @Schema(description = "逻辑删除") @TableField("is_deleted") private Byte isDeleted; }
注意:
逻辑删除功能只对Mybatis-Plus自动注入的sql起效,也就是说,对于手动在
Mapper.xml
文件配置的sql不会生效,需要单独考虑。
-
2. 保存或更新支付方式
在PaymentTypeController
中增加如下内容
@Operation(summary = "保存或更新支付方式")
@PostMapping("saveOrUpdate")
public Result saveOrUpdatePaymentType(@RequestBody PaymentType paymentType) {
//mp提供了更新或修改的方法
service.saveOrUpdate(paymentType);
return Result.ok();
}
知识点:
保存或更新数据时,前端通常不会传入isDeleted
、createTime
、updateTime
这三个字段,因此我们需要手动赋值。但是数据库中几乎每张表都有上述字段,所以手动去赋值就显得有些繁琐。为简化上述操作,我们可采取以下措施。
-
is_deleted
字段:可将数据库中该字段的默认值设置为0。 -
create_time
和update_time
:可使用mybatis-plus的自动填充功能,所谓自动填充,就是通过统一配置,在插入或更新数据时,自动为某些字段赋值,具体配置如下,详细信息可参考官方文档。-
为相关字段配置触发填充的时机,例如
create_time
需要在插入数据时填充,而update_time
需要在更新数据时填充。具体配置如下,观察@TableField
注解中的fill
属性。@Data public class BaseEntity implements Serializable { @Schema(description = "主键") @TableId(value = "id", type = IdType.AUTO) private Long id; @JsonIgnore @Schema(description = "创建时间") @TableField(value = "create_time", fill = FieldFill.INSERT) private Date createTime; @JsonIgnore @Schema(description = "更新时间") @TableField(value = "update_time", fill = FieldFill.UPDATE) private Date updateTime; @TableLogic @JsonIgnore @Schema(description = "逻辑删除") @TableField("is_deleted") private Byte isDeleted; }
-
配置自动填充的内容,具体配置如下
在common模块下创建
com.atguigu.lease.common.mybatisplus.MybatisMetaObjectHandler
类,内容如下:@Component public class MybatisMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); } }
在做完上述配置后,当写入数据时,Mybatis-Plus会自动将实体对象的
create_time
字段填充为当前时间,当更新数据时,则会自动将实体对象的update_time
字段填充为当前时间。 -
3. 根据ID删除支付方式
在PaymentTypeController
中增加如下内容
@Operation(summary = "根据ID删除支付方式")
@DeleteMapping("deleteById")
public Result deletePaymentById(@RequestParam Long id) {
service.removeById(id);
return Result.ok();
}
知识点:
MybatisPlus逻辑删除功能的使用。
7.2.2.2 房间租期管理
房间租期管理共有三个接口,分别是查询全部租期列表、保存或更新租期信息和根据ID删除租期,具体实现如下。
在LeaseTermController
中增加如下内容
package com.atguigu.lease.web.admin.controller.apartment;
import com.atguigu.lease.common.result.Result;
import com.atguigu.lease.model.entity.LeaseTerm;
import com.atguigu.lease.web.admin.service.LeaseTermService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "租期管理")
@RequestMapping("/admin/term")
@RestController
public class LeaseTermController {
@Autowired
private LeaseTermService Service;
@GetMapping("list")
@Operation(summary = "查询全部租期列表")
public Result<List<LeaseTerm>> listLeaseTerm() {
List<LeaseTerm> list = Service.list();
return Result.ok(list);
}
@PostMapping("saveOrUpdate")
@Operation(summary = "保存或更新租期信息")
public Result saveOrUpdate(@RequestBody LeaseTerm leaseTerm) {
Service.saveOrUpdate(leaseTerm);
return Result.ok();
}
@DeleteMapping("deleteById")
@Operation(summary = "根据ID删除租期")
public Result deleteLeaseTermById(@RequestParam Long id) {
Service.removeById(id);
return Result.ok();
}
}
7.2.2.3 标签管理
标签管理共有三个接口,分别是[根据类型]查询标签列表、保存或更新标签信息和根据ID删除标签,下面逐一实现。
首先在LabelController
中注入LabelInfoService
依赖,如下
@Tag(name = "标签管理")
@RestController
@RequestMapping("/admin/label")
public class LabelController {
@Autowired
private LabelInfoService service;
}
1. [根据类型]查询标签列表
在LabelController
中增加如下内容
@Operation(summary = "(根据类型)查询标签列表")
@GetMapping("list")
public Result<List<LabelInfo>> labelList(@RequestParam(required = false) ItemType type) {
LambdaQueryWrapper<LabelInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(type!=null, LabelInfo::getType, type);
List<LabelInfo> list = Service.list(queryWrapper);
return Result.ok(list);
}
知识点:
上述接口的功能是根据type(公寓/房间),查询标签列表。由于这个type字段在数据库、实体类、前后端交互的过程中有多种不同的形式,因此在请求和响应的过程中,type字段会涉及到多次类型转换。
首先明确一下type字段的各种形式:
-
数据库中
数据库中的type字段为
tinyint
类型+-------------+--------------+ | Field | Type | +-------------+--------------+ | id | bigint | | type | tinyint | | name | varchar(255) | | create_time | timestamp | | update_time | timestamp | | is_deleted | tinyint | +-------------+--------------+
-
实体类
实体类中的type字段为
ItemType
枚举类型LabelInfo
实体类如下@Schema(description = "标签信息表") @TableName(value = "label_info") @Data public class LabelInfo extends BaseEntity { private static final long serialVersionUID = 1L; @Schema(description = "类型") @TableField(value = "type") private ItemType type; @Schema(description = "标签名称") @TableField(value = "name") private String name; }
ItemType
枚举类如下public enum ItemType { APARTMENT(1, "公寓"), ROOM(2, "房间"); private Integer code; private String name; ItemType(Integer code, String name) { this.code = code; this.name = name; } }
-
前后端交互中
前后端交互所传递的数据中type字段为数字(1/2)。
具体转换过程如下图所示:
-
请求流程
说明
-
SpringMVC中的
WebDataBinder
组件负责将HTTP的请求参数绑定到Controller方法的参数,并实现参数类型的转换。 -
Mybatis中的
TypeHandler
用于处理Java中的实体对象与数据库之间的数据类型转换。
-
-
响应流程
说明
-
SpringMVC中的
HTTPMessageConverter
组件负责将Controller方法的返回值(Java对象)转换为HTTP响应体中的JSON字符串,或者将请求体中的JSON字符串转换为Controller方法中的参数(Java对象),例如下一个接口保存或更新标签信息
-
下面介绍一下每个环节的类型转换原理
-
WebDataBinder枚举类型转换
WebDataBinder
依赖于Converter实现类型转换,若Controller方法声明的@RequestParam
参数的类型不是String
,WebDataBinder
就会自动进行数据类型转换。SpringMVC提供了常用类型的转换器,例如String
到Integer
、String
到Date
,String
到Boolean
等等,其中也包括String
到枚举类型,但是String
到枚举类型的默认转换规则是根据实例名称("APARTMENT")转换为枚举对象实例(ItemType.APARTMENT)。若想实现code
属性到枚举对象实例的转换,需要自定义Converter
,代码如下,具体内容可参考官方文档。-
在web-admin模块自定义
com.atguigu.lease.web.admin.custom.converter.StringToItemTypeConverter
//将类交给Spring容器管理,之后进行注册交给mvc进行管理 @Component public class StringToItemTypeConverter implements Converter<String, ItemType> { @Override public ItemType convert(String code) { //获取当前枚举类的所有值 ItemType[] values = ItemType.values(); //使用循环遍历来寻找传入的数据是否有对应的值 for (ItemType itemType : values) { if (itemType.getCode().equals(Integer.valueOf(code))){ return itemType; } } //如果未找到则返回错误 throw new RuntimeException("code:"+code+"错误"); } }
-
注册上述的
StringToItemTypeConverter
,在web-admin模块创建com.atguigu.lease.web.admin.custom.config.WebMvcConfiguration
,内容如下:@Configuration public class WebMvcConfiguration implements WebMvcConfigurer { //将自定义的类型转换器添加到spring容器中 @Autowired private StringToItemTypeConverter stringToItemTypeConverter; //添加类型转换器 @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(this.stringToItemTypeConverter); } }
但是我们有很多的枚举类型都需要考虑类型转换这个问题,按照上述思路,我们需要为每个枚举类型都定义一个Converter,并且每个Converter的转换逻辑都完全相同,针对这种情况,我们使用ConverterFactory接口更为合适,这个接口可以将同一个转换逻辑应用到一个接口的所有实现类,因此我们可以定义一个
BaseEnum
接口,然后另所有的枚举类都实现该接口,然后就可以自定义ConverterFactory
,集中编写各枚举类的转换逻辑了。具体实现如下:-
在model模块定义
com.atguigu.lease.model.enums.BaseEnum
接口public interface BaseEnum { Integer getCode(); String getName(); }
-
令所有
com.atguigu.lease.model.enums
包下的枚举类都实现BaseEnun
接口 -
在web-admin模块自定义
com.atguigu.lease.web.admin.custom.converter.StringToBaseEnumConverterFactory
package com.atguigu.lease.web.admin.custom.converter; import com.atguigu.lease.model.enums.BaseEnum; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.stereotype.Component; @Component public class StringToBaseEnumConverterFactory implements ConverterFactory<String, BaseEnum> { @Override public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) { //使用匿名内部类 return new Converter<String, T>() { @Override public T convert(String code) { //获取当前调用的枚举的枚举值 T[] enumConstants = targetType.getEnumConstants(); for (T enumConstant : enumConstants) { if (enumConstant.getCode().equals(Integer.valueOf(code))){ return enumConstant; } } throw new IllegalArgumentException("非法的枚举值:" + code); } }; } }
-
注册上述的
ConverterFactory
,在web-admin模块创建com.atguigu.lease.web.admin.custom.config.WebMvcConfiguration
,内容如下:package com.atguigu.lease.web.admin.custom.config; import com.atguigu.lease.web.admin.custom.converter.StringToBaseEnumConverterFactory; import com.atguigu.lease.web.admin.custom.converter.StringToItemTypeConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfiguration implements WebMvcConfigurer { //将自定义的类型转换器添加到spring容器中 // @Autowired // private StringToItemTypeConverter stringToItemTypeConverter; //将自定义的类型转换器添加到spring容器中 @Autowired private StringToBaseEnumConverterFactory stringToBaseEnumConverterFactory; //添加类型转换器 @Override public void addFormatters(FormatterRegistry registry) { // registry.addConverter(this.stringToItemTypeConverter); //包含所有实现BasaEnum接口的枚举类都可以进行转化 registry.addConverterFactory(this.stringToBaseEnumConverterFactory); } }
注意:
最终采用的是
ConverterFactory
方案,因此StringToItemTypeConverter
相关代码可以直接删除。
-
-
TypeHandler枚举类型转换
Mybatis预置的
TypeHandler
可以处理常用的数据类型转换,例如String
、Integer
、Date
等等,其中也包含枚举类型,但是枚举类型的默认转换规则是枚举对象实例(ItemType.APARTMENT)和实例名称("APARTMENT")相互映射。若想实现code
属性到枚举对象实例的相互映射,需要自定义TypeHandler
。不过MybatisPlus提供了一个通用的处理枚举类型的TypeHandler。其使用十分简单,只需在
ItemType
枚举类的code
属性上增加一个注解@EnumValue
,Mybatis-Plus便可完成从ItemType
对象到code
属性之间的相互映射,具体配置如下。public enum ItemType { APARTMENT(1, "公寓"), ROOM(2, "房间"); @EnumValue private Integer code; private String name; ItemType(Integer code, String name) { this.code = code; this.name = name; } }
-
HTTPMessageConverter枚举类型转换
HttpMessageConverter
依赖于Json序列化框架(默认使用Jackson)。其对枚举类型的默认处理规则也是枚举对象实例(ItemType.APARTMENT)和实例名称("APARTMENT")相互映射。不过其提供了一个注解@JsonValue
,同样只需在ItemType
枚举类的code
属性上增加一个注解@JsonValue
,Jackson便可完成从ItemType
对象到code
属性之间的互相映射。具体配置如下,详细信息可参考Jackson官方文档。@Getter public enum ItemType { APARTMENT(1, "公寓"), ROOM(2, "房间"); @EnumValue @JsonValue private Integer code; private String name; ItemType(Integer code, String name) { this.code = code; this.name = name; } }
2.保存或更新标签信息
在LabelController
中增加如下内容
@Operation(summary = "新增或修改标签信息")
@PostMapping("saveOrUpdate")
public Result saveOrUpdateLabel(@RequestBody LabelInfo labelInfo) {
Service.saveOrUpdate(labelInfo);
return Result.ok();
}
3. 根据ID删除标签
在LabelController
中增加如下内容
@Operation(summary = "根据id删除标签信息")
@DeleteMapping("deleteById")
public Result deleteLabelById(@RequestParam Long id) {
Service.removeById(id);
return Result.ok();
}
7.2.2.4 配套管理
配套管理共有三个接口,分别是[根据类型]查询配套列表、保存或更新配套信息和根据ID删除配套,具体实现如下。
在FacilityController
中增加如下内容
@Tag(name = "配套管理")
@RestController
@RequestMapping("/admin/facility")
public class FacilityController {
@Autowired
private FacilityInfoService service;
@Operation(summary = "[根据类型]查询配套信息列表")
@GetMapping("list")
public Result<List<FacilityInfo>> listFacility(@RequestParam(required = false) ItemType type) {
LambdaQueryWrapper<FacilityInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(type!=null, FacilityInfo::getType, type);
List<FacilityInfo> list = service.list(queryWrapper);
return Result.ok(list);
}
@Operation(summary = "新增或修改配套信息")
@PostMapping("saveOrUpdate")
public Result saveOrUpdate(@RequestBody FacilityInfo facilityInfo) {
service.saveOrUpdate(facilityInfo);
return Result.ok();
}
@Operation(summary = "根据id删除配套信息")
@DeleteMapping("deleteById")
public Result removeFacilityById(@RequestParam Long id) {
service.removeById(id);
return Result.ok();
}
}
7.2.2.5 基本属性管理
房间基本属性管理共有五个接口,分别是保存或更新属性名称、保存或更新属性值、查询全部属性名称和属性值列表、根据ID删除属性名称、根据ID删除属性值。下面逐一是实现。
首先在AttrController
中注入AttrKeyService
和AttrValueService
,如下:
@Tag(name = "房间属性管理")
@RestController
@RequestMapping("/admin/attr")
public class AttrController {
@Autowired
private AttrKeyService attrKeyService;
@Autowired
private AttrValueService attrValueService;
}
1. 保存或更新属性名称
在AttrController
增加如下内容
@Operation(summary = "新增或更新属性名称")
@PostMapping("key/saveOrUpdate")
public Result saveOrUpdateAttrKey(@RequestBody AttrKey attrKey) {
attrkeyService.saveOrUpdate(attrKey);
return Result.ok();
}
2. 保存或更新属性值
在AttrController
中增加如下内容
@Operation(summary = "新增或更新属性值")
@PostMapping("value/saveOrUpdate")
public Result saveOrUpdateAttrValue(@RequestBody AttrValue attrValue) {
attrValueService.saveOrUpdate(attrValue);
return Result.ok();
}
3. 查询全部属性名称和属性值列表
-
查看响应的数据结构
查看web-admin模块下的
com.atguigu.lease.web.admin.vo.attr.AttrKeyVo
,内容如下:public class AttrKeyVo extends AttrKey { @Schema(description = "属性value列表") private List<AttrValue> attrValueList; }
-
编写Controller层逻辑
在
AttrController
中添加如下内容@Operation(summary = "查询全部属性名称和属性值列表") @GetMapping("list") public Result<List<AttrKeyVo>> listAttrInfo() { List<AttrKeyVo> list = attrkeyService.listAttrInfo(); return Result.ok(list); }
-
编写Service层逻辑
在
AttrKeyService
中增加如下内容/** * 查询所有的属性表和属性值 * * @return */ List<AttrKeyVo> listAttrInfo();
在
AttrKeyServiceImpl
中增加如下内容@Autowired private AttrKeyMapper attrKeyMapper; /** * 查询所有的属性表和属性值 * * @return */ @Override public List<AttrKeyVo> listAttrInfo() { List<AttrKeyVo> list = attrKeyMapper.listAttrInfo(); return list; }
-
编写Mapper层逻辑
在
AttrKeyMapper
中增加如下内容List<AttrKeyVo> listAttrInfo();
对应的在
AttrKeyMapper.xml
中增加如下内容<resultMap id="listAttr" type="com.atguigu.lease.web.admin.vo.attr.AttrKeyVo"> <id property="id" column="id"/> <result property="name" column="name"/> <collection property="attrValueList" ofType="com.atguigu.lease.model.entity.AttrValue"> <id property="id" column="attr_key_id"/> <result property="name" column="attr_key_name"/> <result property="attrKeyId" column="attr_key_id"/> </collection> </resultMap> <select id="listAttrInfo" resultMap="listAttr"> select k.id, k.name, v.id as attr_key_id , v.name as attr_key_name, v.attr_key_id from attr_key k left join attr_value v on k.id = v.attr_key_id and v.is_deleted = 0 where k.is_deleted = 0 </select>
另一种实现方式
List<AttrKeyVo> listAttrInfo();
// AttrValue listAttrvalue(Integer keyid);
<!-- 可用方法不同 \-->
<!-- <resultMap id="listAttr" type="com.atguigu.lease.web.admin.vo.attr.AttrKeyVo">-->
<!-- <id property="id" column="id"/>-->
<!-- <result property="name" column="name"/>-->
<!-- <collection property="attrValueList" ofType="com.atguigu.lease.model.entity.AttrValue" column="id" select="listAttrvalue"/>-->
<!-- </resultMap>-->
<!-- <select id="listAttrInfo" resultMap="listAttr">-->
<!-- select * from attr_key-->
<!-- </select>-->
<!-- <select id="listAttrvalue" resultType="com.atguigu.lease.model.entity.AttrValue"-->
<!-- parameterType="java.lang.Integer">-->
<!-- select * from attr_value where attr_key_id = #{keyid}-->
<!-- </select>-->
4. 根据ID删除属性名称
在AttrController
中增加如下内容,注意删除属性名称时,会一并删除其下的所有属性值
@Operation(summary = "根据id删除属性名称")
@DeleteMapping("key/deleteById")
public Result removeAttrKeyById(@RequestParam Long attrKeyId) {
//删除名字
attrkeyService.removeById(attrKeyId);
//删除数据
LambdaQueryWrapper<AttrValue> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AttrValue::getAttrKeyId, attrKeyId);
attrValueService.remove(wrapper);
return Result.ok();
}
5. 根据ID删除属性值
在AttrController
中增加如下内容
@Operation(summary = "根据id删除属性值")
@DeleteMapping("value/deleteById")
public Result removeAttrValueById(@RequestParam Long id) {
attrValueService.removeById(id);
return Result.ok();
}
7.2.2.6 公寓杂费管理
房间基本属性管理共有五个接口,分别是保存或更新杂费名称、保存或更新杂费值、查询全部杂费名称和杂费值列表、根据ID删除杂费名称、根据ID删除杂费值。下面逐一实现
首先在FeeController
中注入FeeKeyService
和FeeValueService
,如下
@Tag(name = "房间杂费管理")
@RestController
@RequestMapping("/admin/fee")
public class FeeController {
@Autowired
private FeeKeyService feeKeyService;
@Autowired
private FeeValueService feeValueService;
}
1. 保存或更新杂费名称
在FeeController
中增加如下内容
@Operation(summary = "保存或更新杂费名称")
@PostMapping("key/saveOrUpdate")
public Result saveOrUpdateFeeKey(@RequestBody FeeKey feeKey) {
feeKeyService.saveOrUpdate(feeKey);
return Result.ok();
}
2. 保存或更新杂费值
在FeeController
中增加如下内容
@Operation(summary = "保存或更新杂费值")
@PostMapping("value/saveOrUpdate")
public Result saveOrUpdateFeeValue(@RequestBody FeeValue feeValue) {
feeValueService.saveOrUpdate(feeValue);
return Result.ok();
}
3. 查询全部杂费名称和杂费值列表
-
查看响应的数据结构
查看web-admin模块下创的
com.atguigu.lease.web.admin.vo.fee.FeeKeyVo
,内容如下@Data public class FeeKeyVo extends FeeKey { @Schema(description = "杂费value列表") private List<FeeValue> feeValueList; }
-
编写Controller层逻辑
在
FeeController
中增加如下内容@Operation(summary = "查询全部杂费名称和杂费值列表") @GetMapping("list") public Result<List<FeeKeyVo>> feeInfoList() { List<FeeKeyVo> list = feeKeyService.feeInfoList(); return Result.ok(list); }
-
编写Service层逻辑
-
在
FeeKeyService
中增加如下内容List<FeeKeyVo> feeInfoList();
-
在
FeeKeyServiceImpl
中增加如下内容@Autowired private FeeKeyMapper feeKeyMapper; @Override public List<FeeKeyVo> feeInfoList() { List<FeeKeyVo> list = feeKeyMapper.feeInfoList(); return list; }
-
-
编写Mapper层逻辑
-
在
FeeKeyMapper
中增加如下内容List<FeeKeyVo> feeInfoList();
-
在
FeeKeyMapper.xml
中增加如下内容<resultMap id="feeVo" type="com.atguigu.lease.web.admin.vo.fee.FeeKeyVo"> <id property="id" column="id"/> <result property="name" column="name"/> <collection property="feeValueList" ofType="com.atguigu.lease.model.entity.FeeValue"> <id property="id" column="fee_value_id"/> <result property="name" column="fee_value_name"/> <result property="unit" column="unit"/> <result property="feeKeyId" column="fee_key_id"/> </collection> </resultMap> <select id="feeInfoList" resultMap="feeVo"> select k.id, k.name, v.id fee_value_id, v.name fee_value_name, v.unit, v.fee_key_id from fee_key k left join fee_value v on k.id = v.fee_key_id and v.is_deleted = 0 where k.is_deleted = 0; </select>
-
4. 根据ID删除杂费名称
在FeeController
中增加如下内容
@Operation(summary = "根据id删除杂费名称")
@DeleteMapping("key/deleteById")
@Transactional
public Result deleteFeeKeyById(@RequestParam Long feeKeyId) {
//删除名称
feeKeyService.removeById(feeKeyId);
//删除值
LambdaQueryWrapper<FeeValue> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FeeValue::getFeeKeyId,feeKeyId);
feeValueService.remove(queryWrapper);
return Result.ok();
}
5. 根据ID删除杂费值
在FeeController
中增加如下内容
@Operation(summary = "根据id删除杂费值")
@DeleteMapping("value/deleteById")
public Result deleteFeeValueById(@RequestParam Long id) {
feeValueService.removeById(id);
return Result.ok();
}
7.2.2.7 地区信息管理
地区信息管理共有三个接口,分别是查询省份信息列表,根据省份ID查询城市信息列表和根据城市ID查询区县信息列表,具体实现如下
在RegionInfoController
中增加如下内容
@Tag(name = "地区信息管理")
@RestController
@RequestMapping("/admin/region")
public class RegionInfoController {
@Autowired
private ProvinceInfoService provinceInfoService;
@Autowired
private CityInfoService cityInfoService;
@Autowired
private DistrictInfoService districtInfoService;
@Operation(summary = "查询省份信息列表")
@GetMapping("province/list")
public Result<List<ProvinceInfo>> listProvince() {
List<ProvinceInfo> list = provinceInfoService.list();
return Result.ok(list);
}
@Operation(summary = "根据省份id查询城市信息列表")
@GetMapping("city/listByProvinceId")
public Result<List<CityInfo>> listCityInfoByProvinceId(@RequestParam Long id) {
LambdaQueryWrapper<CityInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CityInfo::getProvinceId,id);
List<CityInfo> list = cityInfoService.list(queryWrapper);
return Result.ok(list);
}
@GetMapping("district/listByCityId")
@Operation(summary = "根据城市id查询区县信息")
public Result<List<DistrictInfo>> listDistrictInfoByCityId(@RequestParam Long id) {
LambdaQueryWrapper<DistrictInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DistrictInfo::getCityId,id);
List<DistrictInfo> list = districtInfoService.list(queryWrapper);
return Result.ok(list);
}
}
7.2.2.8 图片上传管理
由于公寓、房间等实体均包含图片信息,所以在新增或修改公寓、房间信息时,需要上传图片,因此我们需要实现一个上传图片的接口。
1. 图片上传流程
下图展示了新增房间或公寓时,上传图片的流程。
可以看出图片上传接口接收的是图片文件,返回的Minio对象的URL。
2. 图片上传接口开发
下面为该接口的具体实现
-
配置Minio Client
-
引入Minio Maven依赖
在common模块的
pom.xml
文件增加如下内容:<!--minio依赖--> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> </dependency>
-
配置Minio相关参数(注意:配置在web-admin下的配置文件中)
在
application.yml
中配置Minio的endpoint
、accessKey
、secretKey
、bucketName
等参数minio: endpoint: http://192.168.21.100:9000 access-key: minioadmin secret-key: minioadmin bucket-name: lease
注意:上述
<hostname>
、<port>
等信息需根据实际情况进行修改。 -
在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; }
-
在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(); } } //@Configuration @EnableConfigurationProperties(MinioProperties.class) //@ConfigurationPropertiesScan(basePackages = "com.atguigu.lease.common.minio") //public class MinioConfiguration { // // @Autowired // private MinioProperties properties; // // @Bean // public MinioClient minioClient() { // // 打印配置值以进行调试 // System.out.println("Endpoint: " + properties.getEndpoint()); // System.out.println("Access Key: " + properties.getAccessKey()); // System.out.println("Secret Key: " + properties.getSecretKey()); // System.out.println("Bucket Name: " + properties.getBucketName()); // // return MinioClient.builder().endpoint(properties.getEndpoint()).credentials(properties.getAccessKey(), properties.getSecretKey()).build(); // } //}
-
-
开发图片上传接口
-
编写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) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { String url = service.upload(file); return Result.ok(url); } }
说明:
MultipartFile
是Spring框架中用于处理文件上传的类,它包含了上传文件的信息(如文件名、文件内容等)。 -
编写Service层逻辑
-
在
FileService
中增加如下内容String upload(MultipartFile file);
-
在
FileServiceImpl
中增加如下内容@Service public class FileServiceImpl implements FileService { @Autowired private MinioConfiguration minioConfiguration; @Autowired private MinioProperties minioProperties; @Override public String upload(MultipartFile file) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { //判断桶是否存在 MinioClient minioClient = minioConfiguration.minioClient(); boolean b = minioClient.bucketExists(BucketExistsArgs.builder() .bucket(minioProperties.getBucketName()).build()); if (!b){ //桶不存在 //创建桶 minioClient.makeBucket(MakeBucketArgs.builder() .bucket(minioProperties.getBucketName()).build()); //给这个桶设置配置文件 minioClient.setBucketPolicy(SetBucketPolicyArgs.builder() .bucket(minioProperties.getBucketName()) .config(createBucketPolicyConfig(minioProperties.getBucketName())) .build()); } //开始上传稿文件 //putObject方法可以上传流文件 String filename = filename(file); minioClient.putObject(PutObjectArgs.builder() .bucket(minioProperties.getBucketName()) .contentType(file.getContentType()) .stream(file.getInputStream(),file.getSize(),-1) .object(filename).build()); return String.join("/", minioProperties.getEndpoint(), minioProperties.getBucketName(), filename); } private String filename(MultipartFile file){ return new SimpleDateFormat("yyyyMMdd").format(new Date()) +"/"+ UUID.randomUUID()+"-"+file.getOriginalFilename(); } private String createBucketPolicyConfig(String bucketName) { return """ { "Statement" : [ { "Action" : "s3:GetObject", "Effect" : "Allow", "Principal" : "*", "Resource" : "arn:aws:s3:::%s/*" } ], "Version" : "2012-10-17" } """.formatted(bucketName); } }
注意:
上述
createBucketPolicyConfig
方法的作用是生成用于描述指定bucket访问权限的JSON字符串。最终生成的字符串格式如下,其表示,允许(Allow
)所有人(*
)获取(s3:GetObject
)指定桶(<bucket-name>
)的内容。{ "Statement" : [ { "Action" : "s3:GetObject", "Effect" : "Allow", "Principal" : "*", "Resource" : "arn:aws:s3:::%s/*" } ], "Version" : "2012-10-17" }
由于公寓、房间的图片为公开信息,所以将其设置为所有人可访问。
-
异常处理
-
问题说明
上述代码只是对
MinioClient
方法抛出的各种异常进行了捕获,然后打印了异常信息,目前这种处理逻辑,无论Minio是否发生异常,前端在上传文件时,总是会受到成功的响应信息。可按照以下步骤进行操作,查看具体现象关闭虚拟机中的Minio服务
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(); } }
-
全局异常处理
按照上述写法,所有的Controller层方法均需要增加
try-catch
逻辑,使用Spring MVC提供的全局异常处理功能,可以将所有处理异常的逻辑集中起来,进而统一处理所有异常,使代码更容易维护。具体用法如下,详细信息可参考官方文档:
在common模块中创建
com.atguigu.lease.common.exception.GlobalExceptionHandler
类,内容如下//`@ControllerAdvice`用于声明处理全局Controller方法异常的类 @ControllerAdvice public class GlobalExceptionHandler { //注解表明该方法要捕获的异常 @ExceptionHandler(Exception.class) //ResponseBody返回数据 @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
类,内容如下:package com.atguigu.lease.web.admin.vo.apartment; import com.atguigu.lease.model.entity.ApartmentInfo; import com.atguigu.lease.web.admin.vo.graph.GraphVo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; @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.saveOrupdateBatchapartment(apartmentSubmitVo); return Result.ok(); }
-
编写Service层逻辑
-
在
ApartmentInfoService
中增加如下内容void saveOrupdateBatchapartment(ApartmentSubmitVo apartmentSubmitVo);
-
在
ApartmentInfoServiceImpl
中增加如下内容注意:所需
Service
和Mapper
的注入语句省略未写。@Override public void saveOrupdateBatchapartment(ApartmentSubmitVo apartmentSubmitVo) { //先判断是否有id即判断是新增还是修改 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) { //创建一个page对象 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="pageApartmentItemByQuery" 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参数不方便调试
可在application.yml文件中增加如下配置,将queryVo做打平处理
springdoc: default-flat-param-object: true
将
spring.default-flat-param-object
参数设置为true
后,效果如下。 -
3. 根据ID获取公寓详细信息
-
查看响应数据结构
查看web-admin下的
com.atguigu.lease.web.admin.vo.apartment.ApartmentDetailVo
,内容如下@Schema(description = "公寓信息") @Data public class ApartmentDetailVo extends ApartmentInfo { @Schema(description = "图片列表") private List<GraphVo> graphVoList; @Schema(description = "标签列表") private List<LabelInfo> labelInfoList; @Schema(description = "配套列表") private List<FacilityInfo> facilityInfoList; @Schema(description = "杂费列表") private List<FeeValueVo> feeValueVoList; }
-
编写Controller层逻辑
在
ApartmentController
中增加如下内容@Operation(summary = "根据ID获取公寓详细信息") @GetMapping("getDetailById") public Result<ApartmentDetailVo> getDetailById(@RequestParam Long id) { ApartmentDetailVo detail = service.getDetailbyId(id); return Result.ok(detail); }
-
编写Service层逻辑
-
在
ApartmentInfoService
中增加如下内容ApartmentDetailVo getApartmentDetailById(Long id);
-
在
-