Files
device-voice-h5/utils/silence.js
邹进 b035105f80
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Has been cancelled
fix: 沉默期已过期未延期时每次回首页都弹提醒
已过期(含有未支付延期订单)不再限频,直到支付或延期;待实名临近截止仍为每次登录一次
2026-11-07 21:21:57 +09:00

185 lines
7.4 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) => {
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) : '-');
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)}`;