Java全栈项目-大学生租房管理系统(5)

代码5:

<template>
  <div class="house-detail-container">
    <!-- 头部导航 -->
    <div class="header">
      <div class="container">
        <div class="logo">大学生租房管理系统</div>
        <div class="nav">
          <router-link to="/home" class="nav-item">首页</router-link>
          <router-link to="/house/map" class="nav-item">地图找房</router-link>
          <router-link to="/news" class="nav-item">租房资讯</router-link>
        </div>
        <div class="user-info">
          <template v-if="isLogin">
            <el-dropdown @command="handleCommand">
              <span class="el-dropdown-link">
                {{ username }}<i class="el-icon-arrow-down el-icon--right"></i>
              </span>
              <el-dropdown-menu slot="dropdown">
                <el-dropdown-item command="profile">个人中心</el-dropdown-item>
                <el-dropdown-item command="favorite">我的收藏</el-dropdown-item>
                <el-dropdown-item v-if="isAdmin" command="admin">管理后台</el-dropdown-item>
                <el-dropdown-item v-if="isLandlord" command="landlord">房源管理</el-dropdown-item>
                <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
              </el-dropdown-menu>
            </el-dropdown>
          </template>
          <template v-else>
            <router-link to="/login" class="login-btn">登录</router-link>
            <router-link to="/register" class="register-btn">注册</router-link>
          </template>
        </div>
      </div>
    </div>

    <!-- 房源详情内容 -->
    <div class="detail-content">
      <div class="container">
        <div class="house-title">
          <h1>{{ house.title }}</h1>
          <div class="house-tags">
            <span class="tag">{{ house.houseType }}</span>
            <span class="tag">{{ house.area }}㎡</span>
            <span class="tag">{{ house.orientation }}</span>
            <span class="tag">{{ house.decoration }}</span>
          </div>
        </div>

        <div class="detail-main">
          <!-- 左侧图片 -->
          <div class="detail-left">
            <div class="house-images">
              <el-carousel height="400px">
                <el-carousel-item v-for="(image, index) in houseImages" :key="index">
                  <img :src="image.url" alt="房源图片">
                </el-carousel-item>
              </el-carousel>
            </div>
          </div>

          <!-- 右侧信息 -->
          <div class="detail-right">
            <div class="price-info">
              <div class="price">
                <span class="price-num">{{ house.price }}</span>
                <span class="price-unit">元/月</span>
              </div>
              <div class="price-tags">
                <span>押一付三</span>
                <span>{{ houseDetail.rentType }}</span>
              </div>
            </div>

            <div class="landlord-info">
              <div class="landlord-avatar">
                <img src="https://via.placeholder.com/60" alt="房东头像">
              </div>
              <div class="landlord-detail">
                <div class="landlord-name">{{ house.contact }}</div>
                <div class="landlord-phone">{{ house.contactPhone }}</div>
              </div>
              <el-button type="primary" @click="handleContact">联系房东</el-button>
            </div>

            <div class="action-buttons">
              <el-button type="primary" @click="handleAppointment">预约看房</el-button>
              <el-button :type="isFavorite ? 'danger' : 'info'" @click="handleFavorite">
                <i :class="isFavorite ? 'el-icon-star-on' : 'el-icon-star-off'"></i>
                {{ isFavorite ? '已收藏' : '收藏' }}
              </el-button>
            </div>
          </div>
        </div>

        <!-- 房源详情信息 -->
        <div class="detail-info">
          <el-tabs v-model="activeTab">
            <el-tab-pane label="房源信息" name="info">
              <div class="info-section">
                <h3>基本信息</h3>
                <div class="info-list">
                  <div class="info-item">
                    <span class="info-label">房源类型</span>
                    <span class="info-value">{{ houseDetail.houseCategory }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">户型</span>
                    <span class="info-value">{{ house.houseType }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">面积</span>
                    <span class="info-value">{{ house.area }}㎡</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">楼层</span>
                    <span class="info-value">{{ house.floor }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">朝向</span>
                    <span class="info-value">{{ house.orientation }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">装修</span>
                    <span class="info-value">{{ house.decoration }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">电梯</span>
                    <span class="info-value">{{ houseDetail.hasElevator === 1 ? '有' : '无' }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">停车位</span>
                    <span class="info-value">{{ houseDetail.hasParking === 1 ? '有' : '无' }}</span>
                  </div>
                </div>
              </div>

              <div class="info-section">
                <h3>租金信息</h3>
                <div class="info-list">
                  <div class="info-item">
                    <span class="info-label">租金</span>
                    <span class="info-value">{{ house.price }}元/月</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">付款方式</span>
                    <span class="info-value">{{ houseDetail.paymentType }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">出租方式</span>
                    <span class="info-value">{{ houseDetail.rentType }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">最短租期</span>
                    <span class="info-value">{{ houseDetail.minRentPeriod }}个月</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">入住时间</span>
                    <span class="info-value">{{ formatDate(houseDetail.checkInTime) }}</span>
                  </div>
                </div>
              </div>

              <div class="info-section">
                <h3>费用信息</h3>
                <div class="info-list">
                  <div class="info-item">
                    <span class="info-label">水费</span>
                    <span class="info-value">{{ houseDetail.waterFee }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">电费</span>
                    <span class="info-value">{{ houseDetail.electricityFee }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">燃气费</span>
                    <span class="info-value">{{ houseDetail.gasFee }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">网费</span>
                    <span class="info-value">{{ houseDetail.internetFee }}</span>
                  </div>
                  <div class="info-item">
                    <span class="info-label">物业费</span>
                    <span class="info-value">{{ houseDetail.propertyFee }}</span>
                  </div>
                </div>
              </div>

              <div class="info-section">
                <h3>房源描述</h3>
                <div class="description">
                  {{ houseDetail.description }}
                </div>
              </div>

              <div class="info-section">
                <h3>配套设施</h3>
                <div class="facilities">
                  <el-tag v-for="(item, index) in facilitiesList" :key="index" size="medium">{{ item }}</el-tag>
                </div>
              </div>
            </el-tab-pane>

            <el-tab-pane label="位置交通" name="location">
              <div class="info-section">
                <h3>位置信息</h3>
                <div class="location-info">
                  <div class="address">
                    <span class="info-label">小区名称</span>
                    <span class="info-value">{{ house.community }}</span>
                  </div>
                  <div class="address">
                    <span class="info-label">详细地址</span>
                    <span class="info-value">{{ house.address }}</span>
                  </div>
                </div>
                <div class="map-container" id="map" style="height: 400px; margin-top: 20px;"></div>
              </div>

              <div class="info-section">
                <h3>交通情况</h3>
                <div class="transportation">
                  {{ houseDetail.transportation }}
                </div>
              </div>

              <div class="info-section">
                <h3>周边配套</h3>
                <div class="surroundings">
                  {{ houseDetail.surroundings }}
                </div>
              </div>
            </el-tab-pane>
          </el-tabs>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { getHouseDetail, getHouseImages } from '@/api/house';
import { addFavorite, cancelFavorite, checkFavorite } from '@/api/favorite';
import { mapGetters } from 'vuex';
import { formatDate } from '@/utils/date';

export default {
  name: 'HouseDetail',
  data() {
    return {
      house: {},
      houseDetail: {},
      houseImages: [],
      activeTab: 'info',
      isFavorite: false,
      facilitiesList: []
    };
  },
  computed: {
    ...mapGetters(['isLogin', 'username', 'userId', 'isAdmin', 'isLandlord']),
    houseId() {
      return this.$route.params.id;
    }
  },
  created() {
    this.fetchHouseDetail();
    this.fetchHouseImages();
    if (this.isLogin) {
      this.checkIsFavorite();
    }
  },
  methods: {
    formatDate,
    async fetchHouseDetail() {
      try {
        const res = await getHouseDetail(this.houseId);
        if (res.code === 200) {
          this.house = res.data.house;
          this.houseDetail = res.data.houseDetail;
          
          // 处理配套设施
          if (this.houseDetail.facilities) {
            this.facilitiesList = this.houseDetail.facilities.split(',');
          }
          
          // 加载地图
          this.$nextTick(() => {
            this.initMap();
          });
        }
      } catch (error) {
        this.$message.error('获取房源详情失败');
        console.error(error);
      }
    },
    async fetchHouseImages() {
      try {
        const res = await getHouseImages(this.houseId);
        if (res.code === 200) {
          this.houseImages = res.data;
        }
      } catch (error) {
        this.$message.error('获取房源图片失败');
        console.error(error);
      }
    },
    async checkIsFavorite() {
      try {
        const res = await checkFavorite(this.houseId);
        if (res.code === 200) {
          this.isFavorite = res.data;
        }
      } catch (error) {
        console.error(error);
      }
    },
    async handleFavorite() {
      if (!this.isLogin) {
        this.$message.warning('请先登录');
        this.$router.push(`/login?redirect=/house/detail/${this.houseId}`);
        return;
      }
      
      try {
        if (this.isFavorite) {
          const res = await cancelFavorite(this.houseId);
          if (res.code === 200) {
            this.isFavorite = false;
            this.$message.success('取消收藏成功');
          }
        } else {
          const res = await addFavorite(this.houseId);
          if (res.code === 200) {
            this.isFavorite = true;
            this.$message.success('收藏成功');
          }
        }
      } catch (error) {
        this.$message.error('操作失败');
        console.error(error);
      }
    },
    handleContact() {
      if (!this.isLogin) {
        this.$message.warning('请先登录');
        this.$router.push(`/login?redirect=/house/detail/${this.houseId}`);
        return;
      }
      
      this.$alert(`联系人:${this.house.contact}<br/>联系电话:${this.house.contactPhone}`, '联系房东', {
        dangerouslyUseHTMLString: true,
        confirmButtonText: '确定'
      });
    },
    handleAppointment() {
      if (!this.isLogin) {
        this.$message.warning('请先登录');
        this.$router.push(`/login?redirect=/house/detail/${this.houseId}`);
        return;
      }
      
      this.$message.info('预约功能即将上线,敬请期待');
    },
    initMap() {
      // 此处为地图初始化代码,需要引入地图API
      // 例如使用百度地图或高德地图
      console.log('初始化地图,经度:', this.house.longitude, '纬度:', this.house.latitude);
    },
    handleCommand(command) {
      switch (command) {
        case 'profile':
          this.$router.push('/user/profile');
          break;
        case 'favorite':
          this.$router.push('/user/favorite');
          break;
        case 'admin':
          this.$router.push('/admin');
          break;
        case 'landlord':
          this.$router.push('/landlord');
          break;
        case 'logout':
          this.$store.dispatch('logout');
          this.$router.push('/login');
          break;
      }
    }
  }
};
</script>

<style scoped>
.house-detail-container {
  min-height: 100vh;
  background-color: #f5f5f5;
}

.header {
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  position: sticky;
  top: 0;
  z-index: 100;
}

.header .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 60px;
  padding: 0 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.logo {
  font-size: 20px;
  font-weight: bold;
  color: #409EFF;
}

.nav {
  display: flex;
}

.nav-item {
  margin: 0 15px;
  color: #333;
  text-decoration: none;
}

.nav-item:hover {
  color: #409EFF;
}

.user-info {
  display: flex;
  align-items: center;
}

.login-btn, .register-btn {
  padding: 5px 15px;
  margin-left: 10px;
  border-radius: 4px;
  text-decoration: none;
}

.login-btn {
  color: #409EFF;
}

.register-btn {
  background-color: #409EFF;
  color: #fff;
}

.detail-content {
  padding: 20px 0;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}

.house-title {
  margin-bottom: 20px;
}

.house-title h1 {
  font-size: 24px;
  margin-bottom: 10px;
}

.house-tags {
  display: flex;
  flex-wrap: wrap;
}

.tag {
  background-color: #f0f9ff;
  color: #409EFF;
  padding: 2px 8px;
  margin-right: 10px;
  border-radius: 4px;
  font-size: 12px;
}

.detail-main {
  display: flex;
  margin-bottom: 30px;
}

.detail-left {
  flex: 2;
  margin-right: 20px;
}

.detail-right {
  flex: 1;
  background-color: #fff;
  padding: 20px;
  border-radius: 4px;
}

.price-info {
  margin-bottom: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid #eee;
}

.price {
  display: flex;
  align-items: baseline;
  margin-bottom: 10px;
}

.price-num {
  font-size: 28px;
  color: #ff5a5f;
  font-weight: bold;
}

.price-unit {
  font-size: 14px;
  color: #666;
  margin-left: 5px;
}

.price-tags span {
  background-color: #fff5f5;
  color: #ff5a5f;
  padding: 2px 8px;
  margin-right: 10px;
  border-radius: 4px;
  font-size: 12px;
}

.landlord-info {
  display: flex;
  align-items: center;
  margin-bottom: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid #eee;
}

.landlord-avatar {
  margin-right: 15px;
}

.landlord-avatar img {
  width: 60px;
  height: 60px;
  border-radius: 50%;
}

.landlord-detail {
  flex: 1;
}

.landlord-name {
  font-size: 16px;
  font-weight: bold;
  margin-bottom: 5px;
}

.landlord-phone {
  color: #666;
}

.action-buttons {
  display: flex;
  justify-content: space-between;
}

.detail-info {
  background-color: #fff;
  padding: 20px;
  border-radius: 4px;
}

.info-section {
  margin-bottom: 30px;
}

.info-section h3 {
  font-size: 18px;
  margin-bottom: 15px;
  padding-bottom: 10px;
  border-bottom: 1px solid #eee;
}

.info-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 15px;
}

.info-item {
  display: flex;
}

.info-label {
  color: #666;
  margin-right: 10px;
  min-width: 70px;
}

.info-value {
  color: #333;
  font-weight: 500;
}

.description, .transportation, .surroundings {
  line-height: 1.8;
  color: #333;
}

.facilities {
  display: flex;
  flex-wrap: wrap;
}

.facilities .el-tag {
  margin-right: 10px;
  margin-bottom: 10px;
}

.location-info {
  margin-bottom: 15px;
}

.address {
  margin-bottom: 10px;
}
</style>
API Service Files
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\api\house.js

javascript
CopyInsert
import request from '@/utils/request';

// 获取房源列表
export function getHouseList(params) {
  return request({
    url: '/house/list',
    method: 'get',
    params
  });
}

// 搜索房源
export function searchHouse(params) {
  return request({
    url: '/house/search',
    method: 'get',
    params
  });
}

// 获取房源详情
export function getHouseDetail(id) {
  return request({
    url: `/house/detail/${id}`,
    method: 'get'
  });
}

// 获取房源图片
export function getHouseImages(houseId) {
  return request({
    url: `/house/images/${houseId}`,
    method: 'get'
  });
}

// 添加房源
export function addHouse(data) {
  return request({
    url: '/house/add',
    method: 'post',
    data
  });
}

// 更新房源
export function updateHouse(data) {
  return request({
    url: '/house/update',
    method: 'put',
    data
  });
}

// 删除房源
export function deleteHouse(id) {
  return request({
    url: `/house/delete/${id}`,
    method: 'delete'
  });
}

// 上传房源图片
export function uploadHouseImage(data) {
  return request({
    url: '/house/upload',
    method: 'post',
    data,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  });
}

// 设置封面图片
export function setCoverImage(houseId, imageId) {
  return request({
    url: '/house/setCover',
    method: 'put',
    data: {
      houseId,
      imageId
    }
  });
}

// 获取房东的房源列表
export function getLandlordHouses() {
  return request({
    url: '/house/landlord',
    method: 'get'
  });
}
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\api\favorite.js

javascript
CopyInsert
import request from '@/utils/request';

// 添加收藏
export function addFavorite(houseId) {
  return request({
    url: '/favorite/add',
    method: 'post',
    data: {
      houseId
    }
  });
}

// 取消收藏
export function cancelFavorite(houseId) {
  return request({
    url: '/favorite/cancel',
    method: 'delete',
    params: {
      houseId
    }
  });
}

// 获取收藏列表
export function getFavoriteList() {
  return request({
    url: '/favorite/list',
    method: 'get'
  });
}

// 检查是否已收藏
export function checkFavorite(houseId) {
  return request({
    url: '/favorite/check',
    method: 'get',
    params: {
      houseId
    }
  });
}
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\api\user.js

javascript
CopyInsert
import request from '@/utils/request';

// 用户登录
export function login(data) {
  return request({
    url: '/user/login',
    method: 'post',
    data
  });
}

// 用户注册
export function register(data) {
  return request({
    url: '/user/register',
    method: 'post',
    data
  });
}

// 获取用户信息
export function getUserInfo() {
  return request({
    url: '/user/info',
    method: 'get'
  });
}

// 更新用户信息
export function updateUserInfo(data) {
  return request({
    url: '/user/update',
    method: 'put',
    data
  });
}

// 修改密码
export function changePassword(data) {
  return request({
    url: '/user/changePassword',
    method: 'put',
    data
  });
}

// 上传头像
export function uploadAvatar(data) {
  return request({
    url: '/user/avatar',
    method: 'post',
    data,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  });
}
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\api\verification.js

javascript
CopyInsert
import request from '@/utils/request';

// 提交学生认证
export function submitStudentVerification(data) {
  return request({
    url: '/verification/student/submit',
    method: 'post',
    data,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  });
}

// 提交房东认证
export function submitLandlordVerification(data) {
  return request({
    url: '/verification/landlord/submit',
    method: 'post',
    data,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  });
}

// 获取学生认证状态
export function getStudentVerificationStatus() {
  return request({
    url: '/verification/student/status',
    method: 'get'
  });
}

// 获取房东认证状态
export function getLandlordVerificationStatus() {
  return request({
    url: '/verification/landlord/status',
    method: 'get'
  });
}

// 获取学生认证列表(管理员)
export function getStudentVerificationList(params) {
  return request({
    url: '/verification/student/list',
    method: 'get',
    params
  });
}

// 获取房东认证列表(管理员)
export function getLandlordVerificationList(params) {
  return request({
    url: '/verification/landlord/list',
    method: 'get',
    params
  });
}

// 审核学生认证(管理员)
export function reviewStudentVerification(data) {
  return request({
    url: '/verification/student/review',
    method: 'put',
    data
  });
}

// 审核房东认证(管理员)
export function reviewLandlordVerification(data) {
  return request({
    url: '/verification/landlord/review',
    method: 'put',
    data
  });
}
User Profile Component
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\views\user\Profile.vue

vue
CopyInsert
<template>
  <div class="profile-container">
    <el-card class="profile-card">
      <div slot="header" class="card-header">
        <span>个人资料</span>
      </div>
      
      <el-form :model="userForm" :rules="rules" ref="userForm" label-width="100px">
        <div class="avatar-container">
          <el-avatar :size="100" :src="avatarUrl"></el-avatar>
          <el-upload
            class="avatar-uploader"
            action="#"
            :http-request="uploadAvatar"
            :show-file-list="false"
            :before-upload="beforeAvatarUpload">
            <el-button size="small" type="primary">更换头像</el-button>
          </el-upload>
        </div>
        
        <el-form-item label="用户名" prop="username">
          <el-input v-model="userForm.username" disabled></el-input>
        </el-form-item>
        
        <el-form-item label="真实姓名" prop="realName">
          <el-input v-model="userForm.realName"></el-input>
        </el-form-item>
        
        <el-form-item label="手机号码" prop="phone">
          <el-input v-model="userForm.phone"></el-input>
        </el-form-item>
        
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="userForm.email"></el-input>
        </el-form-item>
        
        <el-form-item label="性别" prop="gender">
          <el-radio-group v-model="userForm.gender">
            <el-radio :label="1">男</el-radio>
            <el-radio :label="2">女</el-radio>
            <el-radio :label="0">保密</el-radio>
          </el-radio-group>
        </el-form-item>
        
        <el-form-item>
          <el-button type="primary" @click="submitForm('userForm')">保存修改</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    
    <el-card class="profile-card">
      <div slot="header" class="card-header">
        <span>修改密码</span>
      </div>
      
      <el-form :model="passwordForm" :rules="passwordRules" ref="passwordForm" label-width="100px">
        <el-form-item label="原密码" prop="oldPassword">
          <el-input v-model="passwordForm.oldPassword" type="password"></el-input>
        </el-form-item>
        
        <el-form-item label="新密码" prop="newPassword">
          <el-input v-model="passwordForm.newPassword" type="password"></el-input>
        </el-form-item>
        
        <el-form-item label="确认密码" prop="confirmPassword">
          <el-input v-model="passwordForm.confirmPassword" type="password"></el-input>
        </el-form-item>
        
        <el-form-item>
          <el-button type="primary" @click="submitPasswordForm('passwordForm')">修改密码</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    
    <el-card class="profile-card">
      <div slot="header" class="card-header">
        <span>身份认证</span>
      </div>
      
      <div class="verification-container">
        <div class="verification-item">
          <div class="verification-title">学生认证</div>
          <div class="verification-status">
            <el-tag :type="studentVerificationStatusType">{{ studentVerificationStatusText }}</el-tag>
          </div>
          <div class="verification-action">
            <el-button 
              type="primary" 
              size="small" 
              @click="goToStudentVerification"
              :disabled="studentVerificationStatus === 1">
              {{ studentVerificationButtonText }}
            </el-button>
          </div>
        </div>
        
        <div class="verification-item">
          <div class="verification-title">房东认证</div>
          <div class="verification-status">
            <el-tag :type="landlordVerificationStatusType">{{ landlordVerificationStatusText }}</el-tag>
          </div>
          <div class="verification-action">
            <el-button 
              type="primary" 
              size="small" 
              @click="goToLandlordVerification"
              :disabled="landlordVerificationStatus === 1">
              {{ landlordVerificationButtonText }}
            </el-button>
          </div>
        </div>
      </div>
    </el-card>
  </div>
</template>

<script>
import { getUserInfo, updateUserInfo, changePassword, uploadAvatar } from '@/api/user';
import { getStudentVerificationStatus, getLandlordVerificationStatus } from '@/api/verification';
import { mapActions } from 'vuex';

export default {
  name: 'UserProfile',
  data() {
    const validateConfirmPassword = (rule, value, callback) => {
      if (value !== this.passwordForm.newPassword) {
        callback(new Error('两次输入密码不一致'));
      } else {
        callback();
      }
    };
    
    return {
      userForm: {
        username: '',
        realName: '',
        phone: '',
        email: '',
        gender: 0
      },
      passwordForm: {
        oldPassword: '',
        newPassword: '',
        confirmPassword: ''
      },
      rules: {
        realName: [
          { required: true, message: '请输入真实姓名', trigger: 'blur' },
          { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
        ],
        phone: [
          { required: true, message: '请输入手机号码', trigger: 'blur' },
          { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
        ],
        email: [
          { required: true, message: '请输入邮箱', trigger: 'blur' },
          { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
        ]
      },
      passwordRules: {
        oldPassword: [
          { required: true, message: '请输入原密码', trigger: 'blur' },
          { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
        ],
        newPassword: [
          { required: true, message: '请输入新密码', trigger: 'blur' },
          { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
        ],
        confirmPassword: [
          { required: true, message: '请再次输入新密码', trigger: 'blur' },
          { validator: validateConfirmPassword, trigger: 'blur' }
        ]
      },
      avatarUrl: '',
      studentVerificationStatus: 0, // 0-未认证,1-已认证,2-认证中,3-认证失败
      landlordVerificationStatus: 0
    };
  },
  computed: {
    studentVerificationStatusText() {
      const statusMap = {
        0: '未认证',
        1: '已认证',
        2: '认证中',
        3: '认证失败'
      };
      return statusMap[this.studentVerificationStatus] || '未认证';
    },
    landlordVerificationStatusText() {
      const statusMap = {
        0: '未认证',
        1: '已认证',
        2: '认证中',
        3: '认证失败'
      };
      return statusMap[this.landlordVerificationStatus] || '未认证';
    },
    studentVerificationStatusType() {
      const typeMap = {
        0: 'info',
        1: 'success',
        2: 'warning',
        3: 'danger'
      };
      return typeMap[this.studentVerificationStatus] || 'info';
    },
    landlordVerificationStatusType() {
      const typeMap = {
        0: 'info',
        1: 'success',
        2: 'warning',
        3: 'danger'
      };
      return typeMap[this.landlordVerificationStatus] || 'info';
    },
    studentVerificationButtonText() {
      const textMap = {
        0: '去认证',
        1: '已认证',
        2: '认证中',
        3: '重新认证'
      };
      return textMap[this.studentVerificationStatus] || '去认证';
    },
    landlordVerificationButtonText() {
      const textMap = {
        0: '去认证',
        1: '已认证',
        2: '认证中',
        3: '重新认证'
      };
      return textMap[this.landlordVerificationStatus] || '去认证';
    }
  },
  created() {
    this.getUserInfo();
    this.getVerificationStatus();
  },
  methods: {
    ...mapActions(['setUserInfo']),
    async getUserInfo() {
      try {
        const res = await getUserInfo();
        if (res.code === 200) {
          const { username, realName, phone, email, gender, avatar } = res.data;
          this.userForm = { username, realName, phone, email, gender };
          this.avatarUrl = avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png';
        }
      } catch (error) {
        this.$message.error('获取用户信息失败');
        console.error(error);
      }
    },
    async getVerificationStatus() {
      try {
        const studentRes = await getStudentVerificationStatus();
        if (studentRes.code === 200) {
          this.studentVerificationStatus = studentRes.data.status;
        }
        
        const landlordRes = await getLandlordVerificationStatus();
        if (landlordRes.code === 200) {
          this.landlordVerificationStatus = landlordRes.data.status;
        }
      } catch (error) {
        console.error(error);
      }
    },
    submitForm(formName) {
      this.$refs[formName].validate(async (valid) => {
        if (valid) {
          try {
            const res = await updateUserInfo(this.userForm);
            if (res.code === 200) {
              this.$message.success('个人资料更新成功');
              this.setUserInfo(res.data);
            }
          } catch (error) {
            this.$message.error('更新失败');
            console.error(error);
          }
        } else {
          return false;
        }
      });
    },
    submitPasswordForm(formName) {
      this.$refs[formName].validate(async (valid) => {
        if (valid) {
          try {
            const res = await changePassword(this.passwordForm);
            if (res.code === 200) {
              this.$message.success('密码修改成功,请重新登录');
              this.$store.dispatch('logout');
              this.$router.push('/login');
            }
          } catch (error) {
            this.$message.error('密码修改失败');
            console.error(error);
          }
        } else {
          return false;
        }
      });
    },
    beforeAvatarUpload(file) {
      const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
      const isLt2M = file.size / 1024 / 1024 < 2;

      if (!isJPG) {
        this.$message.error('上传头像图片只能是 JPG 或 PNG 格式!');
      }
      if (!isLt2M) {
        this.$message.error('上传头像图片大小不能超过 2MB!');
      }
      return isJPG && isLt2M;
    },
    async uploadAvatar(options) {
      const formData = new FormData();
      formData.append('file', options.file);
      
      try {
        const res = await uploadAvatar(formData);
        if (res.code === 200) {
          this.avatarUrl = res.data;
          this.$message.success('头像上传成功');
          
          // 更新用户信息
          await this.getUserInfo();
          this.setUserInfo({ avatar: res.data });
        }
      } catch (error) {
        this.$message.error('头像上传失败');
        console.error(error);
      }
    },
    goToStudentVerification() {
      this.$router.push('/verification/student');
    },
    goToLandlordVerification() {
      this.$router.push('/verification/landlord');
    }
  }
};
</script>

<style scoped>
.profile-container {
  max-width: 800px;
  margin: 20px auto;
}

.profile-card {
  margin-bottom: 20px;
}

.card-header {
  font-size: 18px;
  font-weight: bold;
}

.avatar-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 20px;
}

.avatar-uploader {
  margin-top: 10px;
}

.verification-container {
  padding: 10px 0;
}

.verification-item {
  display: flex;
  align-items: center;
  padding: 15px 0;
  border-bottom: 1px solid #eee;
}

.verification-item:last-child {
  border-bottom: none;
}

.verification-title {
  flex: 1;
  font-weight: bold;
}

.verification-status {
  margin-right: 20px;
}

.verification-action {
  width: 100px;
  text-align: right;
}
</style>
Verification Components
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\views\verification\StudentVerification.vue

vue
CopyInsert
<template>
  <div class="verification-container">
    <el-card class="verification-card">
      <div slot="header" class="card-header">
        <span>学生认证</span>
      </div>
      
      <div v-if="verificationStatus === 1" class="verification-success">
        <i class="el-icon-success"></i>
        <p>恭喜您,学生认证已通过!</p>
        <el-button type="primary" @click="$router.push('/user/profile')">返回个人中心</el-button>
      </div>
      
      <div v-else-if="verificationStatus === 2" class="verification-pending">
        <i class="el-icon-loading"></i>
        <p>您的学生认证申请正在审核中,请耐心等待...</p>
        <el-button type="primary" @click="$router.push('/user/profile')">返回个人中心</el-button>
      </div>
      
      <div v-else-if="verificationStatus === 3" class="verification-failed">
        <i class="el-icon-error"></i>
        <p>很遗憾,您的学生认证申请未通过审核。</p>
        <p class="reason">原因:{{ verificationRemark }}</p>
        <el-button type="primary" @click="resetForm">重新认证</el-button>
      </div>
      
      <el-form 
        v-else
        :model="verificationForm" 
        :rules="rules" 
        ref="verificationForm" 
        label-width="100px"
        class="verification-form">
        
        <el-form-item label="学号" prop="studentId">
          <el-input v-model="verificationForm.studentId"></el-input>
        </el-form-item>
        
        <el-form-item label="学校" prop="school">
          <el-input v-model="verificationForm.school"></el-input>
        </el-form-item>
        
        <el-form-item label="学院" prop="college">
          <el-input v-model="verificationForm.college"></el-input>
        </el-form-item>
        
        <el-form-item label="专业" prop="major">
          <el-input v-model="verificationForm.major"></el-input>
        </el-form-item>
        
        <el-form-item label="身份证号" prop="idCard">
          <el-input v-model="verificationForm.idCard"></el-input>
        </el-form-item>
        
        <el-form-item label="身份证正面" prop="idCardFront">
          <el-upload
            class="upload-container"
            action="#"
            :http-request="uploadIdCardFront"
            :show-file-list="false"
            :before-upload="beforeUpload">
            <img v-if="idCardFrontUrl" :src="idCardFrontUrl" class="upload-image">
            <i v-else class="el-icon-plus upload-icon"></i>
            <div class="upload-text">点击上传身份证正面照片</div>
          </el-upload>
        </el-form-item>
        
        <el-form-item label="身份证背面" prop="idCardBack">
          <el-upload
            class="upload-container"
            action="#"
            :http-request="uploadIdCardBack"
            :show-file-list="false"
            :before-upload="beforeUpload">
            <img v-if="idCardBackUrl" :src="idCardBackUrl" class="upload-image">
            <i v-else class="el-icon-plus upload-icon"></i>
            <div class="upload-text">点击上传身份证背面照片</div>
          </el-upload>
        </el-form-item>
        
        <el-form-item label="学生证" prop="studentCard">
          <el-upload
            class="upload-container"
            action="#"
            :http-request="uploadStudentCard"
            :show-file-list="false"
            :before-upload="beforeUpload">
            <img v-if="studentCardUrl" :src="studentCardUrl" class="upload-image">
            <i v-else class="el-icon-plus upload-icon"></i>
            <div class="upload-text">点击上传学生证照片</div>
          </el-upload>
        </el-form-item>
        
        <el-form-item>
          <el-button type="primary" @click="submitForm('verificationForm')">提交认证</el-button>
          <el-button @click="$router.push('/user/profile')">返回</el-button>
        </el-form-item>
      </el-form>
      
      <div class="verification-tips">
        <h3>认证须知:</h3>
        <p>1. 请确保上传的证件照片清晰可见,信息完整。</p>
        <p>2. 身份证信息必须与实名认证信息一致。</p>
        <p>3. 学生证必须在有效期内,且能清晰看到学校名称、学院、专业和学号。</p>
        <p>4. 认证审核通常在1-3个工作日内完成,请耐心等待。</p>
      </div>
    </el-card>
  </div>
</template>

<script>
import { submitStudentVerification, getStudentVerificationStatus } from '@/api/verification';

export default {
  name: 'StudentVerification',
  data() {
    return {
      verificationStatus: 0, // 0-未认证,1-已认证,2-认证中,3-认证失败
      verificationRemark: '',
      verificationForm: {
        studentId: '',
        school: '',
        college: '',
        major: '',
        idCard: '',
        idCardFront: '',
        idCardBack: '',
        studentCard: ''
      },
      idCardFrontUrl: '',
      idCardBackUrl: '',
      studentCardUrl: '',
      rules: {
        studentId: [
          { required: true, message: '请输入学号', trigger: 'blur' },
          { min: 5, max: 20, message: '长度在 5 到 20 个字符', trigger: 'blur' }
        ],
        school: [
          { required: true, message: '请输入学校', trigger: 'blur' }
        ],
        college: [
          { required: true, message: '请输入学院', trigger: 'blur' }
        ],
        major: [
          { required: true, message: '请输入专业', trigger: 'blur' }
        ],
        idCard: [
          { required: true, message: '请输入身份证号', trigger: 'blur' },
          { pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: '请输入正确的身份证号', trigger: 'blur' }
        ],
        idCardFront: [
          { required: true, message: '请上传身份证正面照片', trigger: 'change' }
        ],
        idCardBack: [
          { required: true, message: '请上传身份证背面照片', trigger: 'change' }
        ],
        studentCard: [
          { required: true, message: '请上传学生证照片', trigger: 'change' }
        ]
      }
    };
  },
  created() {
    this.getVerificationStatus();
  },
  methods: {
    async getVerificationStatus() {
      try {
        const res = await getStudentVerificationStatus();
        if (res.code === 200) {
          this.verificationStatus = res.data.status;
          this.verificationRemark = res.data.remark || '';
          
          // 如果有已提交的认证信息,填充表单
          if (res.data.verification) {
            const { studentId, school, college, major, idCard, idCardFront, idCardBack, studentCard } = res.data.verification;
            this.verificationForm = { studentId, school, college, major, idCard, idCardFront, idCardBack, studentCard };
            this.idCardFrontUrl = idCardFront;
            this.idCardBackUrl = idCardBack;
            this.studentCardUrl = studentCard;
          }
        }
      } catch (error) {
        this.$message.error('获取认证状态失败');
        console.error(error);
      }
    },
    beforeUpload(file) {
      const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
      const isLt2M = file.size / 1024 / 1024 < 2;

      if (!isJPG) {
        this.$message.error('上传图片只能是 JPG 或 PNG 格式!');
      }
      if (!isLt2M) {
        this.$message.error('上传图片大小不能超过 2MB!');
      }
      return isJPG && isLt2M;
    },
    uploadIdCardFront(options) {
      const formData = new FormData();
      formData.append('file', options.file);
      
      // 模拟上传
      setTimeout(() => {
        const url = URL.createObjectURL(options.file);
        this.idCardFrontUrl = url;
        this.verificationForm.idCardFront = url;
        this.$refs.verificationForm.validateField('idCardFront');
      }, 500);
    },
    uploadIdCardBack(options) {
      const formData = new FormData();
      formData.append('file', options.file);
      
      // 模拟上传
      setTimeout(() => {
        const url = URL.createObjectURL(options.file);
        this.idCardBackUrl = url;
        this.verificationForm.idCardBack = url;
        this.$refs.verificationForm.validateField('idCardBack');
      }, 500);
    },
    uploadStudentCard(options) {
      const formData = new FormData();
      formData.append('file', options.file);
      
      // 模拟上传
      setTimeout(() => {
        const url = URL.createObjectURL(options.file);
        this.studentCardUrl = url;
        this.verificationForm.studentCard = url;
        this.$refs.verificationForm.validateField('studentCard');
      }, 500);
    },
    submitForm(formName) {
      this.$refs[formName].validate(async (valid) => {
        if (valid) {
          try {
            const formData = new FormData();
            Object.keys(this.verificationForm).forEach(key => {
              formData.append(key, this.verificationForm[key]);
            });
            
            const res = await submitStudentVerification(formData);
            if (res.code === 200) {
              this.$message.success('认证申请提交成功,请等待审核');
              this.verificationStatus = 2; // 认证中
            }
          } catch (error) {
            this.$message.error('提交失败');
            console.error(error);
          }
        } else {
          return false;
        }
      });
    },
    resetForm() {
      this.verificationStatus = 0;
      this.verificationForm = {
        studentId: '',
        school: '',
        college: '',
        major: '',
        idCard: '',
        idCardFront: '',
        idCardBack: '',
        studentCard: ''
      };
      this.idCardFrontUrl = '';
      this.idCardBackUrl = '';
      this.studentCardUrl = '';
    }
  }
};
</script>

<style scoped>
.verification-container {
  max-width: 800px;
  margin: 20px auto;
}

.verification-card {
  margin-bottom: 20px;
}

.card-header {
  font-size: 18px;
  font-weight: bold;
}

.verification-success,
.verification-pending,
.verification-failed {
  text-align: center;
  padding: 40px 0;
}

.verification-success i,
.verification-pending i,
.verification-failed i {
  font-size: 60px;
  margin-bottom: 20px;
}

.verification-success i {
  color: #67C23A;
}

.verification-pending i {
  color: #E6A23C;
}

.verification-failed i {
  color: #F56C6C;
}

.verification-success p,
.verification-pending p,
.verification-failed p {
  font-size: 18px;
  margin-bottom: 20px;
}

.reason {
  color: #F56C6C;
  font-size: 14px;
}

.upload-container {
  width: 220px;
  height: 140px;
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.upload-container:hover {
  border-color: #409EFF;
}

.upload-icon {
  font-size: 28px;
  color: #8c939d;
}

.upload-text {
  color: #8c939d;
  font-size: 14px;
  margin-top: 10px;
}

.upload-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.verification-tips {
  margin-top: 20px;
  padding: 15px;
  background-color: #f8f8f8;
  border-radius: 4px;
}

.verification-tips h3 {
  margin-top: 0;
  margin-bottom: 10px;
  font-size: 16px;
}

.verification-tips p {
  margin: 5px 0;
  font-size: 14px;
  color: #666;
}
</style>
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\utils\request.js

javascript
CopyInsert
import axios from 'axios';
import { Message } from 'element-ui';
import store from '@/store';
import router from '@/router';

// 创建axios实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API || '/api', // url = base url + request url
  timeout: 5000 // 请求超时时间
});

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 在发送请求之前做些什么
    if (store.getters.token) {
      // 让每个请求携带token
      config.headers['Authorization'] = 'Bearer ' + store.getters.token;
    }
    return config;
  },
  error => {
    // 对请求错误做些什么
    console.log(error); // for debug
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  /**
   * 如果您想获取http信息,如头信息或状态
   * 请返回response => response
  */
  /**
   * 通过自定义代码确定请求状态
   * 这里只是一个例子
   * 您还可以通过HTTP状态码判断状态
   */
  response => {
    const res = response.data;

    // 如果自定义代码不是200,则判断为错误
    if (res.code !== 200) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      });

      // 401: 未登录或token过期
      if (res.code === 401) {
        // 重新登录
        store.dispatch('resetToken').then(() => {
          // 为了重新实例化vue-router对象,避免bug
          location.reload();
        });
      }
      return Promise.reject(new Error(res.message || 'Error'));
    } else {
      return res;
    }
  },
  error => {
    console.log('err' + error); // for debug
    let message = error.message;
    if (error.response) {
      switch (error.response.status) {
        case 401:
          message = '未授权,请重新登录';
          // 清除token并跳转登录页
          store.dispatch('resetToken');
          router.replace({
            path: '/login',
            query: { redirect: router.currentRoute.fullPath }
          });
          break;
        case 403:
          message = '拒绝访问';
          break;
        case 404:
          message = `请求地址出错: ${error.response.config.url}`;
          break;
        case 408:
          message = '请求超时';
          break;
        case 500:
          message = '服务器内部错误';
          break;
        case 501:
          message = '服务未实现';
          break;
        case 502:
          message = '网关错误';
          break;
        case 503:
          message = '服务不可用';
          break;
        case 504:
          message = '网关超时';
          break;
        case 505:
          message = 'HTTP版本不受支持';
          break;
        default:
          message = `连接出错(${error.response.status})!`;
      }
    }
    Message({
      message: message,
      type: 'error',
      duration: 5 * 1000
    });
    return Promise.reject(error);
  }
);

export default service;
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\utils\date.js

javascript
CopyInsert
/**
 * 日期格式化
 * @param {Date} date 日期
 * @param {String} fmt 格式化字符串,默认:yyyy-MM-dd
 * @returns {String} 格式化后的日期字符串
 */
export function formatDate(date, fmt = 'yyyy-MM-dd') {
  if (!date) {
    return '';
  }
  
  if (typeof date === 'string') {
    date = new Date(date);
  }
  
  if (!(date instanceof Date)) {
    return '';
  }
  
  const o = {
    'M+': date.getMonth() + 1, // 月份
    'd+': date.getDate(), // 日
    'h+': date.getHours() % 12 === 0 ? 12 : date.getHours() % 12, // 小时
    'H+': date.getHours(), // 小时
    'm+': date.getMinutes(), // 分
    's+': date.getSeconds(), // 秒
    'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
    'S': date.getMilliseconds() // 毫秒
  };
  
  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
  }
  
  for (let k in o) {
    if (new RegExp('(' + k + ')').test(fmt)) {
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)));
    }
  }
  
  return fmt;
}

/**
 * 计算两个日期之间的天数
 * @param {Date} date1 日期1
 * @param {Date} date2 日期2
 * @returns {Number} 天数
 */
export function daysBetween(date1, date2) {
  const time1 = date1.getTime();
  const time2 = date2.getTime();
  const diff = Math.abs(time2 - time1);
  return Math.floor(diff / (1000 * 60 * 60 * 24));
}

/**
 * 日期加减
 * @param {Date} date 日期
 * @param {Number} days 天数
 * @returns {Date} 新日期
 */
export function addDays(date, days) {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

/**
 * 获取当前月的第一天
 * @param {Date} date 日期
 * @returns {Date} 当月第一天
 */
export function getFirstDayOfMonth(date) {
  const result = new Date(date);
  result.setDate(1);
  return result;
}

/**
 * 获取当前月的最后一天
 * @param {Date} date 日期
 * @returns {Date} 当月最后一天
 */
export function getLastDayOfMonth(date) {
  const result = new Date(date);
  result.setMonth(result.getMonth() + 1);
  result.setDate(0);
  return result;
}
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\utils\auth.js

javascript
CopyInsert
import Cookies from 'js-cookie';

const TokenKey = 'rental_token';
const UserKey = 'rental_user';

// Token相关操作
export function getToken() {
  return Cookies.get(TokenKey);
}

export function setToken(token) {
  return Cookies.set(TokenKey, token);
}

export function removeToken() {
  return Cookies.remove(TokenKey);
}

// 用户信息相关操作
export function getUserInfo() {
  const userStr = localStorage.getItem(UserKey);
  return userStr ? JSON.parse(userStr) : null;
}

export function setUserInfo(userInfo) {
  return localStorage.setItem(UserKey, JSON.stringify(userInfo));
}

export function removeUserInfo() {
  return localStorage.removeItem(UserKey);
}

// 判断用户角色
export function hasRole(role) {
  const userInfo = getUserInfo();
  if (!userInfo || !userInfo.roles) {
    return false;
  }
  return userInfo.roles.includes(role);
}

// 判断是否是学生
export function isStudent() {
  return hasRole('ROLE_STUDENT');
}

// 判断是否是房东
export function isLandlord() {
  return hasRole('ROLE_LANDLORD');
}

// 判断是否是管理员
export function isAdmin() {
  return hasRole('ROLE_ADMIN');
}
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\views\user\Favorite.vue

vue
CopyInsert
<template>
  <div class="favorite-container">
    <el-card class="favorite-card">
      <div slot="header" class="card-header">
        <span>我的收藏</span>
      </div>
      
      <div v-if="loading" class="loading-container">
        <el-skeleton :rows="3" animated />
        <el-skeleton :rows="3" animated />
      </div>
      
      <div v-else-if="favoriteList.length === 0" class="empty-container">
        <el-empty description="暂无收藏房源">
          <el-button type="primary" @click="$router.push('/home')">去浏览房源</el-button>
        </el-empty>
      </div>
      
      <div v-else class="house-list">
        <el-row :gutter="20">
          <el-col :xs="24" :sm="12" :md="8" v-for="item in favoriteList" :key="item.id">
            <el-card class="house-card" shadow="hover" @click.native="viewHouseDetail(item.houseId)">
              <div class="house-image">
                <img :src="item.coverUrl || 'https://via.placeholder.com/300x200'" alt="房源图片">
                <div class="house-price">{{ item.price }}元/月</div>
              </div>
              <div class="house-info">
                <h3 class="house-title">{{ item.title }}</h3>
                <div class="house-tags">
                  <el-tag size="mini">{{ item.houseType }}</el-tag>
                  <el-tag size="mini" type="success">{{ item.area }}㎡</el-tag>
                  <el-tag size="mini" type="info">{{ item.orientation }}</el-tag>
                </div>
                <div class="house-address">
                  <i class="el-icon-location"></i>
                  <span>{{ item.community }}</span>
                </div>
                <div class="house-time">收藏时间:{{ formatDate(item.createTime) }}</div>
              </div>
              <div class="house-actions">
                <el-button type="text" size="mini" @click.stop="cancelFavorite(item.id, item.houseId)">
                  <i class="el-icon-star-off"></i> 取消收藏
                </el-button>
              </div>
            </el-card>
          </el-col>
        </el-row>
      </div>
      
      <el-pagination
        v-if="total > 0"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="currentPage"
        :page-sizes="[12, 24, 36, 48]"
        :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
        class="pagination">
      </el-pagination>
    </el-card>
  </div>
</template>

<script>
import { getFavoriteList, cancelFavorite } from '@/api/favorite';
import { formatDate } from '@/utils/date';

export default {
  name: 'UserFavorite',
  data() {
    return {
      favoriteList: [],
      loading: true,
      currentPage: 1,
      pageSize: 12,
      total: 0
    };
  },
  created() {
    this.fetchFavoriteList();
  },
  methods: {
    formatDate,
    async fetchFavoriteList() {
      this.loading = true;
      try {
        const res = await getFavoriteList({
          page: this.currentPage,
          size: this.pageSize
        });
        if (res.code === 200) {
          this.favoriteList = res.data.list;
          this.total = res.data.total;
        }
      } catch (error) {
        this.$message.error('获取收藏列表失败');
        console.error(error);
      } finally {
        this.loading = false;
      }
    },
    async cancelFavorite(id, houseId) {
      try {
        const res = await cancelFavorite(houseId);
        if (res.code === 200) {
          this.$message.success('取消收藏成功');
          this.favoriteList = this.favoriteList.filter(item => item.id !== id);
          if (this.favoriteList.length === 0 && this.currentPage > 1) {
            this.currentPage--;
            this.fetchFavoriteList();
          }
        }
      } catch (error) {
        this.$message.error('取消收藏失败');
        console.error(error);
      }
    },
    viewHouseDetail(houseId) {
      this.$router.push(`/house/detail/${houseId}`);
    },
    handleSizeChange(val) {
      this.pageSize = val;
      this.fetchFavoriteList();
    },
    handleCurrentChange(val) {
      this.currentPage = val;
      this.fetchFavoriteList();
    }
  }
};
</script>

<style scoped>
.favorite-container {
  max-width: 1200px;
  margin: 20px auto;
}

.favorite-card {
  margin-bottom: 20px;
}

.card-header {
  font-size: 18px;
  font-weight: bold;
}

.loading-container {
  padding: 20px 0;
}

.empty-container {
  padding: 40px 0;
  text-align: center;
}

.house-list {
  margin-bottom: 20px;
}

.house-card {
  margin-bottom: 20px;
  cursor: pointer;
  transition: all 0.3s;
}

.house-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.house-image {
  position: relative;
  height: 180px;
  overflow: hidden;
}

.house-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.house-price {
  position: absolute;
  bottom: 0;
  right: 0;
  background-color: rgba(255, 90, 95, 0.9);
  color: #fff;
  padding: 5px 10px;
  font-weight: bold;
}

.house-info {
  padding: 10px 0;
}

.house-title {
  margin: 0 0 10px;
  font-size: 16px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.house-tags {
  margin-bottom: 10px;
}

.house-tags .el-tag {
  margin-right: 5px;
}

.house-address {
  color: #666;
  font-size: 14px;
  margin-bottom: 5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.house-time {
  color: #999;
  font-size: 12px;
}

.house-actions {
  border-top: 1px solid #eee;
  padding-top: 10px;
  text-align: right;
}

.pagination {
  text-align: center;
  margin-top: 20px;
}
</style>
Admin Components
File: d:\htsun\Docs\资料\文章\rental-system\frontend\src\views\admin\Dashboard.vue

vue
CopyInsert
<template>
  <div class="dashboard-container">
    <el-row :gutter="20">
      <el-col :span="6">
        <el-card class="data-card">
          <div class="data-header">
            <div class="data-title">总用户数</div>
            <i class="el-icon-user data-icon"></i>
          </div>
          <div class="data-content">
            <div class="data-value">{{ statistics.userCount || 0 }}</div>
            <div class="data-change">
              <span :class="statistics.userIncrease > 0 ? 'increase' : 'decrease'">
                <i :class="statistics.userIncrease > 0 ? 'el-icon-top' : 'el-icon-bottom'"></i>
                {{ Math.abs(statistics.userIncrease || 0) }}%
              </span>
              较上周
            </div>
          </div>
        </el-card>
      </el-col>
      
      <el-col :span="6">
        <el-card class="data-card">
          <div class="data-header">
            <div class="data-title">总房源数</div>
            <i class="el-icon-house data-icon"></i>
          </div>
          <div class="data-content">
            <div class="data-value">{{ statistics.houseCount || 0 }}</div>
            <div class="data-change">
              <span :class="statistics.houseIncrease > 0 ? 'increase' : 'decrease'">
                <i :class="statistics.houseIncrease > 0 ? 'el-icon-top' : 'el-icon-bottom'"></i>
                {{ Math.abs(statistics.houseIncrease || 0) }}%
              </span>
              较上周
            </div>
          </div>
        </el-card>
      </el-col>
      
      <el-col :span="6">
        <el-card class="data-card">
          <div class="data-header">
            <div class="data-title">待审核认证</div>
            <i class="el-icon-document-checked data-icon"></i>
          </div>
          <div class="data-content">
            <div class="data-value">{{ statistics.pendingVerification || 0 }}</div>
            <div class="data-change">
              <router-link to="/admin/verification" class="link">查看详情</router-link>
            </div>
          </div>
        </el-card>
      </el-col>
      
      <el-col :span="6">
        <el-card class="data-card">
          <div class="data-header">
            <div class="data-title">今日访问量</div>
            <i class="el-icon-view data-icon"></i>
          </div>
          <div class="data-content">
            <div class="data-value">{{ statistics.todayVisits || 0 }}</div>
            <div class="data-change">
              <span :class="statistics.visitsIncrease > 0 ? 'increase' : 'decrease'">
                <i :class="statistics.visitsIncrease > 0 ? 'el-icon-top' : 'el-icon-bottom'"></i>
                {{ Math.abs(statistics.visitsIncrease || 0) }}%
              </span>
              较昨日
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    
    <el-row :gutter="20" class="chart-row">
      <el-col :span="12">
        <el-card class="chart-card">
          <div slot="header" class="chart-header">
            <span>用户注册统计</span>
            <el-radio-group v-model="userChartType" size="mini">
              <el-radio-button label="week">本周</el-radio-button>
              <el-radio-button label="month">本月</el-radio-button>
              <el-radio-button label="year">本年</el-radio-button>
            </el-radio-group>
          </div>
          <div class="chart-container" ref="userChart"></div>
        </el-card>
      </el-col>
      
      <el-col :span="12">
        <el-card class="chart-card">
          <div slot="header" class="chart-header">
            <span>房源发布统计</span>
            <el-radio-group v-model="houseChartType" size="mini">
              <el-radio-button label="week">本周</el-radio-button>
              <el-radio-button label="month">本月</el-radio-button>
              <el-radio-button label="year">本年</el-radio-button>
            </el-radio-group>
          </div>
          <div class="chart-container" ref="houseChart"></div>
        </el-card>
      </el-col>
    </el-row>
    
    <el-row :gutter="20">
      <el-col :span="12">
        <el-card class="table-card">
          <div slot="header" class="table-header">
            <span>最新用户</span>
            <el-button type="text" @click="$router.push('/admin/user')">查看更多</el-button>
          </div>
          <el-table :data="latestUsers" style="width: 100%" size="mini">
            <el-table-column prop="username" label="用户名" width="120"></el-table-column>
            <el-table-column prop="realName" label="真实姓名" width="120"></el-table-column>
            <el-table-column prop="roleName" label="角色"></el-table-column>
            <el-table-column prop="createTime" label="注册时间" :formatter="formatDate"></el-table-column>
          </el-table>
        </el-card>
      </el-col>
      
      <el-col :span="12">
        <el-card class="table-card">
          <div slot="header" class="table-header">
            <span>最新房源</span>
            <el-button type="text" @click="$router.push('/admin/house')">查看更多</el-button>
          </div>
          <el-table :data="latestHouses" style="width: 100%" size="mini">
            <el-table-column prop="title" label="标题" width="180"></el-table-column>
            <el-table-column prop="price" label="租金" width="100">
              <template slot-scope="scope">
                {{ scope.row.price }}元/月
              </template>
            </el-table-column>
            <el-table-column prop="statusText" label="状态">
              <template slot-scope="scope">
                <el-tag :type="getStatusType(scope.row.status)">{{ scope.row.statusText }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="createTime" label="发布时间" :formatter="formatDate"></el-table-column>
          </el-table>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
import { formatDate } from '@/utils/date';
import * as echarts from 'echarts';

export default {
  name: 'AdminDashboard',
  data() {
    return {
      statistics: {
        userCount: 0,
        userIncrease: 0,
        houseCount: 0,
        houseIncrease: 0,
        pendingVerification: 0,
        todayVisits: 0,
        visitsIncrease: 0
      },
      userChartType: 'week',
      houseChartType: 'week',
      latestUsers: [],
      latestHouses: [],
      userChart: null,
      houseChart: null
    };
  },
  created() {
    this.fetchStatistics();
    this.fetchLatestUsers();
    this.fetchLatestHouses();
  },
  mounted() {
    this.initCharts();
    window.addEventListener('resize', this.resizeCharts);
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.resizeCharts);
    if (this.userChart) {
      this.userChart.dispose();
    }
    if (this.houseChart) {
      this.houseChart.dispose();
    }
  },
  watch: {
    userChartType() {
      this.updateUserChart();
    },
    houseChartType() {
      this.updateHouseChart();
    }
  },
  methods: {
    formatDate(row, column, cellValue) {
      return formatDate(cellValue);
    },
    getStatusType(status) {
      const typeMap = {
        0: 'info',    // 待审核
        1: 'success', // 已上架
        2: 'warning', // 已下架
        3: 'danger'   // 已出租
      };
      return typeMap[status] || 'info';
    },
    async fetchStatistics() {
      // 模拟数据,实际项目中应该从API获取
      this.statistics = {
        userCount: 256,
        userIncrease: 5.2,
        houseCount: 128,
        houseIncrease: 3.7,
        pendingVerification: 12,
        todayVisits: 1024,
        visitsIncrease: -2.1
      };
    },
    async fetchLatestUsers() {
      // 模拟数据,实际项目中应该从API获取
      this.latestUsers = [
        { id: 1, username: 'student1', realName: '张三', roleName: '学生', createTime: new Date() },
        { id: 2, username: 'landlord1', realName: '李四', roleName: '房东', createTime: new Date() },
        { id: 3, username: 'student2', realName: '王五', roleName: '学生', createTime: new Date() },
        { id: 4, username: 'landlord2', realName: '赵六', roleName: '房东', createTime: new Date() },
        { id: 5, username: 'student3', realName: '钱七', roleName: '学生', createTime: new Date() }
      ];
    },
    async fetchLatestHouses() {
      // 模拟数据,实际项目中应该从API获取
      this.latestHouses = [
        { id: 1, title: '温馨一室一厅', price: 1500, status: 1, statusText: '已上架', createTime: new Date() },
        { id: 2, title: '精装两室一厅', price: 2500, status: 1, statusText: '已上架', createTime: new Date() },
        { id: 3, title: '阳光三室两厅', price: 3500, status: 0, statusText: '待审核', createTime: new Date() },
        { id: 4, title: '豪华四室两厅', price: 4500, status: 2, statusText: '已下架', createTime: new Date() },
        { id: 5, title: '舒适单间', price: 1000, status: 3, statusText: '已出租', createTime: new Date() }
      ];
    },
    initCharts() {
      // 初始化用户注册统计图表
      this.userChart = echarts.init(this.$refs.userChart);
      this.updateUserChart();
      
      // 初始化房源发布统计图表
      this.houseChart = echarts.init(this.$refs.houseChart);
      this.updateHouseChart();
    },
    updateUserChart() {
      // 根据选择的类型生成不同的数据
      let xData = [];
      let yData = [];
      
      if (this.userChartType === 'week') {
        xData = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
        yData = [5, 7, 3, 9, 12, 6, 8];
      } else if (this.userChartType === 'month') {
        xData = Array.from({ length: 30 }, (_, i) => i + 1 + '日');
        yData = Array.from({ length: 30 }, () => Math.floor(Math.random() * 10) + 1);
      } else {
        xData = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
        yData = [30, 25, 35, 45, 55, 40, 35, 30, 50, 60, 45, 40];
      }
      
      const option = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: xData,
          axisTick: {
            alignWithLabel: true
          }
        },
        yAxis: {
          type: 'value'
        },
        series: [{
          name: '注册用户数',
          type: 'bar',
          barWidth: '60%',
          data: yData,
          itemStyle: {
            color: '#409EFF'
          }
        }]
      };
      
      this.userChart.setOption(option);
    },
    updateHouseChart() {
      // 根据选择的类型生成不同的数据
      let xData = [];
      let yData = [];
      
      if (this.houseChartType === 'week') {
        xData = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
        yData = [3, 5, 2, 7, 9, 4, 6];
      } else if (this.houseChartType === 'month') {
        xData = Array.from({ length: 30 }, (_, i) => i + 1 + '日');
        yData = Array.from({ length: 30 }, () => Math.floor(Math.random() * 5) + 1);
      } else {
        xData = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
        yData = [20, 15, 25, 30, 40, 25, 20, 15, 35, 45, 30, 25];
      }
      
      const option = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'line'
          }
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: xData,
          axisTick: {
            alignWithLabel: true
          }
        },
        yAxis: {
          type: 'value'
        },
        series: [{
          name: '发布房源数',
          type: 'line',
          smooth: true,
          data: yData,
          itemStyle: {
            color: '#67C23A'
          },
          areaStyle: {
            color: {
              type: 'linear',
              x: 0,
              y: 0,
              x2: 0,
              y2: 1,
              colorStops: [{
                offset: 0, color: 'rgba(103, 194, 58, 0.3)'
              }, {
                offset: 1, color: 'rgba(103, 194, 58, 0.1)'
              }]
            }
          }
        }]
      };
      
      this.houseChart.setOption(option);
    },
    resizeCharts() {
      if (this.userChart) {
        this.userChart.resize();
      }
      if (this.houseChart) {
        this.houseChart.resize();
      }
    }
  }
};
</script>

<style scoped>
.dashboard-container {
  padding: 20px;
}

.data-card {
  height: 120px;
  margin-bottom: 20px;
}

.data-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}

.data-title {
  font-size: 16px;
  color: #666;
}

.data-icon {
  font-size: 24px;
  color: #409EFF;
}

.data-content {
  display: flex;
  flex-direction: column;
}

.data-value {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 5px;
}

.data-change {
  font-size: 12px;
  color: #999;
}

.increase {
  color: #67C23A;
  margin-right: 5px;
}

.decrease {
  color: #F56C6C;
  margin-right: 5px;
}

.link {
  color: #409EFF;
  text-decoration: none;
}

.chart-row {
  margin-bottom: 20px;
}

.chart-card {
  margin-bottom: 20px;
}

.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.chart-container {
  height: 300px;
}

.table-card {
  margin-bottom: 20px;
}

.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>
Main Application Entry Point
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\java\com\rental\RentalApplication.java

java
CopyInsert
package com.rental;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * 租房管理系统启动类
 */
@SpringBootApplication
@EnableTransactionManagement
public class RentalApplication {

    public static void main(String[] args) {
        SpringApplication.run(RentalApplication.class, args);
    }
}
Security Configuration
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\java\com\rental\config\SecurityConfig.java

java
CopyInsert
package com.rental.config;

import com.rental.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 安全配置
 */
@Configuration
public class SecurityConfig implements WebMvcConfigurer {

    /**
     * 注册拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册认证拦截器
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login", "/user/register", "/house/list", "/house/search", "/house/detail/**");
    }

    /**
     * 跨域配置
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .maxAge(3600);
    }

    /**
     * 认证拦截器
     */
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
}
Authentication Interceptor
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\java\com\rental\interceptor\AuthenticationInterceptor.java

java
CopyInsert
package com.rental.interceptor;

import com.rental.dto.Result;
import com.rental.entity.User;
import com.rental.exception.UnauthorizedException;
import com.rental.service.UserService;
import com.rental.util.JwtUtil;
import com.rental.util.JsonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 认证拦截器
 */
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    private UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从请求头中获取token
        String token = request.getHeader("Authorization");
        if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
            token = token.substring(7);
        }

        // 如果没有token,返回未授权错误
        if (!StringUtils.hasText(token)) {
            handleUnauthorized(response);
            return false;
        }

        try {
            // 验证token
            Long userId = JwtUtil.getUserId(token);
            if (userId == null) {
                handleUnauthorized(response);
                return false;
            }

            // 查询用户信息
            User user = userService.getUserById(userId);
            if (user == null || user.getStatus() != 1) {
                handleUnauthorized(response);
                return false;
            }

            // 将用户信息存入请求属性中
            request.setAttribute("userId", userId);
            request.setAttribute("user", user);
            return true;
        } catch (Exception e) {
            handleUnauthorized(response);
            return false;
        }
    }

    /**
     * 处理未授权请求
     */
    private void handleUnauthorized(HttpServletResponse response) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(JsonUtil.toJson(Result.error(401, "未授权,请先登录")));
    }
}
JWT Utility
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\java\com\rental\util\JwtUtil.java

java
CopyInsert
package com.rental.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT工具类
 */
@Component
public class JwtUtil {

    private static String secret;

    private static Long expiration;

    @Value("${jwt.secret}")
    public void setSecret(String secret) {
        JwtUtil.secret = secret;
    }

    @Value("${jwt.expiration}")
    public void setExpiration(Long expiration) {
        JwtUtil.expiration = expiration;
    }

    /**
     * 生成token
     *
     * @param userId 用户ID
     * @return token
     */
    public static String generateToken(Long userId) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        return generateToken(claims);
    }

    /**
     * 从token中获取用户ID
     *
     * @param token token
     * @return 用户ID
     */
    public static Long getUserId(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims != null ? claims.get("userId", Long.class) : null;
    }

    /**
     * 验证token是否过期
     *
     * @param token token
     * @return 是否过期
     */
    public static boolean isTokenExpired(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims != null && claims.getExpiration().before(new Date());
    }

    /**
     * 生成token
     *
     * @param claims 数据声明
     * @return token
     */
    private static String generateToken(Map<String, Object> claims) {
        Date createdDate = new Date();
        Date expirationDate = new Date(createdDate.getTime() + expiration * 1000);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从token中获取数据声明
     *
     * @param token token
     * @return 数据声明
     */
    private static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }
}
Password Utility
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\java\com\rental\util\PasswordUtil.java

java
CopyInsert
package com.rental.util;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

/**
 * 密码工具类
 */
public class PasswordUtil {

    /**
     * 加密密码
     *
     * @param password 原始密码
     * @return 加密后的密码
     */
    public static String encode(String password) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(hash);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("加密密码失败", e);
        }
    }

    /**
     * 验证密码
     *
     * @param rawPassword     原始密码
     * @param encodedPassword 加密后的密码
     * @return 是否匹配
     */
    public static boolean matches(String rawPassword, String encodedPassword) {
        return encode(rawPassword).equals(encodedPassword);
    }
}
JSON Utility
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\java\com\rental\util\JsonUtil.java

java
CopyInsert
package com.rental.util;

import com.fasterxml.jackson.core.
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

/**
 * JSON工具类
 */
public class JsonUtil {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    static {
        // 注册Java 8时间模块
        objectMapper.registerModule(new JavaTimeModule());
        // 禁用日期时间作为时间戳输出
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }

    /**
     * 对象转JSON字符串
     *
     * @param obj 对象
     * @return JSON字符串
     */
    public static String toJson(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("转换JSON失败", e);
        }
    }

    /**
     * JSON字符串转对象
     *
     * @param json  JSON字符串
     * @param clazz 对象类型
     * @param <T>   泛型
     * @return 对象
     */
    public static <T> T fromJson(String json, Class<T> clazz) {
        try {
            return objectMapper.readValue(json, clazz);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("解析JSON失败", e);
        }
    }
}
Global Exception Handler
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\java\com\rental\exception\GlobalExceptionHandler.java

java
CopyInsert
package com.rental.exception;

import com.rental.dto.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Set;

/**
 * 全局异常处理器
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理自定义异常
     */
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        log.error("业务异常:{}", e.getMessage(), e);
        return Result.error(e.getCode(), e.getMessage());
    }

    /**
     * 处理未授权异常
     */
    @ExceptionHandler(UnauthorizedException.class)
    public Result<Void> handleUnauthorizedException(UnauthorizedException e) {
        log.error("未授权异常:{}", e.getMessage(), e);
        return Result.error(401, e.getMessage());
    }

    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("参数校验异常:{}", e.getMessage(), e);
        BindingResult bindingResult = e.getBindingResult();
        StringBuilder sb = new StringBuilder("参数校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        if (msg.endsWith(", ")) {
            msg = msg.substring(0, msg.length() - 2);
        }
        return Result.error(400, msg);
    }

    /**
     * 处理参数绑定异常
     */
    @ExceptionHandler(BindException.class)
    public Result<Void> handleBindException(BindException e) {
        log.error("参数绑定异常:{}", e.getMessage(), e);
        BindingResult bindingResult = e.getBindingResult();
        StringBuilder sb = new StringBuilder("参数绑定失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        if (msg.endsWith(", ")) {
            msg = msg.substring(0, msg.length() - 2);
        }
        return Result.error(400, msg);
    }

    /**
     * 处理约束违反异常
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
        log.error("约束违反异常:{}", e.getMessage(), e);
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        StringBuilder sb = new StringBuilder("参数校验失败:");
        for (ConstraintViolation<?> violation : violations) {
            sb.append(violation.getPropertyPath()).append(":").append(violation.getMessage()).append(", ");
        }
        String msg = sb.toString();
        if (msg.endsWith(", ")) {
            msg = msg.substring(0, msg.length() - 2);
        }
        return Result.error(400, msg);
    }

    /**
     * 处理其他异常
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常:{}", e.getMessage(), e);
        return Result.error(500, "系统繁忙,请稍后再试");
    }
}
Custom Exceptions
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\java\com\rental\exception\BusinessException.java

java
CopyInsert
package com.rental.exception;

import lombok.Getter;

/**
 * 业务异常
 */
@Getter
public class BusinessException extends RuntimeException {

    /**
     * 错误码
     */
    private final Integer code;

    /**
     * 错误消息
     */
    private final String message;

    public BusinessException(String message) {
        this(500, message);
    }

    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }
}
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\java\com\rental\exception\UnauthorizedException.java

java
CopyInsert
package com.rental.exception;

/**
 * 未授权异常
 */
public class UnauthorizedException extends RuntimeException {

    public UnauthorizedException() {
        super("未授权,请先登录");
    }

    public UnauthorizedException(String message) {
        super(message);
    }
}
MyBatis Mapper XML Files
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\resources\mapper\UserMapper.xml

xml
CopyInsert
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.rental.dao.UserDao">

    <resultMap id="BaseResultMap" type="com.rental.entity.User">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="real_name" property="realName"/>
        <result column="phone" property="phone"/>
        <result column="email" property="email"/>
        <result column="avatar" property="avatar"/>
        <result column="gender" property="gender"/>
        <result column="status" property="status"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
    </resultMap>

    <sql id="Base_Column_List">
        id, username, password, real_name, phone, email, avatar, gender, status, create_time, update_time
    </sql>

    <select id="selectById" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from user
        where id = #{id}
    </select>

    <select id="selectByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from user
        where username = #{username}
    </select>

    <select id="selectByPhone" parameterType="java.lang.String" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from user
        where phone = #{phone}
    </select>

    <select id="selectList" parameterType="com.rental.entity.User" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from user
        <where>
            <if test="username != null and username != ''">
                and username like concat('%', #{username}, '%')
            </if>
            <if test="realName != null and realName != ''">
                and real_name like concat('%', #{realName}, '%')
            </if>
            <if test="phone != null and phone != ''">
                and phone like concat('%', #{phone}, '%')
            </if>
            <if test="email != null and email != ''">
                and email like concat('%', #{email}, '%')
            </if>
            <if test="gender != null">
                and gender = #{gender}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
        order by create_time desc
    </select>

    <insert id="insert" parameterType="com.rental.entity.User" useGeneratedKeys="true" keyProperty="id">
        insert into user
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="username != null">username,</if>
            <if test="password != null">password,</if>
            <if test="realName != null">real_name,</if>
            <if test="phone != null">phone,</if>
            <if test="email != null">email,</if>
            <if test="avatar != null">avatar,</if>
            <if test="gender != null">gender,</if>
            <if test="status != null">status,</if>
            create_time,
            update_time,
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="username != null">#{username},</if>
            <if test="password != null">#{password},</if>
            <if test="realName != null">#{realName},</if>
            <if test="phone != null">#{phone},</if>
            <if test="email != null">#{email},</if>
            <if test="avatar != null">#{avatar},</if>
            <if test="gender != null">#{gender},</if>
            <if test="status != null">#{status},</if>
            now(),
            now(),
        </trim>
    </insert>

    <update id="update" parameterType="com.rental.entity.User">
        update user
        <set>
            <if test="username != null">username = #{username},</if>
            <if test="password != null">password = #{password},</if>
            <if test="realName != null">real_name = #{realName},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="email != null">email = #{email},</if>
            <if test="avatar != null">avatar = #{avatar},</if>
            <if test="gender != null">gender = #{gender},</if>
            <if test="status != null">status = #{status},</if>
            update_time = now(),
        </set>
        where id = #{id}
    </update>

    <update id="updateStatus">
        update user
        set status = #{status}, update_time = now()
        where id = #{id}
    </update>

    <delete id="deleteById" parameterType="java.lang.Long">
        delete from user
        where id = #{id}
    </delete>

    <insert id="insertUserRole">
        insert into user_role (user_id, role_id) values (#{userId}, #{roleId})
    </insert>

    <delete id="deleteUserRole" parameterType="java.lang.Long">
        delete from user_role
        where user_id = #{userId}
    </delete>
</mapper>
File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\resources\mapper\HouseMapper.xml

xml
CopyInsert
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.rental.dao.HouseDao">

    <resultMap id="BaseResultMap" type="com.rental.entity.House">
        <id column="id" property="id"/>
        <result column="landlord_id" property="landlordId"/>
        <result column="title" property="title"/>
        <result column="price" property="price"/>
        <result column="area" property="area"/>
        <result column="house_type" property="houseType"/>
        <result column="floor" property="floor"/>
        <result column="orientation" property="orientation"/>
        <result column="decoration" property="decoration"/>
        <result column="community" property="community"/>
        <result column="address" property="address"/>
        <result column="longitude" property="longitude"/>
        <result column="latitude" property="latitude"/>
        <result column="contact" property="contact"/>
        <result column="contact_phone" property="contactPhone"/>
        <result column="status" property="status"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
    </resultMap>

    <sql id="Base_Column_List">
        id, landlord_id, title, price, area, house_type, floor, orientation, decoration, community, address, 
        longitude, latitude, contact, contact_phone, status, create_time, update_time
    </sql>

    <select id="selectById" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from house
        where id = #{id}
    </select>

    <select id="selectByLandlordId" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from house
        where landlord_id = #{landlordId}
        order by create_time desc
    </select>

    <select id="selectList" parameterType="com.rental.entity.House" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from house
        <where>
            <if test="landlordId != null">
                and landlord_id = #{landlordId}
            </if>
            <if test="title != null and title != ''">
                and title like concat('%', #{title}, '%')
            </if>
            <if test="houseType != null and houseType != ''">
                and house_type = #{houseType}
            </if>
            <if test="community != null and community != ''">
                and community like concat('%', #{community}, '%')
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
        order by create_time desc
    </select>

    <select id="search" parameterType="java.util.Map" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from house
        <where>
            <if test="keyword != null and keyword != ''">
                and (
                    title like concat('%', #{keyword}, '%')
                    or community like concat('%', #{keyword}, '%')
                    or address like concat('%', #{keyword}, '%')
                )
            </if>
            <if test="minPrice != null">
                and price >= #{minPrice}
            </if>
            <if test="maxPrice != null">
                and price <= #{maxPrice}
            </if>
            <if test="minArea != null">
                and area >= #{minArea}
            </if>
            <if test="maxArea != null">
                and area <= #{maxArea}
            </if>
            <if test="houseType != null and houseType != ''">
                and house_type = #{houseType}
            </if>
            <if test="orientation != null and orientation != ''">
                and orientation = #{orientation}
            </if>
            <if test="decoration != null and decoration != ''">
                and decoration = #{decoration}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="status == null">
                and status = 1
            </if>
        </where>
        order by
        <choose>
            <when test="orderBy != null and orderBy == 'price_asc'">
                price asc
            </when>
            <when test="orderBy != null and orderBy == 'price_desc'">
                price desc
            </when>
            <when test="orderBy != null and orderBy == 'area_asc'">
                area asc
            </when>
            <when test="orderBy != null and orderBy == 'area_desc'">
                area desc
            </when>
            <when test="orderBy != null and orderBy == 'time_asc'">
                create_time asc
            </when>
            <otherwise>
                create_time desc
            </otherwise>
        </choose>
    </select>

    <insert id="insert" parameterType="com.rental.entity.House" useGeneratedKeys="true" keyProperty="id">
        insert into house
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="landlordId != null">landlord_id,</if>
            <if test="title != null">title,</if>
            <if test="price != null">price,</if>
            <if test="area != null">area,</if>
            <if test="houseType != null">house_type,</if>
            <if test="floor != null">floor,</if>
            <if test="orientation != null">orientation,</if>
            <if test="decoration != null">decoration,</if>
            <if test="community != null">community,</if>
            <if test="address != null">address,</if>
            <if test="longitude != null">longitude,</if>
            <if test="latitude != null">latitude,</if>
            <if test="contact != null">contact,</if>
            <if test="contactPhone != null">contact_phone,</if>
            <if test="status != null">status,</if>
            create_time,
            update_time,
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="landlordId != null">#{landlordId},</if>
            <if test="title != null">#{title},</if>
            <if test="price != null">#{price},</if>
            <if test="area != null">#{area},</if>
            <if test="houseType != null">#{houseType},</if>
            <if test="floor != null">#{floor},</if>
            <if test="orientation != null">#{orientation},</if>
            <if test="decoration != null">#{decoration},</if>
            <if test="community != null">#{community},</if>
            <if test="address != null">#{address},</if>
            <if test="longitude != null">#{longitude},</if>
            <if test="latitude != null">#{latitude},</if>
            <if test="contact != null">#{contact},</if>
            <if test="contactPhone != null">#{contactPhone},</if>
            <if test="status != null">#{status},</if>
            now(),
            now(),
        </trim>
    </insert>

    <update id="update" parameterType="com.rental.entity.House">
        update house
        <set>
            <if test="title != null">title = #{title},</if>
            <if test="price != null">price = #{price},</if>
            <if test="area != null">area = #{area},</if>
            <if test="houseType != null">house_type = #{houseType},</if>
            <if test="floor != null">floor = #{floor},</if>
            <if test="orientation != null">orientation = #{orientation},</if>
            <if test="decoration != null">decoration = #{decoration},</if>
            <if test="community != null">community = #{community},</if>
            <if test="address != null">address = #{address},</if>
            <if test="longitude != null">longitude = #{longitude},</if>
            <if test="latitude != null">latitude = #{latitude},</if>
            <if test="contact != null">contact = #{contact},</if>
            <if test="contactPhone != null">contact_phone = #{contactPhone},</if>
            <if test="status != null">status = #{status},</if>
            update_time = now(),
        </set>
        where id = #{id}
    </update>

    <update id="updateStatus">
        update house
        set status = #{status}, update_time = now()
        where id = #{id}
    </update>

    <delete id="deleteById" parameterType="java.lang.Long">
        delete from house
        where id = #{id}
    </delete>

    <delete id="deleteBatchByIds" parameterType="java.lang.Long">
        delete from house
        where id in
        <foreach collection="array" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
</mapper>


File: d:\htsun\Docs\资料\文章\rental-system\backend\src\main\resources\sql\appointment_module.sql

sql
CopyInsert
-- 预约看房表
CREATE TABLE appointment (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '预约ID',
    house_id BIGINT NOT NULL COMMENT '房源ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    landlord_id BIGINT NOT NULL COMMENT '房东ID',
    appointment_time DATETIME NOT NULL COMMENT '预约时间',
    contact_name VARCHAR(50) NOT NULL COMMENT '联系人姓名',
    contact_phone VARCHAR(20) NOT NULL COMMENT '联系电话',
    appointment_notes VARCHAR(500) COMMENT '预约备注',
    status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待确认,1-已确认,2-已取消,3-已完成',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    INDEX idx_house_id (house_id),
    INDEX idx_user_id (user_id),
    INDEX idx_landlord_id (landlord_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约看房表';

-- 看房反馈表
CREATE TABLE viewing_feedback (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '反馈ID',
    appointment_id BIGINT NOT NULL COMMENT '预约ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    house_id BIGINT NOT NULL COMMENT '房源ID',
    feedback_content TEXT NOT NULL COMMENT '反馈内容',
    satisfaction_level TINYINT NOT NULL COMMENT '满意度:1-5星',
    is_public TINYINT NOT NULL DEFAULT 0 COMMENT '是否公开:0-私密,1-公开',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    INDEX idx_appointment_id (appointment_id),
    INDEX idx_user_id (user_id),
    INDEX idx_house_id (house_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='看房反馈表';

-- 房源评价表
CREATE TABLE house_review (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '评价ID',
    house_id BIGINT NOT NULL COMMENT '房源ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    content TEXT NOT NULL COMMENT '评价内容',
    rating TINYINT NOT NULL COMMENT '评分:1-5星',
    location_rating TINYINT NOT NULL COMMENT '位置评分:1-5星',
    cleanliness_rating TINYINT NOT NULL COMMENT '清洁度评分:1-5星',
    value_rating TINYINT NOT NULL COMMENT '性价比评分:1-5星',
    landlord_rating TINYINT NOT NULL COMMENT '房东评分:1-5星',
    images VARCHAR(1000) COMMENT '评价图片,多个图片用逗号分隔',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    INDEX idx_house_id (house_id),
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天进步2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值