wechatPay.fontend的部分调试

This commit is contained in:
Xu Chang 2024-04-30 12:24:19 +08:00
parent 2fb6fa4f4b
commit 821c73beb5
41 changed files with 704 additions and 107 deletions

View File

@ -17,16 +17,16 @@ declare const List: <T extends keyof EntityDict>(props: ReactComponentProps<Enti
data: RowWithActions<EntityDict, T>[];
loading: boolean;
tablePagination?: any;
rowSelection?: {
type: "checkbox" | "radio";
selectedRowKeys?: string[] | undefined;
onChange: (selectedRowKeys: string[], row: RowWithActions<EntityDict, T>[], info?: {
type: "multiple" | "single" | "none";
} | undefined) => void;
} | undefined;
hideHeader: boolean;
size?: "small" | "large" | "middle" | undefined;
scroll?: any;
rowSelection?: any;
hideHeader?: boolean | undefined;
disableSerialNumber?: boolean | undefined;
size?: "small" | "middle" | "large" | undefined;
scroll?: ({
x?: string | number | true | undefined;
y?: string | number | undefined;
} & {
scrollToFirstRowOnChange?: boolean | undefined;
}) | undefined;
locale?: any;
}>) => React.ReactElement;
declare const ListPro: <T extends keyof EntityDict>(props: {
@ -43,15 +43,9 @@ declare const ListPro: <T extends keyof EntityDict>(props: {
data: RowWithActions<EntityDict, T>[];
loading?: boolean | undefined;
tablePagination?: any;
rowSelection?: {
type: "checkbox" | "radio";
selectedRowKeys?: string[] | undefined;
onChange: (selectedRowKeys: string[], row: RowWithActions<EntityDict, T>[], info?: {
type: "multiple" | "single" | "none";
} | undefined) => void;
} | undefined;
rowSelection?: any;
disableSerialNumber?: boolean | undefined;
size?: "small" | "large" | "middle" | undefined;
size?: "small" | "middle" | "large" | undefined;
scroll?: any;
locale?: any;
}) => React.ReactElement;
@ -62,14 +56,14 @@ declare const Detail: <T extends keyof EntityDict>(props: ReactComponentProps<En
data: Partial<EntityDict[T]["Schema"]>;
title?: string | undefined;
bordered?: boolean | undefined;
layout?: "horizontal" | "vertical" | undefined;
layout?: "vertical" | "horizontal" | undefined;
}>) => React.ReactElement;
declare const Upsert: <T extends keyof EntityDict>(props: ReactComponentProps<EntityDict, T, false, {
helps: Record<string, string>;
entity: T;
attributes: OakAbsAttrUpsertDef<EntityDict, T, string | number>[];
data: EntityDict[T]["Schema"];
layout: "horizontal" | "vertical";
layout: "vertical" | "horizontal";
mode: "default" | "card";
}>) => React.ReactElement;
export { FilterPanel, List, ListPro, Detail, Upsert, ReactComponentProps, ColumnProps, RowWithActions, OakExtraActionProps, OakAbsAttrDef, onActionFnDef, };

View File

@ -5,6 +5,7 @@ import { CentToString } from 'oak-domain/lib/utils/money';
import classNames from 'classnames';
import AccountDeposit from '../deposit';
import AccountOperList from '../../accountOper/pure/List.pc';
import { AccountBookOutlined, ScheduleOutlined } from '@ant-design/icons';
export default function Render(props) {
const { account, depositMax, unfinishedPayId, onDepositPayId } = props.data;
const { t, createDepositPay, setMessage } = props.methods;
@ -17,7 +18,7 @@ export default function Render(props) {
if (account) {
const { total, avail, '#oakLegalActions': legalActions, accountOper$account: opers } = account;
return (<>
<Card className={Styles.card} title={t('title')} extra={<Flex gap="middle">
<Card className={Styles.card} title={<span><AccountBookOutlined />&nbsp;{t('title')}</span>} extra={<Flex gap="middle">
{legalActions?.includes('deposit') && <Button type="primary" onClick={() => {
if (unfinishedPayId) {
setUfOpen(true);
@ -50,10 +51,10 @@ export default function Render(props) {
<span className={Styles.value}>{t('common::pay.symbol')} {CentToString(total - avail, 2)}</span>
</div>
</Flex>
{opers?.length && (<>
{!!opers?.length && (<>
<Divider />
<div className={Styles.oper}>
<span className={Styles.title}>{t('history')}</span>
<span className={Styles.title}><ScheduleOutlined />&nbsp;{t('history')}</span>
<AccountOperList accountOpers={opers} t={t}/>
</div>
</>)}

View File

@ -2,4 +2,4 @@ import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../oak-app-domain';
export default function Render(props: WebComponentProps<EntityDict, 'accountOper', false, {
accountOpers: RowWithActions<EntityDict, 'accountOper'>[];
}>): null;
}>): string;

View File

@ -1,5 +1,5 @@
export default function Render(props) {
const { accountOpers } = props.data;
const { t } = props.methods;
return null;
return '还没有实现利用pure/List渲染要支持滚动加载 todo';
}

View File

@ -12,6 +12,7 @@ export default OakComponent({
refunded: 1,
timeoutAt: 1,
forbidRefundAt: 1,
externalId: 1,
orderId: 1,
accountId: 1,
account: {

View File

@ -16,5 +16,11 @@
"cc": {
"title": "关闭支付",
"content": "您确认关闭此次支付吗?"
},
"wechat": {
"native": {
"tips": "请用微信扫描二维码支付",
"tips2": "虚拟的支付二维码,不用扫~"
}
}
}

View File

@ -2,7 +2,13 @@ import React from 'react';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../oak-app-domain';
import { OfflinePayConfig, PayConfig } from '../../../types/PayConfig';
export declare function RenderOffline(pay: RowWithActions<EntityDict, 'pay'>, t: (key: string) => string, offline: OfflinePayConfig, updateMeta: (meta: any) => void, metaUpdatable: boolean): React.JSX.Element;
export declare function RenderOffline(props: {
pay: RowWithActions<EntityDict, 'pay'>;
t: (key: string) => string;
offline: OfflinePayConfig;
updateMeta: (meta: any) => void;
metaUpdatable: boolean;
}): React.JSX.Element;
export default function Render(props: WebComponentProps<EntityDict, 'pay', false, {
pay?: RowWithActions<EntityDict, 'pay'>;
application?: EntityDict['application']['Schema'];

View File

@ -1,9 +1,10 @@
import React from 'react';
import { Input, Tag, Card, Form, Descriptions, Alert, Select, Button, Modal } from 'antd';
import { Input, Tag, Card, QRCode, Form, Descriptions, Alert, Select, Button, Modal } from 'antd';
import { CentToString } from 'oak-domain/lib/utils/money';
import Styles from './web.pc.module.less';
import { PAY_CHANNEL_OFFLINE_NAME } from '../../../types/PayConfig';
export function RenderOffline(pay, t, offline, updateMeta, metaUpdatable) {
import { PAY_CHANNEL_OFFLINE_NAME, PAY_CHANNEL_WECHAT_APP_NAME, PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_MP_NAME, PAY_CHANNEL_WECHAT_NATIVE_NAME } from '../../../types/PayConfig';
export function RenderOffline(props) {
const { pay, t, offline, updateMeta, metaUpdatable } = props;
const { meta, iState } = pay;
return (<>
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 14 }} layout="horizontal" style={{ width: '100%', marginTop: 12 }}>
@ -28,7 +29,25 @@ export function RenderOffline(pay, t, offline, updateMeta, metaUpdatable) {
</Form>
</>);
}
function RenderPayMeta(pay, application, t, payConfig, updateMeta) {
function RenderWechatPay(props) {
const { pay, t } = props;
const { externalId, channel } = pay;
switch (channel) {
case PAY_CHANNEL_WECHAT_NATIVE_NAME: {
return (<>
<QRCode value={externalId} size={280}/>
<div className={Styles.qrCodeTips}>
{process.env.NODE_ENV === 'production' ?
<Alert type="info" message={t('wechat.native.tips')}/> :
<Alert type="warning" message={t('wechat.native.tips2')}/>}
</div>
</>);
}
}
return null;
}
function RenderPayMeta(props) {
const { pay, application, t, payConfig, updateMeta } = props;
const { iState, channel } = pay;
if (['unpaid', 'paying'].includes(iState) && pay.applicationId !== application.id && channel !== PAY_CHANNEL_OFFLINE_NAME) {
return <Alert type='warning' message={t('notSameApp')}/>;
@ -41,9 +60,22 @@ function RenderPayMeta(pay, application, t, payConfig, updateMeta) {
&& ele.attrs?.includes('meta'));
return (<>
{iState === 'paying' && <Alert type='info' message={t('offline.tips')}/>}
{RenderOffline(pay, t, payConfig.find(ele => ele.channel === PAY_CHANNEL_OFFLINE_NAME), updateMeta, metaUpdatable)}
{RenderOffline({
pay,
t,
offline: payConfig.find(ele => ele.channel === PAY_CHANNEL_OFFLINE_NAME),
updateMeta,
metaUpdatable
})}
</>);
}
case PAY_CHANNEL_WECHAT_APP_NAME:
case PAY_CHANNEL_WECHAT_H5_NAME:
case PAY_CHANNEL_WECHAT_JS_NAME:
case PAY_CHANNEL_WECHAT_MP_NAME:
case PAY_CHANNEL_WECHAT_NATIVE_NAME: {
return <RenderWechatPay pay={pay} t={t}/>;
}
}
return null;
}
@ -70,7 +102,7 @@ export default function Render(props) {
]}/>
</div>
<div className={Styles.oper}>
{RenderPayMeta(pay, application, t, payConfig, (meta) => update({ meta }))}
<RenderPayMeta pay={pay} t={t} application={application} payConfig={payConfig} updateMeta={(meta) => update({ meta })}/>
</div>
<div className={Styles.btn}>
{oakExecutable === true && (<Button onClick={() => execute()}>

View File

@ -17,6 +17,13 @@
margin-top: 40px;
width: 60%;
max-width: 600px;
display: flex;
flex-direction: column;
align-items: center;
.qrCodeTips {
margin-top: 28px;
}
}
.btn {

View File

@ -98,9 +98,9 @@ export default function Render(props) {
<div className={Styles.spCon}>
<Alert type="warning" message={t('csp.tips')}/>
<Divider style={{ width: '80%', alignSelf: 'flex-end' }}/>
{RenderOffline(spRow, t, offlineConfig, (meta) => {
<RenderOffline pay={spRow} t={t} offline={offlineConfig} metaUpdatable={true} updateMeta={(meta) => {
updateItem({ meta }, spRow.id, 'succeedPaying');
}, true)}
}}/>
<div className={Styles.btn}>
<Button type="primary" onClick={async () => {
await execute();

View File

@ -2,8 +2,8 @@ import React, { useState } from 'react';
import { Button, Row, Tabs } from 'antd';
import Styles from './web.pc.module.less';
import PayConfigUpsert from '../upsert';
import { PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_MP_NAME, PAY_CHANNEL_WECHAT_NATIVE_NAME } from '../../../types/PayConfig';
import { generateNewId } from 'oak-domain/lib/utils/uuid';
import { AppTypeToPayChannelDict } from '../../../utils/payClazz';
const SystemPayChannels = []; // [label, value]
const ApplicationPayChannels = {};
/**
@ -26,18 +26,13 @@ export function registerApplicationPayChannel(type, channel) {
ApplicationPayChannels[type] = [channel];
}
}
const DefaultApplicationPayChannels = {
web: [PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_NATIVE_NAME],
wechatMp: [PAY_CHANNEL_WECHAT_MP_NAME],
wechatPublic: [PAY_CHANNEL_WECHAT_JS_NAME],
};
export default function render(props) {
const { system, oakFullpath, operation, oakDirty } = props.data;
const { t, update, setMessage, execute } = props.methods;
const defaultApplicationPayChannelDict = {
web: DefaultApplicationPayChannels.web.map(ele => [t(`payChannel::${ele}`), ele]),
wechatMp: DefaultApplicationPayChannels.wechatMp.map(ele => [t(`payChannel::${ele}`), ele]),
wechatPublic: DefaultApplicationPayChannels.wechatPublic.map(ele => [t(`payChannel::${ele}`), ele]),
web: AppTypeToPayChannelDict.web.map(ele => [t(`payChannel::internal.${ele}`), ele]),
wechatMp: AppTypeToPayChannelDict.wechatMp.map(ele => [t(`payChannel::internal.${ele}`), ele]),
wechatPublic: AppTypeToPayChannelDict.wechatPublic.map(ele => [t(`payChannel::internal.${ele}`), ele]),
};
const [key, setKey] = useState('');
if (system && oakFullpath) {
@ -54,7 +49,7 @@ export default function render(props) {
{t('system')}
</div>),
key: 'system',
children: (<PayConfigUpsert key="system" config={payConfig} update={(config) => update({ payConfig: config })} channels={SystemPayChannels.concat([[t('payChannel::ACCOUNT'), 'ACCOUNT'], [t('payChannel::OFFLINE'), 'OFFLINE']])} t={t}/>),
children: (<PayConfigUpsert key="system" config={payConfig} update={(config) => update({ payConfig: config })} channels={SystemPayChannels.concat([[t('payChannel::internal.ACCOUNT'), 'ACCOUNT'], [t('payChannel::internal.OFFLINE'), 'OFFLINE']])} t={t}/>),
},
{
label: (<div className={Styles.padding}>

View File

@ -81,6 +81,12 @@ const i18ns = [
"cc": {
"title": "关闭支付",
"content": "您确认关闭此次支付吗?"
},
"wechat": {
"native": {
"tips": "请用微信扫描二维码支付",
"tips2": "虚拟的支付二维码,不用扫~"
}
}
}
},
@ -252,7 +258,16 @@ const i18ns = [
"WECHAT_MP": "微信支付",
"WECHAT_NATIVE": "微信支付",
"WECHAT_H5": "微信支付",
"WECHAT_APP": "微信支付"
"WECHAT_APP": "微信支付",
"internal": {
"ACCOUNT": "系统帐户",
"OFFLINE": "线下支付",
"WECHAT_JS": "微信支付JS",
"WECHAT_MP": "微信支付小程序",
"WECHAT_NATIVE": "微信支付NATIVE二维码",
"WECHAT_H5": "微信支付H5",
"WECHAT_APP": "微信支付APP"
}
}
}
];

View File

@ -5,5 +5,14 @@
"WECHAT_MP": "微信支付",
"WECHAT_NATIVE": "微信支付",
"WECHAT_H5": "微信支付",
"WECHAT_APP": "微信支付"
"WECHAT_APP": "微信支付",
"internal": {
"ACCOUNT": "系统帐户",
"OFFLINE": "线下支付",
"WECHAT_JS": "微信支付JS",
"WECHAT_MP": "微信支付小程序",
"WECHAT_NATIVE": "微信支付NATIVE二维码",
"WECHAT_H5": "微信支付H5",
"WECHAT_APP": "微信支付APP"
}
}

17
es/utils/payClazz/WechatPay.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import { OpSchema, UpdateOperationData } from "../../oak-app-domain/Pay/Schema";
import PayClazz from "../../types/PayClazz";
import { WechatPayChannel, WechatPayConfig } from "../../types/PayConfig";
import { BRC } from "../../types/RuntimeCxt";
export default class WechatPay implements PayClazz {
channel: string;
constructor(channel: WechatPayChannel, appId: string, config: WechatPayConfig);
prepay(pay: OpSchema, data: UpdateOperationData, context: BRC): Promise<void>;
getState(pay: OpSchema): Promise<string | null | undefined>;
close(pay: OpSchema): Promise<void>;
decodeNotification(params: Record<string, string>, body: any): Promise<{
payId: string;
iState: string | null | undefined;
extra?: any;
answer: string;
}[]>;
}

View File

@ -0,0 +1,25 @@
import { EntityDict } from "../../oak-app-domain";
import { OpSchema, UpdateOperationData } from "../../oak-app-domain/Pay/Schema";
import PayClazz from "../../types/PayClazz";
import { WechatPayChannel, WechatPayConfig } from "../../types/PayConfig";
import { BRC } from "../../types/RuntimeCxt";
export declare function registerGetPayStateResult(payState: NonNullable<EntityDict['pay']['OpSchema']['iState']>): void;
export default class WechatPay implements PayClazz {
channel: string;
constructor(channel: WechatPayChannel, appId: string, config: WechatPayConfig);
/**
* prepay接口模拟返回
* @param pay
* @param data
* @param context
*/
prepay(pay: OpSchema, data: UpdateOperationData, context: BRC): Promise<void>;
getState(pay: OpSchema): Promise<string | null | undefined>;
close(pay: OpSchema): Promise<void>;
decodeNotification(params: Record<string, string>, body: any): Promise<{
payId: string;
iState: string | null | undefined;
extra?: any;
answer: string;
}[]>;
}

View File

@ -0,0 +1,68 @@
import { PAY_CHANNEL_WECHAT_APP_NAME, PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_MP_NAME, PAY_CHANNEL_WECHAT_NATIVE_NAME } from "../../types/PayConfig";
let _PAY_STATE = '';
export function registerGetPayStateResult(payState) {
_PAY_STATE = payState;
}
export default class WechatPay {
channel;
constructor(channel, appId, config) {
// todo
this.channel = channel;
}
/**
* 参照微信支付prepay接口模拟返回
* @param pay
* @param data
* @param context
*/
async prepay(pay, data, context) {
switch (this.channel) {
case PAY_CHANNEL_WECHAT_APP_NAME:
case PAY_CHANNEL_WECHAT_JS_NAME:
case PAY_CHANNEL_WECHAT_MP_NAME: {
const prepayId = Math.random().toString();
data.externalId = prepayId;
data.meta = {
prepayId,
};
break;
}
case PAY_CHANNEL_WECHAT_H5_NAME: {
const h5Url = Math.random().toString();
data.externalId = h5Url;
data.meta = {
h5Url,
};
break;
}
case PAY_CHANNEL_WECHAT_NATIVE_NAME: {
const codeUrl = Math.random().toString();
data.externalId = codeUrl;
data.meta = {
codeUrl,
};
break;
}
}
;
data.timeoutAt = Date.now() + 7000 * 1000; // 微信官方文档过期时间2小时提前一点
}
async getState(pay) {
if (_PAY_STATE) {
return _PAY_STATE;
}
const r = Math.random();
if (r < 0.3) {
return 'paying';
}
else if (r > 0.95) {
return 'closed';
}
return 'paid';
}
async close(pay) {
}
decodeNotification(params, body) {
throw new Error("Method not implemented.");
}
}

View File

@ -0,0 +1,19 @@
export default class WechatPay {
channel;
constructor(channel, appId, config) {
// todo
this.channel = channel;
}
prepay(pay, data, context) {
throw new Error("Method not implemented.");
}
getState(pay) {
throw new Error("Method not implemented.");
}
close(pay) {
throw new Error("Method not implemented.");
}
decodeNotification(params, body) {
throw new Error("Method not implemented.");
}
}

View File

@ -1,6 +1,9 @@
import { EntityDict } from '../../oak-app-domain';
import PayClazz from '../../types/PayClazz';
import { BRC } from '../../types/RuntimeCxt';
export declare const AppTypeToPayChannelDict: {
[k in EntityDict['application']['OpSchema']['type']]: string[];
};
type PayClazzConstructor = <ED extends EntityDict>(application: ED['application']['Schema'], channel: string, context: BRC) => Promise<PayClazz>;
export declare function registerAppPayClazzConstructor(channel: string, constructor: PayClazzConstructor): void;
export declare function getPayClazz(appId: string, channel: string, context: BRC): Promise<PayClazz>;

View File

@ -1,11 +1,49 @@
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_OFFLINE_NAME } from "../../types/PayConfig";
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_OFFLINE_NAME, PAY_CHANNEL_WECHAT_APP_NAME, PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_MP_NAME, PAY_CHANNEL_WECHAT_NATIVE_NAME } from "../../types/PayConfig";
import assert from 'assert';
import Offline from './Offline';
import Account from './Account';
import WechatPay from './WechatPay';
export const AppTypeToPayChannelDict = {
web: [PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_NATIVE_NAME],
wechatMp: [PAY_CHANNEL_WECHAT_MP_NAME],
wechatPublic: [PAY_CHANNEL_WECHAT_JS_NAME],
native: [PAY_CHANNEL_WECHAT_APP_NAME],
};
async function createPayClazz(application, channel, context) {
const { type, config, payConfig } = application;
assert(AppTypeToPayChannelDict[type].includes(channel));
const wechatPayConfig = (payConfig?.find(ele => ele.channel === channel));
assert(wechatPayConfig);
const channel2 = channel; // 目前应该只有wechat系的支付
switch (type) {
case 'web': {
const { wechat } = config;
assert(wechat);
const appId = wechat.appId;
return new WechatPay(channel2, appId, wechatPayConfig);
}
case 'wechatMp': {
const { appId } = config;
return new WechatPay(channel2, appId, wechatPayConfig);
}
case 'wechatPublic': {
const { appId } = config;
return new WechatPay(channel2, appId, wechatPayConfig);
}
default: {
assert(false, '暂时不支持');
}
}
}
const PayChannelDict = {};
const PayClazzConstructorDict = {
[PAY_CHANNEL_OFFLINE_NAME]: async () => new Offline(),
[PAY_CHANNEL_ACCOUNT_NAME]: async () => new Account(),
[PAY_CHANNEL_WECHAT_APP_NAME]: createPayClazz,
[PAY_CHANNEL_WECHAT_H5_NAME]: createPayClazz,
[PAY_CHANNEL_WECHAT_JS_NAME]: createPayClazz,
[PAY_CHANNEL_WECHAT_MP_NAME]: createPayClazz,
[PAY_CHANNEL_WECHAT_NATIVE_NAME]: createPayClazz,
};
export function registerAppPayClazzConstructor(channel, constructor) {
PayClazzConstructorDict[channel] = constructor;

View File

@ -3,7 +3,7 @@ import { getPayClazz } from '../utils/payClazz';
import assert from 'assert';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { mergeOperationResult } from 'oak-domain/lib/utils/operationResult';
const QUERY_PAYING_STATE_GAP = 3600 * 1000;
const QUERY_PAYING_STATE_GAP = process.env.NODE_ENV === 'production' ? 3600 * 1000 : 60 * 1000;
const watchers = [
{
name: '对paying状态的订单同步其真实支付状态',

View File

@ -83,6 +83,12 @@ const i18ns = [
"cc": {
"title": "关闭支付",
"content": "您确认关闭此次支付吗?"
},
"wechat": {
"native": {
"tips": "请用微信扫描二维码支付",
"tips2": "虚拟的支付二维码,不用扫~"
}
}
}
},
@ -254,7 +260,16 @@ const i18ns = [
"WECHAT_MP": "微信支付",
"WECHAT_NATIVE": "微信支付",
"WECHAT_H5": "微信支付",
"WECHAT_APP": "微信支付"
"WECHAT_APP": "微信支付",
"internal": {
"ACCOUNT": "系统帐户",
"OFFLINE": "线下支付",
"WECHAT_JS": "微信支付JS",
"WECHAT_MP": "微信支付小程序",
"WECHAT_NATIVE": "微信支付NATIVE二维码",
"WECHAT_H5": "微信支付H5",
"WECHAT_APP": "微信支付APP"
}
}
}
];

View File

@ -5,5 +5,14 @@
"WECHAT_MP": "微信支付",
"WECHAT_NATIVE": "微信支付",
"WECHAT_H5": "微信支付",
"WECHAT_APP": "微信支付"
"WECHAT_APP": "微信支付",
"internal": {
"ACCOUNT": "系统帐户",
"OFFLINE": "线下支付",
"WECHAT_JS": "微信支付JS",
"WECHAT_MP": "微信支付小程序",
"WECHAT_NATIVE": "微信支付NATIVE二维码",
"WECHAT_H5": "微信支付H5",
"WECHAT_APP": "微信支付APP"
}
}

17
lib/utils/payClazz/WechatPay.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import { OpSchema, UpdateOperationData } from "../../oak-app-domain/Pay/Schema";
import PayClazz from "../../types/PayClazz";
import { WechatPayChannel, WechatPayConfig } from "../../types/PayConfig";
import { BRC } from "../../types/RuntimeCxt";
export default class WechatPay implements PayClazz {
channel: string;
constructor(channel: WechatPayChannel, appId: string, config: WechatPayConfig);
prepay(pay: OpSchema, data: UpdateOperationData, context: BRC): Promise<void>;
getState(pay: OpSchema): Promise<string | null | undefined>;
close(pay: OpSchema): Promise<void>;
decodeNotification(params: Record<string, string>, body: any): Promise<{
payId: string;
iState: string | null | undefined;
extra?: any;
answer: string;
}[]>;
}

View File

@ -0,0 +1,25 @@
import { EntityDict } from "../../oak-app-domain";
import { OpSchema, UpdateOperationData } from "../../oak-app-domain/Pay/Schema";
import PayClazz from "../../types/PayClazz";
import { WechatPayChannel, WechatPayConfig } from "../../types/PayConfig";
import { BRC } from "../../types/RuntimeCxt";
export declare function registerGetPayStateResult(payState: NonNullable<EntityDict['pay']['OpSchema']['iState']>): void;
export default class WechatPay implements PayClazz {
channel: string;
constructor(channel: WechatPayChannel, appId: string, config: WechatPayConfig);
/**
* prepay接口模拟返回
* @param pay
* @param data
* @param context
*/
prepay(pay: OpSchema, data: UpdateOperationData, context: BRC): Promise<void>;
getState(pay: OpSchema): Promise<string | null | undefined>;
close(pay: OpSchema): Promise<void>;
decodeNotification(params: Record<string, string>, body: any): Promise<{
payId: string;
iState: string | null | undefined;
extra?: any;
answer: string;
}[]>;
}

View File

@ -0,0 +1,73 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerGetPayStateResult = void 0;
const PayConfig_1 = require("../../types/PayConfig");
let _PAY_STATE = '';
function registerGetPayStateResult(payState) {
_PAY_STATE = payState;
}
exports.registerGetPayStateResult = registerGetPayStateResult;
class WechatPay {
channel;
constructor(channel, appId, config) {
// todo
this.channel = channel;
}
/**
* 参照微信支付prepay接口模拟返回
* @param pay
* @param data
* @param context
*/
async prepay(pay, data, context) {
switch (this.channel) {
case PayConfig_1.PAY_CHANNEL_WECHAT_APP_NAME:
case PayConfig_1.PAY_CHANNEL_WECHAT_JS_NAME:
case PayConfig_1.PAY_CHANNEL_WECHAT_MP_NAME: {
const prepayId = Math.random().toString();
data.externalId = prepayId;
data.meta = {
prepayId,
};
break;
}
case PayConfig_1.PAY_CHANNEL_WECHAT_H5_NAME: {
const h5Url = Math.random().toString();
data.externalId = h5Url;
data.meta = {
h5Url,
};
break;
}
case PayConfig_1.PAY_CHANNEL_WECHAT_NATIVE_NAME: {
const codeUrl = Math.random().toString();
data.externalId = codeUrl;
data.meta = {
codeUrl,
};
break;
}
}
;
data.timeoutAt = Date.now() + 7000 * 1000; // 微信官方文档过期时间2小时提前一点
}
async getState(pay) {
if (_PAY_STATE) {
return _PAY_STATE;
}
const r = Math.random();
if (r < 0.3) {
return 'paying';
}
else if (r > 0.95) {
return 'closed';
}
return 'paid';
}
async close(pay) {
}
decodeNotification(params, body) {
throw new Error("Method not implemented.");
}
}
exports.default = WechatPay;

View File

@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class WechatPay {
channel;
constructor(channel, appId, config) {
// todo
this.channel = channel;
}
prepay(pay, data, context) {
throw new Error("Method not implemented.");
}
getState(pay) {
throw new Error("Method not implemented.");
}
close(pay) {
throw new Error("Method not implemented.");
}
decodeNotification(params, body) {
throw new Error("Method not implemented.");
}
}
exports.default = WechatPay;

View File

@ -1,6 +1,9 @@
import { EntityDict } from '../../oak-app-domain';
import PayClazz from '../../types/PayClazz';
import { BRC } from '../../types/RuntimeCxt';
export declare const AppTypeToPayChannelDict: {
[k in EntityDict['application']['OpSchema']['type']]: string[];
};
type PayClazzConstructor = <ED extends EntityDict>(application: ED['application']['Schema'], channel: string, context: BRC) => Promise<PayClazz>;
export declare function registerAppPayClazzConstructor(channel: string, constructor: PayClazzConstructor): void;
export declare function getPayClazz(appId: string, channel: string, context: BRC): Promise<PayClazz>;

View File

@ -1,15 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPayClazz = exports.registerAppPayClazzConstructor = void 0;
exports.getPayClazz = exports.registerAppPayClazzConstructor = exports.AppTypeToPayChannelDict = void 0;
const tslib_1 = require("tslib");
const PayConfig_1 = require("../../types/PayConfig");
const assert_1 = tslib_1.__importDefault(require("assert"));
const Offline_1 = tslib_1.__importDefault(require("./Offline"));
const Account_1 = tslib_1.__importDefault(require("./Account"));
const WechatPay_1 = tslib_1.__importDefault(require("./WechatPay"));
exports.AppTypeToPayChannelDict = {
web: [PayConfig_1.PAY_CHANNEL_WECHAT_JS_NAME, PayConfig_1.PAY_CHANNEL_WECHAT_H5_NAME, PayConfig_1.PAY_CHANNEL_WECHAT_NATIVE_NAME],
wechatMp: [PayConfig_1.PAY_CHANNEL_WECHAT_MP_NAME],
wechatPublic: [PayConfig_1.PAY_CHANNEL_WECHAT_JS_NAME],
native: [PayConfig_1.PAY_CHANNEL_WECHAT_APP_NAME],
};
async function createPayClazz(application, channel, context) {
const { type, config, payConfig } = application;
(0, assert_1.default)(exports.AppTypeToPayChannelDict[type].includes(channel));
const wechatPayConfig = (payConfig?.find(ele => ele.channel === channel));
(0, assert_1.default)(wechatPayConfig);
const channel2 = channel; // 目前应该只有wechat系的支付
switch (type) {
case 'web': {
const { wechat } = config;
(0, assert_1.default)(wechat);
const appId = wechat.appId;
return new WechatPay_1.default(channel2, appId, wechatPayConfig);
}
case 'wechatMp': {
const { appId } = config;
return new WechatPay_1.default(channel2, appId, wechatPayConfig);
}
case 'wechatPublic': {
const { appId } = config;
return new WechatPay_1.default(channel2, appId, wechatPayConfig);
}
default: {
(0, assert_1.default)(false, '暂时不支持');
}
}
}
const PayChannelDict = {};
const PayClazzConstructorDict = {
[PayConfig_1.PAY_CHANNEL_OFFLINE_NAME]: async () => new Offline_1.default(),
[PayConfig_1.PAY_CHANNEL_ACCOUNT_NAME]: async () => new Account_1.default(),
[PayConfig_1.PAY_CHANNEL_WECHAT_APP_NAME]: createPayClazz,
[PayConfig_1.PAY_CHANNEL_WECHAT_H5_NAME]: createPayClazz,
[PayConfig_1.PAY_CHANNEL_WECHAT_JS_NAME]: createPayClazz,
[PayConfig_1.PAY_CHANNEL_WECHAT_MP_NAME]: createPayClazz,
[PayConfig_1.PAY_CHANNEL_WECHAT_NATIVE_NAME]: createPayClazz,
};
function registerAppPayClazzConstructor(channel, constructor) {
PayClazzConstructorDict[channel] = constructor;

View File

@ -6,7 +6,7 @@ const payClazz_1 = require("../utils/payClazz");
const assert_1 = tslib_1.__importDefault(require("assert"));
const uuid_1 = require("oak-domain/lib/utils/uuid");
const operationResult_1 = require("oak-domain/lib/utils/operationResult");
const QUERY_PAYING_STATE_GAP = 3600 * 1000;
const QUERY_PAYING_STATE_GAP = process.env.NODE_ENV === 'production' ? 3600 * 1000 : 60 * 1000;
const watchers = [
{
name: '对paying状态的订单同步其真实支付状态',

View File

@ -78,7 +78,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'account', f
</div>
</Flex>
{
opers?.length && (
!!opers?.length && (
<>
<Divider />
<div className={Styles.oper}>

View File

@ -14,6 +14,7 @@ export default OakComponent({
refunded: 1,
timeoutAt: 1,
forbidRefundAt: 1,
externalId: 1,
orderId: 1,
accountId: 1,
account: {

View File

@ -16,5 +16,11 @@
"cc": {
"title": "关闭支付",
"content": "您确认关闭此次支付吗?"
},
"wechat": {
"native": {
"tips": "请用微信扫描二维码支付",
"tips2": "虚拟的支付二维码,不用扫~"
}
}
}

View File

@ -17,6 +17,13 @@
margin-top: 40px;
width: 60%;
max-width: 600px;
display: flex;
flex-direction: column;
align-items: center;
.qrCodeTips {
margin-top: 28px;
}
}
.btn {

View File

@ -1,11 +1,14 @@
import React, { useState } from 'react';
import { Input, Radio, Space, Tag, Card, Flex, Form, Descriptions, Typography, Alert, Select, Button, Modal } from 'antd';
import React, { useEffect, useState } from 'react';
import { Input, Space, Tag, Card, QRCode, Form, Descriptions, Typography, Alert, Select, Button, Modal } from 'antd';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '@project/oak-app-domain';
import { CentToString } from 'oak-domain/lib/utils/money';
import { Detail } from '../../AbstractComponents';
import Styles from './web.pc.module.less';
import classNames from 'classnames';
import * as dayJs from 'dayjs';
import duration from 'dayjs/plugin/duration';
dayJs.extend(duration);
import {
OfflinePayConfig,
PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_OFFLINE_NAME, PAY_CHANNEL_WECHAT_APP_NAME,
@ -14,13 +17,14 @@ import {
} from '../../../types/PayConfig';
import { WechatOutlined, MoneyCollectOutlined, WalletOutlined } from '@ant-design/icons';
export function RenderOffline(
export function RenderOffline(props: {
pay: RowWithActions<EntityDict, 'pay'>,
t: (key: string) => string,
offline: OfflinePayConfig,
updateMeta: (meta: any) => void,
metaUpdatable: boolean
) {
}) {
const { pay, t, offline, updateMeta, metaUpdatable } = props;
const { meta, iState } = pay;
return (
<>
@ -65,13 +69,67 @@ export function RenderOffline(
)
}
function RenderPayMeta(
function Counter(props: { deadline: number }) {
const { deadline } = props;
const [counter, setCounter] = useState('');
const timerFn = () => {
const now = Date.now();
if (now < deadline) {
const duration = dayJs.duration(deadline - now);
setCounter(duration.format('HH:mm:ss'));
setTimeout(timerFn, 1000);
}
}
useEffect(() => {
timerFn();
}, []);
return (
<Typography.Title level={3}>{counter}</Typography.Title>
);
}
function RenderWechatPay(props: {
pay: RowWithActions<EntityDict, 'pay'>;
t: (key: string) => string;
}) {
const { pay, t } = props;
const { externalId, channel, timeoutAt, iState } = pay;
switch (channel) {
case PAY_CHANNEL_WECHAT_NATIVE_NAME: {
if (iState! === 'paying') {
return (
<>
<Counter deadline={timeoutAt as number} />
<QRCode
value={externalId!}
size={280}
/>
<div className={Styles.qrCodeTips}>
{
process.env.NODE_ENV === 'production' ?
<Alert type="info" message={t('wechat.native.tips')} /> :
<Alert type="warning" message={t('wechat.native.tips2')} />
}
</div>
</>
);
}
break;
}
}
return null;
}
function RenderPayMeta(props: {
pay: RowWithActions<EntityDict, 'pay'>,
application: EntityDict['application']['Schema'],
t: (key: string) => string,
payConfig: PayConfig,
updateMeta: (meta: any) => void,
) {
}) {
const { pay, application, t, payConfig, updateMeta } = props;
const { iState, channel } = pay;
if (['unpaid', 'paying'].includes(iState!) && pay.applicationId !== application.id && channel !== PAY_CHANNEL_OFFLINE_NAME) {
return <Alert type='warning' message={t('notSameApp')} />
@ -88,15 +146,24 @@ function RenderPayMeta(
<>
{iState === 'paying' && <Alert type='info' message={t('offline.tips')} />}
{RenderOffline(
pay,
t,
payConfig.find(ele => ele.channel === PAY_CHANNEL_OFFLINE_NAME) as OfflinePayConfig,
updateMeta,
metaUpdatable
{
pay,
t,
offline: payConfig.find(ele => ele.channel === PAY_CHANNEL_OFFLINE_NAME) as OfflinePayConfig,
updateMeta,
metaUpdatable
}
)}
</>
);
}
case PAY_CHANNEL_WECHAT_APP_NAME:
case PAY_CHANNEL_WECHAT_H5_NAME:
case PAY_CHANNEL_WECHAT_JS_NAME:
case PAY_CHANNEL_WECHAT_MP_NAME:
case PAY_CHANNEL_WECHAT_NATIVE_NAME: {
return <RenderWechatPay pay={pay} t={t} />
}
}
return null;
}
@ -140,7 +207,13 @@ export default function Render(props: WebComponentProps<EntityDict, 'pay', false
/>
</div>
<div className={Styles.oper}>
{RenderPayMeta(pay, application, t, payConfig!, (meta) => update({ meta }))}
<RenderPayMeta
pay={pay}
t={t}
application={application}
payConfig={payConfig!}
updateMeta={(meta) => update({ meta })}
/>
</div>
<div className={Styles.btn}>
{

View File

@ -133,11 +133,15 @@ export default function Render(props: WebComponentProps<EntityDict, 'pay', false
<div className={Styles.spCon}>
<Alert type="warning" message={t('csp.tips')} />
<Divider style={{ width: '80%', alignSelf: 'flex-end' }} />
{
RenderOffline(spRow as RowWithActions<EntityDict, 'pay'>, t, offlineConfig, (meta) => {
<RenderOffline
pay={spRow as RowWithActions<EntityDict, 'pay'>}
t={t}
offline={offlineConfig}
metaUpdatable={true}
updateMeta={(meta) => {
updateItem({ meta }, spRow!.id!, 'succeedPaying');
}, true)
}
}}
/>
<div className={Styles.btn}>
<Button
type="primary"

View File

@ -83,6 +83,12 @@ const i18ns: I18n[] = [
"cc": {
"title": "关闭支付",
"content": "您确认关闭此次支付吗?"
},
"wechat": {
"native": {
"tips": "请用微信扫描二维码支付",
"tips2": "虚拟的支付二维码,不用扫~"
}
}
}
},
@ -254,7 +260,16 @@ const i18ns: I18n[] = [
"WECHAT_MP": "微信支付",
"WECHAT_NATIVE": "微信支付",
"WECHAT_H5": "微信支付",
"WECHAT_APP": "微信支付"
"WECHAT_APP": "微信支付",
"internal": {
"ACCOUNT": "系统帐户",
"OFFLINE": "线下支付",
"WECHAT_JS": "微信支付JS",
"WECHAT_MP": "微信支付小程序",
"WECHAT_NATIVE": "微信支付NATIVE二维码",
"WECHAT_H5": "微信支付H5",
"WECHAT_APP": "微信支付APP"
}
}
}
];

View File

@ -1,7 +1,6 @@
import { OpSchema, UpdateOperationData } from "@project/oak-app-domain/Pay/Schema";
import PayClazz from "@project/types/PayClazz";
import { PAY_CHANNEL_ACCOUNT_NAME } from '@project/types/PayConfig';
import { BRC } from "@project/types/RuntimeCxt";
import { OpSchema, UpdateOperationData } from "../../oak-app-domain/Pay/Schema";
import PayClazz from "../../types/PayClazz";
import { PAY_CHANNEL_ACCOUNT_NAME } from '../../types/PayConfig';
import assert from 'assert';
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";

View File

@ -1,7 +1,7 @@
import { OpSchema, UpdateOperationData } from "@project/oak-app-domain/Pay/Schema";
import PayClazz from "@project/types/PayClazz";
import { PAY_CHANNEL_OFFLINE_NAME } from "@project/types/PayConfig";
import { BRC } from "@project/types/RuntimeCxt";
import { OpSchema, UpdateOperationData } from "../../oak-app-domain/Pay/Schema";
import PayClazz from "../../types/PayClazz";
import { PAY_CHANNEL_OFFLINE_NAME } from "../../types/PayConfig";
import { BRC } from "../../types/RuntimeCxt";
import assert from "assert";
export default class Offline implements PayClazz {

View File

@ -1,28 +1,80 @@
import { OpSchema, UpdateOperationData } from "@project/oak-app-domain/Pay/Schema";
import PayClazz from "@project/types/PayClazz";
import { WechatPayChannel, WechatPayConfig } from "@project/types/PayConfig";
import { BRC } from "@project/types/RuntimeCxt";
import { EntityDict } from "../../oak-app-domain";
import { OpSchema, UpdateOperationData } from "../../oak-app-domain/Pay/Schema";
import PayClazz from "../../types/PayClazz";
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_WECHAT_APP_NAME, PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_JS_NAME,
PAY_CHANNEL_WECHAT_MP_NAME, PAY_CHANNEL_WECHAT_NATIVE_NAME, WechatPayChannel, WechatPayConfig } from "../../types/PayConfig";
import { BRC } from "../../types/RuntimeCxt";
let _PAY_STATE = '';
export function registerGetPayStateResult(payState: NonNullable<EntityDict['pay']['OpSchema']['iState']>) {
_PAY_STATE = payState;
}
export default class WechatPay implements PayClazz {
channel: string;
channel: string;
constructor(channel: WechatPayChannel, appId: string, config: WechatPayConfig) {
// todo
this.channel = channel;
}
/**
* prepay接口模拟返回
* @param pay
* @param data
* @param context
*/
async prepay(pay: OpSchema, data: UpdateOperationData, context: BRC): Promise<void> {
switch (this.channel) {
case PAY_CHANNEL_WECHAT_APP_NAME:
case PAY_CHANNEL_WECHAT_JS_NAME:
case PAY_CHANNEL_WECHAT_MP_NAME: {
const prepayId = Math.random().toString();
data.externalId = prepayId;
data.meta = {
prepayId,
};
break;
}
case PAY_CHANNEL_WECHAT_H5_NAME: {
const h5Url = Math.random().toString();
data.externalId = h5Url;
data.meta = {
h5Url,
};
break;
}
case PAY_CHANNEL_WECHAT_NATIVE_NAME: {
const codeUrl = Math.random().toString();
data.externalId = codeUrl;
data.meta = {
codeUrl,
};
break;
}
};
data.timeoutAt = Date.now() + 7000 * 1000; // 微信官方文档过期时间2小时提前一点
}
async getState(pay: OpSchema): Promise<string | null | undefined> {
if (_PAY_STATE) {
return _PAY_STATE;
}
const r = Math.random();
if (r < 0.3) {
return 'paying';
}
else if (r > 0.95) {
return 'closed';
}
return 'paid';
}
async close(pay: OpSchema): Promise<void> {
}
decodeNotification(params: Record<string, string>, body: any): Promise<{ payId: string; iState: string | null | undefined; extra?: any; answer: string; }[]> {
throw new Error("Method not implemented.");
}
constructor(channel: WechatPayChannel, appId: string, config: WechatPayConfig) {
// todo
this.channel = channel;
}
prepay(pay: OpSchema, data: UpdateOperationData, context: BRC): Promise<void> {
throw new Error("Method not implemented.");
}
getState(pay: OpSchema): Promise<string | null | undefined> {
throw new Error("Method not implemented.");
}
close(pay: OpSchema): Promise<void> {
throw new Error("Method not implemented.");
}
decodeNotification(params: Record<string, string>, body: any): Promise<{ payId: string; iState: string | null | undefined; extra?: any; answer: string; }[]> {
throw new Error("Method not implemented.");
}
}

View File

@ -1,7 +1,9 @@
import { EntityDict } from '@project/oak-app-domain';
import PayClazz from '@project/types/PayClazz';
import { BRC } from '@project/types/RuntimeCxt';
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_OFFLINE_NAME, PAY_CHANNEL_WECHAT_APP_NAME, PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_MP_NAME, PAY_CHANNEL_WECHAT_NATIVE_NAME, WechatPayChannel, WechatPayConfig } from "@project/types/PayConfig";
import { EntityDict } from '../../oak-app-domain';
import PayClazz from '../../types/PayClazz';
import { BRC } from '../../types/RuntimeCxt';
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_OFFLINE_NAME, PAY_CHANNEL_WECHAT_APP_NAME,
PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_MP_NAME,
PAY_CHANNEL_WECHAT_NATIVE_NAME, WechatPayChannel, WechatPayConfig } from "../../types/PayConfig";
import assert from 'assert';
import Offline from './Offline';

View File

@ -8,7 +8,7 @@ import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { OperationResult } from 'oak-domain/lib/types';
import { mergeOperationResult } from 'oak-domain/lib/utils/operationResult';
const QUERY_PAYING_STATE_GAP = 3600 * 1000;
const QUERY_PAYING_STATE_GAP = process.env.NODE_ENV === 'production' ? 3600 * 1000 : 60 * 1000;
const watchers: Watcher<EntityDict, 'pay', BRC>[] = [
{