feat(M08-C): 补管理员订单处置与代下单

This commit is contained in:
Codex
2026-08-10 10:20:32 +08:00
parent 8b2156cdd5
commit 566e61b5d9
20 changed files with 701 additions and 96 deletions
@@ -41,6 +41,7 @@ export class OrderQueryRepository {
page: number;
pageSize: number;
status?: OrderStatus;
storeId?: string;
}) {
const where = [
'o.tenant_id = ?',
@@ -52,6 +53,10 @@ export class OrderQueryRepository {
where.push('o.status = ?');
params.push(input.status);
}
if (input.storeId) {
where.push('o.store_id = ?');
params.push(input.storeId);
}
const whereSql = where.join(' AND ');
const offset = (input.page - 1) * input.pageSize;
const [counts] = await this.pool.execute<CountRow[]>(
+37 -2
View File
@@ -45,6 +45,7 @@ interface RoomPricingRow extends RowDataPacket {
roomCategoryId: string | null;
}
interface CountRow extends RowDataPacket { total: number }
interface UserRow extends RowDataPacket { id: string }
export class PricingError extends Error {
constructor(public readonly code: string) { super(code); }
@@ -69,8 +70,16 @@ export class PricingRepository {
holdMinutes?: number;
allowedStoreIds?: string[] | null;
benefits?: OrderBenefitInput | null;
actor?: {
userId: string;
source: 'APP' | 'ADMIN';
traceId: string;
ip: string;
userAgent: string;
};
}) {
return this.transaction(async (connection) => {
await this.assertActiveTenantUser(connection, input.tenantId, input.userId);
const room = await this.loadRoom(connection, input.tenantId, input.roomId, true);
if (input.allowedStoreIds && !input.allowedStoreIds.includes(String(room.storeId))) {
throw new PricingError('STORE_SCOPE_FORBIDDEN');
@@ -158,9 +167,24 @@ export class PricingRepository {
(tenant_id, order_id, from_status, to_status, action, actor_type,
actor_id, source, reason, trace_id, metadata)
VALUES (?, ?, NULL, 'PENDING_PAYMENT', 'CREATED', 'USER', ?,
'APP', 'Room hold created', ?, JSON_OBJECT('statusVersion', 1))`,
[input.tenantId, orderId, input.userId, `order-created-${orderId}`]
?, ?, ?, JSON_OBJECT('statusVersion', 1, 'targetUserId', ?))`,
[input.tenantId, orderId, input.actor?.userId ?? input.userId,
input.actor?.source ?? 'APP',
input.actor?.source === 'ADMIN' ? 'Order created on behalf' : 'Room hold created',
input.actor?.traceId ?? `order-created-${orderId}`, input.userId]
);
if (input.actor?.source === 'ADMIN') {
await connection.execute(
`INSERT INTO qipai_audit_logs
(tenant_id, actor_type, actor_id, action, resource_type, resource_id,
trace_id, ip, user_agent, metadata)
VALUES (?, 'USER', ?, 'ORDER_CREATED_ON_BEHALF', 'ORDER', ?, ?, ?, ?,
JSON_OBJECT('targetUserId', ?, 'storeId', ?, 'roomId', ?))`,
[input.tenantId, input.actor.userId, orderId, input.actor.traceId,
input.actor.ip, input.actor.userAgent.slice(0, 255), input.userId,
String(room.storeId), input.roomId]
);
}
return {
orderId,
orderNo,
@@ -173,6 +197,17 @@ export class PricingRepository {
});
}
private async assertActiveTenantUser(
connection: PoolConnection, tenantId: string, userId: string
) {
const [rows] = await connection.execute<UserRow[]>(
`SELECT id FROM qipai_users
WHERE tenant_id = ? AND id = ? AND status = 'ACTIVE' LIMIT 1`,
[tenantId, userId]
);
if (!rows[0]) throw new PricingError('USER_NOT_FOUND');
}
async releaseExpired(
tenantId?: string,
roomId?: string,