feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口

补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
wikiboss
2026-09-23 18:01:47 +08:00
父节点 8ea9c25386
当前提交 62e0ae4435
共有 227 个文件被更改,包括 21855 次插入 和 5264 次删除

查看文件

@@ -82,4 +82,171 @@ describe('【财务与运营新账统计】TeamFinanceAnalyticsService 单元测
expect(response.code).toBe(1000);
expect(response.data.summary.mrr).toBeDefined();
});
it('6. 无发票/无订阅可归属时 regionalRevenue 与 recentLedger 必须为空数组,不得填演示数据', async () => {
service.teamInvoiceEntity = {
find: async () => [],
} as unknown as TeamFinanceAnalyticsService['teamInvoiceEntity'];
service.teamSubscriptionEntity = {
find: async () => [],
} as unknown as TeamFinanceAnalyticsService['teamSubscriptionEntity'];
const result = await service.getOverviewAnalytics({ period: 'all' });
expect(result.regionalRevenue).toEqual([]);
expect(result.recentLedger).toEqual([]);
const ledgerDump = JSON.stringify(result.recentLedger);
expect(ledgerDump).not.toMatch(/Vanguard|Dubai Apex|Berlin Precision|Silicon Horizon|Nordic Clean/i);
const regionPercents = result.regionalRevenue.map((r) => r.percentage);
expect(regionPercents).not.toEqual([46, 28, 14, 12]);
});
it('7. 区域占比必须按已有发票币种归属真实汇总,百分比来自实际收款占比而非固定比例', async () => {
const invoices: Partial<TeamInvoiceEntity>[] = [
{
id: 11,
invoiceNo: 'INV-20260921-0001',
currency: 'USD',
totalAmount: 1000,
taxAmount: 0,
status: 'paid',
title: 'Pro Plan',
billingInfo: { companyName: 'Acme North LLC' },
tenantId: 101,
createTime: new Date('2026-09-10T10:00:00Z') as unknown as Date,
},
{
id: 12,
invoiceNo: 'INV-20260921-0002',
currency: 'EUR',
totalAmount: 500,
taxAmount: 0,
status: 'paid',
title: 'Enterprise Plan',
billingInfo: { companyName: 'Rhine Tools GmbH' },
tenantId: 102,
createTime: new Date('2026-09-11T10:00:00Z') as unknown as Date,
},
{
id: 13,
invoiceNo: 'INV-20260921-0003',
currency: 'AED',
totalAmount: 500,
taxAmount: 0,
status: 'paid',
title: 'Pro Plan',
billingInfo: { companyName: 'Gulf Bridge Trading' },
tenantId: 103,
createTime: new Date('2026-09-12T10:00:00Z') as unknown as Date,
},
];
service.teamInvoiceEntity = {
find: async () => invoices as TeamInvoiceEntity[],
} as unknown as TeamFinanceAnalyticsService['teamInvoiceEntity'];
service.teamSubscriptionEntity = {
find: async () => [],
} as unknown as TeamFinanceAnalyticsService['teamSubscriptionEntity'];
const result = await service.getOverviewAnalytics({ period: 'all' });
expect(result.regionalRevenue.length).toBeGreaterThanOrEqual(1);
const totalPct = result.regionalRevenue.reduce((s, r) => s + r.percentage, 0);
expect(totalPct).toBeCloseTo(100, 0);
const byRegion = Object.fromEntries(
result.regionalRevenue.map((r) => [r.region, r])
);
// USD 1000 / 2000 = 50%, EUR 500 = 25%, AED 500 = 25%
const usdRegion = Object.values(byRegion).find((r) => r.revenue === 1000);
const eurRegion = Object.values(byRegion).find((r) => r.revenue === 500 && r.percentage === 25);
expect(usdRegion?.percentage).toBe(50);
expect(eurRegion).toBeDefined();
const fixedDemo = result.regionalRevenue.map((r) => r.percentage);
expect(fixedDemo).not.toEqual([46, 28, 14, 12]);
});
it('8. recentLedger 只能映射已有发票字段,租户名取自 billingInfo,不得出现演示租户', async () => {
const invoices: Partial<TeamInvoiceEntity>[] = [
{
id: 21,
invoiceNo: 'INV-20260920-0100',
currency: 'USD',
totalAmount: 196,
taxAmount: 0,
status: 'paid',
title: 'Pro Seat Addon',
billingInfo: { companyName: 'Orbit Studio Co.' },
tenantId: 201,
createTime: new Date('2026-09-20T08:00:00Z') as unknown as Date,
},
{
id: 22,
invoiceNo: 'INV-20260919-0099',
currency: 'EUR',
totalAmount: 588,
taxAmount: 0,
status: 'issued',
title: 'Enterprise Renewal',
billingInfo: { companyName: 'Nordic Labs AB' },
tenantId: 202,
createTime: new Date('2026-09-19T08:00:00Z') as unknown as Date,
},
];
service.teamInvoiceEntity = {
find: async () => invoices as TeamInvoiceEntity[],
} as unknown as TeamFinanceAnalyticsService['teamInvoiceEntity'];
service.teamSubscriptionEntity = {
find: async () => [],
} as unknown as TeamFinanceAnalyticsService['teamSubscriptionEntity'];
const result = await service.getOverviewAnalytics({ period: 'all' });
expect(result.recentLedger.length).toBe(2);
expect(result.recentLedger.map((l) => l.invoiceNo)).toEqual([
'INV-20260920-0100',
'INV-20260919-0099',
]);
expect(result.recentLedger[0].tenantName).toBe('Orbit Studio Co.');
expect(result.recentLedger[1].tenantName).toBe('Nordic Labs AB');
expect(result.recentLedger[0].amount).toBe(196);
expect(result.recentLedger[1].status).toBe('issued');
const names = result.recentLedger.map((l) => l.tenantName).join(' ');
expect(names).not.toMatch(/Vanguard|Dubai Apex|Berlin Precision|Silicon Horizon/i);
});
it('9. 仅有未付款发票时区域营收为空,流水仍可来自真实发票', async () => {
const invoices: Partial<TeamInvoiceEntity>[] = [
{
id: 31,
invoiceNo: 'INV-20260918-0001',
currency: 'USD',
totalAmount: 299,
taxAmount: 0,
status: 'issued',
title: 'Pro Plan',
billingInfo: { companyName: 'Pending Buyer Inc.' },
tenantId: 301,
createTime: new Date('2026-09-18T08:00:00Z') as unknown as Date,
},
];
service.teamInvoiceEntity = {
find: async () => invoices as TeamInvoiceEntity[],
} as unknown as TeamFinanceAnalyticsService['teamInvoiceEntity'];
service.teamSubscriptionEntity = {
find: async () => [],
} as unknown as TeamFinanceAnalyticsService['teamSubscriptionEntity'];
const result = await service.getOverviewAnalytics({ period: 'all' });
expect(result.regionalRevenue).toEqual([]);
expect(result.recentLedger).toHaveLength(1);
expect(result.recentLedger[0].tenantName).toBe('Pending Buyer Inc.');
expect(result.recentLedger[0].invoiceNo).toBe('INV-20260918-0001');
});
});