Files
dukang/server/dukang-api/src/modules/ops/admin-dashboard-analytics.ts
T
jacy 68a4b7f984
CI / verify (pull_request) Has been cancelled
v4.0.14概览和日报
2026-09-02 21:11:58 +08:00

250 lines
9.6 KiB
TypeScript

import { Prisma } from '@prisma/client';
import type { DashboardGranularity } from '@dukang/shared-types';
const COL_ALLOW = new Set([
'u.created_at',
'o.created_at',
'o.paid_at',
'p.created_at',
's.created_at',
'r.created_at',
]);
const SERIES_COL_ALLOW = new Set([
'promo.promo_code_id',
'u.assoc_partner_account_id',
'ap.activity_poster_id',
's.partner_account_id',
'o.user_id',
'o.product_id',
'r.store_id',
]);
export function sqlSeriesId(column: string): Prisma.Sql {
if (!SERIES_COL_ALLOW.has(column)) throw new Error(`bad series col ${column}`);
return Prisma.sql`COALESCE(CAST(${Prisma.raw(column)} AS CHAR), 'none')`;
}
export function shanghaiSqlBucket(column: string, grain: DashboardGranularity): Prisma.Sql {
if (!COL_ALLOW.has(column)) throw new Error(`bad column ${column}`);
const col = Prisma.raw(column);
switch (grain) {
case 'day':
return Prisma.sql`DATE_FORMAT(CONVERT_TZ(${col}, '+00:00', '+08:00'), '%Y-%m-%d')`;
case 'week':
return Prisma.sql`DATE_FORMAT(DATE_SUB(DATE(CONVERT_TZ(${col}, '+00:00', '+08:00')), INTERVAL WEEKDAY(DATE(CONVERT_TZ(${col}, '+00:00', '+08:00'))) DAY), '%Y-%m-%d')`;
case 'month':
return Prisma.sql`DATE_FORMAT(CONVERT_TZ(${col}, '+00:00', '+08:00'), '%Y-%m')`;
case 'quarter':
return Prisma.sql`CONCAT(YEAR(CONVERT_TZ(${col}, '+00:00', '+08:00')), '-Q', QUARTER(CONVERT_TZ(${col}, '+00:00', '+08:00')))`;
case 'year':
return Prisma.sql`DATE_FORMAT(CONVERT_TZ(${col}, '+00:00', '+08:00'), '%Y')`;
}
}
export type CityFilter =
| { kind: 'all' }
| { kind: 'none' }
| { kind: 'empty' }
| { kind: 'ids'; ids: bigint[]; codes: string[] };
export function sqlAnd(parts: Prisma.Sql[]): Prisma.Sql {
return parts.length ? Prisma.join(parts, ' AND ') : Prisma.sql`1=1`;
}
export function sqlInBigints(columnSql: Prisma.Sql, ids: bigint[]): Prisma.Sql {
if (!ids.length) return Prisma.sql`1=0`;
return Prisma.sql`${columnSql} IN (${Prisma.join(ids)})`;
}
export function sqlInStrings(columnSql: Prisma.Sql, values: string[]): Prisma.Sql {
if (!values.length) return Prisma.sql`1=0`;
return Prisma.sql`${columnSql} IN (${Prisma.join(values)})`;
}
export function sqlRange(columnSql: Prisma.Sql, start: Date, endExclusive: Date): Prisma.Sql {
return Prisma.sql`${columnSql} >= ${start} AND ${columnSql} < ${endExclusive}`;
}
export type PromoPick = { none?: boolean; id?: bigint };
export type UserWhereOpts = {
city: CityFilter;
promo?: PromoPick;
assocPartnerId?: bigint;
activityPosterId?: bigint;
createdStart?: Date | null;
createdEndExclusive?: Date | null;
createdLte?: Date | null;
};
export type OrderWhereOpts = {
city: CityFilter;
promo?: PromoPick;
assocPartnerId?: bigint;
activityPosterId?: bigint;
productId?: bigint;
createdStart?: Date | null;
createdEndExclusive?: Date | null;
};
function sqlAssocPartnerUsers(assocPartnerId: bigint): Prisma.Sql {
return Prisma.sql`SELECT id FROM user_user WHERE assoc_partner_account_id = ${assocPartnerId} AND status = 1 AND merged_into_user_id IS NULL`;
}
function sqlActivityPosterUsers(activityPosterId: bigint): Prisma.Sql {
return Prisma.sql`SELECT u.id FROM user_user u INNER JOIN partner_account p ON p.id = u.assoc_partner_account_id WHERE p.activity_poster_id = ${activityPosterId} AND p.is_primary = 1 AND u.status = 1 AND u.merged_into_user_id IS NULL`;
}
function sqlActivityPosterPartners(activityPosterId: bigint): Prisma.Sql {
return Prisma.sql`SELECT id FROM partner_account WHERE activity_poster_id = ${activityPosterId} AND is_primary = 1`;
}
function applyPromo(parts: Prisma.Sql[], columnSql: Prisma.Sql, promo?: PromoPick) {
if (promo?.none) parts.push(Prisma.sql`${columnSql} IS NULL`);
else if (promo?.id !== undefined) parts.push(Prisma.sql`${columnSql} = ${promo.id}`);
}
export function userSqlWhere(opts: UserWhereOpts): Prisma.Sql {
const parts: Prisma.Sql[] = [
Prisma.sql`u.status = 1`,
Prisma.sql`u.merged_into_user_id IS NULL`,
];
if (opts.createdStart && opts.createdEndExclusive) {
parts.push(sqlRange(Prisma.sql`u.created_at`, opts.createdStart, opts.createdEndExclusive));
} else if (opts.createdLte) {
parts.push(Prisma.sql`u.created_at <= ${opts.createdLte}`);
}
if (opts.city.kind === 'empty') parts.push(Prisma.sql`1=0`);
else if (opts.city.kind === 'none') {
parts.push(Prisma.sql`(pref.id IS NULL OR pref.selected_city_code IS NULL)`);
} else if (opts.city.kind === 'ids') {
parts.push(sqlInStrings(Prisma.sql`pref.selected_city_code`, opts.city.codes));
}
if (opts.promo?.none) parts.push(Prisma.sql`promo.id IS NULL`);
else if (opts.promo?.id !== undefined) parts.push(Prisma.sql`promo.promo_code_id = ${opts.promo.id}`);
if (opts.assocPartnerId !== undefined) {
parts.push(Prisma.sql`u.assoc_partner_account_id = ${opts.assocPartnerId}`);
}
if (opts.activityPosterId !== undefined) {
parts.push(Prisma.sql`u.assoc_partner_account_id IN (${sqlActivityPosterPartners(opts.activityPosterId)})`);
}
return sqlAnd(parts);
}
export function orderSqlWhere(opts: OrderWhereOpts): Prisma.Sql {
const parts: Prisma.Sql[] = [];
if (opts.createdStart && opts.createdEndExclusive) {
parts.push(sqlRange(Prisma.sql`o.created_at`, opts.createdStart, opts.createdEndExclusive));
}
if (opts.city.kind === 'none' || opts.city.kind === 'empty') parts.push(Prisma.sql`1=0`);
else if (opts.city.kind === 'ids') parts.push(sqlInBigints(Prisma.sql`o.city_id`, opts.city.ids));
applyPromo(parts, Prisma.sql`o.promo_code_id`, opts.promo);
if (opts.assocPartnerId !== undefined) {
parts.push(Prisma.sql`o.user_id IN (${sqlAssocPartnerUsers(opts.assocPartnerId)})`);
}
if (opts.activityPosterId !== undefined) {
parts.push(Prisma.sql`o.user_id IN (${sqlActivityPosterUsers(opts.activityPosterId)})`);
}
if (opts.productId !== undefined) parts.push(Prisma.sql`o.product_id = ${opts.productId}`);
return sqlAnd(parts.length ? parts : [Prisma.sql`1=1`]);
}
export function paidOrderSqlWhere(
opts: OrderWhereOpts & {
paidStart: Date;
paidEndExclusive: Date;
partnerIdAtPay?: bigint;
},
): Prisma.Sql {
const parts: Prisma.Sql[] = [
Prisma.sql`o.pay_status = 'PAID'`,
sqlRange(Prisma.sql`o.paid_at`, opts.paidStart, opts.paidEndExclusive),
];
if (opts.city.kind === 'none' || opts.city.kind === 'empty') parts.push(Prisma.sql`1=0`);
else if (opts.city.kind === 'ids') parts.push(sqlInBigints(Prisma.sql`o.city_id`, opts.city.ids));
applyPromo(parts, Prisma.sql`o.promo_code_id`, opts.promo);
if (opts.partnerIdAtPay !== undefined) {
parts.push(Prisma.sql`o.partner_account_id_at_pay = ${opts.partnerIdAtPay}`);
}
if (opts.assocPartnerId !== undefined) {
parts.push(Prisma.sql`o.user_id IN (${sqlAssocPartnerUsers(opts.assocPartnerId)})`);
}
if (opts.activityPosterId !== undefined) {
parts.push(Prisma.sql`o.user_id IN (${sqlActivityPosterUsers(opts.activityPosterId)})`);
}
if (opts.productId !== undefined) parts.push(Prisma.sql`o.product_id = ${opts.productId}`);
return sqlAnd(parts);
}
export function partnerSqlWhere(
city: CityFilter,
partnerId: bigint | undefined,
createdStart: Date | null,
createdEndExclusive: Date | null,
createdLte: Date | null,
): Prisma.Sql {
const parts: Prisma.Sql[] = [Prisma.sql`p.is_primary = 1`];
if (createdStart && createdEndExclusive) {
parts.push(sqlRange(Prisma.sql`p.created_at`, createdStart, createdEndExclusive));
} else if (createdLte) {
parts.push(Prisma.sql`p.created_at <= ${createdLte}`);
}
if (city.kind === 'empty') parts.push(Prisma.sql`1=0`);
else if (city.kind === 'none') parts.push(Prisma.sql`p.city_id IS NULL`);
else if (city.kind === 'ids') parts.push(sqlInBigints(Prisma.sql`p.city_id`, city.ids));
if (partnerId !== undefined) parts.push(Prisma.sql`p.id = ${partnerId}`);
return sqlAnd(parts);
}
export function storeSqlWhere(
city: CityFilter,
partnerId: bigint | undefined,
createdStart: Date | null,
createdEndExclusive: Date | null,
createdLte: Date | null,
): Prisma.Sql {
const parts: Prisma.Sql[] = [];
if (createdStart && createdEndExclusive) {
parts.push(sqlRange(Prisma.sql`s.created_at`, createdStart, createdEndExclusive));
} else if (createdLte) {
parts.push(Prisma.sql`s.created_at <= ${createdLte}`);
}
if (city.kind === 'none' || city.kind === 'empty') parts.push(Prisma.sql`1=0`);
else if (city.kind === 'ids') parts.push(sqlInBigints(Prisma.sql`s.city_id`, city.ids));
if (partnerId !== undefined) parts.push(Prisma.sql`s.partner_account_id = ${partnerId}`);
return sqlAnd(parts.length ? parts : [Prisma.sql`1=1`]);
}
export function redeemSqlWhere(
city: CityFilter,
createdStart: Date,
createdEndExclusive: Date,
opts?: { storeId?: bigint; assocPartnerId?: bigint; storePartnerId?: bigint },
): Prisma.Sql {
const parts: Prisma.Sql[] = [
sqlRange(Prisma.sql`r.created_at`, createdStart, createdEndExclusive),
];
if (city.kind === 'none' || city.kind === 'empty') parts.push(Prisma.sql`1=0`);
else if (city.kind === 'ids') parts.push(sqlInBigints(Prisma.sql`s.city_id`, city.ids));
if (opts?.storeId !== undefined) parts.push(Prisma.sql`r.store_id = ${opts.storeId}`);
if (opts?.storePartnerId !== undefined) {
parts.push(Prisma.sql`s.partner_account_id = ${opts.storePartnerId}`);
}
if (opts?.assocPartnerId !== undefined) {
parts.push(Prisma.sql`r.user_id IN (${sqlAssocPartnerUsers(opts.assocPartnerId)})`);
}
return sqlAnd(parts);
}
export function money(v: unknown): number {
if (v == null) return 0;
const n = typeof v === 'number' ? v : Number(v);
return Math.round(n * 100) / 100;
}
export function toCount(v: unknown): number {
if (v == null) return 0;
return typeof v === 'number' ? v : Number(v);
}