使用 Next.js、Novu 和 Appwrite 构建具有实时聊天和通知功能的客户支持应用程序
在本文中,我将向您介绍如何使用 Next.js、Novu、Appwrite 和 EmailJS 构建具有实时聊天、应用内和电子邮件通知功能的客户支持应用程序。
Appwrite Cloud处理身份验证、数据库和文件存储方面,Novu处理应用内通知,EmailJS处理电子邮件消息传递。
完成后,您将能够使用 Appwrite 构建需要聊天功能的高级应用程序,并通过 EmailJS 和 Novu 向您的应用程序添加电子邮件和应用内通知。
💡 PS:本教程假设您具有 React 或 Next.js 的基础知识

申请流程
在开始编码之前,让我先总结一下该应用程序的工作原理。该应用程序执行以下操作:
- 允许客户创建支持票,
- 对员工进行身份验证,并确保除员工之外没有人可以登录应用程序,
- 当客户创建支持单时通知工作人员,
- 允许员工和客户通过Appwrite Cloud提供的实时聊天功能解决纠纷,并且
- 当客户开具票据时以及当工作人员向他们发送有关开具票据的消息时,向客户发送确认电子邮件。
简要演示
UI设计流程
在这里,我将引导您创建 Web 应用程序所需的页面。
首先,您需要创建一个主页,客户可以在该主页上创建支持票,工作人员也可以通过该页面导航到登录页面。
接下来,创建员工登录页面,对访问者进行身份验证,以确保他们是员工,然后才允许他们访问仪表板。
您还需要一个仪表板页面,根据工单状态显示所有工单。每张支持工单都必须可点击,并将工作人员重定向到其详细信息页面。

接下来,创建一个页面,显示与特定工单相关的所有数据。工作人员可以更新支持工单的状态并下载其附件(如果有)。

然后,创建一个聊天页面,以便客服人员和客户可以实时沟通问题。
该页面无需身份验证,只需将访问代码发送到客户的邮箱即可。聊天 URL 类似于此https://firm-support.vercel.app/chat/<ticket_ID>
。

最后,您需要一个管理页面,用于在应用程序中添加或删除员工。这样,您就可以让多个用户管理支持工单或回复客户的疑问。
将 Appwrite 添加到 Next.js 应用程序
Appwrite是一个强大的开源后端平台,可帮助您创建安全且可扩展的 Web 和移动应用程序。使用 Appwrite,您无需担心应用程序的后端资源,因为 Appwrite 可以实现“快速构建、大规模扩展、一站式”的体验。
Appwrite Cloud 使您无需在计算机上设置后端服务器,而是专注于开发应用程序;同时,它管理关键的后端功能,例如用户身份验证、数据库管理、文件存储等。
安装和配置
要将 Appwrite 添加到 Next.js 应用,请按照以下步骤操作:
首先,通过运行下面的代码创建一个 Next.js 项目。
npx create-next-app customer-support-app
访问Appwrite 的网站并创建一个新帐户。
创建新的组织和项目。每个项目都包含构建功能齐全的应用程序所需的所有资源。
接下来,您需要选择在何处以及如何使用 Appwrite,可以作为 Web 或移动 SDK,或者您需要将其与您的(现有)服务器集成。
由于我们正在使用 Appwrite Cloud 构建 Next.js 应用程序,因此从 SDK 平台菜单中选择 Web 应用程序并在项目下注册一个新应用程序。
从下图中,我为应用程序提供了一个名称,并使用星号作为主机名。在 Vercel 上部署应用程序后,您可以将主机名更改为Vercel提供的 URL 。
如下所示将 Appwrite Node.js SDK 安装到您的 Next.js 项目中。
npm install appwrite
在项目根目录下创建一个.env.local
文件。appwrite.js
touch appwrite.js .env.local
将下面的代码复制到appwrite.js
文件中。
import { Client, Account, Databases, Storage } from "appwrite";
const client = new Client();
client
.setEndpoint("https://cloud.appwrite.io/v1")
.setProject(process.env.NEXT_PUBLIC_PROJECT_ID);
export const account = new Account(client);
export const db = new Databases(client);
export const storage = new Storage(client);
上面的代码片段使我们能够访问并与 Appwrite 提供的身份验证、数据库和文件存储功能进行交互。
将下面的代码复制到.env.local
文件中。
NEXT_PUBLIC_PROJECT_ID=<your_project_id>
NEXT_PUBLIC_DB_ID=<your_database_id>
NEXT_PUBLIC_TICKETS_COLLECTION_ID=<your_collection_id>
NEXT_PUBLIC_USERS_COLLECTION_ID=<your_collection_id>
NEXT_PUBLIC_BUCKET_ID=<your_bucket_id>
上面的代码片段包含环境变量,其中包含与 Appwrite Cloud 交互所需的所有私钥。
在您的项目仪表板上,单击Project ID
按钮复制项目的 ID 并将其粘贴到.env.local
变量中。
使用 Appwrite Cloud 设置身份验证
由于您使用的是电子邮件和密码身份验证方法,因此在使用该服务之前无需在 Appwrite Cloud 上添加任何配置,因为它已默认配置。
但是,让我们通过更新默认会话长度来为项目添加额外的安全性。
Auth
从侧边栏菜单中选择并切换到Security
选项卡。
向下滚动到“会话时长”部分,并将其从 更改365 days
为1 hour
。用户使用应用程序一小时后需要重新进行身份验证,并且如果用户未退出我们的应用程序,则会在一小时后自动注销。
单击Update
添加新设置,即可开始使用。🚀
设置 Appwrite 数据库
在这里,您将学习如何在 Appwrite Cloud 上设置数据库。
Database
从侧边栏菜单中选择创建一个新的数据库。
接下来,您需要创建两个数据库集合,一个用于支持工单,另一个用于应用程序内的员工。
您可能想知道为什么我们需要为用户创建另一个集合。那么在 Appwrite Auth 上保存的用户呢?
原因是您无法使用 Appwrite Cloud获取所有用户或删除某个用户。只有使用Node.js SDK时才可以。
但是,由于我们需要从应用程序的管理页面查看所有用户(员工)并在必要时添加或删除;因此,您需要创建一个users
包含与 Auth 部分类似信息的数据库。
为支持票证和用户创建两个数据库集合,并将他们的 ID 复制到env.local
文件中。
选择users
集合并创建三个必需的属性,如下所示。
最后,单击下方的“设置”菜单,users
并更新权限以仅允许用户从集合中创建、读取和删除用户。
设置 Appwrite 存储
从侧边栏菜单中选择Storage
,并为每个支持工单附带的图片创建一个新的存储桶。用户可以在创建支持工单时上传他们遇到的问题的屏幕截图。
复制存储桶 ID 并将其粘贴到.env.local
文件中。
与 Appwrite 通信:验证用户身份
与传统应用程序不同,此应用程序没有注册页面,因为它是员工专用的。因此,您可以通过 Appwrite Cloud 上的仪表板创建初始帐户。
在本节中,您将学习如何为应用程序设置身份验证流程。
您可以创建一个utils
包含函数的文件夹,并将其导入到所需的组件中。
mkdir utils
cd utils
touch functions.js
将以下导入添加到文件中,以便我们能够与后端功能进行交互。我们将在接下来的部分中使用它们。
import { account, db, storage } from "./appwrite";
import { ID } from "appwrite";
登录应用程序
记住,我们有 Appwrite Auth 和users
数据库上的集合来存储用户的详细信息。因此,要将用户登录到应用程序,您需要使用 Appwrite Auth 对用户进行身份验证,然后过滤集合users
以验证用户的详细信息是否存在,然后再授予对应用程序的访问权限。
//👇🏻 filters the users' list
const checkUserFromList = async (email, router) => {
try {
const response = await db.listDocuments(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_USERS_COLLECTION_ID
);
const users = response.documents;
const result = users.filter((user) => user.email === email);
//👉🏻 USER OBJECT ==> console.log(result[0])
if (result.length > 0) {
successMessage("Welcome back 🎉");
router.push("/staff/dashboard");
} else {
errorMessage("Unauthorized...Contact Management.");
}
} catch (error) {
errorMessage("An error occurred 😪");
console.error(error);
}
};
//👇🏻 authenticates the user
export const logIn = async (email, password, router) => {
try {
//👇🏻 Appwrite login method
await account.createEmailSession(email, password);
//👇🏻 calls the filter function
await checkUserFromList(email, router);
} catch (error) {
console.log(error);
errorMessage("Invalid credentials ❌");
}
};
上面的代码片段从表单字段接受用户的电子邮件和密码,使用 Appwrite Auth 对用户进行身份验证,然后在授予应用程序权限之前检查用户是否在员工列表中。
注销
注销用户仍然可以通过常规方式进行。Appwrite 还提供了一种account.deleteSession()
方法,允许用户退出正在进行的会话。
export const logOut = async (router) => {
try {
await account.deleteSession("current");
router.push("/");
successMessage("See ya later 🎉");
} catch (error) {
console.log(error);
errorMessage("Encountered an error 😪");
}
};
保护页面免受未经身份验证的用户的访问
为此,您可以将用户的信息对象存储到登录后的状态中,或者使用 Appwrite 的account.get()
方法。
使用account.get()
方法:
export const checkAuthStatus = async (setUser, setLoading, router) => {
try {
const response = await account.get();
setUser(response);
setLoading(false);
} catch (err) {
router.push("/");
console.error(err);
}
};
上面的代码片段获取了与当前登录用户相关的所有信息。它检查用户是否处于活动状态,并返回包含所有用户详细信息的对象。
您可以在页面加载时对包含受保护数据的路由执行该功能,例如仪表板、票证详细信息和管理路由。
管理页面:添加和删除员工
在本节中,我将指导您如何在用户集合中添加、获取和删除员工。从技术上讲,我们正在构建管理页面的后端功能。
添加新员工
下面的代码片段在您提交表单时接受用户的姓名、电子邮件和密码,在 Appwrite Auth 上创建一个帐户,并将电子邮件和姓名保存在users
数据库中。
//👇🏻 generates random string as ID
const generateID = () => Math.random().toString(36).substring(2, 24);
export const addUser = async (name, email, password) => {
try {
//👇🏻 create a new acct on Appwrite Auth
await account.create(generateID(), email, password, name);
//👇🏻 adds the user's details to the users database
await db.createDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_USERS_COLLECTION_ID,
ID.unique(),
{ user_id: generateID(), name, email }
);
successMessage("User added successfully 🎉");
} catch (error) {
console.log(error);
}
};
获取员工名单
代码片段访问 Appwrite Cloud 上的文档并返回users
集合中的所有数据。
export const getUsers = async (setUsers) => {
try {
const response = await db.listDocuments(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_USERS_COLLECTION_ID
);
setUsers(response.documents);
} catch (error) {
console.log(error);
}
};
移除员工
为此,您需要在单击“删除”按钮时将选定员工的 ID 传递到函数中。
export const deleteUser = async (id) => {
try {
await db.deleteDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_USERS_COLLECTION_ID,
id
);
successMessage("User removed 🎉"); // Success
} catch (error) {
console.log(error); // Failure
errorMessage("Encountered an error 😪");
}
};
💡 用户的凭据仍然在 Appwrite Auth 中可用,但由于我们
users
在用户登录时根据集合验证用户的凭据,因此此方法仍然有效。
支持票:数据结构、数据库和存储
在本节中,您将学习如何为客户支持应用程序创建后端(数据库)。
创建支持票
当客户创建支持工单时,您必须从表单中检索所有详细信息并将其保存到数据库。因此,您需要创建一个函数,该函数接受所有工单的详细信息,并确保无论客户是否上传他们所遇到问题的屏幕截图,该函数都能完美运行。
export const sendTicket = async (name, email, subject, message, attachment) => {
if (attachment !== null) {
//👇🏻 Customer attached an image
console.log({ name, email, subject, message, attachment });
} else {
//👇🏻 No attachment
console.log({ name, email, subject, message });
}
};
要将票证的信息添加到tickets
Appwrite Cloud 上的集合,请将以下属性添加到集合中。
姓名 | 必需的? | 类型 |
---|---|---|
姓名 | 是的 | 细绳 |
电子邮件 | 是的 | 细绳 |
主题 | 是的 | 细绳 |
地位 | 是的 | 细绳 |
内容 | 是的 | 细绳 |
访问代码 | 是的 | 细绳 |
附件网址 | 不 | 细绳 |
消息 | 细绳[] |
切换到“设置”选项卡并更新集合的权限tickets
,如下所示。
最后,更新sendTicket
功能以将事件详细信息添加到 Appwrite Cloud。
export const sendTicket = async (name, email, subject, message, attachment) => {
const createTicket = async (file_url = "https://google.com") => {
try {
const response = await db.createDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
ID.unique(),
{
name,
email,
subject,
content: message,
status: "open",
messages: [
JSON.stringify({
id: generateID(),
content: message,
admin: false,
name: "Customer",
}),
],
attachment_url: file_url,
access_code: generateID(),
}
);
//👇🏻 send notification to the customer
console.log("RESPONSE >>>", response);
successMessage("Ticket created 🎉");
} catch (error) {
errorMessage("Encountered saving ticket ❌");
}
};
if (attachment !== null) {
try {
const response = await storage.createFile(
process.env.NEXT_PUBLIC_BUCKET_ID,
ID.unique(),
attachment
);
const file_url = `https://cloud.appwrite.io/v1/storage/buckets/${process.env.NEXT_PUBLIC_BUCKET_ID}/files/${response.$id}/view?project=${process.env.NEXT_PUBLIC_PROJECT_ID}&mode=admin`;
//👇🏻 creates ticket with its image
createTicket(file_url);
} catch (error) {
errorMessage("Error uploading the image ❌");
}
} else {
//👇🏻 creates ticket even without an image (screenshot)
await createTicket();
}
};
- 从上面的代码片段来看:
- 嵌套函数
createTicket
接受所有工单的属性,并在 Appwrite Cloud 上创建一个新文档。由于上传附件是可选的,因此该flier_url
属性具有默认的 URL 值。 - array
messages
属性为实时聊天功能创建了一个新结构。它将内容和客户信息转换为 JSON 字符串,并将其添加到messages
数组中。 - if 和 else 代码块会检查客户是否上传了图片。如果是,代码会将图片上传到 Cloud Storage,检索其 URL,并将其传递给
createTicket
函数。 - 否则,该
createTicket
函数使用默认值(https://google.com
)作为附件 URL。
- 嵌套函数
从 Appwrite Cloud 获取支持票
在仪表板页面上,您需要检索所有支持工单。下面的函数将检索它们并根据其状态进行分组。
export const getTickets = async (
setOpenTickets,
setInProgressTickets,
setCompletedTickets
) => {
try {
const response = await db.listDocuments(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID
);
const tickets = response.documents;
const openTickets = tickets.filter((ticket) => ticket.status === "open");
const inProgressTickets = tickets.filter(
(ticket) => ticket.status === "in-progress"
);
const completedTickets = tickets.filter(
(ticket) => ticket.status === "completed"
);
setCompletedTickets(completedTickets);
setOpenTickets(openTickets);
setInProgressTickets(inProgressTickets);
} catch (error) {
console.log(error); // Failure
}
};
获取票务详情
当您单击仪表板上的每个支持票时,它需要将您重定向到另一个包含与该支持票相关的所有信息页面。
因此,您需要创建一个文件,使用页面路由ticket/[id].js
通过服务器端渲染检索票证的详细信息,如下所示。id
export async function getServerSideProps(context) {
let ticketObject = {};
try {
const response = await db.getDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
context.query.id
);
ticketObject = response;
} catch (err) {
ticketObject = {};
}
return {
props: { ticketObject },
};
}
更新工单状态
在详细信息页面上,您可以使用 HTML select 标签更新支持工单的状态,并接受三种类型的值 - "open"
、"in-progress"
和"completed"
。您可以使用以下函数更新状态:
export const updateTicketStatus = async (id, status) => {
try {
await db.updateDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
id,
{ status }
);
successMessage("Status updated, refresh page 🎉");
} catch (error) {
console.log(error); // Failure
errorMessage("Encountered an error ❌");
}
};
使用 Next.js 和 Appwrite 的实时聊天功能
在本节中,我将指导您向应用程序添加聊天功能。为此,请创建一个类似下图的聊天页面。
此页面在加载时需要访问代码,但无需身份验证即可访问。当用户发送消息时,运行以下代码片段。
export const sendMessage = async (text, docId) => {
//👇🏻 get the ticket ID
const doc = await db.getDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
docId
);
try {
//👇🏻 gets the user's object (admin)
const user = await account.get();
const result = await db.updateDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
docId,
{
messages: [
...doc.messages,
JSON.stringify({
id: generateID(),
content: text,
admin: true,
name: user.name,
}),
],
}
);
//👇🏻 message was added successfully
if (result.$id) {
successMessage("Message Sent! ✅");
//👉🏻 email the customer with access code and chat URL
} else {
errorMessage("Error! Try resending your message❌");
}
} catch (error) {
//👇🏻 means the user is a customer
const result = await db.updateDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
docId,
{
messages: [
...doc.messages,
JSON.stringify({
id: generateID(),
content: text,
admin: false,
name: "Customer",
}),
],
}
);
if (result.$id) {
successMessage("Message Sent! ✅");
//👉🏻 notify staff via notifications
} else {
errorMessage("Error! Try resending your message❌");
}
}
};
上面的代码片段在将消息添加到数组之前检查用户是员工还是客户messages
。
实时消息传递
立即显示您发送的聊天消息。
在集合上创建一个事件监听器tickets
,并messages
使用更改更新数组,如下所示。
//👉🏻 "client" is from your appwrite.js file
useEffect(() => {
const unsubscribe = client.subscribe(
`databases.${process.env.NEXT_PUBLIC_DB_ID}.collections.${process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID}.documents`,
(data) => {
const messages = data.payload.messages;
setMessages(messages.map(parseJSON));
}
);
return () => {
unsubscribe();
};
}, []);
您可以在 Appwrite 中了解有关实时监听器的更多信息。
自动滚动功能
div
在聊天消息元素下方添加一个空白。
<div className='chat__container'>
//👇🏻 chat messages element
{/* {messages.map((message) => (
<div>{message}</div>
))} */}
<div ref={lastMessageRef} />
</div>
创建对元素的引用div
,并在有新消息时转移鼠标焦点,如下所示。
const lastMessageRef = useRef(null);
useEffect(() => {
// 👇️ scroll to bottom every time messages change
lastMessageRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
使用 Novu 和 EmailJS 添加应用内和电子邮件通知
在这里,您将学习如何使用 Novu 添加应用内通知以及如何使用 EmailJS 发送电子邮件。
回想一下,当客户通过聊天页面发送消息以及创建新工单时,我们需要应用内通知。
当客服人员发送消息以及客户创建新工单时,我们需要电子邮件通知。
在 Next.js 中使用 Novu 设置应用内通知
Novu是第一个管理所有通信形式的开源通知基础架构。在本文中,我们将使用其应用内通知功能。
通过运行以下代码安装 Novu Node.js SDK 及其通知中心。
npm install @novu/node @novu/notification-center
运行npx novu init
以创建并访问您的仪表板。
将您的 Novu 订阅者 ID、应用程序 ID 和 API 密钥添加到.env.local
文件中。
NEXT_PUBLIC_NOVU_SUBSCRIBER_ID=<subscriber_ID>
NEXT_PUBLIC_NOVU_APP_ID=<your_app_ID>
NEXT_PUBLIC_NOVU_API_KEY=<your_api_key>
在您的 Novu 仪表板上创建两个通知模板,并针对两种情况(客户创建工单和发送聊天消息)编辑其内容。将通知功能添加到您的 Next.js 应用中,如下所示。
const { Novu } = require("@novu/node");
const novu = new Novu(process.env.NEXT_PUBLIC_NOVU_API_KEY);
export default async function handler(req, res) {
const { status, title, username } = req.body;
const response = await novu
.trigger("<template_name>", {
to: {
subscriberId: process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID,
},
payload: {
status,
title,
username,
},
})
.catch((err) => console.error(err));
res.status(200).json(response.data);
}
最后,创建通知铃以在您的应用程序内显示通知,并将其添加到应用程序中的导航组件。
import {
NovuProvider,
PopoverNotificationCenter,
NotificationBell,
} from "@novu/notification-center";
function Novu() {
function onNotificationClick(message) {
if (message?.cta?.data?.url) {
window.location.href = message.cta.data.url;
}
}
return (
<NovuProvider
subscriberId={process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID}
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID}
>
<PopoverNotificationCenter
onNotificationClick={onNotificationClick}
colorScheme='light'
>
{({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
</PopoverNotificationCenter>
</NovuProvider>
);
}
export default Novu;
使用 EmailJS 发送电子邮件通知
在这里,您需要在客户创建支持工单时以及工作人员向其发送消息时向其发送电子邮件。请按照以下步骤操作。
通过运行以下代码安装 EmailJS。
npm install @emailjs/browser
配置您的EmailJS 帐户并将您的凭据复制到.env.local
文件中。
NEXT_PUBLIC_EMAIL_SERVICE_ID=<your_service_id>
NEXT_PUBLIC_EMAIL_API_KEY=<your_api_key>
NEXT_PUBLIC_TICKET_CREATION_ID=<template_id>
NEXT_PUBLIC_NEW_MESSAGE_ID=<template_id>
为这两种情况创建模板并向您的客户发送电子邮件通知。
总结
愿意工作🙂
你喜欢这篇文章吗?或者你需要一位经验丰富的 React 技术作家和开发人员来担任远程、全职或合同制职位?欢迎随时联系我。GitHub || LinkedIn
|| Twitter
