Artifact 53a5e9b73b8431961349518c7cf73fb8aadb111105f04b11ac73905ab7c4b0fe:
- File
backend/src/session.gleam
— part of check-in
[3f662701f1]
at
2026-10-23 03:38:22
on branch trunk
— 把发往来肯的请求收成固定间隔,避免翻页过密被封。
订单轮询不低于 5 分钟,登录、翻页和查明细共用至少 2 秒的间隔。 (user: pluto size: 3767)
import clock import gleam/erlang/process.{type Subject} import gleam/option.{type Option, None, Some} import gleam/otp/actor import gleam/result import pace import ql361api.{type Client, type LoginResult} import ql361api/models.{type ApiError, ApiFailure, AuthExpired} pub type Message { Ensure(reply: Subject(Result(#(Client, LoginResult), ApiError))) Invalidate(reply: Subject(Nil)) Cached(reply: Subject(Bool)) Reserve(reply: Subject(Nil)) } type State { State( username: String, password: String, base_url: String, gap_ms: Int, last_request_ms: Int, session: Option(#(Client, LoginResult)), ) } pub fn start( username: String, password: String, base_url: String, gap_ms: Int, ) -> Result(Subject(Message), actor.StartError) { let state = State( username: username, password: password, base_url: base_url, gap_ms: gap_ms, last_request_ms: 0, session: None, ) case actor.new(state) |> actor.on_message(handle) |> actor.start() { Ok(started) -> Ok(started.data) Error(err) -> Error(err) } } pub fn ensure( session: Subject(Message), ) -> Result(#(Client, LoginResult), ApiError) { actor.call(session, 120_000, Ensure) } pub fn invalidate(session: Subject(Message)) -> Nil { actor.call(session, 5000, Invalidate) } pub fn cached(session: Subject(Message)) -> Bool { actor.call(session, 5000, Cached) } pub fn with_client( session: Subject(Message), f: fn(Client) -> Result(a, ApiError), ) -> Result(a, ApiError) { use pair <- result.try(ensure(session)) reserve(session) case f(pair.0) { Error(AuthExpired(_)) -> { invalidate(session) use pair <- result.try(ensure(session)) reserve(session) f(pair.0) } other -> other } } fn reserve(session: Subject(Message)) -> Nil { actor.call(session, 120_000, Reserve) } fn handle(state: State, message: Message) -> actor.Next(State, Message) { case message { Cached(reply) -> { let active = case state.session { Some(_) -> True None -> False } process.send(reply, active) actor.continue(state) } Invalidate(reply) -> { process.send(reply, Nil) actor.continue(State(..state, session: None)) } Reserve(reply) -> { let state = take_slot(state) process.send(reply, Nil) actor.continue(state) } Ensure(reply) -> case state.session { Some(pair) -> { process.send(reply, Ok(pair)) actor.continue(state) } None -> { let state = take_slot(state) case login(state) { Ok(pair) -> { process.send(reply, Ok(pair)) actor.continue(State(..state, session: Some(pair))) } Error(err) -> { process.send(reply, Error(err)) actor.continue(state) } } } } } } fn take_slot(state: State) -> State { let now = clock.monotonic_ms() let wait = pace.wait_ms(now, state.last_request_ms, state.gap_ms) case wait > 0 { True -> process.sleep(wait) False -> Nil } State(..state, last_request_ms: clock.monotonic_ms()) } fn login(state: State) -> Result(#(Client, LoginResult), ApiError) { case state.username == "" || state.password == "" { True -> Error(ApiFailure("未配置 QL361_USERNAME 或 QL361_PASSWORD")) False -> case ql361api.login(username: state.username, password: state.password) { Error(err) -> Error(err) Ok(#(client, info)) -> { let client = case state.base_url { "" -> client url -> ql361api.with_base_url(client, url) } Ok(#(client, info)) } } } }