11.python与flask的结合应用(二)


九. 鱼漂业务与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.pypending_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.pydirft.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.Redrawdrift.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'))

十八. 邮寄成功

在交易界面, 点击已邮寄表示交易完成 image

完成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))

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/80828986