fix: 钉钉和快捷登录兼容

This commit is contained in:
npc0-hue
2026-01-26 00:25:06 +08:00
parent 8284f9c3c8
commit 4807f03e0a
24 changed files with 1359 additions and 339 deletions
+34
View File
@@ -27,3 +27,37 @@ type SystemOAuth2Request struct {
Test bool `json:"test" gorm:"default:0;comment:是否测试链接联通性"` // 是否测试链接联通性
Code string `json:"code" gorm:"default:0;comment:code代码"` // code代码
}
// AuthorizationConfig 认证配置
type AuthorizationConfig struct {
Type string `json:"type"` // none | bearer | basic
Token string `json:"token"` // Bearer Token
Username string `json:"username"` // Basic Auth用户名
Password string `json:"password"` // Basic Auth密码
}
// BodyData Body数据配置
type BodyData struct {
FormData []map[string]string `json:"form_data"` // form-data格式数据
Urlencoded []map[string]string `json:"urlencoded"` // x-www-form-urlencoded格式数据
Raw string `json:"raw"` // raw JSON字符串
}
// EmailApiConfig 第三方邮箱API配置
type EmailApiConfig struct {
Enabled bool `json:"enabled"` // 是否启用
URL string `json:"url"` // API地址
Method string `json:"method"` // HTTP方法
RequestParamField string `json:"request_param_field"` // 请求参数字段名
BodyType string `json:"body_type"` // Body类型: form-data | x-www-form-urlencoded | raw
Headers map[string]string `json:"headers"` // 请求头
Authorization AuthorizationConfig `json:"authorization"` // 认证配置
BodyData BodyData `json:"body_data"` // Body数据
ResponseEmailField string `json:"response_email_field"` // 响应邮箱字段路径
}
// DingTalkConfigRequest 钉钉集成配置
type DingTalkConfigRequest struct {
EmailApi EmailApiConfig `json:"email_api"` // 第三方邮箱API配置
}
+78
View File
@@ -157,6 +157,11 @@ func (e *SystemIntegratedService) TestConnection(integrate gaia.SystemIntegratio
if _, err := e.DingTalkConfigAvailable(integrate); err != nil {
return errors.New("钉钉链接失败: " + err.Error())
}
// 验证第三方邮箱API配置
if err := e.ValidateEmailApiConfig(integrate); err != nil {
global.GVA_LOG.Warn("第三方邮箱API配置验证失败", zap.Error(err))
// 不阻止保存,只记录警告
}
return nil
case gaia.SystemIntegrationOAuth2:
// 测试OAuth2连接
@@ -166,6 +171,79 @@ func (e *SystemIntegratedService) TestConnection(integrate gaia.SystemIntegratio
}
}
// ValidateEmailApiConfig 验证第三方邮箱API配置
// @Tags System Integrated
// @Summary 验证第三方邮箱API配置
// @param: integrate gaia.SystemIntegration
// @return: error
func (e *SystemIntegratedService) ValidateEmailApiConfig(integrate gaia.SystemIntegration) error {
// 解析Config字段
if integrate.Config == "" {
return nil // 配置为空不算错误
}
var configMap request.DingTalkConfigRequest
if err := json.Unmarshal([]byte(integrate.Config), &configMap); err != nil {
return fmt.Errorf("解析配置失败: %s", err.Error())
}
// 检查是否启用邮箱API
if !configMap.EmailApi.Enabled {
return nil // 未启用不需要验证
}
// 验证必填字段
if configMap.EmailApi.URL == "" {
return errors.New("邮箱API URL不能为空")
}
if configMap.EmailApi.Method == "" {
configMap.EmailApi.Method = "GET"
}
if configMap.EmailApi.RequestParamField == "" {
return errors.New("邮箱请求字段不能为空")
}
if configMap.EmailApi.ResponseEmailField == "" {
return errors.New("邮箱信息提取字段不能为空")
}
// 验证Body类型(仅POST/PUT/DELETE需要)
if configMap.EmailApi.Method != "GET" {
bodyType := strings.ToLower(configMap.EmailApi.BodyType)
if bodyType == "" {
configMap.EmailApi.BodyType = "raw" // 默认raw
} else if bodyType != "form-data" && bodyType != "x-www-form-urlencoded" && bodyType != "raw" {
return fmt.Errorf("不支持的Body类型: %s,支持的类型: form-data, x-www-form-urlencoded, raw", bodyType)
}
}
// 验证Authorization配置
authType := strings.ToLower(configMap.EmailApi.Authorization.Type)
if authType != "" && authType != "none" {
if authType == "bearer" {
if configMap.EmailApi.Authorization.Token == "" {
return errors.New("Bearer Token不能为空")
}
} else if authType == "basic" {
if configMap.EmailApi.Authorization.Username == "" || configMap.EmailApi.Authorization.Password == "" {
return errors.New("Basic Auth需要填写Username和Password")
}
} else {
return fmt.Errorf("不支持的Authorization类型: %s,支持的类型: none, bearer, basic", authType)
}
}
global.GVA_LOG.Info("第三方邮箱API配置验证通过",
zap.String("url", configMap.EmailApi.URL),
zap.String("method", configMap.EmailApi.Method),
zap.String("body_type", configMap.EmailApi.BodyType),
zap.String("auth_type", configMap.EmailApi.Authorization.Type))
return nil
}
// TestOAuth2Connection 测试OAuth2连接
// @Tags System Integrated
// @Summary 测试OAuth2连接
@@ -87,7 +87,253 @@
<el-button type="primary" plain icon="connection" @click="testConnection">
测试连接
</el-button>
<el-button v-if="openEdit" type="primary" icon="goods-filled" @click="update">
</div>
<div class="clear-both" />
</div>
</div>
<el-divider />
<div class="card-section">
<div class="section-title">
第三方邮箱配置
</div>
<div class="bg-gray-50 dark:bg-slate-800 p-5 border dark:border-slate-700 rounded-lg">
<!-- 基础配置 -->
<div class="flex items-center mb-4">
<span class="info-label">邮箱详情的URL:</span>
<el-input
v-if="openEdit"
v-model="emailApiConfig.url"
class="info-value flex-1"
placeholder="请输入钉钉通过用户名获取邮箱地址的链接地址"
/>
<span v-else class="info-value">{{ emailApiConfig.url || '未配置' }}</span>
</div>
<div class="flex items-center mb-4">
<span class="info-label">HTTP方法:</span>
<el-select
v-if="openEdit"
v-model="emailApiConfig.method"
class="info-value flex-1"
placeholder="选择HTTP方法"
>
<el-option label="GET" value="GET" />
<el-option label="POST" value="POST" />
<el-option label="PUT" value="PUT" />
<el-option label="DELETE" value="DELETE" />
</el-select>
<span v-else class="info-value">{{ emailApiConfig.method || 'GET' }}</span>
</div>
<!-- 标签页配置 -->
<div v-if="openEdit" class="mt-4">
<el-tabs v-model="activeTab" type="border-card">
<!-- Headers标签 -->
<el-tab-pane :label="`Headers (${getHeadersCount()})`" name="headers">
<div class="headers-editor">
<div v-for="(header, index) in emailApiHeaders" :key="index" class="flex items-center mb-2 gap-2">
<el-input
v-model="header.key"
placeholder="Header名称 (如: Authorization)"
class="flex-1"
/>
<el-input
v-model="header.value"
placeholder="Header值"
class="flex-1"
/>
<el-button
type="danger"
icon="delete"
circle
size="small"
@click="removeHeader(index)"
/>
</div>
<el-button
type="primary"
plain
icon="plus"
size="small"
@click="addHeader"
>
添加Header
</el-button>
</div>
</el-tab-pane>
<!-- Body标签 (仅POST/PUT/DELETE显示) -->
<el-tab-pane
v-if="emailApiConfig.method !== 'GET'"
label="Body"
name="body"
>
<div class="body-editor">
<div class="mb-3">
<el-radio-group v-model="emailApiConfig.body_type">
<el-radio label="form-data">form-data</el-radio>
<el-radio label="x-www-form-urlencoded">x-www-form-urlencoded</el-radio>
<el-radio label="raw">raw (JSON)</el-radio>
</el-radio-group>
</div>
<!-- form-data -->
<div v-if="emailApiConfig.body_type === 'form-data'" class="body-content">
<div v-for="(item, index) in bodyFormData" :key="index" class="flex items-center mb-2 gap-2">
<el-input
v-model="item.key"
placeholder="字段名"
class="flex-1"
:disabled="item.isSystemField"
/>
<el-input
v-model="item.value"
placeholder="字段值(系统自动填充)"
class="flex-1"
:disabled="item.isSystemField"
/>
<el-button
v-if="!item.isSystemField"
type="danger"
icon="delete"
circle
size="small"
@click="removeFormDataItem(index)"
/>
<el-tag v-if="item.isSystemField" type="info" size="small">系统字段</el-tag>
</div>
<el-button type="primary" plain icon="plus" size="small" @click="addFormDataItem">
添加字段
</el-button>
</div>
<!-- x-www-form-urlencoded -->
<div v-if="emailApiConfig.body_type === 'x-www-form-urlencoded'" class="body-content">
<div v-for="(item, index) in bodyUrlEncoded" :key="index" class="flex items-center mb-2 gap-2">
<el-input
v-model="item.key"
placeholder="字段名"
class="flex-1"
:disabled="item.isSystemField"
/>
<el-input
v-model="item.value"
placeholder="字段值(系统自动填充)"
class="flex-1"
:disabled="item.isSystemField"
/>
<el-button
v-if="!item.isSystemField"
type="danger"
icon="delete"
circle
size="small"
@click="removeUrlEncodedItem(index)"
/>
<el-tag v-if="item.isSystemField" type="info" size="small">系统字段</el-tag>
</div>
<el-button type="primary" plain icon="plus" size="small" @click="addUrlEncodedItem">
添加字段
</el-button>
</div>
<!-- raw JSON -->
<div v-if="emailApiConfig.body_type === 'raw'" class="body-content">
<el-input
v-model="bodyRaw"
type="textarea"
:rows="8"
placeholder='请输入JSON格式,例如: {"userId": "xxx", "other": "value"}'
/>
</div>
</div>
</el-tab-pane>
<!-- Authorization标签 -->
<el-tab-pane label="Authorization" name="authorization">
<div class="auth-editor">
<div class="mb-3">
<el-radio-group v-model="emailApiConfig.authorization.type">
<el-radio label="none">None</el-radio>
<el-radio label="bearer">Bearer Token</el-radio>
<el-radio label="basic">Basic Auth</el-radio>
</el-radio-group>
</div>
<!-- Bearer Token -->
<div v-if="emailApiConfig.authorization.type === 'bearer'" class="auth-content">
<el-input
v-model="emailApiConfig.authorization.token"
placeholder="请输入Bearer Token"
type="password"
show-password
/>
</div>
<!-- Basic Auth -->
<div v-if="emailApiConfig.authorization.type === 'basic'" class="auth-content">
<el-input
v-model="emailApiConfig.authorization.username"
placeholder="Username"
class="mb-2"
/>
<el-input
v-model="emailApiConfig.authorization.password"
placeholder="Password"
type="password"
show-password
/>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 只读模式显示 -->
<div v-else class="mt-4">
<div v-if="Object.keys(emailApiConfig.headers).length > 0" class="mb-3">
<div class="text-sm font-medium mb-2">Headers:</div>
<div v-for="(value, key) in emailApiConfig.headers" :key="key" class="flex items-center mb-1">
<span class="info-label text-sm">{{ key }}:</span>
<span class="info-value text-sm ml-2">{{ value }}</span>
</div>
</div>
<div v-if="emailApiConfig.authorization.type !== 'none'" class="mb-3">
<div class="text-sm font-medium mb-2">Authorization:</div>
<span class="info-value text-sm">{{ emailApiConfig.authorization.type === 'bearer' ? 'Bearer Token' : 'Basic Auth' }}</span>
</div>
</div>
<!-- 邮箱请求字段和提取 -->
<div class="flex items-center mb-4 mt-4">
<span class="info-label">邮箱请求字段:</span>
<el-input
v-if="openEdit"
v-model="emailApiConfig.request_param_field"
class="info-value flex-1"
placeholder="例如: userId"
/>
<span v-else class="info-value">{{ emailApiConfig.request_param_field || 'userId' }}</span>
</div>
<div class="flex items-center mb-4">
<span class="info-label">邮箱信息提取:</span>
<el-input
v-if="openEdit"
v-model="emailApiConfig.response_email_field"
class="info-value flex-1"
placeholder="例如: data[0].userName"
>
<template #append>
<el-tooltip content="支持点号路径(data.email)和数组索引(data[0].userName)" placement="top">
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</template>
</el-input>
<span v-else class="info-value">{{ emailApiConfig.response_email_field || 'data[0].userName' }}</span>
</div>
<div v-if="openEdit" class="flex justify-end">
<el-button type="primary" icon="goods-filled" @click="update">
保存
</el-button>
</div>
@@ -124,8 +370,9 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'
import { getSystemDingTalk, setSystemDingTalk } from "@/api/gaia/system";
defineOptions({
@@ -144,13 +391,158 @@ const config = ref({
app_key: "",
app_secret: "",
test: false,
config: ""
})
// 第三方邮箱API配置
const emailApiConfig = ref({
enabled: false,
url: '',
method: 'GET',
request_param_field: 'userId',
body_type: 'raw', // form-data | x-www-form-urlencoded | raw
headers: {},
authorization: {
type: 'none', // none | bearer | basic
token: '',
username: '',
password: ''
},
response_email_field: 'data[0].userName'
})
// 标签页管理
const activeTab = ref('headers')
// Headers编辑器数组
const emailApiHeaders = ref([{ key: '', value: '' }])
// Body编辑器数据(根据body_type不同使用不同格式)
const bodyFormData = ref([{ key: '', value: '' }]) // form-data
const bodyUrlEncoded = ref([{ key: '', value: '' }]) // x-www-form-urlencoded
const bodyRaw = ref('') // raw JSON
// 验证配置是否有效
const isConfigValid = computed(() => {
return !!(config.value.corp_id && config.value.agent_id && config.value.app_key && config.value.app_secret);
})
// 验证邮箱API配置是否有效
const isEmailApiConfigValid = computed(() => {
if (!emailApiConfig.value.enabled) {
return true; // 未启用时认为有效
}
return !!(
emailApiConfig.value.url &&
emailApiConfig.value.method &&
emailApiConfig.value.request_param_field &&
emailApiConfig.value.response_email_field
);
})
// Headers管理
const getHeadersCount = () => {
return emailApiHeaders.value.filter(h => h.key && h.value).length
}
const addHeader = () => {
emailApiHeaders.value.push({ key: '', value: '' })
}
const removeHeader = (index) => {
emailApiHeaders.value.splice(index, 1)
if (emailApiHeaders.value.length === 0) {
emailApiHeaders.value.push({ key: '', value: '' })
}
}
// Headers数组转对象
const headersArrayToObject = () => {
const headers = {}
emailApiHeaders.value.forEach(header => {
if (header.key && header.value) {
headers[header.key] = header.value
}
})
return headers
}
// Headers对象转数组
const headersObjectToArray = (headersObj) => {
const arr = []
for (const [key, value] of Object.entries(headersObj || {})) {
arr.push({ key, value })
}
if (arr.length === 0) {
arr.push({ key: '', value: '' })
}
return arr
}
// 确保系统字段存在(主请求字段)
const ensureSystemField = (bodyArray, fieldName) => {
// 查找是否已存在系统字段
const systemFieldIndex = bodyArray.value.findIndex(item => item.isSystemField)
if (systemFieldIndex >= 0) {
// 更新现有系统字段的key
bodyArray.value[systemFieldIndex].key = fieldName
} else {
// 添加新的系统字段到第一个位置
bodyArray.value.unshift({
key: fieldName,
value: '',
isSystemField: true
})
}
}
// 移除系统字段标记(如果需要)
const removeSystemField = (bodyArray) => {
const systemFieldIndex = bodyArray.value.findIndex(item => item.isSystemField)
if (systemFieldIndex >= 0) {
bodyArray.value.splice(systemFieldIndex, 1)
}
}
// Body form-data管理
const addFormDataItem = () => {
bodyFormData.value.push({ key: '', value: '', isSystemField: false })
}
const removeFormDataItem = (index) => {
// 不允许删除系统字段
if (bodyFormData.value[index].isSystemField) {
return
}
bodyFormData.value.splice(index, 1)
// 确保至少有一个非系统字段或系统字段存在
const hasSystemField = bodyFormData.value.some(item => item.isSystemField)
const hasOtherField = bodyFormData.value.some(item => !item.isSystemField)
if (!hasSystemField && !hasOtherField) {
ensureSystemField(bodyFormData, emailApiConfig.value.request_param_field)
}
}
// Body x-www-form-urlencoded管理
const addUrlEncodedItem = () => {
bodyUrlEncoded.value.push({ key: '', value: '', isSystemField: false })
}
const removeUrlEncodedItem = (index) => {
// 不允许删除系统字段
if (bodyUrlEncoded.value[index].isSystemField) {
return
}
bodyUrlEncoded.value.splice(index, 1)
// 确保至少有一个非系统字段或系统字段存在
const hasSystemField = bodyUrlEncoded.value.some(item => item.isSystemField)
const hasOtherField = bodyUrlEncoded.value.some(item => !item.isSystemField)
if (!hasSystemField && !hasOtherField) {
ensureSystemField(bodyUrlEncoded, emailApiConfig.value.request_param_field)
}
}
// 处理状态变更
const handleStatusChange = (val) => {
if (val && !isConfigValid.value) {
@@ -198,11 +590,150 @@ const testConnection = async () => {
}
}
// 监听URL变化,自动启用/禁用邮箱API配置
watch(() => emailApiConfig.value.url, (newUrl) => {
emailApiConfig.value.enabled = !!newUrl && newUrl.trim() !== ''
})
// 监听request_param_field变化,更新Body中的系统字段
watch(() => emailApiConfig.value.request_param_field, (newField) => {
if (!newField) return
// 如果当前是form-data或x-www-form-urlencoded,更新系统字段
if (emailApiConfig.value.method !== 'GET') {
if (emailApiConfig.value.body_type === 'form-data') {
ensureSystemField(bodyFormData, newField)
} else if (emailApiConfig.value.body_type === 'x-www-form-urlencoded') {
ensureSystemField(bodyUrlEncoded, newField)
}
}
})
// 监听body_type变化,管理系统字段
watch(() => emailApiConfig.value.body_type, (newType, oldType) => {
if (emailApiConfig.value.method === 'GET') return
const fieldName = emailApiConfig.value.request_param_field
// 从旧类型移除系统字段
if (oldType === 'form-data') {
removeSystemField(bodyFormData)
} else if (oldType === 'x-www-form-urlencoded') {
removeSystemField(bodyUrlEncoded)
}
// 在新类型中添加系统字段
if (newType === 'form-data') {
ensureSystemField(bodyFormData, fieldName)
} else if (newType === 'x-www-form-urlencoded') {
ensureSystemField(bodyUrlEncoded, fieldName)
}
})
// 监听method变化,管理系统字段
watch(() => emailApiConfig.value.method, (newMethod, oldMethod) => {
const fieldName = emailApiConfig.value.request_param_field
// 如果从非GET变为GET,移除系统字段
if (oldMethod !== 'GET' && newMethod === 'GET') {
if (emailApiConfig.value.body_type === 'form-data') {
removeSystemField(bodyFormData)
} else if (emailApiConfig.value.body_type === 'x-www-form-urlencoded') {
removeSystemField(bodyUrlEncoded)
}
}
// 如果从GET变为非GET,添加系统字段
else if (oldMethod === 'GET' && newMethod !== 'GET') {
if (emailApiConfig.value.body_type === 'form-data') {
ensureSystemField(bodyFormData, fieldName)
} else if (emailApiConfig.value.body_type === 'x-www-form-urlencoded') {
ensureSystemField(bodyUrlEncoded, fieldName)
}
}
})
const initForm = async() => {
const res = await getSystemDingTalk()
if (res.code === 0) {
host.value = res.data.host
config.value = res.data.config
// 解析config字段中的email_api配置
if (config.value.config) {
try {
const configData = typeof config.value.config === 'string'
? JSON.parse(config.value.config)
: config.value.config
if (configData.email_api) {
const url = configData.email_api.url || ''
emailApiConfig.value = {
enabled: !!url && url.trim() !== '', // 根据URL自动设置enabled
url: url,
method: configData.email_api.method || 'GET',
request_param_field: configData.email_api.request_param_field || 'userId',
body_type: configData.email_api.body_type || 'raw',
headers: configData.email_api.headers || {},
authorization: configData.email_api.authorization || {
type: 'none',
token: '',
username: '',
password: ''
},
response_email_field: configData.email_api.response_email_field || 'data[0].userName'
}
// 转换headers为数组格式供编辑
emailApiHeaders.value = headersObjectToArray(configData.email_api.headers)
// 转换body数据
if (configData.email_api.body_data) {
if (emailApiConfig.value.body_type === 'form-data') {
const formData = configData.email_api.body_data.form_data || []
// 转换为带isSystemField标记的格式
bodyFormData.value = formData.map(item => ({
key: item.key || '',
value: item.value || '',
isSystemField: false
}))
// 确保系统字段存在
if (emailApiConfig.value.method !== 'GET') {
ensureSystemField(bodyFormData, emailApiConfig.value.request_param_field)
}
} else if (emailApiConfig.value.body_type === 'x-www-form-urlencoded') {
const urlencoded = configData.email_api.body_data.urlencoded || []
// 转换为带isSystemField标记的格式
bodyUrlEncoded.value = urlencoded.map(item => ({
key: item.key || '',
value: item.value || '',
isSystemField: false
}))
// 确保系统字段存在
if (emailApiConfig.value.method !== 'GET') {
ensureSystemField(bodyUrlEncoded, emailApiConfig.value.request_param_field)
}
} else {
bodyRaw.value = configData.email_api.body_data.raw || ''
}
} else {
// 初始化body数据
bodyFormData.value = []
bodyUrlEncoded.value = []
bodyRaw.value = ''
// 如果方法不是GET,添加系统字段
if (emailApiConfig.value.method !== 'GET') {
if (emailApiConfig.value.body_type === 'form-data') {
ensureSystemField(bodyFormData, emailApiConfig.value.request_param_field)
} else if (emailApiConfig.value.body_type === 'x-www-form-urlencoded') {
ensureSystemField(bodyUrlEncoded, emailApiConfig.value.request_param_field)
}
}
}
}
} catch (e) {
console.error('解析邮箱API配置失败:', e)
}
}
}
}
initForm()
@@ -219,6 +750,52 @@ const update = async() => {
return;
}
// 验证邮箱API配置
if (!isEmailApiConfigValid.value) {
ElMessage({
type: 'warning',
message: '请填写完整的第三方邮箱API配置'
});
return;
}
// 将emailApiConfig合并到config字段中
// 转换headers数组为对象
const headers = headersArrayToObject()
// 准备body数据
const bodyData = {}
if (emailApiConfig.value.method !== 'GET') {
if (emailApiConfig.value.body_type === 'form-data') {
// 保存所有字段(包括系统字段),但移除isSystemField标记
bodyData.form_data = bodyFormData.value
.filter(item => item.key) // 只过滤掉空的key
.map(item => ({
key: item.key,
value: item.value || '' // 系统字段的value为空,由后端填充
}))
} else if (emailApiConfig.value.body_type === 'x-www-form-urlencoded') {
// 保存所有字段(包括系统字段),但移除isSystemField标记
bodyData.urlencoded = bodyUrlEncoded.value
.filter(item => item.key) // 只过滤掉空的key
.map(item => ({
key: item.key,
value: item.value || '' // 系统字段的value为空,由后端填充
}))
} else {
bodyData.raw = bodyRaw.value
}
}
const configData = {
email_api: {
...emailApiConfig.value,
headers,
body_data: bodyData
}
}
config.value.config = JSON.stringify(configData)
const res = await setSystemDingTalk(config.value)
if (res.code === 0) {
ElMessage({
@@ -239,7 +816,7 @@ const goToSecuritySettings = () => {
});
return;
}
const url = `https://open-dev.dingtalk.com/fe/ai?hash=#/app/${config.value.app_id}/security#/app/${config.value.app_id}/security`;
window.open(url, '_blank');
}