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 次删除

查看文件

@@ -361,6 +361,136 @@ describe('Point 24: 国际多币种发票生成与历史收据导出 单元测
});
});
// ==========================================
// 6b. 发票下载真实文件内容 (P38)
// ==========================================
describe('6b. 发票下载真实文件内容 (P38)', () => {
let service: TeamInvoiceService;
let invoices: TeamInvoiceEntity[];
let nextId = 1;
beforeEach(() => {
invoices = [];
nextId = 1;
const mockRepo = {
count: jest.fn(async () => invoices.length),
save: jest.fn(async (entity: TeamInvoiceEntity) => {
const saved = Object.assign(new TeamInvoiceEntity(), entity, {
id: nextId++,
createTime: new Date(),
updateTime: new Date(),
});
invoices.push(saved);
return saved;
}),
find: jest.fn(async (options?: { where?: Partial<TeamInvoiceEntity> }) => {
if (!options?.where) return [...invoices];
return invoices.filter(item => {
const itemMap = item as unknown as Record<string, unknown>;
for (const [key, val] of Object.entries(options.where!)) {
if (itemMap[key] !== val) return false;
}
return true;
});
}),
findOne: jest.fn(
async (options?: { where?: Partial<TeamInvoiceEntity> }) => {
if (!options?.where) return null;
const found = invoices.find(item => {
const itemMap = item as unknown as Record<string, unknown>;
for (const [key, val] of Object.entries(options.where!)) {
if (itemMap[key] !== val) return false;
}
return true;
});
return found || null;
}
),
findOneBy: jest.fn(async (where: Partial<TeamInvoiceEntity>) => {
const found = invoices.find(item => {
const itemMap = item as unknown as Record<string, unknown>;
for (const [key, val] of Object.entries(where)) {
if (itemMap[key] !== val) return false;
}
return true;
});
return found || null;
}),
};
service = new TeamInvoiceService();
service.teamInvoiceEntity =
mockRepo as unknown as TeamInvoiceService['teamInvoiceEntity'];
});
it('downloadInvoice 发票不存在时应抛 CoolCommException,不得返回成功 URL', async () => {
await expect(service.downloadInvoice(99999, 101)).rejects.toThrow(
CoolCommException
);
await expect(service.downloadInvoice(99999, 101)).rejects.toThrow(
/不存在|not found/i
);
});
it('downloadInvoice 应返回真实文件内容,且含发票号与金额', async () => {
const created = await service.generateInvoice(101, {
subtotal: 200,
taxRate: 0.1,
currency: 'USD',
title: 'Enterprise Plan',
});
const result = await service.downloadInvoice(created.id, 101);
expect(result).toBeDefined();
expect(result.content).toBeTruthy();
expect(typeof result.content).toBe('string');
expect(result.content.length).toBeGreaterThan(20);
expect(result.content).toContain(created.invoiceNo);
expect(result.content).toMatch(/220\.00|\$220/);
expect(result.filename).toMatch(
new RegExp(`${created.invoiceNo}\\.(txt|pdf)$`)
);
expect(result.contentType).toMatch(/text\/plain|application\/pdf/);
expect(result.invoiceNo).toBe(created.invoiceNo);
// 禁止假 CDN / 空壳外链
expect(result).not.toHaveProperty('url');
expect(result.content).not.toMatch(
/cdn\.omnicard|https?:\/\/.*invoice.*\.pdf/i
);
});
it('downloadInvoice 跨租户或无权访问时应报错,不得返回成功 URL', async () => {
const created = await service.generateInvoice(101, {
subtotal: 50,
currency: 'USD',
title: 'Cross-tenant check',
});
await expect(service.downloadInvoice(created.id, 202)).rejects.toThrow(
CoolCommException
);
});
it('downloadInvoice 在无 Stripe 发票号时不得假装已从 Stripe 拉取 PDF', async () => {
const created = await service.generateInvoice(101, {
subtotal: 88,
currency: 'EUR',
title: 'No Stripe PDF',
});
const result = await service.downloadInvoice(created.id, 101);
expect(result.content).toContain(created.invoiceNo);
expect(result.content).not.toMatch(
/stripe\.com|invoicestripes|invoice_pdf/i
);
expect(JSON.stringify(result)).not.toMatch(
/https:\/\/files\.stripe\.com|invoice_pdf/i
);
});
});
// ==========================================
// 7. 控制器接口调用测试 (AdminTeamBillingController)
// ==========================================
@@ -389,6 +519,14 @@ describe('Point 24: 国际多币种发票生成与历史收据导出 单元测
return [inv];
});
invoiceService.downloadInvoice = jest.fn(async () => ({
content:
'INVOICE / RECEIPT\nInvoice No : INV-20260920-0001\nTOTAL AMOUNT : $110.00\n',
contentType: 'text/plain; charset=utf-8',
filename: 'INV-20260920-0001.txt',
invoiceNo: 'INV-20260920-0001',
}));
controller = new AdminTeamBillingController();
controller.teamInvoiceService = invoiceService;
controller.ok = (data: unknown) => ({
@@ -422,5 +560,32 @@ describe('Point 24: 国际多币种发票生成与历史收据导出 单元测
expect(res.code).toBe(1000);
expect(invoiceService.listInvoices).toHaveBeenCalledWith(888);
});
it('POST /invoice/download 应按 id 下载真实文件内容并委派 service', async () => {
const res = (await controller.downloadInvoice({ id: 1 })) as {
code: number;
message: string;
data: {
content: string;
filename: string;
invoiceNo: string;
contentType: string;
};
};
expect(res.code).toBe(1000);
expect(invoiceService.downloadInvoice).toHaveBeenCalledWith(1, 888);
expect(res.data.content).toContain('INV-20260920-0001');
expect(res.data.content).toMatch(/110\.00|\$110/);
expect(res.data).not.toHaveProperty('url');
});
it('POST /invoice/download 缺少 id 时应抛 CoolCommException', async () => {
await expect(controller.downloadInvoice({})).rejects.toThrow(
CoolCommException
);
await expect(controller.downloadInvoice({ id: 0 })).rejects.toThrow(
CoolCommException
);
});
});
});