enhance(backend): api/usersに+pv -pvを追加 (MisskeyIO#653)

(cherry picked from commit 26cdf6fc09f8b696ff31f5649f01f2e91bcccbec)
This commit is contained in:
nenohi 2024-07-13 01:50:57 +09:00 committed by kakkokari-gtyih
parent c83c831c53
commit 01cbe12931
4 changed files with 101 additions and 11 deletions

View File

@ -52,4 +52,12 @@ export default class PerUserPvChart extends Chart<typeof schema> { // eslint-dis
'pv.visitor': 1,
}, user.id);
}
@bindThis
public async getChartUsers(span: 'hour' | 'day', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{
userId: string;
count: number;
}[]> {
return await this.getChartPv(span, amount, cursor, limit, offset);
}
}

View File

@ -15,7 +15,7 @@ import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { MiRepository, miRepository } from '@/models/_.js';
import type { DataSource, Repository } from 'typeorm';
import type { DataSource, ObjectLiteral, Repository } from 'typeorm';
const COLUMN_PREFIX = '___' as const;
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
@ -94,6 +94,8 @@ type ToJsonSchema<S> = {
required: (keyof S)[];
};
type MiAndOrmRepository<T extends ObjectLiteral> = Repository<T> & MiRepository<T>;
export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
const unflatten = (str: string, parent: Record<string, any>) => {
const keys = str.split('.');
@ -146,11 +148,12 @@ export default abstract class Chart<T extends Schema> {
group: string | null;
}[] = [];
// ↓にしたいけどfindOneとかで型エラーになる
//private repositoryForHour: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
//private repositoryForDay: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
//private repositoryForHour: MiAndOrmRepository<RawRecord<T>>;
//private repositoryForDay: MiAndOrmRepository<RawRecord<T>>;
private repositoryForHour: MiAndOrmRepository<{ id: number; group?: string | null; date: number;}>;
private repositoryForDay: MiAndOrmRepository<{ id: number; group?: string | null; date: number;}>;
private repositoryUserPvForHour: MiAndOrmRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>;
private repositoryUserPvForDay: MiAndOrmRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>;
/**
* 1(CASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
*/
@ -276,8 +279,10 @@ export default abstract class Chart<T extends Schema> {
this.logger = logger;
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository);
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository);
this.repositoryUserPvForHour = db.getRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>(hour).extend(miRepository);
this.repositoryUserPvForDay = db.getRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>(day).extend(miRepository);
}
@bindThis
@ -727,4 +732,51 @@ export default abstract class Chart<T extends Schema> {
}
return object as Unflatten<ChartResult<T>>;
}
@bindThis
public async getChartPv(span: 'hour' | 'day', amount: number, cursor: Date | null, limit: number, offset: number): Promise<
{
userId: string,
count: number,
}[]
> {
const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate();
const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never;
const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
const gt =
span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
new Error('not happen') as never;
const repository =
span === 'hour' ? this.repositoryUserPvForHour :
span === 'day' ? this.repositoryUserPvForDay :
new Error('not happen') as never;
// ログ取得
const logs = await repository.createQueryBuilder()
.where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) })
.orderBy('___pv_visitor + ___upv_visitor + ___pv_user + ___upv_user', 'DESC')
.skip(offset)
.take(limit)
.getMany() as {
___pv_visitor: number,
___upv_visitor: number,
___pv_user: number,
___upv_user: number,
group: string,
}[];
const result = [] as {
userId: string,
count: number,
}[];
for (const row of logs) {
const userId = row.group;
const count = row.___pv_user + row.___upv_user + row.___pv_visitor + row.___upv_visitor;
result.push({ userId, count });
}
return result;
}
}

View File

@ -6,6 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
import { QueryService } from '@/core/QueryService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
@ -31,7 +32,7 @@ export const paramDef = {
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+pv', '-pv'] },
state: { type: 'string', enum: ['all', 'alive'], default: 'all' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
hostname: {
@ -49,6 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private perUserPvChart: PerUserPvChart,
private userEntityService: UserEntityService,
private queryService: QueryService,
@ -70,7 +72,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.hostname) {
query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() });
}
const chartUsers: { userId: string; count: number; }[] = [];
if (ps.sort?.endsWith('pv')) {
await this.perUserPvChart.getChartUsers('hour', 0, null, ps.limit, ps.offset).then(users => {
chartUsers.push(...users);
});
}
switch (ps.sort) {
case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
@ -78,6 +85,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
case '-createdAt': query.orderBy('user.id', 'ASC'); break;
case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break;
case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break;
case '+pv':
if (chartUsers.length > 0) {
query.andWhere('user.id IN (:...userIds)', { userIds: chartUsers.map(user => user.userId) });
}
break;
case '-pv':
if (chartUsers.length > 0) {
query.andWhere('user.id IN (:...userIds)', { userIds: chartUsers.map(user => user.userId) });
}
break;
default: query.orderBy('user.id', 'ASC'); break;
}
@ -88,6 +105,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.offset(ps.offset);
const users = await query.getMany();
if (ps.sort === '+pv') {
users.sort((a, b) => {
const aPv = chartUsers.find(user => user.userId === a.id)?.count ?? 0;
const bPv = chartUsers.find(user => user.userId === b.id)?.count ?? 0;
return bPv - aPv;
});
} else if (ps.sort === '-pv') {
users.sort((a, b) => {
const aPv = chartUsers.find(user => user.userId === a.id)?.count ?? 0;
const bPv = chartUsers.find(user => user.userId === b.id)?.count ?? 0;
return aPv - bPv;
});
}
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
});

View File

@ -24986,7 +24986,7 @@ export type operations = {
/** @default 0 */
offset?: number;
/** @enum {string} */
sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt';
sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt' | '+pv' | '-pv';
/**
* @default all
* @enum {string}