Browse Source

feat: 支付功能迁移(初稿)

dhb52 1 year ago
parent
commit
a6eab3cc9a

+ 1 - 0
package.json

@@ -60,6 +60,7 @@
     "nprogress": "^0.2.0",
     "pinia": "^2.1.6",
     "qrcode": "^1.5.3",
+    "qrcode.vue": "^3.4.0",
     "qs": "^6.11.2",
     "steady-xml": "^0.1.0",
     "url": "^0.11.1",

+ 6 - 0
src/api/pay/app/index.ts

@@ -76,3 +76,9 @@ export const exportApp = (params: AppExportReqVO) => {
 export const getAppListByMerchantId = (merchantId: number) => {
   return request.get({ url: '/pay/app/list-merchant-id', params: { merchantId: merchantId } })
 }
+
+export const getAppList = () => {
+  return request.get({
+    url: '/pay/app/list'
+  })
+}

+ 36 - 0
src/api/pay/demo/index.ts

@@ -0,0 +1,36 @@
+import request from '@/config/axios'
+
+export interface DemoOrderVO {
+  spuId: number
+  createTime: Date
+}
+
+// 创建示例订单
+export function createDemoOrder(data: DemoOrderVO) {
+  return request.post({
+    url: '/pay/demo-order/create',
+    data: data
+  })
+}
+
+// 获得示例订单
+export function getDemoOrder(id: number) {
+  return request.get({
+    url: '/pay/demo-order/get?id=' + id
+  })
+}
+
+// 获得示例订单分页
+export function getDemoOrderPage(query: PageParam) {
+  return request.get({
+    url: '/pay/demo-order/page',
+    params: query
+  })
+}
+
+// 退款示例订单
+export function refundDemoOrder(id) {
+  return request.put({
+    url: '/pay/demo-order/refund?id=' + id
+  })
+}

+ 15 - 0
src/api/pay/notify/index.ts

@@ -0,0 +1,15 @@
+import request from '@/config/axios'
+
+export const getNotifyTaskDetail = (id) => {
+  return request.get({
+    url: '/pay/notify/get-detail?id=' + id
+  })
+}
+
+// 获得支付通知分页
+export const getNotifyTaskPage = (query) => {
+  return request.get({
+    url: '/pay/notify/page',
+    params: query
+  })
+}

+ 1 - 1
src/api/pay/order/index.ts

@@ -95,7 +95,7 @@ export const getOrderDetail = async (id: number) => {
 
 // 新增支付订单
 export const createOrder = async (data: OrderVO) => {
-  return await request.post({ url: '/pay/order/create', data })
+  return await request.post({ url: '/pay/order/submit', data })
 }
 
 // 修改支付订单

+ 18 - 0
src/router/modules/remaining.ts

@@ -410,6 +410,24 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       }
     ]
+  },
+  {
+    path: '/pay',
+    component: Layout,
+    name: 'pay',
+    meta: { hidden: true },
+    children: [
+      {
+        path: 'cashier',
+        name: 'PayCashier',
+        meta: {
+          title: '收银台',
+          noCache: true,
+          hidden: true
+        },
+        component: () => import('@/views/pay/cashier/index.vue')
+      }
+    ]
   }
 ]
 

+ 25 - 0
src/utils/constants.ts

@@ -99,6 +99,10 @@ export const PayChannelEnum = {
     code: 'wx_app',
     name: '微信 APP 支付'
   },
+  WX_BAR: {
+    code: 'wx_bar',
+    name: '微信条码支付'
+  },
   ALIPAY_PC: {
     code: 'alipay_pc',
     name: '支付宝 PC 网站支付'
@@ -125,6 +129,27 @@ export const PayChannelEnum = {
   }
 }
 
+/**
+ * 支付的展示模式每局
+ */
+export const PayDisplayModeEnum = {
+  URL: {
+    mode: 'url'
+  },
+  IFRAME: {
+    mode: 'iframe'
+  },
+  FORM: {
+    mode: 'form'
+  },
+  QR_CODE: {
+    mode: 'qr_code'
+  },
+  APP: {
+    mode: 'app'
+  }
+}
+
 /**
  * 支付类型枚举
  */

+ 5 - 1
src/utils/dict.ts

@@ -134,13 +134,17 @@ export enum DICT_TYPE {
   PAY_CHANNEL_ALIPAY_SIGN_TYPE = 'pay_channel_alipay_sign_type', // 支付渠道支付宝算法类型
   PAY_CHANNEL_ALIPAY_MODE = 'pay_channel_alipay_mode', // 支付宝公钥类型
   PAY_CHANNEL_ALIPAY_SERVER_TYPE = 'pay_channel_alipay_server_type', // 支付宝网关地址
+  PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型
   PAY_CHANNEL_CODE_TYPE = 'pay_channel_code_type', // 支付渠道编码类型
-  PAY_ORDER_NOTIFY_STATUS = 'pay_order_notify_status', // 商户支付订单回调状态
+  // PAY_ORDER_NOTIFY_STATUS = 'pay_order_notify_status', // 商户支付订单回调状态
   PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态
   PAY_ORDER_REFUND_STATUS = 'pay_order_refund_status', // 商户支付订单退款状态
   PAY_REFUND_ORDER_STATUS = 'pay_refund_order_status', // 退款订单状态
   PAY_REFUND_ORDER_TYPE = 'pay_refund_order_type', // 退款订单类别
 
+  PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态
+  PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态
+
   // ========== MP 模块 ==========
   MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
   MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型

+ 485 - 0
src/views/pay/cashier/index.vue

@@ -0,0 +1,485 @@
+<template>
+  <!-- 支付信息 -->
+  <el-card v-loading="loading">
+    <el-descriptions title="支付信息" :column="3" border>
+      <el-descriptions-item label="支付单号">{{ payOrder.id }}</el-descriptions-item>
+      <el-descriptions-item label="商品标题">{{ payOrder.subject }}</el-descriptions-item>
+      <el-descriptions-item label="商品内容">{{ payOrder.body }}</el-descriptions-item>
+      <el-descriptions-item label="支付金额"
+        >¥{{ (payOrder.price / 100.0).toFixed(2) }}</el-descriptions-item
+      >
+      <el-descriptions-item label="创建时间">{{
+        formatDate(payOrder.createTime)
+      }}</el-descriptions-item>
+      <el-descriptions-item label="过期时间">{{
+        formatDate(payOrder.expireTime)
+      }}</el-descriptions-item>
+    </el-descriptions>
+  </el-card>
+
+  <!-- 支付选择框 -->
+  <el-card style="margin-top: 10px" v-loading="submitLoading" element-loading-text="提交支付中...">
+    <!-- 支付宝 -->
+    <el-descriptions title="选择支付宝支付" />
+    <div class="pay-channel-container">
+      <div
+        class="box"
+        v-for="channel in channelsAlipay"
+        :key="channel.code"
+        @click="submit(channel.code)"
+      >
+        <img :src="channel.icon" />
+        <div class="title">{{ channel.name }}</div>
+      </div>
+    </div>
+    <!-- 微信支付 -->
+    <el-descriptions title="选择微信支付" style="margin-top: 20px" />
+    <div class="pay-channel-container">
+      <div
+        class="box"
+        v-for="channel in channelsWechat"
+        :key="channel.code"
+        @click="submit(channel.code)"
+      >
+        <img :src="channel.icon" />
+        <div class="title">{{ channel.name }}</div>
+      </div>
+    </div>
+    <!-- 其它支付 -->
+    <el-descriptions title="选择其它支付" style="margin-top: 20px" />
+    <div class="pay-channel-container">
+      <div
+        class="box"
+        v-for="channel in channelsMock"
+        :key="channel.code"
+        @click="submit(channel.code)"
+      >
+        <img :src="channel.icon" />
+        <div class="title">{{ channel.name }}</div>
+      </div>
+    </div>
+  </el-card>
+
+  <!-- 展示形式:二维码 URL -->
+  <el-dialog
+    :title="qrCode.title"
+    v-model="qrCode.visible"
+    width="350px"
+    append-to-body
+    :close-on-press-escape="false"
+  >
+    <qrcode-vue :value="qrCode.url" :size="310" level="L" />
+  </el-dialog>
+
+  <!-- 展示形式:BarCode 条形码 -->
+  <el-dialog
+    :title="barCode.title"
+    v-model="barCode.visible"
+    width="500px"
+    append-to-body
+    :close-on-press-escape="false"
+  >
+    <el-form ref="form" label-width="80px">
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="条形码" prop="name">
+            <el-input v-model="barCode.value" placeholder="请输入条形码" required />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <div style="text-align: right">
+            或使用
+            <el-link
+              type="danger"
+              target="_blank"
+              href="https://baike.baidu.com/item/条码支付/10711903"
+              >(扫码枪/扫码盒)</el-link
+            >
+            扫码
+          </div>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button
+        type="primary"
+        @click="submit0(barCode.channelCode)"
+        :disabled="barCode.value.length === 0"
+        >确认支付</el-button
+      >
+      <el-button @click="barCode.visible = false">取 消</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts" name="PayCashier">
+import QrcodeVue from 'qrcode.vue'
+import { getOrder, createOrder } from '@/api/pay/order'
+import { PayChannelEnum, PayDisplayModeEnum, PayOrderStatusEnum } from '@/utils/constants'
+import { ref, onMounted } from 'vue'
+import { formatDate } from '@/utils/formatTime'
+
+// TODO: ugly
+import svg_alipay_pc from '@/assets/svgs/pay/icon/alipay_pc.svg'
+import svg_alipay_wap from '@/assets/svgs/pay/icon/alipay_wap.svg'
+import svg_alipay_app from '@/assets/svgs/pay/icon/alipay_app.svg'
+import svg_alipay_qr from '@/assets/svgs/pay/icon/alipay_qr.svg'
+import svg_alipay_bar from '@/assets/svgs/pay/icon/alipay_bar.svg'
+import svg_wx_pub from '@/assets/svgs/pay/icon/wx_pub.svg'
+import svg_wx_lite from '@/assets/svgs/pay/icon/wx_lite.svg'
+import svg_wx_app from '@/assets/svgs/pay/icon/wx_app.svg'
+import svg_wx_native from '@/assets/svgs/pay/icon/wx_native.svg'
+import svg_wx_bar from '@/assets/svgs/pay/icon/wx_bar.svg'
+import svg_mock from '@/assets/svgs/pay/icon/mock.svg'
+
+const message = useMessage() // 消息弹窗
+const route = useRoute()
+const router = useRouter()
+
+const id = ref(undefined) // 请假编号
+const returnUrl = ref<string | undefined>(undefined) // 支付完的回调地址
+const loading = ref(false) // 支付信息的 loading
+const payOrder = ref({}) // 支付信息
+const channelsAlipay = [
+  {
+    name: '支付宝 PC 网站支付',
+    icon: svg_alipay_pc,
+    code: 'alipay_pc'
+  },
+  {
+    name: '支付宝 Wap 网站支付',
+    icon: svg_alipay_wap,
+    code: 'alipay_wap'
+  },
+  {
+    name: '支付宝 App 网站支付',
+    icon: svg_alipay_app,
+    code: 'alipay_app'
+  },
+  {
+    name: '支付宝扫码支付',
+    icon: svg_alipay_qr,
+    code: 'alipay_qr'
+  },
+  {
+    name: '支付宝条码支付',
+    icon: svg_alipay_bar,
+    code: 'alipay_bar'
+  }
+]
+
+const channelsWechat = [
+  {
+    name: '微信公众号支付',
+    icon: svg_wx_pub,
+    code: 'wx_pub'
+  },
+  {
+    name: '微信小程序支付',
+    icon: svg_wx_lite,
+    code: 'wx_lite'
+  },
+  {
+    name: '微信 App 支付',
+    icon: svg_wx_app,
+    code: 'wx_app'
+  },
+  {
+    name: '微信扫码支付',
+    icon: svg_wx_native,
+    code: 'wx_native'
+  },
+  {
+    name: '微信条码支付',
+    icon: svg_wx_bar,
+    code: 'wx_bar'
+  }
+]
+
+const channelsMock = [
+  {
+    name: '模拟支付',
+    icon: svg_mock,
+    code: 'mock'
+  }
+]
+
+const submitLoading = ref(false) // 提交支付的 loading
+const interval = ref<any>(undefined) // 定时任务,轮询是否完成支付
+const qrCode = ref({
+  // 展示形式:二维码
+  url: '',
+  title: '',
+  visible: false
+})
+const barCode = ref({
+  // 展示形式:条形码
+  channelCode: '',
+  value: '',
+  title: '',
+  visible: false
+})
+
+onMounted(() => {
+  id.value = route.query.id
+  if (route.query.returnUrl) {
+    returnUrl.value = decodeURIComponent(route.query.returnUrl)
+  }
+  getDetail()
+})
+
+/** 获得支付信息 */
+const getDetail = () => {
+  // 1.1 未传递订单编号
+  if (!id.value) {
+    message.error('未传递支付单号,无法查看对应的支付信息')
+    goReturnUrl('cancel')
+    return
+  }
+  getOrder(id.value).then((data) => {
+    // 1.2 无法查询到支付信息
+    if (!data) {
+      message.error('支付订单不存在,请检查!')
+      goReturnUrl('cancel')
+      return
+    }
+    // 1.3 如果已支付、或者已关闭,则直接跳转
+    if (data.status === PayOrderStatusEnum.SUCCESS.status) {
+      message.success('支付成功')
+      goReturnUrl('success')
+      return
+    } else if (data.status === PayOrderStatusEnum.CLOSED.status) {
+      message.error('无法支付,原因:订单已关闭')
+      goReturnUrl('close')
+      return
+    }
+
+    // 2. 可以展示
+    payOrder.value = data
+  })
+}
+
+/** 提交支付 */
+const submit = (channelCode) => {
+  // 条形码支付,需要特殊处理
+  if (channelCode === PayChannelEnum.ALIPAY_BAR.code) {
+    barCode.value = {
+      channelCode: channelCode,
+      value: '',
+      title: '“支付宝”条码支付',
+      visible: true
+    }
+    return
+  }
+  if (channelCode === PayChannelEnum.WX_BAR.code) {
+    barCode.value = {
+      channelCode: channelCode,
+      value: '',
+      title: '“微信”条码支付',
+      visible: true
+    }
+    return
+  }
+
+  // 微信公众号、小程序支付,无法在 PC 网页中进行
+  if (channelCode === PayChannelEnum.WX_PUB.code) {
+    message.error('微信公众号支付:不支持 PC 网站')
+    return
+  }
+  if (channelCode === PayChannelEnum.WX_LITE.code) {
+    message.error('微信小程序:不支持 PC 网站')
+    return
+  }
+
+  // 默认的提交处理
+  submit0(channelCode)
+}
+
+const submit0 = (channelCode) => {
+  submitLoading.value = true
+  createOrder({
+    id: id.value,
+    channelCode: channelCode,
+    returnUrl: location.href, // 支付成功后,支付渠道跳转回当前页;再由当前页,跳转回 {@link returnUrl} 对应的地址
+    ...buildSubmitParam(channelCode)
+  })
+    .then((data) => {
+      // 直接返回已支付的情况,例如说扫码支付
+      if (data.status === PayOrderStatusEnum.SUCCESS.status) {
+        clearQueryInterval()
+        message.success('支付成功!')
+        goReturnUrl()
+        return
+      }
+
+      // 展示对应的界面
+      if (data.displayMode === PayDisplayModeEnum.URL.mode) {
+        displayUrl(channelCode, data)
+      } else if (data.displayMode === PayDisplayModeEnum.QR_CODE.mode) {
+        displayQrCode(channelCode, data)
+      } else if (data.displayMode === PayDisplayModeEnum.APP.mode) {
+        displayApp(channelCode)
+      }
+
+      // 打开轮询任务
+      createQueryInterval()
+    })
+    .catch(() => {
+      submitLoading.value = false
+    })
+}
+
+/** 构建提交支付的额外参数 */
+const buildSubmitParam = (channelCode) => {
+  // ① 支付宝 BarCode 支付时,需要传递 authCode 条形码
+  if (channelCode === PayChannelEnum.ALIPAY_BAR.code) {
+    return {
+      channelExtras: {
+        auth_code: barCode.value.value
+      }
+    }
+  }
+  // ② 微信 BarCode 支付时,需要传递 authCode 条形码
+  if (channelCode === PayChannelEnum.WX_BAR.code) {
+    return {
+      channelExtras: {
+        authCode: barCode.value.value
+      }
+    }
+  }
+  return {}
+}
+
+/** 提交支付后,URL 的展示形式 */
+const displayUrl = (_channelCode, data) => {
+  location.href = data.displayContent
+  submitLoading.value = false
+}
+
+/** 提交支付后(扫码支付) */
+const displayQrCode = (channelCode, data) => {
+  let title = '请使用手机浏览器“扫一扫”'
+  if (channelCode === PayChannelEnum.ALIPAY_WAP.code) {
+    // 考虑到 WAP 测试,所以引导手机浏览器搞
+  } else if (channelCode.indexOf('alipay_') === 0) {
+    title = '请使用支付宝“扫一扫”扫码支付'
+  } else if (channelCode.indexOf('wx_') === 0) {
+    title = '请使用微信“扫一扫”扫码支付'
+  }
+  qrCode.value = {
+    title: title,
+    url: data.displayContent,
+    visible: true
+  }
+  submitLoading.value = false
+}
+
+/** 提交支付后(App) */
+const displayApp = (channelCode) => {
+  if (channelCode === PayChannelEnum.ALIPAY_APP.code) {
+    message.error('支付宝 App 支付:无法在网页支付!')
+  }
+  if (channelCode === PayChannelEnum.WX_APP.code) {
+    message.error('微信 App 支付:无法在网页支付!')
+  }
+  submitLoading.value = false
+}
+
+/** 轮询查询任务 */
+const createQueryInterval = () => {
+  if (interval.value) {
+    return
+  }
+  interval.value = setInterval(() => {
+    getOrder(id.value).then((data) => {
+      // 已支付
+      if (data.status === PayOrderStatusEnum.SUCCESS.status) {
+        clearQueryInterval()
+        message.success('支付成功!')
+        goReturnUrl()
+      }
+      // 已取消
+      if (data.status === PayOrderStatusEnum.CLOSED.status) {
+        clearQueryInterval()
+        message.error('支付已关闭!')
+        goReturnUrl()
+      }
+    })
+  }, 1000 * 2)
+}
+
+/** 清空查询任务 */
+const clearQueryInterval = () => {
+  // 清空各种弹窗
+  qrCode.value = {
+    title: '',
+    url: '',
+    visible: false
+  }
+  // 清空任务
+  clearInterval(interval.value)
+  interval.value = undefined
+}
+
+/**
+ * 回到业务的 URL
+ *
+ * @param payResult 支付结果
+ *                  ① success:支付成功
+ *                  ② cancel:取消支付
+ *                  ③ close:支付已关闭
+ */
+const goReturnUrl = (payResult) => {
+  // 清理任务
+  clearQueryInterval()
+
+  // 未配置的情况下,只能关闭
+  if (!returnUrl.value) {
+    // TODO: dhb52 需要找到对应 $tab 功能
+    // this.$tab.closePage()
+    return
+  }
+
+  const url =
+    returnUrl.value.indexOf('?') >= 0
+      ? returnUrl.value + '&payResult=' + payResult
+      : returnUrl.value + '?payResult=' + payResult
+  // 如果有配置,且是 http 开头,则浏览器跳转
+  if (returnUrl.value.indexOf('http') === 0) {
+    location.href = url
+  } else {
+    // TODO: dhb52 需要找到对应 $tab 功能
+    // this.$tab.closePage(() => {
+    //   router.push({
+    //     path: url
+    //   })
+    // })
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.pay-channel-container {
+  display: flex;
+  margin-top: -10px;
+
+  .box {
+    width: 130px;
+    border: 1px solid #e6ebf5;
+    cursor: pointer;
+    text-align: center;
+    padding-top: 10px;
+    padding-bottom: 5px;
+    margin-right: 10px;
+
+    img {
+      width: 40px;
+      height: 40px;
+    }
+
+    .title {
+      padding-top: 5px;
+    }
+  }
+}
+</style>

+ 242 - 0
src/views/pay/demo/index.vue

@@ -0,0 +1,242 @@
+<template>
+  <!-- 操作工具栏 -->
+  <el-row :gutter="10" class="mb8">
+    <el-col :span="1.5">
+      <el-button type="primary" plain @click="handleAdd"><Icon icon="ep:plus" />发起订单</el-button>
+    </el-col>
+  </el-row>
+
+  <!-- 列表 -->
+  <el-table v-loading="loading" :data="list">
+    <el-table-column label="订单编号" align="center" prop="id" />
+    <el-table-column label="用户编号" align="center" prop="userId" />
+    <el-table-column label="商品名字" align="center" prop="spuName" />
+    <el-table-column label="支付价格" align="center" prop="price">
+      <template #default="scope">
+        <span>¥{{ (scope.row.price / 100.0).toFixed(2) }}</span>
+      </template>
+    </el-table-column>
+    <el-table-column label="退款金额" align="center" prop="refundPrice">
+      <template #default="scope">
+        <span>¥{{ (scope.row.refundPrice / 100.0).toFixed(2) }}</span>
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="创建时间"
+      align="center"
+      prop="createTime"
+      width="180"
+      :formatter="dateFormatter"
+    />
+    <el-table-column label="支付单号" align="center" prop="payOrderId" />
+    <el-table-column label="是否支付" align="center" prop="payStatus">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.payStatus" />
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="支付时间"
+      align="center"
+      prop="payTime"
+      width="180"
+      :formatter="dateFormatter"
+    />
+    <el-table-column label="退款时间" align="center" prop="refundTime" width="180">
+      <template #default="scope">
+        <span v-if="scope.row.refundTime">{{ formatDate(scope.row.refundTime) }}</span>
+        <span v-else-if="scope.row.payRefundId">退款中,等待退款结果</span>
+      </template>
+    </el-table-column>
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template #default="scope">
+        <el-button
+          size="small"
+          type="text"
+          icon="el-icon-edit"
+          @click="handlePay(scope.row)"
+          v-if="!scope.row.payStatus"
+          >前往支付</el-button
+        >
+        <el-button
+          size="small"
+          type="text"
+          icon="el-icon-delete"
+          @click="handleRefund(scope.row)"
+          v-if="scope.row.payStatus && !scope.row.payRefundId"
+          >发起退款</el-button
+        >
+      </template>
+    </el-table-column>
+  </el-table>
+  <!-- 分页组件 -->
+  <pagination
+    v-show="total > 0"
+    :total="total"
+    v-model:page="queryParams.pageNo"
+    v-model:limit="queryParams.pageSize"
+    @pagination="getList"
+  />
+
+  <!-- 对话框(添加 / 修改) -->
+  <el-dialog :title="title" v-model="open" width="500px" append-to-body destroy-on-close>
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+      <el-form-item label="商品" prop="spuId">
+        <el-select
+          v-model="form.spuId"
+          placeholder="请输入下单商品"
+          clearable
+          size="small"
+          style="width: 380px"
+        >
+          <el-option v-for="item in spus" :key="item.id" :label="item.name" :value="item.id">
+            <span style="float: left">{{ item.name }}</span>
+            <span style="float: right; color: #8492a6; font-size: 13px"
+              >¥{{ (item.price / 100.0).toFixed(2) }}</span
+            >
+          </el-option>
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="cancel">取 消</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup name="PayDemoOrder">
+import { createDemoOrder, getDemoOrderPage, refundDemoOrder } from '@/api/pay/demo'
+import { ref, onMounted } from 'vue'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+
+const router = useRouter() // 路由对象
+const message = useMessage() // 消息弹窗
+
+// 遮罩层
+const loading = ref(true)
+// 总条数
+const total = ref(0)
+// 示例订单列表
+const list = ref([])
+// 弹出层标题
+const title = ref('')
+// 是否显示弹出层
+const open = ref(false)
+// 查询参数
+const queryParams = ref({
+  pageNo: 1,
+  pageSize: 10
+})
+
+// 表单参数
+const form = ref({})
+// 表单校验
+const rules = {
+  spuId: [{ required: true, message: '商品编号不能为空', trigger: 'blur' }]
+}
+
+// 商品数组
+const spus = ref([
+  {
+    id: 1,
+    name: '华为手机',
+    price: 1
+  },
+  {
+    id: 2,
+    name: '小米电视',
+    price: 10
+  },
+  {
+    id: 3,
+    name: '苹果手表',
+    price: 100
+  },
+  {
+    id: 4,
+    name: '华硕笔记本',
+    price: 1000
+  },
+  {
+    id: 5,
+    name: '蔚来汽车',
+    price: 200000
+  }
+])
+
+const formRef = ref()
+
+/** 查询列表 */
+const getList = () => {
+  loading.value = true
+  // 执行查询
+  getDemoOrderPage(queryParams.value).then((data) => {
+    list.value = data.list
+    total.value = data.total
+    loading.value = false
+  })
+}
+
+/** 取消按钮 */
+const cancel = () => {
+  open.value = false
+  reset()
+}
+
+/** 表单重置 */
+const reset = () => {
+  form.value = {
+    spuId: undefined
+  }
+  formRef.value?.resetFields()
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset()
+  open.value = true
+  title.value = '发起订单'
+}
+
+/** 提交按钮 */
+const submitForm = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) {
+    return
+  }
+  // 添加的提交
+  createDemoOrder(form.value).then(() => {
+    message.success('新增成功')
+    open.value = false
+    getList()
+  })
+}
+
+/** 支付按钮操作 */
+const handlePay = (row) => {
+  router.push({
+    name: 'PayCashier',
+    query: {
+      id: row.payOrderId,
+      returnUrl: encodeURIComponent('/pay/demo-order?id=' + row.id)
+    }
+  })
+}
+
+/** 退款按钮操作 */
+const handleRefund = async (row: any) => {
+  const id = row.id
+
+  try {
+    await message.confirm('是否确认退款编号为"' + id + '"的示例订单?')
+    await refundDemoOrder(id)
+    getList()
+    message.success('发起退款成功!')
+  } catch {}
+}
+
+onMounted(() => {
+  getList()
+})
+</script>

+ 275 - 0
src/views/pay/notify/index.vue

@@ -0,0 +1,275 @@
+<template>
+  <doc-alert title="支付功能开启" url="https://doc.iocoder.cn/pay/build/" />
+
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      v-show="showSearch"
+      label-width="100px"
+    >
+      <el-form-item label="应用编号" prop="appId">
+        <el-select clearable v-model="queryParams.appId" filterable placeholder="请选择应用信息">
+          <el-option v-for="item in appList" :key="item.id" :label="item.name" :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="通知类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择通知类型" clearable size="small">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.PAY_NOTIFY_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="关联编号" prop="dataId">
+        <el-input
+          v-model="queryParams.dataId"
+          placeholder="请输入关联编号"
+          clearable
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="通知状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择通知状态" clearable>
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.PAY_NOTIFY_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="商户订单编号" prop="merchantOrderId">
+        <el-input
+          v-model="queryParams.merchantOrderId"
+          placeholder="请输入商户订单编号"
+          clearable
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          style="width: 240px"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          type="daterange"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="任务编号" align="center" prop="id" />
+      <el-table-column label="应用编号" align="center" prop="appName" />
+      <el-table-column label="商户订单编号" align="center" prop="merchantOrderId" />
+      <el-table-column label="通知类型" align="center" prop="type">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PAY_NOTIFY_TYPE" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column label="关联编号" align="center" prop="dataId" />
+      <el-table-column label="通知状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PAY_NOTIFY_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="最后通知时间"
+        align="center"
+        prop="lastExecuteTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        label="下次通知时间"
+        align="center"
+        prop="nextNotifyTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="通知次数" align="center" prop="notifyTimes">
+        <template #default="scope">
+          <el-tag size="small" type="success">
+            {{ scope.row.notifyTimes }} / {{ scope.row.maxNotifyTimes }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            size="small"
+            type="text"
+            icon="el-icon-search"
+            @click="handleDetail(scope.row)"
+            v-hasPermi="['pay:notify:query']"
+            >查看详情
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 对话框(详情) -->
+  <el-dialog title="通知详情" v-model:visible="open" width="700px" append-to-body destroy-on-close>
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="商户订单编号">
+        <el-tag size="small">{{ notifyDetail.merchantOrderId }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="通知状态">
+        <dict-tag :type="DICT_TYPE.PAY_NOTIFY_STATUS" :value="notifyDetail.status" size="small" />
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="应用编号">{{ notifyDetail.appId }}</el-descriptions-item>
+      <el-descriptions-item label="应用名称">{{ notifyDetail.appName }}</el-descriptions-item>
+    </el-descriptions>
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="关联编号">{{ notifyDetail.dataId }}</el-descriptions-item>
+      <el-descriptions-item label="通知类型">
+        <dict-tag :type="DICT_TYPE.PAY_NOTIFY_TYPE" :value="notifyDetail.type" />
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="通知次数">{{ notifyDetail.notifyTimes }}</el-descriptions-item>
+      <el-descriptions-item label="最大通知次数">{{
+        notifyDetail.maxNotifyTimes
+      }}</el-descriptions-item>
+    </el-descriptions>
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="最后通知时间">{{
+        formatDate(notifyDetail.lastExecuteTime)
+      }}</el-descriptions-item>
+      <el-descriptions-item label="下次通知时间">{{
+        formatDate(notifyDetail.nextNotifyTime)
+      }}</el-descriptions-item>
+    </el-descriptions>
+    <el-descriptions :column="2" label-class-name="desc-label">
+      <el-descriptions-item label="创建时间">{{
+        formatDate(notifyDetail.createTime)
+      }}</el-descriptions-item>
+      <el-descriptions-item label="更新时间">{{
+        formatDate(notifyDetail.updateTime)
+      }}</el-descriptions-item>
+    </el-descriptions>
+    <!-- 分割线 -->
+    <el-divider />
+    <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
+      <el-descriptions-item label="回调日志">
+        <el-table :data="notifyDetail.logs">
+          <el-table-column label="日志编号" align="center" prop="id" />
+          <el-table-column label="通知状态" align="center" prop="status">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.PAY_NOTIFY_STATUS" :value="scope.row.status" />
+            </template>
+          </el-table-column>
+          <el-table-column label="通知次数" align="center" prop="notifyTimes" />
+          <el-table-column label="通知时间" align="center" prop="lastExecuteTime" width="180">
+            <template #default="scope">
+              <span>{{ formatDate(scope.row.createTime) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="响应结果" align="center" prop="response" />
+        </el-table>
+      </el-descriptions-item>
+    </el-descriptions>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup name="PayNotify">
+import { getNotifyTaskPage, getNotifyTaskDetail } from '@/api/pay/notify'
+import { getAppList } from '@/api/pay/app'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+
+// 遮罩层
+const loading = ref(true)
+// 显示搜索条件
+const showSearch = ref(true)
+// 总条数
+const total = ref(0)
+// 支付通知列表
+const list = ref([])
+// 是否显示弹出层
+const open = ref(false)
+// 查询参数
+const queryParams = ref({
+  pageNo: 1,
+  pageSize: 10,
+  appId: null,
+  type: null,
+  dataId: null,
+  status: null,
+  merchantOrderId: null,
+  createTime: []
+})
+
+// 支付应用列表集合
+const appList = ref([])
+// 通知详情
+const notifyDetail = ref<any>({
+  logs: []
+})
+
+const queryFormRef = ref()
+
+onMounted(async () => {
+  await getList()
+  // 获得筛选项
+  const data = await getAppList()
+  appList.value = data
+})
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  // 执行查询
+  const data = await getNotifyTaskPage(queryParams.value)
+  list.value = data.list
+  total.value = data.total
+  loading.value = false
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 详情按钮操作 */
+const handleDetail = async (row: any) => {
+  notifyDetail.value = {}
+  const data = await getNotifyTaskDetail(row.id)
+  // 设置值
+  notifyDetail.value = data
+  // 弹窗打开
+  open.value = true
+}
+</script>

+ 3 - 3
src/views/pay/order/OrderDetail.vue

@@ -19,11 +19,11 @@
         </el-tag>
       </el-descriptions-item>
       <el-descriptions-item label="金额">
-        <el-tag type="success">¥{{ parseFloat(detailData.amount / 100, 2).toFixed(2) }}</el-tag>
+        <el-tag type="success">¥{{ parseFloat(detailData.price / 100, 2).toFixed(2) }}</el-tag>
       </el-descriptions-item>
       <el-descriptions-item label="手续费">
         <el-tag type="warning">
-          ¥{{ parseFloat(detailData.channelFeeAmount / 100, 2).toFixed(2) }}
+          ¥{{ parseFloat(detailData.channelFeePrice / 100, 2).toFixed(2) }}
         </el-tag>
       </el-descriptions-item>
       <el-descriptions-item label="手续费比例">
@@ -61,7 +61,7 @@
       <el-descriptions-item label="退款次数">{{ detailData.refundTimes }}</el-descriptions-item>
       <el-descriptions-item label="退款金额">
         <el-tag type="warning">
-          {{ parseFloat(detailData.refundAmount / 100, 2) }}
+          {{ parseFloat(detailData.refundPrice / 100, 2) }}
         </el-tag>
       </el-descriptions-item>
     </el-descriptions>

+ 30 - 40
src/views/pay/order/index.vue

@@ -139,44 +139,38 @@
 
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="订单编号" align="center" prop="id" />
-      <el-table-column label="商户名称" align="center" prop="merchantName" width="120" />
-      <el-table-column label="应用名称" align="center" prop="appName" width="120" />
-      <el-table-column label="渠道名称" align="center" prop="channelCodeName" width="120" />
-      <el-table-column label="渠道订单号" align="center" prop="merchantOrderId" width="120" />
-      <el-table-column label="商品标题" align="center" prop="subject" width="250" />
-      <el-table-column label="商品描述" align="center" prop="body" width="250" />
-      <el-table-column label="异步通知地址" align="center" prop="notifyUrl" width="250" />
-      <el-table-column label="回调状态" align="center" prop="notifyStatus">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="scope.row.notifyStatus" />
-        </template>
-      </el-table-column>
-      <el-table-column label="支付订单" width="280">
-        <template #default="scope">
-          <p class="order-font">
-            <el-tag>商户</el-tag>
-            {{ scope.row.merchantOrderId }}
-          </p>
-          <p class="order-font">
-            <el-tag type="warning">支付</el-tag>
-            {{ scope.row.channelOrderNo }}
-          </p>
-        </template>
+      <el-table-column label="编号" align="center" prop="id" width="80" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="支付金额" align="center" prop="price" width="100">
+        <template #default="scope"> ¥{{ parseFloat(scope.row.price / 100).toFixed(2) }} </template>
       </el-table-column>
-      <el-table-column label="支付金额" align="center" prop="amount">
+      <el-table-column label="退款金额" align="center" prop="refundPrice" width="100">
         <template #default="scope">
-          ¥{{ parseFloat(scope.row.amount / 100).toFixed(2) }}
+          ¥{{ parseFloat(scope.row.refundPrice / 100).toFixed(2) }}
         </template>
       </el-table-column>
-      <el-table-column label="手续金额" align="center" prop="channelFeeAmount">
+      <el-table-column label="手续金额" align="center" prop="channelFeePrice" width="100">
         <template #default="scope">
-          ¥{{ parseFloat(scope.row.channelFeeAmount / 100).toFixed(2) }}
+          ¥{{ parseFloat(scope.row.channelFeePrice / 100).toFixed(2) }}
         </template>
       </el-table-column>
-      <el-table-column label="退款金额" align="center" prop="refundAmount">
+      <el-table-column label="订单号" align="left" width="300">
         <template #default="scope">
-          ¥{{ parseFloat(scope.row.refundAmount / 100).toFixed(2) }}
+          <p class="order-font">
+            <el-tag size="small"> 商户</el-tag> {{ scope.row.merchantOrderId }}
+          </p>
+          <p class="order-font" v-if="scope.row.no">
+            <el-tag size="small" type="warning">支付</el-tag> {{ scope.row.no }}
+          </p>
+          <p class="order-font" v-if="scope.row.channelOrderNo">
+            <el-tag size="small" type="success">渠道</el-tag> {{ scope.row.channelOrderNo }}
+          </p>
         </template>
       </el-table-column>
       <el-table-column label="支付状态" align="center" prop="status">
@@ -184,18 +178,11 @@
           <dict-tag :type="DICT_TYPE.PAY_ORDER_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column label="回调状态" align="center" prop="notifyStatus">
+      <el-table-column label="支付渠道" align="center" prop="channelCode" width="140">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="scope.row.notifyStatus" />
+          <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="scope.row.channelCode" />
         </template>
       </el-table-column>
-      <el-table-column
-        label="创建时间"
-        align="center"
-        prop="createTime"
-        width="180"
-        :formatter="dateFormatter"
-      />
       <el-table-column
         label="支付时间"
         align="center"
@@ -203,11 +190,14 @@
         width="180"
         :formatter="dateFormatter"
       />
+      <el-table-column label="支付应用" align="center" prop="appName" width="100" />
+      <el-table-column label="商品标题" align="center" prop="subject" width="180" />
       <el-table-column label="操作" align="center" fixed="right">
         <template #default="scope">
           <el-button
             type="primary"
             link
+            size="small"
             @click="openDetail(scope.row.id)"
             v-hasPermi="['pay:order:query']"
           >
@@ -231,7 +221,7 @@
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import * as MerchantApi from '@/api/pay/merchant'
+// import * as MerchantApi from '@/api/pay/merchant'
 import * as OrderApi from '@/api/pay/order'
 import OrderDetail from './OrderDetail.vue'
 import download from '@/utils/download'

+ 2 - 2
src/views/pay/refund/index.vue

@@ -235,7 +235,7 @@
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import * as MerchantApi from '@/api/pay/merchant'
+// import * as MerchantApi from '@/api/pay/merchant'
 import * as RefundApi from '@/api/pay/refund'
 import RefundDetail from './RefundDetail.vue'
 import download from '@/utils/download'
@@ -330,7 +330,7 @@ const openDetail = (id: number) => {
 onMounted(async () => {
   await getList()
   // 加载商户列表
-  merchantList.value = await MerchantApi.getMerchantListByName()
+  // merchantList.value = await MerchantApi.getMerchantListByName()
   // TODO 芋艿:候选少一个查询应用列表的接口
   // appList.value = await AppApi.getAppListByMerchantId()
 })