feat(M03-B): 完成装修广告与媒体管理

This commit is contained in:
Codex
2026-06-18 15:06:16 +08:00
parent 7e9b53487b
commit f74624296d
19 changed files with 1241 additions and 13 deletions
@@ -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 {