Kvietimas nuo pradžios iki galo, vienoje transakcijoje
Šeši failai kortelėse, iš apačios į viršų: rodinio modelis, jo atvaizdavimai, kvietimo valdiklis, tada savininko detalės puslapis, rodantis formą ir sąrašą. signing.go → signing_view.go → web/signing.go → documents.go → documents.templ → router.go.
1. internal/handlers/signing.go (naujas) — InviteSigners ir parseEmails. Valdiklis — transakcija gyvai:
tx, err := h.Pool.Begin(r.Context())
// ...
defer tx.Rollback(context.Background()) // nieko nedaro po sėkmingo Commit
q := h.Queries.WithTx(tx) // tie patys užklausų metodai, susieti su tx
for _, addr := range emails {
raw, hash, err := auth.GenerateToken() // raw → nuoroda, hash → eilutė
q.CreateSigner(r.Context(), db.CreateSignerParams{ /* ...TokenHash: hash... */ })
invites = append(invites, invite{email: addr, link: h.Cfg.BaseURL + "/sign/" + raw})
}
q.MarkDocumentSent(r.Context(), /* ... */)
tx.Commit(r.Context())
defer tx.Rollback — apsaugos tinklas: jei bet kuris žingsnis grįžta anksti, atšaukimas suveikia ir niekas neįrašoma; po sėkmingo Commit atšaukimas nieko nedaro. El. laiškai apdorojami ir siunčiami po commit, tad konsolės siuntėjas spausdina nuorodas tik tikrai išsiųstam dokumentui. parseEmails suskaido teksto lauką (nauja eilutė/kablelis/tarpas), normalizuoja, patikrina ir pašalina dublikatus; tuščias ar visai neteisingas sąrašas iš naujo atvaizduoja detalės puslapį su klaida, o ne siunčia.
2. internal/handlers/signing_view.go (naujas) — viewSigner atvaizduoja db.Signer į web.SignerView, o viewSigners sudaro SignerRoster (viso + pasirašiusiųjų skaičius). Pasirašymo būsenos laukai sujungti dabar; jie lieka tušti, kol kas nors pasirašys kitą pamoką.
3. internal/web/signing.go (naujas) — SignerView ir SignerRoster rodinio struktūros. Paprastos eilutės, jokio pgtype.
4. internal/handlers/documents.go (keitimas) — ViewDocument dabar deleguoja naujam renderDocumentDetail, kuris įkelia pasirašytojų sąrašą ir atvaizduoja detalės puslapį. Ir peržiūros maršrutas, ir nepavykęs kvietimas atvaizduoja per šį vieną kelią.
5. internal/web/documents.templ (keitimas) — DocumentDetail įgyja du parametrus (roster, errMsg) ir du skyrius: kvietimo formą, kol dokumentas juodraštis, ir pasirašytojų sąrašą, kai jis išsiųstas.
6. internal/handlers/router.go (keitimas) — vienas maršrutas autentifikuotoje grupėje: pr.Post("/documents/{id}/invite", h.InviteSigners).
Patikra — perseeduok, paleisk, prisijunk, atverk juodraščio detalės puslapį:
- Invite signers forma jau ten. Įvesk du adresus (po vieną eilutėje) ir Send for signature.
- Puslapis persikrauna rodydamas dokumentą kaip sent, su Signers (0 of 2 signed) sąrašu — abu
pending. - Dev pašto siuntėjas atspausdino kiekvieną pasirašymo nuorodą į terminalą:
────────────────────────────────────────────────────────
To: alice@example.com
Subject: Please sign "contract.pdf"
You've been invited to sign "contract.pdf" on SignFlow.
Open your signing link (valid 30 days):
http://localhost:8080/sign/9f3b2c...e1
────────────────────────────────────────────────────────
- Patvirtink, kad DB saugo tik maišas ir kad kvietimas buvo atominis (dokumentas
sentir abi pasirašytojų eilutės yra):
$ psql "$DATABASE_URL" -c "select email, status, left(token_hash,16) as token_hash from signers;"
email | status | token_hash
-------------------+---------+------------------
alice@example.com | pending | 3a7f9c2b1e0d4f6a
bob@example.com | pending | c1d2e3f4a5b6c7d8
Neapdorotas žetonas gyvena tik išsiųstoje nuorodoje; eilutė laiko jo SHA-256. Tos nuorodos atvėrimas — kita pamoka.