增加了sysAccount/survey的部分能力

This commit is contained in:
Xu Chang 2024-06-11 18:33:07 +08:00
parent 3a0e6d3035
commit 4340d8bbe1
80 changed files with 1542 additions and 399 deletions

View File

@ -143,6 +143,9 @@ export async function getWithdrawCreateData(params, context) {
data: ele,
})));
}
else {
data.refund$entity = [];
}
if (totalPrice > price2) {
// 如果还有要退的部分就从withdrawAccount来进行转账
const rest = totalPrice - price2;
@ -196,6 +199,10 @@ export async function getWithdrawCreateData(params, context) {
}
];
}
else {
// 保持结构完整让上层可以和withdraw$detail同构渲染
data.withdrawTransfer$withdraw = [];
}
data.loss = totalLoss;
return data;
}

View File

@ -1,5 +1,7 @@
import { EntityDict } from '../oak-app-domain';
import { RuntimeCxt } from '../types/RuntimeCxt';
import { Checker } from 'oak-domain/lib/types/Auth';
export declare function registerAccountEntity<ED extends EntityDict>(entity: keyof ED): void;
export declare const accountEntities: string[];
declare const triggers: Checker<EntityDict, keyof EntityDict, RuntimeCxt>[];
export default triggers;

View File

@ -1,9 +1,11 @@
import { generateNewId } from 'oak-domain/lib/utils/uuid';
import { getPayClazzAccountEntities } from '../utils/payClazz';
// 当注入一个新的pay entity时将account的删除与之相关联
const entities = getPayClazzAccountEntities();
// 当注入一个新的account entity时将withdrawChannel的删除与之相关联
export function registerAccountEntity(entity) {
accountEntities.push(entity);
}
export const accountEntities = ['wpAccount', 'offlineAccount'];
const triggers = [
...entities.filter(ele => !!ele).map((entity) => [
...accountEntities.filter(ele => !!ele).map((entity) => [
{
entity,
action: 'remove',

View File

@ -5,7 +5,9 @@ import applicationCheckers from './application';
import offlineAccountCheckers from './offlineAccount';
import wpProductCheckers from './wpProduct';
import abstractCheckers from './abstractChecker';
import withdrawAccounts from './withdrawAccount';
const checkers = [
...withdrawAccounts,
...abstractCheckers,
...aoCheckers,
...payCheckers,

5
es/checkers/withdrawAccount.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { Checker } from 'oak-domain/lib/types/Auth';
import { EntityDict } from '../oak-app-domain';
import { RuntimeCxt } from '../types/RuntimeCxt';
declare const checkers: Checker<EntityDict, 'withdrawAccount', RuntimeCxt>[];
export default checkers;

View File

@ -0,0 +1,71 @@
import { generateNewId } from 'oak-domain/lib/utils/uuid';
import { pipeline } from 'oak-domain/lib/utils/executor';
import assert from 'assert';
const checkers = [
{
entity: 'withdrawAccount',
type: 'logical',
action: 'create',
checker(operation, context, option) {
const { data } = operation;
if (data) {
const { id, entity, entityId, isDefault } = data;
if (entity && entityId && isDefault) {
return context.operate('withdrawAccount', {
id: generateNewId(),
action: 'update',
data: {
isDefault: false,
},
filter: {
entity,
entityId,
isDefault: true,
id: {
$ne: id,
},
}
}, option);
}
}
}
},
{
entity: 'withdrawAccount',
type: 'logical',
action: 'update',
checker(operation, context, option) {
const { data, filter } = operation;
if (data?.isDefault) {
return pipeline(() => context.select('withdrawAccount', {
data: {
id: 1,
entity: 1,
entityId: 1,
},
filter,
}, {}), (accounts) => {
assert(accounts.length === 1);
const [account] = accounts;
const { entity, entityId, id } = account;
return context.operate('withdrawAccount', {
id: generateNewId(),
action: 'update',
data: {
isDefault: false,
},
filter: {
entity,
entityId,
isDefault: true,
id: {
$ne: id,
},
}
}, option);
});
}
}
}
];
export default checkers;

View File

@ -0,0 +1,6 @@
import { EntityDict } from "../../../oak-app-domain";
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, boolean, {
entities: string[];
systemId: string;
}>) => React.ReactElement;
export default _default;

View File

@ -0,0 +1,73 @@
import assert from 'assert';
import { uniq } from "oak-domain/lib/utils/lodash";
export default OakComponent({
properties: {
entities: [],
systemId: '',
},
lifetimes: {
ready() {
this.refreshData();
}
},
methods: {
refreshData() {
const { entities, systemId } = this.props;
const schema = this.features.cache.getSchema();
uniq(['wpAccount', 'offlineAccount'].concat(entities || [])).forEach((entity) => {
const projection = {
id: 1,
$$createAt$$: 1,
$$updateAt$$: 1,
};
Object.keys(schema[entity].attributes).forEach(ele => {
if (!ele.startsWith('$$')) {
projection[ele] = 1;
}
});
this.features.cache.refresh(entity, {
data: projection,
filter: {
systemId,
}
});
});
},
},
formData({ features }) {
const { entities, systemId } = this.props;
assert(systemId);
const schema = features.cache.getSchema();
let total = 0;
const accounts = uniq(['wpAccount', 'offlineAccount'].concat(entities || [])).map((entity) => {
const projection = {
id: 1,
$$createAt$$: 1,
$$updateAt$$: 1,
};
Object.keys(schema[entity].attributes).forEach(ele => {
if (!ele.startsWith('$$')) {
projection[ele] = 1;
}
});
const [data] = this.features.cache.get(entity, {
data: projection,
filter: {
systemId,
}
}, true);
const { price, id } = data;
total += price;
return {
entity,
id,
price,
data,
};
});
return {
total,
accounts,
};
}
});

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,12 @@
import { EntityDict } from "../../../oak-app-domain";
import { WebComponentProps } from "oak-frontend-base";
import React from 'react';
export default function render(props: WebComponentProps<EntityDict, 'offlineAccount', false, {
total?: number;
accounts?: Array<{
id: string;
entity: string;
data: any;
price: number;
}>;
}>): React.JSX.Element | null;

View File

@ -0,0 +1,11 @@
import React from 'react';
import Styles from './web.pc.module.less';
export default function render(props) {
const { accounts, total } = props.data;
if (accounts) {
return (<div className={Styles.container}>
{total}
</div>);
}
return null;
}

View File

@ -0,0 +1,7 @@
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
}

View File

@ -34,8 +34,8 @@ export default OakComponent({
return {
withdrawCreate: withdrawData && {
...withdrawData,
refund$entity: withdrawData.refund$entity.map((ele) => ele.data),
withdrawTransfer$withdraw: withdrawData.withdrawTransfer$withdraw.map((ele) => ele.data),
refund$entity: withdrawData.refund$entity?.map((ele) => ele.data),
withdrawTransfer$withdraw: withdrawData.withdrawTransfer$withdraw?.map((ele) => ele.data),
},
withdrawable,
withdrawLossText,
@ -81,6 +81,9 @@ export default OakComponent({
chooseWa: false,
withdrawAccountId: '',
waFilter: undefined,
pickWithdrawChannelMp(id) {
this.pickWithdrawChannel(id);
}
},
methods: {
setValue(valueYuan) {
@ -108,19 +111,21 @@ export default OakComponent({
},
async createWithdrawData() {
const { value, refundAmount, avail, withdrawAccountId } = this.state;
if (value > avail) {
this.setMessage({
type: 'error',
content: this.t('error::withdraw.overflow'),
});
}
else if (value <= refundAmount) {
this.resetCreateData();
}
else if (!withdrawAccountId) {
this.setState({
chooseWa: true,
});
if (value) {
if (value > avail) {
this.setMessage({
type: 'error',
content: this.t('error::withdraw.overflow'),
});
}
else if (value <= refundAmount) {
this.resetCreateData();
}
else if (!withdrawAccountId) {
this.setState({
chooseWa: true,
});
}
}
},
async resetCreateData() {
@ -208,6 +213,10 @@ export default OakComponent({
},
goBack() {
this.navigateBack();
},
onGoToHistoryMp() {
const { onGoToHistory } = this.props;
onGoToHistory && onGoToHistory();
}
},
});

View File

@ -1,6 +1,17 @@
@import '../../../config/styles/mp/mixins.less';
@import '../../../config/styles/mp/index.less';
.errorContainer {
padding: 48rpx;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 40rpx;
}
.container {
height: 100%;
box-sizing: border-box;
@ -93,6 +104,16 @@
}
}
.gotoHistory {
font-size: 32rpx;
color: @oak-color-primary;
margin-top: 56rpx;
text-decoration: underline;
display: flex;
justify-content: flex-end;
align-items: center;
}
.footer {
position: fixed;
width: 100%;

View File

@ -1,4 +1,21 @@
<block wx:if="{{withdrawCreate}}">
<block wx:if="{{!withdrawable}}">
<view class="errorContainer">
<oak-icon name="delete_fill" size="48" color="#FF3141" />
<view>{{t('notSetYet')}}</view>
<l-button bind:lintap="goBack">{{t('common::back')}}</l-button>
</view>
</block>
<block wx:elif="{{chooseWa && waFilter}}">
<withdrawAccount-picker
oakPath="$$opb-withdraw-create-wapicker"
oakFilters="{{[waFilter]}}"
onPicker="{{pickWithdrawChannelMp}}"
onCancel="{{restartAll}}"
entity="user"
entityId="{{userId}}"
/>
</block>
<block wx:elif="{{withdrawCreate}}">
<view class="container">
<withdraw-display
withdraw="{{withdrawCreate}}"
@ -10,7 +27,7 @@
size="long"
plain="true"
shape="square"
bind:lintap="clearWithdrawData"
bind:lintap="restartAll"
l-class="cancelBtn"
>
{{t('common::action.cancel')}}
@ -25,12 +42,13 @@
</view>
</view>
</block>
<block wx:elif="{{account && avail > 0}}">
<block wx:elif="{{account}}">
<view class="main">
<view class="label">{{t('label')}}</view>
<view class="input">
<span class="symbol">{{t('common::pay.symbol')}}</span>
<l-input
focus="{{true}}"
hide-label="{{true}}"
showRow="{{false}}"
type="number"
@ -38,16 +56,21 @@
value="{{valueYuan ? valueYuan : undefind}}"
l-input-class="my-input-class"
bind:lininput="InputValueMp"
bind:linconfirm="createWithdrawData"
/>
</view>
<view class="tips">
<view class="tipLine">
<span>{{t('tips.1-1')}}</span>
<span class="value">{{availYuan}}</span>
<span>{{t('tips.1-2')}}</span>
<block wx:if="{{avail > 0}}">
<span class="value" style="text-decoration: underline; margin-left: 16rpx;" bindtap="setValueMp" data-value="{{availYuan}}">{{t('tips.fill')}}</span>
<block wx:if="{{avail>0}}">
<span class="value" style="text-decoration: underline; margin-left: 16rpx;" bindtap="setValueMp" data-value="{{availYuan}}">
<span class="value">{{availYuan}}</span>
</span>
</block>
<block wx:else>
<span class="value">{{availYuan}}</span>
</block>
<span>{{t('tips.1-2')}}</span>
</view>
<view wx:if="{{refundAmount > 0}}" class="tipLine">
<span>{{t('tips.2-1')}}</span>
@ -56,7 +79,7 @@
</view>
<view wx:if="{{manualAmount > 0}}" class="tipLine">
<span>{{t('tips.3-1')}}</span>
<span class="value">{{manualAmountYuan}}</span>
<span class="value">{{manualAmountYuan}}</span>
<span>{{t('tips.3-2')}}</span>
</view>
</view>
@ -72,7 +95,11 @@
<oak-icon wx:else name="unfold" size="18" color="skyblue"/>
<span style="margin-left: 16rpx;color:{{showLossHelp ? 'blue' : 'skyblue'}};">{{t('helps.label.loss')}}</span>
</view>
<view wx:if="{{showLossHelp}}" class="content2">{{t('helps.content.loss')}}</view>
<view wx:if="{{showLossHelp}}" class="content2">{{withdrawLossText}}</view>
</view>
<view class="gotoHistory" bindtap="onGoToHistoryMp">
<oak-icon name="coupons" size="18" color="@oak-color-primary" style="margin-right:8rpx;"/>
<span>{{t('gotoHistory')}}</span>
</view>
<view class="footer">
<l-button

View File

@ -36,7 +36,7 @@ export default function render(props) {
</div>
<div className={Styles.input}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
<Input autoFocus type="number" min={0} max={availYuan} placeholder={t('placeholder')} value={valueYuan ? `${valueYuan}` : undefined} onChange={(value) => setValue(value)} style={{ '--font-size': '28px' }}/>
<Input autoFocus type="number" min={0} max={availYuan} placeholder={t('placeholder')} value={valueYuan ? `${valueYuan}` : undefined} onChange={(value) => setValue(value)} style={{ '--font-size': '28px' }} onEnterPress={() => createWithdrawData()}/>
</div>
<div className={Styles.tips}>
<div className={Styles.tipLine}>

View File

@ -9,10 +9,6 @@ export default OakComponent({
dealLoss: 1,
dealPrice: 1,
iState: 1,
withdrawAccount: {
id: 1,
name: 1,
},
reason: 1,
meta: 1,
refund$entity: {

View File

@ -8,7 +8,7 @@ export default OakComponent({
formData({ features }) {
const { withdraw, create } = this.props;
const { refund$entity: refundData, withdrawTransfer$withdraw: transferData } = withdraw;
const refundData2 = (refundData).map((refund) => {
const refundData2 = (refundData)?.map((refund) => {
const { meta, price, loss, iState, $$updateAt$$, reason } = refund;
const { lossExplanation, channel } = meta;
return {
@ -24,8 +24,8 @@ export default OakComponent({
updateAt: !create && dayJs($$updateAt$$).format('YYYY-MM-DD HH:mm'),
reason,
};
});
const transferData2 = transferData.map((transfer) => {
}) || [];
const transferData2 = transferData?.map((transfer) => {
const { price, loss, iState, meta, withdrawAccountId, withdrawAccount, $$updateAt$$, reason } = transfer;
let withdrawChannel = withdrawAccount?.channel;
if (!withdrawChannel && withdrawAccountId) {
@ -65,7 +65,7 @@ export default OakComponent({
updateAt: !create && dayJs($$updateAt$$).format('YYYY-MM-DD HH:mm'),
reason,
};
});
}) || [];
const withdrawExactPrice = ['successful', 'partiallySuccessful', 'failed'].includes(withdraw.iState) ? withdraw.dealPrice - withdraw.dealLoss : withdraw.price - withdraw.loss;
return {
createAtStr: dayJs(withdraw.$$createAt$$).format('YYYY-MM-DD HH:mm'),

View File

@ -24,7 +24,9 @@ export default function render(props) {
</div>
{itemData && itemData.map((data, idx) => (<div className={Styles.refundItem} key={idx}>
<div className={Styles.header2}>
<Tag color={data.typeColor}>{data.type}</Tag>
<Tag fill="outline">
{data.type}
</Tag>
</div>
{data.iState && <div className={Styles.item}>
<span className={Styles.label}>{t('refund:attr.iState')}</span>

View File

@ -46,7 +46,9 @@ export default function render(props) {
{itemData && <div className={Styles.refunds}>
{itemData.map((data, idx) => (<div className={Styles.refundItem} key={idx}>
<div className={Styles.header2}>
<Tag color={data.typeColor}>{data.type}</Tag>
<Tag>
{data.type}
</Tag>
</div>
{data.iState && <div className={Styles.item}>
<span className={Styles.label}>{t('refund:attr.iState')}</span>

View File

@ -34,7 +34,7 @@ export default OakComponent({
},
},
},
withdrawTransfer$withdraw: {
withdrawTransfer$withdraw$$aggr: {
$entity: 'withdrawTransfer',
data: {
'#count-1': {
@ -60,5 +60,10 @@ export default OakComponent({
};
}),
};
},
methods: {
goBack() {
this.navigateBack();
}
}
});

View File

@ -1,5 +1,6 @@
{
"loss": "预计手续费%{value}%元",
"dealLoss": "手续费%{value}%元",
"count": "将分%{value}笔提现到您账户"
"loss": "预计手续费%{value}元",
"dealLoss": "手续费%{value}元",
"count": "将分%{value}笔提现到您账户",
"noData": "您尚无提现记录"
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import { EntityDict } from "../../../oak-app-domain";
import { WebComponentProps } from "oak-frontend-base";
export default function render(props: WebComponentProps<EntityDict, 'withdraw', false, {
gotoDetail: (id: string) => void;
withdraws?: ({
id: string;
iState: string;
iStateColor: string;
price: string;
lossDescription: string;
countDescription: number;
createAt: string;
})[];
}, {
goBack: () => void;
}>): React.JSX.Element;

View File

@ -1 +1,20 @@
"use strict";
import React from 'react';
import { List, Tag, Result, Button } from 'antd-mobile';
import { HandPayCircleOutline } from 'antd-mobile-icons';
export default function render(props) {
const { withdraws, gotoDetail } = props.data;
const { t, goBack } = props.methods;
if (withdraws?.length) {
return (<List>
{withdraws.map((ele) => (<List.Item prefix={<HandPayCircleOutline fontSize={38}/>} extra={<Tag color={ele.iStateColor}>{ele.iState}</Tag>} title={ele.lossDescription} description={ele.createAt} onClick={() => gotoDetail(ele.id)}>
<>
<span style={{ marginRight: 3 }}>{t('common::pay.symbol')}</span>
{ele.price}
</>
</List.Item>))}
</List>);
}
return (<Result status="info" title={t('noData')} description={<Button onClick={goBack}>
{t('common::back')}
</Button>}/>);
}

View File

@ -12,4 +12,6 @@ export default function render(props: WebComponentProps<EntityDict, 'withdraw',
countDescription: number;
createAt: string;
})[];
}>): React.JSX.Element | null;
}, {
goBack: () => void;
}>): React.JSX.Element;

View File

@ -1,14 +1,14 @@
import React from 'react';
import { List, Avatar, Button } from 'antd';
import { CalendarOutlined } from '@ant-design/icons';
import { List, Avatar, Result, Button } from 'antd';
import { CalendarOutlined, NodeExpandOutlined } from '@ant-design/icons';
import Styles from './web.pc.module.less';
export default function render(props) {
const { withdraws, gotoDetail } = props.data;
const { t } = props.methods;
const { t, goBack } = props.methods;
if (withdraws?.length) {
return (<List itemLayout="horizontal" dataSource={withdraws} renderItem={(item, index) => (<List.Item key={index} actions={[
<Button onClick={() => gotoDetail(item.id)}>
{t('common::detail')}
{t('common::action.detail')}
</Button>
]}>
<List.Item.Meta avatar={<Avatar size={50} style={{
@ -25,13 +25,18 @@ export default function render(props) {
<span>{item.lossDescription}</span>
</div>
</div>} description={<div className={Styles.description}>
<div className={Styles.count}>{item.countDescription}</div>
<div className={Styles.count}>
<NodeExpandOutlined />
<span className={Styles.data}>{item.countDescription}</span>
</div>
<div className={Styles.createAt}>
<CalendarOutlined />
<span className={Styles.date}>{item.createAt}</span>
<span className={Styles.data}>{item.createAt}</span>
</div>
</div>}/>
</List.Item>)}/>);
}
return null;
return (<Result title={t('noData')} extra={<Button onClick={goBack}>
{t('common::back')}
</Button>}/>);
}

View File

@ -22,9 +22,7 @@
.description {
padding: auto;
.createAt {
.date {
margin-left: 4px;
}
.data {
margin-left: 4px;
}
}

View File

@ -63,6 +63,7 @@ export default OakComponent({
return {
entity,
entityId,
enabled: true,
};
}
}
@ -87,19 +88,37 @@ export default OakComponent({
});
},
async removeAccount(id) {
return this.execute(undefined, undefined, undefined, [
{
entity: 'withdrawAccount',
operation: {
id: await generateNewIdAsync(),
action: 'remove',
data: {},
filter: {
id,
},
const row = this.state.withdrawAccounts.find(ele => ele.id === id);
if (row['#oakLegalActions']?.includes('remove')) {
return this.execute(undefined, undefined, undefined, [
{
entity: 'withdrawAccount',
operation: {
id: await generateNewIdAsync(),
action: 'remove',
data: {},
filter: {
id,
},
}
}
}
]);
]);
}
else {
return this.execute(undefined, undefined, undefined, [
{
entity: 'withdrawAccount',
operation: {
id: await generateNewIdAsync(),
action: 'disable',
data: {},
filter: {
id,
},
}
}
]);
}
},
resetAll() {
this.clean();
@ -111,6 +130,11 @@ export default OakComponent({
await this.execute();
this.resetAll();
},
switchDefault(id) {
const row = this.state.withdrawAccounts.find(ele => ele.id === id);
this.clean();
this.updateItem({ isDefault: !row.isDefault }, row.id);
},
pickOne(id) {
this.setState({
selectedId: id,

View File

@ -1,5 +1,6 @@
{
"noData": "您还没有配置过提现账号",
"confirmDelete": "确定删除",
"areYouSure": "确认删除本账号吗?"
"areYouSure": "确认删除本账号吗?",
"default": "默认"
}

View File

@ -0,0 +1,29 @@
import React from 'react';
import { EntityDict } from "../../../oak-app-domain";
import { RowWithActions, WebComponentProps } from "oak-frontend-base";
export default function render(props: WebComponentProps<EntityDict, 'withdrawAccount', false, {
withdrawAccounts?: (RowWithActions<EntityDict, 'withdrawAccount'> & {
label: string;
symbol: string;
allowUpdate?: boolean;
allowRemove?: boolean;
})[];
upsertId: string;
entity: string;
entityId: string;
isCreate?: boolean;
asPicker?: boolean;
allowCreate?: boolean;
selectedId?: string;
onCancel?: () => void;
}, {
setUpsertId: (id: string) => void;
doUpdate: () => Promise<void>;
resetAll: () => void;
updateAccount: (id: string) => void;
newAccount: () => void;
removeAccount: (id: string) => Promise<void>;
switchDefault: (id: string) => void;
pickOne: (id: string) => void;
confirmPick: () => void;
}>): React.JSX.Element;

View File

@ -1 +1,87 @@
"use strict";
import React from 'react';
import { Popup, SwipeAction, List, Button, Result, Checkbox, Switch, Modal } from 'antd-mobile';
import Styles from './web.module.less';
import WdaUpsert from '../upsert';
export default function render(props) {
const { isCreate, withdrawAccounts, upsertId, oakFullpath, entity, entityId, asPicker, allowCreate, selectedId, onCancel, oakExecutable } = props.data;
const { t, setUpsertId, doUpdate, resetAll, updateAccount, newAccount, pickOne, confirmPick, removeAccount, switchDefault } = props.methods;
const U = upsertId && (<Popup visible={!!upsertId} destroyOnClose onMaskClick={() => resetAll()} onClose={() => resetAll()}>
<WdaUpsert oakPath={`${oakFullpath}.${upsertId}`} oakId={upsertId} entity={entity} entityId={entityId}/>
<div className={Styles.btns}>
<Button block onClick={() => resetAll()}>
{t('common::action.cancel')}
</Button>
<Button color="primary" block disabled={oakExecutable !== true} onClick={() => doUpdate()}>
{t('common::confirm')}
</Button>
</div>
</Popup>);
if (!withdrawAccounts?.length) {
return (<div className={Styles.container}>
{U}
<Result status="warning" title={t('noData')} description={<Button color="primary" onClick={() => newAccount()}>
{t('common::action.create')}
</Button>}/>
</div>);
}
const getItem = (ele, index) => (<List.Item key={index} prefix={<div className={Styles.avatar}>
{ele.symbol}
</div>} title={ele.name} description={ele.code} extra={asPicker ? <Checkbox checked={ele.id === selectedId} onChange={(checked) => {
if (checked) {
pickOne(ele.id);
}
else {
pickOne('');
}
}}/> : <Switch style={{ '--width': '72px' }} checked={ele.isDefault} checkedText={t('default')} onChange={() => switchDefault(ele.id)}/>}>
{ele.label}
</List.Item>);
return (<div className={Styles.container2}>
{U}
<div className={Styles.list}>
<List>
{withdrawAccounts.map((ele, idx) => {
const Item = getItem(ele, idx);
if (asPicker) {
return Item;
}
return (<SwipeAction key={idx} rightActions={[
{
key: 'delete',
text: t('common::action.remove'),
color: 'danger',
onClick: () => Modal.confirm({
title: t('confirmDelete'),
content: t('areYouSure'),
onConfirm: () => removeAccount(ele.id),
})
}
]}>
{Item}
</SwipeAction>);
})}
</List>
</div>
<div className={Styles.btns}>
{asPicker ? (<>
<Button block onClick={() => onCancel()} style={{ marginRight: 6 }}>
{t('common::action.cancel')}
</Button>
<Button block color="primary" disabled={!selectedId} onClick={() => confirmPick()}>
{t('common::confirm')}
</Button>
</>) : (oakExecutable === true ?
<>
<Button block onClick={() => resetAll()}>
{t('common::reset')}
</Button>
<Button block color="primary" onClick={() => doUpdate()}>
{t('common::action.update')}
</Button>
</> :
allowCreate && <Button block color="primary" onClick={() => newAccount()}>
{t('common::action.create')}
</Button>)}
</div>
</div>);
}

View File

@ -1,4 +1,41 @@
.container {
width: 100%;
height: 100%;
}
.container2 {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
.list {
flex: 1;
.avatar {
font-size: 24px;
font-weight: bold;
width: 48px;
height: 48px;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
color: var(--oak-color-primary);
background-color: silver;
}
.switch {
width: 72px;
}
}
}
.btns {
display: flex;
flex-direction: row;
justify-content: flex-end;
}

View File

@ -23,6 +23,7 @@ export default function render(props: WebComponentProps<EntityDict, 'withdrawAcc
updateAccount: (id: string) => void;
newAccount: () => void;
removeAccount: (id: string) => Promise<void>;
switchDefault: (id: string) => void;
pickOne: (id: string) => void;
confirmPick: () => void;
}>): React.JSX.Element;

View File

@ -1,10 +1,10 @@
import React from 'react';
import { Modal, Avatar, List, Button, Result, Checkbox } from 'antd';
import { Modal, Switch, Avatar, List, Button, Result, Checkbox } from 'antd';
import Styles from './web.pc.module.less';
import WdaUpsert from '../upsert';
export default function render(props) {
const { isCreate, withdrawAccounts, upsertId, oakFullpath, entity, entityId, asPicker, allowCreate, selectedId, onCancel } = props.data;
const { t, setUpsertId, doUpdate, resetAll, updateAccount, newAccount, pickOne, confirmPick, removeAccount } = props.methods;
const { isCreate, withdrawAccounts, upsertId, oakFullpath, entity, entityId, asPicker, allowCreate, selectedId, onCancel, oakExecutable } = props.data;
const { t, setUpsertId, doUpdate, resetAll, updateAccount, newAccount, pickOne, confirmPick, removeAccount, switchDefault } = props.methods;
const U = upsertId && (<Modal open={!!upsertId} destroyOnClose title={`${isCreate ? t('common::action.create') : t('common::action.update')}${t('withdrawAccount:name')}`} onCancel={() => resetAll()} closeIcon={null} onOk={() => doUpdate()} okText={t('common::confirm')} cancelText={t('common::action.cancel')}>
<WdaUpsert oakPath={`${oakFullpath}.${upsertId}`} oakId={upsertId} entity={entity} entityId={entityId}/>
</Modal>);
@ -19,7 +19,7 @@ export default function render(props) {
return (<div className={Styles.container2}>
{U}
<div className={Styles.list}>
<List itemLayout="horizontal" dataSource={withdrawAccounts} renderItem={(item, index) => (<List.Item actions={asPicker ? [<Checkbox checked={item.id === selectedId} onChange={({ target }) => {
<List itemLayout="horizontal" dataSource={withdrawAccounts} renderItem={(item, index) => (<List.Item key={index} actions={asPicker ? [<Checkbox checked={item.id === selectedId} onChange={({ target }) => {
const { checked } = target;
if (checked) {
pickOne(item.id);
@ -28,10 +28,9 @@ export default function render(props) {
pickOne('');
}
}}/>] : [
item.allowUpdate && <Button size="small" onClick={() => updateAccount(item.id)}>
{t('common::action.update')}
</Button>,
item.allowRemove && <Button size="small" danger onClick={() => Modal.confirm({
item.allowUpdate &&
<Switch checkedChildren={t('default')} value={item.isDefault} onChange={(value) => switchDefault(item.id)}/>,
<Button size="small" danger onClick={() => Modal.confirm({
title: t('confirmDelete'),
content: t('areYouSure'),
okText: t('common::confirm'),
@ -48,17 +47,25 @@ export default function render(props) {
</List.Item>)}/>
</div>
<div className={Styles.btns}>
{!asPicker && allowCreate && <Button type="primary" onClick={() => newAccount()}>
{t('common::action.create')}
</Button>}
{asPicker && <>
<Button onClick={() => onCancel()} style={{ marginRight: 6 }}>
{t('common::action.cancel')}
</Button>
<Button type="primary" disabled={!selectedId} onClick={() => confirmPick()}>
{t('common::confirm')}
</Button>
</>}
{asPicker ? (<>
<Button onClick={() => onCancel()} style={{ marginRight: 6 }}>
{t('common::action.cancel')}
</Button>
<Button type="primary" disabled={!selectedId} onClick={() => confirmPick()}>
{t('common::confirm')}
</Button>
</>) : (oakExecutable === true ?
<>
<Button onClick={() => resetAll()}>
{t('common::reset')}
</Button>
<Button type="primary" onClick={() => doUpdate()}>
{t('common::action.update')}
</Button>
</> :
allowCreate && <Button type="primary" onClick={() => newAccount()}>
{t('common::action.create')}
</Button>)}
</div>
</div>);
}

View File

@ -0,0 +1,16 @@
import React from 'react';
import { EntityDict } from "../../../oak-app-domain";
import { RowWithActions, WebComponentProps } from "oak-frontend-base";
export default function render(props: WebComponentProps<EntityDict, 'withdrawAccount', false, {
withdrawAccount?: RowWithActions<EntityDict, 'withdrawAccount'>;
channels?: {
label: string;
value: string;
}[];
channel?: EntityDict['withdrawChannel']['Schema'];
isBank?: boolean;
isOffline?: boolean;
}, {
onSetChannelId: (id: string) => void;
onUpdate: <T extends keyof EntityDict['withdrawAccount']['OpSchema']>(attr: T, value: EntityDict['withdrawAccount']['OpSchema'][T] | null) => void;
}>): React.JSX.Element | undefined;

View File

@ -1 +1,34 @@
"use strict";
import React from 'react';
import { Form, Switch, Input, Selector } from 'antd-mobile';
export default function render(props) {
const { withdrawAccount, channels, channel, isBank } = props.data;
const { t, onSetChannelId, onUpdate } = props.methods;
if (withdrawAccount) {
return (<Form layout="horizontal">
{channels && <Form.Item label={t('withdrawAccount:attr.channel')}>
<Selector disabled={!channels} options={channels} value={channel ? [channel.id] : undefined} onChange={(ids) => {
onSetChannelId(ids[0]);
}}/>
</Form.Item>}
{isBank &&
<Form.Item label={t('label.bank.org')}>
<Input value={withdrawAccount.org} onChange={(value) => {
onUpdate('org', value);
}}/>
</Form.Item>}
<Form.Item label={isBank ? t('label.bank.name') : t('label.others.name')}>
<Input value={withdrawAccount.name} onChange={(value) => {
onUpdate('name', value);
}}/>
</Form.Item>
<Form.Item label={t('withdrawAccount:attr.code')}>
<Input value={withdrawAccount.code} onChange={(value) => {
onUpdate('code', value);
}}/>
</Form.Item>
<Form.Item label={t('withdrawAccount:attr.isDefault')}>
<Switch checked={withdrawAccount.isDefault} onChange={(value) => onUpdate('isDefault', value)}/>
</Form.Item>
</Form>);
}
}

View File

@ -3,6 +3,8 @@ export const authDeduceRelationMap = {};
export const selectFreeEntities = [
'offlineAccount',
'wpProduct',
'withdrawChannel',
'wpAccount',
];
export const updateFreeDict = {};
export default {

View File

@ -38,6 +38,41 @@ const actionAuths = [
id: 'user-account-withdraw',
pathId: 'user-account-withdraw',
deActions: ['select'],
},
//account
{
id: 'account-user',
pathId: 'user-acc',
deActions: ['select', 'deposit', 'withdraw', 'consume', 'loan'],
},
//deposit
{
id: 'deposit-creator',
pathId: 'creator-deposit',
deActions: ['create'],
},
{
id: 'deposit-account-user',
pathId: 'user-account-deposit',
deActions: ['fail'],
},
// accountOper
{
id: 'user-acc-oper',
pathId: 'user-acc-oper',
deActions: ['select', 'create'],
},
// withdrawAccount
{
id: 'user-withdrawAccount',
pathId: 'user-withdrawAccount',
deActions: ['select', 'create', 'remove', 'disable', 'update'],
},
// withdrawTransfer
{
id: 'user-acc-wdtransfer',
pathId: 'user-acc-wdtransfer',
deActions: ['select', 'create'],
}
];
export default actionAuths;

View File

@ -457,9 +457,10 @@ const i18ns = [
module: "oak-pay-business",
position: "src/components/withdraw/list",
data: {
"loss": "预计手续费%{value}%元",
"dealLoss": "手续费%{value}%元",
"count": "将分%{value}笔提现到您账户"
"loss": "预计手续费%{value}元",
"dealLoss": "手续费%{value}元",
"count": "将分%{value}笔提现到您账户",
"noData": "您尚无提现记录"
}
},
{
@ -471,7 +472,8 @@ const i18ns = [
data: {
"noData": "您还没有配置过提现账号",
"confirmDelete": "确定删除",
"areYouSure": "确认删除本账号吗?"
"areYouSure": "确认删除本账号吗?",
"default": "默认"
}
},
{

View File

@ -68,6 +68,48 @@ const paths = [
destEntity: 'withdraw',
value: 'account.user',
recursive: false,
},
{
id: 'user-acc',
sourceEntity: 'user',
destEntity: 'account',
value: 'user',
recursive: false,
},
{
id: 'user-acc-oper',
sourceEntity: 'user',
destEntity: 'accountOper',
value: 'account.user',
recursive: false,
},
{
id: 'creator-deposit',
sourceEntity: 'user',
destEntity: 'deposit',
value: 'creator',
recursive: false,
},
{
id: 'user-account-deposit',
sourceEntity: 'user',
destEntity: 'deposit',
value: 'account.user',
recursive: false,
},
{
id: 'user-withdrawAccount',
sourceEntity: 'user',
destEntity: 'withdrawAccount',
value: 'user',
recursive: false,
},
{
id: 'user-acc-wdtransfer',
sourceEntity: 'user',
destEntity: 'withdrawTransfer',
value: 'withdraw.account.user',
recursive: false,
}
];
export default paths;

View File

@ -1,10 +1,9 @@
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import assert from 'assert';
import { getPayClazzAccountEntities } from '../utils/payClazz';
// 这里一定要注意两者的先后顺序如果有注入更多的payEntity的话
const entities = getPayClazzAccountEntities();
import { accountEntities } from '../checkers/abstractChecker';
const triggers = [
...entities.filter(ele => !!ele).map((entity) => [
...accountEntities.filter(ele => !!ele).map((entity) => [
{
name: `${entity}的帐户生成时则生成对应的withDrawAccount`,
entity,

View File

@ -150,6 +150,7 @@ const triggers = [
entity: 'pay',
action: 'create',
when: 'after',
asRoot: true,
fn: async ({ operation }, context, option) => {
const { data } = operation;
assert(!(data instanceof Array));
@ -171,6 +172,7 @@ const triggers = [
action: ['startPaying', 'succeedPaying', 'continuePaying', 'startClosing', 'succeedClosing', 'startRefunding',
'refundAll', 'refundPartially'],
when: 'after',
asRoot: true,
fn: async ({ operation }, context, option) => {
const { data, filter, action, id } = operation;
assert(typeof filter.id === 'string');
@ -286,6 +288,7 @@ const triggers = [
action: 'close',
when: 'before',
priority: 99,
asRoot: true,
fn: async ({ operation }, context, option) => {
const { data, filter } = operation;
const pays = await context.select('pay', {
@ -372,6 +375,7 @@ const triggers = [
entity: 'pay',
action: 'succeedPaying',
when: 'after',
asRoot: true,
fn: async ({ operation }, context) => {
const { data, filter } = operation;
const projection = {
@ -441,6 +445,7 @@ const triggers = [
{
name: '当account类型的pay的paid达到price改为支付成功',
entity: 'pay',
asRoot: true,
action: ['startPaying', 'continuePaying'],
check(operation) {
return (!!operation.data.paid) && operation.data.paid > 0;

View File

@ -117,6 +117,7 @@ const triggers = [
entity: 'refund',
action: 'succeedRefunding',
when: 'after',
asRoot: true,
name: '退款成功时更新对应的pay状态以及对应的withdraw状态',
fn: async ({ operation }, context) => {
const { filter } = operation;
@ -210,6 +211,7 @@ const triggers = [
entity: 'refund',
action: 'failRefunding',
when: 'after',
asRoot: true,
name: '退款失败时更新对应的pay状态以及对应的withdraw状态',
fn: async ({ operation }, context) => {
const { filter } = operation;
@ -252,6 +254,7 @@ const triggers = [
entity: 'refund',
name: '当发起退款时将对应的pay置退款中状态',
action: 'create',
asRoot: true,
when: 'before',
fn: async ({ operation }, context) => {
const { data } = operation;

View File

@ -1,76 +1,2 @@
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import assert from 'assert';
const triggers = [
{
name: '当withdrawAccount帐户被设置为default时将其它的default设为false',
entity: 'withdrawAccount',
action: 'update',
check(operation) {
return operation.data.isDefault === true;
},
when: 'after',
fn: async ({ operation }, context, option) => {
const { filter } = operation;
const accounts = await context.select('withdrawAccount', {
data: {
id: 1,
entity: 1,
entityId: 1,
},
filter,
}, {});
assert(accounts.length === 1);
for (const account of accounts) {
const { entity, entityId, id } = account;
await context.operate('withdrawAccount', {
id: await generateNewIdAsync(),
action: 'update',
data: {
isDefault: false,
},
filter: {
entity,
entityId,
isDefault: true,
id: {
$ne: id,
},
}
}, {});
}
return 1;
},
},
{
name: '当withdrawAccount帐户创建为default时将其它的default设为false',
entity: 'withdrawAccount',
action: 'create',
when: 'after',
fn: async ({ operation }, context, option) => {
const { data } = operation;
assert(!(data instanceof Array));
const { id, entity, entityId, isDefault } = data;
assert(entity && entityId);
if (isDefault) {
await context.operate('withdrawAccount', {
id: await generateNewIdAsync(),
action: 'update',
data: {
isDefault: false,
},
filter: {
entity,
entityId,
isDefault: true,
id: {
$ne: id,
},
}
}, {});
return 1;
}
return 0;
}
},
];
const triggers = [];
export default triggers;

View File

@ -2,11 +2,11 @@ import { EntityDict } from '../../oak-app-domain';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
import PayClazz from '../../types/PayClazz';
import { BRC } from '../../types/RuntimeCxt';
import { StorageSchema } from 'oak-domain/lib/types/Storage';
type PayClazzConstructor = (applicationId: string, entityId: string, context: BRC) => Promise<PayClazz>;
export declare function registerPayClazzEntity<ED extends EntityDict & BaseEntityDict>(entity: keyof ED, def: {
export declare function registerPayClazzEntity<ED extends EntityDict & BaseEntityDict, T extends keyof ED>(entity: T, def: {
clazzConstructor: PayClazzConstructor;
accountEntity: keyof ED;
}): void;
export declare function getPayClazzAccountEntities<ED extends EntityDict & BaseEntityDict>(): (keyof ED)[];
}, schema: StorageSchema<ED>): void;
export declare function getPayClazz(applicationId: string, entity: EntityDict['pay']['OpSchema']['entity'], entityId: string, context: BRC): Promise<PayClazz>;
export {};

View File

@ -2,6 +2,7 @@ import assert from 'assert';
import Offline from './Offline';
import Account from './Account';
import WechatPay from './WechatPay';
import { registerAccountEntity } from '../../checkers/abstractChecker';
const PayChannelDict = {};
const PayClazzEntityDict = {
'account': {
@ -106,19 +107,27 @@ const PayClazzEntityDict = {
// 这里用一个flag来表达先后顺序如果有registerPayClazzEntity框架应保证register在get之前
// 目前因为没有registerPayClazzEntity所以没测可能不对。by Xc 20240608
let MODULE_USED = false;
export function registerPayClazzEntity(entity, def) {
assert(!MODULE_USED);
export function registerPayClazzEntity(entity, def, schema) {
if (!MODULE_USED) {
assert(!MODULE_USED);
}
PayClazzEntityDict[entity] = {
accountEntity: def.accountEntity,
clazzConstructor: def.clazzConstructor,
};
}
export function getPayClazzAccountEntities() {
MODULE_USED = true;
return Object.keys(PayClazzEntityDict).map(ele => PayClazzEntityDict[ele].accountEntity);
// 检查此entity是否符合注册要求
const { attributes } = schema[entity];
const { attributes: payAttr } = schema.pay;
const { attributes: accountAttr } = schema[def.accountEntity];
assert(payAttr.entity.enumeration?.includes(entity));
assert(accountAttr.price && accountAttr.price.type === 'decimal');
assert(accountAttr.systemId && accountAttr.systemId.type === 'ref' && accountAttr.systemId.ref === 'system');
registerAccountEntity(def.accountEntity);
}
export async function getPayClazz(applicationId, entity, entityId, context) {
MODULE_USED = true;
if (!MODULE_USED) {
assert(!MODULE_USED);
}
const key = entity === 'account' ? entity : `${entity}.${entityId}`;
if (PayChannelDict.hasOwnProperty(key)) {
return PayChannelDict[key];

View File

@ -147,6 +147,9 @@ async function getWithdrawCreateData(params, context) {
data: ele,
})));
}
else {
data.refund$entity = [];
}
if (totalPrice > price2) {
// 如果还有要退的部分就从withdrawAccount来进行转账
const rest = totalPrice - price2;
@ -200,6 +203,10 @@ async function getWithdrawCreateData(params, context) {
}
];
}
else {
// 保持结构完整让上层可以和withdraw$detail同构渲染
data.withdrawTransfer$withdraw = [];
}
data.loss = totalLoss;
return data;
}

View File

@ -1,5 +1,7 @@
import { EntityDict } from '../oak-app-domain';
import { RuntimeCxt } from '../types/RuntimeCxt';
import { Checker } from 'oak-domain/lib/types/Auth';
export declare function registerAccountEntity<ED extends EntityDict>(entity: keyof ED): void;
export declare const accountEntities: string[];
declare const triggers: Checker<EntityDict, keyof EntityDict, RuntimeCxt>[];
export default triggers;

View File

@ -1,11 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.accountEntities = exports.registerAccountEntity = void 0;
const uuid_1 = require("oak-domain/lib/utils/uuid");
const payClazz_1 = require("../utils/payClazz");
// 当注入一个新的pay entity时将account的删除与之相关联
const entities = (0, payClazz_1.getPayClazzAccountEntities)();
// 当注入一个新的account entity时将withdrawChannel的删除与之相关联
function registerAccountEntity(entity) {
exports.accountEntities.push(entity);
}
exports.registerAccountEntity = registerAccountEntity;
exports.accountEntities = ['wpAccount', 'offlineAccount'];
const triggers = [
...entities.filter(ele => !!ele).map((entity) => [
...exports.accountEntities.filter(ele => !!ele).map((entity) => [
{
entity,
action: 'remove',

View File

@ -8,7 +8,9 @@ const application_1 = tslib_1.__importDefault(require("./application"));
const offlineAccount_1 = tslib_1.__importDefault(require("./offlineAccount"));
const wpProduct_1 = tslib_1.__importDefault(require("./wpProduct"));
const abstractChecker_1 = tslib_1.__importDefault(require("./abstractChecker"));
const withdrawAccount_1 = tslib_1.__importDefault(require("./withdrawAccount"));
const checkers = [
...withdrawAccount_1.default,
...abstractChecker_1.default,
...accountOper_1.default,
...pay_1.default,

5
lib/checkers/withdrawAccount.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { Checker } from 'oak-domain/lib/types/Auth';
import { EntityDict } from '../oak-app-domain';
import { RuntimeCxt } from '../types/RuntimeCxt';
declare const checkers: Checker<EntityDict, 'withdrawAccount', RuntimeCxt>[];
export default checkers;

View File

@ -0,0 +1,74 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const uuid_1 = require("oak-domain/lib/utils/uuid");
const executor_1 = require("oak-domain/lib/utils/executor");
const assert_1 = tslib_1.__importDefault(require("assert"));
const checkers = [
{
entity: 'withdrawAccount',
type: 'logical',
action: 'create',
checker(operation, context, option) {
const { data } = operation;
if (data) {
const { id, entity, entityId, isDefault } = data;
if (entity && entityId && isDefault) {
return context.operate('withdrawAccount', {
id: (0, uuid_1.generateNewId)(),
action: 'update',
data: {
isDefault: false,
},
filter: {
entity,
entityId,
isDefault: true,
id: {
$ne: id,
},
}
}, option);
}
}
}
},
{
entity: 'withdrawAccount',
type: 'logical',
action: 'update',
checker(operation, context, option) {
const { data, filter } = operation;
if (data?.isDefault) {
return (0, executor_1.pipeline)(() => context.select('withdrawAccount', {
data: {
id: 1,
entity: 1,
entityId: 1,
},
filter,
}, {}), (accounts) => {
(0, assert_1.default)(accounts.length === 1);
const [account] = accounts;
const { entity, entityId, id } = account;
return context.operate('withdrawAccount', {
id: (0, uuid_1.generateNewId)(),
action: 'update',
data: {
isDefault: false,
},
filter: {
entity,
entityId,
isDefault: true,
id: {
$ne: id,
},
}
}, option);
});
}
}
}
];
exports.default = checkers;

View File

@ -6,6 +6,8 @@ exports.authDeduceRelationMap = {};
exports.selectFreeEntities = [
'offlineAccount',
'wpProduct',
'withdrawChannel',
'wpAccount',
];
exports.updateFreeDict = {};
exports.default = {

View File

@ -40,6 +40,41 @@ const actionAuths = [
id: 'user-account-withdraw',
pathId: 'user-account-withdraw',
deActions: ['select'],
},
//account
{
id: 'account-user',
pathId: 'user-acc',
deActions: ['select', 'deposit', 'withdraw', 'consume', 'loan'],
},
//deposit
{
id: 'deposit-creator',
pathId: 'creator-deposit',
deActions: ['create'],
},
{
id: 'deposit-account-user',
pathId: 'user-account-deposit',
deActions: ['fail'],
},
// accountOper
{
id: 'user-acc-oper',
pathId: 'user-acc-oper',
deActions: ['select', 'create'],
},
// withdrawAccount
{
id: 'user-withdrawAccount',
pathId: 'user-withdrawAccount',
deActions: ['select', 'create', 'remove', 'disable', 'update'],
},
// withdrawTransfer
{
id: 'user-acc-wdtransfer',
pathId: 'user-acc-wdtransfer',
deActions: ['select', 'create'],
}
];
exports.default = actionAuths;

View File

@ -459,9 +459,10 @@ const i18ns = [
module: "oak-pay-business",
position: "src/components/withdraw/list",
data: {
"loss": "预计手续费%{value}%元",
"dealLoss": "手续费%{value}%元",
"count": "将分%{value}笔提现到您账户"
"loss": "预计手续费%{value}元",
"dealLoss": "手续费%{value}元",
"count": "将分%{value}笔提现到您账户",
"noData": "您尚无提现记录"
}
},
{
@ -473,7 +474,8 @@ const i18ns = [
data: {
"noData": "您还没有配置过提现账号",
"confirmDelete": "确定删除",
"areYouSure": "确认删除本账号吗?"
"areYouSure": "确认删除本账号吗?",
"default": "默认"
}
},
{

View File

@ -70,6 +70,48 @@ const paths = [
destEntity: 'withdraw',
value: 'account.user',
recursive: false,
},
{
id: 'user-acc',
sourceEntity: 'user',
destEntity: 'account',
value: 'user',
recursive: false,
},
{
id: 'user-acc-oper',
sourceEntity: 'user',
destEntity: 'accountOper',
value: 'account.user',
recursive: false,
},
{
id: 'creator-deposit',
sourceEntity: 'user',
destEntity: 'deposit',
value: 'creator',
recursive: false,
},
{
id: 'user-account-deposit',
sourceEntity: 'user',
destEntity: 'deposit',
value: 'account.user',
recursive: false,
},
{
id: 'user-withdrawAccount',
sourceEntity: 'user',
destEntity: 'withdrawAccount',
value: 'user',
recursive: false,
},
{
id: 'user-acc-wdtransfer',
sourceEntity: 'user',
destEntity: 'withdrawTransfer',
value: 'withdraw.account.user',
recursive: false,
}
];
exports.default = paths;

View File

@ -3,11 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const uuid_1 = require("oak-domain/lib/utils/uuid");
const assert_1 = tslib_1.__importDefault(require("assert"));
const payClazz_1 = require("../utils/payClazz");
// 这里一定要注意两者的先后顺序如果有注入更多的payEntity的话
const entities = (0, payClazz_1.getPayClazzAccountEntities)();
const abstractChecker_1 = require("../checkers/abstractChecker");
const triggers = [
...entities.filter(ele => !!ele).map((entity) => [
...abstractChecker_1.accountEntities.filter(ele => !!ele).map((entity) => [
{
name: `${entity}的帐户生成时则生成对应的withDrawAccount`,
entity,

View File

@ -153,6 +153,7 @@ const triggers = [
entity: 'pay',
action: 'create',
when: 'after',
asRoot: true,
fn: async ({ operation }, context, option) => {
const { data } = operation;
(0, assert_1.default)(!(data instanceof Array));
@ -174,6 +175,7 @@ const triggers = [
action: ['startPaying', 'succeedPaying', 'continuePaying', 'startClosing', 'succeedClosing', 'startRefunding',
'refundAll', 'refundPartially'],
when: 'after',
asRoot: true,
fn: async ({ operation }, context, option) => {
const { data, filter, action, id } = operation;
(0, assert_1.default)(typeof filter.id === 'string');
@ -289,6 +291,7 @@ const triggers = [
action: 'close',
when: 'before',
priority: 99,
asRoot: true,
fn: async ({ operation }, context, option) => {
const { data, filter } = operation;
const pays = await context.select('pay', {
@ -375,6 +378,7 @@ const triggers = [
entity: 'pay',
action: 'succeedPaying',
when: 'after',
asRoot: true,
fn: async ({ operation }, context) => {
const { data, filter } = operation;
const projection = {
@ -444,6 +448,7 @@ const triggers = [
{
name: '当account类型的pay的paid达到price改为支付成功',
entity: 'pay',
asRoot: true,
action: ['startPaying', 'continuePaying'],
check(operation) {
return (!!operation.data.paid) && operation.data.paid > 0;

View File

@ -120,6 +120,7 @@ const triggers = [
entity: 'refund',
action: 'succeedRefunding',
when: 'after',
asRoot: true,
name: '退款成功时更新对应的pay状态以及对应的withdraw状态',
fn: async ({ operation }, context) => {
const { filter } = operation;
@ -213,6 +214,7 @@ const triggers = [
entity: 'refund',
action: 'failRefunding',
when: 'after',
asRoot: true,
name: '退款失败时更新对应的pay状态以及对应的withdraw状态',
fn: async ({ operation }, context) => {
const { filter } = operation;
@ -255,6 +257,7 @@ const triggers = [
entity: 'refund',
name: '当发起退款时将对应的pay置退款中状态',
action: 'create',
asRoot: true,
when: 'before',
fn: async ({ operation }, context) => {
const { data } = operation;

View File

@ -1,79 +1,4 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const uuid_1 = require("oak-domain/lib/utils/uuid");
const assert_1 = tslib_1.__importDefault(require("assert"));
const triggers = [
{
name: '当withdrawAccount帐户被设置为default时将其它的default设为false',
entity: 'withdrawAccount',
action: 'update',
check(operation) {
return operation.data.isDefault === true;
},
when: 'after',
fn: async ({ operation }, context, option) => {
const { filter } = operation;
const accounts = await context.select('withdrawAccount', {
data: {
id: 1,
entity: 1,
entityId: 1,
},
filter,
}, {});
(0, assert_1.default)(accounts.length === 1);
for (const account of accounts) {
const { entity, entityId, id } = account;
await context.operate('withdrawAccount', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'update',
data: {
isDefault: false,
},
filter: {
entity,
entityId,
isDefault: true,
id: {
$ne: id,
},
}
}, {});
}
return 1;
},
},
{
name: '当withdrawAccount帐户创建为default时将其它的default设为false',
entity: 'withdrawAccount',
action: 'create',
when: 'after',
fn: async ({ operation }, context, option) => {
const { data } = operation;
(0, assert_1.default)(!(data instanceof Array));
const { id, entity, entityId, isDefault } = data;
(0, assert_1.default)(entity && entityId);
if (isDefault) {
await context.operate('withdrawAccount', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'update',
data: {
isDefault: false,
},
filter: {
entity,
entityId,
isDefault: true,
id: {
$ne: id,
},
}
}, {});
return 1;
}
return 0;
}
},
];
const triggers = [];
exports.default = triggers;

View File

@ -2,11 +2,11 @@ import { EntityDict } from '../../oak-app-domain';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
import PayClazz from '../../types/PayClazz';
import { BRC } from '../../types/RuntimeCxt';
import { StorageSchema } from 'oak-domain/lib/types/Storage';
type PayClazzConstructor = (applicationId: string, entityId: string, context: BRC) => Promise<PayClazz>;
export declare function registerPayClazzEntity<ED extends EntityDict & BaseEntityDict>(entity: keyof ED, def: {
export declare function registerPayClazzEntity<ED extends EntityDict & BaseEntityDict, T extends keyof ED>(entity: T, def: {
clazzConstructor: PayClazzConstructor;
accountEntity: keyof ED;
}): void;
export declare function getPayClazzAccountEntities<ED extends EntityDict & BaseEntityDict>(): (keyof ED)[];
}, schema: StorageSchema<ED>): void;
export declare function getPayClazz(applicationId: string, entity: EntityDict['pay']['OpSchema']['entity'], entityId: string, context: BRC): Promise<PayClazz>;
export {};

View File

@ -1,11 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPayClazz = exports.getPayClazzAccountEntities = exports.registerPayClazzEntity = void 0;
exports.getPayClazz = exports.registerPayClazzEntity = void 0;
const tslib_1 = require("tslib");
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"));
const abstractChecker_1 = require("../../checkers/abstractChecker");
const PayChannelDict = {};
const PayClazzEntityDict = {
'account': {
@ -110,21 +111,28 @@ const PayClazzEntityDict = {
// 这里用一个flag来表达先后顺序如果有registerPayClazzEntity框架应保证register在get之前
// 目前因为没有registerPayClazzEntity所以没测可能不对。by Xc 20240608
let MODULE_USED = false;
function registerPayClazzEntity(entity, def) {
(0, assert_1.default)(!MODULE_USED);
function registerPayClazzEntity(entity, def, schema) {
if (!MODULE_USED) {
(0, assert_1.default)(!MODULE_USED);
}
PayClazzEntityDict[entity] = {
accountEntity: def.accountEntity,
clazzConstructor: def.clazzConstructor,
};
// 检查此entity是否符合注册要求
const { attributes } = schema[entity];
const { attributes: payAttr } = schema.pay;
const { attributes: accountAttr } = schema[def.accountEntity];
(0, assert_1.default)(payAttr.entity.enumeration?.includes(entity));
(0, assert_1.default)(accountAttr.price && accountAttr.price.type === 'decimal');
(0, assert_1.default)(accountAttr.systemId && accountAttr.systemId.type === 'ref' && accountAttr.systemId.ref === 'system');
(0, abstractChecker_1.registerAccountEntity)(def.accountEntity);
}
exports.registerPayClazzEntity = registerPayClazzEntity;
function getPayClazzAccountEntities() {
MODULE_USED = true;
return Object.keys(PayClazzEntityDict).map(ele => PayClazzEntityDict[ele].accountEntity);
}
exports.getPayClazzAccountEntities = getPayClazzAccountEntities;
async function getPayClazz(applicationId, entity, entityId, context) {
MODULE_USED = true;
if (!MODULE_USED) {
(0, assert_1.default)(!MODULE_USED);
}
const key = entity === 'account' ? entity : `${entity}.${entityId}`;
if (PayChannelDict.hasOwnProperty(key)) {
return PayChannelDict[key];

View File

@ -1,15 +1,17 @@
import { generateNewId } from 'oak-domain/lib/utils/uuid';
import { EntityDict } from '../oak-app-domain';
import { BRC, RuntimeCxt } from '../types/RuntimeCxt';
import assert from 'assert';
import { getPayClazzAccountEntities } from '../utils/payClazz';
import { Checker, LogicalChecker } from 'oak-domain/lib/types/Auth';
// 当注入一个新的pay entity时将account的删除与之相关联
const entities = getPayClazzAccountEntities<EntityDict>();
// 当注入一个新的account entity时将withdrawChannel的删除与之相关联
export function registerAccountEntity<ED extends EntityDict>(entity: keyof ED) {
accountEntities.push(entity as string);
}
export const accountEntities = ['wpAccount', 'offlineAccount'];
const triggers: Checker<EntityDict, keyof EntityDict, RuntimeCxt>[] = [
...entities.filter(ele => !!ele).map(
...accountEntities.filter(ele => !!ele).map(
(entity) => [
{
entity,

View File

@ -12,10 +12,25 @@ export default OakComponent({
systemId: 1,
price: 1,
enabled: 1,
taxLossRatio: 1,
refundCompensateRatio: 1,
refundGapDays: 1,
allowWithdrawTransfer: 1,
withdrawTransferLossRatio: 1,
},
properties: {
systemId: '',
},
filters: [
{
filter() {
const { systemId } = this.props;
return {
systemId,
};
}
}
],
formData({ data, legalActions }) {
return {
accounts: data.map(

View File

@ -12,7 +12,7 @@
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
align-items: stretch;
.item {
border: 0.1px solid silver;

View File

@ -8,31 +8,33 @@ import Styles from './web.pc.module.less';
import Upsert from '../upsert';
import { OakAttrNotNullException, OakException } from 'oak-domain/lib/types';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { ToYuan } from 'oak-domain/lib/utils/money';
function OfflineAccount(props: {
account: RowWithActions<EntityDict, 'offlineAccount'> & { color: string };
export function OfflineAccount(props: {
data: RowWithActions<EntityDict, 'offlineAccount'> & { color: string };
t: (k: string, param?: any) => string;
onUpdate: () => void;
onRemove: () => void;
onQrCodeClick: () => void;
onUpdate?: () => void;
onRemove?: () => void;
onQrCodeClick?: () => void;
}) {
const { account, t, onUpdate, onRemove, onQrCodeClick } = props;
const { type, channel, name, qrCode, allowDeposit, allowPay, color, enabled, price } = account;
const legalActions = account['#oakLegalActions'];
const { data: account, t, onUpdate, onRemove, onQrCodeClick } = props;
const { type, channel, name, qrCode, allowDeposit, allowPay, color, enabled, price,
taxLossRatio, refundCompensateRatio, refundGapDays, allowWithdrawTransfer, withdrawTransferLossRatio
} = account;
return (
<Descriptions
style={{ width: 340, height: 435 }}
style={{ width: 380 }}
title={t(`offlineAccount:v.type.${type}`)}
extra={<>
{legalActions.includes('update') && <Button
{onUpdate && <Button
style={{ marginRight: 4 }}
size="small"
onClick={() => onUpdate()}
>
{t('common::action.update')}
</Button>}
{legalActions.includes('remove') && <Button
{onRemove && <Button
danger
size="small"
onClick={() => onRemove()}
@ -43,7 +45,7 @@ function OfflineAccount(props: {
column={1}
bordered
size="small"
labelStyle={{ width: 98 }}
labelStyle={{ width: 198 }}
>
<Descriptions.Item label={t('offlineAccount:attr.type')}>{t(`offlineAccount:v.type.${type}`)}</Descriptions.Item>
{channel && <Descriptions.Item label={t(`offlineAccount::label.channel.${type}`)}>{channel}</Descriptions.Item>}
@ -51,10 +53,15 @@ function OfflineAccount(props: {
{qrCode && <Descriptions.Item label={t(`offlineAccount::label.qrCode.${type}`)}>
{type === 'bank' ? qrCode : <span className={Styles.qrCode} onClick={onQrCodeClick}><QRCode value={qrCode} size={name ? 80 : 128} color={color} /></span>}
</Descriptions.Item>}
<Descriptions.Item label={t('offlineAccount:attr.price')}>{price}</Descriptions.Item>
<Descriptions.Item label={t('offlineAccount:attr.allowDeposit')}>{t(`common::${allowDeposit}`)}</Descriptions.Item>
<Descriptions.Item label={t('offlineAccount:attr.price')}>{ToYuan(price!)}{t('common::pay.scale')}</Descriptions.Item>
<Descriptions.Item label={t('offlineAccount:attr.allowPay')}>{t(`common::${allowPay}`)}</Descriptions.Item>
<Descriptions.Item label={t('offlineAccount:attr.enabled')}>{t(`common::${enabled}`)}</Descriptions.Item>
<Descriptions.Item label={t('offlineAccount:attr.taxLossRatio')}>{taxLossRatio}%</Descriptions.Item>
<Descriptions.Item label={t('offlineAccount:attr.refundCompensateRatio')}>{refundCompensateRatio}%</Descriptions.Item>
<Descriptions.Item label={t('offlineAccount:attr.refundGapDays')}>{refundGapDays}</Descriptions.Item>
<Descriptions.Item label={t('offlineAccount:attr.taxLossRatio')}>{taxLossRatio}%</Descriptions.Item>
<Descriptions.Item label={t('offlineAccount:attr.allowWithdrawTransfer')}>{t(`common::${allowWithdrawTransfer}`)}</Descriptions.Item>
<Descriptions.Item label={t('offlineAccount:attr.withdrawTransferLossRatio')}>{withdrawTransferLossRatio}%</Descriptions.Item>
</Descriptions>
);
}
@ -129,9 +136,9 @@ export default function render(props: WebComponentProps<EntityDict, 'offlineAcco
accounts.filter(ele => ele.$$createAt$$ as number > 1).map(
(ele, idx) => <div className={Styles.item} key={idx}>
<OfflineAccount
account={ele}
data={ele}
t={t}
onRemove={() => {
onRemove={ele['#oakLegalActions']?.includes('remove') ? () => {
Modal.confirm({
title: t('confirmDelete'),
content: t('areYouSure'),
@ -149,8 +156,8 @@ export default function render(props: WebComponentProps<EntityDict, 'offlineAcco
}
]),
})
}}
onUpdate={() => setUpsertId(ele.id!)}
} : undefined}
onUpdate={ele['#oakLegalActions']?.includes('update') ? () => setUpsertId(ele.id!) : undefined}
onQrCodeClick={() => setShowQrCodeId(ele.id!)}
/>
</div>

View File

@ -25,6 +25,8 @@ export default OakComponent({
return {
operation: operation && operation[0].operation,
system: data,
canUpdate: !!data?.['#oakLegalActions']?.includes('update'),
};
},
actions: ['update'],
});

View File

@ -30,7 +30,7 @@ export function registerPayChannelComponent<ED extends EntityDict & BaseEntityDi
function PayConfig(props: {
payConfig: EntityDict['system']['OpSchema']['payConfig'];
update: (config: EntityDict['system']['OpSchema']['payConfig']) => void;
update?: (config: EntityDict['system']['OpSchema']['payConfig']) => void;
t: (key: string) => string;
}) {
const { payConfig, update, t } = props;
@ -38,7 +38,7 @@ function PayConfig(props: {
const depositLoss = payConfig?.depositLoss;
const updateDepositLoss = (data: Partial<NonNullable<EntityDict['system']['OpSchema']['payConfig']>['depositLoss']>) => {
update({
update && update({
depositLoss: {
...depositLoss,
...data,
@ -49,7 +49,7 @@ function PayConfig(props: {
});
};
const updateWithdrawLoss = (data: Partial<NonNullable<EntityDict['system']['OpSchema']['payConfig']>['withdrawLoss']>) => {
update({
update && update({
depositLoss: depositLoss || {},
withdrawLoss: {
conservative: !!(withdrawLoss?.conservative),
@ -84,6 +84,7 @@ function PayConfig(props: {
addonAfter={"%"}
step={0.01}
precision={2}
disabled={!update}
onChange={(value) => {
const ratio = value;
updateDepositLoss({
@ -99,6 +100,7 @@ function PayConfig(props: {
value={depositLoss?.highest}
min={0}
step={1}
disabled={!update}
onChange={(value) => {
const highest = value;
updateDepositLoss({
@ -115,6 +117,7 @@ function PayConfig(props: {
value={depositLoss?.lowest}
min={0}
step={1}
disabled={!update}
onChange={(value) => {
const lowest = value;
updateDepositLoss({
@ -144,6 +147,7 @@ function PayConfig(props: {
label={t('payConfig.label.conservative')}
>
<Switch
disabled={!update}
value={withdrawLoss?.conservative}
onChange={(conservative) => {
updateWithdrawLoss({ conservative });
@ -154,7 +158,7 @@ function PayConfig(props: {
label={t('payConfig.label.ratio')}
>
<InputNumber
disabled={!!withdrawLoss?.conservative}
disabled={!!withdrawLoss?.conservative || !update}
value={withdrawLoss?.ratio}
max={20}
min={0.01}
@ -173,7 +177,7 @@ function PayConfig(props: {
label={t('payConfig.label.highest')}
>
<InputNumber
disabled={!!withdrawLoss?.conservative}
disabled={!!withdrawLoss?.conservative || !update}
value={withdrawLoss?.highest}
min={0}
step={1}
@ -190,7 +194,7 @@ function PayConfig(props: {
label={t('payConfig.label.lowest')}
>
<InputNumber
disabled={!!withdrawLoss?.conservative}
disabled={!!withdrawLoss?.conservative || !update}
value={withdrawLoss?.lowest}
min={0}
step={1}
@ -207,7 +211,7 @@ function PayConfig(props: {
label={t('payConfig.label.trim')}
>
<Radio.Group
disabled={!!withdrawLoss?.conservative}
disabled={!!withdrawLoss?.conservative || !update}
options={[
{
label: t('payConfig.label.jiao'),
@ -238,8 +242,9 @@ export default function render(props: WebComponentProps<EntityDict, 'system', fa
system: RowWithActions<EntityDict, 'system'>;
operation?: EntityDict['system']['Update'];
serverUrl?: string;
canUpdate?: boolean;
}>) {
const { system, oakFullpath, operation, oakDirty, serverUrl, oakExecutable } = props.data;
const { system, oakFullpath, operation, oakDirty, serverUrl, oakExecutable, canUpdate } = props.data;
const { t, update, clean, execute } = props.methods;
if (system && oakFullpath) {
@ -262,7 +267,7 @@ export default function render(props: WebComponentProps<EntityDict, 'system', fa
>
<PayConfig
payConfig={system.payConfig}
update={(payConfig) => update({ payConfig })}
update={canUpdate ? (payConfig) => update({ payConfig }) : undefined}
t={t}
/>
<Flex

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,95 @@
import assert from 'assert';
import { EntityDict } from "../../../oak-app-domain";
import { uniq } from "oak-domain/lib/utils/lodash";
export default OakComponent({
properties: {
entities: [] as string[],
systemId: '',
},
lifetimes: {
ready() {
this.refreshData();
}
},
methods: {
refreshData() {
const { entities, systemId } = this.props;
const schema = this.features.cache.getSchema();
uniq(['wpAccount', 'offlineAccount'].concat(entities || [])).forEach(
(entity) => {
const projection: EntityDict['offlineAccount']['Selection']['data'] = {
id: 1,
$$createAt$$: 1,
$$updateAt$$: 1,
};
Object.keys(schema[entity as keyof EntityDict].attributes!).forEach(
ele => {
if (!ele.startsWith('$$')) {
projection[ele] = 1;
}
}
);
this.features.cache.refresh(entity as 'offlineAccount', {
data: projection,
filter: {
systemId,
}
});
}
);
},
},
formData({ features }) {
const { entities, systemId } = this.props;
assert(systemId);
const schema = features.cache.getSchema();
let total = 0;
const accounts = uniq(['wpAccount', 'offlineAccount'].concat(entities || [])).map(
(entity) => {
const projection: EntityDict['offlineAccount']['Selection']['data'] = {
id: 1,
$$createAt$$: 1,
$$updateAt$$: 1,
};
Object.keys(schema[entity as keyof EntityDict].attributes!).forEach(
ele => {
if (!ele.startsWith('$$')) {
projection[ele] = 1;
}
}
);
const data = this.features.cache.get(entity as 'offlineAccount', {
data: projection,
filter: {
systemId,
}
});
data.forEach(
ele => total += ele.price || 0,
);
return data.map(
(ele) => {
return {
entity,
data: {
...ele,
color: entity === 'offlineAccount' && features.style.getColor('offlineAccount', 'type', ele.type!),
}
}
}
);
}
).flat();
return {
total,
accounts,
systemId,
};
},
features: ['cache'],
})

View File

@ -0,0 +1,7 @@
{
"sysAccount": "系统账户统计",
"total": "总余额",
"count": "账户个数",
"qrCode": "二维码收款",
"noDetailRender": "没有注入相应的详情渲染组件"
}

View File

@ -0,0 +1,67 @@
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
.total {
font-weight: bold;
color: var(--oak-color-primary);
.symbol {
margin-right: 4px;
}
}
.sysAccounts {
display: flex;
padding: 8px;
flex-wrap: wrap;
margin-top: 12px;
.sysAccount {
width: 244px;
height: 284px;
border: solid 0.1px silver;
border-radius: 5px;
margin-right: 14px;
display: flex;
flex-direction: column;
align-items: stretch;
.top {
height: 90px;
.title {
font-size: larger;
font-weight: bold;
font-family: 黑体;
}
.subtitle {
color: var(--oak-text-color-disabled);
font-size: smaller;
}
}
.middle {
flex: 1;
font-weight: bold;
color: var(--oak-color-primary);
font-size: large;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.bottom {
padding: 10px;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
}
}
}

View File

@ -0,0 +1,232 @@
import { EntityDict } from "../../../oak-app-domain";
import { RowWithActions, WebComponentProps } from "oak-frontend-base";
import React from 'react';
import { Descriptions, Tag, Form, Switch, Modal, Input, Select, Flex, Button } from 'antd';
import { AlipayOutlined, WechatOutlined } from '@ant-design/icons';
import Styles from './web.pc.module.less';
import { ThousandCont, ToYuan } from "oak-domain/lib/utils/money";
import { OfflineAccount as OADetail } from '../../offlineAccount/config/web.pc';
import { WpAccount as WADetail } from '../../wpAccount/config/web.pc';
function OfflineAccount(props: {
data: EntityDict['offlineAccount']['OpSchema'] & { color?: string };
t: (key: string) => string;
}) {
const { data, t } = props;
const { type, channel, color, name } = data;
switch (type) {
case 'bank': {
return (
<Flex
style={{ height: '100%' }}
vertical
gap="small"
align="center"
justify="center"
>
<Tag>{t('offlineAccount:name')}</Tag>
<div className={Styles.title}>{t(`offlineAccount:v.type.${type}`)}</div>
<div className={Styles.subtitle}>{channel}</div>
</Flex>
);
}
case 'alipay': {
return (
<Flex
style={{ height: '100%' }}
vertical
gap="small"
align="center"
justify="center"
>
<Tag>{t('offlineAccount:name')}</Tag>
<AlipayOutlined style={{ color, fontSize: 18 }} />
<div className={Styles.subtitle}>{name || t('qrCode')}</div>
</Flex>
);
}
case 'wechat': {
return (
<Flex
style={{ height: '100%' }}
vertical
gap="small"
align="center"
justify="center"
>
<Tag>{t('offlineAccount:name')}</Tag>
<WechatOutlined style={{ color, fontSize: 18 }} />
<div className={Styles.subtitle}>{name || t('qrCode')}</div>
</Flex>
);
}
case 'shouqianba':
case 'others': {
return (
<Flex
style={{ height: '100%' }}
vertical
gap="small"
align="center"
justify="center"
>
<Tag>{t('offlineAccount:name')}</Tag>
<div className={Styles.title}>{t(`offlineAccount:v.type.${type}`)}</div>
<div className={Styles.subtitle}>{name || t('qrCode')}</div>
</Flex>
);
}
}
}
function WpAccount(props: {
data: EntityDict['offlineAccount']['OpSchema'] & { color?: string };
t: (key: string) => string;
}) {
const { data, t } = props;
return (
<Flex
style={{ height: '100%' }}
vertical
gap="middle"
align="center"
justify="center"
>
<Tag>{t('wpAccount:name')}</Tag>
<WechatOutlined style={{ color: 'green', fontSize: 34 }} />
</Flex>
);
}
function GeneralAccount(props: {
entity: string;
t: (key: string) => string;
}) {
const { entity, t } = props;
return (
<Flex
style={{ height: '100%' }}
vertical
gap="small"
align="center"
justify="center"
>
<Tag>{t(`${entity}:name`)}</Tag>
</Flex>
);
}
const RenderSysAccountCardTopDict: Record<string, (props: {
data: any,
t: (k: string) => string
}) => JSX.Element> = {
'offlineAccount': OfflineAccount,
'wpAccount': WpAccount,
}
const RenderSysAccountDetailDict: Record<string, (props: {
data: any,
t: (k: string) => string
systemId: string,
}) => JSX.Element> = {
'offlineAccount': OADetail,
'wpAccount': WADetail,
}
export default function render(props: WebComponentProps<EntityDict, 'offlineAccount', false, {
total?: number;
accounts?: Array<{
entity: string;
data: any;
price: number
}>;
systemId?: string;
}>) {
const { accounts, total, systemId } = props.data;
const { t, setMessage } = props.methods;
if (accounts && systemId) {
return (
<div className={Styles.container}>
<Descriptions
title={t('sysAccount')}
bordered
items={[
{
label: t('total'),
children: <div className={Styles.total}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
<span className={Styles.value}>{ThousandCont(ToYuan(total || 0), 2)}</span>
</div>
},
{
label: t('count'),
children: accounts.length,
}
]}
column={2}
/>
<div className={Styles.sysAccounts}>
{
accounts.map(
({ entity, data }) => {
const TopRender = RenderSysAccountCardTopDict[entity];
return (
<div className={Styles.sysAccount}>
<div className={Styles.top}>
{
TopRender ? <TopRender
data={data}
t={t}
/> : <GeneralAccount
entity={entity}
t={t}
/>
}
</div>
<div className={Styles.middle}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
<span className={Styles.value}>{ThousandCont(ToYuan(data.price || 0), 2)}</span>
</div>
<div className={Styles.bottom}>
<Button
onClick={() => {
const DetailRender = RenderSysAccountDetailDict[entity];
if (DetailRender) {
Modal.info({
width: 560,
title: `${t(`${entity}:name`)}${t('common::action.detail')}`,
content: (
<DetailRender
data={data}
systemId={systemId}
t={t}
/>
),
onOk() { },
});
}
else {
setMessage({
title: t('noDetailRender'),
type: 'error',
})
}
}}
>
{t('common::action.detail')}
</Button>
</div>
</div>
);
}
)
}
</div>
</div>
);
}
return null;
}

View File

@ -5,14 +5,30 @@ export default OakComponent({
id: 1,
price: 1,
mchId: 1,
refundGapDays: 1,
taxLossRatio: 1,
enabled: 1,
taxLossRatio: 1,
refundCompensateRatio: 1,
refundGapDays: 1,
allowWithdrawTransfer: 1,
withdrawTransferLossRatio: 1,
},
properties: {
systemId: '',
},
filters: [
{
filter() {
const { systemId } = this.props;
return {
systemId,
};
}
}
],
formData({ data, legalActions }) {
return {
accounts: data,
canCreate: legalActions?.includes('create'),
canCreate: legalActions?.includes('create') && !data?.find(ele => ele.enabled),
};
},
actions: ['create', 'update', 'remove'],

View File

@ -9,77 +9,88 @@ import Upsert from '../upsert';
import WpProductConfig from '../../wpProduct/config';
import { OakAttrNotNullException, OakException } from 'oak-domain/lib/types';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { ToYuan } from 'oak-domain/lib/utils/money';
function WpAccount(props: {
account: RowWithActions<EntityDict, 'wpAccount'>,
oakFullpath: string;
export function WpAccount(props: {
data: RowWithActions<EntityDict, 'wpAccount'>,
systemId: string;
t: (k: string, param?: any) => string;
onUpdate: () => void;
onRemove: () => void;
onUpdate?: () => void;
onRemove?: () => void;
}) {
const { account, t, onUpdate, onRemove, oakFullpath, systemId } = props;
const { refundGapDays, mchId, enabled, price, taxLossRatio } = account;
const legalActions = account['#oakLegalActions'];
const { data: account, t, onUpdate, onRemove, systemId } = props;
const { refundGapDays, mchId, enabled, price, taxLossRatio,
refundCompensateRatio, allowWithdrawTransfer, withdrawTransferLossRatio
} = account;
const [activeKey, setActiveKey] = useState("1");
return (
<Tabs
activeKey={activeKey}
onTabClick={(activeKey) => {
setActiveKey(activeKey)
}}
tabBarExtraContent={
activeKey === "1" && <>
{legalActions.includes('update') && <Button
style={{ marginRight: 4 }}
size="small"
onClick={() => onUpdate()}
>
{t('common::action.update')}
</Button>}
{legalActions.includes('remove') && <Button
danger
size="small"
onClick={() => onRemove()}
>
{t('common::action.remove')}
</Button>}
</>
}
style={{ width: 380, height: 520 }}
items={[
{
key: '1',
label: t('common::action.detail'),
children: (
<Descriptions
column={1}
bordered
size="small"
>
<Descriptions.Item label={t('wpAccount:attr.mchId')}>{mchId}</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.price')}>{price}</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.refundGapDays')}>{refundGapDays}</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.taxLossRatio')}>{taxLossRatio}%</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.enabled')}>{t(`common::${enabled}`)}</Descriptions.Item>
</Descriptions>
)
},
{
key: '2',
label: t('wpProduct:name'),
children: (
<WpProductConfig
oakPath={`$$wpProduct-${account.id!}`}
systemId={systemId}
wpAccountId={account.id!}
/>
)
}
]}
/>
const D = (
<Descriptions
column={1}
bordered
title={t('wpAccount:name')}
size="small"
>
<Descriptions.Item label={t('wpAccount:attr.mchId')}>{mchId}</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.price')}>{ToYuan(price!)}{t('common::pay.scale')}</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.taxLossRatio')}>{taxLossRatio}%</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.refundCompensateRatio')}>{refundCompensateRatio}%</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.refundGapDays')}>{refundGapDays}</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.taxLossRatio')}>{taxLossRatio}%</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.allowWithdrawTransfer')}>{t(`common::${allowWithdrawTransfer}`)}</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.withdrawTransferLossRatio')}>{withdrawTransferLossRatio}%</Descriptions.Item>
<Descriptions.Item label={t('wpAccount:attr.enabled')}>{t(`common::${enabled}`)}</Descriptions.Item>
</Descriptions>
);
if (onUpdate) {
return (
<Tabs
activeKey={activeKey}
onTabClick={(activeKey) => {
setActiveKey(activeKey)
}}
tabBarExtraContent={
activeKey === "1" && <>
{onUpdate && <Button
style={{ marginRight: 4 }}
size="small"
onClick={() => onUpdate()}
>
{t('common::action.update')}
</Button>}
{onRemove && <Button
danger
size="small"
onClick={() => onRemove()}
>
{t('common::action.remove')}
</Button>}
</>
}
style={{ width: 380, height: 520 }}
items={[
{
key: '1',
label: t('common::action.detail'),
children: D,
},
{
key: '2',
label: t('wpProduct:name'),
children: (
<WpProductConfig
oakPath={`$$wpProduct-${account.id!}`}
systemId={systemId}
wpAccountId={account.id!}
/>
)
}
]}
/>
);
}
return D;
}
export default function render(props: WebComponentProps<EntityDict, 'wpAccount', true, {
@ -143,11 +154,10 @@ export default function render(props: WebComponentProps<EntityDict, 'wpAccount',
accounts.filter(ele => ele.$$createAt$$ as number > 1).map(
(ele, idx) => <div className={Styles.item} key={idx}>
<WpAccount
oakFullpath={oakFullpath}
systemId={systemId}
account={ele}
data={ele}
t={t}
onRemove={() => {
onRemove={ele['#oakLegalActions']?.includes('remove') ? () => {
Modal.confirm({
title: t('confirmDelete'),
content: t('areYouSure'),
@ -165,8 +175,8 @@ export default function render(props: WebComponentProps<EntityDict, 'wpAccount',
}
]),
})
}}
onUpdate={() => setUpsertId(ele.id!)}
} : undefined}
onUpdate={ele['#oakLegalActions']?.includes('update') ? () => setUpsertId(ele.id!) : undefined}
/>
</div>
)

View File

@ -48,6 +48,8 @@ export default OakComponent({
price: 0,
enabled: true,
taxLossRatio: 0.6,
refundCompensateRatio: 100,
allowWithdrawTransfer: false,
});
const { systemId } = this.props;
const { data: [ wechatPay ]} = await this.features.cache.refresh('wechatPay', {

View File

@ -9,7 +9,7 @@
"refundNotifyUrl": "endpoint",
"apiV3Key": "需要登录商户后台获取",
"refundGapDays": "超过这个天数后无法退款",
"allowWithdrawTransfer": "开启转账允许后,提现时用户即可选择从此商户号提现到微信零钱,帐户需要开启此项能力",
"allowWithdrawTransfer": "开启转账允许后,提现时用户即可选择从此商户号提现到微信零钱,微信商户必须要开启此项能力(当前尚未实现)",
"withdrawTransferLossRatio": "渠道转账提现时收取的手续费百分比0.6代表千分之六"
},
"wechatPayIsShared": "以上配置是全局的,请谨慎修改"

View File

@ -331,6 +331,19 @@ const i18ns: I18n[] = [
}
}
},
{
id: "c014581e9b3508d49c8dc75b2c7f5730",
namespace: "oak-pay-business-c-sysAccount-survey",
language: "zh-CN",
module: "oak-pay-business",
position: "src/components/sysAccount/survey",
data: {
"sysAccount": "系统账户统计",
"total": "总余额",
"count": "账户个数",
"qrCode": "二维码收款"
}
},
{
id: "79255be8c093dfef9765b3f367cab553",
namespace: "oak-pay-business-c-wechatPay-upsert",
@ -527,7 +540,7 @@ const i18ns: I18n[] = [
"refundNotifyUrl": "endpoint",
"apiV3Key": "需要登录商户后台获取",
"refundGapDays": "超过这个天数后无法退款",
"allowWithdrawTransfer": "开启转账允许后,提现时用户即可选择从此商户号提现到微信零钱,帐户需要开启此项能力",
"allowWithdrawTransfer": "开启转账允许后,提现时用户即可选择从此商户号提现到微信零钱,微信商户必须要开启此项能力(当前尚未实现)",
"withdrawTransferLossRatio": "渠道转账提现时收取的手续费百分比0.6代表千分之六"
},
"wechatPayIsShared": "以上配置是全局的,请谨慎修改"

View File

@ -2,15 +2,12 @@ import { CreateTriggerInTxn, RemoveTriggerInTxn, Trigger, UpdateTriggerInTxn } f
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { EntityDict } from '../oak-app-domain';
import { BRC } from '../types/RuntimeCxt';
import { DATA_SUBSCRIBER_KEYS } from '../config/constants';
import assert from 'assert';
import { getPayClazzAccountEntities } from '../utils/payClazz';
// 这里一定要注意两者的先后顺序如果有注入更多的payEntity的话
const entities = getPayClazzAccountEntities<EntityDict>();
import { accountEntities } from '../checkers/abstractChecker';
const triggers: Trigger<EntityDict, keyof EntityDict, BRC>[] = [
...entities.filter(ele => !!ele).map(
...accountEntities.filter(ele => !!ele).map(
(entity) => [
{
name: `${entity}的帐户生成时则生成对应的withDrawAccount`,

View File

@ -9,6 +9,7 @@ import Account from './Account';
import { WebConfig, WechatMpConfig, WechatPublicConfig } from 'oak-general-business/lib/entities/Application';
import WechatPay from './WechatPay';
import { StorageSchema } from 'oak-domain/lib/types/Storage';
import { registerAccountEntity } from '../../checkers/abstractChecker';
type PayClazzConstructor = (applicationId: string, entityId: string, context: BRC) => Promise<PayClazz>;
@ -141,13 +142,9 @@ export function registerPayClazzEntity<ED extends EntityDict & BaseEntityDict, T
assert(payAttr.entity.enumeration?.includes(entity as string));
assert(accountAttr.price && accountAttr.price.type === 'decimal');
}
assert(accountAttr.systemId && accountAttr.systemId.type === 'ref' && accountAttr.systemId.ref === 'system');
export function getPayClazzAccountEntities<ED extends EntityDict & BaseEntityDict>() {
MODULE_USED = true;
return Object.keys(PayClazzEntityDict).map(
ele => PayClazzEntityDict[ele].accountEntity as keyof ED
);
registerAccountEntity(def.accountEntity);
}