feat(M08-A): 接入顾客端订单查询

This commit is contained in:
Codex
2026-06-24 21:42:02 +08:00
parent 47ec7fc30e
commit eea8eea12d
20 changed files with 769 additions and 2 deletions
@@ -0,0 +1,163 @@
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(',')})`;
}