基于 FastAPI 的房源租赁系统设计与实现
項目背景
傳統的線下租房不便、途徑少、信息更新慢,導致房屋租賃效率低。為了有效的提升租賃效率和房源信息管理、提供更優質的租賃服務。讓房東出租宣傳展示與房源管理、租客更好的檢索房源信息、發布租房需求以及入住預定、后臺房源管理、審核等一站式租賃服務平臺。
- 租客:瀏覽房源、收藏房源、預定房源、發布租房需求、查看電子合同。
- 房東:發布房源、訂單管理、查看電子合同。
- 管家:查看房源信息、回復咨詢、線下帶看房源。
- 管理員:用戶管理、房源管理、訂單管理、租房需求、實名認證、系統公告管理。
TODO
- 房源全文檢索
- 租房需求支持評論
- 日租、合租模式
- 房源推薦系統(Go開發)
項目特色
- 采用了七牛云OSS、CDN服務加速一些圖片資源。
- 采用 FastAPI 的后臺任務實現異步發送短信驗證碼。
- 采用 tortoise-orm 完成數據庫操作的封裝。
- 通過模板字符串動態渲染富文本實現電子合同功能。
- 對接阿里支付實現了訂單、支付模塊,對接百度地圖實現當前城市定位、房源附近信息查詢等功能。
- 前端界面采用 Vue.js + Element ui 實現數據渲染,Bootstrap 實現自適應布局。
項目體驗
項目體驗地址 http://43.138.220.206:9999/huihome 由于注冊需要發送短信驗證碼,而手機驗證碼服務現在只能給我的測試手機號發送驗證碼,因此不能使用注冊服務。大家可以使用已有賬號去登錄體驗。
| 用戶賬號 | hui | 123456 | 租客賬號 |
| 用戶賬號 | wang | 123456 | 房東賬號 |
項目源碼:HuiDBK/HuiHome: 基于FastAPI的房屋租賃系統 (github.com)
項目還沒有太完善,服務器也只是學習級別的,可能會出現很多異常,望大家多擔待。
項目啟動
?
項目部署
系統整體功能圖
?
項目結構
項目開發整體采用的是Python的FastAPI框架來搭建系統的接口服務,接口設計遵循?Restful API接口規范。接口前后端交互都采用json格式進行數據交互,項目整體的結構如下:
?
項目Redis緩存設計
Redis key 規范:
project : module : business : unique key項目名 : 模塊名 : 業務 : 唯一區別key 例如:用戶手機短信驗證碼緩存 house_rental:user:sms_code:13022331752 復制代碼用戶模塊緩存
房源模塊緩存
?其他緩存
?系統整體ER圖
?房屋屬性太多故在整體ER圖省略
實際表屬性更多進行了垂直分表。
代碼細節
實名認證裝飾器
def real_auth_required(func):""" 實名認證裝飾器 """@wraps(func)async def warp(*args, **kwargs):"""通過請求上下文的user對象來判斷用戶有沒有實名認證"""cur_request = context_util.REQUEST_CONTEXT.get()user = cur_request.user or Noneif not user:raise AuthorizationException()if user.role == UserRole.admin.value:# 管理員不需要實名認證return await func(*args, **kwargs)# 此時不同直接通過 user.auth_status 來驗證# 應該通過 user_id 去數據庫中查詢最新的狀態user_profile = await UserProfileManager.get_by_id(user.id)if user_profile.auth_status != UserAuthStatus.authorized.value:raise BusinessException().exc_data(ErrorCodeEnum.REALNAME_AUTH_ERR)return await func(*args, **kwargs)return warp分頁數據封裝裝飾器
from pydantic import BaseModel, Fieldclass ResponseBaseModel(BaseModel):""" 統一響應模型 """code: intmessage: strdata: dictclass ListResponseDataModel(BaseModel):""" 分頁列表響應data模型 """total: int = Field(default=0, description="數據總數量")data_list: list = Field(default=[], description='數據列表')has_more: bool = Field(default=False, description="是否有下一頁")next_offset: int = Field(default=0, description="offset下次起步")def list_page(func):""" 分頁數據封裝裝飾器 """@wraps(func)async def warp(*args, **kwargs):"""尋找函數參數 ListPageRequestModel 的實例 有獲取 limit、offset所有分頁請求入參都繼承 ListPageRequestModel"""limit, offset = None, None# 位置參數中尋找for arg in args:if limit is not None and offset is not None:breakif isinstance(arg, ListPageRequestModel):limit, offset = arg.limit, arg.offset# 關鍵字參數中尋找for key, value in kwargs.items():if limit is not None and offset is not None:breakif isinstance(value, ListPageRequestModel):# 關鍵字參數值是否是 ListPageRequestModellimit, offset = value.limit, value.offsetelif key == 'limit':# 也支持關鍵參數 key 為 limit 和 offset的情況limit = valueelif key == 'offset':offset = valueif limit is None or offset is None:# 沒有成功賦值, 則不支持logger.debug('不支持分頁數據封裝')# 執行函數獲取分頁響應的數據, 有兩種情況# 1 返回使用了pydantic model ListResponseDataModel (盡量使用這種來返回業務數據)# 2 返回 total data_list (元組)data_obj = await func(*args, **kwargs)# 分頁數據返回的參數都必須遵守 ListResponseDataModelif isinstance(data_obj, ListResponseDataModel):# ListResponseDataModel 處理data_obj.next_offset = offset + limitdata_obj.has_more = False if data_obj.next_offset > data_obj.total else Trueelif isinstance(data_obj, tuple):# 元組 處理total = data_obj[0] if isinstance(data_obj[0], int) else data_obj[1]data_list = data_obj[1] if isinstance(data_obj[1], list) else data_obj[0]data_obj = ListResponseDataModel(total=total,data_list=data_list,next_offset=offset + limit,has_more=False if offset + limit > total else True)list_page_resp = data_objreturn list_page_respreturn warpjson數據緩存裝飾器
def cache_json(cache_info=None, key=None, timeout=60):"""緩存裝飾器 (適合緩存字符串json數據):param key: 緩存的key:param timeout: 緩存的時間 默認60秒:param cache_info: 封裝好的緩存信息對象 RedisCacheInfo:return:"""if cache_info:# 有封裝的緩存對象key = cache_info.keytimeout = cache_info.timeoutdef cache_decorator(api_func):@wraps(api_func)async def warp(*args, **kwargs):# 1、沒有設置key則根據接口函數的信息和系統密鑰自動生成(盡量設置key)nonlocal keyif not key:# 應用名:函數所在模塊:函數名:函數位置參數:函數關鍵字參數:系統密鑰 進行hashparam_args_str = ','.join([str(arg) for arg in args])param_kwargs_str = ','.join(sorted([f'{k}:{v}' for k, v in kwargs.items()]))hash_str = f'{constants.APP_NAME}:{api_func.__module__}:{api_func.__name__}:' \f'{param_args_str}:{param_kwargs_str}:{settings.SECRET}'has_result = hashlib.md5(hash_str.encode()).hexdigest()# 根據哈希結果生成keykey = f'{constants.APP_NAME}:{api_func.__module__}:{api_func.__name__}:{has_result}'# 2、先查看是否有緩存from house_rental.commons.utils.redis_util import RedisUtilredis_client = await RedisUtil().get_redis_conn()cache_data = await redis_client.get(key)if cache_data:return json.loads(cache_data)# 3、執行接口函數獲取結果api_result = await api_func(*args, **kwargs)# 4、設置緩存if isinstance(api_result, BaseModel):# 結果是pydantic的模型對象處理api_result_json = api_result.json()elif isinstance(api_result, dict):# 字典api_result_json = json.dumps(api_result)else:# 其他可以json序列化的api_result_json = json.dumps(api_result)await redis_client.setex(key, timeout, api_result_json)return api_resultreturn warpreturn cache_decorator項目接口依賴(Depends)
async def jwt_authentication(request: Request):""" jwt 鑒權"""# for api_url in settings.API_URL_WHITE_LIST:# # 在白名單的接口無需token驗證# if str(request.url.path).startswith(api_url):# returntoken = request.headers.get('Authorization') or Noneif not token:raise AuthorizationException()# Bearer 占了7位if not str(token).startswith('Bearer '):raise AuthorizationException()token = str(token)[7:]user_info = jwt_util.verify_jwt(token)if not user_info:# 無效tokenraise AuthorizationException()# 校驗通過保存到request.user中user_id = user_info.get('user_id')user = await UserBasicManager.get_by_id(user_id)if user.role != UserRole.admin.value and str(request.url.path).startswith('/api/v1/admin'):# 不是管理員無法訪問了后臺模塊接口raise AuthorizationException()request.scope['user'] = userasync def request_context(request: Request):""" 保存當前request對象到上下文中 """context_util.REQUEST_CONTEXT.set(request)async def login_required(request: Request):""" 登錄權限校驗 """try:user = request.userexcept:raise AuthorizationException().exc_data(ErrorCodeEnum.AUTHORIZATION_ERR)if not user:raise AuthorizationException().exc_data(ErrorCodeEnum.AUTHORIZATION_ERR)響應序列化遞歸工具函數
def obj2DataModel(data_obj: Union[Dict,Type[BaseOrmModel],List[Dict],List[BaseOrmModel]],data_model: Type[BaseModel] ) -> Union[BaseModel, List[BaseModel], None]:"""將數據對象轉換成 pydantic的響應模型對象, 如果是數據庫模型對象則調用to_dict()后遞歸:param data_obj: 支持 字典對象, 數據庫模型對象, 列表對象:param data_model: 轉換后數據模型:return:"""if isinstance(data_obj, dict):# 字典處理return data_model(**data_obj)elif isinstance(data_obj, BaseOrmModel):# 數據模型對象處理, to_dict()后遞歸調用return obj2DataModel(data_obj.to_dict(), data_model=data_model)elif isinstance(data_obj, list):# 列表處理return [obj2DataModel(item, data_model=data_model) for item in data_obj]else:logger.debug(f'不支持此{data_obj}類型的轉換')return入參、出參代碼樣式
未調整前:
?調整后:
?
?
不要為了單獨一個而全部調整,只要達到了美觀就行無需全部對齊,重要是關注每個入參和出參有沒有關聯等,一些必傳啊,枚舉參數等,相關聯的放到一起,這樣的代碼才更賞心悅目。
房源設施雙色圖標展示
首先我可以獲取所有房源設施的信息,接口返回當前房源有的房源信息,只要判斷不在總房源設施列表里的就顯示 灰色圖標、文字下劃線 在則顯示不同的顏色(數據庫只存了灰色圖標)
1、通過 filter 函數濾鏡函數實現圖標不同顏色的陰影,然后原圖標偏移圖標寬度然后隱藏,就只剩下帶顏色的圖標陰影(本項目所采用的方案)
.facility_no {filter: grayscale(100%);-webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-o-filter: grayscale(100%);text-decoration: line-through; }.facility_yes {filter: drop-shadow(46px 0px 0px #fd5332);backdrop-filter: blur(0px); }.facility_text {width: 46px;text-align: center; }.facility_hidden {width: 46px;height: 46px;text-indent: -46px;overflow: hidden; }<li v-for="item in all_house_facility"><div class="facility_no"v-if="house_facility_ids.indexOf(item.facility_id) == -1"><div><img :src="item.icon" :title="item.name" width="46" :alt="item.name"height="46"></div><p class="facility_text">{{ item.name }}</p></div><div v-else><div class="facility_hidden"><img :src="item.icon" :title="item.name" class="facility_yes"width="46"height="46"></div><p class="facility_text">{{ item.name }}</p></div> </li>2、數據庫存存儲兩張不同顏色的圖標
3、數據庫還是存儲一張圖標但一張圖標包含兩種圖標,前端通過切圖來分割圖標?background-image?屬性搭配
background-positon:x軸起點 y軸起點; background-size:背景圖片的大小; width:終點x軸位置; height:終點y軸位置;支付狀態章印顯示
通過?position?屬性實現子絕父相定位章印元素,border-radius?控制邊框圓角
?
.seal{width: 115px;height: 115px;border: solid 5px #B4B4B4;border-radius: 100%;background-color: rgba(255, 255, 255, 0.8);position: relative;display: flex;justify-content: center;align-items: center; } .seal-son{width: 110px;height: 110px;border: solid 2px #B4B4B4;border-radius: 100%;background-color: rgba(255, 255, 255, 0.8);position: relative; }.seal-lg-text{position: absolute;top: 32px;text-align: center;font-size: 18px;transform: rotate(-45deg);right: 40px;color: #B4B4B4;font-weight: 900; } .seal-sm-text{position: absolute;top: 66px;text-align: center;font-size: 10px;transform: rotate(-45deg);left: 40px;color: #B4B4B4; }實際使用如果位置不夠好,通過style重寫覆蓋css屬性調整,有點像類繼承一樣
<div class="seal" style="position: absolute;right: -12px;top: 45px;"><div class="seal-son"><span class="seal-lg-text"><span v-if="order_detail_item.state === order_state_enum.payed">已支付</span><span v-else-if="order_detail_item.state === order_state_enum.ordered">已預訂</span><span v-else-if="order_detail_item.state === order_state_enum.no_pay">未支付</span><span v-else-if="order_detail_item.state === order_state_enum.finished">已完成</span><span v-else-if="order_detail_item.state === order_state_enum.canceled">已取消</span></span><span class="seal-sm-text">{{ order_detail_item.update_ts }}</span></div> </div>元素大小縮放提高交互
.el_scale:hover {transform: scale(1.03) }項目界面展示
首頁
?
?登錄注冊
?房源列表
?房源收藏
房源詳情
??
房源地圖服務
房源訂單
電子合同
求租管理
系統公告
房東房源管理
房東發布房源
總結
以上是生活随笔為你收集整理的基于 FastAPI 的房源租赁系统设计与实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 急需能临时发邮件的临时邮箱 临时邮箱怎么
- 下一篇: 没有提取码怎么获取百度网盘资源?