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

查看文件

@@ -221,12 +221,162 @@ describe('Point 11: 国际主流 CRM (HubSpot / Salesforce / Webhook) 对接服
syncService.sendWebhook('', 'secret', mockLead)
).rejects.toThrow(CoolCommException);
});
it.each([
'http://localhost/hook',
'http://127.0.0.1:9000/hook',
'http://10.1.2.3/hook',
'http://172.16.0.1/hook',
'http://172.31.255.1/hook',
'http://192.168.1.10/hook',
'ftp://hooks.example.com/crm',
'file:///etc/passwd',
])('拒绝内网或非 http(s) URL:%s,不发请求且不记成功', async url => {
const postSpy = jest.spyOn(axios, 'post');
await expect(
syncService.sendWebhook(url, 'wh_secret', mockLead, { delayMs: 1 })
).rejects.toThrow(CoolCommException);
expect(postSpy).not.toHaveBeenCalled();
});
it('公网 https URL 仍应发送并附带 X-OmniCard-Signature', async () => {
const postSpy = jest.spyOn(axios, 'post').mockResolvedValueOnce({
status: 200,
data: { ok: true },
});
const secret = 'wh_secret_public';
const url = 'https://hooks.example.com/crm/lead';
const result = await syncService.sendWebhook(url, secret, mockLead, {
delayMs: 1,
});
expect(result.success).toBe(true);
expect(postSpy).toHaveBeenCalledTimes(1);
const [, payload, config] = postSpy.mock.calls[0];
expect(config?.headers?.['X-OmniCard-Signature']).toBe(
syncService.generateSignature(payload, secret)
);
});
});
// ==========================================
// 5. 控制器接口集成 (/admin/crm/lead/sync-external)
// 5. Slack / 飞书:组包 ≠ 已送达 (delivered)
// ==========================================
describe('5. 控制器接口 /admin/crm/lead/sync-external 集成测试', () => {
describe('5. Slack / 飞书组包与送达标记', () => {
it('formatToSlack 仅组包,不发起 HTTP', () => {
const postSpy = jest.spyOn(axios, 'post');
const payload = syncService.formatToSlack(mockLead);
expect(payload).toMatchObject({
text: expect.stringContaining('John Doe'),
});
expect(payload.text).toContain('Acme Global Corp');
expect(postSpy).not.toHaveBeenCalled();
});
it('formatToFeishu 仅组包,不发起 HTTP', () => {
const postSpy = jest.spyOn(axios, 'post');
const payload = syncService.formatToFeishu(mockLead);
expect(payload.msg_type).toBe('text');
expect(payload.content?.text).toContain('John Doe');
expect(payload.content?.text).toContain('Acme Global Corp');
expect(postSpy).not.toHaveBeenCalled();
});
it('sendSlack 无 Webhook URL 时只组包,delivered 必须为 false', async () => {
const postSpy = jest.spyOn(axios, 'post');
const result = await syncService.sendSlack(undefined, mockLead);
expect(result.delivered).toBe(false);
expect(result.channel).toBe('slack');
expect(result.payload).toBeDefined();
expect(result.reason).toBeTruthy();
expect(postSpy).not.toHaveBeenCalled();
});
it('sendFeishu 空 URL 时只组包,delivered 必须为 false', async () => {
const postSpy = jest.spyOn(axios, 'post');
const result = await syncService.sendFeishu('', mockLead);
expect(result.delivered).toBe(false);
expect(result.channel).toBe('feishu');
expect(result.payload).toBeDefined();
expect(result.reason).toBeTruthy();
expect(postSpy).not.toHaveBeenCalled();
});
it('sendSlack 内网 URL 在发请求前抛 CoolCommException', async () => {
const postSpy = jest.spyOn(axios, 'post');
await expect(
syncService.sendSlack('http://192.168.1.10/slack', mockLead)
).rejects.toThrow(CoolCommException);
expect(postSpy).not.toHaveBeenCalled();
});
it('sendFeishu 内网 URL 在发请求前抛 CoolCommException', async () => {
const postSpy = jest.spyOn(axios, 'post');
await expect(
syncService.sendFeishu('http://10.0.0.8/feishu', mockLead)
).rejects.toThrow(CoolCommException);
expect(postSpy).not.toHaveBeenCalled();
});
it('sendSlack 公网 HTTP 2xx 才 delivered:true', async () => {
const postSpy = jest.spyOn(axios, 'post').mockResolvedValueOnce({
status: 200,
data: { ok: true },
});
const result = await syncService.sendSlack(
'https://hooks.slack.com/services/T00/B00/xxx',
mockLead,
{ delayMs: 1 }
);
expect(result.delivered).toBe(true);
expect(result.channel).toBe('slack');
expect(postSpy).toHaveBeenCalledTimes(1);
const [, body] = postSpy.mock.calls[0];
expect(body).toMatchObject({ text: expect.any(String) });
});
it('sendFeishu 公网 HTTP 非 2xx / 失败时 delivered:false,不伪造成功', async () => {
jest.spyOn(axios, 'post').mockRejectedValue(new Error('502 Bad Gateway'));
const result = await syncService.sendFeishu(
'https://open.feishu.cn/open-apis/bot/v2/hook/public-demo',
mockLead,
{ delayMs: 1, maxRetries: 1 }
);
expect(result.delivered).toBe(false);
expect(result.channel).toBe('feishu');
expect(result.error).toBeTruthy();
});
it('组包成功不得等同于 delivered:true(sendSlack / sendFeishu 无 URL)', async () => {
const slack = await syncService.sendSlack(undefined, mockLead);
const feishu = await syncService.sendFeishu(undefined, mockLead);
expect(slack.delivered).toBe(false);
expect(feishu.delivered).toBe(false);
expect(slack.payload).toBeDefined();
expect(feishu.payload).toBeDefined();
});
});
// ==========================================
// 6. 控制器接口集成 (/admin/crm/lead/sync-external)
// ==========================================
describe('6. 控制器接口 /admin/crm/lead/sync-external 集成测试', () => {
let controller: AdminCrmLeadController;
let mockLeadRepo: { findOne: jest.Mock };
@@ -285,6 +435,8 @@ describe('Point 11: 国际主流 CRM (HubSpot / Salesforce / Webhook) 对接服
data: {
target: 'hubspot',
leadId: 101,
synced: false,
reason: '未配置 HubSpot',
payload: {
properties: {
email: 'john.doe@acme.com',
@@ -313,6 +465,8 @@ describe('Point 11: 国际主流 CRM (HubSpot / Salesforce / Webhook) 对接服
data: {
target: 'salesforce',
leadId: 101,
synced: false,
reason: '未配置 Salesforce',
payload: {
FirstName: 'John',
LastName: 'Doe',