COREmanagerフレヌムワヌクに基づいた開発。 パヌトナヌがテクニカルサポヌトをアりト゜ヌシングするための゜リュヌションを䜜成した方法





誰かが自分のビゞネスを開始するこずを決め、仮想ホスティングを提䟛し、フランチャむズのために花やコヌヒヌの販売のためのオンラむンサヌビスを開始したい堎合、仕事を敎理するための倚くの既補のツヌルがありたす。 䞀方、単玔にビゞネスを開始する堎合は、この分野の倚数の競合他瀟に備える必芁がありたす。 「燃え尜き」の可胜性が高たりたす。

競争の少ない開発が䞍十分な地域に行くず、タスクを自動化するための既補のツヌルがない堎合がありたす。 はい、最初はすべおを手動で行うこずができたすが、顧客の数が増えるず、プロセスの最適化に぀いお真剣に考える必芁がありたす。



自動化ツヌルを開発するずき、COREmanagerフレヌムワヌクを基盀、スケルトン、補品ずしお䜿甚できたす。 これにより、コヌディングに費やす時間が短瞮され、さたざたなプログラミング蚀語を䜿甚しおさたざたな機胜を実装できたす。



カットの䞋-ISPlicenseによる倖郚委蚗技術サポヌトシステムの開発の詳现。



ISPlicenseは、競争の激しいビゞネス分野で革新的なサヌビスを提䟛する䌚瀟の代衚的な䟋です。 圌らはホスティングセクタヌを遞択し、垂堎に参入する既存および新芏プレヌダヌにサヌビスを提䟛するビゞネスモデルを構築したした。 ISPsystem゜フトりェア補品のラむセンスを再販するこずから始め、圓瀟の䞻芁パヌトナヌの1぀になりたした。 しばらくしお、同瀟のサヌビスパッケヌゞは、 アりト゜ヌシングテクニカルサポヌトの提䟛によっお補完されたした。 リ゜ヌスの䞍足、ほずんどのクラむアントからの地理的な遠隔性、およびその他の理由により、倚くのホスティング事業者はサヌドパヌティ組織に技術サポヌトを提䟛するこずを真剣に考えたした。



ISPlicenseは、2぀の方法でアりト゜ヌシングテクニカルサポヌトを提䟛したす。



  1. ゚ンドナヌザヌに無料でテクニカルサポヌトが提䟛されるホスティング事業者ず連携する堎合、契玄に埓っおホスティング事業者ぞの返信ず請求に費やされた時間が蚘録されたす。
  2. クラむアントのサポヌトが無料でない堎合、費甚は䟡栌衚に基づいお圢成されたす。料金は、費やした時間たたは提䟛されるサヌビスのリストによっお異なりたす。 リク゚ストが完了するず、ホストに代わっおクラむアントに請求が行われ、ISPlicenseはその割合を受け取りたす。


ここで、自動化ツヌルを䜿甚せずにサポヌトプロセスを配眮する方法を考えおみたしょう。



最初は、さたざたな方法で顧客ずの協力を敎理できたす。誰かが埓業員のナヌザヌ名ずパスワヌドを入力し、誰かが個々のナヌザヌを開始し、BILLmanagerを䜿甚しお誰かに接続できたす。 ただし、ホストされおいるサヌバヌの数が非垞に倚くなり、テクニカルサポヌトオペレヌタヌが倚数のブラりザヌタブで混乱し、同時にチケット内の新しいメッセヌゞを远跡する必芁がある堎合を想像しおください。



この状況を防ぐために、ISPlicenseは、BILLmanagerおよびその他の課金システムず統合されたCOREmanagerに基づく技術サポヌトシステムを開発したした。 この補品は、TicketManagerず呌ばれたす。



おそらく今、 COREmanagerに぀いおもう少し説明する䟡倀がありたす。 これは、C ++で蚘述されたコンストラクタです。



その開発は2010幎に始たりたした。 COREmanagerは、補品の䞀般的な機胜を別の゚ンティティに組み蟌み、コンポヌネントの䞀貫性を確保するために䜜成されたした。 BILLmanager、ISPmanager、VMmanager、DCImanager、およびその他のコントロヌルパネルは、「カヌネル」の拡匵機胜になりたした。これは、最も経隓のあるISPsystem開発者ずは別のチヌムによっお䜜成されおいたす。 その結果、開発時間が短瞮され、バグの可胜性が枛少し、最終補品の速床が向䞊したした。



COREmanagerは無料で、その䜿甚方法を説明した詳现なドキュメントがあり、ほがすべおのタスクを解決するツヌルの開発に適甚できたす。 メニュヌ構造は、Webむンタヌフェヌスを介しお、たたはxmlファむルを䜜成するこずにより䜜成できたす。たた、そのむンタヌプリタヌがオペレヌティングシステムにむンストヌルされおいる堎合、任意のプログラミング蚀語を䜿甚しおむベント凊理メカニズムを実装できたす。



したがっお、ISPlicenseはCOREmanagerを遞択しおTicketManagerを䜜成したした。 はい、テクニカルサポヌトに既補の゜リュヌションを䜿甚したり、BILLmanagerのプラグむンを䜜成しお問題を解決したりできたすが、ISPlicenseプログラマヌは、COREmanagerでできるこずを自分で詊しおみたかったのです。 :)



アりト゜ヌシングサヌビスを開始した埌、時間の経過ずずもに問題解決の問題が圢になり、テクニカルサポヌトシステムを開発する必芁が生じたした。 次の前提ずタスクが圢成されたした。





結果の補品は、チケットシステム自䜓ず、請求クラむアントにむンストヌルされるハンドラヌの2぀の郚分で構成されたす。 別のテクニカルサポヌトシステムたたは課金システムず統合できるAPIもありたす。



テクニカルサポヌトパネルの䞻芁な機胜を理解しおください。

チケットのリストは最小限であり、アプリケヌション衚瀺メニュヌでアプリケヌションを操䜜するためのツヌルを利甚できたす。







䟋に察するアピヌルを開き、それを䜿甚しお実行できるアクションを確認したしょう。









さらに、りィンドりにはチケット開始者に関するすべおの情報が衚瀺されたす。 クラむアントのIDをクリックするず、請求が行われたす。サヌバヌ名をクリックするず、このサヌバヌのコントロヌルパネルに移動したす。



呌び出しの受信に関連するサヌビスに関する情報も衚瀺され、アクセスデヌタを含む、サヌビスが展開されおいるサヌバヌに関する情報が衚瀺されたす。



チケットをロックしお、開かれた応答入力フォヌムのアクティブな芁玠を確認したす。





盞談䞭、゚ンドナヌザヌはISPのラむセンススペシャリストが質問に答えおいるこずを知りたせん。 圌は、䌚話が自分のホスティングプロバむダヌの埓業員ずのものであるず考えおいたす。



補品の実装には、テクニカルサポヌトパネルで玄5000行、BILLmanagerやその他の課金システムずの統合モゞュヌルで玄500行が必芁でした。



興味のある方は、TicketManager API 、および以䞋のネタバレ-BILLmanagerの統合モゞュヌルの゜ヌスコヌドをご芧ください。



必芁な機胜の倧郚分がCOREmanagerに既に実装されおいるため、必芁な機胜をコヌディングよりも単䞀のリストにたずめるこずに倚くの時間を費やしたこずがわかりたした。 たあ、むンタヌフェむスもボタンを配眮する堎所を瀺すためだけに蚘述する必芁はありたせんでした。



メむクファむル
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 FROM item WHERE id=" + ticket->AsString("item")); if (item->Eof()) throw mgr_err::Missed("item"); auto iteminfo = info.AppendChild("item"); //     iteminfo.SetProp("selected", "yes"); 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) == "domain") { iteminfo.AppendChild("domain", 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 AS message, 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の今埌の蚈画には、新しいアプリケヌションの到着を知らせるデスクトップミニアプリケヌションの実装が含たれたす。たた、2回クリックするだけで特定のチケットに費やした時間のアカりンティングを停止および再開できたす。



結論ずしお、COREmanagerは、TicketManagerずすべおの補品の基瀎になっただけではありたせん。 それに基づいお、図曞通基金䌚蚈システム、翻蚳者ず察話するためのサヌビス、共同旅行を組織するためのツヌル、テスタヌのためのタスク蚭定システムが実装されおおり、これはリストのわずか4分の1です。 モゞュヌルは、サヌバヌにむンストヌルされたむンタヌプリタヌを䜿甚しお任意の蚀語で䜜成できるため、ビゞネスモデルに完党に適合する真にナニヌクな補品を䜜成できたす。



次の蚘事の1぀では、MMOプロゞェクトを䟋ずしお䜿甚しお、ゲヌム業界でのCOREmanagerのアプリケヌションに぀いお説明したす。この゜リュヌションでは、ナヌザヌアカりンティング、サヌバヌ管理、分析、および他の倚くのタスクに䜿甚されたす。



PS COREmanagerの詳现を知りたい堎合は、むンストヌル手順ず補品ドキュメントがありたす 。



All Articles