fix(auth+miniprogram): 清除全部审计遗留问题
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 构建 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<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)))?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user