feat(h5): 新增支付宝代运营授权落地页,路由独立到 isv-auth 命名空间

This commit is contained in:
bootx
2026-07-09 22:14:22 +08:00
parent 87cca2cda1
commit b24ea206a9
9 changed files with 288 additions and 0 deletions

View File

@@ -84,6 +84,15 @@ const routeModuleList: Array<RouteRecordRaw> = [
title: '支付宝认证',
},
},
// 支付宝代运营授权落地页(移动独占:授权回跳换 app_auth_token
{
path: RoutePath.ISV_AUTH_ALIPAY,
name: 'AlipayIsvAuth',
component: () => import('@/mobile/views/isv-auth/alipay/index.vue'),
meta: {
title: '支付宝代运营授权',
},
},
// 微信认证落地页(跨端:与 PC 端同 path 各指各 view微信公众号 OAuth 重定向回调取 code
{
path: RoutePath.AUTH_WECHAT,

View File

@@ -0,0 +1,221 @@
<script lang="ts" setup>
import { showDialog } from 'vant'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { isvAuthCallback } from '@/shared/api/isv-auth'
import alipayLogo from '@/shared/assets/icons/channel/alipay.svg'
defineOptions({ name: 'AlipayIsvAuthPage' })
declare const AlipayJSBridge: {
call: (name: string) => void
}
const { t } = useI18n()
const route = useRoute()
const loading = ref(true)
const failed = ref(false)
const failMsg = ref('')
/**
* Base64 解码 state(通道商户号)
*/
function base64Decode(str: string): string {
const base64 = str.replace(/-/g, '+').replace(/_/g, '/')
return decodeURIComponent(
atob(base64)
.split('')
.map(c => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join(''),
)
}
/**
* 标记失败
*/
function markFailed(msg: string) {
failed.value = true
failMsg.value = msg
loading.value = false
}
/**
* 关闭支付宝内嵌 WebView
*/
function handleClose() {
try {
if (typeof AlipayJSBridge !== 'undefined') {
AlipayJSBridge.call('closeWebview')
return
}
}
catch {
// 非支付宝环境忽略
}
window.history.back()
}
onMounted(() => {
const appAuthCode = route.query.app_auth_code as string | undefined
const state = route.query.state as string | undefined
if (!appAuthCode) {
markFailed(t('isvAuth.alipay.authCodeMissing'))
return
}
if (!state) {
markFailed(t('isvAuth.alipay.stateMissing'))
return
}
let channelMchNo = ''
try {
channelMchNo = base64Decode(state)
}
catch {
markFailed(t('isvAuth.alipay.stateInvalid'))
return
}
if (!channelMchNo) {
markFailed(t('isvAuth.alipay.stateInvalid'))
return
}
isvAuthCallback({
channelMchNo,
code: appAuthCode,
})
.then(() => {
loading.value = false
showDialog({
message: t('isvAuth.alipay.successMsg'),
confirmButtonText: t('isvAuth.alipay.close'),
}).then(() => {
handleClose()
})
})
.catch((err: Error) => {
markFailed(err?.message || t('isvAuth.alipay.authFail'))
})
})
</script>
<template>
<div class="auth-container">
<!-- 加载中 -->
<div v-if="loading" class="loading-box">
<div class="logo-wrapper">
<img class="channel-logo" :src="alipayLogo" alt="Alipay" width="64" height="64">
</div>
<van-loading vertical color="#1677ff" size="32px">
<span class="loading-text">{{ t('isvAuth.alipay.loading') }}</span>
</van-loading>
<div class="footer-tip">
<span>{{ t('isvAuth.alipay.secureTip') }}</span>
</div>
</div>
<!-- 失败 -->
<div v-else-if="failed" class="result-box">
<div class="status-icon">
<svg viewBox="0 0 1024 1024" width="64" height="64" aria-hidden="true">
<path fill="#ee0a24" d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m193.5 561.7-41.8 41.8L512 515.8 360.3 667.5l-41.8-41.8L470.2 474 318.5 322.3l41.8-41.8L512 432.2l151.7-151.7 41.8 41.8L553.8 474l151.7 151.7z" />
</svg>
</div>
<h3 class="result-title">
{{ t('isvAuth.alipay.failTitle') }}
</h3>
<p class="fail-msg">
{{ failMsg }}
</p>
<div class="action-buttons">
<van-button plain round block class="close-btn" @click="handleClose">
{{ t('isvAuth.alipay.close') }}
</van-button>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.auth-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
background-color: #f5f5f5;
.loading-box,
.result-box {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 340px;
padding: 40px 24px;
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 12px rgb(0 0 0 / 5%);
}
.loading-box {
.logo-wrapper {
margin-bottom: 24px;
.channel-logo {
display: block;
width: 64px;
height: 64px;
object-fit: contain;
}
}
.loading-text {
margin-top: 12px;
font-size: 14px;
color: #666;
}
.footer-tip {
margin-top: 32px;
font-size: 12px;
color: #999;
}
}
.result-box {
.status-icon {
margin-bottom: 16px;
line-height: 0;
}
.result-title {
margin: 0 0 24px;
font-size: 20px;
font-weight: 600;
color: #333;
}
.fail-msg {
margin: 0 0 24px;
font-size: 14px;
color: #666;
text-align: center;
word-break: break-all;
}
.action-buttons {
width: 100%;
.close-btn {
color: #666;
border-color: #eee;
}
}
}
}
</style>

View File

@@ -0,0 +1,24 @@
import { RequestEnum } from '@/shared/enums/httpEnum'
import { http } from '@/shared/utils/http/axios'
/**
* 服务商(ISV)代运营授权: 授权码换取 app_auth_token
*/
export function isvAuthCallback(param: IsvAuthParam): Promise<void> {
return http.request<void>({
url: '/unipay/assist/alipay/isv/auth/callback',
method: RequestEnum.POST,
data: param,
}, {
// 由页面自行处理错误展示
isShowMessage: false,
})
}
/** 代运营授权回调参数 */
export interface IsvAuthParam {
/** 通道商户号 */
channelMchNo: string
/** 应用授权码 */
code: string
}

View File

@@ -5,6 +5,7 @@ import common from './common.json'
import error from './error.json'
import home from './home.json'
import http from './http.json'
import isvAuth from './isv-auth.json'
import protocol from './protocol.json'
import statement from './statement.json'
@@ -12,6 +13,7 @@ export default {
common,
http,
auth,
isvAuth,
cashier,
codePay,
protocol,

View File

@@ -0,0 +1,13 @@
{
"alipay": {
"loading": "Completing agent authorization...",
"secureTip": "Alipay agent authorization",
"successMsg": "Authorization successful",
"failTitle": "Authorization failed",
"close": "Close",
"authCodeMissing": "Failed to get authorization code",
"stateMissing": "Failed to get channel merchant info",
"stateInvalid": "Invalid channel merchant info",
"authFail": "Authorization failed, please try again later"
}
}

View File

@@ -5,6 +5,7 @@ import common from './common.json'
import error from './error.json'
import home from './home.json'
import http from './http.json'
import isvAuth from './isv-auth.json'
import protocol from './protocol.json'
import statement from './statement.json'
@@ -12,6 +13,7 @@ export default {
common,
http,
auth,
isvAuth,
cashier,
codePay,
protocol,

View File

@@ -0,0 +1,13 @@
{
"alipay": {
"loading": "正在完成代运营授权...",
"secureTip": "支付宝代运营授权",
"successMsg": "授权成功",
"failTitle": "授权失败",
"close": "关闭页面",
"authCodeMissing": "授权码获取失败",
"stateMissing": "通道商户信息获取失败",
"stateInvalid": "通道商户信息无效",
"authFail": "授权失败,请稍后重试"
}
}

View File

@@ -29,6 +29,8 @@ export const EXCLUSIVE_ROUTES: ExclusiveRoute[] = [
{ path: RoutePath.CODE_PAY, name: 'CodePay', device: 'mobile' },
// 支付宝认证落地页:仅移动端实现(支付宝 App 内 JSAPI
{ path: RoutePath.AUTH_ALIPAY, name: 'AlipayAuth', device: 'mobile' },
// 支付宝代运营授权落地页:仅移动端实现(支付宝授权回跳)
{ path: RoutePath.ISV_AUTH_ALIPAY, name: 'AlipayIsvAuth', device: 'mobile' },
// 商户对账单:仅 PC 端实现
{ path: RoutePath.MERCHANT_STATEMENT, name: 'MerchantStatement', device: 'pc' },
]

View File

@@ -25,6 +25,8 @@ export const RoutePath = {
PROTOCOL: '/protocol/:protocolType',
/** 支付宝认证落地页移动独占JSAPI 取码) */
AUTH_ALIPAY: '/auth/alipay/:aliAppId/:queryCode',
/** 支付宝代运营授权落地页(移动独占:支付宝授权回跳换 app_auth_token */
ISV_AUTH_ALIPAY: '/isv-auth/alipay',
/** 微信认证落地页(跨端:公众号 OAuth 重定向回调PC 微信内置浏览器也可访问) */
AUTH_WECHAT: '/auth/wechat/:authToken',
} as const