第八章 管理支付和訂單

8 管理支付和訂單

在上一章中邦马,你創(chuàng)建了一個(gè)包括商品目錄和訂單系統(tǒng)的在線商店贱鼻。你還學(xué)習(xí)了如何用Celery啟動(dòng)異步任務(wù)宴卖。在這一章中,你會(huì)學(xué)習(xí)如何在網(wǎng)站中集成支付網(wǎng)關(guān)邻悬。你還會(huì)擴(kuò)展管理站點(diǎn)症昏,用于管理訂單和導(dǎo)出不同格式的訂單。

我們會(huì)在本章覆蓋以下知識(shí)點(diǎn):

  • 在項(xiàng)目中集成支付網(wǎng)關(guān)
  • 管理支付通知
  • 導(dǎo)出訂單到CSV文件中
  • 為管理站點(diǎn)創(chuàng)建自定義視圖
  • 動(dòng)態(tài)生成PDF單據(jù)

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

支付網(wǎng)關(guān)允許你在線處理支付父丰。你可以使用支付網(wǎng)關(guān)管理用戶訂單肝谭,以及通過(guò)可靠的,安全的第三方代理處理支付蛾扇。這意味著你不用考慮在自己的系統(tǒng)中存儲(chǔ)信用卡攘烛。

有很多支付網(wǎng)關(guān)可供選擇。我們將集成PayPal镀首,它是最流行的支付網(wǎng)關(guān)之一医寿。

PayPal提供了幾種方法在網(wǎng)站中集成它的網(wǎng)關(guān)。標(biāo)準(zhǔn)集成包括一個(gè)Buy now按鈕蘑斧,你可能在其它網(wǎng)站見過(guò)。這個(gè)按鈕把顧客重定向到PayPal來(lái)處理支付须眷。我們將在網(wǎng)站中集成包括一個(gè)自定義Buy now按鈕的PayPal Payments Standard竖瘾。PayPal會(huì)處理支付,并發(fā)送一條支付狀態(tài)的信息到我們的服務(wù)器花颗。

8.1.1 創(chuàng)建PayPal賬戶

你需要一個(gè)PayPal商家賬戶捕传,才能在網(wǎng)站中集成支付網(wǎng)關(guān)。如果你還沒(méi)有PayPal賬戶扩劝,在這里注冊(cè)庸论。確保你選擇了商家賬戶。

在注冊(cè)表單填寫詳細(xì)信息完成注冊(cè)棒呛。PayPal會(huì)給你發(fā)送一封郵件確認(rèn)賬戶聂示。

8.1.2 安裝django-paypal

django-paypal是一個(gè)第三方Django應(yīng)用,可以簡(jiǎn)化在Django項(xiàng)目中集成PayPal簇秒。我們將用它在我們的商店中集成PayPal Payments Standard鱼喉。你可以在這里查看django-paypal的文檔。

在終端使用以下命令安裝django-paypal:

pip install django-paypal

編輯項(xiàng)目的settings.py文件趋观,在INSTALLED_APPS設(shè)置中添加paypal.standard.ipn

INSTALLED_APPS = [
    # ...
    'paypal.standard.ipn',
]

這個(gè)應(yīng)用是django-paypal提供的扛禽,通過(guò)Instant Payment Notification(IPN)集成PayPal Payments Standard。我們之后會(huì)處理支付通知皱坛。

myshopsettings.py文件添加以下設(shè)置來(lái)配置django-paypal:

# django-paypal settings
PAYPAL_RECEIVER_EMAIL = 'mypaypalemail@myshop.com'
PAYPAL_TEST = True

這些設(shè)置分別是:

  • PAYPAL_RECEIVER_EMAIL:你PayPal賬戶的郵箱地址编曼。用你創(chuàng)建PayPal賬戶的郵箱替換mypaypalemail@myshop.com
  • PAYPAL_TEST:一個(gè)布爾值剩辟,表示是否用PayPal的Sandbox環(huán)境處理支付掐场。在遷移到生產(chǎn)環(huán)境之前往扔,你可以用Sandbox測(cè)試PayPal集成。

打開終端執(zhí)行以下命令刻肄,同步django-paypal的模型到數(shù)據(jù)庫(kù)中:

python manage.py migrate

你會(huì)看到類似這樣結(jié)尾的輸出:

Running migrations:
  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

現(xiàn)在django-paypal的模型已經(jīng)同步到數(shù)據(jù)庫(kù)中瓤球。你還需要添加django-paypal的URL模式到項(xiàng)目中。編輯myshop項(xiàng)目的主urls.py文件敏弃,并添加以下URL模式卦羡。記住,把它放在shop.urls模式之前麦到,避免錯(cuò)誤的模式匹配:

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

讓我們把支付網(wǎng)關(guān)添加到結(jié)賬過(guò)程中绿饵。

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

結(jié)賬流程是這樣的:

  1. 用戶添加商品到購(gòu)物車中。
  2. 用戶結(jié)賬購(gòu)物車瓶颠。
  3. 重定向用戶到PayPal進(jìn)行支付拟赊。
  4. PayPal發(fā)送支付通知到我們的服務(wù)器。
  5. PayPal重定向用戶返回我們的網(wǎng)站粹淋。

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

python manage.py startapp payment

我們將使用這個(gè)應(yīng)用管理結(jié)賬流程和用戶支付吸祟。

編輯項(xiàng)目的settings.py文件,在INSTALLED_APP設(shè)置中添加payment

INSTALLED_APPS = [
    # ...
    'paypal.standard.ipn',
    'payment',
]

現(xiàn)在payment應(yīng)用已經(jīng)在項(xiàng)目中激活了桃移。編輯orders應(yīng)用的views.py文件屋匕,添加以下導(dǎo)入:

from django.shortcuts import render, redirect
from django.core.urlresolvers import reverse

找到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)
request.session['order_id'] = order.id
return redirect(reverse('payment:process'))

創(chuàng)建訂單成功之后,我們用order_id會(huì)話鍵在當(dāng)前會(huì)話中設(shè)置訂單ID借杰。然后我們把用戶重定向到接下來(lái)會(huì)創(chuàng)建的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 paypal.standard.forms import PayPalPaymentsForm
from orders.models import Order

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})

payment_process視圖中蔗衡,我們生成了一個(gè)自定義PayPal的Buy now按鈕用于支付纤虽。首先我們從order_id會(huì)話鍵中獲得當(dāng)前訂單,這個(gè)鍵值之前在order_create視圖中設(shè)置過(guò)绞惦。我們獲得指定ID的Order對(duì)象逼纸,并創(chuàng)建了包括以下字段的PayPalPaymentForm

  • business:處理支付的PayPal商家賬戶。在這里我們使用PAYPAL_RECEIVER_EMAIL設(shè)置中定義的郵箱賬戶济蝉。
  • amount:向顧客收取的總價(jià)樊展。
  • item_name:出售的商品名。我們使用商品ID堆生,因?yàn)橛唵卫锟赡馨ǘ鄠€(gè)商品专缠。
  • invoice:?jiǎn)螕?jù)ID。每次支付對(duì)應(yīng)的這個(gè)ID應(yīng)用是唯一的淑仆。我們使用訂單ID涝婉。
  • currency_code:這次支付的貨幣。我們?cè)O(shè)置為USD使用美元蔗怠。使用與PayPal賬戶中設(shè)置的相同貨幣(EUR對(duì)應(yīng)歐元)墩弯。
  • notify_url:PayPal發(fā)送IPN請(qǐng)求到這個(gè)URL吩跋。我們使用django-paypal提供的paypal-ipn URL。這個(gè)URL關(guān)聯(lián)的視圖處理負(fù)責(zé)支付通知和在數(shù)據(jù)庫(kù)中保存支付通知渔工。
  • return_url:支付成功后重定向用戶到這個(gè)URL锌钮。我們使用之后會(huì)創(chuàng)建的payment:done URL。
  • cancel_return:如果支付取消引矩,或者遇到其它問(wèn)題梁丘,重定向用戶到這個(gè)URL。我們使用之后會(huì)創(chuàng)建的payment:canceled URL旺韭。

PayPalPaymentForm會(huì)被渲染為帶隱藏字典的標(biāo)準(zhǔn)表單氛谜,用戶只能看到Buy now按鈕。點(diǎn)用戶點(diǎn)擊這個(gè)按鈕区端,表單會(huì)通過(guò)POST提交到PayPal值漫。

讓我們創(chuàng)建一個(gè)簡(jiǎn)單的視圖,當(dāng)支付完成织盼,或者因?yàn)槟承┰蛉∠Ц堆詈危孭ayPal重定向用戶。在同一個(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')

因?yàn)镻ayPal可以通過(guò)POST重定向用戶到這些視圖的任何一個(gè)沥邻,所以我們用csrf_exempt裝飾器避免Django期望的CSRF令牌危虱。在payment應(yīng)用目錄中創(chuà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'),
]

這些是支付流程的URL谋国。我們包括了以下URL模式:

  • process:用于生成帶Buy now按鈕的PayPal表單的視圖
  • done:當(dāng)支付成功后,用于PayPal重定向用戶
  • canceled:當(dāng)支付取消后迁沫,用于PayPal重定向用戶

編輯myshop項(xiàng)目的主urls.py文件芦瘾,引入payment應(yīng)用的URL模式:

url(r'^payment/', include('payment.urls', namespace='payment')),

記住把它放在shop.urls模式之前,避免錯(cuò)誤的模式匹配集畅。

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

templates/
    payment/
        process.html
        done.html
        canceled.html

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

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

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

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

這個(gè)模板用于渲染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 content %}

用戶支付成功后祷愉,會(huì)重定向到這個(gè)模板頁(yè)面。

編輯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 content %}

處理支付遇到問(wèn)題二鳄,或者用戶取消支付時(shí),會(huì)重定向到這個(gè)模板頁(yè)面媒怯。

讓我們嘗試完整的支付流程订讼。

8.1.4 使用PayPal的Sandbox

在瀏覽器中打開http://developer.paypal.com,并用你的PayPal商家賬戶登錄扇苞。點(diǎn)擊Dashboard菜單項(xiàng)欺殿,然后點(diǎn)擊Sandbox下的Accounts選項(xiàng)寄纵。你會(huì)看到你的sandbox測(cè)試賬戶列表,如下圖所示:

最初脖苏,你會(huì)看到一個(gè)商家賬戶和一個(gè)PayPal自動(dòng)生成的個(gè)人測(cè)試賬戶程拭。你可以點(diǎn)擊Create Account按鈕創(chuàng)建新的sandbox測(cè)試賬戶。

點(diǎn)擊列表中TypePERSONAL的賬戶棍潘,然后點(diǎn)擊Pofile鏈接恃鞋。你會(huì)看到測(cè)試賬戶的信息,包括郵箱地址和個(gè)人資料信息蜒谤,如下圖所示:

Funding標(biāo)簽頁(yè)中山宾,你會(huì)看到銀行賬戶,信用卡數(shù)據(jù)鳍徽,以及PayPal貸方余額资锰。

當(dāng)你的網(wǎng)站使用sandbox環(huán)境時(shí),測(cè)試賬戶可以用來(lái)處理支付阶祭。導(dǎo)航到Profile標(biāo)簽頁(yè)绷杜,然后點(diǎn)擊修改Change password鏈接。為這個(gè)測(cè)試賬戶創(chuàng)建一個(gè)自定義密碼濒募。

在終端執(zhí)行python manage.py runserver命令啟動(dòng)開發(fā)服務(wù)器鞭盟。在瀏覽器中打開http://127.0.0.1:8000/,添加一些商品到購(gòu)物車中瑰剃,然后填寫結(jié)賬表單齿诉。當(dāng)你點(diǎn)擊Place order按鈕時(shí),訂單會(huì)存儲(chǔ)到數(shù)據(jù)庫(kù)中晌姚,訂單ID會(huì)保存在當(dāng)前會(huì)話中粤剧,然后會(huì)重定向到支付處理頁(yè)面。這個(gè)頁(yè)面從會(huì)話中獲得訂單挥唠,并渲染帶Buy now按鈕的PayPal表單抵恋,如下圖所示:

譯者注:啟動(dòng)開發(fā)服務(wù)器后,還需要啟動(dòng)RabbitMQ和Celery宝磨,因?yàn)槲覀円盟鼈儺惒桨l(fā)送郵件弧关,否則會(huì)拋出異常。

你可以看一眼HTML源碼唤锉,查看生成的表單字段世囊。

點(diǎn)擊Buy now按鈕。你會(huì)被重定向到PayPal窿祥,如下圖所示:

輸入顧客測(cè)試賬號(hào)的郵箱地址和密碼茸习,然后點(diǎn)擊登錄按鈕。你會(huì)被重定向到以下頁(yè)面:

譯者注:即之前修改過(guò)密碼的個(gè)人賬戶壁肋。

現(xiàn)在點(diǎn)擊立即付款按鈕号胚。最后籽慢,你會(huì)看到一個(gè)包括交易ID的確認(rèn)頁(yè)面,如下圖所示:

點(diǎn)擊返回商家按鈕猫胁。你會(huì)被重定向到PayPalPaymentFormreturn_url字段指定的URL箱亿。這是payment_done視圖的URL,如下圖所示:

支付成功弃秆!但是因?yàn)槲覀冊(cè)诒镜剡\(yùn)行項(xiàng)目届惋,127.0.0.1不是一個(gè)公網(wǎng)IP,所以PayPal不能給我們的應(yīng)用發(fā)送支付狀態(tài)通知菠赚。我們接下來(lái)學(xué)習(xí)如何讓我們的網(wǎng)站可以從Internet訪問(wèn)脑豹,從而接收IPN通知。

8.1.5 獲得支付通知

IPN是大部分支付網(wǎng)關(guān)都會(huì)提供的方法衡查,用于實(shí)時(shí)跟蹤購(gòu)買瘩欺。當(dāng)網(wǎng)關(guān)處理完一個(gè)支付后,會(huì)立即給你的服務(wù)器發(fā)送一個(gè)通知拌牲。該通知包括所有支付細(xì)節(jié)俱饿,包括狀態(tài)和用于確認(rèn)通知來(lái)源的支付簽名。這個(gè)通知作為獨(dú)立的HTTP請(qǐng)求發(fā)送到你的服務(wù)器塌忽。出現(xiàn)問(wèn)題的時(shí)候拍埠,PayPal會(huì)多次嘗試發(fā)送通知。

django-payapl自帶兩個(gè)不同的IPN信號(hào)土居,分別是:

  • valid_ipn_received:當(dāng)從PayPal接收的IPN消息是正確的枣购,并且不會(huì)與數(shù)據(jù)庫(kù)中現(xiàn)在消息重復(fù)時(shí)觸發(fā)
  • invalid_ipn_received:當(dāng)從PayPal接收的消息包括無(wú)效數(shù)據(jù)或者格式不對(duì)時(shí)觸發(fā)

我們將創(chuàng)建一個(gè)自定義接收函數(shù),并把它連接到valid_ipn_received信號(hào)來(lái)確認(rèn)支付擦耀。

payment應(yīng)用目錄中創(chuàng)建signals.py文件棉圈,并添加以下代碼:

from django.shortcuts import get_object_or_404
from paypal.standard.models import ST_PP_COMPLETED
from paypal.standard.ipn.signals import valid_ipn_received
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. 我們接收sender對(duì)象埂奈,它是在paypal.standard.ipn.models中定義的PayPalPN模型的一個(gè)實(shí)例迄损。
  2. 我們檢查paypal_status屬性定躏,確保它等于django-paypal的完成狀態(tài)账磺。這個(gè)狀態(tài)表示支付處理成功。
  3. 接著我們用get_object_or_404快捷函數(shù)獲得訂單痊远,這個(gè)訂單的ID必須匹配我們提供給PayPal的invoice參數(shù)垮抗。
  4. 我們?cè)O(shè)置訂單的paid屬性為True,標(biāo)記訂單狀態(tài)為已支付碧聪,并把Order對(duì)象保存到數(shù)據(jù)庫(kù)中冒版。

當(dāng)valid_ipn_received信號(hào)觸發(fā)時(shí),你必須確保信號(hào)模塊已經(jīng)加載逞姿,這樣接收函數(shù)才會(huì)被調(diào)用辞嗡。最好的方式是在包括它們的應(yīng)用加載的時(shí)候捆等,加載你自己的信號(hào)⌒遥可以通過(guò)定義一個(gè)自定義的應(yīng)用配置來(lái)實(shí)現(xiàn)栋烤,我們會(huì)在下一節(jié)中講解。

8.1.6 配置我們的應(yīng)用

你已經(jīng)在第六章學(xué)習(xí)了應(yīng)用配置挺狰。我們將為payment應(yīng)用定義一個(gè)自定義配置明郭,用來(lái)加載我們的信號(hào)接收函數(shù)。

payment應(yīng)用目錄中創(chuàng)建apps.py文件丰泊,并添加以下代碼:

from django.apps import AppConfig

class PaymentConfig(AppConfig):
    name = 'payment'
    verbose_name = 'Payment'

    def ready(self):
        # improt signal handlers
        import payment.signals

在這段代碼中薯定,我們?yōu)?code>payment應(yīng)用定義了一個(gè)AppConfig類。name參數(shù)是應(yīng)用的名字瞳购,verbose_name是一個(gè)可讀的名字话侄。我們?cè)?code>ready()方法中導(dǎo)入信號(hào)模板,確保應(yīng)用初始化時(shí)會(huì)加載信號(hào)模塊苛败。

編輯payment應(yīng)用的__init__.py文件满葛,并添加這一行代碼:

default_app_config = 'payment.apps.PaymentConfig'

這會(huì)讓Django自動(dòng)加載你的自定義應(yīng)用配置類。你可以在這里閱讀更多關(guān)于應(yīng)用配置的信息罢屈。

8.1.7 測(cè)試支付通知

因?yàn)槲覀冊(cè)诒镜丨h(huán)境開發(fā)嘀韧,所以我們需要讓PayPal可以訪問(wèn)我們的網(wǎng)站。有幾個(gè)應(yīng)用程序可以讓開發(fā)環(huán)境通過(guò)Internet訪問(wèn)缠捌。我們將使用Ngrok锄贷,是最流行的之一。

這里下載你的操作系統(tǒng)版本的Ngrok曼月,并使用以下命令運(yùn)行:

./ngrok http 8000

這個(gè)命令告訴Ngrok在8000端口為你的本地主機(jī)創(chuàng)建一個(gè)鏈路谊却,并為它分配一個(gè)Internet可訪問(wèn)的主機(jī)名。你可以看到類似這樣的輸入:

Session Status                online
Account                       lakerszhy (Plan: Free)
Update                        update available (version 2.2.4, Ctrl-U to update)
Version                       2.1.18
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://c0f17d7c.ngrok.io -> localhost:8000
Forwarding                    https://c0f17d7c.ngrok.io -> localhost:8000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

Ngrok告訴我們哑芹,我們網(wǎng)站使用的Django開發(fā)服務(wù)器在本機(jī)的8000端口運(yùn)行炎辨,現(xiàn)在可以通過(guò)http://c0f17d7c.ngrok.iohttps://c0f17d7c.ngrok.io(分別對(duì)應(yīng)HTTP和HTTPS協(xié)議)在Internet上訪問(wèn)。Ngrok還提供了一個(gè)網(wǎng)頁(yè)URL聪姿,這個(gè)網(wǎng)頁(yè)顯示發(fā)送到這個(gè)服務(wù)器的信息碴萧。在瀏覽器中打開Ngrok提供的URL,比如http://c0f17d7c.ngrok.io末购。在購(gòu)物車中添加一些商品破喻,下單,然后用PayPal測(cè)試賬戶支付盟榴。此時(shí)曹质,PayPal可以訪問(wèn)payment_process視圖中PayPalPaymentFormnotify_url字段生成的URL。如果你查看渲染的表單,你會(huì)看類似這樣的HTML表單:

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

完成支付處理后羽德,在瀏覽器中打開http://127.0.0.1:8000/admin/ipn/paypalipn/几莽。你會(huì)看到一個(gè)IPN對(duì)象,對(duì)應(yīng)狀態(tài)是Completed的最新一筆支付宅静。這個(gè)對(duì)象包括支付的所有信息银觅,它由PayPal發(fā)送到你提供給IPN通知的URL。

譯者注:如果通過(guò)http://c0f17d7c.ngrok.io訪問(wèn)在線商店坏为,則需要在項(xiàng)目的settings.py文件的ALLOWED_HOSTS設(shè)置中添加c0f17d7c.ngrok.io究驴。

譯者注:我在后臺(tái)看到的一直都是Pending狀態(tài),一直沒(méi)有找出原因匀伏。哪位朋友知道的話洒忧,請(qǐng)給我留言左胞,謝謝碘举。

你也可以在這里使用PayPal的模擬器發(fā)送IPN。模擬器允許你指定通知的字段和類型柒室。

除了PayPal Payments Standard履磨,PayPal還提供了Website Payments Pro蛉抓,它是一個(gè)訂購(gòu)服務(wù),可以在你的網(wǎng)站接收支付剃诅,而不用重定向到PayPal巷送。你可以在這里查看如何集成Website Payments Pro

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

有時(shí)你可能希望把模型中的信息導(dǎo)出到文件中矛辕,然后把它導(dǎo)入到其它系統(tǒng)中笑跛。其中使用最廣泛的格式是Comma-Separated Values(CSV)。CSV文件是一個(gè)由若干條記錄組成的普通文本文件聊品。通常一行包括一條記錄和一些定界符號(hào)飞蹂,一般是逗號(hào),用于分割記錄的字段翻屈。我們將自定義管理站點(diǎn)陈哑,讓它可以到處訂單到CSV文件。

8.2.1 在管理站點(diǎn)你添加自定義操作

Django提供了大量自定義管理站點(diǎn)的選項(xiàng)伸眶。我們將修改對(duì)象列表視圖惊窖,在其中包括一個(gè)自定義的管理操作。

一個(gè)管理操作是這樣工作的:用戶在管理站點(diǎn)的對(duì)象列表頁(yè)面用復(fù)選框選擇對(duì)象赚抡,然后選擇一個(gè)在所有選中選項(xiàng)上執(zhí)行的操作爬坑,最后執(zhí)行操作纠屋。下圖顯示了操作位于管理站點(diǎn)的哪個(gè)位置:

創(chuàng)建自定義管理操作允許工作人員一次在多個(gè)元素上進(jìn)行操作涂臣。

你可以編寫一個(gè)常規(guī)函數(shù)來(lái)創(chuàng)建自定義操作,該函數(shù)需要接收以下參數(shù):

  • 當(dāng)前顯示的ModelAdmin
  • 當(dāng)前請(qǐng)求對(duì)象——一個(gè)HttpRequest實(shí)例
  • 一個(gè)用戶選中對(duì)象的QuerySet

當(dāng)在管理站點(diǎn)觸發(fā)操作時(shí),會(huì)執(zhí)行這個(gè)函數(shù)赁遗。

我們將創(chuàng)建一個(gè)自定義管理操作署辉,來(lái)下載一組訂單的CSV文件。編輯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'

在這段代碼中執(zhí)行了以下任務(wù):

  1. 我們創(chuàng)建了一個(gè)HttpResponse實(shí)例哭尝,其中包括定制的text/csv內(nèi)容類型,告訴瀏覽器該響應(yīng)看成一個(gè)CSV文件剖煌。我們還添加了Content-Disposition頭部材鹦,表示HTTP響應(yīng)包括一個(gè)附件。
  2. 我們創(chuàng)建了CSV的writer對(duì)象耕姊,用于向response對(duì)象中寫入數(shù)據(jù)桶唐。
  3. 我們用模型的_meta選項(xiàng)的get_fields()方法動(dòng)態(tài)獲得模型的字段。我們派出了對(duì)多對(duì)和一對(duì)多關(guān)系茉兰。
  4. 我們用字段名寫入標(biāo)題行尤泽。
  5. 我們迭代給定的QuerySet,并為QuerySet返回的每個(gè)對(duì)象寫入一行數(shù)據(jù)规脸。因?yàn)镃SV的輸出值必須為字符串坯约,所以我們格式化datetime對(duì)象。
  6. 我們?cè)O(shè)置函數(shù)的short_description屬性莫鸭,指定這個(gè)操作在模板中顯示的名字闹丐。

我們創(chuàng)建了一個(gè)通用的管理操作,可以添加到所有ModelAdmin類上被因。

最后妇智,如下添加export_to_csv管理操作到OrderAdmin類上:

calss OrderAdmin(admin.ModelAdmin):
    # ...
    actions = [export_to_csv]

在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order/,管理操作如下圖所示:

選中幾條訂單氏身,然后在選擇框中選擇Export to CSV操作巍棱,接著點(diǎn)擊Go按鈕。你的瀏覽器會(huì)下載生成的order.csv文件蛋欣。用文本編輯器打開下載的文件航徙。你會(huì)看到以下格式的內(nèi)容,其中包括標(biāo)題行陷虎,以及你選擇的每個(gè)Order對(duì)象行:

ID,first name,last name,email,address,postal code,city,created,updated,paid
1,allen,iverson,lakerszhy@gmail.com,北京市朝陽(yáng)區(qū),100012,北京市,11/05/2017,11/05/2017,False
2,allen,kobe,lakerszhy@gmail.com,北京市朝陽(yáng)區(qū),100012,北京市,11/05/2017,11/05/2017,False

正如你所看到的到踏,創(chuàng)建管理操作非常簡(jiǎn)單。

8.3 用自定義視圖擴(kuò)展管理站點(diǎn)

有時(shí)尚猿,你可能希望通過(guò)配置ModelAdmin窝稿,創(chuàng)建管理操作和覆寫管理目標(biāo)來(lái)定制管理站點(diǎn)。這種情況下凿掂,你需要?jiǎng)?chuàng)建自定義的管理視圖伴榔。使用自定義視圖纹蝴,可以創(chuàng)建任何你需要的功能。你只需要確保只有工作人員能訪問(wèn)你的視圖踪少,以及讓你的模板繼承自管理模板來(lái)維持管理站點(diǎn)的外觀塘安。

讓我們創(chuàng)建一個(gè)自定義視圖,顯示訂單的相關(guān)信息援奢。編輯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)求這個(gè)頁(yè)面的用戶的is_activeis_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):

admin/
    orders/
        order/
            detail.html

編輯detail.html模板绕娘,添加以下代碼:

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

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

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

{% 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 breadcrumbs %}

{% 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 content %}

這個(gè)模板用于在管理站點(diǎn)顯示訂單詳情。模板擴(kuò)展自Django管理站點(diǎn)的admin/base_site.html模板栽连,其中包括主HTML結(jié)構(gòu)和管理站的CSS樣式险领。我們加載自定義的靜態(tài)文件css/admin.css

為了使用靜態(tài)文件秒紧,我們可以從本章的示例代碼中獲得它們绢陌。拷貝orders應(yīng)用的static/目錄中的靜態(tài)文件熔恢,添加到你項(xiàng)目中的相同位置脐湾。

我們使用父模板中定義的塊引入自己的內(nèi)容。我們顯示訂單信息和購(gòu)買的商品叙淌。

當(dāng)你想要擴(kuò)展一個(gè)管理模板時(shí)秤掌,你需要了解它的結(jié)構(gòu),并確定它存在哪些塊鹰霍。你可以在這里查看所有管理模板闻鉴。

如果需要,你也可以覆蓋一個(gè)管理模板茂洒。把要覆蓋的模板拷貝到templates目錄中孟岛,保留一樣的相對(duì)路徑和文件。Django的管理站點(diǎn)會(huì)使用你自定義的模板代替默認(rèn)模板督勺。

最后渠羞,讓我們?yōu)楣芾碚军c(diǎn)的列表顯示頁(yè)中每個(gè)Order對(duì)象添加一個(gè)鏈接。編輯orders應(yīng)用的amdin.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

這個(gè)函數(shù)接收一個(gè)Order對(duì)象作為參數(shù)次询,并返回一個(gè)admin_order_detail的HTML鏈接。默認(rèn)情況下瓷叫,Django會(huì)轉(zhuǎn)義HTML輸出屯吊。我們必須設(shè)置函數(shù)的allow_tags屬性為True送巡,從而避免自動(dòng)轉(zhuǎn)義。

在任何Model方法雌芽,ModelAdmin方法,或者可調(diào)用函數(shù)中設(shè)置allow_tags屬性為True可以避免HTML轉(zhuǎn)義辨嗽。使用allow_tags時(shí)世落,確保轉(zhuǎn)義用戶的輸入,以避免跨站點(diǎn)腳本糟需。

然后編輯OrderAdmin類來(lái)顯示鏈接:

class OrderAdmin(admin.ModelAdmin):
    list_display = [... order_detail]

在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order/屉佳,現(xiàn)在每行都包括一個(gè)View鏈接,如下圖所示:

點(diǎn)擊任何一個(gè)訂單的View鏈接洲押,會(huì)加載自定義的訂單詳情頁(yè)面武花,如下圖所示:

8.4 動(dòng)態(tài)生成PDF單據(jù)

我們現(xiàn)在已經(jīng)有了完成的結(jié)賬和支付系統(tǒng),可以為每個(gè)訂單生成PDF單據(jù)了杈帐。有幾個(gè)Python庫(kù)可以生成PDF文件体箕。一個(gè)流行的生成PDF文件的Python庫(kù)是Reportlab。你可以在這里查看如果使用Reportlab輸出PDF文件挑童。

大部分情況下累铅,你必須在PDF文件中添加自定義樣式和格式。你會(huì)發(fā)現(xiàn)站叼,讓Python遠(yuǎn)離表現(xiàn)層娃兽,渲染一個(gè)HTML模板,然后把它轉(zhuǎn)換為PDF文件更加方便尽楔。我們將采用這種方法投储,在Django中用模塊生成PDF文件。我們會(huì)使用WeasyPrint阔馋,它是一個(gè)Python庫(kù)玛荞,可以從HTML模板生成PDF文件。

8.4.1 安裝WeasyPrint

首先呕寝,為你的操作系統(tǒng)安裝WeasyPrint的依賴冲泥,請(qǐng)?jiān)L問(wèn)這里

然后用以下命令安裝WeasyPrint:

pip install WeasyPrint

8.4.2 創(chuàng)建PDF模板

我們需要一個(gè)HTML文檔作為WeasyPrint的輸入壁涎。我們將創(chuàng)建一個(gè)HTML模板凡恍,用Django渲染它,然后把它傳遞給WeasyPrint生成PDF文件怔球。

orders應(yīng)用的templates/orders/order/目錄中創(chuàng)建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單據(jù)的模板。在這個(gè)模板中竟坛,我們顯示所有訂單詳情和一個(gè)包括商品的HTML的<table>元素闽巩。我們還包括一個(gè)消息钧舌,顯示訂單是否支付。

8.4.3 渲染PDF文件

我們將創(chuàng)建一個(gè)視圖涎跨,在管理站點(diǎn)中生成已存在訂單的PDF單據(jù)洼冻。編輯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

這個(gè)視圖用于生成訂單的PDF單據(jù)隅很。我們用staff_member_required裝飾器確保只有工作人員可以訪問(wèn)這個(gè)視圖撞牢。我們用給定的ID獲得Order對(duì)象,并用Django提供的render_to_string()函數(shù)渲染orders/order/pdf.html文件叔营。被渲染的HTML保存在html變量中屋彪。然后,我們生成一個(gè)新的HttpResponse對(duì)象绒尊,指定application/pdf內(nèi)容類型畜挥,并用Content-Disposition指定文件名。我們用WeasyPrint從被渲染的HTML代碼生成一個(gè)PDF文件婴谱,并把文件寫到HttpResponse對(duì)象中蟹但。我們用css/pdf.css靜態(tài)文件為生成的PDF文件添加CSS樣式。我們從STATIC_ROOT設(shè)置中的本地路徑加載它谭羔。最后返回生成的響應(yīng)矮湘。

因?yàn)槲覀冃枰褂?code>STATIC_ROOT設(shè)置,所以需要把它添加到我們項(xiàng)目中口糕。這是項(xiàng)目的靜態(tài)文件存放的路徑缅阳。編輯myshop項(xiàng)目的settings.py文件,添加以下設(shè)置:

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

接著執(zhí)行python manage.py collectstatic命令景描。你會(huì)看到這樣結(jié)尾的輸出:

You have requested to collect static files at the destination
location as specified in your settings:

    /Users/lakerszhy/Documents/GitHub/Django-By-Example/code/Chapter 8/myshop/static

This will overwrite existing files!
Are you sure you want to do this?

輸入yes并按下Enter十办。你會(huì)看到一條消息,顯示靜態(tài)文件已經(jīng)拷貝到STATIC_ROOT目錄中超棺。

collectstatic命令拷貝應(yīng)用中所有靜態(tài)文件到STATIC_ROOT設(shè)置中定義的目錄向族。這樣每個(gè)應(yīng)用可以在static/目錄中包括靜態(tài)文件。你還可以在STATICFILES_DIRS設(shè)置中提供其它靜態(tài)文件源棠绘。執(zhí)行collectstatic命令時(shí)件相,STATICFILES_DIRS中列出的所有目錄都會(huì)被拷貝到STATIC_ROOT目錄中。

編輯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)在夜矗,我們可以編輯管理列表顯示頁(yè)面,為Order模型的每條記錄添加一個(gè)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屬性中,如下所示:

class OrderAdmin(admin.ModelAdmin):
    list_display = [..., order_detail, order_pdf]

如果你為可調(diào)用對(duì)象指定了short_description屬性赡突,Django將把它作為列名对扶。

在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order区赵。每行都會(huì)包括一個(gè)PDF鏈接,如下圖所示:

點(diǎn)擊任意一條訂單的PDF鏈接浪南。你會(huì)看到生成的PDF文件笼才,下圖是未支付的訂單:

已支付訂單如下圖所示:

8.4.4 通過(guò)郵件發(fā)送PDF文件

當(dāng)收到支付時(shí)络凿,讓我們給顧客發(fā)送一封包括PDF單據(jù)的郵件骡送。編輯payment應(yīng)用的signals.py文件了赵,并添加以下導(dǎo)入:

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

然后在order.save()行之后添加以下代碼淹父,保持相同的縮進(jìn):

# 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()

在這個(gè)信號(hào)中,我們用Django提供的EmailMessage類創(chuàng)建了一個(gè)郵件對(duì)象怎虫。然后把模板渲染到html變量中暑认。我們從渲染的模板中生成PDF文件困介,并把它輸出到一個(gè)BytesIO實(shí)例(內(nèi)存中的字節(jié)緩存)中。接著我們用EmailMessage對(duì)象的attach()方法蘸际,把生成的PDF文件和out緩存中的內(nèi)容添加到EmailMessage對(duì)象中座哩。

記得在項(xiàng)目settings.py文件中設(shè)置發(fā)送郵件的SMTP設(shè)置,你可以參考第二章粮彤。

現(xiàn)在打開Ngrok提供的應(yīng)用URL根穷,完成一筆新的支付,就能在郵件中收到PDF單據(jù)了导坟。

8.5 總結(jié)

在這一章中屿良,你在項(xiàng)目中集成了支付網(wǎng)關(guān)。你自定義了Django管理站點(diǎn)惫周,并學(xué)習(xí)了如果動(dòng)態(tài)生成CSV和PDF文件尘惧。

下一章會(huì)深入了解Django項(xiàng)目的國(guó)際化和本地化。你還會(huì)創(chuàng)建一個(gè)優(yōu)惠券系統(tǒng)和商品推薦引擎递递。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末喷橙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子登舞,更是在濱河造成了極大的恐慌贰逾,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件菠秒,死亡現(xiàn)場(chǎng)離奇詭異疙剑,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)践叠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門言缤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人酵熙,你說(shuō)我怎么就攤上這事轧简。” “怎么了匾二?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵哮独,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我察藐,道長(zhǎng)皮璧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任分飞,我火速辦了婚禮悴务,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己讯檐,他們只是感情好羡疗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著别洪,像睡著了一般叨恨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上挖垛,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天痒钝,我揣著相機(jī)與錄音,去河邊找鬼痢毒。 笑死送矩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哪替。 我是一名探鬼主播栋荸,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼夷家!你這毒婦竟也來(lái)了蒸其?” 一聲冷哼從身側(cè)響起敏释,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤库快,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后钥顽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體义屏,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年蜂大,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了闽铐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奶浦,死狀恐怖兄墅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情澳叉,我是刑警寧澤隙咸,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站成洗,受9級(jí)特大地震影響五督,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瓶殃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一充包、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧遥椿,春花似錦基矮、人聲如沸淆储。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)遏考。三九已至,卻和暖如春蓝谨,著一層夾襖步出監(jiān)牢的瞬間灌具,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工譬巫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咖楣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓芦昔,卻偏偏與公主長(zhǎng)得像诱贿,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咕缎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

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