feat(M03-A): 完成门店与房间基础管理
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
import { AuthRepository } from '../dist/auth/auth-repository.js';
|
||||
import { RbacRepository } from '../dist/auth/rbac-repository.js';
|
||||
import { UserManagementRepository } from '../dist/auth/user-management-repository.js';
|
||||
import { StoreRoomRepository, StoreRoomError } from '../dist/stores/store-room-repository.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -33,8 +34,11 @@ const expectedTables = [
|
||||
'qipai_platform_apps',
|
||||
'qipai_role_permissions',
|
||||
'qipai_roles',
|
||||
'qipai_room_categories',
|
||||
'qipai_room_disabled_periods',
|
||||
'qipai_rooms',
|
||||
'qipai_schema_migrations',
|
||||
'qipai_store_business_hours',
|
||||
'qipai_stores',
|
||||
'qipai_tenant_apps',
|
||||
'qipai_tenant_configs',
|
||||
@@ -64,9 +68,10 @@ async function readMigrationVersions(pool) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT version, name
|
||||
FROM qipai_schema_migrations
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?)
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?)
|
||||
ORDER BY version`,
|
||||
['2026061601', '2026061802', '2026061803', '2026061804', '2026061805', '2026061806']
|
||||
['2026061601', '2026061802', '2026061803', '2026061804',
|
||||
'2026061805', '2026061806', '2026061807']
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
@@ -315,6 +320,75 @@ async function assertUserManagement(pool, context) {
|
||||
assert.deepEqual(auditRows.map((row) => row.action), ['STAFF_CREATED', 'USER_UPDATED']);
|
||||
}
|
||||
|
||||
async function assertStoreRoomDomain(pool, context) {
|
||||
const [adminRows] = await pool.query(
|
||||
`SELECT u.id FROM qipai_users u
|
||||
INNER JOIN qipai_user_roles ur ON ur.tenant_id = u.tenant_id AND ur.user_id = u.id
|
||||
INNER JOIN qipai_roles r ON r.id = ur.role_id AND r.tenant_id = ur.tenant_id
|
||||
WHERE u.tenant_id = ? AND r.code = 'TENANT_ADMIN' LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const adminId = String(adminRows[0].id);
|
||||
const rbac = new RbacRepository(pool);
|
||||
const access = await rbac.getAccessProfile(context.tenantId, adminId);
|
||||
const repository = new StoreRoomRepository(pool);
|
||||
const actor = {
|
||||
tenantId: context.tenantId, userId: adminId, access,
|
||||
traceId: 'm03a-live-test', ip: '127.0.0.1', userAgent: 'M03-A live test'
|
||||
};
|
||||
const store = await repository.createStore(actor, {
|
||||
name: 'M03A Store', address: 'Sanitized address',
|
||||
longitude: 121.4737, latitude: 31.2304, contactPhone: '13800000003',
|
||||
timezone: 'Asia/Shanghai', businessStatus: 'OPEN',
|
||||
wifiSsid: 'M03A-WIFI', wifiPassword: 'sanitized-password',
|
||||
notificationUrl: '', sortOrder: 1,
|
||||
businessHours: [
|
||||
{ weekday: 1, openMinute: 600, closeMinute: 1320, isClosed: false },
|
||||
{ weekday: 2, openMinute: 600, closeMinute: 1320, isClosed: false }
|
||||
]
|
||||
});
|
||||
const roomInput = {
|
||||
storeId: store.storeId, categoryName: '标准间', name: 'M03A Room', roomNo: 'A01',
|
||||
capacity: 4, basePriceCents: 3000, weekdayPriceCents: 2800,
|
||||
holidayPriceCents: 3500, overnightPriceCents: 12000, depositCents: 5000,
|
||||
minimumMinutes: 60, maxAdvanceStartMinutes: 30, maxAdvanceDays: 30,
|
||||
configurationStatus: 'ENABLED', operationalStatus: 'AVAILABLE',
|
||||
tags: ['麻将', '禁烟'], images: ['https://api.txyundm.cn/uploads/test-room.jpg'],
|
||||
sortOrder: 1
|
||||
};
|
||||
const room = await repository.createRoom(actor, roomInput);
|
||||
await repository.addDisabledPeriod(actor, room.roomId, {
|
||||
storeId: store.storeId,
|
||||
startsAt: new Date('2026-06-20T02:00:00.000Z'),
|
||||
endsAt: new Date('2026-06-20T04:00:00.000Z'),
|
||||
reason: 'maintenance rehearsal'
|
||||
});
|
||||
const stores = await repository.listStores(actor);
|
||||
const rooms = await repository.listRooms(actor, store.storeId);
|
||||
assert.equal(stores.find((item) => item.id === store.storeId)?.timezone, 'Asia/Shanghai');
|
||||
assert.equal(rooms.find((item) => item.id === room.roomId)?.holidayPriceCents, 3500);
|
||||
assert.deepEqual(rooms.find((item) => item.id === room.roomId)?.tags, ['麻将', '禁烟']);
|
||||
await assert.rejects(
|
||||
() => repository.listRooms({
|
||||
...actor,
|
||||
access: {
|
||||
roles: ['STORE_ADMIN'],
|
||||
capabilities: ['store.operation.read', 'store.operation.write'],
|
||||
storeIds: [String(Number(store.storeId) + 999)]
|
||||
}
|
||||
}, store.storeId),
|
||||
(error) => error instanceof StoreRoomError && error.code === 'STORE_SCOPE_FORBIDDEN'
|
||||
);
|
||||
const [auditRows] = await pool.query(
|
||||
`SELECT action FROM qipai_audit_logs
|
||||
WHERE tenant_id = ? AND trace_id = 'm03a-live-test' ORDER BY id`,
|
||||
[context.tenantId]
|
||||
);
|
||||
assert.deepEqual(auditRows.map((row) => row.action), [
|
||||
'STORE_CREATED', 'ROOM_CREATED', 'ROOM_DISABLED_PERIOD_CREATED'
|
||||
]);
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
assert.equal(config.mysql.passwordConfigured, true, 'Live migration test requires a temporary password.');
|
||||
assert.match(
|
||||
@@ -344,12 +418,14 @@ try {
|
||||
{ version: '2026061803', name: 'm02a_tenant_apps' },
|
||||
{ version: '2026061804', name: 'm02b_wechat_auth' },
|
||||
{ version: '2026061805', name: 'm02c_rbac' },
|
||||
{ version: '2026061806', name: 'm02d_user_management' }
|
||||
{ version: '2026061806', name: 'm02d_user_management' },
|
||||
{ version: '2026061807', name: 'm03a_store_room_domain' }
|
||||
]);
|
||||
await assertTaskDurability(pool);
|
||||
const loginContext = await assertPlatformTenantIsolation(pool);
|
||||
await assertRevocableAuthSession(pool, loginContext);
|
||||
await assertUserManagement(pool, loginContext);
|
||||
await assertStoreRoomDomain(pool, loginContext);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: first up, verify, tenant isolation and revocable auth checks completed.');
|
||||
|
||||
@@ -367,7 +443,8 @@ try {
|
||||
{ version: '2026061803', name: 'm02a_tenant_apps' },
|
||||
{ version: '2026061804', name: 'm02b_wechat_auth' },
|
||||
{ version: '2026061805', name: 'm02c_rbac' },
|
||||
{ version: '2026061806', name: 'm02d_user_management' }
|
||||
{ version: '2026061806', name: 'm02d_user_management' },
|
||||
{ version: '2026061807', name: 'm03a_store_room_domain' }
|
||||
]);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: second up and verify restored the schema.');
|
||||
@@ -403,7 +480,12 @@ try {
|
||||
'cross-tenant store grant rejection',
|
||||
'staff creation and store assignment',
|
||||
'access-change session revocation',
|
||||
'user-management audit log'
|
||||
'user-management audit log',
|
||||
'store business hours and coordinates',
|
||||
'room category and integer-cent pricing',
|
||||
'configuration and operational status separation',
|
||||
'room disabled period',
|
||||
'cross-store management rejection'
|
||||
]
|
||||
}, null, 2));
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user