Przeglądaj źródła

✨ feat(avatar): 重构头像上传功能,使用fileId替代URL管理头像

- 修改SimpleAvatarUpload组件接口,新增fileId属性,支持通过fileId加载头像
- 调整useAvatarUploadMinio钩子,onSuccess回调返回fileId而非URL
- 更新ProfileEditPage页面,使用avatarFileId状态管理头像文件ID
- 优化头像URL生成逻辑,通过fileId构建/api/v1/files/{fileId}访问路径
- 完善MinIO上传工具,确保fileId在上传成功后正确返回和传递

🐛 fix(upload): 修复文件上传完成后无法正确获取fileId的问题

- 修改multipart-complete接口响应,确保fileId为数字类型
- 修复MinIOXHRMultipartUploader中completeMultipartUpload方法返回fileId
- 移除uploadFile工具函数,避免使用过时的URL管理方式

♻️ refactor(profile): 优化用户档案数据获取方式

- 修复ProfileEditPage中silverUsersClient调用方式,移除多余类型断言
- 调整用户头像状态管理逻辑,统一使用fileId进行头像资源管理
yourname 7 miesięcy temu
rodzic
commit
5db5af2e42

+ 9 - 3
src/client/mobile/components/SimpleAvatarUpload.tsx

@@ -23,14 +23,16 @@ const COLORS = {
 
 interface SimpleAvatarUploadProps {
   currentAvatar?: string | null;
-  onUploadSuccess?: (avatarUrl: string) => void;
+  onUploadSuccess?: (fileId: number) => void;
   size?: number;
+  fileId?: number | null;
 }
 
 export const SimpleAvatarUpload: React.FC<SimpleAvatarUploadProps> = ({
   currentAvatar,
   onUploadSuccess,
   size = 96,
+  fileId,
 }) => {
   const {
     isUploading,
@@ -40,9 +42,13 @@ export const SimpleAvatarUpload: React.FC<SimpleAvatarUploadProps> = ({
     handleCropComplete,
     handleCropCancel,
     triggerFileSelect,
-  } = useAvatarUploadMinio({ onSuccess: onUploadSuccess });
+  } = useAvatarUploadMinio({
+    onSuccess: (url: string, fileId: number) => {
+      onUploadSuccess?.(fileId);
+    }
+  });
 
-  const avatarSrc = currentAvatar;
+  const avatarSrc = currentAvatar || (fileId ? `/api/v1/files/${fileId}` : '');
 
   return (
     <>

+ 5 - 15
src/client/mobile/hooks/useAvatarUploadMinio.ts

@@ -6,7 +6,7 @@ import { compressImage, isImageFile, checkFileSize } from '@/client/utils/upload
 import { useAuth } from './AuthProvider';
 
 interface UseAvatarUploadOptions {
-  onSuccess?: (avatarUrl: string) => void;
+  onSuccess?: (fileId: number) => void;
   onError?: (error: Error) => void;
 }
 
@@ -44,27 +44,17 @@ export const useAvatarUploadMinio = ({ onSuccess, onError }: UseAvatarUploadOpti
         }
       );
 
-      // 4. 更新用户头像
-      const updateResponse = await userClient[user.id].$put({
-        json: {
-          avatar: result.fileUrl,
-        }
-      });
-
-      if (updateResponse.status !== 200) {
-        throw new Error('更新用户信息失败');
-      }
-
-      return result.fileUrl;
+      // 4. 返回文件ID(uploadMinIOWithPolicy会自动创建文件记录并返回fileId)
+      return result.fileId;
     },
-    onSuccess: (avatarUrl) => {
+    onSuccess: (fileId) => {
       // 清除相关缓存
       queryClient.invalidateQueries({ queryKey: ['userProfile'] });
       queryClient.invalidateQueries({ queryKey: ['currentUser'] });
       queryClient.invalidateQueries({ queryKey: ['user', user?.id] });
       
       if (onSuccess) {
-        onSuccess(avatarUrl);
+        onSuccess(fileId);
       }
     },
     onError: (error) => {

+ 8 - 7
src/client/mobile/pages/ProfileEditPage.tsx

@@ -15,7 +15,7 @@ const ProfileEditPage: React.FC = () => {
   const [form] = Form.useForm();
   const [loading, setLoading] = useState(false);
   const [profile, setProfile] = useState<any>(null);
-  const [avatarUrl, setAvatarUrl] = useState<string>('');
+  const [avatarFileId, setAvatarFileId] = useState<number | null>(null);
 
   useEffect(() => {
     if (user) {
@@ -25,7 +25,7 @@ const ProfileEditPage: React.FC = () => {
 
   const loadProfile = async () => {
     try {
-      const response = await (silverUsersClient as any)['profiles'].$get({
+      const response = await silverUsersClient['profiles'].$get({
         query: { filters: JSON.stringify({ userId: user.id }) }
       });
       
@@ -34,7 +34,7 @@ const ProfileEditPage: React.FC = () => {
         if (data.data && data.data.length > 0) {
           const profileData = data.data[0];
           setProfile(profileData);
-          setAvatarUrl(profileData.avatar || '');
+          setAvatarFileId(profileData.avatarFileId || null);
           
           form.setFieldsValue({
             ...profileData,
@@ -59,8 +59,8 @@ const ProfileEditPage: React.FC = () => {
     }
   };
 
-  const handleAvatarChange = (newAvatarUrl: string) => {
-    setAvatarUrl(newAvatarUrl);
+  const handleAvatarChange = (fileId: number) => {
+    setAvatarFileId(fileId);
   };
 
   const handleSubmit = async (values: any) => {
@@ -81,7 +81,7 @@ const ProfileEditPage: React.FC = () => {
         // 更新银龄档案
         const profileData = {
           ...values,
-          avatar: avatarUrl,
+          avatarFileId: avatarFileId,
           birthDate: values.birthDate?.format('YYYY-MM-DD'),
           workStartDate: values.workStartDate?.format('YYYY-MM-DD'),
           userId: user.id
@@ -141,9 +141,10 @@ const ProfileEditPage: React.FC = () => {
           <Form.Item label="头像">
             <div className="flex items-center space-x-4">
               <SimpleAvatarUpload
-                currentAvatar={avatarUrl}
+                currentAvatar={avatarFileId ? (silverUsersClient as any)['profiles'].$get({ query: { id: avatarFileId } }) : ''}
                 size={80}
                 onUploadSuccess={handleAvatarChange}
+                fileId={avatarFileId}
               />
               <span className="text-sm text-gray-500">点击头像上传新照片</span>
             </div>

+ 10 - 13
src/client/utils/minio.ts

@@ -23,6 +23,7 @@ export interface UploadResult {
   fileUrl:string;
   fileKey:string;
   bucketName:string;
+  fileId: number;
 }
 
 interface UploadPart {
@@ -116,7 +117,7 @@ export class MinIOXHRMultipartUploader {
     
     // 完成上传
     try {
-      await this.completeMultipartUpload(policy, key, uploadedParts);
+      const fileId = await this.completeMultipartUpload(policy, key, uploadedParts);
       
       callbacks?.onProgress?.({
         stage: 'complete',
@@ -129,7 +130,8 @@ export class MinIOXHRMultipartUploader {
       return {
         fileUrl: `${policy.host}/${key}`,
         fileKey: key,
-        bucketName: policy.bucket
+        bucketName: policy.bucket,
+        fileId
       };
     } catch (error) {
       callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
@@ -195,7 +197,7 @@ export class MinIOXHRMultipartUploader {
     policy: MinioMultipartUploadPolicy,
     key: string,
     uploadedParts: UploadPart[]
-  ): Promise<void> {
+  ): Promise<number> {
     const response = await fileClient["multipart-complete"].$post({
         json:{
             bucket: policy.bucket,
@@ -208,6 +210,9 @@ export class MinIOXHRMultipartUploader {
     if (!response.ok) {
       throw new Error(`完成分段上传失败: ${response.status} ${response.statusText}`);
     }
+    
+    const result = await response.json();
+    return result.fileId;
   }
 }
 
@@ -270,7 +275,8 @@ export class MinIOXHRUploader {
                     resolve({
                         fileUrl:`${policy.uploadPolicy.host}/${key}`,
                         fileKey: key,
-                        bucketName: policy.uploadPolicy.bucket
+                        bucketName: policy.uploadPolicy.bucket,
+                        fileId: policy.file.id
                     });
                 } else {
                     const error = new Error(`上传失败: ${xhr.status} ${xhr.statusText}`);
@@ -345,15 +351,6 @@ export async function getMultipartUploadPolicy(totalSize: number, fileKey: strin
   return await policyResponse.json();
 }
 
-export async function uploadFile(
-  file: File,
-  folder: string = 'uploads'
-): Promise<string> {
-  const fileKey = `${folder}/${Date.now()}-${file.name}`;
-  const result = await uploadMinIOWithPolicy('', file, fileKey);
-  return result.fileUrl;
-}
-
 export async function uploadMinIOWithPolicy(
   uploadPath: string,
   file: File | Blob,

+ 2 - 2
src/server/api/files/multipart-complete/post.ts

@@ -41,9 +41,9 @@ const CompleteMultipartUploadDto = z.object({
 
 // 完成分片上传响应Schema
 const CompleteMultipartUploadResponse = z.object({
-  fileId: z.string().openapi({
+  fileId: z.number().openapi({
     description: '文件ID',
-    example: 'file_123456'
+    example: 1
   }),
   url: z.string().openapi({
     description: '文件访问URL',