diff --git a/.changeset/great-carrots-refuse.md b/.changeset/great-carrots-refuse.md new file mode 100644 index 0000000000..dfc0485ddc --- /dev/null +++ b/.changeset/great-carrots-refuse.md @@ -0,0 +1,7 @@ +--- +"@scow/mis-server": patch +"@scow/mis-web": patch +"@scow/grpc-api": patch +--- + +管理系统下的平台数据统计提交作业前十的用户数横坐标改为以 userName 的方式显示. diff --git a/apps/mis-server/src/services/job.ts b/apps/mis-server/src/services/job.ts index 69116199ce..58922daa35 100644 --- a/apps/mis-server/src/services/job.ts +++ b/apps/mis-server/src/services/job.ts @@ -412,6 +412,40 @@ export const jobServiceServer = plugin((server) => { ]; }, + // 返回用户名,需要联表查询 + getUsersWithMostJobSubmissions: async ({ request, em }) => { + // topNUsers不传默认为10,最大限制为10 + const { startTime, endTime, topNUsers = 10 } = ensureNotUndefined(request, ["startTime", "endTime"]); + + // 控制topNUsers的数量 + if (typeof topNUsers == "number" && (topNUsers > 10 || topNUsers < 0)) { + throw { code: status.INVALID_ARGUMENT, message:"topNUsers must be between 0 and 10" }; + } + // 直接使用Knex查询构建器 + const knex = em.getKnex(); + + const results: {userName: string, userId: string, count: number}[] = await knex("job_info as j") + .select([ + "u.name as userName", + "j.user as userId", + knex.raw("COUNT(*) as count"), + ]) + .join("user as u", "u.user_id", "=", "j.user") + .where("j.time_submit", ">=", startTime) + .andWhere("j.time_submit", "<=", endTime) + .groupBy("j.user") + .orderBy("count", "desc") + .limit(Math.min(topNUsers, 10)); + + // 直接返回构建的结果 + return [ + { + results, + }, + ]; + }, + + getNewJobCount: async ({ request, em }) => { const { startTime, endTime, timeZone = "UTC" } = ensureNotUndefined(request, ["startTime", "endTime"]); diff --git a/apps/mis-server/tests/job/JobService.test.ts b/apps/mis-server/tests/job/JobService.test.ts index 99e7f591bb..918d97d439 100644 --- a/apps/mis-server/tests/job/JobService.test.ts +++ b/apps/mis-server/tests/job/JobService.test.ts @@ -250,6 +250,8 @@ it("get Top Submit Job Users correctly", async () => { }); + + it("get new job count correctly in UTC+8 timezone", async () => { const today = dayjs(); @@ -303,3 +305,31 @@ it("get new job count correctly in UTC+8 timezone", async () => { ]); }); + + +it("get Users With Most Job Submissions correctly", async () => { + const em = server.ext.orm.em.fork(); + + const today = dayjs(); + + const userAJobs = range(0, 20).map((_) => + mockOriginalJobData(data.uaAA, new Decimal(20), new Decimal(10), today.toDate())); + const userBJobs = range(0, 30).map((_) => + mockOriginalJobData(data.uaBB, new Decimal(20), new Decimal(10), today.toDate())); + const userCJobs = range(0, 40).map((_) => + mockOriginalJobData(data.uaCC, new Decimal(20), new Decimal(10), today.toDate())); + await em.persistAndFlush([...userAJobs, ...userBJobs, ...userCJobs]); + + const client = createClient(); + const reply = await asyncClientCall(client, "getUsersWithMostJobSubmissions", { + startTime: today.startOf("day").toISOString(), + endTime: today.endOf("day").toISOString(), + }); + + expect(reply.results).toMatchObject([ + { userName: data.userC.name, userId: data.userC.userId, count: 40 }, + { userName: data.userB.name, userId: data.userB.userId, count: 30 }, + { userName: data.userA.name, userId: data.userA.userId, count: 20 }, + ]); + +}); diff --git a/apps/mis-web/src/apis/api.mock.ts b/apps/mis-web/src/apis/api.mock.ts index 7e2d183233..fecfc214c8 100644 --- a/apps/mis-web/src/apis/api.mock.ts +++ b/apps/mis-web/src/apis/api.mock.ts @@ -297,6 +297,8 @@ export const mockApi: MockApi = { getTopSubmitJobUser: async () => ({ results: [{ userId: "test", count:10 }]}), + getUsersWithMostJobSubmissions: async () => ({ results: [{ userName: "name1", userId: "test1", count:10 }]}), + getNewJobCount: async () => ({ results: [{ date: { year: 2023, month: 12, day: 21 }, count: 10 }]}), getTenantUsers: async () => ({ results: mockUsers }), diff --git a/apps/mis-web/src/apis/api.ts b/apps/mis-web/src/apis/api.ts index 06b1736e8b..f1cf5551ba 100644 --- a/apps/mis-web/src/apis/api.ts +++ b/apps/mis-web/src/apis/api.ts @@ -39,6 +39,7 @@ import type { GetTenantUsersSchema } from "src/pages/api/admin/getTenantUsers"; import type { GetTopChargeAccountSchema } from "src/pages/api/admin/getTopChargeAccount"; import type { GetTopPayAccountSchema } from "src/pages/api/admin/getTopPayAccount"; import type { GetTopSubmitJobUserSchema } from "src/pages/api/admin/getTopSubmitJobUser"; +import type { GetUsersWithMostJobSubmissionsSchema } from "src/pages/api/admin/getUsersWithMostJobSubmissions"; import type { ImportUsersSchema } from "src/pages/api/admin/importUsers"; import type { GetAlarmDbIdSchema } from "src/pages/api/admin/monitor/getAlarmDbId"; import type { GetAlarmLogsSchema } from "src/pages/api/admin/monitor/getAlarmLogs"; @@ -172,6 +173,7 @@ export const api = { queryJobTimeLimit: apiClient.fromTypeboxRoute("GET", "/api/job/queryJobTimeLimit"), getRunningJobs: apiClient.fromTypeboxRoute("GET", "/api/job/runningJobs"), getTopSubmitJobUser: apiClient.fromTypeboxRoute("GET", "/api/admin/getTopSubmitJobUser"), + getUsersWithMostJobSubmissions: apiClient.fromTypeboxRoute("GET", "/api/admin/getUsersWithMostJobSubmissions"), getNewJobCount: apiClient.fromTypeboxRoute("GET", "/api/admin/getNewJobCount"), getOperationLogs: apiClient.fromTypeboxRoute("GET", "/api/log/getOperationLog"), getCustomEventTypes: apiClient.fromTypeboxRoute("GET", "/api/log/getCustomEventTypes"), diff --git a/apps/mis-web/src/pages/admin/statistic.tsx b/apps/mis-web/src/pages/admin/statistic.tsx index f585d9b29a..5db7be9187 100644 --- a/apps/mis-web/src/pages/admin/statistic.tsx +++ b/apps/mis-web/src/pages/admin/statistic.tsx @@ -175,14 +175,25 @@ requireAuth((u) => u.platformRoles.includes(PlatformRole.PLATFORM_ADMIN)) const { data: dailyPay, isLoading: dailyPayLoading } = useAsync({ promiseFn: getDailyPayFn }); - const getTopSubmitJobUserFn = useCallback(async () => { - return await api.getTopSubmitJobUser({ query: { + // const getTopSubmitJobUserFn = useCallback(async () => { + // return await api.getTopSubmitJobUser({ query: { + // startTime: query.filterTime[0].startOf("day").toISOString(), + // endTime: query.filterTime[1].endOf("day").toISOString(), + // } }); + // }, [query]); + + // const { data: topSubmitJobUser, isLoading: topSubmitJobUserLoading } = + // useAsync({ promiseFn: getTopSubmitJobUserFn }); + + const getUsersWithMostJobSubmissionsFn = useCallback(async () => { + return await api.getUsersWithMostJobSubmissions({ query: { startTime: query.filterTime[0].startOf("day").toISOString(), endTime: query.filterTime[1].endOf("day").toISOString(), } }); }, [query]); - const { data: topSubmitJobUser, isLoading: topSubmitJobUserLoading } = useAsync({ promiseFn: getTopSubmitJobUserFn }); + const { data: topSubmitJobUser, isLoading: topSubmitJobUserLoading } = + useAsync({ promiseFn: getUsersWithMostJobSubmissionsFn }); const getNewJobCountFn = useCallback(async () => { return await api.getNewJobCount({ query: { @@ -264,7 +275,7 @@ requireAuth((u) => u.platformRoles.includes(PlatformRole.PLATFORM_ADMIN)) const topSubmitJobUserData = useMemo(() => { return topSubmitJobUser?.results.map((r) => ({ - x: r.userId, + x: r.userName, y: r.count, })) || []; }, [query, topSubmitJobUser]); diff --git a/apps/mis-web/src/pages/api/admin/getUsersWithMostJobSubmissions.ts b/apps/mis-web/src/pages/api/admin/getUsersWithMostJobSubmissions.ts new file mode 100644 index 0000000000..9831367aee --- /dev/null +++ b/apps/mis-web/src/pages/api/admin/getUsersWithMostJobSubmissions.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { typeboxRoute, typeboxRouteSchema } from "@ddadaal/next-typed-api-routes-runtime"; +import { asyncClientCall } from "@ddadaal/tsgrpc-client"; +import { JobServiceClient } from "@scow/protos/build/server/job"; +import { Static, Type } from "@sinclair/typebox"; +import { authenticate } from "src/auth/server"; +import { PlatformRole } from "src/models/User"; +import { getClient } from "src/utils/client"; + +export const GetUsersWithMostJobSubmissionsResponse = Type.Object({ + results: Type.Array(Type.Object({ + userName: Type.String(), + userId:Type.String(), + count: Type.Number(), + })), +}); + +// 定义错误相应类型 +export const ErrorResponse = Type.Object({ + message: Type.String(), +}); + +export type GetUsersWithMostJobSubmissionsResponse = Static; + + +export const GetUsersWithMostJobSubmissionsSchema = typeboxRouteSchema({ + method: "GET", + + query: Type.Object({ + + startTime: Type.String({ format: "date-time" }), + + endTime: Type.String({ format: "date-time" }), + + // 最大为10,不传默认为10 + topNUsers: Type.Optional(Type.Number()), + + }), + + responses: { + 200: GetUsersWithMostJobSubmissionsResponse, + 400: ErrorResponse, + }, +}); + +const auth = authenticate((info) => info.platformRoles.includes(PlatformRole.PLATFORM_ADMIN)); + +export default typeboxRoute(GetUsersWithMostJobSubmissionsSchema, + async (req, res) => { + + const info = await auth(req, res); + if (!info) { + return; + } + + + const { startTime, endTime, topNUsers } = req.query; + // 检查 topNUsers 是否符合要求 + if (typeof topNUsers == "number" && (topNUsers > 10 || topNUsers < 0)) { + res.status(400).send({ message: "Parameter topNUsers must be between 0 and 10." }); + return; + }; + + const client = getClient(JobServiceClient); + + const { results } = await asyncClientCall(client, "getUsersWithMostJobSubmissions", { + startTime, + endTime, + topNUsers, + }); + + return { + 200: { + results, + }, + }; + }); diff --git a/protos/server/job.proto b/protos/server/job.proto index 41a0b366c0..600554ac68 100644 --- a/protos/server/job.proto +++ b/protos/server/job.proto @@ -168,6 +168,24 @@ message GetTopSubmitJobUsersResponse { repeated SubmitJobUser results = 1; } +message GetUsersWithMostJobSubmissionsRequest { + google.protobuf.Timestamp start_time = 1; + google.protobuf.Timestamp end_time = 2; + + //需要获取top 多少 + //最大为10 + optional uint32 top_n_users = 3; +} + +message GetUsersWithMostJobSubmissionsResponse { + message UserInfo { + string user_name = 1; + string user_id = 2; + uint32 count = 3; + } + repeated UserInfo results = 1; +} + message GetNewJobCountRequest { google.protobuf.Timestamp start_time = 1; google.protobuf.Timestamp end_time = 2; @@ -215,6 +233,8 @@ service JobService { rpc GetTopSubmitJobUsers(GetTopSubmitJobUsersRequest) returns (GetTopSubmitJobUsersResponse); + rpc GetUsersWithMostJobSubmissions(GetUsersWithMostJobSubmissionsRequest) returns (GetUsersWithMostJobSubmissionsResponse); + rpc GetNewJobCount(GetNewJobCountRequest) returns (GetNewJobCountResponse); rpc GetJobTotalCount(GetJobTotalCountRequest) returns (GetJobTotalCountResponse);