feat: C 端套餐沉默期展示与自助延期
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 23m46s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 23m46s
- 套餐历史展示沉默期状态、截止日期、累计延期与调整审批中,可延期时显示延期入口 - 新增延期页:资格、档位与自定义天数试算、支付方式、下单(request_id 幂等) - 支付宝走现有支付页;微信为链接时在微信外直接跳转,否则展示二维码与复制链接 - 新增我的延期订单列表与详情、沉默期记录,沉默期错误码转客户提示
This commit is contained in:
50
utils/silence-payment.js
Normal file
50
utils/silence-payment.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { markPendingPaymentRefresh, openAlipayPayment } from '@/utils/payment.js';
|
||||
import { isPaymentLink } from '@/utils/silence.js';
|
||||
|
||||
export const SILENCE_REFRESH_TARGET = {
|
||||
EXTENSION: 'silence-extension',
|
||||
ORDER_DETAIL: 'silence-order-detail'
|
||||
};
|
||||
|
||||
const SILENCE_PAYMENT_DATA_KEY = 'pending_silence_payment_data';
|
||||
|
||||
export const getPendingSilencePayment = () => uni.getStorageSync(SILENCE_PAYMENT_DATA_KEY) || null;
|
||||
|
||||
const isInWechat = () => typeof navigator !== 'undefined' && /micromessenger/i.test(navigator.userAgent);
|
||||
|
||||
/**
|
||||
* 用延期订单返回的 qr_content 拉起支付。
|
||||
* - 支付宝:走现有支付宝支付页(浏览器打开链接 / 截图扫码)。
|
||||
* - 微信:后端给延期订单返回的是微信 H5 支付链接或扫码内容,不是公众号支付参数。
|
||||
* 在微信外且为链接时直接跳转;其余情况进入延期支付页展示二维码与链接。
|
||||
*/
|
||||
export const openSilencePayment = (order, refreshTarget) => {
|
||||
const content = String(order?.qr_content || '').trim();
|
||||
if (!content) return Promise.reject(new Error('支付内容为空'));
|
||||
|
||||
if (order.payment_method === 'alipay') {
|
||||
return openAlipayPayment(content, refreshTarget);
|
||||
}
|
||||
|
||||
markPendingPaymentRefresh(refreshTarget);
|
||||
uni.setStorageSync(SILENCE_PAYMENT_DATA_KEY, {
|
||||
orderId: order.id,
|
||||
orderNo: order.order_no,
|
||||
amount: order.amount,
|
||||
content,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
if (isPaymentLink(content) && !isInWechat() && typeof window !== 'undefined') {
|
||||
window.location.href = content;
|
||||
return Promise.resolve({ redirecting: true });
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/silence-payment/silence-payment',
|
||||
success: () => resolve({ redirecting: true }),
|
||||
fail: () => reject(new Error('打开支付页失败'))
|
||||
});
|
||||
});
|
||||
};
|
||||
129
utils/silence.js
Normal file
129
utils/silence.js
Normal file
@@ -0,0 +1,129 @@
|
||||
// 沉默期展示、延期下单的纯函数工具(不依赖 uni,便于测试)。
|
||||
|
||||
export const SILENCE_PAYMENT_METHODS = ['alipay', 'wechat'];
|
||||
|
||||
export const SILENCE_PAYMENT_LABELS = {
|
||||
alipay: '支付宝支付',
|
||||
wechat: '微信支付'
|
||||
};
|
||||
|
||||
// 资格接口 reason:付费延期不能办理的原因
|
||||
const REASON_TEXT = {
|
||||
not_expired: '套餐沉默期尚未过期,暂不需要延期',
|
||||
no_silence: '该套餐没有沉默期,无需延期',
|
||||
extension_disabled: '该套餐不支持付费延期,请联系客服',
|
||||
no_policy: '暂未开放延期,请联系客服',
|
||||
exceeds_limit: '已超过可延期期限,请联系客服',
|
||||
refunded: '该套餐已退款,不能延期',
|
||||
order_pending: '已有未完成的延期订单,请先完成或等待其关闭',
|
||||
activated: '套餐已激活,无需延期',
|
||||
adjust_pending: '沉默期调整审批中,请稍后再试'
|
||||
};
|
||||
|
||||
export const getSilenceReasonText = (reason) => REASON_TEXT[reason] || '当前暂不能办理延期,请联系客服';
|
||||
|
||||
const rangeText = (data, minKey, maxKey, unit) => {
|
||||
const min = Number(data?.[minKey]);
|
||||
const max = Number(data?.[maxKey]);
|
||||
if (Number.isFinite(min) && Number.isFinite(max) && max > 0) return `${min}~${max} ${unit}`;
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 沉默期错误码转客户提示;不是沉默期错误码时返回后端 msg 或兜底文案。
|
||||
* error 为 request.js reject 的原始响应(含 code、msg、data)。
|
||||
*/
|
||||
export const getSilenceErrorMessage = (error, fallback = '操作失败,请稍后重试') => {
|
||||
const code = Number(error?.code);
|
||||
const data = error?.data || {};
|
||||
switch (code) {
|
||||
case 1263:
|
||||
return data.reason ? getSilenceReasonText(data.reason) : '套餐当前不需要延期';
|
||||
case 1264:
|
||||
return '该套餐不支持付费延期,请联系客服';
|
||||
case 1265: {
|
||||
const range = rangeText(data, 'min_months', 'max_months', '个月');
|
||||
return range ? `超过可延期期限,可延期 ${range}` : '超过可延期期限,请联系客服';
|
||||
}
|
||||
case 1266: {
|
||||
const range = rangeText(data, 'min_days', 'max_days', '天');
|
||||
return range ? `延期天数需在 ${range}之间` : '延期天数不在允许范围';
|
||||
}
|
||||
case 1267:
|
||||
return '免费延期次数已用完';
|
||||
case 1268:
|
||||
return '沉默期已过期的套餐不能退款';
|
||||
case 1270:
|
||||
return '暂未开放延期,请联系客服';
|
||||
case 1271:
|
||||
return '已有未完成的延期订单,请先完成支付或等待其关闭';
|
||||
case 1274:
|
||||
return '延期功能暂未开放';
|
||||
default:
|
||||
if (error?.paymentResultUnknown) return '网络异常,请稍后在"我的延期订单"中查看结果';
|
||||
return error?.msg || error?.message || fallback;
|
||||
}
|
||||
};
|
||||
|
||||
export const isSilenceFeatureDisabled = (error) => Number(error?.code) === 1274;
|
||||
|
||||
/** 延期可用支付方式:资产允许的支付方式与 wechat、alipay 取交集,支付宝优先 */
|
||||
export const getSilencePaymentOptions = (allowedMethods) => {
|
||||
const allowed = Array.isArray(allowedMethods) ? allowedMethods : [];
|
||||
return SILENCE_PAYMENT_METHODS
|
||||
.filter((method) => allowed.includes(method))
|
||||
.map((value) => ({ value, label: SILENCE_PAYMENT_LABELS[value] }));
|
||||
};
|
||||
|
||||
/** 天数校验:整数且在 [min, max];返回错误文案,合法返回空串 */
|
||||
export const validateExtensionDays = (days, min, max) => {
|
||||
const value = Number(days);
|
||||
if (days === '' || days === null || days === undefined || !Number.isInteger(value)) {
|
||||
return '请输入整数天数';
|
||||
}
|
||||
const lower = Number(min) || 1;
|
||||
const upper = Number(max) || 0;
|
||||
if (value < lower || (upper > 0 && value > upper)) {
|
||||
return upper > 0 ? `延期天数需在 ${lower}~${upper} 天之间` : `延期天数不能少于 ${lower} 天`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/** 下单幂等键:时间戳 + 随机串,最长 64 */
|
||||
export const createSilenceRequestId = () => {
|
||||
const random = Math.random().toString(36).slice(2, 12);
|
||||
return `h5-silence-${Date.now()}-${random}`.slice(0, 64);
|
||||
};
|
||||
|
||||
/**
|
||||
* 管理一次提交的 request_id:同一组 (天数, 支付方式) 的重试复用,任一变化后生成新的。
|
||||
*/
|
||||
export const createRequestIdKeeper = (generate = createSilenceRequestId) => {
|
||||
let current = null;
|
||||
return {
|
||||
get(days, paymentMethod) {
|
||||
const key = `${days}|${paymentMethod}`;
|
||||
if (!current || current.key !== key) current = { key, id: generate() };
|
||||
return current.id;
|
||||
},
|
||||
reset() {
|
||||
current = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/** 沉默期状态颜色 */
|
||||
export const getSilenceStatusClass = (status) => ({
|
||||
waiting: 'silence-waiting',
|
||||
expired: 'silence-expired',
|
||||
ended: 'silence-ended'
|
||||
}[status] || '');
|
||||
|
||||
/** 延期订单状态是否已结束(不会再变化) */
|
||||
export const isSilenceOrderFinal = (status) => ['completed', 'closed', 'rejected', 'refunded'].includes(status);
|
||||
|
||||
/** qr_content 是否为可直接跳转的支付链接 */
|
||||
export const isPaymentLink = (content) => /^https?:\/\//i.test(String(content || '').trim());
|
||||
|
||||
/** 沉默期流水 / 订单日期:只取到日 */
|
||||
export const formatSilenceDate = (value) => (value ? String(value).replace('T', ' ').slice(0, 10) : '-');
|
||||
Reference in New Issue
Block a user