This commit is contained in:
2026-05-14 13:42:12 +08:00
112 changed files with 5766 additions and 626 deletions

View File

@@ -30,6 +30,7 @@ use time::OffsetDateTime;
use tracing::{info, warn};
use crate::config::AppConfig;
use crate::wechat_pay::{WechatPayClient, map_wechat_pay_init_error};
use crate::wechat_provider::build_wechat_provider;
const ADMIN_ROLE: &str = "admin";
@@ -55,6 +56,7 @@ pub struct AppState {
wechat_auth_state_service: WechatAuthStateService,
wechat_auth_service: WechatAuthService,
wechat_provider: WechatProvider,
wechat_pay_client: WechatPayClient,
#[cfg_attr(not(test), allow(dead_code))]
ai_task_service: AiTaskService,
spacetime_client: SpacetimeClient,
@@ -110,6 +112,7 @@ pub enum AppStateInitError {
RefreshCookie(RefreshCookieError),
AuthStore(String),
SmsProvider(SmsProviderError),
WechatPay(String),
Oss(OssError),
Llm(LlmError),
}
@@ -174,6 +177,8 @@ impl AppState {
WechatAuthStateService::new(auth_store.clone(), config.wechat_state_ttl_minutes);
let wechat_auth_service = WechatAuthService::new(auth_store.clone());
let wechat_provider = build_wechat_provider(&config);
let wechat_pay_client =
WechatPayClient::from_config(&config).map_err(map_wechat_pay_init_error)?;
let refresh_session_service =
RefreshSessionService::new(auth_store.clone(), config.refresh_session_ttl_days);
// AI 编排服务当前先挂接内存态 store后续再按 task table / procedure 接到 SpacetimeDB 真相源。
@@ -206,6 +211,7 @@ impl AppState {
wechat_auth_state_service,
wechat_auth_service,
wechat_provider,
wechat_pay_client,
ai_task_service,
spacetime_client,
llm_client,
@@ -333,23 +339,14 @@ impl AppState {
OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000,
)
.map_err(|_| SpacetimeClientError::Runtime("认证快照更新时间超出 i64 范围".to_string()))?;
// 本地 auth_store 是当前认证请求的即时真相源SpacetimeDB 快照用于跨进程恢复。
// 本地 auth_store 是当前认证请求的即时真相源SpacetimeDB 正式认证表用于跨进程恢复。
// 远端数据库挂起或网络异常时,只降级远端恢复能力,不能让已成功的登录/刷新/退出回滚为失败。
#[cfg(not(test))]
if let Err(error) = self
.spacetime_client
.upsert_auth_store_snapshot(snapshot_json, updated_at_micros)
.import_auth_store_snapshot_json(snapshot_json, updated_at_micros)
.await
{
warn!(
error = %error,
"认证快照写入 SpacetimeDB 失败,当前认证流程继续"
);
return Ok(());
}
// 写入快照后尝试拆入正式认证表;失败只影响远端表恢复,不阻断当前认证响应。
#[cfg(not(test))]
if let Err(error) = self.spacetime_client.import_auth_store_snapshot().await {
warn!(
error = %error,
"认证快照导入 SpacetimeDB 正式表失败,当前认证流程继续"
@@ -454,6 +451,10 @@ impl AppState {
&self.wechat_provider
}
pub fn wechat_pay_client(&self) -> &WechatPayClient {
&self.wechat_pay_client
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn ai_task_service(&self) -> &AiTaskService {
&self.ai_task_service
@@ -600,6 +601,54 @@ impl AppState {
#[cfg(test)]
impl AppState {
pub(crate) fn seed_test_refresh_session_for_user(
&self,
user: &module_auth::AuthUser,
seed: &str,
) -> String {
let session = self
.refresh_session_service()
.create_session(
module_auth::CreateRefreshSessionInput {
user_id: user.id.clone(),
refresh_token_hash: platform_auth::hash_refresh_session_token(&format!(
"test-refresh-token-{seed}"
)),
issued_by_provider: module_auth::AuthLoginMethod::Password,
client_info: module_auth::RefreshSessionClientInfo {
client_type: "web_browser".to_string(),
client_runtime: "test".to_string(),
client_platform: "test".to_string(),
client_instance_id: Some(seed.to_string()),
device_fingerprint: Some(format!("test-device-{seed}")),
device_display_name: "Test Browser".to_string(),
mini_program_app_id: None,
mini_program_env: None,
user_agent: Some("GenarrativeApiServerTest/1.0".to_string()),
ip: Some("127.0.0.1".to_string()),
},
},
OffsetDateTime::now_utc(),
)
.expect("test refresh session should create");
session.session.session_id
}
pub(crate) fn seed_test_refresh_session_for_user_id(
&self,
user_id: &str,
seed: &str,
) -> String {
let user = self
.auth_user_service()
.get_user_by_id(user_id)
.expect("test user lookup should succeed")
.expect("test user should exist");
self.seed_test_refresh_session_for_user(&user, seed)
}
fn cache_test_creation_entry_config(&self, config: CreationEntryConfigResponse) {
*self
.test_creation_entry_config
@@ -801,8 +850,8 @@ fn select_auth_store_restore_candidate(
fn auth_store_restore_source_priority(source: AuthStoreRestoreSource) -> u8 {
match source {
AuthStoreRestoreSource::SpacetimeSnapshot => 3,
AuthStoreRestoreSource::SpacetimeTables => 2,
AuthStoreRestoreSource::SpacetimeTables => 3,
AuthStoreRestoreSource::SpacetimeSnapshot => 2,
AuthStoreRestoreSource::LocalFile => 1,
}
}
@@ -812,7 +861,7 @@ impl fmt::Display for AppStateInitError {
match self {
Self::Jwt(error) => write!(f, "{error}"),
Self::RefreshCookie(error) => write!(f, "{error}"),
Self::AuthStore(error) => write!(f, "{error}"),
Self::AuthStore(error) | Self::WechatPay(error) => write!(f, "{error}"),
Self::SmsProvider(error) => write!(f, "{error}"),
Self::Oss(error) => write!(f, "{error}"),
Self::Llm(error) => write!(f, "{error}"),