iT邦幫忙

2022 iThome 鐵人賽

DAY 29
0

Day 29 大花錢,Django 訂單串接金流

今天要來將昨天的購物車生成訂單,填寫訂購人資訊後串接第三方金流服務,直接結帳付款,事不宜遲,馬上就來大花錢吧!

新增訂單模型

在 orders 應用程式的 models.py 建立 Order 模型。因為多筆訂單可能會對應到多筆商品,這裡會使用到 ManyToManyField 關聯欄位。

status 欄位則是訂單的當前狀態,分為未付款、付款失敗、等待出貨、運送中、已取消,預設欄位是未付款。

order_id 是訂單編號,在 save 函數會判斷建立訂單時自動新增為特定的編號格式。

# orders/models.py

from django.db import models
from django.utils.translation import gettext_lazy as _

# Create your models here.

class Order(models.Model):
    '''
    訂單
    '''
    order_id = models.CharField(_('Order Id'), max_length=20)
    email = models.EmailField('E-mail', max_length=255)
    product = models.ManyToManyField('products.Product', related_name='order_set', through='products.RelationalProduct')
    name = models.CharField(_('Name'), max_length=50)
    phone = models.CharField(_('Phone'), max_length=50)
    district = models.CharField(_('District'), max_length=50)
    zipcode = models.CharField(_('Zip Code'), max_length=50)
    address = models.CharField(_('Address'), max_length=255)
    total = models.PositiveIntegerField(_('Total'), default=0)
    created = models.DateTimeField(_('Created Date'), auto_now_add=True)
    modified = models.DateTimeField(_('Modified Date'), auto_now=True)
    status = models.CharField(
        _('Status'), 
        max_length=100, 
        choices=(("unpaid", _("Unpaid")), ("payment_fail", _("Payment Fail")), ("waiting_for_shipment", _("Waiting for shipment")), ("transporting", _("Transporting")), ("completed", _("Completed")), ("cancelled", _("Cancelled"))), 
        default="unpaid"
    )
    
    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if not self.order_id:
            self.order_id = f'ORDER{self.id:08}'
            super().save(*args, **kwargs)
    
    class Meta:
        verbose_name = '訂單'
        verbose_name_plural = '訂單'

    def __str__(self):
        return f'{self.order_id}'

在 Django 預設的 ManyToManyField 會自動生成中間表,這次我們要 products 應用程式的模型自定義中間表 RelationalProduct,為了加上商品數量欄位,並且使用 property 定義商品名稱、價格就能在管理介面快速查看。

# products/models.py

class RelationalProduct(models.Model):
    product = models.ForeignKey('products.Product', on_delete=models.CASCADE, verbose_name='商品名稱')
    order = models.ForeignKey('orders.Order', on_delete=models.CASCADE)
    number = models.IntegerField('數量', default=1)

    @property
    def name(self):
        return self.product.name

    @property
    def price(self):
        return self.product.price

    def __str__(self):
        return ""

建立資料庫遷移檔案

docker exec --workdir /opt/app/web example_tenant_web \
    python3.10 manage.py makemigrations

...

Migrations for 'orders':
  orders/migrations/0001_initial.py
    - Create model Order
  orders/migrations/0002_order_product.py
    - Add field product to order
Migrations for 'products':
  products/migrations/0004_relationalproduct.py
    - Create model RelationalProduct

執行資料庫遷移

docker exec --workdir /opt/app/web example_tenant_web \
    python3.10 manage.py migrate

...

=== Starting migration
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, core, customers, django_q, epaper, orders, products, sessions, sites
Running migrations:
  Applying orders.0001_initial...
 OK
  Applying products.0004_relationalproduct...
 OK
  Applying orders.0002_order_product...
 OK
=== Starting migration
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, core, customers, django_q, epaper, orders, products, sessions, sites
Running migrations:
  Applying orders.0001_initial...
 OK
  Applying products.0004_relationalproduct...
 OK
  Applying orders.0002_order_product...
 OK
=== Starting migration
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, core, customers, django_q, epaper, orders, products, sessions, sites
Running migrations:
  Applying orders.0001_initial...
 OK
  Applying products.0004_relationalproduct...
 OK
  Applying orders.0002_order_product...
 OK

訂單管理介面

在 orders 應用程式的 admin.py 新增訂單管理介面 OrderAdmin 與內嵌商品列表管理介面 RelationalProductInline。

在內嵌商品列表管理介面會使用在 RelationalProduct 模型透過 property 定義的商品名稱、訂單總價格欄位,並且全部設定為只可查看不可修改的 readonly_fields 欄位。

# orders/admin.py

from django.contrib import admin

# Register your models here.
from orders.models import Order
from products.models import RelationalProduct

class RelationalProductInline(admin.TabularInline):
    model = RelationalProduct
    model = Order.product.through
    verbose_name = '商品名稱'
    extra = 2
    fields = ('name', 'price', 'number')
    readonly_fields = ('name', 'price', 'number')

    def has_add_permission(self, request, obj):
        return False

    def has_delete_permission(self, request, obj=None):
        return False

class OrderAdmin(admin.ModelAdmin):
    model = Order
    search_fields = ['order_id', 'name']
    fields = ('order_id', 'name', 'email', 'phone', 'district', 'zipcode', 'address', 'total', 'status', 'created', 'modified')
    list_display = ('order_id', 'name', 'email', 'total')
    list_filter = ('status',)
    readonly_fields = ('order_id', 'name', 'email', 'phone', 'district', 'zipcode', 'address', 'total', 'created', 'modified')
    inlines = [RelationalProductInline,]

admin.site.register(Order, OrderAdmin)

建立結帳功能

我們將使用『裝潢大廳,套用 Template 版面』中的模板 aviato/checkout.html 來套用,會有左側的訂購人資訊表單與右側的購物車商品列表與訂單總金額。

結帳表單

從 Order 模型中取出欄位並生成結帳表單 ModelForm。

widgets 為根據套用的模板 form 定義的 class 屬性。

# orders/forms.py

from django import forms
from orders.models import Order

class OrderForm(forms.ModelForm):
    class Meta:
        model = Order
        fields = (
            'email',
            'name',
            'phone',
            'district',
            'zipcode',
            'address',
        )
        widgets = {
            'name': forms.TextInput(attrs={'class': 'form-control'}),
            'address': forms.TextInput(attrs={'class': 'form-control'}),
            'zipcode': forms.TextInput(attrs={'class': 'form-control'}),
            'district': forms.TextInput(attrs={'class': 'form-control'}),
            'email': forms.TextInput(attrs={'class': 'form-control'}),
            'phone': forms.TextInput(attrs={'class': 'form-control'}),
        }

結帳視圖

在 orders 應用程式的 views.py 繼承 FormView 來建立 CheckoutView 結帳視圖。

透過 cookie 取得 cart 資料來顯示右側的購物車資料與訂單總金額。

在表單通過驗證後,透過 cookie 取得 cart 資料儲存至訂單的 product 欄位,並計算訂單總金額儲存至 total 欄位,最後將訂單編號儲存至 context 傳遞至前往付款頁面。

# orders/views.py

# ...

class CheckoutView(FormView):
    template_name = "orders/checkout.html"   # 結帳頁面

    form_class = OrderForm
    success_url = 'orders/confirmation.html' # 前往付款頁面

    def get_cart_cookie(self, request):
        cart_str = request.COOKIES.get('cart', '')
        product_dict = {}
        if cart_str:
            cart_bytes = cart_str.encode()
            cart_bytes = base64.b64decode(cart_bytes)
            cart_dict = pickle.loads(cart_bytes)
        return cart_dict
    
    def get(self, request, *args, **kwargs):
        cart_dict = self.get_cart_cookie(request)
        if cart_dict:
            product_dict = cart_dict.copy()
            total = 0
            for product_id in cart_dict:
                if product := Product.objects.filter(id=product_id):
                    product_dict[product_id]["product"] = product.first()
                    total += int(product_dict[product_id]["count"]) * int(product.first().price)
                else:
                    del product_dict[product_id]
        context = self.get_context_data(**kwargs)
        context["product_dict"] = product_dict
        context["total"] = total
        return self.render_to_response(context)
    
    def form_valid(self, form):
        self.object = form.save(commit=False)
        cart_dict = self.get_cart_cookie(self.request)
        self.object.save()
        if cart_dict:
            total = 0
            for product_id in cart_dict:
                if product := Product.objects.filter(id=product_id):
                    RelationalProduct.objects.create(order=self.object, product=product.first(), number=cart_dict[product_id]["count"])
                    total += int(cart_dict[product_id]["count"]) * int(product.first().price)
        self.object.total = total
        self.object.save()
        context = self.get_context_data(form=form)
        context['order_id'] = self.object.order_id
        return render(self.request, self.success_url, context=context)

新增 URLS 路由

checkout 對應 結帳視圖

以下為 urls.py:

# orders/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('cart/', views.CartView.as_view(), name='cart'),
    path('addcart/<int:product_id>/', views.AddCartView.as_view(), name='add_cart'),
    path('deletecart/<int:product_id>/', views.DeleteCartView.as_view(), name='delete_cart'),
    path('checkout/', views.CheckoutView.as_view(), name='checkout'),
]

購物車頁面新增結帳頁面 url

調整原本的 checkout 按鈕

<!-- orders/templates/orders/cart.html -->

<!-- ... -->

<a href="{% url 'orders:checkout' %}" class="btn btn-main pull-right">Checkout</a>

<!-- ... -->

結帳頁面模板

使用結帳視圖傳遞過來的 form 參數生成表單,這裡因為要套用模板,所以逐個欄位進行套用。

<!-- orders/templates/orders/checkout.html -->

{% extends "base.html" %}
{% load static %}
{% load i18n %}

{% block content %}
<section class="page-header">
   <div class="container">
      <div class="row">
         <div class="col-md-6">
            <ol class="breadcrumb">
               <li><a href="{% url 'products:home' %}">{% translate "Home page" %}</a></li>
               <li><a href="{% url 'orders:cart' %}">{% translate "Shopping cart" %}</a></li>
               <li class="active">{% translate "Checkout" %}</li>
            </ol>
         </div>
   </div>
   </div>
</section>
<div class="page-wrapper">
   
   <div class="checkout shopping">
      <div class="container">
         <div class="row">
            <div class="col-md-8">
               <div class="block billing-details">
                  <h4 class="widget-title">Billing Details</h4>
                  <form method="post" class="checkout-form">{% csrf_token %}
                     <div class="form-group">
                        <label for="name">{{ form.name.label }}</label>
                        {{ form.name }}
                     </div>
                     <div class="form-group">
                        <label for="email">{{ form.email.label }}</label>
                        {{ form.email }}
                     </div>
                     <div class="form-group">
                        <label for="phone">{{ form.phone.label }}</label>
                        {{ form.phone }}
                     </div>
                     <div class="checkout-country-code clearfix">
                        <div class="form-group">
                           <label for="zipcode">{{ form.zipcode.label }}</label>
                           {{ form.zipcode }}
                        </div>
                        <div class="form-group" >
                           <label for="district">{{ form.district.label }}</label>
                           {{ form.district }}
                        </div>
                     </div>
                     <div class="form-group">
                        <label for="address">{{ form.address.label }}</label>
                        {{ form.address }}
                     </div>
                     <input type="submit" class="btn btn-main mt-20" value="{% translate 'Place Order' %}">
                  </form>
               </div>
            </div>
            <div class="col-md-4">
               <div class="product-checkout-details">
                  <div class="block">
                     <h4 class="widget-title">Order Summary</h4>
                     {% for product_id, product in product_dict.items %}
                        <div class="media product-card">
                           <a class="pull-left" href="{% url 'products:detail' product_id %}">
                              {% if product.product.product_image_set.all %}
                                 {% if product.product.product_image_set.all.0.image %}
                                    <img class="media-object" src="{{ product.product.product_image_set.all.0.image.url }}" alt="Image" />
                                 {% endif %}
                              {% endif %}
                           </a>
                           <div class="media-body">
                              <h4 class="media-heading"><a href="{% url 'products:detail' product_id %}">{{ product.product.name }}</a></h4>
                              <p class="price">{{ product.count }} x ${{ product.product.price }}</p>
                              <a href="javascript:void(0)" onclick="getAjax('{% url 'orders:delete_cart' product_id %}', '已移除商品', 'true');" class="remove" >Remove</a>
                           </div>
                        </div>
                     {% endfor %}
                     <ul class="summary-prices"></ul>
                     <div class="summary-total">
                        <span>Total</span>
                        <span>${{ total }}</span>
                     </div>
                  </div>
               </div>
            </div>
         </div>
      </div>
   </div>
</div>

前往付款頁面

接收 order_id 參數並將訂單傳遞給綠界科技金流表單(會在後面實作)

<!-- orders/templates/orders/confirmation.html -->

{% extends "base.html" %}
{% load static %}
{% load i18n %}

{% block content %}
<!-- Page Wrapper -->
<section class="page-wrapper success-msg">
  <div class="container">
    <div class="row">
      <div class="col-md-6 col-md-offset-3">
        <div class="block text-center">
          <i class="tf-ion-android-checkmark-circle"></i>
          <h2 class="text-center">訂單已建立完成,請立即前往付款</h2>
          <form method="post" action="{% url 'orders:ecpay' %}">
            {% csrf_token %}
            <input type="hidden" name="order_id" value="{{ order_id }}" />
            <button type="submit" class="btn btn-main">前往付款</button>
          </form>
        </div>
      </div>
    </div>
  </div>
</section><!-- /.page-warpper -->
{% endblock content %}

綠界科技金流介接

API 參考文件

https://www.ecpay.com.tw/Service/API_Dwnld

Python SDK

https://github.com/ECPay/ECPayAIO_PYTHON

下載 sdk 中的 ecpay_payment_sdk.py 後放入 orders 應用程式目錄。

Ngork 測試

因需要 https 才能通過,需使用 ngork 進行測試

docker run -it -e NGROK_AUTHTOKEN=<token> ngrok/ngrok http 8000

取得 ngork domain 後加入至 example01 租戶 domain,以租戶 example01 為例,將 domain 加入至以下租戶設定檔:

http://example01.localhost:8000/admin/customers/client/1/change/

新增 URLS 路由

ecpay 對應 綠界科技金流表單視圖
return 對應 綠界科技金流伺服器端回應視圖
orderresult 對應 綠界科技金流用戶端端回應視圖
order_success 對應 付款成功視圖
order_fail 對應 付款失敗視圖

以下為 urls.py:

# orders/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('cart/', views.CartView.as_view(), name='cart'),
    path('addcart/<int:product_id>/', views.AddCartView.as_view(), name='add_cart'),
    path('deletecart/<int:product_id>/', views.DeleteCartView.as_view(), name='delete_cart'),
    path('checkout/', views.CheckoutView.as_view(), name='checkout'),
    path('ecpay/', views.ECPayView.as_view(), name='ecpay'),
    path('return/', views.ReturnView.as_view(), name='return'),
    path('orderresult/', views.OrderResultView.as_view(), name='orderresult'),
    path('order_success/', views.OrderSuccessView.as_view(), name='order_success'),
    path('order_fail/', views.OrderFailView.as_view(), name='order_fail'),
]

綠界科技金流表單視圖

取得前往付款頁面傳遞來的 order_id 來生成訂單資訊。

order_params 為綠界科技金流的需求欄位,可參考 API 文件定義。

透過 API 最後會生成一個 HTML 表單,將此表單存入 ecpay_form 變數,並傳遞給綠界科技金流表單頁面。

# orders/views.py

# ...

class ECPayView(TemplateView):
    template_name = "orders/ecpay.html"

    def post(self, request, *args, **kwargs):
        scheme = request.is_secure() and "https" or "http"
        domain = request.META['HTTP_HOST']

        order_id = request.POST.get("order_id")
        order = Order.objects.get(order_id=order_id)
        product_list = "#".join([product.name for product in order.product.all()])
        order_params = {
            'MerchantTradeNo': order.order_id,
            'StoreID': '',
            'MerchantTradeDate': datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
            'PaymentType': 'aio',
            'TotalAmount': order.total,
            'TradeDesc': order.order_id,
            'ItemName': product_list,
            'ReturnURL': f'{scheme}://{domain}/orders/return/', # ReturnURL為付款結果通知回傳網址,為特店server或主機的URL,用來接收綠界後端回傳的付款結果通知。
            'ChoosePayment': 'ALL',
            'ClientBackURL': f'{scheme}://{domain}/products/list/', # 消費者點選此按鈕後,會將頁面導回到此設定的網址(返回商店按鈕)
            'ItemURL': f'{scheme}://{domain}/products/list/', # 商品銷售網址
            'Remark': '交易備註',
            'ChooseSubPayment': '',
            'OrderResultURL': f'{scheme}://{domain}/orders/orderresult/', # 消費者付款完成後,綠界會將付款結果參數以POST方式回傳到到該網址
            'NeedExtraPaidInfo': 'Y',
            'DeviceSource': '',
            'IgnorePayment': '',
            'PlatformID': '',
            'InvoiceMark': 'N',
            'CustomField1': '',
            'CustomField2': '',
            'CustomField3': '',
            'CustomField4': '',
            'EncryptType': 1,
        }
        # 建立實體
        ecpay_payment_sdk = module.ECPayPaymentSdk(
            MerchantID='2000132',
            HashKey='5294y06JbISpM5x9',
            HashIV='v77hoKGq4kWxNNIS'
        )
        # 產生綠界訂單所需參數
        final_order_params = ecpay_payment_sdk.create_order(order_params)

        # 產生 html 的 form 格式
        action_url = 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'  # 測試環境
        # action_url = 'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5' # 正式環境
        ecpay_form = ecpay_payment_sdk.gen_html_post_form(action_url, final_order_params)
        context = self.get_context_data(**kwargs)
        context['ecpay_form'] = ecpay_form
        return self.render_to_response(context)

綠界科技金流表單頁面

使用 模板語法 {% autoescape off %}{% endautoescape %} 將要直接執行 HTML 包在其中,即可生成表單而不是純文字,綠界金流表單會自動送出後直接導轉至綠界科技金流付款頁面(第三方)。

<!-- orders/templates/orders/ecpay.html -->

{% autoescape off %}{{ecpay_form}}{% endautoescape %}

伺服器端回應視圖

訂購人付款後綠界科技金流會回傳一個 POST 回應至伺服器端,這裡要寫一個 ReturnView 視圖驗證檢查碼後回應 '1|OK' 代表成功。

# orders/views.py

# ...

class ReturnView(View):
    
    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        return super(ReturnView, self).dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        ecpay_payment_sdk = module.ECPayPaymentSdk(
            MerchantID='2000132',
            HashKey='5294y06JbISpM5x9',
            HashIV='v77hoKGq4kWxNNIS'
        )
        res = request.POST.dict()
        back_check_mac_value = request.POST.get('CheckMacValue')
        check_mac_value = ecpay_payment_sdk.generate_check_value(res)
        if check_mac_value == back_check_mac_value:
            return HttpResponse('1|OK')
        return HttpResponse('0|Fail')

用戶端回應視圖

訂購人付款後綠界科技金流會回傳一個 POST 回應至用戶端,這裡要寫一個 OrderResultView視圖驗證檢查碼、RtnMsg 付款結果訊息、RtnCode回應代碼,皆通過後導轉至付款成功頁面並將訂單狀態更新為等待出貨,付款失敗則導轉至付款失敗頁面。

# orders/views.py

# ...

class OrderResultView(View):
    
    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        return super(OrderResultView, self).dispatch(request, *args, **kwargs)
    
    def post(self, request, *args, **kwargs):

        ecpay_payment_sdk = module.ECPayPaymentSdk(
            MerchantID='2000132',
            HashKey='5294y06JbISpM5x9',
            HashIV='v77hoKGq4kWxNNIS'
        )
        res = request.POST.dict()
        back_check_mac_value = request.POST.get('CheckMacValue')
        order_id = request.POST.get('MerchantTradeNo')
        rtnmsg = request.POST.get('RtnMsg')
        rtncode = request.POST.get('RtnCode')
        check_mac_value = ecpay_payment_sdk.generate_check_value(res)
        if check_mac_value == back_check_mac_value and rtnmsg == 'Succeeded' and rtncode == '1':
            order = Order.objects.get(order_id=order_id)
            order.status = 'waiting_for_shipment'
            order.save()
            return HttpResponseRedirect('/orders/order_success/')
        return HttpResponseRedirect('/orders/order_fail/')

付款成功視圖

只需呈現付款成功模板

# orders/views.py

# ...

class OrderSuccessView(TemplateView):
    template_name = "orders/order_success.html"

    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

付款成功頁面

顯示付款成功訊息

<!-- orders/templates/orders/order_success.html -->

{% extends "base.html" %}
{% load static %}
{% load i18n %}

{% block content %}
<!-- Page Wrapper -->
<section class="page-wrapper success-msg">
  <div class="container">
    <div class="row">
      <div class="col-md-6 col-md-offset-3">
        <div class="block text-center">
          <i class="tf-ion-android-checkmark-circle"></i>
          <h2 class="text-center">付款成功</h2>
          <a href="shop.html" class="btn btn-main mt-20">Continue Shopping</a>
        </div>
      </div>
    </div>
  </div>
</section><!-- /.page-warpper -->
{% endblock content %}

付款失敗視圖

只需呈現付款失敗模板

# orders/views.py

# ...

class OrderFailView(TemplateView):
    template_name = "orders/order_fail.html"

    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

付款失敗頁面
顯示付款失敗訊息

<!-- orders/templates/orders/order_fail.html -->

{% extends "base.html" %}
{% load static %}
{% load i18n %}

{% block content %}
<!-- Page Wrapper -->
<section class="page-wrapper success-msg">
  <div class="container">
    <div class="row">
      <div class="col-md-6 col-md-offset-3">
        <div class="block text-center">
          <i class="tf-ion-android-checkmark-circle"></i>
          <h2 class="text-center">付款失敗,請重新下單</h2>
          <a href="shop.html" class="btn btn-main mt-20">Continue Shopping</a>
        </div>
      </div>
    </div>
  </div>
</section><!-- /.page-warpper -->
{% endblock content %}

到這裡綠界科技金流就串接完成了!

功能展示

購物車頁面,點選 checkout 進入結帳頁面。
https://ithelp.ithome.com.tw/upload/images/20221011/201516569JR4FQwuzK.png

結帳頁面左側為訂購人資訊,右側為購物清單。
https://ithelp.ithome.com.tw/upload/images/20221011/20151656R3fRNCFSFu.png

訂單建立完成,前往付款頁面。
https://ithelp.ithome.com.tw/upload/images/20221011/20151656tY6R3YIMQG.png

綠界金流付款頁面(第三方)。
https://ithelp.ithome.com.tw/upload/images/20221011/20151656BbbrGgrBet.png

綠界金流付款頁面(第三方)送出付款資料。
https://ithelp.ithome.com.tw/upload/images/20221011/201516563Tmnt649HY.png

綠界金流交易安全驗證頁面(第三方)。
https://ithelp.ithome.com.tw/upload/images/20221011/20151656UI0nfhYfcG.png

付款成功,導轉至付款成功頁面。
https://ithelp.ithome.com.tw/upload/images/20221011/20151656e4NMy56Now.png
在訂單管理介面確認訂單。
https://ithelp.ithome.com.tw/upload/images/20221012/20151656PYz28uPfHs.png
完成!!

今天不只完成了訂單也實作了金流串接,最後一天將會介紹如何進行部署,下一回『改造完成,雲端部署 Django 多租戶架構電商網站』。


上一篇
Day 28 全都要買,Django 實作購物車功能
下一篇
Day 30 改造完成,雲端部署 Django 多租戶架構電商網站
系列文
全能住宅改造王,Django 多租戶架構的應用 —— 實作一個電商網站30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言