diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ce94b9e --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# MySQL +MYSQL_ROOT_PASSWORD=root +MYSQL_DATABASE=sino_mci +MYSQL_USER=root +MYSQL_PORT=3306 + +# Redis +REDIS_PORT=6379 + +# RabbitMQ +RABBITMQ_DEFAULT_USER=guest +RABBITMQ_DEFAULT_PASS=guest +RABBITMQ_AMQP_PORT=5672 +RABBITMQ_MGMT_PORT=15672 + +# SeaweedFS +SEAWEEDFS_MASTER_PORT=9333 +SEAWEEDFS_S3_PORT=8888 + +# Application ports +MCI_SERVER_PORT=8080 +ADMIN_WEB_PORT=80 +CHANNEL_SERVICE_PORT=8000 + +# JVM options for mci-server +JAVA_OPTS=-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 + +# Spring profile +SPRING_PROFILES_ACTIVE=dev + +# OpenAI API key (optional) +OPENAI_API_KEY=your-openai-api-key-here diff --git a/.gitignore b/.gitignore index 03fd31a..86912da 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,11 @@ __pycache__/ venv/ .env +# Local runtime data +data/ +*-data/ +logs/ + # Node node_modules/ dist/ diff --git a/admin-web/src/api/account.ts b/admin-web/src/api/account.ts index 6cd6004..151b060 100644 --- a/admin-web/src/api/account.ts +++ b/admin-web/src/api/account.ts @@ -6,20 +6,70 @@ export function login(data: { tenantCode: string; username: string; password: st }) } -export function createTenant(data: { tenantCode: string; tenantName: string }) { +export function createTenant(data: { + tenantCode: string + tenantName: string + inboundWebhookUrl?: string + initAdminUsername?: string + initAdminPassword?: string +}) { return client.post('/api/v1/account/tenant', data) } +export function updateTenant( + id: number, + data: { tenantName: string; inboundWebhookUrl?: string; status?: number } +) { + return client.put(`/api/v1/account/tenant/${id}`, data) +} + +export function deleteTenant(id: number) { + return client.delete(`/api/v1/account/tenant/${id}`) +} + export function listTenants() { return client.get('/api/v1/account/tenant/list') } -export function createUser(data: { username: string; password: string; realName?: string; roleCodes?: string[] }) { - return client.post('/api/v1/account/user', data) +export function listRoles() { + return client.get('/api/v1/account/role/list') } -export function listUsers() { - return client.get('/api/v1/account/user/list') +export function createUser( + tenantCode: string, + data: { + username: string + password: string + realName?: string + phone?: string + roleCodes?: string[] + } +) { + return client.post('/api/v1/account/user', data, { + headers: { 'X-Tenant-Code': tenantCode }, + }) +} + +export function updateUser( + id: number, + data: { + realName: string + phone?: string + status?: number + roleCodes?: string[] + } +) { + return client.put(`/api/v1/account/user/${id}`, data) +} + +export function deleteUser(id: number) { + return client.delete(`/api/v1/account/user/${id}`) +} + +export function listUsers(tenantCode?: string) { + return client.get('/api/v1/account/user/list', { + params: tenantCode ? { tenantCode } : {}, + }) } export function me() { diff --git a/admin-web/src/api/channel.ts b/admin-web/src/api/channel.ts index d9d33cc..066edf2 100644 --- a/admin-web/src/api/channel.ts +++ b/admin-web/src/api/channel.ts @@ -1,24 +1,6 @@ import client from './client' - -export function createChannelSubject(data: any) { - return client.post('/api/v1/channel-subject', data) -} - -export function listChannelSubjects() { - return client.get('/api/v1/channel-subject/list') -} - -export function createChannelAccount(data: any) { - return client.post('/api/v1/channel-account', data) -} - -export function listChannelAccounts() { - return client.get('/api/v1/channel-account/list') -} - -export function initChannelAccount(data: any) { - return client.post('/api/v1/channel-account/init', data) -} +export * from './channelSubject' +export * from './channelAccount' export function createConversation(data: any) { return client.post('/api/v1/conversation', data) @@ -28,6 +10,14 @@ export function listConversations() { return client.get('/api/v1/conversation/list') } +export function updateConversation(id: number, data: any) { + return client.put(`/api/v1/conversation/${id}`, data) +} + +export function deleteConversation(id: number) { + return client.delete(`/api/v1/conversation/${id}`) +} + export function bindConversationAccount(conversationId: number, data: any) { return client.post(`/api/v1/conversation/${conversationId}/bind-account`, data) } diff --git a/admin-web/src/api/channelAccount.ts b/admin-web/src/api/channelAccount.ts new file mode 100644 index 0000000..2522951 --- /dev/null +++ b/admin-web/src/api/channelAccount.ts @@ -0,0 +1,33 @@ +import client from './client' + +export function createChannelAccount(data: any) { + return client.post('/api/v1/channel-account', data) +} + +export function listChannelAccounts() { + return client.get('/api/v1/channel-account/list') +} + +export function updateChannelAccount(id: number, data: any) { + return client.put(`/api/v1/channel-account/${id}`, data) +} + +export function deleteChannelAccount(id: number) { + return client.delete(`/api/v1/channel-account/${id}`) +} + +export function updateChannelAccountStatus(id: number, data: any) { + return client.patch(`/api/v1/channel-account/${id}/status`, data) +} + +export function getChannelAccountStatus(id: number) { + return client.get(`/api/v1/channel-account/${id}/status`) +} + +export function initChannelAccount(data: any) { + return client.post('/api/v1/channel-account/init', data) +} + +export function syncChannelAccountConversations(id: number) { + return client.post(`/api/v1/channel-account/${id}/sync-conversations`) +} diff --git a/admin-web/src/api/channelSubject.ts b/admin-web/src/api/channelSubject.ts new file mode 100644 index 0000000..14c385b --- /dev/null +++ b/admin-web/src/api/channelSubject.ts @@ -0,0 +1,21 @@ +import client from './client' + +export function createChannelSubject(data: any) { + return client.post('/api/v1/channel-subject', data) +} + +export function listChannelSubjects() { + return client.get('/api/v1/channel-subject/list') +} + +export function updateChannelSubject(id: number, data: any) { + return client.put(`/api/v1/channel-subject/${id}`, data) +} + +export function deleteChannelSubject(id: number) { + return client.delete(`/api/v1/channel-subject/${id}`) +} + +export function updateChannelSubjectStatus(id: number, data: any) { + return client.patch(`/api/v1/channel-subject/${id}/status`, data) +} diff --git a/admin-web/src/api/client.ts b/admin-web/src/api/client.ts index fccfa25..2c9fb72 100644 --- a/admin-web/src/api/client.ts +++ b/admin-web/src/api/client.ts @@ -21,7 +21,13 @@ client.interceptors.request.use((config) => { }) client.interceptors.response.use( - (response) => response.data, + (response) => { + const data = response.data + if (data && typeof data === 'object' && 'code' in data && data.code !== 0) { + return Promise.reject(new Error(data.message || '请求失败')) + } + return data + }, (error) => { if (error.response?.status === 401) { localStorage.clear() diff --git a/admin-web/src/api/crm.ts b/admin-web/src/api/crm.ts index 4e023b2..b149bcc 100644 --- a/admin-web/src/api/crm.ts +++ b/admin-web/src/api/crm.ts @@ -8,10 +8,42 @@ export function listPersons() { return client.get('/api/v1/person/list') } -export function listTags() { - return client.get('/api/v1/tag/list') +export function updatePerson(id: number, data: any) { + return client.put(`/api/v1/person/${id}`, data) +} + +export function deletePerson(id: number) { + return client.delete(`/api/v1/person/${id}`) +} + +export function listPersonIdentities(personId: number) { + return client.get(`/api/v1/person/${personId}/identities`) +} + +export function createPersonIdentity(personId: number, data: any) { + return client.post(`/api/v1/person/${personId}/identities`, data) +} + +export function deletePersonIdentity(personId: number, identityId: number) { + return client.delete(`/api/v1/person/${personId}/identities/${identityId}`) +} + +export function bindPersonTags(personId: number, data: any) { + return client.post(`/api/v1/person/${personId}/bind-tags`, data) } export function createTag(data: any) { return client.post('/api/v1/tag', data) } + +export function listTags() { + return client.get('/api/v1/tag/list') +} + +export function updateTag(id: number, data: any) { + return client.put(`/api/v1/tag/${id}`, data) +} + +export function deleteTag(id: number) { + return client.delete(`/api/v1/tag/${id}`) +} diff --git a/admin-web/src/api/inboundFlow.ts b/admin-web/src/api/inboundFlow.ts new file mode 100644 index 0000000..c3a398d --- /dev/null +++ b/admin-web/src/api/inboundFlow.ts @@ -0,0 +1,75 @@ +import client from './client' + +export interface FlowNodeConfig { + [key: string]: any +} + +export interface FlowNode { + id: string + type: string + config: FlowNodeConfig +} + +export interface FlowEdge { + source: string + target: string + label?: string +} + +export interface FlowDefinition { + nodes: FlowNode[] + edges: FlowEdge[] +} + +export interface FlowRecord { + id: number + flowCode: string + flowName: string + flowType: number + status: number + definitionJson: string | FlowDefinition + version?: number + createTime?: string + updateTime?: string +} + +export interface SaveFlowPayload { + flowCode: string + flowName: string + flowType?: string + definitionJson: FlowDefinition +} + +export function listFlows() { + return client.get('/api/v1/inbound/flow/list') +} + +export function getFlow(flowCode: string) { + return client.get(`/api/v1/inbound/flow/${flowCode}`) +} + +export function saveFlow(data: SaveFlowPayload) { + return client.post('/api/v1/inbound/flow', { + ...data, + flowType: data.flowType || 'inbound', + }) +} + +export function updateFlow(flowCode: string, data: SaveFlowPayload) { + return client.post(`/api/v1/inbound/flow/${flowCode}`, { + ...data, + flowType: data.flowType || 'inbound', + }) +} + +export function publishFlow(flowCode: string) { + return client.post(`/api/v1/inbound/flow/${flowCode}/publish`) +} + +export function offlineFlow(flowCode: string) { + return client.post(`/api/v1/inbound/flow/${flowCode}/offline`) +} + +export function deleteFlow(flowCode: string) { + return client.delete(`/api/v1/inbound/flow/${flowCode}`) +} diff --git a/admin-web/src/api/inboundWebhook.ts b/admin-web/src/api/inboundWebhook.ts new file mode 100644 index 0000000..c388053 --- /dev/null +++ b/admin-web/src/api/inboundWebhook.ts @@ -0,0 +1,13 @@ +import client from './client' + +export function getInboundWebhookConfig() { + return client.get('/api/v1/account/tenant/webhook') +} + +export function updateInboundWebhookConfig(data: any) { + return client.post('/api/v1/account/tenant/webhook', data) +} + +export function testInboundWebhook(data: { url: string }) { + return client.post('/api/v1/account/tenant/webhook/test', data) +} diff --git a/admin-web/src/api/ivrFlow.ts b/admin-web/src/api/ivrFlow.ts new file mode 100644 index 0000000..a18c30c --- /dev/null +++ b/admin-web/src/api/ivrFlow.ts @@ -0,0 +1,75 @@ +import client from './client' + +export interface FlowNodeConfig { + [key: string]: any +} + +export interface FlowNode { + id: string + type: string + config: FlowNodeConfig +} + +export interface FlowEdge { + source: string + target: string + label?: string +} + +export interface FlowDefinition { + nodes: FlowNode[] + edges: FlowEdge[] +} + +export interface FlowRecord { + id: number + flowCode: string + flowName: string + flowType: number + status: number + definitionJson: string | FlowDefinition + version?: number + createTime?: string + updateTime?: string +} + +export interface SaveFlowPayload { + flowCode: string + flowName: string + flowType?: string + definitionJson: FlowDefinition +} + +export function listFlows() { + return client.get('/api/v1/flow/list') +} + +export function getFlow(flowCode: string) { + return client.get(`/api/v1/flow/${flowCode}`) +} + +export function saveFlow(data: SaveFlowPayload) { + return client.post('/api/v1/flow', { + ...data, + flowType: data.flowType || 'ivr', + }) +} + +export function updateFlow(flowCode: string, data: SaveFlowPayload) { + return client.post(`/api/v1/flow/${flowCode}`, { + ...data, + flowType: data.flowType || 'ivr', + }) +} + +export function publishFlow(flowCode: string) { + return client.post(`/api/v1/flow/${flowCode}/publish`) +} + +export function offlineFlow(flowCode: string) { + return client.post(`/api/v1/flow/${flowCode}/offline`) +} + +export function deleteFlow(flowCode: string) { + return client.delete(`/api/v1/flow/${flowCode}`) +} diff --git a/admin-web/src/api/sendPolicy.ts b/admin-web/src/api/sendPolicy.ts new file mode 100644 index 0000000..26a9d42 --- /dev/null +++ b/admin-web/src/api/sendPolicy.ts @@ -0,0 +1,25 @@ +import client from './client' + +export function listPolicyGroups() { + return client.get('/api/v1/policy-group/list') +} + +export function getPolicyGroup(groupCode: string) { + return client.get(`/api/v1/policy-group/${groupCode}`) +} + +export function createPolicyGroup(data: any) { + return client.post('/api/v1/policy-group', data) +} + +export function updatePolicyGroup(groupCode: string, data: any) { + return client.post(`/api/v1/policy-group/${groupCode}`, data) +} + +export function updatePolicyGroupStatus(groupCode: string, status: number) { + return client.post(`/api/v1/policy-group/${groupCode}/status`, { status }) +} + +export function deletePolicyGroup(groupCode: string) { + return client.post(`/api/v1/policy-group/${groupCode}/delete`) +} diff --git a/admin-web/src/api/sendRecord.ts b/admin-web/src/api/sendRecord.ts new file mode 100644 index 0000000..3c8de4a --- /dev/null +++ b/admin-web/src/api/sendRecord.ts @@ -0,0 +1,31 @@ +import client from './client' + +export interface QueryRecordsParams { + taskId?: string + start?: string + end?: string + channelType?: string + status?: number | string + page?: number + size?: number +} + +export function queryRecords(params: QueryRecordsParams) { + return client.get('/api/v1/send/records', { params }) +} + +export function getTaskStatus(taskId: string) { + return client.get(`/api/v1/send/${taskId}/status`) +} + +export function cancelTask(taskId: string) { + return client.post(`/api/v1/send/${taskId}/cancel`) +} + +export function testSend(data: any) { + return client.post('/api/v1/send/test', data) +} + +export function getStatistics(start: string, end: string) { + return client.get('/api/v1/send/statistics', { params: { start, end } }) +} diff --git a/admin-web/src/components/Layout.vue b/admin-web/src/components/Layout.vue index 15444aa..bd42040 100644 --- a/admin-web/src/components/Layout.vue +++ b/admin-web/src/components/Layout.vue @@ -27,12 +27,21 @@ 入站流程 + + IVR 流程 + 流程追踪 消息测试 + + 入站 Webhook + + + 发送策略 + 租户管理 diff --git a/admin-web/src/router/index.ts b/admin-web/src/router/index.ts index 9b7d78a..84472df 100644 --- a/admin-web/src/router/index.ts +++ b/admin-web/src/router/index.ts @@ -8,10 +8,13 @@ import Person from '../views/Person.vue' import Tag from '../views/Tag.vue' import SendRecord from '../views/SendRecord.vue' import InboundFlow from '../views/InboundFlow.vue' +import IvrFlow from '../views/IvrFlow.vue' import FlowTrace from '../views/FlowTrace.vue' import MessageTest from '../views/MessageTest.vue' import TenantManagement from '../views/TenantManagement.vue' import UserManagement from '../views/UserManagement.vue' +import InboundWebhook from '../views/InboundWebhook.vue' +import SendPolicy from '../views/SendPolicy.vue' import Login from '../views/Login.vue' import { useAuthStore } from '../stores/auth' @@ -34,8 +37,11 @@ const routes = [ { path: 'tag', component: Tag, meta: { title: '标签' } }, { path: 'send-record', component: SendRecord, meta: { title: '发送记录' } }, { path: 'inbound-flow', component: InboundFlow, meta: { title: '入站流程' } }, + { path: 'ivr-flow', component: IvrFlow, meta: { title: 'IVR 流程' } }, { path: 'flow-trace', component: FlowTrace, meta: { title: '流程追踪' } }, { path: 'message-test', component: MessageTest, meta: { title: '消息测试' } }, + { path: 'inbound-webhook', component: InboundWebhook, meta: { title: '入站 Webhook' } }, + { path: 'send-policy', component: SendPolicy, meta: { title: '发送策略' } }, { path: 'tenant-management', component: TenantManagement, meta: { title: '租户管理' } }, { path: 'user-management', component: UserManagement, meta: { title: '用户管理' } }, ], diff --git a/admin-web/src/stores/auth.ts b/admin-web/src/stores/auth.ts index 8ee8c1f..5f74f6c 100644 --- a/admin-web/src/stores/auth.ts +++ b/admin-web/src/stores/auth.ts @@ -1,11 +1,21 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' +function parseJson(value: string | null): string[] { + if (!value) return [] + try { + const parsed = JSON.parse(value) + return Array.isArray(parsed) ? parsed : [] + } catch { + return [] + } +} + export const useAuthStore = defineStore('auth', () => { const token = ref(localStorage.getItem('mci-token') || '') const tenantCode = ref(localStorage.getItem('mci-tenant-code') || '') - const username = ref('') - const roleCodes = ref([]) + const username = ref(localStorage.getItem('mci-username') || '') + const roleCodes = ref(parseJson(localStorage.getItem('mci-role-codes'))) const isLoggedIn = computed(() => !!token.value) @@ -16,6 +26,8 @@ export const useAuthStore = defineStore('auth', () => { roleCodes.value = roles localStorage.setItem('mci-token', t) localStorage.setItem('mci-tenant-code', tc) + localStorage.setItem('mci-username', user) + localStorage.setItem('mci-role-codes', JSON.stringify(roles)) } function clearAuth() { @@ -25,6 +37,8 @@ export const useAuthStore = defineStore('auth', () => { roleCodes.value = [] localStorage.removeItem('mci-token') localStorage.removeItem('mci-tenant-code') + localStorage.removeItem('mci-username') + localStorage.removeItem('mci-role-codes') } return { token, tenantCode, username, roleCodes, isLoggedIn, setAuth, clearAuth } diff --git a/admin-web/src/views/ChannelAccount.vue b/admin-web/src/views/ChannelAccount.vue index 2620fb2..0c30cbb 100644 --- a/admin-web/src/views/ChannelAccount.vue +++ b/admin-web/src/views/ChannelAccount.vue @@ -1,68 +1,192 @@ + + diff --git a/admin-web/src/views/ChannelSubject.vue b/admin-web/src/views/ChannelSubject.vue index 736c9e4..ceacfda 100644 --- a/admin-web/src/views/ChannelSubject.vue +++ b/admin-web/src/views/ChannelSubject.vue @@ -1,59 +1,173 @@ + + diff --git a/admin-web/src/views/Conversation.vue b/admin-web/src/views/Conversation.vue index 5f90592..06e23ca 100644 --- a/admin-web/src/views/Conversation.vue +++ b/admin-web/src/views/Conversation.vue @@ -1,54 +1,99 @@ + + diff --git a/admin-web/src/views/FlowTrace.vue b/admin-web/src/views/FlowTrace.vue index 3b48999..1f35977 100644 --- a/admin-web/src/views/FlowTrace.vue +++ b/admin-web/src/views/FlowTrace.vue @@ -1,13 +1,9 @@ @@ -107,4 +110,10 @@ async function handleSubmit() { width: 100%; margin-top: 8px; } +.login-hint { + margin-top: 16px; + font-size: 12px; + color: #909399; + text-align: center; +} diff --git a/admin-web/src/views/MessageTest.vue b/admin-web/src/views/MessageTest.vue index ea162af..2b87ec0 100644 --- a/admin-web/src/views/MessageTest.vue +++ b/admin-web/src/views/MessageTest.vue @@ -1,6 +1,5 @@