django by example 實(shí)踐 myshop 項(xiàng)目(二)


點(diǎn)我查看本文集的說明及目錄朝抖。


本項(xiàng)目相關(guān)內(nèi)容包括:

實(shí)現(xiàn)過程

CH7 創(chuàng)建在線商店

CH8 管理支付和訂單

CH9 擴(kuò)展商店


CH8 支付和訂單管理


上一章娜谊,我們新建了一個(gè)包含商品目錄和訂單系統(tǒng)的基礎(chǔ)在線網(wǎng)站涧至。我們還學(xué)習(xí)了使用 Celery 加載異步任務(wù)。本章退个,我們將學(xué)習(xí)如何在網(wǎng)站中集成支付網(wǎng)站募壕,還將擴(kuò)展 admin網(wǎng)站來管理訂單并輸出不同格式的訂單。

本章包含以下內(nèi)容:

  • 在項(xiàng)目中集成支付網(wǎng)關(guān)
  • 管理支付通知
  • 將訂單輸出到 CSV 文件
  • 為 admin網(wǎng)站創(chuàng)建自定義視圖
  • 自動(dòng)生成 PDF發(fā)貨單

集成支付網(wǎng)關(guān)


支付網(wǎng)關(guān)可以實(shí)現(xiàn)用戶在線支付语盈。支付網(wǎng)關(guān)可以用來管理用戶訂單并將付款過程委托給可靠司抱、安全的第三方。這意味著我們不需要在自己的系統(tǒng)中存儲(chǔ)信用卡黎烈。

我們可以選擇很多支付網(wǎng)關(guān)供應(yīng)商提供的服務(wù)。這里將集成最流行的支付網(wǎng)關(guān) Paypal匀谣。

PayPal 提供幾種集成網(wǎng)關(guān)的方法照棋。標(biāo)準(zhǔn)集成方法包括一個(gè) Buy now 按鈕(我們?cè)谄渌W(wǎng)站看到過),按鈕將用戶重定向到 PayPal 處理支付過程武翎。我們將在網(wǎng)站集成包含自定義 Buy now 按鈕的 Paypal 標(biāo)準(zhǔn)支付烈炭。Paypal 將處理支付過程并向我們的服務(wù)器發(fā)送支付狀態(tài)通知。

創(chuàng)建PayPal賬號(hào)


在網(wǎng)站中集成 Paypal 需要 Paypal 商家賬戶宝恶,如果你沒有 Paypal 賬戶符隙,請(qǐng)?jiān)?https://www.paypal.com/c2/home 創(chuàng)建商家賬戶,如下圖所示:

CH8-1.png

筆者注:

原文需要確保創(chuàng)建的是商家賬戶并選擇 Paypal 標(biāo)準(zhǔn)解決方案垫毙,但筆者注冊(cè)過程中沒有選擇 Paypal 標(biāo)準(zhǔn)方案的選項(xiàng)霹疫,因此只是進(jìn)行了商家賬戶注冊(cè)。

在注冊(cè)表單中填入詳細(xì)信息并完成注冊(cè)综芥, Paypal 將向你發(fā)送郵件來驗(yàn)證賬戶丽蝎。

安裝 django-paypal

Django-Paypal 是簡(jiǎn)化集成 Paypal 的第三方應(yīng)用。我們使用它在商店中集成 Paypal 支付標(biāo)準(zhǔn)解決方案膀藐。django-paypal 的文檔請(qǐng)查閱 http://django-paypal.readthedocs.io/en/stable/屠阻。

在 shell 中運(yùn)行以下命令來安裝 django-paypal:

pip install django-paypal 

編輯項(xiàng)目的 settings.py 文件在 INSTALLED_APPS 中添加 'paypal.standard.ipn' :

INSTALLED_APPS = ['django.contrib.admin', 'django.contrib.auth',
                  'django.contrib.contenttypes', 'django.contrib.sessions',
                  'django.contrib.messages', 'django.contrib.staticfiles',
                  'shop', 'cart', 'orders', 'paypal.standard.ipn']

這是 django-paypal 提供的集成 Paypal 支付標(biāo)準(zhǔn)和即時(shí)支付通知(IPN) 的應(yīng)用红省。我們稍后再處理付款通知。

在 myshop 的settings.py 文件中為 django-paypal 進(jìn)行以下配置:

# django-paypal settings

PAYPAL_RECEIVER_EMAIL = 'mypaypalemail@myshop.com'
PAYPAL_TEST = True

這些設(shè)置包括:

  • PAYPAL_RECEIVER_EMAIL: 你的 Paypal 賬戶郵箱国觉。使用創(chuàng)建 Paypal 賬戶時(shí)的郵箱代替 'mypaypalemail@myshop.com 吧恃。

  • PAYPAL_TEST:表示是否使用 Paypal 的沙盒環(huán)境來處理付款的布爾值。沙盒允許用戶在遷移到生產(chǎn)環(huán)境之前測(cè)試 PayPal 集成麻诀。

打開 shell 并運(yùn)行以下命令來同步 django-paypal 的模型:

python manage.py migrate

你應(yīng)該可以看到這樣的輸出:

  Applying ipn.0001_initial... OK
  Applying ipn.0002_paypalipn_mp_id... OK
  Applying ipn.0003_auto_20141117_1647... OK
  Applying ipn.0004_auto_20150612_1826... OK
  Applying ipn.0005_auto_20151217_0948... OK
  Applying ipn.0006_auto_20160108_1112... OK
  Applying ipn.0007_auto_20160219_1135... OK
  Applying orders.0001_initial... OK

django-paypal 的模型現(xiàn)在已經(jīng)同步到數(shù)據(jù)庫了痕寓。我們還需要將 django-paypal 的 URL 模式添加到項(xiàng)目中。編輯 myshop 目錄下的 urls.py 文件并添加以下 URL模式针饥。記得將其放在 shop.urls 模式之前以避免錯(cuò)誤的模式匹配:

url(r'paypal/',include('paypal.standard.ipn.urls')),

接下來厂抽,我們將支付網(wǎng)關(guān)添加到結(jié)算過程。

添加支付網(wǎng)關(guān)


結(jié)算過程這樣工作:

  1. 將商品添加到購物車丁眼;

  2. 結(jié)算購物車中的商品筷凤;

  3. 重定向到 Paypal 進(jìn)行支付;

  4. Paypal 向網(wǎng)站發(fā)送支付通知苞七;

  5. Paypal 將用戶重定向到網(wǎng)站藐守。

使用以下命令在項(xiàng)目中新建一個(gè)應(yīng)用:

python manage.py startapp payment

我們使用這個(gè)應(yīng)用管理支付過程和用戶支付。

編輯項(xiàng)目的 settings.py 文件并將 payment 添加到INSTALLED_APPS中:

INSTALLED_APPS = ['django.contrib.admin', 'django.contrib.auth',
                  'django.contrib.contenttypes', 'django.contrib.sessions',
                  'django.contrib.messages', 'django.contrib.staticfiles',
                  'shop', 'cart', 'orders', 'paypal.standard.ipn', 'payment']

payment 應(yīng)用現(xiàn)在已經(jīng)激活了蹂风,編輯 orders 應(yīng)用的 views.py 文件并確認(rèn)包含下面的 import :

from django.urls import reverse
from django.shortcuts import render, redirect

將 order_create 視圖中的下列代碼:

# launch asynchronous task
order_created.delay(order.id)
return render(request, 'orders/order/created.html',
              {'order': order})

修改為:

# launch asynchronous task
order_created.delay(order.id)
# set the order in the session
request.session['order_id'] = order.id
# redirect to the payment
return redirect(reverse('payment:process'))

成功創(chuàng)建一個(gè)訂單后卢厂,我們使用 order_id 會(huì)話關(guān)鍵詞將訂單 ID 設(shè)置到當(dāng)前會(huì)話中。然后惠啄,重定向到 ‘payment:process’ URL 慎恒。

編輯 payment 應(yīng)用的 views.py 文件并添加以下代碼:

from decimal import Decimal

from django.conf import settings
from django.core.urlresolvers import reverse
from django.shortcuts import render, get_object_or_404
from orders.models import Order
from paypal.standard.forms import PayPalPaymentsForm


# Create your views here.

def payment_process(request):
    order_id = request.session.get('order_id')
    order = get_object_or_404(Order, id=order_id)
    host = request.get_host()

    paypal_dict = {'business': settings.PAYPAL_RECEIVER_EMAIL,
        'amount': '%.2f' % order.get_total_cost().quantize(Decimal('.01')),
        'item_name': 'Order {}'.format(order.id), 'invoice': str(order.id),
        'currency_code': 'USD',
        'notify_url': 'http://{}{}'.format(host, reverse('paypal-ipn')),
        'return_url': 'http://{}{}'.format(host, reverse('payment:done')),
        'cancel_return': 'http://{}{}'.format(host,
                                              reverse('payment:canceled')), }
    form = PayPalPaymentsForm(initial=paypal_dict)
    return render(request, 'payment/process.html',
                  {'order': order, 'form': form})

在 patment_process 視圖中,我們生成了一個(gè)自定義 Paypal Buy now 表單來支付訂單撵渡。首先融柬,根據(jù) order_id (剛剛在 order_create 視圖中添加的)從會(huì)話中獲得當(dāng)前訂單。獲得給定 ID 的 Order 對(duì)象并新建一個(gè)包含以下字段的 PayPalPaymentForm 表單:

  • business:處理支付的 PayPal 商家賬號(hào)趋距。這里使用設(shè)置中定義的 PAYPAL_RECEIVER_EMAIL 郵箱賬戶粒氧。
  • amount:用戶需要支付的總費(fèi)用。
  • item_name:將要銷售的商品名稱节腐,由于訂單可能包含多種商品外盯,這里使用訂單 ID 。
  • invoice: 發(fā)貨單 ID 翼雀。要求每筆支付的 invoice 唯一饱苟。這里我們使用訂單 ID 。
  • currency_code:支付所用貨幣代碼锅纺。我們將其設(shè)置為表示美元的 USD 掷空。 需要與用戶賬戶中設(shè)置的貨幣相同。
  • notify_url : Paypal 發(fā)送 IPN 請(qǐng)求的 URL 。我們使用 django-paypal 提供的 paypal-ipn URL 坦弟。 該 URL 對(duì)應(yīng)的視圖處理支付通知并保存到數(shù)據(jù)庫护锤。
  • return_url:支付成功后,用戶跳轉(zhuǎn)到的頁面酿傍。我們使用后續(xù)將創(chuàng)建的 payment:done URL 烙懦。
  • cancel_return: 支付取消時(shí),用戶跳轉(zhuǎn)到的頁面赤炒。我們使用后續(xù)將創(chuàng)建的 payment:cancel URL 氯析。

PayPalPaymentForm 渲染為包含隱藏字段的標(biāo)準(zhǔn)表單,用戶僅能看到 Buy Now 按鈕莺褒。當(dāng)用戶點(diǎn)擊按鈕時(shí)掩缓,表單將通過 POST 方式提交到 PayPal 。

接下來創(chuàng)建支付成功遵岩、支付取消時(shí)用戶跳轉(zhuǎn)到的簡(jiǎn)單視圖你辣。向同一個(gè) views.py 文件添加以下代碼:

from django.views.decorators.csrf import csrf_exempt


@csrf_exempt
def payment_done(request):
    return render(request, 'payment/done.html')


@csrf_exempt
def payment_canceled(request):
    return render(request, 'payment/canceled.html')

由于 PayPal 通過 POST 方式將用戶重定向到上面的任意一個(gè)視圖,這里使用 csrf_exempt 裝飾器避免 Django 檢驗(yàn) CSRF 令牌尘执。在 payment 應(yīng)用目錄下新建名為 urls.py 的文件并添加以下代碼:

from django.conf.urls import url

from . import views

urlpatterns = [url(r'^process/$', views.payment_process, name='process'),
    url(r'^done/$', views.payment_done, name='done'),
    url(r'^canceled/$', views.payment_canceled, name='canceled'), ]

這是支付流程的 URLs舍哄。包含下面的 URL模式:

  • process:為 Paypal 表單生成 Buy Now 按鈕的視圖;
  • done:支付成功后誊锭,用戶跳轉(zhuǎn)到的頁面表悬;
  • canceled:支付取消后,用戶跳轉(zhuǎn)到的頁面丧靡。

編輯 myshop 項(xiàng)目的 urls.py 文件并在 payment 應(yīng)用中包含下面的 URL 模式:

url(r'payment/',include('payment.urls')),

記得將其放在 shop.urls 模式之前以避免錯(cuò)誤的模式匹配蟆沫。

在 payment 應(yīng)用目錄下創(chuàng)建下面的文件結(jié)構(gòu):

CH8-2.png

編輯 payment/process.html 模板并添加以下代碼:

{% extends "shop/base.html" %}

{% block title %}Pay using PayPal{% endblock %}

{% block content %}
  <h1>Pay using PayPal</h1>
  {{ form.render }}
{% endblock %}

這是渲染 PayPalPaymentForm 并展示 Buy Now 按鈕的模板。

編輯 payment/done.html 模板并添加以下代碼:

{% extends "shop/base.html" %}

{% block content %}
    <h1>Your payment was successful</h1>
    <p>Your payment has been successfully received.</p>
{% endblock %}

這是用戶支付成功后跳轉(zhuǎn)到的頁面的模板温治。

編輯 payment/canceled.html 模板并添加以下代碼:

{% extends "shop/base.html" %}

{% block content %}
    <h1>Your payment has not been processed</h1>
    <p>There was a problem processing your payment.</p>
{% endblock %}

這是用戶由于某些原因取消支付后跳轉(zhuǎn)到的頁面的模板饥追。

使用 PayPal 沙盒

在瀏覽器中打開 https://developer.paypal.com/ 并使用 Paypal 商家賬戶登錄。點(diǎn)擊 Dashboard 目錄項(xiàng)罐盔,并點(diǎn)擊左側(cè)目錄中 Sandbox 下的 accounts 選項(xiàng),應(yīng)該可以看到沙盒測(cè)試用戶列表:

CH8-3.png

我們可以看到 Paypal 自動(dòng)創(chuàng)建的商業(yè)賬戶和個(gè)人賬戶救崔。我們還可以使用 Create Accounts 按鈕創(chuàng)建新的沙盒測(cè)試賬戶惶看。

點(diǎn)擊列表中個(gè)人賬戶 Email Address ,會(huì)出現(xiàn) Profile 和 notifications 鏈接六孵,點(diǎn)擊 Profile 鏈接纬黎。你將看到測(cè)試賬戶的 e-mail 和用戶信息等消息:


CH8-4.png

在 Funding 選項(xiàng)中,我們可以找到銀行賬戶劫窒、信用卡信息和 Paypal 信用賬單本今。

測(cè)試賬戶可以使用沙盒環(huán)境在網(wǎng)站上進(jìn)行支付。回到 Profile 選項(xiàng)并點(diǎn)擊 Change password 鏈接冠息,為兩個(gè)測(cè)試賬戶創(chuàng)建自定義密碼挪凑。

打開 shell 使用 python manage.py runserver 命令啟動(dòng)開發(fā)服務(wù)器。在瀏覽器中打開 http://127.0.0.1:8000逛艰,向購物車中添加一些商品躏碳,并填寫結(jié)賬表單。當(dāng)你點(diǎn)擊 Place order 按鈕散怖,訂單將保存到數(shù)據(jù)庫菇绵,訂單 ID 將保存到當(dāng)前會(huì)話( session )中,你將重定向到支付頁面镇眷。這個(gè)頁面從會(huì)話中獲取訂單并將 Paypal 表單渲染為 Buy Now 按鈕:

CH8-5.png

我們可以通過 HTML 源碼查看生成的表單字段咬最。

筆者注:

這里一定要確保第七章中的 rabbitmq-server 和 celery 處于運(yùn)行狀態(tài)。

點(diǎn)擊 Buy Now 按鈕欠动,你將重定向到 PayPal 永乌,你將看到下面的頁面:


CH8-6.png

輸入沙盒中 buyer 賬戶的 e-mail 和密碼并點(diǎn)擊 Log In 按鈕,你將重定向到下面的頁面:

CH8-7.png

現(xiàn)在翁垂,點(diǎn)擊立即付款按鈕铆遭。最終,你將看到包含交易 ID 的確認(rèn)頁面沿猜,頁面看起來是這樣的:


CH8-9.png

點(diǎn)擊返回商家按鈕枚荣。你將重定向 PayPalPaymentsForm 表單的 return_url 字段指定的 URL。這是 payment_done 視圖的 URL啼肩。頁面看起來這樣:

CH8-10.png

支付已經(jīng)成功了橄妆。然而, PayPal 無法將支付狀態(tài)通知發(fā)送到我們的應(yīng)用祈坠。這是由于我們的項(xiàng)目運(yùn)行在外部無法訪問的 127.0.0.1 上害碾。下面我們將學(xué)習(xí)如何在互聯(lián)網(wǎng)上訪問我們的網(wǎng)站并能獲得 IPN 通知。

獲得支付通知


許多支付網(wǎng)關(guān)使用 IPN 提供的實(shí)時(shí)追蹤支付情況赦拘。網(wǎng)關(guān)處理完一個(gè)付款后會(huì)實(shí)時(shí)向網(wǎng)站服務(wù)器發(fā)送一個(gè)通知慌随。這個(gè)通知包含狀態(tài)、支付簽名(幫助我們確認(rèn)通知是否是原始通知)等所有支付細(xì)節(jié)躺同。網(wǎng)關(guān)使用獨(dú)立的 HTTP 請(qǐng)求向服務(wù)器發(fā)送這條通知阁猜。至于連接問題,Paypal 將多次嘗試通知網(wǎng)站蹋艺。

django-paypal 應(yīng)用包含兩種 IPNs 信號(hào)剃袍,這些信號(hào)包括:

  • valid_ipn_received: 從 Paypal 接收到的消息正確并且尚未保存到數(shù)據(jù)庫時(shí)觸發(fā)的信息

  • invalid_ipn_received: 從 Paypal 接收到無效數(shù)據(jù)或者格式不正確時(shí)觸發(fā)的消息

我們將創(chuàng)建一個(gè)自定義接收函數(shù)并將其連接到 valid_ipn_received 信號(hào)來確定支付。

在 payment 應(yīng)用目錄下新建一個(gè) signals 的文件夾捎谨,文件夾內(nèi)新建 __init__.py 和 handlers.py 的文件民效,并在 handlers.py 中包含以下代碼:

from django.shortcuts import get_object_or_404
from paypal.standard.ipn.signals import valid_ipn_received
from paypal.standard.models import ST_PP_COMPLETED

from orders.models import Order


def payment_notification(sender, **kwargs):
    ipn_obj = sender
    if ipn_obj.payment_status == ST_PP_COMPLETED:
        # payment was successful
        order = get_object_or_404(Order, id=ipn_obj.invoice)
        # mark the order as paid
        order.paid = True
        order.save()


valid_ipn_received.connect(payment_notification)

這里將 payment_notification 接收函數(shù)連接到 django-paypal 提供的 valid_ipn_received 信號(hào)憔维,接收函數(shù)的工作原理為:

  1. 接收發(fā)送的對(duì)象,該對(duì)象是 paypal.standard.ipn.models 中定義的 PayPalIPN 模型的實(shí)例畏邢;

  2. 檢查 payment_status 屬性是否與 django-paypal 的完成屬性一致业扒。 django-paypal 的完成屬性表示支付成功;

  3. 使用快捷函數(shù) get_object_or_404() 獲得發(fā)送給 Paypal 的訂單對(duì)象棵红。

  4. 將訂單對(duì)象的 paid 屬性設(shè)置為 True 凶赁,表示支付成功。

我們需要確保加載了信號(hào)逆甜,這樣 valid_ipn_received 觸發(fā)時(shí)才能調(diào)用接收函數(shù)虱肄。加載信息的最佳實(shí)踐是加載包含信號(hào)的應(yīng)用。我們可以通過下一節(jié)講到的定義自定義應(yīng)用配置來實(shí)現(xiàn)上述功能交煞。

筆者注:

與第六章的配置 signals 時(shí)的原因相同咏窿。我們這里使用了 signals 文件夾。

配置應(yīng)用


我們已經(jīng)在第六章了解了如何配置應(yīng)用素征,現(xiàn)在為 payment 應(yīng)用設(shè)置自定義配置來加載我們的信息接收函數(shù)集嵌。

在我們的 payment 應(yīng)用目錄的 apps.py 中添加以下代碼:

from django.apps import AppConfig


class PaymentConfig(AppConfig):
    name = 'payment'

    def ready(self):
        # import signal handlers
        from .signals import handler

我們?cè)?ready 方法中加載信號(hào)模塊,從而在應(yīng)用初始化的過程中加載信號(hào)模塊御毅。

編輯 payment 應(yīng)用的__init__.py文件并添加以下代碼:

default_app_config = 'payment.apps.AppConfig'

筆者注:

python 3.3 以上版本的模塊可以不使用 __init__.py 文件根欧,因此,最好在 INSTALLED_APPS 中使用 'payment.apps.AppConfig'端蛆,而不是在 __init__.py 文件中定義凤粗。

這樣捕发,Django 將自動(dòng)加載自定義應(yīng)用配置類廊驼。關(guān)于應(yīng)用配置的更多信息詳見 https://docs.djangoproject.com/en/1.11/ref/applications/

測(cè)試支付通知


由于在本地環(huán)境工作唧喉,我們需要確保網(wǎng)站可以被 Paypal 訪問呆躲。有一些應(yīng)用可以實(shí)現(xiàn)因特網(wǎng)訪問開發(fā)環(huán)境异逐,我們將使用最流行的 Ngrok 。

https://ngrok.com/ 為操作系統(tǒng)下載 Ngrok 并在 shell 中(Ngrok 所在的文件夾中)使用以下命令運(yùn)行:

./ngrok http 8000

這個(gè)命令告訴 Ngrok 在 8000 端口為本地主機(jī)創(chuàng)建一個(gè)通道并為其設(shè)置一個(gè)網(wǎng)絡(luò)可以訪問的主機(jī)名稱插掂。你看到的輸出應(yīng)該與下面的輸出類似:



Session Status                online                                            

Session Expires               7 hours, 59 minutes                               

Version                       2.2.8                                             

Region                        United States (us)                                

Web Interface                 http://127.0.0.1:4040                             

Forwarding                    http://4c94fca6.ngrok.io -> localhost:8000        

Forwarding                    https://4c94fca6.ngrok.io -> localhost:8000       

                                                                                

Connections                   ttl     opn     rt1     rt5     p50     p90       

                              0       0       0.00    0.00    0.00    0.00  

Ngrok 告訴我們使用開發(fā)服務(wù)器運(yùn)行在本地 8000 端口的網(wǎng)站可以分別通過 http://4c94fca6.ngrok.iohttps://4c94fca6.ngrok.io 在網(wǎng)絡(luò)上進(jìn)行訪問灰瞻。Ngrok 還提供一個(gè) URL 來訪問 web 接口(展示發(fā)送到服務(wù)器的請(qǐng)求消息)。

使用瀏覽器打開 Ngrok 提供的 URL(測(cè)試使用的是上面的 http://4c94fca6.ngrok.io )辅甥,向購物車添加幾件商品箩祥,下單,并使用 Paypal 測(cè)試賬戶進(jìn)行支付肆氓。這次,Paypal 將能夠訪問為 payment_process 視圖的 PaypalPaymentForm 的notify_url 字段生成的 URL 底瓣。如果你看下渲染的表單谢揪,你將看到HTML 表單字段是這樣的:

<input id="id_notify_url" name="notify_url" type="hidden" value="http://4c94fca6.ngrok.io/paypal/">

筆者注:

使用 http://4c94fca6.ngrok.io 需要將 ‘4c94fca6.ngrok.io’ 和 ‘127.0.0.1 加入到項(xiàng)目 settings.py 文件中的ALLOWED_HOSTS 中蕉陋。

完成支付過程后,在瀏覽器中打開http://127.0.0.1:8000/admin/ipn/paypalipn/拨扶。你將會(huì)看到一個(gè)上次支付的 IPN 對(duì)象凳鬓,它的狀態(tài)為 Completed 。 這個(gè)對(duì)象包含支付的所有信息患民,這些信息由 PayPal 發(fā)送你提供的接收 IPN 通知的 URL 缩举。IPN admin 列表頁面看起來是這樣的:

CH8-11.png

我們也可以使用 Paypal 的 IPN 仿真器加載 IPN ,仿真器地址為https://developer.paypal.com/developer/ipnSimulator/ 匹颤。仿真器可以指定字段和發(fā)送的通知類型仅孩。

除了 PayPal Payment Standard,PayPal 提供訂閱服務(wù) Website Payments Pro印蓖,它可以接受網(wǎng)站付款而無需將用戶重定向到PayPal辽慕。你可以在http://django-paypal.readthedocs.io/en/v0.4.1/pro/index.html了解如何集成 Website Payments Pro。

將訂單導(dǎo)出到 CSV 文件


有時(shí)赦肃,我們希望將模型中的數(shù)據(jù)導(dǎo)出到文件中溅蛉,以便于在其它系統(tǒng)中使用。導(dǎo)出/導(dǎo)入數(shù)據(jù)最常用的格式為CSV他宛。 CSV 文件是包含記錄的純文本文件船侧。文件中通常一行為一條記錄,記錄的字段通過一些分隔字符(通常使用逗號(hào))進(jìn)行分隔厅各。

向 admin網(wǎng)站添加自定義動(dòng)作


Django提供自定義 admin網(wǎng)站的一些選項(xiàng)镜撩。我們將為對(duì)象列表視圖增加一些自定義 admin動(dòng)作。

admin動(dòng)作的工作方式如下:用戶在 admin 對(duì)象列表頁面使用選擇框選擇對(duì)象讯检,然后選擇對(duì)選中對(duì)象執(zhí)行的操作琐鲁,最后執(zhí)行操作。下面的截圖展示了 admin 網(wǎng)站中動(dòng)作的位置人灼。

CH8_CSV_1.png

注意:

創(chuàng)建自定義 admin動(dòng)作可以幫助用戶同時(shí)對(duì)多個(gè)對(duì)象進(jìn)行操作围段。

我們可以通過創(chuàng)建接收以下參數(shù)的常規(guī)函數(shù)來創(chuàng)建自定義動(dòng)作:

  • 需要展示的當(dāng)前 ModelAdmin;
  • 當(dāng)前請(qǐng)求對(duì)象( HttpRequest 實(shí)例);
  • 表示用戶選擇的對(duì)象的 QuerySet 投放;

我們將創(chuàng)建一個(gè)自定義 admin動(dòng)作來下載訂單列表奈泪。編輯 orders 應(yīng)用的 admin.py 文件并在 OrderAdmin 之前添加以下代碼:

import csv
import datetime
from django.http import HttpResponse


def export_to_csv(modeladmin, request, queryset):
    opts = modeladmin.model._meta
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = 'attachment; \
    filename={}.csv'.format(opts.verbose_name)

    writer = csv.writer(response)

    fields = [field for field in opts.get_fields() if
              not field.many_to_many and not field.one_to_many]
    # Write a first row with header information
    writer.writerow([field.verbose_name for field in fields])
    # Write data rows
    for obj in queryset:
        data_row = []
        for field in fields:
            value = getattr(obj, field.name)
            if isinstance(value, datetime.datetime):
                value = value.strftime('%d/%m/%Y')
            data_row.append(value)
        writer.writerow(data_row)
    return response


export_to_csv.short_description = 'Export to CSV'

在上面的代碼中,我們實(shí)現(xiàn)了以下工作:

  1. 創(chuàng)建了一個(gè)包含自定義 text/csv 內(nèi)容類型的 HttpResponse 實(shí)例通知瀏覽器響應(yīng)是一個(gè) CSV文件灸芳。我們還添加了Content-Disposition 標(biāo)頭表示 HTTP 響應(yīng)包含一個(gè)附加文件涝桅;
  2. 創(chuàng)建了一個(gè) CSV writer 對(duì)象寫入 response 對(duì)象;
  3. 使用模型 _meta 選項(xiàng)的 get_fields() 方法動(dòng)態(tài)獲取 model 字段烙样。這里排除了多對(duì)多和一對(duì)多關(guān)系冯遂。
  4. 將字段名稱作為標(biāo)題行寫入文件;
  5. 對(duì)指定的 QuerySet 進(jìn)行迭代谒获,QuerySet 返回的每個(gè)對(duì)象為一行蛤肌;這里處理 datetime 格式是由于 CSV 的輸出值必須是字符串格式壁却。
  6. 通過設(shè)置函數(shù)的 short_description 屬性自定義模板中操作動(dòng)作的名稱。

我們已經(jīng)創(chuàng)建了一個(gè)可以添加到任意 ModelAdmin 類的通用 admin 動(dòng)作裸准。

最后展东,將新的 export_to_csv admin 動(dòng)作添加到 OrderAdmin 中,如下所示:

class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email', 'address',
                    'postal_code', 'city', 'paid', 'created', 'updated']
    list_filter = ['paid', 'created', 'updated']
    inlines = [OrderItemInline]
    actions = [export_to_csv]

在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order/炒俱,將會(huì)看到以下頁面:

CH8_CSV_2.png

選中一些訂單并從動(dòng)作選擇框中選擇 Export to CSV 動(dòng)作并點(diǎn)擊 Go 按鈕盐肃,瀏覽器將下載名為 order.csv 的 CSV文件。使用文本編輯器打開下載的文件权悟。你應(yīng)該可以看到以下格式的內(nèi)容砸王,包含標(biāo)題行和選中的 Order 對(duì)象:

ID,first name,last name,email,address,postal code,city,created,updated,paid
1,***,***,***@163.com,*******,*****,****,24/02/2018,24/02/2018,True

我們可以發(fā)現(xiàn),創(chuàng)建 admin動(dòng)作非常簡(jiǎn)單僵芹。

使用自定義視圖擴(kuò)展 admin網(wǎng)站


有時(shí)处硬,我們需要自定義 admin網(wǎng)站的范圍會(huì)超出 ModelAdmin 配置的范圍,比如創(chuàng)建 admin動(dòng)作拇派、覆蓋 admin模板等荷辕。這種情況下,需要?jiǎng)?chuàng)建一個(gè)自定義 admin 視圖件豌〈剑可以使用自定義視圖來實(shí)現(xiàn)任何需要的功能。需要確認(rèn)的是只有員工才能訪問視圖以及通過擴(kuò)展 admin模塊維持 admin 的統(tǒng)一風(fēng)格茧彤。

我們創(chuàng)建自定義視圖來展示訂單的信息骡显。編輯 orders 應(yīng)用的 views.py 文件并添加以下代碼:

from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import get_object_or_404
from .models import Order


@staff_member_required
def admin_order_detail(request, order_id):
    order = get_object_or_404(Order, id=order_id)
    return render(request, 'admin/orders/order/detail.html', {'order': order})

staff_member_required 裝飾器檢查請(qǐng)求頁面的用戶的 is_active 和 is_staff 字段是否均為 True 。在這個(gè)視圖中曾掂,我們通過給定的 id 獲得 Order 對(duì)象并渲染一個(gè)模板來展示訂單惫谤。

現(xiàn)在,編輯 orders 應(yīng)用的 urls.py 文件并添加以下 URL 模式:

url(r'^admin/order/(?P<order_id>\d+)/$',
    views.admin_order_detail, name='admin_order_detail'),

在 orders 應(yīng)用的 templates/ 目錄下創(chuàng)建如下的文件結(jié)構(gòu):


CH8_CSV_3.png

編輯 detail.html 模板并添加以下內(nèi)容:

{% extends "admin/base_site.html" %}
{% load static %}

{% block extrastyle %}
    <link rel="stylesheet" type="text/css" href="{% static "css/admin.css" %}"/>
{% endblock %}

{% block title %}
    Order {{ order.id }} {{ block.super }}
{% endblock %}

{% block breadcrumbs %}
    <div class="breadcrumbs">
        <a href="{% url "admin:index" %}">Home</a> &rsaquo;
        <a href="{% url "admin:orders_order_changelist" %}">Orders</a>
        &rsaquo;
        <a href="{% url "admin:orders_order_change" order.id %}">Order {{ order.id }}</a>
        &rsaquo; Detail
    </div>
{% endblock %}

{% block content %}
    <h1>Order {{ order.id }}</h1>
    <ul class="object-tools">
        <li>
            <a href="#" onclick="window.print();">Print order</a>
        </li>
    </ul>
    <table>
        <tr>
            <th>Created</th>
            <td>{{ order.created }}</td>
        </tr>
        <tr>
            <th>Customer</th>
            <td>{{ order.first_name }} {{ order.last_name }}</td>
        </tr>
        <tr>
            <th>E-mail</th>
            <td><a href="mailto:{{ order.email }}">{{ order.email }}</a></td>
        </tr>
        <tr>
            <th>Address</th>
            <td>{{ order.address }}, {{ order.postal_code }} {{ order.city }}</td>
        </tr>
        <tr>
            <th>Total amount</th>
            <td>${{ order.get_total_cost }}</td>
        </tr>
        <tr>
            <th>Status</th>
            <td>{% if order.paid %}Paid{% else %}Pending payment{% endif %}</td>
        </tr>
    </table>

    <div class="module">
        <div class="tabular inline-related last-related">
            <table>
                <h2>Items bought</h2>
                <thead>
                <tr>
                    <th>Product</th>
                    <th>Price</th>
                    <th>Quantity</th>
                    <th>Total</th>
                </tr>
                </thead>
                <tbody>
                {% for item in order.items.all %}
                    <tr class="row{% cycle "1" "2" %}">
                        <td>{{ item.product.name }}</td>
                        <td class="num">${{ item.price }}</td>
                        <td class="num">{{ item.quantity }}</td>
                        <td class="num">${{ item.get_cost }}</td>
                    </tr>
                {% endfor %}
                <tr class="total">
                    <td colspan="3">Total</td>
                    <td class="num">${{ order.get_total_cost }}</td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>
{% endblock %}


這是 admin 網(wǎng)站展示訂單詳情的模板珠洗。模板擴(kuò)展 Django admin 網(wǎng)站 admin/base_site.html 模板溜歪,這個(gè)模板包含 admin 網(wǎng)站的 HTML 結(jié)構(gòu)和 CSS 樣式。我們加載了自定義靜態(tài)文件 css/admin.css许蓖。

為了使用靜態(tài)文件蝴猪,需要獲得本章代碼中的相應(yīng)靜態(tài)文件〔沧Γ拷貝 orders 應(yīng)用下的 static 目錄下的靜態(tài)文件并將它們添加到你的目錄中自阱。

我們使用父模板中定義的 blocks 來添加自定義內(nèi)容,包括展示訂單和訂單中的商品信息米酬。

擴(kuò)展 admin模板需要了解它的結(jié)構(gòu)并識(shí)別存在的 blocks 沛豌。我們可以從這里找到所有的 admin 模板https://github.com/django/django/tree/master/django/contrib/admin/templates/admin

如果需要赃额,還可以覆蓋 admin模板加派。覆蓋 admin模板只需要將它拷貝到 templates 目錄下并保持相同的相對(duì)路徑和文件名阁簸。Django 的 admin網(wǎng)站將使用自定義模板覆蓋默認(rèn)模板。

最后哼丈,我們?yōu)?admin 網(wǎng)站列表頁面的每個(gè) Order 對(duì)象添加鏈接。編輯 orders 應(yīng)用的 admin.py 文件并在 OrderAdmin 類之前添加以下代碼:

from django.core.urlresolvers import reverse

def order_detail(obj):
    return '<a href="{}">View</a>'.format(
        reverse('orders:admin_order_detail', args=[obj.id]))


order_detail.allow_tags = True

這是輸入 Order 對(duì)象為參數(shù)并返回 admin_site_detail URL 鏈接的函數(shù)筛严。Django 默認(rèn)轉(zhuǎn)義 HTML 輸出醉旦。我們需要設(shè)置 這個(gè)可調(diào)用函數(shù)的 allow_tags 屬性為 True 來防止自動(dòng)轉(zhuǎn)義。

注意:

將 allow_tags 屬性設(shè)置為 True 將防止任意模型桨啃、ModelAdmin 方法及其他可調(diào)用函數(shù)的 HTML 轉(zhuǎn)義车胡。確保轉(zhuǎn)義用戶的輸入以防止跨網(wǎng)站腳本。

然后照瘾,編輯 Order Admin 網(wǎng)站展示鏈接:

list_display = ['id', 'first_name', 'last_name', 'email', 'address',
               'postal_code', 'city', 'paid', 'created', 'updated',
                order_detail]                    

在瀏覽器中打開 http://127.0.0.1:8000/admin/orders/order/匈棘,現(xiàn)在每行都增加了下面的 View 列:

CH8_CSV_4.png

點(diǎn)擊任意訂單的 View 鏈接加載自定義訂單詳情頁面。你應(yīng)該可以看到下面的頁面析命。


CH8_CSV_5.png

動(dòng)態(tài)生成PDF通知


現(xiàn)在已經(jīng)有了結(jié)算和支付系統(tǒng)主卫,我們還可以為每個(gè)訂單生成 PDF 發(fā)貨單。生成 PDF文件的 Python 庫有很多鹃愤,比較受歡迎的是 Reportlab 簇搅,我們可以從這里找到使用 Reportlab 輸出 PDF 文件的方法 https://docs.djangoproject.com/en/1.11/howto/outputting-pdf/

大多數(shù)情況需要為 PDF 文件添加自定義樣式和格式软吐。我們可以發(fā)現(xiàn)渲染 HTML 模板然后將其轉(zhuǎn)換為 PDF更加方便瘩将,這樣可以使 Python 遠(yuǎn)離表示層 。我們將使用這個(gè)方法并使用一個(gè)模塊在 django 中生成 PDF 文件凹耙。我們將使用 WeasyPrint 姿现,WeasyPrint 是將 HTML 模板轉(zhuǎn)換為 PDF 文件的 Python 庫文件。

安裝 WeasyPrint


首先肖抱,從 http://weasyprint.readthedocs.io/en/latest/install.html 為系統(tǒng)安裝 WeasyPrint 依賴程序备典。

然后,使用 pip 安裝WeasyPrint:

pip install WeasyPrint

創(chuàng)建PDF模板


WeasyPrint 輸入一個(gè) HTML文件虐沥。我們將創(chuàng)建一個(gè) HTML 模板熊经,使用 Django 進(jìn)行渲染,然后將其傳入 WeasyPrint 生成 PDF 文件欲险。

在 orders 應(yīng)用的 templates/orders/order/ 目錄下新建一個(gè) pdf.html 的文件镐依,并添加以下代碼:

<html>
<body>
<h1>My Shop</h1>
<p>
    Invoice no. {{ order.id }}<br>
    <span class="secondary">
      {{ order.created|date:"M d, Y" }}
    </span>
</p>

<h3>Bill to</h3>
<p>
    {{ order.first_name }} {{ order.last_name }}<br>
    {{ order.email }}<br>
    {{ order.address }}<br>
    {{ order.postal_code }}, {{ order.city }}
</p>

<h3>Items bought</h3>
<table>
    <thead>
    <tr>
        <th>Product</th>
        <th>Price</th>
        <th>Quantity</th>
        <th>Cost</th>
    </tr>
    </thead>
    <tbody>
    {% for item in order.items.all %}
        <tr class="row{% cycle "1" "2" %}">
            <td>{{ item.product.name }}</td>
            <td class="num">${{ item.price }}</td>
            <td class="num">{{ item.quantity }}</td>
            <td class="num">${{ item.get_cost }}</td>
        </tr>
    {% endfor %}
    <tr class="total">
        <td colspan="3">Total</td>
        <td class="num">${{ order.get_total_cost }}</td>
    </tr>
    </tbody>
</table>

<span class="{% if order.paid %}paid{% else %}pending{% endif %}">
    {% if order.paid %}Paid{% else %}Pending payment{% endif %}
  </span>
</body>
</html>

這是 PDF 發(fā)貨單的模板。在模板中天试,我們展示了訂單詳情和一個(gè)訂單中的商品 <table>槐壳,以及訂單是否支付的信息。

渲染 PDF 文件


我們將使用 admin網(wǎng)站創(chuàng)建一個(gè)視圖來生成已有訂單的 PDF 發(fā)貨單喜每。編輯 orders 應(yīng)用的 views.py 文件并添加以下代碼:

from django.conf import settings
from django.http import HttpResponse
from django.template.loader import render_to_string
import weasyprint


@staff_member_required
def admin_order_pdf(request, order_id):
    order = get_object_or_404(Order, id=order_id)
    html = render_to_string('orders/order/pdf.html', {'order': order})
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = 'filename=\
        "order_{}.pdf"'.format(order.id)
    weasyprint.HTML(string=html).write_pdf(response, stylesheets=[
        weasyprint.CSS(settings.STATIC_ROOT + 'css/pdf.css')])
    return response

這是生成訂單 PDF 發(fā)貨單的視圖务唐。 staff_member_required 裝飾器用來保證只有工作人員才能訪問這個(gè)視圖雳攘,通過給定的 ID 獲得 Order 對(duì)象,然后使用 Django 提供的 render_to_string() 函數(shù)渲染 orders/order/pdf.html 枫笛。渲染的 HTML 保存到 html 變量中吨灭,然后生成一個(gè) application/pdf 格式的 HttpResponse 對(duì)象,該對(duì)象包含指定文件名的 Content-Disposition 頭刑巧。我們使用 WeasyPrint 從渲染的 HTML 生成 PDF 文件并將文件寫到 HttpResponse 對(duì)象中喧兄。 css/pdf.css 為生成 PDF 文件添加 CSS 格式。這里使用 STATIC_ROOT 設(shè)置從本地路徑加載靜態(tài)文件啊楚。 最后吠冤,返回生成的響應(yīng)。

由于需要使用 STATIC_ROOT 設(shè)置恭理,我們將其添加到項(xiàng)目中拯辙,它是項(xiàng)目存放靜態(tài)文件的路徑。編輯 myshop 項(xiàng)目的 settings.py 文件并添加以下代碼:

 STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

然后颜价,運(yùn)行命令 python mange.py collectstatic 涯保。你應(yīng)該可以看到項(xiàng)目使用的所有的靜態(tài)文件都被拷貝到了 STATIC_ROOT 目錄下了。

collectstatic 命令將項(xiàng)目應(yīng)用的靜態(tài)文件拷貝到 STATIC_ROOT 設(shè)置的目錄下拍嵌。每個(gè)應(yīng)用都可以使用 static 目錄提供自己的靜態(tài)文件遭赂。此外,還可以在 STATICFILE_DIRS 設(shè)置的路徑中提供額外的靜態(tài)文件横辆。執(zhí)行 collectstatic 命令時(shí) STATICFILE_DIRS 指定的所有目錄都將被拷貝到 STATIC_ROOT 設(shè)置的目錄下撇他。

編輯 orders 應(yīng)用的 urls.py 文件并添加以下 URL 模式:

url(r'^admin/order/(?P<order_id>\d+)/pdf/$',
    views.admin_order_pdf, name='admin_order_pdf'),

現(xiàn)在,我們可以編輯 Order 模型的admin 列表展示頁面來為條記錄添加生成 PDF 文件的鏈接狈蚤。編輯 orders 應(yīng)用的 admin.py 文件困肩,在 OrderAdmin 類之前添加以下代碼:

def order_pdf(obj):
    return '<a href="{}">PDF</a>'.format(
        reverse('orders:admin_order_pdf', args=[obj.id]))


order_pdf.allow_tags = True
order_pdf.short_description = 'PDF bill' 

將 order_pdf 添加到 OrderAdmin 的 list_display 屬性中:

list_display = ['id', 'first_name', 'last_name', 'email', 'address',
                'postal_code', 'city', 'paid', 'created', 'updated',
                order_detail, order_pdf]

如果調(diào)用函數(shù)指定了 short_description,Django 將使用它作為列名脆侮,否則使用函數(shù)名锌畸。

在瀏覽器中打開 http://127.0.0.1:8000/admin/orders/order/,將會(huì)看到下面的頁面:

CH8-12.png

點(diǎn)擊任意訂單的 PDF 鏈接靖避,沒有付款的訂單將會(huì)顯示下面這樣的 PDF 文件:

CH8-13.png

已經(jīng)付款的訂單將會(huì)顯示下面這樣的 PDF 文件:

CH8-14.png

通過email 發(fā)送PDF文件

客戶付款后潭枣,我們向客戶發(fā)送包含 PDF 發(fā)貨單的郵件。編輯 payment 應(yīng)用 signals 中的 handlers.py 文件并導(dǎo)入下面庫文件:

from io import BytesIO

import weasyprint
from django.conf import settings
from django.core.mail import EmailMessage
from django.template.loader import render_to_string

然后幻捏,在 order.save() 后面添加下面的代碼:

# create invoice e-mail
subject = 'My Shop - Invoice no. {}'.format(order.id)
message = 'Please, find attached the invoice for your recent purchase.'
email = EmailMessage(subject, message, 'admin@myshop.com',
                     [order.email])

# generate PDF
html = render_to_string('orders/order/pdf.html', {'order': order})
out = BytesIO()
stylesheets = [weasyprint.CSS(settings.STATIC_ROOT + 'css/pdf.css')]
weasyprint.HTML(string=html).write_pdf(out, stylesheets=stylesheets)
# attach PDF file
email.attach('order_{}.pdf'.format(order.id), out.getvalue(),
             'application/pdf')
# send e-mail
email.send()

這里盆犁,我們使用 Django 提供的 EmailMessage 類創(chuàng)建一個(gè) e-mail 對(duì)象,然后渲染模板并保存到 html 變量中篡九,并根據(jù)渲染的模板生成 PDF文件并輸出到內(nèi)存字節(jié)緩沖區(qū) BytesIO 實(shí)例中谐岁。這時(shí)我們可以使用 attach 方法將生成的 PDF文件以附件的形式放到 EmailMessage 對(duì)象中。

發(fā)送郵件需要在項(xiàng)目的 settings.py 文件中設(shè)置 SMTP, SMTP 配置在第二章中介紹過伊佃。

現(xiàn)在窜司,打開 Ngrok 為我們的應(yīng)用提供的 URL 并完成一個(gè)新的支付過程,從而接收 PDF 發(fā)貨單郵件航揉。

總結(jié)


本章塞祈,我們?cè)陧?xiàng)目中集成了支付網(wǎng)關(guān),自定義了 Django admin網(wǎng)站帅涂,并且學(xué)習(xí)了如何動(dòng)態(tài)生成 CSV 和 PDF 文件织咧。

下一章,我們將介紹 Django 項(xiàng)目的全球化和本地化漠秋,還將創(chuàng)建折扣系統(tǒng)和商品推薦引擎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抵屿,一起剝皮案震驚了整個(gè)濱河市庆锦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌轧葛,老刑警劉巖搂抒,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異尿扯,居然都是意外死亡求晶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門衷笋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芳杏,“玉大人,你說我怎么就攤上這事辟宗【粽裕” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵泊脐,是天一觀的道長(zhǎng)空幻。 經(jīng)常有香客問我,道長(zhǎng)容客,這世上最難降的妖魔是什么秕铛? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮缩挑,結(jié)果婚禮上但两,老公的妹妹穿的比我還像新娘。我一直安慰自己调煎,他們只是感情好镜遣,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般悲关。 火紅的嫁衣襯著肌膚如雪谎僻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天寓辱,我揣著相機(jī)與錄音艘绍,去河邊找鬼。 笑死秫筏,一個(gè)胖子當(dāng)著我的面吹牛诱鞠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播这敬,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼航夺,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了崔涂?” 一聲冷哼從身側(cè)響起阳掐,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎冷蚂,沒想到半個(gè)月后缭保,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蝙茶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年艺骂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片隆夯。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡钳恕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蹄衷,到底是詐尸還是另有隱情苞尝,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布宦芦,位于F島的核電站宙址,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏调卑。R本人自食惡果不足惜抡砂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望恬涧。 院中可真熱鬧注益,春花似錦、人聲如沸溯捆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至啤月,卻和暖如春煮仇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谎仲。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工浙垫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人郑诺。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓夹姥,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親辙诞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子辙售,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容