Розробка на базі фреймворку COREmanager. Наші партнери створювали рішення для аутсорсингу техпідтримки



Якщо хтось вирішив почати свою справу і хоче надавати віртуальний хостинг, запустити онлайн-сервіс з продажу квітів або каву по франшизі, є безліч готових інструментів для організації роботи. З іншого боку, якщо запустити якийсь бізнес просто, треба бути готовим до великої кількості конкурентів в цій сфері. Ймовірність «прогоріти» підвищується.
Якщо ж заходити в якусь мало освоєну область, де конкуренція менше, готових інструментів для автоматизації завдань може і не бути. Так, перший час можна все робити вручну, проте коли кількість клієнтів зросте, доведеться серйозно замислитися над оптимізацією процесів.

При розробці засоби автоматизації можна використовувати фреймворк COREmanager як основу, кістяк, продукту. Це скоротить витрачений на кодування час, а також дозволить застосувати різні мови програмування для реалізації різних функцій.

Під катом — подробиці розробки системи для аутсорс-техпідтримки компанією ISPlicense.


ISPlicense — яскравий приклад компанії, що пропонує нестандартні послуги в дуже конкурентному сфері бізнесу. Вони вибрали сферу хостингу, побудувавши бізнес модель на обслуговування вже існуючих і знову приходять на даний ринок гравців. Компанія почала з реселлінга ліцензій програмних продуктів ISPsystem, ставши одним з наших великих партнерів. Через час, пакет послуг компанії доповнився наданням технічної підтримки на аутсорсингу, що виявилося дуже затребуваним. Брак ресурсів, географічна віддаленість від більшості клієнтів та інші причини спонукали чимало хостерів всерйоз задуматися про те, щоб віддати в техпідтримку ведення сторонньої організації.

ISPlicense виконує аутсорсингову техпідтримку за двома схемами:

  1. При роботі з хостером, у якого для кінцевих користувачів техпідтримка надається безкоштовно, виконується облік витраченого на відповіді часу і виставлення рахунку хостеру згідно договору.
  2. Якщо підтримка для клієнта не безкоштовна, то вартість формується на підставі прайса, де розмір плати залежить від витраченого часу або від переліку наданих послуг. Після виконання запиту клієнта виставляється рахунок від особи хостера, ISPlicense отримує відсоток від суми.
Тепер давайте припустимо, як можна поставити процес підтримки без використання інструментів автоматизації.

На перших порах можна організовувати співпрацю із замовниками за різними схемами: хтось дає логін-пароль свого співробітника, хтось заводить окремого користувача, до кого-то можна підключитися засобами BILLmanager. Але уявіть, якщо кількість обслуговуваних хостерів збільшилася настільки, що оператор техпідтримки плутається в десятках вкладок браузера і має при цьому відслідковувати нові повідомлення в тікет.

Для попередження такої ситуації компанія ISPlicense розробила на базі COREmanager систему техпідтримки, інтегровану з BILLmanager та іншими биллингами. Продукт отримав назву TicketManager.

Мабуть, тепер варто розповісти трохи докладніше про COREmanager. Це написаний на C++ фреймворк, конструктор.

Його розробка почалася в 2010 році. COREmanager створювався для того, щоб винести загальну функціональність наших продуктів в окрему сутність і таким чином забезпечити узгодженість компонентів. BILLmanager, ISPmanager, VMmanager, DCImanager та інші панелі управління стали розширеннями «ядра», яке пишеться окремою командою з найбільш досвідчених розробників ISPsystem. В результаті скоротилася час розробки, зменшилася ймовірність появи помилок і піднялася швидкість роботи кінцевих продуктів.

COREmanager поширюється безкоштовно, має докладну документацію, що описує його методи використання і застосовується в розробці інструментів для рішення практично будь-якої задачі. Створити структуру меню можна через веб-інтерфейс або шляхом написання xml-файлу, а для реалізації механізму обробки подій допускається використовувати будь-яку мову програмування, якщо його інтерпретатор встановлений в операційній системі.

Тому ISPlicense вибрала COREmanager для створення TicketManager. Так, можна було скористатися готовими рішеннями для техпідтримки або вирішити завдання шляхом написання плагінів для BILLmanager, але програмістам ISPlicense дуже вже хотілося самим випробувати, що може COREmanager. :)

Після запуску послуги аутсорсингу, з плином часу оформилася потребує вирішення проблематика і з'явилася потреба в розробці системи техпідтримки. Утворилися наступні передумови та завдання:

  • Якщо кожен раз заходити в панель кожного провайдера для консультації користувачів, це забирає надто багато часу. Крім цього, коли співробітник техпідтримки працює з декількома провайдерами, може виникнути плутанина, оскільки працівнику потрібно відкрити декілька вкладок або вікон браузера. При цьому хостери можуть використовувати білінги від різних розробників. Стало бути, потрібно агрегувати потоки тікетів в один продукт і привести їх до єдиного вигляду.
  • Оскільки тарифікація підтримки погодинна, потрібно реалізувати зручний облік витраченого на тікет часу.
  • Для зручності та економії часу реалізувати автоматичне формування рахунків, які приходять кінцевим користувачам (наприклад, за платне адміністрування).
  • Створення звітів і рахунків для хостерів теж автоматизувати.
  • щоб уникнути зайвих питань як у бік провайдера, так і в бік ISPlicense кінцевий клієнт не повинен знати про те, що техпідтримка аутсорсингова.
Одержаний продукт складається з двох частин: безпосередньо самої системи тікетів і обробника, який встановлюється в біллінг клієнта. Також є API, який дозволяє виконати інтеграцію з іншою системою техпідтримки або білінгом.

Познайомимося з ключовими особливостями панелі техпідтримки.
Список тікетів виконано мінімалістично, інструменти для тонкої роботи із заявками доступні в меню перегляду заявки.



Відкриємо будь-яке звернення для прикладу і подивимося, які дії з ним можна зробити.



  • Відповісти. Блокує тікет за оператором, відкриває форму введення відповіді і починає облік витраченого часу.
  • Примітка. Дозволяє залишити для колег примітка, прив'язане до цього запиту. Воно не буде видно ні для провайдера для кінцевого клієнта.
  • Клієнт. Відкриває велике текстове поле для заміток, прив'язане до кінцевого клієнта.
  • Проект. Викликає аналогічне поле за хостеру.
  • Зняти гроші. Списує гроші за надання послуг з кінцевого клієнта в білінговій системі хостера.
  • Закрити. Повертає до списку відкритих тікетів.
Крім цього, у вікні показується вся інформація про ініціатора тікета. При клацанні на id клієнта виконується перехід в його біллінг, при клацанні на ім'я сервера відбувається перехід в панель керування цим сервером.

Також відображається інформація про послугу, у зв'язку з якого надійшло звернення, і вказується інформація про сервер, де розгорнута послуга, в тому числі дані для доступу.

Заблокуємо тікет і подивимося на активні елементи відкрилася форми введення відповіді.

  • Розблок. Відв'язує тікет від оператора і дозволяє взятися за відповідь іншій людині.
  • Відділ. Служить для передачі заявки в інший підрозділ хостинг-провайдера. Наприклад, від технічних спеціалістів у бухгалтерію, якщо мова йде про вирішення фінансових питань.
  • Внутрішній коментар. Дозволяє написати в тикеті повідомлення, яке буде приховано від клієнта і від провайдера.
  • Витрачений час. Тривалість відповіді на тікет. Вимірюється в хвилинах. При ручному зміні значення автоматичний підрахунок часу зупиняється.
  • Зупинити лічильник часу. Використовується в тих ситуаціях, коли передбачається, що оператор не зробить найближчим часом ніяких дій за рішенням запиту. Наприклад, співробітник перед розгортанням ISPmanager чекає поки завершиться інсталяція операційної системи на віртуальній машині.
  • Статус. Стан тікета.

    • Відкритий. Ведеться робота над запитом.
    • Закритий. Запит виконаний.
    • В процесі. Використовується, якщо запит виконується третьою особою, операція тривала, і потрібен контроль процесу. Запит знаходиться в нижній частині списку, щоб не відволікати від «палаючих» завдань.
    • Відкладений. Тікет не відображається у загальному списку до часу, який вказується в полі «Відкласти до».
  • Відправити внутрішній коментар провайдера. Написане не буде видно кінцевому користувачеві, але буде показано співробітникам хостинг-провайдера.
  • Заборонити закриття тікета клієнтом. Використовується при вирішенні питань заборгованості або протизаконної діяльності.
В ході консультації кінцевий користувач не знає, що на його запитання відповідає фахівець ISPlicense; він бачить, що розмова йде з співробітником його хостинг провайдера.

Реалізація продукту зайняла приблизно 5000 рядків коду для панелі техпідтримки і по 500 рядків для модулів інтеграції з BILLmanager та іншими биллингами.

Бажаючі можуть вивчити API TicketManager, а нижче під спойлерами — вихідний код модуля інтеграції для BILLmanager.

Виявилося, що більше часу витратили на підбиття потрібної функціональності в єдиний список, ніж на кодування, оскільки левова частка потрібних функцій вже була реалізована в COREmanager. Ну і інтерфейс теж не треба було писати, лише вказати, де повинні бути кнопки.

Makefile
MGR = billmgr
PLUGIN = ticketmgri
VERSION = 5.0.1
LIB += ticketmgri
ticketmgri_SOURCES = ticketmgri.cpp

WRAPPER += ticketmgri_syncticket
ticketmgri_syncticket_SOURCES = ticketmgri_syncticket.cpp
ticketmgri_syncticket_LDADD = -lbase

BASE ?= /usr/local/mgr5
include $(BASE)/src/isp.mk

billmgr_mod_ticketmgri.xml
<?xml version="1.0" encoding="UTF-8"?>
<mgrdata>
<library name="ticketmgri" />
</mgrdata>

ticketmgri.cpp
#include <api/action.h>
#include <api/module.h>
#include <api/stdconfig.h>
#include <billmgr/db.h>
#include <mgr/mgrdb_struct.h>
#include <mgr/mgrlog.h>
#include <mgr/mgrtask.h>

MODULE("ticketmgri");

using namespace isp_api;

namespace {

StringVector allowedDepartments, hideDepartments;

/**
* Синхронізує тікет, запускаючи за допомогою LongTask (фонового завдання) бінарний файл sbin/ticketmgri_syncticket
*
* [in] _id Ідентифікатор тікета
*/
void SyncTicket(int _id) {
string id = str::Str(_id);
Warning("Sync %s", id.c_str());
if (!_id) return;
mgr_task::LongTask("sbin/ticketmgri_syncticket", "ticket_" + id,
"ticketmgri_sync")
.SetParam(id)
.Start();
}

/**
* Базова структура для обробки подій виклику функцій редагування і передачі тікета
*
* Отримує ідентифікатор тікета і викликає для нього синхронізацію
*/
struct eTicketEdit : public Event {
/**
* Конструктор
*
* Створює об'єкт обробника подій редагування або передачі тікета
*
* ev ім'я функції, на яку встановити обробник події
* elid_name вказує спосіб отримання даних для синхронізації. В залежності від значення
* вибирається спосіб отримання ідентифікатор тікета з бази даних або з сесії
*/
eTicketEdit(const string &ev, const string &elid_name = "elid")
: Event(ev, "ticketmgri_" + ev), elid_name_(elid_name) {
Warning("eTicketEdit created");
}

/**
* Синхронізує тікет при редагуванні
*
* Подія виконується після того, як завершилася функція
* [in] ses Поточна сесія
*/
void AfterExecute(Session &ses) const override {
Warning("subm %d cb %s elid %s", ses.IsSubmitted(),
ses.Param("clicked_button").c_str(), ses.Param("elid").c_str());
string button = ses.Param("clicked_button");

string elid;
if (elid_name_ == "elid_ticket2user") {
elid = db->Query("SELECT ticket FROM ticket2user WHERE id='" +
ses.Param("elid") + "'")
->Str();
} else {
elid = ses.Param("elid");
}

if ((ses.IsSubmitted() || ses.Param("sv_field") == "ok_message") &&
(button == "ok" || button == "" || button == "ok_message")) {
if (!ses.Has(elid_name_)) {
SyncTicket(db->Query("SELECT MAX(id) FROM ticket")->Int());
} else {
SyncTicket(str::Int(elid));
}
}
}

string elid_name_;
};

/**
* Структура, обробна список відділів
*/
struct eClientTicketEdit : public eTicketEdit {
eClientTicketEdit() : eTicketEdit("clientticket.edit") {}

/**
* Видаляє зі списку відділів, відображуваного клієнтам, приховані відділи
*
* Подія виконується після того, як завершилася функція
*
* [in] ses Поточна сесія
*/
void AfterExecute(Session &ses) const override {
eTicketEdit::AfterExecute(ses);
for (auto &i : hideDepartments) {
ses.xml.RemoveNodes("//slist[@name='client_department']/val[@key='" + i +
"']");
}
}
};

/**
* Структура, що встановлює фільтр по клієнту
*/
struct aTicketintegrationSetFilter : public Action {
aTicketintegrationSetFilter()
: Action("ticketintegration.setfilter", MinLevel(lvAdmin)) {}

/**
* Встановлює фільтр по клієнту
*
* Виконує внутрішній виклик встановлення фільтра по клієнту
*
* [in] ses Поточна сесія
*/
void Execute(Session &ses) const override {
InternalCall(ses, "account.setfilter", "elid=" + ses.Param("elid"));
ses.Ok(ses.okTop);
}
};

/**
* Структура зберігає тікет
*/
struct aTicketintegrationPost : public Action {
aTicketintegrationPost()
: Action("ticketintegration.post", MinLevel(lvAdmin)) {}

void Execute(Session &ses) const override { Execute(ses, true); }

/**
* Функція зберігає тікет локально
*
* [in] ses Поточна сесія
* [in] retry параметр, який відповідає за передачу тікета першому дозволеним відділу,
* якщо немає відкритих тікетів
*/
void Execute(Session &ses, bool retry) const {
auto openTickets = db->Query("SELECT id FROM ticket2user WHERE ticket=" +
ses.Param("elid") + " AND user IN (" +
str::Join(allowedDepartments, ",") + ")");
string elid;
if (openTickets->Eof()) {
if (ses.Param("type") == "setstatus" && ses.Param("status") == "closed") {
ses.NewNode("ok");
return;
}
if (retry) {
InternalCall(ses, "support_tool_responsible",
"set_responsible_default=off&sok=ok&set_responsible=e%5F" +
allowedDepartments[0] + "&elid=" + ses.Param("elid"));
Execute(ses, false);
return;
} else {
throw mgr_err::Error("cannot_open_ticket");
}
} else {
elid = openTickets->Str();
}

if (ses.Param("type") == "setstatus" && ses.Param("status") == "new") {
return;
}

auto ret2 = InternalCall(
ses, "ticket.edit",
string() + "sok=ok&show_optional=on" + "&clicked_button=" +
(ses.Param("status") == "new" ? "ok_message" : "ok") + "&" +
(!ses.Checked("internal") ? "message" : "note_message") + "=" +
str::url::Encode(ses.Param("message")) + "&elid=" + elid);
// TODO: attachments, sender_name
ses.NewNode("ok");
}
};

/**
* Структура, що описує таблицю, що містить останній коментар до тікети
*/
struct TicketmgriLastNote : public mgr_db::CustomTable {
mgr_db::ReferenceField Ticket;
mgr_db::ReferenceField LastNote;

TicketmgriLastNote()
: mgr_db::CustomTable("ticketmgri_last_note"),
Ticket(this, "ticket", mgr_db::rtRestrict),
LastNote(this, "last_note", "ticket_note", mgr_db::rtRestrict) {
Ticket.info().set_primary();
}
};

/**
* Клас, додає last_note в таблицю ticketmgri_last_note
*/
struct aTicketintegrationLastNote : public Action {
aTicketintegrationLastNote()
: Action("ticketintegraion.last_note", MinLevel(lvSuper)) {}

/**
* Функція, що зберігає і повертає значення last_note для тікета
* у таблиці ticketmgri_last_note
*
* [in] ses Поточна сесія
*/
void Execute(Session &ses) const override {
auto t = db->Get<TicketmgriLastNote>();
if (!t->Find(ses.Param("elid"))) {
t->New();
t->Ticket = str::Int(ses.Param("elid"));
}
if (ses.IsSubmitted()) {
t->LastNote = str::Int(ses.Param("last_note"));
t->Post();
ses.Ok();
} else {
ses.NewNode("last_note", t->LastNote);
}
}
};

/**
* Структура, перезапускающая фонові завдання синхронізації тікетів, які завершилися з помилкою
*/
struct aTicketintegrationPushTasks : public Action {
aTicketintegrationPushTasks()
: Action("ticketintegraion.push_tasks", MinLevel(lvSuper)) {}

/**
* Отримує список фонових завдань синхронізації тікетів, які завершилися з помилкою і
* знову запускає їх
*
* [in] ses Поточна сесія
*/
void Execute(Session &ses) const override {
mgr_xml::XPath xpath =
InternalCall("longtask", "filter=yes&state=err&queue=ticketmgri_sync")
.GetNodes("//elem[queue='ticketmgri_sync' and status='err']");
for (auto elem : xpath) {
auto data = InternalCall("longtask.edit",
"elid=" + elem.FindNode("pidfile").Str());
mgr_task::LongTask(data.GetNode("//realname"), data.GetNode("//id"),
"ticketmgri_sync")
.SetParam(data.GetNode("//params"))
.Start();
}
}
};

/**
* Структура для отримання значення балансу клієнта
*/
struct aTicketintegrationGetBalance : public Action {
aTicketintegrationGetBalance()
: Action("ticketintegration.getbalance", MinLevel(lvAdmin)) {}

/**
* За допомогою внутрішнього виклику запитує значення балансу клієнта
*
* [in] ses Поточна сесія
*/
void Execute(Session &ses) const override {
ses.NewNode("balance",
InternalCall(ses, "account.edit", "elid=" + ses.Param("elid"))
.GetNode("//balance")
.Str());
}

bool IsModify(const Session &) const override { return false; }
};

/**
* Структура, списує кошти з рахунку клієнта
*/
struct aTicketintegrationDeduct : public Action {
aTicketintegrationDeduct()
: Action("ticketintegration.deduct", MinLevel(lvAdmin)) {}

/**
* Функція для списання грошових коштів за тікет
*
* За допомогою SQL-запиту шукає тікет в дозволених відділах.
* Далі через внутрениий запит викликається списання грошових коштів за тікет.
* Якщо тікет не знайдений, кидається виключення
*
* [in] ses Поточна сесія
*/
void Execute(Session &ses) const override {
auto openTickets = db->Query("SELECT id FROM ticket2user WHERE ticket=" +
ses.Param("ticket") + " AND user IN (" +
str::Join(allowedDepartments, ",") + ")");
if (openTickets->Eof()) {
throw mgr_err::Value("ticket");
}
string elid = openTickets->AsString(0);
InternalCall(ses, "ticket.edit", "sok=ok&show_optional=on&elid=" + elid +
"&ticket_expense=" +
ses.Param("amount"));
}
};

} // namespace

//Ініціалізація модуля, додавання параметрів в конфігураційний файл,
//реєстрація таблиці в базі даних
MODULE_INIT(ticketmgri, "") {
Warning("Init TICKETmanager integtration");
mgr_cf::AddParam("TicketmgrUrl",
"https://tickets.isplicense.ru:1500/ticketmgr");
mgr_cf::AddParam("TicketmgrLogin");
mgr_cf::AddParam("TicketmgrPassword");
mgr_cf::AddParam("TicketmgrBillmgrUrl");
mgr_cf::AddParam("TicketmgrUserId");
mgr_cf::AddParam("TicketmgrAllowedDepartments");
mgr_cf::AddParam("TicketmgrHideDepartments");
str::Split(mgr_cf::GetParam("TicketmgrAllowedDepartments"), ",",
allowedDepartments);
if (allowedDepartments.empty()) {
allowedDepartments.push_back(0);
}
str::Split(mgr_cf::GetParam("TicketmgrHideDepartments"), ",",
hideDepartments);
db->Register<TicketmgriLastNote>();
new eClientTicketEdit;
new eTicketEdit("ticket.edit", "elid_ticket2user");
new eTicketEdit("support_tool_responsible", "plid");
new aTicketintegrationSetFilter;
new aTicketintegrationPost;
new aTicketintegrationLastNote;
new aTicketintegrationPushTasks;
new aTicketintegrationGetBalance;
new aTicketintegrationDeduct;
}

ticketmgri_syncticket.cpp
#include <billmgr/db.h>
#include <billmgr/defines.h>
#include <billmgr/sbin_utils.h>
#include <ispbin.h>
#include <mgr/mgrclient.h>
#include <mgr/mgrdb_struct.h>
#include <mgr/mgrenv.h>
#include <mgr/mgrlog.h>
#include <mgr/mgrproc.h>
#include <mgr/mgrrpc.h>

MODULE("syncticket");

using sbin::DB;
using sbin::GetMgrConfParam;
using sbin::Client;
using sbin::ClientQuery;

/**
* Ініціалізація клієнта для виконання запитів до віддаленої системи Ticketmanager
*
* Системи адресу і дані для авторизації беруться з відповідних параметрів
* конфігураційного файлу
*/
mgr_client::Client &ticketmgr() {
static mgr_client::Client *ret = []() {
mgr_client::Remote *ret =
new mgr_client::Remote(GetMgrConfParam("TicketmgrUrl"));
ret->AddParam("authinfo", GetMgrConfParam("TicketmgrLogin") + ":" +
GetMgrConfParam("TicketmgrPassword"));
return ret;
}();
return *ret;
}

/**
* Збереження тікетів в системі TICKETmanager
*
* Отримує дані з таблиці, формує xml документ c інформацією про клієнта,
* користувача, посиланням, модулі обробки, тикеті
*/
void PostTicket(const string &elid) {
//отримання інформації про тикеті, клієнта, користувача
auto ticket = DB()->Query("SELECT * FROM ticket WHERE id=" + elid);
if (ticket->Eof()) throw mgr_err::Missed("ticket");
auto account = DB()->Query("SELECT * FROM account WHERE id=" +
ticket->AsString("account_client"));
if (account->Eof()) throw mgr_err::Missed("account");
auto user = DB()->Query("SELECT * FROM user WHERE account=" +
account->AsString("id") + " ORDER BY id LIMIT 1");
if (user->Eof()) throw mgr_err::Missed("user");

//формування xml-документа з інформацією про клієнта та користувача
mgr_xml::Xml infoXml;
auto info = infoXml.GetRoot();
auto customer = info.AppendChild("customer");
customer.AppendChild("id", account->AsString("id"));
customer.AppendChild("name", account->AsString("name"));
customer.AppendChild("email", user->AsString("email"));
customer.AppendChild("phone", user->AsString("phone"));
customer.AppendChild("link",
GetMgrConfParam("TicketmgrBillmgrUrl") +
"?startform=ticketintegration.setfilter&elid=" +
account->AsString("id"));

if (!ticket->IsNull("item")) {
auto item =
DB()->Query("SELECT id, name, processingmodule item FROM WHERE id=" +
ticket->AsString("item"));
if (item->Eof()) throw mgr_err::Missed("item");
auto iteminfo = info.AppendChild("item");
//додавання інформації про модулі обробки
iteminfo.SetProp("selected", "так");
iteminfo.AppendChild("id", item->AsString("id"));
iteminfo.AppendChild("name", item->AsString("name"));
iteminfo.AppendChild("serverid", item->AsString("processingmodule"));
//додавання інформації про параметри послуги
ForEachQuery(DB(), "SELECT intname, value FROM itemparam WHERE item=" +
ticket->AsString("item"),
i) {
if (i->AsString(0) == "ip") {
iteminfo.AppendChild("ip", i->AsString(1));
} else if (i->AsString(0) == "username") {
iteminfo.AppendChild("login", i->AsString(1));
} else if (i->AsString(0) == "password") {
iteminfo.AppendChild("password", i->AsString(1));
} else if (i->AsString(0) == "-") {
iteminfo.AppendChild("-", i->AsString(1));
}
}
}

//формування інформації про тикеті для системи Ticketmanager
StringMap args = {{"remoteid", ticket->AsString("id")},
{"department", ticket->AsString("responsible")},
{"info", infoXml.Str()},
{"subject", ticket->AsString("name")}};

ticketmgr().Query("func=clientticket.add&sok=ok", args);
}

int ISP_MAIN(int ac, char **av) {
if (ac != 2) {
fprintf(stderr, "Usage: ticketmgri_syncticket ID");
return 1;
}

string elid = av[1];

try {
mgr_log::Init("ticketmgri");
string status = "closed";
int lastmessage = 0;

//перевірка статусу тікета, що знаходиться у вказаних відділах
string newStatus =
DB()->Query("SELECT COUNT(*) FROM ticket2user WHERE ticket=" + elid +
" AND user IN (" +
GetMgrConfParam("TicketmgrAllowedDepartments") + ")")
->Int()
? "new"
: "closed";
bool inDepartment =
DB()->Query("SELECT COUNT(*) FROM ticket WHERE id=" + elid +
" AND responsible IN (" +
GetMgrConfParam("TicketmgrAllowedDepartments") + ")")
->Int();
if (newStatus != "new" && !inDepartment) {
LogNote("Skip ticket %s: status=%s, inDepartment=%d", elid.c_str(),
newStatus.c_str(), inDepartment);
return 0;
}
try {
//отримання інформації про тикеті системі Ticketmanager
auto r = ticketmgr().Query("func=clientticket.info&remoteid=?", elid);
status = r.value("status");
lastmessage = str::Int(r.value("lastmessage"));
} catch (mgr_err::Error &e) {
if (e.type() == "missed" && e.object() == "remoteid") {
//створення тікета, якщо він не знайдений в системі
PostTicket(elid);
} else {
throw;
}
}

//отримання last_note для тікета
int lastnote =
str::Int(Client()
.Query("func=ticketintegraion.last_note&elid=" + elid)
.value("last_note"));

//отримання повідомлень для тікета
auto msg = DB()->Query(
string() +
"SELECT ticket_message.id user.realname AS username, user.level AS "
"userlevel, message, 1 AS type, ticket_message.date_post " +
"FROM ticket_message " + "JOIN user ON ticket_message.user=user.id " +
"WHERE ticket_message.id > " + str::Str(lastmessage) + " " +
"AND user != " + GetMgrConfParam("TicketmgrUserId") + " " +
"AND ticket = " + elid + " " +
"UNION "
"SELECT ticket_note.id user.realname AS username, user.level AS "
"userlevel, note message AS, 2 AS type, ticket_note.date_post " +
"FROM ticket_note " + "JOIN user ON ticket_note.user=user.id " +
"WHERE ticket_note.id > " + str::Str(lastnote) + " " + "AND user != " +
GetMgrConfParam("TicketmgrUserId") + " " + "AND ticket = " + elid +
" " + "ORDER BY date_post");

//якщо повідомлень не знайдено і статус не співпадає, то збереження статусу в Ticketmanager
if (msg->Eof() && status != newStatus) {
StringMap params = {
{"remoteid", elid}, {"status", newStatus},
};
ticketmgr().Query(
"func=clientticket.post&sok=ok&sender=staff&sender_name=System&type="
"setstatus",
params);
} else {
//збереження повідомлень у Ticketmanager
lastnote = 0;
for (msg->First(); !msg->Eof(); msg->Next()) {
StringMap params = {
{"remoteid", elid},
{"status", newStatus},
{"sender_name", msg->AsString("username")},
{"sender", msg->AsInt("userlevel") >= 28 ? "staff" : "client"},
{"message", msg->AsString("message")},
};

int attachments = 0;

if (msg->AsInt("type") == 1) {
params["messageid"] = msg>AsString("id");
//додавання вкладень
ForEachQuery(
DB(),
"SELECT * FROM ticket_message_attach WHERE ticket_message=" +
msg->AsString("id"),
attach) {
string id = str::Str(attachments++);
auto info =
ClientQuery("func=ticket.file&elid=" + attach->AsString("id"));
params["attachment_name_" + id] =
info.xml.GetNode("//content/name").Str();
params["attachment_content_" + id] = str::base64::Encode(
mgr_file::Read(info.xml.GetNode("//content/data").Str()));
}
} else {
lastnote = std::max(lastnote, msg->AsInt("id"));
params["internal"] = "on";
}
params["attachments"] = str::Str(attachments);

ticketmgr().Query("func=clientticket.post&sok=ok&type=message", params);
}
// збереження last_note
if (lastnote) {
Client().Query("func=ticketintegraion.last_note&sok=ok&elid=" + elid +
"&last_note=" + str::Str(lastnote));
}
}
} catch (std::exception &e) {
fprintf(stderr, "%s\n", e.what());
return 1;
}
return 0;
}

Підсумок розробки — продукт, який зробив роботу операторів техпідтримки зручніше, забезпечив автоматичне формування документів, а запити кінцевих користувачів стали оброблятися швидше.

У подальших планах ISPlicense — реалізація десктопного міні-додатки, яке буде подавати сигнал при надходженні нової заявки, а також дозволить припиняти і поновлювати облік витраченого на той чи інший тікет часу в два кліка.

На завершення хочеться додати, що COREmanager став основою не тільки TicketManager і всіх наших продуктів. На його основі реалізована система обліку бібліотечного фонду, сервіс для взаємодії з перекладачами, інструмент організації спільних поїздок, система постановки завдань для тестувальників, і це тільки чверть списку. Завдяки тому, що модулі можна писати на будь-якій мові при наявності встановленого на сервері інтерпретатора, ви можете написати справді унікальний продукт, що ідеально вписується в вашу модель бізнесу.

В одній з наступних статей ми розповімо про застосування COREmanager в ігровій індустрії на прикладі MMO проекту, в якому рішення використовується обліку користувачів, керування серверами, аналітики і багатьох інших завдань.

p.s. Якщо ви хочете докладніше познайомитися з COREmanager, то до ваших послуг інструкція по установці, документация по продукту.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.