This commit is contained in:
Rao 2025-04-08 09:29:14 +08:00
parent 0dad0e18ec
commit ca079e0096
14 changed files with 282 additions and 183 deletions

View File

@ -1,3 +1,6 @@
{
"marscode.chatLanguage": "cn"
"marscode.chatLanguage": "cn",
"marscode.codeCompletionPro": {
"enableCodeCompletionPro": true
}
}

View File

@ -11,9 +11,9 @@ export class ShareCodeRouter {
router = this.trpc.router({
generateShareCode: this.trpc.procedure
.input(z.object({ fileId: z.string() }))
.input(z.object({ fileId: z.string(), expiresAt: z.date(), canUseTimes: z.number() }))
.mutation(async ({ input }) => {
return this.shareCodeService.generateShareCode(input.fileId);
return this.shareCodeService.generateShareCode(input.fileId, input.expiresAt, input.canUseTimes);
}),
validateAndUseCode: this.trpc.procedure
.input(z.object({ code: z.string() }))
@ -41,9 +41,9 @@ export class ShareCodeRouter {
return this.shareCodeService.getFileByShareCode(input.code);
}),
generateShareCodeByFileId: this.trpc.procedure
.input(z.object({ fileId: z.string() }))
.input(z.object({ fileId: z.string(), expiresAt: z.date(), canUseTimes: z.number() }))
.mutation(async ({ input }) => {
return this.shareCodeService.generateShareCodeByFileId(input.fileId);
return this.shareCodeService.generateShareCodeByFileId(input.fileId, input.expiresAt, input.canUseTimes);
}),
});
}

View File

@ -5,8 +5,11 @@ import { Cron, CronExpression } from '@nestjs/schedule';
import { ResourceService } from '@server/models/resource/resource.service';
import * as fs from 'fs'
import * as path from 'path'
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
export interface ShareCode {
id: string;
code: string;
@ -15,10 +18,12 @@ export interface ShareCode {
expiresAt: Date;
isUsed: boolean;
fileName?: string | null;
canUseTimes: number | null;
}
export interface GenerateShareCodeResponse {
code: string;
expiresAt: Date;
canUseTimes: number;
}
interface ResourceMeta {
@ -40,6 +45,8 @@ export class ShareCodeService {
async generateShareCode(
fileId: string,
expiresAt: Date,
canUseTimes: number,
fileName?: string,
): Promise<GenerateShareCodeResponse> {
try {
@ -55,8 +62,6 @@ export class ShareCodeService {
// 生成分享码
const code = this.generateCode();
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小时后过期
//const expiresAt = new Date(Date.now() + 10 * 1000); // 24小时后过期
// 查找是否已有分享码记录
const existingShareCode = await db.shareCode.findUnique({
where: { fileId },
@ -69,6 +74,7 @@ export class ShareCodeService {
data: {
code,
expiresAt,
canUseTimes,
isUsed: false,
// 只在没有现有文件名且提供了新文件名时才更新文件名
...(fileName && !existingShareCode.fileName ? { fileName } : {}),
@ -81,15 +87,17 @@ export class ShareCodeService {
code,
fileId,
expiresAt,
canUseTimes,
isUsed: false,
fileName: fileName || null,
},
});
}
this.logger.log(`Generated share code ${code} for file ${fileId}`);
this.logger.log(`Generated share code ${code} for file ${fileId} canUseTimes: ${canUseTimes}`);
return {
code,
expiresAt,
canUseTimes,
};
} catch (error) {
this.logger.error('Failed to generate share code', error);
@ -106,26 +114,26 @@ export class ShareCodeService {
where: {
code,
isUsed: false,
expiresAt: { gt: new Date() },
expiresAt: { gt: dayjs().tz('Asia/Shanghai').toDate() },
},
});
if (shareCode.canUseTimes <= 0) {
this.logger.log('分享码已使用次数超过限制');
return null;
}
//更新已使用次数
await db.shareCode.update({
where: { id: shareCode.id },
data: { canUseTimes: shareCode.canUseTimes - 1 },
});
this.logger.log('查询结果:', shareCode);
if (!shareCode) {
this.logger.log('分享码无效或已过期');
return null;
}
// 标记分享码为已使用
// await db.shareCode.update({
// where: { id: shareCode.id },
// data: { isUsed: true },
// });
// 记录使用日志
this.logger.log(`Share code ${code} used for file ${shareCode.fileId}`);
// 返回完整的分享码信息,包括文件名
return shareCode;
} catch (error) {
@ -141,7 +149,11 @@ export class ShareCodeService {
try {
const shareCodes = await db.shareCode.findMany({
where: {
OR: [{ expiresAt: { lt: new Date() } }, { isUsed: true }],
OR: [
{ expiresAt: { lt: dayjs().tz('Asia/Shanghai').toDate() } },
{ isUsed: true },
{ canUseTimes: { lte: 0 } }
],
}
})
this.logger.log('需要清理的分享码:', shareCodes);
@ -222,7 +234,7 @@ export class ShareCodeService {
where: {
fileId,
isUsed: false,
expiresAt: { gt: new Date() },
expiresAt: { gt: dayjs().tz('Asia/Shanghai').toDate() },
},
});
@ -249,7 +261,6 @@ export class ShareCodeService {
// 根据分享码获取文件
async getFileByShareCode(code: string) {
this.logger.log('收到验证分享码请求code:', code);
const shareCode = await this.validateAndUseCode(code);
this.logger.log('验证分享码结果:', shareCode);
@ -275,6 +286,7 @@ export class ShareCodeService {
code: shareCode.code,
expiresAt: shareCode.expiresAt,
url: fileUrl,
canUseTimes: shareCode.canUseTimes - 1,
};
this.logger.log('返回给前端的数据:', response); // 添加日志
@ -282,10 +294,10 @@ export class ShareCodeService {
}
// 根据文件ID生成分享码
async generateShareCodeByFileId(fileId: string) {
async generateShareCodeByFileId(fileId: string, expiresAt: Date, canUseTimes: number) {
try {
this.logger.log('收到生成分享码请求fileId:', fileId);
const result = await this.generateShareCode(fileId);
const result = await this.generateShareCode(fileId, expiresAt, canUseTimes);
this.logger.log('生成分享码结果:', result);
return result;
} catch (error) {

View File

@ -62,6 +62,7 @@
"quill": "2.0.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-fast-marquee": "^1.6.5",
"react-hook-form": "^7.54.2",
"react-hot-toast": "^2.4.1",
"react-player": "^2.16.0",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1,7 +1,7 @@
import { ShareCodeGenerator } from "../sharecode/sharecodegenerator";
import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
import { message, Tabs, Form, Spin } from "antd";
import { message, Tabs, Form, Spin, Alert } from "antd";
import { env } from '../../../env'
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
import { useState } from "react";
@ -10,30 +10,20 @@ export default function DeptSettingPage() {
const [form] = Form.useForm();
const uploadFileId = Form.useWatch(["file"], form)?.[0]
const [isGetingFileId, setIsGetingFileId] = useState(false);
// 处理分享码生成成功
const handleShareSuccess = (code: string) => {
message.success('分享码生成成功:' + code);
// 可以在这里添加其他逻辑,比如保存到历史记录
}
// 处理分享码验证成功
const handleValidSuccess = async (fileUrl: string, fileName: string) => {
setIsGetingFileId(true);
try {
// 构建下载URL包含文件名参数
console.log('文件url:', fileUrl);
const downloadUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileUrl}`;
console.log('下载URL:', downloadUrl);
const link = document.createElement('a');
link.href = downloadUrl;
// 直接使用传入的 fileName
link.download = fileName;
link.target = '_blank'; // 在新标签页中打开
// 触发下载
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
message.success('文件下载开始');
} catch (error) {
console.error('下载失败:', error);
@ -49,35 +39,30 @@ export default function DeptSettingPage() {
{
isGetingFileId ?
(<Spin spinning={isGetingFileId} fullscreen />) :
(<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<span className="text-2xl py-4"></span>
(<div style={{ maxWidth: '1000px', margin: '0 auto', padding: '20px' }}>
<div className="flex items-center">
<img src="/logo.svg" className="h-20 w-20 rounded-xl" />
<span className="text-4xl py-4 mx-4"></span>
</div>
<Tabs defaultActiveKey="upload">
<TabPane tab="上传分享" key="upload">
{/* 文件上传区域 */}
<div style={{ marginBottom: '40px' }}>
<span className="text-lg block text-zinc-700 py-2"></span>
{/* 如果没有已上传文件,显示上传区域 */}
<div>
<Form form={form}>
<Form.Item name="file">
<TusUploader
multiple={false}
style={"w-full py-4"}
style={"w-full py-4 mb-0 h-64"}
></TusUploader>
</Form.Item>
</Form>
</div>
{/* 生成分享码区域 */}
<div style={{ marginBottom: '40px' }}>
<span className="text-lg block text-zinc-700 py-4"></span>
<ShareCodeGenerator
fileId={uploadFileId}
onSuccess={handleShareSuccess}
/>
</div>
</TabPane>
{/* 使用分享码区域 */}
<TabPane tab="下载文件" key="download">
<div>
<span className="text-lg block text-zinc-700 py-4">使</span>

View File

@ -1,11 +1,14 @@
import React, { useEffect, useState } from 'react';
import { Button, message } from 'antd';
import { Button, DatePicker, Form, message, Select } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import { env } from '../../../env'
import { useQueryClient } from '@tanstack/react-query';
import { getQueryKey } from '@trpc/react-query';
import { api } from '@nice/client';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
interface ShareCodeGeneratorProps {
fileId: string;
onSuccess?: (code: string) => void;
@ -14,6 +17,7 @@ interface ShareCodeGeneratorProps {
interface ShareCodeResponse {
code?: string;
expiresAt?: Date;
canUseTimes?: number;
}
export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
fileId,
@ -23,10 +27,12 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
const [loading, setLoading] = useState(false);
const [shareCode, setShareCode] = useState<string>('');
const [expiresAt, setExpiresAt] = useState<Date | null>(null);
const [canUseTimes, setCanUseTimes] = useState<number>(null);
const queryClient = useQueryClient();
const queryKey = getQueryKey(api.term);
const [isGenerate, setIsGenerate] = useState(false);
const [currentFileId, setCurrentFileId] = useState<string>('');
const [form] = Form.useForm();
const generateShareCode = api.shareCode.generateShareCodeByFileId.useMutation({
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey });
@ -46,12 +52,17 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
setLoading(true);
console.log('开始生成分享码fileId:', fileId, 'fileName:', fileName);
try {
const data: ShareCodeResponse = await generateShareCode.mutateAsync({ fileId });
console.log('生成分享码结果:', data);
const data: ShareCodeResponse = await generateShareCode.mutateAsync({
fileId,
expiresAt: form.getFieldsValue()?.expiresAt ? form.getFieldsValue().expiresAt.toDate() : dayjs().add(1, 'day').tz('Asia/Shanghai').toDate(),
canUseTimes: form.getFieldsValue()?.canUseTimes ? form.getFieldsValue().canUseTimes : 10,
});
console.log('data', data)
setShareCode(data.code);
setIsGenerate(true);
setExpiresAt(data.expiresAt ? new Date(data.expiresAt) : null);
onSuccess?.(data.code);
setExpiresAt(dayjs(data.expiresAt).tz('Asia/Shanghai').toDate());
setCanUseTimes(data.canUseTimes);
//message.success('分享码生成成功');
} catch (error) {
console.error('生成分享码错误:', error);
@ -73,30 +84,61 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
return Promise.resolve();
}
}
// 组件使用
const handleCopy = (code) => {
copyToClipboard(code)
.then(() => console.log('复制成功'))
.catch(() => console.error('复制失败'));
};
useEffect(() => {
const date = dayjs().add(1, 'day').tz('Asia/Shanghai');
form.setFieldsValue({
expiresAt: date,
canUseTimes: 10
});
}, [form]);
useEffect(()=>{
if (fileId) {
generateCode()
}
}, [fileId])
return (
<div style={{ padding: '20px', backgroundColor: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ marginBottom: '10px' }}>
{/* 添加调试信息 */}
<small style={{ color: '#666' }}>ID: {fileId ? fileId : '未选择文件'}</small>
<div style={{ marginBottom: '3px' }}>
<small style={{ color: '#666' }}>ID: {fileId ? fileId : '暂未上传文件'}</small>
</div>
{!isGenerate ? (
<Button
type="primary"
onClick={generateCode}
loading={loading}
block
>
</Button>
<>
<Form form={form}>
<div className='w-4/5 h-16 flex flex-row justify-between items-center'>
<small style={{ color: '#666' }}>
{"分享码的有效期"}
</small>
<Form.Item name="expiresAt" className='mt-5'>
<DatePicker
showTime
/>
</Form.Item>
<small style={{ color: '#666' }}>
{"分享码的使用次数"}
</small>
<Form.Item name="canUseTimes" className='mt-5'>
<Select
style={{ width: 120 }}
//onChange={handleChange}
options={[
{ value: 10, label: '10' },
{ value: 20, label: '20' },
{ value: 30, label: '30' },
{ value: 40, label: '40' },
{ value: 50, label: '50' },
]}
/>
</Form.Item>
</div>
</Form>
</>
) : (
<div style={{ textAlign: 'center' }}>
<div style={{
@ -126,9 +168,9 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
}}
/>
</div>
{expiresAt ? (
{isGenerate && expiresAt ? (
<div style={{ color: '#666' }}>
: {expiresAt.toLocaleString()}
: {expiresAt.toLocaleString()} 使: {canUseTimes}
</div>
) : (
<div style={{ color: 'red' }}>

View File

@ -1,27 +1,27 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Input, Button, message } from 'antd';
import styles from './ShareCodeValidator.module.css';
import {env} from '../../../env'
import { api } from '@nice/client';
import dayjs from 'dayjs';
interface ShareCodeValidatorProps {
onValidSuccess: (fileId: string, fileName: string) => void;
}
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
onValidSuccess,
}) => {
const [code, setCode] = useState('');
const [loading, setLoading] = useState(false);
const { data: result, isLoading } = api.shareCode.getFileByShareCode.useQuery(
const { data: result, isLoading,refetch } = api.shareCode.getFileByShareCode.useQuery(
{ code: code.trim() },
{
enabled: !!code.trim()
enabled: false
}
)
const validateCode = useCallback(() => {
const validateCode = useCallback(async () => {
if (!code.trim()) {
message.warning('请输入正确的分享码');
@ -29,18 +29,19 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
}
setLoading(true);
try {
console.log('验证分享码返回数据:', result);
onValidSuccess(result.url, result.fileName);
message.success(`验证成功,文件名:${result.fileName},请等待下载...`);
const { data: latestResult } = await refetch();
console.log('验证分享码返回数据:', latestResult);
onValidSuccess(latestResult.url, latestResult.fileName);
} catch (error) {
console.error('验证分享码失败:', error);
message.error('分享码无效或已过期');
} finally {
setLoading(false);
}
},[result,code, onValidSuccess])
}, [refetch, code, onValidSuccess]);
return (
<>
<div className={styles.container}>
<Input
className={styles.input}
@ -56,8 +57,19 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
loading={loading}
disabled={!code.trim()}
>
</Button>
</div>
{
!loading && result && (
<div className='w-full flex justify-between my-2 p-1 antialiased text-secondary-600'>
<span >{`分享码:${result?.code ? result.code : ''}`}</span>
<span >{`文件名:${result?.fileName ? result.fileName : ''}`}</span>
<span >{`过期时间:${result?.expiresAt ? dayjs(result.expiresAt).tz('Asia/Shanghai').toDate().toLocaleString() : ''}`}</span>
<span >{`剩余使用次数:${result?.canUseTimes ? result.canUseTimes : ''}`}</span>
</div>
)
}
</>
);
};

View File

@ -4,7 +4,7 @@ import {
CheckCircleOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import { Upload, Progress, Button } from "antd";
import { Upload, Progress, Button, message } from "antd";
import { useTusUpload } from "@web/src/hooks/useTusUpload";
import toast from "react-hot-toast";
export interface TusUploaderProps {
@ -12,9 +12,9 @@ export interface TusUploaderProps {
onChange?: (value: string[]) => void;
multiple?: boolean;
allowTypes?: string[];
style?:string
icon?:ReactNode,
description?:string
style?: string
icon?: ReactNode,
description?: string
}
interface UploadingFile {
@ -30,7 +30,7 @@ export const TusUploader = ({
onChange,
multiple = true,
allowTypes = undefined,
style="",
style = "",
icon = <UploadOutlined />,
description = "点击或拖拽文件到此区域进行上传",
}: TusUploaderProps) => {
@ -67,8 +67,14 @@ export const TusUploader = ({
const handleBeforeUpload = useCallback(
(file: File) => {
console.log('File object:',file)
// 判断是否为文件
if (!file.type) {
message.error('请选择正确的文件');
return Upload.LIST_IGNORE;
}
if (allowTypes && !allowTypes.includes(file.type)) {
toast.error(`文件类型 ${file.type} 不在允许范围内`);
message.error(`文件类型 ${file.type} 不在允许范围内`);
return Upload.LIST_IGNORE; // 使用 antd 的官方阻止方式
}
const fileKey = `${file.name}-${Date.now()}`;
@ -150,7 +156,9 @@ export const TusUploader = ({
name="files"
multiple={multiple}
showUploadList={false}
beforeUpload={handleBeforeUpload}>
beforeUpload={handleBeforeUpload}
directory={false}
>
<p className="ant-upload-drag-icon">
{icon}
</p>

View File

@ -100,7 +100,7 @@ server {
# 仅供内部使用
internal;
# 代理到认证服务
proxy_pass http://192.168.43.206:3006/auth/file;
proxy_pass http://192.168.43.206:3001/auth/file;
# 请求优化:不传递请求体
proxy_pass_request_body off;

View File

@ -92,7 +92,6 @@ model Staff {
visits Visit[]
posts Post[]
learningPosts Post[] @relation("post_student")
sentMsgs Message[] @relation("message_sender")
receivedMsgs Message[] @relation("message_receiver")
@ -253,6 +252,7 @@ model PostAncestry {
relDepth Int @map("rel_depth")
ancestor Post? @relation("AncestorPosts", fields: [ancestorId], references: [id])
descendant Post @relation("DescendantPosts", fields: [descendantId], references: [id])
// 复合索引优化
// 索引建议
@@index([ancestorId]) // 针对祖先的查询
@ -276,6 +276,7 @@ model Message {
visits Visit[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt @map("updated_at")
@@index([type, createdAt])
@@map("message")
}
@ -403,6 +404,7 @@ model NodeEdge {
@@index([targetId])
@@map("node_edge")
}
model Animal {
id String @id @default(cuid())
name String
@ -411,6 +413,7 @@ model Animal {
personId String?
person Person? @relation(fields: [personId], references: [id])
}
model Person {
id String @id @default(cuid())
name String
@ -418,6 +421,7 @@ model Person {
gender Boolean
animals Animal[]
}
model ShareCode {
id String @id @default(cuid())
code String? @unique
@ -426,7 +430,7 @@ model ShareCode {
expiresAt DateTime? @map("expires_at")
isUsed Boolean? @default(false)
fileName String? @map("file_name")
canUseTimes Int?
@@index([code])
@@index([fileId])
@@index([expiresAt])

View File

@ -380,6 +380,9 @@ importers:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
react-fast-marquee:
specifier: ^1.6.5
version: 1.6.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react-hook-form:
specifier: ^7.54.2
version: 7.54.2(react@18.2.0)
@ -6657,6 +6660,12 @@ packages:
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-fast-marquee@1.6.5:
resolution: {integrity: sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg==}
peerDependencies:
react: '>= 16.8.0 || ^18.0.0'
react-dom: '>= 16.8.0 || ^18.0.0'
react-hook-form@7.54.2:
resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==}
engines: {node: '>=18.0.0'}
@ -14797,6 +14806,11 @@ snapshots:
react-fast-compare@3.2.2: {}
react-fast-marquee@1.6.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-hook-form@7.54.2(react@18.2.0):
dependencies:
react: 18.2.0

Binary file not shown.