Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c083880cbc | ||
|
|
a9ea9d4862 | ||
|
|
e170d5451e |
BIN
0e10fd7fa68c9dda45b221f98145dd7a.jpg
Normal file
BIN
0e10fd7fa68c9dda45b221f98145dd7a.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
43
README.en.md
43
README.en.md
@@ -94,12 +94,24 @@ See [`.env.example`](./.env.example) for the full template.
|
|||||||
|
|
||||||
> `DATABASE_URL` is automatically injected by Docker Compose when using the bundled database.
|
> `DATABASE_URL` is automatically injected by Docker Compose when using the bundled database.
|
||||||
|
|
||||||
### Payment Methods
|
### Payment Providers & Methods
|
||||||
|
|
||||||
Control which payment methods are enabled via `ENABLED_PAYMENT_TYPES` (comma-separated):
|
**Step 1**: Declare which payment providers to load via `PAYMENT_PROVIDERS` (comma-separated):
|
||||||
|
|
||||||
```env
|
```env
|
||||||
ENABLED_PAYMENT_TYPES=alipay,wxpay,stripe
|
# EasyPay only
|
||||||
|
PAYMENT_PROVIDERS=easypay
|
||||||
|
# Stripe only
|
||||||
|
PAYMENT_PROVIDERS=stripe
|
||||||
|
# Both
|
||||||
|
PAYMENT_PROVIDERS=easypay,stripe
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2**: Control which channels are shown to users via `ENABLED_PAYMENT_TYPES`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# EasyPay supports: alipay, wxpay | Stripe supports: stripe
|
||||||
|
ENABLED_PAYMENT_TYPES=alipay,wxpay
|
||||||
```
|
```
|
||||||
|
|
||||||
#### EasyPay (Alipay / WeChat Pay)
|
#### EasyPay (Alipay / WeChat Pay)
|
||||||
@@ -137,10 +149,31 @@ ENABLED_PAYMENT_TYPES=alipay,wxpay,stripe
|
|||||||
|
|
||||||
### UI Customization (Optional)
|
### UI Customization (Optional)
|
||||||
|
|
||||||
|
Display a support contact image and description on the right side of the payment page.
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| `NEXT_PUBLIC_PAY_HELP_IMAGE_URL` | Help image URL (e.g. customer service QR code) |
|
| `PAY_HELP_IMAGE_URL` | Help image URL — external URL or local path (see below) |
|
||||||
| `NEXT_PUBLIC_PAY_HELP_TEXT` | Help text displayed on payment page |
|
| `PAY_HELP_TEXT` | Help text; use `\n` for line breaks, e.g. `Scan to add WeChat\nMon–Fri 9am–6pm` |
|
||||||
|
|
||||||
|
**Two ways to provide the image:**
|
||||||
|
|
||||||
|
- **External URL** (recommended — no Compose changes needed): any publicly accessible image link (CDN, OSS, image hosting).
|
||||||
|
```env
|
||||||
|
PAY_HELP_IMAGE_URL=https://cdn.example.com/help-qr.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Local file**: place the image in `./uploads/` and reference it as `/uploads/<filename>`.
|
||||||
|
The directory must be mounted in `docker-compose.app.yml` (included by default):
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/public/uploads:ro
|
||||||
|
```
|
||||||
|
```env
|
||||||
|
PAY_HELP_IMAGE_URL=/uploads/help-qr.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
> Clicking the help image opens it full-screen in the center of the screen.
|
||||||
|
|
||||||
### Docker Compose Variables
|
### Docker Compose Variables
|
||||||
|
|
||||||
|
|||||||
43
README.md
43
README.md
@@ -94,12 +94,24 @@ docker compose up -d --build
|
|||||||
|
|
||||||
> `DATABASE_URL` 使用自带数据库时由 Compose 自动注入,无需手动填写。
|
> `DATABASE_URL` 使用自带数据库时由 Compose 自动注入,无需手动填写。
|
||||||
|
|
||||||
### 支付方式
|
### 支付服务商与支付方式
|
||||||
|
|
||||||
通过 `ENABLED_PAYMENT_TYPES` 控制开启哪些支付方式(逗号分隔):
|
**第一步**:通过 `PAYMENT_PROVIDERS` 声明启用哪些支付服务商(逗号分隔):
|
||||||
|
|
||||||
```env
|
```env
|
||||||
ENABLED_PAYMENT_TYPES=alipay,wxpay,stripe
|
# 仅易支付
|
||||||
|
PAYMENT_PROVIDERS=easypay
|
||||||
|
# 仅 Stripe
|
||||||
|
PAYMENT_PROVIDERS=stripe
|
||||||
|
# 两者都用
|
||||||
|
PAYMENT_PROVIDERS=easypay,stripe
|
||||||
|
```
|
||||||
|
|
||||||
|
**第二步**:通过 `ENABLED_PAYMENT_TYPES` 控制向用户展示哪些支付渠道:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 易支付支持: alipay, wxpay;Stripe 支持: stripe
|
||||||
|
ENABLED_PAYMENT_TYPES=alipay,wxpay
|
||||||
```
|
```
|
||||||
|
|
||||||
#### EasyPay(支付宝 / 微信支付)
|
#### EasyPay(支付宝 / 微信支付)
|
||||||
@@ -137,10 +149,31 @@ ENABLED_PAYMENT_TYPES=alipay,wxpay,stripe
|
|||||||
|
|
||||||
### UI 定制(可选)
|
### UI 定制(可选)
|
||||||
|
|
||||||
|
在充值页面右侧可展示客服联系方式、说明图片等帮助内容。
|
||||||
|
|
||||||
| 变量 | 说明 |
|
| 变量 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `NEXT_PUBLIC_PAY_HELP_IMAGE_URL` | 帮助图片 URL(如客服二维码) |
|
| `PAY_HELP_IMAGE_URL` | 帮助图片地址(支持外部 URL 或本地路径,见下方说明) |
|
||||||
| `NEXT_PUBLIC_PAY_HELP_TEXT` | 帮助说明文字 |
|
| `PAY_HELP_TEXT` | 帮助说明文字,用 `\n` 换行,如 `扫码加微信\n工作日 9-18 点在线` |
|
||||||
|
|
||||||
|
**图片地址两种方式:**
|
||||||
|
|
||||||
|
- **外部 URL**(推荐,无需改 Compose 配置):直接填图片的公网地址,如 OSS / CDN / 图床链接。
|
||||||
|
```env
|
||||||
|
PAY_HELP_IMAGE_URL=https://cdn.example.com/help-qr.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
- **本地文件**:将图片放到 `./uploads/` 目录,通过 `/uploads/文件名` 引用。
|
||||||
|
需在 `docker-compose.app.yml` 中挂载目录(默认已包含):
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/public/uploads:ro
|
||||||
|
```
|
||||||
|
```env
|
||||||
|
PAY_HELP_IMAGE_URL=/uploads/help-qr.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
> 点击帮助图片可在屏幕中央全屏放大查看。
|
||||||
|
|
||||||
### Docker Compose 专用
|
### Docker Compose 专用
|
||||||
|
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '${APP_PORT:-3001}:3000'
|
- '${APP_PORT:-3001}:3000'
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
# 宿主机 uploads 目录挂载到 Next.js public/uploads,可通过 /uploads/* 访问
|
||||||
|
- ./uploads:/app/public/uploads:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export async function GET(request: NextRequest) {
|
|||||||
maxAmount: env.MAX_RECHARGE_AMOUNT,
|
maxAmount: env.MAX_RECHARGE_AMOUNT,
|
||||||
maxDailyAmount: env.MAX_DAILY_RECHARGE_AMOUNT,
|
maxDailyAmount: env.MAX_DAILY_RECHARGE_AMOUNT,
|
||||||
methodLimits,
|
methodLimits,
|
||||||
|
helpImageUrl: env.PAY_HELP_IMAGE_URL ?? null,
|
||||||
|
helpText: env.PAY_HELP_TEXT ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ interface AppConfig {
|
|||||||
maxAmount: number;
|
maxAmount: number;
|
||||||
maxDailyAmount: number;
|
maxDailyAmount: number;
|
||||||
methodLimits?: Record<string, MethodLimitInfo>;
|
methodLimits?: Record<string, MethodLimitInfo>;
|
||||||
|
helpImageUrl?: string | null;
|
||||||
|
helpText?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PayContent() {
|
function PayContent() {
|
||||||
@@ -60,12 +62,13 @@ function PayContent() {
|
|||||||
maxDailyAmount: 0,
|
maxDailyAmount: 0,
|
||||||
});
|
});
|
||||||
const [userNotFound, setUserNotFound] = useState(false);
|
const [userNotFound, setUserNotFound] = useState(false);
|
||||||
|
const [helpImageOpen, setHelpImageOpen] = useState(false);
|
||||||
|
|
||||||
const effectiveUserId = resolvedUserId || userId;
|
const effectiveUserId = resolvedUserId || userId;
|
||||||
const isEmbedded = uiMode === 'embedded' && isIframeContext;
|
const isEmbedded = uiMode === 'embedded' && isIframeContext;
|
||||||
const hasToken = token.length > 0;
|
const hasToken = token.length > 0;
|
||||||
const helpImageUrl = (process.env.NEXT_PUBLIC_PAY_HELP_IMAGE_URL || '').trim();
|
const helpImageUrl = (config.helpImageUrl || '').trim();
|
||||||
const helpText = (process.env.NEXT_PUBLIC_PAY_HELP_TEXT || '').trim();
|
const helpText = (config.helpText || '').trim();
|
||||||
const hasHelpContent = Boolean(helpImageUrl || helpText);
|
const hasHelpContent = Boolean(helpImageUrl || helpText);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -100,6 +103,8 @@ function PayContent() {
|
|||||||
maxAmount: cfgData.config.maxAmount ?? 1000,
|
maxAmount: cfgData.config.maxAmount ?? 1000,
|
||||||
maxDailyAmount: cfgData.config.maxDailyAmount ?? 0,
|
maxDailyAmount: cfgData.config.maxDailyAmount ?? 0,
|
||||||
methodLimits: cfgData.config.methodLimits,
|
methodLimits: cfgData.config.methodLimits,
|
||||||
|
helpImageUrl: cfgData.config.helpImageUrl ?? null,
|
||||||
|
helpText: cfgData.config.helpText ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (cfgRes.status === 404) {
|
} else if (cfgRes.status === 404) {
|
||||||
@@ -428,13 +433,16 @@ function PayContent() {
|
|||||||
<img
|
<img
|
||||||
src={helpImageUrl}
|
src={helpImageUrl}
|
||||||
alt='help'
|
alt='help'
|
||||||
className='mt-3 max-h-40 w-full rounded-lg object-contain bg-white/70 p-2'
|
onClick={() => setHelpImageOpen(true)}
|
||||||
|
className='mt-3 max-h-40 w-full cursor-zoom-in rounded-lg object-contain bg-white/70 p-2'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{helpText && (
|
{helpText && (
|
||||||
<p className={['mt-3 text-sm leading-6', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
|
<div className={['mt-3 space-y-1 text-sm leading-6', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
|
||||||
{helpText}
|
{helpText.split('\\n').map((line, i) => (
|
||||||
</p>
|
<p key={i}>{line}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -463,6 +471,20 @@ function PayContent() {
|
|||||||
{step === 'result' && (
|
{step === 'result' && (
|
||||||
<OrderStatus status={finalStatus} onBack={handleBack} dark={isDark} />
|
<OrderStatus status={finalStatus} onBack={handleBack} dark={isDark} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{helpImageOpen && helpImageUrl && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4 backdrop-blur-sm"
|
||||||
|
onClick={() => setHelpImageOpen(false)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={helpImageUrl}
|
||||||
|
alt='help'
|
||||||
|
className='max-h-[90vh] max-w-full rounded-xl object-contain shadow-2xl'
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</PayPageLayout>
|
</PayPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ const envSchema = z.object({
|
|||||||
ADMIN_TOKEN: z.string().min(1),
|
ADMIN_TOKEN: z.string().min(1),
|
||||||
|
|
||||||
NEXT_PUBLIC_APP_URL: z.string().url(),
|
NEXT_PUBLIC_APP_URL: z.string().url(),
|
||||||
NEXT_PUBLIC_PAY_HELP_IMAGE_URL: optionalTrimmedString,
|
PAY_HELP_IMAGE_URL: optionalTrimmedString,
|
||||||
NEXT_PUBLIC_PAY_HELP_TEXT: optionalTrimmedString,
|
PAY_HELP_TEXT: optionalTrimmedString,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Env = z.infer<typeof envSchema>;
|
export type Env = z.infer<typeof envSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user