nes-num lines-num-old">
+
+from shortuuidfield import ShortUUIDField
+
+from pai2.basemodels import CreateUpdateMixin
+
+
+class OrderInfo(CreateUpdateMixin):
+ WAITING_PAY = 0
+ PAID = 1
+ # DELETED = 2
+
+ PAY_STATUS = (
+ (WAITING_PAY, u'待支付'),
+ (PAID, u'已支付'),
+ # (DELETED, u'已删除'),
+ )
+
+ order_id = ShortUUIDField(_(u'order_id'), max_length=255, help_text=u'订单唯一标识', db_index=True)
+
+ from_uid = models.CharField(_(u'from_uid'), max_length=255, help_text=u'付款用户唯一标识', db_index=True)
+ to_lid = models.CharField(_(u'to_lid'), max_length=255, blank=True, null=True, help_text=u'收款摄影师唯一标识', db_index=True)
+ to_uid = models.CharField(_(u'to_uid'), max_length=255, blank=True, null=True, help_text=u'收款用户唯一标识', db_index=True)
+
+ body = models.CharField(_(u'body'), max_length=255, blank=True, null=True, help_text=u'商品描述')
+ total_fee = models.IntegerField(_(u'total_fee'), default=0, help_text=u'总金额')
+
+ pay_status = models.IntegerField(_(u'pay_status'), choices=PAY_STATUS, default=WAITING_PAY, help_text=u'支付状态', db_index=True)
+ paid_at = models.DateTimeField(_(u'paid_at'), blank=True, null=True, help_text=_(u'支付时间'))
+
+ class Meta:
+ verbose_name = _('orderinfo')
+ verbose_name_plural = _('orderinfo')
+
+ def __unicode__(self):
+ return u'{0.pk}'.format(self)
+
+ @property
+ def data(self):
+ return {
+ 'order_id': self.order_id,
+ 'from_uid': self.from_uid,
+ 'to_lid': self.to_lid,
+ 'to_uid': self.to_uid,
+ 'pay_status': self.pay_status,
+ 'paid_at': self.paid_at,
+ 'created_at': self.created_at,
+ }
@@ -0,0 +1,3 @@ |
||
1 |
+from django.test import TestCase |
|
2 |
+ |
|
3 |
+# Create your tests here. |
@@ -0,0 +1,94 @@ |
||
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+from django.conf import settings |
|
4 |
+from django.db import transaction |
|
5 |
+from django.http import JsonResponse |
|
6 |
+from django.shortcuts import HttpResponse |
|
7 |
+ |
|
8 |
+from pay.models import OrderInfo |
|
9 |
+ |
|
10 |
+from utils.errno_utils import OrderStatusCode |
|
11 |
+from utils.response_utils import response |
|
12 |
+ |
|
13 |
+from TimeConvert import TimeConvert as tc |
|
14 |
+from wechatpy import WeChatPay, WeChatPayException |
|
15 |
+ |
|
16 |
+import xmltodict |
|
17 |
+ |
|
18 |
+WECHAT = settings.WECHAT |
|
19 |
+ |
|
20 |
+wxpay = WeChatPay(WECHAT['appID'], WECHAT['apiKey'], WECHAT['mchID']) |
|
21 |
+ |
|
22 |
+ |
|
23 |
+@transaction.atomic |
|
24 |
+def order_create_api(request): |
|
25 |
+ from_uid = request.POST.get('from_uid', '') |
|
26 |
+ to_lid = request.POST.get('to_lid', '') |
|
27 |
+ to_uid = request.POST.get('to_uid', '') |
|
28 |
+ |
|
29 |
+ body = request.POST.get('body', '') # 商品描述 |
|
30 |
+ total_fee = int(request.POST.get('total_fee', 0)) # 总金额,单位分 |
|
31 |
+ |
|
32 |
+ # JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app支付,统一下单接口trade_type的传参可参考这里 |
|
33 |
+ trade_type = request.POST.get('trade_type', '') |
|
34 |
+ |
|
35 |
+ # 生成订单 |
|
36 |
+ order = OrderInfo.objects.create(from_uid=from_uid, to_lid=to_lid, to_uid=to_uid, total_fee=total_fee) |
|
37 |
+ |
|
38 |
+ try: |
|
39 |
+ prepay_data = wxpay.order.create( |
|
40 |
+ body=body, |
|
41 |
+ notify_url=settings.API_DOMAIN + '/order/notify_url', |
|
42 |
+ out_trade_no=order.order_id, |
|
43 |
+ total_fee=total_fee, |
|
44 |
+ trade_type=trade_type, |
|
45 |
+ # user_id=None, # 可选,用户在商户appid下的唯一标识。trade_type=JSAPI,此参数必传 |
|
46 |
+ ) |
|
47 |
+ except WeChatPayException: |
|
48 |
+ return response(OrderStatusCode.WX_UNIFIED_ORDER_FAIL) |
|
49 |
+ |
|
50 |
+ prepay_id = prepay_data.get('prepay_id', '') |
|
51 |
+ wxpay_params = wxpay.jsapi.get_jsapi_params(prepay_id) |
|
52 |
+ |
|
53 |
+ return JsonResponse({ |
|
54 |
+ 'status': 200, |
|
55 |
+ 'data': { |
|
56 |
+ 'order_id': order.order_id, |
|
57 |
+ 'prepay_id': prepay_id, |
|
58 |
+ 'wxpay_params': wxpay_params, |
|
59 |
+ } |
|
60 |
+ }) |
|
61 |
+ |
|
62 |
+ |
|
63 |
+def order_paid_success(order): |
|
64 |
+ if order.pay_status == OrderInfo.PAID: |
|
65 |
+ return |
|
66 |
+ |
|
67 |
+ order.pay_status = OrderInfo.PAID |
|
68 |
+ order.paid_at = tc.utc_datetime() |
|
69 |
+ order.save() |
|
70 |
+ |
|
71 |
+ |
|
72 |
+@transaction.atomic |
|
73 |
+def notify_url_api(request): |
|
74 |
+ try: |
|
75 |
+ data = xmltodict.parse(request.body)['xml'] |
|
76 |
+ except xmltodict.ParsingInterrupted: |
|
77 |
+ # 解析 XML 失败 |
|
78 |
+ return HttpResponse(settings.WXPAY_NOTIFY_FAIL) |
|
79 |
+ |
|
80 |
+ out_trade_no = data.get('out_trade_no', '') |
|
81 |
+ return_code = data.get('return_code', '') |
|
82 |
+ result_code = data.get('result_code', '') |
|
83 |
+ |
|
84 |
+ if return_code != 'SUCCESS' or result_code != 'SUCCESS': |
|
85 |
+ return HttpResponse(settings.WXPAY_NOTIFY_FAIL) |
|
86 |
+ |
|
87 |
+ try: |
|
88 |
+ order = OrderInfo.objects.get(order=out_trade_no) |
|
89 |
+ except OrderInfo.DoesNotExist: |
|
90 |
+ return HttpResponse(settings.WXPAY_NOTIFY_FAIL) |
|
91 |
+ |
|
92 |
+ order_paid_success(order) |
|
93 |
+ |
|
94 |
+ return HttpResponse(settings.WXPAY_NOTIFY_SUCCESS) |
@@ -63,7 +63,8 @@ class PhotosInfo(CreateUpdateMixin): |
||
63 | 63 |
def r_photo_url(self): |
64 | 64 |
return u'{0}/{1}'.format(settings.IMG_DOMAIN, self.r_photo_path) if self.r_photo_path else '' |
65 | 65 |
|
66 |
- def _data(self): |
|
66 |
+ @property |
|
67 |
+ def data(self): |
|
67 | 68 |
return { |
68 | 69 |
'pk': self.pk, |
69 | 70 |
'user': self.lensman_id, |
@@ -71,7 +72,8 @@ class PhotosInfo(CreateUpdateMixin): |
||
71 | 72 |
'photo': self.photo_id, |
72 | 73 |
} |
73 | 74 |
|
74 |
- def _detail(self): |
|
75 |
+ @property |
|
76 |
+ def detail(self): |
|
75 | 77 |
return { |
76 | 78 |
'pk': self.pk, |
77 | 79 |
'user': self.lensman_id, |
@@ -79,6 +81,3 @@ class PhotosInfo(CreateUpdateMixin): |
||
79 | 81 |
'photo': self.photo_id, |
80 | 82 |
'photo_url': self.p_photo_url, |
81 | 83 |
} |
82 |
- |
|
83 |
- data = property(_data) |
|
84 |
- detail = property(_detail) |
@@ -2,6 +2,7 @@ CodeConvert==2.0.4 |
||
2 | 2 |
Django==1.8.4 |
3 | 3 |
MySQL-python==1.2.5 |
4 | 4 |
TimeConvert==1.1.6 |
5 |
+cryptography==1.2.1 |
|
5 | 6 |
django-curtail-uuid==1.0.0 |
6 | 7 |
django-multidomain==1.1.4 |
7 | 8 |
django-shortuuidfield==0.1.3 |
@@ -12,5 +13,7 @@ kkconst==1.1.2 |
||
12 | 13 |
pep8==1.6.2 |
13 | 14 |
pillow==2.9.0 |
14 | 15 |
pytz==2015.7 |
16 |
+redis==2.10.5 |
|
15 | 17 |
shortuuid==0.4.2 |
16 | 18 |
uWSGI==2.0.11.1 |
19 |
+wechatpy==1.2.5 |
@@ -15,6 +15,7 @@ class StatusCodeField(ConstIntField): |
||
15 | 15 |
|
16 | 16 |
|
17 | 17 |
class UserStatusCode(BaseStatusCode): |
18 |
+ """ 摄影师/用户相关错误码 400x & 401x """ |
|
18 | 19 |
LENSMAN_NOT_FOUND = StatusCodeField(4000, u'Lensman Not Found', description=u'摄影师不存在') |
19 | 20 |
LENSMAN_PASSWORD_ERROR = StatusCodeField(4001, u'Lensman Password Error', description=u'摄影师密码错误') |
20 | 21 |
USERNAME_HAS_REGISTERED = StatusCodeField(4010, u'Username Has Registered', description=u'用户名已注册') |
@@ -23,27 +24,36 @@ class UserStatusCode(BaseStatusCode): |
||
23 | 24 |
|
24 | 25 |
|
25 | 26 |
class PhotoStatusCode(BaseStatusCode): |
27 |
+ """ 照片相关错误码 403x """ |
|
26 | 28 |
PARAMS_ERROR = StatusCodeField(4039, u'Params Error', description=u'参数错误') |
27 | 29 |
|
28 | 30 |
|
29 | 31 |
class GroupStatusCode(BaseStatusCode): |
32 |
+ """ 群组相关错误码 402x """ |
|
30 | 33 |
GROUP_NOT_FOUND = StatusCodeField(4020, u'Group Not Found', description=u'群组不存在') |
31 | 34 |
GROUP_HAS_LOCKED = StatusCodeField(4021, u'Group Has Locked', description=u'群组已锁定') |
32 | 35 |
NOT_GROUP_ADMIN = StatusCodeField(4022, u'Not Group Admin', description=u'非群组管理员') |
33 | 36 |
NO_UPDATE_PERMISSION = StatusCodeField(40220, u'No Update Permission', description=u'没有更新权限') |
34 | 37 |
NO_LOCK_PERMISSION = StatusCodeField(40221, u'No Lock Permission', description=u'没有锁定权限') |
35 |
- NO_UNLOCK_PERMISSION = StatusCodeField(40221, u'No Unlock Permission', description=u'没有解锁权限') |
|
36 |
- NO_REMOVE_PERMISSION = StatusCodeField(40222, u'No Remove Permission', description=u'没有移除权限') |
|
37 |
- NO_PASS_PERMISSION = StatusCodeField(40223, u'No Pass Permission', description=u'没有通过权限') |
|
38 |
- NO_REFUSE_PERMISSION = StatusCodeField(40224, u'No Refuse Permission', description=u'没有拒绝权限') |
|
38 |
+ NO_UNLOCK_PERMISSION = StatusCodeField(40222, u'No Unlock Permission', description=u'没有解锁权限') |
|
39 |
+ NO_REMOVE_PERMISSION = StatusCodeField(40223, u'No Remove Permission', description=u'没有移除权限') |
|
40 |
+ NO_PASS_PERMISSION = StatusCodeField(40224, u'No Pass Permission', description=u'没有通过权限') |
|
41 |
+ NO_REFUSE_PERMISSION = StatusCodeField(40225, u'No Refuse Permission', description=u'没有拒绝权限') |
|
39 | 42 |
DUPLICATE_JOIN_REQUEST = StatusCodeField(4027, u'Duplicate Join Request', description=u'重复加群申请') |
40 | 43 |
JOIN_REQUEST_NOT_FOUND = StatusCodeField(4028, u'Join Request Not Found', description=u'加群申请不存在') |
41 | 44 |
GROUP_USER_NOT_FOUND = StatusCodeField(4029, u'Group User Not Found', description=u'该用户不在群组') |
42 | 45 |
|
43 | 46 |
|
44 | 47 |
class GroupPhotoStatusCode(BaseStatusCode): |
48 |
+ """ 飞图相关错误码 403x """ |
|
45 | 49 |
GROUP_PHOTO_NOT_FOUND = StatusCodeField(4030, u'Group Photo Not Found', description=u'飞图不存在') |
46 | 50 |
|
47 | 51 |
|
52 |
+class OrderStatusCode(BaseStatusCode): |
|
53 |
+ """ 订单/支付相关错误码 404x """ |
|
54 |
+ WX_UNIFIED_ORDER_FAIL = StatusCodeField(4040, u'WX Unified Order Fail', description=u'微信统一下单失败') |
|
55 |
+ |
|
56 |
+ |
|
48 | 57 |
class MessageStatusCode(BaseStatusCode): |
58 |
+ """ 消息相关错误码 409x """ |
|
49 | 59 |
MESSAGE_NOT_FOUND = StatusCodeField(4091, u'Message Not Found', description=u'消息不存在') |