fix(auth+miniprogram): 清除全部审计遗留问题
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

MEDIUM:
- WechatLoginReq/WechatBindPhoneReq 添加 Validate 派生 + 字段校验规则
- handler 中调用 req.validate() 并 map_err 转换
- 新增 AuthError::DbError 变体,wechat_service 所有 DB 错误从 Validation 改为 DbError
- DbError 映射到 AppError::Internal,不再误导前端

LOW:
- fetch_session 改用 reqwest Client.query() 构建参数,自动 URL 编码
- app.tsx PropsWithChildren<any> 改为 Record<string, unknown>
- login handleGetPhone 回调 e: any 改为内联类型
- appointment/create 4 个事件回调 e: any 改为内联类型
- health/input catch (e: any) 改为 catch (e: unknown) + instanceof 守卫
- report/detail Object.entries 去掉 [string, any] 类型断言
- wechat_service 移除 decrypt_phone_placeholder 函数,内联占位注释
This commit is contained in:
iven
2026-04-24 08:16:01 +08:00
parent ef6d76ef6c
commit 6391a13467
9 changed files with 48 additions and 44 deletions

View File

@@ -1,7 +1,7 @@
import { PropsWithChildren } from 'react';
import './app.scss';
function App({ children }: PropsWithChildren<any>) {
function App({ children }: PropsWithChildren<Record<string, unknown>>) {
return children;
}

View File

@@ -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);
}, []);

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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<LoginResp>,
}
#[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,
}

View File

@@ -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<AuthError> 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,

View File

@@ -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>,
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>,
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(

View File

@@ -15,7 +15,9 @@ use crate::service::token_service::TokenService;
#[derive(Debug, Deserialize)]
struct WechatSessionResp {
openid: Option<String>,
#[allow(dead_code)]
session_key: Option<String>,
#[allow(dead_code)]
unionid: Option<String>,
errcode: Option<i32>,
errmsg: Option<String>,
@@ -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<LoginResp> {
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 构建 LoginResptoken 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<WechatSessionResp> {
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)))?;