250 lines
9.6 KiB
TypeScript
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);
|
|
}
|