租房项目开发实战(四)

「“芯”想事成,造物无敌」嵌入式硬件创意项目征集令 10w+人浏览 177人参与

AI助手已提取文章相关产品:

后台管理系统前后端联调

保持后端项目正常运行

启动前端项目

第一步:安装node,建议安装18.20.3版本长期支持版

第二步:配置npm的镜像

npm config set registry https://registry.npmmirror.com

第三步:导入前端项目rentHouseAdmin,使用webstorm或者vscode

第四步:执行命令

npm install

第五步:配置后端接口地址

修改项目根目录下的.env.development​文件中的VITE_APP_BASE_URL​变量的值为后端接口的地址,此处改为http://localhost:8080​即可,如下

VITE_APP_BASE_URL='http://localhost:8080'

第六步:启动项目

npm run dev

如果遇到报错,请检查node版本,node -v 查看当前node版本,版本太低的话,会导致npm install 出现报错

移动端后端开发

我们移动端后端的路径是web-app下

SpringBoot配置

1.创建application.yml配置文件

在web-app模块的src/main/resources目录下新建application.yml文件,文件内容如下:

server:
  port: 8081

2. 创建SpringBoot启动类

在web-app模块下创建com.yuhuan.lease.AppWebApplication​类,内容如下:

@SpringBootApplication
public class AppWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(AppWebApplication.class);
    }
}

Mybatis-Plus配置

这个配置和web-admin一样,可以复制过来

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://192.168.200.128:3306/lease?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2b8
    username: root
    password: Server@123
    hikari:
      connection-test-query: SELECT 1 # 自动检测连接
      connection-timeout: 60000 #数据库连接超时时间,默认30秒
      idle-timeout: 500000 #空闲连接存活最大时间,默认600000(10分钟)
      max-lifetime: 540000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
      maximum-pool-size: 12 #连接池最大连接数,默认是10
      minimum-idle: 10 #最小空闲连接数量
      pool-name: SPHHikariPool # 连接池名称
  data:
    redis:
      host: 192.168.200.128
      port: 6379
      database: 0

#用于打印框架生成的sql语句,便于调试
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#  global-config:
#    db-config:
#      logic-delete-field: isDeleted # 全局逻辑删除的实体字段名
#      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
#      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)


minio:
  endpoint: http://192.168.200.128:9000
  access-key: minioadmin
  secret-key: minioadmin
  bucket-name: lease

springdoc:
  default-flat-param-object: true

Knife4j配置

1. 配置类

在 web-app 模块下创建配置类:com.yuhuan.lease.web.app.custom.config.Knife4jConfiguration,代码如下:

@Configuration
public class Knife4jConfiguration {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("APP接口")
                        .version("1.0")
                        .description("用户端APP接口")
                        .termsOfService("http://doc.xiaominfo.com")
                        .license(new License().name("Apache 2.0")
                                .url("http://doc.xiaominfo.com")));
    }
    

    @Bean
    public GroupedOpenApi loginAPI() {
        return GroupedOpenApi.builder().group("登录信息").
                pathsToMatch("/app/login/**", "/app/info").
                build();
    }

    @Bean
    public GroupedOpenApi personAPI() {
        return GroupedOpenApi.builder().group("个人信息").
                pathsToMatch(
                        "/app/history/**",
                        "/app/appointment/**",
                        "/app/agreement/**"
                ).
                build();
    }

    @Bean
    public GroupedOpenApi lookForRoomAPI() {
        return GroupedOpenApi.builder().group("找房信息").
                pathsToMatch(
                        "/app/apartment/**",
                        "/app/room/**",
                        "/app/payment/**",
                        "/app/region/**",
                        "/app/term/**"
                ).
                build();
    }
}

导入基础代码

导入的代码和目标位置如下:

导入代码模块包名/路径说明
mapper接口web-app​com.yuhuan.lease.web.app.mapper​
mapper xmlweb-appsrc/main/resources/mapper
serviceweb-app​com.yuhuan.lease.web.app.service​
serviceImplweb-app​com.yuhuan.lease.web.app.service.impl​

导入接口定义代码

需要导入的代码和目标位置如下:

导入代码模块包名/路径说明
controllerweb-app​com.yuhuan.lease.web.app.controller​
voweb-app​com.yuhuan.lease.web.app.vo​View Object,用于封装或定义接口接受及返回的数据结构

启动项目

由于common模块配置了MinioClient这个Bean,而web-app模块依赖common模块,在启动AppWebApplication时,SpringBoot会尝试创建MinioClient实例。但由于web-app模块的application.yml文件中缺少MinioClient所需参数(该模块暂不需要使用MinioClient),导致实例创建失败。

解决方案是给MinioClient的配置类添加@ConditionalOnProperty注解,该注解确保仅当存在minio.endpoint属性时才会启用配置类。

@Configuration
@EnableConfigurationProperties(MinioProperties.class)
@ConditionalOnProperty(name = "minio.endpoint")
public class MinioConfiguration {

    @Autowired
    private MinioProperties properties;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder().endpoint(properties.getEndpoint()).credentials(properties.getAccessKey(), properties.getSecretKey()).build();
    }
}

登录管理

登录管理涉及三个核心接口:短信验证码获取、用户登录和个人信息查询。此外,还需通过HandlerInterceptor为所有受保护接口添加JWT验证逻辑。

接口开发

注入LoginService​

@RestController
@Tag(name = "登录管理")
@RequestMapping("/app/")
public class LoginController {

    @Autowired
    private LoginService service;
}
1.获取短信验证码

我们这里使用阿里云的短信服务,登录阿里云后,搜索短信服务,先申请免费试用

点击发送测试短信

简单配置一下

我们选择java版本,下面有maven的依赖,我们引入进来就行了,在common模块的pom.xml文件中增加如下内容

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>dysmsapi20170525</artifactId>
</dependency>
  • 在application.yml​中增加如下内容
aliyun:
  sms:
    access-key-id: <access-key-id>
    access-key-secret: <access-key-secret>
    endpoint: dysmsapi.aliyuncs.com

上述access-key-id​、access-key-secret​需根据实际情况进行修改。

common模块中创建com.yuhuan.lease.common.sms.AliyunSMSProperties类,具体实现如下:

@Data
@ConfigurationProperties(prefix = "aliyun.sms")
public class AliyunSMSProperties {

    private String accessKeyId;

    private String accessKeySecret;

    private String endpoint;
}
  • 在 common 模块中创建 com.yuhuan.lease.common.sms.AliyunSmsConfiguration 类,具体实现如下:
package com.yuhuan.lease.common.sms;

import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.teaopenapi.models.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(AliyunSMSProperties.class)
@ConditionalOnProperty(name = "aliyun.sms.endpoint")
public class AliyunSMSConfiguration {

    @Autowired
    private AliyunSMSProperties properties;

    @Bean
    public Client smsClient() {
        Config config = new Config();
        config.setAccessKeyId(properties.getAccessKeyId());
        config.setAccessKeySecret(properties.getAccessKeySecret());
        config.setEndpoint(properties.getEndpoint());
        try {
            return new Client(config);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}
  • controller层
    @GetMapping("login/getCode")
    @Operation(summary = "获取短信验证码")
    public Result getCode(@RequestParam String phone) {
        service.getSMSCode(phone);
        return Result.ok();
    }
  • service层
public interface LoginService {
    void getSMSCode(String phone);
}
@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private SmsService smsService;
    @Override
    public void getSMSCode(String phone) {
        String code = CodeUtil.getRandomCode(6);
        String key = RedisConstant.APP_LOGIN_PREFIX + phone;

        Boolean hasKey = stringRedisTemplate.hasKey(key);
        if (hasKey) {
            Long ttl = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
            if (ttl > RedisConstant.APP_LOGIN_CODE_TTL_SEC - RedisConstant.APP_LOGIN_CODE_RESEND_TIME_SEC) {
                throw new LeaseException(ResultCodeEnum.APP_SEND_SMS_TOO_OFTEN);
            }
        }

        stringRedisTemplate.opsForValue().set(key, code, RedisConstant.APP_LOGIN_CODE_TTL_SEC, TimeUnit.SECONDS);
        smsService.sendSms(phone, code);
    }
}
package com.yuhuan.lease.web.app.service;

public interface SmsService {
    void sendSms(String phone, String code);
}
@Service
public class SmsServiceImpl implements SmsService {
    @Autowired
    private Client client;
    @Override
    public void sendSms(String phone, String code) {
        SendSmsRequest smsRequest = new SendSmsRequest();
        smsRequest.setPhoneNumbers(phone);
        smsRequest.setSignName("阿里云短信测试");
        smsRequest.setTemplateCode("SMS_154950909");
        smsRequest.setTemplateParam("{\"code\":\"" + code + "\"}");
        try {
            client.sendSms(smsRequest);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

阿里云现在试用的短信,也要认证,并且个人都没有办法认证,要企业账号,真是垃圾

2. 登录和注册接口

登录注册校验流程

  1. 参数校验

    • 前端提交手机号phone和短信验证码code
    • 若任一参数为空,返回"手机号为空"或"验证码为空"
  2. 验证码校验

    • 根据phone查询Redis存储的验证码
    • 若未查到,返回"验证码已过期"
    • 若验证码不匹配,返回"验证码错误"
  3. 用户处理

    • 查询数据库用户信息
    • 若用户不存在,自动创建新用户并入库
  4. 状态检查

    • 检查用户是否被禁用
    • 若被禁用,返回"账号被禁用"
  5. 认证通过

    • 生成JWT令牌返回给前端
  • 查看数据结构
@Data
@Schema(description = "APP端登录实体")
public class LoginVo {

    @Schema(description = "手机号码")
    private String phone;

    @Schema(description = "短信验证码")
    private String code;
}
  • controller层
    @PostMapping("login")
    @Operation(summary = "登录")
    public Result<String> login(@RequestBody LoginVo loginVo) {
        String token = service.login(loginVo);
        return Result.ok(token);
    }
  • service层
public interface LoginService {
    void getSMSCode(String phone);
    String login(LoginVo loginVo);
}
    /**
     * 用户登录接口实现
     * 功能说明:基于手机号+验证码的登录逻辑,支持新用户自动注册,登录成功后返回JWT令牌
     * @param loginVo 登录请求参数封装对象(包含手机号、验证码等信息)
     * @return JWT令牌(用于后续接口鉴权)
     * @throws LeaseException 业务异常(包含具体错误码和提示信息)
     */
    public String login(LoginVo loginVo) {
        // 1. 基础参数校验:手机号非空校验
        // 手机号是登录的核心标识,必须传递,否则直接抛出参数为空异常
        if (!StringUtils.hasText(loginVo.getPhone())) {
            throw new LeaseException(ResultCodeEnum.APP_LOGIN_PHONE_EMPTY);
        }

        // 1. 基础参数校验:验证码非空校验
        // 验证码是身份验证的关键,必须传递,否则直接抛出参数为空异常
        if (!StringUtils.hasText(loginVo.getCode())) {
            throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_EMPTY);
        }

        // 2. 验证码有效性校验:从Redis中获取存储的验证码
        // 构建Redis中验证码的存储key(前缀+手机号,保证唯一性)
        String key = RedisConstant.APP_LOGIN_PREFIX + loginVo.getPhone();
        // 从Redis获取对应手机号的验证码(验证码发送时已存入,设置了过期时间)
        String code = redisTemplate.opsForValue().get(key);

        // 2.1 验证码过期/未发送校验:Redis中无对应验证码,说明已过期或未发送
        if (code == null) {
            throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_EXPIRED);
        }

        // 2.2 验证码正确性校验:前端传入的验证码与Redis中存储的不一致
        if (!code.equals(loginVo.getCode())) {
            throw new LeaseException(ResultCodeEnum.APP_LOGIN_CODE_ERROR);
        }

        // 3. 用户存在性校验:根据手机号查询用户信息
        // 构建Lambda查询条件(避免硬编码字段名,提高代码可维护性)
        LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserInfo::getPhone, loginVo.getPhone()); // 按手机号精确查询
        UserInfo userInfo = userInfoService.getOne(queryWrapper); // 查询单个用户(手机号唯一)

        // 3.1 新用户自动注册:如果用户不存在,创建默认用户信息
        if (userInfo == null) {
            userInfo = new UserInfo();
            userInfo.setPhone(loginVo.getPhone()); // 绑定登录手机号
            userInfo.setStatus(BaseStatus.ENABLE); // 初始状态:启用(可正常使用)
            // 默认昵称:"用户-手机号后4位"(保护隐私,无需用户手动设置)
            userInfo.setNickname("用户-" + loginVo.getPhone().substring(6));
            userInfoService.save(userInfo); // 保存新用户到数据库
        }

        // 4. 账号状态校验:判断用户是否被禁用
        // 若用户状态为禁用(DISABLE),不允许登录,抛出对应异常
        if (userInfo.getStatus().equals(BaseStatus.DISABLE)) {
            throw new LeaseException(ResultCodeEnum.APP_ACCOUNT_DISABLED_ERROR);
        }

        // 5. 生成JWT令牌:登录成功,创建包含用户ID和手机号的令牌
        // JWT令牌用于后续接口请求的身份认证,避免重复登录校验
        return JwtUtil.createToken(userInfo.getId(), loginVo.getPhone());
    }
  • 编写AuthInterceptor

在web-app模块中创建com.yuhuan.lease.web.app.custom.interceptor.AuthInterceptor类,具体实现如下:
 

@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("access-token");

        Claims claims = JwtUtil.parseToken(token);
        Long userId = claims.get("userId", Long.class);
        String username = claims.get("username", String.class);
        LoginUserHolder.setLoginUser(new LoginUser(userId, username));

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LoginUserHolder.clear();
    }
}
  • 注册AuthInterceptor
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Autowired
    private AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.authInterceptor).addPathPatterns("/app/**").excludePathPatterns("/app/login/**");
    }
}

在增加上述拦截器后,为方便继续调试其他接口,可以获取一个长期有效的Token,将其配置到Knife4j的全局参数中。

3.查询登录用户的个人信息
  • 查看响应数据结构
@Schema(description = "用户基本信息")
@Data
@AllArgsConstructor
public class UserInfoVo {

    @Schema(description = "用户昵称")
    private String nickname;

    @Schema(description = "用户头像")
    private String avatarUrl;
}
  • controller层
    @GetMapping("info")
    @Operation(summary = "获取登录用户信息")
    public Result<UserInfoVo> info() {
        UserInfoVo info = service.getUserInfoById(LoginUserHolder.getLoginUser().getUserId());
        return Result.ok(info);
    }
  • service层
public interface LoginService {
    void getSMSCode(String phone);

    String login(LoginVo loginVo);

    UserInfoVo getUserInfoById(Long userId);
}
    @Override
    public UserInfoVo getUserInfoById(Long userId) {
        UserInfo userInfo = userInfoService.getById(userId);
        return new UserInfoVo(userInfo.getNickname(), userInfo.getAvatarUrl());
    }

找房

地区信息

房源搜索模块的地区信息需通过三个接口获取:查询省份列表、根据省份ID查询城市列表、根据城市ID查询区县列表。具体实现如下:

在RegionController中添加以下接口:

@Tag(name = "地区信息")
@RestController
@RequestMapping("/app/region")
public class RegionController {

    @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);
    }
}

支付方式

关于找房模块的支付方式功能,只需一个接口即可实现全部支付方式的获取。具体实现方案如下:

请在PaymentTypeController中新增以下代码内容:

@Tag(name = "支付方式接口")
@RestController
@RequestMapping("/app/payment")
public class PaymentTypeController {

    @Autowired
    private PaymentTypeService service;
    @Operation(summary = "获取全部支付方式列表")
    @GetMapping("list")
    public Result<List<PaymentType>> list() {
        List<PaymentType> list = service.list();
        return Result.ok(list);
    }
}

房间信息

需要实现三个房间信息相关的接口:

  1. 按条件分页查询房间列表
  2. 根据ID查询房间详情
  3. 按公寓ID分页查询房间列表

具体实现步骤如下:
首先在RoomController中注入RoomInfoService,代码如下:

@Autowired
private RoomInfoService roomInfoService;
1.按条件分页查询房间列表
  • 查看数据请求结构以及返回结构
@Data
@Schema(description = "房间查询实体")
public class RoomQueryVo {


    @Schema(description = "省份Id")
    private Long provinceId;

    @Schema(description = "城市Id")
    private Long cityId;

    @Schema(description = "区域Id")
    private Long districtId;

    @Schema(description = "最小租金")
    private BigDecimal minRent;

    @Schema(description = "最大租金")
    private BigDecimal maxRent;

    @Schema(description = "支付方式")
    private Long paymentTypeId;

    @Schema(description = "价格排序方式", allowableValues = {"desc", "asc"})
    private String orderType;
}
@Schema(description = "APP房间列表实体")
@Data
public class RoomItemVo {

    @Schema(description = "房间id")
    private Long id;

    @Schema(description = "房间号")
    private String roomNumber;

    @Schema(description = "租金(元/月)")
    private BigDecimal rent;

    @Schema(description = "房间图片列表")
    private List<GraphVo> graphVoList;

    @Schema(description = "房间标签列表")
    private List<LabelInfo> labelInfoList;

    @Schema(description = "房间所属公寓信息")
    private ApartmentInfo apartmentInfo;
}
  • controller层
    @Operation(summary = "分页查询房间列表")
    @GetMapping("pageItem")
    public Result<IPage<RoomItemVo>> pageItem(@RequestParam long current, @RequestParam long size, RoomQueryVo queryVo) {
        Page<RoomItemVo> page = new Page<>(current, size);
        IPage<RoomItemVo> list = roomInfoService.pageRoomItemByQuery(page, queryVo);
        return Result.ok(list);
    }
  • service层
public interface RoomInfoService extends IService<RoomInfo> {
    IPage<RoomItemVo> pageRoomItemByQuery(Page<RoomItemVo> page, RoomQueryVo queryVo);
}
public class RoomInfoServiceImpl extends ServiceImpl<RoomInfoMapper, RoomInfo>
        implements RoomInfoService {
    @Autowired
    private RoomInfoMapper roomInfoMapper;
    @Override
    public IPage<RoomItemVo> pageRoomItemByQuery(Page<RoomItemVo> page, RoomQueryVo queryVo) {
        return roomInfoMapper.pageRoomItemByQuery(page, queryVo);
    }
}
  • mapper层
public interface RoomInfoMapper extends BaseMapper<RoomInfo> {

    IPage<RoomItemVo> pageRoomItemByQuery(Page<RoomItemVo> page, RoomQueryVo queryVo);
}
<!--
  RoomInfoMapper:房间信息数据访问层接口映射文件
  核心功能:提供房间列表查询、关联数据(公寓、图片、标签)查询等数据操作
  对应接口:com.yuhuan.lease.web.app.mapper.RoomInfoMapper
-->
<mapper namespace="com.yuhuan.lease.web.app.mapper.RoomInfoMapper">

    <!--
      结果集映射:RoomItemVoMap
      作用:将数据库查询结果映射为前端所需的 RoomItemVo 视图对象
      autoMapping="true":开启属性自动映射(字段名与属性名一致时无需手动配置)
      关联关系说明:
        1. 一对一关联:房间 -> 公寓信息(ApartmentInfo)
        2. 一对多关联:房间 -> 图片列表(GraphVoList)
        3. 一对多关联:房间 -> 标签列表(LabelInfoList)
    -->
    <resultMap id="RoomItemVoMap" type="com.yuhuan.lease.web.app.vo.room.RoomItemVo" autoMapping="true">
        <!-- 主键映射:房间ID(手动指定确保主键匹配,避免自动映射异常) -->
        <id column="id" property="id"/>

        <!--
          一对一关联映射:公寓信息(ApartmentInfo)
          property:RoomItemVo 中存储公寓信息的属性名
          javaType:关联对象的全类名
          autoMapping="true":公寓信息的字段自动映射到 ApartmentInfo 属性
        -->
        <association property="apartmentInfo" javaType="com.yuhuan.lease.model.entity.ApartmentInfo"
                     autoMapping="true">
            <!-- 公寓主键映射:注意column="id"对应公寓表的ID(因查询时未别名,需明确主键) -->
            <id column="id" property="id"/>
        </association>

        <!--
          一对多关联映射:房间图片列表(GraphVoList)
          property:RoomItemVo 中存储图片列表的属性名
          ofType:集合中元素的全类名(GraphVo)
          select:子查询SQL的ID(通过房间ID查询图片列表)
          column:传递给子查询的参数(房间ID,对应主查询的room_info.id)
        -->
        <collection property="graphVoList" ofType="com.yuhuan.lease.web.app.vo.graph.GraphVo"
                    select="selectGraphVoListByRoomId" column="id"/>

        <!--
          一对多关联映射:房间标签列表(LabelInfoList)
          逻辑说明:通过房间ID关联 room_label 中间表,间接查询标签信息
          select:子查询SQL的ID(通过房间ID查询标签列表)
          column:传递给子查询的参数(房间ID)
        -->
        <collection property="labelInfoList" ofType="com.yuhuan.lease.model.entity.LabelInfo"
                    select="selectLabelInfoListByRoomId" column="id"/>
    </resultMap>

    <!--
      分页查询房间列表(带条件筛选)
      id:对应Mapper接口中的方法名 pageRoomItemByQuery
      resultMap:使用上面定义的 RoomItemVoMap 进行结果映射
      核心逻辑:
        1. 主表:room_info(房间表),关联表:apartment_info(公寓表)
        2. 过滤条件:未删除、已发布、未被占用(排除租赁中/已签约状态)
        3. 支持省/市/区、租金范围、支付方式筛选
        4. 支持租金升序/降序排序
    -->
    <select id="pageRoomItemByQuery" resultMap="RoomItemVoMap">
        select
        ri.id, -- 房间ID
        ri.room_number, -- 房间号
        ri.rent, -- 租金
        ai.id apartment_id, -- 公寓ID(别名避免与房间ID冲突)
        ai.name, -- 公寓名称
        ai.introduction, -- 公寓简介
        ai.district_id, -- 区域ID
        ai.district_name, -- 区域名称
        ai.city_id, -- 城市ID
        ai.city_name, -- 城市名称
        ai.province_id, -- 省份ID
        ai.province_name, -- 省份名称
        ai.address_detail, -- 详细地址
        ai.latitude, -- 纬度(用于地图定位)
        ai.longitude, -- 经度(用于地图定位)
        ai.phone, -- 联系电话
        ai.is_release -- 公寓发布状态
        from room_info ri -- 房间表别名 ri
        -- 左连接公寓表:条件包含公寓未删除(ai.is_deleted=0),避免查询已删除公寓的房间
        left join apartment_info ai on ri.apartment_id = ai.id and ai.is_deleted = 0
        <where>
            -- 基础过滤条件:房间未删除(逻辑删除)
            ri.is_deleted = 0
            -- 房间已发布(只有发布状态的房间才对用户可见)
            and ri.is_release = 1
            -- 排除已被占用的房间:查询租赁协议中状态为 2(租赁中)、5(已签约)的房间ID,排除这些房间
            and ri.id not in(
            select room_id
            from lease_agreement
            where is_deleted = 0 -- 协议未删除
            and status in(2,5) -- 协议状态:租赁中/已签约
            )
            <!-- 条件筛选:省份ID(非空时生效) -->
            <if test="queryVo.provinceId != null">
                and ai.province_id = #{queryVo.provinceId}
            </if>
            <!-- 条件筛选:城市ID(非空时生效) -->
            <if test="queryVo.cityId != null">
                and ai.city_id = #{queryVo.cityId}
            </if>
            <!-- 条件筛选:区域ID(非空时生效) -->
            <if test="queryVo.districtId != null">
                and ai.district_id = #{queryVo.districtId}
            </if>
            <!-- 条件筛选:租金范围(最小租金和最大租金都非空时生效) -->
            <if test="queryVo.minRent != null and queryVo.maxRent != null">
                and (ri.rent &gt;= #{queryVo.minRent} and ri.rent &lt;= #{queryVo.maxRent})
            </if>
            <!-- 条件筛选:支付方式(非空时生效) -->
            <if test="queryVo.paymentTypeId != null">
                and ri.id in (
                -- 通过房间-支付方式中间表查询符合支付方式的房间ID
                select room_id
                from room_payment_type
                where is_deleted = 0 -- 中间表记录未删除
                and payment_type_id = #{queryVo.paymentTypeId} -- 匹配指定支付方式ID
                )
            </if>
        </where>
        <!-- 排序条件:orderType为asc(升序)或desc(降序)时生效,按租金排序 -->
        <if test="queryVo.orderType == 'desc' or queryVo.orderType == 'asc'">
            order by ri.rent ${queryVo.orderType}
        </if>
    </select>

    <!--
      根据房间ID查询图片列表
      id:子查询ID,供 resultMap 中的 collection 标签调用
      resultType:查询结果映射为 GraphVo 视图对象
      逻辑说明:
        1. 从 graph_info(图片表)查询
        2. 过滤条件:未删除、关联类型为2(item_type=2 表示房间图片,与业务定义对应)
        3. 按房间ID(item_id=#{id})查询,id为传递的房间ID参数
    -->
    <select id="selectGraphVoListByRoomId" resultType="com.yuhuan.lease.web.app.vo.graph.GraphVo">
        select
            id, -- 图片ID
            name, -- 图片名称
            item_type, -- 关联类型(2=房间图片)
            item_id, -- 关联对象ID(房间ID)
            url -- 图片访问地址
        from graph_info
        where is_deleted = 0 -- 图片未删除
          and item_type = 2 -- 仅查询房间相关图片
          and item_id = #{id} -- 匹配当前房间ID
    </select>

    <!--
      根据房间ID查询标签列表
      id:子查询ID,供 resultMap 中的 collection 标签调用
      resultType:查询结果映射为 LabelInfo 实体对象
      逻辑说明:
        1. 从 label_info(标签表)查询
        2. 通过 room_label(房间-标签中间表)关联,查询房间绑定的标签
        3. 过滤条件:标签未删除、中间表记录未删除、匹配当前房间ID
    -->
    <select id="selectLabelInfoListByRoomId" resultType="com.yuhuan.lease.model.entity.LabelInfo">
        select
            id, -- 标签ID
            type, -- 标签类型
            name -- 标签名称
        from label_info
        where is_deleted = 0 -- 标签未删除
          -- 关联房间-标签中间表,查询当前房间绑定的标签ID
          and id in (
            select label_id
            from room_label
            where is_deleted = 0 -- 中间表记录未删除
              and room_id = #{id} -- 匹配当前房间ID
        )
    </select>
</mapper>
2. 根据ID查询房间详细信息
  • 查看响应数据结构
@Data
@Schema(description = "APP房间详情")
public class RoomDetailVo extends RoomInfo {

    @Schema(description = "所属公寓信息")
    private ApartmentItemVo apartmentItemVo;

    @Schema(description = "图片列表")
    private List<GraphVo> graphVoList;

    @Schema(description = "属性信息列表")
    private List<AttrValueVo> attrValueVoList;

    @Schema(description = "配套信息列表")
    private List<FacilityInfo> facilityInfoList;

    @Schema(description = "标签信息列表")
    private List<LabelInfo> labelInfoList;

    @Schema(description = "支付方式列表")
    private List<PaymentType> paymentTypeList;

    @Schema(description = "杂费列表")
    private List<FeeValueVo> feeValueVoList;

    @Schema(description = "租期列表")
    private List<LeaseTerm> leaseTermList;
}
  • controller层
    @Operation(summary = "根据id获取房间的详细信息")
    @GetMapping("getDetailById")
    public Result<RoomDetailVo> getDetailById(@RequestParam Long id) {
        RoomDetailVo roomInfo = roomInfoService.getDetailById(id);
        return Result.ok(roomInfo);
    }
  • service层
public interface RoomInfoService extends IService<RoomInfo> {
    IPage<RoomItemVo> pageRoomItemByQuery(Page<RoomItemVo> page, RoomQueryVo queryVo);

    RoomDetailVo getDetailById(Long id);
}
    public RoomDetailVo getDetailById(Long id) {
        //1.查询房间信息
        RoomInfo roomInfo = roomInfoMapper.selectById(id);
        if (roomInfo == null) {
            return null;
        }
        //2.查询图片
        List<GraphVo> graphVoList = graphInfoMapper.selectListByItemTypeAndId(ItemType.ROOM, id);
        //3.查询租期
        List<LeaseTerm> leaseTermList = leaseTermMapper.selectListByRoomId(id);
        //4.查询配套
        List<FacilityInfo> facilityInfoList = facilityInfoMapper.selectListByRoomId(id);
        //5.查询标签
        List<LabelInfo> labelInfoList = labelInfoMapper.selectListByRoomId(id);
        //6.查询支付方式
        List<PaymentType> paymentTypeList = paymentTypeMapper.selectListByRoomId(id);
        //7.查询基本属性
        List<AttrValueVo> attrValueVoList = attrValueMapper.selectListByRoomId(id);
        //8.查询杂费信息
        List<FeeValueVo> feeValueVoList = feeValueMapper.selectListByApartmentId(roomInfo.getApartmentId());
        //9.查询公寓信息
        ApartmentItemVo apartmentItemVo = apartmentInfoService.selectApartmentItemVoById(roomInfo.getApartmentId());

        RoomDetailVo roomDetailVo = new RoomDetailVo();
        BeanUtils.copyProperties(roomInfo, roomDetailVo);

        roomDetailVo.setApartmentItemVo(apartmentItemVo);
        roomDetailVo.setGraphVoList(graphVoList);
        roomDetailVo.setAttrValueVoList(attrValueVoList);
        roomDetailVo.setFacilityInfoList(facilityInfoList);
        roomDetailVo.setLabelInfoList(labelInfoList);
        roomDetailVo.setPaymentTypeList(paymentTypeList);
        roomDetailVo.setFeeValueVoList(feeValueVoList);
        roomDetailVo.setLeaseTermList(leaseTermList);
        return roomDetailVo;
    }

编写Mapper层逻辑

  • 编写查询房间图片逻辑

    • 在GraphInfoMapper​中增加如下内容

      List<GraphVo> selectListByItemTypeAndId(ItemType itemType, Long id);
      
    • 在GraphInfoMapper.xml​增加如下内容

      <select id="selectListByItemTypeAndId" resultType="com.atguigu.lease.web.app.vo.graph.GraphVo">
          select name,
                 url
          from graph_info
          where is_deleted = 0
            and item_type = #{itemType}
            and item_id = #{id}
      </select>
      
  • 编写查询房间可选租期逻辑

    • 在LeaseTermMapper​中增加如下内容

      List<LeaseTerm> selectListByRoomId(Long id);
      
    • 在LeaseTermMapper.xml​中增加如下内容

      <select id="selectListByRoomId" resultType="com.atguigu.lease.model.entity.LeaseTerm">
          select id,
                 month_count,
                 unit
          from lease_term
          where is_deleted = 0
            and id in (select lease_term_id
                       from room_lease_term
                       where is_deleted = 0
                         and room_id = #{id})
      </select>
      
  • 编写查询房间配套逻辑

    • 在FacilityInfoMapper​中增加如下内容

      List<FacilityInfo> selectListByRoomId(Long id);
      
    • 在FacilityInfoMapper.xml​中增加如下内容

      <select id="selectListByRoomId" resultType="com.atguigu.lease.model.entity.FacilityInfo">
          select id,
                 type,
                 name,
                 icon
          from facility_info
          where is_deleted = 0
            and id in (select facility_id
                       from room_facility
                       where is_deleted = 0
                         and room_id = #{id})
      </select>
      
  • 编写查询房间标签逻辑

    • 在LabelInfoMapper​中增加如下内容

      List<LabelInfo> selectListByRoomId(Long id);
      
    • 在LabelInfoMapper.xml​中增加如下内容

      <select id="selectListByRoomId" resultType="com.atguigu.lease.model.entity.LabelInfo">
          select id,
                 type,
                 name
          from label_info
          where is_deleted = 0
            and id in (select label_id
                       from room_label
                       where is_deleted = 0
                         and room_id = #{id})
      </select>
      
  • 编写查询房间可选支付方式逻辑

    • 在PaymentTypeMapper​中增加如下内容

      List<PaymentType> selectListByRoomId(Long id);
      
    • 在PaymentTypeMapper.xml​中增加如下内容

      <select id="selectListByRoomId" resultType="com.atguigu.lease.model.entity.PaymentType">
          select id,
                 name,
                 pay_month_count,
                 additional_info
          from payment_type
          where is_deleted = 0
            and id in (select payment_type_id
                       from room_payment_type
                       where is_deleted = 0
                         and room_id = #{id})
      </select>
      
  • 编写查询房间属性逻辑

    • 在AttrValueMapper​中增加如下内容

      List<AttrValueVo> selectListByRoomId(Long id);
      
    • 在AttrValueMapper.xml​中增加如下内容

      <select id="selectListByRoomId" resultType="com.atguigu.lease.web.app.vo.attr.AttrValueVo">
          select av.id,
                 av.name,
                 av.attr_key_id,
                 ak.name attr_key_name
          from attr_value av
                   left join attr_key ak on av.attr_key_id = ak.id and ak.is_deleted = 0
          where av.is_deleted = 0
            and av.id in (select attr_value_id
                          from room_attr_value
                          where is_deleted = 0
                            and room_id = #{id})
      </select>
      
  • 编写查询房间杂费逻辑

    • 在FeeValueMapper​中增加如下内容

      List<FeeValueVo> selectListByApartmentId(Long id);
      
    • 在FeeValueMapper.xml​中增加如下内容

      <select id="selectListByApartmentId" resultType="com.atguigu.lease.web.app.vo.fee.FeeValueVo">
          select fv.id,
                 fv.name,
                 fv.unit,
                 fv.fee_key_id,
                 fk.name fee_key_name
          from fee_value fv
                   left join fee_key fk on fv.fee_key_id = fk.id and fk.is_deleted = 0
          where fv.is_deleted = 0
            and fv.id in (select fee_value_id
                          from apartment_fee_value
                          where is_deleted = 0
                            and apartment_id = #{id})
      </select>
      

编写查询所属公寓信息逻辑

编写Service层逻辑

在ApartmentInfoService​中增加如下内容

ApartmentItemVo selectApartmentItemVoById(Long id);

在ApartmentInfoServiceImpl​中增加如下内容

@Override
public ApartmentItemVo selectApartmentItemVoById(Long id) {

    ApartmentInfo apartmentInfo = apartmentInfoMapper.selectById(id);

    List<LabelInfo> labelInfoList = labelInfoMapper.selectListByApartmentId(id);

    List<GraphVo> graphVoList = graphInfoMapper.selectListByItemTypeAndId(ItemType.APARTMENT, id);

    BigDecimal minRent = roomInfoMapper.selectMinRentByApartmentId(id);

    ApartmentItemVo apartmentItemVo = new ApartmentItemVo();
    BeanUtils.copyProperties(apartmentInfo, apartmentItemVo);

    apartmentItemVo.setGraphVoList(graphVoList);
    apartmentItemVo.setLabelInfoList(labelInfoList);
    apartmentItemVo.setMinRent(minRent);
    return apartmentItemVo;
}

编写Mapper层逻辑

  • 编写查询标签信息逻辑

    • 在LabelInfoMapper​中增加如下内容

        List<LabelInfo> selectListByApartmentId(Long id);
      
    • 在LabelInfoMapper.xml​中增加如下内容

        <select id="selectListByApartmentId" resultType="com.atguigu.lease.model.entity.LabelInfo">
            select id,
                   type,
                   name
            from label_info
            where is_deleted = 0
              and id in (select label_id
                         from apartment_label
                         where is_deleted = 0
                           and apartment_id = #{id})
        </select>
      
    • 编写查询公寓最小租金逻辑

      • 在RoomInfoMapper​中增加如下内容

        BigDecimal selectMinRentByApartmentId(Long id);
        
      • 在RoomInfoMapper.xml​中增加如下内容

        <select id="selectMinRentByApartmentId" resultType="java.math.BigDecimal">
            select min(rent)
            from room_info
            where is_deleted = 0
            and is_release = 1
            and apartment_id = #{id}
        </select>
        

@Async

在 Spring Boot 中,@Async 是 异步方法执行注解,核心作用是:让被标注的方法脱离当前主线程,在独立的线程池(异步线程)中执行,避免主线程阻塞,提高程序并发能力和响应速度。

一、核心场景:解决「同步阻塞」问题

举个直观例子,理解它的价值:

1. 无 @Async(同步执行)
// 同步方法:发送短信(假设耗时3秒)
public void sendSms(String phone) {
    try { Thread.sleep(3000); } catch (InterruptedException e) {}
    System.out.println("短信发送成功:" + phone);
}

// 主方法调用
public static void main(String[] args) {
    long start = System.currentTimeMillis();
    sendSms("13800138000"); // 阻塞3秒
    System.out.println("主线程结束,总耗时:" + (System.currentTimeMillis() - start) + "ms");
}

执行结果

短信发送成功:13800138000
主线程结束,总耗时:3003ms

→ 主线程必须等待 sendSms 执行完才能继续,造成阻塞。

2. 加 @Async(异步执行)
// 异步方法:加 @Async 注解,方法会在独立线程执行
@Async
public CompletableFuture<Void> sendSms(String phone) {
    try { Thread.sleep(3000); } catch (InterruptedException e) {}
    System.out.println("短信发送成功:" + phone);
    return CompletableFuture.runAsync(() -> {}); // 异步方法建议返回 CompletableFuture
}

// 主方法调用
public static void main(String[] args) {
    long start = System.currentTimeMillis();
    sendSms("13800138000"); // 不阻塞,直接返回
    System.out.println("主线程结束,总耗时:" + (System.currentTimeMillis() - start) + "ms");
}

执行结果

主线程结束,总耗时:5ms
短信发送成功:13800138000 // 3秒后在异步线程输出

→ 主线程无需等待,直接继续执行,异步线程在后台完成耗时操作。

二、@Async 的核心作用

  1. 解除主线程阻塞:耗时操作(如发短信、邮件、调用第三方接口、文件上传)异步执行,主线程快速响应(比如接口快速返回结果,不用用户等待)。
  2. 提高并发效率:多个异步方法可同时在不同线程执行(依赖线程池),比如同时发送 10 条短信,同步需要 30 秒,异步可能只需要 3 秒(线程池足够的情况下)。
  3. 解耦业务逻辑:将非核心耗时操作(如日志记录、数据统计)异步化,不影响核心业务流程(如订单提交、支付)。

三、使用条件(必须满足,否则异步不生效)

@Async 不是加了就生效,需要 2 个关键配置:

1. 启动类加 @EnableAsync 注解(开启异步功能)
@SpringBootApplication
@EnableAsync // 必须加!启用Spring的异步方法支持
public class LeaseApplication {
    public static void main(String[] args) {
        SpringApplication.run(LeaseApplication.class, args);
    }
}
2. 异步方法必须是「Spring 管理的 Bean 的方法」
  • 方法所在类必须被 @Component@Service@Controller 等注解标注(让 Spring 扫描为 Bean)。
  • 不能在「同一个类内部调用异步方法」(Spring 通过 AOP 动态代理实现异步,内部调用会绕过代理,导致注解失效)。

❌ 错误示例(内部调用,异步失效):

@Service
public class SmsService {
    // 内部调用异步方法,@Async不生效
    public void test() {
        sendSms("13800138000"); // 同步执行,会阻塞
    }

    @Async
    public CompletableFuture<Void> sendSms(String phone) {
        // 耗时操作
    }
}

✅ 正确示例(外部调用,通过 Spring Bean 代理):

@Service
public class SmsService {
    @Async
    public CompletableFuture<Void> sendSms(String phone) {
        // 耗时操作
    }
}

@Controller
public class OrderController {
    @Autowired
    private SmsService smsService;

    // 外部调用SmsService的异步方法,生效
    @GetMapping("/order")
    public String createOrder() {
        smsService.sendSms("13800138000"); // 异步执行,不阻塞
        return "订单创建成功";
    }
}

四、关键细节

1. 异步方法的返回值
  • 无返回值:可以用 void,但建议返回 CompletableFuture<Void>(方便后续监听异步任务完成状态)。
  • 有返回值:必须返回 CompletableFuture<T>(如 CompletableFuture<String>),通过它获取异步执行的结果。
    @Async
    public CompletableFuture<String> queryData(String id) {
        // 耗时查询(如查数据库、第三方接口)
        String result = "查询结果:" + id;
        return CompletableFuture.completedFuture(result);
    }
    
    // 调用方获取结果
    public void test() {
        CompletableFuture<String> future = queryData("1001");
        future.whenComplete((result, e) -> {
            if (e == null) {
                System.out.println("异步结果:" + result);
            } else {
                System.out.println("异步任务失败:" + e.getMessage());
            }
        });
    }
    
2. 线程池配置(默认线程池的问题)
  • Spring 默认的异步线程池:SimpleAsyncTaskExecutor每次执行异步方法都会创建新线程(无上限,高并发下可能导致线程耗尽)。
  • 建议自定义线程池(控制线程数量、队列大小):
    @Configuration
    @EnableAsync
    public class AsyncConfig {
        // 自定义异步线程池
        @Bean(name = "asyncExecutor")
        public Executor asyncExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(5); // 核心线程数(默认活跃线程)
            executor.setMaxPoolSize(10); // 最大线程数(峰值线程)
            executor.setQueueCapacity(20); // 任务队列大小(超出核心线程时,任务先入队)
            executor.setThreadNamePrefix("Async-"); // 线程名前缀(方便日志排查)
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 队列满时,由调用线程执行(避免任务丢失)
            executor.initialize();
            return executor;
        }
    }
    
    // 异步方法指定使用自定义线程池
    @Async("asyncExecutor")
    public CompletableFuture<Void> sendSms(String phone) {
        // 耗时操作
    }
    
3. 异常处理

异步方法的异常不会直接抛给调用方,需要通过 CompletableFuture 的 whenCompleteexceptionally 处理,或全局配置异常处理器:

// 全局异步异常处理器
@Configuration
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        System.err.println("异步方法异常:" + method.getName() + ",参数:" + Arrays.toString(params));
        System.err.println("异常信息:" + ex.getMessage());
    }
}

// 关联到线程池
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // ... 其他配置
    executor.setTaskDecorator(new AsyncTaskDecorator() {
        @Override
        public Runnable decorate(Runnable runnable) {
            return new DelegatingSecurityContextRunnable(runnable);
        }
    });
    executor.setAsyncUncaughtExceptionHandler(new AsyncExceptionHandler()); // 绑定异常处理器
    executor.initialize();
    return executor;
}

五、常用场景

  1. 耗时的非核心操作:发送短信、邮件、推送通知(用户不需要等待这些操作完成)。
  2. 批量处理:批量导入数据、批量生成报表(多线程并行处理,提高效率)。
  3. 第三方接口调用:调用慢接口(如银行支付回调、物流接口),异步执行避免阻塞主流程。
  4. 日志 / 统计:异步记录操作日志、统计接口访问量(不影响核心业务响应)。

总结

@Async 的核心是「异步解耦 + 提高并发」,让耗时操作在后台线程执行,主线程快速响应。使用时必须满足:@EnableAsync 开启功能、异步方法是 Spring Bean 的外部调用、合理配置线程池(避免资源耗尽)。它是 Spring Boot 中优化系统性能、提升用户体验的常用注解。

项目优化

缓存优化

缓存优化是一种高性价比的性能提升手段,通过简单的配置调整往往能获得显著的性能改善。其核心原理是将频繁访问且查询成本较高的数据(如MySQL中的数据库记录)临时存储在内存(如Redis)中。后续访问相同数据时,可直接从内存快速读取,避免了磁盘I/O操作。由于内存的读写速度远高于磁盘,这种优化能显著提升系统性能。

在缓存优化过程中,数据库与缓存数据的一致性问题尤为关键。若数据库数据变更而缓存未同步更新,就会导致数据不一致。常见的解决方案包括:

  1. 更新数据库时同步更新缓存
  2. 更新数据库时直接删除缓存

以移动端"根据ID获取房间详情"接口为例进行缓存优化。该接口涉及多表查询,频繁访问数据库导致查询性能较低,适合采用缓存策略来提升查询效率。

1.自定义RedisTemplate

本项目使用Reids保存缓存数据,因此我们需要使用RedisTemplate进行读写操作。前文提到过,Spring-data-redis​提供了StringRedisTemplate​和RedisTemplate<Object,Object>​两个实例,但是两个实例均不满足我们当前的需求,所以我们需要自定义RedisTemplate。

在common模块中创建com.yuhuan.lease.common.redis.RedisConfiguration​类,内容如下

@Configuration
public class RedisConfiguration {

    @Bean
    public RedisTemplate<String, Object> stringObjectRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(RedisSerializer.string());
        template.setValueSerializer(RedisSerializer.java());
        return template;
    }
}

2.编写缓存逻辑

修改web-app模块中的com.yuhuan.lease.web.app.service.impl.RoomInfoServiceImpl​中的getDetailById​方法,如下

@Override
public RoomDetailVo getDetailById(Long id) {

        String key = RedisConstant.APP_ROOM_PREFIX + id;
        RoomDetailVo roomDetailVo = (RoomDetailVo) stringObjectRedisTemplate.opsForValue().get(key);

        if (roomDetailVo == null) {
            System.out.println("获取房间详情-" + Thread.currentThread().getName());
            //1.查询房间信息
            RoomInfo roomInfo = roomInfoMapper.selectById(id);
            if (roomInfo == null) {
                return null;
            }
            //2.查询图片
            List<GraphVo> graphVoList = graphInfoMapper.selectListByItemTypeAndId(ItemType.ROOM, id);
            //3.查询租期
            List<LeaseTerm> leaseTermList = leaseTermMapper.selectListByRoomId(id);
            //4.查询配套
            List<FacilityInfo> facilityInfoList = facilityInfoMapper.selectListByRoomId(id);
            //5.查询标签
            List<LabelInfo> labelInfoList = labelInfoMapper.selectListByRoomId(id);
            //6.查询支付方式
            List<PaymentType> paymentTypeList = paymentTypeMapper.selectListByRoomId(id);
            //7.查询基本属性
            List<AttrValueVo> attrValueVoList = attrValueMapper.selectListByRoomId(id);
            //8.查询杂费信息
            List<FeeValueVo> feeValueVoList = feeValueMapper.selectListByApartmentId(roomInfo.getApartmentId());
            //9.查询公寓信息
            ApartmentItemVo apartmentItemVo = apartmentInfoService.selectApartmentItemVoById(roomInfo.getApartmentId());

            roomDetailVo = new RoomDetailVo();
            BeanUtils.copyProperties(roomInfo, roomDetailVo);

            roomDetailVo.setApartmentItemVo(apartmentItemVo);
            roomDetailVo.setGraphVoList(graphVoList);
            roomDetailVo.setAttrValueVoList(attrValueVoList);
            roomDetailVo.setFacilityInfoList(facilityInfoList);
            roomDetailVo.setLabelInfoList(labelInfoList);
            roomDetailVo.setPaymentTypeList(paymentTypeList);
            roomDetailVo.setFeeValueVoList(feeValueVoList);
            roomDetailVo.setLeaseTermList(leaseTermList);


            stringObjectRedisTemplate.opsForValue().set(key, roomDetailVo);
        }

        //10.保存浏览历史
        browsingHistoryService.saveHistory(LoginUserHolder.getLoginUser().getUserId(), id);

        return roomDetailVo;
}

3.编写删除缓存逻辑

为保证缓存数据的一致性,在房间信息发生变化时,需要删除相关缓存。

修改web-admin模块中的com.atguigu.lease.web.admin.service.impl.RoomInfoServiceImpl​中的saveOrUpdateRoom​方法,如下

@Override
public void saveOrUpdateRoom(RoomSubmitVo roomSubmitVo) {
    boolean isUpdate = roomSubmitVo.getId() != null;
    super.saveOrUpdate(roomSubmitVo);

    //若为更新操作,则先删除与Room相关的各项信息列表
    if (isUpdate) {
        //1.删除原有graphInfoList
        ......

        //2.删除原有roomAttrValueList
        ......

        //3.删除原有roomFacilityList
        ......

        //4.删除原有roomLabelList
        ......

        //5.删除原有paymentTypeList
        ......

        //6.删除原有leaseTermList
        ......

        //7.删除缓存
        redisTemplate.delete(RedisConstant.APP_LOGIN_PREFIX + roomSubmitVo.getId());
    }

    //1.保存新的graphInfoList
    ......

    //2.保存新的roomAttrValueList
    ......

    //3.保存新的facilityInfoList
    ......

    //4.保存新的labelInfoList
    ......

    //5.保存新的paymentTypeList
    ......

    //6.保存新的leaseTermList
    ......
}

修改web-admin模块中的com.yuhuan.lease.web.admin.service.impl.RoomInfoServiceImpl​中的removeRoomById​方法,如下

@Override
public void removeRoomById(Long id) {

    //1.删除RoomInfo
    ......

    //2.删除graphInfoList
    ......

    //3.删除attrValueList
    ......

    //4.删除facilityInfoList
    ......

    //5.删除labelInfoList
    ......

    //6.删除paymentTypeList
    ......

    //7.删除leaseTermList
    ......

    //8.删除缓存
    redisTemplate.delete(RedisConstant.APP_ROOM_PREFIX + id);
}

压力测试

可使用Postman或者Apifox等工具对根据ID获取房间详情​这个接口进行压力测试,下图是增加缓存前后的测试报告

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值