Skip to content

Commit

Permalink
feat(mis): 账户状态调整 (#1161)
Browse files Browse the repository at this point in the history
### 做了什么
**一、 在数据库实体Account中发生以下变更**

- 变更原有status字段名为blocked_in_cluster,表示在集群中是否为封锁状态

- 增加 state字段,列值为
"NORMAL","FROZEN","BLOCKED_BY_ADMIN"的枚举值,且通常情况新建账户时state的默认值为NORMAL

- state字段的历史数据迁移遵循以下原则:

如果历史数据中的账户数据,不在白名单中且在集群中状态为封锁且余额大于当前账户封锁阈值(账户封锁阈值不存在时为租 户默认封锁阈值)-->
state="BLOCKED_BY_ADMIN";
    其他情况 state="NORMAL"
    
    请测试老师注意与 #1170 在测试时数据库变更的冲突
    
**二、页面账户状态优化**

![image](https://github.com/PKUHPC/SCOW/assets/43978285/1a0be1b4-f882-4cd1-a0ef-8bc0d27da3ac)

![image](https://github.com/PKUHPC/SCOW/assets/43978285/67ad6936-7960-4158-86d0-613149ce6c92)

![image](https://github.com/PKUHPC/SCOW/assets/43978285/1187e84a-2567-403d-9db6-a8c984df1474)


**1. 创建账户**
   (1)新建账户 :余额(创建时为0)<=租户默认封锁阈值时 账户页面展示状态为欠费,反之正常
   (2)导入用户时创建账户的情况
a.账户之前在集群中状态为未封锁,选择不添加入白名单,同(1)
PKUHPC/scow-internal-dev#509
           b.账户之前在集群中状态为封锁,选择不添加入白名单,认为已被上级手动封锁,,页面状态变为封锁
   
**2. 添加或移出白名单**
     (1)添加入白名单: 默认为解封操做,状态为正常
     (2)移出白名单: 余额<=账户封锁阈值(没有设置时租户默认封锁阈值)时状态变为欠费,余额>封锁阈值为正常
              
` eg.分别将正常,欠费,封锁状态的账户加入白名单时  `     

![image](https://github.com/PKUHPC/SCOW/assets/43978285/cbc31851-0577-43e2-aeed-584cc8f3b55c)

![image](https://github.com/PKUHPC/SCOW/assets/43978285/00afe9f7-69ff-435d-80fe-6ae2d85541d8)

![image](https://github.com/PKUHPC/SCOW/assets/43978285/827ea256-3a4b-4aa5-b222-9631551de525)
`再依次移出白名单,原封锁状态消失,依次根据余额与阈值比较显示状态为正常或欠费`

![image](https://github.com/PKUHPC/SCOW/assets/43978285/e0d114a3-bf59-4f67-b2d0-e10e1f793662)

              
**3.封锁解封**
(1)白名单以外的账户封锁时:状态变更为封锁,支持欠费状态变更为封锁
(2)解封账户:余额<=账户封锁阈值(没有设置时租户默认封锁阈值)时状态变为欠费,余额>封锁阈值为正常
 
` eg.将正常与欠费状态账户封锁 `

![image](https://github.com/PKUHPC/SCOW/assets/43978285/0af3d993-1b24-4bd3-b45d-28f4166832b6)

![image](https://github.com/PKUHPC/SCOW/assets/43978285/1accc2bb-447c-4b80-8821-5ad3817d20a6)


**4.充值和扣费**
     (1)充值: 封锁/正常时 充值状态不变;欠费时根据充值后状态 再次判断余额和封锁阈值,判断是否欠费
     (2)扣费:已在白名单中显示正常状态的账户扣费时状态不变;不在白名单中未欠费显示正常的账户,扣费后再次判断余额和封锁阈值,判断是否欠费
          
` eg.分别对欠费和封锁账户进行充值`

![image](https://github.com/PKUHPC/SCOW/assets/43978285/d2ca1212-8652-4858-bca9-9a1aab5c0b8d)

![image](https://github.com/PKUHPC/SCOW/assets/43978285/42e183b2-76c2-4cba-9b91-4c4962b2307b)

![image](https://github.com/PKUHPC/SCOW/assets/43978285/8e66e162-3413-48b7-b9a5-eab20890ce32)


                 
  **5.修改封锁阈值**
     (1)修改账户封锁阈值
     (2)修改租户下的默认封锁阈值,检查所有使用该租户默认封锁阈值的账户是否需要更新

` eg.将租户封锁阈值由  -100修改为100 `   

![image](https://github.com/PKUHPC/SCOW/assets/43978285/6884682a-9589-4acc-b475-22549619e5b2)

![image](https://github.com/PKUHPC/SCOW/assets/43978285/5a5bb179-a099-4f19-a986-14dd2b870315)

![image](https://github.com/PKUHPC/SCOW/assets/43978285/e486ec73-01eb-438a-8ce2-563043385340)
  
**三、修改账户导出功能**
按新增正常TAB增加正常状态导出,修改导出时状态展示列与列表一致
  • Loading branch information
piccaSun authored Mar 27, 2024
1 parent 6d4b22c commit 8dd8c0e
Show file tree
Hide file tree
Showing 41 changed files with 946 additions and 249 deletions.
7 changes: 7 additions & 0 deletions .changeset/nasty-turtles-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@scow/mis-server": patch
"@scow/mis-web": patch
---

修改 Account 实体中原 blocked 字段名为 blocked_in_cluster ,表示在集群中是否为封锁状态
增加字段 state ,字段值为 "NORMAL" , "FROZEN" , "BLOCKED_BY_ADMIN" 的枚举值,优化页面账户显示状态为正常、封锁、欠费
5 changes: 5 additions & 0 deletions .changeset/tender-bees-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@scow/grpc-api": patch
---

在 GetAccounts 接口中增加账户状态,账户显示状态,账户白名单状态的返回值,exportAccounts 接口中增加欠费,冻结的查询参数
12 changes: 6 additions & 6 deletions apps/mis-server/src/bl/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function updateBlockStatusInSlurm(
) {
const blockedAccounts: string[] = [];
const blockedFailedAccounts: string[] = [];
const accounts = await em.find(Account, { blocked: true });
const accounts = await em.find(Account, { blockedInCluster: true });

for (const account of accounts) {
if (account.whitelist) {
Expand Down Expand Up @@ -100,7 +100,7 @@ export async function updateUnblockStatusInSlurm(
) {
const accounts = await em.find(Account, {
$or: [
{ blocked: false },
{ blockedInCluster: false },
{ whitelist: { $ne: null } },
],
});
Expand Down Expand Up @@ -143,7 +143,7 @@ export async function blockAccount(
account: Loaded<Account, "tenant">, clusterPlugin: ClusterPlugin["clusters"], logger: Logger,
): Promise<"AlreadyBlocked" | "Whitelisted" | "OK"> {

if (account.blocked) { return "AlreadyBlocked"; }
if (account.blockedInCluster) { return "AlreadyBlocked"; }

if (account.whitelist) {
return "Whitelisted";
Expand All @@ -155,7 +155,7 @@ export async function blockAccount(
});
});

account.blocked = true;
account.blockedInCluster = true;

await callHook("accountBlocked", { accountName: account.accountName, tenantName: account.tenant.$.name }, logger);

Expand All @@ -173,15 +173,15 @@ export async function unblockAccount(
account: Loaded<Account, "tenant">, clusterPlugin: ClusterPlugin["clusters"], logger: Logger,
): Promise<"OK" | "ALREADY_UNBLOCKED"> {

if (!account.blocked) { return "ALREADY_UNBLOCKED"; }
if (!account.blockedInCluster) { return "ALREADY_UNBLOCKED"; }

await clusterPlugin.callOnAll(logger, async (client) => {
await asyncClientCall(client.account, "unblockAccount", {
accountName: account.accountName,
});
});

account.blocked = false;
account.blockedInCluster = false;
await callHook("accountUnblocked", { accountName: account.accountName, tenantName: account.tenant.$.name }, logger);

return "OK";
Expand Down
11 changes: 9 additions & 2 deletions apps/mis-server/src/bl/charging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Tenant } from "src/entities/Tenant";
import { UserAccount } from "src/entities/UserAccount";
import { ClusterPlugin } from "src/plugins/clusters";
import { callHook } from "src/plugins/hookClient";
import { getAccountStateInfo } from "src/utils/accountUserState";
import { AnyJson } from "src/utils/types";

interface PayRequest {
Expand All @@ -38,15 +39,21 @@ export function checkShouldBlockAccount(account: Loaded<Account, "tenant">) {
const blockThresholdAmount =
account.blockThresholdAmount ?? account.tenant.$.defaultAccountBlockThreshold;

return account.balance.lte(blockThresholdAmount);
const accountStateInfo =
getAccountStateInfo(account.whitelist?.id, account.state, account.balance, blockThresholdAmount);

return accountStateInfo.shouldBlockInCluster;
}

export function checkShouldUnblockAccount(account: Loaded<Account, "tenant">) {

const blockThresholdAmount =
account.blockThresholdAmount ?? account.tenant.$.defaultAccountBlockThreshold;

return account.balance.gt(blockThresholdAmount);
const accountStateInfo =
getAccountStateInfo(account.whitelist?.id, account.state, account.balance, blockThresholdAmount);

return !accountStateInfo.shouldBlockInCluster;
}

export async function pay(
Expand Down
42 changes: 38 additions & 4 deletions apps/mis-server/src/bl/importUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { Logger } from "@ddadaal/tsgrpc-server";
import { ServiceError } from "@grpc/grpc-js";
import { Status } from "@grpc/grpc-js/build/src/constants";
import { SqlEntityManager } from "@mikro-orm/mysql";
import { unblockAccount } from "src/bl/block";
import { Account } from "src/entities/Account";
import { blockAccount, unblockAccount } from "src/bl/block";
import { Account, AccountState } from "src/entities/Account";
import { AccountWhitelist } from "src/entities/AccountWhitelist";
import { Tenant } from "src/entities/Tenant";
import { User } from "src/entities/User";
Expand Down Expand Up @@ -62,9 +62,11 @@ export async function importUsers(data: ImportUsersData, em: SqlEntityManager,

const accountMap: Record<string, Account> = {};
data.accounts.forEach((account) => {
// 导入账户时,如果在集群中的账户状态为封锁,则scow同步封锁状态,默认为被上级手动封锁
// 导入账户时,如果在集群中的账户状态为正常,则scow同步正常状态
accountMap[account.accountName] = new Account({
accountName: account.accountName, comment: "", blocked:Boolean(account.blocked),
tenant,
accountName: account.accountName, comment: "", blockedInCluster: Boolean(account.blocked),
tenant, state: Boolean(account.blocked) ? AccountState.BLOCKED_BY_ADMIN : AccountState.NORMAL,
});
});
const existingAccounts = await em.find(Account,
Expand Down Expand Up @@ -117,6 +119,7 @@ export async function importUsers(data: ImportUsersData, em: SqlEntityManager,

// 账户信息导入scow完成后,更新slurm的block状态
const failedUnblockAccounts = [] as string[];
const failedBlockAccounts = [] as string[];
if (whitelistAll) {
await Promise.allSettled(accounts.map((acc) => {
return em.transactional(async (em) => {
Expand All @@ -139,10 +142,37 @@ export async function importUsers(data: ImportUsersData, em: SqlEntityManager,
operatorId: "",
});
account.whitelist = toRef(whitelist);
// 加入白名单后账户状态变为正常
account.state = AccountState.NORMAL;
await em.persistAndFlush(whitelist);
}
});
}));
// 如果不选择全部添加白名单时,判断租户默认阈值选择是否在集群中封锁账户
} else {
const shouldBlockInCluster = tenant.defaultAccountBlockThreshold.gte(0);
const shouldBlockAccounts = accounts.filter((a) => !a.blockedInCluster);

if (shouldBlockInCluster) {
await Promise.allSettled(shouldBlockAccounts.map((acc) => {
return em.transactional(async (em) => {
const account = await em.findOne(Account, { accountName: acc.accountName },
{ populate: ["tenant"]});
if (!account) {
failedBlockAccounts.push(acc.accountName);
} else {
try {
await blockAccount(account, clusterPlugin, logger);
} catch (e) {
// 集群封锁账户失败,记录失败账户
failedBlockAccounts.push(account.accountName);
throw e;
}
}
});
}));
}

}
logger.info(
`Import users complete. ${accounts.length} accounts, \
Expand All @@ -155,6 +185,10 @@ export async function importUsers(data: ImportUsersData, em: SqlEntityManager,
logger.warn(`${failedUnblockAccounts.length} accounts failed to unblock.`);
logger.warn(failedUnblockAccounts.join(", "));
}
if (failedBlockAccounts.length !== 0) {
logger.warn(`${failedBlockAccounts.length} accounts failed to block.`);
logger.warn(failedBlockAccounts.join(", "));
}

return {
accountCount: accounts.length - existingAccounts.length,
Expand Down
20 changes: 15 additions & 5 deletions apps/mis-server/src/entities/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import { Collection, Entity,
Enum,
ManyToOne, OneToMany, OneToOne, PrimaryKey, Property,
Ref } from "@mikro-orm/core";
import { Decimal } from "@scow/lib-decimal";
Expand All @@ -20,6 +21,12 @@ import { UserAccount } from "src/entities/UserAccount";
import { DECIMAL_DEFAULT_RAW, DecimalType } from "src/utils/decimal";
import { DATETIME_TYPE, EntityOrRef, toRef } from "src/utils/orm";

export enum AccountState {
NORMAL = "NORMAL",
FROZEN = "FROZEN",
BLOCKED_BY_ADMIN = "BLOCKED_BY_ADMIN",
}

@Entity()
export class Account {
@PrimaryKey()
Expand All @@ -32,7 +39,7 @@ export class Account {
tenant: Ref<Tenant>;

@Property()
blocked: boolean;
blockedInCluster: boolean;

@OneToMany(() => UserAccount, (u) => u.account)
users = new Collection<UserAccount>(this);
Expand All @@ -51,26 +58,29 @@ export class Account {
@Property({ type: DecimalType, nullable: true })
blockThresholdAmount: Decimal | undefined;

@Enum({ items: () => AccountState, default: AccountState.NORMAL, comment: Object.values(AccountState).join(", ") })
state: AccountState;

@Property({ columnType: DATETIME_TYPE, nullable: true })
createTime: Date;

constructor(init: {
accountName: string;
whitelist?: EntityOrRef<AccountWhitelist>;
tenant: EntityOrRef<Tenant>;
blocked: boolean;
blockedInCluster: boolean;
comment?: string;
state?: AccountState;
createTime?: Date;
}) {
this.accountName = init.accountName;
this.blocked = init.blocked;
this.blockedInCluster = init.blockedInCluster;
this.tenant = toRef(init.tenant);
if (init.whitelist) {
this.whitelist = toRef(init.whitelist);
}
this.comment = init.comment || "";
this.state = init.state || AccountState.NORMAL;
this.createTime = init.createTime ?? new Date();
}


}
19 changes: 17 additions & 2 deletions apps/mis-server/src/migrations/.snapshot-scow_server.json
Original file line number Diff line number Diff line change
Expand Up @@ -1191,8 +1191,8 @@
"nullable": false,
"mappedType": "integer"
},
"blocked": {
"name": "blocked",
"blocked_in_cluster": {
"name": "blocked_in_cluster",
"type": "tinyint(1)",
"unsigned": false,
"autoincrement": false,
Expand Down Expand Up @@ -1242,6 +1242,21 @@
"scale": 4,
"mappedType": "decimal"
},
"state": {
"name": "state",
"type": "enum('FROZEN','BLOCKED_BY_ADMIN','NORMAL')",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"enumItems": [
"FROZEN",
"BLOCKED_BY_ADMIN",
"NORMAL"
],
"comment": "FROZEN, BLOCKED_BY_ADMIN, NORMAL",
"mappedType": "enum"
},
"create_time": {
"name": "create_time",
"type": "DATETIME(6)",
Expand Down
27 changes: 27 additions & 0 deletions apps/mis-server/src/migrations/Migration20240304064259.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Migration } from '@mikro-orm/migrations';

export class Migration20240304064259 extends Migration {

async up(): Promise<void> {
this.addSql('alter table `account` add `state` enum(\'FROZEN\', \'BLOCKED_BY_ADMIN\', \'NORMAL\') not null comment \'FROZEN, BLOCKED_BY_ADMIN, NORMAL\';');
this.addSql('alter table `account` change `blocked` `blocked_in_cluster` tinyint(1) not null;');

this.addSql(`
UPDATE account a
SET a.state =
CASE
WHEN a.whitelist_id IS NULL AND a.blocked_in_cluster = 1
AND a.balance > COALESCE(a.block_threshold_amount, (SELECT t.default_account_block_threshold FROM tenant t WHERE a.tenant_id = t.id))
THEN 'BLOCKED_BY_ADMIN'
ELSE 'NORMAL'
END;
`);
}

async down(): Promise<void> {
this.addSql('alter table `account` drop column `state`;');

this.addSql('alter table `account` change `blocked_in_cluster` `blocked` tinyint(1) not null;');
}

}
Loading

0 comments on commit 8dd8c0e

Please sign in to comment.