+
+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'消息不存在') |