This commit is contained in:
parent
0ac3fd5645
commit
be81b63b0e
11
App.vue
11
App.vue
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onLaunch, onShow } from '@dcloudio/uni-app'
|
import { onLaunch, onShow } from '@dcloudio/uni-app'
|
||||||
import { APP_CONFIG, DEFAULT_MERCHANT } from '@/config'
|
import { APP_CONFIG, DEFAULT_MERCHANT } from '@/config'
|
||||||
|
import { captureMerchantUrlFromLaunchOptions } from '@/utils/merchant-context'
|
||||||
import { VersionUpdate } from '@/utils/common'
|
import { VersionUpdate } from '@/utils/common'
|
||||||
|
|
||||||
// 定义全局数据
|
// 定义全局数据
|
||||||
|
|
@ -10,16 +11,18 @@
|
||||||
appid: APP_CONFIG.APP_ID,
|
appid: APP_CONFIG.APP_ID,
|
||||||
appId: APP_CONFIG.APP_ID,
|
appId: APP_CONFIG.APP_ID,
|
||||||
title: '卓享汇收银台',
|
title: '卓享汇收银台',
|
||||||
defaultMerchant: DEFAULT_MERCHANT
|
defaultMerchant: DEFAULT_MERCHANT,
|
||||||
|
pendingMerchantUrl: ''
|
||||||
}
|
}
|
||||||
// 使用 uni 对象来存储全局数据
|
// 使用 uni 对象来存储全局数据
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
uni.$globalData = $globalData
|
uni.$globalData = $globalData
|
||||||
|
|
||||||
onLaunch(() => {
|
onLaunch((options) => {
|
||||||
console.log('Cashier App Launch')
|
captureMerchantUrlFromLaunchOptions(options || {})
|
||||||
})
|
})
|
||||||
onShow(() => {
|
onShow((options) => {
|
||||||
|
captureMerchantUrlFromLaunchOptions(options || {})
|
||||||
// 检测小程序热更新
|
// 检测小程序热更新
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
VersionUpdate()
|
VersionUpdate()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
## 页面
|
## 页面
|
||||||
|
|
||||||
- `pages/cashier/index`:收银台首页,支持动态商家名称、顶部背景图和物业费公积金比例。
|
- `pages/pay/pay`:收银台首页,支持动态商家名称、顶部背景图和物业费公积金比例。
|
||||||
|
- `pages/pay/checkout`:半屏确认支付页。
|
||||||
|
- `pages/cashier/result`:扫码支付结果页。
|
||||||
- `pages/records/index`:付款记录占位页。
|
- `pages/records/index`:付款记录占位页。
|
||||||
- `pages/house/bind`:绑定房源占位页。
|
- `pages/house/bind`:绑定房源占位页。
|
||||||
|
|
||||||
|
|
@ -13,7 +15,7 @@
|
||||||
收银台首页支持通过页面参数临时覆盖商家信息:
|
收银台首页支持通过页面参数临时覆盖商家信息:
|
||||||
|
|
||||||
```text
|
```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` 中的默认商家配置。
|
如果传入 `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>({
|
const response = await httpRequest<LoginResponse>({
|
||||||
url: `${APP_CONFIG.API_BASE_URL}/user/v3/login.code`,
|
url: `${APP_CONFIG.API_BASE_URL}/user/v3/login.code`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: toFormUrlEncoded({
|
data: toFormUrlEncoded({
|
||||||
code,
|
code,
|
||||||
app_id: APP_CONFIG.APP_ID
|
app_id: APP_CONFIG.APP_ID,
|
||||||
|
platform: resolveLoginPlatform()
|
||||||
}),
|
}),
|
||||||
header: {
|
header: {
|
||||||
'content-type': 'application/x-www-form-urlencoded'
|
'content-type': 'application/x-www-form-urlencoded'
|
||||||
|
|
@ -29,3 +49,34 @@ export const loginByWechatCode = async (code: string) => {
|
||||||
|
|
||||||
return response.data;
|
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 { get, post, postForm } from '../utils/request';
|
||||||
|
import { resolveLoginPlatform } from './auth';
|
||||||
|
|
||||||
export interface MerchantConfigQuery {
|
export interface MerchantConfigQuery {
|
||||||
merchant_id?: string;
|
merchant_id?: string;
|
||||||
|
|
@ -161,9 +162,9 @@ export const cashierRecordList = (data: any = {}) => get('v1/cashier/records', d
|
||||||
|
|
||||||
export const tradeList = (data: TradeListQuery = {}) =>
|
export const tradeList = (data: TradeListQuery = {}) =>
|
||||||
get<ApiResult<TradeListItem[]>>('user/v1/trade.list', {
|
get<ApiResult<TradeListItem[]>>('user/v1/trade.list', {
|
||||||
trade_no: data.trade_no ?? '',
|
trade_no: data.trade_no != null ? data.trade_no : '',
|
||||||
page: data.page ?? 1,
|
page: data.page != null ? data.page : 1,
|
||||||
limit: data.limit ?? 10
|
limit: data.limit != null ? data.limit : 10
|
||||||
});
|
});
|
||||||
|
|
||||||
export const bindMerchantByUrl = (url: string) =>
|
export const bindMerchantByUrl = (url: string) =>
|
||||||
|
|
@ -188,8 +189,8 @@ export const platformScore = () => get<ApiResult<PlatformScoreResult>>('user/v1/
|
||||||
|
|
||||||
export const platformScoreRecord = (data: ScoreRecordQuery = {}) =>
|
export const platformScoreRecord = (data: ScoreRecordQuery = {}) =>
|
||||||
get<ApiResult<ScoreRecordItem[]>>('user/v1/platform.score.record', {
|
get<ApiResult<ScoreRecordItem[]>>('user/v1/platform.score.record', {
|
||||||
page: data.page ?? 1,
|
page: data.page != null ? data.page : 1,
|
||||||
limit: data.limit ?? 10
|
limit: data.limit != null ? data.limit : 10
|
||||||
});
|
});
|
||||||
|
|
||||||
export const cashTradeInfo = (tradeNo: string) =>
|
export const cashTradeInfo = (tradeNo: string) =>
|
||||||
|
|
@ -198,6 +199,6 @@ export const cashTradeInfo = (tradeNo: string) =>
|
||||||
export const cashTradePay = (data: CashTradePayPayload) =>
|
export const cashTradePay = (data: CashTradePayPayload) =>
|
||||||
post<ApiResult<CashTradePayResult>>('user/v1/cash.trade', {
|
post<ApiResult<CashTradePayResult>>('user/v1/cash.trade', {
|
||||||
trade_no: data.trade_no,
|
trade_no: data.trade_no,
|
||||||
platform: data.platform ?? 'mp-weixin',
|
platform: data.platform != null ? data.platform : resolveLoginPlatform(),
|
||||||
remark: data.remark ?? ''
|
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 = {
|
export const APP_CONFIG = {
|
||||||
API_BASE_URL: 'https://dev.api.leapy.cn',
|
API_BASE_URL: 'https://dev.api.leapy.cn',
|
||||||
APP_ID: '123018',
|
APP_ID,
|
||||||
RESOURCE_URL: 'https://resource.leapy.cn/staff/',
|
RESOURCE_URL: 'https://resource.leapy.cn/staff/',
|
||||||
PROPERTY_IMG_URL: 'https://resource.leapy.cn/',
|
PROPERTY_IMG_URL: 'https://resource.leapy.cn/',
|
||||||
STORAGE_TOKEN_KEY: 'cashier:auth:token',
|
STORAGE_TOKEN_KEY: 'cashier:auth:token',
|
||||||
// 小程序代码可被反编译,密钥仅用于避免 token 明文落盘,不替代服务端的有效期与失效策略。
|
// 小程序代码可被反编译,密钥仅用于避免 token 明文落盘,不替代服务端的有效期与失效策略。
|
||||||
STORAGE_ENCRYPTION_KEY: 'cashier-storage-key-123018-v1-32'
|
STORAGE_ENCRYPTION_KEY: 'cashier-storage-key-v1-32'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PROPERTY_APP = {
|
export const PROPERTY_APP = {
|
||||||
APP_ID: 'wx42076f086669c49e',
|
APP_ID: 'wx42076f086669c49e',
|
||||||
|
/** 物业微信小程序对外名称,支付宝端引导复制用 */
|
||||||
|
WECHAT_NAME: '乐享云空间服务',
|
||||||
|
/** 物业支付宝小程序 appId,配置后支付宝端可跳转 */
|
||||||
|
ALIPAY_APP_ID: '',
|
||||||
BIND_HOUSE_PATH: 'pagesC/certification/index?from=cashier',
|
BIND_HOUSE_PATH: 'pagesC/certification/index?from=cashier',
|
||||||
MINE_PATH: 'pages/mine/mine',
|
MINE_PATH: 'pages/mine/mine',
|
||||||
WECHAT_APP_ID: 'wx02de0e1de9f4bda3'
|
WECHAT_APP_ID: 'wx02de0e1de9f4bda3'
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,14 @@
|
||||||
"minified" : true
|
"minified" : true
|
||||||
},
|
},
|
||||||
"usingComponents" : true,
|
"usingComponents" : true,
|
||||||
"navigateToMiniProgramAppIdList" : [
|
"navigateToMiniProgramAppIdList" : [ "wx42076f086669c49e" ]
|
||||||
"wx42076f086669c49e"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"mp-alipay" : {
|
"mp-alipay" : {
|
||||||
"usingComponents" : true
|
"usingComponents" : true,
|
||||||
|
"appid" : "2021006169600123",
|
||||||
|
"component2" : true,
|
||||||
|
"styleIsolation" : "shared",
|
||||||
|
"enableAppxNg" : true
|
||||||
},
|
},
|
||||||
"mp-baidu" : {
|
"mp-baidu" : {
|
||||||
"usingComponents" : true
|
"usingComponents" : true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"format": 2,
|
||||||
|
"compileOptions": {
|
||||||
|
"component2": true,
|
||||||
|
"enableAppxNg": true,
|
||||||
|
"transpile": {}
|
||||||
|
},
|
||||||
|
"developOptions": {
|
||||||
|
"skipTranspile": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/cashier/index",
|
"path": "pages/pay/pay",
|
||||||
"style": {
|
"style": {
|
||||||
"enablePullDownRefresh": false,
|
"enablePullDownRefresh": false,
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ import { onLoad } from '@dcloudio/uni-app';
|
||||||
import { queryTrade } from '@/api/cashier';
|
import { queryTrade } from '@/api/cashier';
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 1000;
|
const POLL_INTERVAL_MS = 1000;
|
||||||
const POLL_MAX_ATTEMPTS = 120;
|
const POLL_MAX_ATTEMPTS = 240;
|
||||||
|
|
||||||
type ResultStatus = 'pending' | 'success' | 'failed';
|
type ResultStatus = 'pending' | 'success' | 'failed';
|
||||||
|
|
||||||
|
|
@ -142,7 +142,7 @@ const goCashier = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
uni.reLaunch({
|
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" />
|
<image class="fund-icon-image" :src="fundIconUrl" mode="aspectFit" />
|
||||||
<view class="fund-text">
|
<view class="fund-text">
|
||||||
<text class="fund-title">物业基金</text>
|
<text class="fund-title">物业基金</text>
|
||||||
<text class="fund-desc">查看我的物业基金余额</text>
|
<text class="fund-desc">{{ propertyFundDesc }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="fund-action">
|
<view class="fund-action">
|
||||||
|
|
@ -36,11 +36,17 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { onPullDownRefresh, onShow } from '@dcloudio/uni-app';
|
import { onPullDownRefresh, onShow } from '@dcloudio/uni-app';
|
||||||
import { platformScore } from '@/api/cashier';
|
import { platformScore } from '@/api/cashier';
|
||||||
|
import { resolveLoginPlatform } from '@/api/auth';
|
||||||
import { APP_CONFIG, PROPERTY_APP } from '@/config';
|
import { APP_CONFIG, PROPERTY_APP } from '@/config';
|
||||||
|
|
||||||
|
const IS_ALIPAY = resolveLoginPlatform() === 'mp-alipay';
|
||||||
|
|
||||||
const platformScoreText = ref('0.00');
|
const platformScoreText = ref('0.00');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const fundIconUrl = `${APP_CONFIG.PROPERTY_IMG_URL}property/new/cashier/fangwu.png`;
|
const fundIconUrl = `${APP_CONFIG.PROPERTY_IMG_URL}property/new/cashier/fangwu.png`;
|
||||||
|
const propertyFundDesc = IS_ALIPAY
|
||||||
|
? `请前往微信「${PROPERTY_APP.WECHAT_NAME}」查看`
|
||||||
|
: '查看我的物业基金余额';
|
||||||
|
|
||||||
const loadPlatformScore = async () => {
|
const loadPlatformScore = async () => {
|
||||||
const response = await platformScore();
|
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 = () => {
|
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({
|
uni.navigateToMiniProgram({
|
||||||
appId: PROPERTY_APP.APP_ID,
|
appId: PROPERTY_APP.APP_ID,
|
||||||
path: PROPERTY_APP.MINE_PATH,
|
path: PROPERTY_APP.MINE_PATH,
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ const formatMoney = (value?: string | number) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatRecordType = (item: ScoreRecordItem) => {
|
const formatRecordType = (item: ScoreRecordItem) => {
|
||||||
const type = String(item.type ?? '');
|
const type = String(item.type != null ? item.type : '');
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
'1': '积分收入',
|
'1': '积分收入',
|
||||||
'2': '积分支出',
|
'2': '积分支出',
|
||||||
|
|
@ -61,7 +61,7 @@ const formatRecordType = (item: ScoreRecordItem) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatScoreChange = (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);
|
const amount = Number(change || 0);
|
||||||
if (!Number.isFinite(amount)) return '0.00';
|
if (!Number.isFinite(amount)) return '0.00';
|
||||||
const prefix = amount > 0 ? '+' : '';
|
const prefix = amount > 0 ? '+' : '';
|
||||||
|
|
@ -69,11 +69,13 @@ const formatScoreChange = (item: ScoreRecordItem) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const scoreClass = (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';
|
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) => {
|
const fetchRecords = async (reset = false) => {
|
||||||
if (loading.value) return;
|
if (loading.value) return;
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,12 @@
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { onLoad, onUnload } from '@dcloudio/uni-app';
|
import { onLoad, onUnload } from '@dcloudio/uni-app';
|
||||||
import { cancelTrade, cashTradeInfo, cashTradePay, queryTrade } from '@/api/cashier';
|
import { cancelTrade, cashTradeInfo, cashTradePay, queryTrade } from '@/api/cashier';
|
||||||
|
import { resolveLoginPlatform } from '@/api/auth';
|
||||||
import { isCheckoutClosing, returnToHostMiniProgram, resetCheckoutClosing } from '@/utils/checkout-return';
|
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_INTERVAL_MS = 1000;
|
||||||
const POLL_MAX_ATTEMPTS = 120;
|
const POLL_MAX_ATTEMPTS = 240;
|
||||||
|
|
||||||
const tradeNo = ref('');
|
const tradeNo = ref('');
|
||||||
const subjectText = ref('');
|
const subjectText = ref('');
|
||||||
|
|
@ -249,7 +250,7 @@ const handlePay = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await cashTradePay({
|
const response = await cashTradePay({
|
||||||
trade_no: tradeNo.value,
|
trade_no: tradeNo.value,
|
||||||
platform: 'mp-weixin',
|
platform: resolveLoginPlatform(),
|
||||||
remark: remark.value.trim()
|
remark: remark.value.trim()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -263,20 +264,9 @@ const handlePay = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = response.data || {};
|
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 {
|
try {
|
||||||
await requestWxPayment(params);
|
await requestPlatformPayment(payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isPaymentCancelled(error)) {
|
if (isPaymentCancelled(error)) {
|
||||||
await finishCancelledAndReturn();
|
await finishCancelledAndReturn();
|
||||||
|
|
@ -295,7 +285,7 @@ const handlePay = async () => {
|
||||||
|
|
||||||
await backToHost('success', 0);
|
await backToHost('success', 0);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message = error?.message || '支付失败';
|
const message = formatPaymentError(error);
|
||||||
if (isTradeNotPayable(message)) {
|
if (isTradeNotPayable(message)) {
|
||||||
await finishCancelledAndReturn();
|
await finishCancelledAndReturn();
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,43 @@
|
||||||
:disabled="paying"
|
:disabled="paying"
|
||||||
/>
|
/>
|
||||||
</view>
|
</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
|
<view
|
||||||
class="property-row"
|
class="property-row"
|
||||||
hover-class="property-row-hover"
|
hover-class="property-row-hover"
|
||||||
|
|
@ -79,8 +116,9 @@
|
||||||
>
|
>
|
||||||
{{ propertyRowText }}
|
{{ propertyRowText }}
|
||||||
</text>
|
</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>
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
@ -106,7 +144,7 @@
|
||||||
></text>
|
></text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-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 class="ri-add-line property-sheet-footer-icon"></text>
|
||||||
<text>绑定新房源</text>
|
<text>绑定新房源</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
@ -151,6 +189,7 @@ import { computed, ref } from 'vue';
|
||||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||||
import { bindMerchantByUrl, cancelTrade, changeMerchant, merchantListByUrl, supplierByUrl } from '@/api/cashier';
|
import { bindMerchantByUrl, cancelTrade, changeMerchant, merchantListByUrl, supplierByUrl } from '@/api/cashier';
|
||||||
import type { MerchantListItem } from '@/api/cashier';
|
import type { MerchantListItem } from '@/api/cashier';
|
||||||
|
import { mobileAuthBind, resolveLoginPlatform } from '@/api/auth';
|
||||||
import { postForm } from '@/utils/request';
|
import { postForm } from '@/utils/request';
|
||||||
import { DEFAULT_MERCHANT, APP_CONFIG, PROPERTY_APP } from '@/config';
|
import { DEFAULT_MERCHANT, APP_CONFIG, PROPERTY_APP } from '@/config';
|
||||||
import {
|
import {
|
||||||
|
|
@ -158,12 +197,14 @@ import {
|
||||||
getPropertyBindingCacheKeyByHash,
|
getPropertyBindingCacheKeyByHash,
|
||||||
getSupplierCacheKey,
|
getSupplierCacheKey,
|
||||||
getSupplierCacheKeyByHash,
|
getSupplierCacheKeyByHash,
|
||||||
resolveMerchantUrlFromOptions,
|
clearPendingMerchantUrl,
|
||||||
|
clearActiveMerchantContext,
|
||||||
|
resolveMerchantUrlFromEntry,
|
||||||
resolveMerchantUrlFromScanResult,
|
resolveMerchantUrlFromScanResult,
|
||||||
syncActiveMerchantContext
|
syncActiveMerchantContext
|
||||||
} from '@/utils/merchant-context';
|
} from '@/utils/merchant-context';
|
||||||
import { encryptedStorage } from '@/utils/storage';
|
import { encryptedStorage } from '@/utils/storage';
|
||||||
import { isPaymentCancelled, requestWxPayment } from '@/utils/wxpay';
|
import { formatPaymentError, isPaymentCancelled, requestPlatformPayment } from '@/utils/wxpay';
|
||||||
|
|
||||||
interface MerchantInfo {
|
interface MerchantInfo {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -177,6 +218,7 @@ const PROPERTY_CACHE_TTL_SECONDS = 10 * 60;
|
||||||
const SUPPLIER_CACHE_TTL_SECONDS = 10 * 60;
|
const SUPPLIER_CACHE_TTL_SECONDS = 10 * 60;
|
||||||
const emptyStateBackground = '/static/cashier/bg-image.png';
|
const emptyStateBackground = '/static/cashier/bg-image.png';
|
||||||
const brandName = '卓享汇支付';
|
const brandName = '卓享汇支付';
|
||||||
|
const IS_ALIPAY = resolveLoginPlatform() === 'mp-alipay';
|
||||||
|
|
||||||
const hasMerchantUrl = ref(false);
|
const hasMerchantUrl = ref(false);
|
||||||
const merchant = ref<MerchantInfo>({ ...defaultMerchant });
|
const merchant = ref<MerchantInfo>({ ...defaultMerchant });
|
||||||
|
|
@ -190,7 +232,11 @@ const propertyMerchantId = ref('');
|
||||||
const propertyList = ref<MerchantListItem[]>([]);
|
const propertyList = ref<MerchantListItem[]>([]);
|
||||||
const propertySheetVisible = ref(false);
|
const propertySheetVisible = ref(false);
|
||||||
const switchingProperty = ref(false);
|
const switchingProperty = ref(false);
|
||||||
|
const bindingPhone = ref(false);
|
||||||
|
const alipayPropertyUnbound = ref(false);
|
||||||
const scanning = ref(false);
|
const scanning = ref(false);
|
||||||
|
/** 已判定无效的进页码,避免 onShow 用同一 launch qrCode 反复弹窗 */
|
||||||
|
const rejectedMerchantUrl = ref('');
|
||||||
const numberKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
const numberKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
|
|
||||||
const canPay = computed(() => Number(amount.value) > 0 && !paying.value);
|
const canPay = computed(() => Number(amount.value) > 0 && !paying.value);
|
||||||
|
|
@ -204,9 +250,17 @@ const benefitTitle = computed(() => `预计获得${scoreName.value || '物业公
|
||||||
const propertyRowText = computed(() => {
|
const propertyRowText = computed(() => {
|
||||||
if (propertyName.value) return propertyName.value;
|
if (propertyName.value) return propertyName.value;
|
||||||
if (propertyList.value.length) return '选择物业';
|
if (propertyList.value.length) return '选择物业';
|
||||||
|
if (IS_ALIPAY) {
|
||||||
|
if (alipayPropertyUnbound.value) return '暂未绑定,请去微信小程序绑定';
|
||||||
|
return '绑定手机号查看房源';
|
||||||
|
}
|
||||||
return '业主去绑定';
|
return '业主去绑定';
|
||||||
});
|
});
|
||||||
const isPropertyActionText = computed(() => !propertyName.value);
|
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 ? '切换物业' : '选择物业'));
|
const propertySheetTitle = computed(() => (propertyName.value ? '切换物业' : '选择物业'));
|
||||||
|
|
||||||
interface PropertyBindingCache {
|
interface PropertyBindingCache {
|
||||||
|
|
@ -234,7 +288,7 @@ const hasFeeRate = (feeRate: unknown) => feeRate !== undefined && feeRate !== nu
|
||||||
|
|
||||||
const resolveFeeRateFromPayload = (payload: Record<string, any> = {}) => {
|
const resolveFeeRateFromPayload = (payload: Record<string, any> = {}) => {
|
||||||
const supplierData = payload.supplier || {};
|
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> = {}) => {
|
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 buildSupplierCache = (payload: Record<string, any> = {}, previous?: SupplierCache | null): SupplierCache => {
|
||||||
const supplierData = payload.supplier || {};
|
const supplierData = payload.supplier || {};
|
||||||
return {
|
return {
|
||||||
supplier_id: supplierData.supplier_id ?? payload.supplier_id ?? previous?.supplier_id,
|
supplier_id:
|
||||||
supplier_name: supplierData.supplier_name ?? payload.supplier_name ?? previous?.supplier_name,
|
supplierData.supplier_id != null
|
||||||
logo: supplierData.logo ?? payload.logo ?? previous?.logo,
|
? supplierData.supplier_id
|
||||||
fee_rate: resolveFeeRateFromPayload(payload) ?? previous?.fee_rate
|
: 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);
|
return getPropertyBindingCacheKey(merchantUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initWithMerchantUrl = async (merchantUrl: string) => {
|
const resetToEmptyMerchantState = () => {
|
||||||
hasMerchantUrl.value = true;
|
hasMerchantUrl.value = false;
|
||||||
activeMerchantUrl.value = merchantUrl;
|
activeMerchantUrl.value = '';
|
||||||
|
merchant.value = { ...defaultMerchant };
|
||||||
|
amount.value = '';
|
||||||
|
remark.value = '';
|
||||||
propertyName.value = '';
|
propertyName.value = '';
|
||||||
propertyMerchantId.value = '';
|
propertyMerchantId.value = '';
|
||||||
propertyList.value = [];
|
propertyList.value = [];
|
||||||
propertySheetVisible.value = false;
|
propertySheetVisible.value = false;
|
||||||
|
alipayPropertyUnbound.value = false;
|
||||||
scoreName.value = '物业公积金';
|
scoreName.value = '物业公积金';
|
||||||
await loadPropertyBinding(merchantUrl);
|
clearPendingMerchantUrl();
|
||||||
await loadSupplier(merchantUrl);
|
clearActiveMerchantContext();
|
||||||
};
|
};
|
||||||
|
|
||||||
const promptScan = () => {
|
const promptScan = (message?: string) => {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: '未识别到商家码,请扫描商家收款码进入收银台',
|
content: message || '未识别到商家码,请扫描商家收款码进入收银台',
|
||||||
confirmText: '去扫码',
|
confirmText: '去扫码',
|
||||||
showCancel: false,
|
showCancel: false,
|
||||||
success: (res) => {
|
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 = () => {
|
const handleScan = () => {
|
||||||
if (scanning.value) return;
|
if (scanning.value) return;
|
||||||
|
|
||||||
|
|
@ -397,7 +546,7 @@ const handleScan = () => {
|
||||||
promptScan();
|
promptScan();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await initWithMerchantUrl(merchantUrl);
|
await initWithMerchantUrl(merchantUrl, { force: true });
|
||||||
},
|
},
|
||||||
fail: (err: any) => {
|
fail: (err: any) => {
|
||||||
const errMsg = err?.errMsg || '';
|
const errMsg = err?.errMsg || '';
|
||||||
|
|
@ -468,16 +617,12 @@ const loadSupplier = async (merchantUrl: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await supplierByUrl(merchantUrl);
|
const result = await fetchSupplierByUrl(merchantUrl);
|
||||||
if (Number(response?.code) !== 200 || !response?.data) return;
|
if (!result.ok) return false;
|
||||||
|
return true;
|
||||||
const payload = response.data || {};
|
|
||||||
const latestSupplier = buildSupplierCache(payload, cached);
|
|
||||||
applyPropertyInfo(resolvePropertyFromPayload(payload));
|
|
||||||
applySupplierInfo(latestSupplier);
|
|
||||||
encryptedStorage.set(cacheKey, latestSupplier, SUPPLIER_CACHE_TTL_SECONDS);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('supplier fallback cache', error);
|
console.log('supplier fallback cache', error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -506,7 +651,118 @@ const deleteKey = () => {
|
||||||
amount.value = amount.value.slice(0, -1);
|
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 = () => {
|
const goBindProperty = () => {
|
||||||
|
if (IS_ALIPAY) {
|
||||||
|
showAlipayUnboundTip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (paying.value) return;
|
if (paying.value) return;
|
||||||
propertySheetVisible.value = false;
|
propertySheetVisible.value = false;
|
||||||
|
|
||||||
|
|
@ -543,6 +799,12 @@ const openPropertySheet = async () => {
|
||||||
|
|
||||||
const list = await fetchMerchantList(activeMerchantUrl.value);
|
const list = await fetchMerchantList(activeMerchantUrl.value);
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
|
if (IS_ALIPAY) {
|
||||||
|
if (alipayPropertyUnbound.value) {
|
||||||
|
showAlipayUnboundTip();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
goBindProperty();
|
goBindProperty();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -552,9 +814,18 @@ const openPropertySheet = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePropertyRow = async () => {
|
const handlePropertyRow = async () => {
|
||||||
if (paying.value || switchingProperty.value) return;
|
if (paying.value || switchingProperty.value || bindingPhone.value) return;
|
||||||
if (!activeMerchantUrl.value) return;
|
if (!activeMerchantUrl.value) return;
|
||||||
|
|
||||||
|
if (IS_ALIPAY && alipayPropertyUnbound.value) {
|
||||||
|
showAlipayUnboundTip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IS_ALIPAY && showAlipayPhoneAuthBtn.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!propertyName.value) {
|
if (!propertyName.value) {
|
||||||
try {
|
try {
|
||||||
if (propertyList.value.length) {
|
if (propertyList.value.length) {
|
||||||
|
|
@ -612,7 +883,9 @@ const switchToProperty = async (
|
||||||
},
|
},
|
||||||
PROPERTY_CACHE_TTL_SECONDS
|
PROPERTY_CACHE_TTL_SECONDS
|
||||||
);
|
);
|
||||||
await loadPropertyBinding(merchantUrl, { skipFallback: options.skipBindingFallback ?? true });
|
await loadPropertyBinding(merchantUrl, {
|
||||||
|
skipFallback: options.skipBindingFallback != null ? options.skipBindingFallback : true
|
||||||
|
});
|
||||||
await loadSupplier(merchantUrl);
|
await loadSupplier(merchantUrl);
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -652,10 +925,10 @@ const handlePay = async () => {
|
||||||
throw new Error('缺少支付参数:商户码 url');
|
throw new Error('缺少支付参数:商户码 url');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 扫商户码支付:直接调用后端 jspay,拿到微信 requestPayment 所需签名参数
|
// 扫商户码支付:调用后端 jspay,按当前端传入 platform
|
||||||
const payload = {
|
const payload = {
|
||||||
amount: payableAmount.value,
|
amount: payableAmount.value,
|
||||||
platform: 'mp-weixin',
|
platform: resolveLoginPlatform(),
|
||||||
remark: remark.value.trim(),
|
remark: remark.value.trim(),
|
||||||
url: activeMerchantUrl.value
|
url: activeMerchantUrl.value
|
||||||
};
|
};
|
||||||
|
|
@ -665,26 +938,14 @@ const handlePay = async () => {
|
||||||
throw new Error(payRes?.msg || '获取支付参数失败');
|
throw new Error(payRes?.msg || '获取支付参数失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
const wechat = payRes?.data || {};
|
const payData = payRes?.data || {};
|
||||||
const params: any = {
|
const tradeNo = payData.trade_no || payData.tradeNo || '';
|
||||||
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 || '';
|
|
||||||
if (!tradeNo) {
|
if (!tradeNo) {
|
||||||
throw new Error('缺少交易单号');
|
throw new Error('缺少交易单号');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await requestWxPayment(params);
|
await requestPlatformPayment(payData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isPaymentCancelled(error)) {
|
if (isPaymentCancelled(error)) {
|
||||||
await cancelTrade(tradeNo).catch(() => {});
|
await cancelTrade(tradeNo).catch(() => {});
|
||||||
|
|
@ -698,7 +959,7 @@ const handlePay = async () => {
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: error?.message || '支付失败',
|
title: formatPaymentError(error),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -707,15 +968,25 @@ const handlePay = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
onLoad((options: any) => {
|
onLoad((options: any) => {
|
||||||
const merchantUrl = resolveMerchantUrlFromOptions(options || {});
|
const merchantUrl = resolveMerchantUrlFromEntry(options || {});
|
||||||
if (!merchantUrl) {
|
if (!merchantUrl) {
|
||||||
handleMissingMerchantUrl();
|
handleMissingMerchantUrl();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
clearPendingMerchantUrl();
|
||||||
initWithMerchantUrl(merchantUrl);
|
initWithMerchantUrl(merchantUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
|
if (!hasMerchantUrl.value || !activeMerchantUrl.value) {
|
||||||
|
const merchantUrl = resolveMerchantUrlFromEntry({});
|
||||||
|
if (merchantUrl && merchantUrl !== rejectedMerchantUrl.value) {
|
||||||
|
clearPendingMerchantUrl();
|
||||||
|
initWithMerchantUrl(merchantUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasMerchantUrl.value || !activeMerchantUrl.value) return;
|
if (!hasMerchantUrl.value || !activeMerchantUrl.value) return;
|
||||||
loadPropertyBinding(activeMerchantUrl.value);
|
loadPropertyBinding(activeMerchantUrl.value);
|
||||||
loadSupplier(activeMerchantUrl.value);
|
loadSupplier(activeMerchantUrl.value);
|
||||||
|
|
@ -1037,6 +1308,7 @@ onShow(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-row {
|
.property-row {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
@ -1044,6 +1316,27 @@ onShow(() => {
|
||||||
padding: 12rpx 4rpx;
|
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 {
|
.property-row-hover {
|
||||||
opacity: 0.88;
|
opacity: 0.88;
|
||||||
}
|
}
|
||||||
|
|
@ -1060,6 +1353,11 @@ onShow(() => {
|
||||||
color: #1F5BD8;
|
color: #1F5BD8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.property-name-tip {
|
||||||
|
color: #8B929E;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.property-arrow {
|
.property-arrow {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: 4rpx;
|
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';
|
import { authStorage } from '@/utils/storage';
|
||||||
|
|
||||||
let loginPromise: Promise<string> | null = null;
|
let loginPromise: Promise<string> | null = null;
|
||||||
|
|
||||||
const getWechatCode = () => {
|
const getLoginCode = () => {
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
uni.login({
|
uni.login({
|
||||||
|
// #ifdef MP-ALIPAY
|
||||||
|
provider: 'alipay',
|
||||||
|
scopes: 'auth_base',
|
||||||
|
// #endif
|
||||||
success: (result) => {
|
success: (result) => {
|
||||||
if (!result.code) {
|
const code = result.code || (result as any).authCode;
|
||||||
reject(new Error('微信登录凭证获取失败'));
|
if (!code) {
|
||||||
|
reject(new Error('登录凭证获取失败'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolve(result.code);
|
resolve(code);
|
||||||
},
|
},
|
||||||
fail: () => reject(new Error('微信登录凭证获取失败'))
|
fail: () => reject(new Error('登录凭证获取失败'))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
// #ifndef MP-WEIXIN
|
// #ifndef MP-WEIXIN
|
||||||
return Promise.reject(new Error('当前端暂不支持微信无感登录'));
|
// #ifndef MP-ALIPAY
|
||||||
|
return Promise.reject(new Error('当前端暂不支持无感登录'));
|
||||||
|
// #endif
|
||||||
// #endif
|
// #endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -28,8 +35,8 @@ export const ensureLogin = () => {
|
||||||
if (loginPromise) return loginPromise;
|
if (loginPromise) return loginPromise;
|
||||||
|
|
||||||
const pendingLogin = (async () => {
|
const pendingLogin = (async () => {
|
||||||
const code = await getWechatCode();
|
const code = await getLoginCode();
|
||||||
const result = await loginByWechatCode(code);
|
const result = await loginByCode(code);
|
||||||
const token = result?.data?.token;
|
const token = result?.data?.token;
|
||||||
|
|
||||||
if (Number(result?.code) !== 200 || !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_URL_CACHE_KEY = 'cashier:property:active-url';
|
||||||
export const ACTIVE_QR_HASH_CACHE_KEY = 'cashier:property:active-url-hash';
|
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) => {
|
const decodeOption = (value?: string) => {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
|
|
@ -39,6 +40,95 @@ export const resolveMerchantUrlFromScanResult = (raw: string) => {
|
||||||
return '';
|
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) => {
|
export const getMerchantUrlHash = (merchantUrl: string) => {
|
||||||
return CryptoJS.SHA256(merchantUrl).toString();
|
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 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 {
|
export interface WxPayParams {
|
||||||
timeStamp: string;
|
timeStamp: string;
|
||||||
nonceStr: string;
|
nonceStr: string;
|
||||||
|
|
@ -6,6 +8,47 @@ export interface WxPayParams {
|
||||||
paySign: string;
|
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) => {
|
export const requestWxPayment = (params: WxPayParams) => {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
uni.requestPayment({
|
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) => {
|
export const isPaymentCancelled = (error: unknown) => {
|
||||||
const message = String((error as any)?.errMsg || '');
|
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';
|
import uni from '@dcloudio/vite-plugin-uni';
|
||||||
|
|
||||||
/** 此路径根据自己项目路径修改;默认为 uniapp 插件市场导入路径;*/
|
/** 此路径根据自己项目路径修改;默认为 uniapp 插件市场导入路径;*/
|
||||||
import viteVueUnocss, { unocss, flex, pseudo, border } from './unocss/a-hua-unocss';
|
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: [
|
plugins: [
|
||||||
uni(),
|
uni(),
|
||||||
|
alipayEsCompatPlugin(),
|
||||||
viteVueUnocss({
|
viteVueUnocss({
|
||||||
/** 预设数组;默认[unocss()] */
|
|
||||||
presets: [
|
presets: [
|
||||||
/**
|
|
||||||
* 默认预设;
|
|
||||||
* text-24、uno-text-24、xx-text-24...
|
|
||||||
*/
|
|
||||||
unocss(),
|
unocss(),
|
||||||
/**
|
|
||||||
* 弹性盒速写;
|
|
||||||
* flex-center、flex-col-center、flex-row-center...
|
|
||||||
*/
|
|
||||||
flex(),
|
flex(),
|
||||||
/**
|
|
||||||
* 伪类、伪元素预设;
|
|
||||||
* after:text-24 after:(text-24 text-white)...
|
|
||||||
*/
|
|
||||||
pseudo(),
|
pseudo(),
|
||||||
/**
|
|
||||||
* 边框速写;
|
|
||||||
* border-1 => border: 1px solid currentColor
|
|
||||||
* border-1-red => border: 1px solid red
|
|
||||||
* border-red => border: 1px solid red
|
|
||||||
* ...
|
|
||||||
* 默认配置如下;
|
|
||||||
*/
|
|
||||||
border({
|
border({
|
||||||
width: 1,
|
width: 1,
|
||||||
style: 'solid',
|
style: 'solid',
|
||||||
color: 'currentColor'
|
color: 'currentColor'
|
||||||
}),
|
})
|
||||||
],
|
],
|
||||||
/**
|
|
||||||
* 自定义原子化 CSS 前缀;
|
|
||||||
* 默认配置如下;
|
|
||||||
*/
|
|
||||||
prefix: ['li'],
|
prefix: ['li'],
|
||||||
/**
|
|
||||||
* 排出检测原子化 CSS 规则;
|
|
||||||
* 默认配置如下;
|
|
||||||
*/
|
|
||||||
exclude: ['node_modules', 'uni_modules'],
|
exclude: ['node_modules', 'uni_modules'],
|
||||||
/** 主题配置 */
|
|
||||||
theme: {
|
theme: {
|
||||||
/**
|
|
||||||
* 是否读取项目根目录 uni.scss 文件内容生成颜色预设;
|
|
||||||
* 默认 false
|
|
||||||
*/
|
|
||||||
generator: true,
|
generator: true,
|
||||||
|
|
||||||
/** 自定义颜色预设 */
|
|
||||||
colors: {
|
colors: {
|
||||||
/** text-very-cool */
|
|
||||||
veryCool: '#37A5FF',
|
veryCool: '#37A5FF',
|
||||||
brand: {
|
brand: {
|
||||||
/** bg-brand-primary */
|
|
||||||
primary: '#37A5FF',
|
primary: '#37A5FF',
|
||||||
/** bg-brand */
|
|
||||||
DEFAULT: '#942192'
|
DEFAULT: '#942192'
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue