Merge "fix(TorrentComment):修复了获取评论列表中没有正确得到userId和时间的bug"
diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts
index 919ae3b..92f45b3 100644
--- a/react-ui/config/routes.ts
+++ b/react-ui/config/routes.ts
@@ -75,6 +75,10 @@
},
{
+ path: '/torrent/comments/:torrentId',
+ component: './Torrent/Comments',
+ },
+ {
name: 'tool',
path: '/tool',
routes: [
diff --git a/react-ui/src/components/Footer/index.tsx b/react-ui/src/components/Footer/index.tsx
index f204ac2..162587e 100644
--- a/react-ui/src/components/Footer/index.tsx
+++ b/react-ui/src/components/Footer/index.tsx
@@ -4,31 +4,32 @@
const Footer: React.FC = () => {
return (
- <DefaultFooter
- style={{
- background: 'none',
- }}
- links={[
- {
- key: 'Ant Design Pro',
- title: 'Ant Design Pro',
- href: 'https://pro.ant.design',
- blankTarget: true,
- },
- {
- key: 'github',
- title: <GithubOutlined />,
- href: 'https://github.com/ant-design/ant-design-pro',
- blankTarget: true,
- },
- {
- key: 'Ant Design',
- title: 'Ant Design',
- href: 'https://ant.design',
- blankTarget: true,
- },
- ]}
- />
+ // <DefaultFooter
+ // style={{
+ // background: 'none',
+ // }}
+ // links={[
+ // {
+ // key: 'Ant Design Pro',
+ // title: 'Ant Design Pro',
+ // href: 'https://pro.ant.design',
+ // blankTarget: true,
+ // },
+ // {
+ // key: 'github',
+ // title: <GithubOutlined />,
+ // href: 'https://github.com/ant-design/ant-design-pro',
+ // blankTarget: true,
+ // },
+ // {
+ // key: 'Ant Design',
+ // title: 'Ant Design',
+ // href: 'https://ant.design',
+ // blankTarget: true,
+ // },
+ // ]}
+ // />
+ <></>
);
};
diff --git a/react-ui/src/pages/Message/data.d.ts b/react-ui/src/pages/Message/data.d.ts
new file mode 100644
index 0000000..2a537ea
--- /dev/null
+++ b/react-ui/src/pages/Message/data.d.ts
@@ -0,0 +1,63 @@
+// 消息接口
+export interface SysUserMessage {
+ messageId: number;
+ senderId: number;
+ receiverId: number;
+ content: string;
+ createTime: Date;
+ delFlag: string;
+}
+
+// 聊天对象接口
+export interface ChatContact {
+ userId: number;
+ userName: string;
+ avatar?: string;
+ lastMessage: string;
+ lastMessageTime: Date;
+}
+
+// 获取聊天对象列表参数
+export interface ChatContactListParams {
+ pageNum?: number;
+ pageSize?: number;
+ keyword?: string; // 搜索关键词
+}
+
+// 获取聊天记录参数
+export interface ChatHistoryParams {
+ userId: number; // 聊天对象用户ID
+ currentUserId?: number; // 当前用户ID
+ pageNum?: number;
+ pageSize?: number;
+}
+
+// 发送消息参数
+export interface SendMessageParams {
+ receiverId: number;
+ content: string;
+}
+
+// 用户信息接口
+export interface UserInfo {
+ userId: number;
+ userName: string;
+ avatar?: string;
+}
+
+// API 响应基础接口
+export interface ApiResponse<T = any> {
+ code: number;
+ message: string;
+ data: T;
+ success: boolean;
+}
+
+// 分页响应接口
+export interface PageResponse<T = any> {
+ list: T[];
+ total: number;
+ pageNum: number;
+ pageSize: number;
+ pages: number;
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/Message/index.less b/react-ui/src/pages/Message/index.less
new file mode 100644
index 0000000..0911278
--- /dev/null
+++ b/react-ui/src/pages/Message/index.less
@@ -0,0 +1,72 @@
+.message-page {
+ .selected {
+ background-color: #f0f2f5;
+ }
+
+ .chat-message {
+ margin: 8px 0;
+ padding: 8px;
+ border-radius: 4px;
+
+ &.sent {
+ background-color: #e6f7ff;
+ margin-left: 40px;
+ }
+
+ &.received {
+ background-color: #f0f2f5;
+ margin-right: 40px;
+ }
+ }
+
+ .message-input {
+ border-top: 1px solid #f0f0f0;
+ padding-top: 16px;
+ }
+
+ .message-list {
+ height: calc(100vh - 200px);
+ overflow-y: auto;
+ }
+
+ .chat-container {
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - 100px);
+ }
+
+ .chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+ }
+
+ .ant-list-item {
+ transition: background-color 0.3s;
+
+ &:hover {
+ background-color: #f5f5f5;
+ }
+ }
+
+}
+
+.message-page {
+ .chat-message {
+ &.sent {
+ .ant-list-item-meta {
+ text-align: right;
+ }
+ }
+
+ &.received {
+ .ant-list-item-meta {
+ text-align: left;
+ }
+ }
+ }
+
+ .selected {
+ background-color: #f0f8ff;
+ }
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/Message/index.tsx b/react-ui/src/pages/Message/index.tsx
new file mode 100644
index 0000000..b07630a
--- /dev/null
+++ b/react-ui/src/pages/Message/index.tsx
@@ -0,0 +1,361 @@
+import React, { useState, useEffect } from 'react';
+import { Card, List, Avatar, Input, Button, Row, Col, message } from 'antd';
+import { SysUserMessage, ChatContact } from './data.d';
+import { getChatContactList, getChatHistory, sendMessage, getUserInfo } from './service';
+import './index.less';
+
+const MessagePage: React.FC = () => {
+ const [chatContacts, setChatContacts] = useState<ChatContact[]>([]);
+ const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
+ const [inputMessage, setInputMessage] = useState('');
+ const [chatMessages, setChatMessages] = useState<SysUserMessage[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [sending, setSending] = useState(false);
+
+ // 获取聊天对象列表
+ const fetchChatContacts = async () => {
+ try {
+ setLoading(true);
+ const response = await getChatContactList();
+
+ // 按最后消息时间排序
+ const sortedContacts = response.sort((a: ChatContact, b: ChatContact) =>
+ new Date(b.lastMessageTime).getTime() - new Date(a.lastMessageTime).getTime()
+ );
+ setChatContacts(sortedContacts);
+ } catch (error) {
+ message.error('获取聊天列表失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 获取与特定用户的聊天记录
+ const fetchChatHistory = async (userId: number) => {
+ try {
+ setLoading(true);
+ const response = await getChatHistory({
+ userId,
+ currentUserId: 1, // 假设当前用户ID为1,实际应该从用户状态获取
+ pageSize: 100 // 获取最近100条消息
+ });
+
+ // 按时间排序
+ const sortedMessages = response.sort((a: SysUserMessage, b: SysUserMessage) =>
+ new Date(a.createTime).getTime() - new Date(b.createTime).getTime()
+ );
+ setChatMessages(sortedMessages);
+ } catch (error) {
+ message.error('获取聊天记录失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 发送消息
+ const handleSendMessage = async () => {
+ if (!inputMessage.trim() || !selectedUserId || sending) {
+ return;
+ }
+
+ try {
+ setSending(true);
+
+ // 先在界面上显示消息(乐观更新)
+ const tempMessage: SysUserMessage = {
+ messageId: Date.now(), // 临时ID
+ senderId: 1, // 当前用户ID
+ receiverId: selectedUserId,
+ content: inputMessage,
+ createTime: new Date(),
+ delFlag: '0'
+ };
+ setChatMessages(prev => [...prev, tempMessage]);
+
+ // 清空输入框
+ const messageContent = inputMessage;
+ setInputMessage('');
+
+ // 调用API发送消息
+ await sendMessage({
+ receiverId: selectedUserId,
+ content: messageContent
+ });
+
+ // 更新聊天对象列表中的最后消息
+ setChatContacts(prevContacts => {
+ const updatedContacts = prevContacts.map(contact =>
+ contact.userId === selectedUserId
+ ? {
+ ...contact,
+ lastMessage: messageContent,
+ lastMessageTime: new Date()
+ }
+ : contact
+ );
+ // 重新排序,将当前聊天对象移到最前面
+ return updatedContacts.sort((a, b) =>
+ new Date(b.lastMessageTime).getTime() - new Date(a.lastMessageTime).getTime()
+ );
+ });
+
+ message.success('消息发送成功');
+
+ } catch (error) {
+ message.error('发送消息失败');
+ // 发送失败时,移除临时消息
+ setChatMessages(prev => prev.filter(msg => msg.messageId !== Date.now()));
+ } finally {
+ setSending(false);
+ }
+ };
+
+ // 选择聊天对象
+ const handleSelectUser = (userId: number) => {
+ setSelectedUserId(userId);
+ fetchChatHistory(userId);
+ };
+
+ // 格式化时间显示
+ const formatTime = (time: Date | string) => {
+ const date = new Date(time);
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const minutes = Math.floor(diff / (1000 * 60));
+ const hours = Math.floor(diff / (1000 * 60 * 60));
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+
+ if (minutes < 60) {
+ return `${minutes}分钟前`;
+ } else if (hours < 24) {
+ return `${hours}小时前`;
+ } else {
+ return `${days}天前`;
+ }
+ };
+
+ // 获取用户名
+ const getUserName = (userId: number) => {
+ const contact = chatContacts.find(c => c.userId === userId);
+ return contact?.userName || `用户${userId}`;
+ };
+
+ // 处理回车键发送
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSendMessage();
+ }
+ };
+
+ useEffect(() => {
+ fetchChatContacts();
+ }, []);
+
+ return (
+ <div className="message-page" style={{ height: '100vh', padding: '16px', display: 'flex', flexDirection: 'column' }}>
+ <Row gutter={16} style={{ flex: 1, overflow: 'hidden' }}>
+ {/* 左侧聊天对象列表 */}
+ <Col span={8}>
+ <Card
+ title="聊天列表"
+ bordered={false}
+ style={{ height: '100%' }}
+ loading={loading && !selectedUserId}
+ >
+ <List
+ style={{ height: 'calc(100vh - 140px)', overflowY: 'auto' }}
+ dataSource={chatContacts}
+ renderItem={(contact) => (
+ <List.Item
+ onClick={() => handleSelectUser(contact.userId)}
+ style={{
+ cursor: 'pointer',
+ backgroundColor: selectedUserId === contact.userId ? '#f0f8ff' : 'transparent',
+ padding: '12px',
+ borderRadius: '8px',
+ margin: '4px 0'
+ }}
+ className={selectedUserId === contact.userId ? 'selected' : ''}
+ >
+ <List.Item.Meta
+ avatar={
+ <Avatar size="large">
+ {contact.userName.charAt(0)}
+ </Avatar>
+ }
+ title={
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+ <span style={{
+ fontWeight: 'normal',
+ fontSize: '14px'
+ }}>
+ {contact.userName}
+ </span>
+ <span style={{ fontSize: '12px', color: '#999' }}>
+ {formatTime(contact.lastMessageTime)}
+ </span>
+ </div>
+ }
+ description={
+ <div style={{
+ color: '#666',
+ fontWeight: 'normal',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ fontSize: '13px'
+ }}>
+ {contact.lastMessage}
+ </div>
+ }
+ />
+ </List.Item>
+ )}
+ />
+ </Card>
+ </Col>
+
+ {/* 右侧聊天界面 */}
+ <Col span={16}>
+ <div style={{
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ position: 'relative'
+ }}>
+ {/* 聊天标题 */}
+ <Card
+ title={selectedUserId ? `与 ${getUserName(selectedUserId)} 的对话` : '选择一个联系人开始聊天'}
+ bordered={false}
+ style={{
+ marginBottom: 0,
+ borderBottom: '1px solid #f0f0f0'
+ }}
+ bodyStyle={{ padding: 0 }}
+ />
+
+ {/* 聊天消息区域 */}
+ <div style={{
+ flex: 1,
+ overflow: 'hidden',
+ display: 'flex',
+ flexDirection: 'column',
+ backgroundColor: '#fff',
+ border: '1px solid #f0f0f0',
+ borderTop: 'none'
+ }}>
+ {selectedUserId ? (
+ <>
+ <List
+ style={{
+ flex: 1,
+ overflowY: 'auto',
+ padding: '16px',
+ paddingBottom: '80px' // 为输入框预留空间
+ }}
+ dataSource={chatMessages}
+ loading={loading && selectedUserId !== null}
+ renderItem={(item) => (
+ <List.Item
+ className={`chat-message ${item.senderId === 1 ? 'sent' : 'received'}`}
+ style={{
+ border: 'none',
+ padding: '8px 0',
+ display: 'flex',
+ justifyContent: item.senderId === 1 ? 'flex-end' : 'flex-start'
+ }}
+ >
+ <div style={{
+ maxWidth: '70%',
+ display: 'flex',
+ flexDirection: item.senderId === 1 ? 'row-reverse' : 'row',
+ alignItems: 'flex-start',
+ gap: '8px'
+ }}>
+ <Avatar size="small">
+ {item.senderId === 1 ? 'Me' : getUserName(item.senderId).charAt(0)}
+ </Avatar>
+ <div>
+ <div style={{
+ fontSize: '12px',
+ color: '#999',
+ marginBottom: '4px',
+ textAlign: item.senderId === 1 ? 'right' : 'left'
+ }}>
+ {item.senderId === 1 ? '我' : getUserName(item.senderId)} · {new Date(item.createTime).toLocaleTimeString()}
+ </div>
+ <div style={{
+ backgroundColor: item.senderId === 1 ? '#1890ff' : '#f0f0f0',
+ color: item.senderId === 1 ? '#fff' : '#000',
+ padding: '8px 12px',
+ borderRadius: '12px',
+ wordBreak: 'break-word',
+ lineHeight: '1.4'
+ }}>
+ {item.content}
+ </div>
+ </div>
+ </div>
+ </List.Item>
+ )}
+ />
+
+ {/* 输入框区域 - 固定在底部 */}
+ <div style={{
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ backgroundColor: '#fff',
+ padding: '16px',
+ borderTop: '1px solid #f0f0f0',
+ display: 'flex',
+ gap: '8px',
+ zIndex: 10,
+ boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.1)'
+ }}>
+ <Input.TextArea
+ value={inputMessage}
+ onChange={(e) => setInputMessage(e.target.value)}
+ onKeyDown={handleKeyPress}
+ placeholder="输入消息...(按 Enter 发送,Shift+Enter 换行)"
+ style={{
+ flex: 1,
+ resize: 'none',
+ minHeight: '40px',
+ maxHeight: '120px'
+ }}
+ autoSize={{ minRows: 1, maxRows: 4 }}
+ />
+ <Button
+ type="primary"
+ onClick={handleSendMessage}
+ loading={sending}
+ style={{ height: '40px' }}
+ >
+ 发送
+ </Button>
+ </div>
+ </>
+ ) : (
+ <div style={{
+ flex: 1,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: '#999',
+ fontSize: '16px'
+ }}>
+ 请从左侧选择一个联系人开始聊天
+ </div>
+ )}
+ </div>
+ </div>
+ </Col>
+ </Row>
+ </div>
+ );
+};
+
+export default MessagePage;
\ No newline at end of file
diff --git a/react-ui/src/pages/Message/service.ts b/react-ui/src/pages/Message/service.ts
new file mode 100644
index 0000000..958290e
--- /dev/null
+++ b/react-ui/src/pages/Message/service.ts
@@ -0,0 +1,155 @@
+import { request } from '@umijs/max';
+import type {
+ SysUserMessage,
+ ChatContact,
+ ChatContactListParams,
+ ChatHistoryParams,
+ SendMessageParams,
+} from './data.d';
+
+// API 路径配置 - 可根据实际后端接口调整
+const API_CONFIG = {
+ CHAT_CONTACTS: '/api/system/message/contacts', // 可改为: '/api/message/contacts' 或其他路径
+ CHAT_HISTORY: '/api/system/message/list', // 可改为: '/api/message/history' 或其他路径
+ SEND_MESSAGE: '/api/system/message', // 可改为: '/api/message/send' 或其他路径
+ USER_INFO: '/api/system/user', // 可改为: '/api/user' 或其他路径
+};
+
+/** 获取聊天对象列表 */
+export async function getChatContactList(params?: ChatContactListParams) {
+ // 默认数据
+ const defaultData = [
+ {
+ userId: 2,
+ userName: '张三',
+ avatar: '',
+ lastMessage: '你好,请问有什么可以帮助你的吗?',
+ lastMessageTime: new Date(Date.now() - 1000 * 60 * 30),
+ unreadCount: 2
+ },
+ {
+ userId: 3,
+ userName: '李四',
+ avatar: '',
+ lastMessage: '关于最近的活动,我想了解更多细节。',
+ lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 2),
+ unreadCount: 0
+ },
+ {
+ userId: 4,
+ userName: '王五',
+ avatar: '',
+ lastMessage: '好的,我知道了,谢谢!',
+ lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 24),
+ unreadCount: 1
+ }
+ ];
+
+ try {
+ const queryString = params
+ ? `?${new URLSearchParams(params as Record<string, any>).toString()}`
+ : '';
+ const response = await request(`${API_CONFIG.CHAT_CONTACTS}${queryString}`, {
+ method: 'get',
+ });
+
+ // 如果接口返回空数据,使用默认数据
+ if (!response || response.length === 0) {
+ return defaultData;
+ }
+ return response;
+ } catch (error) {
+ // 接口报错时返回默认数据
+ console.warn('获取聊天对象列表接口异常,使用默认数据:', error);
+ return defaultData;
+ }
+
+}
+
+/** 获取与指定用户的聊天记录 */
+export async function getChatHistory(params: ChatHistoryParams) {
+ // 默认数据
+ const defaultData = [
+ {
+ messageId: 1,
+ senderId: params.userId,
+ receiverId: params.currentUserId || 1,
+ content: `这是来自用户${params.userId}的第一条消息`,
+ createTime: new Date(Date.now() - 1000 * 60 * 60 * 2),
+ delFlag: '0'
+ },
+ {
+ messageId: 2,
+ senderId: params.currentUserId || 1,
+ receiverId: params.userId,
+ content: '收到,感谢你的消息',
+ createTime: new Date(Date.now() - 1000 * 60 * 60),
+ delFlag: '0'
+ },
+ {
+ messageId: 3,
+ senderId: params.userId,
+ receiverId: params.currentUserId || 1,
+ content: `这是来自用户${params.userId}的最新消息,包含更多详细信息`,
+ createTime: new Date(Date.now() - 1000 * 60 * 30),
+ delFlag: '0'
+ }
+ ];
+
+ // try {
+ // const queryString = `?${new URLSearchParams(params as Record<string, any>).toString()}`;
+ // const response = await request(`${API_CONFIG.CHAT_HISTORY}${queryString}`, {
+ // method: 'get',
+ // });
+
+ // // 如果接口返回空数据,使用默认数据
+ // if (!response || response.length === 0) {
+ // return defaultData;
+ // }
+ // return response;
+ // } catch (error) {
+ // // 接口报错时返回默认数据
+ // console.warn('获取聊天记录接口异常,使用默认数据:', error);
+ // return defaultData;
+ // }
+ return defaultData;
+}
+
+/** 发送消息 */
+export async function sendMessage(params: SendMessageParams) {
+ try {
+ return await request(API_CONFIG.SEND_MESSAGE, {
+ method: 'post',
+ data: params,
+ });
+ } catch (error) {
+ // 发送消息失败时记录错误但不返回默认数据,让组件处理错误
+ console.error('发送消息接口异常:', error);
+ throw error; // 重新抛出错误,让调用方处理
+ }
+}
+
+/** 获取用户信息(用于聊天对象显示) */
+export async function getUserInfo(userId: number) {
+ // 默认用户信息
+ const defaultUserInfo = {
+ userId,
+ userName: `用户${userId}`,
+ avatar: ''
+ };
+
+ try {
+ const response = await request(`${API_CONFIG.USER_INFO}/${userId}`, {
+ method: 'get',
+ });
+
+ if (!response) {
+ return defaultUserInfo;
+ }
+ return response;
+ } catch (error) {
+ // 接口报错时返回默认用户信息
+ console.warn(`获取用户${userId}信息接口异常,使用默认数据:`, error);
+ return defaultUserInfo;
+ }
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/Reward/components/UpdateForm.tsx b/react-ui/src/pages/Reward/components/UpdateForm.tsx
new file mode 100644
index 0000000..526b946
--- /dev/null
+++ b/react-ui/src/pages/Reward/components/UpdateForm.tsx
@@ -0,0 +1,122 @@
+import { Modal, Form, Input, InputNumber, Select } from 'antd';
+import React from 'react';
+import { useIntl } from 'umi';
+import { RewardItem } from '../data';
+export type FormValueType = {
+ rewardId?: number;
+ title?: string;
+ description?: string;
+ amount?: number;
+ status?: string;
+ remark?: string;
+} & Partial<RewardItem>;
+
+export type UpdateFormProps = {
+ onCancel: (flag?: boolean, formVals?: FormValueType) => void;
+ onSubmit: (values: FormValueType) => Promise<void>;
+ open: boolean;
+ values: Partial<RewardItem>;
+ statusOptions: any;
+ readOnly?: boolean;
+};
+
+const UpdateForm: React.FC<UpdateFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { statusOptions } = props;
+
+ const intl = useIntl();
+
+ React.useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ rewardId: props.values.rewardId,
+ title: props.values.title,
+ description: props.values.description,
+ amount: props.values.amount,
+ status: props.values.status,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ form.resetFields();
+ };
+ const handleFinish = async (values: FormValueType) => {
+ props.onSubmit(values);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={props.readOnly ? '查看悬赏' : (props.values.rewardId ? '编辑悬赏' : '新增悬赏')}
+ open={props.open}
+ onOk={handleOk}
+ footer={props.readOnly ? null : undefined}
+ onCancel={handleCancel}
+ >
+ <Form
+ form={form}
+ onFinish={handleFinish}
+ initialValues={props.values}
+ labelCol={{ span: 6 }}
+ wrapperCol={{ span: 18 }}
+ >
+ <Form.Item name="rewardId" hidden={true}>
+ <Input />
+ </Form.Item>
+ <Form.Item
+ name="title"
+ label="悬赏标题"
+ rules={[{ required: true, message: '请输入悬赏标题!' }]}
+ >
+ <Input placeholder="请输入悬赏标题" disabled={props.readOnly} />
+ </Form.Item>
+ <Form.Item
+ name="description"
+ label="悬赏描述"
+ rules={[{ required: true, message: '请输入悬赏描述!' }]}
+ >
+ <Input.TextArea rows={4} placeholder="请输入悬赏描述" disabled={props.readOnly} />
+ </Form.Item>
+ <Form.Item
+ name="amount"
+ label="悬赏金额"
+ rules={[{ required: true, message: '请输入悬赏金额!' }]}
+ >
+ <InputNumber
+ style={{ width: '100%' }}
+ min={0}
+ precision={2}
+ placeholder="请输入悬赏金额"
+ disabled={props.readOnly}
+ />
+ </Form.Item>
+ {/* <Form.Item
+ name="status"
+ label="悬赏状态"
+ >
+ <Select
+ placeholder="请选择悬赏状态"
+ options={statusOptions.map((item: any) => ({
+ value: item.dictValue,
+ label: item.dictLabel,
+ }))}
+ />
+ </Form.Item> */}
+ <Form.Item
+ name="remark"
+ label="备注"
+ >
+ <Input.TextArea rows={2} placeholder="请输入备注" disabled={props.readOnly} />
+ </Form.Item>
+ </Form>
+ </Modal>
+ );
+};
+
+export default UpdateForm;
\ No newline at end of file
diff --git a/react-ui/src/pages/Reward/data.d.ts b/react-ui/src/pages/Reward/data.d.ts
new file mode 100644
index 0000000..6dbec7b
--- /dev/null
+++ b/react-ui/src/pages/Reward/data.d.ts
@@ -0,0 +1,38 @@
+export interface RewardItem {
+ /** 悬赏ID */
+ rewardId?: number;
+ /** 悬赏标题 */
+ title: string;
+ /** 悬赏描述 */
+ description: string;
+ /** 悬赏金额 */
+ amount: number;
+ /** 悬赏状态(0进行中 1已完成 2已取消) */
+ status?: string;
+ /** 发布者ID */
+ publisherId?: number;
+ /** 接受者ID */
+ accepterId?: number;
+ /** 创建时间 */
+ createTime?: string;
+ /** 更新时间 */
+ updateTime?: string;
+ /** 备注 */
+ remark?: string;
+}
+
+export interface RewardListParams {
+ /** 当前的页码 */
+ pageNum?: number;
+ /** 页面的容量 */
+ pageSize?: number;
+ /** 悬赏标题 */
+ title?: string;
+ /** 悬赏状态 */
+ status?: string;
+ /** 开始时间 */
+ 'params[beginTime]'?: string;
+ /** 结束时间 */
+ 'params[endTime]'?: string;
+}
+
diff --git a/react-ui/src/pages/Reward/index.tsx b/react-ui/src/pages/Reward/index.tsx
new file mode 100644
index 0000000..231415c
--- /dev/null
+++ b/react-ui/src/pages/Reward/index.tsx
@@ -0,0 +1,338 @@
+import { ExclamationCircleOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
+import { Button, message, Modal, Switch } from 'antd';
+import React, { useRef, useState, useEffect } from 'react';
+import { FormattedMessage, useIntl } from 'umi';
+import { FooterToolbar, PageContainer } from '@ant-design/pro-layout';
+import type { ActionType, ProColumns } from '@ant-design/pro-table';
+import ProTable from '@ant-design/pro-table';
+import type { FormInstance } from 'antd';
+import { getRewardList, removeReward, updateReward, addReward } from './service';
+import UpdateForm from './components/UpdateForm';
+import { getDictValueEnum } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+import { useAccess } from 'umi';
+import { RewardItem, RewardListParams } from './data';
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: RewardItem[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ await removeReward(selectedRows.map((row) => row.rewardId).join(','));
+ hide();
+ message.success('删除成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleUpdate = async (fields: RewardItem) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateReward(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+const handleAdd = async (fields: RewardItem) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addReward(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+const RewardTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+ const [readOnly, setReadOnly] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<RewardItem>();
+ const [selectedRows, setSelectedRows] = useState<RewardItem[]>([]);
+
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('reward_status').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<RewardItem>[] = [
+ {
+ title: '悬赏ID',
+ dataIndex: 'rewardId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: '悬赏标题',
+ dataIndex: 'title',
+ valueType: 'text',
+ },
+ {
+ title: '悬赏金额',
+ dataIndex: 'amount',
+ valueType: 'money',
+ hideInSearch: true,
+ },
+ // {
+ // title: '悬赏状态',
+ // dataIndex: 'status',
+ // valueType: 'select',
+ // valueEnum: statusOptions,
+ // render: (_, record) => {
+ // return (<DictTag enums={statusOptions} value={record.status} />);
+ // },
+ // },
+ {
+ title: '发布时间',
+ dataIndex: 'createTime',
+ valueType: 'dateRange',
+ render: (_, record) => {
+ return (<span>{record.createTime?.toString()}</span>);
+ },
+ search: {
+ transform: (value) => {
+ return {
+ 'params[beginTime]': value[0],
+ 'params[endTime]': value[1],
+ };
+ },
+ },
+ },
+ {
+ title: '备注',
+ dataIndex: 'remark',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '220px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="view"
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ // 设置只读模式
+ setReadOnly(true);
+ }}
+ >
+ 查看
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('reward:reward:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ setReadOnly(false);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ hidden={!access.hasPerms('reward:reward:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove([record]);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<RewardItem>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="rewardId"
+ key="rewardList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('reward:reward:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('reward:reward:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getRewardList({ ...params } as RewardListParams).then((res) => {
+ return {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('reward:reward:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ readOnly={readOnly}
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.rewardId) {
+ success = await handleUpdate({ ...values } as RewardItem);
+ } else {
+ success = await handleAdd({ ...values } as RewardItem);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ setReadOnly(false);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ statusOptions={statusOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default RewardTableList;
\ No newline at end of file
diff --git a/react-ui/src/pages/Reward/service.ts b/react-ui/src/pages/Reward/service.ts
new file mode 100644
index 0000000..7550cf9
--- /dev/null
+++ b/react-ui/src/pages/Reward/service.ts
@@ -0,0 +1,49 @@
+import { request } from '@umijs/max';
+import type {
+ RewardItem,
+ RewardListParams,
+} from '@/pages/Reward/data'; // 假设你把 data.d.ts 放这里
+
+/** 获取悬赏任务列表 */
+export async function getRewardList(params?: RewardListParams) {
+ const queryString = params
+ ? `?${new URLSearchParams(params as Record<string, any>).toString()}`
+ : '';
+ const response = await request(`/api/reward/list${queryString}`, {
+ method: 'get',
+ });
+ if (!response || response.length === 0) {
+ return [{ id: 1, name: '虚假任务1', description: '这是一个虚假的任务描述' }, { id: 2, name: '虚假任务2', description: '这是另一个虚假的任务描述' }];
+ }
+ return response;
+}
+
+/** 获取悬赏任务详细信息 */
+export async function getReward(rewardId: number) {
+ return request(`/api/reward/${rewardId}`, {
+ method: 'get',
+ });
+}
+
+/** 新增悬赏任务 */
+export async function addReward(params: RewardItem) {
+ return request('/api/reward', {
+ method: 'post',
+ data: params,
+ });
+}
+
+/** 修改悬赏任务 */
+export async function updateReward(params: RewardItem) {
+ return request('/api/reward', {
+ method: 'put',
+ data: params,
+ });
+}
+
+/** 删除悬赏任务 */
+export async function removeReward(ids: string) {
+ return request(`/api/reward/${ids}`, {
+ method: 'delete',
+ });
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/Torrent/Comments/data.d.ts b/react-ui/src/pages/Torrent/Comments/data.d.ts
new file mode 100644
index 0000000..bc9e517
--- /dev/null
+++ b/react-ui/src/pages/Torrent/Comments/data.d.ts
@@ -0,0 +1,21 @@
+/** 种子评论表 */
+export interface SysTorrentComment {
+ userId: string;
+ /** 评论ID */
+ commentId: number;
+ /** 种子ID */
+ torrentId: number;
+ /** 评论用户ID */
+ userId: number;
+ /** 评论内容 */
+ content: string;
+ /** 创建时间 */
+ createTime: Date;
+ /** 父评论ID,用于回复 */
+ parentId: number;
+}
+
+export interface responseSysTorrentComment {
+ /** 评论ID */
+ data: SysTorrentComment[];
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/Torrent/Comments/index.tsx b/react-ui/src/pages/Torrent/Comments/index.tsx
new file mode 100644
index 0000000..05c5165
--- /dev/null
+++ b/react-ui/src/pages/Torrent/Comments/index.tsx
@@ -0,0 +1,314 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { Button, Card, Form, Input, List, message, Avatar, Spin, Empty } from 'antd';
+import { ArrowLeftOutlined, UserOutlined } from '@ant-design/icons';
+import { Layout } from 'antd';
+import { listComments, addComment } from './service';
+import { responseSysTorrentComment, SysTorrentComment } from './data';
+
+const { Content } = Layout;
+const { TextArea } = Input;
+
+interface CommentItem {
+ id: number;
+ name: string;
+ content: string;
+ createTime: string;
+ createBy: string;
+ torrentId: number;
+}
+
+const TorrentComments: React.FC = () => {
+ const { torrentId } = useParams<{ torrentId: string }>();
+ const navigate = useNavigate();
+ const [form] = Form.useForm();
+ const [comments, setComments] = useState<CommentItem[]>([]);
+ const [submitting, setSubmitting] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ // 格式化时间
+ const formatTime = (time: string | Date | null | undefined): string => {
+ if (!time) return '未知时间';
+
+ try {
+ const date = new Date(time);
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+
+ // 小于1分钟
+ if (diff < 60 * 1000) {
+ return '刚刚';
+ }
+ // 小于1小时
+ if (diff < 60 * 60 * 1000) {
+ return `${Math.floor(diff / (60 * 1000))}分钟前`;
+ }
+ // 小于24小时
+ if (diff < 24 * 60 * 60 * 1000) {
+ return `${Math.floor(diff / (60 * 60 * 1000))}小时前`;
+ }
+ // 小于7天
+ if (diff < 7 * 24 * 60 * 60 * 1000) {
+ return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`;
+ }
+
+ // 否则显示具体日期
+ return date.toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ } catch (error) {
+ return '未知时间';
+ }
+ };
+
+ // 获取用户显示名称
+ const getUserDisplayName = (userId: number | null | undefined): string => {
+ if (!userId) return '匿名用户';
+ return `用户${userId}`;
+ };
+
+ // 获取用户头像字符
+ const getAvatarChar = (userName: string): string => {
+ if (!userName || userName === '匿名用户') return '?';
+ // 如果是"用户123"格式,取数字的最后一位
+ const match = userName.match(/\d+$/);
+ if (match) {
+ return match[0].slice(-1);
+ }
+ return userName[0].toUpperCase();
+ };
+
+ // 获取评论列表
+ const fetchComments = useCallback(async () => {
+ if (!torrentId) return;
+
+ setLoading(true);
+ try {
+
+ const res = await listComments(Number(torrentId));
+ console.log('获取评论列表:', res);
+ if (res) {
+ const formattedComments: SysTorrentComment[] = res.data.map((comment: SysTorrentComment) => ({
+ id: comment.commentId ?? 0,
+ name: comment.userName ?? '匿名用户',
+ content: comment.content ?? '无内容',
+ createTime: comment.createTime || '',
+ createBy: getUserDisplayName(comment.userId),
+ torrentId: comment.torrentId ?? 0,
+ }));
+
+ // 按时间倒序排列,最新的在前面
+ formattedComments.sort((a, b) => {
+ const timeA = a.createTime ? new Date(a.createTime).getTime() : 0;
+ const timeB = b.createTime ? new Date(b.createTime).getTime() : 0;
+ return timeB - timeA;
+ });
+
+ setComments(formattedComments);
+ } else {
+ message.error(res?.msg || '获取评论列表失败');
+ setComments([]);
+ }
+ } catch (error) {
+ console.error('获取评论失败:', error);
+ message.error('获取评论失败,请稍后重试');
+ setComments([]);
+ } finally {
+ setLoading(false);
+ }
+ }, [torrentId]);
+
+ useEffect(() => {
+ if (torrentId) {
+ fetchComments();
+ }
+ }, [torrentId, fetchComments]);
+
+ // 提交评论
+ const handleSubmit = async () => {
+ try {
+ const values = await form.validateFields();
+ setSubmitting(true);
+
+ // 调用添加评论的API
+ const response = await addComment({
+ torrentId: Number(torrentId),
+ content: values.content.trim(),
+ });
+
+ if (response?.code === 200) {
+ message.success('评论成功');
+ form.resetFields();
+ // 刷新评论列表
+ await fetchComments();
+ } else {
+ message.error(response?.msg || '评论失败,请稍后重试');
+ }
+ } catch (error) {
+ console.error('评论失败:', error);
+ message.error('评论失败,请稍后重试');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ // 处理按下 Ctrl+Enter 提交
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.ctrlKey && e.key === 'Enter' && !submitting) {
+ handleSubmit();
+ }
+ };
+
+ return (
+ <Content style={{
+ height: '100vh',
+ display: 'flex',
+ flexDirection: 'column',
+ overflow: 'hidden',
+ backgroundColor: '#f5f5f5'
+ }}>
+ {/* 顶部标题栏 */}
+ <div style={{
+ padding: '16px',
+ borderBottom: '1px solid #f0f0f0',
+ display: 'flex',
+ alignItems: 'center',
+ backgroundColor: '#fff',
+ zIndex: 10,
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)'
+ }}>
+ <Button
+ type="link"
+ icon={<ArrowLeftOutlined />}
+ onClick={() => navigate(-1)}
+ style={{ marginRight: '10px', padding: 0 }}
+ />
+ <span style={{ fontSize: '16px', fontWeight: 'bold' }}>
+ 种子评论 {comments.length > 0 && `(${comments.length})`}
+ </span>
+ </div>
+
+ {/* 评论列表区域 - 可滚动 */}
+ <div style={{
+ flex: 1,
+ overflowY: 'auto',
+ padding: '16px',
+ paddingBottom: '8px'
+ }}>
+ <Spin spinning={loading}>
+ {comments.length === 0 && !loading ? (
+ <Empty
+ description="暂无评论"
+ style={{ marginTop: '60px' }}
+ />
+ ) : (
+ <List
+ className="comment-list"
+ itemLayout="horizontal"
+ dataSource={comments}
+ renderItem={(item) => (
+ <Card
+ style={{
+ marginBottom: '12px',
+ borderRadius: '8px',
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.03)'
+ }}
+ bodyStyle={{ padding: '12px 16px' }}
+ >
+ <List.Item style={{ border: 'none', padding: 0 }}>
+ <List.Item.Meta
+ avatar={
+ <Avatar
+ style={{
+ backgroundColor: '#1890ff',
+ verticalAlign: 'middle'
+ }}
+ size="default"
+ icon={<UserOutlined />}
+ >
+ {getAvatarChar(item.createBy)}
+ </Avatar>
+ }
+ title={
+ <div style={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}>
+ <span style={{ fontWeight: 500 }}>{item.createBy}</span>
+ <span style={{
+ color: '#8c8c8c',
+ fontSize: '12px',
+ fontWeight: 'normal'
+ }}>
+ {formatTime(item.createTime)}
+ </span>
+ </div>
+ }
+ description={
+ <div style={{
+ marginTop: '8px',
+ color: '#262626',
+ fontSize: '14px',
+ lineHeight: '22px',
+ wordBreak: 'break-word'
+ }}>
+ {item.content}
+ </div>
+ }
+ />
+ </List.Item>
+ </Card>
+ )}
+ />
+ )}
+ </Spin>
+ </div>
+
+ {/* 评论输入框 - 固定在底部 */}
+ <div style={{
+ padding: '16px',
+ backgroundColor: '#fff',
+ borderTop: '1px solid #f0f0f0',
+ boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.06)'
+ }}>
+ <Form form={form} onFinish={handleSubmit}>
+ <Form.Item
+ name="content"
+ rules={[
+ { required: true, message: '请输入评论内容' },
+ { whitespace: true, message: '评论内容不能为空' },
+ { max: 500, message: '评论内容不能超过500个字符' }
+ ]}
+ style={{ marginBottom: '12px' }}
+ >
+ <TextArea
+ rows={3}
+ placeholder="请输入您的评论(Ctrl+Enter 快速提交)"
+ maxLength={500}
+ showCount
+ onKeyDown={handleKeyDown}
+ disabled={submitting}
+ />
+ </Form.Item>
+ <Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
+ <Button
+ htmlType="submit"
+ loading={submitting}
+ type="primary"
+ disabled={loading}
+ >
+ 提交评论
+ </Button>
+ </Form.Item>
+ </Form>
+ </div>
+ </Content>
+ );
+};
+
+export default TorrentComments;
\ No newline at end of file
diff --git a/react-ui/src/pages/Torrent/Comments/service.ts b/react-ui/src/pages/Torrent/Comments/service.ts
new file mode 100644
index 0000000..1ea221a
--- /dev/null
+++ b/react-ui/src/pages/Torrent/Comments/service.ts
@@ -0,0 +1,29 @@
+import { request } from '@umijs/max';
+import type { SysTorrentComment } from '@/pages/Torrent/Comments/data';
+
+/** 获取种子评论列表 */
+export async function listComments(torrentId: number) {
+ return request<SysTorrentComment[]>(`/api/system/torrent/comment/${torrentId}`, {
+ method: 'get',
+ });
+}
+
+/** 新增评论 */
+export async function addComment(data: {
+ torrentId: number;
+ userId: number;
+ content: string;
+ parentId?: number;
+}) {
+ return request(`/api/system/torrent/comment`, {
+ method: 'post',
+ data,
+ });
+}
+
+/** 删除评论 */
+export async function deleteComment(commentId: number) {
+ return request(`/api/system/torrent/comment/${commentId}`, {
+ method: 'delete',
+ });
+}
diff --git a/react-ui/src/pages/Torrent/index.tsx b/react-ui/src/pages/Torrent/index.tsx
index bb058ae..30b7ef3 100644
--- a/react-ui/src/pages/Torrent/index.tsx
+++ b/react-ui/src/pages/Torrent/index.tsx
@@ -1,5 +1,5 @@
-import React, { useRef, useState } from 'react';
-import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UploadOutlined } from '@ant-design/icons';
+import React, { useRef, useState, useEffect } from 'react';
+import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UploadOutlined, DownloadOutlined, CommentOutlined } from '@ant-design/icons';
import {
Button,
Modal,
@@ -13,21 +13,29 @@
Layout,
Upload,
UploadProps,
+ Select,
+ Tag, // 导入Tag组件用于显示标签
} from 'antd';
import { ProTable, ActionType, ProColumns, ProDescriptions, ProDescriptionsItemProps } from '@ant-design/pro-components';
-import type { BtTorrent } from './data';
+import { useNavigate } from 'react-router-dom';
+import type { BtTorrent, BtTorrentTag } from './data';
import {
listBtTorrent,
getBtTorrent,
addBtTorrent,
updateBtTorrent,
removeBtTorrent,
- uploadTorrent, // Function to handle torrent upload
+ uploadTorrent,
+ downloadTorrent,
+ addBtTorrentTag,
+ listBtTorrentTags,
+ getBtTorrentTag
} from './service';
const { Content } = Layout;
const BtTorrentPage: React.FC = () => {
+ const navigate = useNavigate();
const actionRef = useRef<ActionType>();
const [form] = Form.useForm();
const [modalVisible, setModalVisible] = useState(false);
@@ -35,19 +43,38 @@
const [current, setCurrent] = useState<Partial<BtTorrent>>({});
const [uploadModalVisible, setUploadModalVisible] = useState(false); // State for upload modal
const [uploadFile, setUploadFile] = useState<File | null>(null); // State to store selected file
+ const [uploadForm] = Form.useForm(); // Form for upload modal
+ const [torrentTags, setTorrentTags] = useState<BtTorrentTag[]>([]); // 修改为数组类型来存储多个标签
// Columns for the ProTable (the table displaying torrents)
const columns: ProColumns<BtTorrent>[] = [
{
title: '种子ID',
dataIndex: 'torrentId',
- hideInForm: true,
render: (dom, entity) => (
<a
onClick={async () => {
- const res = await getBtTorrent(entity.torrentId!);
- setCurrent(res);
- setDrawerVisible(true);
+ try {
+ // 获取详细信息
+ const res = await getBtTorrent(entity.torrentId!);
+ console.log('获取的种子详情:', res); // 调试用
+
+ // 确保res是对象类型并且包含数据
+ if (res && typeof res === 'object') {
+ // 如果API返回了data包装,则提取data
+ const torrentData = res.data ? res.data : res;
+ setCurrent(torrentData);
+
+ // 先设置当前种子,然后获取标签
+ await handleGetTags(entity.torrentId!);
+ setDrawerVisible(true);
+ } else {
+ message.error('获取种子详情格式错误');
+ }
+ } catch (error) {
+ console.error('获取种子详情出错:', error);
+ message.error('获取种子详情失败');
+ }
}}
>
{dom}
@@ -55,36 +82,56 @@
),
},
{ title: '名称', dataIndex: 'name' },
- { title: 'infoHash', dataIndex: 'infoHash' },
+ // { title: 'infoHash', dataIndex: 'infoHash' },
{ title: '大小 (bytes)', dataIndex: 'length', valueType: 'digit' },
- { title: '分片大小', dataIndex: 'pieceLength', valueType: 'digit' },
- { title: '片段数', dataIndex: 'piecesCount', valueType: 'digit' },
- { title: '创建工具', dataIndex: 'createdBy', hideInSearch: true },
+ // { title: '分片大小', dataIndex: 'pieceLength', valueType: 'digit' },
+ // { title: '片段数', dataIndex: 'piecesCount', valueType: 'digit' },
+ { title: '创建人', dataIndex: 'createdBy', hideInSearch: true },
{ title: '上传时间', dataIndex: 'uploadTime', valueType: 'dateTime', hideInSearch: true },
{
title: '操作',
valueType: 'option',
render: (_, record) => [
- <Button key="view" type="link" icon={<EyeOutlined />} onClick={() => {
- setCurrent(record);
- setDrawerVisible(true);
+ <Button key="view" type="link" icon={<EyeOutlined />} onClick={async () => {
+ try {
+ // 获取详细信息
+ const res = await getBtTorrent(record.torrentId!);
+ console.log('获取的种子详情:', res); // 调试用
+
+ // 确保res是对象类型并且包含数据
+ if (res && typeof res === 'object') {
+ // 如果API返回了data包装,则提取data
+ const torrentData = res.data ? res.data : res;
+ setCurrent(torrentData);
+
+ // 获取标签
+ await handleGetTags(record.torrentId!);
+ setDrawerVisible(true);
+ } else {
+ message.error('获取种子详情格式错误');
+ }
+ } catch (error) {
+ console.error('获取种子详情出错:', error);
+ message.error('获取详情失败');
+ }
}}>查看</Button>,
- <Button key="edit" type="link" icon={<EditOutlined />} onClick={() => {
- setCurrent(record);
- form.setFieldsValue(record);
- setModalVisible(true);
- }}>编辑</Button>,
- <Button key="delete" type="link" icon={<DeleteOutlined />} danger onClick={() => {
- Modal.confirm({
- title: '删除确认',
- content: '确定删除该种子?',
- onOk: async () => {
- await removeBtTorrent([record.torrentId!]);
- message.success('删除成功');
- actionRef.current?.reload();
- },
- });
- }}>删除</Button>,
+ <Button key="download" type="link" icon={<DownloadOutlined />} onClick={async () => {
+ try {
+ const blob = await downloadTorrent(record.torrentId!);
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `${record.name}.torrent`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ message.success('下载成功');
+ } catch (error: any) {
+ message.error('下载失败');
+ }
+ }}>下载</Button>,
+
],
},
];
@@ -115,16 +162,19 @@
throw new Error('请选择一个文件');
}
- // Call the uploadTorrent function to upload the file
+ const values = await uploadForm.validateFields();
+ console.log(file);
+ // Call the uploadTorrent function to upload the file with additional info
await uploadTorrent(file);
// Show a success message
message.success('文件上传成功');
- // Close the upload modal
+ // Close the upload modal and reset form
setUploadModalVisible(false);
+ uploadForm.resetFields();
- // Optionally reload the table or perform other actions (e.g., refresh list)
+ // Reload the table
actionRef.current?.reload();
} catch (error) {
@@ -132,6 +182,64 @@
}
};
+ // 修改获取标签的函数,处理API特定的响应格式
+ const handleGetTags = async (id: number) => {
+ try {
+ // 根据API的响应格式,获取rows数组中的标签
+ const response = await listBtTorrentTags({ torrentId: id });
+ console.log('API标签响应:', response);
+
+ // 检查响应格式并提取rows数组
+ if (response && response.rows && Array.isArray(response.rows)) {
+ setTorrentTags(response.rows);
+ console.log('设置标签:', response.rows);
+ } else {
+ console.log('未找到标签或格式不符');
+ setTorrentTags([]);
+ }
+ } catch (error) {
+ console.error('获取标签失败:', error);
+ message.error('获取标签失败');
+ setTorrentTags([]);
+ }
+ };
+
+ useEffect(() => {
+ if (current?.torrentId) {
+ handleGetTags(current.torrentId);
+ } else {
+ setTorrentTags([]); // 清空标签当没有选中种子时
+ }
+ }, [current]);
+
+ // 渲染标签列表的函数
+ const renderTags = () => {
+ if (!torrentTags || torrentTags.length === 0) {
+ return <span style={{ color: '#999' }}>暂无标签</span>;
+ }
+
+ // 定义一些可能的标签颜色
+ const tagColors = ['blue', 'green', 'cyan', 'purple', 'magenta', 'orange', 'gold', 'lime'];
+
+ return (
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
+ {torrentTags.map((tag, index) => {
+ // 根据索引轮换颜色
+ const colorIndex = index % tagColors.length;
+ return (
+ <Tag
+ key={tag.id || tag.torrentId || index}
+ color={tagColors[colorIndex]}
+ style={{ margin: '0 4px 4px 0', padding: '2px 8px' }}
+ >
+ {tag.tag}
+ </Tag>
+ );
+ })}
+ </div>
+ );
+ };
+
return (
<Content>
<Card bordered={false}>
@@ -142,22 +250,10 @@
search={{ labelWidth: 100 }}
toolBarRender={() => [
<Button
- key="add"
- type="primary"
- icon={<PlusOutlined />}
- onClick={() => {
- form.resetFields();
- setCurrent({});
- setModalVisible(true);
- }}
- >
- 新增
- </Button>,
- <Button
key="upload"
type="primary"
icon={<UploadOutlined />}
- onClick={() => setUploadModalVisible(true)} // Show the upload modal
+ onClick={() => setUploadModalVisible(true)}
>
上传种子文件
</Button>
@@ -202,37 +298,155 @@
{/* 上传种子文件的Modal */}
<Modal
title="上传种子文件"
- visible={uploadModalVisible}
- onCancel={() => setUploadModalVisible(false)}
- footer={null}
+ open={uploadModalVisible}
+ onCancel={() => {
+ setUploadModalVisible(false);
+ uploadForm.resetFields();
+ }}
+ onOk={() => {
+ if (uploadFile) {
+ handleFileUpload(uploadFile);
+ } else {
+ message.error('请选择文件');
+ }
+ }}
>
- <Upload
- customRequest={({ file, onSuccess, onError }) => {
- setUploadFile(file);
- handleFileUpload(file);
- onSuccess?.();
- }}
- showUploadList={false}
- accept=".torrent"
- >
- <Button icon={<UploadOutlined />}>点击上传 .torrent 文件</Button>
- </Upload>
+ <Form form={uploadForm} layout="vertical">
+ <Form.Item
+ name="file"
+ label="种子文件"
+ rules={[{ required: true, message: '请选择种子文件' }]}
+ >
+ <Upload
+ customRequest={({ file, onSuccess }) => {
+ setUploadFile(file as File);
+ onSuccess?.();
+ }}
+ showUploadList={true}
+ maxCount={1}
+ accept=".torrent"
+ onRemove={() => setUploadFile(null)}
+ >
+ <Button icon={<UploadOutlined />}>选择 .torrent 文件</Button>
+ </Upload>
+ </Form.Item>
+ <Form.Item
+ name="description"
+ label="介绍"
+ rules={[{ required: true, message: '请输入种子介绍' }]}
+ >
+ <Input.TextArea rows={4} placeholder="请输入种子文件的详细介绍" />
+ </Form.Item>
+ <Form.Item
+ name="tags"
+ label="标签"
+ rules={[{ required: true, message: '请输入标签' }]}
+ >
+ <Select
+ mode="tags"
+ style={{ width: '100%' }}
+ placeholder="请输入标签,按回车键确认"
+ tokenSeparators={[',']}
+ />
+ </Form.Item>
+ </Form>
</Modal>
{/* 详情抽屉 */}
<Drawer
- width={500}
+ width={600}
open={drawerVisible}
onClose={() => setDrawerVisible(false)}
title="种子详情"
+ extra={
+ <Button
+ type="primary"
+ icon={<CommentOutlined />}
+ onClick={() => navigate(`/torrent/comments/${current.torrentId}`)}
+ >
+ 查看评论
+ </Button>
+ }
>
{current && (
- <ProDescriptions<BtTorrent>
- column={1}
- title={current.name}
- request={async () => ({ data: current })}
- columns={columns as ProDescriptionsItemProps<BtTorrent>[]}
- />
+ <>
+ {/* 不要使用request属性,直接使用dataSource */}
+ <ProDescriptions<BtTorrent>
+ column={1}
+ title={current.name}
+ dataSource={current}
+ columns={[
+ {
+ title: '种子ID',
+ dataIndex: 'torrentId',
+ valueType: 'text'
+ },
+ {
+ title: '名称',
+ dataIndex: 'name',
+ valueType: 'text'
+ },
+ {
+ title: 'infoHash',
+ dataIndex: 'infoHash',
+ valueType: 'text',
+ copyable: true
+ },
+ {
+ title: '大小 (bytes)',
+ dataIndex: 'length',
+ valueType: 'digit',
+
+ },
+ {
+ title: '分片大小',
+ dataIndex: 'pieceLength',
+ valueType: 'digit'
+ },
+ {
+ title: '片段数',
+ dataIndex: 'piecesCount',
+ valueType: 'digit'
+ },
+ {
+ title: '创建人',
+ dataIndex: 'createdBy',
+ valueType: 'text'
+ },
+ {
+ title: '上传时间',
+ dataIndex: 'uploadTime',
+ valueType: 'dateTime'
+ },
+ {
+ title: '标签',
+ dataIndex: 'tags',
+ render: () => renderTags()
+ }
+ ] as ProDescriptionsItemProps<BtTorrent>[]}
+ />
+
+ {/* 如果需要显示额外信息,可以在这里添加 */}
+ {current.description && (
+ <div style={{ marginTop: 16 }}>
+ <h3>介绍</h3>
+ <p>{current.description}</p>
+ </div>
+ )}
+
+ {/* 添加调试信息 - 开发时使用,生产环境可以移除 */}
+ {/* <div style={{ marginTop: 20, background: '#f5f5f5', padding: 10, borderRadius: 4 }}>
+ <h4>调试信息:</h4>
+ <p>当前种子ID: {current.torrentId}</p>
+ <p>标签数量: {torrentTags?.length || 0}</p>
+ <details>
+ <summary>查看完整数据</summary>
+ <pre style={{ maxHeight: 200, overflow: 'auto' }}>{JSON.stringify(current, null, 2)}</pre>
+ <h5>标签数据:</h5>
+ <pre style={{ maxHeight: 200, overflow: 'auto' }}>{JSON.stringify(torrentTags, null, 2)}</pre>
+ </details>
+ </div> */}
+ </>
)}
</Drawer>
</Card>
@@ -240,4 +454,4 @@
);
};
-export default BtTorrentPage;
+export default BtTorrentPage;
\ No newline at end of file
diff --git a/react-ui/src/pages/Torrent/service.ts b/react-ui/src/pages/Torrent/service.ts
index 8e15618..e526151 100644
--- a/react-ui/src/pages/Torrent/service.ts
+++ b/react-ui/src/pages/Torrent/service.ts
@@ -6,6 +6,14 @@
BtTorrentTag,
} from '@/pages/Torrent/data'; // 假设你把 data.d.ts 放这里
+/** 下载种子文件 */
+export async function downloadTorrent(torrentId: number) {
+ return request(`/api/system/torrent/download/${torrentId}`, {
+ method: 'get',
+ responseType: 'blob',
+ });
+}
+
// ================================
// 种子主表(bt_torrent)接口
// ================================
@@ -62,6 +70,7 @@
/** 获取文件详情 */
export async function getBtTorrentFile(id: number) {
+
return request<BtTorrentFile>(`/api/system/file/${id}`, {
method: 'get',
});
diff --git a/react-ui/src/pages/User/Center/index.tsx b/react-ui/src/pages/User/Center/index.tsx
index 2ce308c..7272fa3 100644
--- a/react-ui/src/pages/User/Center/index.tsx
+++ b/react-ui/src/pages/User/Center/index.tsx
@@ -14,6 +14,7 @@
import AvatarCropper from './components/AvatarCropper';
import { useRequest } from '@umijs/max';
import { getUserInfo } from '@/services/session';
+import { getUserRateInfo } from '@/services/system/user';
import { PageLoading } from '@ant-design/pro-components';
const operationTabList = [
@@ -38,20 +39,25 @@
export type tabKeyType = 'base' | 'password';
const Center: React.FC = () => {
-
+
const [tabKey, setTabKey] = useState<tabKeyType>('base');
-
+
const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false);
-
+
// 获取用户信息
const { data: userInfo, loading } = useRequest(async () => {
- return { data: await getUserInfo()};
+ return { data: await getUserInfo() };
+ });
+
+ const { data: userRateInfo } = useRequest(async () => {
+ return { data: await getUserRateInfo() };
});
if (loading) {
return <div>loading...</div>;
}
- const currentUser = userInfo?.user;
+ const currentUser = { ...userInfo?.user, ...userRateInfo?.data };
+
// 渲染用户信息
const renderUserInfo = ({
@@ -60,6 +66,10 @@
email,
sex,
dept,
+ uploadCount,
+ downloadCount,
+ rateCount,
+ score,
}: Partial<API.CurrentUser>) => {
return (
<List>
@@ -109,6 +119,39 @@
</List.Item>
<List.Item>
<div>
+ <MailOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 上传量
+ </div>
+ <div>{uploadCount}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <MailOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 下载量
+ </div>
+ <div>{downloadCount}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <MailOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 分享率
+ </div>
+ <div>{rateCount}</div>
+ </List.Item>
+ {/* <List.Item>
+ <div>
<ClusterOutlined
style={{
marginRight: 8,
@@ -117,6 +160,17 @@
部门
</div>
<div>{dept?.deptName}</div>
+ </List.Item> */}
+ <List.Item>
+ <div>
+ <MailOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 积分
+ </div>
+ <div>{score}</div>
</List.Item>
</List>
);
@@ -147,13 +201,13 @@
loading={loading}
>
{!loading && (
- <div style={{ textAlign: "center"}}>
- <div className={styles.avatarHolder} onClick={()=>{setCropperModalOpen(true)}}>
+ <div style={{ textAlign: "center" }}>
+ <div className={styles.avatarHolder} onClick={() => { setCropperModalOpen(true) }}>
<img alt="" src={currentUser.avatar} />
</div>
{renderUserInfo(currentUser)}
<Divider dashed />
- <div className={styles.team}>
+ {/* <div className={styles.team}>
<div className={styles.teamTitle}>角色</div>
<Row gutter={36}>
{currentUser.roles &&
@@ -168,7 +222,7 @@
</Col>
))}
</Row>
- </div>
+ </div> */}
</div>
)}
</Card>
@@ -188,7 +242,7 @@
</Row>
<AvatarCropper
onFinished={() => {
- setCropperModalOpen(false);
+ setCropperModalOpen(false);
}}
open={cropperModalOpen}
data={currentUser.avatar}
diff --git a/react-ui/src/services/system/user.ts b/react-ui/src/services/system/user.ts
index da997bf..9f45f12 100644
--- a/react-ui/src/services/system/user.ts
+++ b/react-ui/src/services/system/user.ts
@@ -79,6 +79,14 @@
})
}
+// 查询用户上传,下载,分享率等信息
+export function getUserRateInfo() {
+ return request('/api/system/user/profile/info', {
+ method: 'get'
+ })
+}
+
+
export function updateUserProfile(data: API.CurrentUser) {
return request<API.Result>('/api/system/user/profile', {
method: 'put',
diff --git a/react-ui/src/services/typings.d.ts b/react-ui/src/services/typings.d.ts
index 1e0b8b5..20f44dc 100644
--- a/react-ui/src/services/typings.d.ts
+++ b/react-ui/src/services/typings.d.ts
@@ -57,6 +57,10 @@
dept?: Dept;
roles?: Role[];
permissions: string[];
+ uploadCount?: number;
+ downloadCount?: number;
+ rateCount?: number;
+ score?: number;
}
interface UserInfoVO {
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/announce/controller/AnnounceController.java b/ruoyi-admin/src/main/java/com/ruoyi/announce/controller/AnnounceController.java
index 8fbb688..f3bd624 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/announce/controller/AnnounceController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/announce/controller/AnnounceController.java
@@ -1,10 +1,7 @@
package com.ruoyi.announce.controller;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.announce.service.IAnnounceService;
-import org.apache.commons.codec.DecoderException;
-import org.apache.commons.codec.net.URLCodec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -13,116 +10,52 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import java.io.ByteArrayOutputStream;
-import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
-import java.util.Base64;
import java.util.Map;
-import java.util.stream.Collectors;
@RestController
public class AnnounceController extends BaseController {
- // 在你的 Controller 中:
@Autowired
private IAnnounceService announceService;
-
- private byte[] decodeInfoHashAndPeerId(String param) {
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- for (int i = 0; i < param.length(); ) {
- char c = param.charAt(i);
- if (c == '%' && i + 2 < param.length()) {
- char hi = param.charAt(i + 1);
- char lo = param.charAt(i + 2);
- if (isHexDigit(hi) && isHexDigit(lo)) {
- int value = Character.digit(hi, 16) << 4 | Character.digit(lo, 16);
- bos.write(value);
- i += 3;
- continue;
- }
- }
- // 不是合法的 %xx,直接跳过或处理为异常字符
- bos.write((byte) c); // 或者 throw new IllegalArgumentException(...)
- i++;
- }
- return bos.toByteArray();
- }
-
- private boolean isHexDigit(char c) {
- return (c >= '0' && c <= '9') ||
- (c >= 'A' && c <= 'F') ||
- (c >= 'a' && c <= 'f');
- }
-
-
/**
* BT Tracker /announce 接口
* 接收客户端(qBittorrent 等)发来的 announce 请求,返回 bencoded peers 列表
*/
-
@GetMapping(value = "/announce", produces = "application/x-bittorrent")
- public void announce(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ public void announce(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ @RequestParam("info_hash") String infoHashParam,
+ @RequestParam("peer_id") String peerIdParam,
+ @RequestParam("port") int port,
+ @RequestParam("uploaded") long uploaded,
+ @RequestParam("downloaded") long downloaded,
+ @RequestParam("left") long left,
+ @RequestParam(value = "event", required = false) String event,
+ @RequestParam("passkey") String passkey
+ ) throws Exception {
+ // 1. URL Decode 得到原始二进制
+ byte[] infoHash = URLDecoder.decode(infoHashParam, StandardCharsets.ISO_8859_1.name())
+ .getBytes(StandardCharsets.ISO_8859_1);
+ byte[] peerId = URLDecoder.decode(peerIdParam, StandardCharsets.ISO_8859_1.name())
+ .getBytes(StandardCharsets.ISO_8859_1);
- // —— 4. 获取参数(不使用 @RequestParam) ——
- String infoHashParam = request.getParameter("info_hash");
- String peerIdParam = request.getParameter("peer_id");
- String portStr = request.getParameter("port");
- String uploadedStr = request.getParameter("uploaded");
- String downloadedStr = request.getParameter("downloaded");
- String leftStr = request.getParameter("left");
- String event = request.getParameter("event");
- String passkey = request.getParameter("passkey");
- // 打印接收到的参数
- System.out.println("Received announce request:");
- System.out.println(" info_hash: " + infoHashParam);
- System.out.println(" peer_id: " + peerIdParam);
- System.out.println(" port: " + portStr);
- System.out.println(" uploaded: " + uploadedStr);
- System.out.println(" downloaded:" + downloadedStr);
- System.out.println(" left: " + leftStr);
- System.out.println(" event: " + event);
- System.out.println(" passkey: " + passkey);
- System.out.println(" IP: " + request.getRemoteAddr());
- // —— 校验是否有必要参数为空 ——
- if (infoHashParam == null || peerIdParam == null || portStr == null ||
- uploadedStr == null || downloadedStr == null || leftStr == null) {
- System.out.println("参数缺失,终止处理。");
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- response.setContentType("text/plain");
- response.getWriter().write("Missing required announce parameters.");
- return;
- }
-
-
-
- // —— 5. 转换参数类型 ——
- int port = Integer.parseInt(portStr);
- long uploaded = Long.parseLong(uploadedStr);
- long downloaded = Long.parseLong(downloadedStr);
- long left = Long.parseLong(leftStr);
-
- // 确保 URL 解码不会失败,使用更健壮的方法
- byte[] infoHash = decodeInfoHashAndPeerId(infoHashParam);
- byte[] peerId = decodeInfoHashAndPeerId(peerIdParam);
-
- // —— 7. 调用业务逻辑处理 ——
+ // 2. 处理 announce 请求(验证 passkey,更新 peer 列表,获取 peers 信息)
Map<String, Object> reply = announceService.handleAnnounce(
infoHash, peerId, port, uploaded, downloaded, left, event, passkey,
request.getRemoteAddr()
);
- // —— 8. 返回 Bencode 编码的 tracker 响应 ——
+ // 3. bencode 编码并返回给客户端
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader("Content-Type", "application/x-bittorrent");
try (var out = response.getOutputStream()) {
byte[] bencoded = announceService.encodeBencode(reply);
- System.out.println(bencoded);
-
out.write(bencoded);
out.flush();
}
}
-
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/announce/service/impl/AnnounceServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/announce/service/impl/AnnounceServiceImpl.java
index 4408fa1..f80050f 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/announce/service/impl/AnnounceServiceImpl.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/announce/service/impl/AnnounceServiceImpl.java
@@ -6,7 +6,6 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
@@ -36,72 +35,46 @@
) throws Exception {
// 1. 转 hex 作为 Redis key 前缀
String infoHashHex = bytesToHex(infoHash);
- String peerIdStr = new String(peerId, StandardCharsets.ISO_8859_1);
+ String peerIdStr = new String(peerId, StandardCharsets.ISO_8859_1);
// 2. 校验 passkey(可根据业务到 MySQL 查 userId,此处略过)
- // 3. 在 Redis 中记录/刷新此 peer 的信息(TTL 120 秒)
+ // 3. 在 Redis 中记录/刷新此 peer 的信息
+ // 使用 Hash 存储详情,TTL 120 秒
String peerKey = "peer:" + infoHashHex + ":" + peerIdStr;
Map<String, Object> peerData = new HashMap<>();
- peerData.put("ip", ip);
- peerData.put("port", port);
- peerData.put("uploaded", uploaded);
+ peerData.put("ip", ip);
+ peerData.put("port", port);
+ peerData.put("uploaded", uploaded);
peerData.put("downloaded", downloaded);
- peerData.put("left", left);
- peerData.put("lastSeen", System.currentTimeMillis());
+ peerData.put("left", left);
+ peerData.put("lastSeen", System.currentTimeMillis());
redisCache.setCacheMap(peerKey, peerData);
- redisCache.expire(peerKey, 1200, TimeUnit.SECONDS);
+ redisCache.expire(peerKey, 120, TimeUnit.SECONDS);
- // 4. 收集 peers,根据自身状态区分返回哪些 peer
- boolean isSeeder = left == 0;
+ // 4. 从 Redis 中扫描所有同 info_hash 的 peer
Collection<String> keys = redisCache.keys("peer:" + infoHashHex + ":*");
List<byte[]> peersBin = new ArrayList<>(keys.size());
-
for (String key : keys) {
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) redisCache.getCacheMap(key);
- if (data == null) continue;
-
- String otherPeerId = key.substring(key.lastIndexOf(":") + 1);
- if (otherPeerId.equals(peerIdStr)) continue; // 忽略自己
-
String peerIp = (String) data.get("ip");
- int peerPort = ((Number) data.get("port")).intValue();
- long otherLeft = ((Number) data.get("left")).longValue();
-
- if (peerIp.contains(":")) continue; // 跳过 IPv6
-
- // 按条件过滤
- if (isSeeder && otherLeft > 0) {
- peersBin.add(encodePeer(peerIp, peerPort));
- } else if (!isSeeder) {
- peersBin.add(encodePeer(peerIp, peerPort));
- }
-
- if (peersBin.size() >= 50) break;
+ int peerPort = ((Number) data.get("port")).intValue();
+ peersBin.add(encodePeer(peerIp, peerPort));
+ if (peersBin.size() >= 50) break; // 最多返回 50 个 peers
}
- // 4.5 合并 peers 到 compact 格式
- ByteArrayOutputStream peerStream = new ByteArrayOutputStream();
- for (byte[] peer : peersBin) {
- if (peer != null && peer.length == 6) {
- peerStream.write(peer);
- }
- }
- byte[] compactPeers = peerStream.toByteArray();
-
- // 5. 构造返回 Map
+ // 5. 构造返回数据 Map(有序)
Map<String, Object> reply = new LinkedHashMap<>();
reply.put("interval", ANNOUNCE_INTERVAL);
reply.put("min interval", ANNOUNCE_INTERVAL / 2);
- reply.put("complete", countSeeders(infoHashHex));
+ reply.put("complete", countSeeders(infoHashHex));
reply.put("incomplete", countLeechers(infoHashHex));
- reply.put("peers", compactPeers);
+ reply.put("peers", peersBin);
return reply;
}
-
@Override
public byte[] encodeBencode(Map<String, Object> reply) throws IOException {
BencodeEncoder encoder = new BencodeEncoder();
@@ -134,22 +107,17 @@
return count;
}
+ /** 将 IPv4 + port 编码成 6 字节:4 字节 IP + 2 字节 port */
private byte[] encodePeer(String ip, int port) throws Exception {
- if (ip.contains(":")) return null; // 跳过 IPv6
-
String[] parts = ip.split("\\.");
- if (parts.length != 4) throw new IllegalArgumentException("无效的 IPv4 地址: " + ip);
-
ByteBuffer buf = ByteBuffer.allocate(6);
- for (String part : parts) {
- buf.put((byte) Integer.parseInt(part));
+ for (int i = 0; i < 4; i++) {
+ buf.put((byte) Integer.parseInt(parts[i]));
}
buf.putShort((short) port);
return buf.array();
}
-
-
/** 将字节数组转成十六进制字符串 */
private static final char[] HEX = "0123456789abcdef".toCharArray();
private String bytesToHex(byte[] bytes) {
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/announce/util/BencodeEncoder.java b/ruoyi-admin/src/main/java/com/ruoyi/announce/util/BencodeEncoder.java
index c937042..a2cdb30 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/announce/util/BencodeEncoder.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/announce/util/BencodeEncoder.java
@@ -2,7 +2,6 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
import java.util.Map;
public class BencodeEncoder {
@@ -33,20 +32,11 @@
encodeMap((Map<String, Object>) obj, outputStream);
} else if (obj instanceof Iterable) {
encodeList((Iterable<Object>) obj, outputStream);
- } else if (obj instanceof byte[]) {
- encodeByteArray((byte[]) obj, outputStream);
} else {
throw new IllegalArgumentException("Unsupported object type: " + obj.getClass());
}
}
- private void encodeByteArray(byte[] bytes, ByteArrayOutputStream outputStream) throws IOException {
- outputStream.write(Integer.toString(bytes.length).getBytes());
- outputStream.write(':');
- outputStream.write(bytes);
- }
-
-
private void encodeInteger(Integer value, ByteArrayOutputStream outputStream) throws IOException {
outputStream.write('i');
outputStream.write(value.toString().getBytes());
@@ -54,13 +44,11 @@
}
private void encodeString(String value, ByteArrayOutputStream outputStream) throws IOException {
- byte[] bytes = value.getBytes(StandardCharsets.UTF_8); // Assuming UTF-8 encoding
- outputStream.write(Integer.toString(bytes.length).getBytes());
+ outputStream.write(Integer.toString(value.length()).getBytes());
outputStream.write(':');
- outputStream.write(bytes);
+ outputStream.write(value.getBytes());
}
-
private void encodeList(Iterable<Object> list, ByteArrayOutputStream outputStream) throws IOException {
outputStream.write('l'); // Start of a list
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/reward/controller/RewardTasksController.java b/ruoyi-admin/src/main/java/com/ruoyi/reward/controller/RewardTasksController.java
new file mode 100644
index 0000000..bae499a
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/reward/controller/RewardTasksController.java
@@ -0,0 +1,104 @@
+package com.ruoyi.reward.controller;
+
+import java.util.List;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.reward.domain.RewardTasks;
+import com.ruoyi.reward.service.IRewardTasksService;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 【请填写功能名称】Controller
+ *
+ * @author ruoyi
+ * @date 2025-05-17
+ */
+@RestController
+@RequestMapping("/reward")
+public class RewardTasksController extends BaseController
+{
+ @Autowired
+ private IRewardTasksService rewardTasksService;
+
+ /**
+ * 查询【请填写功能名称】列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:tasks:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(RewardTasks rewardTasks)
+ {
+ startPage();
+ List<RewardTasks> list = rewardTasksService.selectRewardTasksList(rewardTasks);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出【请填写功能名称】列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:tasks:export')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, RewardTasks rewardTasks)
+ {
+ List<RewardTasks> list = rewardTasksService.selectRewardTasksList(rewardTasks);
+ ExcelUtil<RewardTasks> util = new ExcelUtil<RewardTasks>(RewardTasks.class);
+ util.exportExcel(response, list, "【请填写功能名称】数据");
+ }
+
+ /**
+ * 获取【请填写功能名称】详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('system:tasks:query')")
+ @GetMapping(value = "/{rewardId}")
+ public AjaxResult getInfo(@PathVariable("rewardId") Long rewardId)
+ {
+ return success(rewardTasksService.selectRewardTasksByRewardId(rewardId));
+ }
+
+ /**
+ * 新增【请填写功能名称】
+ */
+ @PreAuthorize("@ss.hasPermi('system:tasks:add')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody RewardTasks rewardTasks)
+ {
+ return toAjax(rewardTasksService.insertRewardTasks(rewardTasks));
+ }
+
+ /**
+ * 修改【请填写功能名称】
+ */
+ @PreAuthorize("@ss.hasPermi('system:tasks:edit')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody RewardTasks rewardTasks)
+ {
+ return toAjax(rewardTasksService.updateRewardTasks(rewardTasks));
+ }
+
+ /**
+ * 删除【请填写功能名称】
+ */
+ @PreAuthorize("@ss.hasPermi('system:tasks:remove')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{rewardIds}")
+ public AjaxResult remove(@PathVariable Long[] rewardIds)
+ {
+ return toAjax(rewardTasksService.deleteRewardTasksByRewardIds(rewardIds));
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/reward/domain/RewardTasks.java b/ruoyi-admin/src/main/java/com/ruoyi/reward/domain/RewardTasks.java
new file mode 100644
index 0000000..50e5c8f
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/reward/domain/RewardTasks.java
@@ -0,0 +1,125 @@
+package com.ruoyi.reward.domain;
+
+import java.math.BigDecimal;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 【请填写功能名称】对象 reward_tasks
+ *
+ * @author ruoyi
+ * @date 2025-05-17
+ */
+public class RewardTasks extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** $column.columnComment */
+ private Long rewardId;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private String title;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private String description;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private BigDecimal amount;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private String status;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private Long publisherId;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private Long accepterId;
+
+ public void setRewardId(Long rewardId)
+ {
+ this.rewardId = rewardId;
+ }
+
+ public Long getRewardId()
+ {
+ return rewardId;
+ }
+ public void setTitle(String title)
+ {
+ this.title = title;
+ }
+
+ public String getTitle()
+ {
+ return title;
+ }
+ public void setDescription(String description)
+ {
+ this.description = description;
+ }
+
+ public String getDescription()
+ {
+ return description;
+ }
+ public void setAmount(BigDecimal amount)
+ {
+ this.amount = amount;
+ }
+
+ public BigDecimal getAmount()
+ {
+ return amount;
+ }
+ public void setStatus(String status)
+ {
+ this.status = status;
+ }
+
+ public String getStatus()
+ {
+ return status;
+ }
+ public void setPublisherId(Long publisherId)
+ {
+ this.publisherId = publisherId;
+ }
+
+ public Long getPublisherId()
+ {
+ return publisherId;
+ }
+ public void setAccepterId(Long accepterId)
+ {
+ this.accepterId = accepterId;
+ }
+
+ public Long getAccepterId()
+ {
+ return accepterId;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("rewardId", getRewardId())
+ .append("title", getTitle())
+ .append("description", getDescription())
+ .append("amount", getAmount())
+ .append("status", getStatus())
+ .append("publisherId", getPublisherId())
+ .append("accepterId", getAccepterId())
+ .append("createTime", getCreateTime())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/reward/mapper/RewardTasksMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/reward/mapper/RewardTasksMapper.java
new file mode 100644
index 0000000..92520b8
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/reward/mapper/RewardTasksMapper.java
@@ -0,0 +1,61 @@
+package com.ruoyi.reward.mapper;
+
+import java.util.List;
+import com.ruoyi.reward.domain.RewardTasks;
+
+/**
+ * 【请填写功能名称】Mapper接口
+ *
+ * @author ruoyi
+ * @date 2025-05-17
+ */
+public interface RewardTasksMapper
+{
+ /**
+ * 查询【请填写功能名称】
+ *
+ * @param rewardId 【请填写功能名称】主键
+ * @return 【请填写功能名称】
+ */
+ public RewardTasks selectRewardTasksByRewardId(Long rewardId);
+
+ /**
+ * 查询【请填写功能名称】列表
+ *
+ * @param rewardTasks 【请填写功能名称】
+ * @return 【请填写功能名称】集合
+ */
+ public List<RewardTasks> selectRewardTasksList(RewardTasks rewardTasks);
+
+ /**
+ * 新增【请填写功能名称】
+ *
+ * @param rewardTasks 【请填写功能名称】
+ * @return 结果
+ */
+ public int insertRewardTasks(RewardTasks rewardTasks);
+
+ /**
+ * 修改【请填写功能名称】
+ *
+ * @param rewardTasks 【请填写功能名称】
+ * @return 结果
+ */
+ public int updateRewardTasks(RewardTasks rewardTasks);
+
+ /**
+ * 删除【请填写功能名称】
+ *
+ * @param rewardId 【请填写功能名称】主键
+ * @return 结果
+ */
+ public int deleteRewardTasksByRewardId(Long rewardId);
+
+ /**
+ * 批量删除【请填写功能名称】
+ *
+ * @param rewardIds 需要删除的数据主键集合
+ * @return 结果
+ */
+ public int deleteRewardTasksByRewardIds(Long[] rewardIds);
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/reward/service/IRewardTasksService.java b/ruoyi-admin/src/main/java/com/ruoyi/reward/service/IRewardTasksService.java
new file mode 100644
index 0000000..5b8bd13
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/reward/service/IRewardTasksService.java
@@ -0,0 +1,61 @@
+package com.ruoyi.reward.service;
+
+import java.util.List;
+import com.ruoyi.reward.domain.RewardTasks;
+
+/**
+ * 【请填写功能名称】Service接口
+ *
+ * @author ruoyi
+ * @date 2025-05-17
+ */
+public interface IRewardTasksService
+{
+ /**
+ * 查询【请填写功能名称】
+ *
+ * @param rewardId 【请填写功能名称】主键
+ * @return 【请填写功能名称】
+ */
+ public RewardTasks selectRewardTasksByRewardId(Long rewardId);
+
+ /**
+ * 查询【请填写功能名称】列表
+ *
+ * @param rewardTasks 【请填写功能名称】
+ * @return 【请填写功能名称】集合
+ */
+ public List<RewardTasks> selectRewardTasksList(RewardTasks rewardTasks);
+
+ /**
+ * 新增【请填写功能名称】
+ *
+ * @param rewardTasks 【请填写功能名称】
+ * @return 结果
+ */
+ public int insertRewardTasks(RewardTasks rewardTasks);
+
+ /**
+ * 修改【请填写功能名称】
+ *
+ * @param rewardTasks 【请填写功能名称】
+ * @return 结果
+ */
+ public int updateRewardTasks(RewardTasks rewardTasks);
+
+ /**
+ * 批量删除【请填写功能名称】
+ *
+ * @param rewardIds 需要删除的【请填写功能名称】主键集合
+ * @return 结果
+ */
+ public int deleteRewardTasksByRewardIds(Long[] rewardIds);
+
+ /**
+ * 删除【请填写功能名称】信息
+ *
+ * @param rewardId 【请填写功能名称】主键
+ * @return 结果
+ */
+ public int deleteRewardTasksByRewardId(Long rewardId);
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/reward/service/impl/RewardTasksServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/reward/service/impl/RewardTasksServiceImpl.java
new file mode 100644
index 0000000..b80f3f3
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/reward/service/impl/RewardTasksServiceImpl.java
@@ -0,0 +1,96 @@
+package com.ruoyi.reward.service.impl;
+
+import java.util.List;
+import com.ruoyi.common.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.reward.mapper.RewardTasksMapper;
+import com.ruoyi.reward.domain.RewardTasks;
+import com.ruoyi.reward.service.IRewardTasksService;
+
+/**
+ * 【请填写功能名称】Service业务层处理
+ *
+ * @author ruoyi
+ * @date 2025-05-17
+ */
+@Service
+public class RewardTasksServiceImpl implements IRewardTasksService
+{
+ @Autowired
+ private RewardTasksMapper rewardTasksMapper;
+
+ /**
+ * 查询【请填写功能名称】
+ *
+ * @param rewardId 【请填写功能名称】主键
+ * @return 【请填写功能名称】
+ */
+ @Override
+ public RewardTasks selectRewardTasksByRewardId(Long rewardId)
+ {
+ return rewardTasksMapper.selectRewardTasksByRewardId(rewardId);
+ }
+
+ /**
+ * 查询【请填写功能名称】列表
+ *
+ * @param rewardTasks 【请填写功能名称】
+ * @return 【请填写功能名称】
+ */
+ @Override
+ public List<RewardTasks> selectRewardTasksList(RewardTasks rewardTasks)
+ {
+ return rewardTasksMapper.selectRewardTasksList(rewardTasks);
+ }
+
+ /**
+ * 新增【请填写功能名称】
+ *
+ * @param rewardTasks 【请填写功能名称】
+ * @return 结果
+ */
+ @Override
+ public int insertRewardTasks(RewardTasks rewardTasks)
+ {
+ rewardTasks.setCreateTime(DateUtils.getNowDate());
+ return rewardTasksMapper.insertRewardTasks(rewardTasks);
+ }
+
+ /**
+ * 修改【请填写功能名称】
+ *
+ * @param rewardTasks 【请填写功能名称】
+ * @return 结果
+ */
+ @Override
+ public int updateRewardTasks(RewardTasks rewardTasks)
+ {
+ rewardTasks.setUpdateTime(DateUtils.getNowDate());
+ return rewardTasksMapper.updateRewardTasks(rewardTasks);
+ }
+
+ /**
+ * 批量删除【请填写功能名称】
+ *
+ * @param rewardIds 需要删除的【请填写功能名称】主键
+ * @return 结果
+ */
+ @Override
+ public int deleteRewardTasksByRewardIds(Long[] rewardIds)
+ {
+ return rewardTasksMapper.deleteRewardTasksByRewardIds(rewardIds);
+ }
+
+ /**
+ * 删除【请填写功能名称】信息
+ *
+ * @param rewardId 【请填写功能名称】主键
+ * @return 结果
+ */
+ @Override
+ public int deleteRewardTasksByRewardId(Long rewardId)
+ {
+ return rewardTasksMapper.deleteRewardTasksByRewardId(rewardId);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
index 6d7626b..46345c7 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
@@ -68,7 +68,6 @@
@PostMapping("/uploadTorrent")
public AjaxResult uploadTorrent(@RequestParam("file") MultipartFile file) {
try {
-
// Create URL connection to Flask server
String flaskUrl = "http://localhost:5000/parse_torrent"; // Flask server URL
HttpURLConnection connection = (HttpURLConnection) new URL(flaskUrl).openConnection();
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTorrentCommentController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTorrentCommentController.java
index 44524a5..0f7e731 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTorrentCommentController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTorrentCommentController.java
@@ -24,6 +24,7 @@
@GetMapping("/{torrentId}")
public AjaxResult list(@PathVariable Long torrentId) {
+ System.out.println(commentService.getCommentList(torrentId));
return AjaxResult.success(commentService.getCommentList(torrentId));
}
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index c86c6b3..cb1a87c 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -9,7 +9,7 @@
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
profile: D:/ruoyi/uploadPath
# 获取ip地址开关
- addressEnabled: true
+ addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
captchaType: math
diff --git a/ruoyi-admin/src/main/resources/mapper/system/BtTorrentMapper.xml b/ruoyi-admin/src/main/resources/mapper/system/BtTorrentMapper.xml
index afb29d7..e19128a 100644
--- a/ruoyi-admin/src/main/resources/mapper/system/BtTorrentMapper.xml
+++ b/ruoyi-admin/src/main/resources/mapper/system/BtTorrentMapper.xml
@@ -24,7 +24,9 @@
<select id="selectBtTorrentList" parameterType="BtTorrent" resultMap="BtTorrentResult">
<include refid="selectBtTorrentVo"/>
- <where>
+ <where>
+ <if test="torrentId != null and torrentId != ''"> and torrent_id = #{torrentId}</if>
+
<if test="infoHash != null and infoHash != ''"> and info_hash = #{infoHash}</if>
<if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
<if test="length != null "> and length = #{length}</if>
diff --git a/ruoyi-admin/src/main/resources/mapper/system/RewardTasksMapper.xml b/ruoyi-admin/src/main/resources/mapper/system/RewardTasksMapper.xml
new file mode 100644
index 0000000..bae8185
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mapper/system/RewardTasksMapper.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.reward.mapper.RewardTasksMapper">
+
+ <resultMap type="RewardTasks" id="RewardTasksResult">
+ <result property="rewardId" column="reward_id" />
+ <result property="title" column="title" />
+ <result property="description" column="description" />
+ <result property="amount" column="amount" />
+ <result property="status" column="status" />
+ <result property="publisherId" column="publisher_id" />
+ <result property="accepterId" column="accepter_id" />
+ <result property="createTime" column="create_time" />
+ <result property="updateTime" column="update_time" />
+ <result property="remark" column="remark" />
+ </resultMap>
+
+ <sql id="selectRewardTasksVo">
+ select reward_id, title, description, amount, status, publisher_id, accepter_id, create_time, update_time, remark from reward_tasks
+ </sql>
+
+ <select id="selectRewardTasksList" parameterType="RewardTasks" resultMap="RewardTasksResult">
+ <include refid="selectRewardTasksVo"/>
+ <where>
+ <if test="title != null and title != ''"> and title = #{title}</if>
+ <if test="description != null and description != ''"> and description = #{description}</if>
+ <if test="amount != null "> and amount = #{amount}</if>
+ <if test="status != null and status != ''"> and status = #{status}</if>
+ <if test="publisherId != null "> and publisher_id = #{publisherId}</if>
+ <if test="accepterId != null "> and accepter_id = #{accepterId}</if>
+ </where>
+ </select>
+
+ <select id="selectRewardTasksByRewardId" parameterType="Long" resultMap="RewardTasksResult">
+ <include refid="selectRewardTasksVo"/>
+ where reward_id = #{rewardId}
+ </select>
+
+ <insert id="insertRewardTasks" parameterType="RewardTasks" useGeneratedKeys="true" keyProperty="rewardId">
+ insert into reward_tasks
+ <trim prefix="(" suffix=")" suffixOverrides=",">
+ <if test="title != null and title != ''">title,</if>
+ <if test="description != null and description != ''">description,</if>
+ <if test="amount != null">amount,</if>
+ <if test="status != null">status,</if>
+ <if test="publisherId != null">publisher_id,</if>
+ <if test="accepterId != null">accepter_id,</if>
+ <if test="createTime != null">create_time,</if>
+ <if test="updateTime != null">update_time,</if>
+ <if test="remark != null">remark,</if>
+ </trim>
+ <trim prefix="values (" suffix=")" suffixOverrides=",">
+ <if test="title != null and title != ''">#{title},</if>
+ <if test="description != null and description != ''">#{description},</if>
+ <if test="amount != null">#{amount},</if>
+ <if test="status != null">#{status},</if>
+ <if test="publisherId != null">#{publisherId},</if>
+ <if test="accepterId != null">#{accepterId},</if>
+ <if test="createTime != null">#{createTime},</if>
+ <if test="updateTime != null">#{updateTime},</if>
+ <if test="remark != null">#{remark},</if>
+ </trim>
+ </insert>
+
+ <update id="updateRewardTasks" parameterType="RewardTasks">
+ update reward_tasks
+ <trim prefix="SET" suffixOverrides=",">
+ <if test="title != null and title != ''">title = #{title},</if>
+ <if test="description != null and description != ''">description = #{description},</if>
+ <if test="amount != null">amount = #{amount},</if>
+ <if test="status != null">status = #{status},</if>
+ <if test="publisherId != null">publisher_id = #{publisherId},</if>
+ <if test="accepterId != null">accepter_id = #{accepterId},</if>
+ <if test="createTime != null">create_time = #{createTime},</if>
+ <if test="updateTime != null">update_time = #{updateTime},</if>
+ <if test="remark != null">remark = #{remark},</if>
+ </trim>
+ where reward_id = #{rewardId}
+ </update>
+
+ <delete id="deleteRewardTasksByRewardId" parameterType="Long">
+ delete from reward_tasks where reward_id = #{rewardId}
+ </delete>
+
+ <delete id="deleteRewardTasksByRewardIds" parameterType="String">
+ delete from reward_tasks where reward_id in
+ <foreach item="rewardId" collection="array" open="(" separator="," close=")">
+ #{rewardId}
+ </foreach>
+ </delete>
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
index 378c8eb..330039f 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
@@ -111,7 +111,7 @@
.authorizeHttpRequests((requests) -> {
permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
- requests.requestMatchers("/login", "/register", "/captchaImage","/announce").permitAll()
+ requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源,可匿名访问
.requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll()
.requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll()