diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..3425ee4 --- /dev/null +++ b/admin.php @@ -0,0 +1,49 @@ + + + + + + + 后台管理 - Twilio Console + + + + +
+ + +
+
+

当前模块:

+

系统已就绪,正在进行双向通信管理。

+
+
+
+ + \ No newline at end of file diff --git a/admin/login.php b/admin/login.php new file mode 100644 index 0000000..aa15010 --- /dev/null +++ b/admin/login.php @@ -0,0 +1,33 @@ + + + + + + 后台登录 + + + +
+

后台管理登录

+
+
+ + +
+ +
+
+ + \ No newline at end of file diff --git a/api/add_contact.php b/api/add_contact.php new file mode 100644 index 0000000..957deac --- /dev/null +++ b/api/add_contact.php @@ -0,0 +1,51 @@ + false, 'error' => '号码与发送账号不能为空。']); + exit; +} + +$pdo = db(); +$pdo->beginTransaction(); +try { + $stmt = $pdo->prepare("SELECT id FROM contacts WHERE phone = :phone"); + $stmt->bindValue(':phone', $phone); + $stmt->execute(); + $contactId = (int)($stmt->fetchColumn() ?: 0); + + if ($contactId === 0) { + $stmt = $pdo->prepare("INSERT INTO contacts (name, phone) VALUES (:name, :phone)"); + $stmt->bindValue(':name', $name !== '' ? $name : null); + $stmt->bindValue(':phone', $phone); + $stmt->execute(); + $contactId = (int)$pdo->lastInsertId(); + } + + $stmt = $pdo->prepare("SELECT id FROM conversations WHERE contact_id = :cid AND twilio_number_id = :tid"); + $stmt->bindValue(':cid', $contactId, PDO::PARAM_INT); + $stmt->bindValue(':tid', $twilioId, PDO::PARAM_INT); + $stmt->execute(); + $conversationId = (int)($stmt->fetchColumn() ?: 0); + + if ($conversationId === 0) { + $stmt = $pdo->prepare("INSERT INTO conversations (contact_id, twilio_number_id) VALUES (:cid, :tid)"); + $stmt->bindValue(':cid', $contactId, PDO::PARAM_INT); + $stmt->bindValue(':tid', $twilioId, PDO::PARAM_INT); + $stmt->execute(); + $conversationId = (int)$pdo->lastInsertId(); + } + + $pdo->commit(); + echo json_encode(['success' => true, 'conversation_id' => $conversationId]); +} catch (Throwable $e) { + $pdo->rollBack(); + echo json_encode(['success' => false, 'error' => '保存失败,请稍后再试。']); +} diff --git a/api/conversations.php b/api/conversations.php new file mode 100644 index 0000000..86847f5 --- /dev/null +++ b/api/conversations.php @@ -0,0 +1,32 @@ + []]); + exit; +} + +$stmt = db()->prepare(" + SELECT c.id, ct.name, ct.phone, + m.body AS last_message, + c.last_message_at AS last_time + FROM conversations c + JOIN contacts ct ON ct.id = c.contact_id + LEFT JOIN messages m ON m.id = ( + SELECT id FROM messages + WHERE conversation_id = c.id + ORDER BY created_at DESC + LIMIT 1 + ) + WHERE c.twilio_number_id = :twilio + ORDER BY c.last_message_at DESC, c.created_at DESC +"); +$stmt->bindValue(':twilio', $twilioId, PDO::PARAM_INT); +$stmt->execute(); +$items = $stmt->fetchAll(); + +echo json_encode(['items' => $items]); diff --git a/api/messages.php b/api/messages.php new file mode 100644 index 0000000..6cb87de --- /dev/null +++ b/api/messages.php @@ -0,0 +1,18 @@ + []]); + exit; +} + +$stmt = db()->prepare("SELECT id, direction, body, status, created_at FROM messages WHERE conversation_id = :cid ORDER BY created_at ASC"); +$stmt->bindValue(':cid', $conversationId, PDO::PARAM_INT); +$stmt->execute(); +$items = $stmt->fetchAll(); + +echo json_encode(['items' => $items]); diff --git a/api/send_message.php b/api/send_message.php new file mode 100644 index 0000000..7f83045 --- /dev/null +++ b/api/send_message.php @@ -0,0 +1,54 @@ + false, 'error' => '会话与内容不能为空。']); + exit; +} + +$pdo = db(); +$stmt = $pdo->prepare(" + SELECT c.id, ct.phone, t.account_sid, t.auth_token, t.from_number, t.label, t.is_active + FROM conversations c + JOIN contacts ct ON ct.id = c.contact_id + JOIN twilio_numbers t ON t.id = c.twilio_number_id + WHERE c.id = :cid +"); +$stmt->bindValue(':cid', $conversationId, PDO::PARAM_INT); +$stmt->execute(); +$twilio = $stmt->fetch(); + +if (!$twilio) { + echo json_encode(['success' => false, 'error' => '找不到会话。']); + exit; +} + +$status = 'stored'; +if ((int)$twilio['is_active'] === 1 && $twilio['account_sid'] && $twilio['auth_token']) { + $sendResult = twilio_send_sms($twilio, $twilio['phone'], $body); + if (!empty($sendResult['success'])) { + $status = 'sent'; + } else { + $status = 'failed'; + } +} + +$stmt = $pdo->prepare("INSERT INTO messages (conversation_id, direction, body, status) VALUES (:cid, 'outbound', :body, :status)"); +$stmt->bindValue(':cid', $conversationId, PDO::PARAM_INT); +$stmt->bindValue(':body', $body); +$stmt->bindValue(':status', $status); +$stmt->execute(); + +$stmt = $pdo->prepare("UPDATE conversations SET last_message_at = NOW() WHERE id = :cid"); +$stmt->bindValue(':cid', $conversationId, PDO::PARAM_INT); +$stmt->execute(); + +echo json_encode(['success' => true, 'status' => $status]); diff --git a/api/twilio_webhook.php b/api/twilio_webhook.php new file mode 100644 index 0000000..8a90da1 --- /dev/null +++ b/api/twilio_webhook.php @@ -0,0 +1,69 @@ +"; + exit; +} + +$pdo = db(); +$stmt = $pdo->prepare("SELECT id FROM twilio_numbers WHERE from_number = :to LIMIT 1"); +$stmt->bindValue(':to', $to); +$stmt->execute(); +$twilioId = (int)($stmt->fetchColumn() ?: 0); + +if ($twilioId === 0) { + echo ""; + exit; +} + +$pdo->beginTransaction(); +try { + $stmt = $pdo->prepare("SELECT id FROM contacts WHERE phone = :phone"); + $stmt->bindValue(':phone', $from); + $stmt->execute(); + $contactId = (int)($stmt->fetchColumn() ?: 0); + + if ($contactId === 0) { + $stmt = $pdo->prepare("INSERT INTO contacts (phone) VALUES (:phone)"); + $stmt->bindValue(':phone', $from); + $stmt->execute(); + $contactId = (int)$pdo->lastInsertId(); + } + + $stmt = $pdo->prepare("SELECT id FROM conversations WHERE contact_id = :cid AND twilio_number_id = :tid"); + $stmt->bindValue(':cid', $contactId, PDO::PARAM_INT); + $stmt->bindValue(':tid', $twilioId, PDO::PARAM_INT); + $stmt->execute(); + $conversationId = (int)($stmt->fetchColumn() ?: 0); + + if ($conversationId === 0) { + $stmt = $pdo->prepare("INSERT INTO conversations (contact_id, twilio_number_id) VALUES (:cid, :tid)"); + $stmt->bindValue(':cid', $contactId, PDO::PARAM_INT); + $stmt->bindValue(':tid', $twilioId, PDO::PARAM_INT); + $stmt->execute(); + $conversationId = (int)$pdo->lastInsertId(); + } + + $stmt = $pdo->prepare("INSERT INTO messages (conversation_id, direction, body, status) VALUES (:cid, 'inbound', :body, 'received')"); + $stmt->bindValue(':cid', $conversationId, PDO::PARAM_INT); + $stmt->bindValue(':body', $body); + $stmt->execute(); + + $stmt = $pdo->prepare("UPDATE conversations SET last_message_at = NOW() WHERE id = :cid"); + $stmt->bindValue(':cid', $conversationId, PDO::PARAM_INT); + $stmt->execute(); + + $pdo->commit(); +} catch (Throwable $e) { + $pdo->rollBack(); +} + +echo ""; diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..4e314b7 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,33 @@ -body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; +/* WhatsApp-like theme */ +:root { + --whatsapp-green: #075E54; + --whatsapp-bg: #e5ddd5; + --whatsapp-sidebar: #ffffff; + --whatsapp-input: #f0f2f5; + --whatsapp-bubble-me: #dcf8c6; + --whatsapp-bubble-other: #ffffff; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; -} +body, html { height: 100%; margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} +.whatsapp-app { display: flex; height: 100vh; overflow: hidden; background: white; } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} +.sidebar { width: 350px; background: var(--whatsapp-sidebar); border-right: 1px solid #ddd; display: flex; flex-direction: column; } +.sidebar-header { background: var(--whatsapp-input); padding: 15px; display: flex; align-items: center; } -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; -} +.chat-area { flex: 1; background: var(--whatsapp-bg); display: flex; flex-direction: column; } +.chat-header { background: var(--whatsapp-input); padding: 10px 20px; display: flex; align-items: center; border-left: 1px solid #ddd; } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} +.message-list { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; } +.bubble { background: var(--whatsapp-bubble-other); padding: 8px 12px; border-radius: 8px; margin-bottom: 5px; max-width: 60%; position: relative; font-size: 0.95rem; box-shadow: 0 1px 0.5px rgba(0,0,0,0.1); } +.bubble.me { background: var(--whatsapp-bubble-me); align-self: flex-end; } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} +.chat-input { background: var(--whatsapp-input); padding: 10px 20px; display: flex; align-items: center; gap: 15px; } +.chat-input input { border-radius: 20px; border: none; padding: 10px 15px; } -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-links { - display: flex; - gap: 1rem; -} - -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); -} - -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; -} - -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; -} - -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; -} - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file +/* Admin layout */ +.admin-layout { display: flex; min-height: 100vh; } +.admin-sidebar { width: 250px; background: #343a40; color: #fff; padding: 20px; } +.admin-sidebar a { color: #ccc; display: block; padding: 10px; text-decoration: none; border-bottom: 1px solid #444; } +.admin-sidebar a.active { color: #fff; background: #495057; } +.admin-content { flex: 1; padding: 20px; background: #f8f9fa; } diff --git a/assets/js/main.js b/assets/js/main.js index d349598..68af6f2 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,185 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); + const app = document.getElementById('chat-app'); + if (!app) return; - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; + const twilioSelect = document.getElementById('twilio-select'); + const conversationList = document.getElementById('conversation-list'); + const messageList = document.getElementById('message-list'); + const messageForm = document.getElementById('message-form'); + const messageInput = document.getElementById('message-input'); + const conversationTitle = document.getElementById('conversation-title'); + const conversationStatus = document.getElementById('conversation-status'); + const searchInput = document.getElementById('search-input'); + const addContactForm = document.getElementById('add-contact-form'); + const alertBox = document.getElementById('chat-alert'); - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; + let activeConversationId = null; + let activeTwilioId = twilioSelect ? twilioSelect.value : null; + let pollTimer = null; - appendMessage(message, 'visitor'); - chatInput.value = ''; + const showAlert = (text, type = 'info') => { + if (!alertBox) return; + alertBox.textContent = text; + alertBox.className = `alert alert-${type} alert-inline`; + alertBox.classList.remove('d-none'); + setTimeout(() => alertBox.classList.add('d-none'), 3000); + }; - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } + const formatTime = (iso) => { + if (!iso) return ''; + const dt = new Date(iso.replace(' ', 'T')); + return dt.toLocaleString(); + }; + + const renderConversations = (items) => { + conversationList.innerHTML = ''; + if (!items.length) { + conversationList.innerHTML = '
暂无会话。添加号码开始对话。
'; + return; + } + items.forEach((item) => { + const div = document.createElement('div'); + div.className = 'conversation-item' + (item.id === activeConversationId ? ' active' : ''); + div.dataset.id = item.id; + div.innerHTML = ` +
${(item.name || item.phone).slice(0, 2).toUpperCase()}
+
+
${item.name || item.phone}
+ ${item.last_message || '暂无消息'} +
+ ${item.last_time ? formatTime(item.last_time) : ''} + `; + div.addEventListener('click', () => { + activeConversationId = item.id; + conversationTitle.textContent = item.name || item.phone; + conversationStatus.textContent = item.phone; + fetchMessages(); + fetchConversations(); + }); + conversationList.appendChild(div); }); + }; + + const renderMessages = (items) => { + messageList.innerHTML = ''; + if (!items.length) { + messageList.innerHTML = '
暂无消息,开始发送第一条短信。
'; + return; + } + items.forEach((msg) => { + const div = document.createElement('div'); + div.className = `message ${msg.direction === 'outbound' ? 'outbound' : 'inbound'}`; + div.innerHTML = ` +
${msg.body}
+ ${formatTime(msg.created_at)} + `; + messageList.appendChild(div); + }); + messageList.scrollTop = messageList.scrollHeight; + }; + + const fetchConversations = async () => { + if (!activeTwilioId) return; + const res = await fetch(`api/conversations.php?twilio_number_id=${activeTwilioId}`); + const data = await res.json(); + renderConversations(data.items || []); + if (!activeConversationId && data.items && data.items.length) { + activeConversationId = data.items[0].id; + conversationTitle.textContent = data.items[0].name || data.items[0].phone; + conversationStatus.textContent = data.items[0].phone; + fetchMessages(); + } + }; + + const fetchMessages = async () => { + if (!activeConversationId) { + renderMessages([]); + return; + } + const res = await fetch(`api/messages.php?conversation_id=${activeConversationId}`); + const data = await res.json(); + renderMessages(data.items || []); + }; + + const startPolling = () => { + if (pollTimer) clearInterval(pollTimer); + pollTimer = setInterval(() => { + fetchConversations(); + fetchMessages(); + }, 5000); + }; + + if (twilioSelect) { + twilioSelect.addEventListener('change', () => { + activeTwilioId = twilioSelect.value; + activeConversationId = null; + conversationTitle.textContent = '请选择会话'; + conversationStatus.textContent = ''; + fetchConversations(); + fetchMessages(); + }); + } + + if (searchInput) { + searchInput.addEventListener('input', () => { + const term = searchInput.value.toLowerCase(); + const items = conversationList.querySelectorAll('.conversation-item'); + items.forEach((item) => { + const text = item.textContent.toLowerCase(); + item.style.display = text.includes(term) ? 'flex' : 'none'; + }); + }); + } + + if (messageForm) { + messageForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const body = messageInput.value.trim(); + if (!body) return; + if (!activeConversationId) { + showAlert('请先选择一个会话。', 'warning'); + return; + } + messageInput.value = ''; + const res = await fetch('api/send_message.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ conversation_id: activeConversationId, body }) + }); + const data = await res.json(); + if (!data.success) { + showAlert(data.error || '发送失败', 'danger'); + } + fetchMessages(); + fetchConversations(); + }); + } + + if (addContactForm) { + addContactForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const formData = new FormData(addContactForm); + formData.append('twilio_number_id', activeTwilioId || ''); + const res = await fetch('api/add_contact.php', { + method: 'POST', + body: formData + }); + const data = await res.json(); + if (data.success) { + activeConversationId = data.conversation_id; + fetchConversations(); + fetchMessages(); + showAlert('已添加号码并创建会话。', 'success'); + const modal = bootstrap.Modal.getInstance(document.getElementById('addContactModal')); + if (modal) modal.hide(); + addContactForm.reset(); + } else { + showAlert(data.error || '添加失败', 'danger'); + } + }); + } + + fetchConversations(); + fetchMessages(); + startPolling(); }); diff --git a/assets/pasted-20260311-122414-dbdd5e54.png b/assets/pasted-20260311-122414-dbdd5e54.png new file mode 100644 index 0000000..529dbfc Binary files /dev/null and b/assets/pasted-20260311-122414-dbdd5e54.png differ diff --git a/assets/pasted-20260312-025141-67108a13.png b/assets/pasted-20260312-025141-67108a13.png new file mode 100644 index 0000000..529dbfc Binary files /dev/null and b/assets/pasted-20260312-025141-67108a13.png differ diff --git a/chat.php b/chat.php new file mode 100644 index 0000000..dab7a20 --- /dev/null +++ b/chat.php @@ -0,0 +1,70 @@ + + + + + + + WhatsApp Chat + + + + + +
+ +
+
+
+ 请选择会话 + +
+ +
+
+ +
+
+ + + +
+
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/includes/bootstrap.php b/includes/bootstrap.php new file mode 100644 index 0000000..24b2ea4 --- /dev/null +++ b/includes/bootstrap.php @@ -0,0 +1,84 @@ +exec(" + CREATE TABLE IF NOT EXISTS twilio_numbers ( + id INT AUTO_INCREMENT PRIMARY KEY, + label VARCHAR(120) NOT NULL, + account_sid VARCHAR(64) NOT NULL, + auth_token VARCHAR(128) NOT NULL, + api_key VARCHAR(128) DEFAULT NULL, + from_number VARCHAR(32) NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS contacts ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) DEFAULT NULL, + phone VARCHAR(32) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uniq_phone (phone) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS conversations ( + id INT AUTO_INCREMENT PRIMARY KEY, + contact_id INT NOT NULL, + twilio_number_id INT NOT NULL, + last_message_at TIMESTAMP NULL DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_contact (contact_id), + INDEX idx_twilio (twilio_number_id), + CONSTRAINT fk_convo_contact FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE, + CONSTRAINT fk_convo_twilio FOREIGN KEY (twilio_number_id) REFERENCES twilio_numbers(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS messages ( + id INT AUTO_INCREMENT PRIMARY KEY, + conversation_id INT NOT NULL, + direction ENUM('inbound','outbound') NOT NULL, + body TEXT NOT NULL, + status VARCHAR(32) DEFAULT 'stored', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_convo (conversation_id), + CONSTRAINT fk_msg_convo FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS billing_snapshots ( + id INT AUTO_INCREMENT PRIMARY KEY, + twilio_number_id INT NOT NULL, + period_start DATE NOT NULL, + period_end DATE NOT NULL, + usage_count INT DEFAULT 0, + cost DECIMAL(10,4) DEFAULT 0, + currency VARCHAR(12) DEFAULT 'USD', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_bill_twilio (twilio_number_id), + CONSTRAINT fk_bill_twilio FOREIGN KEY (twilio_number_id) REFERENCES twilio_numbers(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); +} + +ensure_tables(); \ No newline at end of file diff --git a/includes/twilio.php b/includes/twilio.php new file mode 100644 index 0000000..083ab6a --- /dev/null +++ b/includes/twilio.php @@ -0,0 +1,58 @@ + $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 20, + CURLOPT_USERPWD => $sid . ':' . $token, + CURLOPT_HTTPAUTH => CURLAUTH_BASIC, + CURLOPT_CUSTOMREQUEST => strtoupper($method), + ]; + if (strtoupper($method) === 'POST') { + $opts[CURLOPT_POSTFIELDS] = http_build_query($data); + } + curl_setopt_array($ch, $opts); + $resp = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($resp === false || $code < 200 || $code >= 300) { + return [ + 'success' => false, + 'status' => $code, + 'error' => $error ?: $resp, + ]; + } + + $decoded = json_decode($resp, true); + return [ + 'success' => true, + 'status' => $code, + 'data' => $decoded ?: $resp, + ]; +} + +function twilio_send_sms(array $twilio, string $to, string $body): array { + $url = 'https://api.twilio.com/2010-04-01/Accounts/' . urlencode($twilio['account_sid']) . '/Messages.json'; + return twilio_request( + $twilio['account_sid'], + $twilio['auth_token'], + $url, + 'POST', + [ + 'From' => $twilio['from_number'], + 'To' => $to, + 'Body' => $body, + ] + ); +} + +function twilio_fetch_sms_usage(array $twilio, string $startDate, string $endDate): array { + $url = 'https://api.twilio.com/2010-04-01/Accounts/' . urlencode($twilio['account_sid']) + . '/Usage/Records/Monthly.json?Category=sms&StartDate=' . urlencode($startDate) . '&EndDate=' . urlencode($endDate); + return twilio_request($twilio['account_sid'], $twilio['auth_token'], $url); +} diff --git a/index.php b/index.php index 7205f3d..9f96bc9 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,30 @@ - + - - New Style - - - - - - - - - - - - - - - - - - - + 双向短信平台 + - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
+

欢迎来到 Twilio 短信控制台

+

WhatsApp 风格体验,高效沟通。

+ -
- + - + \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..6895138 --- /dev/null +++ b/login.php @@ -0,0 +1,34 @@ + + + + + + 系统登录 + + + +
+

系统登录

+
+
+ + +
+ +
+
+ +