增加活动页面
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"/>