5 Commits

Author SHA1 Message Date
erio
af9820a2ee fix: 易支付移动端使用 payurl2 进行微信H5支付 2026-03-16 22:35:27 +08:00
erio
a3f3fa83f1 chore: add MIT license
Closes #11
2026-03-16 14:07:26 +08:00
erio
2590145a2c fix: 易支付移动端传 device=jump 以支持微信H5支付唤起 2026-03-16 13:47:56 +08:00
erio
e2018cbcf9 fix: 渠道 PUT schema 兼容字符串类型的 models/features 字段
前端 linesToJsonString 传的是 JSON 字符串,而 .strict() schema
只接受数组/对象,导致所有渠道编辑保存失败"参数校验失败"。
移除 .strict(),models/features 改为 union 接受 string | array/record。
2026-03-16 05:33:24 +08:00
erio
a1d3f3b639 chore: 从 git 中移除 CLAUDE.md 并加入 gitignore 2026-03-15 19:23:23 +08:00
7 changed files with 46 additions and 19 deletions

3
.gitignore vendored
View File

@@ -42,3 +42,6 @@ next-env.d.ts
# third-party source code (local reference only) # third-party source code (local reference only)
/third-party /third-party
# Claude Code project instructions (contains sensitive deployment info)
CLAUDE.md

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025-present touwaeriol
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -193,7 +193,7 @@ describe('Payment Flow - PC/Mobile, QR/Redirect', () => {
).toBe(true); ).toBe(true);
}); });
it('EasyPay does not use isMobile flag itself (delegates to frontend)', async () => { it('EasyPay forwards isMobile to client for device=jump on mobile', async () => {
mockEasyPayCreatePayment.mockResolvedValue({ mockEasyPayCreatePayment.mockResolvedValue({
code: 1, code: 1,
trade_no: 'EP-003', trade_no: 'EP-003',
@@ -212,16 +212,14 @@ describe('Payment Flow - PC/Mobile, QR/Redirect', () => {
await provider.createPayment(request); await provider.createPayment(request);
// EasyPay client is called the same way regardless of isMobile // EasyPay client receives isMobile so it can set device=jump
expect(mockEasyPayCreatePayment).toHaveBeenCalledWith( expect(mockEasyPayCreatePayment).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
outTradeNo: 'order-ep-003', outTradeNo: 'order-ep-003',
paymentType: 'alipay', paymentType: 'alipay',
isMobile: true,
}), }),
); );
// No isMobile parameter forwarded to the underlying client
const callArgs = mockEasyPayCreatePayment.mock.calls[0][0];
expect(callArgs).not.toHaveProperty('isMobile');
}); });
}); });

View File

@@ -3,19 +3,17 @@ import { z } from 'zod';
import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth'; import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
const updateChannelSchema = z const updateChannelSchema = z.object({
.object({ group_id: z.number().int().positive().optional(),
group_id: z.number().int().positive().optional(), name: z.string().min(1).max(100).optional(),
name: z.string().min(1).max(100).optional(), platform: z.string().min(1).max(50).optional(),
platform: z.string().min(1).max(50).optional(), rate_multiplier: z.number().positive().optional(),
rate_multiplier: z.number().positive().optional(), description: z.string().max(500).nullable().optional(),
description: z.string().max(500).nullable().optional(), models: z.union([z.array(z.string()), z.string()]).nullable().optional(),
models: z.array(z.string()).nullable().optional(), features: z.union([z.record(z.string(), z.unknown()), z.string()]).nullable().optional(),
features: z.record(z.string(), z.unknown()).nullable().optional(), sort_order: z.number().int().min(0).optional(),
sort_order: z.number().int().min(0).optional(), enabled: z.boolean().optional(),
enabled: z.boolean().optional(), });
})
.strict();
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
if (!(await verifyAdminToken(request))) return unauthorizedResponse(request); if (!(await verifyAdminToken(request))) return unauthorizedResponse(request);

View File

@@ -9,6 +9,7 @@ export interface CreatePaymentOptions {
clientIp: string; clientIp: string;
productName: string; productName: string;
returnUrl?: string; returnUrl?: string;
isMobile?: boolean;
} }
function normalizeCidList(cid?: string): string | undefined { function normalizeCidList(cid?: string): string | undefined {
@@ -68,6 +69,10 @@ export async function createPayment(opts: CreatePaymentOptions): Promise<EasyPay
params.cid = cid; params.cid = cid;
} }
if (opts.isMobile) {
params.device = 'mobile';
}
const sign = generateSign(params, env.EASY_PAY_PKEY); const sign = generateSign(params, env.EASY_PAY_PKEY);
params.sign = sign; params.sign = sign;
params.sign_type = 'MD5'; params.sign_type = 'MD5';

View File

@@ -29,11 +29,12 @@ export class EasyPayProvider implements PaymentProvider {
clientIp: request.clientIp || '127.0.0.1', clientIp: request.clientIp || '127.0.0.1',
productName: request.subject, productName: request.subject,
returnUrl: request.returnUrl, returnUrl: request.returnUrl,
isMobile: request.isMobile,
}); });
return { return {
tradeNo: result.trade_no, tradeNo: result.trade_no,
payUrl: result.payurl, payUrl: (request.isMobile && result.payurl2) || result.payurl,
qrCode: result.qrcode, qrCode: result.qrcode,
}; };
} }

View File

@@ -18,6 +18,7 @@ export interface EasyPayCreateResponse {
trade_no: string; trade_no: string;
O_id?: string; O_id?: string;
payurl?: string; payurl?: string;
payurl2?: string;
qrcode?: string; qrcode?: string;
img?: string; img?: string;
} }