feat(M03-B): 完成装修广告与媒体管理
This commit is contained in:
@@ -14,6 +14,7 @@ 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 { ContentRepository, ContentError } from '../dist/content/content-repository.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -21,11 +22,13 @@ import {
|
||||
} from '../dist/db/migration-runner.js';
|
||||
|
||||
const expectedTables = [
|
||||
'qipai_advertisements',
|
||||
'qipai_async_tasks',
|
||||
'qipai_audit_logs',
|
||||
'qipai_auth_sessions',
|
||||
'qipai_devices',
|
||||
'qipai_legacy_table_mappings',
|
||||
'qipai_media_assets',
|
||||
'qipai_members',
|
||||
'qipai_orders',
|
||||
'qipai_outbox_events',
|
||||
@@ -39,6 +42,7 @@ const expectedTables = [
|
||||
'qipai_rooms',
|
||||
'qipai_schema_migrations',
|
||||
'qipai_store_business_hours',
|
||||
'qipai_store_decorations',
|
||||
'qipai_stores',
|
||||
'qipai_tenant_apps',
|
||||
'qipai_tenant_configs',
|
||||
@@ -68,10 +72,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', '2026061807']
|
||||
'2026061805', '2026061806', '2026061807', '2026061808']
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
@@ -389,6 +393,70 @@ async function assertStoreRoomDomain(pool, context) {
|
||||
]);
|
||||
}
|
||||
|
||||
async function assertContentManagement(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 [storeRows] = await pool.query(
|
||||
`SELECT id FROM qipai_stores WHERE tenant_id = ? AND name = 'M03A Store' LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const adminId = String(adminRows[0].id);
|
||||
const storeId = String(storeRows[0].id);
|
||||
const rbac = new RbacRepository(pool);
|
||||
const access = await rbac.getAccessProfile(context.tenantId, adminId);
|
||||
const actor = {
|
||||
tenantId: context.tenantId, userId: adminId, access,
|
||||
traceId: 'm03b-live-test', ip: '127.0.0.1', userAgent: 'M03-B live test'
|
||||
};
|
||||
const repository = new ContentRepository(pool);
|
||||
const asset = await repository.registerAsset(actor, storeId, {
|
||||
storagePath: `tenants/${context.tenantId}/stores/${storeId}/sanitized.webp`,
|
||||
publicUrl: `https://api.txyundm.cn/uploads/tenants/${context.tenantId}/stores/${storeId}/sanitized.webp`,
|
||||
mimeType: 'image/webp', byteSize: 1024, width: 1200, height: 600,
|
||||
checksumSha256: 'a'.repeat(64)
|
||||
});
|
||||
const draft1 = await repository.saveDecoration(actor, {
|
||||
storeId, templateCode: 'classic', schemaVersion: 1,
|
||||
content: { components: [{ type: 'HERO', props: { assetId: asset.assetId } }] }
|
||||
});
|
||||
const draft2 = await repository.saveDecoration(actor, {
|
||||
storeId, templateCode: 'modern', schemaVersion: 1,
|
||||
content: { components: [{ type: 'ROOM_LIST', props: {} }] }
|
||||
});
|
||||
assert.equal(draft1.version, 1);
|
||||
assert.equal(draft2.version, 2);
|
||||
await repository.publishDecoration(actor, draft1.decorationId, storeId);
|
||||
await repository.publishDecoration(actor, draft2.decorationId, storeId);
|
||||
const [decorationRows] = await pool.query(
|
||||
`SELECT version, status FROM qipai_store_decorations
|
||||
WHERE tenant_id = ? AND store_id = ? ORDER BY version`,
|
||||
[context.tenantId, storeId]
|
||||
);
|
||||
assert.deepEqual(decorationRows, [
|
||||
{ version: 1, status: 'ARCHIVED' },
|
||||
{ version: 2, status: 'PUBLISHED' }
|
||||
]);
|
||||
const ad = await repository.saveAdvertisement(actor, {
|
||||
scopeType: 'STORE', storeId, title: 'Store banner', imageAssetId: asset.assetId,
|
||||
targetType: 'PAGE', targetValue: '/pages/index/index',
|
||||
startsAt: null, endsAt: null, status: 'ACTIVE', sortOrder: 1
|
||||
});
|
||||
assert.match(ad.advertisementId, /^[1-9]\d*$/);
|
||||
assert.equal((await repository.listAdvertisements(actor))[0].scopeType, 'STORE');
|
||||
await assert.rejects(
|
||||
() => repository.saveAdvertisement(actor, {
|
||||
scopeType: 'PLATFORM', title: 'forbidden', imageAssetId: asset.assetId,
|
||||
targetType: 'NONE', targetValue: '', status: 'DRAFT', sortOrder: 0
|
||||
}),
|
||||
(error) => error instanceof ContentError && error.code === 'PLATFORM_AD_FORBIDDEN'
|
||||
);
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
assert.equal(config.mysql.passwordConfigured, true, 'Live migration test requires a temporary password.');
|
||||
assert.match(
|
||||
@@ -419,13 +487,15 @@ try {
|
||||
{ version: '2026061804', name: 'm02b_wechat_auth' },
|
||||
{ version: '2026061805', name: 'm02c_rbac' },
|
||||
{ version: '2026061806', name: 'm02d_user_management' },
|
||||
{ version: '2026061807', name: 'm03a_store_room_domain' }
|
||||
{ version: '2026061807', name: 'm03a_store_room_domain' },
|
||||
{ version: '2026061808', name: 'm03b_decoration_ads_media' }
|
||||
]);
|
||||
await assertTaskDurability(pool);
|
||||
const loginContext = await assertPlatformTenantIsolation(pool);
|
||||
await assertRevocableAuthSession(pool, loginContext);
|
||||
await assertUserManagement(pool, loginContext);
|
||||
await assertStoreRoomDomain(pool, loginContext);
|
||||
await assertContentManagement(pool, loginContext);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: first up, verify, tenant isolation and revocable auth checks completed.');
|
||||
|
||||
@@ -444,7 +514,8 @@ try {
|
||||
{ version: '2026061804', name: 'm02b_wechat_auth' },
|
||||
{ version: '2026061805', name: 'm02c_rbac' },
|
||||
{ version: '2026061806', name: 'm02d_user_management' },
|
||||
{ version: '2026061807', name: 'm03a_store_room_domain' }
|
||||
{ version: '2026061807', name: 'm03a_store_room_domain' },
|
||||
{ version: '2026061808', name: 'm03b_decoration_ads_media' }
|
||||
]);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: second up and verify restored the schema.');
|
||||
@@ -485,7 +556,11 @@ try {
|
||||
'room category and integer-cent pricing',
|
||||
'configuration and operational status separation',
|
||||
'room disabled period',
|
||||
'cross-store management rejection'
|
||||
'cross-store management rejection',
|
||||
'tenant-isolated media asset',
|
||||
'versioned decoration publish and archive',
|
||||
'store advertisement delivery scope',
|
||||
'platform advertisement rejection'
|
||||
]
|
||||
}, null, 2));
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user