164 lines
5.9 KiB
TypeScript
164 lines
5.9 KiB
TypeScript
import type { RowDataPacket } from 'mysql2/promise';
|
|
import type { AccessProfile } from '../auth/rbac-repository.js';
|
|
import type { MySqlPool } from '../db/mysql.js';
|
|
import type { OrderStatus } from './order-state-repository.js';
|
|
|
|
export class OrderQueryError extends Error {
|
|
constructor(public readonly code: string) { super(code); }
|
|
}
|
|
|
|
export interface OrderQueryAccess {
|
|
tenantId: string;
|
|
userId: string;
|
|
access: AccessProfile;
|
|
}
|
|
|
|
interface OrderListRow extends RowDataPacket {
|
|
id: string;
|
|
orderNo: string;
|
|
storeId: string;
|
|
storeName: string;
|
|
roomId: string;
|
|
roomName: string;
|
|
roomNo: string;
|
|
status: OrderStatus;
|
|
startAt: Date;
|
|
endAt: Date;
|
|
totalAmountCents: number;
|
|
paidAmountCents: number;
|
|
latestPaymentId: string | null;
|
|
latestPaymentProvider: string | null;
|
|
latestPaymentStatus: string | null;
|
|
createdAt: Date;
|
|
}
|
|
|
|
interface CountRow extends RowDataPacket { total: number }
|
|
|
|
export class OrderQueryRepository {
|
|
constructor(private readonly pool: MySqlPool) {}
|
|
|
|
async listMine(input: OrderQueryAccess & {
|
|
page: number;
|
|
pageSize: number;
|
|
status?: OrderStatus;
|
|
}) {
|
|
const where = [
|
|
'o.tenant_id = ?',
|
|
'o.deleted_at IS NULL',
|
|
'(a.user_id = ? OR ' + managerScopeSql(input.access, 'o.store_id') + ')'
|
|
];
|
|
const params: Array<string | number> = [input.tenantId, input.userId];
|
|
if (input.status) {
|
|
where.push('o.status = ?');
|
|
params.push(input.status);
|
|
}
|
|
const whereSql = where.join(' AND ');
|
|
const offset = (input.page - 1) * input.pageSize;
|
|
const [counts] = await this.pool.execute<CountRow[]>(
|
|
`SELECT COUNT(DISTINCT o.id) AS total
|
|
FROM qipai_orders o
|
|
LEFT JOIN qipai_order_user_access a
|
|
ON a.tenant_id = o.tenant_id AND a.order_id = o.id AND a.revoked_at IS NULL
|
|
WHERE ${whereSql}`,
|
|
params
|
|
);
|
|
const [rows] = await this.pool.execute<OrderListRow[]>(
|
|
`SELECT DISTINCT o.id, o.order_no AS orderNo, o.store_id AS storeId, s.name AS storeName,
|
|
o.room_id AS roomId, r.name AS roomName, r.room_no AS roomNo,
|
|
o.status, o.start_at AS startAt, o.end_at AS endAt,
|
|
o.total_amount_cents AS totalAmountCents,
|
|
o.paid_amount_cents AS paidAmountCents, p.id AS latestPaymentId,
|
|
p.provider AS latestPaymentProvider, p.status AS latestPaymentStatus,
|
|
o.created_at AS createdAt
|
|
FROM qipai_orders o
|
|
INNER JOIN qipai_stores s ON s.tenant_id = o.tenant_id AND s.id = o.store_id
|
|
INNER JOIN qipai_rooms r ON r.tenant_id = o.tenant_id AND r.id = o.room_id
|
|
LEFT JOIN qipai_order_user_access a
|
|
ON a.tenant_id = o.tenant_id AND a.order_id = o.id AND a.revoked_at IS NULL
|
|
LEFT JOIN qipai_payments p
|
|
ON p.tenant_id = o.tenant_id AND p.order_id = o.id
|
|
AND p.id = (
|
|
SELECT MAX(p2.id) FROM qipai_payments p2
|
|
WHERE p2.tenant_id = o.tenant_id AND p2.order_id = o.id
|
|
AND p2.deleted_at IS NULL
|
|
)
|
|
WHERE ${whereSql}
|
|
ORDER BY o.created_at DESC, o.id DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[...params, input.pageSize, offset]
|
|
);
|
|
return {
|
|
items: rows.map(publicOrder),
|
|
total: Number(counts[0]?.total ?? 0),
|
|
page: input.page,
|
|
pageSize: input.pageSize
|
|
};
|
|
}
|
|
|
|
async getMine(input: OrderQueryAccess & { orderId: string }) {
|
|
const result = await this.listMine({ ...input, page: 1, pageSize: 1 });
|
|
const order = result.items.find((item) => item.id === input.orderId);
|
|
if (order) return order;
|
|
const [rows] = await this.pool.execute<OrderListRow[]>(
|
|
`SELECT o.id, o.order_no AS orderNo, o.store_id AS storeId, s.name AS storeName,
|
|
o.room_id AS roomId, r.name AS roomName, r.room_no AS roomNo,
|
|
o.status, o.start_at AS startAt, o.end_at AS endAt,
|
|
o.total_amount_cents AS totalAmountCents,
|
|
o.paid_amount_cents AS paidAmountCents, p.id AS latestPaymentId,
|
|
p.provider AS latestPaymentProvider, p.status AS latestPaymentStatus,
|
|
o.created_at AS createdAt
|
|
FROM qipai_orders o
|
|
INNER JOIN qipai_stores s ON s.tenant_id = o.tenant_id AND s.id = o.store_id
|
|
INNER JOIN qipai_rooms r ON r.tenant_id = o.tenant_id AND r.id = o.room_id
|
|
LEFT JOIN qipai_order_user_access a
|
|
ON a.tenant_id = o.tenant_id AND a.order_id = o.id AND a.revoked_at IS NULL
|
|
LEFT JOIN qipai_payments p
|
|
ON p.tenant_id = o.tenant_id AND p.order_id = o.id
|
|
AND p.id = (
|
|
SELECT MAX(p2.id) FROM qipai_payments p2
|
|
WHERE p2.tenant_id = o.tenant_id AND p2.order_id = o.id
|
|
AND p2.deleted_at IS NULL
|
|
)
|
|
WHERE o.tenant_id = ? AND o.id = ? AND o.deleted_at IS NULL
|
|
AND (a.user_id = ? OR ${managerScopeSql(input.access, 'o.store_id')})
|
|
LIMIT 1`,
|
|
[input.tenantId, input.orderId, input.userId]
|
|
);
|
|
if (!rows[0]) throw new OrderQueryError('ORDER_NOT_FOUND');
|
|
return publicOrder(rows[0]);
|
|
}
|
|
}
|
|
|
|
function publicOrder(row: OrderListRow) {
|
|
return {
|
|
id: String(row.id),
|
|
orderNo: row.orderNo,
|
|
storeId: String(row.storeId),
|
|
storeName: row.storeName,
|
|
roomId: String(row.roomId),
|
|
roomName: row.roomName,
|
|
roomNo: row.roomNo,
|
|
status: row.status,
|
|
startAt: row.startAt,
|
|
endAt: row.endAt,
|
|
totalAmountCents: row.totalAmountCents,
|
|
paidAmountCents: row.paidAmountCents,
|
|
latestPayment: row.latestPaymentId === null ? null : {
|
|
id: String(row.latestPaymentId),
|
|
provider: row.latestPaymentProvider,
|
|
status: row.latestPaymentStatus
|
|
},
|
|
createdAt: row.createdAt
|
|
};
|
|
}
|
|
|
|
function managerScopeSql(access: AccessProfile, storeExpression: string) {
|
|
if (access.capabilities.includes('tenant.manage') || access.roles.includes('PLATFORM_ADMIN')) {
|
|
return '1 = 1';
|
|
}
|
|
if (!access.capabilities.includes('store.operation.read') || access.storeIds.length === 0) {
|
|
return '1 = 0';
|
|
}
|
|
return `${storeExpression} IN (${access.storeIds.map((id) => Number(id)).join(',')})`;
|
|
}
|