// 沉默期展示、延期下单的纯函数工具(不依赖 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)}`;