八、 编写向赠送者请求此书
编写书籍详情页面的“向他请求此书”的业务逻辑:
8.1 成品的网站功能展示
图示关系:
8.2 编写交易模型Drift
新建目录:
在app|models|drift.py
中:
from sqlalchemy import Column, Integer, String
from app.models.base import Base
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))
# 请求者的信息
requester_id = Column(Integer)
requester_nickname = Column(String(20))
# 赠送者信息
gifter_id = Column(Integer)
gift_id = Column(Integer)
gifter_nickname = Column(String(20))
思考题:
- 如何设计交易状态?
- 所有信息均是平铺的,没有模型关联。这么写有什么好处?
8.3 合理利用数据冗余记录历史状态
比较 | 模型关联 | 重复记录信息 |
---|---|---|
在gift 模型中:user = relationship('User') | 在drift 模型中:如上所述,全部重复记录 | |
特点 | 实时:一变则变 | 两个地方都记录了相同的信息,这叫数据冗余 历史状态的记录 |
生活场景 | 淘宝上的交易订单记录,其价格是过去某时间点的价格 日志 |
数据库设计时,到底是选择模型关联,还是重复记录信息,应根据使用场景确定,各有优劣。
8.4 鱼漂的三个条件检测
对于状态类型,最好使用枚举类型,而不是使用数字代表,后者可读性差。
新建目录:
在自定义模块的枚举类app|libs|enums.py
中:
from enum import Enum
class PendingStatus(Enum):
Waiting = 1
Success = 2 # pending = PendingStatus.Success + Success = 2,相比直接 pending = 2要更加易读
Reject = 3
Redraw = 4
在模型层app|models|drift.py
中:
...
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))
# 请求者的信息
requester_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|models|gift.py
中:
...
class Gift(Base):
id = Column(Integer, primary_key=True)
launched = Column(Boolean, default=False)
user = relationship('User')
uid = Column(Integer, ForeignKey('user.id'))
isbn = Column(String(15), nullable=False)
def is_yourself_gift(self, uid): # 定义一个新的函数,供下面调用
'''判断你想要的书,是否是跟你想赠送给别人的书是同一本'''
return True if self.id == uid else False
在模型层app|models|user.py
中:
from math import floor
class User(Base, UserMixin):
id = Column(Integer, primary_key=True)
nickname = Column(String(24), nullable=False)
phone_number = Column(String(18), unique=True)
_password = Column('password', String(128), nullable=False)
email = Column(String(50), unique=True, nullable=False)
confirmed = Column(Boolean, default=False) # 下面四个:随以后业务发展,后续介绍
beans = Column(Float, default=0)
send_counter = Column(Integer, default=0)
receive_counter = Column(Integer, default=0)
def can_send_drift(self): # 新定义一个函数,以供下面调用
''' 判断用户是否能够发送鱼漂'''
if self.beans < 1:
return False # 把一定不能发送的特殊情况,先列出来
# 当前用户已成功赠书的数量
success_gifts_count = Gift.query.filter_by(uid=self.id, launched=True).count()
# 当前用户已成功索取的书的数量 # 从鱼漂模型中找
success_receive_count = Drift.query.filter_by(requester_id=self.id,
pending=PendingStatus.Success).count()
# 每索取两本书,自己必须赠送一本书 # floor取底板值,取整呗
return True if \
floor(success_receive_count / 2) <= floor(success_gifts_count)\
else False
@property
def summary(self): # 这次不用viewmodel,因为该数据具有一些具体的意义
return dict(nickname=self.nickname, beans=self.beans, email=self.email,
send_receive=str(self.send_counter)+'/'+str(self.receive_counter))
在视图函数app|web|drift.py
中:
...
@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): # 调用上面的is_yourself_gift函数
flash('这本书是你自己的 :) ,不能向自己索要书籍噢')
return redirect(url_for('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 = Gift.user.summary # 调用上面用户模型中,刚写好的summary属性,以将原始数据,转换为视图数据
return render_template('drift.html',
gifter=gifter, user_beans=current_user.beans)
一般情况下,适配页面的话,定义在viewmodel里。
上述不用viewmodel,是因为数据具有一些具体的意义。再如果,当这几个数据组合在一起的使用频度非常高,也建议定义在模型中。
8.5 完成鱼漂即请求此书
上述是完成了GET
请求的逻辑,接下来处理POST
的业务逻辑。
在验证层app|forms|book.py
中:
class DriftForm(Form):
'''检测drift的相关表单信息'''
recipient_name = StringField(validators=[DataRequired(), Length(min=2, max=20, message='收件人姓名长度必须在2到20个字符之间')])
mobile = StringField(validators=[DataRequired(), Regexp('^1[0-9]{10}$', 0, '请输入正确的手机号')]) # 正则验证手机号
message = StringField()
address = StringField(validators=[DataRequired(), Length(min=10, max=70, message='地址还不到10个字吗?尽量写详细一些吧')])
在视图函数app|web|drift.py
中:
...
@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(url_for('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) # 调用下方的新函数,以把所有drift的信息保存到数据库中
send_mail(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): # 单独提取出来的方法,建议放在视图函数的模块最后
'''因为drift的信息较多,所以单独编写一个函数用于保存至数据库'''
with db.auto_commit():
drift = Drift()
# 四个邮寄信息
drift_form.populate_obj(drift) # 将前者form表单的信息,复制装载到刚实例化的对象后者中 # 前后两者里面的变量名必须一致
# 请求者的信息
drift.requester_id = current_user.id
drift.requester_nickname = current_user.nickname
# 赠送者信息
drift.gift_id = current_gift.id
drift.gifter_id = current_gift.user.id
drift.gifter_nickname = current_gift.user.nickname
# 书籍信息
# book = current_gift.book.first
# drift.book_title = book['title'] # 这里的book是个字典
book = BookViewModel(current_gift.book) # 这里的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)
在书籍详情页面,请求此书:
填写作为索书人的邮寄信息:
书籍拥有者的邮箱会受到一封邮件:
我的鱼豆由1减为了0:
上述是当鱼豆充足时的情况,下面演示下鱼豆为0时的情况:
九、 编写交易记录页面
9.1 查询drift模型,得到交易记录页面
该页面的交易历史记录,是存储在drift表中。如果检索所有的交易记录,就是对drift模型做查询。
在视图函数app|web|drift.py
中:
...
@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(url_for('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_mail(current_gift.user.email, '有人想要一本书', 'email/get_gift.html', wisher=current_user, gift=current_gift)
return redirect(url_for('web.pending')) # 增加,以返回即将编写的鱼漂交易记录页面
gifter = current_gift.user.summary
return render_template('drift.html', gifter=gifter, user_beans=current_user.beans, form=form)
@web.route('/pending')
@login_required
def pending(): # 鱼漂交易记录页面
'''一个一个的历史交易记录'''
drifts = Drift.query.filter(or_(Drift.requester_id == current_user.id, Drift.gifter_id == current_user.id)).order_by(desc(Drift.create_time)).all()
9.2 Drift ViewModel
1.成品的交易记录页面的功能展示:
(1)有可能是“请求者”,也有可能是“向他请求”。
(2)有可能是“等待你邮寄”,也有可能是“等待对方邮寄”。
以上的不同,根源在于用户的身份不同。
真正的产品制作时,建议将两种情况分开,代码写起来简单些。本项目放在一起,是为了多学点知识点。
2.不建议在viewmodel中直接使用current_user
- 新建目录:
- 在视图模型
app|viewmodels|drift.py
中:
from flask_login import current_user
def requester_or_gifter(self, drift):
'''判断用户是索书人,还是赠书人'''
if drift.requester_id == current_user.id: # 不建议在viewmodel中使用current_user
...
不建议在viewmodel中直接使用current_user。这是面向对象中类的设计原则。因为破坏了类的封装性,让viewmodel与current_user形成了紧密的耦合。
所以,应该将其作为参数,传递进来。即:
- 在视图模型
app|viewmodels|drift.py
中:
def requester_or_gifter(self, drift, current_user_id): # 作为变量参数,传递进来
'''判断用户是索书人,还是赠书人'''
if drift.requester_id == current_user_id:
...
3.编码鱼漂交易记录页面
在视图模型app|viewmodels|drift.py
中:
from app.libs.enums import PendingStatus
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: # 对每一个drift,都构建一个viewmodel
temp = DriftViewModel(drift, current_user_id) # 实例化一个对象
self.data.append(temp.data) # 将对象中的字典,存储添加到上面的空列表中去
class DriftViewModel:
'''处理单本'''
def __init__(self, drift, current_user_id): # 不用一个个定义实例属性了,用一个字典来代表所有的实例属性
self.data = {}
self.data = self.__parse(drift, current_user_id) # 下面定义完了,那么就在构造函数里逐级调用下,并添加
@staticmethod
def requester_or_gifter(drift, current_user_id):
'''判断用户是索书人,还是赠书人'''
if drift.requester_id == current_user_id: # 不建议在viewmodel中使用current_user
you_are = 'requester'
else:
you_are = 'gifter'
return you_are
def __parse(self, drift, current_user_id): # 处理原始数据,向viewmodel转化的过程
you_are = self.requester_or_gifter(drift, current_user_id)
pending_status = PendingStatus.pending_str(drift.pending, you_are) # 调用刚在下面定义的枚举类中的新方法
r = {
'you_are': you_are, # 非直接显示的变量,却是控制显示的间接变量
'drift_id': drift.id,
'book_title': drift.book_title,
'book_author': drift.book_author,
'book_img': drift.book_img,
'date': drift.create_datetime.strftime('%Y-%m-%d'), # 将时间戳转换为年月日
'operator': drift.requester_nickname if you_are != 'requester' else drift.gifter_nickname,
'message': drift.message,
'address': drift.address,
'status_str': pending_status, # 上面的pending_status,过来赋值一下
'recipient_name': drift.recipient_name,
'mobile': drift.mobile,
'status': drift.pending
}
return r
在自定义文件app|libs|enums.py
中:
from enum import Enum
class PendingStatus(Enum): # 枚举类
Waiting = 1
Success = 2
Reject = 3
Redraw = 4
@classmethod
def pending_str(cls, status, key): # 定义一个类方法,以供上面调用
'''用于汇集、取出所有的状态'''
# 双层字典:一个status:对应两个key # python没有switch case语句,所以通常用字典来模拟。
key_map = {
1: {
'requester': '等待对方邮寄',
'gifter': '等待你邮寄'
},
3: {
'requester': '对方已拒绝',
'gifter': '你已拒绝'
},
4: {
'requester': '你已撤销',
'gifter': '对方已撤销'
},
2: {
'requester': '对方已邮寄',
'gifter': '你已邮寄,交易完成'
}
}
return key_map[status][key] # 最终是要拿到里面的文字
在视图函数app|web|drift.py
中:
把上面定义好了,最后在视图函数中的调用,真是简洁啊~
...
@web.route('/pending')
@login_required
def pending():
'''该页面显示一个一个的历史交易记录'''
drifts = Drift.query.filter(or_(Drift.requester_id == current_user.id, Drift.gifter_id == current_user.id)).order_by(desc(Drift.create_time)).all()
views = DriftCollection(drifts, current_user.id) # 调用上面的视图模型中的类,实例化一个viewmodel #这里用的是current_user.id,不是current_user_id
return render_template('pending.html', drifts=views.data) # views.data才是真正的数据
在浏览器中,查看鱼漂即书籍交易页面:
根据下面的成品页面,少了些按钮,后续补。
9.3 对比三种单体与集合的类模式
总结归纳所有的viewmodel
(1)关于book的viewmodel:
class BookViewModel():
'''处理单本书籍'''
def __init__(self, book): # 定义构造函数
self.title = book['title'] # 把所有的字段,一个一个定义出来
self.publisher = book['publisher']
self.author = '、'.join(book['author'])
self.image = book['image']
self.price = book['price']
self.summary = book['summary']
self.isbn = book['isbn']
self.pages = book['pages']
self.pubdate = book['pubdate']
self.binding = book['binding']
@property
def intro(self): # 定义实例方法
intros = filter(lambda x: True if x else False, [self.author, self.publisher, self.price])
return '/'.join(intros)
class BookCollection():
''''处理一组书籍'''
def __init__(self): # 定义构造函数
self.total = 0
self.books = []
self.keyword = ''
def fill(self, yushu_book, keyword): # 定义实例方法
self.total = yushu_book.total
self.books = [BookViewModel(book) for book in yushu_book.books]
self.keyword = keyword
(2)关于gift的viewmodel
:
from app.view_models.book import BookViewModel
class MyGifts:
'''集合的一组礼物''''
def __init__(self, gifts_of_mine, wish_count_list):
self.gifts = []
self.__gifts_of_mine = gifts_of_mine
self.__wish_count_list = wish_count_list
self.gifts = self.__parse()
def __parse(self):
temp_gifts = []
for gift in self.__gifts_of_mine:
my_gift = self.__matching(gift)
temp_gifts.append(my_gift)
return temp_gifts
def __matching(self, gift):
count = 0
for wish_count in self.__wish_count_list:
if gift.isbn == wish_count['isbn']:
count = wish_count['count']
r = { # 以字典的形式,承载单体的概念,而不是再定义一个类
'wishes_count': count,
'book': BookViewModel(gift.book),
'id': gift.id
}
return r
(3)关于drift的viewmodel
:
from app.libs.enums import PendingStatus
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)
class DriftViewModel:
'''处理单本'''
def __init__(self, drift, current_user_id):
self.data = {} # 没有在构造函数中,定义所有的字段
self.data = self.__parse(drift, current_user_id)
@staticmethod
def requester_or_gifter(drift, current_user_id):
'''判断用户是索书人,还是赠书人'''
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)
r = { # 你上面的构造函数不一个一个定义字段,这那么这里字典就得补上这些字段
'you_are': you_are,
'drift_id': drift.id,
'book_title': drift.book_title,
'book_author': drift.book_author,
'book_img': drift.book_img,
'date': drift.create_datetime.strftime('%Y-%m-%d'),
'operator': drift.requester_nickname if you_are != 'requester' else drift.gifter_nickname,
'message': drift.message,
'address': drift.address,
'status_str': pending_status,
'recipient_name': drift.recipient_name,
'mobile': drift.mobile,
'status': drift.pending
}
return r
对比 | 关于book的viewmodel | 关于gift的viewmodel | 关于drift的viewmodel |
---|---|---|---|
共性 | 既有单体的一本书,也有集合的一组书 | 既有单体的一本书,也有集合的一组书 | 既有单体的一本书,也有集合的一组书 |
特点 | 因为用最标准的类来处理,所以写法最典型、最基础、最标准 | 形式上只有集合,没有单体。 实质有单体,隐藏在了字典中 | 结合了前两个的特点: 兼具字典方便的特性,又可以扩展 |
优点 | 怎么扩展都可以,直接定义新方法 | 写的代码少 | 自己写起来简单 |
缺点 | ps:个人觉得很工整,不麻烦 :) | 没办法扩展,不能为单体增加一些处理方法 | 别人读起来复杂 |
推荐 | 最推荐 | 最不推荐 | 不太推荐 |
以上非特例,基本上很多业务项目都会面临单体与集合的概念。
不只是viewmodel
,设计任何对象时,都可以运用上面的三种写法。用最适合自己的。
9.4 更好的使用枚举
在视图函数app|web|drift.py
中:
1.pending
是数字,Redraw
是枚举类。如何拿到Redraw
枚举类对应的数字?
@web.route('/drift/<int:did>/redraw')
@login_required
def redraw_drift(did): # did:drift的id号
'''撤销操作'''
with db.auto_commit:
drift = Drift.query.filter(Drift.id == did).first_or_404()
drift.pending = PendingStatus.Redraw # 将默认的状态1改为4
return redirect(url_for('web.pending')) # 真实项目中,这里最好用js来写ajax的代码
下面在别人看1、2、3、4时,别人不知道代表什么意思
在自定义文件app|libs|enums.py
中:
from enum import Enum
class PendingStatus(Enum): # 枚举类
Waiting = 1
Success = 2
Reject = 3
Redraw = 4
@classmethod
def pending_str(cls, status, key):
'''用于汇集、取出所有的状态'''
key_map = {
1: {
'requester': '等待对方邮寄',
'gifter': '等待你邮寄'
},
3: {
'requester': '对方已拒绝',
'gifter': '你已拒绝'
},
4: {
'requester': '你已撤销',
'gifter': '对方已撤销'
},
2: {
'requester': '对方已邮寄',
'gifter': '你已邮寄,交易完成'
}
}
return key_map[status][key]
改为:
from enum import Enum
class PendingStatus(Enum):
Waiting = 1 # 视为类变量,下面字典中的status就可以不用数字了
Success = 2
Reject = 3
Redraw = 4
@classmethod
def pending_str(cls, status, key):
'''用于'''
# 双层字典:一个status:对应两个key
key_map = {
cls.Waiting: {
'requester': '等待对方邮寄',
'gifter': '等待你邮寄'
},
cls.Reject: {
'requester': '对方已拒绝',
'gifter': '你已拒绝'
},
cls.Redraw: {
'requester': '你已撤销',
'gifter': '对方已撤销'
},
cls.Success: {
'requester': '对方已邮寄',
'gifter': '你已邮寄,交易完成'
}
}
return key_map[status][key] # 最终是要拿到状态文字
在模型层app|models|drift.py
中:
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))
# 请求者的信息
requester_id = Column(Integer)
requester_nickname = Column(String(20))
# 赠送者信息
gift_id = Column(Integer)
gifter_id = Column(Integer)
gifter_nickname = Column(String(20))
_pending = Column('pending', SmallInteger, default=1) # 变为私有变量 # 表示鱼漂的状态,是个数字
@property
def pending(self): # 目的:方法转换成属性
'''将数字转换为枚举类型'''
return PendingStatus(self._pending) # 类似类的实例化,以返回的是枚举类型
@pending.setter
def pending(self, status): # status:传进来的是上面的枚举类型
'''将上面的得到的属性:枚举类型转换为数字类型'''
self._pending = status.value
此处,为什么要有setter?
在浏览器中,查看鱼漂即书籍交易页面,成功显示了状态的按钮:
9.5 动态语言与开发人员的素养
动态语言不易维护、不易读,非常的乱?
动态语言只是给了你一种写出随意代码的能力,更轻松。但是这并不代表随便、胡乱写代码,该遵守的编程素养还是要遵守。
动态语言写的非常快,但是过段时间就不知道自己写的是什么了。这不是语言问题,而是开发人员的素养问题。
老师:
动态语言比静态语言难很多。建议第一门语言,是java或C#。
9.6 超权现象防范
1.问题
继续编写撤销操作redraw_drift()
:
在视图函数app|web|drift.py
中:
@web.route('/drift/<int:did>/redraw')
@login_required
def redraw_drift(did):
'''撤销操作'''
with db.auto_commit:
drift = Drift.query.filter(Drift.id == did).first_or_404()
drift.pending = PendingStatus.Redraw
current_user.beans += 1 # 将鱼豆还给你
return redirect(url_for('web.pending'))
上述业务逻辑编写完了,但是有个严重的安全问题:
一般情况下:
uid1 ---> did1
uid2 ---> did2
但是:
用户1,可以通过修改URL@web.route('/drift/<int:did>/redraw')
中的did
编号,修改不属于他的鱼漂2。
而必须登录的login_required
只能限制必须是登录状态,不能防止超权。
所以,只能增加一个判断条件:发送鱼漂想要书的请求者id,必须与当前用户的id是同一个人。
2.改正
在视图函数app|web|drift.py
中:
...
@web.route('/drift/<int:did>/redraw')
@login_required
def redraw_drift(did):
'''撤销操作'''
with db.auto_commit:
drift = Drift.query.filter_by(requester_id=current_user.id, id=did).first_or_404() # 增加判断条件
drift.pending = PendingStatus.Redraw
current_user.beans += 1
return redirect(url_for('web.pending'))
编写网站,潜意识认为:用户总是会按照给他设计好的一个个的按钮来访问视图函数。但是,每个视图函数总是对应一个URl的。
换句话说,用户是可以自己来编写或伪造一个URL来访问你的视图函数的。
十、完成交易记录页面
10.1 拒绝请求
编写“拒绝”按钮的业务逻辑:
在视图函数app|web|drift.py
中:
...
@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 # 将默认的状态1改为3
requester = User.query.get_or_404(drift.requester_id)
requester.beans += 1 # 将鱼豆还给你
return redirect(url_for('web.pending')) # 真实项目中,这里最好用js来写ajax的代码
我向用户“哲欣”,发送了个鱼漂,作为索取方我想要这本书。但是对方不想给我,于是他的操作如下:
同时,我这边是也受到了他的拒绝:
10.2 邮寄成功
上面完成了拒绝的操作,相应的,如果同意赠送给他这本书,并且已经把书邮寄给了书籍索要者的话,那么就需要点击“已邮寄”按钮,已确认完成这次交易:
成品网站的功能展示:
在视图函数app|web|drift.py
中:
...
@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 # 将默认的状态1改为2
current_user.beans += 1
# 把该礼物的状态,改为已送出
gift = Gift.query.filter_by(id=drift.gift_id).first_or_404() # 写法好理解些
gift.launched = True
# 心愿已完成 # 就像淘宝你把购物车中的订单支付完了,购物车的商品就要自动消失
Wish.query.filter_by(isbn=drift.isbn, uid=drift.requester_id, launched=False).update({Wish.launched: True}) # 本质跟上面两行的写法一样
return redirect(url_for('web.pending'))
我向用户“哲欣”,发送了个鱼漂,作为索取方我想要这本书。对方也想赠送给我这本书,且已邮寄给我,于是他的操作如下:
十一、其他功能
11.1 撤销礼物
在赠送清单和心愿清单的两个页面中,“撤销”按钮的业务逻辑:
在视图函数app|web|gift.py
中:
...
@web.route('/gifts/<gid>/redraw')
@login_required
def redraw_from_gifts(gid):
gift = Gift.query.filter_by(id=gid, launched=False).first_or_404()
with db.auto_commit():
current_user.beans -= current_app.config['BEANS_UPLOAD_ONE_BOOK']
gift.delete() # 当所有的模型,遇到删除操作时,都可以这样来编写。可读性好
# gift.status = 0
return redirect(url_for('web.my_gifts'))
在模型层app|models|base.py
中:
...
class Base(db.Model):
__abstract__ = True
status = Column(SmallInteger, default=1)
create_time = Column('create_time', Integer)
def __init__(self):
self.create_time = int(datetime.now().timestamp())
pass
def delete(self): # 在基类模型中,新定义一个方法
self.status = 0
在上述两个清单中撤销操作时,礼物不能是等待交易的状态:要想撤销操作,必须先把交易完成。
在视图函数app|web|gift.py
中:
...
@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('这个礼物正处于交易状态,请先前往鱼漂完成该交易')
else:
with db.auto_commit():
current_user.beans -= current_app.config['BEANS_UPLOAD_ONE_BOOK']
gift.delete() # 当所有的模型,遇到删除操作时,都可以这样来编写。可读性好
return redirect(url_for('web.my_gifts'))
在浏览器中,成功的撤销了礼物:
11.2 撤销心愿操作
在视图函数app|web|wish.py
中:
...
@web.route('/wish/book/<isbn>/redraw')
@login_required
def redraw_from_wish(isbn):
'''鱼漂只和gift有关联,与wish是没有关联的'''
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'))
在浏览器中,成功的撤销了心愿:
11.3 向他人赠送书籍
之前编写了作为索书人,向赠送者请求此书;
现在编写,作为赠书人,主动向他人送书。
两者的业务逻辑其实是镜像关系,但是如果镜像编写,相当麻烦。因此,可以尽量重复利用之前的逻辑线。
1.关系图示:
2.简单编码一个函数即可
在视图函数app|web|wish.py
中:
...
@web.route('/satisfy/wish/<int:wid>')
@login_required
def satisfy_wish(wid):
'''向他人赠送此书'''
wish = Wish.query.get_or_404(wid) # 要满足别人的心愿,哪个心愿?赋值给变量wish
gift = Gift.query.filter_by(uid=current_user.id, isbn=wish.isbn).first() # 我的赠送清单中,有没有这个礼物?
if not gift:
flash('你还没有上传此书,'
'请点击“加入到赠送清单”添加此书。添加前,请确保自己可以赠送此书')
else:
send_mail(wish.user.email, '有人想送你一本书', 'email/satisfy_wish.html', wish=wish, gift=gift)
flash('已向他/她发送了一封邮件,如果他/她愿意接受你的赠送,你将受到一个鱼漂')
return redirect(url_for('web.book_detail', isbn=wish.isbn))
3.在浏览器中,成功通过邮件的方式,向他人赠送书籍:
以上,成功实现了向他人赠送此书的业务逻辑。
11.4 个人中心页面
在视图函数app|web|main.py
中:
@web.route('/personal')
@login_required
def personal_center(): # 个人中心的页面视图函数
return render_template('personal.html', user=current_user.summary) # 调用的是下面的summary属性
在模型层app|models|user.py
中:
class User(Base, UserMixin):
...
@property
def summary(self): # 这次不用viewmodel,因为数据具有一些具体的意义
return dict(nickname=self.nickname, beans=self.beans, email=self.email,
send_receive=str(self.send_counter)+'/'+str(self.receive_counter))
本节编写好了个人中心的业务逻辑,但是,其中的“修改”按钮即关于更改密码的还没有完成。
11.5 修改密码
在视图函数app|web|auth.py
中:
@web.route('/change/password', methods=['GET', 'POST'])
@login_required
def change_password():
form = ChangePasswordForm(request.form) # 调用下面刚写好的验证层ChangePasswordForm
if request.method == 'POST' and form.validate():
with db.auto_commit():
current_user.password = form.new_password1.data # 为什么要加个data?
flash('密码已更新成功')
return redirect(url_for('web.personal_center'))
return render_template('auth/change_password.html', form=form)
在验证层app|forms|auth.py
中:
class ChangePasswordForm(Form):
old_password = PasswordField(validators=[DataRequired()])
new_password1 = PasswordField(validators=[DataRequired(), Length(6, 32, message='密码长度至少需要在6到20个字符串之间'), EqualTo('new_password2', message='再次输入的密码不相同')])
new_password2 = PasswordField(validators=[DataRequired()])
在浏览器中成功修改了密码:
11.6 限频装饰器
自己编写一个装饰器,用于限制邮件发送的次数。因课程遗漏,且没看懂老师代码,暂放于此。后续待补。
Comments | NOTHING