支付的部分逻辑代码

This commit is contained in:
Xu Chang 2024-04-22 21:16:42 +08:00
parent 855a0467c2
commit 9734af84e0
24 changed files with 805 additions and 90 deletions

8
src/checkers/account.ts Normal file
View File

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

View File

@ -0,0 +1,79 @@
import assert from 'assert';
import { Checker } from 'oak-domain/lib/types/Auth';
import { EntityDict } from '../oak-app-domain';
import { RuntimeCxt } from '../types/RuntimeCxt';
import { OakInputIllegalException } from 'oak-domain/lib/types';
const checkers: Checker<EntityDict, 'accountOper', RuntimeCxt>[] = [
{
entity: 'accountOper',
action: 'create',
type: 'row',
filter: {
account: {
ableState: 'enabled',
},
},
errMsg: 'account.update.accountDisabled',
},
{
entity: 'accountOper',
action: 'create',
type: 'data',
checker(data, context) {
assert(!(data instanceof Array));
const { type, totalPlus, availPlus } = data;
if (typeof totalPlus !== 'number') {
throw new OakInputIllegalException('accountOper', ['totalPlus'], 'accountOper中的totalPlus不是数字');
}
if (typeof availPlus !== 'number') {
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper中的availPlus不是数字');
}
switch (type) {
case 'consume': {
if (totalPlus >= 0 || availPlus > 0 || totalPlus > availPlus) {
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为consume时其totalPlus/availPlus必须为0或负数且totalPlus的绝对值要更大');
}
break;
}
case 'deposit': {
if (totalPlus < 0 || availPlus < 0 || totalPlus !== availPlus) {
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为deposit时其totalPlus/availPlus必须为正数且相等');
}
break;
}
case 'loan': {
if (totalPlus !== 0 || availPlus >= 0) {
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为loan时其totalPlus必须为0且availPlus必须为负数');
}
break;
}
case 'repay': {
if (totalPlus !== 0 || availPlus <= 0) {
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为repay时其totalPlus必须为0且availPlus必须为正数');
}
break;
}
case 'withdraw': {
if (totalPlus >= 0 || availPlus >= 0 || totalPlus !== availPlus) {
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为withdraw时其totalPlus和availPlus必须为不相等的负数');
}
break;
}
case 'withdrawBack': {
if (totalPlus <= 0 || availPlus <= 0 || totalPlus !== availPlus) {
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为withdraw时其totalPlus和availPlus必须为不相等的正数');
}
break;
}
default: {
assert(false);
break;
}
}
}
}
];
export default checkers;

View File

@ -1,9 +1,10 @@
import { EntityDict } from '@oak-app-domain';
import { Checker } from 'oak-domain/lib/types';
import { RuntimeCxt } from '../types/RuntimeCxt';
import aoCheckers from './accountOper';
const checkers = [
...aoCheckers,
] as Checker<EntityDict, keyof EntityDict, RuntimeCxt>[];
export default checkers;

100
src/checkers/pay.ts Normal file
View File

@ -0,0 +1,100 @@
import { Checker } from 'oak-domain/lib/types/Auth';
import { EntityDict } from '@project/oak-app-domain';
import { RuntimeCxt } from '../types/RuntimeCxt';
import { OakInputIllegalException } from 'oak-domain/lib/types';
import { generateNewId, generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import assert from 'assert';
import { PAY_CHANNEL_ACCOUNT_ID } from '@project/config/constants';
const checkers: Checker<EntityDict, 'pay', RuntimeCxt>[] = [
{
entity: 'pay',
type: 'logical',
action: 'create',
checker(operation) {
const { data } = operation as EntityDict['pay']['CreateSingle'];
data.refunded = 0;
data.paid = 0;
},
},
{
entity: 'pay',
type: 'data',
action: 'create',
checker(data) {
const { channelId, price, orderId, accountId } = data as EntityDict['pay']['CreateSingle']['data'];
if (price! <= 0) {
throw new OakInputIllegalException('pay', ['price'], '支付金额必须大于0');
}
if (!orderId) {
// 充值类订单
if (!accountId) {
throw new OakInputIllegalException('pay', ['accountId'], '充值类支付必须指定accountId');
}
else if (channelId === PAY_CHANNEL_ACCOUNT_ID) {
throw new OakInputIllegalException('pay', ['channelId'], '充值类支付不能使用帐户支付');
}
}
else {
if (channelId === PAY_CHANNEL_ACCOUNT_ID) {
if (!accountId) {
throw new OakInputIllegalException('pay', ['accountId'], '使用account支付必须指向accountId');
}
}
}
}
},
{
entity: 'pay',
type: 'logicalData',
action: 'create',
checker(operation, context) {
const { data } = operation as EntityDict['pay']['CreateSingle'];
const { orderId, price } = data;
if (orderId) {
// 所有已经支付和正在支付的pay之和不能超过订单总和
const order = context.select('order', {
data: {
id: 1,
price: 1,
pay$order: {
$entity: 'pay',
data: {
id: 1,
price: 1,
},
filter: {
iState: {
$in: ['paying', 'paid'],
}
}
}
},
filter: {
id: orderId,
}
}, {});
const checkPays = (order: Partial<EntityDict['order']['Schema']>) => {
const { price: orderPrice, pay$order: pays } = order;
let pricePaying = 0;
pays!.forEach(
(pay) => pricePaying += pay.price!
);
if (pricePaying + price! > orderPrice!) {
throw new OakInputIllegalException('pay', ['price'], 'pay.create.priceOverflow');
}
};
if (order instanceof Promise) {
return order.then(
([o]) => checkPays(o)
);
}
return checkPays(order[0]);
}
}
}
];
export default checkers;

View File

@ -1,33 +0,0 @@
import { EntityDict } from '@oak-app-domain';
import { Checker } from 'oak-domain/lib/types';
import { RuntimeCxt } from '../types/RuntimeCxt';
import { checkAttributesNotNull } from 'oak-domain/lib/utils/validator';
import { CreateOperationData as CreateStoreData } from '@oak-app-domain/Store/Schema';
export const checkers: Checker<EntityDict, 'store', RuntimeCxt>[] = [
{
type: 'data',
action: 'create',
entity: 'store',
checker: (data, context) => {
if (data instanceof Array) {
data.forEach((ele) => {
checkAttributesNotNull('store', data, [
'coordinate',
'name',
'addrDetail',
'areaId',
]);
});
} else {
checkAttributesNotNull('store', data, [
'coordinate',
'name',
'addrDetail',
'areaId',
]);
}
return 0;
},
},
];

View File

@ -1 +1,6 @@
export const DATA_SUBSCRIBER_KEYS = {};
export const DATA_SUBSCRIBER_KEYS = {};
export const PAY_CHANNEL_ACCOUNT_ID = 'payChannelAccountId';
export const PAY_CHANNEL_ACCOUNT_CODE = 'ACCOUNT';
export const PAY_CHANNEL_OFFLINE_ID = 'payChannelOfflineId';
export const PAY_CHANNEL_OFFLINE_CODE = 'OFFLINE';

View File

@ -1,8 +0,0 @@
import { CreateOperationData as ActionAuth } from '@oak-app-domain/ActionAuth/Schema';
const actionAuths: ActionAuth[] = [
];
export default actionAuths;

View File

@ -1,5 +0,0 @@
// 本文件为自动编译产生,请勿直接修改
import { CreateOperationData as I18n } from "../oak-app-domain/I18n/Schema";
const i18ns: I18n[] = [];
export default i18ns;

View File

@ -1,13 +1,7 @@
import { relations } from '@project/oak-app-domain/Relation';
import actionAuth from './actionAuth';
import relationAuth from './relationAuth';
import path from './path';
import i18n from './i18n';
import payChannel from './payChannel';
export default {
relation: relations,
actionAuth,
relationAuth,
path,
i18n,
payChannel,
};

View File

@ -1,5 +0,0 @@
import { CreateOperationData as Path } from '@oak-app-domain/Path/Schema';
const paths: Path[] = [];
export default paths;

16
src/data/payChannel.ts Normal file
View File

@ -0,0 +1,16 @@
import { PAY_CHANNEL_ACCOUNT_CODE, PAY_CHANNEL_ACCOUNT_ID, PAY_CHANNEL_OFFLINE_CODE, PAY_CHANNEL_OFFLINE_ID } from '@project/config/constants';
import { CreateOperationData as PayChannel } from '../oak-app-domain/PayChannel/Schema';
const data: PayChannel[] = [
{
id: PAY_CHANNEL_ACCOUNT_ID,
code: PAY_CHANNEL_ACCOUNT_CODE,
svg: 'todo url',
},
{
id: PAY_CHANNEL_OFFLINE_ID,
code: PAY_CHANNEL_OFFLINE_CODE,
svg: 'todo url',
}
];
export default data;

View File

@ -1,8 +0,0 @@
import { CreateOperationData as RelationAuth } from '@oak-app-domain/RelationAuth/Schema';
const relationAuths: RelationAuth[] = [
]
export default relationAuths;

68
src/entities/Account.ts Normal file
View File

@ -0,0 +1,68 @@
import {
String,
Text,
Price,
Boolean,
Datetime,
} from 'oak-domain/lib/types/DataType';
import { AbleAction, AbleState, makeAbleActionDef } from 'oak-domain/lib/actions/action';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { EntityDesc, ActionDef } from 'oak-domain/lib/types';
export interface Schema extends EntityShape {
total: Price;
avail: Price;
};
type IAction = 'deposit' | 'withdraw' | 'withdrawBack' | 'consume' | 'loan' | 'repay';
export type Action = AbleAction | IAction;
export const AbleActionDef: ActionDef<AbleAction, AbleState> = makeAbleActionDef('enabled');
export const entityDesc: EntityDesc<Schema, Action, '', {
ableState: AbleState;
}> = {
locales: {
zh_CN: {
name: '帐户',
attr: {
total: '总余额',
avail: '可用余额',
ableState: '状态',
},
action: {
enable: '启用',
disable: '禁用',
deposit: '充值',
withdraw: '提现',
withdrawBack: '提现退还',
consume: '消费',
loan: '抵押',
repay: '偿还',
},
v: {
ableState: {
enabled: '可用的',
disabled: '禁用的',
}
}
},
},
style: {
icon: {
enable: '',
disable: '',
deposit: '',
withdraw: '',
consume: '',
loan: '',
repay: '',
},
color: {
ableState: {
enabled: '#008000',
disabled: '#A9A9A9'
}
}
}
};

View File

@ -0,0 +1,63 @@
import {
String,
Text,
Price,
Boolean,
Datetime,
} from 'oak-domain/lib/types/DataType';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { EntityDesc, ActionDef } from 'oak-domain/lib/types';
import { Schema as Account } from './Account';
type Type = 'deposit' | 'withdraw' | 'consume' | 'loan' | 'repay' | 'withdrawBack';
export interface Schema extends EntityShape {
account: Account;
type: Type;
totalPlus: Price;
availPlus: Price;
entity: String<32>;
entityId: String<64>;
};
export const entityDesc: EntityDesc<Schema, '', '', {
type: Type;
}> = {
locales: {
zh_CN: {
name: '帐号操作',
attr: {
account: '帐号',
type: '类型',
totalPlus: '余额变化',
availPlus: '可用余额变化',
entity: '关联对象',
entityId: '关联对象Id',
},
v: {
type: {
deposit: '充值',
withdraw: '提现',
consume: '消费',
loan: '抵押',
repay: '偿还',
withdrawBack: '提现失败',
},
},
},
},
style: {
color: {
type: {
deposit: '#3498DB',
withdraw: '#F7DC6F',
consume: '#A569BD',
loan: '#CD6155',
repay: '#82E0AA',
}
}
},
configuration: {
actionType: 'appendOnly',
}
}

View File

@ -9,35 +9,40 @@ import { EntityShape } from 'oak-domain/lib/types/Entity';
import { EntityDesc, ActionDef } from 'oak-domain/lib/types';
import { Schema as Order } from './Order';
import { Schema as PayChannel } from './PayChannel';
import { Schema as Account } from './Account';
import { Schema as AccountOper } from './AccountOper';
/**
* payorderId为nullaccountId指向要充值的帐户channel不能是ACCOUNT
* payorderId指向订单accountId指向付款的帐户channel必须是ACCOUNT
*/
export interface Schema extends EntityShape {
price: Price;
paid: Price;
refunded: Price;
order: Order;
channel: PayChannel;
timeoutAt: Datetime;
forbidRefundAt?: Datetime;
account?: Account;
order?: Order;
opers: AccountOper[];
};
type IAction = 'startPaying' | 'payAll' | 'payPartially' | 'payNone' | 'timeout' | 'cancel' | 'startRefunding' | 'refundAll' | 'refundPartially' | 'refundNone';
type IState = 'paid' | 'unPaid' | 'timeout' | 'cancelled' | 'paying' | 'partiallyPaid' | 'paid' | 'refunding' | 'partiallyRefunded' | 'refunded';
type IAction = 'startPaying' | 'succeedPaying' | 'startClosing' | 'succeedClosing' | 'startRefunding' | 'refundAll' | 'refundPartially';
type IState = 'unpaid' | 'paying' | 'paid' | 'closing' | 'closed' | 'refunding' | 'partiallyRefunded' | 'refunded';
export const IActionDef: ActionDef<IAction, IState> = {
stm: {
startPaying: ['unPaid', 'paying'],
payAll: [['unPaid', 'paying', 'partiallyPaid'], 'paid'],
payPartially: [['unPaid', 'paying'], 'partiallyPaid'],
payNone: ['paying', 'unPaid'],
timeout: ['unPaid', 'timeout'],
cancel: ['unPaid', 'cancelled'],
startRefunding: [['paid', 'partiallyPaid'], 'refunding'],
refundAll: [['paid', 'refunding', 'partiallyPaid', 'partiallyRefunded'], 'refunded'],
refundPartially: [['paid', 'refunding', 'partiallyPaid', 'partiallyRefunded'], 'partiallyRefunded'],
refundNone: ['refunding', 'paid'],
startPaying: [['unpaid', 'paying'], 'paying'],
succeedPaying: [['unpaid', 'paying'], 'paid'],
startClosing: ['paying', 'closing'],
succeedClosing: [['closing', 'paying'], 'closed'],
startRefunding: [['paid', 'partiallyRefunded', 'refunding'], 'refunding'],
refundAll: [['paid', 'refunding', 'partiallyRefunded'], 'refunded'],
refundPartially: [['paid', 'refunding', 'partiallyRefunded'], 'partiallyRefunded'],
},
is: 'unPaid',
is: 'unpaid',
};
type Action = IAction;
@ -67,6 +72,8 @@ export const entityDesc: EntityDesc<Schema, Action, '', {
order: '所属订单',
timeoutAt: '过期时间',
forbidRefundAt: '停止退款时间',
account: '充值帐户',
opers: '被关联帐户操作',
},
action: {
startPaying: '开始支付',

72
src/entities/Withdraw.ts Normal file
View File

@ -0,0 +1,72 @@
import {
String,
Text,
Price,
Boolean,
Datetime,
} from 'oak-domain/lib/types/DataType';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { EntityDesc, ActionDef } from 'oak-domain/lib/types';
import { Schema as Account } from './Account';
import { Schema as WithdrawChannel } from './WithdrawChannel';
import { Schema as AccountOper } from './AccountOper';
export interface Schema extends EntityShape {
account: Account;
price: Price;
channel: WithdrawChannel;
opers: AccountOper[];
};
type IState = 'withdrawing' | 'successful' | 'failed';
type IAction = 'succeed' | 'fail';
export const IActionDef: ActionDef<IAction, IState> = {
stm: {
succeed: ['withdrawing', 'successful'],
fail: ['withdrawing', 'failed'],
},
is: 'withdrawing',
};
type Action = IAction;
export const entityDesc: EntityDesc<Schema, Action, '', {
iState: IState;
}> = {
locales: {
zh_CN: {
name: '提现',
attr: {
account: '帐户',
price: '金额',
channel: '途径',
iState: '状态',
opers: '被关联帐户操作',
},
v: {
iState: {
withdrawing: '提现中',
successful: '成功的',
failed: '失败的',
},
},
action: {
succeed: '成功',
fail: '失败',
},
},
},
style: {
icon: {
succeed: '',
fail: '',
},
color: {
iState: {
withdrawing: '#D2B4DE',
successful: '#2E86C1',
failed: '#D6DBDF',
}
}
}
}

View File

@ -0,0 +1,44 @@
import {
String,
Text,
Price,
Boolean,
Datetime,
} from 'oak-domain/lib/types/DataType';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { EntityDesc, ActionDef } from 'oak-domain/lib/types';
import { Schema as PayChannel } from './PayChannel';
export interface Schema extends EntityShape {
name: String<64>;
code: String<64>;
data: Object;
payChannel?: PayChannel;
};
export const entityDesc: EntityDesc<Schema> = {
locales: {
zh_CN: {
name: '提现途径',
attr: {
name: '姓名',
code: '帐号',
data: 'metadata',
payChannel: '关联支付途径',
}
},
},
indexes: [
{
name: 'code_uniqe',
attributes: [
{
name: 'code',
}
],
config: {
unique: true,
},
},
],
};

View File

@ -1,4 +0,0 @@
import initialize from './initialize.dev';
export default initialize;
console.log('不应该走到这里');

View File

@ -0,0 +1,46 @@
import { CreateTriggerInTxn, Trigger } from 'oak-domain/lib/types/Trigger';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { EntityDict } from '../oak-app-domain';
import BackendRuntimeContext from '@project/context/BackendRuntimeContext';
import assert from 'assert';
const triggers: Trigger<EntityDict, 'accountOper', BackendRuntimeContext>[] = [
{
name: '当生成accountOper时修改account中的值',
entity: 'accountOper',
action: 'create',
when: 'after',
fn: async ({ operation }, context, option) => {
const { data } = operation;
assert(!(data instanceof Array));
const { accountId, totalPlus, availPlus, type } = data;
const [account] = await context.select('account', {
data: {
id: 1,
total: 1,
avail: 1,
},
filter: {
id: accountId!,
}
}, { forUpdate: true });
const { total, avail } = account;
await context.operate('account', {
id: await generateNewIdAsync(),
action: type!,
data: {
total: total! + totalPlus!,
avail: avail! + availPlus!,
},
filter: {
id: accountId!,
}
}, option);
return 1;
},
} as CreateTriggerInTxn<EntityDict, 'accountOper', BackendRuntimeContext>
];
export default triggers;

View File

@ -1,10 +1,12 @@
import { EntityDict } from '@oak-app-domain';
import { Trigger } from 'oak-domain/lib/types';
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
import aoTriggers from './accountOper';
import payTriggers from './pay';
const triggers = [
...aoTriggers,
...payTriggers,
] as Trigger<EntityDict, keyof EntityDict, BackendRuntimeContext>[];
export default triggers;

9
src/triggers/order.ts Normal file
View File

@ -0,0 +1,9 @@
import { CreateTriggerInTxn, Trigger, UpdateTriggerInTxn } from 'oak-domain/lib/types/Trigger';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { EntityDict } from '@project/oak-app-domain';
import BackendRuntimeContext from '@project/context/BackendRuntimeContext';
import assert from 'assert';
import { OperateOption } from 'oak-domain/lib/types';
const triggers: Trigger<EntityDict, 'order', BackendRuntimeContext>[] = [
];

239
src/triggers/pay.ts Normal file
View File

@ -0,0 +1,239 @@
import { CreateTriggerInTxn, Trigger, UpdateTriggerInTxn } from 'oak-domain/lib/types/Trigger';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { EntityDict } from '@project/oak-app-domain';
import BackendRuntimeContext from '@project/context/BackendRuntimeContext';
import assert from 'assert';
import { OperateOption } from 'oak-domain/lib/types';
async function changeOrderStateByPay(
filter: NonNullable<EntityDict['order']['Update']['filter']>,
context: BackendRuntimeContext,
option: OperateOption) {
const orders = await context.select('order', {
data: {
id: 1,
iState: 1,
price: 1,
paid: 1,
refunded: 1,
pay$order: {
$entity: 'pay',
data: {
id: 1,
price: 1,
paid: 1,
refunded: 1,
iState: 1,
},
},
},
filter,
}, { dontCollect: true });
assert(orders.length === 1);
const [{ id: orderId, pay$order: pays, price: orderPrice,
paid: orderPaid, refunded: orderRefunded, iState: orderIState }] = orders;
let hasPaying = false, hasRefunding = false;
let payPaid = 0, payRefunded = 0;
for (const pay of pays!) {
const { price, iState, paid, refunded } = pay;
switch (iState!) {
case 'paying': {
hasPaying = true;
break;
}
case 'refunding': {
hasRefunding = true;
break;
}
default: {
break;
}
}
if (paid) {
payPaid += paid;
}
if (refunded) {
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,
},
id: orderId!,
}, option);
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,
},
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,
},
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,
},
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: {
},
id: orderId!,
}, option);
return 1;
}
}
}
const triggers: Trigger<EntityDict, 'pay', BackendRuntimeContext>[] = [
{
name: '当生成pay时自动开始支付流程',
entity: 'pay',
action: 'create',
when: 'after',
fn: async ({ operation }, context, option) => {
const { data } = operation;
assert(!(data instanceof Array));
const { accountId, price, orderId, id } = data;
if (orderId) {
if (accountId) {
// 使用帐户支付,直接成功并扣款
await context.operate('pay', {
id: await generateNewIdAsync(),
action: 'payAll',
data: {
accountOper$entity: [
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
totalPlus: -price!,
availPlus: -price!,
},
}
]
},
filter: {
id,
}
}, option);
return 1;
}
}
// 其余情况都是进入paying流程
await context.operate('pay', {
id: await generateNewIdAsync(),
action: 'startPaying',
data: {
},
filter: {
id,
}
} option);
return 1;
},
} as CreateTriggerInTxn<EntityDict, 'pay', BackendRuntimeContext>,
{
name: '当pay的状态发生变化时修改相应的order的状态或者account的状态',
entity: 'pay',
action: ['startPaying', 'succeedPaying', 'startClosing', 'succeedClosing', 'startRefunding',
'refundAll', 'refundPartially'],
when: 'after',
fn: async ({ operation }, context, option) => {
const { filter, action } = operation as EntityDict['pay']['Update'];
assert(typeof filter!.id === 'string');
const [pay] = await context.select('pay', {
data: {
id: 1,
orderId: 1,
accountId: 1,
price: 1,
},
filter,
}, { dontCollect: true });
const { orderId, accountId } = pay;
if (orderId) {
return await changeOrderStateByPay({ id: orderId }, context, option);
}
else {
assert(accountId);
// 如果是支付成功,则增加帐户余额,其它暂时不支持(提现暂时设计成不走退款)
if (action === 'succeedPaying') {
const payPrice = pay.price!;
await context.operate('accountOper', {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
totalPlus: payPrice,
availPlus: payPrice,
entity: 'pay',
entityId: pay.id!,
}
}, option);
return 1;
}
return 0;
}
},
} as UpdateTriggerInTxn<EntityDict, 'pay', BackendRuntimeContext>,
];
export default triggers;

View File

@ -1,8 +1,11 @@
import { Watcher } from 'oak-domain/lib/types';
import { EntityDict } from '@oak-app-domain';
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
import orderWatchers from './order';
const watchers = [] as Watcher<
const watchers = [
...orderWatchers,
] as Watcher<
EntityDict,
keyof EntityDict,
BackendRuntimeContext

22
src/watchers/order.ts Normal file
View File

@ -0,0 +1,22 @@
import { BBWatcher } from 'oak-domain/lib/types/Watcher';
import { EntityDict } from '@project/oak-app-domain';
const watchers: BBWatcher<EntityDict, 'order'>[] = [
{
name: '使过期的order结束',
entity: 'order',
filter: () => {
const now = Date.now();
return {
iState: 'unpaid',
timeoutAt: {
$lte: now,
},
};
},
action: 'timeout',
actionData: {},
}
];
export default watchers;