feat(payment): 虎皮椒聚合支付 - 官方插件与跨平台可执行文件

新增 xunhupay 官方包,支付 RPC 与宿主只保留订单和验签边界;官方支付包同时带上各系统 provider,避免把 Linux ELF 当成本机入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-17 23:32:11 +08:00
parent e5a5f0ccc8
commit a23df0cb32
63 changed files with 2070 additions and 128 deletions

View File

@@ -0,0 +1,23 @@
.payment-checkout-code {
display: grid;
place-items: center;
box-sizing: border-box;
width: 232px;
height: 232px;
padding: 12px;
border-radius: 16px;
background: #ffffff;
}
.payment-checkout-code img,
.payment-checkout-code canvas,
.payment-checkout-code svg {
display: block;
width: 208px;
height: 208px;
}
.payment-checkout-code img {
object-fit: contain;
background: #ffffff;
}

View File

@@ -0,0 +1,24 @@
import { QRCode } from "antd";
import { isPaymentQrImageURL } from "@/lib/payment-checkout-code";
import "./payment-checkout-code.css";
const QR_SIZE = 208;
const QR_DARK = "#111111";
const QR_LIGHT = "#ffffff";
export function PaymentCheckoutCode({ value }: { value: string }) {
if (isPaymentQrImageURL(value)) {
return (
<div className="payment-checkout-code">
<img src={value} alt="支付二维码" width={QR_SIZE} height={QR_SIZE} referrerPolicy="no-referrer" />
</div>
);
}
return (
<div className="payment-checkout-code">
<QRCode value={value} size={QR_SIZE} bordered={false} color={QR_DARK} bgColor={QR_LIGHT} />
</div>
);
}

View File

@@ -0,0 +1,19 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isPaymentQrImageURL } from "./payment-checkout-code";
test("isPaymentQrImageURL 识别虎皮椒二维码图片地址", () => {
assert.equal(isPaymentQrImageURL("https://api.xunhupay.com/qrcode.png"), true);
assert.equal(isPaymentQrImageURL(" https://api.dpweixin.com/qrcode.png "), true);
assert.equal(isPaymentQrImageURL("https://pay.example/qrcode"), true);
assert.equal(isPaymentQrImageURL("http://pay.example/images/order-qr.jpg"), true);
});
test("isPaymentQrImageURL 不把待编码载荷或收银页当成图片", () => {
assert.equal(isPaymentQrImageURL("weixin://wxpay/bizpayurl?pr=abc"), false);
assert.equal(isPaymentQrImageURL("https://api.xunhupay.com/pay/jump"), false);
assert.equal(isPaymentQrImageURL("https://pay.example/checkout?qrcode=1"), false);
assert.equal(isPaymentQrImageURL(""), false);
assert.equal(isPaymentQrImageURL("/qrcode.png"), false);
});

View File

@@ -0,0 +1,20 @@
const IMAGE_PATH = /\.(?:png|jpe?g|gif|webp|bmp|svg)$/i;
const QRCODE_PATH = /(?:^|\/)qrcode(?:[-_./]|$)/i;
/** 渠道已生成的二维码图片地址,应直接展示,不能再编码成新的二维码。 */
export function isPaymentQrImageURL(value: string): boolean {
const raw = value.trim();
if (!raw) {
return false;
}
let parsed: URL;
try {
parsed = new URL(raw);
} catch {
return false;
}
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
return false;
}
return IMAGE_PATH.test(parsed.pathname) || QRCODE_PATH.test(parsed.pathname);
}

View File

@@ -4,7 +4,7 @@
## 1. 包格式
上传文件必须是 `.yingce-plugin` ZIP 包,大小不超过 16 MiB。包内必须有根目录 `manifest.json`;不能上传裸 JSON。清单和可选的 Web 运行时代码、静态资源属于同一个版本、权限和生命周期:
上传文件必须是 `.yingce-plugin` ZIP 包,大小不超过 48 MiB。包内必须有根目录 `manifest.json`;不能上传裸 JSON。清单和可选的 Web 运行时代码、静态资源属于同一个版本、权限和生命周期:
```text
my-plugin.yingce-plugin

View File

@@ -82,7 +82,7 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
>
<CloudUpload className="plugin-upload-dropzone-icon" />
<p className="ant-upload-text">{isDraggingPlugin ? "释放文件以上传插件" : "点击选择插件文件,也可拖拽到此处"}</p>
<p className="ant-upload-hint">支持 .yingce-plugin 包 · 大小不超过 16 MiB</p>
<p className="ant-upload-hint">支持 .yingce-plugin 包 · 大小不超过 48 MiB</p>
</Upload.Dragger>
</div>
<div className="plugin-upload-notice">