P0 - Panic 风险修复: - payment.rs: unwrap() → let-else safe handling - payment.rs: get_jwt_secret() expect → Result/AppError - auth.rs: openid 切片添加 len >= 8 守卫 - main.rs: 启动时 expect → unwrap_or_else 描述性 panic - main.rs: Directive::from_str 添加 fallback P1 - 逻辑/安全修复: - payment.rs: urlencoding() + 解码 bug 修复 (移除 had_escape) - payment.rs: Mock 支付添加 check_mock_payment_allowed 检查 - db.rs: 永久会员 NULL → 2099-12-31 一致化 - user.rs: 维护模式添加安全说明注释 - 自动清理 unused_variables 警告 (_is_mobile) P2 - 错误吞没修复: - main.rs: 3 处定时任务 let _ = → if let Err = tracing::error! - db.rs + admin.rs: 7 处通知/审计日志 let _ = → tracing::warn! - auth.rs: refresh token 保存 add warn 日志 P3 - 死代码清理: - models.rs: 移除 TokenResponse (dead) - models.rs: 移除 AppState 中 5 个未使用字段 (env var 直接读取) - error.rs: 移除 3 个 dead ErrorResponse 方法 - rate_limiter.rs: extract_client_ip_from_header → #[cfg(test)] - models.rs: 注释 typo fix (user_ytpe → user_type) - db.rs: RefreshToken 添加 deserialization 注释 Shell 脚本修复: - deploy.sh: run_migrations 移到 restart_service 之前 - test.sh: 移除 EXIT trap 覆盖; heredoc 引号修复; 维护模式添加 restart - common.sh: mock_key 添加 sed 转义 (防 / & 注入) 验证: cargo check 0 warnings, 8 tests passed
This commit is contained in:
@@ -59,11 +59,13 @@ pub async fn admin_update_user_payment(
|
||||
// 更新用户付费状态
|
||||
db::update_user_payment_status(pool.get_ref(), target_user_id, body.is_member, membership_expires_at).await?;
|
||||
// 审计日志
|
||||
let _ = db::insert_payment_audit_log(
|
||||
if let Err(e) = db::insert_payment_audit_log(
|
||||
pool.get_ref(), "ADMIN_MANUAL", target_user_id, "admin_revoke",
|
||||
Some(claims.user_id),
|
||||
Some(&format!("管理员手动更新付费状态: is_member={}, expires_at={:?}", body.is_member, body.membership_expires_at)),
|
||||
).await;
|
||||
).await {
|
||||
tracing::warn!("插入支付审计日志失败: {}", e);
|
||||
}
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
"success": true,
|
||||
"message": "用户付费状态已更新"
|
||||
|
||||
@@ -84,7 +84,11 @@ pub async fn login(
|
||||
|
||||
let openid = match wechat_data.openid {
|
||||
Some(id) => {
|
||||
let masked_openid = format!("{}***{}", &id[0..4], &id[id.len() - 4..]);
|
||||
let masked_openid = if id.len() >= 8 {
|
||||
format!("{}***{}", &id[0..4], &id[id.len() - 4..])
|
||||
} else {
|
||||
id.to_string()
|
||||
};
|
||||
debug!("获取到用户openid: {}", masked_openid);
|
||||
id
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ fn build_alipay_form_html(
|
||||
subject: &str,
|
||||
notify_url: &str,
|
||||
return_url: &str,
|
||||
is_mobile: bool, // true=手机网站支付, false=电脑网站支付
|
||||
_is_mobile: bool, // 保留参数:待支付宝开通 wap.pay 后启用
|
||||
) -> Result<String, String> {
|
||||
// 注意:当前支付宝产品仅开通了 alipay.trade.page.pay(电脑网站支付)
|
||||
// 该接口在手机浏览器中也能正常唤起支付宝 APP 或显示移动端页面
|
||||
@@ -295,8 +295,10 @@ fn extract_token(req: &HttpRequest) -> Option<String> {
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn get_jwt_secret() -> String {
|
||||
std::env::var("JWT_SECRET").expect("JWT_SECRET must be set")
|
||||
fn get_jwt_secret() -> Result<String, AppError> {
|
||||
std::env::var("JWT_SECRET").map_err(|_| {
|
||||
AppError::Internal("JWT_SECRET 环境变量未设置".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
// ===== Handler: GET /payment — 套餐选择页(网页端微信扫码登录) =====
|
||||
@@ -742,7 +744,8 @@ pub async fn payment_page(
|
||||
.or_else(|| query.jwt.clone())
|
||||
.ok_or_else(|| AppError::Unauthorized("未登录".to_string()))?;
|
||||
|
||||
let claims = crate::auth::verify_token(&token, &get_jwt_secret())
|
||||
let secret = get_jwt_secret()?;
|
||||
let claims = crate::auth::verify_token(&token, &secret)
|
||||
.map_err(|_| AppError::Unauthorized("Token 无效".to_string()))?;
|
||||
|
||||
let pkg = get_package_info(&query.package_)
|
||||
@@ -802,6 +805,7 @@ pub async fn payment_page(
|
||||
let return_url = format!("{}/payment/success?order_no={}&jwt={}", base_url, order_no, token);
|
||||
|
||||
let Some(config) = AlipayConfig::from_env() else {
|
||||
check_mock_payment_allowed(&req)?;
|
||||
let jwt_for_mock = token.clone();
|
||||
let mock_html = build_mock_pay_html(&order_no, pkg.display_name, &jwt_for_mock);
|
||||
return Ok(HttpResponse::Ok()
|
||||
@@ -855,7 +859,8 @@ pub async fn alipay_pay_page(
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
check_payment_maintenance()?;
|
||||
let token = extract_token(&req).ok_or_else(|| AppError::Unauthorized("未登录".to_string()))?;
|
||||
crate::auth::verify_token(&token, &get_jwt_secret())
|
||||
let secret = get_jwt_secret()?;
|
||||
crate::auth::verify_token(&token, &secret)
|
||||
.map_err(|_| AppError::Unauthorized("Token 无效".to_string()))?;
|
||||
|
||||
let pkg = get_package_info(&query.package_type)
|
||||
@@ -947,13 +952,12 @@ fn parse_alipay_form(body: &[u8]) -> BTreeMap<String, String> {
|
||||
}
|
||||
|
||||
/// 手动 URL 解码(percent-decoding)
|
||||
/// 注意:`+` 始终解码为空格(form-urlencoded 标准),而非仅在 `%` 转义之后
|
||||
fn urlencoding(s: &str) -> String {
|
||||
let mut result = String::with_capacity(s.len());
|
||||
let mut chars = s.bytes().peekable();
|
||||
let mut had_escape = false;
|
||||
while let Some(b) = chars.next() {
|
||||
if b == b'%' {
|
||||
had_escape = true;
|
||||
let hi = chars.next().and_then(hex_val);
|
||||
let lo = chars.next().and_then(hex_val);
|
||||
if let (Some(h), Some(l)) = (hi, lo) {
|
||||
@@ -961,7 +965,7 @@ fn urlencoding(s: &str) -> String {
|
||||
} else {
|
||||
result.push('%');
|
||||
}
|
||||
} else if b == b'+' && had_escape {
|
||||
} else if b == b'+' {
|
||||
result.push(' ');
|
||||
} else {
|
||||
result.push(b as char);
|
||||
@@ -1570,8 +1574,15 @@ pub async fn payment_login_status(
|
||||
}
|
||||
|
||||
// 已确认 → 使用已生成的 token
|
||||
let token = token.unwrap();
|
||||
let user_id = user_id.unwrap();
|
||||
let token = token.unwrap_or_default();
|
||||
let Some(user_id) = user_id else {
|
||||
return Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
"success": false, "confirmed": false,
|
||||
"token": None::<String>,
|
||||
"is_active_member": false,
|
||||
"membership_expires_at": None::<String>,
|
||||
})));
|
||||
};
|
||||
|
||||
let (is_active_member, membership_expires_at): (bool, Option<String>) =
|
||||
match sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>(
|
||||
|
||||
@@ -18,6 +18,8 @@ pub async fn get_current_user_profile(
|
||||
let user = db::get_user_by_id(pool.get_ref(), user_id).await?;
|
||||
let is_maintenance = std::env::var("PAYMENT_MAINTENANCE_MODE").ok() == Some("true".to_string());
|
||||
let is_active_member = if is_maintenance {
|
||||
// 维护模式下所有已认证用户视为活跃会员,确保支付故障期间服务可用
|
||||
// 注意:此路由有 JWT 中间件保护,未认证请求已在前置中间件被拦截
|
||||
true
|
||||
} else {
|
||||
user.is_member &&
|
||||
|
||||
Reference in New Issue
Block a user