Day24 - [丰收款] 以Django Web框架实作永丰API线上支付模拟情境(5) - 我的订单

今天这篇是我们实作库米狗屋●KummyShop的情境电商模拟的最终章了!今天我们要把先前建的一堆订单可以在我的订单页面呈现出来。

我们预计,可以在里面看到:

  • 订单排序,新的在最上面
  • 订单会有分页功能
  • 会列出订单编号、总金额、付款方式、付款状态、额外资讯或功能(例如可以重新刷卡、显示ATM转帐帐号)
  • 表格的列依不同状态区分颜色:失败为红色、尚未付款为蓝色,只有成功付款才会是白色。
  • 将金额加上千位逗号日期格式化、将原本付款方式、状态代码等换成对应的文字

简化的部份说明

为了专注我们想谈的功能,这边作了很多的简化,觉得和正式电商相比有缺少之处,都请自行脑补。

  1. 没有其他的User,所以也不用分了,所以全店的资料都归同一个人所有,不用筛选了 (小孩才作选择!)
  2. 订单里面不会存当时选的商品内容,反正就是一笔一笔的订单有金额以及当时选择的付款方式
  3. 不会多做额外的例外处理防护

那麽就可以开始往下看了。

准备一下取Order的Model

这支程序很简单,我们需要可以取得以id来排序的降幂排序,最多回传100笔资料。

def get_my_orders(top=100):
    return Payment.objects.all().order_by('-id')[:top]
程序说明

使用Model的objects.all()取回所有资料,但使用order_by()id反向排序,作法就是在前面加上一个减号。而取排序後的前top笔资料,参数预设值为100。

接着在View新增my_orders

在这里我们需要处理的是取得model中的orders资料,以及加上分页的机制。我们可使用Django内建的Paginator模组来快速产生分页的计算与资料切割,相当方便。

from django.core.paginator import Paginator

def my_orders(request):
    page_number = request.GET.get('page', 1)
    orders_total = get_my_orders()
    paginator = Paginator(orders_total, 10)
    page_orders = paginator.page(page_number)
    context = {"page_orders": page_orders}
    return render(request, 'order/my_orders.html', context)
程序说明

这里需要使用Paginagor的类别来装载我们取回的order资料,并设定其分页一页大小,我们设定10笔为一页。
接着就可以从request传入一个page参数来决定等一下返回给Template资料是第几页的资料内容,这些全部都交给paginator机制来运算。

更有效的处理代码转换问题

我们有很多的值都是以代码储存,那显示的时候怎麽办呢?
在上一篇文章有提到一些上次作了一小部份代码转换的想法,这次我们使用了Django Template的TemplateTags的作法来解决。

我们会需要准备两个转换代码的方法,一个来处理付款方式(C或A),一个来处理付款状态(W、S、F)。我们需要在Django 对应的App底下(我们这里是order App),建立一个名为TemplateTags的目录,在底下新增一个空白的__init__.py作为识别用途,以及一个我们要处理的逻辑order_converter.py

在我们的Django目录如下所示:
https://ithelp.ithome.com.tw/upload/images/20211009/20130354DmA1eRuGa9.png

from django import template

register = template.Library()


@register.filter
def convert_pay_status(value):
    convert_dict = {"W": "尚未付款", "S": "付款成功", "F": "付款失败"}
    return convert_dict.get(value.upper())


@register.filter
def convert_pay_type(value):
    convert_dict = {"A": "ATM转帐", "C": "信用卡"}
    return convert_dict.get(value.upper())

程序说明

接着来说明一下这支converter会用到的部份,这个是要给Template使用的,所以我们要先行引入django的template以及使用其Library()方法建立一个filter注册物件。

我们使用属性宣告方式在这两个方法前面加上@register.filter,接着就使用简单的方式把对应的代码作转换後回传。

这里我们为了不失焦,不特别去处理其他的异常处理作法,但在产品级的系统里,请要额外处理不在我们认知的值的处理机制。而且这样的对应表,通常也会另外准备在外部的properties档或资料库中去定义,不会直接hard code在程序码里面。总之,铁人赛这一系列文章中,仅处理需要的逻辑与简易式写法,不会特别去处理try/catch的异常处理机制,有需要参考者,请自行花时间去撰写具备保护力(咦?)的程序码喔!

重头戏,我们的最後呈现Template

{% extends "base.html" %}
{% load bootstrap5 %}
{% load humanize %}
{% load order_converter %}

{% block title %}{{ title }}{% endblock %}

{% block body %}
<nav id="app" class="row g-3 align-self-center">
<h1 class="display-4 text-center  mb-3 mt-5">我的订单</h1>
<p class="lead  text-center">看看我都买了些什麽呀…</p>
<hr/>
<div class="">
<table class="table">
  <thead>
    <tr>
      <th scope="col">订单号码</th>
      <th scope="col">总金额</th>
      <th scope="col">订单时间</th>
      <th scope="col">付款方式</th>
      <th scope="col">付款状态</th>
      <th scope="col">动作</th>
    </tr>
  </thead>
  <tbody>
    {% for order in page_orders %}
    <tr class={% if order.pay_status == 'F' %}"table-danger"{% elif order.pay_status == 'W' %}table-info{% endif %}>
      <th scope="row">{{ order.order_no }}</th>
      <td>NT$ {{ order.amount|intcomma }}</td>
      <td>{{ order.create_time|date:'Y-m-d H:i' }}</td>
      <td>{{ order.pay_type|convert_pay_type }}</td>
      <td>{{ order.pay_status|convert_pay_status }}</td>
      <td>{% if order.pay_status == 'F' %}<a href="{{ order.card_pay_url }}" >重新刷卡</a>{% endif %}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>
</div>

<div class="text-center">
    <span class="current">
        第 {{ page_orders.number }} 页 / 共 {{ page_orders.paginator.num_pages }} 页
    </span>
</div>

<ul class="pagination justify-content-center">
    {% if page_orders.has_previous %}
        <li class="page-item"><a class="page-link" href="?page=1">第一页</a></li>
        <li class="page-item"><a class="page-link" href="?page={{ page_orders.previous_page_number }}">上一页</a></li>
    {% endif %}

    {% if page_orders.has_next %}
        <li class="page-item"><a class="page-link" href="?page={{ page_orders.next_page_number }}">下一页</a></li>
        <li class="page-item"><a class="page-link" href="?page={{ page_orders.paginator.num_pages }}">最末页</a></li>

    {% endif %}
</ul>

</div>
{% endblock %}

{% block script %}
{% endblock %}
程序说明

刚前面有提到我们的几个资料显示处理要点,忘记的可以再到上面确认一下。

我们取回了具备已分页完的order资料,因此我们想要在表面上,使用for回圈把资料显示在上面,所以需要在tr的地方加上所需的回圈逻辑。

我们搭配使用了Bootstrap的Table显示,因此等一下针对不同付款状态会有不同的bootstrap的颜色,我们会带入相对应的table color CSS。

处理格式与代码转换

其中,我们会显示五个栏位的值:订单编号、总金额、付款方式、付款状态、额外资讯或功能

总金额的地方,我们需要将数字加上千位数逗号,这部份可使用humanize模组的intcomma功能,要使用之前,需在settings.py中的INSTALLED_APPS加上模组的设定才能使用。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.humanize',
    'bootstrap5',
    'order',
    'greetings',
]

接着就可以很简单在Template中的变数後面加上|後套入使用,如此一来NT$ 12345就会变成NT$ 12,345了。
而时间格式也可以使用filter带入,如下方code所示。

{% load humanize %}

<!-- …略… -->

      <td>NT$ {{ order.amount|intcomma }}</td>
      <td>{{ order.create_time|date:'Y-m-d H:i' }}</td>      

付款方式、付款状态的地方,我们就可以套用写好的TemplateTags filter,分别是convert_pay_typeconvert_pay_status,使用方法和上面一样。

{% load order_converter %}

<!-- …略… -->

      <td>{{ order.pay_type|convert_pay_type }}</td>
      <td>{{ order.pay_status|convert_pay_status }}</td>

如此一来,就可以将付款状态例如S转换成付款成功的文字表示。

处理付款状态的表格Row颜色

我们可依据订单的付款状态,来改变Bootstrap的颜色。
所以这里很简单依状态,将付款失败改成table-danger或尚未付款改为table-info。

额外资讯或功能

当订单在付款成功的状态,我们颜色就是维持白色系,额外资讯也会保持空白。

  1. 如果是ATM转帐「尚未付款或付款失败」:我们会将所需要转入的付款虚拟帐号再带出来,让顾客可以再次执行转帐动作。(如果可以,应该把失效期限也跟顾客说明)
  2. 如果是信用卡「尚未付款或付款失败」:我们会将线上刷卡的网址再带给顾客进行刷卡,但当然有可能带出来的页面已失效无法使用。

处理分页程序

在分页的处理,我们原先从View转至Template时,记得page_orders物件是怎麽来的吗?
看一下这三行,我们是使用了paginator的page()取回仍具备有分页功能的物件。

    orders_total = get_my_orders()
    paginator = Paginator(orders_total, 10)
    page_orders = paginator.page(page_number)

所以在Template中,我们依旧可使用其相关的功能来作判断或进行分页页数的取得。

number:目前的页数
has_previous:判断是否有上一页 (如果没有,表示就在第一页)
previous_page_number:如果有,则自动取回上一页的页码。

has_next:判断是否有下一页 (如果没有,表示就在最後一页)
next_page_number:如果有,则自动取回下一页的页码。
paginator.num_pages:取回总页数

则我们可以依这些好用的属性,带入我们的URL中进去换页的page参数。

好罗,可以看一下我们的画面了!

https://ithelp.ithome.com.tw/upload/images/20211009/201303541H3JI1tuJu.png

这样就把我的订单页面完成了!

明天开始就回顾一下这些日子来,对丰收款的一些使用与设计上的心得,之後剩下几天再来看要谈哪些没谈到的议题,或者是来研究一点Shioaji了。


<<:  [Day 24] SQL union / union all

>>:  Day24 - Time complexity (DS篇)

[Day5] HTTP Header Injection - HTTP Header 注入

前言 在上一篇的HTTP请求走私之後,已经知道HTTP Header也可以被拿来利用,这篇会更直接的...

[面试][资料库]关联式资料库要如何设计避免超卖?

库存只剩 1 件,但却有 10 个人买到? 网路商城特卖会常常会推出特定商品限量 1 组的抢购活动...

C# 入门之处理用户的输入

在很多情况下,我们的程序,通过命令终端与用户交互。让用户输入,yes 或 no 是一种很常见的场景。...

如何兼顾 产品开发 与 品质维护

软件开发中,最怕遇到的就是前面有新功能的开发在赶,後面有线上的 bug 在等着处理,呈现蜡烛两头烧的...

EP29 - 秽土转生~到了 AWS 也要能够备份~

在 EP13 - 灾难演练,重建你的 VPC, 我们在重建 VPC 之前, 有带着大家怎麽进行单次备...