给account充值时扣掉相应的损耗

This commit is contained in:
Xu Chang 2024-05-19 17:17:51 +08:00
parent 8df9090cb2
commit 73abb9419e
52 changed files with 468 additions and 55 deletions

View File

@ -62,6 +62,12 @@ const checkers = [
}
break;
}
case 'loss': {
if (totalPlus >= 0 || availPlus > 0 || totalPlus !== availPlus) {
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为consume时其totalPlus/availPlus必须为0或负数且totalPlus的绝对值要更大');
}
break;
}
default: {
assert(false);
break;

View File

@ -6,15 +6,15 @@ export default function Render(props) {
return (<List itemLayout="horizontal" dataSource={accountOpers} renderItem={(item, index) => {
const { $$createAt$$, type, totalPlus, availPlus } = item;
const plus = ToYuan(totalPlus || availPlus);
const sign = plus > 0 ? '+' : '-';
const sign = plus > 0 ? '+' : '';
const d = dayjs($$createAt$$);
return (<List.Item key={index}>
<List.Item.Meta avatar={<Avatar>{t(`accountOper:v.type.${type}`).at(0)}</Avatar>} title={t(`accountOper:v.type.${type}`)} description={d.format('M月D日 HH时mm分')}/>
<div style={{
fontSize: 'x-large',
color: sign ? 'gold' : 'black'
color: sign ? 'gold' : 'rosybrown'
}}>
{sign}{ThousandCont(plus)}
{sign}{ThousandCont(plus, 2)}
</div>
</List.Item>);
}}/>);

View File

@ -20,6 +20,7 @@ export class BackendRuntimeContext extends DependentBackendRuntimeContext {
payConfig.forEach((config) => {
unset(config, 'publicKeyFilePath');
unset(config, 'privateKeyFilePath');
unset(config, 'apiV3Key');
});
}
}

View File

@ -2,7 +2,7 @@ import { String, Price } from 'oak-domain/lib/types/DataType';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { EntityDesc } from 'oak-domain/lib/types';
import { Schema as Account } from './Account';
type Type = 'deposit' | 'withdraw' | 'consume' | 'loan' | 'repay' | 'withdrawBack';
type Type = 'deposit' | 'withdraw' | 'consume' | 'loan' | 'repay' | 'withdrawBack' | 'loss';
export interface Schema extends EntityShape {
account: Account;
type: Type;

View File

@ -19,6 +19,7 @@ export const entityDesc = {
loan: '抵押',
repay: '偿还',
withdrawBack: '提现失败',
loss: '损耗'
},
},
},
@ -32,6 +33,7 @@ export const entityDesc = {
consume: '#A569BD',
loan: '#CD6155',
repay: '#82E0AA',
loss: '#2E4053'
}
}
},

View File

@ -7,7 +7,7 @@ import { Price, String } from "oak-domain/lib/types/DataType";
import * as Account from "../Account/Schema";
import * as Pay from "../Pay/Schema";
import * as Withdraw from "../Withdraw/Schema";
type Type = "deposit" | "withdraw" | "consume" | "loan" | "repay" | "withdrawBack";
type Type = "deposit" | "withdraw" | "consume" | "loan" | "repay" | "withdrawBack" | "loss";
export type OpSchema = EntityShape & {
accountId: ForeignKey<"account">;
type: Type;

View File

@ -9,7 +9,7 @@ export const desc = {
type: {
notNull: true,
type: "enum",
enumeration: ["deposit", "withdraw", "consume", "loan", "repay", "withdrawBack"]
enumeration: ["deposit", "withdraw", "consume", "loan", "repay", "withdrawBack", "loss"]
},
totalPlus: {
notNull: true,

View File

@ -7,6 +7,7 @@ export const style = {
consume: '#A569BD',
loan: '#CD6155',
repay: '#82E0AA',
loss: '#2E4053'
}
}
};

View File

@ -1 +1 @@
{ "name": "帐号操作", "attr": { "account": "帐号", "type": "类型", "totalPlus": "余额变化", "availPlus": "可用余额变化", "entity": "关联对象", "entityId": "关联对象Id" }, "v": { "type": { "deposit": "充值", "withdraw": "提现", "consume": "消费", "loan": "抵押", "repay": "偿还", "withdrawBack": "提现失败" } } }
{ "name": "帐号操作", "attr": { "account": "帐号", "type": "类型", "totalPlus": "余额变化", "availPlus": "可用余额变化", "entity": "关联对象", "entityId": "关联对象Id" }, "v": { "type": { "deposit": "充值", "withdraw": "提现", "consume": "消费", "loan": "抵押", "repay": "偿还", "withdrawBack": "提现失败", "loss": "损耗" } } }

View File

@ -1 +1 @@
{ "name": "订单", "attr": { "price": "订单金额", "paid": "已支付金额", "refunded": "已退款金额", "iState": "支付状态", "channel": "支付渠道", "order": "所属订单", "timeoutAt": "过期时间", "forbidRefundAt": "停止退款时间", "account": "充值帐户", "meta": "支付metadata", "externalId": "外部订单Id", "opers": "被关联帐户操作", "application": "关联应用", "creator": "创建者", "phantom1": "索引项一", "phantom2": "索引项二", "phantom3": "索引项三", "phantom4": "索引项四" }, "action": { "startPaying": "开始支付", "succeedPaying": "支付成功", "close": "关闭", "startRefunding": "开始退款", "refundAll": "完全退款", "refundPartially": "部分退款" }, "v": { "iState": { "unpaid": "待付款", "paying": "支付中", "paid": "已付款", "closed": "已关闭", "refunding": "退款中", "refunded": "已退款", "partiallyRefunded": "已部分退款" } } }
{ "name": "订单", "attr": { "price": "应支付金额", "paid": "已支付金额", "refunded": "已退款金额", "iState": "支付状态", "channel": "支付渠道", "order": "所属订单", "timeoutAt": "过期时间", "forbidRefundAt": "停止退款时间", "account": "充值帐户", "meta": "支付metadata", "externalId": "外部订单Id", "opers": "被关联帐户操作", "application": "关联应用", "creator": "创建者", "phantom1": "索引项一", "phantom2": "索引项二", "phantom3": "索引项三", "phantom4": "索引项四" }, "action": { "startPaying": "开始支付", "succeedPaying": "支付成功", "close": "关闭", "startRefunding": "开始退款", "refundAll": "完全退款", "refundPartially": "部分退款" }, "v": { "iState": { "unpaid": "待付款", "paying": "支付中", "paid": "已付款", "closed": "已关闭", "refunding": "退款中", "refunded": "已退款", "partiallyRefunded": "已部分退款" } } }

View File

@ -22,6 +22,7 @@ export type OpSchema = EntityShape & {
env: Environment;
refreshedAt: Datetime;
value: String<64>;
oldValue?: String<64> | null;
ableState?: AbleState | null;
};
export type OpAttr = keyof OpSchema;
@ -35,6 +36,7 @@ export type Schema = EntityShape & {
env: Environment;
refreshedAt: Datetime;
value: String<64>;
oldValue?: String<64> | null;
ableState?: AbleState | null;
application?: Application.Schema | null;
user?: User.Schema | null;
@ -63,6 +65,7 @@ type AttrFilter = {
env: JsonFilter<Environment>;
refreshedAt: Q_DateValue;
value: Q_StringValue;
oldValue: Q_StringValue;
ableState: Q_EnumValue<AbleState>;
email: Email.Filter;
mobile: Mobile.Filter;
@ -89,6 +92,7 @@ export type Projection = {
env?: number | JsonProjection<Environment>;
refreshedAt?: number;
value?: number;
oldValue?: number;
ableState?: number;
email?: Email.Projection;
mobile?: Mobile.Projection;
@ -149,6 +153,8 @@ export type SortAttr = {
refreshedAt: number;
} | {
value: number;
} | {
oldValue: number;
} | {
ableState: number;
} | {

View File

@ -46,6 +46,12 @@ export const desc = {
length: 64
}
},
oldValue: {
type: "varchar",
params: {
length: 64
}
},
ableState: {
type: "enum",
enumeration: ["enabled", "disabled"]

View File

@ -6,7 +6,7 @@ export const style = {
color: {
ableState: {
enabled: '#008000',
disabled: '#A9A9A9'
disabled: '#A9A9A9',
},
},
};

View File

@ -1 +1 @@
{ "name": "令牌", "attr": { "application": "应用", "entity": "关联对象", "entityId": "关联对象id", "user": "用户", "player": "扮演者", "env": "环境", "ableState": "状态", "disablesAt": "禁用时间", "refreshedAt": "刷新时间", "value": "令牌值" }, "action": { "enable": "激活", "disable": "禁用" }, "v": { "ableState": { "enabled": "使用中", "disabled": "已禁用" } } }
{ "name": "令牌", "attr": { "application": "应用", "entity": "关联对象", "entityId": "关联对象id", "user": "用户", "player": "扮演者", "env": "环境", "ableState": "状态", "disablesAt": "禁用时间", "refreshedAt": "刷新时间", "value": "令牌值", "oldValue": "老令牌" }, "action": { "enable": "激活", "disable": "禁用" }, "v": { "ableState": { "enabled": "使用中", "disabled": "已禁用" } } }

5
es/triggers/account.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { Trigger } from 'oak-domain/lib/types/Trigger';
import { EntityDict } from '../oak-app-domain';
import { BRC } from '../types/RuntimeCxt';
declare const triggers: Trigger<EntityDict, 'account', BRC>[];
export default triggers;

20
es/triggers/account.js Normal file
View File

@ -0,0 +1,20 @@
import { DATA_SUBSCRIBER_KEYS } from '../config/constants';
import assert from 'assert';
const triggers = [
{
name: '当account帐户的值发生变化时向订阅者推送',
entity: 'account',
action: ['deposit', 'withdraw', 'withdrawBack', 'consume', 'loan', 'repay'],
check(operation) {
return operation.data.hasOwnProperty('total') || operation.data.hasOwnProperty('avail');
},
when: 'after',
fn: async ({ operation }, context, option) => {
const { id, filter } = operation;
assert(typeof filter?.id === 'string');
context.saveOperationToEvent(id, `${DATA_SUBSCRIBER_KEYS.accountNumberChanged}-${filter.id}`);
return 1;
},
},
];
export default triggers;

View File

@ -1,6 +1,7 @@
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import assert from 'assert';
import { DATA_SUBSCRIBER_KEYS } from '../config/constants';
import { getDepositRatio } from '../utils/pay';
const triggers = [
{
name: '当生成accountOper时修改account中的值并向订阅者推送',
@ -36,6 +37,61 @@ const triggers = [
context.saveOperationToEvent(id, `${DATA_SUBSCRIBER_KEYS.accountNumberChanged}-${accountId}`);
return 1;
},
}
},
{
name: '当account depoist时根据系统配置扣除掉损耗',
entity: 'accountOper',
action: 'create',
when: 'after',
check(operation) {
const { data } = operation;
assert(!(data instanceof Array));
return data.type === 'deposit';
},
fn: async ({ operation }, context, option) => {
const { data } = operation;
assert(!(data instanceof Array));
const { entity, entityId } = data;
assert(entity === 'pay' && entityId);
const [pay] = await context.select('pay', {
data: {
id: 1,
price: 1,
channel: 1,
application: {
id: 1,
payConfig: 1,
system: {
id: 1,
payConfig: 1,
},
}
},
filter: {
id: entityId,
},
}, {});
const { price, application, channel } = pay;
const ratio = getDepositRatio(channel, application);
if (ratio > 0) {
const loss = Math.ceil(price * ratio / 100);
await context.operate('accountOper', {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
type: 'loss',
totalPlus: -loss,
availPlus: -loss,
entity,
entityId,
accountId: data.accountId,
}
}, {});
return 1;
}
return 0;
}
},
];
export default triggers;

View File

@ -1,7 +1,7 @@
import aoTriggers from './accountOper';
import payTriggers from './pay';
import userSystemTriggers from './userSystem';
import accountTriggers from './acount';
import accountTriggers from './account';
const triggers = [
...aoTriggers,
...payTriggers,

View File

@ -2,29 +2,31 @@ export type WechatPayChannel = 'WECHAT_JS' | 'WECHAT_MP' | 'WECHAT_NATIVE' | 'WE
export interface ConfigBase {
channel: string;
}
export type WechatPayConfig = {
export interface ThirdPartyConfig extends ConfigBase {
payNotifyUrl: string;
refundNotifyUrl: string;
lossRatio: number;
}
export interface WechatPayConfig extends ThirdPartyConfig {
channel: WechatPayChannel;
mchId: string;
publicKeyFilePath: string;
privateKeyFilePath: string;
apiV3Key: string;
payNotifyUrl: string;
refundNotifyUrl: string;
lossRatio: number;
};
}
export type AccountPayChannel = 'ACCOUNT';
export type AccountPayConfig = {
export interface AccountPayConfig extends ConfigBase {
channel: AccountPayChannel;
depositLoss: boolean;
depositLossRatio?: number;
};
}
export type OfflinePayChannel = 'OFFLINE';
export type OfflinePayConfig = {
export interface OfflinePayConfig extends ConfigBase {
tips: string;
channel: OfflinePayChannel;
options?: string[];
allowUser?: boolean;
};
}
export type Channel = WechatPayChannel | AccountPayChannel | OfflinePayChannel;
export type PayConfig = Array<WechatPayConfig | AccountPayConfig | OfflinePayConfig>;
export declare const PAY_CHANNEL_ACCOUNT_NAME = "ACCOUNT";
@ -34,3 +36,4 @@ export declare const PAY_CHANNEL_WECHAT_MP_NAME = "WECHAT_MP";
export declare const PAY_CHANNEL_WECHAT_H5_NAME = "WECHAT_H5";
export declare const PAY_CHANNEL_WECHAT_APP_NAME = "WECHAT_APP";
export declare const PAY_CHANNEL_WECHAT_NATIVE_NAME = "WECHAT_NATIVE";
export declare const PAY_WECHAT_CHANNELS: string[];

View File

@ -1,3 +1,5 @@
;
;
export const PAY_CHANNEL_ACCOUNT_NAME = 'ACCOUNT';
export const PAY_CHANNEL_OFFLINE_NAME = 'OFFLINE';
export const PAY_CHANNEL_WECHAT_JS_NAME = 'WECHAT_JS';
@ -5,3 +7,10 @@ export const PAY_CHANNEL_WECHAT_MP_NAME = 'WECHAT_MP';
export const PAY_CHANNEL_WECHAT_H5_NAME = 'WECHAT_H5';
export const PAY_CHANNEL_WECHAT_APP_NAME = 'WECHAT_APP';
export const PAY_CHANNEL_WECHAT_NATIVE_NAME = 'WECHAT_NATIVE';
export const PAY_WECHAT_CHANNELS = [
PAY_CHANNEL_WECHAT_JS_NAME,
PAY_CHANNEL_WECHAT_MP_NAME,
PAY_CHANNEL_WECHAT_H5_NAME,
PAY_CHANNEL_WECHAT_APP_NAME,
PAY_CHANNEL_WECHAT_NATIVE_NAME
];

7
es/utils/pay.d.ts vendored
View File

@ -5,3 +5,10 @@ import BackendRuntimeContext from '../context/BackendRuntimeContext';
import { IncomingHttpHeaders } from 'http';
export declare const fullPayProjection: EntityDict['pay']['Selection']['data'];
export declare function payNotify<ED extends EntityDict & BaseEntityDict>(context: BackendRuntimeContext<ED>, body: any, payId: string, headers: IncomingHttpHeaders): Promise<void>;
/**
*
* @param context
* @param channel
* @param application
*/
export declare function getDepositRatio<ED extends EntityDict & BaseEntityDict>(channel: string, application: ED['application']['Schema']): number;

View File

@ -1,6 +1,7 @@
import { getPayClazz } from './payClazz';
import assert from 'assert';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_WECHAT_CHANNELS } from '../types/PayConfig';
export const fullPayProjection = {
id: 1,
applicationId: 1,
@ -75,3 +76,25 @@ export async function payNotify(context, body, payId, headers) {
}
return;
}
/**
* 计算充值的损耗比例
* @param context
* @param channel
* @param application
*/
export function getDepositRatio(channel, application) {
const { payConfig, system } = application;
const { payConfig: systemPayConfig } = system;
const accountConfig = systemPayConfig.find(ele => ele.channel === PAY_CHANNEL_ACCOUNT_NAME);
const { depositLoss, depositLossRatio } = accountConfig;
if (!depositLoss) {
return 0;
}
if (depositLossRatio) {
return depositLossRatio;
}
const config = systemPayConfig?.find(ele => ele.channel === channel) || payConfig?.find(ele => ele.channel === channel);
assert(config && PAY_WECHAT_CHANNELS.includes(channel));
const { lossRatio } = config;
return lossRatio || 0;
}

View File

@ -65,6 +65,12 @@ const checkers = [
}
break;
}
case 'loss': {
if (totalPlus >= 0 || availPlus > 0 || totalPlus !== availPlus) {
throw new types_1.OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为consume时其totalPlus/availPlus必须为0或负数且totalPlus的绝对值要更大');
}
break;
}
default: {
(0, assert_1.default)(false);
break;

View File

@ -23,6 +23,7 @@ class BackendRuntimeContext extends DependentContext_1.BackendRuntimeContext {
payConfig.forEach((config) => {
(0, lodash_1.unset)(config, 'publicKeyFilePath');
(0, lodash_1.unset)(config, 'privateKeyFilePath');
(0, lodash_1.unset)(config, 'apiV3Key');
});
}
}

View File

@ -2,7 +2,7 @@ import { String, Price } from 'oak-domain/lib/types/DataType';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { EntityDesc } from 'oak-domain/lib/types';
import { Schema as Account } from './Account';
type Type = 'deposit' | 'withdraw' | 'consume' | 'loan' | 'repay' | 'withdrawBack';
type Type = 'deposit' | 'withdraw' | 'consume' | 'loan' | 'repay' | 'withdrawBack' | 'loss';
export interface Schema extends EntityShape {
account: Account;
type: Type;

View File

@ -22,6 +22,7 @@ exports.entityDesc = {
loan: '抵押',
repay: '偿还',
withdrawBack: '提现失败',
loss: '损耗'
},
},
},
@ -35,6 +36,7 @@ exports.entityDesc = {
consume: '#A569BD',
loan: '#CD6155',
repay: '#82E0AA',
loss: '#2E4053'
}
}
},

View File

@ -7,7 +7,7 @@ import { Price, String } from "oak-domain/lib/types/DataType";
import * as Account from "../Account/Schema";
import * as Pay from "../Pay/Schema";
import * as Withdraw from "../Withdraw/Schema";
type Type = "deposit" | "withdraw" | "consume" | "loan" | "repay" | "withdrawBack";
type Type = "deposit" | "withdraw" | "consume" | "loan" | "repay" | "withdrawBack" | "loss";
export type OpSchema = EntityShape & {
accountId: ForeignKey<"account">;
type: Type;

View File

@ -12,7 +12,7 @@ exports.desc = {
type: {
notNull: true,
type: "enum",
enumeration: ["deposit", "withdraw", "consume", "loan", "repay", "withdrawBack"]
enumeration: ["deposit", "withdraw", "consume", "loan", "repay", "withdrawBack", "loss"]
},
totalPlus: {
notNull: true,

View File

@ -10,6 +10,7 @@ exports.style = {
consume: '#A569BD',
loan: '#CD6155',
repay: '#82E0AA',
loss: '#2E4053'
}
}
};

View File

@ -1 +1 @@
{ "name": "帐号操作", "attr": { "account": "帐号", "type": "类型", "totalPlus": "余额变化", "availPlus": "可用余额变化", "entity": "关联对象", "entityId": "关联对象Id" }, "v": { "type": { "deposit": "充值", "withdraw": "提现", "consume": "消费", "loan": "抵押", "repay": "偿还", "withdrawBack": "提现失败" } } }
{ "name": "帐号操作", "attr": { "account": "帐号", "type": "类型", "totalPlus": "余额变化", "availPlus": "可用余额变化", "entity": "关联对象", "entityId": "关联对象Id" }, "v": { "type": { "deposit": "充值", "withdraw": "提现", "consume": "消费", "loan": "抵押", "repay": "偿还", "withdrawBack": "提现失败", "loss": "损耗" } } }

View File

@ -1 +1 @@
{ "name": "订单", "attr": { "price": "订单金额", "paid": "已支付金额", "refunded": "已退款金额", "iState": "支付状态", "channel": "支付渠道", "order": "所属订单", "timeoutAt": "过期时间", "forbidRefundAt": "停止退款时间", "account": "充值帐户", "meta": "支付metadata", "externalId": "外部订单Id", "opers": "被关联帐户操作", "application": "关联应用", "creator": "创建者", "phantom1": "索引项一", "phantom2": "索引项二", "phantom3": "索引项三", "phantom4": "索引项四" }, "action": { "startPaying": "开始支付", "succeedPaying": "支付成功", "close": "关闭", "startRefunding": "开始退款", "refundAll": "完全退款", "refundPartially": "部分退款" }, "v": { "iState": { "unpaid": "待付款", "paying": "支付中", "paid": "已付款", "closed": "已关闭", "refunding": "退款中", "refunded": "已退款", "partiallyRefunded": "已部分退款" } } }
{ "name": "订单", "attr": { "price": "应支付金额", "paid": "已支付金额", "refunded": "已退款金额", "iState": "支付状态", "channel": "支付渠道", "order": "所属订单", "timeoutAt": "过期时间", "forbidRefundAt": "停止退款时间", "account": "充值帐户", "meta": "支付metadata", "externalId": "外部订单Id", "opers": "被关联帐户操作", "application": "关联应用", "creator": "创建者", "phantom1": "索引项一", "phantom2": "索引项二", "phantom3": "索引项三", "phantom4": "索引项四" }, "action": { "startPaying": "开始支付", "succeedPaying": "支付成功", "close": "关闭", "startRefunding": "开始退款", "refundAll": "完全退款", "refundPartially": "部分退款" }, "v": { "iState": { "unpaid": "待付款", "paying": "支付中", "paid": "已付款", "closed": "已关闭", "refunding": "退款中", "refunded": "已退款", "partiallyRefunded": "已部分退款" } } }

View File

@ -22,6 +22,7 @@ export type OpSchema = EntityShape & {
env: Environment;
refreshedAt: Datetime;
value: String<64>;
oldValue?: String<64> | null;
ableState?: AbleState | null;
};
export type OpAttr = keyof OpSchema;
@ -35,6 +36,7 @@ export type Schema = EntityShape & {
env: Environment;
refreshedAt: Datetime;
value: String<64>;
oldValue?: String<64> | null;
ableState?: AbleState | null;
application?: Application.Schema | null;
user?: User.Schema | null;
@ -63,6 +65,7 @@ type AttrFilter = {
env: JsonFilter<Environment>;
refreshedAt: Q_DateValue;
value: Q_StringValue;
oldValue: Q_StringValue;
ableState: Q_EnumValue<AbleState>;
email: Email.Filter;
mobile: Mobile.Filter;
@ -89,6 +92,7 @@ export type Projection = {
env?: number | JsonProjection<Environment>;
refreshedAt?: number;
value?: number;
oldValue?: number;
ableState?: number;
email?: Email.Projection;
mobile?: Mobile.Projection;
@ -149,6 +153,8 @@ export type SortAttr = {
refreshedAt: number;
} | {
value: number;
} | {
oldValue: number;
} | {
ableState: number;
} | {

View File

@ -49,6 +49,12 @@ exports.desc = {
length: 64
}
},
oldValue: {
type: "varchar",
params: {
length: 64
}
},
ableState: {
type: "enum",
enumeration: ["enabled", "disabled"]

View File

@ -9,7 +9,7 @@ exports.style = {
color: {
ableState: {
enabled: '#008000',
disabled: '#A9A9A9'
disabled: '#A9A9A9',
},
},
};

View File

@ -1 +1 @@
{ "name": "令牌", "attr": { "application": "应用", "entity": "关联对象", "entityId": "关联对象id", "user": "用户", "player": "扮演者", "env": "环境", "ableState": "状态", "disablesAt": "禁用时间", "refreshedAt": "刷新时间", "value": "令牌值" }, "action": { "enable": "激活", "disable": "禁用" }, "v": { "ableState": { "enabled": "使用中", "disabled": "已禁用" } } }
{ "name": "令牌", "attr": { "application": "应用", "entity": "关联对象", "entityId": "关联对象id", "user": "用户", "player": "扮演者", "env": "环境", "ableState": "状态", "disablesAt": "禁用时间", "refreshedAt": "刷新时间", "value": "令牌值", "oldValue": "老令牌" }, "action": { "enable": "激活", "disable": "禁用" }, "v": { "ableState": { "enabled": "使用中", "disabled": "已禁用" } } }

5
lib/triggers/account.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { Trigger } from 'oak-domain/lib/types/Trigger';
import { EntityDict } from '../oak-app-domain';
import { BRC } from '../types/RuntimeCxt';
declare const triggers: Trigger<EntityDict, 'account', BRC>[];
export default triggers;

23
lib/triggers/account.js Normal file
View File

@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const constants_1 = require("../config/constants");
const assert_1 = tslib_1.__importDefault(require("assert"));
const triggers = [
{
name: '当account帐户的值发生变化时向订阅者推送',
entity: 'account',
action: ['deposit', 'withdraw', 'withdrawBack', 'consume', 'loan', 'repay'],
check(operation) {
return operation.data.hasOwnProperty('total') || operation.data.hasOwnProperty('avail');
},
when: 'after',
fn: async ({ operation }, context, option) => {
const { id, filter } = operation;
(0, assert_1.default)(typeof filter?.id === 'string');
context.saveOperationToEvent(id, `${constants_1.DATA_SUBSCRIBER_KEYS.accountNumberChanged}-${filter.id}`);
return 1;
},
},
];
exports.default = triggers;

View File

@ -4,6 +4,7 @@ const tslib_1 = require("tslib");
const uuid_1 = require("oak-domain/lib/utils/uuid");
const assert_1 = tslib_1.__importDefault(require("assert"));
const constants_1 = require("../config/constants");
const pay_1 = require("../utils/pay");
const triggers = [
{
name: '当生成accountOper时修改account中的值并向订阅者推送',
@ -39,6 +40,61 @@ const triggers = [
context.saveOperationToEvent(id, `${constants_1.DATA_SUBSCRIBER_KEYS.accountNumberChanged}-${accountId}`);
return 1;
},
}
},
{
name: '当account depoist时根据系统配置扣除掉损耗',
entity: 'accountOper',
action: 'create',
when: 'after',
check(operation) {
const { data } = operation;
(0, assert_1.default)(!(data instanceof Array));
return data.type === 'deposit';
},
fn: async ({ operation }, context, option) => {
const { data } = operation;
(0, assert_1.default)(!(data instanceof Array));
const { entity, entityId } = data;
(0, assert_1.default)(entity === 'pay' && entityId);
const [pay] = await context.select('pay', {
data: {
id: 1,
price: 1,
channel: 1,
application: {
id: 1,
payConfig: 1,
system: {
id: 1,
payConfig: 1,
},
}
},
filter: {
id: entityId,
},
}, {});
const { price, application, channel } = pay;
const ratio = (0, pay_1.getDepositRatio)(channel, application);
if (ratio > 0) {
const loss = Math.ceil(price * ratio / 100);
await context.operate('accountOper', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
id: await (0, uuid_1.generateNewIdAsync)(),
type: 'loss',
totalPlus: -loss,
availPlus: -loss,
entity,
entityId,
accountId: data.accountId,
}
}, {});
return 1;
}
return 0;
}
},
];
exports.default = triggers;

View File

@ -4,11 +4,11 @@ const tslib_1 = require("tslib");
const accountOper_1 = tslib_1.__importDefault(require("./accountOper"));
const pay_1 = tslib_1.__importDefault(require("./pay"));
const userSystem_1 = tslib_1.__importDefault(require("./userSystem"));
const acount_1 = tslib_1.__importDefault(require("./acount"));
const account_1 = tslib_1.__importDefault(require("./account"));
const triggers = [
...accountOper_1.default,
...pay_1.default,
...userSystem_1.default,
...acount_1.default,
...account_1.default,
];
exports.default = triggers;

View File

@ -2,29 +2,31 @@ export type WechatPayChannel = 'WECHAT_JS' | 'WECHAT_MP' | 'WECHAT_NATIVE' | 'WE
export interface ConfigBase {
channel: string;
}
export type WechatPayConfig = {
export interface ThirdPartyConfig extends ConfigBase {
payNotifyUrl: string;
refundNotifyUrl: string;
lossRatio: number;
}
export interface WechatPayConfig extends ThirdPartyConfig {
channel: WechatPayChannel;
mchId: string;
publicKeyFilePath: string;
privateKeyFilePath: string;
apiV3Key: string;
payNotifyUrl: string;
refundNotifyUrl: string;
lossRatio: number;
};
}
export type AccountPayChannel = 'ACCOUNT';
export type AccountPayConfig = {
export interface AccountPayConfig extends ConfigBase {
channel: AccountPayChannel;
depositLoss: boolean;
depositLossRatio?: number;
};
}
export type OfflinePayChannel = 'OFFLINE';
export type OfflinePayConfig = {
export interface OfflinePayConfig extends ConfigBase {
tips: string;
channel: OfflinePayChannel;
options?: string[];
allowUser?: boolean;
};
}
export type Channel = WechatPayChannel | AccountPayChannel | OfflinePayChannel;
export type PayConfig = Array<WechatPayConfig | AccountPayConfig | OfflinePayConfig>;
export declare const PAY_CHANNEL_ACCOUNT_NAME = "ACCOUNT";
@ -34,3 +36,4 @@ export declare const PAY_CHANNEL_WECHAT_MP_NAME = "WECHAT_MP";
export declare const PAY_CHANNEL_WECHAT_H5_NAME = "WECHAT_H5";
export declare const PAY_CHANNEL_WECHAT_APP_NAME = "WECHAT_APP";
export declare const PAY_CHANNEL_WECHAT_NATIVE_NAME = "WECHAT_NATIVE";
export declare const PAY_WECHAT_CHANNELS: string[];

View File

@ -1,6 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PAY_CHANNEL_WECHAT_NATIVE_NAME = exports.PAY_CHANNEL_WECHAT_APP_NAME = exports.PAY_CHANNEL_WECHAT_H5_NAME = exports.PAY_CHANNEL_WECHAT_MP_NAME = exports.PAY_CHANNEL_WECHAT_JS_NAME = exports.PAY_CHANNEL_OFFLINE_NAME = exports.PAY_CHANNEL_ACCOUNT_NAME = void 0;
exports.PAY_WECHAT_CHANNELS = exports.PAY_CHANNEL_WECHAT_NATIVE_NAME = exports.PAY_CHANNEL_WECHAT_APP_NAME = exports.PAY_CHANNEL_WECHAT_H5_NAME = exports.PAY_CHANNEL_WECHAT_MP_NAME = exports.PAY_CHANNEL_WECHAT_JS_NAME = exports.PAY_CHANNEL_OFFLINE_NAME = exports.PAY_CHANNEL_ACCOUNT_NAME = void 0;
;
;
exports.PAY_CHANNEL_ACCOUNT_NAME = 'ACCOUNT';
exports.PAY_CHANNEL_OFFLINE_NAME = 'OFFLINE';
exports.PAY_CHANNEL_WECHAT_JS_NAME = 'WECHAT_JS';
@ -8,3 +10,10 @@ exports.PAY_CHANNEL_WECHAT_MP_NAME = 'WECHAT_MP';
exports.PAY_CHANNEL_WECHAT_H5_NAME = 'WECHAT_H5';
exports.PAY_CHANNEL_WECHAT_APP_NAME = 'WECHAT_APP';
exports.PAY_CHANNEL_WECHAT_NATIVE_NAME = 'WECHAT_NATIVE';
exports.PAY_WECHAT_CHANNELS = [
exports.PAY_CHANNEL_WECHAT_JS_NAME,
exports.PAY_CHANNEL_WECHAT_MP_NAME,
exports.PAY_CHANNEL_WECHAT_H5_NAME,
exports.PAY_CHANNEL_WECHAT_APP_NAME,
exports.PAY_CHANNEL_WECHAT_NATIVE_NAME
];

7
lib/utils/pay.d.ts vendored
View File

@ -5,3 +5,10 @@ import BackendRuntimeContext from '../context/BackendRuntimeContext';
import { IncomingHttpHeaders } from 'http';
export declare const fullPayProjection: EntityDict['pay']['Selection']['data'];
export declare function payNotify<ED extends EntityDict & BaseEntityDict>(context: BackendRuntimeContext<ED>, body: any, payId: string, headers: IncomingHttpHeaders): Promise<void>;
/**
*
* @param context
* @param channel
* @param application
*/
export declare function getDepositRatio<ED extends EntityDict & BaseEntityDict>(channel: string, application: ED['application']['Schema']): number;

View File

@ -1,10 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.payNotify = exports.fullPayProjection = void 0;
exports.getDepositRatio = exports.payNotify = exports.fullPayProjection = void 0;
const tslib_1 = require("tslib");
const payClazz_1 = require("./payClazz");
const assert_1 = tslib_1.__importDefault(require("assert"));
const uuid_1 = require("oak-domain/lib/utils/uuid");
const PayConfig_1 = require("../types/PayConfig");
exports.fullPayProjection = {
id: 1,
applicationId: 1,
@ -80,3 +81,26 @@ async function payNotify(context, body, payId, headers) {
return;
}
exports.payNotify = payNotify;
/**
* 计算充值的损耗比例
* @param context
* @param channel
* @param application
*/
function getDepositRatio(channel, application) {
const { payConfig, system } = application;
const { payConfig: systemPayConfig } = system;
const accountConfig = systemPayConfig.find(ele => ele.channel === PayConfig_1.PAY_CHANNEL_ACCOUNT_NAME);
const { depositLoss, depositLossRatio } = accountConfig;
if (!depositLoss) {
return 0;
}
if (depositLossRatio) {
return depositLossRatio;
}
const config = systemPayConfig?.find(ele => ele.channel === channel) || payConfig?.find(ele => ele.channel === channel);
(0, assert_1.default)(config && PayConfig_1.PAY_WECHAT_CHANNELS.includes(channel));
const { lossRatio } = config;
return lossRatio || 0;
}
exports.getDepositRatio = getDepositRatio;

View File

@ -66,6 +66,12 @@ const checkers: Checker<EntityDict, 'accountOper', RuntimeCxt>[] = [
}
break;
}
case 'loss' : {
if (totalPlus >= 0 || availPlus > 0 || totalPlus !== availPlus) {
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为consume时其totalPlus/availPlus必须为0或负数且totalPlus的绝对值要更大');
}
break;
}
default: {
assert(false);
break;

View File

@ -15,7 +15,7 @@ export default function Render(props: {
renderItem={(item, index) => {
const { $$createAt$$, type, totalPlus, availPlus } = item;
const plus = ToYuan(totalPlus || availPlus);
const sign = plus > 0 ? '+' : '-';
const sign = plus > 0 ? '+' : '';
const d = dayjs($$createAt$$ as number);
return (
<List.Item key={index}>
@ -26,9 +26,9 @@ export default function Render(props: {
/>
<div style={{
fontSize: 'x-large',
color: sign ? 'gold' : 'black'
color: sign ? 'gold' : 'rosybrown'
}}>
{sign}{ThousandCont(plus)}
{sign}{ThousandCont(plus, 2)}
</div>
</List.Item>
);

View File

@ -28,6 +28,7 @@ export class BackendRuntimeContext<ED extends EntityDict & BaseEntityDict> exten
(config) => {
unset(config, 'publicKeyFilePath');
unset(config, 'privateKeyFilePath');
unset(config, 'apiV3Key');
}
);
}

View File

@ -9,7 +9,7 @@ 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';
type Type = 'deposit' | 'withdraw' | 'consume' | 'loan' | 'repay' | 'withdrawBack' | 'loss';
export interface Schema extends EntityShape {
account: Account;
@ -42,6 +42,7 @@ export const entityDesc: EntityDesc<Schema, '', '', {
loan: '抵押',
repay: '偿还',
withdrawBack: '提现失败',
loss: '损耗'
},
},
},
@ -54,7 +55,8 @@ export const entityDesc: EntityDesc<Schema, '', '', {
withdrawBack: '#F7DC6F',
consume: '#A569BD',
loan: '#CD6155',
repay: '#82E0AA',
repay: '#82E0AA',
loss: '#2E4053'
}
}
},

View File

@ -4,6 +4,7 @@ import { EntityDict } from '../oak-app-domain';
import { BRC } from '@project/types/RuntimeCxt';
import assert from 'assert';
import { DATA_SUBSCRIBER_KEYS } from '@project/config/constants';
import { getDepositRatio } from '@project/utils/pay';
const triggers: Trigger<EntityDict, 'accountOper', BRC>[] = [
{
@ -43,7 +44,66 @@ const triggers: Trigger<EntityDict, 'accountOper', BRC>[] = [
context.saveOperationToEvent(id, `${DATA_SUBSCRIBER_KEYS.accountNumberChanged}-${accountId}`);
return 1;
},
} as CreateTriggerInTxn<EntityDict, 'accountOper', BRC>
} as CreateTriggerInTxn<EntityDict, 'accountOper', BRC>,
{
name: '当account depoist时根据系统配置扣除掉损耗',
entity: 'accountOper',
action: 'create',
when: 'after',
check(operation) {
const { data } = operation;
assert(!(data instanceof Array));
return data.type ==='deposit';
},
fn: async({ operation }, context, option) => {
const { data } = operation;
assert(!(data instanceof Array));
const { entity, entityId } = data;
assert(entity === 'pay' && entityId);
const [pay] = await context.select('pay', {
data: {
id: 1,
price: 1,
channel: 1,
application: {
id: 1,
payConfig: 1,
system: {
id: 1,
payConfig: 1,
},
}
},
filter: {
id: entityId,
},
}, {});
const { price, application, channel } = pay;
const ratio = getDepositRatio(channel!, application!);
if (ratio > 0) {
const loss = Math.ceil(price! * ratio / 100);
await context.operate('accountOper', {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
type: 'loss',
totalPlus: -loss,
availPlus: -loss,
entity,
entityId,
accountId: data.accountId!,
}
}, {});
return 1;
}
return 0;
}
} as CreateTriggerInTxn<EntityDict, 'accountOper', BRC>,
];
export default triggers;

View File

@ -4,7 +4,7 @@ import { BRC } from '@project/types/RuntimeCxt';
import aoTriggers from './accountOper';
import payTriggers from './pay';
import userSystemTriggers from './userSystem';
import accountTriggers from './acount';
import accountTriggers from './account';
const triggers = [
...aoTriggers,

View File

@ -4,26 +4,29 @@ export interface ConfigBase {
channel: string;
}
export type WechatPayConfig = {
export interface ThirdPartyConfig extends ConfigBase {
payNotifyUrl: string; // 支付通知回调接口
refundNotifyUrl: string; // 退款通知回调接口
lossRatio: number; // 损耗比例,百分数
}
export interface WechatPayConfig extends ThirdPartyConfig {
channel: WechatPayChannel;
mchId: string;
publicKeyFilePath: string;
privateKeyFilePath: string;
apiV3Key: string;
payNotifyUrl: string; // 支付通知回调接口
refundNotifyUrl: string; // 退款通知回调接口
lossRatio: number; // 损耗比例,百分数
};
export type AccountPayChannel = 'ACCOUNT';
export type AccountPayConfig = {
export interface AccountPayConfig extends ConfigBase {
channel: AccountPayChannel;
depositLoss: boolean;
depositLossRatio?: number; // 充值损耗额度,百分数,不设置则按充值途径的损耗扣
};
export type OfflinePayChannel = 'OFFLINE';
export type OfflinePayConfig = {
export interface OfflinePayConfig extends ConfigBase {
tips: string;
channel: OfflinePayChannel;
options?: string[];
@ -41,3 +44,11 @@ export const PAY_CHANNEL_WECHAT_MP_NAME = 'WECHAT_MP';
export const PAY_CHANNEL_WECHAT_H5_NAME = 'WECHAT_H5';
export const PAY_CHANNEL_WECHAT_APP_NAME = 'WECHAT_APP';
export const PAY_CHANNEL_WECHAT_NATIVE_NAME = 'WECHAT_NATIVE';
export const PAY_WECHAT_CHANNELS = [
PAY_CHANNEL_WECHAT_JS_NAME,
PAY_CHANNEL_WECHAT_MP_NAME,
PAY_CHANNEL_WECHAT_H5_NAME,
PAY_CHANNEL_WECHAT_APP_NAME,
PAY_CHANNEL_WECHAT_NATIVE_NAME
];

View File

@ -6,6 +6,7 @@ import { getPayClazz } from './payClazz';
import { BRC } from '../types/RuntimeCxt';
import assert from 'assert';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { AccountPayConfig, PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_WECHAT_APP_NAME, PAY_WECHAT_CHANNELS, PayConfig, ThirdPartyConfig } from '@project/types/PayConfig';
export const fullPayProjection: EntityDict['pay']['Selection']['data'] = {
id: 1,
@ -85,4 +86,42 @@ export async function payNotify<ED extends EntityDict & BaseEntityDict>(context:
}
return;
}
/**
*
* @param context
* @param channel
* @param application
*/
export function getDepositRatio<ED extends EntityDict & BaseEntityDict>(
channel: string,
application: ED['application']['Schema']) {
const { payConfig, system } = application!;
const { payConfig: systemPayConfig } = system!;
const accountConfig = systemPayConfig!.find(
ele => ele.channel === PAY_CHANNEL_ACCOUNT_NAME
)! as AccountPayConfig;
const { depositLoss, depositLossRatio } = accountConfig;
if (!depositLoss) {
return 0;
}
if (depositLossRatio) {
return depositLossRatio;
}
const config = systemPayConfig?.find(
ele => ele.channel === channel
) || payConfig?.find(
ele => ele.channel === channel
);
assert(config && PAY_WECHAT_CHANNELS.includes(channel));
const { lossRatio } = config as ThirdPartyConfig;
return lossRatio || 0;
}