九. 鱼漂业务与Drift模型
向赠书人请求书籍的逻辑:点击赠书人
初步涉及drift模型:
新建app/models/drift.py
from .base import Base
from sqlalchemy import Column, Integer, String
class Drift(Base):
'''
一次具体的交易信息
'''
id = Column(Integer, primary_key=True)
# 邮寄信息
recipient_name = Column(String(20), nullable=False)
address = Column(String(100), nullable=False)
message = Column(String(200))
mobile = Column(String(20), nullable=False)
# 书籍信息
isbn = Column(String(13))
book_title = Column(String(50))
book_author = Column(String(30))
book_img = Column(String(50))
# 请求者信息
request_id = Column(Integer)
requester_nickname = Column(String(20))
# 赠送者信息
gifter_id = Column(Integer)
gift_id = Column(Integer)
gifter_nickname = Column(String(20))
问题: 请求者信息、书籍信息、赠送者信息都不是与对应模型做关联, 而是平铺在Drift这个模型中。这样做的理由是什么?
对于记录性质的场景, 平铺的直接记录(无关联)比较好。以淘宝购物为例:
有关联: 昨天买的商品, 今天商品价格改变了。 我们购买记录中的历史购买价格也随之改变。
无关联: 购买记录中的历史购买价格不变。
设计数据库时, 是否采用关联,是非常值得考虑的事。
十. 鱼漂检测能否发起交易
在app/models/drift.py的Drift模型中增加pending
from .base import Base
from sqlalchemy import Column, Integer, String, SmallInteger
class Drift(Base):
'''
一次具体的交易信息
'''
id = Column(Integer, primary_key=True)
# 邮寄信息
recipient_name = Column(String(20), nullable=False)
address = Column(String(100), nullable=False)
message = Column(String(200))
mobile = Column(String(20), nullable=False)
# 书籍信息
isbn = Column(String(13))
book_title = Column(String(50))
book_author = Column(String(30))
book_img = Column(String(50))
# 请求者信息
request_id = Column(Integer)
requester_nickname = Column(String(20))
# 赠送者信息
gifter_id = Column(Integer)
gift_id = Column(Integer)
gifter_nickname = Column(String(20))
pending = Column('pending', SmallInteger, default=1) # 状态
对于状态, 最好使用枚举类型,而不是数字。新建app/libs/enums.py:
from enum import Enum
class PendingStatus(Enum):
'''
交易4个状态
'''
Waiting = 1
Success = 2
Reject = 3
Redraw = 4
完善app/web/drift.py中send_drift视图函数的代码:
@web.route('/drift/<int:gid>', methods=['GET', 'POST'])
@login_required
def send_drift(gid):
current_gift = Gift.query.get_or_404(gid)
if current_gift.is_yourself_gift(current_user.id):
flash('这本书是你自己的,不能向自己索要书籍')
return redirect('web.book_detail', isbn=current_gift.isbn)
can = current_user.can_send_drift() # 当前用户是否可以发起鱼漂
if not can:
return render_template('not_enough_beans.html', beans=current_user.beans)
gifter = current_gift.user.summary
# 得到适配页面的信息, 也可以写到view_model中, 但summary有具体意义且可能有较高的使用频率, 所以在模型中书写好一些
return render_template('drift.html', gifter=gifter, user_beans=current_user.beans)
在Gift模型中增加is_yourself_gift方法:
def is_yourself_gift(self, uid):
return True if self.uid == uid else False
在User模型中增加can_send_drift方法以及summary摘要:
def can_send_drift(self):
if self.beans < 1:
return False
success_send_count = Gift.query.filter_by(uid=self.id, launched=True).count()
success_receive_count = Drift.query.filter_by(request_id=self.id, pending=PendingStatus.Success).count()
return True if floor(success_receive_count/2) <= floor(success_send_count) else False # /2是因为 每获取2本, 必须送出1本
@property
def summary(self):
return dict(
nickname=self.nickname,
beans=self.beans,
email=self.email,
send_receive=str(self.send_counter)+'/'+str(self.receive_counter)
)
十一. 完成鱼漂业务逻辑
完善send_drift视图函数:
@web.route('/drift/<int:gid>', methods=['GET', 'POST'])
@login_required
def send_drift(gid):
current_gift = Gift.query.get_or_404(gid)
if current_gift.is_yourself_gift(current_user.id):
flash('这本书是你自己的,不能向自己索要书籍')
return redirect('web.book_detail', isbn=current_gift.isbn)
can = current_user.can_send_drift() # 当前用户是否可以发起鱼漂
if not can:
return render_template('not_enough_beans.html', beans=current_user.beans)
form = DriftForm(request.form)
if request.method == 'POST' and form.validate():
save_drift(form, current_gift)
send_email(current_gift.user.email, '有人想要一本书', 'email/get_gift.html', wisher=current_user, gift=current_gift) # 发邮件通知
gifter = current_gift.user.summary
return render_template('drift.html', gifter=gifter, user_beans=current_user.beans, form=form)
···
# 一系列其他视图函数之后
def save_drift(drift_form, current_gift):
with db.auto_commit():
drift = Drift()
drift_form.populate_obj(drift) # form数据传递给模型
drift.gift_id = current_gift.id
drift.request_id = current_user.id
drift.requester_nickname = current_user.nickname
drift.gifter_nickname = current_gift.nickname
drift.grifter_id = current_gift.user.id
book = BookViewModel(current_gift.book)
drift.book_title = book.title
drift.book_author = book.author
drift.book_img = book.image
drift.isbn = book.isbn
current_user.beans -= 1
db.session.add(drift)
十二. 交易记录页面
新建app/web/drift.py pending视图函数:
@web.route('/pending')
@login_required
def pending():
# 这样写的话 只能and的不关系, 我们想要or关系:
drifts = Drift.query.filter_by(request_id=current_user.id,
gifter_id=current_user.id).order_by(desc(Drift.create_time)).all()
这样写的话 只能and的不关系, 我们想要or关系:
from sqlalchemy import desc, or_
@web.route('/pending')
@login_required
def pending():
drifts = Drift.query.filter(or_(Drift.request_id==current_user.id,
Drift.gifter_id==current_user.id)).order_by(desc(Drift.create_time)).all()
# 用filter代替filter_by, 并使用or_
十三. Drift ViewModel
在交易记录页面, 因为 是索取的书籍还是赠送的书籍 的不同, 显示的信息也不同。提高了ViewModel的编写难度:
西游记: 我索取的书籍
我的第一本编程书: 我赠送的书籍
新建app/view_models/drift.py:
class DriftViewModel:
def __init__(self, drift, current_user_id):
self.data = {} # 代表所有属性
@staticmethod
def request_or_gifter(drift, current_user_id):
# 不建议DriftViewModel中导入current_user, 使DriftViewModel永远离不开current_user
if drift.requester_id == current_user_id:
you_are = 'requester'
else:
you_are = 'gifter'
return you_are
def __parse(self, drift, current_user_id):
you_are = self.requester_or_gifter(drift, current_user_id)
r = {
'you_are': you_are,
'operator': drift.requester_nickname if you_are != 'requester' else drift.gifter_nickname,
'drift_id': drift.id,
'book_title': drift.book_title,
'book_author': drift.book_author,
'book_img': drift.book_img,
'date': drift.create_datetime.striftime('%Y-%m-%d'),
'message': drift.mesage,
'address': drift.address,
'recipient_name': drift.recipient_name,
'mobile': drift.mobile,
'status': drift.pending
}
状态的信息没有编写, 我们在app/libs/enums.py中新增类方法pending_str:
from enum import Enum
class PendingStatus(Enum):
'''
交易4个状态
'''
Waiting = 1
Success = 2
Reject = 3
Redraw = 4
@classmethod
def pending_str(cls, status, key):
key_map = {
1: {
'requester': '等待对方邮寄',
'gifter': '等待你邮寄'
},
2: {
'requester': '对方已邮寄',
'gifter': '你已邮寄,交易完成'
},
3: {
'requester': '对方已拒绝',
'gifter': '你已拒绝'
},
4: {
'requester': '你已撤销',
'gifter': '对方已撤销'
},
}
return key_map[status][key]
完善DriftViewModel:
from app.libs.enums import PendingStatus
class DriftViewModel:
def __init__(self, drift, current_user_id):
self.data = {}
self.data = self.__parse(drift, current_user_id) # 调用__parse
@staticmethod
def request_or_gifter(drift, current_user_id):
# 不建议DriftViewModel中导入current_user, 使DriftViewModel永远离不开current_user
if drift.requester_id == current_user_id:
you_are = 'requester'
else:
you_are = 'gifter'
return you_are
def __parse(self, drift, current_user_id):
you_are = self.requester_or_gifter(drift, current_user_id)
pending_status = PendingStatus.pending_str(drift.pending, you_are) # 使用enum.py的pending_str
r = {
'you_are': you_are,
'operator': drift.requester_nickname if you_are != 'requester' else drift.gifter_nickname,
'status_str': pending_status, # 状态
'drift_id': drift.id,
'book_title': drift.book_title,
'book_author': drift.book_author,
'book_img': drift.book_img,
'date': drift.create_datetime.striftime('%Y-%m-%d'),
'message': drift.mesage,
'address': drift.address,
'recipient_name': drift.recipient_name,
'mobile': drift.mobile,
'status': drift.pending
}
return r
单个的view_model完成, 现在编写DriftCollection:
class DriftCollection:
def __init__(self, drifts, current_user_id):
self.data = []
self.__parse(drifts, current_user_id)
def __parse(self, drifts, current_user_id):
for drift in drifts:
temp = DriftViewModel(drift, current_user_id)
self.data.append(temp.data)
在视图函数pending中调用:
@web.route('/pending')
@login_required
def pending():
drifts = Drift.query.filter(or_(Drift.request_id == current_user.id,
Drift.gifter_id == current_user.id)).order_by(
desc(Drift.create_time)).all()
views = DriftCollection(drifts, current_user.id)
return render_template('pending.html', drifts=views.data)
十四. 三种view_model的总结
对比book.py、dirft.py、gift.py中的view_model:
基本都遵循先处理单个的代码,再处理集合的代码的编写顺序。
最推荐book.py的写法, 最不推荐gift.py的写法。book.py的写法方便拓展,容易读懂。
十五. 更好的使用枚举
实现在交易记录页面的撤销功能
原理: 将status修改为4
我们编写app/web/drift.py的redraw_drift视图函数:
web.route('/drift/<int:did>/redraw')
@login_required
def redraw_drift(did):
"""
撤销请求,只有书籍请求者才可以撤销请求
"""
with db.auto_commit():
drift = Drift.query.filter_by(id=did).first_or_404()
drift.pending = PendingStatus.Redraw
current_user.beans += 1
return redirect(url_for('web.pending'))
但是运行调试的时候发现了大问题 drift.pending = PendingStatus.Redraw
drift.pending获得的值是0, 正确的应该是4。枚举没有生效。
解决方案:
1.代码改为: drift.pending = PendingStatus.Redraw.value
2.代码改为:drift.pending = 4
但这两种方案都不是很好, 没有达到想要的枚举效果。
修改app/models/drift.py:
from .base import Base
from sqlalchemy import Column, Integer, String, SmallInteger
from app.libs.enums import PendingStatus
class Drift(Base):
'''
一次具体的交易信息
'''
id = Column(Integer, primary_key=True)
# 邮寄信息
recipient_name = Column(String(20), nullable=False)
address = Column(String(100), nullable=False)
message = Column(String(200))
mobile = Column(String(20), nullable=False)
# 书籍信息
isbn = Column(String(13))
book_title = Column(String(50))
book_author = Column(String(30))
book_img = Column(String(50))
# 请求者信息
request_id = Column(Integer)
requester_nickname = Column(String(20))
# 赠送者信息
gifter_id = Column(Integer)
gift_id = Column(Integer)
gifter_nickname = Column(String(20))
_pending = Column('pending', SmallInteger, default=1) # 状态
@property
def pending(self):
'''
读取_pending,转化为枚举类型
:return:
'''
return PendingStatus(self._pending)
@pending.setter
def pending(self, status):
'''
把枚举转化为_pending
:param status:
:return:
'''
self._pending = status.value
pending函数让枚举变得优雅了。
在enum.py中,key_map的key,由1,2,3,4
改为枚举的四种状态:
from enum import Enum
class PendingStatus(Enum):
'''
交易4个状态
'''
Waiting = 1
Success = 2
Reject = 3
Redraw = 4
@classmethod
def pending_str(cls, status, key):
key_map = {
cls.Waiting: {
'requester': '等待对方邮寄',
'gifter': '等待你邮寄'
},
cls.Success: {
'requester': '对方已邮寄',
'gifter': '你已邮寄,交易完成'
},
cls.Reject: {
'requester': '对方已拒绝',
'gifter': '你已拒绝'
},
cls.Redraw: {
'requester': '你已撤销',
'gifter': '对方已撤销'
},
}
return key_map[status][key]
十六. 超权现象的防范
redraw_drift视图函数:
web.route('/drift/<int:did>/redraw')
@login_required
def redraw_drift(did):
"""
撤销请求,只有书籍请求者才可以撤销请求
注意需要验证超权
"""
with db.auto_commit():
drift = Drift.query.filter_by(id=did).first_or_404()
drift.pending = PendingStatus.Redraw
current_user.beans += 1
return redirect(url_for('web.pending'))
uid 为1的用户去访问/drift/<int:did>/redraw
的url, 自己修改了url的did,将did改为uid为2的用户的did, 这样他就可以撤销uid为2的用户的操作了。 这是很严重的安全隐患。
我们在查询的时候做些限制, 让用户查询不到其他人的did:
web.route('/drift/<int:did>/redraw')
@login_required
def redraw_drift(did):
"""
撤销请求,只有书籍请求者才可以撤销请求
注意需要验证超权
"""
with db.auto_commit():
#查询的时候 增加request_id=current_user.id的限制条件
drift = Drift.query.filter_by(request_id=current_user.id, id=did).first_or_404()
drift.pending = PendingStatus.Redraw
current_user.beans += 1
return redirect(url_for('web.pending'))
十七. 拒绝请求
当别人向我索要时, 我可以拒绝。只有书籍赠送者,才会有拒绝按钮。
reject_drift视图函数
@web.route('/drift/<int:did>/reject')
@login_required
def reject_drift(did):
"""
拒绝请求,只有书籍赠送者才能拒绝请求
注意需要验证超权
"""
with db.auto_commit():
drift = Drift.query.filter(Gift.uid == current_user.id, Drift.id == did).first_or_404()
drift.pending = PendingStatus.Reject
requester = User.query.get_or_404(drift.request_id)
requester.beans += 1
return redirect(url_for('web.pending'))
十八. 邮寄成功
在交易界面, 点击已邮寄
表示交易完成
完成mailed_drift视图函数
@web.route('/drift/<int:did>/mailed')
@login_required
def mailed_drift(did):
"""
确认邮寄,只有书籍赠送者才可以确认邮寄
注意需要验证超权
"""
with db.auto_commit():
drift = Drift.query.filter_by(gifter_id=current_user.id, id=did).first_or_404()
drift.pending = PendingStatus.success # 该为success 表示交易完成
current_user.beans += 1
gift = Gift.query.filter_by(id=drift.gift_id).first_or_404()
gift.launched = True # gift成功交易出去了
wish = Wish.query.filter_by(isbn=drift.isbn, uid=drift.quester_id, launched=False).first_or_404()
wish.launched = True # 心愿单也完成了
return redirect(url_for('web.pending'))
十九. 撤销礼物与心愿
我的礼物和我的心愿页面, 都有撤销操作, 我们完成它。
app/web/gift.py的redraw_from_gifts视图函数:
@web.route('/gifts/<gid>/redraw')
@login_required
def redraw_from_gifts(gid):
gift = Gift.query.filter_by(id=gid, launched=False).first_or_404()
drift = Drift.query.filter_by(gift_id=gid, pending=PendingStatus.Waiting).first()
if drift:
flash('这个礼物正处于交易状态, 请先在鱼漂页面处理')
with db.auto_commit():
current_user.beans -= current_app.config['BEANS_UPLOAD_ONE_BOOK'] # 扣除鱼豆
gift.delete()
return redirect(url_for('web.my_gifts'))
gift的撤销,与交易有关。而wish的撤销与交易无关 app/web/wish.py的redraw_from_wish视图函数:
@web.route('/wish/book/<isbn>/redraw')
@login_required
def redraw_from_wish(isbn):
wish = Wish.query.filter_by(isbn=isbn, launched=False).first_or_404()
with db.auto_commit():
wish.delete()
return redirect(url_for('web.my_wish'))
我们数据库的删除,是软删除, 直接status=0,可读性差。 我们在Base模型下添加delete方法:
class Base(db.Model):
__abstract__ = True
create_time = Column('create_time', Integer)
status = Column(SmallInteger, default=1)
def __init__(self):
self.create_time = int(datetime.now().timestamp()) # 自动生成时间戳
def set_attrs(self, attrs_dict):
for key, value in attrs_dict.items():
if hasattr(self, key) and key != 'id':
setattr(self, key, value)
@property
def create_datetime(self):
if self.create_time:
return datetime.fromtimestamp(self.create_time)
else:
return None
def delete(self): # 删除数据库数据
self.status = 0
二十. 向他人赠送书籍
点击向他赠送此书
, 会发送给对方一个邮件, 对方点击邮件中的url, 赠书完成。
app/web/wish.py中的satisfy_wish视图函数。
@web.route('/satisfy/wish/<int:wid>')
@login_required
def satisfy_wish(wid):
"""
向想要这本书的人发送一封邮件
注意,这个接口需要做一定的频率限制
这接口比较适合写成一个ajax接口
"""
wish = Wish.query.get_or_404(wid)
gift = Gift.query.filter_by(uid=current_user.id, isbn=wish.isbn).first()
if not gift:
flash('您还没有上传此书。请点击"加入到赠送清单"添加此书。添加前确保自己可以赠送此书')
else:
send_email(wish.user.email, '有人想送你一本书', 'email/satisify_wish.html', wish=wish, gift=gift)
flash('已向他/她发送了一封邮件, 如果他/她愿意接受你的赠送。你将收到一个鱼漂')
return redirect(url_for('web.book_detail', isbn=wish.isbn))