Files
device-voice-h5/utils/silence.js
邹进 d13be3580f
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 19m17s
feat: 延期微信支付在微信内直接拉起
- 微信支付下单带 app_type;拿到 pay_config 后与套餐购买一样拉起微信支付,继续支付同样处理
- 微信内打开时支付方式默认微信;取消支付保留同一请求号,再次确认不重复建单
2026-11-07 21:57:26 +09:00

186 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 沉默期展示、延期下单的纯函数工具(不依赖 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, preferWechat = false) => {
const allowed = Array.isArray(allowedMethods) ? allowedMethods : [];
const order = preferWechat ? ['wechat', 'alipay'] : SILENCE_PAYMENT_METHODS;
return order
.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) : '-');
const DAY_MS = 86400000;
/** 待实名时,截止日前多少天开始提醒 */
export const SILENCE_WAITING_REMIND_DAYS = 7;
const flattenNodes = (items = []) => items.flatMap((node) => [node, ...flattenNodes(node?.children || [])]);
/**
* 从套餐历史中挑出首页要弹的沉默期提醒:
* - 优先"沉默期已过期且可延期"(kind = expired,带延期入口);
* - 其次"待实名且 N 天内到截止日"(kind = waiting),截止越近越优先。
* 没有符合条件的返回 null。now 便于测试注入。
*/
export const pickSilenceReminder = (items, now = Date.now()) => {
const candidates = flattenNodes(Array.isArray(items) ? items : [])
.filter((node) => node?.silence && node.package_usage_id);
const expired = candidates.find((node) => node.silence.silence_status === 'expired' && node.silence.extendable);
if (expired) {
return { kind: 'expired', packageUsageId: expired.package_usage_id, packageName: expired.package_name || '', silence: expired.silence };
}
const waiting = candidates
.filter((node) => node.silence.silence_status === 'waiting' && node.silence.silence_deadline_at)
.map((node) => ({ node, deadline: new Date(node.silence.silence_deadline_at).getTime() }))
.filter(({ deadline }) => Number.isFinite(deadline) && deadline >= now && deadline - now <= SILENCE_WAITING_REMIND_DAYS * DAY_MS)
.sort((a, b) => a.deadline - b.deadline)[0];
if (waiting) {
return {
kind: 'waiting',
packageUsageId: waiting.node.package_usage_id,
packageName: waiting.node.package_name || '',
silence: waiting.node.silence,
remainingDays: Math.max(0, Math.ceil((waiting.deadline - now) / DAY_MS))
};
}
return null;
};
const shortHash = (value) => {
let hash = 0;
const text = String(value || '');
for (let index = 0; index < text.length; index++) {
hash = (hash * 31 + text.charCodeAt(index)) | 0;
}
return (hash >>> 0).toString(36);
};
/**
* 每次登录、每个资产、每笔套餐、每种提醒最多弹一次的存储键:
* 带上登录凭证摘要,重新登录后会再次提醒;同一次登录内反复回首页不重复弹。
*/
/** 已过期未延期(含有未支付延期订单)每次回首页都要提醒;只有"待实名临近截止"按每次登录一次 */
export const silenceReminderRepeats = (reminder) => reminder?.kind === 'expired';
export const silenceReminderKey = (identifier, reminder, sessionToken) =>
`silence_reminder:${identifier}:${reminder.packageUsageId}:${reminder.kind}:${shortHash(sessionToken)}`;