Баримт ба цахим гарын үсэг · Documents & e-signatures
io.gerege.nexus.documents апп нь баримтыг батлах урсгалд оруулж, иргэний бодит
eID батламжаар гарын үсэг зуруулдаг. Энэ бичиг нь тэр урсгалын ойлгомжгүй
хэсгүүдийг — яагаад тийм болсныг — тайлбарлана.
1. Гарын үсэг гэж юу вэ
eID Mongolia-д баримт зурах endpoint байхгүй. Байгаа зүйл нь: иргэн өөрийн бүртгэлтэй төхөөрөмж дээр, өөрийн гэрчилгээгээр, RP-ийн сонгосон текстийн эсрэг өгдөг батламж. Тэр батламж нь өөрөө гарын үсэг.
Тиймээс ceremony нь хоёр дуудлагатай:
POST /documents/{id}/sign/eid/start иргэний eID апп руу батлах хүсэлт түлхэнэ
POST /documents/{id}/sign/eid/poll батлах / татгалзах / хугацаа дуусахыг хүлээнэ
poll нь COMPLETE буцаах мөчид гарын үсэг бүртгэгдэнэ. Live горимд API нь
хүсэлтийг 25 секунд хүртэл онгойлгож барьдаг тул клиент AbortSignal дамжуулж,
дэлгэц хаагдмагц тасалдаг.
Статус кодууд ялгаатай гэдэг нь чухал: eID-тэй холбогдож чадаагүй нь 503
(дахин оролдоно), иргэн/оператор буруу зүйл хийсэн нь 400, баримт хүлээгдэхээ
больсон нь 409. Клиентийн цикл 4xx дээр зогсож, 5xx-ийг тэвчдэг — эс бөгөөс
сүлжээний түр асуудал иргэн утсаа гартаа барьж байхад ceremony-г таслах байсан.
Харагдах текст нь хамгаалалт
Иргэнд харагдах текст баримтыг нэрлэдэг:
Гарын үсэг: Хамтран ажиллах гэрээ 2026
«Gerege Nexus-д нэвтрэх» гэдгийг батлах нь гэрээнд гарын үсэг зурахыг зөвшөөрөх биш. Иргэн юуг батлаж байгаагаа зөвхөн энэ текстээр мэддэг.
Хязгаар нь 60 БАЙТ. eID-ийн талбар нь Smart-ID-ийн displayText60, core
client нь dt[:60] гэж байтаар хайчилдаг. Кирилл нь үсэг тутамд 2 байт, тэгэхээр
60 байт ≈ 30 үсэг, бас rune дундуур тасалж хүчингүй UTF-8 гаргаж болно. Иймд:
- Зорилго эхэнд байна — таслагддаг нь сүүл учраас. Гарчгийг эхэнд тавихад «гарын үсэг зурах» гэсэн хэсэг бүхэлдээ таслагдаж, иргэн юу батлаж байгаагаа мэдэхгүй болно (яг ийм байсан).
- Хайчлалт бидний талд, rune-ийн зааг дээр — core-ийн байтын slice хэзээ ч хүрэхгүй.
Session нь баримттай холбогдсон, нэг удаагийн
document_eid_sign_sessions (migration 00015) нь session-ыг эхлүүлсэн
баримттайгаа хамт хадгална. Ингэснээр:
- A баримтын нэрийг харж өгсөн батламжийг B баримт дээр бичих боломжгүй;
- session нэг гарын үсэг гаргасны дараа
consumed_atтэмдэглэгдэж, дахин хэрэглэгдэхгүй (дахин poll хийвэл аль хэдийн гарсан баримтыг буцаана); - өөр tenant session id-г гартаа авсан ч poll хийж чадахгүй.
Session нь eID-ийн өөрийн хугацаатай (expires_at) хадгалагдаж, poll түүнийг
шалгадаг (цагийн зөрүүнд 2 минутын нөөц). Өмнө нь хугацаа хаягдаж байсан тул
хэдэн хоногийн өмнөх батламжийг өнөөдрийн огноотой гарын үсэг болгож болох
байсан.
Гэхдээ eID push session-д хугацаа хэлдэггүй. Тэгэхээр:
- eID хугацаа хэлсэн бол → түүнийг хадгална, poll түүнийг барина;
- хэлээгүй бол → NULL. Бид өөрсдөө хугацаа зохиохгүй.
Учир нь зохиосон хугацаа нь хугацаа биш, тасалбар: push session нь үүссэнээс
хойш 9 минутын дараа ч RUNNING байсныг production дээр хэмжсэн. Хоёр минутыг
хугацаа гэж үзвэл утсаа олж, түгжээгээ тайлж, PIN оруулахад түүнээс их зарцуулсан
иргэн өөрийн өгөх гэж байсан батламжаа алдана (нэвтрэх урсгал дээр яг ийм
байсан, #22-д зассан).
NULL session-ыг signSessionBackstop = 15 минут барина — энэ нь хугацаа биш,
харин log-д хэвтэж байгаа approval id-г хязгааргүй хугацаанд ашиглах боломжийг
хаах хамгаалалт. Хүлээлтийг дуусгадаг нь eID-ийн өөрийн EXPIRED.
Дэлгэц ч мөн адил: eID хугацаа хэлсэн үед л тоолуур харуулна, эс бөгөөс зөвхөн дотоод backstop-той хүлээнэ. Өөрсдийн зохиосон тоолуур нь иргэнийг шаардаж, дараа нь eID хүлээж байхад «хугацаа дууслаа» гэж хэлэх байсан.
API-ийн write deadline нь
eid.PollWindow-оос гаралтай (cmd/api), тэгэхээр 25 секунд онгойлгож барих poll — нэвтрэх ба гарын үсэг хоёулаа — тасрахгүй.
Гарын үсэг бүртгэгдэхэд session нь яг тэр транзакцид зарлагадана. Дараа нь тусад нь хийвэл холболт тасрахад «500 алдаа» гэж хэлээд гарын үсэг нь бүртгэгдсэн байх, session нь дахин хэрэглэгдэхээр үлдэх байдал үүсдэг байсан.
Батлахыг буцаасан регистр нь push илгээсэн регистртэй таарах ёстой. Bodlogo ба батлах хэлхээг эхлэх үед биш, дуусах үед дахин уншина — хооронд минут өнгөрсөн байж болох тул шийдэх нь тэр мөч.
Хэр олон удаа асууж болох вэ
Гарын үсэг эхлүүлэх нь иргэний утас руу мэдэгдэл түлхдэг тул хязгаартай:
минутад 10, бас 10 хүртэл дараалан (maxChainSteps). ДАН нь юу ч түлхдэггүй
ч тэр л хувингаас иддэг — 6 оронтой кодыг таах оролдлогыг зогсоох нь тэрний зорилго.
- Burst нь хамгийн урт хэлхээний урттай тэнцүү нь санамсаргүй биш: арван хүн нэг ширээн дээр дараалан зурах нь бодит явдал, 5-аар тасалбал хагасаас нь татгалзана (яг ингэж туршиж олсон).
- Poll ба уншилтад хязгаар байхгүй. Иргэн утсаа хайх хугацаа нь өөрийнх; хүлээхийг нь боомилбол зөвхөн батламж алдагдана.
Мөн нэг иргэнээс дахин асуувал өмнөх асуулт дуусна: тэр баримт, тэр иргэнд ганц л session хүчинтэй үлдэнэ (өмнө нь 30 удаа асуувал 30 хүчинтэй approval id үлддэг байсан).
ДАН яагаад өөр вэ
ДАН нь энэ платформд батлах push түлхдэггүй. Тиймээс POST /documents/{id}/sign/dan нь регистр + нэг удаагийн кодын хэлбэрээрээ хэвээр, бас
DAN_MOCK_MODE унтарсан үед ажиллахгүй — live ДАН OTP клиент кодод байхгүй.
Дэлгэц дээр суваг тус бүр өөрийн юу хийдгээ бичдэг.
2. Хэн зурж болох вэ
Гурван зүйл шийднэ:
| Хаана | Юу шийднэ |
|---|---|
documents.sign эрх |
Ерөөсөө гарын үсэг зурах/татгалзах эрхтэй эсэх |
Баримтын өөрийн шат (document_approval_steps) |
Дараагийн батламжийг хэн өгөх ёстой |
Гарын үсгийн бодлого (/module/documents/signatures) |
Ямар сувгаар (E-ID/ДАН), нээлттэй шат зөвшөөрөх эсэх |
Нэрлэсэн шат нь өөрөө хүчинтэй. Шат регистрийн дугаар агуулж байвал тэр
батламжийг зөвхөн тэр иргэн өгч болно — bodlogo-д ямар нэг тэмдэглэгээ хийх
шаардлагагүй. (Өмнө нь require_named_signer унтарсан үед шатны регистр огт
хэрэгсэхгүй байсан, гэтэл дэлгэц нь хүчинтэй юм шиг харуулдаг байсан.)
Шат хоосон бол нээлттэй: гарын үсэг зурах эрхтэй хэн ч авч болно — гэхдээ хожмын шатад нэрлэгдсэн хүн нээлттэй шатыг авч чадахгүй. Нэг хүн баримтад нэг л удаа зурдаг тул тэр гарын үсгээ эрт зарцуулбал өөрийн шат хэзээ ч дүүрэхгүй болно. (Монголд түгээмэл дараалал — «Хянагч» нээлттэй, дараа «Захирал» нэрлэгдсэн — яг ийм тохиолдол.)
Аль хэдийн зурсан хүнээс дахин батламж гуйхгүй: хоёр суваг хоёулаа push илгээхээс өмнө татгалздаг.
require_named_signer гэдэг нь «нээлттэй шат байхгүй» гэсэн үг. Тиймээс
хадгалахад шат бүр нэрлэгдсэн, бас өөр өөр хүн нэрлэгдсэн байхыг шаардана —
хоосон шат хэзээ ч дүүрэхгүй, нэг хүнийг хоёр шатад нэрлэвэл (нэг хүн баримтад
нэг л удаа зурдаг тул) хоёр дахь нь хэзээ ч дүүрэхгүй. Хоёр тохиолдолд ч тэр
төрөл хэнд ч батлагдахгүй болно, тиймээс хоёр дэлгэц хоёулаа энэ дүрмийг
шалгадаг — бас хоёулаа нэг advisory lock авдаг тул зэрэг хадгалахад хориглосон
төлөв commit болохгүй.
Дүүрэх боломжгүй нэрлэсэн шат
Хадгалагдсан хэлхээ хэн ч дүүргэж чадахгүй шат агуулж болох гурван зам бий:
- Нэг хүнийг хоёр шатад нэрлэсэн — нэг хүн баримтад нэг л удаа зурдаг тул хоёр дахь нь хэзээ ч дүүрэхгүй.
- Регистрийн дугаар биш юм нэрлэсэн (8 тэмдэгтээс багa, ж. нь
AA9) — ийм дугаарыг ямар ч provider хүлээж авахгүй тул тэр шатыг хэн ч дүүргэж чадахгүй. - Хэлбэржээгүй дугаар (
aa90010111, эсвэл хажуудаа зайтай) — гарын үсгийн зам иргэний өгсөн дугаарыг том болгож, зай хайчилж харьцуулдаг тул ийм шат өөрийн иргэнтэйгээ хэзээ ч тэнцэхгүй.
Гурав дахийг эхлээд зассан нь чухал: AA90010111 ба aa90010111 хоёр нь нэг
иргэн хоёр удаа, гэтэл хэлбэржүүлэхээс нааш давхардал гэж танигдахгүй.
Гурвуулааг өмнө нь хэн ч шалгадаггүй байсан: дугаарыг зөвхөн bodlogo шаардсан
үед л хардаг байсан тул AA9 гэдэг нь «нэг гарын үсэг нэмж» гэсэн хоргүй typo
байсан. Одоо нэрлэсэн шат өөрөө тухайн иргэнд холбогддог болсон тул ийм шат
баримтыг татгалзахаас өөр гарцгүй болгоно.
Тиймээс дүрэм нь нэг Go функцэд байна, SQL-д биш:
| Функц | Юу хийдэг |
|---|---|
normaliseRegNumber |
зай хайчиж, том болгоно — бүх харьцуулалт үүнийг дамжина |
plausibleRegNumber |
rune-аар 8…64 хооронд эсэхийг хэлнэ |
fillableChain |
дүүрэхгүй шатыг нээж, бусдыг хэлбэржүүлж үлдээнэ |
ReplaceWorkflow нь эдгээрээр хадгалахаас татгалздаг,
snapshotApprovalChain нь fillableChain-аар хуулдаг — тэгэхээр баримт дээр
буусан хэлхээ нь хадгалах зам татгалзах хэлхээ хэзээ ч байхгүй.
Гарын үсгийн хоёр зам ч (SignWithDAN, PollEIDSignature) provider-ын буцаасан
дугаарыг normaliseRegNumber-ээр дамжуулдаг — гуравхан харьцуулалт («энэ шат
түүний үү», «аль хэдийн зурсан уу», хүснэгтийн
UNIQUE(document_id, signer_reg_number)) бүгд энэ мөрөн дээр тулгуурладаг тул
provider-ын бичиглэлд найдаж болохгүй.
Гурав дахь урхи — хайлт.
ILIKEнь database-ийн ctype-аар том/жижиг үсгийг тохируулдаг.LC_CTYPE=Cдээр латиныг тохируулж, кириллийг тохируулдаггүй:'ГЭРЭЭ 2026' ILIKE '%гэрээ%'ньfalse. Бидpostgres:16-alpineдээр deploy хийдэг бөгөөд musl-д locale байхгүй тул initdb яг ийм cluster гаргадаг — өөрөөр хэлбэл монголоор хайх нь production дээр том/жижиг үсэг хамааруулах байсан. ТиймээсtitleMatchнь ICU collation байгаа эсэхийг нэг удаа асууж, байвалd.title COLLATE "und-x-icu" ILIKE …гэж хайна (C-ctype дээр хэмжиж баталсан). ICU байхгүй бол хуучин хэлбэрээр ажиллаж, log-д сануулга бичнэ — байхгүй collation-ыг нэрлэвэл хайлт бүхэлдээ алдана.Хоёр урхи, хоёулаа туссан:
- Go-ийн
len()нь байт, Postgres-ийнlength()нь тэмдэгт. Монгол регистр кирилл (УБ99010111= 10 тэмдэгт, 20 байт) тулУБ9901нь 8 байт боловч 6 тэмдэгт. Байтаар хэмжихэд хадгалах зам түүнийг нэрлэсэн шат гэж хүлээж авч, хуулах зам нээлттэй болгодог байсан — дэлгэц баримтад байхгүй батлагчийг нэрлэж, тэр шатыг хэн ч дүүргэх байдалд оруулна. Одоо бүх хязгаар rune-аар (VARCHAR(64)ч тэмдэгтээр тоолдог).- SQL-ийн
upper()нь cluster-ийн ctype-аас шалтгаална.LC_CTYPE=Cдээрupper('уб99010111')нь хөдлөхгүй, Go бол хөдөлгөнө. Тиймээс хэлбэржүүлэлт Go дээр байна: migration00017§0 нь хадгалагдсан хэлхээг сайн дураараа зассан ч, баримт дээр буух хэлхээ нь locale-аас үл хамааран зөв байна.
Хожмын шатыг нээх нь зөв уншлага: tenant тэр тооны батламж хүссэн бөгөөд тэр тоогоо авсаар байна, зүгээр л тэр шатыг зурж чадах хүн дүүргэнэ. Ингэж §0-г хуулахаас өмнө гүйлгэдэг нь хуучин гарын үсгийг өөрийн шатад буулгах боломжийг өгдөг — үгүй бол хэлхээний сүүлд «парк» болно.
00019 нь зөвхөн тэмдэглэгээг авч үздэг: §0 хэлхээг зассаны дараа хангагдах
боломжгүй болсон «зөвхөн нэрлэсэн хүн» flag-ийг арилгана.
documents.sign нь зориудаар documents.manage-аас салангид: баримт зохиох
нь гарын үсэг зурах эрх биш. Сервер тал дээр appRequestPermission нь
/sign/… ба /reject-ыг documents.sign-аар шалгана (/signatures уншилт нь
documents.read хэвээр).
Анхаар: tenant үүсэх үеийн trigger нь
admin-д тухайн үед байгаа бүх эрхийг,manager/user-т зөвхөн%.readба%.manage-ыг олгодог. Тиймээсdocuments.signнь manager/user-т хэзээ ч автоматаар олгогдохгүй — tenant админ өөрөө олгох ёстой. Эрхийн мөр өмнө нь суулгасан tenant-уудад байдаггүй байсныг migration00016нөхсөн.
3. Хэдэн гарын үсэг
document_signatures (migration 00014) нь ledger: гарын үсэг тутамд нэг
мөр, хоёр unique хязгаартай:
| Хязгаар | Юу боломжгүй болгодог | Модуль юу гэж хэлдэг |
|---|---|---|
..._once_per_signer (баримт, зурагч) |
нэг хүн хоёр батламж болж тоологдох | ErrAlreadySigned → 409 |
..._one_per_approval (баримт, шат) (migration 00020) |
хоёр өөр хүн нэг батламжид бичигдэх | «нэг батламжид хоёр гарын үсэг» → 500, учир нь энэ нь дуудагчийн бус бидний алдаа |
Хоёр дахийг зориудаар ялгаж хэлдэг: «та аль хэдийн зурсан» гэдэг нь эхнийх дээр үнэн, хоёр дахь дээр хуурамч — оператор сэтгэл амарч явах ч батламж бүртгэгдээгүй байх байсан.
Мөр тус бүр аль шатыг дүүргэснийг (step_order) хадгална. Ямар ч шат
дүүргээгүй гарын үсэг (хэлхээ нэрлээгүй хүн) нь хэлхээний хамгийн том дугаарын
дараа бичигдэнэ — тооны дараа биш: 2, 3 гэж дугаарлагдсан хэлхээ дээр тоогоор
бодвол бодит нэрлэсэн шат руу оршиж, тэр батламжийг өөр хүнд бичих байсан.
Баримт өөрийн шаардлагаа өөртөө агуулна
Баримт батлах хүлээлтэд орох мөчид (үүсэх эсвэл route) тухайн үеийн хэлхээ нь
баримт дээр хуулбарлагдана (document_approval_steps), шаардагдах тоо нь
document_records.required_signatures-д бичигдэнэ (migration 00017).
Өмнө нь шаардлагыг уншилт бүрд төрлийн хэлхээнээс тоолдог, дуусгавар болох шийдвэрийг цоожны гадна уншсан тоотой харьцуулдаг байсан. Тэндээс гурван алдаа гарсан:
- хооронд нь хэлхээ засагдвал баримт шаардсанаас бага гарын үсгээр батлагдаж болох;
- хэлхээг хураахад гарын үсэг зурагдсан баримт PENDING хэвээр гацаж, дэлгэц «2/2 дүүрэн» ба «Хүлээгдэж буй» гэсэн хоёр зөрчилтэй зүйл зэрэг харуулдаг, бас түүнийг дуусгах арга байхгүй;
- хэлхээ засахад хэдэн сарын өмнө шийдэгдсэн баримтын явц дахин бичигдэж, батлагдсан гэрээ «3-аас 1 гарын үсэг» гэж хэлдэг болдог.
Хэлхээ нэрлээгүй хүний хуучин гарын үсэг нь нэрлэсэн шатыг эзэлдэггүй — хэлхээний гадна дугаарлагдана. Эс бөгөөс нэрлэгдсэн иргэн өөрийн шатаас үүрд хаагдаж, баримт нь түүнгүйгээр батлагдаж, ledger нь тэр батламжийг өөр хүнд хамааруулах байсан.
Одоо баримт эхэлсэн дүрмээ хадгална. Дараагийн шат нь дүүрээгүй шатуудын хамгийн бага дугаар — «гарын үсгийн тоо + 1» биш. Учир нь хэлхээ баримтын шинж болохоос өмнөх гарын үсгүүд ямар ч дараалалтай байж болно (тэр үед дараалал хамаагүй байсан), тиймээс migration тэднийг нэрлэсэн шатад тааруулж тавьдаг. Ингэснээр шат 1 хоосон байхад шат 2 дүүрсэн байх нь бүрэн хэвийн, бас баримтыг дуусгах боломжтой хэвээр байна.
Дуусгавар болох нөхцөл нь: хэлхээний шат бүр дүүрсэн ба гарын үсгийн тоо
шаардлагаас доогуур биш. Хоёр тоог харьцуулах нь хүрэлцэхгүй — тиймээс API нь
outstanding_steps (дүүрээгүй шатны тоо) буцаадаг, дэлгэц «дүүрэн» гэж зөвхөн тэр
тэг байхад хэлдэг. Эс бөгөөс хэлхээ нэрлээгүй хүний гарын үсэг тоонд нэмэгдэж,
амбер «Хүлээгдэж буй»-ийн хажууд эмералд «дүүрэн» гарч ирнэ.
Хэлхээний ямар ч шатыг дүүргээгүй хуучин гарын үсэг (хэн ч нэрлээгүй, бас нээлттэй шат ч байхгүй) хэлхээний гадна дугаарлагдана, бас шаардлагад нэмэгдэнэ — тэгэхээр баримт «2-аас 2» гэж хэлж байхад нэрлэсэн батламж өртэй хэвээр байх боломжгүй.
Дуусгавар болохыг шийддэг бүхэн — шаардлага, тоолол, дараагийн шат — бүгд цоожилсон транзакцид уншигдана. Иргэний танилтын дуудлага цоожийн гадна байрлана: eID-ийн сүлжээний хүсэлтийг мөр цоожилсон хэвээр хийж болохгүй.
document_records нь хамгийн сүүлийн гарын үсгийг өөр дээрээ тольдож хадгална,
ингэснээр жагсаалт нэг query хэвээр байна. Бүтэн түүх нь ledger-т.
4. Төлөвийн эргэлт
DRAFT ──POST /{id}/route──▶ PENDING_APPROVAL ──хэлхээ дуусав──▶ APPROVED
│
└──POST /{id}/reject──▶ REJECTED
CreateDocument нь шууд PENDING_APPROVAL бичдэг тул аппын үүсгэсэн зүйл
ноорог болдоггүй. Энэ нь зориудаар: үүсгэх нь нэг алхам болж, оператор «дараа
илгээх»-ийг санах шаардлагагүй, бас мартагдсан ноорог хэн ч мэдэлгүй хэвтэхгүй.
DRAFT нь баганын үндсэн утга бөгөөд route нь өөр замаар (шууд SQL, хожмын
модуль) орж ирсэн мөрийг хөдөлгөнө.
Нэг алхмын үнэ нь гарчгийн алдаа шууд батлагчид хүрэх — тиймээс:
PUT /documents/{id}/title гарын үсэг зурагдтал гарчгийг засна
Анхны гарын үсгийн дараа гарчиг хөшинө (409 ErrTitleFrozen). Учир нь тэр
гарчиг бол иргэн өөрийн төхөөрөмж дээр уншиж зөвшөөрсөн текст
(signatureDisplayText-ийг хар) — дараа нь өөрчлөгдвөл тэр зөвшөөрөл өөр зүйлийн
зөвшөөрөл болно. Шийдэгдсэн (APPROVED/REJECTED) баримт ч түүх тул хөшинө.
Хамгаалалт нь тэр UPDATE дотор байна (NOT EXISTS (гарын үсэг)), уншаад дараа
бичдэггүй — эс бөгөөс уншилт ба бичилтийн хооронд гарын үсэг буух зай гарна, тэр нь
яг засагдах ёсгүй тохиолдол. Аудит нь хуучин ба шинэ гарчиг хоёуланг бичнэ. Дэлгэц дээр таних төлөв бүр өөрийн шошготой,
танихгүй төлөв raw хэвээр харагдана — «Хүлээгдэж буй» гэж худлаа хэлэхгүйн
тулд.
4.5 Жагсаалт: хуудас, шүүлт, хайлт
GET /documents нь бүхэл жагсаалтыг биш, нэг хуудсыг буцаана:
{ "documents": [ … ], "total": 20007, "limit": 200, "offset": 0 }
Учир нь мөр тутам өөрийн гарын үсгийн тоо ба дүүрээгүй шатыг тоолдог (documentColumns)
— 20 мянган баримт нь хуудас ачаалах тутам 5.9 MB байсан. Одоо 200 мөр ≈ 60 KB.
| Параметр | Юу хийдэг |
|---|---|
status |
DRAFT/PENDING_APPROVAL/APPROVED/REJECTED. Танихгүй бол 400, хоосон хуудас биш |
doc_type |
DocTypes-ийн нэг. Танихгүй бол 400 |
q |
Гарчгийн дотор хайх (том/жижиг үсэг хамаарахгүй) |
order |
oldest — эс бөгөөс шинэээс |
limit |
Үндсэн 200, дээд тал 500 (хэтрүүлбэл багасгаад буцаж хэлнэ) |
offset |
Хуудсыг ур гүй (тест ба тогтвортой хэсэгт) |
after_at + after_id |
Курсор — өмнө харсан мөрийн дараагаас. Хоёулаа хамт, эс бөгөөс 400 |
total нь чимээгүй хязгаарыг таслах зорилготой. Дэлгэц «701-ээс хамгийн шинэ
200-г харуулж байна» гэж хэлж, «Дараагийнхыг ачаалах» нь дараагийн хуудсыг нэмж
авна.
Дэлгэц нь offset-оор биш курсорoор явдаг. offset нь бусад хүн өөрчилж
байгаа олонлогийн эхнээс тоолдог: хоёр хүсэлтийн хооронд нэг баримт батлагдвал
бусад нь нэгээр дээшилж, дараагийн хүсэлт нэгийг алгасана — алгасагдсан баримт
нь ямар ч дэлгэц дээр байхгүй болно, батлах дараалал дээр бол «хэн ч зурж чадахгүй
гарын үсэг». Курсор нь зай биш, газар нэрлэдэг тул хооронд нь юу ч хөдөлж
чадахгүй. 700 баримтыг хуудасны хооронд нэгийг батлуулж явж туршсан: 700 мөр, 700
өөр, алгасалт үгүй.
Курсорын предикат нь (tenant_id, created_at, id) index-ыг ашигладаг (migration
00021): 20 мянган баримттай tenant дээр хуудас нь 2.5мс → 0.06мс (index-only,
200 бичлэг уншина). Index нэг — статусаар тэргүүлсэн хувилбар нь дарааллыг 0.08мс
хурдан үйлчлэх ч шүүлтгүй жагсаалтыг огт үйлчлэхгүй.
Дуусахыг тооноос үл хамааран мэднэ: хуудас дүүрэн биш ирвэл өгөгдөл дууссан
гэсэн үг. total нь хооронд хөдөлж болох тул түүнтэй харьцуулах нь бүрэн бус
жагсаалтыг «бүрэн» гэж хэлж болно.
(Түрүүлж limit-ыг өсгөх хувилбар бичсэн — сервер 500 дээр таслахад товч
ажиллахаа больж, 500-аас хойшхи баримтыг хаанаас ч гарын үсэг зурах боломжгүй
болсон.)
Батлах дараалал нь серверээс шүүнэ, клиент дээр биш: батлагдсан баримтаар дүүрсэн хуудасны сүүлээс хүлээж байгаа баримт унаж болохгүй. Бас хуучнаас эрэмбэлдэг — дараалал хуучин талаасаа ажиллагддаг, бас «хамгийн урт хүлээлт» tile нь тэгэхэд л зөв байна (шинээс эрэмбэлбэл хамгийн урт хүлээсэн нь сүүлийн хуудсанд байх болно).
Нэг ачаалалт нь хэд хэдэн хуудсыг гүйдэг тул:
- шүүлтийг нэг л удаа уншина (хуудас тутамд биш) — эс бөгөөс хоёр өөр шүүлтээс бүрдсэн мөрийн олонлог гарна;
- зөвхөн хамгийн шинэ ачаалалт бичих эрхтэй (ticket) — хоцорсон нь өөрийн уналтаа ч дуугардаггүй;
- хайлтын хайрцгийн
blurнь өөрчлөгдсөн үед л дахин ачаална, бас ачаалах зуур хүснэгт байрандаа үлдэнэ — эс бөгөөс дарсан товч (Гарын үсэг / Татгалзах) хүснэгттэй хамт унаж, үйлдэл болохгүй байсан.
Хайлт нь бичсэн чигээрээ: %, _, !-ийг wildcard биш гэж үзнэ
(ESCAPE '!'). Кирилл том/жижиг үсгийн тухай §2-ын урхиг хар.
5. Аудит
Гарын үсэг зурах нь энэ модулийн хамгийн аудит шаардсан үйлдэл. audit.Record
дараах зүйлсийг бичнэ:
| Үйлдэл | Хэзээ | Юу агуулдаг |
|---|---|---|
documents.created |
Баримт үүсэхэд | төрөл, гарчиг, төлөв, шаардлагатай гарын үсгийн тоо |
documents.routed |
Батлахад илгээхэд | төрөл, баримтад буусан хэлхээ |
documents.signature_requested |
eID батлах хүсэлт илгээхэд | хэн асуусан, харагдах текст, session |
documents.signed |
Гарын үсэг бүртгэгдэхэд | хэн зурсан, суваг, гэрчилгээ, шат, хэлхээ дуусав уу |
documents.rejected |
Татгалзахад | төрөл, гарчиг, тэр үед хэдэн гарын үсэг байсан |
documents.signature_policy_changed / .approval_chain_changed / .retention_rule_changed |
Тохиргоо | шинэ утга |
documents.template_created / .template_changed / .template_deleted |
Загвар | нэр, төрөл (устгасны дараа мөр байхгүй тул нэрийг бүртгэлд үлдээнэ) |
Асуусан хүн ба зурсан хүн хоёр өөр зүйл: ledger нь зөвхөн хоёр дахийг мэддэг тул хүсэлтийг тусад нь бичдэг.
Бүртгэл бүр actorFor(ctx) — өөрөөр хэлбэл аль операторын session тэр үйлдлийг
хийснийг — иргэний регистрийн хажууд агуулна. Платформ хоёрыг хооронд холбож
чаддаггүй (users-д регистр байхгүй) ч «аль операторын session тэр иргэний гарын
үсгийг гаргасан» гэдэг нь бүртгэлээс уншигдана.
documents.routed нь баримтад буусан хэлхээг агуулдаг нь санамсаргүй биш:
төрлийн хэлхээ хожим засагдаж болох ч тэр баримтад хүрэхгүй тул «энэ баримтыг ямар
дүрмээр батлав» гэдгийг зөвхөн энэ бүртгэлээс мэдэж болно.
6. Тест
# Нэгж тестүүд — DB шаардахгүй
go test ./internal/apps/documents/
# Бүрэн тестүүд — migration хийсэн хаяж болох DB шаардана
DOCUMENTS_TEST_DATABASE_URL=postgres://... go test ./internal/apps/documents/
Integration тестүүд нь SQL дотор амьдардаг зүйлсийг барина: төлөвийн хамгаалалт, нэг зурагч нэг л удаа гэсэн constraint, хэлхээнээс гарах тоолол, tenant-ийн хуваалт, бас гол хоёр нь — батламжийг өөр баримт руу зөөж болохгүй ба session нь өөр tenant-д хамаарахгүй.
CI нь DOCUMENTS_TEST_DATABASE_URL-ыг тавьж, тестүүд чимээгүй skip болвол job-ыг
унагадаг — gov_services-д байдгийн адил, эс бөгөөс skip нь pass шиг харагдана.
7. Гараар турших
Mock дээрх нэг зүйл: eID mock нь session-ыг санах ойд барьдаг тул API-г дундуур нь дахин ачаалбал явж байсан ceremony
EXPIREDболно. Энэ нь mock-ийн шинж, production-ы алдаа биш — live үед session нь eID тал дээр байдаг тул deploy түүнийг таслахгүй. (Тэгэхэд ч баримт эрүүл үлдэж, шинэ ceremony эхлүүлж болдгийг туршиж баталсан.)
Mock горимд бүтэн ceremony-г HTTP-ээр гүйцэтгэж болно. EID_MOCK_MODE=true үед
иргэн 1.5 секундын дараа өөрөө "батална".
API=http://127.0.0.1:8080/api/v1
TOKEN=$(curl -s -X POST "$API/auth/login" -H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"Password123!"}' | jq -r .token)
AUTH="Authorization: Bearer $TOKEN"
# Хоёр шаттай хэлхээ: нэг дэх нь нэрлэгдсэн, хоёр дах нь нээлттэй
curl -s -X PUT "$API/documents/workflows/CONTRACT" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"steps":[{"name":"Хэлтсийн дарга","signer_reg_number":"AA90010111"},
{"name":"Захирал","signer_reg_number":""}]}'
DOC=$(curl -s -X POST "$API/documents" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"title":"Хамтран ажиллах гэрээ 2026","doc_type":"CONTRACT"}' | jq -r .id)
# Танихгүй хүн нэрлэгдсэн шатыг авч чадахгүй → 400
curl -s -X POST "$API/documents/$DOC/sign/eid/start" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"reg_number":"ZZ99999999"}'
# Нэрлэгдсэн хүн ceremony-г эхлүүлнэ
SID=$(curl -s -X POST "$API/documents/$DOC/sign/eid/start" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"reg_number":"AA90010111"}' | jq -r .session_id)
# COMPLETE болтол poll (mock: 2-3 удаа)
curl -s -X POST "$API/documents/$DOC/sign/eid/poll" -H "$AUTH" -H 'Content-Type: application/json' \
-d "{\"session_id\":\"$SID\"}"
# Хоёр дах шат нээлттэй тул хэн ч дуусгана → APPROVED 2/2
curl -s -X POST "$API/documents/$DOC/sign/dan" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"reg_number":"ZZ99999999","otp_code":"123456"}'
# Ledger: шат тус бүрийг хэн дүүргэсэн
curl -s "$API/documents/$DOC/signatures" -H "$AUTH"
Дараах зүйлсийг ингэж баталсан: /documents/templates нь баримтын id гэж
уншигдахгүй; харагдах текст яг 60 байт, зорилго эхэндээ; шатны эрх 400-аар
тодорхой татгалздаг; шийдэгдсэн баримт 409 буцаана; урт гарчиг SQLSTATE биш
ойлгомжтой 400 өгнө.
8. Одоогийн цоорхойнууд
ДАН live биш.
AuthenticateDANCitizenнь mock-гүйгээр алдаа буцаана.Прод дээр E-ID нь
EID_MOCK_MODE=falseүед бодит холбогчоор ажиллана (EID_RP_UUID/EID_RP_SECRETшаардана); mock горимд батламж 1.5 секундын дараа өөрөө "батлагдана".Зурагчийг нэвтэрсэн хэрэглэгчтэй тулгадаггүй.
usersхүснэгтэд регистр хадгалагддаггүй (eID linking нь зориудаар PII биш digest хадгалдаг). Одоогоор баримтын шатад нэрлэгдсэн регистртэй л тулгаж болно.Хэлхээг засах нь хүлээж байгаа баримтуудад хүрэхгүй — энэ нь зориудаар. Хүсвэл хүлээж байгаа баримтуудыг шинэ хэлхээ рүү шилжүүлэх тусдаа үйлдэл хэрэгтэй болно. Дагавар нь: хэлхээгүй үед үүссэн баримт хэлхээ, bodlogo хожим тохируулсан ч нэг нээлттэй гарын үсгээр батлагдана — тэр эхэлсэн дүрэм нь тэр байсан.
Ноорог төлөв: шийдвэр гарсан.
CreateDocumentнь баримтыг шуудPENDING_APPROVALболгож бичдэг (origin/mainдээр ч ингэж байсан — платформын өмнөх шийдвэр). Гэтэлdocument_records.status-ийн default ньDRAFT, басRouteDocument, «Батлахад илгээх» товч, DRAFT тэмдэглэгээ,documents.routedаудит бүгд байгаа. Тэгэхээр хоёр уншлага байна:- Одоогийнх: үүсгэмэгц батлах дараалалд орно. «Батлахад илгээх» нь зөвхөн өөр аргаар (шууд SQL, хожмын модуль) үүссэн ноорогт хэрэгтэй.
- Товч ямар байхыг хэлж байгаа нь: үүсгээд, шалгаад, дараа нь илгээх. Ингэвэл гарчгаа зассан ч болно.
Нэг дахийг сонгосон — операторт хялбар нь тэр: нэг алхам, санах юм байхгүй, мартагдсан ноорог гарахгүй. Хоёр алхам бол «илгээх»-ийг мартсан баримт хэн ч мэдэлгүй хэвтэх бөгөөд түүнийг сануулах юм байхгүй.
Нэг алхмын үнэ (гарчгийн алдаа засагдахгүй) нь
PUT /documents/{id}/title-ээр хаагдсан: гарын үсэг зурагдтал засаж болно, дараа нь хөшинө (§4-ийг хар).RouteDocumentбаDRAFTнь өөр замаар орж ирсэн мөрийн гарц болж үлдэнэ — тэднийг устгавал тийм мөр хаанаас ч хөдлөхгүй болно.Татгалзал нь нэрлэсэн хүнд хязгаарлагдахгүй.
documents.signэрхтэй хэн ч татгалзаж болно, хэлхээ өөр хүнийг нэрлэсэн байсан ч. Батлах нь иргэний танилтад тулгуурладаг, татгалзал нь оператор дээр тулгуурладаг — бид операторын регистрийг мэддэггүй (дээрх цоорхой). Хэн татгалзсаныг аудит бүртгэдэг (documents.rejected).Загварт агуулга байхгүй — баримт нь гарчиг + төрөл.
Хадгалалт юу ч устгадаггүй — хугацаа дууссаныг л тайлагнана.
Эрхийн нэр/тайлбар зөвхөн англи —
internal.PermissionDefinition-дLabelsталбар байхгүй (платформын цоорхой,TRANSLATION_GUIDE.md§7).