新增求种和个人中心接口

Change-Id: Ibf3eef5b91a45a0ccf7b99d08afa29960884a8cf
diff --git a/front/src/UserProfile.js b/front/src/UserProfile.js
index f6ef9f4..eed84da 100644
--- a/front/src/UserProfile.js
+++ b/front/src/UserProfile.js
@@ -28,7 +28,7 @@
   const [userStats, setUserStats] = useState({

     magic: 0,

     upload: 0,

-    download: 0,

+    viptime: 0,

     ratio: 0,

   });

 

@@ -42,13 +42,18 @@
   const [exchangeResult, setExchangeResult] = useState(0);

 

   // 兑换比例

-  const exchangeRate = { uploaded: 10, downloaded: 10, vip_downloads: 100 };

+  const exchangeRate = { uploaded: 0.1, downloaded: 0.1, vip_downloads: 100 };

 

   // 用户申诉相关

   const [appealOpen, setAppealOpen] = useState(false);

   const [appealTitle, setAppealTitle] = useState('');

   const [appealFile, setAppealFile] = useState(null);

 

+  // 账号迁移相关

+  const [migrationOpen, setMigrationOpen] = useState(false);

+  const [migrationEmail, setMigrationEmail] = useState('');

+  const [migrationPassword, setMigrationPassword] = useState('');

+  const [migrationStatus, setMigrationStatus] = useState('');

   // 兑换结果计算

   React.useEffect(() => {

     if (!exchangeMagic || isNaN(exchangeMagic)) {

@@ -71,7 +76,7 @@
         const res = await fetch(`${API_BASE_URL}/api/user-profile?userid=${userid}`);

         if (res.ok) {

           const data = await res.json();

-          console.log("获取用户信息:", data);

+          // console.log("获取用户信息:", data);

           setUserInfo(data);

           setTempUserInfo(data);

         }

@@ -102,18 +107,30 @@
     };

     fetchUserSeeds();

   }, []);

-

-  // 收藏种子(示例数据)

+  // 获取收藏种子

   useEffect(() => {

-    setUserFavorites([

-      { seedid: 'fav1', title: '收藏种子1', tags: '标签A', downloadtimes: 10 },

-    ]);

+    const fetchUserFavorites = async () => {

+      const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');

+      const userid = match ? match[2] : null;

+      if (!userid) return;

+      try {

+        const res = await fetch(`${API_BASE_URL}/api/user-favorites?userid=${userid}`);

+        if (res.ok) {

+          const data = await res.json();

+          // console.log("获取收藏种子列表:", data);

+          setUserFavorites(data);

+        }

+      } catch (err) {

+        console.error("获取收藏种子列表失败", err);

+      }

+    };

+    fetchUserFavorites();

   }, []);

-

   // 获取活跃度

   useEffect(() => {

     const fetchUserStats = async () => {

-      const userid = "550e8400-e29b-41d4-a716-446655440000";

+      const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');

+      const userid = match ? match[2] : null;

       if (!userid) return;

       try {

         const res = await fetch(`${API_BASE_URL}/api/user-stats?userid=${userid}`);

@@ -171,57 +188,205 @@
   };

 

   // 邀请

-  const handleInvite = () => {

-    if (!inviteEmail) return;

+  const handleInvite = async () => {

+    if (!inviteEmail) {

+      setInviteStatus("请输入邀请邮箱");

+      return;

+    }

+    // 获取userid

+    const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');

+    const userid = match ? match[2] : null;

+    if (!userid) {

+      setInviteStatus("未获取到用户ID");

+      return;

+    }

     if (userInfo.invite_left <= 0) {

       setInviteStatus("邀请次数已用完");

       return;

     }

-    setInviteStatus("邀请成功!(示例,无后端)");

-    setUserInfo((prev) => ({

-      ...prev,

-      invite_left: prev.invite_left - 1,

-    }));

-    setTempUserInfo((prev) => ({

-      ...prev,

-      invite_left: prev.invite_left - 1,

-    }));

-    setInviteEmail('');

-  };

-

-  // 兑换

-  const handleExchange = () => {

+    try {

+        const res = await fetch(`${API_BASE_URL}/api/invite`, {

+        method: 'POST',

+        headers: { 'Content-Type': 'application/json' },

+        body: JSON.stringify({ userid, invite_email: inviteEmail }),

+      });

+      if (res.ok) {

+        const data = await res.json();

+        setInviteStatus("邀请成功");

+        // 更新剩余次数

+        const left = data.invite_left !== undefined ? data.invite_left : userInfo.invite_left - 1;

+        setUserInfo(prev => ({ ...prev, invite_left: left }));

+        setTempUserInfo(prev => ({ ...prev, invite_left: left }));

+        setInviteEmail('');

+      } else {

+        const errorText = await res.text();

+        setInviteStatus("邀请失败:" + errorText);

+      }

+    } catch (err) {

+      console.error("邀请失败", err);

+      setInviteStatus("邀请失败,请检查网络");

+    }

+  };  // 兑换

+  const handleExchange = async () => {

     const magic = Number(exchangeMagic);

     if (!magic || isNaN(magic) || magic <= 0) return;

     if (magic > userStats.magic) {

       alert("魔力值不足!");

       return;

     }

-    let newStats = { ...userStats };

-    if (exchangeType === "uploaded") {

-      newStats.upload += magic / exchangeRate.uploaded;

-    } else if (exchangeType === "downloaded") {

-      newStats.download = Math.max(0, newStats.download - magic / exchangeRate.downloaded);

-    } else if (exchangeType === "vip_downloads") {

-      newStats.vip_downloads += magic / exchangeRate.vip_downloads;

+    

+    // 检查兑换结果是否为整数

+    const calculatedExchangeResult = magic / exchangeRate[exchangeType];

+    if (!Number.isInteger(calculatedExchangeResult)) {

+      alert("兑换结果必须为整数,请调整魔力值!");

+      return;

     }

-    newStats.magic -= magic;

-    setUserStats(newStats);

-    setExchangeMagic('');

-    alert("兑换成功!(示例,无后端)");

-  };

+    

+    // 获取userid

+    const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');

+    const userid = match ? match[2] : null;

+    if (!userid) {

+      alert("未获取到用户ID");

+      return;

+    }

 

+    console.log("兑换请求参数:", { userid, magic, exchangeType, exchangeResult: calculatedExchangeResult });

+    try {

+      // 发送兑换请求到后端

+      const res = await fetch(`${API_BASE_URL}/api/exchange`, {

+        method: 'POST',

+        headers: { 'Content-Type': 'application/json' },

+        body: JSON.stringify({ 

+          userid, 

+          magic, 

+          exchangeType,

+          exchangeResult: calculatedExchangeResult

+        }),

+      });

+      // console.log("兑换请求结果:", res);

+      if (res.ok) {

+        // 兑换成功后重新获取用户数据

+        const statsRes = await fetch(`${API_BASE_URL}/api/user-stats?userid=${userid}`);

+        if (statsRes.ok) {

+          const updatedStats = await statsRes.json();

+          setUserStats(updatedStats);

+        }

+        setExchangeMagic('');

+        alert("兑换成功!");

+      } else {

+        const errorText = await res.text();

+        alert("兑换失败:" + errorText);

+      }

+    } catch (err) {

+      console.error("兑换失败", err);

+      alert("兑换失败,请检查网络");

+    }

+  };

   // 删除种子

   const handleDeleteSeed = (seedid) => {

     setUserSeeds(userSeeds.filter((s) => s.seedid !== seedid));

   };

 

+  // 取消收藏

+  const handleRemoveFavorite = async (seedid) => {

+    const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');

+    const userid = match ? match[2] : null;

+    if (!userid) {

+      alert('未获取到用户ID');

+      return;

+    }

+    try {

+      const res = await fetch(`${API_BASE_URL}/api/remove-favorite`, {

+        method: 'POST',

+        headers: { 'Content-Type': 'application/json' },

+        body: JSON.stringify({ userid, seedid }),

+      });      if (res.ok) {

+        setUserFavorites(userFavorites.filter((s) => (s.seedid || s.seed_id) !== seedid));

+        alert('已取消收藏');

+      } else {

+        alert('取消收藏失败,请重试');

+      }

+    } catch (err) {

+      console.error('取消收藏失败', err);

+      alert('取消收藏失败,请检查网络');

+    }

+  };

+

   // 申诉提交逻辑

-  const handleAppealSubmit = () => {

-    alert('申诉已提交!(示例,无后端)');

-    setAppealOpen(false);

-    setAppealTitle('');

-    setAppealFile(null);

+  const handleAppealSubmit = async () => {

+    if (!appealTitle || !appealFile) return;

+    // 获取userid

+    const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');

+    const userid = match ? match[2] : null;

+    if (!userid) {

+      alert('未获取到用户ID');

+      return;

+    }

+    // 构建表单数据

+    const formData = new FormData();

+    formData.append('userid', userid);

+    formData.append('content', appealTitle);

+    formData.append('file', appealFile);

+    try {

+      const res = await fetch(`${API_BASE_URL}/api/submit-appeal`, {

+        method: 'POST',

+        body: formData,

+      });

+      if (res.ok) {

+        alert('申诉已提交');

+        setAppealOpen(false);

+        setAppealTitle('');

+        setAppealFile(null);

+      } else {

+        const errorText = await res.text();

+        alert('申诉失败:' + errorText);

+      }

+    } catch (err) {

+      console.error('申诉失败', err);

+      alert('申诉失败,请检查网络');

+    }

+  };

+  // 账号迁移提交逻辑

+  const handleMigrationSubmit = async () => {

+    if (!appealFile) {

+      setMigrationStatus('请选择PDF文件');

+      return;

+    }

+    

+    // 获取当前用户ID

+    const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');

+    const currentUserId = match ? match[2] : null;

+    if (!currentUserId) {

+      setMigrationStatus('未获取到当前用户ID');

+      return;

+    }

+    

+    try {

+      // 构建表单数据

+      const formData = new FormData();

+      formData.append('userid', currentUserId);

+      formData.append('file', appealFile);

+      

+      const res = await fetch(`${API_BASE_URL}/api/migrate-account`, {

+        method: 'POST',

+        body: formData,

+      });

+      

+      if (res.ok) {

+        setMigrationStatus('账号迁移申请已提交,请等待管理员审核');

+        setTimeout(() => {

+          setMigrationOpen(false);

+          setAppealFile(null);

+          setMigrationStatus('');

+        }, 2000);

+      } else {

+        const errorText = await res.text();

+        setMigrationStatus('迁移失败:' + errorText);

+      }

+    } catch (err) {

+      console.error('账号迁移失败', err);

+      setMigrationStatus('迁移失败,请检查网络');

+    }

   };

 

   return (

@@ -369,20 +534,25 @@
               <MenuItem value="m">男性</MenuItem>

               <MenuItem value="f">女性</MenuItem>

             </TextField>

-          </div>

-          <div style={{ display: 'flex', gap: 16, marginTop: 24, justifyContent: 'flex-end' }}>

+          </div>          <div style={{ display: 'flex', gap: 16, marginTop: 24, justifyContent: 'flex-end' }}>

             <Button

               variant="contained"

               color="primary"

               onClick={handleSave}

-              sx={{ fontSize: 16, borderRadius: 2, padding: '10px 24px' }}

+              sx={{ fontSize: 16, borderRadius: 2, padding: '6px 12px' }}

             >保存</Button>

             <Button

               variant="contained"

               color="error"

               onClick={() => setAppealOpen(true)}

-              sx={{ fontSize: 16, borderRadius: 2, padding: '10px 24px' }}

+              sx={{ fontSize: 16, borderRadius: 2, padding: '6px 12px' }}

             >用户申诉</Button>

+            <Button

+              variant="contained"

+              color="warning"

+              onClick={() => setMigrationOpen(true)}

+              sx={{ fontSize: 16, borderRadius: 2, padding: '6px 12px' }}

+            >账号迁移</Button>

           </div>

         </div>

       </div>

@@ -432,11 +602,14 @@
               <MenuItem value="uploaded">上传量(增加)</MenuItem>

               <MenuItem value="downloaded">下载量(减少)</MenuItem>

               <MenuItem value="vip_downloads">VIP下载次数(增加)</MenuItem>

-            </TextField>

-            <span style={{ marginLeft: 8, color: '#43a047' }}>

-              可兑换:<b>{exchangeResult}</b> {exchangeType === 'vip_downloads' ? '次' : 'GB'}

-            </span>

-            <Button

+            </TextField>            <span style={{ marginLeft: 8, color: '#43a047' }}>

+              可兑换:<b>{exchangeResult}</b> {exchangeType === 'vip_downloads' ? '次' : 'MB'}

+              {!Number.isInteger(exchangeResult) && exchangeResult > 0 && (

+                <span style={{ color: '#e53935', fontSize: '12px', marginLeft: 8 }}>

+                  (结果必须为整数)

+                </span>

+              )}

+            </span><Button

               variant="contained"

               color="primary"

               onClick={handleExchange}

@@ -444,19 +617,19 @@
                 !exchangeMagic ||

                 isNaN(exchangeMagic) ||

                 Number(exchangeMagic) <= 0 ||

-                Number(exchangeMagic) > userStats.magic

+                Number(exchangeMagic) > userStats.magic ||

+                !Number.isInteger(exchangeResult)

               }

               sx={{

                 marginLeft: 2,

                 minWidth: 80,

-                background: (!exchangeMagic || isNaN(exchangeMagic) || Number(exchangeMagic) <= 0 || Number(exchangeMagic) > userStats.magic) ? '#ccc' : undefined

+                background: (!exchangeMagic || isNaN(exchangeMagic) || Number(exchangeMagic) <= 0 || Number(exchangeMagic) > userStats.magic || !Number.isInteger(exchangeResult)) ? '#ccc' : undefined

               }}

             >兑换</Button>

-          </div>

-          <div>上传量:<b style={{ color: '#43a047' }}>{userStats.upload?.toFixed(2)} GB</b></div>

-          <div>下载量:<b style={{ color: '#e53935' }}>{userStats.download?.toFixed(2)} GB</b></div>

+          </div>          <div>上传量:<b style={{ color: '#43a047' }}>{(userStats.upload / 1000000)?.toFixed(2)} MB</b></div>

+          <div>下载量:<b style={{ color: '#e53935' }}>{(userStats.download / 1000000)?.toFixed(2)} MB</b></div>

           <div>上传/下载值:<b style={{ color: '#ff9800' }}>{userStats.download === 0 ? "∞" : (userStats.upload / userStats.download).toFixed(2)}</b></div>

-          <div>VIP下载次数:<b style={{ color: '#1976d2' }}>{userStats.vip_downloads}</b></div>

+          <div>VIP下载次数:<b style={{ color: '#1976d2' }}>{userStats.viptime}</b></div>

         </div>

       </div>

       {/* 右上:个人上传种子列表 */}

@@ -509,11 +682,14 @@
                     variant="contained"

                     color="error"

                     size="small"

-                    sx={{ marginLeft: 2, borderRadius: 1, minWidth: 60 }}

-                    onClick={async e => {

+                    sx={{ marginLeft: 2, borderRadius: 1, minWidth: 60 }}                    onClick={async e => {

                       e.stopPropagation();

-                      // const userid = localStorage.getItem("userid");

-                      const userid = "550e8400-e29b-41d4-a716-446655440000"; // 示例userid

+                      const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');

+                      const userid = match ? match[2] : null;

+                      if (!userid) {

+                        alert('未获取到用户ID');

+                        return;

+                      }

                       try {

                         const res = await fetch(`${API_BASE_URL}/api/delete-seed`, {

                           method: 'POST',

@@ -558,8 +734,7 @@
         }}>

           {userFavorites.length === 0 ? (

             <div style={{ color: '#b2b2b2', fontSize: 18, textAlign: 'center' }}>(暂无收藏种子)</div>

-          ) : (

-            <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>

+          ) : (            <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>

               {userFavorites.map((seed, idx) => (

                 <li

                   key={seed.seedid || idx}

@@ -570,16 +745,25 @@
                     borderBottom: idx === userFavorites.length - 1 ? 'none' : '1px solid #e0e7ff',

                     cursor: 'pointer',

                     transition: 'background 0.15s'

-                  }}

-                  onClick={e => {

-                    navigate(`/torrent/${seed.seedid}`);

+                  }}                  onClick={e => {                    if (e.target.classList.contains('remove-favorite-btn')) return;

+                    navigate(`/torrent/${seed.seedid || seed.seed_id}`);

                   }}

                   onMouseOver={e => e.currentTarget.style.background = '#f3f6ff'}

                   onMouseOut={e => e.currentTarget.style.background = ''}

                 >

-                  <span style={{ flex: 2, fontWeight: 500, color: '#1a237e', textDecoration: 'underline' }}>{seed.title}</span>

-                  <span style={{ flex: 1, color: '#5c6bc0' }}>{seed.tags}</span>

-                  <span style={{ flex: 1, color: '#ff9800', textAlign: 'right' }}>人气: {seed.downloadtimes}</span>

+                  <span style={{ flex: 2, fontWeight: 500, color: '#1a237e', textDecoration: 'underline', cursor: 'pointer' }}>{seed.seed.title}</span>

+                  <span style={{ flex: 1, color: '#5c6bc0' }}>{seed.seed.tags}</span>

+                  <span style={{ flex: 1, color: '#ff9800', textAlign: 'right' }}>人气: {seed.seed.downloadtimes}</span>

+                  <Button

+                    className="remove-favorite-btn"

+                    variant="contained"

+                    color="warning"

+                    size="small"

+                    sx={{ marginLeft: 2, borderRadius: 1, minWidth: 80 }}                    onClick={e => {

+                      e.stopPropagation();

+                      handleRemoveFavorite(seed.seedid || seed.seed_id);

+                    }}

+                  >取消收藏</Button>

                 </li>

               ))}

             </ul>

@@ -598,19 +782,66 @@
               onChange={e => setAppealTitle(e.target.value)}

               size="small"

             />

-          </div>

-          <div>

+          </div>          <div>

             <input

               type="file"

-              onChange={e => setAppealFile(e.target.files[0])}

+              accept=".pdf"

+              onChange={e => {

+                const file = e.target.files[0];

+                if (file && file.type !== 'application/pdf') {

+                  alert('请选择PDF文件');

+                  e.target.value = '';

+                  setAppealFile(null);

+                } else {

+                  setAppealFile(file);

+                }

+              }}

               style={{ marginTop: 8 }}

             />

+            <div style={{ fontSize: 12, color: '#666', marginTop: 4 }}>

+              请选择PDF文件(最大100MB)

+            </div>

           </div>

         </DialogContent>

         <DialogActions>

           <Button onClick={handleAppealSubmit} variant="contained" color="primary" disabled={!appealTitle || !appealFile}>提交</Button>

           <Button onClick={() => setAppealOpen(false)} variant="outlined">取消</Button>

         </DialogActions>

+      </Dialog>      {/* 账号迁移弹窗 */}

+      <Dialog open={migrationOpen} onClose={() => setMigrationOpen(false)}>

+        <DialogTitle>账号迁移</DialogTitle>

+        <DialogContent>

+          <div style={{ marginBottom: 16 }}>

+          </div>          <div>

+            <input

+              type="file"

+              accept=".pdf"

+              onChange={e => {

+                const file = e.target.files[0];

+                if (file && file.type !== 'application/pdf') {

+                  alert('请选择PDF文件');

+                  e.target.value = '';

+                  setAppealFile(null);

+                } else {

+                  setAppealFile(file);

+                }

+              }}

+              style={{ marginTop: 8 }}

+            />

+            <div style={{ fontSize: 12, color: '#666', marginTop: 4 }}>

+              请选择PDF文件(最大10MB)

+            </div>

+          </div>

+          {migrationStatus && (

+            <div style={{ color: migrationStatus.includes('成功') ? '#43a047' : '#e53935', fontSize: 14, marginTop: 8 }}>

+              {migrationStatus}

+            </div>

+          )}

+        </DialogContent>

+        <DialogActions>

+          <Button onClick={handleMigrationSubmit} variant="contained" color="primary">提交迁移</Button>

+          <Button onClick={() => setMigrationOpen(false)} variant="outlined">取消</Button>

+        </DialogActions>

       </Dialog>

     </div>

   );