refactor: replace server-only dead_code allows with cfg(feature = "server")

Replace #[allow(dead_code)] on server-only helpers with #[cfg(feature = "server")]
to make the server/WASM split explicit and avoid compiling unused server logic
into the WASM frontend.

- Gate password/session/auth/comment helpers and model parsers with server feature
- Gate related imports and tests accordingly
- Remove genuinely unused CreatePostRequest and CreateCommentRequest
- Keep #[allow(dead_code)] for true dead code (stub methods, unused public APIs)
- Use #[cfg(any(target_arch = "wasm32", test))] for THEME_KEY
- Update AGENTS.md note
This commit is contained in:
xfy 2026-06-16 16:45:08 +08:00
parent b7afd12538
commit cafbddb861
11 changed files with 34 additions and 62 deletions

View File

@ -71,7 +71,7 @@ Dioxus 0.7 fullstack project with **two independent gates** — the most common
**Stub pattern**: `src/db/mod.rs` provides a `DummyPool` when `server` feature is disabled — do not remove.
**Dead code allowances**: `src/auth/password.rs` and `src/auth/session.rs` carry `#[allow(dead_code)]` because the compiler sees them as unused in WASM builds where server function bodies are stripped.
**Server-only helpers**: `src/auth/password.rs`, `src/auth/session.rs`, `src/api/auth.rs`, `src/api/comments/helpers.rs`, and several model helper methods are gated with `#[cfg(feature = "server")]` because they are only called from server function bodies, which are stripped in WASM builds.
## Dual API Architecture

View File

@ -23,7 +23,7 @@ use crate::db::pool::get_conn;
use crate::models::user::{User, UserRole};
use crate::models::user::PublicUser;
#[allow(dead_code)]
#[cfg(feature = "server")]
fn validate_username(username: &str) -> Result<(), String> {
if username.len() < 3 || username.len() > 50 {
return Err("用户名长度必须在 3-50 字符之间".to_string());
@ -34,7 +34,7 @@ fn validate_username(username: &str) -> Result<(), String> {
Ok(())
}
#[allow(dead_code)]
#[cfg(feature = "server")]
fn validate_email(email: &str) -> Result<(), String> {
let re = regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
if !re.is_match(email) {
@ -43,7 +43,7 @@ fn validate_email(email: &str) -> Result<(), String> {
Ok(())
}
#[allow(dead_code)]
#[cfg(feature = "server")]
fn validate_password(password: &str) -> Result<(), String> {
if password.len() < 8 {
return Err("密码长度至少 8 位".to_string());
@ -387,7 +387,7 @@ pub async fn get_current_admin_user() -> Result<User, AppError> {
Ok(user)
}
#[cfg(test)]
#[cfg(all(test, feature = "server"))]
mod tests {
use super::*;

View File

@ -1,7 +1,6 @@
//! 评论模块的辅助函数:数据转换、校验、哈希与头像生成。
//!
//! 大部分工具函数仅在 `feature = "server"` 启用的服务端构建中使用;
//! 校验函数同时在前端构建中保留签名,避免编译器提示未使用。
//! 所有工具函数仅在 `feature = "server"` 启用的服务端构建中使用。
#![allow(clippy::unused_unit, deprecated)]
@ -87,7 +86,7 @@ pub fn format_relative_time(dt: chrono::DateTime<chrono::Utc>) -> String {
}
/// 校验评论作者昵称:非空且不超过 50 字符。
#[allow(dead_code)]
#[cfg(feature = "server")]
pub fn validate_comment_name(name: &str) -> Result<(), String> {
let trimmed = name.trim();
if trimmed.is_empty() {
@ -100,7 +99,7 @@ pub fn validate_comment_name(name: &str) -> Result<(), String> {
}
/// 校验评论作者邮箱格式。
#[allow(dead_code)]
#[cfg(feature = "server")]
pub fn validate_comment_email(email: &str) -> Result<(), String> {
let re = regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
if !re.is_match(email.trim()) {
@ -110,7 +109,7 @@ pub fn validate_comment_email(email: &str) -> Result<(), String> {
}
/// 校验评论作者网址:为空时允许,非空时必须以 http:// 或 https:// 开头且不超过 200 字符。
#[allow(dead_code)]
#[cfg(feature = "server")]
pub fn validate_comment_url(url: &str) -> Result<(), String> {
let trimmed = url.trim();
if trimmed.is_empty() {
@ -127,7 +126,7 @@ pub fn validate_comment_url(url: &str) -> Result<(), String> {
}
/// 校验评论内容:非空且不超过 10000 字符。
#[allow(dead_code)]
#[cfg(feature = "server")]
pub fn validate_comment_content(content: &str) -> Result<(), String> {
let trimmed = content.trim();
if trimmed.is_empty() {

View File

@ -3,24 +3,6 @@
use crate::models::comment::{AdminComment, PublicComment};
use serde::{Deserialize, Serialize};
/// 创建评论请求体(客户端使用)。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct CreateCommentRequest {
/// 目标文章 id。
pub post_id: i32,
/// 父评论 id顶层评论为 None。
pub parent_id: Option<i64>,
/// 评论者昵称。
pub author_name: String,
/// 评论者邮箱。
pub author_email: String,
/// 评论者个人网址。
pub author_url: Option<String>,
/// 评论 Markdown 原文。
pub content_md: String,
}
/// 创建/审核/删除评论的统一响应结构。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommentResponse {
@ -52,13 +34,15 @@ pub struct CommentTreeResponse {
/// 评论计数响应。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct CommentCountResponse {
/// 评论数量。
pub count: i64,
}
/// 待审核评论列表响应。
///
/// 当前前端未直接调用 `get_pending_comments`,此类型仅在服务端函数体中构造;
/// 保留 `#[allow(dead_code)]` 以避免 WASM 构建中因函数体被剥离而产生的未使用警告。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct PendingCommentsResponse {

View File

@ -2,26 +2,6 @@
use crate::models::post::{Post, PostStats, Tag};
/// 创建/更新文章请求体(客户端使用)。
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[allow(dead_code)]
pub struct CreatePostRequest {
/// 文章标题。
pub title: String,
/// 自定义 slug为空时由标题自动生成。
pub slug: Option<String>,
/// 文章摘要,为空时自动从正文提取。
pub summary: Option<String>,
/// Markdown 格式正文。
pub content_md: String,
/// 文章状态(如 draft / published
pub status: String,
/// 标签列表。
pub tags: Vec<String>,
/// 封面图 URL。
pub cover_image: Option<String>,
}
/// 创建/更新/删除文章的统一响应结构。
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CreatePostResponse {

View File

@ -1,15 +1,17 @@
//! 密码哈希与校验Argon2
//!
//! 使用随机 salt 生成 PHC 字符串格式哈希,并通过 Argon2 验证。
//! `#[allow(dead_code)]` 用于避免 WASM 构建中服务端函数体被剥离后的未使用警告
//! 仅在 `feature = "server"` 启用的服务端构建中使用
#[cfg(feature = "server")]
use argon2::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
#[cfg(feature = "server")]
use rand::rngs::OsRng;
#[allow(dead_code)]
#[cfg(feature = "server")]
/// 使用 Argon2 对明文密码进行哈希。
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
let salt = SaltString::generate(&mut OsRng);
@ -18,7 +20,7 @@ pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Er
Ok(password_hash.to_string())
}
#[allow(dead_code)]
#[cfg(feature = "server")]
/// 校验明文密码是否与已存储的哈希匹配。
pub fn verify_password(password: &str, hash: &str) -> Result<bool, argon2::password_hash::Error> {
let parsed_hash = PasswordHash::new(hash)?;
@ -30,7 +32,7 @@ pub fn verify_password(password: &str, hash: &str) -> Result<bool, argon2::passw
}
}
#[cfg(test)]
#[cfg(all(test, feature = "server"))]
mod tests {
use super::*;

View File

@ -4,17 +4,20 @@
//! Cookie 包含 HttpOnly、SameSite=Lax 与可选 Secure 标志。
//! 服务端上下文解析函数仅在 `feature = "server"` 时可用。
#[cfg(feature = "server")]
use chrono::{DateTime, Duration, Utc};
#[cfg(feature = "server")]
use sha2::{Digest, Sha256};
#[cfg(feature = "server")]
use uuid::Uuid;
#[allow(dead_code)]
#[cfg(feature = "server")]
/// 生成新的随机会话 tokenUUID 格式)。
pub fn generate_token() -> String {
Uuid::new_v4().to_string()
}
#[allow(dead_code)]
#[cfg(feature = "server")]
/// 使用 SHA-256 对 token 进行哈希,用于数据库存储。
pub fn hash_token(token: &str) -> String {
let mut hasher = Sha256::new();
@ -22,7 +25,7 @@ pub fn hash_token(token: &str) -> String {
hex::encode(hasher.finalize())
}
#[allow(dead_code)]
#[cfg(feature = "server")]
/// 返回默认会话过期时间(当前时间 + 30 天)。
pub fn default_expiry() -> DateTime<Utc> {
Utc::now() + Duration::days(30)

View File

@ -33,7 +33,7 @@ impl CommentStatus {
}
/// 将 CommentStatus 序列化为小写字符串。
#[allow(dead_code)]
#[cfg(test)]
pub fn as_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
@ -145,6 +145,7 @@ mod tests {
use super::*;
#[test]
#[cfg(feature = "server")]
fn comment_status_from_str() {
assert_eq!(CommentStatus::from_str("pending"), CommentStatus::Pending);
assert_eq!(CommentStatus::from_str("approved"), CommentStatus::Approved);
@ -153,6 +154,7 @@ mod tests {
}
#[test]
#[cfg(feature = "server")]
fn comment_status_from_str_unknown_defaults_to_pending() {
assert_eq!(CommentStatus::from_str("unknown"), CommentStatus::Pending);
assert_eq!(CommentStatus::from_str(""), CommentStatus::Pending);

View File

@ -17,7 +17,6 @@ pub enum PostStatus {
impl PostStatus {
/// 将状态序列化为数据库或 API 使用的小写字符串。
#[allow(dead_code)]
pub fn as_str(&self) -> &'static str {
match self {
PostStatus::Draft => "draft",
@ -26,7 +25,7 @@ impl PostStatus {
}
/// 将字符串解析为 PostStatus无法识别时返回 None。
#[allow(dead_code)]
#[cfg(feature = "server")]
pub fn from_str(s: &str) -> Option<Self> {
match s {
"draft" => Some(PostStatus::Draft),
@ -175,6 +174,7 @@ mod tests {
}
#[test]
#[cfg(feature = "server")]
fn post_status_from_str() {
assert_eq!(PostStatus::from_str("draft"), Some(PostStatus::Draft));
assert_eq!(
@ -192,6 +192,7 @@ mod tests {
}
#[test]
#[cfg(feature = "server")]
fn post_status_roundtrip() {
for status in [PostStatus::Draft, PostStatus::Published] {
assert_eq!(PostStatus::from_str(status.as_str()), Some(status.clone()));

View File

@ -17,7 +17,7 @@ pub enum UserRole {
impl UserRole {
/// 将数据库中的角色字符串解析为 UserRole无法识别时返回 None。
#[allow(dead_code)]
#[cfg(feature = "server")]
pub fn from_str(s: &str) -> Option<Self> {
match s {
"admin" => Some(UserRole::Admin),
@ -89,6 +89,7 @@ mod tests {
}
#[test]
#[cfg(feature = "server")]
fn user_role_from_str() {
assert_eq!(UserRole::from_str("admin"), Some(UserRole::Admin));
assert_eq!(UserRole::from_str("blocked"), Some(UserRole::Blocked));

View File

@ -8,7 +8,7 @@
use dioxus::prelude::*;
/// localStorage 中存储主题值的键名。
#[allow(dead_code)]
#[cfg(any(target_arch = "wasm32", test))]
const THEME_KEY: &str = "yggdrasil-theme";
/// 应用主题枚举。