jiaweiqi 2 éve
szülő
commit
0a5530f68b

+ 3 - 1
.gitignore

@@ -9,4 +9,6 @@ local_settings.*
 *.txt
 *.whl
 venv
-.idea
+.idea
+*.pem
+*.p12

+ 1 - 0
apps/customer/distributor/views.py

@@ -31,6 +31,7 @@ class DataTypeView(APIView):
         '''
         分销商页面用来判断进入那个页面  如果用户登陆且是分销商 要请求下级分销商数据
         如果用户没有登陆或者用户不是分销商 要请求成为分销商要购买的商品信息
+        因为分销中心是一个导航 不能根据登录本地存储数据进行判断跳转到哪个页面
         :param request:
         :return:
         '''

+ 4 - 1
cosmetics_shop/settings.py

@@ -242,4 +242,7 @@ ONLINE = 1
 SALES_STATUS_CHOICES = (
     (OFFLINE, u'下架'),
     (ONLINE, u'上架'),
-)
+)
+
+PRIVATE_CERT_ROOT = os.path.join(BASE_DIR, 'utils/private_cert/')
+PUBLIC_CERT_ROOT = os.path.join(BASE_DIR, 'utils/public_cert/')

+ 2 - 1
requirements

@@ -7,4 +7,5 @@ mysqlclient
 Pillow
 pycryptodome
 requests
-xmltodict
+xmltodict
+cryptography

+ 2 - 2
uis/views/customer/index.html

@@ -28,9 +28,9 @@
           <div class="layui-col-md12">
             <div class="LAY-btns" style="margin-bottom: 10px;">
               <div class="layui-col-xs12">
-                  <div style="float: left">
+                  <!--<div style="float: left">
                       <button class="layui-btn" id="btn_download" ><i class="layui-icon layui-icon-download-circle"></i>导出</button>
-                  </div>
+                  </div>-->
                     <form class="layui-form" lay-filter="query-form-element">
                         <div class="seach_items">
                             <button class="layui-btn" lay-submit lay-filter="query-form-element"><i class="layui-icon layui-icon-search"></i>查询</button>

+ 2 - 2
uis/views/order/order_detail.html

@@ -105,13 +105,13 @@
             even: true, //不开启隔行背景
             cols: [[
                 {title: '序号', type: 'numbers'},
-                {field: 'commodity_name', title: '名称', width: '20%',},
+                {field: 'commodity_name', title: '名称', width: '30%',},
                 {field: 'price', title: '价格', width: '10%',},
                 {field: 'point', title: '积分', width: '10%',},
                 {field: 'count', title: '数量', width: '10%',},
                 {field: 'amount', title: '总金额', width: '10%',},
                 {field: 'point_amount', title: '总积分', width: '10%',},
-                {field: 'images', title: '缩略图', templet: '#paramImage', width: '20%',},
+                {field: 'images', title: '缩略图', templet: '#paramImage', width: '10%',},
             ]]
         });
 

+ 0 - 0
utils/private_cert/__init__.py


+ 0 - 0
utils/public_cert/__init__.py


+ 0 - 0
utils/wechatpayv3/__init__.py


+ 213 - 0
utils/wechatpayv3/core.py

@@ -0,0 +1,213 @@
+# -*- coding: utf-8 -*-
+
+import json
+import os
+from datetime import datetime
+from django.conf import settings
+import requests
+
+from .type import RequestType, SignType
+from .utils import aes_decrypt, build_authorization, hmac_sign, load_certificate, load_private_key, rsa_decrypt, rsa_encrypt, rsa_sign, rsa_verify
+
+
+class Core(object):
+    def __init__(self, mchid, cert_serial_no, apiv3_key, logger=None, proxy=None):
+        self._mchid = mchid                                  # 商户号
+        self._cert_serial_no = cert_serial_no                # 商户证书序列号
+        self._private_key = self._innt_private_key()         # 商户证书私钥
+        self._apiv3_key = apiv3_key                          # 商户APIv3密钥
+        self._gate_way = 'https://api.mch.weixin.qq.com'
+        self._certificates = []
+        self._cert_dir = settings.PUBLIC_CERT_ROOT            # 平台证书存放目录(减少证书下载调用次数)
+        self._logger = logger                                 # 日志记录器
+        self._proxy = proxy                                   # 代理设置
+        self._init_certificates()
+
+    def _innt_private_key(self):
+        '''
+        加载商户私钥  PRIVATE_CERT_ROOT是在settings里边设置商户私钥证书地址
+        :return:
+        '''
+        os_path = settings.PRIVATE_CERT_ROOT + "apiclient_key.pem"
+        with open(os_path) as f:
+            private_key = f.read()
+        return load_private_key(private_key)
+
+    def _init_certificates(self):
+        '''
+        初始化平台证书 如果存在平台证书就加载平台证书 如果没有平台证书或者平台证书过期就下载平台证书
+        :return:
+        '''
+        if self._cert_dir and os.path.exists(self._cert_dir):
+            for file_name in os.listdir(self._cert_dir):
+                if not file_name.lower().endswith('.pem'):
+                    continue
+                with open(self._cert_dir + file_name, encoding="utf-8") as f:
+                    certificate = load_certificate(f.read())
+                now = datetime.utcnow()
+                if certificate and now >= certificate.not_valid_before and now <= certificate.not_valid_after:
+                    self._certificates.append(certificate)
+        if not self._certificates:
+            self._update_certificates()
+        if not self._certificates:
+            raise Exception('未发现平台证书,请仔细检查您的初始化参数!')
+
+    def _update_certificates(self):
+        '''
+        下载平台证书
+        :return:
+        '''
+        path = '/v3/certificates'
+        self._certificates.clear()
+        code, message = self.request(path, skip_verify=True)
+        if code != 200:
+            return
+        data = json.loads(message).get('data')
+        for value in data:
+            serial_no = value.get('serial_no')
+            effective_time = value.get('effective_time')
+            expire_time = value.get('expire_time')
+            encrypt_certificate = value.get('encrypt_certificate')
+            algorithm = nonce = associated_data = ciphertext = None
+            if encrypt_certificate:
+                algorithm = encrypt_certificate.get('algorithm')
+                nonce = encrypt_certificate.get('nonce')
+                associated_data = encrypt_certificate.get('associated_data')
+                ciphertext = encrypt_certificate.get('ciphertext')
+            if not (serial_no and effective_time and expire_time and algorithm and nonce and associated_data and ciphertext):
+                continue
+            cert_str = aes_decrypt(nonce=nonce, ciphertext=ciphertext, associated_data=associated_data, apiv3_key=self._apiv3_key)
+            certificate = load_certificate(cert_str)
+            if not certificate:
+                continue
+            now = datetime.utcnow()
+            if now < certificate.not_valid_before or now > certificate.not_valid_after:
+                continue
+            self._certificates.append(certificate)
+            if not self._cert_dir:
+                continue
+            if not os.path.exists(self._cert_dir):
+                os.makedirs(self._cert_dir)
+            if not os.path.exists(self._cert_dir + serial_no + '.pem'):
+                with open(self._cert_dir + serial_no + '.pem', 'w') as f:
+                    f.write(cert_str)
+
+    def _verify_signature(self, headers, body):
+        signature = headers.get('Wechatpay-Signature')
+        timestamp = headers.get('Wechatpay-Timestamp')
+        nonce = headers.get('Wechatpay-Nonce')
+        serial_no = headers.get('Wechatpay-Serial')
+        cert_found = False
+        for cert in self._certificates:
+            if int('0x' + serial_no, 16) == cert.serial_number:
+                cert_found = True
+                certificate = cert
+                break
+        if not cert_found:
+            self._update_certificates()
+            for cert in self._certificates:
+                if int('0x' + serial_no, 16) == cert.serial_number:
+                    cert_found = True
+                    certificate = cert
+                    break
+            if not cert_found:
+                return False
+        if not rsa_verify(timestamp, nonce, body, signature, certificate):
+            return False
+        return True
+
+    def request(self, path, method=RequestType.GET, data=None, skip_verify=False, sign_data=None, files=None, cipher_data=False, headers={}):
+        headers.update({'Content-Type': 'application/json'})
+        if files:
+            headers['Content-Type'] = 'multipart/form-data'
+        headers.update({'Accept': 'application/json'})
+        if cipher_data:
+            headers.update({'Wechatpay-Serial': hex(self._last_certificate().serial_number)[2:].upper()})
+        authorization = build_authorization(path, method.value, self._mchid, self._cert_serial_no, self._private_key, data=sign_data if sign_data else data)
+        headers.update({'Authorization': authorization})
+        if method == RequestType.GET:
+            response = requests.get(url=self._gate_way + path, headers=headers, proxies=self._proxy)
+        elif method == RequestType.POST:
+            response = requests.post(url=self._gate_way + path, json=None if files else data, data=data if files else None, headers=headers, files=files, proxies=self._proxy)
+        elif method == RequestType.PATCH:
+            response = requests.patch(url=self._gate_way + path, json=data, headers=headers, proxies=self._proxy)
+        elif method == RequestType.PUT:
+            response = requests.put(url=self._gate_way + path, json=data, headers=headers, proxies=self._proxy)
+        elif method == RequestType.DELETE:
+            response = requests.delete(url=self._gate_way + path, headers=headers, proxies=self._proxy)
+        else:
+            raise Exception('请求类型不被支持!')
+        if response.status_code in range(200, 300) and not skip_verify:
+            if not self._verify_signature(response.headers, response.text):
+                raise Exception('验证签名失败!')
+        return response.status_code, response.text if 'application/json' in response.headers.get('Content-Type') else response.content
+
+    def sign(self, data, sign_type=SignType.RSA_SHA256):
+        if sign_type == SignType.RSA_SHA256:
+            sign_str = '\n'.join(data) + '\n'
+            return rsa_sign(self._private_key, sign_str)
+        elif sign_type == SignType.HMAC_SHA256:
+            key_list = sorted(data.keys())
+            sign_str = ''
+            for k in key_list:
+                v = data[k]
+                sign_str += str(k) + '=' + str(v) + '&'
+            sign_str += 'key=' + self._apiv3_key
+            return hmac_sign(self._apiv3_key, sign_str)
+        else:
+            raise Exception('错误的签名类型!')
+
+    def decrypt_callback(self, headers, body):
+        if isinstance(body, bytes):
+            body = body.decode('UTF-8')
+        if not self._verify_signature(headers, body):
+            return None
+        data = json.loads(body)
+        resource_type = data.get('resource_type')
+        if resource_type != 'encrypt-resource':
+            return None
+        resource = data.get('resource')
+        if not resource:
+            return None
+        algorithm = resource.get('algorithm')
+        if algorithm != 'AEAD_AES_256_GCM':
+            raise Exception('该算法不被支持!')
+        nonce = resource.get('nonce')
+        ciphertext = resource.get('ciphertext')
+        associated_data = resource.get('associated_data')
+        if not (nonce and ciphertext):
+            return None
+        if not associated_data:
+            associated_data = ''
+        result = aes_decrypt(
+            nonce=nonce,
+            ciphertext=ciphertext,
+            associated_data=associated_data,
+            apiv3_key=self._apiv3_key)
+        return result
+
+    def callback(self, headers, body):
+        if isinstance(body, bytes):
+            body = body.decode('UTF-8')
+        result = self.decrypt_callback(headers=headers, body=body)
+        if result:
+            data = json.loads(body)
+            data.update({'resource': json.loads(result)})
+            return data
+        else:
+            return result
+
+    def decrypt(self, ciphtext):
+        return rsa_decrypt(ciphertext=ciphtext, private_key=self._private_key)
+
+    def encrypt(self, text):
+        return rsa_encrypt(text=text, certificate=self._last_certificate())
+
+    def _last_certificate(self):
+        if not self._certificates:
+            self._update_certificates()
+        certificate = self._certificates[0]
+        for cert in self._certificates:
+            if certificate.not_valid_after < cert.not_valid_after:
+                certificate = cert
+        return certificate

+ 66 - 0
utils/wechatpayv3/transfer.py

@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+
+from .type import RequestType
+from .core import Core
+
+from apps.WechatApplet.models import WechatApplet
+
+
+class Transfer(object):
+    def __init__(self, appid):
+        wx = WechatApplet.objects.filter(authorizer_appid=appid).first()
+        if not wx:
+            raise Exception(u'小程序appid认证失败!')
+        self._core = Core(wx.agent_num, wx.cert_serial_no, wx.apiv3_key)
+
+    def transfer_batch(self, appid, out_batch_no, batch_name, batch_remark, total_amount, total_num, transfer_detail_list=[]):
+        '''
+        商户可以通过该接口同时向多个用户微信零钱进行转账操作
+        :param appid string[1,32] 直连商户的appid: 申请商户号的appid或商户号绑定的appid(企业号corpid即为此appid)示例值:wxf636efh567hg4356
+        :param out_batch_no string[1,32] 商家批次单号: 商户系统内部的商家批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013'
+        :param batch_name string[1,32] 批次名称: 该笔批量转账的名称 示例值:2019年1月深圳分部报销单
+        :param batch_remark string[1,32] 批次备注: 转账说明,UTF8编码,最多允许32个字符  示例值:2019年1月深圳分部报销单
+        :param total_amount int 转账总金额: 转账金额单位为“分”。转账总金额必须与批次内所有明细转账金额之和保持一致,否则无法发起转账操作
+        :param total_num int 转账总笔数: 一个转账批次单最多发起三千笔转账。转账总笔数必须与批次内所有明细之和保持一致,否则无法发起转账操作
+        :param transfer_detail_list array 转账明细列表: 发起批量转账的明细列表,最多三千笔
+               out_detail_no string[1,32] 商家明细单号 商户系统内部区分转账批次单下不同转账明细单的唯一标识,要求此参数只能由数字、大小写字母组成  示例值:x23zy545Bd5436
+               transfer_amount int 转账金额 转账金额单位为分
+               transfer_remark string[1,32] 转账备注 单条转账备注(微信用户会收到该备注),UTF8编码,最多允许32个字符   示例值:2020年4月报销
+               openid string[1,128] 用户在直连商户应用下的用户标示
+        注意:
+        商户上送敏感信息时使用微信支付平台公钥加密,证书序列号包含在请求HTTP头部的Wechatpay-Serial
+        批量转账一旦发起后,不允许撤销,批次受理成功后开始执行转账
+        转账批次单中涉及金额的字段单位为  分
+        当返回错误码为 "SYSTEM_ERROR" 时,请不要更换商家批次单号, 一定要使用原商家批次单号重试,否则可能造成重复转账等资金风险。
+        不同的商家批次单号 out_batch_no  请求为一个全新的批次, 在未查询到明确的转账批次但处理结果之前,请勿修改商家批次单号 重新提交!
+        请商户在自身的系统中合理设置转账频次并做好并发控制,防范错付风险
+
+        https://api.mch.weixin.qq.com/v3/transfer/batches
+        POST
+        '''
+        if not appid:
+            raise Exception(u'缺少参数appid')
+        if not out_batch_no:
+            raise Exception(u'缺少参数out_batch_no')
+        if not out_batch_no:
+            raise Exception(u'缺少参数batch_name')
+        if not out_batch_no:
+            raise Exception(u'缺少参数batch_remark')
+        if not out_batch_no:
+            raise Exception(u'缺少参数total_amount')
+        if not out_batch_no:
+            raise Exception(u'缺少参数total_num')
+        if not transfer_detail_list:
+            raise Exception(u'缺少参数transfer_detail_list')
+
+        params = {
+            'appid': appid,
+            'out_batch_no': out_batch_no,
+            'batch_name': batch_name,
+            'batch_remark': batch_remark,
+            'total_amount': total_amount,
+            'total_num': total_num,
+            'transfer_detail_list': transfer_detail_list
+        }
+        path = '/v3/transfer/batches'
+        return self._core.request(path, method=RequestType.POST, data=params, cipher_data=False)

+ 26 - 0
utils/wechatpayv3/type.py

@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+from enum import Enum, unique
+
+
+@unique
+class RequestType(Enum):
+    GET = 'GET'
+    POST = 'POST'
+    PATCH = 'PATCH'
+    PUT = 'PUT'
+    DELETE = 'DELETE'
+
+
+class WeChatPayType(Enum):
+    JSAPI = 0
+    APP = 1
+    H5 = 2
+    NATIVE = 3
+    MINIPROG = 4
+
+
+class SignType(Enum):
+    RSA_SHA256 = 0
+    HMAC_SHA256 = 1
+    MD5 = 2

+ 152 - 0
utils/wechatpayv3/utils.py

@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+
+import json
+import time
+import uuid
+from base64 import b64decode, b64encode
+
+from cryptography.exceptions import InvalidSignature, InvalidTag
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PKCS1v15
+from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SM3, Hash
+from cryptography.hazmat.primitives.hmac import HMAC
+from cryptography.hazmat.primitives.serialization import load_pem_private_key
+from cryptography.x509 import load_pem_x509_certificate
+
+
+def build_authorization(path, method, mchid, serial_no, private_key, data=None, nonce_str=None):
+    '''
+    一、构建签名串
+        签名串一共有五行,每一行为一个参数。行尾以 \n(换行符,ASCII编码值为0x0A)结束,包括最后一行。如果参数本身以\n结束,也需要附加一个\n。
+        第一步,获取HTTP请求的方法(GET, POST, PUT)等
+        第二步,获取请求的绝对URL,并去除域名部分得到参与签名的URL。如果请求中有查询参数,URL末尾应附加有'?'和对应的查询字符串。
+        第三步,获取发起请求时的系统当前时间戳,即格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数,作为请求时间戳。微信支付会拒绝处理很久之前发起的请求,请商户保持自身系统的时间准确。
+        第四步,生成一个请求随机串,可参见生成随机数算法。这里,我们使用命令行直接生成一个。
+        第五步,获取请求中的请求报文主体(request body)。
+            请求方法为GET时,报文主体为空。
+            当请求方法为POST或PUT时,请使用真实发送的JSON报文。
+            图片上传API,请使用meta对应的JSON报文。
+            对于下载证书的接口来说,请求报文主体是一个空串。
+        二、计算签名值
+        绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。
+        三、设置HTTP头
+         微信支付商户API V3要求通过HTTP Authorization头来传递签名  Authorization由认证类型和签名信息两部分组成
+        1.认证类型,目前为WECHATPAY2-SHA256-RSA2048
+        2.签名信息
+            发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
+            商户API证书序列号serial_no,用于声明所使用的证书
+            请求随机串nonce_str
+            时间戳timestamp
+            签名值signature
+        注:以上五项签名信息,无顺序要求。
+    :param path:
+    :param method:
+    :param mchid:
+    :param serial_no:
+    :param private_key:
+    :param data:
+    :param nonce_str:
+    :return:
+    '''
+    timeStamp = str(int(time.time()))
+    nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper()
+    body = data if isinstance(data, str) else json.dumps(data) if data else ''
+    sign_str = '%s\n%s\n%s\n%s\n%s\n' % (method, path, timeStamp, nonce_str, body)
+    signature = rsa_sign(private_key=private_key, sign_str=sign_str)
+    authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"' % (mchid, nonce_str, signature, timeStamp, serial_no)
+    return authorization
+
+
+def rsa_sign(private_key, sign_str):
+    message = sign_str.encode('UTF-8')
+    signature = private_key.sign(data=message, padding=PKCS1v15(), algorithm=SHA256())
+    sign = b64encode(signature).decode('UTF-8').replace('\n', '')
+    return sign
+
+
+def aes_decrypt(nonce, ciphertext, associated_data, apiv3_key):
+    key_bytes = apiv3_key.encode('UTF-8')
+    nonce_bytes = nonce.encode('UTF-8')
+    associated_data_bytes = associated_data.encode('UTF-8')
+    data = b64decode(ciphertext)
+    aesgcm = AESGCM(key=key_bytes)
+    try:
+        result = aesgcm.decrypt(nonce=nonce_bytes, data=data, associated_data=associated_data_bytes).decode('UTF-8')
+    except InvalidTag:
+        result = None
+    return result
+
+
+def format_private_key(private_key_str):
+    pem_start = '-----BEGIN PRIVATE KEY-----\n'
+    pem_end = '\n-----END PRIVATE KEY-----'
+    if not private_key_str.startswith(pem_start):
+        private_key_str = pem_start + private_key_str
+    if not private_key_str.endswith(pem_end):
+        private_key_str = private_key_str + pem_end
+    return private_key_str
+
+
+def load_certificate(certificate_str):
+    try:
+        return load_pem_x509_certificate(data=certificate_str.encode('UTF-8'), backend=default_backend())
+    except:
+        return None
+
+
+def load_private_key(private_key_str):
+    try:
+        return load_pem_private_key(data=format_private_key(private_key_str).encode('UTF-8'), password=None, backend=default_backend())
+    except:
+        raise Exception('商户证书私钥加载失败!')
+
+
+def rsa_verify(timestamp, nonce, body, signature, certificate):
+    sign_str = '%s\n%s\n%s\n' % (timestamp, nonce, body)
+    public_key = certificate.public_key()
+    message = sign_str.encode('UTF-8')
+    signature = b64decode(signature)
+    try:
+        public_key.verify(signature, message, PKCS1v15(), SHA256())
+    except InvalidSignature:
+        return False
+    return True
+
+
+def rsa_encrypt(text, certificate):
+    data = text.encode('UTF-8')
+    public_key = certificate.public_key()
+    cipherbyte = public_key.encrypt(
+        plaintext=data,
+        padding=OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None)
+    )
+    return b64encode(cipherbyte).decode('UTF-8')
+
+
+def rsa_decrypt(ciphertext, private_key):
+    data = private_key.decrypt(
+        ciphertext=b64decode(ciphertext),
+        padding=OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None)
+    )
+    result = data.decode('UTF-8')
+    return result
+
+
+def hmac_sign(key, sign_str):
+    hmac = HMAC(key.encode('UTF-8'), SHA256())
+    hmac.update(sign_str.encode('UTF-8'))
+    sign = hmac.finalize().hex().upper()
+    return sign
+
+
+def sha256(data):
+    hash = Hash(SHA256())
+    hash.update(data)
+    return hash.finalize().hex()
+
+
+def sm3(data):
+    hash = Hash(SM3())
+    hash.update(data)
+    return hash.finalize().hex()