选择Python作为后端语言主要基于其快速开发特性和丰富的生态库。Django和Flask框架的选择取决于项目规模:
我们最终采用Flask框架,因其轻量级特性更符合租赁系统的快速迭代需求。数据库选用PostgreSQL,因其对地理空间数据和复杂查询的支持更好(未来可能扩展GPS功能)。
前端选择Vue3+TypeScript的组合主要考虑:
采用经典的三层架构:
code复制客户端层 -> API网关层 -> 业务逻辑层 -> 数据访问层
特别设计了租赁状态机来管理渔具生命周期:
python复制# 状态机核心逻辑示例
from transitions import Machine
class FishingGear:
states = ['available', 'reserved', 'rented', 'maintenance']
def __init__(self):
self.machine = Machine(model=self, states=FishingGear.states, initial='available')
self.machine.add_transition('reserve', 'available', 'reserved')
self.machine.add_transition('cancel', 'reserved', 'available')
self.machine.add_transition('rent', 'reserved', 'rented')
self.machine.add_transition('return', 'rented', 'available')
self.machine.add_transition('report', '*', 'maintenance')
重要提示:状态转换必须保证原子性,建议配合数据库事务使用
python复制from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class FishingGear(db.Model):
__tablename__ = 'fishing_gear'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
category = db.Column(db.String(50), index=True) # 竿/轮/线等分类
specs = db.Column(db.JSON) # 存储规格参数
daily_price = db.Column(db.Numeric(10,2))
deposit = db.Column(db.Numeric(10,2))
images = db.Column(db.ARRAY(db.String)) # 图片URL数组
status = db.Column(db.String(20), default='available')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
使用Flask-Reuploaded处理图片上传,关键配置:
python复制from flask_uploads import UploadSet, configure_uploads, IMAGES
photos = UploadSet('photos', IMAGES)
app.config['UPLOADED_PHOTOS_DEST'] = 'static/uploads'
configure_uploads(app, photos)
# 上传接口
@app.route('/upload', methods=['POST'])
def upload():
if 'photo' in request.files:
filename = photos.save(request.files['photo'])
return jsonify(url=url_for('static', filename=f'uploads/{filename}'))
return jsonify(error='No file uploaded'), 400
设计订单状态与渔具状态的联动机制:
python复制@app.route('/api/orders', methods=['POST'])
def create_order():
data = request.get_json()
try:
with db.session.begin_nested():
gear = FishingGear.query.get(data['gear_id'])
if gear.status != 'available':
raise ValueError('Gear not available')
order = Order(
user_id=data['user_id'],
gear_id=data['gear_id'],
start_date=data['start_date'],
days=data['days'],
total_price=gear.daily_price * data['days'],
status='pending_payment'
)
gear.status = 'reserved'
db.session.add(order)
db.session.commit()
return jsonify(order.to_dict())
except Exception as e:
db.session.rollback()
return jsonify(error=str(e)), 400
对接支付宝沙箱环境示例:
python复制from alipay import AliPay
alipay = AliPay(
appid="your_appid",
app_notify_url=None,
app_private_key_string=app_private_key,
alipay_public_key_string=alipay_public_key,
sign_type="RSA2",
debug=True
)
@app.route('/api/pay', methods=['POST'])
def pay():
order_id = request.json.get('order_id')
order = Order.query.get(order_id)
if not order:
return jsonify(error="Order not found"), 404
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=order.id,
total_amount=str(order.total_price),
subject=f"渔具租赁-{order.gear.name}",
return_url="https://yourdomain.com/pay/success",
notify_url="https://yourdomain.com/pay/notify"
)
return jsonify(pay_url=f"https://openapi.alipaydev.com/gateway.do?{order_string}")
渔具列表组件实现:
vue复制<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
const gearList = ref([])
const loading = ref(false)
const pagination = ref({
page: 1,
pageSize: 10,
total: 0
})
const fetchGears = async () => {
try {
loading.value = true
const res = await axios.get('/api/gears', {
params: {
page: pagination.value.page,
page_size: pagination.value.pageSize
}
})
gearList.value = res.data.items
pagination.value.total = res.data.total
} catch (error) {
ElMessage.error(error.message)
} finally {
loading.value = false
}
}
onMounted(fetchGears)
</script>
<template>
<el-table :data="gearList" v-loading="loading">
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="daily_price" label="日租金" width="100">
<template #default="{row}">
¥{{ row.daily_price.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{row}">
<el-tag :type="{
available: 'success',
reserved: 'warning',
rented: 'danger'
}[row.status]">
{{ {
available: '可租',
reserved: '已预订',
rented: '已租出'
}[row.status] }}
</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
@current-change="fetchGears"
@size-change="fetchGears"
/>
</template>
使用Pinia管理全局状态:
typescript复制// stores/rental.ts
import { defineStore } from 'pinia'
export const useRentalStore = defineStore('rental', {
state: () => ({
cart: [] as Array<{
gear_id: number
name: string
days: number
price: number
}>,
currentOrder: null as Order | null
}),
getters: {
totalPrice: (state) => state.cart.reduce((sum, item) => sum + item.price * item.days, 0)
},
actions: {
async checkout() {
const res = await axios.post('/api/orders', {
items: this.cart
})
this.currentOrder = res.data
this.cart = []
}
}
})
Docker-compose配置示例:
yaml复制version: '3.8'
services:
backend:
build: ./backend
ports:
- "5000:5000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/fishing_rental
depends_on:
- db
volumes:
- ./backend:/app
frontend:
build: ./frontend
ports:
- "8080:8080"
volumes:
- ./frontend:/app
db:
image: postgres:13
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=fishing_rental
volumes:
- pgdata:/var/lib/postgresql/data
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- backend
- frontend
volumes:
pgdata:
python复制# 使用joinedload避免N+1查询问题
from sqlalchemy.orm import joinedload
gears = db.session.query(FishingGear).options(
joinedload(FishingGear.category),
joinedload(FishingGear.images)
).filter(
FishingGear.status == 'available'
).all()
vue复制<template>
<img
v-for="img in gear.images"
:key="img.id"
:src="img.thumbnail"
:data-src="img.fullsize"
v-lazy-load
/>
</template>
<script setup>
// 自定义懒加载指令
const vLazyLoad = {
mounted(el) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = el.dataset.src
observer.unobserve(el)
}
})
})
observer.observe(el)
}
}
</script>
基于物品协同过滤的推荐算法实现:
python复制from collections import defaultdict
import math
class ItemCF:
def __init__(self):
self.item_sim_matrix = defaultdict(dict)
def train(self, orders):
# 构建用户-物品倒排表
user_items = defaultdict(set)
for order in orders:
user_items[order.user_id].add(order.gear_id)
# 计算物品共现矩阵
item_cooccur = defaultdict(int)
item_popular = defaultdict(int)
for items in user_items.values():
for i in items:
item_popular[i] += 1
for j in items:
if i == j:
continue
item_cooccur[(i,j)] += 1
# 计算相似度矩阵
for (i,j), count in item_cooccur.items():
self.item_sim_matrix[i][j] = count / math.sqrt(item_popular[i] * item_popular[j])
def recommend(self, user_id, user_items, top_n=5):
rank = defaultdict(float)
for item in user_items[user_id]:
for related, sim in self.item_sim_matrix.get(item, {}).items():
if related in user_items[user_id]:
continue
rank[related] += sim
return sorted(rank.items(), key=lambda x: -x[1])[:top_n]
基于租赁行为的信用评分模型:
python复制class CreditSystem:
BASE_SCORE = 600
@classmethod
def calculate(cls, user):
# 按时归还加分
on_time_returns = RentalRecord.query.filter_by(
user_id=user.id,
status='returned_on_time'
).count()
# 延期归还扣分
late_returns = RentalRecord.query.filter_by(
user_id=user.id,
status='returned_late'
).count()
# 损坏赔偿扣分
damages = DamageRecord.query.filter_by(
user_id=user.id,
paid=True
).count()
score = cls.BASE_SCORE
score += on_time_returns * 10
score -= late_returns * 20
score -= damages * 50
# 确保分数在合理范围
return max(300, min(900, score))
在开发过程中积累的几个关键经验:
python复制@transaction.atomic
def reserve_gear(gear_id, user_id):
gear = FishingGear.objects.select_for_update().get(pk=gear_id)
if gear.status != 'available':
raise ValueError('Gear not available')
gear.status = 'reserved'
gear.save()
Order.objects.create(
user_id=user_id,
gear_id=gear_id,
status='pending_payment'
)
vue复制<template>
<RecycleScroller
class="scroller"
:items="gearList"
:item-size="200"
key-field="id"
>
<template #default="{ item }">
<GearCard :gear="item" />
</template>
</RecycleScroller>
</template>
python复制@app.errorhandler(Exception)
def handle_exception(e):
if isinstance(e, APIError):
return jsonify({
'error': e.message,
'code': e.code
}), e.status_code
# 未知错误处理
app.logger.exception(e)
return jsonify({
'error': 'Internal server error',
'code': 'INTERNAL_ERROR'
}), 500