代码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),