| @@ -5,8 +5,9 @@ from django.conf.urls import url | ||
| 5 | 5 | from account import views as account_views | 
| 6 | 6 | from group import views as group_views | 
| 7 | 7 | from message import views as message_views | 
| 8 | -from photo import views as photo_views | |
| 9 | 8 | from operation import views as op_views | 
| 9 | +from pay import views as pay_views | |
| 10 | +from photo import views as photo_views | |
| 10 | 11 |  | 
| 11 | 12 |  | 
| 12 | 13 | # 帐户相关 | 
| @@ -69,3 +70,9 @@ urlpatterns += [ | ||
| 69 | 70 | url(r'^op/upgrade$', op_views.upgrade_api, name='upgrade_api'), # APP 升级 | 
| 70 | 71 | url(r'^op/splash$', op_views.splash_api, name='splash_api'), # 启动页面 | 
| 71 | 72 | ] | 
| 73 | + | |
| 74 | +# 支付相关 | |
| 75 | +urlpatterns += [ | |
| 76 | + url(r'^order/create$', pay_views.order_create_api, name='order_create_api'), # 订单创建 | |
| 77 | + url(r'^order/notify_url$', pay_views.notify_url_api, name='notify_url_api'), # 支付异步通知回调地址 | |
| 78 | +] | 
| @@ -25,6 +25,9 @@ import os | ||
| 25 | 25 | import shortuuid | 
| 26 | 26 |  | 
| 27 | 27 |  | 
| 28 | +r = settings.REDIS_CACHE | |
| 29 | + | |
| 30 | + | |
| 28 | 31 | @transaction.atomic | 
| 29 | 32 | def group_create_api(request): | 
| 30 | 33 |      user_id = request.POST.get('user_id', '') | 
| @@ -0,0 +1,11 @@ | ||
| 1 | +# -*- coding: utf-8 -*- | |
| 2 | + | |
| 3 | +import redis | |
| 4 | + | |
| 5 | + | |
| 6 | +def redis_connect(settings): | |
| 7 | +    return redis.StrictRedis(**{ | |
| 8 | +        'host': settings.get('HOST', ''), | |
| 9 | +        'port': settings.get('PORT', 0), | |
| 10 | +        'password': '{user}:{pwd}'.format(settings.get('USER', ''), settings.get('PASSWORD', '')) if settings.get('USER', '') else '' | |
| 11 | + }) | 
| @@ -46,6 +46,7 @@ INSTALLED_APPS = ( | ||
| 46 | 46 | 'group', | 
| 47 | 47 | 'message', | 
| 48 | 48 | 'operation', | 
| 49 | + 'pay', | |
| 49 | 50 | 'photo', | 
| 50 | 51 | ) | 
| 51 | 52 |  | 
| @@ -158,6 +159,41 @@ REST_FRAMEWORK = { | ||
| 158 | 159 | 'PAGE_SIZE': 1 | 
| 159 | 160 | } | 
| 160 | 161 |  | 
| 162 | +# Redis 设置 | |
| 163 | +REDIS = { | |
| 164 | +    'default': { | |
| 165 | + 'HOST': '127.0.0.1', | |
| 166 | + 'PORT': 6379, | |
| 167 | + 'USER': '', | |
| 168 | + 'PASSWORD': '' | |
| 169 | + } | |
| 170 | +} | |
| 171 | + | |
| 172 | +# Redis 缓存时间设置 | |
| 173 | +REDIS_EXPIRED_HOUR = 3600 # 60 * 60 | |
| 174 | +REDIS_EXPIRED_DAY = 86400 # 24 * 60 * 60 | |
| 175 | +REDIS_EXPIRED_WEEK = 604800 # 7 * 24 * 60 * 60 | |
| 176 | +REDIS_EXPIRED_MONTH = 2678400 # 31 * 24 * 60 * 60 | |
| 177 | +REDIS_EXPIRED_YEAR = 31622400 # 366 * 24 * 60 * 60 | |
| 178 | + | |
| 179 | +# 微信设置 | |
| 180 | +WECHAT = { | |
| 181 | + 'token': '5201314', | |
| 182 | + 'appID': '', | |
| 183 | + 'appsecret': '', | |
| 184 | + 'mchID': '', | |
| 185 | + 'apiKey': '', | |
| 186 | +} | |
| 187 | + | |
| 188 | +WECHAT_GET_CODE = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=%s#wechat_redirect' | |
| 189 | +WECHAT_GET_CODE_USERINFO = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=%s#wechat_redirect' | |
| 190 | +WECHAT_GET_ACCESS_TOKEN = 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code' | |
| 191 | + | |
| 192 | +WECHAT_GET_USERINFO = 'https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s' | |
| 193 | + | |
| 194 | +WXPAY_NOTIFY_SUCCESS = '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>' | |
| 195 | +WXPAY_NOTIFY_FAIL = '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[XML PARSE FAIL]]></return_msg></xml>' | |
| 196 | + | |
| 161 | 197 | # 唯一标识设置 | 
| 162 | 198 | CURTAIL_UUID_LENGTH = 7 | 
| 163 | 199 |  | 
| @@ -168,10 +204,9 @@ WATERMARK_LOGO = os.path.join(PROJ_DIR, 'static/pai2/img/paiai_96_96.png').repla | ||
| 168 | 204 | THUMBNAIL_MAX_WIDTH = 360 | 
| 169 | 205 |  | 
| 170 | 206 | # 域名设置 | 
| 171 | -# DOMAIN = 'http://xfoto.com.cn' | |
| 172 | -# IMG_DOMAIN = 'http://img.xfoto.com.cn' | |
| 173 | 207 | DOMAIN = 'http://pai.ai' | 
| 174 | 208 | IMG_DOMAIN = 'http://img.pai.ai' | 
| 209 | +API_DOMAIN = 'http://api.pai.ai' | |
| 175 | 210 |  | 
| 176 | 211 | # 消息图片设置 | 
| 177 | 212 | PAI2_LOGO_URL = DOMAIN + '/static/pai2/img/paiai_96_96.png' | 
| @@ -186,3 +221,9 @@ try: | ||
| 186 | 221 | from local_settings import * | 
| 187 | 222 | except ImportError: | 
| 188 | 223 | pass | 
| 224 | + | |
| 225 | +try: | |
| 226 | + from func_settings import redis_connect | |
| 227 | +    REDIS_CACHE = redis_connect(REDIS.get('default', {})) | |
| 228 | +except ImportError: | |
| 229 | + REDIS_CACHE = None | 
| @@ -0,0 +1,13 @@ | ||
| 1 | +# -*- coding: utf-8 -*- | |
| 2 | + | |
| 3 | +from django.contrib import admin | |
| 4 | + | |
| 5 | +from pay.models import OrderInfo | |
| 6 | + | |
| 7 | + | |
| 8 | +class OrderInfoAdmin(admin.ModelAdmin): | |
| 9 | +    list_display = ('order_id', 'from_uid', 'to_lid', 'to_uid', 'pay_status', 'paid_at', 'status', 'created_at', 'updated_at') | |
| 10 | +    list_filter = ('pay_status', 'status') | |
| 11 | + | |
| 12 | + | |
| 13 | +admin.site.register(OrderInfo, OrderInfoAdmin) | 
| @@ -0,0 +1,35 @@ | ||
| 1 | +# -*- coding: utf-8 -*- | |
| 2 | +from __future__ import unicode_literals | |
| 3 | + | |
| 4 | +from django.db import models, migrations | |
| 5 | +import shortuuidfield.fields | |
| 6 | + | |
| 7 | + | |
| 8 | +class Migration(migrations.Migration): | |
| 9 | + | |
| 10 | + dependencies = [ | |
| 11 | + ] | |
| 12 | + | |
| 13 | + operations = [ | |
| 14 | + migrations.CreateModel( | |
| 15 | + name='OrderInfo', | |
| 16 | + fields=[ | |
| 17 | +                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | |
| 18 | +                ('status', models.BooleanField(default=True, help_text='\u72b6\u6001', db_index=True, verbose_name='status')), | |
| 19 | +                ('created_at', models.DateTimeField(help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at', auto_now_add=True)), | |
| 20 | +                ('updated_at', models.DateTimeField(help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at', auto_now=True)), | |
| 21 | +                ('order_id', shortuuidfield.fields.ShortUUIDField(help_text='\u8ba2\u5355\u552f\u4e00\u6807\u8bc6', max_length=22, editable=False, db_index=True, blank=True)), | |
| 22 | +                ('from_uid', models.CharField(help_text='\u4ed8\u6b3e\u7528\u6237\u552f\u4e00\u6807\u8bc6', max_length=255, verbose_name='from_uid', db_index=True)), | |
| 23 | +                ('to_lid', models.CharField(max_length=255, blank=True, help_text='\u6536\u6b3e\u6444\u5f71\u5e08\u552f\u4e00\u6807\u8bc6', null=True, verbose_name='to_lid', db_index=True)), | |
| 24 | +                ('to_uid', models.CharField(max_length=255, blank=True, help_text='\u6536\u6b3e\u7528\u6237\u552f\u4e00\u6807\u8bc6', null=True, verbose_name='to_uid', db_index=True)), | |
| 25 | +                ('body', models.CharField(help_text='\u5546\u54c1\u63cf\u8ff0', max_length=255, null=True, verbose_name='body', blank=True)), | |
| 26 | +                ('total_fee', models.IntegerField(default=0, help_text='\u603b\u91d1\u989d', verbose_name='total_fee')), | |
| 27 | +                ('pay_status', models.IntegerField(default=0, help_text='\u652f\u4ed8\u72b6\u6001', db_index=True, verbose_name='pay_status', choices=[(0, '\u5f85\u652f\u4ed8'), (1, '\u5df2\u652f\u4ed8')])), | |
| 28 | +                ('paid_at', models.DateTimeField(help_text='\u652f\u4ed8\u65f6\u95f4', null=True, verbose_name='paid_at', blank=True)), | |
| 29 | + ], | |
| 30 | +            options={ | |
| 31 | + 'verbose_name': 'orderinfo', | |
| 32 | + 'verbose_name_plural': 'orderinfo', | |
| 33 | + }, | |
| 34 | + ), | |
| 35 | + ] | 
| @@ -0,0 +1,52 @@ | ||
| 1 | +# -*- coding: utf-8 -*- | |
| 2 | + | |
| 3 | +from django.conf import settings | |
| 4 | +from django.db import models | |
| 5 | +from django.utils.translation import ugettext_lazy as _ | |
| 6 | + | |
| 7 | +from shortuuidfield import ShortUUIDField | |
| 8 | + | |
| 9 | +from pai2.basemodels import CreateUpdateMixin | |
| 10 | + | |
| 11 | + | |
| 12 | +class OrderInfo(CreateUpdateMixin): | |
| 13 | + WAITING_PAY = 0 | |
| 14 | + PAID = 1 | |
| 15 | + # DELETED = 2 | |
| 16 | + | |
| 17 | + PAY_STATUS = ( | |
| 18 | + (WAITING_PAY, u'待支付'), | |
| 19 | + (PAID, u'已支付'), | |
| 20 | + # (DELETED, u'已删除'), | |
| 21 | + ) | |
| 22 | + | |
| 23 | + order_id = ShortUUIDField(_(u'order_id'), max_length=255, help_text=u'订单唯一标识', db_index=True) | |
| 24 | + | |
| 25 | + from_uid = models.CharField(_(u'from_uid'), max_length=255, help_text=u'付款用户唯一标识', db_index=True) | |
| 26 | + to_lid = models.CharField(_(u'to_lid'), max_length=255, blank=True, null=True, help_text=u'收款摄影师唯一标识', db_index=True) | |
| 27 | + to_uid = models.CharField(_(u'to_uid'), max_length=255, blank=True, null=True, help_text=u'收款用户唯一标识', db_index=True) | |
| 28 | + | |
| 29 | + body = models.CharField(_(u'body'), max_length=255, blank=True, null=True, help_text=u'商品描述') | |
| 30 | + total_fee = models.IntegerField(_(u'total_fee'), default=0, help_text=u'总金额') | |
| 31 | + | |
| 32 | + pay_status = models.IntegerField(_(u'pay_status'), choices=PAY_STATUS, default=WAITING_PAY, help_text=u'支付状态', db_index=True) | |
| 33 | + paid_at = models.DateTimeField(_(u'paid_at'), blank=True, null=True, help_text=_(u'支付时间')) | |
| 34 | + | |
| 35 | + class Meta: | |
| 36 | +        verbose_name = _('orderinfo') | |
| 37 | +        verbose_name_plural = _('orderinfo') | |
| 38 | + | |
| 39 | + def __unicode__(self): | |
| 40 | +        return u'{0.pk}'.format(self) | |
| 41 | + | |
| 42 | + @property | |
| 43 | + def data(self): | |
| 44 | +        return { | |
| 45 | + 'order_id': self.order_id, | |
| 46 | + 'from_uid': self.from_uid, | |
| 47 | + 'to_lid': self.to_lid, | |
| 48 | + 'to_uid': self.to_uid, | |
| 49 | + 'pay_status': self.pay_status, | |
| 50 | + 'paid_at': self.paid_at, | |
| 51 | + 'created_at': self.created_at, | |
| 52 | + } | 
| @@ -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'消息不存在') |