import { getMetadataArgsStorage } from 'typeorm'; import { BaseEntity, transformerTime } from '../../../src/comm/entity/base'; import { CardInfoEntity } from '../../../src/modules/card/entity/info'; import { CardProfile, CardStatus } from '../../../src/comm/types/omni'; describe('后端统一实体基类与名片实体 (Entity & BaseEntity)', () => { const metadata = getMetadataArgsStorage(); const matchEntity = (target: any) => target === BaseEntity || target === CardInfoEntity || (target && target.prototype instanceof BaseEntity); describe('1. 通用实体基类 (comm/entity/base.ts)', () => { it('BaseEntity 应定义标准字段: id, tenantId, createTime, updateTime, deletedAt', () => { const columns = metadata.columns.filter(c => matchEntity(c.target)); const columnNames = columns.map(c => c.propertyName); expect(columnNames).toContain('id'); expect(columnNames).toContain('tenantId'); expect(columnNames).toContain('createTime'); expect(columnNames).toContain('updateTime'); expect(columnNames).toContain('deletedAt'); }); it('基类字段的数据库列名应严格遵循下划线命名规范', () => { const columns = metadata.columns.filter(c => matchEntity(c.target)); const tenantIdCol = columns.find(c => c.propertyName === 'tenantId'); expect(tenantIdCol?.options.name).toBe('tenant_id'); const createTimeCol = columns.find(c => c.propertyName === 'createTime'); expect(createTimeCol?.options.name).toBe('create_time'); const updateTimeCol = columns.find(c => c.propertyName === 'updateTime'); expect(updateTimeCol?.options.name).toBe('update_time'); const deletedAtCol = columns.find(c => c.propertyName === 'deletedAt'); expect(deletedAtCol?.options.name).toBe('deleted_at'); }); it('deletedAt 必须被标记为软删除列或具备软删除特性', () => { const deleteDateCols = metadata.columns.filter( c => matchEntity(c.target) && c.propertyName === 'deletedAt' ); expect(deleteDateCols.length).toBeGreaterThan(0); expect(deleteDateCols[0].mode).toBe('deleteDate'); }); it('基类索引应正确声明在 tenantId 上以保障多租户隔离查询性能', () => { const indexes = metadata.indices.filter(i => matchEntity(i.target)); const tenantIndex = indexes.find(i => { if (Array.isArray(i.columns)) { return i.columns.includes('tenantId') || i.columns.includes('tenant_id'); } if (typeof i.columns === 'function') { return (i.columns as any)({ tenantId: true }); } return false; }); expect(tenantIndex).toBeDefined(); }); it('transformerTime 应保证时间格式化正确', () => { const formatted = transformerTime.to('2026-09-20 12:00:00'); expect(formatted).toBe('2026-09-20 12:00:00'); const currentFormatted = transformerTime.to(undefined); expect(currentFormatted).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); }); }); describe('2. 名片实体 (modules/card/entity/info.ts)', () => { it('实体表名必须为 card_info', () => { const table = metadata.tables.find(t => t.target === CardInfoEntity); expect(table).toBeDefined(); expect(table?.name).toBe('card_info'); }); it('应完整包含 CardProfile 契约与扩展字段,且全部使用下划线数据库列名', () => { const columns = metadata.columns.filter(c => c.target === CardInfoEntity); const expectedColumnMappings: Record = { userId: 'user_id', name: 'name', title: 'title', company: 'company', slogan: 'slogan', avatarUrl: 'avatar_url', phone: 'phone', email: 'email', whatsapp: 'whatsapp', linkedin: 'linkedin', telegram: 'telegram', twitter: 'twitter', wechat: 'wechat', timezone: 'timezone', audioPronunciationUrl: 'audio_pronunciation_url', status: 'status', themeCode: 'theme_code', slug: 'slug', }; for (const [prop, dbCol] of Object.entries(expectedColumnMappings)) { const col = columns.find(c => c.propertyName === prop); expect(col).toBeDefined(); expect(col?.options.name || col?.propertyName).toBe(dbCol); } }); it('slug 字段必须设置唯一索引 (unique: true) 用于短链与 NFC 极速路由', () => { const indexes = metadata.indices.filter(i => i.target === CardInfoEntity); const slugIndex = indexes.find(i => { const matchProp = Array.isArray(i.columns) && (i.columns.includes('slug') || (i.columns as any[])[0] === 'slug'); return matchProp && i.unique === true; }); expect(slugIndex).toBeDefined(); expect(slugIndex?.unique).toBe(true); }); it('userId 必须设置索引以优化个人名片列表查询', () => { const indexes = metadata.indices.filter(i => i.target === CardInfoEntity); const userIndex = indexes.find(i => { if (Array.isArray(i.columns)) { return i.columns.includes('userId') || i.columns.includes('user_id'); } return false; }); expect(userIndex).toBeDefined(); }); it('默认值设置应合理且符合规范 (status=\'draft\', themeCode=\'corporate_navy\', timezone=\'UTC+8\')', () => { const columns = metadata.columns.filter(c => c.target === CardInfoEntity); const statusCol = columns.find(c => c.propertyName === 'status'); expect(statusCol?.options.default).toBe('draft'); const themeCol = columns.find(c => c.propertyName === 'themeCode'); expect(themeCol?.options.default).toBe('corporate_navy'); const tzCol = columns.find(c => c.propertyName === 'timezone'); expect(tzCol?.options.default).toBe('UTC+8'); }); it('CardInfoEntity 实例应能赋值并完全满足 CardProfile 契约接口', () => { const card = new CardInfoEntity(); card.id = 1; card.tenantId = 100; card.userId = 10; card.name = 'Alex Zhang'; card.title = 'Global BD Director'; card.company = 'OmniCard Global Ltd.'; card.slogan = 'Connecting Global Business Seamlessly'; card.avatarUrl = 'https://cdn.omnicard.ai/avatar/alex.jpg'; card.phone = '+86 13800138000'; card.email = 'alex@omnicard.ai'; card.whatsapp = '+8613800138000'; card.linkedin = 'https://linkedin.com/in/alex-zhang'; card.telegram = '@alex_zhang'; card.twitter = 'https://x.com/alexzhang'; card.wechat = 'alex_wx'; card.timezone = 'UTC+8'; card.audioPronunciationUrl = 'https://cdn.omnicard.ai/audio/alex.mp3'; card.status = 'published'; card.themeCode = 'tech_dark'; card.slug = 'alex-zhang-bd'; // 类型断言与赋值检查 const profile: CardProfile = card; expect(profile.name).toBe('Alex Zhang'); expect(profile.status).toBe('published'); expect(profile.themeCode).toBe('tech_dark'); expect(profile.telegram).toBe('@alex_zhang'); expect(profile.twitter).toBe('https://x.com/alexzhang'); expect(profile.wechat).toBe('alex_wx'); expect(card.slug).toBe('alex-zhang-bd'); }); }); });