diff --git a/apps/miniprogram/src/app.tsx b/apps/miniprogram/src/app.tsx index 6ff1956..e0a0931 100644 --- a/apps/miniprogram/src/app.tsx +++ b/apps/miniprogram/src/app.tsx @@ -1,7 +1,7 @@ import { PropsWithChildren } from 'react'; import './app.scss'; -function App({ children }: PropsWithChildren) { +function App({ children }: PropsWithChildren>) { return children; } diff --git a/apps/miniprogram/src/pages/appointment/create/index.tsx b/apps/miniprogram/src/pages/appointment/create/index.tsx index 505c922..917c05e 100644 --- a/apps/miniprogram/src/pages/appointment/create/index.tsx +++ b/apps/miniprogram/src/pages/appointment/create/index.tsx @@ -29,7 +29,7 @@ export default function AppointmentCreate() { const currentPatient = useAuthStore((s) => s.currentPatient); // Step 1: 选择科室后加载医生列表 - const onDepartmentChange = useCallback(async (e: any) => { + const onDepartmentChange = useCallback(async (e: { detail: { value: number } }) => { const idx = e.detail.value; const dept = DEPARTMENTS[idx]; setDeptPickerIndex(idx); @@ -50,17 +50,17 @@ export default function AppointmentCreate() { }, []); // Step 3: 日期变更 - const onDateChange = useCallback((e: any) => { + const onDateChange = useCallback((e: { detail: { value: string } }) => { setAppointmentDate(e.detail.value); }, []); // Step 3: 时段变更 - const onTimeSlotChange = useCallback((e: any) => { + const onTimeSlotChange = useCallback((e: { detail: { value: string } }) => { setTimeSlot(e.detail.value); }, []); // Step 3: 备注变更 - const onReasonChange = useCallback((e: any) => { + const onReasonChange = useCallback((e: { detail: { value: string } }) => { setReason(e.detail.value); }, []); diff --git a/apps/miniprogram/src/pages/health/input/index.tsx b/apps/miniprogram/src/pages/health/input/index.tsx index 3c48a33..85549af 100644 --- a/apps/miniprogram/src/pages/health/input/index.tsx +++ b/apps/miniprogram/src/pages/health/input/index.tsx @@ -59,8 +59,9 @@ export default function HealthInput() { } Taro.showToast({ title: '录入成功', icon: 'success' }); setTimeout(() => Taro.navigateBack(), 1000); - } catch (e: any) { - Taro.showToast({ title: e.message || '录入失败', icon: 'none' }); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : '录入失败'; + Taro.showToast({ title: msg, icon: 'none' }); } finally { setSubmitting(false); } diff --git a/apps/miniprogram/src/pages/login/index.tsx b/apps/miniprogram/src/pages/login/index.tsx index 43d4934..3f0b667 100644 --- a/apps/miniprogram/src/pages/login/index.tsx +++ b/apps/miniprogram/src/pages/login/index.tsx @@ -23,7 +23,7 @@ export default function Login() { } }; - const handleGetPhone = async (e: any) => { + const handleGetPhone = async (e: { detail: { errMsg: string; encryptedData: string; iv: string } }) => { if (e.detail.errMsg !== 'getPhoneNumber:ok') { Taro.showToast({ title: '需要授权手机号', icon: 'none' }); return; diff --git a/apps/miniprogram/src/pages/report/detail/index.tsx b/apps/miniprogram/src/pages/report/detail/index.tsx index ce394a9..7b84c86 100644 --- a/apps/miniprogram/src/pages/report/detail/index.tsx +++ b/apps/miniprogram/src/pages/report/detail/index.tsx @@ -32,7 +32,7 @@ export default function ReportDetail() { const indicators: IndicatorItem[] = React.useMemo(() => { if (!report?.indicators || typeof report.indicators !== 'object') return []; - return Object.entries(report.indicators).map(([name, val]: [string, any]) => ({ + return Object.entries(report.indicators).map(([name, val]) => ({ name, value: val.value, unit: val.unit, diff --git a/crates/erp-auth/src/dto.rs b/crates/erp-auth/src/dto.rs index 0a38577..40cc692 100644 --- a/crates/erp-auth/src/dto.rs +++ b/crates/erp-auth/src/dto.rs @@ -30,8 +30,9 @@ pub struct RefreshReq { // --- Wechat DTOs --- -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct WechatLoginReq { + #[validate(length(min = 1, message = "code 不能为空"))] pub code: String, } @@ -42,10 +43,13 @@ pub struct WechatLoginResp { pub token: Option, } -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct WechatBindPhoneReq { + #[validate(length(min = 1, message = "openid 不能为空"))] pub openid: String, + #[validate(length(min = 1, message = "encrypted_data 不能为空"))] pub encrypted_data: String, + #[validate(length(min = 1, message = "iv 不能为空"))] pub iv: String, } diff --git a/crates/erp-auth/src/error.rs b/crates/erp-auth/src/error.rs index b8d7dcd..fef2372 100644 --- a/crates/erp-auth/src/error.rs +++ b/crates/erp-auth/src/error.rs @@ -21,6 +21,9 @@ pub enum AuthError { #[error("JWT 错误: {0}")] JwtError(#[from] jsonwebtoken::errors::Error), + #[error("数据库错误: {0}")] + DbError(String), + #[error("{0}")] Validation(String), @@ -36,6 +39,7 @@ impl From for AppError { AuthError::TokenRevoked => AppError::Unauthorized, AuthError::UserDisabled(s) => AppError::Forbidden(s), AuthError::Validation(s) => AppError::Validation(s), + AuthError::DbError(_) => AppError::Internal(err.to_string()), AuthError::HashError(_) => AppError::Internal(err.to_string()), AuthError::JwtError(_) => AppError::Unauthorized, AuthError::VersionMismatch => AppError::VersionMismatch, diff --git a/crates/erp-auth/src/handler/wechat_handler.rs b/crates/erp-auth/src/handler/wechat_handler.rs index ccfc6df..6d5b0a6 100644 --- a/crates/erp-auth/src/handler/wechat_handler.rs +++ b/crates/erp-auth/src/handler/wechat_handler.rs @@ -1,5 +1,6 @@ use axum::extract::{FromRef, State}; use axum::response::Json; +use validator::Validate; use erp_core::error::AppError; use erp_core::types::ApiResponse; @@ -30,9 +31,8 @@ where AuthState: FromRef, S: Clone + Send + Sync + 'static, { - if req.code.is_empty() { - return Err(AppError::Validation("code 不能为空".to_string())); - } + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; let tenant_id = state.default_tenant_id; let resp = WechatService::login(&state, tenant_id, &req.code).await?; @@ -60,9 +60,8 @@ where AuthState: FromRef, S: Clone + Send + Sync + 'static, { - if req.openid.is_empty() { - return Err(AppError::Validation("openid 不能为空".to_string())); - } + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; let tenant_id = state.default_tenant_id; let resp = WechatService::bind_phone( diff --git a/crates/erp-auth/src/service/wechat_service.rs b/crates/erp-auth/src/service/wechat_service.rs index 70ff5ee..84e9c01 100644 --- a/crates/erp-auth/src/service/wechat_service.rs +++ b/crates/erp-auth/src/service/wechat_service.rs @@ -15,7 +15,9 @@ use crate::service::token_service::TokenService; #[derive(Debug, Deserialize)] struct WechatSessionResp { openid: Option, + #[allow(dead_code)] session_key: Option, + #[allow(dead_code)] unionid: Option, errcode: Option, errmsg: Option, @@ -24,7 +26,6 @@ struct WechatSessionResp { pub struct WechatService; impl WechatService { - /// 用微信 code 换取 openid,查询绑定状态。 pub async fn login( state: &AuthState, tenant_id: Uuid, @@ -42,7 +43,7 @@ impl WechatService { .filter(wechat_user::Column::DeletedAt.is_null()) .one(&state.db) .await - .map_err(|e| AuthError::Validation(e.to_string()))?; + .map_err(|e| AuthError::DbError(e.to_string()))?; if let Some(wu) = existing { let token = build_login_resp( @@ -70,7 +71,6 @@ impl WechatService { } } - /// 绑定手机号:创建/关联 user → 创建 wechat_user → 签发 JWT。 pub async fn bind_phone( state: &AuthState, tenant_id: Uuid, @@ -78,7 +78,8 @@ impl WechatService { _encrypted_data: &str, _iv: &str, ) -> AuthResult { - let phone = Self::decrypt_phone_placeholder(); + // MVP 占位:开发阶段使用固定手机号,上线前替换为真实微信手机号解密 + let phone = "13800000000"; let existing = wechat_user::Entity::find() .filter(wechat_user::Column::Openid.eq(openid)) @@ -86,14 +87,14 @@ impl WechatService { .filter(wechat_user::Column::DeletedAt.is_null()) .one(&state.db) .await - .map_err(|e| AuthError::Validation(e.to_string()))?; + .map_err(|e| AuthError::DbError(e.to_string()))?; if existing.is_some() { return Err(AuthError::Validation("该微信已绑定账号".to_string())); } let user_id = - Self::find_or_create_user_by_phone(&state.db, tenant_id, &phone).await?; + Self::find_or_create_user_by_phone(&state.db, tenant_id, phone).await?; let now = Utc::now(); let wu = wechat_user::ActiveModel { @@ -102,7 +103,7 @@ impl WechatService { openid: Set(openid.to_string()), union_id: Set(None), user_id: Set(user_id), - phone: Set(Some(phone)), + phone: Set(Some(phone.to_string())), created_at: Set(now), updated_at: Set(now), created_by: Set(Some(user_id)), @@ -112,7 +113,7 @@ impl WechatService { }; wu.insert(&state.db) .await - .map_err(|e| AuthError::Validation(e.to_string()))?; + .map_err(|e| AuthError::DbError(e.to_string()))?; build_login_resp( &state.db, @@ -127,12 +128,6 @@ impl WechatService { .await } - /// MVP 占位:开发阶段返回固定手机号。 - fn decrypt_phone_placeholder() -> String { - "13800000000".to_string() - } - - /// 按手机号查找已有 user,若不存在则创建。 async fn find_or_create_user_by_phone( db: &sea_orm::DatabaseConnection, tenant_id: Uuid, @@ -146,7 +141,7 @@ impl WechatService { .filter(user::Column::DeletedAt.is_null()) .one(db) .await - .map_err(|e| AuthError::Validation(e.to_string()))?; + .map_err(|e| AuthError::DbError(e.to_string()))?; if let Some(u) = existing { return Ok(u.id); @@ -155,12 +150,11 @@ impl WechatService { let now = Utc::now(); let user_id = Uuid::now_v7(); let suffix = &phone[phone.len().saturating_sub(4)..]; - let username = format!("wx_{}", suffix); let new_user = user::ActiveModel { id: Set(user_id), tenant_id: Set(tenant_id), - username: Set(username), + username: Set(format!("wx_{}", suffix)), display_name: Set(Some(format!("微信用户{}", suffix))), phone: Set(Some(phone.to_string())), email: Set(None), @@ -177,13 +171,12 @@ impl WechatService { new_user .insert(db) .await - .map_err(|e| AuthError::Validation(e.to_string()))?; + .map_err(|e| AuthError::DbError(e.to_string()))?; Ok(user_id) } } -/// 为已确认的 user 构建 LoginResp(token pair + user info)。 async fn build_login_resp( db: &sea_orm::DatabaseConnection, user_id: Uuid, @@ -196,7 +189,7 @@ async fn build_login_resp( let user_model = user::Entity::find_by_id(user_id) .one(db) .await - .map_err(|e| AuthError::Validation(e.to_string()))? + .map_err(|e| AuthError::DbError(e.to_string()))? .ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?; let roles = TokenService::get_user_roles(user_id, tenant_id, db).await?; @@ -239,18 +232,21 @@ async fn build_login_resp( }) } -/// 调用微信 jscode2session 接口获取 openid。 async fn fetch_session( appid: &str, secret: &str, code: &str, ) -> AuthResult { - let url = format!( - "https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code", - appid, secret, code - ); - - let resp = reqwest::get(&url) + let client = reqwest::Client::new(); + let resp = client + .get("https://api.weixin.qq.com/sns/jscode2session") + .query(&[ + ("appid", appid), + ("secret", secret), + ("js_code", code), + ("grant_type", "authorization_code"), + ]) + .send() .await .map_err(|e| AuthError::Validation(format!("微信 API 请求失败: {}", e)))?;