| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- import { postRemittance } from "@/lib/remittance-client";
- export type RemittanceChannel = {
- id: string;
- code: string;
- requestUrl: string;
- icon: string;
- name: string;
- description: string;
- amountRange: string;
- processingTime: string;
- fee: string;
- groupName: string;
- groupOrder: number;
- channelType: number | null;
- bankValid: number;
- /** 支付金额倍率;与标价相乘。为 null 或非有限数时按 1 处理。 */
- rate: number | null;
- };
- export type BankChannelOption = {
- value: string;
- label: string;
- currency: string;
- /** 与外层通道 `rate` 同语义;为 null 或非有限数时乘价按 1。 */
- rate: number | null;
- };
- function toNumber(v: unknown): number | null {
- if (typeof v === "number" && Number.isFinite(v)) return v;
- if (typeof v === "string" && v.trim()) {
- const n = Number(v);
- if (Number.isFinite(n)) return n;
- }
- return null;
- }
- function pickString(record: Record<string, unknown>, keys: string[]): string {
- for (const key of keys) {
- const value = record[key];
- if (typeof value === "string" && value.trim()) return value.trim();
- }
- return "";
- }
- function pickAmountRange(record: Record<string, unknown>): string {
- const min = toNumber(
- record.minAmount ?? record.amountMin ?? record.lowerAmount ?? record.min,
- );
- const max = toNumber(
- record.maxAmount ?? record.amountMax ?? record.upperAmount ?? record.max,
- );
- const unit = pickString(record, ["currency", "currencyCode", "coin", "unit"]).toUpperCase();
- if (min !== null && max !== null) {
- return `$${min} - $${max}${unit ? ` ${unit}` : ""}`;
- }
- const rangeText = pickString(record, ["amountRange", "limitRange", "amountDesc"]);
- return rangeText || "-";
- }
- function pickFee(record: Record<string, unknown>): string {
- const feeRate = toNumber(record.feeRate);
- if (feeRate !== null) {
- if (feeRate <= 1) return `${(feeRate * 100).toFixed(2).replace(/\.00$/, "")}%`;
- return `${feeRate.toFixed(2).replace(/\.00$/, "")}%`;
- }
- const fee = pickString(record, ["fee", "charge", "feeDesc"]);
- return fee || "0%";
- }
- const CHANNEL_GROUP_NAME_MAP: Record<number, string> = {
- 1: "国际转账支付",
- 2: "中国网银支付",
- 3: "数字货币",
- 4: "电子钱包",
- 5: "CWG 电子卡",
- };
- const CHANNEL_GROUP_DISPLAY_ORDER: Record<number, number> = {
- 3: 1, // 数字货币
- 2: 2, // 中国网银支付
- 1: 3, // 国际转账支付
- 4: 4, // 电子钱包
- 5: 5, // CWG 电子卡
- };
- function pickGroupId(record: Record<string, unknown>): number | null {
- const id = toNumber(
- record.type ??
- record.channelType ??
- record.payTypeType ??
- record.groupId ??
- record.channelGroupId ??
- record.channelClassify ??
- record.channelCategory ??
- record.categoryId ??
- record.payTypeGroup ??
- record.payTypeClassify,
- );
- if (id === null) return null;
- if (id >= 1 && id <= 5) return id;
- return null;
- }
- function normalizeChannelList(raw: unknown): RemittanceChannel[] {
- if (!raw || typeof raw !== "object") return [];
- const container = raw as Record<string, unknown>;
- const inner =
- container.data ??
- container.list ??
- container.rows ??
- container.records ??
- container.channels;
- const source = Array.isArray(inner) ? inner : [];
- const output: RemittanceChannel[] = [];
- for (const item of source) {
- if (!item || typeof item !== "object") continue;
- const row = item as Record<string, unknown>;
- const id = String(
- row.id ??
- row.channelId ??
- row.payTypeId ??
- row.code ??
- row.channelCode ??
- output.length + 1,
- );
- const name =
- pickString(row, ["name", "channelName", "payType", "channelCode", "code"]) || "-";
- const code = pickString(row, ["code", "channelCode", "payCode", "channelNo"]) || id;
- const requestUrl = pickString(row, ["requestUrl", "payUrl", "url"]) || "/xfgpay/pay";
- const icon = pickString(row, ["icon", "logo", "img", "image", "iconUrl"]);
- const processingTime =
- pickString(row, ["processingTime", "processTime", "arrivalTime", "timeDesc"]) || "1 hours";
- const groupId = pickGroupId(row);
- const fallbackGroupName =
- pickString(row, [
- "groupName",
- "categoryName",
- "channelGroup",
- "payScene",
- "group",
- ]) || "支付通道";
- const groupName = groupId ? CHANNEL_GROUP_NAME_MAP[groupId] : fallbackGroupName;
- const groupOrder = groupId ? (CHANNEL_GROUP_DISPLAY_ORDER[groupId] ?? 999) : 999;
- output.push({
- id,
- code,
- requestUrl,
- icon,
- name,
- description: name || "-",
- amountRange: pickAmountRange(row),
- processingTime,
- fee: pickFee(row),
- groupName,
- groupOrder,
- channelType: toNumber(row.type ?? row.channelType ?? row.payTypeType),
- bankValid: toNumber(row.bankValid) ?? 0,
- rate: toNumber(row.rate),
- });
- }
- return output;
- }
- /** 支付展示/提交金额 = 标价 × 通道 `rate`;`rate` 为 null 或非有限数时按 1。金额最多保留两位小数。 */
- export function payAmountWithChannelRate(basePrice: number, rate: number | null): number {
- if (!Number.isFinite(basePrice) || basePrice < 0) return 0;
- const mult = rate != null && Number.isFinite(rate) ? rate : 1;
- const product = basePrice * mult;
- if (!Number.isFinite(product)) return 0;
- return Math.round(product * 100) / 100;
- }
- /**
- * 有二级「选择付款方式」且已选中时,用该项的 `rate`;
- * 否则用 `/remittance/channel/list` 外层通道的 `rate`。
- */
- export function resolvePayRateMultiplier(input: {
- bankSelectorVisible: boolean;
- selectedBankCode: string;
- bankOptions: BankChannelOption[];
- channelRate: number | null;
- }): number | null {
- if (input.bankSelectorVisible && input.selectedBankCode.trim()) {
- const bank = input.bankOptions.find((b) => b.value === input.selectedBankCode);
- if (bank) return bank.rate;
- }
- return input.channelRate;
- }
- export async function fetchRemittanceChannels(): Promise<RemittanceChannel[]> {
- // 存款通道固定走 remittance 路径,避免误取到提款通道(/remit/channel/list)。
- const raw = await postRemittance<unknown>("/remittance/channel/list", {});
- return normalizeChannelList(raw);
- }
- function normalizeBankChannelList(raw: unknown): BankChannelOption[] {
- if (!raw || typeof raw !== "object") return [];
- const container = raw as Record<string, unknown>;
- const inner =
- container.data ??
- container.list ??
- container.rows ??
- container.records ??
- container.channels;
- const source = Array.isArray(inner) ? inner : [];
- const output: BankChannelOption[] = [];
- for (const item of source) {
- if (!item || typeof item !== "object") continue;
- const row = item as Record<string, unknown>;
- const valueFromCodeKeys: unknown[] = [row.code, row.channelCode, row.payCode];
- let value = "";
- for (const v of valueFromCodeKeys) {
- if (typeof v === "number" && Number.isFinite(v)) {
- value = String(v);
- break;
- }
- if (typeof v === "string" && v.trim()) {
- value = v.trim();
- break;
- }
- }
- if (!value) value = pickString(row, ["name", "currency", "enName"]);
- if (!value) continue;
- const label = pickString(row, ["name", "enName", "currency", "code"]) || value;
- const currency = pickString(row, ["currency", "code", "name"]) || "USDT";
- output.push({ value, label, currency, rate: toNumber(row.rate) });
- }
- return output;
- }
- export async function fetchBankChannelOptions(channelCode?: string): Promise<BankChannelOption[]> {
- const body = channelCode ? { channelCode } : {};
- const raw = await postRemittance<unknown>("/channel/bank/list", body);
- return normalizeBankChannelList(raw);
- }
- /**
- * 银联 / 电汇等走 `/telegraphic/pay` 的通道:金额、银行等参数放在请求体,不拼在 `.../pay/...` 路径后面。
- */
- export function isTelegraphicStylePayRequestUrl(requestUrl: string): boolean {
- const n = `/${requestUrl.replace(/^\/+|\/+$/g, "")}`.toLowerCase();
- return n.includes("telegraphic");
- }
- export type PayGoodsDetail = {
- goodsId: string;
- goodsNum: number;
- };
- export async function submitXfgPayOrder(input: {
- requestUrl: string;
- amount: number;
- bankCode?: string;
- code?: string;
- goodsDetails: PayGoodsDetail[];
- payName: string;
- payPhone: string;
- }): Promise<{ raw: unknown; resultUrl: string | null }> {
- const normalizedRequestUrl = `/${input.requestUrl.replace(/^\/+|\/+$/g, "")}`;
- // 仍以后端下发 requestUrl 为准(动态);仅当明显是提款路径时兜底,避免误打提款申请。
- const payRequestPath = /^\/withdraw(\/|$)/i.test(normalizedRequestUrl)
- ? "/xfgpay/pay"
- : normalizedRequestUrl;
- const amount = String(input.amount);
- const useBodyForPayParams = isTelegraphicStylePayRequestUrl(payRequestPath);
- const path = useBodyForPayParams
- ? payRequestPath
- : input.bankCode
- ? `${payRequestPath}/1/${encodeURIComponent(amount)}/${encodeURIComponent(input.bankCode)}/0`
- : `${payRequestPath}/1/${encodeURIComponent(amount)}/0`;
- const body = useBodyForPayParams
- ? {
- goodsDetails: input.goodsDetails,
- payName: input.payName,
- payPhone: input.payPhone,
- amount: input.amount,
- ...(input.code ? { code: input.code } : {}),
- ...(input.bankCode ? { bankCode: input.bankCode } : {}),
- }
- : {
- goodsDetails: input.goodsDetails,
- payName: input.payName,
- payPhone: input.payPhone,
- /** 与路径中的金额一致:标价×通道 rate 后的实付额;避免后端只读 body 时落到商品原价。 */
- amount: input.amount,
- };
- const data = await postRemittance<unknown>(path, body);
- return { raw: data, resultUrl: pickResultUrl(data) };
- }
- function pickResultUrl(raw: unknown): string | null {
- if (!raw || typeof raw !== "object") return null;
- const o = raw as Record<string, unknown>;
- const candidates: unknown[] = [
- o.result,
- o.url,
- o.payUrl,
- o.redirectUrl,
- ];
- if (o.data && typeof o.data === "object" && o.data !== null) {
- const d = o.data as Record<string, unknown>;
- candidates.push(d.result, d.url, d.payUrl, d.redirectUrl);
- }
- for (const item of candidates) {
- if (typeof item === "string" && item.trim()) return item.trim();
- }
- return null;
- }
|