تطبيقات المراسلة الفورية ، بما في ذلك الخادم والإدارة والعميل
تم نشره وإطلاقه ، مرحبًا بك في تجربة جانب العميل والإدارة
يرجى عدم تغيير الدور الافتراضي والأذونات في الإرادة. يرجى الشعور بالحب قليلاً ولا تشكل بعض الأسماء غير المتحضرة.
باستخدام إطار البيض ، جانب الخادم من خدمة IM
منذ تطوير الإنترنت عبر الهاتف المحمول ، تم دمج خدمات المراسلة الفورية بقيادة WeChat في كل ركن من أركان حياتنا وتلعب أيضًا دورًا مهمًا في بعض أعمال الشركة. استخدمت شركتنا خدمات المراسلة الفورية ، ولكن لا يمكن تحقيق العديد من الاحتياجات المخصصة. لذلك ، تقرر تطوير خدمة مراسلة فورية تلبي الاحتياجات المخصصة داخليًا.
تم استخدام إطار socket.io لأن الواجهة الخلفية كانت أقل من الناس في ذلك الوقت. بعد قراءة بعض الأمثلة ، اعتقدت أنه كان مناسبًا حقًا لاستخدام ودعم على النظام الأساسي بأكمله. لذلك ، تم تنفيذ هذه الخدمات المجهرية في الفريق الأمامي ، والتأثير جيد جدًا حاليًا.
يحتوي المجتمع حاليًا على محتوى أقل أو بسيطًا جدًا في هذا المجال (لا يوجد سوى غرفة دردشة عامة واحدة). بالإضافة إلى ذلك ، من غير المريح للغاية أن أكون رئيسًا للاتصالات أثناء عملية تطوير الأعمال ، لذلك أريد الابتعاد عن بعض الأشياء التجارية الفريدة وأدرك تطبيق IM بسيطًا وكاملًا لا يمزج بين أعمال الشركة مع وظائف بسيطة ، بما في ذلك الخادم والإدارة والعميل. كائن تقليد العميل هو WeChat ، لأنني على دراية به ولا يجب أن أفكر كثيرًا في المنتج. بالإضافة إلى ذلك ، فإن الأشخاص الذين يجربونها على دراية بها ولا يحتاجون إلى الكثير من تكاليف الاتصال.
لتطوير مجموعة كاملة من خدمات المراسلة الفورية ، هناك حاجة إلى الأجزاء التالية:
ولد لأطر والتطبيقات على مستوى المؤسسة
السبب في أنني اخترت إطار عمل Alibaba's Egg.js كدعم هو أنهم قاموا بعمل جيد في التنفيذ والأمان على نطاق واسع داخلها. السبب في أنهم لم يختاروا Nest هو أنه من المزعج دمج socket.io . يستخدم ORM تتمة ، وقاعدة البيانات هي MySQL. لقد استخدمته من قبل ، لذلك من الصعب البدء.
حل الواجهة الأمامية/التصميم خارج الصندوق
سبب اختيار ANT Design Pro هو أنني على دراية بـ Vue Family Bucket. أريد أن أغتنم هذه الفرصة للتعرف على عملية تطوير النظام الإيكولوجي لـ React بأكمله وأشعر بالاختلافات الأساسية والمسارات المختلفة لأطر التنمية الرئيسية في الصين. تم إصدار ANT Design Pro لعدة سنوات ، وقد جلب بالفعل تحسينات في الكفاءة على الشركات الصغيرة والمتوسطة الحجم ، وهو أمر مناسب فقط لاحتياجاتي.
الأدوات القياسية التي طورتها Vue.js
استخدم @Vue/CLI لإنشاء عميل خدمة IM ، ومشروع H5 للهاتف المحمول ، ويستخدم إطار واجهة المستخدم Youzan Vant ، والذي يدمج مكوني مفتوح المصدر Vue-Page و Mr. Huang أفضل لتحقيق الوظائف الأساسية لـ IM
كمهندس واجهة ، لا تتطلب معظم المهام اليومية التفكير في علاقات الكيان. ومع ذلك ، بناءً على تجربتي الفعلية ، يمكن أن يساعدنا فهم علاقات الكيان على فهم نماذج الأعمال بشكل أفضل. تحسين فهم المنتجات والتجاريات هو مساعدة كبيرة لنا. يمكننا أن نجد العديد من الجوانب غير المنطقية أثناء مراجعة الطلب (لماذا يتعين علينا الشكوى من مدير المنتج مرة أخرى). إذا استطعنا أن نقترحها في هذا الوقت ، فسنأخذ المبادرة لتجنب التطور المتكرر في العملية اللاحقة ، وفي الوقت نفسه ، يمكننا تشكيل تفاعل جيد نسبيًا مع الطلاب على جانب المنتج (بدلاً من المواجهة). فيما يلي بعض علاقات الكيان الأكثر أهمية:
من الشكل أعلاه ، يمكننا أن نرى أن المستخدم هو جوهر مخطط العلاقة بأكمله. فيما يلي العلاقة بين الكيانات المختلفة:
فيما يلي مقدمة مفصلة عن الجلسات والأدوار والأذونات:
عند الانتهاء من تطبيق المراسلة الفورية ، فإن أول ما تحتاج إلى مراعاته هو المحادثة ، وهي نافذة المحادثة في WeChat لدينا. يتطلب الأمر الكثير من الجهد للتفكير في العلاقة بين المحادثات والرسائل والمستخدمين والمجموعات ، وأخيراً تشكيل العلاقة الأساسية التالية:
بمعنى آخر ، ليس لدى المستخدم أي علاقة مباشرة مع الجلسة ، ويمكنه فقط الحصول على الجلسة من خلال الدردشة الفردية المقابلة للمستخدم والدردشة الجماعية. يمكن أن يكون لهذا الفوائد التالية:
من أجل تصميم نظام إدارة الإذارات الشامل والمريح ، يعتمد هذا النظام التحكم RBAC (التحكم القائم على الأدوار) لتصميم منصة "دور المستخدم" العام لراحة التوسع اللاحق.
يشير RBAC (التحكم في الوصول القائم على الأدوار) إلى أن المستخدم يرتبط بالأذونات من خلال الأدوار. وهذا يعني أن المستخدم له عدة أدوار ، وكل دور له عدة أذونات (بالطبع ، لا يجمع بين الأدوار والأذونات المتضاربة معًا). وبهذه الطريقة ، يتم إنشاء نموذج ترخيص لـ "عوامل استخدام المستخدم". في هذا النموذج ، هناك عمومًا علاقة كثيرة بين المستخدمين والأدوار وبين الأدوار والأذونات.
يحتوي هذا النظام على الأدوار الافتراضية للمسؤول والمستخدم العام والمستخدم المحظور والمستخدم المحظور ، ويتم تعيين أذونات مختلفة لأدوار مختلفة. لذلك ، من الضروري إجراء معالجة المصادقة الموحدة (من خلال الوسيطة) لتوجيه الواجهة مثل الإدارة والكلام. سيتم شرح الطرق والأساليب المحددة بالتفصيل في المشروع الخلفي. يستخدم هذا النظام مؤقتًا طريقة الأدوار والأذونات المحددة مسبقًا. إذا كنت ترغب في التوسع في المستقبل ، فيمكنك تحرير الأدوار والأذونات.
لم أر قط الجانب الإداري لـ WeChat ، ولكن يمكنك أن تتخيل أنه يمكن للمسؤولين تكوين أدوار المستخدم والأذونات وتحرير حالة المجموعة:
بعد التسجيل وتسجيل الدخول ، يمكنك إضافة أصدقاء والانضمام إلى المجموعات بشكل طبيعي ، وتعديل المعلومات الأساسية وتطبيقات المعالجة.
غير قادر على تسجيل الدخول
على سبيل المثال: يوجد الآن إصدار جديد من المركز الشخصي الذي يجب اختباره عبر الإنترنت. أولاً ، قم بإنشاء دور جديد "مركز شخصي" ، ثم تعيين الأذونات المقابلة للدور ؛ ثم قم بتجميع المستخدمين العاديين وحدد بعض الأشخاص لتكوين هذا الدور ، بحيث يمكنك اختباره.
دعنا نتحدث عن مبدأ الاتصال الأساسي لخدمات المراسلة الفورية. مثل خدمات HTTP العامة ، يوجد خادم وعميل للاتصال ، لكن البروتوكول المفصل وطرق المعالجة مختلفة.
لأسباب تاريخية ، أصبح بروتوكول HTTP السائد الآن بروتوكولًا عديمي الجنسية (لا يستخدم HTTP2 على نطاق واسع في الوقت الحالي). بشكل عام ، يبدأ العميل الطلب بنشاط ثم يستجيب إليه. لذلك من أجل إدراك أن الخادم يدفع المعلومات إلى العميل ، يحتاج الواجهة الأمامية إلى استطلاع الواجهة الخلفية بنشاط. هذه الطريقة غير فعالة ومعرضة للخطأ. يتم ذلك بالفعل على صفحتنا الرئيسية للإدارة من قبل (5s مرة واحدة).
من أجل تحقيق هذه الحاجة إلى جانب الخادم لدفع المعلومات بنشاط ، بدأ HTML5 في توفير بروتوكول للاتصال الكامل على اتصال TCP واحد ، وهو WebSocket. يجعل WebSocket تبادل البيانات بين العملاء والخوادم أسهل ، مما يسمح للخادم بدفع البيانات إلى العملاء بشكل نشط. ولد بروتوكول WebSocket في عام 2008 وأصبح معيارًا دوليًا في عام 2011. حاليًا ، دعمها معظم المتصفحات بالفعل.
استخدام WebSocket بسيط للغاية:
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
مع بروتوكول WebSocket ، يحتوي الخادم على أسلحة متقدمة لدفع المعلومات بنشاط. فهل هناك أي طريقة لتكون متوافقة مع المتصفحات القديمة والجديدة؟ في الواقع ، يفكر الكثير من الناس في هذا ، والجواب هو socket.io
socket.io يقوم socket.io بتغليف واجهة WebSocket ، ويمكن أن يتحول تلقائيًا إلى استخدام الاقتراع في المتصفحات القديمة للاتصال (لن يدرك مستخدمونا) ، مما يشكل مجموعة موحدة من الواجهات ، مما يقلل بشكل كبير من عبء التطوير. لديها بشكل أساسي المزايا التالية:
هذه هي الصفحة الرئيسية للمقبس
محرك المراسلة الفوري الأسرع والأكثر موثوقية (يضم محرك في الوقت الفعلي الأسرع والأكثر موثوقية)
من السهل الاستخدام حقًا:
var io = require('socket.io')(80);
var cfg = require('./config.json');
var tw = require('node-tweet-stream')(cfg);
tw.track('socket.io');
tw.track('javascript');
tw.on('tweet', function(tweet){
io.emit('tweet', tweet);
});
دعنا نركز على عدة نقاط مهمة في مشروع جانب الخادم. يحتاج معظم المحتوى إلى الاطلاع على الموقع الرسمي للبيض.
استخدم Scaffolding npm init egg --type=simple لتهيئة مشروع الخادم ، وتثبيت mySQL (my is is is prose 8.0) ، وتكوين كلمة مرور ارتباط قاعدة البيانات المطلوبة للتتابع ، وما إلى ذلك ، ويمكنك بدء تشغيله
// 目录结构说明
├── package.json // 项目信息
├── app.js // 启动文件,其中有一些钩子函数
├── app
| ├── router.js // 路由
│ ├── controller
│ ├── service
│ ├── middleware // 中间件
│ ├── model // 实体模型
│ └── io // socket.io 相关
│ ├── controller
│ └── middleware // io独有的中间件
├── config // 配置文件
| ├── plugin.js // 插件配置文件
| └── config.default.js // 默认的配置文件
├── logs // server运行期间产生的log文件
└── public // 静态文件和上传文件目录
يستخدم جهاز التوجيه بشكل أساسي لوصف المراسلات بين عنوان URL للطلب ووحدة التحكم التي تتعهد على وجه التحديد بإجراء التنفيذ ، أي app/router
app/middleware/auth.jsapp/middleware/admin.js نظرًا لأن هذا النظام له أدوار مختلفة مثل المسؤولين ومستخدمي الاتصالات العامة ، فإن معالجة المصادقة الموحدة مطلوبة لتوجيه الواجهة للإدارة والاتصالات.
على سبيل المثال ، الطريق جانب الإدارة /v1/admin/... ، أريد إضافة مصادقة الإدارة إلى جميع الطرق في هذه السلسلة. في هذا الوقت ، يمكنك استخدام البرامج الوسيطة للمصادقة. فيما يلي مثال محدد لاستخدام الوسيطة في جهاز توجيه المشرف.
// middware
module.exports = () => {
return async function admin(ctx, next) {
let { session } = ctx;
// 判断admin权限
if (session.user && session.user.rights.some(right => right.keyName === 'admin')) {
await next();
} else {
ctx.redirect('/login');
}
};
};
// router
const admin = app.middleware.admin();
router.get('/api/v1/admin/rights', admin, controller.v1.admin.rightsIndex);
مجموعة Sequelize+MySQL المستخدمة ، والبيض أيضا تتمة المكونات ذات الصلة. Sequelize عبارة عن ORM المستخدمة في بيئة العقدة ، ودعم Postgres و MySQL و MariadB و SQLite و Microsoft SQL Server ، وهو مريح للغاية للاستخدام. تحتاج إلى تحديد العلاقة المباشرة بين النموذج والنموذج أولاً. بعد إنشاء العلاقة ، يمكنك استخدام بعض أساليب الإعداد المسبق.
المعلومات الأساسية للنموذج أسهل في المعالجة. ما يجب الانتباه إليه هو تصميم العلاقة بين الكيانات ، أي ، مشارك. فيما يلي وصف علاقة المستخدم
// User.js
module.exports = app => {
const { STRING } = app.Sequelize;
const User = app.model.define('user', {
provider: {
type: STRING
},
username: {
type: STRING,
unique: 'username'
},
password: {
type: STRING
}
});
User.associate = function() {
// One-To-One associations
app.model.User.hasOne(app.model.UserInfo);
// One-To-Many associations
app.model.User.hasMany(app.model.Apply);
// Many-To-Many associations
app.model.User.belongsToMany(app.model.Group, { through: 'user_group' });
app.model.User.belongsToMany(app.model.Role, { through: 'user_role' });
};
return User;
};
على سبيل المثال ، العلاقة بين المستخدم والمستخدم هي علاقة فردية. بعد تعريفه ، يمكننا استخدام user.setUserInfo(userInfo) عند إنشاء مستخدم جديد. عندما تريد الحصول على المعلومات الأساسية لهذا المستخدم ، يمكنك أيضًا استخدام user.getUserInfo()
العلاقة بين المستخدم والتطبيق هي واحدة لنسخة ، أي يمكن للمستخدم أن يتوافق مع تطبيقات متعددة ، ويتم تطبيق تطبيقات الأصدقاء والتطبيقات الجماعية حاليًا:
عند إضافة تطبيق ، يمكنك استخدام user.addApply(apply) ، وعند الحصول عليه ، يمكنك استخدامه على النحو التالي:
const result = await ctx.model.Apply.findAndCountAll({
where: {
userId: ctx.session.user.id,
hasHandled: false
}
});
العلاقة بين المستخدم والمجموعة كثيرة ، أي يمكن للمستخدم أن يتوافق مع مجموعات متعددة ، ويمكن للمجموعة أن تتوافق مع العديد من المستخدمين. وبهذه الطريقة ، ستقوم SequeLize بإنشاء مجموعة مستخدمية جدول متوسطة لتحقيق هذه العلاقة.
عادة ما أستخدم هذا:
group.addUser(user); // 建立群组和用户的关系
user.getGroups(); // 获取用户的群组信息
يوفر البيض مقعد البيض. تحتاج إلى فتح المكون الإضافي في config/plugin.js بعد تثبيت Egg Socket.io. IO لديها البرامج الوسيطة الخاصة بها ووحدة التحكم.
يختلف طريق IO عن مسار طلبات HTTP العامة. لاحظ أنه لا يمكن معالجة المسار هنا باستخدام الوسيطة (لم تنجح) ، لذلك تعاملت مع معالجة الحظر في وحدة التحكم.
// 加入群
io.of('/').route('/v1/im/join', app.io.controller.im.join);
// 发送消息
io.of('/').route('/v1/im/new-message', app.io.controller.im.newMessage);
// 查询消息
io.of('/').route('/v1/im/get-messages', app.io.controller.im.getMessages);
ملاحظة: أعتبر علاقة كل من المجموعة والصديق كغرفة (أي جلسة) ، حتى أتمكن من إرسال رسائل مباشرة إلى Romm ، ويمكن للجميع بداخلها استلامها.
هناك نوعان من الوسيطة الافتراضية ، أحدهما هو الوسيطة الوسيطة التي تسمى عند الاتصال والفصل ، والتي يتم استخدامها للتحقق من حالة تسجيل الدخول ومعالجة منطق الأعمال ؛ والآخر هو الوسيطة الحزمة التي تسمى في كل مرة يتم إرسال رسالة ، والتي يتم استخدامها لطباعة السجل
نظرًا لأن إذن المكالمة مسبقًا مسبقًا ، تتم معالجته في وحدة التحكم
// 对用户发言的权限进行判断
if (!ctx.session.user.rights.some(right => right.keyName === 'speak')) {
return;
}
تنقسم الدردشات إلى دردشات واحدة ومحادثات جماعية. تتضمن معلومات الدردشة مؤقتًا نصًا عامًا وصورًا ومقاطع فيديو ورسائل تحديد المواقع ، والتي يمكن توسيعها إلى أوامر أو منتجات وفقًا للشركة.
يشير تصميم هيكل الرسالة إلى تصميم العديد من خدمات الطرف الثالث ، كما تم تعديله مع وضع هذا المشروع نفسه. يمكن توسيعه في الإرادة ، ويتم تقديم التفسير التالي:
const Message = app.model.define('message', {
/**
* 消息类型:
* 0:单聊
* 1:群聊
*/
type: {
type: STRING
},
// 消息体
body: {
type: JSON
},
fromId: { type: INTEGER },
toId: { type: INTEGER }
});
يقوم الجسم بتخزين جسم الرسالة ، الذي يستخدم لتخزين تنسيقات رسائل مختلفة باستخدام JSON:
// 文本消息
{
"type": "txt",
"msg":"哈哈哈" //消息内容
}
// 图片消息
{
"type": "img",
"url": "http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e",
"ext":"jpg",
"w":360, //宽
"h":480, //高
"size": 388245
}
// 视频消息
{
"type": 'video',
"url": "http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e",
"ext":"mp4",
"w":360, //宽
"h":480, //高
"size": 388245
}
// 地理位置消息
{
"type": "loc",
"title":"中国 浙江省 杭州市 网商路 599号", //地理位置title
"lng":120.1908686708565, // 经度
"lat":30.18704515647036 // 纬度
}
لا يوجد سوى واحد في الوقت الحاضر ، وهو تحديث رمز Baidu. الأمر بسيط للغاية هنا ، فقط الرجوع إلى الوثائق الرسمية.
وحدة تخصيص الحوار الذكي وخدمة منصة الخدمة
هذا مثير للاهتمام للغاية. يمكنك إنشاء روبوت جديد وإضافة المهارات المقابلة على https://ai.baidu.com/ . أنا أتحدث هنا ، وهناك أسئلة وأجوبة ذكية ، إلخ.
إذا كنت لا ترغب في البدء ، فيمكنك حذف ctx.service.baidu.getToken();
بادئ ذي بدء ، تحتاج إلى تكوينه في ملف التكوين. لقد حصرت حجم الملف هنا وتنسيق ملف فيديو iOS عبر الموقع:
config.multipart = {
mode: 'file',
fileSize: '3mb',
fileExtensions: ['.mov']
};
يتم استخدام واجهة موحدة لمعالجة تحميل الملفات. المشكلة الأساسية هي كتابة الملفات ، والملفات هي قائمة الملفات المنقولة من الواجهة الأمامية
for (const file of ctx.request.files) {
// 生成文件路径,注意upload文件路径需要存在
const filePath = `./public/upload/${
Date.now() + Math.floor(Math.random() * 100000).toString() + '.' + file.filename.split('.').pop()
}`;
const reader = fs.createReadStream(file.filepath); // 创建可读流
const upStream = fs.createWriteStream(filePath); // 创建可写流
reader.pipe(upStream); // 可读流通过管道写入可写流
data.push({
url: filePath.slice(1)
});
}
أقوم بتخزين /public/upload/ في دليل الخادم. يتطلب هذا الدليل تكوين ملف ثابت:
config.static = {
prefix: '/public/',
dir: path.join(appInfo.baseDir, 'public')
};
وثيقة البيض الرسمية لهذا الفصل هي قتلك ، لا يوجد شيء أمثلة ، يجب عليك قراءة رمز المصدر. إنه أمر فظيع للغاية. لقد درستها لفترة طويلة قبل أن أحسب ما يحدث.
لأنني أرغب في التحكم في تسجيل دخول كلمة مرور الحساب بحرية أكبر ، فأنا لا أستخدم Passport لتسجيل الدخول إلى كلمة مرور الحساب ، لكن استخدم مصادقة واجهة واجية عادية.
فيما يلي وصف مفصل لعملية تسجيل الدخول باستخدام منصة طرف ثالث (اخترت Github):
افتح المكون الإضافي:
// config/plugin.js
module.exports.passport = {
enable: true,
package: 'egg-passport',
};
module.exports.passportGithub = {
enable: true,
package: 'egg-passport-github',
};
// config.default.js
config.passportGithub = {
key: 'your_clientID',
secret: 'your_clientSecret',
callbackURL: 'http://localhost:3000/api/v1/passport/github/callback' // 注意这里非常的关键,这里需要和你在github上面设置的Authorization callback URL一致
};
this.app.passport.verify(verify);
const github = app.passport.authenticate('github', { successRedirect: '/' }); // successRedirect就是最后校验完毕后前端会跳转的路由,我这里直接跳转到主页了
router.get('/v1/passport/github', github);
router.get('/v1/passport/github/callback', github);
/v1/passport/github على الواجهة الأمامية وبدء ترخيص GitHub لهذا التطبيق. بعد النجاح ، ستذهب Github إلى http://localhost:3000/v1/passport/github/callback?code=12313123123 . سيحصل المكون الإضافي GitHubPassport الخاص بنا على معلومات المستخدم على GitHub. بعد الحصول على المعلومات التفصيلية ، نحتاج إلى التحقق من معلومات المستخدم في app/passport/verify.js // verify.js
module.exports = async (ctx, githubUser) => {
const { service } = ctx;
const { provider, name, photo, displayName } = githubUser;
ctx.logger.info('githubUser', { provider, name, photo, displayName });
let user = await ctx.model.User.findOne({
where: {
username: name
}
});
if (!user) {
user = await ctx.model.User.create({
provider,
username: name
});
const userInfo = await ctx.model.UserInfo.create({
nickname: displayName,
photo
});
const role = await ctx.model.Role.findOne({
where: {
keyName: 'user'
}
});
user.setUserInfo(userInfo);
user.addRole(role);
await user.save();
}
const { rights, roles } = await service.user.getUserAttribute(user.id);
// 权限判断
if (!rights.some(item => item.keyName === 'login')) {
ctx.body = {
statusCode: '1',
errorMessage: '不具备登录权限'
};
return;
}
ctx.session.user = {
id: user.id,
roles,
rights
};
return githubUser;
};
انتبه إلى الرمز أعلاه. إذا كان هذا هو الترخيص الأول ، فسيتم إنشاء المستخدم. إذا كان هذا هو التفويض الثاني ، فقد تم إنشاء المستخدم.
عند نشر النظام أو تشغيله ، يجب أن تكون بعض البيانات والجداول مسبقًا ، والرموز موجودة في app.js و app/service/startup.js
المنطق هو أنه بعد بدء تشغيل المشروع ، استخدم النموذج لمزامنة بنية الجدول في قاعدة البيانات ، ثم ابدأ في إنشاء بعض البيانات الأساسية:
بعد الانتهاء من ما سبق ، تم الانتهاء من البيانات الأولية ويمكن أن تعمل بشكل طبيعي
لقد اشتريت Server Centos على Tencent Cloud ، اسم المجال الذي اشتريته على Alibaba Cloud ، العقدة المثبتة (12.18.2) ، Nginx و MySQL8.0 ، وبدأت مباشرة على CentOS. يستخدم الواجهة الأمامية nginx للوكيل العكسي. نظرًا لموارد الخادم المحدودة ، لا توجد أدوات أتمتة Jenkins و Docker ، مما يؤدي إلى بعض العمليات اليدوية عند التحديث.