Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions frontend/components/common/merchant/merchant-distribute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@ import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Spinner } from "@/components/ui/spinner"
import { MerchantService } from "@/lib/services"
import { MerchantService, type MerchantAPIKey } from "@/lib/services"

interface DistributeDialogProps {
/** 自定义触发器 */
trigger?: React.ReactNode
/** 商户凭证 */
apiKey?: Pick<MerchantAPIKey, "client_id" | "client_secret">
}

/**
* 商户分发对话框
* 商户向用户分发积分
*/
export function DistributeDialog({ trigger }: DistributeDialogProps) {
export function DistributeDialog({ trigger, apiKey }: DistributeDialogProps) {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)

Expand Down Expand Up @@ -88,11 +90,19 @@ export function DistributeDialog({ trigger }: DistributeDialogProps) {
try {
setLoading(true)

if (!apiKey?.client_id || !apiKey?.client_secret) {
toast.error('缺少应用凭证', { description: '请先创建应用并确认凭证可用' })
return
}

const result = await MerchantService.distribute({
user_id: userId.trim(),
user_id: Number(userId.trim()),
username: username.trim(),
amount: amountNum,
remark: remark.trim() || undefined,
}, {
client_id: apiKey.client_id,
client_secret: apiKey.client_secret,
})

toast.success('分发成功', {
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/common/merchant/merchant-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export function MerchantInfo({ apiKey, onUpdate, onDelete, updateAPIKey }: Merch
}
/>

<DistributeDialog />
<DistributeDialog apiKey={apiKey} />

<AlertDialog>
<AlertDialogTrigger asChild>
Expand Down
4 changes: 3 additions & 1 deletion frontend/lib/services/core/base.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export class BaseService {
* @template T - 响应数据类型
* @param url - 完整 URL
* @param data - 请求数据
* @param config - 额外的请求配置
* @returns 响应数据(不经过 response.data.data 解包)
*
* @remarks
Expand All @@ -178,8 +179,9 @@ export class BaseService {
protected static async rawPost<T>(
url: string,
data?: unknown,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const response = await apiClient.post<T>(url, data);
const response = await apiClient.post<T>(url, data, config);
return response.data;
}
}
Expand Down
20 changes: 15 additions & 5 deletions frontend/lib/services/merchant/merchant.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { AxiosHeaders, type InternalAxiosRequestConfig } from 'axios';
import { BaseService } from '../core/base.service';
import { encodeBase64 } from '../../utils';
import type {
MerchantAPIKey,
CreateAPIKeyRequest,
Expand Down Expand Up @@ -456,7 +458,7 @@ export class MerchantService extends BaseService {
* @example
* ```typescript
* const result = await MerchantService.distribute({
* user_id: '123',
* user_id: 123,
* username: 'alice',
* amount: 100,
* out_trade_no: 'DIST20251231001',
Expand All @@ -466,16 +468,24 @@ export class MerchantService extends BaseService {
* ```
*
* @remarks
* - 使用 POST 请求调用 `/pay/distribute` 接口
* - 使用 POST 请求调用 `/pay/distribute` 接口(前端通过 `/lpay/distribute` 代理)
* - 需要通过 Basic Auth 提供商户凭证
* - 不能分发给商户自己
* - 商户余额必须充足
* - 分发会扣除分发费率(根据商户的支付等级)
*/
static async distribute(
request: MerchantDistributeRequest
request: MerchantDistributeRequest,
auth?: { client_id: string; client_secret: string }
): Promise<MerchantDistributeResponse> {
return this.rawPost<MerchantDistributeResponse>('/pay/distribute', request);
const config: InternalAxiosRequestConfig | undefined = auth
? {
headers: new AxiosHeaders({
Authorization: `Basic ${encodeBase64(`${auth.client_id}:${auth.client_secret}`)}`,
}),
}
: undefined;

return this.rawPost<MerchantDistributeResponse>('/lpay/distribute', request, config);
}
}

2 changes: 1 addition & 1 deletion frontend/lib/services/merchant/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ export interface RefundMerchantOrderResponse {
*/
export interface MerchantDistributeRequest {
/** 接收用户 ID (必填) */
user_id: string;
user_id: number;
/** 接收用户名,用于验证 (必填) */
username: string;
/** 分发金额 (必填) */
Expand Down
22 changes: 22 additions & 0 deletions frontend/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@ export function formatLocalDate(date: Date): string {
return `${ year }-${ month }-${ day }T${ hours }:${ minutes }:${ seconds }+08:00`
}

type Base64Buffer = {
from: (input: string, encoding: 'utf-8') => { toString: (encoding: 'base64') => string }
}

/**
* Base64 编码
* @param value 待编码字符串
* @returns Base64 编码后的字符串
*/
export function encodeBase64(value: string): string {
if (typeof globalThis.btoa === 'function') {
return globalThis.btoa(value)
}

const bufferConstructor = (globalThis as typeof globalThis & { Buffer?: Base64Buffer }).Buffer
if (bufferConstructor) {
return bufferConstructor.from(value, 'utf-8').toString('base64')
}

throw new Error('当前环境不支持 Base64 编码')
}

/**
* 生成交易缓存的唯一键
* @param params 交易查询参数
Expand Down
Loading