This commit is contained in:
parent
0ac3fd5645
commit
be81b63b0e
11
App.vue
11
App.vue
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { onLaunch, onShow } from '@dcloudio/uni-app'
|
||||
import { APP_CONFIG, DEFAULT_MERCHANT } from '@/config'
|
||||
import { captureMerchantUrlFromLaunchOptions } from '@/utils/merchant-context'
|
||||
import { VersionUpdate } from '@/utils/common'
|
||||
|
||||
// 定义全局数据
|
||||
|
|
@ -10,16 +11,18 @@
|
|||
appid: APP_CONFIG.APP_ID,
|
||||
appId: APP_CONFIG.APP_ID,
|
||||
title: '卓享汇收银台',
|
||||
defaultMerchant: DEFAULT_MERCHANT
|
||||
defaultMerchant: DEFAULT_MERCHANT,
|
||||
pendingMerchantUrl: ''
|
||||
}
|
||||
// 使用 uni 对象来存储全局数据
|
||||
// @ts-ignore
|
||||
uni.$globalData = $globalData
|
||||
|
||||
onLaunch(() => {
|
||||
console.log('Cashier App Launch')
|
||||
onLaunch((options) => {
|
||||
captureMerchantUrlFromLaunchOptions(options || {})
|
||||
})
|
||||
onShow(() => {
|
||||
onShow((options) => {
|
||||
captureMerchantUrlFromLaunchOptions(options || {})
|
||||
// 检测小程序热更新
|
||||
// #ifdef MP-WEIXIN
|
||||
VersionUpdate()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
|
||||
## 页面
|
||||
|
||||
- `pages/cashier/index`:收银台首页,支持动态商家名称、顶部背景图和物业费公积金比例。
|
||||
- `pages/pay/pay`:收银台首页,支持动态商家名称、顶部背景图和物业费公积金比例。
|
||||
- `pages/pay/checkout`:半屏确认支付页。
|
||||
- `pages/cashier/result`:扫码支付结果页。
|
||||
- `pages/records/index`:付款记录占位页。
|
||||
- `pages/house/bind`:绑定房源占位页。
|
||||
|
||||
|
|
@ -13,7 +15,7 @@
|
|||
收银台首页支持通过页面参数临时覆盖商家信息:
|
||||
|
||||
```text
|
||||
/pages/cashier/index?merchant_id=xxx&name=商家名&background=图片地址&rate=0.04
|
||||
/pages/pay/pay?merchant_id=xxx&name=商家名&background=图片地址&rate=0.04
|
||||
```
|
||||
|
||||
如果传入 `merchant_id` 或 `scene`,页面会尝试请求 `v1/cashier/merchant` 获取商家配置;接口失败时使用 `App.vue` 中的默认商家配置。
|
||||
|
|
|
|||
55
api/auth.ts
55
api/auth.ts
|
|
@ -10,13 +10,33 @@ export interface LoginResponse {
|
|||
};
|
||||
}
|
||||
|
||||
export const loginByWechatCode = async (code: string) => {
|
||||
export type LoginPlatform = 'mp-weixin' | 'mp-alipay';
|
||||
|
||||
export const resolveLoginPlatform = (): LoginPlatform => {
|
||||
// #ifdef MP-ALIPAY
|
||||
return 'mp-alipay';
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
return 'mp-weixin';
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// #ifndef MP-ALIPAY
|
||||
return 'mp-weixin';
|
||||
// #endif
|
||||
// #endif
|
||||
};
|
||||
|
||||
/** 微信 code / 支付宝 authCode 都走同一登录接口 */
|
||||
export const loginByCode = async (code: string) => {
|
||||
const response = await httpRequest<LoginResponse>({
|
||||
url: `${APP_CONFIG.API_BASE_URL}/user/v3/login.code`,
|
||||
method: 'POST',
|
||||
data: toFormUrlEncoded({
|
||||
code,
|
||||
app_id: APP_CONFIG.APP_ID
|
||||
app_id: APP_CONFIG.APP_ID,
|
||||
platform: resolveLoginPlatform()
|
||||
}),
|
||||
header: {
|
||||
'content-type': 'application/x-www-form-urlencoded'
|
||||
|
|
@ -29,3 +49,34 @@ export const loginByWechatCode = async (code: string) => {
|
|||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/** @deprecated 使用 loginByCode */
|
||||
export const loginByWechatCode = loginByCode;
|
||||
|
||||
export interface MobileAuthBindResponse {
|
||||
code: number | string;
|
||||
msg?: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
/** 小程序授权绑定手机号(微信/支付宝) */
|
||||
export const mobileAuthBind = async (code: string) => {
|
||||
const response = await httpRequest<MobileAuthBindResponse>({
|
||||
url: `${APP_CONFIG.API_BASE_URL}/user/v1/mobile.auth.bind`,
|
||||
method: 'POST',
|
||||
data: toFormUrlEncoded({
|
||||
code,
|
||||
app_id: APP_CONFIG.APP_ID,
|
||||
platform: resolveLoginPlatform()
|
||||
}),
|
||||
header: {
|
||||
'content-type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw new HttpStatusError('手机号绑定失败', response.statusCode, response.data);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { get, post, postForm } from '../utils/request';
|
||||
import { resolveLoginPlatform } from './auth';
|
||||
|
||||
export interface MerchantConfigQuery {
|
||||
merchant_id?: string;
|
||||
|
|
@ -161,9 +162,9 @@ export const cashierRecordList = (data: any = {}) => get('v1/cashier/records', d
|
|||
|
||||
export const tradeList = (data: TradeListQuery = {}) =>
|
||||
get<ApiResult<TradeListItem[]>>('user/v1/trade.list', {
|
||||
trade_no: data.trade_no ?? '',
|
||||
page: data.page ?? 1,
|
||||
limit: data.limit ?? 10
|
||||
trade_no: data.trade_no != null ? data.trade_no : '',
|
||||
page: data.page != null ? data.page : 1,
|
||||
limit: data.limit != null ? data.limit : 10
|
||||
});
|
||||
|
||||
export const bindMerchantByUrl = (url: string) =>
|
||||
|
|
@ -188,8 +189,8 @@ export const platformScore = () => get<ApiResult<PlatformScoreResult>>('user/v1/
|
|||
|
||||
export const platformScoreRecord = (data: ScoreRecordQuery = {}) =>
|
||||
get<ApiResult<ScoreRecordItem[]>>('user/v1/platform.score.record', {
|
||||
page: data.page ?? 1,
|
||||
limit: data.limit ?? 10
|
||||
page: data.page != null ? data.page : 1,
|
||||
limit: data.limit != null ? data.limit : 10
|
||||
});
|
||||
|
||||
export const cashTradeInfo = (tradeNo: string) =>
|
||||
|
|
@ -198,6 +199,6 @@ export const cashTradeInfo = (tradeNo: string) =>
|
|||
export const cashTradePay = (data: CashTradePayPayload) =>
|
||||
post<ApiResult<CashTradePayResult>>('user/v1/cash.trade', {
|
||||
trade_no: data.trade_no,
|
||||
platform: data.platform ?? 'mp-weixin',
|
||||
remark: data.remark ?? ''
|
||||
platform: data.platform != null ? data.platform : resolveLoginPlatform(),
|
||||
remark: data.remark != null ? data.remark : ''
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,31 @@
|
|||
// #ifdef MP-ALIPAY
|
||||
const APP_ID = '123019';
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
const APP_ID = '123018';
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
// #ifndef MP-ALIPAY
|
||||
const APP_ID = '123018';
|
||||
// #endif
|
||||
// #endif
|
||||
|
||||
export const APP_CONFIG = {
|
||||
API_BASE_URL: 'https://dev.api.leapy.cn',
|
||||
APP_ID: '123018',
|
||||
APP_ID,
|
||||
RESOURCE_URL: 'https://resource.leapy.cn/staff/',
|
||||
PROPERTY_IMG_URL: 'https://resource.leapy.cn/',
|
||||
STORAGE_TOKEN_KEY: 'cashier:auth:token',
|
||||
// 小程序代码可被反编译,密钥仅用于避免 token 明文落盘,不替代服务端的有效期与失效策略。
|
||||
STORAGE_ENCRYPTION_KEY: 'cashier-storage-key-123018-v1-32'
|
||||
STORAGE_ENCRYPTION_KEY: 'cashier-storage-key-v1-32'
|
||||
} as const;
|
||||
|
||||
export const PROPERTY_APP = {
|
||||
APP_ID: 'wx42076f086669c49e',
|
||||
/** 物业微信小程序对外名称,支付宝端引导复制用 */
|
||||
WECHAT_NAME: '乐享云空间服务',
|
||||
/** 物业支付宝小程序 appId,配置后支付宝端可跳转 */
|
||||
ALIPAY_APP_ID: '',
|
||||
BIND_HOUSE_PATH: 'pagesC/certification/index?from=cashier',
|
||||
MINE_PATH: 'pages/mine/mine',
|
||||
WECHAT_APP_ID: 'wx02de0e1de9f4bda3'
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@
|
|||
"minified" : true
|
||||
},
|
||||
"usingComponents" : true,
|
||||
"navigateToMiniProgramAppIdList" : [
|
||||
"wx42076f086669c49e"
|
||||
]
|
||||
"navigateToMiniProgramAppIdList" : [ "wx42076f086669c49e" ]
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
"usingComponents" : true,
|
||||
"appid" : "2021006169600123",
|
||||
"component2" : true,
|
||||
"styleIsolation" : "shared",
|
||||
"enableAppxNg" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"format": 2,
|
||||
"compileOptions": {
|
||||
"component2": true,
|
||||
"enableAppxNg": true,
|
||||
"transpile": {}
|
||||
},
|
||||
"developOptions": {
|
||||
"skipTranspile": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/cashier/index",
|
||||
"path": "pages/pay/pay",
|
||||
"style": {
|
||||
"enablePullDownRefresh": false,
|
||||
"navigationBarTextStyle": "black",
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ import { onLoad } from '@dcloudio/uni-app';
|
|||
import { queryTrade } from '@/api/cashier';
|
||||
|
||||
const POLL_INTERVAL_MS = 1000;
|
||||
const POLL_MAX_ATTEMPTS = 120;
|
||||
const POLL_MAX_ATTEMPTS = 240;
|
||||
|
||||
type ResultStatus = 'pending' | 'success' | 'failed';
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ const goCashier = () => {
|
|||
return;
|
||||
}
|
||||
uni.reLaunch({
|
||||
url: `/pages/cashier/index?url=${encodeURIComponent(merchantUrl.value)}`
|
||||
url: `/pages/pay/pay?url=${encodeURIComponent(merchantUrl.value)}`
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<image class="fund-icon-image" :src="fundIconUrl" mode="aspectFit" />
|
||||
<view class="fund-text">
|
||||
<text class="fund-title">物业基金</text>
|
||||
<text class="fund-desc">查看我的物业基金余额</text>
|
||||
<text class="fund-desc">{{ propertyFundDesc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="fund-action">
|
||||
|
|
@ -36,11 +36,17 @@
|
|||
import { ref } from 'vue';
|
||||
import { onPullDownRefresh, onShow } from '@dcloudio/uni-app';
|
||||
import { platformScore } from '@/api/cashier';
|
||||
import { resolveLoginPlatform } from '@/api/auth';
|
||||
import { APP_CONFIG, PROPERTY_APP } from '@/config';
|
||||
|
||||
const IS_ALIPAY = resolveLoginPlatform() === 'mp-alipay';
|
||||
|
||||
const platformScoreText = ref('0.00');
|
||||
const loading = ref(false);
|
||||
const fundIconUrl = `${APP_CONFIG.PROPERTY_IMG_URL}property/new/cashier/fangwu.png`;
|
||||
const propertyFundDesc = IS_ALIPAY
|
||||
? `请前往微信「${PROPERTY_APP.WECHAT_NAME}」查看`
|
||||
: '查看我的物业基金余额';
|
||||
|
||||
const loadPlatformScore = async () => {
|
||||
const response = await platformScore();
|
||||
|
|
@ -66,7 +72,56 @@ const refreshPage = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const copyPropertyWechatName = () => {
|
||||
uni.setClipboardData({
|
||||
data: PROPERTY_APP.WECHAT_NAME,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '已复制,请到微信搜索',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '复制失败,请手动搜索',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const showPropertyFundWechatTip = () => {
|
||||
uni.showModal({
|
||||
title: '查看物业基金',
|
||||
content: `请打开微信,搜索并进入「${PROPERTY_APP.WECHAT_NAME}」小程序,在「我的」中查看物业基金。`,
|
||||
cancelText: '知道了',
|
||||
confirmText: '复制名称',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
copyPropertyWechatName();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const goPropertyFund = () => {
|
||||
if (IS_ALIPAY) {
|
||||
if (PROPERTY_APP.ALIPAY_APP_ID) {
|
||||
uni.navigateToMiniProgram({
|
||||
appId: PROPERTY_APP.ALIPAY_APP_ID,
|
||||
path: PROPERTY_APP.MINE_PATH,
|
||||
extraData: { from: 'cashier' },
|
||||
envVersion: APP_CONFIG.API_BASE_URL.includes('dev.') ? 'trial' : 'release',
|
||||
fail: () => {
|
||||
showPropertyFundWechatTip();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
showPropertyFundWechatTip();
|
||||
return;
|
||||
}
|
||||
|
||||
uni.navigateToMiniProgram({
|
||||
appId: PROPERTY_APP.APP_ID,
|
||||
path: PROPERTY_APP.MINE_PATH,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const formatMoney = (value?: string | number) => {
|
|||
};
|
||||
|
||||
const formatRecordType = (item: ScoreRecordItem) => {
|
||||
const type = String(item.type ?? '');
|
||||
const type = String(item.type != null ? item.type : '');
|
||||
const map: Record<string, string> = {
|
||||
'1': '积分收入',
|
||||
'2': '积分支出',
|
||||
|
|
@ -61,7 +61,7 @@ const formatRecordType = (item: ScoreRecordItem) => {
|
|||
};
|
||||
|
||||
const formatScoreChange = (item: ScoreRecordItem) => {
|
||||
const change = item.change_score ?? item.score;
|
||||
const change = item.change_score != null ? item.change_score : item.score;
|
||||
const amount = Number(change || 0);
|
||||
if (!Number.isFinite(amount)) return '0.00';
|
||||
const prefix = amount > 0 ? '+' : '';
|
||||
|
|
@ -69,11 +69,13 @@ const formatScoreChange = (item: ScoreRecordItem) => {
|
|||
};
|
||||
|
||||
const scoreClass = (item: ScoreRecordItem) => {
|
||||
const change = Number(item.change_score ?? item.score ?? 0);
|
||||
const raw = item.change_score != null ? item.change_score : item.score;
|
||||
const change = Number(raw != null ? raw : 0);
|
||||
return change >= 0 ? 'score-plus' : 'score-minus';
|
||||
};
|
||||
|
||||
const recordKey = (item: ScoreRecordItem, index: number) => String(item.id ?? `${item.create_time || 'record'}-${index}`);
|
||||
const recordKey = (item: ScoreRecordItem, index: number) =>
|
||||
String(item.id != null ? item.id : `${item.create_time || 'record'}-${index}`);
|
||||
|
||||
const fetchRecords = async (reset = false) => {
|
||||
if (loading.value) return;
|
||||
|
|
|
|||
|
|
@ -77,11 +77,12 @@
|
|||
import { computed, ref } from 'vue';
|
||||
import { onLoad, onUnload } from '@dcloudio/uni-app';
|
||||
import { cancelTrade, cashTradeInfo, cashTradePay, queryTrade } from '@/api/cashier';
|
||||
import { resolveLoginPlatform } from '@/api/auth';
|
||||
import { isCheckoutClosing, returnToHostMiniProgram, resetCheckoutClosing } from '@/utils/checkout-return';
|
||||
import { isPaymentCancelled, requestWxPayment } from '@/utils/wxpay';
|
||||
import { formatPaymentError, isPaymentCancelled, requestPlatformPayment } from '@/utils/wxpay';
|
||||
|
||||
const POLL_INTERVAL_MS = 1000;
|
||||
const POLL_MAX_ATTEMPTS = 120;
|
||||
const POLL_MAX_ATTEMPTS = 240;
|
||||
|
||||
const tradeNo = ref('');
|
||||
const subjectText = ref('');
|
||||
|
|
@ -249,7 +250,7 @@ const handlePay = async () => {
|
|||
try {
|
||||
const response = await cashTradePay({
|
||||
trade_no: tradeNo.value,
|
||||
platform: 'mp-weixin',
|
||||
platform: resolveLoginPlatform(),
|
||||
remark: remark.value.trim()
|
||||
});
|
||||
|
||||
|
|
@ -263,20 +264,9 @@ const handlePay = async () => {
|
|||
}
|
||||
|
||||
const payload = response.data || {};
|
||||
const params = {
|
||||
timeStamp: String(payload.timestamp || payload.timeStamp || ''),
|
||||
nonceStr: payload.nonce_str || payload.nonceStr || '',
|
||||
package: payload.package || '',
|
||||
signType: payload.sign_type || payload.signType || 'RSA',
|
||||
paySign: payload.pay_sign || payload.paySign || ''
|
||||
};
|
||||
|
||||
if (!params.paySign) {
|
||||
throw new Error('支付参数不完整');
|
||||
}
|
||||
|
||||
try {
|
||||
await requestWxPayment(params);
|
||||
await requestPlatformPayment(payload);
|
||||
} catch (error) {
|
||||
if (isPaymentCancelled(error)) {
|
||||
await finishCancelledAndReturn();
|
||||
|
|
@ -295,7 +285,7 @@ const handlePay = async () => {
|
|||
|
||||
await backToHost('success', 0);
|
||||
} catch (error: any) {
|
||||
const message = error?.message || '支付失败';
|
||||
const message = formatPaymentError(error);
|
||||
if (isTradeNotPayable(message)) {
|
||||
await finishCancelledAndReturn();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,43 @@
|
|||
:disabled="paying"
|
||||
/>
|
||||
</view>
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<button
|
||||
v-if="showAlipayPhoneAuthBtn"
|
||||
class="property-row property-row-btn"
|
||||
hover-class="property-row-hover"
|
||||
open-type="getAuthorize"
|
||||
scope="phoneNumber"
|
||||
:disabled="paying || bindingPhone"
|
||||
@tap="handleAlipayPhoneAuthTap"
|
||||
@getAuthorize="handleAlipayPhoneAuthorize"
|
||||
@error="handleAlipayPhoneAuthError"
|
||||
>
|
||||
<text class="property-name li-single-line property-name-action">
|
||||
{{ propertyRowText }}
|
||||
</text>
|
||||
<text v-if="showPropertyArrow" class="ri-arrow-right-s-line property-arrow"></text>
|
||||
</button>
|
||||
<view
|
||||
v-else
|
||||
class="property-row"
|
||||
:class="{ 'property-row--disabled': alipayPropertyUnbound }"
|
||||
hover-class="property-row-hover"
|
||||
@tap.stop="handlePropertyRow"
|
||||
>
|
||||
<text
|
||||
class="property-name li-single-line"
|
||||
:class="{
|
||||
'property-name-action': isPropertyActionText && !alipayPropertyUnbound,
|
||||
'property-name-tip': alipayPropertyUnbound
|
||||
}"
|
||||
>
|
||||
{{ propertyRowText }}
|
||||
</text>
|
||||
<text v-if="showPropertyArrow" class="ri-arrow-right-s-line property-arrow"></text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<view
|
||||
class="property-row"
|
||||
hover-class="property-row-hover"
|
||||
|
|
@ -79,8 +116,9 @@
|
|||
>
|
||||
{{ propertyRowText }}
|
||||
</text>
|
||||
<text class="ri-arrow-right-s-line property-arrow"></text>
|
||||
<text v-if="showPropertyArrow" class="ri-arrow-right-s-line property-arrow"></text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -106,7 +144,7 @@
|
|||
></text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="property-sheet-footer" @tap="goBindProperty">
|
||||
<view v-if="!IS_ALIPAY" class="property-sheet-footer" @tap="goBindProperty">
|
||||
<text class="ri-add-line property-sheet-footer-icon"></text>
|
||||
<text>绑定新房源</text>
|
||||
</view>
|
||||
|
|
@ -151,6 +189,7 @@ import { computed, ref } from 'vue';
|
|||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { bindMerchantByUrl, cancelTrade, changeMerchant, merchantListByUrl, supplierByUrl } from '@/api/cashier';
|
||||
import type { MerchantListItem } from '@/api/cashier';
|
||||
import { mobileAuthBind, resolveLoginPlatform } from '@/api/auth';
|
||||
import { postForm } from '@/utils/request';
|
||||
import { DEFAULT_MERCHANT, APP_CONFIG, PROPERTY_APP } from '@/config';
|
||||
import {
|
||||
|
|
@ -158,12 +197,14 @@ import {
|
|||
getPropertyBindingCacheKeyByHash,
|
||||
getSupplierCacheKey,
|
||||
getSupplierCacheKeyByHash,
|
||||
resolveMerchantUrlFromOptions,
|
||||
clearPendingMerchantUrl,
|
||||
clearActiveMerchantContext,
|
||||
resolveMerchantUrlFromEntry,
|
||||
resolveMerchantUrlFromScanResult,
|
||||
syncActiveMerchantContext
|
||||
} from '@/utils/merchant-context';
|
||||
import { encryptedStorage } from '@/utils/storage';
|
||||
import { isPaymentCancelled, requestWxPayment } from '@/utils/wxpay';
|
||||
import { formatPaymentError, isPaymentCancelled, requestPlatformPayment } from '@/utils/wxpay';
|
||||
|
||||
interface MerchantInfo {
|
||||
id: string;
|
||||
|
|
@ -177,6 +218,7 @@ const PROPERTY_CACHE_TTL_SECONDS = 10 * 60;
|
|||
const SUPPLIER_CACHE_TTL_SECONDS = 10 * 60;
|
||||
const emptyStateBackground = '/static/cashier/bg-image.png';
|
||||
const brandName = '卓享汇支付';
|
||||
const IS_ALIPAY = resolveLoginPlatform() === 'mp-alipay';
|
||||
|
||||
const hasMerchantUrl = ref(false);
|
||||
const merchant = ref<MerchantInfo>({ ...defaultMerchant });
|
||||
|
|
@ -190,7 +232,11 @@ const propertyMerchantId = ref('');
|
|||
const propertyList = ref<MerchantListItem[]>([]);
|
||||
const propertySheetVisible = ref(false);
|
||||
const switchingProperty = ref(false);
|
||||
const bindingPhone = ref(false);
|
||||
const alipayPropertyUnbound = ref(false);
|
||||
const scanning = ref(false);
|
||||
/** 已判定无效的进页码,避免 onShow 用同一 launch qrCode 反复弹窗 */
|
||||
const rejectedMerchantUrl = ref('');
|
||||
const numberKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
|
||||
const canPay = computed(() => Number(amount.value) > 0 && !paying.value);
|
||||
|
|
@ -204,9 +250,17 @@ const benefitTitle = computed(() => `预计获得${scoreName.value || '物业公
|
|||
const propertyRowText = computed(() => {
|
||||
if (propertyName.value) return propertyName.value;
|
||||
if (propertyList.value.length) return '选择物业';
|
||||
if (IS_ALIPAY) {
|
||||
if (alipayPropertyUnbound.value) return '暂未绑定,请去微信小程序绑定';
|
||||
return '绑定手机号查看房源';
|
||||
}
|
||||
return '业主去绑定';
|
||||
});
|
||||
const isPropertyActionText = computed(() => !propertyName.value);
|
||||
const showAlipayPhoneAuthBtn = computed(
|
||||
() => IS_ALIPAY && !propertyName.value && !propertyList.value.length && !alipayPropertyUnbound.value
|
||||
);
|
||||
const showPropertyArrow = computed(() => !IS_ALIPAY || !alipayPropertyUnbound.value);
|
||||
const propertySheetTitle = computed(() => (propertyName.value ? '切换物业' : '选择物业'));
|
||||
|
||||
interface PropertyBindingCache {
|
||||
|
|
@ -234,7 +288,7 @@ const hasFeeRate = (feeRate: unknown) => feeRate !== undefined && feeRate !== nu
|
|||
|
||||
const resolveFeeRateFromPayload = (payload: Record<string, any> = {}) => {
|
||||
const supplierData = payload.supplier || {};
|
||||
return supplierData.fee_rate ?? payload.fee_rate;
|
||||
return supplierData.fee_rate != null ? supplierData.fee_rate : payload.fee_rate;
|
||||
};
|
||||
|
||||
const resolvePropertyFromPayload = (payload: Record<string, any> = {}) => {
|
||||
|
|
@ -301,10 +355,23 @@ const isSameMerchantId = (left: string | number, right: string | number) => {
|
|||
const buildSupplierCache = (payload: Record<string, any> = {}, previous?: SupplierCache | null): SupplierCache => {
|
||||
const supplierData = payload.supplier || {};
|
||||
return {
|
||||
supplier_id: supplierData.supplier_id ?? payload.supplier_id ?? previous?.supplier_id,
|
||||
supplier_name: supplierData.supplier_name ?? payload.supplier_name ?? previous?.supplier_name,
|
||||
logo: supplierData.logo ?? payload.logo ?? previous?.logo,
|
||||
fee_rate: resolveFeeRateFromPayload(payload) ?? previous?.fee_rate
|
||||
supplier_id:
|
||||
supplierData.supplier_id != null
|
||||
? supplierData.supplier_id
|
||||
: payload.supplier_id != null
|
||||
? payload.supplier_id
|
||||
: previous?.supplier_id,
|
||||
supplier_name:
|
||||
supplierData.supplier_name != null
|
||||
? supplierData.supplier_name
|
||||
: payload.supplier_name != null
|
||||
? payload.supplier_name
|
||||
: previous?.supplier_name,
|
||||
logo: supplierData.logo != null ? supplierData.logo : payload.logo != null ? payload.logo : previous?.logo,
|
||||
fee_rate: (() => {
|
||||
const rate = resolveFeeRateFromPayload(payload);
|
||||
return rate != null ? rate : previous?.fee_rate;
|
||||
})()
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -354,22 +421,26 @@ const syncBindingCacheForUrl = (merchantUrl: string) => {
|
|||
return getPropertyBindingCacheKey(merchantUrl);
|
||||
};
|
||||
|
||||
const initWithMerchantUrl = async (merchantUrl: string) => {
|
||||
hasMerchantUrl.value = true;
|
||||
activeMerchantUrl.value = merchantUrl;
|
||||
const resetToEmptyMerchantState = () => {
|
||||
hasMerchantUrl.value = false;
|
||||
activeMerchantUrl.value = '';
|
||||
merchant.value = { ...defaultMerchant };
|
||||
amount.value = '';
|
||||
remark.value = '';
|
||||
propertyName.value = '';
|
||||
propertyMerchantId.value = '';
|
||||
propertyList.value = [];
|
||||
propertySheetVisible.value = false;
|
||||
alipayPropertyUnbound.value = false;
|
||||
scoreName.value = '物业公积金';
|
||||
await loadPropertyBinding(merchantUrl);
|
||||
await loadSupplier(merchantUrl);
|
||||
clearPendingMerchantUrl();
|
||||
clearActiveMerchantContext();
|
||||
};
|
||||
|
||||
const promptScan = () => {
|
||||
const promptScan = (message?: string) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '未识别到商家码,请扫描商家收款码进入收银台',
|
||||
content: message || '未识别到商家码,请扫描商家收款码进入收银台',
|
||||
confirmText: '去扫码',
|
||||
showCancel: false,
|
||||
success: (res) => {
|
||||
|
|
@ -380,6 +451,84 @@ const promptScan = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const failInvalidMerchant = (merchantUrl: string, message?: string) => {
|
||||
rejectedMerchantUrl.value = merchantUrl;
|
||||
resetToEmptyMerchantState();
|
||||
promptScan(message);
|
||||
};
|
||||
|
||||
const fetchSupplierByUrl = async (merchantUrl: string) => {
|
||||
const response = await supplierByUrl(merchantUrl);
|
||||
if (Number(response?.code) !== 200 || !response?.data) {
|
||||
return {
|
||||
ok: false as const,
|
||||
msg: response?.msg || '未识别到商家码,请扫描商家收款码进入收银台'
|
||||
};
|
||||
}
|
||||
|
||||
const payload = response.data || {};
|
||||
const cacheKey = getSupplierCacheKey(merchantUrl);
|
||||
const cached = encryptedStorage.get<SupplierCache>(cacheKey);
|
||||
const latestSupplier = buildSupplierCache(payload, cached);
|
||||
if (!latestSupplier.supplier_name && !latestSupplier.supplier_id) {
|
||||
return {
|
||||
ok: false as const,
|
||||
msg: response?.msg || '未识别到商家码,请扫描商家收款码进入收银台'
|
||||
};
|
||||
}
|
||||
|
||||
applyPropertyInfo(resolvePropertyFromPayload(payload));
|
||||
applySupplierInfo(latestSupplier);
|
||||
encryptedStorage.set(cacheKey, latestSupplier, SUPPLIER_CACHE_TTL_SECONDS);
|
||||
return { ok: true as const, supplier: latestSupplier };
|
||||
};
|
||||
|
||||
const initWithMerchantUrl = async (
|
||||
merchantUrl: string,
|
||||
options: { force?: boolean } = {}
|
||||
) => {
|
||||
if (!options.force && rejectedMerchantUrl.value === merchantUrl) {
|
||||
resetToEmptyMerchantState();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [supplierResult, bindResponse] = await Promise.all([
|
||||
fetchSupplierByUrl(merchantUrl),
|
||||
bindMerchantByUrl(merchantUrl).catch(() => null)
|
||||
]);
|
||||
|
||||
if (!supplierResult.ok) {
|
||||
failInvalidMerchant(merchantUrl, supplierResult.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
const bindCode = Number(bindResponse?.code);
|
||||
const bindMsg = String(bindResponse?.msg || '');
|
||||
if (bindResponse && bindCode !== 200 && /重新扫码/.test(bindMsg)) {
|
||||
failInvalidMerchant(merchantUrl, bindMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
rejectedMerchantUrl.value = '';
|
||||
hasMerchantUrl.value = true;
|
||||
activeMerchantUrl.value = merchantUrl;
|
||||
propertyName.value = '';
|
||||
propertyMerchantId.value = '';
|
||||
propertyList.value = [];
|
||||
propertySheetVisible.value = false;
|
||||
alipayPropertyUnbound.value = false;
|
||||
scoreName.value = '物业公积金';
|
||||
applySupplierInfo(supplierResult.supplier);
|
||||
await loadPropertyBinding(merchantUrl);
|
||||
} catch (error: any) {
|
||||
failInvalidMerchant(
|
||||
merchantUrl,
|
||||
error?.message || '未识别到商家码,请扫描商家收款码进入收银台'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScan = () => {
|
||||
if (scanning.value) return;
|
||||
|
||||
|
|
@ -397,7 +546,7 @@ const handleScan = () => {
|
|||
promptScan();
|
||||
return;
|
||||
}
|
||||
await initWithMerchantUrl(merchantUrl);
|
||||
await initWithMerchantUrl(merchantUrl, { force: true });
|
||||
},
|
||||
fail: (err: any) => {
|
||||
const errMsg = err?.errMsg || '';
|
||||
|
|
@ -468,16 +617,12 @@ const loadSupplier = async (merchantUrl: string) => {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await supplierByUrl(merchantUrl);
|
||||
if (Number(response?.code) !== 200 || !response?.data) return;
|
||||
|
||||
const payload = response.data || {};
|
||||
const latestSupplier = buildSupplierCache(payload, cached);
|
||||
applyPropertyInfo(resolvePropertyFromPayload(payload));
|
||||
applySupplierInfo(latestSupplier);
|
||||
encryptedStorage.set(cacheKey, latestSupplier, SUPPLIER_CACHE_TTL_SECONDS);
|
||||
const result = await fetchSupplierByUrl(merchantUrl);
|
||||
if (!result.ok) return false;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('supplier fallback cache', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -506,7 +651,118 @@ const deleteKey = () => {
|
|||
amount.value = amount.value.slice(0, -1);
|
||||
};
|
||||
|
||||
const showAlipayUnboundTip = () => {
|
||||
uni.showModal({
|
||||
title: '暂未绑定房源',
|
||||
content: '您在微信小程序中暂无绑定房源,请前往微信小程序完成绑定后再使用。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
});
|
||||
};
|
||||
|
||||
const refreshPropertyAfterPhoneBind = async () => {
|
||||
if (!activeMerchantUrl.value) return;
|
||||
|
||||
alipayPropertyUnbound.value = false;
|
||||
clearPropertyBinding();
|
||||
propertyList.value = [];
|
||||
encryptedStorage.remove(getPropertyBindingCacheKey(activeMerchantUrl.value));
|
||||
await loadPropertyBinding(activeMerchantUrl.value);
|
||||
|
||||
if (propertyName.value || propertyList.value.length) {
|
||||
uni.showToast({
|
||||
title: '已匹配到物业信息',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
alipayPropertyUnbound.value = true;
|
||||
};
|
||||
|
||||
const extractAlipayPhoneCode = (phoneRes: any) => {
|
||||
const response = phoneRes?.response;
|
||||
if (typeof response === 'string') return response;
|
||||
if (response && typeof response === 'object') {
|
||||
return response.response || response.code || '';
|
||||
}
|
||||
return phoneRes?.code || '';
|
||||
};
|
||||
|
||||
const requestAlipayPhoneNumber = () =>
|
||||
new Promise<any>((resolve, reject) => {
|
||||
// #ifdef MP-ALIPAY
|
||||
if (typeof my !== 'undefined' && typeof my.getPhoneNumber === 'function') {
|
||||
my.getPhoneNumber({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
|
||||
uni.getPhoneNumber({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
|
||||
const handleAlipayPhoneAuthTap = () => {
|
||||
console.log('[收银台][手机号授权] 按钮点击');
|
||||
};
|
||||
|
||||
const handleAlipayPhoneAuthError = (event?: any) => {
|
||||
console.log('[收银台][手机号授权] error 回调', event);
|
||||
};
|
||||
|
||||
const handleAlipayPhoneAuthorize = async (event?: any) => {
|
||||
console.log('[收银台][手机号授权] getAuthorize 回调', event);
|
||||
|
||||
if (paying.value || bindingPhone.value) return;
|
||||
|
||||
bindingPhone.value = true;
|
||||
try {
|
||||
const phoneRes = await requestAlipayPhoneNumber();
|
||||
console.log('[收银台][手机号授权] getPhoneNumber 成功', phoneRes);
|
||||
|
||||
const code = extractAlipayPhoneCode(phoneRes);
|
||||
console.log('[收银台][手机号授权] 解析 code', {
|
||||
code,
|
||||
response: phoneRes?.response,
|
||||
phoneCode: phoneRes?.code
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
throw new Error('手机号授权失败');
|
||||
}
|
||||
|
||||
const response = await mobileAuthBind(code);
|
||||
console.log('[收银台][手机号授权] mobileAuthBind 响应', response);
|
||||
|
||||
if (Number(response?.code) !== 200) {
|
||||
throw new Error(response?.msg || '手机号绑定失败');
|
||||
}
|
||||
|
||||
await refreshPropertyAfterPhoneBind();
|
||||
} catch (error: any) {
|
||||
console.log('[收银台][手机号授权] 授权失败', error);
|
||||
const errMsg = error?.errMsg || error?.message || '';
|
||||
if (errMsg.includes('cancel') || errMsg.includes('拒绝')) return;
|
||||
uni.showToast({
|
||||
title: errMsg || '手机号授权失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
bindingPhone.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const goBindProperty = () => {
|
||||
if (IS_ALIPAY) {
|
||||
showAlipayUnboundTip();
|
||||
return;
|
||||
}
|
||||
|
||||
if (paying.value) return;
|
||||
propertySheetVisible.value = false;
|
||||
|
||||
|
|
@ -543,6 +799,12 @@ const openPropertySheet = async () => {
|
|||
|
||||
const list = await fetchMerchantList(activeMerchantUrl.value);
|
||||
if (!list.length) {
|
||||
if (IS_ALIPAY) {
|
||||
if (alipayPropertyUnbound.value) {
|
||||
showAlipayUnboundTip();
|
||||
}
|
||||
return;
|
||||
}
|
||||
goBindProperty();
|
||||
return;
|
||||
}
|
||||
|
|
@ -552,9 +814,18 @@ const openPropertySheet = async () => {
|
|||
};
|
||||
|
||||
const handlePropertyRow = async () => {
|
||||
if (paying.value || switchingProperty.value) return;
|
||||
if (paying.value || switchingProperty.value || bindingPhone.value) return;
|
||||
if (!activeMerchantUrl.value) return;
|
||||
|
||||
if (IS_ALIPAY && alipayPropertyUnbound.value) {
|
||||
showAlipayUnboundTip();
|
||||
return;
|
||||
}
|
||||
|
||||
if (IS_ALIPAY && showAlipayPhoneAuthBtn.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!propertyName.value) {
|
||||
try {
|
||||
if (propertyList.value.length) {
|
||||
|
|
@ -612,7 +883,9 @@ const switchToProperty = async (
|
|||
},
|
||||
PROPERTY_CACHE_TTL_SECONDS
|
||||
);
|
||||
await loadPropertyBinding(merchantUrl, { skipFallback: options.skipBindingFallback ?? true });
|
||||
await loadPropertyBinding(merchantUrl, {
|
||||
skipFallback: options.skipBindingFallback != null ? options.skipBindingFallback : true
|
||||
});
|
||||
await loadSupplier(merchantUrl);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
|
|
@ -652,10 +925,10 @@ const handlePay = async () => {
|
|||
throw new Error('缺少支付参数:商户码 url');
|
||||
}
|
||||
|
||||
// 扫商户码支付:直接调用后端 jspay,拿到微信 requestPayment 所需签名参数
|
||||
// 扫商户码支付:调用后端 jspay,按当前端传入 platform
|
||||
const payload = {
|
||||
amount: payableAmount.value,
|
||||
platform: 'mp-weixin',
|
||||
platform: resolveLoginPlatform(),
|
||||
remark: remark.value.trim(),
|
||||
url: activeMerchantUrl.value
|
||||
};
|
||||
|
|
@ -665,26 +938,14 @@ const handlePay = async () => {
|
|||
throw new Error(payRes?.msg || '获取支付参数失败');
|
||||
}
|
||||
|
||||
const wechat = payRes?.data || {};
|
||||
const params: any = {
|
||||
timeStamp: wechat.timeStamp || wechat.time_stamp || wechat.timestamp || '',
|
||||
nonceStr: wechat.nonceStr || wechat.nonce_str || '',
|
||||
package: wechat.package || wechat.pack || '',
|
||||
signType: wechat.signType || wechat.sign_type || '',
|
||||
paySign: wechat.paySign || wechat.pay_sign || ''
|
||||
};
|
||||
|
||||
if (!params.paySign) {
|
||||
throw new Error('支付参数不完整');
|
||||
}
|
||||
|
||||
const tradeNo = wechat.trade_no || wechat.tradeNo || '';
|
||||
const payData = payRes?.data || {};
|
||||
const tradeNo = payData.trade_no || payData.tradeNo || '';
|
||||
if (!tradeNo) {
|
||||
throw new Error('缺少交易单号');
|
||||
}
|
||||
|
||||
try {
|
||||
await requestWxPayment(params);
|
||||
await requestPlatformPayment(payData);
|
||||
} catch (error) {
|
||||
if (isPaymentCancelled(error)) {
|
||||
await cancelTrade(tradeNo).catch(() => {});
|
||||
|
|
@ -698,7 +959,7 @@ const handlePay = async () => {
|
|||
});
|
||||
} catch (error: any) {
|
||||
uni.showToast({
|
||||
title: error?.message || '支付失败',
|
||||
title: formatPaymentError(error),
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
|
|
@ -707,15 +968,25 @@ const handlePay = async () => {
|
|||
};
|
||||
|
||||
onLoad((options: any) => {
|
||||
const merchantUrl = resolveMerchantUrlFromOptions(options || {});
|
||||
const merchantUrl = resolveMerchantUrlFromEntry(options || {});
|
||||
if (!merchantUrl) {
|
||||
handleMissingMerchantUrl();
|
||||
return;
|
||||
}
|
||||
clearPendingMerchantUrl();
|
||||
initWithMerchantUrl(merchantUrl);
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
if (!hasMerchantUrl.value || !activeMerchantUrl.value) {
|
||||
const merchantUrl = resolveMerchantUrlFromEntry({});
|
||||
if (merchantUrl && merchantUrl !== rejectedMerchantUrl.value) {
|
||||
clearPendingMerchantUrl();
|
||||
initWithMerchantUrl(merchantUrl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMerchantUrl.value || !activeMerchantUrl.value) return;
|
||||
loadPropertyBinding(activeMerchantUrl.value);
|
||||
loadSupplier(activeMerchantUrl.value);
|
||||
|
|
@ -1037,6 +1308,7 @@ onShow(() => {
|
|||
}
|
||||
|
||||
.property-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
|
@ -1044,6 +1316,27 @@ onShow(() => {
|
|||
padding: 12rpx 4rpx;
|
||||
}
|
||||
|
||||
.property-row--disabled {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.property-row-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 12rpx 4rpx;
|
||||
border: none;
|
||||
background: transparent;
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.property-row-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.property-row-hover {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
|
@ -1060,6 +1353,11 @@ onShow(() => {
|
|||
color: #1F5BD8;
|
||||
}
|
||||
|
||||
.property-name-tip {
|
||||
color: #8B929E;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.property-arrow {
|
||||
flex-shrink: 0;
|
||||
margin-left: 4rpx;
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* 可选:手动对支付宝产物再跑一遍降级。
|
||||
* 日常「运行/发行到支付宝」会由 vite.config.ts 的 alipayEsCompatPlugin 自动处理。
|
||||
*
|
||||
* 用法:node scripts/alipay-es-compat.js [dir...]
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function resolveTransformSync() {
|
||||
try {
|
||||
return require('esbuild').transformSync;
|
||||
} catch (error) {
|
||||
try {
|
||||
const vitePath = require.resolve('vite');
|
||||
const esbuildPath = require.resolve('esbuild', { paths: [path.dirname(vitePath)] });
|
||||
return require(esbuildPath).transformSync;
|
||||
} catch (inner) {
|
||||
throw new Error('找不到 esbuild,请先通过 vite/uni 运行支付宝构建,或安装 esbuild');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const transformSync = resolveTransformSync();
|
||||
|
||||
const TRANSFORM_OPTIONS = {
|
||||
loader: 'js',
|
||||
target: 'es2015',
|
||||
sourcemap: false,
|
||||
supported: {
|
||||
arrow: false,
|
||||
'template-literal': false,
|
||||
'optional-chain': false,
|
||||
'nullish-coalescing': false,
|
||||
'optional-catch-binding': false,
|
||||
'object-rest-spread': false,
|
||||
'class-field': false
|
||||
}
|
||||
};
|
||||
|
||||
function walkJsFiles(dir, files = []) {
|
||||
if (!fs.existsSync(dir)) return files;
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const fullPath = path.join(dir, name);
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
walkJsFiles(fullPath, files);
|
||||
} else if (name.endsWith('.js')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function needsTransform(source) {
|
||||
return (
|
||||
source.includes('=>') ||
|
||||
source.includes('`') ||
|
||||
source.includes('?.') ||
|
||||
source.includes('??') ||
|
||||
/catch\s*\{/.test(source)
|
||||
);
|
||||
}
|
||||
|
||||
function transformFile(filePath) {
|
||||
const source = fs.readFileSync(filePath, 'utf8');
|
||||
if (!needsTransform(source)) return false;
|
||||
const result = transformSync(source, TRANSFORM_OPTIONS);
|
||||
if (!result.code || result.code === source) return false;
|
||||
fs.writeFileSync(filePath, result.code, 'utf8');
|
||||
return true;
|
||||
}
|
||||
|
||||
function run(roots) {
|
||||
let changed = 0;
|
||||
for (const root of roots) {
|
||||
const abs = path.resolve(__dirname, '..', root);
|
||||
for (const file of walkJsFiles(abs)) {
|
||||
try {
|
||||
if (transformFile(file)) changed += 1;
|
||||
} catch (error) {
|
||||
console.warn('[alipay-es-compat] skip', file, error && error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`[alipay-es-compat] transformed ${changed} file(s)`);
|
||||
}
|
||||
|
||||
const cliRoots = process.argv.slice(2);
|
||||
run(
|
||||
cliRoots.length
|
||||
? cliRoots
|
||||
: ['unpackage/dist/dev/mp-alipay', 'unpackage/dist/build/mp-alipay']
|
||||
);
|
||||
|
|
@ -1,26 +1,33 @@
|
|||
import { loginByWechatCode } from '@/api/auth';
|
||||
import { loginByCode } from '@/api/auth';
|
||||
import { authStorage } from '@/utils/storage';
|
||||
|
||||
let loginPromise: Promise<string> | null = null;
|
||||
|
||||
const getWechatCode = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
const getLoginCode = () => {
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
uni.login({
|
||||
// #ifdef MP-ALIPAY
|
||||
provider: 'alipay',
|
||||
scopes: 'auth_base',
|
||||
// #endif
|
||||
success: (result) => {
|
||||
if (!result.code) {
|
||||
reject(new Error('微信登录凭证获取失败'));
|
||||
const code = result.code || (result as any).authCode;
|
||||
if (!code) {
|
||||
reject(new Error('登录凭证获取失败'));
|
||||
return;
|
||||
}
|
||||
resolve(result.code);
|
||||
resolve(code);
|
||||
},
|
||||
fail: () => reject(new Error('微信登录凭证获取失败'))
|
||||
fail: () => reject(new Error('登录凭证获取失败'))
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
return Promise.reject(new Error('当前端暂不支持微信无感登录'));
|
||||
// #ifndef MP-ALIPAY
|
||||
return Promise.reject(new Error('当前端暂不支持无感登录'));
|
||||
// #endif
|
||||
// #endif
|
||||
};
|
||||
|
||||
|
|
@ -28,8 +35,8 @@ export const ensureLogin = () => {
|
|||
if (loginPromise) return loginPromise;
|
||||
|
||||
const pendingLogin = (async () => {
|
||||
const code = await getWechatCode();
|
||||
const result = await loginByWechatCode(code);
|
||||
const code = await getLoginCode();
|
||||
const result = await loginByCode(code);
|
||||
const token = result?.data?.token;
|
||||
|
||||
if (Number(result?.code) !== 200 || !token) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { encryptedStorage } from '@/utils/storage';
|
|||
|
||||
export const ACTIVE_QR_URL_CACHE_KEY = 'cashier:property:active-url';
|
||||
export const ACTIVE_QR_HASH_CACHE_KEY = 'cashier:property:active-url-hash';
|
||||
export const PENDING_MERCHANT_URL_KEY = 'cashier:pending-merchant-url';
|
||||
|
||||
const decodeOption = (value?: string) => {
|
||||
if (!value) return '';
|
||||
|
|
@ -39,6 +40,95 @@ export const resolveMerchantUrlFromScanResult = (raw: string) => {
|
|||
return '';
|
||||
};
|
||||
|
||||
export const setPendingMerchantUrl = (merchantUrl: string) => {
|
||||
if (!merchantUrl) return;
|
||||
encryptedStorage.set(PENDING_MERCHANT_URL_KEY, merchantUrl);
|
||||
// @ts-ignore
|
||||
if (uni.$globalData) {
|
||||
// @ts-ignore
|
||||
uni.$globalData.pendingMerchantUrl = merchantUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPendingMerchantUrl = () => {
|
||||
// @ts-ignore
|
||||
const fromMemory = uni.$globalData?.pendingMerchantUrl;
|
||||
if (fromMemory) return String(fromMemory);
|
||||
|
||||
return encryptedStorage.get<string>(PENDING_MERCHANT_URL_KEY) || '';
|
||||
};
|
||||
|
||||
export const clearPendingMerchantUrl = () => {
|
||||
encryptedStorage.remove(PENDING_MERCHANT_URL_KEY);
|
||||
// @ts-ignore
|
||||
if (uni.$globalData) {
|
||||
// @ts-ignore
|
||||
uni.$globalData.pendingMerchantUrl = '';
|
||||
}
|
||||
};
|
||||
|
||||
const resolveMerchantUrlFromRawEntry = (raw?: string) => {
|
||||
if (!raw) return '';
|
||||
return resolveMerchantUrlFromScanResult(decodeOption(raw));
|
||||
};
|
||||
|
||||
const getLaunchQuery = () => {
|
||||
try {
|
||||
const launchOptions = uni.getLaunchOptionsSync?.();
|
||||
return (launchOptions?.query || {}) as Record<string, string>;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/** App.onLaunch / App.onShow:捕获支付宝普通二维码 qrCode */
|
||||
export const captureMerchantUrlFromLaunchOptions = (options: Record<string, any> = {}) => {
|
||||
const query = options.query || {};
|
||||
const qrCode = query.qrCode || query.qr_code || '';
|
||||
if (!qrCode) return '';
|
||||
|
||||
const merchantUrl = resolveMerchantUrlFromRawEntry(String(qrCode));
|
||||
if (!merchantUrl) return '';
|
||||
|
||||
setPendingMerchantUrl(merchantUrl);
|
||||
return merchantUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一解析进页 merchantUrl:
|
||||
* 1. ?url= / ?qr_url=(开发调试、内部跳转)
|
||||
* 2. 微信普通二维码 options.q
|
||||
* 3. 支付宝 query.qrCode(页面参数 / getLaunchOptionsSync)
|
||||
* 4. App 缓存 pendingMerchantUrl
|
||||
*/
|
||||
export const resolveMerchantUrlFromEntry = (options: Record<string, any> = {}) => {
|
||||
const direct = resolveMerchantUrlFromOptions(options as Record<string, string>);
|
||||
if (direct) {
|
||||
const merchantUrl = resolveMerchantUrlFromScanResult(direct);
|
||||
if (merchantUrl) return merchantUrl;
|
||||
}
|
||||
|
||||
if (options.q) {
|
||||
const fromQ = resolveMerchantUrlFromRawEntry(String(options.q));
|
||||
if (fromQ) return fromQ;
|
||||
}
|
||||
|
||||
const pageQrCode = options.qrCode || options.qr_code;
|
||||
if (pageQrCode) {
|
||||
const fromPageQr = resolveMerchantUrlFromRawEntry(String(pageQrCode));
|
||||
if (fromPageQr) return fromPageQr;
|
||||
}
|
||||
|
||||
const launchQuery = getLaunchQuery();
|
||||
const launchQrCode = launchQuery.qrCode || launchQuery.qr_code;
|
||||
if (launchQrCode) {
|
||||
const fromLaunch = resolveMerchantUrlFromRawEntry(String(launchQrCode));
|
||||
if (fromLaunch) return fromLaunch;
|
||||
}
|
||||
|
||||
return getPendingMerchantUrl();
|
||||
};
|
||||
|
||||
export const getMerchantUrlHash = (merchantUrl: string) => {
|
||||
return CryptoJS.SHA256(merchantUrl).toString();
|
||||
};
|
||||
|
|
@ -74,3 +164,8 @@ export const syncActiveMerchantContext = (merchantUrl: string) => {
|
|||
};
|
||||
|
||||
export const getActiveMerchantUrl = () => encryptedStorage.get<string>(ACTIVE_QR_URL_CACHE_KEY) || '';
|
||||
|
||||
export const clearActiveMerchantContext = () => {
|
||||
encryptedStorage.remove(ACTIVE_QR_URL_CACHE_KEY);
|
||||
encryptedStorage.remove(ACTIVE_QR_HASH_CACHE_KEY);
|
||||
};
|
||||
|
|
|
|||
112
utils/wxpay.ts
112
utils/wxpay.ts
|
|
@ -1,3 +1,5 @@
|
|||
import { resolveLoginPlatform } from '@/api/auth';
|
||||
|
||||
export interface WxPayParams {
|
||||
timeStamp: string;
|
||||
nonceStr: string;
|
||||
|
|
@ -6,6 +8,47 @@ export interface WxPayParams {
|
|||
paySign: string;
|
||||
}
|
||||
|
||||
export const extractWxPayParams = (data: Record<string, any> = {}): WxPayParams => ({
|
||||
timeStamp: String(data.timeStamp || data.time_stamp || data.timestamp || ''),
|
||||
nonceStr: data.nonceStr || data.nonce_str || '',
|
||||
package: data.package || data.pack || '',
|
||||
signType: data.signType || data.sign_type || 'RSA',
|
||||
paySign: data.paySign || data.pay_sign || ''
|
||||
});
|
||||
|
||||
/** 支付宝小程序 tradeNO,后端 jspay 通常放在 prepay_id 字段 */
|
||||
export const extractAlipayTradeNo = (data: Record<string, any> = {}) =>
|
||||
String(data.prepay_id || data.tradeNO || '');
|
||||
|
||||
const ALIPAY_RESULT_MESSAGES: Record<string, string> = {
|
||||
'4000': '订单处理失败,请确认 tradeNO 与 buyer_id 是否正确',
|
||||
'6001': '已取消支付',
|
||||
'6002': '网络连接出错,请稍后重试',
|
||||
'6004': '支付结果未知,请在订单页查询',
|
||||
'8000': '支付处理中,请在订单页查询'
|
||||
};
|
||||
|
||||
export const formatPaymentError = (error: unknown) => {
|
||||
const err = error as any;
|
||||
if (err?.message && !/^支付失败$/.test(String(err.message))) {
|
||||
return String(err.message);
|
||||
}
|
||||
|
||||
const memo = err?.memo || err?.errMsg || err?.errorMessage || '';
|
||||
if (memo && !/^requestPayment:fail/.test(String(memo))) {
|
||||
return String(memo);
|
||||
}
|
||||
|
||||
const resultCode = String(err?.resultCode || '');
|
||||
if (resultCode && ALIPAY_RESULT_MESSAGES[resultCode]) {
|
||||
return ALIPAY_RESULT_MESSAGES[resultCode];
|
||||
}
|
||||
|
||||
if (memo) return String(memo);
|
||||
|
||||
return '支付失败';
|
||||
};
|
||||
|
||||
export const requestWxPayment = (params: WxPayParams) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
uni.requestPayment({
|
||||
|
|
@ -21,7 +64,74 @@ export const requestWxPayment = (params: WxPayParams) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const requestAlipayPayment = (tradeNO: string) => {
|
||||
console.log('[收银台][支付宝支付] 调起 tradeNO', tradeNO);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// #ifdef MP-ALIPAY
|
||||
if (typeof my !== 'undefined' && typeof my.tradePay === 'function') {
|
||||
my.tradePay({
|
||||
tradeNO,
|
||||
success: (res: any) => {
|
||||
console.log('[收银台][支付宝支付] success', res);
|
||||
const resultCode = String(res?.resultCode || '');
|
||||
if (resultCode === '9000' || resultCode === '8000' || resultCode === '6004') {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (resultCode === '6001') {
|
||||
reject({ resultCode, errMsg: 'requestPayment:cancel', memo: '用户取消支付' });
|
||||
return;
|
||||
}
|
||||
reject({
|
||||
resultCode,
|
||||
memo: res?.memo || ALIPAY_RESULT_MESSAGES[resultCode] || '支付失败'
|
||||
});
|
||||
},
|
||||
fail: (error: any) => {
|
||||
console.log('[收银台][支付宝支付] fail', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
|
||||
uni.requestPayment({
|
||||
provider: 'alipay',
|
||||
orderInfo: tradeNO,
|
||||
success: (res) => {
|
||||
console.log('[收银台][支付宝支付] uni.requestPayment success', res);
|
||||
resolve();
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log('[收银台][支付宝支付] uni.requestPayment fail', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/** 按当前端解析 jspay / cash.trade 返回并调起支付 */
|
||||
export const requestPlatformPayment = async (data: Record<string, any> = {}) => {
|
||||
if (resolveLoginPlatform() === 'mp-alipay') {
|
||||
const tradeNO = extractAlipayTradeNo(data);
|
||||
if (!tradeNO) {
|
||||
throw new Error('支付参数不完整');
|
||||
}
|
||||
await requestAlipayPayment(tradeNO);
|
||||
return;
|
||||
}
|
||||
|
||||
const params = extractWxPayParams(data);
|
||||
if (!params.paySign) {
|
||||
throw new Error('支付参数不完整');
|
||||
}
|
||||
await requestWxPayment(params);
|
||||
};
|
||||
|
||||
export const isPaymentCancelled = (error: unknown) => {
|
||||
const message = String((error as any)?.errMsg || '');
|
||||
return message.includes('cancel');
|
||||
const resultCode = String((error as any)?.resultCode || '');
|
||||
return message.includes('cancel') || resultCode === '6001';
|
||||
};
|
||||
|
|
|
|||
140
vite.config.ts
140
vite.config.ts
|
|
@ -1,77 +1,127 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { defineConfig, transformWithEsbuild, type Plugin } from 'vite';
|
||||
import uni from '@dcloudio/vite-plugin-uni';
|
||||
|
||||
/** 此路径根据自己项目路径修改;默认为 uniapp 插件市场导入路径;*/
|
||||
import viteVueUnocss, { unocss, flex, pseudo, border } from './unocss/a-hua-unocss';
|
||||
|
||||
export default defineConfig({
|
||||
const isAlipay = () => process.env.UNI_PLATFORM === 'mp-alipay';
|
||||
|
||||
/**
|
||||
* 支付宝旧 Babel(CE1000)对箭头函数 / 模板字符串 / ?? / ?. / catch{} 等解析不稳。
|
||||
* 仅 mp-alipay 构建时把产物降到 es2015(保留 const/let;esbuild 不支持 const→es5)。
|
||||
*/
|
||||
const ALIPAY_ESBUILD_OPTIONS = {
|
||||
loader: 'js' as const,
|
||||
target: 'es2015',
|
||||
sourcemap: false as const,
|
||||
supported: {
|
||||
arrow: false,
|
||||
'template-literal': false,
|
||||
'optional-chain': false,
|
||||
'nullish-coalescing': false,
|
||||
'optional-catch-binding': false,
|
||||
'object-rest-spread': false,
|
||||
'class-field': false
|
||||
}
|
||||
};
|
||||
|
||||
const needsAlipayTransform = (source: string) =>
|
||||
source.includes('=>') ||
|
||||
source.includes('`') ||
|
||||
source.includes('?.') ||
|
||||
source.includes('??') ||
|
||||
/catch\s*\{/.test(source);
|
||||
|
||||
const walkJsFiles = (dir: string, files: string[] = []) => {
|
||||
if (!fs.existsSync(dir)) return files;
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const fullPath = path.join(dir, name);
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
walkJsFiles(fullPath, files);
|
||||
} else if (name.endsWith('.js')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
};
|
||||
|
||||
const transformAlipayOutputDir = async (root: string) => {
|
||||
const files = walkJsFiles(root);
|
||||
let changed = 0;
|
||||
for (const file of files) {
|
||||
const source = fs.readFileSync(file, 'utf8');
|
||||
if (!needsAlipayTransform(source)) continue;
|
||||
try {
|
||||
const result = await transformWithEsbuild(source, file, ALIPAY_ESBUILD_OPTIONS);
|
||||
if (result.code && result.code !== source) {
|
||||
fs.writeFileSync(file, result.code, 'utf8');
|
||||
changed += 1;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn('[alipay-es-compat] skip', file, error?.message || error);
|
||||
}
|
||||
}
|
||||
console.log(`[alipay-es-compat] ${root} transformed ${changed} file(s)`);
|
||||
};
|
||||
|
||||
function alipayEsCompatPlugin(): Plugin {
|
||||
return {
|
||||
name: 'cashier-alipay-es-compat',
|
||||
apply: 'build',
|
||||
enforce: 'post',
|
||||
async renderChunk(code) {
|
||||
if (!isAlipay() || !needsAlipayTransform(code)) return null;
|
||||
const result = await transformWithEsbuild(code, 'chunk.js', ALIPAY_ESBUILD_OPTIONS);
|
||||
return { code: result.code, map: null };
|
||||
},
|
||||
async closeBundle() {
|
||||
if (!isAlipay()) return;
|
||||
const roots = [
|
||||
path.resolve(__dirname, 'unpackage/dist/dev/mp-alipay'),
|
||||
path.resolve(__dirname, 'unpackage/dist/build/mp-alipay')
|
||||
];
|
||||
for (const root of roots) {
|
||||
await transformAlipayOutputDir(root);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
build: isAlipay()
|
||||
? {
|
||||
target: 'es2015',
|
||||
cssTarget: 'chrome61'
|
||||
}
|
||||
: undefined,
|
||||
plugins: [
|
||||
uni(),
|
||||
alipayEsCompatPlugin(),
|
||||
viteVueUnocss({
|
||||
/** 预设数组;默认[unocss()] */
|
||||
presets: [
|
||||
/**
|
||||
* 默认预设;
|
||||
* text-24、uno-text-24、xx-text-24...
|
||||
*/
|
||||
unocss(),
|
||||
/**
|
||||
* 弹性盒速写;
|
||||
* flex-center、flex-col-center、flex-row-center...
|
||||
*/
|
||||
flex(),
|
||||
/**
|
||||
* 伪类、伪元素预设;
|
||||
* after:text-24 after:(text-24 text-white)...
|
||||
*/
|
||||
pseudo(),
|
||||
/**
|
||||
* 边框速写;
|
||||
* border-1 => border: 1px solid currentColor
|
||||
* border-1-red => border: 1px solid red
|
||||
* border-red => border: 1px solid red
|
||||
* ...
|
||||
* 默认配置如下;
|
||||
*/
|
||||
border({
|
||||
width: 1,
|
||||
style: 'solid',
|
||||
color: 'currentColor'
|
||||
}),
|
||||
})
|
||||
],
|
||||
/**
|
||||
* 自定义原子化 CSS 前缀;
|
||||
* 默认配置如下;
|
||||
*/
|
||||
prefix: ['li'],
|
||||
/**
|
||||
* 排出检测原子化 CSS 规则;
|
||||
* 默认配置如下;
|
||||
*/
|
||||
exclude: ['node_modules', 'uni_modules'],
|
||||
/** 主题配置 */
|
||||
theme: {
|
||||
/**
|
||||
* 是否读取项目根目录 uni.scss 文件内容生成颜色预设;
|
||||
* 默认 false
|
||||
*/
|
||||
generator: true,
|
||||
|
||||
/** 自定义颜色预设 */
|
||||
colors: {
|
||||
/** text-very-cool */
|
||||
veryCool: '#37A5FF',
|
||||
brand: {
|
||||
/** bg-brand-primary */
|
||||
primary: '#37A5FF',
|
||||
/** bg-brand */
|
||||
DEFAULT: '#942192'
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
Loading…
Reference in New Issue