完善了N多pay的逻辑和component
This commit is contained in:
parent
68983bd5eb
commit
fe8caf3da5
|
|
@ -5,7 +5,11 @@ const checkers = [
|
|||
action: 'create',
|
||||
checker: (operation, context) => {
|
||||
const { data } = operation;
|
||||
data.creatorId = context.getCurrentUserId();
|
||||
if (!data.creatorId) {
|
||||
data.creatorId = context.getCurrentUserId();
|
||||
}
|
||||
data.paid = 0;
|
||||
data.refunded = 0;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ const checkers = [
|
|||
data.paid = 0;
|
||||
data.applicationId = context.getApplicationId();
|
||||
data.creatorId = context.getCurrentUserId();
|
||||
if (!data.meta) {
|
||||
data.meta = {};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default OakComponent({
|
|||
},
|
||||
actions: ['deposit', 'withdraw'],
|
||||
methods: {
|
||||
async createDepositPay(price, channel, meta, success) {
|
||||
async createDepositPay(price, channel, meta) {
|
||||
const payId = await generateNewIdAsync();
|
||||
const { oakId } = this.props;
|
||||
await this.execute(undefined, undefined, undefined, [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import { EntityDict } from "../../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "order", false, {
|
||||
accountId: string;
|
||||
accountAvailMax: number;
|
||||
onPayReady: (operation: EntityDict['order']['Update'], payId: string) => void;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import { PAY_CHANNEL_ACCOUNT_NAME } from "../../../types/PayConfig";
|
||||
import { generateNewId } from "oak-domain/lib/utils/uuid";
|
||||
export default OakComponent({
|
||||
entity: 'order',
|
||||
projection: {
|
||||
id: 1,
|
||||
price: 1,
|
||||
paid: 1,
|
||||
iState: 1,
|
||||
pay$order: {
|
||||
$entity: 'pay',
|
||||
data: {
|
||||
id: 1,
|
||||
price: 1,
|
||||
paid: 1,
|
||||
iState: 1,
|
||||
},
|
||||
filter: {
|
||||
iState: 'paying',
|
||||
},
|
||||
},
|
||||
},
|
||||
isList: false,
|
||||
properties: {
|
||||
accountId: '', // 是否可以使用帐户中的余额抵扣
|
||||
accountAvailMax: 0, // 本次交易可以使用的帐户中的Avail max值,调用者自己保证此数值的一致性,不要扣成负数
|
||||
onPayReady: (operation, payId) => undefined,
|
||||
},
|
||||
formData({ data }) {
|
||||
const payConfig = this.features.pay.getPayConfigs();
|
||||
const accountConfig = payConfig?.find(ele => ele.channel === PAY_CHANNEL_ACCOUNT_NAME);
|
||||
const payConfig2 = payConfig?.filter(ele => ele !== accountConfig);
|
||||
const activePay = data && data.pay$order?.[0];
|
||||
const { accountPrice } = this.state;
|
||||
return {
|
||||
order: data,
|
||||
activePay,
|
||||
accountConfig,
|
||||
payConfig: payConfig2,
|
||||
rest: data ? data.price - data.paid - accountPrice : 0,
|
||||
legal: !!(data?.['#oakLegalActions']?.includes('startPaying'))
|
||||
};
|
||||
},
|
||||
features: ['application'],
|
||||
data: {
|
||||
useAccount: false,
|
||||
accountPrice: 0,
|
||||
channel: '',
|
||||
meta: undefined,
|
||||
},
|
||||
methods: {
|
||||
setUseAccount(v) {
|
||||
this.setState({ useAccount: v }, () => this.tryCreatePay());
|
||||
},
|
||||
setAccountPrice(price) {
|
||||
const { order } = this.state;
|
||||
this.setState({ accountPrice: price, rest: order.price - order.paid - price }, () => this.tryCreatePay());
|
||||
},
|
||||
onPickChannel(channel) {
|
||||
this.setState({
|
||||
channel,
|
||||
}, () => this.tryCreatePay());
|
||||
},
|
||||
onSetChannelMeta(meta) {
|
||||
this.setState({
|
||||
meta,
|
||||
}, () => this.tryCreatePay());
|
||||
},
|
||||
tryCreatePay() {
|
||||
const { oakId, accountId } = this.props;
|
||||
const { useAccount, accountPrice, channel, meta, order } = this.state;
|
||||
const pays = [];
|
||||
let rest = order.price - order.paid;
|
||||
let payId = '';
|
||||
if (useAccount && accountPrice) {
|
||||
pays.push({
|
||||
id: generateNewId(),
|
||||
channel: PAY_CHANNEL_ACCOUNT_NAME,
|
||||
price: accountPrice,
|
||||
accountId,
|
||||
});
|
||||
rest = rest - accountPrice;
|
||||
}
|
||||
if (rest && channel) {
|
||||
payId = generateNewId();
|
||||
pays.push({
|
||||
id: payId,
|
||||
channel,
|
||||
meta,
|
||||
price: rest,
|
||||
});
|
||||
rest = 0;
|
||||
}
|
||||
const { onPayReady } = this.props;
|
||||
if (rest === 0) {
|
||||
onPayReady({
|
||||
id: generateNewId(),
|
||||
action: 'startPaying',
|
||||
data: {
|
||||
pay$order: pays.map(ele => ({
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: ele,
|
||||
})),
|
||||
},
|
||||
filter: {
|
||||
id: oakId,
|
||||
},
|
||||
}, payId);
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: ['startPaying'],
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"price": "订单金额",
|
||||
"choose": "请选择支付方式(%{price}元)",
|
||||
"useAccount": "使用余额抵扣",
|
||||
"accountMax": "当前可用余额为%{max}元",
|
||||
"illegalState": "订单的当前状态【%{state}】无法开始支付",
|
||||
"paying": "有一个正在支付的订单"
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { AccountPayConfig, PayConfig } from '../../../types/PayConfig';
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'order', false, {
|
||||
accountId?: string;
|
||||
accountAvailMax: number;
|
||||
order: EntityDict['order']['OpSchema'];
|
||||
activePay?: EntityDict['pay']['OpSchema'];
|
||||
accountConfig?: AccountPayConfig;
|
||||
payConfig?: PayConfig;
|
||||
accountPrice: number;
|
||||
channel?: string;
|
||||
meta?: object;
|
||||
useAccount: boolean;
|
||||
rest: number;
|
||||
legal: false;
|
||||
}, {
|
||||
setAccountPrice: (price: number) => void;
|
||||
onPickChannel: (channel: string) => void;
|
||||
onSetChannelMeta: (meta?: object) => void;
|
||||
setUseAccount: (v: boolean) => void;
|
||||
}>): React.JSX.Element | null;
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import React from 'react';
|
||||
import { ToYuan, ToCent } from 'oak-domain/lib/utils/money';
|
||||
import Styles from './web.pc.module.less';
|
||||
import PayChannelPicker from '../../pay/channelPicker';
|
||||
import { Divider, Checkbox, InputNumber, Flex, Result } from 'antd';
|
||||
function RenderPayChannel(props) {
|
||||
const { price, payConfig, t, channel, meta, onPick, onSetMeta } = props;
|
||||
return (<div className={Styles.pc1}>
|
||||
<div className={Styles.content}>
|
||||
<div>
|
||||
{t('choose', { price: ToYuan(price) })}
|
||||
</div>
|
||||
<Divider />
|
||||
<PayChannelPicker payConfig={payConfig} channel={channel} meta={meta} onPick={onPick} onSetMeta={onSetMeta}/>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
function RenderAccountPay(props) {
|
||||
const { max, t, accountPrice, setAccountPrice, useAccount, setUseAccount, accountAvail } = props;
|
||||
return (<div className={Styles.pc1}>
|
||||
<div className={Styles.content}>
|
||||
<Checkbox checked={useAccount} onChange={() => {
|
||||
setUseAccount(!useAccount);
|
||||
if (useAccount) {
|
||||
setAccountPrice(0);
|
||||
}
|
||||
}}>
|
||||
{t('useAccount')}
|
||||
</Checkbox>
|
||||
{useAccount && (<>
|
||||
<Divider />
|
||||
<Flex align='baseline'>
|
||||
<InputNumber max={ToYuan(max)} addonAfter="¥" value={typeof accountPrice === 'number' ? ToYuan(accountPrice) : null} onChange={(v) => {
|
||||
if (typeof v === 'number') {
|
||||
setAccountPrice(Math.floor(ToCent(v)));
|
||||
}
|
||||
}}/>
|
||||
<div className={Styles.tips}>{t('accountMax', { max: ToYuan(accountAvail) })}</div>
|
||||
</Flex>
|
||||
</>)}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
export default function Render(props) {
|
||||
const { accountId, accountAvailMax, legal, accountPrice, useAccount, order, activePay, payConfig, channel, meta, rest } = props.data;
|
||||
const { t, setAccountPrice, onPickChannel, onSetChannelMeta, setUseAccount } = props.methods;
|
||||
if (order) {
|
||||
if (activePay) {
|
||||
return (<Result status="warning" title={t('paying')}/>);
|
||||
}
|
||||
if (!legal) {
|
||||
return (<Result status="warning" title={t('illegalState', { state: t(`order:v.iState.${order.iState}`) })}/>);
|
||||
}
|
||||
return (<div className={Styles.container}>
|
||||
<div className={Styles.info}>
|
||||
<div className={Styles.should}>{t('price')}</div>
|
||||
<div className={Styles.price}>
|
||||
<div>{t('common::pay.symbol')}</div>
|
||||
<div>{ToYuan(order.price)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{accountId && accountAvailMax && <div className={Styles.ctrl}>
|
||||
<RenderAccountPay max={Math.min(accountAvailMax, rest + accountPrice)} t={t} setAccountPrice={setAccountPrice} useAccount={useAccount} setUseAccount={setUseAccount} accountPrice={accountPrice} accountAvail={accountAvailMax}/>
|
||||
</div>}
|
||||
{!!(rest && rest > 0) && <div className={Styles.ctrl}>
|
||||
<RenderPayChannel payConfig={payConfig} price={rest} t={t} channel={channel} meta={meta} onPick={onPickChannel} onSetMeta={onSetChannelMeta}/>
|
||||
</div>}
|
||||
</div>);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
background-color: var(--oak-bg-color-page);
|
||||
// min-height: 500px;
|
||||
|
||||
.info {
|
||||
margin: 8px;
|
||||
height: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--oak-color-info);
|
||||
|
||||
.should {
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 3px;
|
||||
font-size: xx-large;
|
||||
font-weight: bolder;
|
||||
color: var(--oak-color-primary)
|
||||
}
|
||||
}
|
||||
|
||||
.ctrl {
|
||||
margin: 8px;
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
|
||||
.pc1 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 4px;
|
||||
|
||||
.content {
|
||||
background-color: var(--oak-bg-color-container);
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
padding: 16px;
|
||||
|
||||
.tips {
|
||||
color: var(--oak-color-warning);
|
||||
font-size: small;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { PayConfig } from "../../../types/PayConfig";
|
|||
* 支持自定义支付通道的选择器注入
|
||||
* 未来遇到真正需求时再实现,by Xc 20240426
|
||||
*/
|
||||
type ExtraPicker = {
|
||||
export type ExtraPicker = {
|
||||
label: string;
|
||||
name: string;
|
||||
icon: React.ForwardRefExoticComponent<any>;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { PAY_CHANNEL_OFFLINE_NAME } from '../types/PayConfig';
|
||||
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_OFFLINE_NAME } from '../types/PayConfig';
|
||||
const attrUpdateMatrix = {
|
||||
pay: {
|
||||
meta: {
|
||||
|
|
@ -13,6 +13,15 @@ const attrUpdateMatrix = {
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
accountOper$entity: {
|
||||
actions: ['succeedPaying'],
|
||||
filter: {
|
||||
accountId: {
|
||||
$exists: true,
|
||||
},
|
||||
channel: PAY_CHANNEL_ACCOUNT_NAME,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -45,6 +45,21 @@ const i18ns = [
|
|||
position: "src/components/accountOper/list",
|
||||
data: {}
|
||||
},
|
||||
{
|
||||
id: "d39ddaee363037f4557bad511722e250",
|
||||
namespace: "oak-pay-business-c-order-pay",
|
||||
language: "zh-CN",
|
||||
module: "oak-pay-business",
|
||||
position: "src/components/order/pay",
|
||||
data: {
|
||||
"price": "订单金额",
|
||||
"choose": "请选择支付方式(%{price}元)",
|
||||
"useAccount": "使用余额抵扣",
|
||||
"accountMax": "当前可用余额为%{max}元",
|
||||
"illegalState": "订单的当前状态【%{state}】无法开始支付",
|
||||
"paying": "有一个正在支付的订单"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "3500cc465492fca3797b75c9c0dbf517",
|
||||
namespace: "oak-pay-business-c-pay-channelPicker",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
;
|
||||
export const IActionDef = {
|
||||
stm: {
|
||||
startPaying: ['unpaid', 'paying'],
|
||||
startPaying: [['unpaid', 'partiallyPaid'], 'paying'],
|
||||
payAll: [['unpaid', 'paying', 'partiallyPaid'], 'paid'],
|
||||
payPartially: [['unpaid', 'paying'], 'partiallyPaid'],
|
||||
payNone: ['paying', 'unpaid'],
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ export default class Pay extends Feature {
|
|||
private application;
|
||||
constructor(application: GeneralFeatures<EntityDict>['application']);
|
||||
getPayChannels(): string[];
|
||||
getPayConfigs(): (import("../types/PayConfig").AccountPayConfig | import("../types/PayConfig").WechatPayConfig | import("../types/PayConfig").OfflinePayConfig)[];
|
||||
getPayConfigs(): (import("../types/PayConfig").WechatPayConfig | import("../types/PayConfig").AccountPayConfig | import("../types/PayConfig").OfflinePayConfig)[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const IActionDef = {
|
||||
stm: {
|
||||
startPaying: ['unpaid', 'paying'],
|
||||
startPaying: [['unpaid', 'partiallyPaid'], 'paying'],
|
||||
payAll: [['unpaid', 'paying', 'partiallyPaid'], 'paid'],
|
||||
payPartially: [['unpaid', 'paying'], 'partiallyPaid'],
|
||||
payNone: ['paying', 'unpaid'],
|
||||
|
|
|
|||
|
|
@ -50,91 +50,95 @@ async function changeOrderStateByPay(filter, context, option) {
|
|||
payRefunded += refunded;
|
||||
}
|
||||
}
|
||||
if (hasPaying) {
|
||||
// 一定是在支付状态
|
||||
assert(!hasRefunding && payRefunded === 0 && payPaid < orderPrice);
|
||||
if (orderIState !== 'paying' || orderPaid !== payPaid) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'startPaying',
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
const closeFn = context.openRootMode();
|
||||
try {
|
||||
if (hasPaying) {
|
||||
// 一定是在支付状态
|
||||
assert(!hasRefunding && payRefunded === 0 && payPaid < orderPrice);
|
||||
if (orderIState !== 'paying' || orderPaid !== payPaid) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'startPaying',
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else if (hasRefunding) {
|
||||
assert(!hasPaying && payPaid === orderPrice && payPaid === orderPaid && payRefunded < orderPrice);
|
||||
if (orderIState !== 'refunding' || orderRefunded !== payRefunded) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'startRefunding',
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else if (payRefunded) {
|
||||
// 已经在退款流程当中
|
||||
assert(payPaid === orderPrice && payPaid === orderPaid);
|
||||
const iState = payPaid === orderPrice ? 'refunded' : 'partiallyRefunded';
|
||||
if (orderIState !== iState || orderRefunded !== payRefunded) {
|
||||
const action = payPaid === orderPrice ? 'refundAll' : 'refundPartially';
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action,
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else if (payPaid) {
|
||||
// 在支付流程当中
|
||||
assert(orderRefunded === 0);
|
||||
const iState = payPaid === orderPrice ? 'paid' : 'partiallyPaid';
|
||||
if (orderIState !== iState || orderPaid !== payPaid) {
|
||||
const action = payPaid === orderPrice ? 'payAll' : 'payPartially';
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action,
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const iState = 'unpaid';
|
||||
assert(orderRefunded === 0 && orderPaid === 0);
|
||||
if (orderIState !== iState) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'payNone',
|
||||
data: {},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
closeFn();
|
||||
return 1;
|
||||
}
|
||||
else if (hasRefunding) {
|
||||
assert(!hasPaying && payPaid === orderPrice && payPaid === orderPaid && payRefunded < orderPrice);
|
||||
if (orderIState !== 'refunding' || orderRefunded !== payRefunded) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'startRefunding',
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else if (payRefunded) {
|
||||
// 已经在退款流程当中
|
||||
assert(payPaid === orderPrice && payPaid === orderPaid);
|
||||
const iState = payPaid === orderPrice ? 'refunded' : 'partiallyRefunded';
|
||||
if (orderIState !== iState || orderRefunded !== payRefunded) {
|
||||
const action = payPaid === orderPrice ? 'refundAll' : 'refundPartially';
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action,
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else if (payPaid) {
|
||||
// 在支付流程当中
|
||||
assert(orderRefunded === 0);
|
||||
const iState = payPaid === orderPrice ? 'paid' : 'partiallyPaid';
|
||||
if (orderIState !== iState || orderPaid !== payPaid) {
|
||||
const action = payPaid === orderPrice ? 'payAll' : 'payPartially';
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action,
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const iState = 'unpaid';
|
||||
assert(orderRefunded === 0 && orderPaid === 0);
|
||||
if (orderIState !== iState) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'payNone',
|
||||
data: {},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
catch (err) {
|
||||
closeFn();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const triggers = [
|
||||
|
|
@ -147,32 +151,42 @@ const triggers = [
|
|||
const { data } = operation;
|
||||
assert(!(data instanceof Array));
|
||||
const { accountId, price, orderId, id } = data;
|
||||
if (orderId) {
|
||||
/* if (orderId) {
|
||||
if (accountId) {
|
||||
// 使用帐户支付,直接成功并扣款
|
||||
await context.operate('pay', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'payAll',
|
||||
data: {
|
||||
accountOper$entity: [
|
||||
{
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
const close = context.openRootMode();
|
||||
try {
|
||||
await context.operate('pay', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'succeedPaying',
|
||||
data: {
|
||||
accountOper$entity: [
|
||||
{
|
||||
id: await generateNewIdAsync(),
|
||||
totalPlus: -price,
|
||||
availPlus: -price,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
}
|
||||
}, option);
|
||||
action: 'create',
|
||||
data: {
|
||||
id: await generateNewIdAsync(),
|
||||
totalPlus: -price!,
|
||||
availPlus: -price!,
|
||||
type: 'consume',
|
||||
accountId,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
}
|
||||
}, option);
|
||||
close();
|
||||
}
|
||||
catch (err: any) {
|
||||
close();
|
||||
throw err;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
} */
|
||||
// 其余情况都是进入paying流程
|
||||
await context.operate('pay', {
|
||||
id: await generateNewIdAsync(),
|
||||
|
|
|
|||
|
|
@ -18,9 +18,13 @@ export default class Account {
|
|||
id: await generateNewIdAsync(),
|
||||
totalPlus: -price,
|
||||
availPlus: -price,
|
||||
type: 'consume',
|
||||
accountId,
|
||||
},
|
||||
}
|
||||
];
|
||||
data.meta = {};
|
||||
data.paid = pay.price;
|
||||
}
|
||||
getState(pay) {
|
||||
throw new Error("account类型的pay不应该需要查询此状态");
|
||||
|
|
|
|||
|
|
@ -24,11 +24,12 @@ const watchers = [
|
|||
fn: async (context, data) => {
|
||||
const results = [];
|
||||
for (const pay of data) {
|
||||
const { applicationId, channel, timeoutAt } = pay;
|
||||
const { applicationId, channel, timeoutAt, price } = pay;
|
||||
const clazz = await getPayClazz(applicationId, channel, context);
|
||||
const iState = await clazz.getState(pay);
|
||||
if (iState !== pay.iState) {
|
||||
let action = 'close';
|
||||
const data2 = {};
|
||||
switch (iState) {
|
||||
case 'closed': {
|
||||
// action = 'close';
|
||||
|
|
@ -36,6 +37,7 @@ const watchers = [
|
|||
}
|
||||
case 'paid': {
|
||||
action = 'succeedPaying';
|
||||
data2.paid = price;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
|
@ -45,7 +47,7 @@ const watchers = [
|
|||
const result = await context.operate('pay', {
|
||||
id: await generateNewIdAsync(),
|
||||
action,
|
||||
data: {},
|
||||
data: data2,
|
||||
filter: {
|
||||
id: pay.id,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ const checkers = [
|
|||
action: 'create',
|
||||
checker: (operation, context) => {
|
||||
const { data } = operation;
|
||||
data.creatorId = context.getCurrentUserId();
|
||||
if (!data.creatorId) {
|
||||
data.creatorId = context.getCurrentUserId();
|
||||
}
|
||||
data.paid = 0;
|
||||
data.refunded = 0;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ const checkers = [
|
|||
data.paid = 0;
|
||||
data.applicationId = context.getApplicationId();
|
||||
data.creatorId = context.getCurrentUserId();
|
||||
if (!data.meta) {
|
||||
data.meta = {};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,6 +15,15 @@ const attrUpdateMatrix = {
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
accountOper$entity: {
|
||||
actions: ['succeedPaying'],
|
||||
filter: {
|
||||
accountId: {
|
||||
$exists: true,
|
||||
},
|
||||
channel: PayConfig_1.PAY_CHANNEL_ACCOUNT_NAME,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,6 +47,21 @@ const i18ns = [
|
|||
position: "src/components/accountOper/list",
|
||||
data: {}
|
||||
},
|
||||
{
|
||||
id: "d39ddaee363037f4557bad511722e250",
|
||||
namespace: "oak-pay-business-c-order-pay",
|
||||
language: "zh-CN",
|
||||
module: "oak-pay-business",
|
||||
position: "src/components/order/pay",
|
||||
data: {
|
||||
"price": "订单金额",
|
||||
"choose": "请选择支付方式(%{price}元)",
|
||||
"useAccount": "使用余额抵扣",
|
||||
"accountMax": "当前可用余额为%{max}元",
|
||||
"illegalState": "订单的当前状态【%{state}】无法开始支付",
|
||||
"paying": "有一个正在支付的订单"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "3500cc465492fca3797b75c9c0dbf517",
|
||||
namespace: "oak-pay-business-c-pay-channelPicker",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ exports.entityDesc = exports.IActionDef = void 0;
|
|||
;
|
||||
exports.IActionDef = {
|
||||
stm: {
|
||||
startPaying: ['unpaid', 'paying'],
|
||||
startPaying: [['unpaid', 'partiallyPaid'], 'paying'],
|
||||
payAll: [['unpaid', 'paying', 'partiallyPaid'], 'paid'],
|
||||
payPartially: [['unpaid', 'paying'], 'partiallyPaid'],
|
||||
payNone: ['paying', 'unpaid'],
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.actionDefDict = exports.actions = exports.IActionDef = void 0;
|
||||
exports.IActionDef = {
|
||||
stm: {
|
||||
startPaying: ['unpaid', 'paying'],
|
||||
startPaying: [['unpaid', 'partiallyPaid'], 'paying'],
|
||||
payAll: [['unpaid', 'paying', 'partiallyPaid'], 'paid'],
|
||||
payPartially: [['unpaid', 'paying'], 'partiallyPaid'],
|
||||
payNone: ['paying', 'unpaid'],
|
||||
|
|
|
|||
|
|
@ -53,91 +53,95 @@ async function changeOrderStateByPay(filter, context, option) {
|
|||
payRefunded += refunded;
|
||||
}
|
||||
}
|
||||
if (hasPaying) {
|
||||
// 一定是在支付状态
|
||||
(0, assert_1.default)(!hasRefunding && payRefunded === 0 && payPaid < orderPrice);
|
||||
if (orderIState !== 'paying' || orderPaid !== payPaid) {
|
||||
await context.operate('order', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: 'startPaying',
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
const closeFn = context.openRootMode();
|
||||
try {
|
||||
if (hasPaying) {
|
||||
// 一定是在支付状态
|
||||
(0, assert_1.default)(!hasRefunding && payRefunded === 0 && payPaid < orderPrice);
|
||||
if (orderIState !== 'paying' || orderPaid !== payPaid) {
|
||||
await context.operate('order', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: 'startPaying',
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else if (hasRefunding) {
|
||||
(0, assert_1.default)(!hasPaying && payPaid === orderPrice && payPaid === orderPaid && payRefunded < orderPrice);
|
||||
if (orderIState !== 'refunding' || orderRefunded !== payRefunded) {
|
||||
await context.operate('order', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: 'startRefunding',
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else if (payRefunded) {
|
||||
// 已经在退款流程当中
|
||||
(0, assert_1.default)(payPaid === orderPrice && payPaid === orderPaid);
|
||||
const iState = payPaid === orderPrice ? 'refunded' : 'partiallyRefunded';
|
||||
if (orderIState !== iState || orderRefunded !== payRefunded) {
|
||||
const action = payPaid === orderPrice ? 'refundAll' : 'refundPartially';
|
||||
await context.operate('order', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action,
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else if (payPaid) {
|
||||
// 在支付流程当中
|
||||
(0, assert_1.default)(orderRefunded === 0);
|
||||
const iState = payPaid === orderPrice ? 'paid' : 'partiallyPaid';
|
||||
if (orderIState !== iState || orderPaid !== payPaid) {
|
||||
const action = payPaid === orderPrice ? 'payAll' : 'payPartially';
|
||||
await context.operate('order', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action,
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const iState = 'unpaid';
|
||||
(0, assert_1.default)(orderRefunded === 0 && orderPaid === 0);
|
||||
if (orderIState !== iState) {
|
||||
await context.operate('order', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: 'payNone',
|
||||
data: {},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
closeFn();
|
||||
return 1;
|
||||
}
|
||||
else if (hasRefunding) {
|
||||
(0, assert_1.default)(!hasPaying && payPaid === orderPrice && payPaid === orderPaid && payRefunded < orderPrice);
|
||||
if (orderIState !== 'refunding' || orderRefunded !== payRefunded) {
|
||||
await context.operate('order', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: 'startRefunding',
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else if (payRefunded) {
|
||||
// 已经在退款流程当中
|
||||
(0, assert_1.default)(payPaid === orderPrice && payPaid === orderPaid);
|
||||
const iState = payPaid === orderPrice ? 'refunded' : 'partiallyRefunded';
|
||||
if (orderIState !== iState || orderRefunded !== payRefunded) {
|
||||
const action = payPaid === orderPrice ? 'refundAll' : 'refundPartially';
|
||||
await context.operate('order', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action,
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else if (payPaid) {
|
||||
// 在支付流程当中
|
||||
(0, assert_1.default)(orderRefunded === 0);
|
||||
const iState = payPaid === orderPrice ? 'paid' : 'partiallyPaid';
|
||||
if (orderIState !== iState || orderPaid !== payPaid) {
|
||||
const action = payPaid === orderPrice ? 'payAll' : 'payPartially';
|
||||
await context.operate('order', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action,
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const iState = 'unpaid';
|
||||
(0, assert_1.default)(orderRefunded === 0 && orderPaid === 0);
|
||||
if (orderIState !== iState) {
|
||||
await context.operate('order', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: 'payNone',
|
||||
data: {},
|
||||
filter: {
|
||||
id: orderId,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
catch (err) {
|
||||
closeFn();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const triggers = [
|
||||
|
|
@ -150,32 +154,42 @@ const triggers = [
|
|||
const { data } = operation;
|
||||
(0, assert_1.default)(!(data instanceof Array));
|
||||
const { accountId, price, orderId, id } = data;
|
||||
if (orderId) {
|
||||
/* if (orderId) {
|
||||
if (accountId) {
|
||||
// 使用帐户支付,直接成功并扣款
|
||||
await context.operate('pay', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: 'payAll',
|
||||
data: {
|
||||
accountOper$entity: [
|
||||
{
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
totalPlus: -price,
|
||||
availPlus: -price,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
}
|
||||
}, option);
|
||||
const close = context.openRootMode();
|
||||
try {
|
||||
await context.operate('pay', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'succeedPaying',
|
||||
data: {
|
||||
accountOper$entity: [
|
||||
{
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: await generateNewIdAsync(),
|
||||
totalPlus: -price!,
|
||||
availPlus: -price!,
|
||||
type: 'consume',
|
||||
accountId,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
}
|
||||
}, option);
|
||||
close();
|
||||
}
|
||||
catch (err: any) {
|
||||
close();
|
||||
throw err;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
} */
|
||||
// 其余情况都是进入paying流程
|
||||
await context.operate('pay', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
|
|
|
|||
|
|
@ -21,9 +21,13 @@ class Account {
|
|||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
totalPlus: -price,
|
||||
availPlus: -price,
|
||||
type: 'consume',
|
||||
accountId,
|
||||
},
|
||||
}
|
||||
];
|
||||
data.meta = {};
|
||||
data.paid = pay.price;
|
||||
}
|
||||
getState(pay) {
|
||||
throw new Error("account类型的pay不应该需要查询此状态");
|
||||
|
|
|
|||
|
|
@ -27,11 +27,12 @@ const watchers = [
|
|||
fn: async (context, data) => {
|
||||
const results = [];
|
||||
for (const pay of data) {
|
||||
const { applicationId, channel, timeoutAt } = pay;
|
||||
const { applicationId, channel, timeoutAt, price } = pay;
|
||||
const clazz = await (0, payClazz_1.getPayClazz)(applicationId, channel, context);
|
||||
const iState = await clazz.getState(pay);
|
||||
if (iState !== pay.iState) {
|
||||
let action = 'close';
|
||||
const data2 = {};
|
||||
switch (iState) {
|
||||
case 'closed': {
|
||||
// action = 'close';
|
||||
|
|
@ -39,6 +40,7 @@ const watchers = [
|
|||
}
|
||||
case 'paid': {
|
||||
action = 'succeedPaying';
|
||||
data2.paid = price;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
|
@ -48,7 +50,7 @@ const watchers = [
|
|||
const result = await context.operate('pay', {
|
||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||
action,
|
||||
data: {},
|
||||
data: data2,
|
||||
filter: {
|
||||
id: pay.id,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ const checkers: Checker<EntityDict, 'order', RuntimeCxt>[] = [
|
|||
action: 'create',
|
||||
checker: (operation, context) => {
|
||||
const { data } = operation as EntityDict['order']['CreateSingle'];
|
||||
data.creatorId = context.getCurrentUserId()!;
|
||||
if (!data.creatorId) {
|
||||
data.creatorId = context.getCurrentUserId()!;
|
||||
}
|
||||
data.paid = 0;
|
||||
data.refunded = 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ const checkers: Checker<EntityDict, 'pay', RuntimeCxt>[] = [
|
|||
data.paid = 0;
|
||||
data.applicationId = context.getApplicationId()!;
|
||||
data.creatorId = context.getCurrentUserId()!;
|
||||
if (!data.meta) {
|
||||
data.meta = {};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Input, Radio, Space, Form, Select, Flex } from 'antd';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '@project/oak-app-domain';
|
||||
import Styles from './web.pc.module.less';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'accountOper', false, {
|
||||
accountOpers: RowWithActions<EntityDict, 'accountOper'>[];
|
||||
}>) {
|
||||
const { accountOpers } = props.data;
|
||||
const { t } = props.methods;
|
||||
return '还没有实现,todo';
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import { EntityDict } from "@project/oak-app-domain";
|
||||
import { PAY_CHANNEL_ACCOUNT_NAME } from "../../../types/PayConfig";
|
||||
import { generateNewId, generateNewIdAsync } from "oak-domain/lib/utils/uuid";
|
||||
import assert from 'assert';
|
||||
|
||||
export default OakComponent({
|
||||
entity: 'order',
|
||||
projection: {
|
||||
id: 1,
|
||||
price: 1,
|
||||
paid: 1,
|
||||
iState: 1,
|
||||
pay$order: {
|
||||
$entity: 'pay',
|
||||
data: {
|
||||
id: 1,
|
||||
price: 1,
|
||||
paid: 1,
|
||||
iState: 1,
|
||||
},
|
||||
filter: {
|
||||
iState: 'paying',
|
||||
},
|
||||
},
|
||||
},
|
||||
isList: false,
|
||||
properties: {
|
||||
accountId: '', // 是否可以使用帐户中的余额抵扣
|
||||
accountAvailMax: 0, // 本次交易可以使用的帐户中的Avail max值,调用者自己保证此数值的一致性,不要扣成负数
|
||||
onPayReady: (operation: EntityDict['order']['Update'], payId: string) => undefined as void,
|
||||
},
|
||||
formData({ data }) {
|
||||
const payConfig = this.features.pay.getPayConfigs();
|
||||
const accountConfig = payConfig?.find(ele => ele.channel === PAY_CHANNEL_ACCOUNT_NAME);
|
||||
const payConfig2 = payConfig?.filter(
|
||||
ele => ele !== accountConfig
|
||||
);
|
||||
|
||||
const activePay = data && data.pay$order?.[0];
|
||||
const { accountPrice } = this.state;
|
||||
return {
|
||||
order: data,
|
||||
activePay,
|
||||
accountConfig,
|
||||
payConfig: payConfig2,
|
||||
rest: data ? data.price! - data.paid! - accountPrice : 0,
|
||||
legal: !!(data?.['#oakLegalActions']?.includes('startPaying'))
|
||||
};
|
||||
},
|
||||
features: ['application'],
|
||||
data: {
|
||||
useAccount: false,
|
||||
accountPrice: 0,
|
||||
channel: '',
|
||||
meta: undefined as undefined | object,
|
||||
},
|
||||
methods: {
|
||||
setUseAccount(v: boolean) {
|
||||
const { accountAvailMax } = this.props;
|
||||
const { order } = this.state;
|
||||
const accountMaxPrice = Math.min(accountAvailMax!, order!.price!);
|
||||
this.setState({
|
||||
useAccount: v,
|
||||
accountPrice: accountMaxPrice,
|
||||
rest: order.price! - accountMaxPrice,
|
||||
}, () => this.tryCreatePay());
|
||||
},
|
||||
setAccountPrice(price: number) {
|
||||
const { order } = this.state;
|
||||
this.setState({ accountPrice: price, rest: order!.price! - order!.paid! - price }, () => this.tryCreatePay());
|
||||
},
|
||||
onPickChannel(channel: string) {
|
||||
this.setState({
|
||||
channel,
|
||||
}, () => this.tryCreatePay());
|
||||
},
|
||||
onSetChannelMeta(meta?: object) {
|
||||
this.setState({
|
||||
meta,
|
||||
}, () => this.tryCreatePay());
|
||||
},
|
||||
tryCreatePay() {
|
||||
const { oakId, accountId } = this.props;
|
||||
const { useAccount, accountPrice, channel, meta, order } = this.state;
|
||||
const pays: Partial<EntityDict['pay']['CreateSingle']['data']>[] = [];
|
||||
let rest = order!.price! - order!.paid!;
|
||||
|
||||
let payId = '';
|
||||
if (useAccount && accountPrice) {
|
||||
pays.push({
|
||||
id: generateNewId(),
|
||||
channel: PAY_CHANNEL_ACCOUNT_NAME,
|
||||
price: accountPrice,
|
||||
accountId,
|
||||
});
|
||||
rest = rest - accountPrice;
|
||||
}
|
||||
if (rest && channel) {
|
||||
payId = generateNewId();
|
||||
pays.push({
|
||||
id: payId,
|
||||
channel,
|
||||
meta,
|
||||
price: rest,
|
||||
});
|
||||
rest = 0;
|
||||
}
|
||||
const { onPayReady } = this.props;
|
||||
if (rest === 0) {
|
||||
onPayReady!({
|
||||
id: generateNewId(),
|
||||
action: 'startPaying',
|
||||
data: {
|
||||
pay$order: pays.map(
|
||||
ele => ({
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: ele,
|
||||
})
|
||||
),
|
||||
},
|
||||
filter: {
|
||||
id: oakId,
|
||||
},
|
||||
}, payId);
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: ['startPaying'],
|
||||
})
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
.info {
|
||||
margin: 8px;
|
||||
height: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--oak-color-info);
|
||||
|
||||
.should {
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 3px;
|
||||
font-size: xx-large;
|
||||
font-weight: bolder;
|
||||
color: var(--oak-color-primary);
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import Styles from './info.module.less';
|
||||
|
||||
export default function Info(props: { price: number, t: (k: string) => string }) {
|
||||
const { price, t } = props;
|
||||
return (
|
||||
<div className={Styles.info}>
|
||||
<div className={Styles.should}>{t('price')}</div>
|
||||
<div className={Styles.price}>
|
||||
<div>{t('common::pay.symbol')}</div>
|
||||
<div>{price}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"price": "订单金额",
|
||||
"choose": "请选择支付方式(%{price}元)",
|
||||
"useAccount": "使用余额抵扣",
|
||||
"accountMax": "当前可用余额为%{max}元",
|
||||
"illegalState": "订单的当前状态【%{state}】无法开始支付",
|
||||
"paying": "有一个正在支付的订单"
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
background-color: var(--oak-bg-color-page);
|
||||
// min-height: 500px;
|
||||
|
||||
.ctrl {
|
||||
margin: 8px;
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
|
||||
.pc1 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 4px;
|
||||
|
||||
.content {
|
||||
background-color: var(--oak-bg-color-container);
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
padding: 16px;
|
||||
|
||||
.tips {
|
||||
color: var(--oak-color-warning);
|
||||
font-size: small;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
background-color: var(--oak-bg-color-page);
|
||||
// min-height: 500px;
|
||||
|
||||
.ctrl {
|
||||
margin: 8px;
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
|
||||
.pc1 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 4px;
|
||||
|
||||
.content {
|
||||
background-color: var(--oak-bg-color-container);
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
padding: 16px;
|
||||
|
||||
.tips {
|
||||
color: var(--oak-color-warning);
|
||||
font-size: small;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
import React, { useState } from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { ToYuan, ToCent } from 'oak-domain/lib/utils/money';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { AccountPayConfig, PayConfig } from '../../../types/PayConfig';
|
||||
import Styles from './web.pc.module.less';
|
||||
import PayChannelPicker from '../../pay/channelPicker';
|
||||
import { Divider, Checkbox, InputNumber, Flex, Result } from 'antd';
|
||||
import Info from './info';
|
||||
|
||||
function RenderPayChannel(props: {
|
||||
payConfig?: PayConfig;
|
||||
price: number;
|
||||
t: (k: string, params?: any) => string;
|
||||
channel?: string;
|
||||
meta?: object;
|
||||
onPick: (channel: string) => void;
|
||||
onSetMeta: (meta?: object) => void;
|
||||
}) {
|
||||
const { price, payConfig, t, channel, meta, onPick, onSetMeta } = props;
|
||||
return (
|
||||
<div className={Styles.pc1}>
|
||||
<div className={Styles.content}>
|
||||
<div>
|
||||
{t('choose', { price: ToYuan(price) })}
|
||||
</div>
|
||||
<Divider />
|
||||
<PayChannelPicker
|
||||
payConfig={payConfig}
|
||||
channel={channel}
|
||||
meta={meta}
|
||||
onPick={onPick}
|
||||
onSetMeta={onSetMeta}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderAccountPay(props: {
|
||||
max: number;
|
||||
t: (k: string, params?: any) => string;
|
||||
setAccountPrice: (price: number) => void;
|
||||
useAccount: boolean;
|
||||
accountPrice: number;
|
||||
accountAvail: number;
|
||||
setUseAccount: (v: boolean) => void;
|
||||
}) {
|
||||
const { max, t, accountPrice, setAccountPrice, useAccount, setUseAccount, accountAvail } = props;
|
||||
|
||||
return (
|
||||
<div className={Styles.pc1}>
|
||||
<div className={Styles.content}>
|
||||
<Checkbox
|
||||
checked={useAccount}
|
||||
onChange={() => {
|
||||
setUseAccount(!useAccount);
|
||||
if (useAccount) {
|
||||
setAccountPrice(0);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('useAccount')}
|
||||
</Checkbox>
|
||||
{
|
||||
useAccount && (
|
||||
<>
|
||||
<Divider />
|
||||
<Flex align='baseline'>
|
||||
<InputNumber
|
||||
max={ToYuan(max)}
|
||||
addonAfter="¥"
|
||||
value={typeof accountPrice === 'number' ? ToYuan(accountPrice) : null}
|
||||
onChange={(v) => {
|
||||
if (typeof v === 'number') {
|
||||
setAccountPrice(Math.floor(ToCent(v)));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className={Styles.tips}>{t('accountMax', { max: ToYuan(accountAvail) })}</div>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'order', false, {
|
||||
accountId?: string;
|
||||
accountAvailMax: number;
|
||||
order: EntityDict['order']['OpSchema'];
|
||||
activePay?: EntityDict['pay']['OpSchema'];
|
||||
accountConfig?: AccountPayConfig;
|
||||
payConfig?: PayConfig;
|
||||
accountPrice: number;
|
||||
channel?: string;
|
||||
meta?: object;
|
||||
useAccount: boolean;
|
||||
rest: number;
|
||||
legal: false;
|
||||
}, {
|
||||
setAccountPrice: (price: number) => void;
|
||||
onPickChannel: (channel: string) => void;
|
||||
onSetChannelMeta: (meta?: object) => void;
|
||||
setUseAccount: (v: boolean) => void;
|
||||
}>) {
|
||||
const { accountId, accountAvailMax, legal, accountPrice, useAccount,
|
||||
order, activePay, payConfig, channel, meta, rest } = props.data;
|
||||
const { t, setAccountPrice, onPickChannel, onSetChannelMeta, setUseAccount } = props.methods;
|
||||
|
||||
if (order) {
|
||||
if (activePay) {
|
||||
return (
|
||||
<Result
|
||||
status="warning"
|
||||
title={t('paying')}
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
||||
if (!legal) {
|
||||
return (
|
||||
<Result
|
||||
status="warning"
|
||||
title={t('illegalState', { state: t(`order:v.iState.${order.iState}`) })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={Styles.container}>
|
||||
<Info
|
||||
t={t}
|
||||
price={ToYuan(order.price!)}
|
||||
/>
|
||||
{
|
||||
accountId && accountAvailMax && <div className={Styles.ctrl}>
|
||||
<RenderAccountPay
|
||||
max={Math.min(accountAvailMax, rest + accountPrice)}
|
||||
t={t}
|
||||
setAccountPrice={setAccountPrice}
|
||||
useAccount={useAccount}
|
||||
setUseAccount={setUseAccount}
|
||||
accountPrice={accountPrice}
|
||||
accountAvail={accountAvailMax}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!!(rest && rest > 0) && <div className={Styles.ctrl}>
|
||||
<RenderPayChannel
|
||||
payConfig={payConfig}
|
||||
price={rest}
|
||||
t={t}
|
||||
channel={channel}
|
||||
meta={meta}
|
||||
onPick={onPickChannel}
|
||||
onSetMeta={onSetChannelMeta}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import React, { useState } from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { ToYuan, ToCent } from 'oak-domain/lib/utils/money';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { AccountPayConfig, PayConfig } from '../../../types/PayConfig';
|
||||
import Styles from './web.mobile.module.less';
|
||||
import PayChannelPicker from '../../pay/channelPicker';
|
||||
import { InputNumber } from 'antd';
|
||||
import { Checkbox, Divider, ErrorBlock } from 'antd-mobile';
|
||||
import Info from './info';
|
||||
|
||||
function RenderPayChannel(props: {
|
||||
payConfig?: PayConfig;
|
||||
price: number;
|
||||
t: (k: string, params?: any) => string;
|
||||
channel?: string;
|
||||
meta?: object;
|
||||
onPick: (channel: string) => void;
|
||||
onSetMeta: (meta?: object) => void;
|
||||
}) {
|
||||
const { price, payConfig, t, channel, meta, onPick, onSetMeta } = props;
|
||||
return (
|
||||
<div className={Styles.pc1}>
|
||||
<div className={Styles.content}>
|
||||
<div>
|
||||
{t('choose', { price: ToYuan(price) })}
|
||||
</div>
|
||||
<Divider />
|
||||
<PayChannelPicker
|
||||
payConfig={payConfig}
|
||||
channel={channel}
|
||||
meta={meta}
|
||||
onPick={onPick}
|
||||
onSetMeta={onSetMeta}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderAccountPay(props: {
|
||||
max: number;
|
||||
t: (k: string, params?: any) => string;
|
||||
setAccountPrice: (price: number) => void;
|
||||
useAccount: boolean;
|
||||
accountPrice: number;
|
||||
accountAvail: number;
|
||||
setUseAccount: (v: boolean) => void;
|
||||
}) {
|
||||
const { max, t, accountPrice, setAccountPrice, useAccount, setUseAccount, accountAvail } = props;
|
||||
|
||||
return (
|
||||
<div className={Styles.pc1}>
|
||||
<div className={Styles.content}>
|
||||
<Checkbox
|
||||
checked={useAccount}
|
||||
onChange={() => {
|
||||
setUseAccount(!useAccount);
|
||||
if (useAccount) {
|
||||
setAccountPrice(0);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('useAccount')}
|
||||
</Checkbox>
|
||||
{
|
||||
useAccount && (
|
||||
<>
|
||||
<Divider />
|
||||
<div>
|
||||
<InputNumber
|
||||
max={ToYuan(max)}
|
||||
addonAfter="¥"
|
||||
value={typeof accountPrice === 'number' ? ToYuan(accountPrice) : null}
|
||||
onChange={(v) => {
|
||||
if (typeof v === 'number') {
|
||||
setAccountPrice(Math.floor(ToCent(v)));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className={Styles.tips}>{t('accountMax', { max: ToYuan(accountAvail) })}</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'order', false, {
|
||||
accountId?: string;
|
||||
accountAvailMax: number;
|
||||
order: EntityDict['order']['OpSchema'];
|
||||
activePay?: EntityDict['pay']['OpSchema'];
|
||||
accountConfig?: AccountPayConfig;
|
||||
payConfig?: PayConfig;
|
||||
accountPrice: number;
|
||||
channel?: string;
|
||||
meta?: object;
|
||||
useAccount: boolean;
|
||||
rest: number;
|
||||
legal: false;
|
||||
}, {
|
||||
setAccountPrice: (price: number) => void;
|
||||
onPickChannel: (channel: string) => void;
|
||||
onSetChannelMeta: (meta?: object) => void;
|
||||
setUseAccount: (v: boolean) => void;
|
||||
}>) {
|
||||
const { accountId, accountAvailMax, legal, accountPrice, useAccount,
|
||||
order, activePay, payConfig, channel, meta, rest } = props.data;
|
||||
const { t, setAccountPrice, onPickChannel, onSetChannelMeta, setUseAccount } = props.methods;
|
||||
|
||||
if (order) {
|
||||
if (activePay) {
|
||||
return (
|
||||
<ErrorBlock
|
||||
status="default"
|
||||
title={t('paying')}
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
||||
if (!legal) {
|
||||
return (
|
||||
<ErrorBlock
|
||||
status="default"
|
||||
title={t('illegalState', { state: t(`order:v.iState.${order.iState}`) })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={Styles.container}>
|
||||
<Info
|
||||
t={t}
|
||||
price={ToYuan(order.price!)}
|
||||
/>
|
||||
{
|
||||
accountId && accountAvailMax && <div className={Styles.ctrl}>
|
||||
<RenderAccountPay
|
||||
max={Math.min(accountAvailMax, rest + accountPrice)}
|
||||
t={t}
|
||||
setAccountPrice={setAccountPrice}
|
||||
useAccount={useAccount}
|
||||
setUseAccount={setUseAccount}
|
||||
accountPrice={accountPrice}
|
||||
accountAvail={accountAvailMax}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!!(rest && rest > 0) && <div className={Styles.ctrl}>
|
||||
<RenderPayChannel
|
||||
payConfig={payConfig}
|
||||
price={rest}
|
||||
t={t}
|
||||
channel={channel}
|
||||
meta={meta}
|
||||
onPick={onPickChannel}
|
||||
onSetMeta={onSetChannelMeta}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import assert from 'assert';
|
|||
* 支持自定义支付通道的选择器注入
|
||||
* 未来遇到真正需求时再实现,by Xc 20240426
|
||||
*/
|
||||
type ExtraPicker = {
|
||||
export type ExtraPicker = {
|
||||
label: string;
|
||||
name: string;
|
||||
icon: React.ForwardRefExoticComponent<any>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
.span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
.radio {
|
||||
height: 40px;
|
||||
}
|
||||
|
|
@ -78,6 +78,7 @@ export default function Render(props: WebComponentProps<EntityDict, keyof Entity
|
|||
payConfig.map(
|
||||
(v) => (
|
||||
<Radio
|
||||
className={Styles.radio}
|
||||
value={v.channel}
|
||||
key={v.channel}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export default OakComponent({
|
|||
application,
|
||||
iStateColor,
|
||||
payConfig,
|
||||
closable: !!(data?.["#oakLegalActions"]?.includes('close')),
|
||||
};
|
||||
},
|
||||
features: ['application'],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
.container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.qrCodeTips {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-size: var(--oak-font-size-headline-medium);
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
.padding {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.btnItem {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
|||
import { EntityDict } from '@project/oak-app-domain';
|
||||
import { CentToString } from 'oak-domain/lib/utils/money';
|
||||
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);
|
||||
|
|
@ -14,7 +13,6 @@ import {
|
|||
PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_MP_NAME,
|
||||
PAY_CHANNEL_WECHAT_NATIVE_NAME, PayConfig
|
||||
} from '../../../types/PayConfig';
|
||||
import { WechatOutlined, MoneyCollectOutlined, WalletOutlined } from '@ant-design/icons';
|
||||
|
||||
export function RenderOffline(props: {
|
||||
pay: RowWithActions<EntityDict, 'pay'>,
|
||||
|
|
@ -34,7 +32,7 @@ export function RenderOffline(props: {
|
|||
style={{ width: '100%', marginTop: 12 }}
|
||||
>
|
||||
<Form.Item label={t("offline.label.tips")}>
|
||||
<span style={{ wordBreak: 'break-all' }}>{offline.tips}</span>
|
||||
<span style={{ wordBreak: 'break-all', textDecoration: 'underline' }}>{offline.tips}</span>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("offline.label.option")}>
|
||||
<Select
|
||||
|
|
@ -80,7 +78,7 @@ function Counter(props: { deadline: number }) {
|
|||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
timerFn();
|
||||
timerFn();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
@ -105,11 +103,11 @@ function RenderWechatPay(props: {
|
|||
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')} />
|
||||
}
|
||||
{
|
||||
process.env.NODE_ENV === 'production' ?
|
||||
<Alert type="info" message={t('wechat.native.tips')} /> :
|
||||
<Alert type="warning" message={t('wechat.native.tips2')} />
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
@ -174,12 +172,12 @@ export default function Render(props: WebComponentProps<EntityDict, 'pay', false
|
|||
iStateColor?: string;
|
||||
payConfig?: PayConfig;
|
||||
onClose: () => undefined;
|
||||
closable: boolean;
|
||||
}>) {
|
||||
const { pay, application, iStateColor, payConfig, oakExecutable, onClose } = props.data;
|
||||
const { t, update, execute } = props.methods;
|
||||
const { pay, application, iStateColor, payConfig, oakExecutable, onClose, closable } = props.data;
|
||||
const { t, update, execute, clean } = props.methods;
|
||||
if (pay && application) {
|
||||
const { iState, channel, price, '#oakLegalActions': legalActions } = pay;
|
||||
const closable = !!legalActions?.find(ele => ele === 'close');
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
|
@ -217,11 +215,19 @@ export default function Render(props: WebComponentProps<EntityDict, 'pay', false
|
|||
<div className={Styles.btn}>
|
||||
{
|
||||
oakExecutable === true && (
|
||||
<Button
|
||||
onClick={() => execute()}
|
||||
>
|
||||
{t('common::action.update')}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => execute()}
|
||||
>
|
||||
{t('common::action.update')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => clean()}
|
||||
>
|
||||
{t('common::reset')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,22 +1,264 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Input, Radio, Space, Form, Select, Flex } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Tag, List, Button, Modal, Form, Selector, Input, TextArea } from 'antd-mobile';
|
||||
import { QRCode, Alert } from 'antd';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '@project/oak-app-domain';
|
||||
import Styles from './web.pc.module.less';
|
||||
import classNames from 'classnames';
|
||||
import Styles from './web.mobile.module.less';
|
||||
import * as dayJs from 'dayjs';
|
||||
import { CentToString } from 'oak-domain/lib/utils/money';
|
||||
import {
|
||||
OfflinePayConfig,
|
||||
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, PayConfig
|
||||
} from '../../../types/PayConfig';
|
||||
import { WechatOutlined, MoneyCollectOutlined, WalletOutlined } from '@ant-design/icons';
|
||||
import { PayCircleOutline, GlobalOutline } from 'antd-mobile-icons';
|
||||
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Form
|
||||
layout="horizontal"
|
||||
style={{ width: '100%', marginTop: 12 }}
|
||||
>
|
||||
<Form.Item label={t("offline.label.tips")}>
|
||||
<span style={{ wordBreak: 'break-all', textDecoration: 'underline' }}>{offline.tips}</span>
|
||||
</Form.Item>
|
||||
{!!(offline.options?.length) && <Form.Item label={t("offline.label.option")}>
|
||||
<Selector
|
||||
value={(meta as { option?: string })?.option ? [(meta as { option: string })?.option] : undefined}
|
||||
options={offline.options!.map(
|
||||
ele => ({
|
||||
label: ele,
|
||||
value: ele,
|
||||
}),
|
||||
)}
|
||||
disabled={!metaUpdatable}
|
||||
onChange={(v) => updateMeta({
|
||||
...meta,
|
||||
option: v[0],
|
||||
})}
|
||||
/>
|
||||
</Form.Item>}
|
||||
<Form.Item label={t("offline.label.serial")}>
|
||||
<TextArea
|
||||
autoSize={{ minRows: 3 }}
|
||||
value={(meta as { serial?: string })?.serial}
|
||||
disabled={!metaUpdatable}
|
||||
placeholder={metaUpdatable ? t('offline.placeholder.serial') : t('offline.placeholder.none')}
|
||||
onChange={(value) => updateMeta({
|
||||
...meta,
|
||||
serial: value,
|
||||
})}
|
||||
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={Styles.counter}>{counter}</div>
|
||||
);
|
||||
}
|
||||
|
||||
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')} />
|
||||
}
|
||||
switch (channel) {
|
||||
case PAY_CHANNEL_OFFLINE_NAME: {
|
||||
const { '#oakLegalActions': legalActions } = pay;
|
||||
const metaUpdatable = !!legalActions?.find(
|
||||
ele => typeof ele === 'object'
|
||||
&& ele.action === 'update'
|
||||
&& ele.attrs?.includes('meta')
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{iState === 'paying' && <Alert type='info' message={t('offline.tips')} />}
|
||||
{RenderOffline(
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'pay', false, {
|
||||
pay: RowWithActions<EntityDict, 'pay'>;
|
||||
application?: EntityDict['application']['Schema'];
|
||||
iStateColor?: string;
|
||||
payConfig?: PayConfig;
|
||||
onClose: () => undefined;
|
||||
closable: boolean;
|
||||
}>) {
|
||||
const { pay } = props.data;
|
||||
const { t } = props.methods;
|
||||
const { pay, application, iStateColor, payConfig, oakExecutable, onClose, closable } = props.data;
|
||||
const { t, update, execute, clean } = props.methods;
|
||||
if (pay && application) {
|
||||
const { iState, channel, price, '#oakLegalActions': legalActions } = pay;
|
||||
|
||||
return (
|
||||
<div className={Styles.container}>
|
||||
<Card
|
||||
title={t('title')}
|
||||
extra={<Tag color={iStateColor}>{t(`pay:v.iState.${iState}`)}</Tag>}
|
||||
>
|
||||
<div>
|
||||
<List>
|
||||
<List.Item
|
||||
prefix={<PayCircleOutline />}
|
||||
extra={CentToString(price!, 2)}
|
||||
>
|
||||
{t('pay:attr.price')}
|
||||
</List.Item>
|
||||
<List.Item
|
||||
prefix={<GlobalOutline />}
|
||||
extra={t(`payChannel::${channel}`)}
|
||||
>
|
||||
{t('pay:attr.channel')}
|
||||
</List.Item>
|
||||
</List>
|
||||
</div>
|
||||
</Card>
|
||||
<div className={Styles.meta}>
|
||||
<RenderPayMeta
|
||||
pay={pay}
|
||||
t={t}
|
||||
application={application}
|
||||
payConfig={payConfig!}
|
||||
updateMeta={(meta) => update({ meta })}
|
||||
/>
|
||||
</div>
|
||||
<div className={Styles.padding} />
|
||||
<div className={Styles.btn}>
|
||||
{
|
||||
oakExecutable === true && (
|
||||
<>
|
||||
<div className={Styles.btnItem}>
|
||||
<Button
|
||||
block
|
||||
color='primary'
|
||||
onClick={() => execute()}
|
||||
>
|
||||
{t('common::action.update')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={Styles.btnItem}>
|
||||
<Button
|
||||
block
|
||||
onClick={() => clean()}
|
||||
>
|
||||
{t('common::reset')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
closable && !(oakExecutable === true) && (
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: t('cc.title'),
|
||||
content: t('cc.content'),
|
||||
onConfirm: async () => {
|
||||
await execute('close');
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('pay:action.close')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { AttrUpdateMatrix } from 'oak-domain/lib/types/EntityDesc';
|
||||
import { EntityDict } from '@project/oak-app-domain';
|
||||
import { PAY_CHANNEL_OFFLINE_NAME } from '@project/types/PayConfig';
|
||||
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_OFFLINE_NAME } from '@project/types/PayConfig';
|
||||
|
||||
|
||||
const attrUpdateMatrix: AttrUpdateMatrix<EntityDict> = {
|
||||
|
|
@ -17,6 +17,15 @@ const attrUpdateMatrix: AttrUpdateMatrix<EntityDict> = {
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
accountOper$entity: {
|
||||
actions: ['succeedPaying'],
|
||||
filter: {
|
||||
accountId: {
|
||||
$exists: true,
|
||||
},
|
||||
channel: PAY_CHANNEL_ACCOUNT_NAME,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,6 +47,21 @@ const i18ns: I18n[] = [
|
|||
position: "src/components/accountOper/list",
|
||||
data: {}
|
||||
},
|
||||
{
|
||||
id: "d39ddaee363037f4557bad511722e250",
|
||||
namespace: "oak-pay-business-c-order-pay",
|
||||
language: "zh-CN",
|
||||
module: "oak-pay-business",
|
||||
position: "src/components/order/pay",
|
||||
data: {
|
||||
"price": "订单金额",
|
||||
"choose": "请选择支付方式(%{price}元)",
|
||||
"useAccount": "使用余额抵扣",
|
||||
"accountMax": "当前可用余额为%{max}元",
|
||||
"illegalState": "订单的当前状态【%{state}】无法开始支付",
|
||||
"paying": "有一个正在支付的订单"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "3500cc465492fca3797b75c9c0dbf517",
|
||||
namespace: "oak-pay-business-c-pay-channelPicker",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export type IState = 'paid' | 'unpaid' | 'timeout' | 'cancelled' | 'paying' | 'p
|
|||
|
||||
export const IActionDef: ActionDef<IAction, IState> = {
|
||||
stm: {
|
||||
startPaying: ['unpaid', 'paying'],
|
||||
startPaying: [['unpaid', 'partiallyPaid'], 'paying'],
|
||||
payAll: [['unpaid', 'paying', 'partiallyPaid'], 'paid'],
|
||||
payPartially: [['unpaid', 'paying'], 'partiallyPaid'],
|
||||
payNone: ['paying', 'unpaid'],
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export const entityDesc: EntityDesc<Schema, Action, '', {
|
|||
zh_CN: {
|
||||
name: '订单',
|
||||
attr: {
|
||||
price: '订单金额',
|
||||
price: '应支付金额',
|
||||
paid: '已支付金额',
|
||||
refunded: '已退款金额',
|
||||
iState: '支付状态',
|
||||
|
|
|
|||
|
|
@ -61,92 +61,96 @@ async function changeOrderStateByPay(
|
|||
}
|
||||
}
|
||||
|
||||
if (hasPaying) {
|
||||
// 一定是在支付状态
|
||||
assert(!hasRefunding && payRefunded === 0 && payPaid < orderPrice!);
|
||||
if (orderIState !== 'paying' || orderPaid !== payPaid) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'startPaying',
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId!,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
const closeFn = context.openRootMode();
|
||||
try {
|
||||
if (hasPaying) {
|
||||
// 一定是在支付状态
|
||||
assert(!hasRefunding && payRefunded === 0 && payPaid < orderPrice!);
|
||||
if (orderIState !== 'paying' || orderPaid !== payPaid) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'startPaying',
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId!,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else if (hasRefunding) {
|
||||
assert(!hasPaying && payPaid === orderPrice && payPaid === orderPaid && payRefunded < orderPrice);
|
||||
if (orderIState !== 'refunding' || orderRefunded !== payRefunded) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'startRefunding',
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId!,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else if (payRefunded) {
|
||||
// 已经在退款流程当中
|
||||
assert(payPaid === orderPrice && payPaid === orderPaid);
|
||||
const iState = payPaid === orderPrice ? 'refunded' : 'partiallyRefunded';
|
||||
if (orderIState !== iState || orderRefunded !== payRefunded) {
|
||||
const action = payPaid === orderPrice ? 'refundAll' : 'refundPartially';
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action,
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId!,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else if (payPaid) {
|
||||
// 在支付流程当中
|
||||
assert(orderRefunded === 0);
|
||||
const iState = payPaid === orderPrice ? 'paid' : 'partiallyPaid';
|
||||
if (orderIState !== iState || orderPaid !== payPaid) {
|
||||
const action = payPaid === orderPrice ? 'payAll' : 'payPartially';
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action,
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId!,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const iState = 'unpaid';
|
||||
assert(orderRefunded === 0 && orderPaid === 0);
|
||||
if (orderIState !== iState) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'payNone',
|
||||
data: {
|
||||
},
|
||||
filter: {
|
||||
id: orderId!,
|
||||
},
|
||||
}, option);
|
||||
}
|
||||
}
|
||||
closeFn();
|
||||
return 1;
|
||||
}
|
||||
else if (hasRefunding) {
|
||||
assert(!hasPaying && payPaid === orderPrice && payPaid === orderPaid && payRefunded < orderPrice);
|
||||
if (orderIState !== 'refunding' || orderRefunded !== payRefunded) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'startRefunding',
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId!,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else if (payRefunded) {
|
||||
// 已经在退款流程当中
|
||||
assert(payPaid === orderPrice && payPaid === orderPaid);
|
||||
const iState = payPaid === orderPrice ? 'refunded' : 'partiallyRefunded';
|
||||
if (orderIState !== iState || orderRefunded !== payRefunded) {
|
||||
const action = payPaid === orderPrice ? 'refundAll' : 'refundPartially';
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action,
|
||||
data: {
|
||||
refunded: payRefunded,
|
||||
},
|
||||
filter: {
|
||||
id: orderId!,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else if (payPaid) {
|
||||
// 在支付流程当中
|
||||
assert(orderRefunded === 0);
|
||||
const iState = payPaid === orderPrice ? 'paid' : 'partiallyPaid';
|
||||
if (orderIState !== iState || orderPaid !== payPaid) {
|
||||
const action = payPaid === orderPrice ? 'payAll' : 'payPartially';
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action,
|
||||
data: {
|
||||
paid: payPaid,
|
||||
},
|
||||
filter: {
|
||||
id: orderId!,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const iState = 'unpaid';
|
||||
assert(orderRefunded === 0 && orderPaid === 0);
|
||||
if (orderIState !== iState) {
|
||||
await context.operate('order', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'payNone',
|
||||
data: {
|
||||
},
|
||||
filter: {
|
||||
id: orderId!,
|
||||
},
|
||||
}, option);
|
||||
return 1;
|
||||
}
|
||||
catch (err: any) {
|
||||
closeFn();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,32 +164,42 @@ const triggers: Trigger<EntityDict, 'pay', BRC>[] = [
|
|||
const { data } = operation;
|
||||
assert(!(data instanceof Array));
|
||||
const { accountId, price, orderId, id } = data;
|
||||
if (orderId) {
|
||||
/* if (orderId) {
|
||||
if (accountId) {
|
||||
// 使用帐户支付,直接成功并扣款
|
||||
await context.operate('pay', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'payAll',
|
||||
data: {
|
||||
accountOper$entity: [
|
||||
{
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
const close = context.openRootMode();
|
||||
try {
|
||||
await context.operate('pay', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'succeedPaying',
|
||||
data: {
|
||||
accountOper$entity: [
|
||||
{
|
||||
id: await generateNewIdAsync(),
|
||||
totalPlus: -price!,
|
||||
availPlus: -price!,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
}
|
||||
}, option);
|
||||
action: 'create',
|
||||
data: {
|
||||
id: await generateNewIdAsync(),
|
||||
totalPlus: -price!,
|
||||
availPlus: -price!,
|
||||
type: 'consume',
|
||||
accountId,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
}
|
||||
}, option);
|
||||
close();
|
||||
}
|
||||
catch (err: any) {
|
||||
close();
|
||||
throw err;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
// 其余情况都是进入paying流程
|
||||
await context.operate('pay', {
|
||||
|
|
|
|||
|
|
@ -22,9 +22,13 @@ export default class Account implements PayClazz {
|
|||
id: await generateNewIdAsync(),
|
||||
totalPlus: -price!,
|
||||
availPlus: -price!,
|
||||
type: 'consume',
|
||||
accountId,
|
||||
},
|
||||
}
|
||||
];
|
||||
data.meta = {};
|
||||
data.paid = pay.price;
|
||||
}
|
||||
|
||||
getState(pay: OpSchema): Promise<string | null | undefined> {
|
||||
|
|
|
|||
|
|
@ -30,11 +30,12 @@ const watchers: Watcher<EntityDict, 'pay', BRC>[] = [
|
|||
fn: async (context, data) => {
|
||||
const results = [] as OperationResult<EntityDict>[];
|
||||
for (const pay of data) {
|
||||
const { applicationId, channel, timeoutAt } = pay;
|
||||
const { applicationId, channel, timeoutAt, price } = pay;
|
||||
const clazz = await getPayClazz(applicationId!, channel!, context);
|
||||
const iState = await clazz.getState(pay as EntityDict['pay']['OpSchema']);
|
||||
if (iState !== pay.iState!) {
|
||||
let action: EntityDict['pay']['Action'] = 'close';
|
||||
const data2: EntityDict['pay']['Update']['data'] = {};
|
||||
switch (iState) {
|
||||
case 'closed': {
|
||||
// action = 'close';
|
||||
|
|
@ -42,6 +43,7 @@ const watchers: Watcher<EntityDict, 'pay', BRC>[] = [
|
|||
}
|
||||
case 'paid': {
|
||||
action = 'succeedPaying';
|
||||
data2.paid = price!;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
|
@ -52,7 +54,7 @@ const watchers: Watcher<EntityDict, 'pay', BRC>[] = [
|
|||
const result = await context.operate('pay', {
|
||||
id: await generateNewIdAsync(),
|
||||
action,
|
||||
data: {},
|
||||
data: data2,
|
||||
filter: {
|
||||
id: pay.id!,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue