Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ YK_ALLOWED_HOSTS=www.yuketang.cn,pro.yuketang.cn,changjiang.yuketang.cn,huanghe.
# 复用雨课堂的 CaptchaAppId(实测不限域名)
YK_CAPTCHA_APP_ID=2091064951

# --- 微信公众号(M5 微信内扫码签到;不填则微信内自动降级为相机扫码)---
# 需已认证服务号/订阅号的 JS 安全域名配置,且与部署域名一致
WECHAT_APP_ID=
WECHAT_APP_SECRET=

# --- 日志 ---
# tracing EnvFilter,如 info / debug / rain_course_backend=debug,tower_http=info
LOG_DIR=logs
Expand Down
1 change: 1 addition & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ anyhow = "1"
axum-extra = { version = "0.10", features = ["cookie"] }
hmac = "0.12"
sha2 = "0.10"
sha1 = "0.10"
hex = "0.4"

[dev-dependencies]
Expand Down
14 changes: 13 additions & 1 deletion backend/src/auth/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,34 @@ use super::token;
use super::yk_client::{self, YkClient};
use crate::config::Config;
use crate::error::AppError;
use crate::wechat::client::WechatClient;

pub const SESSION_COOKIE: &str = "sid";

/// 全局应用状态
pub struct AppState {
pub config: Config,
pub yk: YkClient,
/// 微信 JS-SDK 客户端(M5):access_token / jsapi_ticket 内存缓存
pub wechat: WechatClient,
/// user_id → 雨课堂 Cookie 头(内存态,重启即失)
pub sessions: RwLock<HashMap<i64, String>>,
/// WS 房间 Hub(docs/DESIGN.md §4,内存态)
pub hub: Arc<crate::ws::hub::Hub>,
}

impl AppState {
/// 统一构造(Hub 使用配置中的 limits)
/// 统一构造(Hub 使用配置中的 limits;微信凭证来自配置,未配置则相关接口报 40307)
pub fn new(config: Config, yk: YkClient) -> Self {
let hub = Arc::new(crate::ws::hub::Hub::new(config.limits.clone()));
let wechat = WechatClient::new(
config.wechat_app_id.clone(),
config.wechat_app_secret.clone(),
);
Self {
config,
yk,
wechat,
sessions: RwLock::new(HashMap::new()),
hub,
}
Expand Down Expand Up @@ -245,6 +253,8 @@ mod tests {
yk_allowed_hosts: vec!["www.yuketang.cn".into()],
limits: Default::default(),
log_dir: "logs".into(),
wechat_app_id: None,
wechat_app_secret: None,
},
YkClient::new(yk_base),
)))
Expand Down Expand Up @@ -427,6 +437,8 @@ mod tests {
yk_allowed_hosts: vec![],
limits: Default::default(),
log_dir: "logs".into(),
wechat_app_id: None,
wechat_app_secret: None,
},
YkClient::new(&server.uri()),
));
Expand Down
28 changes: 28 additions & 0 deletions backend/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
//! - YK_BASE_URL 雨课堂基地址
//! - YK_ALLOWED_HOSTS 雨课堂域名白名单(逗号分隔)
//! - RUST_LOG 日志级别(tracing EnvFilter)
//! - WECHAT_APP_ID 微信公众号 AppID(M5 微信内扫码;不填则微信内扫码不可用)
//! - WECHAT_APP_SECRET 微信公众号 AppSecret(同上,不入库)
//! - WS_* 房间资源上限(见 [`Limits`],防 OOM,全部可调)

use std::time::Duration;
Expand All @@ -25,6 +27,10 @@ pub struct Config {
pub limits: Limits,
/// 日志文件目录(按天滚动)
pub log_dir: String,
/// 微信公众号 AppID(未配置 = 微信内扫码不可用,前端降级相机)
pub wechat_app_id: Option<String>,
/// 微信公众号 AppSecret(未配置同上;不入库)
pub wechat_app_secret: Option<String>,
}

/// WS 房间资源上限(docs/SPEC.md §3.3.3,全部硬性要求,可经环境变量调整)
Expand Down Expand Up @@ -157,6 +163,8 @@ impl Config {
})),
limits: Limits::from_lookup(&lookup),
log_dir: lookup("LOG_DIR").unwrap_or_else(|| "logs".into()),
wechat_app_id: optional_env(&lookup, "WECHAT_APP_ID"),
wechat_app_secret: optional_env(&lookup, "WECHAT_APP_SECRET"),
}
}

Expand All @@ -172,6 +180,13 @@ impl Config {
}
}

/// 可选凭证读取:去空白,空串视作未配置
fn optional_env<F: Fn(&str) -> Option<String>>(lookup: &F, key: &str) -> Option<String> {
lookup(key)
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
}

/// 解析逗号分隔的域名列表:去空白、去空项、统一小写
pub fn parse_hosts(raw: &str) -> Vec<String> {
raw.split(',')
Expand Down Expand Up @@ -201,6 +216,8 @@ mod tests {
"huanghe.yuketang.cn"
]
);
assert_eq!(c.wechat_app_id, None);
assert_eq!(c.wechat_app_secret, None);
}

#[test]
Expand All @@ -212,13 +229,24 @@ mod tests {
("YK_CAPTCHA_APP_ID", "9999"),
("YK_BASE_URL", "https://pro.yuketang.cn"),
("YK_ALLOWED_HOSTS", "Pro.Yuketang.cn, other.cn"),
("WECHAT_APP_ID", " wx12345 "),
("WECHAT_APP_SECRET", "s3cr3t"),
]);
assert_eq!(c.server_secret, "abc");
assert_eq!(c.port, 8080);
assert_eq!(c.cookie_ttl_secs, 3600);
assert_eq!(c.captcha_app_id, "9999");
assert_eq!(c.yk_base_url, "https://pro.yuketang.cn");
assert_eq!(c.yk_allowed_hosts, vec!["pro.yuketang.cn", "other.cn"]);
assert_eq!(c.wechat_app_id.as_deref(), Some("wx12345"));
assert_eq!(c.wechat_app_secret.as_deref(), Some("s3cr3t"));
}

#[test]
fn blank_wechat_credentials_treated_as_unset() {
let c = Config::from_pairs(&[("WECHAT_APP_ID", " "), ("WECHAT_APP_SECRET", "")]);
assert_eq!(c.wechat_app_id, None);
assert_eq!(c.wechat_app_secret, None);
}

#[test]
Expand Down
2 changes: 2 additions & 0 deletions backend/src/courses/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ mod tests {
yk_allowed_hosts: vec!["www.yuketang.cn".into()],
limits: Default::default(),
log_dir: "logs".into(),
wechat_app_id: None,
wechat_app_secret: None,
},
YkClient::new(yk_base),
));
Expand Down
8 changes: 8 additions & 0 deletions backend/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ pub enum AppError {
/// 房间入参非法(字段超长等)
#[error("{0}")]
BadRoomInput(&'static str),
/// 请求参数非法(缺字段等)
#[error("{0}")]
BadInput(&'static str),
/// 微信 JS-SDK 未配置(缺少 WECHAT_APP_ID / WECHAT_APP_SECRET)
#[error("微信扫码未配置,请联系管理员")]
WechatNotConfigured,
/// 其他内部错误
#[error("内部错误")]
Internal(#[from] anyhow::Error),
Expand All @@ -62,6 +68,8 @@ impl AppError {
AppError::RoomNotFound => error_code::ROOM_NOT_FOUND,
AppError::NotRoomOwner => error_code::NOT_ROOM_OWNER,
AppError::BadRoomInput(_) => error_code::BAD_REQUEST,
AppError::BadInput(_) => error_code::BAD_REQUEST,
AppError::WechatNotConfigured => 40307,
AppError::Internal(_) => 50000,
}
}
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod config;
mod courses;
mod error;
mod signin;
mod wechat;
mod ws;

use std::sync::Arc;
Expand Down Expand Up @@ -45,6 +46,7 @@ fn build_app(cfg: Arc<Config>) -> Router {
auth::routes::router(state.clone())
.merge(signin::routes::router(state.clone()))
.merge(courses::routes::router(state.clone()))
.merge(wechat::routes::router(state.clone()))
.merge(ws::routes::router(state))
.merge(health)
}
Expand Down
2 changes: 2 additions & 0 deletions backend/src/signin/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ mod tests {
yk_allowed_hosts: vec!["www.yuketang.cn".into()],
limits: Default::default(),
log_dir: "logs".into(),
wechat_app_id: None,
wechat_app_secret: None,
},
YkClient::new(yk_base),
));
Expand Down
Loading
Loading