From 0515e6ed276a9525679d7188018e8ddaea4bd8b5 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Mon, 13 Jul 2026 14:01:45 +0800 Subject: [PATCH] fix(integration): simplify manual DingTalk sync setup --- .../src/pages/IntegrationConfig/index.tsx | 190 ++++++++++-------- ...ntegration-config-form.integration.test.ts | 26 +++ .../integration-config-form.ts | 13 ++ .../src/integration/config/dto/config.dto.ts | 6 +- .../config/integration-config.service.spec.ts | 49 +++++ .../config/integration-config.service.ts | 7 +- 6 files changed, 203 insertions(+), 88 deletions(-) create mode 100644 apps/admin/src/pages/IntegrationConfig/integration-config-form.integration.test.ts create mode 100644 apps/admin/src/pages/IntegrationConfig/integration-config-form.ts create mode 100644 apps/server/src/integration/config/integration-config.service.spec.ts diff --git a/apps/admin/src/pages/IntegrationConfig/index.tsx b/apps/admin/src/pages/IntegrationConfig/index.tsx index e553f6f..7226466 100644 --- a/apps/admin/src/pages/IntegrationConfig/index.tsx +++ b/apps/admin/src/pages/IntegrationConfig/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { - Card, Form, Input, Button, Space, Spin, Switch, Alert, Descriptions, Tag, - Tabs, Drawer, Tree, Select, TreeSelect, Modal, DatePicker, InputNumber, + Card, Form, Input, Button, Space, Spin, Alert, Descriptions, Tag, Divider, + Drawer, Tree, Select, TreeSelect, Modal, DatePicker, InputNumber, Row, Col, List, } from 'antd'; import { @@ -15,12 +15,15 @@ import api from '../../api'; import { message } from '../../ui/app-message'; import { usePermission } from '../../hooks/usePermission'; import PermissionButton from '../../components/PermissionButton'; +import { + buildDingTalkConfigPayload, + isAppSecretRequired, + type DingTalkConfigFormValues, +} from './integration-config-form'; interface DingTalkConfig { agentId: string; - appSecret: string; corpId: string; - startEnable: boolean; } interface DingOrgTreeNodeExt { @@ -94,9 +97,9 @@ const IntegrationConfigPage: React.FC = () => { const [testing, setTesting] = useState(false); const [config, setConfig] = useState(null); const [verified, setVerified] = useState(null); - const [form] = Form.useForm(); + const [form] = Form.useForm(); - // ── Sync Users Tab ── + // ── Manual organization sync ── const [syncRootDeptId, setSyncRootDeptId] = useState(undefined); const [orgTree, setOrgTree] = useState([]); const [drawerOpen, setDrawerOpen] = useState(false); @@ -137,9 +140,10 @@ const IntegrationConfigPage: React.FC = () => { const handleSave = async () => { const values = await form.validateFields(); + const payload = buildDingTalkConfigPayload(values); setSaving(true); try { - await api.post('/integration/config', { type: 'DINGTALK', config: values }); + await api.post('/integration/config', { type: 'DINGTALK', config: payload }); message.success('配置已保存'); await fetchConfig(); } catch (e: unknown) { @@ -152,11 +156,12 @@ const IntegrationConfigPage: React.FC = () => { const handleTest = async () => { const values = await form.validateFields(); + const payload = buildDingTalkConfigPayload(values); setTesting(true); try { const res = await api.post<{ success: boolean; message: string }>('/integration/config/test', { type: 'DINGTALK', - config: values, + config: payload, }); setVerified(res.success); message.success(res.message); @@ -345,12 +350,8 @@ const IntegrationConfigPage: React.FC = () => { } }; - const syncTabItems = config && hasAllPermissions('sync:read', 'class:view', 'class:edit') - ? [ - { - key: 'sync-users', - label: '同步用户', - children: ( + const syncPanel = config && hasAllPermissions('sync:read', 'class:view', 'class:edit') + ? (
{ />
- ), - }, - ] - : []; - - const tabItems = [ - { - key: 'config', - label: '配置', - children: ( - - {config && ( - - {config.corpId || '-'} - {config.agentId || '-'} - - - {config.startEnable ? '已启用' : '未启用'} - - - - )} - - - -
- - - - - - - - - - - - - - } loading={saving} onClick={handleSave}> - 保存配置 - - - -
-
- ), - }, - ...syncTabItems, - ]; + ) + : null; return ( - - {verified === true && } color="success">已连接} - {verified === false && } color="error">未连接} - - }> - + + {verified === true && ( + } color="success"> + 已连接 + + )} + {verified === false && ( + } color="error"> + 未连接 + + )} + + } + > + + {config && ( + + {config.corpId || '-'} + {config.agentId || '-'} + 手动触发 + + )} + + + +
+ + + + + + + + + + + } + loading={saving} + onClick={handleSave} + > + 保存配置 + + + +
+ + {syncPanel && ( + <> + 组织用户导入 + {syncPanel} + + )} +
); }; diff --git a/apps/admin/src/pages/IntegrationConfig/integration-config-form.integration.test.ts b/apps/admin/src/pages/IntegrationConfig/integration-config-form.integration.test.ts new file mode 100644 index 0000000..5219ac7 --- /dev/null +++ b/apps/admin/src/pages/IntegrationConfig/integration-config-form.integration.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { + buildDingTalkConfigPayload, + isAppSecretRequired, +} from './integration-config-form'; + +describe('DingTalk integration config form', () => { + it('requires AppSecret only for the first configuration', () => { + expect(isAppSecretRequired(false)).toBe(true); + expect(isAppSecretRequired(true)).toBe(false); + }); + + it('builds a manual-sync config without the retired startEnable flag', () => { + expect( + buildDingTalkConfigPayload({ + corpId: 'ding-corp', + agentId: 'app-key', + appSecret: '', + }), + ).toEqual({ + corpId: 'ding-corp', + agentId: 'app-key', + appSecret: undefined, + }); + }); +}); diff --git a/apps/admin/src/pages/IntegrationConfig/integration-config-form.ts b/apps/admin/src/pages/IntegrationConfig/integration-config-form.ts new file mode 100644 index 0000000..4e18c1a --- /dev/null +++ b/apps/admin/src/pages/IntegrationConfig/integration-config-form.ts @@ -0,0 +1,13 @@ +export interface DingTalkConfigFormValues { + agentId: string; + appSecret?: string; + corpId: string; +} + +export const isAppSecretRequired = (hasSavedConfig: boolean) => !hasSavedConfig; + +export const buildDingTalkConfigPayload = (values: DingTalkConfigFormValues) => ({ + corpId: values.corpId.trim(), + agentId: values.agentId.trim(), + appSecret: values.appSecret?.trim() || undefined, +}); diff --git a/apps/server/src/integration/config/dto/config.dto.ts b/apps/server/src/integration/config/dto/config.dto.ts index e0e35ee..d67e4cf 100644 --- a/apps/server/src/integration/config/dto/config.dto.ts +++ b/apps/server/src/integration/config/dto/config.dto.ts @@ -1,18 +1,16 @@ /** 钉钉配置 */ export interface DingTalkThirdConfig { agentId: string; // AppKey - appSecret: string; // AppSecret + appSecret?: string; // AppSecret;更新已有配置时可留空保留旧值 corpId: string; // CorpId - startEnable: boolean; // 是否启用同步 appId?: string; // 内部应用ID,用于消息推送(可选) } /** 企微配置 */ export interface WeComThirdConfig { agentId: string; - appSecret: string; + appSecret?: string; corpId: string; - startEnable: boolean; } /** 对外返回的配置(脱敏后,不含 appSecret) */ diff --git a/apps/server/src/integration/config/integration-config.service.spec.ts b/apps/server/src/integration/config/integration-config.service.spec.ts new file mode 100644 index 0000000..339524a --- /dev/null +++ b/apps/server/src/integration/config/integration-config.service.spec.ts @@ -0,0 +1,49 @@ +import { IntegrationConfigService } from './integration-config.service'; + +describe('IntegrationConfigService.testConnection', () => { + const originalFetch = global.fetch; + + afterEach(() => { + global.fetch = originalFetch; + jest.restoreAllMocks(); + }); + + it('uses the saved AppSecret when testing an existing configuration with a blank secret', async () => { + const configRepo = { + findOne: jest.fn().mockResolvedValue({ id: 1, type: 'THIRD' }), + }; + const detailRepo = { + findOne: jest.fn().mockResolvedValue({ + configId: 1, + type: 'DINGTALK_SYNC', + content: JSON.stringify({ + config: { + corpId: 'ding-corp', + agentId: 'saved-key', + appSecret: 'saved-secret', + }, + }), + }), + }; + global.fetch = jest.fn().mockResolvedValue({ + json: jest.fn().mockResolvedValue({ accessToken: 'token' }), + }) as never; + + const service = new IntegrationConfigService(configRepo as never, detailRepo as never); + + await expect( + service.testConnection('DINGTALK', { + corpId: 'ding-corp', + agentId: 'saved-key', + appSecret: '', + }), + ).resolves.toBe(true); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://api.dingtalk.com/v1.0/oauth2/accessToken', + expect.objectContaining({ + body: JSON.stringify({ appKey: 'saved-key', appSecret: 'saved-secret' }), + }), + ); + }); +}); diff --git a/apps/server/src/integration/config/integration-config.service.ts b/apps/server/src/integration/config/integration-config.service.ts index 56825a2..3216230 100644 --- a/apps/server/src/integration/config/integration-config.service.ts +++ b/apps/server/src/integration/config/integration-config.service.ts @@ -125,7 +125,12 @@ export class IntegrationConfigService { config: DingTalkThirdConfig | WeComThirdConfig, ): Promise { try { - const token = await this.getTokenForTest(type, config as unknown as Record); + const finalConfig = { ...config } as Record; + if (!finalConfig.appSecret) { + const savedConfig = await this.getRawConfig(type); + if (savedConfig?.appSecret) finalConfig.appSecret = savedConfig.appSecret; + } + const token = await this.getTokenForTest(type, finalConfig); return !!token; } catch (e) { this.logger.error(`连接测试失败: ${(e as Error).message}`);