From 778a713591fd999cf0ecc5204a3fb137ee345d58 Mon Sep 17 00:00:00 2001 From: Lxy Date: Sun, 17 May 2026 13:50:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__pycache__/models.cpython-311.pyc | Bin 4175 -> 4581 bytes app/__pycache__/schemas.cpython-311.pyc | Bin 5892 -> 6430 bytes app/api/__pycache__/tasks.cpython-311.pyc | Bin 7617 -> 10445 bytes app/api/tasks.py | 90 ++- app/models.py | 3 + app/schemas.py | 10 +- .../__pycache__/cache.cpython-311.pyc | Bin 11741 -> 11804 bytes .../__pycache__/scheduler.cpython-311.pyc | Bin 6995 -> 9831 bytes app/services/cache.py | 12 +- app/services/scheduler.py | 60 +- app/static/index.html | 698 +++++++++++++++++- data/buffer.db | Bin 2019328 -> 73728 bytes 12 files changed, 816 insertions(+), 57 deletions(-) diff --git a/app/__pycache__/models.cpython-311.pyc b/app/__pycache__/models.cpython-311.pyc index 92ecb2a1335558c0c773389d4f81ab90e8f72ea5..fc97c345f0654ba9be2b269d549c1c6e3acee6d9 100644 GIT binary patch delta 828 zcmX@F@Kl*^IWI340}$*A;>_~p-pI$y!IZ-^xq#7oavaCx$)z0P^@1rvYj~EiFfgnJ zVhD)hWnxHW1!9gA;bn|K86bv$6y_9>H5|*Bfnq=m0a1J))v5d`Y(QC246_8_vTP}0 z7_x#saI3^ItYS`)SR;w%G$F8Y!a%E}FdQN>xsXG$o;gJtLq-&;i!D_wOC09#H8N-( zk-(-3MTab!Yb1d>q*CNk7ITC4-YF}-%=is{EYQaCoFq9Vg2(dD@x4}0^2t|UDxxh zV}iz$-72k5b~HR&vk+o0#6XDT(+Pb~cI*l%Doxege3vVSkQ?Bo_6M*|n2 zVYfI-5{t9rODYReZ*dfr=Eav}=B5^b5?7HcNSZsdI6f^iFS9r!HD&T;o?^z=lihij z+Y5+IV4F}jp=?Id#HuM(D_nP2UGOitAW(Wyp!AABX@lnte$kGy32773W&}>moRYah z<$^`%1^%#${9#x4!x}s$zvY!w6_S{sHbZ=c=?>Ko3=Ce3PE0^b*qP}gnEV1J8@xBO j@&z(722Kv=-|i^E7|Zy90XsRt6P7%Y9 z5rXPsOBK!%fvI03KKUc3Xt+QMiYST-2{hZqfaZv&NTx`oNUxDWwLOh7N+LzJg&|5Z zMJ|{@Q+{$Y*HOVz9-w8z_|qvH3N34kM%S}oBqlTS6*IO@F6Uc5IgVdq^Bn#FMqrq|71-`5${5S|fdM-?!Q~@Z<_iw_A|0U7 E0PW;fj{pDw diff --git a/app/__pycache__/schemas.cpython-311.pyc b/app/__pycache__/schemas.cpython-311.pyc index c8267b2303e9369cd1879995d258572ff7815558..7cafdde1dd317392a05c2a139f7ae4a2da0a9481 100644 GIT binary patch delta 1997 zcmaJ?NlaTu7=8}V`wCvzjVY-jr-ZO1D0L7`qAaFmNpYGq)q%2jok02(+9E4N=voht59+6KvGco6rBuzsx`H z|K9KSo9?t)?wQR7feqJb{dJ$|zQt3b^3ua4KZIRu^9$iI=$?Lh#wVe8Xc&{=+i^0S zOvoWDh2n#8rM0O!c!_QK<0OeCIgyaSAQHRAf4N~&LO3j^qKcFpk_iqXS&^j#iC@Pd zMWTnqTBkIa3Wr(jL{wJ7aWcZ=(4hE@#?4MAditZblD4UDi{)%dA6lLDHB88=!_l~` zkXo?ehIJs-bD&zy<(zjt_a4*C<-NV&O~h^EY)K?m6vYc#FTJMi&qeXFDF5Um9ODsM z!A1>m0DuSNen((gkXAG7HB-GlnhV(c;D-LH>UFpgsQ{=1r~=S3$coWOQX$7+%>!41 z#E*TEt5GDR5Fcz79Mleb?X=DKaqggYe*fQ$X-^AiYybxVUI1uh&`bDD4$)st$MaY8 z?|w<739Jil42hDL7-Yr4csyE|Nhhc~>9Bdm!^;Ef5S#g-*^(arOMh0i&aW zdEMMTLHLTz+!)Yf4gJ#cJT_n>)}pcww!KflBC;b#k*x`9n2j@U<3i;{HfC(W8hGEi z!R#x*9=QU7w~{`z^s;0QTN^y@)*Ky)#2Q;Twl#y?&JoFx=xFL7y=--t{<%20Ha@*_ z>${bS57usfLR@szipH`!Mk-9+lA})=Sm;L!3o9SZ{(gU9<<3o?XIs_i;ei@G@mL6F z^@^Mv4l1Jw+?#@GuYP#%kJ-u9>2KGjzh;N5-d$W-yyfxxeLX!{9ZAK4N@N6ktz<6> zUF4YeKFFkzLS*_hFx+H}sYJs#NqnO&#qnZ5dMVev6&kKMs8u@rMC{4uz43L)X27_u@q_gO7mf@sH`Xn zYmtK;>=`rF$dQ*gN0F6r&PD$&_3YpIJa=kzj=2#(fCZq79w_S@OO-MC(f2o3=Vzbg zNLCla?DU#uX(i;y6|5S1LQ7T%a(+3PJORrE8*kyrR12=C*( zXSag`Z)N;z;L&-I-GQI#I!59s6~&$83iAM#CD$HQJKt1ER@ED-bzyIu(Qk0g=Ck_! a7L{XND29pA4cOh>Q9Vb0akYtXiT?md^Uq-b delta 1389 zcmZuwO-vI}5Z;Auw=MmZ0#>k8jM1X77JpD7f0sEG#? z<3Zp_u8(v1|J0i zK1rzRkk{>;$tSck#axd4bw++GdyY-$R%_uB z_eyZb^;m9%4rLCGsj&^~MHnR|mp65r$A!!r5fTVZ1kmChc9XIRx|J9_bWh{OBwpmM z-+raavB^~g&1h|gTk4=g&`bp1)elIphX{6{i-NEPf%xd!EXZDfV7|Ytc1C?dO?Wk*q;i_8>aXSU7_!kmwXbB^1 zMc9VWA|TBv@>clf@4##h|62~Op<`Kc7$ff%P;#9y&3t(fU1;ut+kq)3h%V5AF?kN& z$tk!TY=!oUSjfwK%pDVs&VrZ0O_3_*XR5KLQ>cvU<5DsTM?z`24eo|EsyrgJ#d>%j zidXPDs0tgE>0|&hYE3^yL+P!Qi4w*4=`^AjCCa3%UOi|)I3%|97Vf)hVK~wij>V^H z7Q>wj^jnOkz^iPA*OA>}Nm1ON5uE4MgY}i)zz;PJ7nLa7UDJ4ld6;6Tm_>!eyoTqg zcUqRDCt^Em#77=J%Cl+&v;M0R;;#RfC9uj8KnrsVpWq)U&1Y_G{QgsZP+u4b-f7JmzPbn4ef?{l$cq;oA^#s|UIA<+B~Aw~4R z-VLH3f&r7YZ9$rBJf@QoUJuV|clOiV(-6|Y(Qwe?q_z_~k^Gy-vZr&xG>Un)pPv+T x*f;CrY)joKc_<)@=C6h+?-$fAt(X#iIr>>z4L2@*LG99tDGs03?r>r7KLKM*Ad&z8 diff --git a/app/api/__pycache__/tasks.cpython-311.pyc b/app/api/__pycache__/tasks.cpython-311.pyc index 705e36ecfe4e6f408b8db32e5cd7b898157b8939..c97bc572e370128f4f9a2d09cea26f139e0239ac 100644 GIT binary patch literal 10445 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 delta 3427 zcmbVOYit}>6`ngY`|`fLFYm7R8QZbFS=;M`(4?fKuY@M1V;tK?SPTxBbIN?yfVYF3&{KE+q^EB;bI36z3L zkgwZ|p;C*|QVJ{KQbdWAqDqtqqM(t{R&)Y~7P~?O;VL|}p~PWbQXOj3Fj;n95n&CU zjkSa(C6e5`x?#6ZaQq~-e_J)Y+(VVJRvkl2=^GlX1VCw3oK|x7riKR3e zJOT#YpV1&16II_m#&&?Q4e$J)*}H(&w&7jy9)s;$21Eac!Iparc7Va1wc~~x9u!DQQZ~+csCH$dVmG z)Mh0Dhz^Vv`?o(pLhM8&A&C>;Vbu1F^!K;t*t@n)Ib)%%>>sw>iHwutPt!O;0wIZz zLh!L^dw)!(QJi4vo3c^RpQAfK)X^>Wd;5ixT_CifW)fglxa$&J;i}VJ_4?-~tNzeJ zZ?!qF;H*aDm1ys})9SL`763j*m_2mYFL*-pPcMA^l3GcAZq>cF;@->t>Nw)_fiWm? z;m%&Dqea0W?45Y4JWULVrJd2NI4nQ_$RXkM2h;MDJX$X{8Z_!5fl$h$GD};-HeiQo z%ap`&@s=kIH`v`6xM7=I{}lB0ZFTxja%HHtofNZscKRI=Q491!F=tn+B;?Ff=t^FwCZjadkt^h^7T| zM~By#Q7C2t6azwo2t5b^gfxI@EsT$AWtC!>Yu-FjPfXLIj~7RbT7LH7ZpVl|HZ(>{ z1%u}BgMNhF2-q=9si>7RR-V0d5Bq~FHPZ`f9SbbY2kpddp5h3crcf;AxtH_+C?CX) z9-w$gYOCN5&(jOX=8nxC{?M1Wm|RHC4*ba%m>sCP{p*6q;^c6iRCi^UGb{dsukJ?6bN$PcE1}2dPcA4oLzx?)%xWlG z31wG8+3Q9%5`E2G4M!HHZiahrgnL)Rxk@=e(#zZmH!ur#Y-E?khb6Qlo!Okk-_w zIrs#ueiL`hviDmaVMn5^j#W8Zk+UmumXW5kNW0l9(f`cc#|P6^(saErtW982rq_P; zi?#DhAN}xhM&9Nd4_q^>-^Y)jsueXu%WKa=onWhUwPoL z%1lQ-KL`Y%=kumi9y>>|6;K}1t+M7P+lo5o1)QAc1vj*n95O6Ha%48r!&_sJ{7Bi* z=(h_+dJNWIgh&4^0G!yvywQE^Y;vHv|8>vOxz$Ln63ML#qNSg`mux+-YR^^dxfOeE z%^A5ATy-WZ&g6=mq?ppCm1@QWEf3cJ#DNm?78CT2!QQnb*mqMM5>P(ti0??Lq>*Jg z;|ujx%|1%SSkfAaPYGKMf=0K`c)Mt`1M7>RcCr)cpvZOYZeg_Elxn+XmZcj1gEM>p ziOZAT<4=7h-Fq;7;sv!(Hbw^NE?7H8vw(0toG{ZMf0DyLk2)Udtvl&M-2(`Xw3#8i zOw&4srFIRq;=o&D@aUHTW(9V-Z68~1dw{LAwatWG*wz7nL|j%N5sQ__IFm7L`Mf$d z2-#sPjtviE=dcYy=O~PiPz*Gc5s)}ckAAjTC=Py0E4|PMgiev8O&2tSKIkHaGX-6v z*q==&s&w2}&?$ClYUL+yVn1zYLdg+vCkV3wb{Bh7)AYj@;L} z(5y798Jzb`3DS+z5yg@>t+m>tSZzE3Ys4n-6LK2+m;IsAm^x9^4$?EQ2_vVUlGsv5 zoW0-in7A&ozMWm{#7+x5ANB`GYFz+WiSFX>pWFCr3vNq-xT``k>mqLLVW&HH^tF(~ rWL-d*BsfgAZ0;Ai+R;UeO1 diff --git a/app/api/tasks.py b/app/api/tasks.py index 4c816be..72be69c 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -2,11 +2,13 @@ 定时任务接口 - 创建/启动/停止/删除/列表 """ import logging +from typing import Optional from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from app.database import get_db +from app.models import ScheduledTask from app.schemas import ( CreateTaskRequest, TaskInfo, @@ -35,18 +37,23 @@ router = APIRouter(prefix="/tasks", tags=["定时任务"]) def create_new_task(req: CreateTaskRequest, db: Session = Depends(get_db)): """ 创建并启动一个定时采集任务。 - 输入品种合约和轮询时长,自动开始定时获取数据。 + 输入品种合约和轮询时长,自动开始定时获取数据。 """ + # 将periods数组转为逗号分隔的字符串 + periods_str = ",".join(req.periods) if isinstance(req.periods, list) else req.periods + task = create_task( db=db, symbol=req.symbol, data_type=req.data_type, - periods=req.periods, + periods=periods_str, interval_seconds=req.interval_seconds, + task_type=req.task_type, + run_time=req.run_time, ) # 注册到调度器 - job_id = add_job(task.id, task.interval_seconds) + job_id = add_job(task.id, task.interval_seconds, task.task_type, task.run_time) task.job_id = job_id db.commit() db.refresh(task) @@ -56,30 +63,63 @@ def create_new_task(req: CreateTaskRequest, db: Session = Depends(get_db)): @router.get("", response_model=TaskListResponse) def list_all_tasks(db: Session = Depends(get_db)): - """列出所有定时任务""" - tasks = list_tasks(db) + """列出所有定时任务(未完成的)""" + tasks = db.query(ScheduledTask).filter( + ScheduledTask.is_finished == False + ).order_by(ScheduledTask.created_at.desc()).all() job_status = get_all_jobs() task_infos = [] for t in tasks: - running = is_job_running(t.id) if t.enabled else False - task_infos.append(TaskInfo( - id=t.id, - symbol=t.symbol, - data_type=t.data_type, - periods=t.periods.split(",") if t.periods else [], - interval_seconds=t.interval_seconds, - enabled=t.enabled, - running=running, - last_run=t.last_run.isoformat() if t.last_run else None, - last_status=t.last_status, - created_at=t.created_at.isoformat(), - updated_at=t.updated_at.isoformat(), - )) + job_id = f"task_{t.id}" + job_info = job_status.get(job_id) + + task_infos.append(_to_task_info(t, job_info)) return TaskListResponse(tasks=task_infos, total=len(task_infos)) +@router.get("/history", response_model=TaskListResponse) +def list_finished_tasks(db: Session = Depends(get_db)): + """列出已完成的历史任务""" + tasks = db.query(ScheduledTask).filter( + ScheduledTask.is_finished == True + ).order_by(ScheduledTask.updated_at.desc()).all() + + task_infos = [] + for t in tasks: + task_infos.append(_to_task_info(t, None)) + + return TaskListResponse(tasks=task_infos, total=len(task_infos)) + + +@router.post("/{task_id}/rerun", response_model=TaskInfo) +def rerun_task(task_id: int, db: Session = Depends(get_db)): + """重新执行已完成的任务""" + task = get_task(db, task_id) + if not task: + raise HTTPException(status_code=404, detail=f"任务 {task_id} 不存在") + + if not task.is_finished: + raise HTTPException(status_code=400, detail=f"任务 {task_id} 尚未完成,无法重新执行") + + # 重置任务状态 + task.is_finished = False + task.enabled = True + task.last_run = None + task.last_status = None + db.commit() + db.refresh(task) + + # 重新注册到调度器 + job_id = add_job(task.id, task.interval_seconds, task.task_type, task.run_time) + task.job_id = job_id + db.commit() + db.refresh(task) + + return _to_task_info(task) + + @router.post("/{task_id}/stop", response_model=TaskInfo) def stop_task(task_id: int, db: Session = Depends(get_db)): """停止定时任务(从调度器移除,但保留配置)""" @@ -101,7 +141,7 @@ def start_task(task_id: int, db: Session = Depends(get_db)): raise HTTPException(status_code=404, detail=f"任务 {task_id} 不存在") enable_task(db, task_id) - add_job(task.id, task.interval_seconds) + add_job(task.id, task.interval_seconds, task.task_type, task.run_time) db.refresh(task) return _to_task_info(task) @@ -139,23 +179,29 @@ def update_interval( # 如果任务正在运行,更新调度器 if task.enabled and is_job_running(task_id): remove_job(task_id) - add_job(task.id, task.interval_seconds) + add_job(task.id, task.interval_seconds, task.task_type, task.run_time) return _to_task_info(task) -def _to_task_info(task) -> TaskInfo: +def _to_task_info(task, job_info: Optional[dict] = None) -> TaskInfo: """ORM -> Pydantic""" + next_run = None + if job_info and job_info.get("next_run_time"): + next_run = job_info["next_run_time"] + return TaskInfo( id=task.id, symbol=task.symbol, data_type=task.data_type, periods=task.periods.split(",") if task.periods else [], interval_seconds=task.interval_seconds, + task_type=task.task_type if hasattr(task, 'task_type') else 'interval', enabled=task.enabled, running=is_job_running(task.id), last_run=task.last_run.isoformat() if task.last_run else None, last_status=task.last_status, + next_run=next_run, created_at=task.created_at.isoformat(), updated_at=task.updated_at.isoformat(), ) diff --git a/app/models.py b/app/models.py index 1e0fa98..d3a9a0b 100644 --- a/app/models.py +++ b/app/models.py @@ -37,7 +37,10 @@ class ScheduledTask(Base): data_type = Column(String(16), nullable=False, default="futures", comment="数据类型") periods = Column(String(256), nullable=False, comment="周期列表(逗号分隔), 如 5min,15min,60min") interval_seconds = Column(Integer, nullable=False, default=300, comment="轮询间隔(秒)") + task_type = Column(String(16), nullable=False, default="interval", comment="任务类型: interval, daily, once") + run_time = Column(String(8), nullable=True, comment="执行时间,格式 HH:MM") enabled = Column(Boolean, nullable=False, default=True, comment="是否启用") + is_finished = Column(Boolean, nullable=False, default=False, comment="是否已完成(仅一次任务执行完成后为True)") job_id = Column(String(64), nullable=True, unique=True, comment="APScheduler job_id") last_run = Column(DateTime, nullable=True, comment="最后执行时间") last_status = Column(String(16), nullable=True, comment="最后状态: success/failed") diff --git a/app/schemas.py b/app/schemas.py index 6545c4b..a868980 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -70,9 +70,9 @@ class CreateTaskRequest(BaseModel): """创建定时任务请求""" symbol: str = Field(..., description="品种合约代码") data_type: str = Field(default="futures", description="数据类型") - periods: List[str] = Field( - default=["5min", "15min", "30min", "60min", "daily"], - description="需要定时获取的周期" + periods: str = Field( + default="5min,15min,30min,60min,daily", + description="需要定时获取的周期,逗号分隔" ) interval_seconds: int = Field( default=300, @@ -80,6 +80,8 @@ class CreateTaskRequest(BaseModel): le=86400, description="轮询间隔(秒),范围 30~86400" ) + task_type: str = Field(default="interval", description="任务类型: interval, daily, once") + run_time: Optional[str] = Field(default=None, description="执行时间,格式 HH:MM") class TaskInfo(BaseModel): @@ -89,10 +91,12 @@ class TaskInfo(BaseModel): data_type: str periods: List[str] interval_seconds: int + task_type: str = Field(default="interval", description="任务类型: interval, daily, once") enabled: bool running: bool = Field(description="当前是否正在运行") last_run: Optional[str] = None last_status: Optional[str] = None + next_run: Optional[str] = Field(default=None, description="下次执行时间") created_at: str updated_at: str diff --git a/app/services/__pycache__/cache.cpython-311.pyc b/app/services/__pycache__/cache.cpython-311.pyc index 4c85702412754d78ed7f5e7d77b636aafbecd633..8c19002b5e670acbe07810661cdc199dc5d576e6 100644 GIT binary patch delta 1342 zcmaJLDrY7| zVI%?jP`qH}y!cSTJlG(uP?)EJf)Bp?Qs}T$Xp!oR49YdyF1pb)$}hYXL8T#ovzEpx#hFHOysp3zWREXIX%N&cFwfix8C4AU2Jw+ zZfAq9YJ-Uom>(fA*!PGc?^NgIdf~`P&C^8Ni+Nfr9-Z{!5G4jt0wQ$~sSph`nF5g> zac(y92w6|covB<>YYpw?z;m@0!8JqqWv%7iHM6tp{5R#jmzgt_RK;<6DBYsg-}Ik zA}k=h0?^ZVcX{P3XN|Oc(H}7V_dWWUj$>{sh=2;h3B&KXJfFOaw*yf__7v2x^DXP6p*V+CosOq0P%r z%FkH2Am0c^i@``f$c%+)eOUdO0Q`cmeJmXH^-6~(S|EMXKUbZh2bZvv0$^GFX7n}g z_B&yay@m84d9`-P|HFET(iQofU3}y?WCgyO$RV6SIEru*;PY62EK8VJlposF#B1Q* zk-N30{9o;N=%XtTE{HbrYXA-^!9@0k(Vu?5g>_(l@^@?RVdcl_4Z0>@tydBokpA7@ zuJ2F{wdNZJzLb1?dUj|DV$nrdl$+CaRe(#W+?n2rDdJ^WY7fK6rQoY>y6iTHaPL$+Up;TlMMz`siJT}5rS4dfKUK6gbK^ej3bBOkgyGa)AobL!~_+4u})?1R`VwAyq=+yeJ5v_RzlP#0dz*Nb}A2-nZ|) zZ)Sh{{>ACxKO&J3fmJ`R%|uN?USiN&kqxr;^|j%*G%qc>A8>+>jWUggSRaqE5bS;) zzhgsrsKrYeLLh^E8GTW_5bv2Dgw!W{cEDgH#8Qlau_Zd9{dj36q z2zq8AS6I&_VW3|Lr4&~ zANE?sCkw+z<&E@LLB&zTS>}^VR$ML(8hah3x>@1`AX0k`m>(i+(OvC<9wTI?@A^>O z0JZ!6n0Lr6U0xh6$~jY!@5~{o=|++gM>$T~=wM?x)bVZ2jynP7|1)q0=HJ`&@RKU% z&6Iqq4+K7H`b6KRIx5^=kw-Gc9skhE30o2Pt3?zchcFJX8syE|53`)vQF*L9G!U4X zYc#iHaZ*0c?Aj9)VJ!Gmy~clRwAzj4^lG48Yn^T3Gss=p^uTE#f5C432e3#yLr!E5 z?mBV~0S=)XkXA6bwG>_<6#aDng9y{Fs>6TIt5bFx@CgYRvR@@>moI52Tguz7cGjTOCZ9bY6HZQt)EtDYQP9kw6k}l z6uK!;6#3}h-R$hn?CkE`%L41lE`C`u@pO26PF-~wq zfZ#~ZAdmr4s|^7I)RaI)sQ?wF12l>0v|x;y0;Z@rVAk43!4kCwtQu|-Y*Bl_uHj~( zD4KKx99qL76i1x_r-oYvCR!3G(QupKin;@C4Yv!Ps5ju%@FIbYmIg{S+#!@j%LC=n zia>?dE*2`IRe`E#b)cFg3yl!;DblwN6<=bz>X7Pen5)EV}3KLz&!@;2&y6oE1}X@7^;`M zt#4Pi0m%oS_%i&VVdAchaM!-tGS)Ku*rLHNv$jL*PLip8!4Z* z4U@$(W#1iGG!cyF>^IZwP3KC*xpJmqfj_dbqp%~%`n|rrdwgU@5%e@e>=3xLww}h(F)adH@TZ8 zu6t~q4^Y=@e4S0y_4bm^62lFK1pG#ct+U2>qtbx!Y7*l$6vpd}FzQB=5k}p3fCBtC zG`qWk8ZBn!_ljnkSHtd63xFRJp93V&1^}v4ltM@fityjKL4il819}h0mmIsvt@1w{ zpFXw;vu?($7O{U&;^N0+Xh{@m$JDJDXwr&BDL(KeVLO2Gv|R6e@>$HIS|TwCOeG|! zOdbo0d^jHCMD&oxfCghh%^OXKmj+QROhb3RSOCF>{Q2*o7lJB@&HTJ!h?YNdHITHd zI$M);VHkpcN+b-NA?KHGBq%f?neze(;^>lmj!IC4fl72s`7jOxI66UrQyI@wg)^Kc zCF}^f!bsS@b0tBgEt=0d>5^oj>}S9Vhm1oe&YUoY4JQo8w77~)oF#|3Vg_(kC(um= zbgNDmy)hC($CuY<)7EGIiuJi+J<0rdO+%K1C0&I7VUkE-&6zp#l=A`@%{xnmGh|Iz6ZzlfPFSVN zoD8TqW~!vnTuwpnkX5Q)=1ExLf4BEy|axc#%Mb3gd5Z&iMLvyTf!gp$S7<>1%V>&iOh!+n#vBx9axToD&te(+1b+FSj6bOwHOeVR&okUN z9vN!WVB;VQiptB^#Vw4ze&@aS+I)jJ&@}tzu8iHfd_|w*3$hL2{9~aMgN*`zfNI#99#53*(pqlj1uG*uKFpBGq1svxf zouawWNzo7K7hW8sU|l>qpi|8G0|C3pAofY0uF!>VzkK_HcbB%0$<4l7AN&gRx^Q*k z&bx2xfA!AA3v=gQoqK(9uiRSFmozu~a5k~|lnoF)o7?R7B7CZ;X2?CXOrjOH42V~a z(U27GS8Y#)gh74>LUE*0(a?ZOfeoNFz=d#tK==?+saX8DYDWBk5DN1MhXaJ25#dux zKOrGONy024j?-qrDTsCe`|Vn$f_C8!eD0x60ICU+nqY)m)*w2JX}uav)02eOLqo(- zKW}_cYez5Y$GjLk2cP(vmgOu~$Q7;)$wiaTxM{}jnK0k9*DChfbSHp0w|8v4Qq?@! zpK-6ty4Nidq~jnt%leeM1D96;(BBN(m1VmWrfaUU=De8hytwzRz0<9k#&y}ob(zZb z*~;}V?MZDPX`1nPCp;tjM)r-umv&*OKa*r~@+oE226%63a2n~g-lt8-PoGfgc>T?= zy;-(bVS4A-y0kRC^TVLRc4ye`EZd#hmD=@*gPmx)>F_BIU-}S$|6&_Syp^wEAOGVv zT*->DW}IrKw#|6U#t)AkPIJ?1Kde*s9~e2D@pfgsU8$WjPVa>K%us6Rlk(ah`KL>i z`qoT+YZhR6>xgNVt(#nXd7r}W$*_B}>>jL$yC$NvNom=luFl#A3e6HwBk>Y)J;y3_!FYniDfEi0q*3zR`dS+d9%IY@7wK3z`n00MTnU|k? zP!^3K6zaPc!dUzIT z{DfErrg?RzXBR_UXKbBb>Uzb)oeXt@F#<0C*7Xo8H2avT8{wf>sgQ7{y4Pv`VE@HqvxOjIKCzzEJVc7GUq^QG2Az> z2ZE=?BC$Rc1KLmJ9`D2C)=MMaRx;UxhYetW{nBsIgNP*;L_{r|gd_#!&R^HMP+i8zA5UUYX>i?G;`AmAm5=s3{GOY*kT(_6Lz z3VYZI)n5M^)f^0R@o+GxnuT~@A6|C|C|ZQlVv=ft@Q1sNxZ5OuR=Scj%jIPa9XL*@ z<)MM4)W0PcAK@AYS6?mC{RcX}Gb5Y4xy+Zuu}CczpBF*`Tp^+-TVTtm2zM6Yn;~$9 zj)dTTs9hG>!pmY@5cseZNBW&`6<*--P0)sU;cCzu=|lK}tzQ?dpq4$bQ68z5U@#U+ zM)_b+w-C*0c+9B<+7682`v<}qixwGG`ba!3=thPAE}TMWxvPh96xo9wuGQ?=Liv{ZhZUmWohfVrRZf?C zR}9cwVPF3OJhm1Q delta 2112 zcmZWqYitx%6ux(6c4yy@?sofbx80Vq23Mfv(Fm0BCi06Ie~Hz`m>3g1XS!`@y*u~2=XKAV zIp^HFza0F2>(r;gppU@VSoDlv2ov%MndRZG5Q{HULOviWQHV-aL8l6ZRn!GTPy~uH zNf!-CkqnpOa(I{SHe^M1Xt(Y$yo%SMW!-1^6~9A!^nfuHRDurU)k8*D2|Kh;j~G!U z>d=0@%7`g3Bd)|9KA=|{2_<3FC^eJ_M5E)i%h76evk>NWm`g*c2feHY0sUIdTxhkn zK(&M#o>C(p33Jg|0XD*{Y-~`gw8l|Ejh&H{CN+MBD9vg$U<=CC1k4&OIqFtxLEq}g zYP7adNv#7u^#orJd^^}mT2kv673!8>K)`nc=+>z&;V4Nr@-OKwKeat-&eE)cq4Ai| zce5;nsql|@b4I*H=>fjSb&+3k$7q<(xSD8`|L(5lKe;0`!hdzw0WZs)w4FaAS9EgK zB)Lq~7%#}_n((X$-v@JPP*RMfh}F)o$(LxH_jzXM1i$4u3tk4jef)Fpk;WLquCS|e z<#NW<*vUd(Gc);I{)EQrc!RIw)n-(?%(7mvY^hKjEwM(BFh4>Q0`wBG#WBrd0c3*+ zZ7B2ZJ(btWR-sf(%d7?^f;z%5H@0Nu%-2{mzvug;X%h(0k=YG(nI?bwNUY_(%*D*~ z?ggQhpYspLw-T@ayytDtIo~_Jv(9g!i~r@Hrfz;JuwR@H?Bxx?KZf>hA-9tKaaeA5 z(}50hdy8)%Al~jv4#?u2WOUzV;m#(C^k(0_KG&Tc0@A${={^zYyMhaL-IZOi>ux|q zx+5|0ELTInZRmgiSsLIPSp+0(BY+(?tsJwmCS*&U&^5-kgLHsTh4<5K{G0Hj-Cbzf zjiw&+#Dt}m-YBvaS8OY4w;?#*LZ($JKjCZ#Xb$tokt6+RV@C=_3ksCev!<3W6;;!A zGtHV{#r#S?gP8{=)_-yR(n+S+9an@m1%5f2q5}Ua+SMYgJ|L7>uwi*z6`|weiiM6U z!@^=$#w49vvwf0`(MdrSmtLewbFNvqPOdLsDFobOV^qdfna@-uIwu9_S$UEIdak(E z#xseB#XIpl;l~ph{z^^CpQg54`E34e?bwumLvlGCJ8f#FJq%NA%g~0|@zRLxW13Mq zsX4TqQ&l7wb{dlrR%(zIoz7%Cakm?cvIW(|{3Vk~rz-3q+9SLK)4TyNO%`Z~ch~nc z{4523kgC6vsvibxA4Xc{*KePX^xTj1Jc#u0uN$|lAG(p4j|}0{f2=}8hVDzl52WFF zY4}Hfbo!7}f8N{lm_Em!Zf@V@bj%>W^8mvC0zCv$TZBMYN9hpsQoNL7JAi(2x{fiJ zOaa{#|DyG+4cPbTPFv1q)lxp2wPn3DHiq$Gg>1KVx{M6^VlIBNtrISUkK0m%SYo?A zS6(@n8SRvopRh{I%#=$dondd=A@g-Tr^C@TPG?HYU|tjk@E(NQIX9An+t9}kq#D;@ z_p=nj$^~cp&<;-FXkmhYz;*^11eV4V^UhVQhiGvKEf< h^Bpbe1g*Mobb-L~*&b)Pv9vh!6R?>tc9bP7!GDce@6P}L diff --git a/app/services/cache.py b/app/services/cache.py index de9eea8..423a4c0 100644 --- a/app/services/cache.py +++ b/app/services/cache.py @@ -177,16 +177,20 @@ def create_task( db: Session, symbol: str, data_type: str, - periods: List[str], + periods: str, interval_seconds: int, + task_type: str = "interval", + run_time: Optional[str] = None, ) -> ScheduledTask: """创建定时任务配置""" existing = db.query(ScheduledTask).filter_by( symbol=symbol, data_type=data_type ).first() if existing: - existing.periods = ",".join(periods) + existing.periods = periods existing.interval_seconds = interval_seconds + existing.task_type = task_type + existing.run_time = run_time existing.enabled = True existing.updated_at = datetime.now() db.commit() @@ -196,8 +200,10 @@ def create_task( task = ScheduledTask( symbol=symbol, data_type=data_type, - periods=",".join(periods), + periods=periods, interval_seconds=interval_seconds, + task_type=task_type, + run_time=run_time, enabled=True, ) db.add(task) diff --git a/app/services/scheduler.py b/app/services/scheduler.py index a451834..a385ee5 100644 --- a/app/services/scheduler.py +++ b/app/services/scheduler.py @@ -2,11 +2,14 @@ 调度服务 - APScheduler 管理定时采集任务 """ import logging -from datetime import datetime +import re +from datetime import datetime, timedelta from typing import Dict, Optional from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger +from apscheduler.triggers.cron import CronTrigger +from apscheduler.triggers.date import DateTrigger from apscheduler.executors.pool import ThreadPoolExecutor from sqlalchemy.orm import Session @@ -54,6 +57,14 @@ def job_handler(task_id: int): save_market_data(db, task.symbol, result) update_task_status(db, task_id, "success") logger.info(f"[定时任务] {task.symbol} 采集成功") + + # 如果是一次性任务,标记为已完成并从调度器移除 + if task.task_type == "once": + task.is_finished = True + task.enabled = False + db.commit() + remove_job(task_id) + logger.info(f"[定时任务] {task.symbol} 一次性任务已完成") else: update_task_status(db, task_id, "failed") logger.error(f"[定时任务] {task.symbol} 采集失败: {result.get('error')}") @@ -82,10 +93,16 @@ def stop_scheduler(): logger.info("调度器已停止") -def add_job(task_id: int, interval_seconds: int) -> str: +def add_job(task_id: int, interval_seconds: int, task_type: str = "interval", run_time: Optional[str] = None) -> str: """ 添加定时任务到调度器。 + Args: + task_id: 任务ID + interval_seconds: 间隔秒数 + task_type: 任务类型 (interval, daily, once) + run_time: 执行时间,格式 HH:MM (用于daily和once类型) + Returns: job_id """ @@ -95,15 +112,50 @@ def add_job(task_id: int, interval_seconds: int) -> str: if scheduler.get_job(job_id): scheduler.remove_job(job_id) + # 根据任务类型选择trigger + try: + if task_type == "daily" and run_time: + # 每天定时执行 - 验证run_time格式 + if not re.match(r'^\d{2}:\d{2}$', run_time): + raise ValueError(f"run_time格式错误: {run_time}, 应为 HH:MM") + + hour, minute = map(int, run_time.split(":")) + if not (0 <= hour <= 23 and 0 <= minute <= 59): + raise ValueError(f"run_time值无效: {run_time}, 小时应为0-23, 分钟应为0-59") + + trigger = CronTrigger(hour=hour, minute=minute, timezone="Asia/Shanghai") + + elif task_type == "once" and run_time: + # 仅执行一次 - 验证run_time格式 + if not re.match(r'^\d{2}:\d{2}$', run_time): + raise ValueError(f"run_time格式错误: {run_time}, 应为 HH:MM") + + hour, minute = map(int, run_time.split(":")) + if not (0 <= hour <= 23 and 0 <= minute <= 59): + raise ValueError(f"run_time值无效: {run_time}, 小时应为0-23, 分钟应为0-59") + + now = datetime.now() + run_dt = now.replace(hour=hour, minute=minute, second=0, microsecond=0) + if run_dt <= now: + # 如果时间已过,设置为明天(使用timedelta避免月末日期计算错误) + run_dt = run_dt + timedelta(days=1) + trigger = DateTrigger(run_date=run_dt, timezone="Asia/Shanghai") + else: + # 默认:间隔执行 + trigger = IntervalTrigger(seconds=interval_seconds) + except ValueError as e: + logger.error(f"任务 {task_id} 时间配置错误: {e}, 使用默认间隔触发器") + trigger = IntervalTrigger(seconds=interval_seconds) + scheduler.add_job( func=job_handler, - trigger=IntervalTrigger(seconds=interval_seconds), + trigger=trigger, args=[task_id], id=job_id, name=f"auto_collect_{task_id}", replace_existing=True, ) - logger.info(f"已添加定时任务: job_id={job_id}, interval={interval_seconds}s") + logger.info(f"已添加定时任务: job_id={job_id}, type={task_type}, trigger={trigger}") return job_id diff --git a/app/static/index.html b/app/static/index.html index e66d5ba..db74328 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -924,29 +924,16 @@
-
批量创建定时任务
-
为配置中的所有品种自动创建定时采集任务
+
定时任务管理
+
创建、管理和监控定时数据采集任务
-
-
-
- - -
-
- - -
-
- - -
-
- +
@@ -956,15 +943,53 @@
任务列表
- + +
+
+
+
+ +

暂无定时任务

+
+
+
+
+ + + + + + + + + + + +