toPay.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <template>
  2. <view class="app-content awaitPayYe awaitPayYe">
  3. <view class="callback-wrap">
  4. <view class="bg_01"></view>
  5. <view class="callback-blo">
  6. <block>
  7. <view class="icon">
  8. <i class="iconfont icondaizhifu"></i>
  9. </view>
  10. </block>
  11. <view class="wait">待付款</view>
  12. <block>
  13. <view class="price">
  14. <span class="app-size-36 app-bold">¥{{ parseFloat(actPrice)}}</span>
  15. </view>
  16. </block>
  17. <view>
  18. <view style="text-align:center;margin:10upx auto 10upx auto;">
  19. <view class="balance" style="margin-bottom:25upx;" v-if="goPay == 0">请扫客户付款码</view>
  20. <view class="balance" style="margin-bottom:25upx;" v-if="goPay == 1">请求中</view>
  21. <view style="text-align:center;">
  22. <image style="width:40upx;height:40upx;margin:0 auto;" :src="`${constant.imgUrl}/icon/loading.gif`" v-if="goPay == 1" mode="scaleToFill" />
  23. <block v-if="goPay == 2">
  24. <view style="color:red;font-weight:bold;font-size:32upx;margin-bottom:20upx;">{{ remindText }}</view>
  25. <view style="width:100%;text-align:center;" v-if="returnCode=='BBS11105' || returnCode=='BBS10000'">
  26. <button style="width:25vh;background-color:red;border:1upx solid red;color:white;" class="admin-button-com big" @click="checkResult()">查询结果</button>
  27. </view>
  28. </block>
  29. </view>
  30. <view style="margin-top:20upx;">
  31. <button style="width:25vh;" @click="beginScan()" class="admin-button-com big blue">点击扫码</button>
  32. </view>
  33. <view style="margin-top:20upx;">
  34. <button style="width:25vh;" class="admin-button-com big blue" @click="gotoOrderDetail()">订单详情</button>
  35. </view>
  36. <view style="margin-top:20upx;">
  37. <button style="width:25vh;" @click="changePayWay(2)" class="admin-button-com big default">挂账</button>
  38. </view>
  39. <view style="margin-top:20upx;">
  40. <button style="width:25vh;" @click="toCancel()" class="admin-button-com big default">取消</button>
  41. </view>
  42. </view>
  43. </view>
  44. <view style="margin-top:20upx;">
  45. <button style="width:25vh;" @click="goBack()" class="admin-button-com big default">返回首页</button>
  46. </view>
  47. <view style="color:#CCCCCC;margin-top:20upx;">10分钟后未付款自动取消</view>
  48. </view>
  49. </view>
  50. </view>
  51. </template>
  52. <script>
  53. /**
  54. * 扫码支付组件(零售花店App/微信端)
  55. * 优化转圈状态锁死问题,支持低频自动复查及网络异常优雅降级
  56. */
  57. import { cancelOrder,debtPay,getDetail,codePay,codePayCheck,codePayVerify} from "@/api/order";
  58. export default {
  59. name: "toPay",
  60. components: {
  61. },
  62. mixins: [],
  63. computed: {},
  64. data() {
  65. return {
  66. orderId:0,
  67. actPrice:0,
  68. orderSn:'',
  69. showQrCode:false,
  70. payQrCodeUrl:'',
  71. payWay:0,
  72. isCashier:false,
  73. orderInfo:{orderType:0},
  74. expire:0,
  75. goPay:0, // 0: 请扫付款码,1: 请求中/自动复查中,2: 支付失败或异常提示
  76. textscan: '扫码',
  77. typescan: 'scan-listener',
  78. remindText:'',
  79. returnCode:'',
  80. pollingTimer:null, // 后台自动复查定时器
  81. pollingCount:0, // 自动复查次数
  82. isPaymentProcessing:false // 防重复扫码标志
  83. };
  84. },
  85. mounted() {
  86. },
  87. onUnload() {
  88. this.clearPolling()
  89. },
  90. beforeDestroy() {
  91. this.clearPolling()
  92. },
  93. onLoad(option) {
  94. this.orderId = option.orderId
  95. this.actPrice = option.actPrice
  96. this.orderSn = option.orderSn
  97. this.option = option // 保存 option 变量,防止 detail 跳转报错
  98. if(!this.$util.isEmpty(option.orderId)){
  99. getDetail({ id: option.orderId }).then(res=>{
  100. if(res.code == 1){
  101. this.orderInfo = res.data
  102. }
  103. })
  104. }
  105. },
  106. methods: {
  107. gotoOrderDetail() {
  108. let id = this.option.orderId ? this.option.orderId : 0
  109. this.$util.pageTo({url: "/admin/order/detail?id="+id,type:2})
  110. },
  111. beginScan(){
  112. this.clearPolling()
  113. this.remindText = ''
  114. this.returnCode = ''
  115. this.isPaymentProcessing = false
  116. // #ifdef MP-WEIXIN
  117. this.toScan()
  118. // #endif
  119. // #ifdef APP-PLUS
  120. let platformType = uni.getSystemInfoSync().platform
  121. if(platformType == 'android'){
  122. this.toScan()
  123. }else if(platformType == 'ios'){
  124. this.toScan()
  125. }else{
  126. this.$msg('系统错误')
  127. }
  128. // #endif
  129. },
  130. toScan(){
  131. let that = this
  132. uni.scanCode({
  133. onlyFromCamera: true,
  134. success: function (res) {
  135. let payCode = res.result
  136. let newPayCode = payCode.replace(/\s+/g, "")
  137. if(that.$util.isEmpty(newPayCode)){
  138. that.$msg('没扫到,请重新扫哦')
  139. return false
  140. }
  141. if(!that.isAllDigits(newPayCode)){
  142. that.$msg('没扫到,请重新扫')
  143. return false
  144. }
  145. that.requestCodePay(newPayCode)
  146. }
  147. })
  148. },
  149. isAllDigits(str) {
  150. return /^\d+$/.test(str)
  151. },
  152. requestCodePay(authCode){
  153. if(this.isPaymentProcessing){
  154. this.$msg('支付处理中,请稍候')
  155. return
  156. }
  157. let that = this
  158. this.goPay = 1
  159. this.isPaymentProcessing = true
  160. this.remindText = ''
  161. this.returnCode = ''
  162. this.clearPolling()
  163. codePay({id:this.orderId,authCode:authCode}).then(res=>{
  164. if(res.code == 1){
  165. if(res.data.returnStatus == 'SUCCESS'){
  166. that.isPaymentProcessing = false
  167. setTimeout(function(){
  168. that.payFinish()
  169. },1200)
  170. }else{
  171. that.remindText = res.msg || '等待支付...'
  172. that.returnCode = res.data.returnCode ? res.data.returnCode : ''
  173. that.goPay = 2
  174. // 11105(用户输入密码中) 或 10000(其它密码状态),启动4秒一次、最多3次(12秒)的自动复查定时器
  175. if (that.returnCode == 'BBS11105' || that.returnCode == 'BBS10000') {
  176. that.startPolling()
  177. } else {
  178. that.isPaymentProcessing = false
  179. }
  180. }
  181. } else {
  182. that.isPaymentProcessing = false
  183. that.remindText = res.msg || '支付请求失败'
  184. that.goPay = 2
  185. }
  186. }).catch(err=>{
  187. // 请求超时或网络断开时,重置支付状态,同时自动运行一轮轮询查询,防万一扣款成功
  188. that.isPaymentProcessing = false
  189. that.remindText = '网络异常,正在自动复查支付结果...'
  190. that.goPay = 2
  191. that.startPolling()
  192. })
  193. },
  194. /**
  195. * 启动低频后台付款复查定时器
  196. * 4秒轮询一次,限查3次,极大地减轻服务器压力的同时保证业务流闭环
  197. */
  198. startPolling(){
  199. this.clearPolling()
  200. this.pollingCount = 0
  201. this.isPaymentProcessing = true
  202. this.goPay = 1 // 显示请求中
  203. let that = this
  204. this.pollingTimer = setInterval(() => {
  205. that.pollingCount++
  206. if (that.pollingCount > 3) {
  207. that.clearPolling()
  208. that.isPaymentProcessing = false
  209. that.goPay = 2
  210. that.remindText = '支付确认超时,请确认顾客扣款,并点查询结果'
  211. return
  212. }
  213. that.remindText = `正在自动复查支付结果 (${that.pollingCount}/3)...`
  214. codePayVerify({id:that.orderId}).then(res=>{
  215. if(res.code == 1){
  216. if(res.data && res.data.returnStatus == 'SUCCESS'){
  217. that.clearPolling()
  218. that.isPaymentProcessing = false
  219. setTimeout(function(){
  220. that.payFinish()
  221. },1000)
  222. }
  223. }
  224. }).catch(err=>{
  225. console.error('Code pay check failed', err)
  226. })
  227. }, 4000)
  228. },
  229. clearPolling(){
  230. if(this.pollingTimer){
  231. clearInterval(this.pollingTimer)
  232. this.pollingTimer = null
  233. }
  234. },
  235. init(){
  236. },
  237. payFinish(){
  238. this.clearPolling()
  239. this.$util.pageTo({ url: '/admin/billing/result?orderId='+this.orderId,type:2})
  240. },
  241. changePayWay(payWay){
  242. this.payWay = payWay
  243. let that = this
  244. if(payWay == 2){
  245. that.$util.confirmModal({content:'确认挂账?'},() => {
  246. debtPay({id:this.orderId}).then(res=>{
  247. if(res.code == 1){
  248. that.$util.pageTo({ url: '/admin/billing/result?orderId='+this.orderId,type:2})
  249. }
  250. })
  251. })
  252. }
  253. },
  254. checkResult(){
  255. let that = this
  256. uni.showLoading({mask:true})
  257. codePayVerify({ id:this.orderId}).then(res=>{
  258. uni.hideLoading()
  259. if(res.code == 1){
  260. if(res.data && res.data.returnStatus == 'SUCCESS'){
  261. that.clearPolling()
  262. that.isPaymentProcessing = false
  263. this.$msg('付款成功')
  264. setTimeout(function(){
  265. that.payFinish()
  266. },1000)
  267. }else{
  268. this.$msg(res.msg || '还没有付款成功')
  269. }
  270. } else {
  271. this.$msg(res.msg || '查询失败')
  272. }
  273. }).catch(err => {
  274. uni.hideLoading()
  275. this.$msg('网络异常')
  276. })
  277. },
  278. goBack(){
  279. this.clearPolling()
  280. uni.navigateBack()
  281. },
  282. toCancel(){
  283. let that = this
  284. let content = '确认取消?'
  285. if(this.returnCode == 'BBS11105' || this.returnCode == 'BBS10000'){
  286. content = '确认取消?'
  287. }
  288. // 取消支付时,必须清除定时器和支付占用状态
  289. that.$util.confirmModal({content:content,cancelText:'返回'},() => {
  290. that.clearPolling()
  291. that.isPaymentProcessing = false
  292. cancelOrder({id:this.orderId}).then(res=>{
  293. if(res.code == 1){
  294. that.$msg(res.msg)
  295. setTimeout(function(){
  296. uni.navigateBack()
  297. },1000)
  298. }
  299. })
  300. })
  301. }
  302. },
  303. };
  304. </script>
  305. <style lang="scss" scoped>
  306. .app-content {
  307. .callback-wrap {
  308. padding-top: 30upx;
  309. background-repeat: no-repeat;
  310. background-size: 100% auto;
  311. position: relative;
  312. .bg_01 {
  313. position: absolute;
  314. top: 0;
  315. left: 0;
  316. /* 手机mobile屏幕小于1000px */
  317. @media screen and (max-width: 1100px) {
  318. width: 100vw;
  319. }
  320. /* 收银台cashier屏幕大于1000px */
  321. @media screen and (min-width:1100px) {
  322. width: 40vw;
  323. }
  324. height: 290upx;
  325. background: #3385ff;
  326. border-radius: 0upx 0upx 83upx 83upx;
  327. z-index: 1;
  328. }
  329. .callback-blo {
  330. position: absolute;
  331. z-index: 9;
  332. left: 50%;
  333. transform: translateX(-50%);
  334. width: 90%;
  335. margin: 0 auto;
  336. padding: 70upx 0;
  337. background-color: #fff;
  338. box-shadow: 2upx 2upx 16upx 0upx rgba(181, 181, 181, 0.29);
  339. border-radius: 10upx;
  340. text-align: center;
  341. // 使用icon
  342. .icon {
  343. .iconfont {
  344. font-size: 130upx;
  345. }
  346. }
  347. // 一个按钮
  348. .call-one-btn {
  349. margin-top:50upx;
  350. .mobile-scan{
  351. color:#b3b1b1;
  352. margin-top:30upx;
  353. font-size:30upx;
  354. }
  355. }
  356. }
  357. }
  358. }
  359. // 待支付 - 公共
  360. .awaitPayWx,
  361. .awaitPayYe {
  362. .status-text {
  363. margin-top: 22upx;
  364. margin-bottom: 52upx;
  365. color: #ffa92e;
  366. }
  367. .surplus {
  368. margin-top: 22upx;
  369. margin-bottom: 38upx;
  370. }
  371. .icon {
  372. .iconfont {
  373. color: #ffa92e;
  374. }
  375. }
  376. }
  377. .wait {
  378. color: #ffa92e;
  379. font-size: 30upx;
  380. font-weight: 700;
  381. margin: 20upx 0;
  382. }
  383. .balance{
  384. margin-top:30upx;
  385. font-size:32upx;
  386. font-weight: bold;
  387. }
  388. </style>