python 微信开发入门篇-微信支付回调+对账单下载(三)
继续上一章节新建的项目,本节完成微信订单下载,支付回调,对账单下载模块
安装 django-simpleui,django-import-export
pip install django-simpleui
pip install django-import-export
pip install wechatpy[cryptography]
settings.py 配置文件如下
""" Django settings for wechatDemo project. Generated by 'django-admin startproject' using Django 2.2.6. For more information on this file, see https://docs.djangoproject.com/en/2.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.2/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '#!2+_0h*+v-i&)yw0d(kkj_&p9smf&6^h0vakxv!^#@i&y3cct' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ 'simpleui', 'import_export', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'app' ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'wechatDemo.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')] , 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], 'libraries': { 'apptags': 'app.templatetags.apptags' }, }, }, ] WSGI_APPLICATION = 'wechatDemo.wsgi.application' # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } # Password validation # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = '/static/'
项目完整目录结构:
数据库设计(商品表,订单表,对账表,对账详细表)
项目中modes.py 源码如下
from django.db import models # Create your models here. # 商品表 class commodity(models.Model): name = models.CharField(max_length=225, verbose_name="商品名称", blank=True, default="") desc = models.TextField(verbose_name="商品描述", blank=True) price = models.FloatField(verbose_name="价格", default=0) class Meta: verbose_name_plural = "商品表" def __str__(self): return self.name # 订单表 class order(models.Model): ordercode = models.CharField(max_length=255, verbose_name="订单号") openid = models.CharField(max_length=255, verbose_name="openid", default="") commodity = models.ManyToManyField(to='commodity', verbose_name="商品表", null=True, blank=True) STATE_CHOICES = ( (0, '待支付'), (1, '已支付') ) state = models.CharField(max_length=2, choices=STATE_CHOICES, default=0, ) # 支付返回结果 appid = models.CharField(max_length=255, verbose_name="appid", default="") bank_type = models.CharField(max_length=255, verbose_name="bank_type", default="") cash_fee = models.IntegerField(verbose_name="支付金额(分)", default=0) fee_type = models.CharField(max_length=255, verbose_name="支付类型", default="") is_subscribe = models.CharField(max_length=255, verbose_name="is_subscribe", default="") mch_id = models.CharField(max_length=255, verbose_name="mch_id", default="") nonce_str = models.CharField(max_length=255, verbose_name="nonce_str", default="") out_trade_no = models.CharField(max_length=255, verbose_name="商户订单号", default="") result_code = models.CharField(max_length=255, verbose_name="result_code", default="") return_code = models.CharField(max_length=255, verbose_name="return_code", default="") time_end = models.CharField(max_length=255, verbose_name="time_end", default="") total_fee = models.CharField(max_length=255, verbose_name="total_fee", default="") trade_type = models.CharField(max_length=255, verbose_name="trade_type", default="") transaction_id = models.CharField(max_length=255, verbose_name="transaction_id", default="") sign = models.CharField(max_length=255, verbose_name="sign", default="") # 对账表 class bill(models.Model): jysh = models.CharField(max_length=255, verbose_name="交易时间", default="") appid = models.CharField(max_length=255, verbose_name="公众账号ID", default="") mch_id = models.CharField(max_length=255, verbose_name="商户号", default="") tyshh = models.CharField(max_length=255, verbose_name="特约商户号", default="") sbh = models.CharField(max_length=255, verbose_name="设备号", default="") wxdd = models.CharField(max_length=255, verbose_name="微信订单号", default="") shdd = models.CharField(max_length=255, verbose_name="商户订单号", default="") openid = models.CharField(max_length=255, verbose_name="用户标识", default="") jylx = models.CharField(max_length=255, verbose_name="交易类型", default="") jyzt = models.CharField(max_length=255, verbose_name="交易状态", default="") fkyh = models.CharField(max_length=255, verbose_name="付款银行", default="") hbzl = models.CharField(max_length=255, verbose_name="货币种类", default="") yjddje = models.CharField(max_length=255, verbose_name="应结订单金额", default="") djjje = models.CharField(max_length=255, verbose_name="代金券金额", default="") wxtkdh = models.CharField(max_length=255, verbose_name="微信退款单号", default="") hstkdh = models.CharField(max_length=255, verbose_name="商户退款单号", default="") tkje = models.CharField(max_length=255, verbose_name="退款金额", default="") czjtkje = models.CharField(max_length=255, verbose_name="充值券退款金额", default="") tklx = models.CharField(max_length=255, verbose_name="退款类型", default="") tkzt = models.CharField(max_length=255, verbose_name="退款状态", default="") spmc = models.CharField(max_length=255, verbose_name="商品名称", default="") shsjb = models.CharField(max_length=255, verbose_name="商户数据包", default="") sxf = models.CharField(max_length=255, verbose_name="手续费", default="") fx = models.CharField(max_length=255, verbose_name="费率", default="") ddje = models.CharField(max_length=255, verbose_name="订单金额", default="") sqtkje = models.CharField(max_length=255, verbose_name="申请退款金额", default="") flbz = models.CharField(max_length=255, verbose_name="费率备注", default="")
执行数据库迁移
python manage.py makemigrations python manage.py migrate
admin.py 源码
from django.contrib import admin from app import models from import_export import resources from import_export.admin import ImportExportModelAdmin from django.apps import apps admin.site.site_header = "XXX后台管理" admin.site.site_title = "XXX后台" # Register your models here. class commodityResource(resources.ModelResource): def __init__(self): super(commodityResource, self).__init__() field_list = models.commodity._meta.fields self.vname_dict = {} for i in field_list: self.vname_dict[i.name] = i.verbose_name # 默认导入导出field的column_name为字段的名称,这里修改为字段的verbose_name def get_export_fields(self): fields = self.get_fields() for field in fields: field_name = self.get_field_name(field) # 如果我们设置过verbose_name,则将column_name替换为verbose_name。否则维持原有的字段名 if field_name in self.vname_dict.keys(): field.column_name = self.vname_dict[field_name] return fields def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): print("after_import") def after_import_instance(self, instance, new, **kwargs): print("after_import_instance") class Meta: model = models.commodity skip_unchanged = True report_skipped = True fields = ("id", "name", "desc", "price",) @admin.register(models.commodity) class AppTypeAdmin(ImportExportModelAdmin): list_display = ("name", "price", "desc") list_display_links = ("name", "price", "desc") search_fields = ('name', "price", 'desc') model_icon = "fa fa-tag" list_per_page = 50 resource_class = commodityResource def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) @admin.register(models.order) class orderAdmin(admin.ModelAdmin): list_display = ("ordercode", "openid") filter_horizontal = ["commodity"]
views.py 源码
from wechatpy.oauth import WeChatOAuth from django.shortcuts import render, redirect from django.http import JsonResponse, HttpResponse, HttpResponseRedirect import time import datetime from django.conf import settings from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.shortcuts import render import uuid from wechatpy import WeChatClient import os import json from wechatpy import WeChatPay from app import models from wechatpy.pay import dict_to_xml # Create your views here. # 公众号id AppID = "appid" # 公众号AppSecret AppSecret = "appsecret" # 商户id MCH_ID = 'mch_id' # 商户API秘钥 API_KEY = 'api_key' # 接收微信支付异步通知回调地址 notify_url = "http://i157422s94.iok.la/wxjssdk/" # 微信认证文件,建议通过nginx配置 def wechatauth(request): return HttpResponse("b1reLtO1xRzEjqxJ") # 定义授权装饰器 def getWeChatOAuth(redirect_url): return WeChatOAuth(AppID, AppSecret, redirect_url, 'snsapi_userinfo') def oauth(method): def warpper(request): if request.session.get('user_info', None) is None: code = request.GET.get('code', None) wechat_oauth = getWeChatOAuth(request.get_raw_uri()) url = wechat_oauth.authorize_url print(url) if code: try: wechat_oauth.fetch_access_token(code) user_info = wechat_oauth.get_user_info() print(user_info) except Exception as e: print(str(e)) # 这里需要处理请求里包含的 code 无效的情况 # abort(403) else: # 建议存储在用户表 request.session['user_info'] = user_info else: return redirect(url) return method(request) return warpper # 获取用户信息UserInfo @oauth def userinfo(request): user_info = request.session.get('user_info') return render(request, 'userinfo.html', {"user_info": user_info}) # 微信JS SDK调用 @oauth def wxjssdk(request): user_info = request.session.get('user_info') trade_type = "JSAPI" body = "商品描述" total_fee = "10" notify_url = "http://wwww.wezoz.com/notify_url/" user_id = user_info["openid"] wechatPay = WeChatPay( appid=AppID, api_key=API_KEY, mch_id=MCH_ID, ) order = wechatPay.order.create(trade_type, body, total_fee, notify_url, user_id=user_id) wxpay_params = wechatPay.jsapi.get_jsapi_params(order['prepay_id']) print(wxpay_params) return render(request, 'index.html', {"wxpay_params": wxpay_params}) @oauth def commodity(request): commoditys = models.commodity.objects.all() return render(request, 'commodity.html', {"commoditys": commoditys}) @csrf_exempt def order_jsapi(request): user_info = request.session.get('user_info') cid = request.POST.get("cid") cty = models.commodity.objects.filter(id=cid).first() trade_type = "JSAPI" body = cty.desc total_fee = int(cty.price * 100) notify_url = "http://wwww.wezoz.com/notify_url/" user_id = user_info["openid"] wechatPay = WeChatPay( appid=AppID, api_key=API_KEY, mch_id=MCH_ID, ) order = wechatPay.order.create(trade_type, body, total_fee, notify_url, user_id=user_id, detail=cty.name, attach=cid) print(order) wxpay_params = wechatPay.jsapi.get_jsapi_params(order['prepay_id']) print(wxpay_params) prepay_id = order["prepay_id"] # 微信订单号 params = wechatPay.order.get_appapi_params(prepay_id) print(params) obj = models.order.objects.create(ordercode=prepay_id, openid=user_id) obj.commodity.add(cid) return JsonResponse(wxpay_params) @csrf_exempt def notify_url(request): wechatPay = WeChatPay( appid=AppID, api_key=API_KEY, mch_id=MCH_ID, ) result = wechatPay.parse_payment_result(request.body) print(result) query = wechatPay.order.query(result["transaction_id"]) print(query) attach = result["attach"] # 附加参数 # 此处验证需要添加业务逻辑,只通过openid 验证不严谨 ord = models.order.objects.filter(openid=result["openid"]).first() ord.appid = result["appid"] ord.bank_type = result["bank_type"] ord.cash_fee = result["cash_fee"] ord.fee_type = result["fee_type"] ord.is_subscribe = result["is_subscribe"] ord.mch_id = result["mch_id"] ord.nonce_str = result["nonce_str"] ord.out_trade_no = result["out_trade_no"] ord.result_code = result["result_code"] ord.return_code = result["return_code"] ord.time_end = result["time_end"] ord.total_fee = result["total_fee"] ord.trade_type = result["trade_type"] ord.transaction_id = result["transaction_id"] ord.sign = result["sign"] ord.state = 1 ord.save() print(result) return HttpResponse("SUCCESS") def validate_date_str(date_str): try: datetime.datetime.strptime(date_str, '%Y-%m-%d') return True except ValueError: return False # 访问地址如下 http://127.0.0.1:8009/downloadBill/2019-10-23.html @csrf_exempt def downloadBill(request, date): if validate_date_str(date): wechatPay = WeChatPay( appid=AppID, api_key=API_KEY, mch_id=MCH_ID ) result = wechatPay.tools.download_bill(date.replace("-", "")) print(result) billArray = result.split("\r\n") # 分割账单,一行为一组数据,分割后第一行为数据标题,倒数第三行为统计标题,倒数第二行为统计金额,最后一行为多余的空行 titleArray = billArray[0].split(',') # 第一行为标题 title_total = billArray[len(billArray) - 2] # 统计标题 data_total = billArray[len(billArray) - 1] # 统计金额 del billArray[0] # 去掉标题 del billArray[len(billArray) - 3] # 去掉总标题 del billArray[len(billArray) - 2] # 去掉总额 del billArray[len(billArray) - 1] # 去掉空行,剩下的为账单详情数据 mybill = [] # 订单详细信息 # 循环账单详情数据 for i in billArray: _detail = i.split('`')[:-1] del _detail[0] # 去掉前边的空数据 _detail_temp = [] for d in _detail: # 每一个数据(去掉最后的逗号) _detail_val = d[:-1] _detail_temp.append(_detail_val) # TODO业务处理 # print(d[:-1]) # TODO业务处理 mybill.append(_detail_temp) # 账单入库 需要两张表 日账单表-->日账单对应的详细表 # 对账逻辑需自行处理结合 订单表 + 对账表 逐条勾兑,建议对账操作放在数据库进行 # 对账调度 Celery 下一章介绍 else: print("日期格式输入有误!") return HttpResponse("SUCCESS") @csrf_exempt def jsapi_signature(request): noncestr = uuid.uuid4() timestamp = int(time.time()) url = request.POST['url'] client = WeChatClient(AppID, AppSecret) ticket_response = client.jsapi.get_ticket() signature = client.jsapi.get_jsapi_signature( noncestr, ticket_response['ticket'], timestamp, url ) ret_dict = { 'noncestr': noncestr, 'timestamp': timestamp, 'url': url, 'signature': signature, } return JsonResponse(ret_dict) def log(request): print('Hello World!') return JsonResponse({ 'status': 'ok', })
templates-->commodity.html
xml version="1.0" encoding="UTF-8"?> DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>商品展示title> <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" name="viewport"/> <meta content="yes" name="apple-mobile-web-app-capable"/> <meta content="black" name="apple-mobile-web-app-status-bar-style"/> <meta content="telephone=no" name="format-detection"/> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <script type="text/javascript" src="https://www.szyfd.xyz/static/js/jquery-3.4.1.js">script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous">script> head> <body> <div class="container-fluid"> <table class="table table-bordered table-striped"> <thead> <tr> <th>商品名称th> <th>商品价格th> tr> thead> <tbody> {% for c in commoditys %} <tr> <th scope="row"> <code>{{ c.name }}code> th> <td>价格{{ c.price }} <button type="button" class="btn btn-primary requestOrder" cid="{{ c.id }}" style="margin-left: 20px">点击支付 button> td> tr> {% endfor %} tbody> table> div> <script type="text/javascript"> $(function () { $.ajaxSetup({ beforeSend: function (xhr, settings) { xhr.setRequestHeader('X-CSRFtoken', $.cookie('csrftoken')); } }); }) script> <script type="text/javascript" src="https://www.szyfd.xyz/static/js/jquery.cookie.js">script> <script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js">script> <script type="text/javascript"> (function (window, $) { var fInitWeixin = function (d) { wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: 'wx975992a7ef6d6c9d', // 必填,公众号的唯一标识 timestamp: d.timestamp, // 必填,生成签名的时间戳 nonceStr: d.noncestr, // 必填,生成签名的随机串 signature: d.signature,// 必填,签名,见附录1 jsApiList: ['checkJsApi', 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareWeibo', 'onMenuShareQZone', 'hideMenuItems', 'showMenuItems', 'hideAllNonBaseMenuItem', 'showAllNonBaseMenuItem', 'translateVoice', 'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'playVoice', 'onVoicePlayEnd', 'pauseVoice', 'stopVoice', 'uploadVoice', 'downloadVoice', 'chooseImage', 'previewImage', 'uploadImage', 'downloadImage', 'getNetworkType', 'openLocation', 'getLocation', 'hideOptionMenu', 'showOptionMenu', 'closeWindow', 'scanQRCode', 'chooseWXPay', 'openProductSpecificView', 'addCard', 'chooseCard', 'openCard'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 }); } var s = $.ajax({ type: 'post', url: '/get_signature/', dataType: 'json', data: {url: location.href}, success: function (d) { fInitWeixin(d) } }) })(window, jQuery) script> <script type="text/javascript"> /* * 注意: * 1. 所有的JS接口只能在公众号绑定的域名下调用,公众号开发者需要先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。 * 2. 如果发现在 Android 不能分享自定义内容,请到官网下载最新的包覆盖安装,Android 自定义分享接口需升级至 6.0.2.58 版本及以上。 * 3. 完整 JS-SDK 文档地址:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html * * 如有问题请通过以下渠道反馈: * 邮箱地址:[email protected] * 邮件主题:【微信JS-SDK反馈】具体问题 * 邮件内容说明:用简明的语言描述问题所在,并交代清楚遇到该问题的场景,可附上截屏图片,微信团队会尽快处理你的反馈。 */ wx.ready(function () { // 1 判断当前版本是否支持指定 JS 接口,支持批量判断 document.querySelector('#checkJsApi').onclick = function () { wx.checkJsApi({ jsApiList: [ 'getNetworkType', 'previewImage' ], success: function (res) { alert(JSON.stringify(res)); } }); }; shareData = { title: '深圳易方达软件', desc: '按时交付完美主义者', link: location.href, imgUrl: 'https://www.szyfd.xyz/static/HOME/style/images/shareLogo.png', trigger: function (res) { // 不要尝试在trigger中使用ajax异步请求修改本次分享的内容,因为客户端分享操作是一个同步操作,这时候使用ajax的回包会还没有返回 }, success: function (res) { }, cancel: function (res) { }, fail: function (res) { //alert(JSON.stringify(res)); } } // 2. 分享接口 wx.onMenuShareAppMessage(shareData); wx.onMenuShareTimeline(shareData); wx.onMenuShareQQ(shareData); wx.onMenuShareWeibo(shareData); wx.onMenuShareQZone(shareData); function decryptCode(code, callback) { } }); wx.error(function (res) { alert(res.errMsg); }); $(".requestOrder").click(function () { currentThis = $(this); cid = $(currentThis).attr("cid"); $.ajax({ type: 'post', url: '/order_jsapi', dataType: 'json', data: {cid: cid}, success: function (d) { wx.chooseWXPay({ timestamp: d.timeStamp, nonceStr: d.nonceStr, package: d.package, signType: d.signType, // 注意:新版支付接口使用 MD5 加密 paySign: d.paySign }); } }) }) script> body> html>
手机效果图:
后台效果图:
商品表