增加活动页面

Change-Id: I63cf84e250d16b2af401f335562fcadd3be13170
diff --git a/react-ui/src/pages/Activity/data.d.ts b/react-ui/src/pages/Activity/data.d.ts
new file mode 100644
index 0000000..d66d25f
--- /dev/null
+++ b/react-ui/src/pages/Activity/data.d.ts
@@ -0,0 +1,101 @@
+// data.d.ts - 活动系统类型定义文件
+
+// 活动类型枚举
+export type ActivityType = 'UPLOAD' | 'DOWNLOAD';
+
+// 活动状态
+export type ActivityStatus = 0 | 1; // 0=已结束,1=进行中
+
+// 活动信息 - 适配后端返回的字段
+export interface SysActivity {
+    createBy?: string | null;
+    createTime?: string | null;
+    updateBy?: string | null;
+    updateTime?: string | null;
+    remark?: string | null;
+    activityId: number;
+    activityName: string;
+    rewardBonus: number;
+    conditionValue: string; // 上传:如"10GB",下载:torrent_id
+    startTime: string; // ISO 8601 格式:2025-06-01T00:00:00.000+08:00
+    endTime: string;   // ISO 8601 格式:2025-06-30T23:59:59.000+08:00
+    status: ActivityStatus;
+    activityType: ActivityType;
+}
+
+// 活动记录
+export interface SysActivityRecord {
+    recordId: number;
+    activityId: number;
+    userId: number;
+    rewardBonus: number;
+    completeTime: string;
+}
+
+// 排行榜条目
+export interface LeaderboardEntry {
+    userId: number;
+    userName: string;
+    score: number;
+}
+
+// 后端实际返回的活动列表响应格式
+export interface GetActivityListResponse {
+    total: number;
+    rows: SysActivity[];
+    code: number;
+    msg: string;
+}
+
+// 获取排行榜响应 - 假设也遵循相同格式
+export interface GetLeaderboardResponse {
+    total: number;
+    rows: LeaderboardEntry[];
+    code: number;
+    msg: string;
+}
+
+// 参与活动响应
+export interface ParticipateActivityResponse {
+    code: number;
+    msg: string;
+    data?: any;
+}
+
+// 活动列表查询参数
+export interface ActivityListParams {
+    pageNum?: number;
+    pageSize?: number;
+    activityName?: string;
+    activityType?: ActivityType;
+    status?: ActivityStatus;
+}
+
+// 排行榜查询参数
+export interface LeaderboardParams {
+    pageNum?: number;
+    pageSize?: number;
+}
+
+// 前端组件使用的标准化数据格式(保持原有格式以减少组件改动)
+export interface StandardizedActivityListResponse {
+    code: number;
+    msg: string;
+    data: {
+        list: SysActivity[];
+        total: number;
+        pageSize: number;
+        current: number;
+    };
+}
+
+export interface StandardizedLeaderboardResponse {
+    code: number;
+    msg: string;
+    data: {
+        list: LeaderboardEntry[];
+        total: number;
+        pageSize: number;
+        current: number;
+    };
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/Activity/index.tsx b/react-ui/src/pages/Activity/index.tsx
new file mode 100644
index 0000000..936b88a
--- /dev/null
+++ b/react-ui/src/pages/Activity/index.tsx
@@ -0,0 +1,318 @@
+// index.tsx - 活动中心页面
+
+import React, { useState, useEffect, useRef } from 'react';
+import {
+    Card,
+    Button,
+    Tag,
+    message,
+    Spin,
+    Empty,
+    List,
+    Space,
+    Typography,
+    Upload
+} from 'antd';
+import {
+    GiftOutlined,
+    UploadOutlined,
+    DownloadOutlined,
+    FireOutlined,
+    CheckCircleOutlined,
+    ClockCircleOutlined
+} from '@ant-design/icons';
+import { PageContainer } from '@ant-design/pro-components';
+import { getActivityList, participateActivity, formatDateTime } from './service';
+import { uploadTorrent, downloadTorrent } from '../Torrent/service';
+import type { SysActivity } from './data';
+
+const { Title, Text } = Typography;
+
+const ActivityPage: React.FC = () => {
+    const [activities, setActivities] = useState<SysActivity[]>([]);
+    const [loading, setLoading] = useState(false);
+    const [participatingIds, setParticipatingIds] = useState<Set<number>>(new Set());
+    const [uploading, setUploading] = useState(false);
+    const [downloading, setDownloading] = useState<number | null>(null);
+    const fileInputRef = useRef<HTMLInputElement>(null);
+
+    // 获取活动列表
+    const fetchActivities = async () => {
+        setLoading(true);
+        try {
+            const res = await getActivityList({ pageNum: 1, pageSize: 20 });
+            console.log('活动列表响应:', res);
+            if (res.code === 0) {
+                // 修正:使用 res.data.list 而不是 res.rows
+                setActivities(res.data.list);
+                console.log('活动数据:', res.data.list);
+                console.log('第一个活动:', res.data.list[0]);
+            } else {
+                message.error(res.msg || '获取活动列表失败');
+            }
+        } catch (error) {
+            console.error('获取活动列表异常:', error);
+            message.error('网络异常,请稍后重试');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    // 获取排行榜
+    // const fetchLeaderboard = async () => {
+    //     try {
+    //         const res = await getLeaderboard({ pageNum: 1, pageSize: 50 });
+    //         if (res.code === 0) {
+    //             setLeaderboard(res.data.list);
+    //         } else {
+    //             message.error(res.msg || '获取排行榜失败');
+    //         }
+    //     } catch (error) {
+    //         console.error('获取排行榜失败', error);
+    //         message.error('获取排行榜异常,请稍后重试');
+    //     }
+    // };
+
+    // 处理文件上传
+    const handleUpload = async (file: File, activity: SysActivity) => {
+        setUploading(true);
+        try {
+            const res = await uploadTorrent(file);
+            if (res.code === 0) {
+                message.success('上传成功!');
+                // 上传成功后自动参与活动
+                await handleParticipate(activity.activityId);
+            } else {
+                message.error(res.msg || '上传失败');
+            }
+        } catch (error) {
+            console.error('上传异常:', error);
+            message.error('上传异常,请稍后重试');
+        } finally {
+            setUploading(false);
+        }
+    };
+
+    // 处理种子下载
+    const handleDownload = async (activity: SysActivity) => {
+        setDownloading(activity.activityId);
+        try {
+            const torrentId = activity.conditionValue; // 种子ID存储在conditionValue中
+            const blob = await downloadTorrent(Number(torrentId));
+
+            // 创建下载链接
+            const url = window.URL.createObjectURL(blob);
+            const link = document.createElement('a');
+            link.href = url;
+            link.download = `torrent_${torrentId}.torrent`;
+            document.body.appendChild(link);
+            link.click();
+            document.body.removeChild(link);
+            window.URL.revokeObjectURL(url);
+
+            message.success('下载成功');
+            // 下载成功后自动参与活动
+            await handleParticipate(activity.activityId);
+        } catch (error) {
+            console.error('下载异常:', error);
+            message.error('下载异常,请稍后重试');
+        } finally {
+            setDownloading(null);
+        }
+    };
+
+    // 触发文件选择
+    const triggerFileUpload = (activity: SysActivity) => {
+        const input = document.createElement('input');
+        input.type = 'file';
+        input.accept = '.torrent';
+        input.onchange = (e) => {
+            const file = (e.target as HTMLInputElement).files?.[0];
+            if (file) {
+                handleUpload(file, activity);
+            }
+        };
+        input.click();
+    };
+
+    // 参与活动
+    const handleParticipate = async (activityId: number) => {
+        // 添加到参与中的活动ID集合
+        setParticipatingIds(prev => new Set(prev).add(activityId));
+
+        try {
+            const res = await participateActivity(activityId);
+            if (res.code === 0) {
+                message.success('参与成功!');
+                fetchActivities();
+                // 已删除排行榜刷新
+            } else {
+                message.error(res.msg || '参与失败');
+            }
+        } catch (error) {
+            console.error('参与活动异常:', error);
+            message.error('网络异常,请稍后重试');
+        } finally {
+            // 从参与中的活动ID集合中移除
+            setParticipatingIds(prev => {
+                const newSet = new Set(prev);
+                newSet.delete(activityId);
+                return newSet;
+            });
+        }
+    };
+
+    useEffect(() => {
+        fetchActivities();
+        // 已删除排行榜获取
+    }, []);
+
+    // 渲染活动类型图标(可点击)
+    const renderActivityIcon = (activity: SysActivity) => {
+        if (activity.status === 0) {
+            // 活动已结束,图标不可点击
+            return activity.activityType === 'UPLOAD' ?
+                <UploadOutlined style={{ fontSize: 20, color: '#d9d9d9' }} /> :
+                <DownloadOutlined style={{ fontSize: 20, color: '#d9d9d9' }} />;
+        }
+
+        if (activity.activityType === 'UPLOAD') {
+            return (
+                <UploadOutlined
+                    style={{
+                        fontSize: 20,
+                        color: '#1890ff',
+                        cursor: 'pointer'
+                    }}
+                    title="点击上传种子文件"
+                    onClick={() => triggerFileUpload(activity)}
+                />
+            );
+        } else {
+            return (
+                <DownloadOutlined
+                    style={{
+                        fontSize: 20,
+                        color: '#52c41a',
+                        cursor: 'pointer'
+                    }}
+                    title="点击下载种子"
+                    onClick={() => handleDownload(activity)}
+                />
+            );
+        }
+    };
+
+    // 渲染状态标签
+    const renderStatusTag = (status: number) => {
+        return status === 1 ? (
+            <Tag icon={<FireOutlined />} color="success">进行中</Tag>
+        ) : (
+            <Tag icon={<CheckCircleOutlined />} color="default">已结束</Tag>
+        );
+    };
+
+    // 格式化条件显示
+    const formatCondition = (activity: SysActivity) => {
+        if (activity.activityType === 'UPLOAD') {
+            return `上传 ${activity.conditionValue}`;
+        }
+        return '完成下载任务';
+    };
+
+    // 格式化时间显示
+    const formatTimeRange = (startTime: string, endTime: string) => {
+        try {
+            // 处理 ISO 8601 格式的时间
+            const start = formatDateTime(startTime);
+            const end = formatDateTime(endTime);
+
+            // 只显示日期部分
+            const startDate = start.split(' ')[0];
+            const endDate = end.split(' ')[0];
+
+            return `${startDate} ~ ${endDate}`;
+        } catch (error) {
+            console.error('时间格式化错误:', error);
+            // 降级处理:如果是老格式,直接使用
+            return `${startTime.split(' ')[0]} ~ ${endTime.split(' ')[0]}`;
+        }
+    };
+
+    // 排行榜相关代码已删除
+
+    return (
+        <PageContainer title="活动中心">
+            <Spin spinning={loading}>
+                <List
+                    grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3 }}
+                    dataSource={activities}
+                    locale={{ emptyText: <Empty description="暂无活动" /> }}
+                    renderItem={(activity) => (
+                        <List.Item>
+                            <Card
+                                hoverable
+                                actions={[
+                                    <Button
+                                        key="participate"
+                                        type="primary"
+                                        disabled={activity.status === 0}
+                                        loading={
+                                            participatingIds.has(activity.activityId) ||
+                                            uploading ||
+                                            downloading === activity.activityId
+                                        }
+                                        onClick={() => handleParticipate(activity.activityId)}
+                                    >
+                                        {activity.status === 1 ? '确认参与' : '已结束'}
+                                    </Button>
+                                ]}
+                            >
+                                <Space direction="vertical" size={8} style={{ width: '100%' }}>
+                                    <Space style={{ width: '100%', justifyContent: 'space-between' }}>
+                                        {renderActivityIcon(activity)}
+                                        {renderStatusTag(activity.status)}
+                                    </Space>
+
+                                    <Title level={5} style={{ margin: 0 }}>
+                                        {activity.activityName}
+                                    </Title>
+
+                                    <Text type="secondary">
+                                        {formatCondition(activity)}
+                                    </Text>
+
+                                    <Space>
+                                        <GiftOutlined style={{ color: '#faad14' }} />
+                                        <Text strong style={{ color: '#faad14' }}>
+                                            {activity.rewardBonus} 积分
+                                        </Text>
+                                    </Space>
+
+                                    <Space size={4} style={{ fontSize: 12 }}>
+                                        <ClockCircleOutlined />
+                                        <Text type="secondary">
+                                            {formatTimeRange(activity.startTime, activity.endTime)}
+                                        </Text>
+                                    </Space>
+
+                                    {/* 操作提示 */}
+                                    {activity.status === 1 && (
+                                        <Text type="secondary" style={{ fontSize: 12 }}>
+                                            {activity.activityType === 'UPLOAD'
+                                                ? '点击上传图标选择种子文件'
+                                                : '点击下载图标获取种子'
+                                            }
+                                        </Text>
+                                    )}
+                                </Space>
+                            </Card>
+                        </List.Item>
+                    )}
+                />
+            </Spin>
+        </PageContainer>
+    );
+};
+
+export default ActivityPage;
\ No newline at end of file
diff --git a/react-ui/src/pages/Activity/service.ts b/react-ui/src/pages/Activity/service.ts
new file mode 100644
index 0000000..25c1d9a
--- /dev/null
+++ b/react-ui/src/pages/Activity/service.ts
@@ -0,0 +1,219 @@
+// service.ts - 活动系统服务层文件
+
+import { request } from '@umijs/max';
+import type {
+    GetActivityListResponse,
+    GetLeaderboardResponse,
+    ParticipateActivityResponse,
+    ActivityListParams,
+    LeaderboardParams,
+    SysActivity,
+    LeaderboardEntry,
+    StandardizedActivityListResponse,
+    StandardizedLeaderboardResponse
+} from './data';
+
+// 开发环境模拟数据开关
+const USE_MOCK = process.env.NODE_ENV === 'development' && false;
+
+// 模拟活动数据 - 适配后端格式
+const mockActivities: SysActivity[] = [
+    {
+        createBy: null,
+        createTime: null,
+        updateBy: null,
+        updateTime: null,
+        remark: null,
+        activityId: 1,
+        activityName: '新手上传挑战',
+        rewardBonus: 100,
+        conditionValue: '10GB',
+        startTime: '2025-06-01T00:00:00.000+08:00',
+        endTime: '2025-06-30T23:59:59.000+08:00',
+        status: 1,
+        activityType: 'UPLOAD'
+    },
+    {
+        createBy: null,
+        createTime: null,
+        updateBy: null,
+        updateTime: null,
+        remark: null,
+        activityId: 2,
+        activityName: '种子下载任务',
+        rewardBonus: 50,
+        conditionValue: '94',
+        startTime: '2025-06-01T00:00:00.000+08:00',
+        endTime: '2025-06-15T23:59:59.000+08:00',
+        status: 1,
+        activityType: 'DOWNLOAD'
+    }
+];
+
+// 模拟排行榜数据
+const mockLeaderboard: LeaderboardEntry[] = [
+    { userId: 1, userName: 'user001', score: 1500 },
+    { userId: 2, userName: 'user002', score: 1200 },
+    { userId: 3, userName: 'user003', score: 1000 }
+];
+
+/**
+ * 时间格式转换工具函数
+ * 将 ISO 8601 格式转换为显示格式
+ */
+export function formatDateTime(isoString: string): string {
+    const date = new Date(isoString);
+    return date.toLocaleString('zh-CN', {
+        year: 'numeric',
+        month: '2-digit',
+        day: '2-digit',
+        hour: '2-digit',
+        minute: '2-digit',
+        second: '2-digit'
+    }).replace(/\//g, '-');
+}
+
+/**
+ * 数据格式标准化函数
+ * 将后端返回格式转换为前端组件期望的格式
+ */
+function normalizeActivityListResponse(
+    backendResponse: GetActivityListResponse,
+    params?: ActivityListParams
+): StandardizedActivityListResponse {
+    return {
+        code: backendResponse.code === 200 ? 0 : backendResponse.code, // 后端成功码是200,前端期望0
+        msg: backendResponse.msg,
+        data: {
+            list: backendResponse.rows,
+            total: backendResponse.total,
+            pageSize: params?.pageSize || 10,
+            current: params?.pageNum || 1,
+        }
+    };
+}
+
+function normalizeLeaderboardResponse(
+    backendResponse: GetLeaderboardResponse,
+    params?: LeaderboardParams
+): StandardizedLeaderboardResponse {
+    return {
+        code: backendResponse.code === 200 ? 0 : backendResponse.code,
+        msg: backendResponse.msg,
+        data: {
+            list: backendResponse.rows,
+            total: backendResponse.total,
+            pageSize: params?.pageSize || 10,
+            current: params?.pageNum || 1,
+        }
+    };
+}
+
+/**
+ * 获取活动列表
+ */
+export async function getActivityList(params?: ActivityListParams): Promise<StandardizedActivityListResponse> {
+    if (USE_MOCK) {
+        let filteredList = mockActivities;
+
+        // 按状态筛选
+        if (params?.status !== undefined) {
+            filteredList = filteredList.filter(item => item.status === params.status);
+        }
+
+        // 按活动类型筛选
+        if (params?.activityType) {
+            filteredList = filteredList.filter(item => item.activityType === params.activityType);
+        }
+
+        // 按活动名称搜索
+        if (params?.activityName) {
+            filteredList = filteredList.filter(item =>
+                item.activityName.includes(params.activityName!)
+            );
+        }
+
+        // 模拟后端返回格式
+        const mockBackendResponse: GetActivityListResponse = {
+            total: filteredList.length,
+            rows: filteredList,
+            code: 200,
+            msg: '查询成功'
+        };
+
+        return normalizeActivityListResponse(mockBackendResponse, params);
+    }
+
+    try {
+        // 调用实际后端接口
+        const response: GetActivityListResponse = await request('/api/system/activity/list', {
+            method: 'GET',
+            params: {
+                pageNum: params?.pageNum,
+                pageSize: params?.pageSize,
+                activityName: params?.activityName,
+                activityType: params?.activityType,
+                status: params?.status,
+            },
+        });
+
+        // 标准化响应格式
+        return normalizeActivityListResponse(response, params);
+    } catch (error) {
+        console.error('获取活动列表失败:', error);
+        throw error;
+    }
+}
+
+/**
+ * 参与活动
+ */
+export async function participateActivity(activityId: number): Promise<ParticipateActivityResponse> {
+    if (USE_MOCK) {
+        return Promise.resolve({
+            code: 0,
+            msg: '参与成功',
+            data: null,
+        });
+    }
+
+    try {
+        return await request(`/api/system/activity/participate/${activityId}`, {
+            method: 'POST',
+        });
+    } catch (error) {
+        console.error('参与活动失败:', error);
+        throw error;
+    }
+}
+
+/**
+ * 获取排行榜
+ */
+export async function getLeaderboard(params?: LeaderboardParams): Promise<StandardizedLeaderboardResponse> {
+    if (USE_MOCK) {
+        const mockBackendResponse: GetLeaderboardResponse = {
+            total: mockLeaderboard.length,
+            rows: mockLeaderboard,
+            code: 200,
+            msg: '查询成功'
+        };
+
+        return normalizeLeaderboardResponse(mockBackendResponse, params);
+    }
+
+    try {
+        const response: GetLeaderboardResponse = await request('/api/system/activity/leaderboard', {
+            method: 'GET',
+            params: {
+                pageNum: params?.pageNum,
+                pageSize: params?.pageSize,
+            },
+        });
+
+        return normalizeLeaderboardResponse(response, params);
+    } catch (error) {
+        console.error('获取排行榜失败:', error);
+        throw error;
+    }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/mapper/system/SysUserInviteMapper.xml b/ruoyi-admin/src/main/resources/mapper/system/SysUserInviteMapper.xml
index 5059af0..ad2c91d 100644
--- a/ruoyi-admin/src/main/resources/mapper/system/SysUserInviteMapper.xml
+++ b/ruoyi-admin/src/main/resources/mapper/system/SysUserInviteMapper.xml
@@ -1,6 +1,6 @@
 <?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.system.mapper.SysUserInviteMapper">
+<mapper namespace="com.ruoyi.authentication.mapper.SysUserInviteMapper">
     <resultMap id="SysUserInviteMap" type="com.ruoyi.authentication.domain.SysUserInvite">
         <id property="codeId" column="code_id"/>
         <result property="code" column="code"/>