@@ -58,7 +58,8 @@ import { POST as CONFIRM_TASK } from '@/app/api/import-tasks/[id]/confirm/route'
5858import { waitForImportBatchTasksForTest } from '@/lib/import-batches' ;
5959import { simulateRestartForTest , waitForImportTaskForTest } from '@/lib/import-tasks' ;
6060
61- let database : PrismaClient ;
61+ let database : PrismaClient < { log : Array < { emit : 'event' ; level : 'query' } > } > ;
62+ const queries : string [ ] = [ ] ;
6263let directory : string ;
6364let sequence = 0 ;
6465const PAGE = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAA==' ;
@@ -109,7 +110,9 @@ beforeAll(async () => {
109110 execFileSync ( process . execPath , [ resolve ( 'node_modules/prisma/build/index.js' ) , 'migrate' , 'deploy' ] , {
110111 env : { ...process . env , DATABASE_URL : url } , stdio : 'pipe' ,
111112 } ) ;
112- database = new PrismaClient ( { datasources : { db : { url } } } ) ;
113+ database = new PrismaClient ( { datasources : { db : { url } } , log : [ { emit : 'event' , level : 'query' } ] } ) ;
114+ // 记录执行的 SQL,用于检查默认读取是否带出整页图片
115+ database . $on ( 'query' , ( event ) => { queries . push ( event . query ) ; } ) ;
113116 await database . $queryRaw `PRAGMA journal_mode = WAL` ;
114117 await database . $queryRaw `PRAGMA busy_timeout = 30000` ;
115118 mocks . prisma = database ;
@@ -437,6 +440,68 @@ describe('照片自动分题批次', () => {
437440 } ) ;
438441} ) ;
439442
443+ describe ( '批次生命周期' , ( ) => {
444+ const DAY = 24 * 60 * 60 * 1000 ;
445+
446+ it ( '批次中 7 天前已保存的题在批次未完成时不被清理,错题不受影响;独立任务按自身保留期清理' , async ( ) => {
447+ const item = await database . errorItem . create ( { data : { userId : 'user-1' , subjectId : 'math' , questionText : '很早入库的题' , originalImageUrl : '' } } ) ;
448+ const old = new Date ( Date . now ( ) - 10 * DAY ) ;
449+ await database . importBatch . create ( { data : { id : 'batch-long-draft' , userId : 'user-1' , source : 'text' , splitter : 'local' , status : 'started' , mode : 'review' , startedAt : old , createdAt : old } } ) ;
450+ await database . importTask . createMany ( { data : [
451+ { id : 'old-saved-in-batch' , userId : 'user-1' , mode : 'review' , status : 'saved' , imageData : '' , inputText : 'a' , errorItemId : item . id , batchId : 'batch-long-draft' , batchItemKey : 'qa' , finishedAt : old , createdAt : old } ,
452+ { id : 'pending-in-batch' , userId : 'user-1' , mode : 'review' , status : 'review' , imageData : '' , inputText : 'b' , result : null , batchId : 'batch-long-draft' , batchItemKey : 'qb' , createdAt : old } ,
453+ { id : 'old-standalone' , userId : 'user-1' , mode : 'auto' , status : 'saved' , imageData : '' , finishedAt : old , createdAt : old } ,
454+ ] } ) ;
455+ const list = await ( await LIST ( ) ) . json ( ) ;
456+ expect ( list . batches . find ( ( batch : { id : string } ) => batch . id === 'batch-long-draft' ) ) . toMatchObject ( { counts : { total : 2 , saved : 1 , review : 1 } } ) ;
457+ expect ( await database . importTask . count ( { where : { id : 'old-saved-in-batch' } } ) ) . toBe ( 1 ) ;
458+ expect ( await database . importTask . count ( { where : { id : 'old-standalone' } } ) ) . toBe ( 0 ) ;
459+ expect ( await database . errorItem . count ( { where : { id : item . id } } ) ) . toBe ( 1 ) ;
460+
461+ // 全部入库且超过保留期后,批次连同其任务一起清理,错题保留
462+ await database . importTask . update ( { where : { id : 'pending-in-batch' } , data : { status : 'saved' , finishedAt : old } } ) ;
463+ await LIST ( ) ;
464+ expect ( await database . importBatch . count ( { where : { id : 'batch-long-draft' } } ) ) . toBe ( 0 ) ;
465+ expect ( await database . importTask . count ( { where : { batchId : 'batch-long-draft' } } ) ) . toBe ( 0 ) ;
466+ expect ( await database . errorItem . count ( { where : { id : item . id } } ) ) . toBe ( 1 ) ;
467+ } ) ;
468+
469+ it ( '100 个较新的已完成批次不会挤掉更早的草稿,已完成的只列最近 10 个' , async ( ) => {
470+ const base = Date . now ( ) - DAY ;
471+ await database . importBatch . create ( { data : { id : 'very-old-draft' , userId : 'user-1' , source : 'text' , splitter : 'local' , status : 'draft' , createdAt : new Date ( base - 30 * DAY ) } } ) ;
472+ await database . importBatch . createMany ( { data : Array . from ( { length : 100 } , ( _ , index ) => ( {
473+ id : `done-batch-${ String ( index ) . padStart ( 3 , '0' ) } ` , userId : 'user-1' , source : 'text' , splitter : 'local' , status : 'started' ,
474+ startedAt : new Date ( base + index * 1000 ) , createdAt : new Date ( base + index * 1000 ) ,
475+ } ) ) } ) ;
476+ const list = await ( await LIST ( ) ) . json ( ) ;
477+ expect ( list . batches . map ( ( batch : { id : string } ) => batch . id ) ) . toContain ( 'very-old-draft' ) ;
478+ expect ( list . batches . filter ( ( batch : { phase : string } ) => batch . phase === 'done' ) ) . toHaveLength ( 10 ) ;
479+ expect ( list . batches [ 0 ] . id ) . toBe ( 'done-batch-099' ) ;
480+ } ) ;
481+
482+ it ( '状态查询、列表与草稿保存不从数据库读出整页图片,只有明确取图时读取' , async ( ) => {
483+ const bigImage = `data:image/jpeg;base64,${ 'A' . repeat ( 2 * 1024 * 1024 ) } ` ;
484+ const draft = { items : [ { key : 'qa' , label : '1' , text : '题' , material : null , section : null , selected : true , regions : [ { x : 0 , y : 0 , width : 1 , height : 0.5 } ] , hasFigure : false , flags : [ ] } ] , ignored : [ ] , warnings : [ ] } ;
485+ await database . importBatch . create ( { data : { id : 'batch-big-image' , userId : 'user-1' , source : 'image' , splitter : 'ai' , status : 'draft' , imageData : bigImage , draft : JSON . stringify ( draft ) } } ) ;
486+ const selectsImage = ( ) => queries . some ( ( query ) => / [ ` " ] I m p o r t B a t c h [ ` " ] \. [ ` " ] i m a g e D a t a [ ` " ] / . test ( query ) && / ^ S E L E C T / i. test ( query ) ) ;
487+
488+ queries . length = 0 ;
489+ const detail = await GET_BATCH ( new Request ( 'http://localhost/api/import-batches/batch-big-image' ) , params ( 'batch-big-image' ) ) ;
490+ const detailText = await detail . text ( ) ;
491+ const listText = await ( await LIST ( ) ) . text ( ) ;
492+ const patched = await PATCH ( json ( { version : 1 , draft } , 'PATCH' ) , params ( 'batch-big-image' ) ) ;
493+ expect ( patched . status ) . toBe ( 200 ) ;
494+ const patchText = await patched . text ( ) ;
495+ expect ( selectsImage ( ) ) . toBe ( false ) ;
496+ for ( const text of [ detailText , listText , patchText ] ) expect ( text . length ) . toBeLessThan ( 50_000 ) ;
497+
498+ queries . length = 0 ;
499+ const { batch } = await getBatch ( 'batch-big-image' , true ) ;
500+ expect ( batch . imageData ) . toBe ( bigImage ) ;
501+ expect ( selectsImage ( ) ) . toBe ( true ) ;
502+ } ) ;
503+ } ) ;
504+
440505describe ( '批次归属与列表' , ( ) => {
441506 it ( '其他用户看不到、改不了、开始不了、丢弃不了他人的批次' , async ( ) => {
442507 const { batch } = await create ( { source : 'text' , text : '1.计算 1 + 1。\n2.计算 2 + 2。' } ) ;
0 commit comments