From fe73da9cf5e16b9dab8229edb6199549ad5a67df Mon Sep 17 00:00:00 2001 From: lhx Date: Fri, 26 Sep 2025 15:58:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 12 ++ .gitignore | 3 + README.md | 75 +++++++ app/__init__.py | 0 app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 123 bytes app/__pycache__/main.cpython-312.pyc | Bin 0 -> 3989 bytes app/api/__init__.py | 5 + app/api/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 299 bytes app/api/__pycache__/account.cpython-312.pyc | Bin 0 -> 3234 bytes app/api/__pycache__/database.cpython-312.pyc | Bin 0 -> 3332 bytes app/api/__pycache__/task.cpython-312.pyc | Bin 0 -> 7746 bytes app/api/account.py | 60 ++++++ app/api/database.py | 56 +++++ app/api/task.py | 187 +++++++++++++++++ app/core/__init__.py | 0 app/core/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 128 bytes app/core/__pycache__/config.cpython-312.pyc | Bin 0 -> 1284 bytes app/core/__pycache__/database.cpython-312.pyc | Bin 0 -> 1132 bytes app/core/config.py | 18 ++ app/core/database.py | 20 ++ app/main.py | 95 +++++++++ app/models/__init__.py | 3 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 199 bytes .../__pycache__/account.cpython-312.pyc | Bin 0 -> 1411 bytes app/models/account.py | 15 ++ app/schemas/__init__.py | 14 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 711 bytes .../__pycache__/account.cpython-312.pyc | Bin 0 -> 1672 bytes .../__pycache__/database.cpython-312.pyc | Bin 0 -> 2075 bytes app/schemas/__pycache__/task.cpython-312.pyc | Bin 0 -> 2264 bytes app/schemas/account.py | 42 ++++ app/schemas/database.py | 31 +++ app/schemas/task.py | 41 ++++ app/services/__init__.py | 4 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 268 bytes .../__pycache__/account.cpython-312.pyc | Bin 0 -> 3851 bytes .../__pycache__/database.cpython-312.pyc | Bin 0 -> 7521 bytes app/services/account.py | 51 +++++ app/services/database.py | 195 ++++++++++++++++++ app/utils/__init__.py | 3 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 260 bytes .../__pycache__/scheduler.cpython-312.pyc | Bin 0 -> 6184 bytes app/utils/scheduler.py | 124 +++++++++++ main.py | 11 + requirements.txt | 10 + 45 files changed, 1075 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-312.pyc create mode 100644 app/__pycache__/main.cpython-312.pyc create mode 100644 app/api/__init__.py create mode 100644 app/api/__pycache__/__init__.cpython-312.pyc create mode 100644 app/api/__pycache__/account.cpython-312.pyc create mode 100644 app/api/__pycache__/database.cpython-312.pyc create mode 100644 app/api/__pycache__/task.cpython-312.pyc create mode 100644 app/api/account.py create mode 100644 app/api/database.py create mode 100644 app/api/task.py create mode 100644 app/core/__init__.py create mode 100644 app/core/__pycache__/__init__.cpython-312.pyc create mode 100644 app/core/__pycache__/config.cpython-312.pyc create mode 100644 app/core/__pycache__/database.cpython-312.pyc create mode 100644 app/core/config.py create mode 100644 app/core/database.py create mode 100644 app/main.py create mode 100644 app/models/__init__.py create mode 100644 app/models/__pycache__/__init__.cpython-312.pyc create mode 100644 app/models/__pycache__/account.cpython-312.pyc create mode 100644 app/models/account.py create mode 100644 app/schemas/__init__.py create mode 100644 app/schemas/__pycache__/__init__.cpython-312.pyc create mode 100644 app/schemas/__pycache__/account.cpython-312.pyc create mode 100644 app/schemas/__pycache__/database.cpython-312.pyc create mode 100644 app/schemas/__pycache__/task.cpython-312.pyc create mode 100644 app/schemas/account.py create mode 100644 app/schemas/database.py create mode 100644 app/schemas/task.py create mode 100644 app/services/__init__.py create mode 100644 app/services/__pycache__/__init__.cpython-312.pyc create mode 100644 app/services/__pycache__/account.cpython-312.pyc create mode 100644 app/services/__pycache__/database.cpython-312.pyc create mode 100644 app/services/account.py create mode 100644 app/services/database.py create mode 100644 app/utils/__init__.py create mode 100644 app/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 app/utils/__pycache__/scheduler.cpython-312.pyc create mode 100644 app/utils/scheduler.py create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.env b/.env new file mode 100644 index 0000000..74dc564 --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +# 数据库配置 +DATABASE_URL=mysql+pymysql://railway:Railway01.@8.134.75.237:3309/railway +DB_HOST=8.134.75.237 +DB_PORT=3309 +DB_USER=railway +DB_PASSWORD=Railway01. +DB_NAME=railway + +# 应用配置 +APP_HOST=0.0.0.0 +APP_PORT=8000 +APP_DEBUG=True \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..313697d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv/ +*.md +!README.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c530481 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# 铁路项目管理系统 + +基于FastAPI、MySQL、SQLAlchemy的铁路项目管理系统。 + +## 功能特性 + +- **账号管理**: 账号的增删改查,包含id、账号、密码、状态、是否更新、标段等字段 +- **数据库管理**: 表数据导入导出、SQL执行、建表删表等数据库操作 +- **定时任务**: 支持cron、间隔、指定时间的定时任务调度 + +## 技术栈 + +- FastAPI: Web框架 +- SQLAlchemy: ORM框架 +- MySQL: 数据库 +- APScheduler: 定时任务调度 +- Pandas: 数据处理 +- Pydantic: 数据验证 + +## 项目结构 + +``` +app/ +├── api/ # API路由 +├── core/ # 核心配置 +├── models/ # 数据库模型 +├── schemas/ # Pydantic模型 +├── services/ # 业务逻辑层 +└── utils/ # 工具类 +``` + +## 安装运行 + +1. 安装依赖: +```bash +pip install -r requirements.txt +``` + +2. 配置环境变量: +复制 `.env.example` 到 `.env` 并修改配置 + +3. 运行应用: +```bash +python main.py +``` + +## API文档 + +启动后访问 http://localhost:8000/docs 查看API文档 + +## 主要API端点 + +### 账号管理 +- `POST /api/accounts/` - 创建账号 +- `GET /api/accounts/` - 获取账号列表 +- `GET /api/accounts/{id}` - 获取单个账号 +- `PUT /api/accounts/{id}` - 更新账号 +- `DELETE /api/accounts/{id}` - 删除账号 + +### 数据库管理 +- `POST /api/database/execute` - 执行SQL +- `POST /api/database/table-data` - 获取表数据 +- `POST /api/database/create-table` - 创建表 +- `DELETE /api/database/drop-table/{name}` - 删除表 +- `POST /api/database/import-data` - 导入数据 +- `GET /api/database/tables` - 获取表列表 + +### 定时任务 +- `POST /api/tasks/cron` - 添加cron任务 +- `POST /api/tasks/interval` - 添加间隔任务 +- `POST /api/tasks/date` - 添加定时任务 +- `GET /api/tasks/` - 获取任务列表 +- `DELETE /api/tasks/{id}` - 删除任务 +- `PUT /api/tasks/{id}/pause` - 暂停任务 +- `PUT /api/tasks/{id}/resume` - 恢复任务 \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8350c9b79f7eb14db54e39aa4aedee9fef90fcc4 GIT binary patch literal 123 zcmX@j%ge<81nm6RGC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!l5(+%DTpb_OwB2c wNh~OciI30B%PfhH*DI*J#bJ}1pHiBWYFESxRLTg%#URE$?7Bt!-!w61ujsS233Qh zc^<7eRuV43$c2b|XG8W7`5CzL@~bR$w%`nQ=#1sFSxJ5-d<6% zPP=-Vc5OEwd}@NJ1YM^4C0RFKd}W>K``Om5ul#g(lyn+uA|Ax1zh%?rEibEzqLD6{ z65s@6y*nO_CgKL!ZN%ib+)n7m3HDk+DCxv7)OfoNv|EjAEQsE%zf;WEI424IUXkGBAFm z-+J%Q)_VhEC;G?FpBnFbBUzdgT74I+w+8N9J{JmwY@4wUE{?r(-Wq)0I`oP4+qbR5 zA6n-Rj~(huR)ftC2tWJ$?jKG!*KSPKF`wr@8aw*OJ6Eq-Z=Je(ai2B#8|(Ohvqi?s zoX2Qo+)Y2>fiEVGp39u@nHkM5{ob+_lQZvN%dN}~LCF-gL_6FJCa=cZ5|ovT>B(Iz zra&l7Q0ilBK{sS-z}3-_G?YYFJm{iDjE{-VuHf3*`xULt+dH=#D$$bL;R4tmlht^r zv)hz3wTzt_Mv?Nbnw9-@AUOxM75!n-+Ot*)6H)u;-TKf7kn_ zcc?5hA~mK9cwr5`B~^?{HN#TPi1bWOylg~T&YToXi4S{n5Q~rz|+dre&e7WiJoT2hn zqtfbOX|=7m20xf8dqU0s=)Z*U_g1tv;p4N{&PP{*E#>IT`f6BzxiZ+wp=g|D+;vF+&UK#voa?0=qsu+5h`atwRm%$Q`chlELIBPU#H2SkPiu&~QCQU) z5ZVkI3Xsri+tk!-n*ABOq{wh{_%O|lNkTv>iE_EBx_Dq|KOd$UuW9QH$A;C zcW?Rm2AE-dQG@$F);<2w{+T@77n(wgLW_dDDHy7u5!0&>JxW!(S()x#gzC_>>|!wS zf~}zElQk{T6-iLFU5)EziL;DSqL8?u%9@_lh!LYBq2!hwL{`At9==iTRH6Ndos_hx zyG`5}g&=Yd>mK{ycaL2NPWMr;;!-$;SA-M%7G&QSVv?2r*vTu97V9tjlQpoKDa2an z!j#75h_174`!tA6o=Hu?7XA*=An@+N4^eg$lc$M67Q8B)oklh<;n<>3x%WMMzRdxbm8XF+SG@`Q~^1_!JI z1J>s~)}QuTmoGQhs#5@k(1&D_{da;w_4#12`D0N=rrkq$WyK$6lG zV7d){`XU(YMBm{H4sPh((6{QdIVYo^K6|oysAS;?|D#*ps#IBJs%YMnz_EODxl(Ox z!ofPz-3GuFK2genBl|>VWrh`s=uMCFKPr0-EtQ zwVg^#1vT}MD08o{(e_@m%F3PIbEJ_-xriXG>=d?91;9E7?V*4}sbsJyWWO~|( zvB7DJQX~?WVhbl)s)x)0ZW6N)P8hXTk;j*;rDZB0m)xf+VR62s1QiyYm`&JJuwONbvtQ9AB*{l!*;1Uj2_EsJW?5mk{xv|Jj=1(&! qU6z&2x`_jmF3}M}BtXHQE@2c`4C9Kv?wh!7(j_?JdYiKEk^cf`Sr@SY literal 0 HcmV?d00001 diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..f1eef64 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,5 @@ +from .account import router as account_router +from .database import router as database_router +from .task import router as task_router + +__all__ = ["account_router", "database_router", "task_router"] \ No newline at end of file diff --git a/app/api/__pycache__/__init__.cpython-312.pyc b/app/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d022cdc215bcd6aafe00fecbe227fe2cb98029c2 GIT binary patch literal 299 zcmX@j%ge<81dROGGL8c2#~=<2FhLog4STA%>re!0@-Yg z4CxFhOp6$!*ejVeSza;%wP`ZmVk^ooElDlXWWL3hn4FwnnpYAJ5xK>ml30?Mlvtb! z6W}gMEY5~<{4_aku|qW!F$0Y%VgVAjIAD5;SV3GCkd7iY5R*MVJ~1aJK7J*`XCRm1 zmz;}LOhHUZW@=7pOkzO+5NF23$7kkcmc+;F6;$5hu*uC&Da}c>E8+tx2RXJ_7)X3z aW@Kc%%b9`qckF7dRr8#!2>{e=h$w zXTJI7pS|Vtxe1gi>9-?4xd{0a8&-j~UY=I+gj^spk-0cYa3L`Kzya`{(mykk|-sg;qK-CE&igE9 z&NJwy4{*T60p%t9690=h^pgdZY1R|0sO=EM^_How?ERAO=PAKzPQ!w+;X2?ib` zP*Buzx9!{A)Rj^x)0}lQNjqgl^X+PB*;juoN|S1=t5b6*YD7&bK~8fvQ$>L;aK*hb zMSYA?fQF-;s$uzXP|$qaqS3BYr&`NsM5WWs)})NhJae>(D#@-+g+i!gb~RIWG!~`D zbE_f-f|BwA_@k@eOrE{=@XDo!=RObenj^_*TkM!7s*!d@{*VG=gtx#*Zs^*yd~qFZz6*~orGq>xhXM8) z81VID@Y;?+0UoC(FeKBWjs-C84a@j(2ooL<#u9_e^7M^~WJ--u>D%h)P#VRpd)ADoRE#9n+e(dI-EncBS|5x`I(|4+egC zaOTSHIy+;3&nWq?ku!#p8SXonV-V-D5ognDfvMN^;fBVR@Q%jThB{z%83gKt>NJlJh9oj4{k%43B5#AT>$(C?1JNkI+PSWfw~@e5Evp+MR@i z&G6D{zGxStm8jjr5rr~L##)|&5fP=*ls5AqD!x$6hvWG$5#Q^X33_sKFypU;nCwzi zEnmNyYgexc*EZE}YpJi(lf5RuMb?B^r)uOk)Qu)2Q>v~ae6^5JteF1+@am`HbStX?7<|PQ3N|Zn9Cx48jFFmqR5B5IUlOcQVkyU@> z7WA)3e=xeT`gU$juj6;ldAjseX?*4E6{*51-F?!d&BcW*4ZUN%MC+{XU* zvRI`*euC9gf$#^pEDvtBnDDOCC8tXIY6rxDaHf1iZ^;<9@lIfAy7{K_SI5tJQ-tSg zxpPWZuLsO}xc!OVMyO3$9;ZM`F`*jMjfhe`QMGaS=-|mI;^GSXYqMAl9DITmhSg+w z@UlfOL^U25J^21DGYr;)S;MRJ>c-sBfm4CLlj$AfD|cj8?&uASaXTm0ZW*cgzG{j% XICnolK$Uhr!3tO2#ZB?x>c{XeN7&Z# literal 0 HcmV?d00001 diff --git a/app/api/__pycache__/database.cpython-312.pyc b/app/api/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ff506845158d788ef7c66f6cd4db2a0aa3a0c2c GIT binary patch literal 3332 zcmb_eU2GIp6rP#g&Q5oCcKZh{yDb7SbeH}thM*A&h43R7LW zd7KxtGF?Zq|&if=dx7~w${$$*CHa3$Rb#Hgqj*T!If}c+#8M8PoZMmYn5-@wS9Lc zXA)}pHj^yrRt-zuws-HYEj=lcHPe}HOEk=+nKRgsL`Ck~9fh zD2Z*?vl(i-G-3NNejZ2~WDlW7(kaqco=}~cTo^pO{l(brQL5cNm8?%L9Z4VfuoHE`S$Zh$12cl+#QDeOEWs+n>y4 z;Ysf=qSc!1rC2zO$iNseApP6}kqb3U$gzSP8>t$3`|`dE`^M#0`?vlUXeb04heRNG zZ{rNea(#gWAz#O1ei1OfP7u5AFM_` zG!VKjMF*G8l$V9X{;dy!To4kmeDDa6ymvWW495E8M2elC7UtQkR3L;b6(@lCe^W7k z3g(4=cAb}lE0ZU`p1gVmpq~YG(2m1Kn=S)6%a_V%IlbGk0$G~YleD+vebUQne;%Nh zg9b%{uoxSFlz@&Gvk3Rk{suT|%XqtPOACgGc#4#(bOwc&S-z zhTWeBa93kMT{h-41{<)UsjtqJ0Q1^%G}d#V-QTFSwnvOxR*li27}KN@_hYq{^LH1wF?6dGJ?Ry9B?q~ z$;&@Xet6zK#dblJIbGBCnH^lap1gR5pHx51V2dJ2z+9_YXQ%>Y!v`GyWOS0Za|URKQG>F2BKt_7i+9|-YpSsaAFd;8BNM6oTbt7K<{S3 z&q2kK+Tha9X=FXc|0=xEjl)2vJv`6<&aE5g*4^Tk-QsG2J`_B>@Q?%Yx5V))|17T> zY|6K+&o{UKQof;I{Db#>96cUAspMm8`lI9g+M8=OUR!=OaSt?N`9&+H@jd3dkFOaI zC$vq2z1Xy$6C|ehZoa7v_14{N+jMQ~)i>{P65p7wZ=S~YSmS+sq2Bsw0l4X!qFzV7 zr5*J)xb$NA2-Az%dThUqJSxU)#fIYz`EWeHW=DU+IRD1Y*41O3qZ#Ju;40?m&}$_} xcY@+9Pv6Toug4wRpAq2(w_@aYJ+d^5)L6609m{R@O3+}8jA literal 0 HcmV?d00001 diff --git a/app/api/__pycache__/task.cpython-312.pyc b/app/api/__pycache__/task.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb504597e388d6c2eb144c81115361f4820c5044 GIT binary patch literal 7746 zcmeHMeQX@n5r4aT-+R70+voT@KF21>#ffc_fPpvxlh`3Nn2!WVj-b=UyK&CBv)$dj ze1Kg!6cw8S32Iu05EQ$jvZGjFTGSw+N~lm&sZw<(NA-1^s%nc3`R^b>RP>+DyuH2K zGwY~S2=Pas?AdqoX5QDn`OVDRKNJOzTV744Pq!!X;@XGw0J<)C+~ ziJre;yfU#8##><+uL|_qO!Uf4^ePN`HOuH#n&?#-^lFz`Up1_+a>@E?4C`BYqBgkl z8csCGsm_jvc0DqXP((>Bx^M5^hdPhM>K}NOgj>^h4JP1Ln%HKb5 z@DWiS9O#!twYWnNwo3#3l=uCFC@X3iuy*xBe@7!R0~;lGL=@4$a_^1ENA#{>Djo%X zhvlBbqL7HeeB9#kNPI9RhLNw92@xf7Fd~cLo|qWvPYhD_Q??dtl@lc}yM;H-Esp>C zqj%q3cz*KZHzq$m{i7hO+6E=@Q1rNJQ6jx^kd-_jD0zWAWo_w!sl)m_Qg%Jz_<$hB zRJ$b7we&C`rWXfPIQYLF13FHeWQg?XZHA|kwHHE;GEz{{EtM!XU8oOxT2chYyth}u zieKYqINhqAVeD|xhZE&} zjT_?nR_U_&r;$OW+dk}o**_q-?Xbn*IkOrcx)#`*a5~*R>Vwg#ZP-^Jc zqWAGCd5xX%>*usBo%#E7I?;{*N5VU-gi zCsfNZQ9PpZPz?_p3ez8!!vhIPwZ)_TfQ^8KV$VRous6ZO5D0i0$OJww!XdS(EsmaE zc>V&6YU>*~7>)|6qhCC(gr!9PMNYNLiJl%&mQ{NkXrxzEEr@4b2|N&*e!km<*Wdc& z?O)s#xX4OtpjpDr4VJ0SLy7*LaDOB&s-+!|b?n;RaqsTVaMz>V+xPBzuzQbWL0JSG z)sILX-rU+6zPDpX_>s&!b%io>QFl@Vo}WB|*T zRXgrFFnBR83pDGB8m-U%xR`zcCruG<#RFeA^8xSL(?SuE=x4MSKktZSxJA zQ}E}#ZPb#nk@Ct>9y&bLKGvS(8`7(SQ|l+!kACm0E#|ieH)J*QJB4^TGC1upM-YQts6^(K)qk zY+I7QCEeIG)jin_@{W|d9_7vWA9-r#J;9VGINdmFzwCL-lk#-G*92sg`^a0J_E)4= z)K9FLYMyLP*55i?oAlqEuBp4w^-9;<-0b%G&7G;uopY^Ssp6eC>=vIZLo80$zizmR zyX-S!vAV8e7nSK1%`@`st(T5YA5Qw)U}Jr*&k5&r{X4^f%$G8rrf0e=U3T)J-PyH? z`_Na>)y#dkmce?n73fpWmS{FOF!!!!z<>VVyTU#~0nhNQdshW*mzF*=zldJQ#Pbz+ zAq+ui5uAd{Tpqj-PrVRg(vqSeFwp{D$i(C69z<}17b+_7LV>KJ?uGI*@d#c+zlC1N z_g{ITVgpoP8-M%SxigDnQ=d$}pm_qR42G6)XCz40NH+jJ^%w~F)P=~Z6+McKxPL<4 zn?R7&y2-i{2_3T3fCOD{R|2<2^9)8g-8V?z@s{vZyth-n@`rz4JSzuj`p9S?b%~QjZ!yvyigZne+m1WCu86%^*)fd!_~2O~Usa9611v zocg#lD9(W!4T?F#Ok4tQ=$F&`ph2;_0FH`=xgT&46w{|6DE1@}8Z~@aeBn7QCI+Cm z2`E9P>wsu9jORoHAY&vQpa293GL7}_fzH2zLq_oe9O@2wmItDIIEe%20f!EOF{LT$ zSZEMQQO5u>h;bMqQWVU&XaS2bxy%5r$;E?$=Rnsou|eGi3gk>|TbF}TOw#=s z=2CXXx*S7Z=(Ug`W)BIn^p@z8mo6`y`q|>x$m01i&G&+fL&v9DFt^eion4Wly}hDD z{b10gc}_>KsNngPRU70gV!t5a>b2v$18dZCqN9{Lk)RKSdo7Em0+&pQm_iS;jBh4) zjJr@4$q@X@Q6MAahK;zZ=J{1Ae$^b`II<&M?i>v=wYRZnK2s9JUl}`s$Qt^~BLRPqWFUhm!u*uea&G{HCLs#=oPxe)8uFufJ%% z>$J(Z>jv4vU56u&E4<0J@4;^9tL>n6Yq&5v{>kJtqI3*Ca?nglwHy&o$QKE< zM;d9U<%s-|qi}r~IS3ylafp{7R< z0$r!{II1H^X`A9`htDPOrDmIS5O@ev@(+P#IEG>VMmDF&=D(8iFIbLY;gX19zi<%d z5hg_*{=2i}?AqkIZOP!>SDg2Z*fI`MR-dlj_^FjEXOpBf<07o%x$2YEqmNG<7^$9P zHfGp$j4N5y^cjKYb&jx`C?~TIzEx_HPfXXK=DwU{zRwle zKH-_|6Ka`O?6T@))#&yK%fu6@z^x-yFoRE7H&c?8f?B2qrD{*sjy^rzK2kf!+>rqR z*J=4P0uPkClQJ|p=q>54osMBoTQtF*oM8J>!IjfPD5&ubJ#H-&9Nj(bpYFL>iL#qM PWm#rD4!fDw=u7$!n+v35 literal 0 HcmV?d00001 diff --git a/app/api/account.py b/app/api/account.py new file mode 100644 index 0000000..b6cd636 --- /dev/null +++ b/app/api/account.py @@ -0,0 +1,60 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from ..core.database import get_db +from ..schemas.account import ( + AccountCreate, AccountUpdate, AccountResponse, + AccountListRequest, AccountGetRequest, AccountUpdateRequest, AccountDeleteRequest +) +from ..services.account import AccountService + +router = APIRouter(prefix="/accounts", tags=["账号管理"]) + +@router.post("/create", response_model=AccountResponse, status_code=status.HTTP_201_CREATED) +def create_account(account: AccountCreate, db: Session = Depends(get_db)): + """创建账号""" + # 检查账号是否已存在 + existing_account = AccountService.get_account_by_account(db, account.account) + if existing_account: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="账号已存在" + ) + + return AccountService.create_account(db, account) + +@router.post("/list", response_model=List[AccountResponse]) +def get_accounts(request: AccountListRequest, db: Session = Depends(get_db)): + """获取账号列表""" + return AccountService.get_accounts(db, skip=request.skip, limit=request.limit) + +@router.post("/get", response_model=AccountResponse) +def get_account(request: AccountGetRequest, db: Session = Depends(get_db)): + """根据ID获取账号""" + account = AccountService.get_account(db, request.account_id) + if not account: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="账号不存在" + ) + return account + +@router.post("/update", response_model=AccountResponse) +def update_account(request: AccountUpdateRequest, db: Session = Depends(get_db)): + """更新账号""" + account = AccountService.update_account(db, request.account_id, request.account_data) + if not account: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="账号不存在" + ) + return account + +@router.post("/delete", status_code=status.HTTP_204_NO_CONTENT) +def delete_account(request: AccountDeleteRequest, db: Session = Depends(get_db)): + """删除账号""" + if not AccountService.delete_account(db, request.account_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="账号不存在" + ) \ No newline at end of file diff --git a/app/api/database.py b/app/api/database.py new file mode 100644 index 0000000..ef59cdf --- /dev/null +++ b/app/api/database.py @@ -0,0 +1,56 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from ..core.database import get_db +from ..schemas.database import ( + SQLExecuteRequest, SQLExecuteResponse, TableDataRequest, + TableDataResponse, CreateTableRequest, ImportDataRequest +) +from ..services.database import DatabaseService + +router = APIRouter(prefix="/database", tags=["数据库管理"]) + +@router.post("/execute", response_model=SQLExecuteResponse) +def execute_sql(request: SQLExecuteRequest, db: Session = Depends(get_db)): + """执行SQL语句""" + result = DatabaseService.execute_sql(db, request.sql) + return SQLExecuteResponse(**result) + +@router.post("/table-data", response_model=TableDataResponse) +def get_table_data(request: TableDataRequest, db: Session = Depends(get_db)): + """获取表数据""" + result = DatabaseService.get_table_data( + db, + request.table_name, + request.limit or 100, + request.offset or 0 + ) + return TableDataResponse(**result) + +@router.post("/create-table", response_model=SQLExecuteResponse) +def create_table(request: CreateTableRequest, db: Session = Depends(get_db)): + """创建表""" + result = DatabaseService.create_table( + db, + request.table_name, + request.columns, + request.primary_key + ) + return SQLExecuteResponse(**result) + +@router.delete("/drop-table/{table_name}", response_model=SQLExecuteResponse) +def drop_table(table_name: str, db: Session = Depends(get_db)): + """删除表""" + result = DatabaseService.drop_table(db, table_name) + return SQLExecuteResponse(**result) + +@router.post("/import-data", response_model=SQLExecuteResponse) +def import_data(request: ImportDataRequest, db: Session = Depends(get_db)): + """导入数据""" + result = DatabaseService.import_data(db, request.table_name, request.data) + return SQLExecuteResponse(**result) + +@router.get("/tables", response_model=List[str]) +def get_table_list(): + """获取所有表名""" + return DatabaseService.get_table_list() \ No newline at end of file diff --git a/app/api/task.py b/app/api/task.py new file mode 100644 index 0000000..7b6bce5 --- /dev/null +++ b/app/api/task.py @@ -0,0 +1,187 @@ +from fastapi import APIRouter, HTTPException, status +from typing import List +from ..schemas.task import ( + JobResponse, AddCronJobRequest, AddIntervalJobRequest, + AddDateJobRequest, TaskResponse +) +from ..utils.scheduler import task_scheduler, example_task, database_cleanup_task + +router = APIRouter(prefix="/tasks", tags=["定时任务管理"]) + +# 可用的任务函数映射 +AVAILABLE_FUNCTIONS = { + "example_task": example_task, + "database_cleanup_task": database_cleanup_task, +} + +@router.post("/cron", response_model=TaskResponse) +def add_cron_job(request: AddCronJobRequest): + """添加cron定时任务""" + try: + if request.func_name not in AVAILABLE_FUNCTIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"函数 {request.func_name} 不可用" + ) + + func = AVAILABLE_FUNCTIONS[request.func_name] + + # 构建cron参数 + cron_kwargs = {} + if request.year is not None: + cron_kwargs['year'] = request.year + if request.month is not None: + cron_kwargs['month'] = request.month + if request.day is not None: + cron_kwargs['day'] = request.day + if request.week is not None: + cron_kwargs['week'] = request.week + if request.day_of_week is not None: + cron_kwargs['day_of_week'] = request.day_of_week + if request.hour is not None: + cron_kwargs['hour'] = request.hour + if request.minute is not None: + cron_kwargs['minute'] = request.minute + if request.second is not None: + cron_kwargs['second'] = request.second + + job = task_scheduler.add_cron_job(func, request.job_id, **cron_kwargs) + + return TaskResponse( + success=True, + message=f"Cron任务 {request.job_id} 添加成功", + data={"job_id": job.id, "next_run": str(job.next_run_time)} + ) + + except Exception as e: + return TaskResponse( + success=False, + message=f"添加Cron任务失败: {str(e)}" + ) + +@router.post("/interval", response_model=TaskResponse) +def add_interval_job(request: AddIntervalJobRequest): + """添加间隔执行任务""" + try: + if request.func_name not in AVAILABLE_FUNCTIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"函数 {request.func_name} 不可用" + ) + + func = AVAILABLE_FUNCTIONS[request.func_name] + + # 构建interval参数 + interval_kwargs = {} + if request.seconds is not None: + interval_kwargs['seconds'] = request.seconds + if request.minutes is not None: + interval_kwargs['minutes'] = request.minutes + if request.hours is not None: + interval_kwargs['hours'] = request.hours + if request.days is not None: + interval_kwargs['days'] = request.days + + job = task_scheduler.add_interval_job(func, request.job_id, **interval_kwargs) + + return TaskResponse( + success=True, + message=f"间隔任务 {request.job_id} 添加成功", + data={"job_id": job.id, "next_run": str(job.next_run_time)} + ) + + except Exception as e: + return TaskResponse( + success=False, + message=f"添加间隔任务失败: {str(e)}" + ) + +@router.post("/date", response_model=TaskResponse) +def add_date_job(request: AddDateJobRequest): + """添加指定时间执行任务""" + try: + if request.func_name not in AVAILABLE_FUNCTIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"函数 {request.func_name} 不可用" + ) + + func = AVAILABLE_FUNCTIONS[request.func_name] + job = task_scheduler.add_date_job(func, request.job_id, run_date=request.run_date) + + return TaskResponse( + success=True, + message=f"定时任务 {request.job_id} 添加成功", + data={"job_id": job.id, "run_date": str(job.next_run_time)} + ) + + except Exception as e: + return TaskResponse( + success=False, + message=f"添加定时任务失败: {str(e)}" + ) + +@router.get("/", response_model=List[JobResponse]) +def get_jobs(): + """获取所有任务""" + jobs = task_scheduler.get_jobs() + result = [] + for job in jobs: + result.append(JobResponse( + id=job.id, + name=job.name, + func=str(job.func), + trigger=str(job.trigger), + next_run_time=job.next_run_time + )) + return result + +@router.delete("/{job_id}", response_model=TaskResponse) +def remove_job(job_id: str): + """删除任务""" + success = task_scheduler.remove_job(job_id) + if success: + return TaskResponse( + success=True, + message=f"任务 {job_id} 删除成功" + ) + else: + return TaskResponse( + success=False, + message=f"删除任务 {job_id} 失败" + ) + +@router.put("/{job_id}/pause", response_model=TaskResponse) +def pause_job(job_id: str): + """暂停任务""" + success = task_scheduler.pause_job(job_id) + if success: + return TaskResponse( + success=True, + message=f"任务 {job_id} 已暂停" + ) + else: + return TaskResponse( + success=False, + message=f"暂停任务 {job_id} 失败" + ) + +@router.put("/{job_id}/resume", response_model=TaskResponse) +def resume_job(job_id: str): + """恢复任务""" + success = task_scheduler.resume_job(job_id) + if success: + return TaskResponse( + success=True, + message=f"任务 {job_id} 已恢复" + ) + else: + return TaskResponse( + success=False, + message=f"恢复任务 {job_id} 失败" + ) + +@router.get("/functions", response_model=List[str]) +def get_available_functions(): + """获取可用的任务函数列表""" + return list(AVAILABLE_FUNCTIONS.keys()) \ No newline at end of file diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/__pycache__/__init__.cpython-312.pyc b/app/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c03a5635a310e24610037c15a8354cb7a982777 GIT binary patch literal 128 zcmX@j%ge<81lM@4Wq|0%AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxdCGTPtQxH>JmtkIamWj77{q765*4 B8|VN4 literal 0 HcmV?d00001 diff --git a/app/core/__pycache__/config.cpython-312.pyc b/app/core/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..840ac205298a73bc1e7f2d811a36b3b972a3a16e GIT binary patch literal 1284 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 literal 0 HcmV?d00001 diff --git a/app/models/account.py b/app/models/account.py new file mode 100644 index 0000000..f82082d --- /dev/null +++ b/app/models/account.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime +from sqlalchemy.sql import func +from ..core.database import Base + +class Account(Base): + __tablename__ = "accounts" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + account = Column(String(50), unique=True, index=True, nullable=False, comment="账号") + password = Column(String(100), nullable=False, comment="密码") + status = Column(Integer, default=1, comment="状态: 1-正常, 0-禁用") + today_updated = Column(Integer, default=0, comment="是否更新") + section = Column(String(100), comment="标段") + created_at = Column(DateTime, server_default=func.now(), comment="创建时间") + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间") \ No newline at end of file diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..b936502 --- /dev/null +++ b/app/schemas/__init__.py @@ -0,0 +1,14 @@ +from .account import ( + AccountBase, AccountCreate, AccountUpdate, AccountResponse, + AccountListRequest, AccountGetRequest, AccountUpdateRequest, AccountDeleteRequest +) +from .database import SQLExecuteRequest, SQLExecuteResponse, TableDataRequest, TableDataResponse, CreateTableRequest, ImportDataRequest +from .task import JobResponse, AddCronJobRequest, AddIntervalJobRequest, AddDateJobRequest, TaskResponse + +__all__ = [ + "AccountBase", "AccountCreate", "AccountUpdate", "AccountResponse", + "AccountListRequest", "AccountGetRequest", "AccountUpdateRequest", "AccountDeleteRequest", + "SQLExecuteRequest", "SQLExecuteResponse", "TableDataRequest", + "TableDataResponse", "CreateTableRequest", "ImportDataRequest", + "JobResponse", "AddCronJobRequest", "AddIntervalJobRequest", "AddDateJobRequest", "TaskResponse" +] \ No newline at end of file diff --git a/app/schemas/__pycache__/__init__.cpython-312.pyc b/app/schemas/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..789885efd2e2cefd3c22904b95293f992021d2d0 GIT binary patch literal 711 zcmaKqJ#W-N5QcaCedqJ}dop=}lpjG) z#}A>RL{My#f{G52A`%rl_W>?|Egoy$d3RyJ@1B<6e=e_5PcgnY$ddrZ7KmoLoc zEh$JrO9GTpu$Tolv%z5wMcpc0@R$ca^C4gX4A=mMYzQF>S9!Y}LBt{$voXXhh6$TM z!V*eelEQgM+4R&+y>HaCNgW;T%;$AisRz6jI@$!gQgHPreBKlo#+&SsXq&ofMe6C~ z+0(}l--vmq#F04ZM62|)FRhJwa?EF?*yD14!$YzK3}i)Z)J60R?25)>Hlw_9c4!RXB+M3IWNmRe@`w=SMu}L z-rcOp)KZjP#+xQ<=L-S6&GLL%EmfZ1X-;);59*>T#eI3xT*3dKy>0N_rj%Zg!%yV! b$|7{%x-!Xq>w+vklf@6m4Xra@-Y~{rvY^3n literal 0 HcmV?d00001 diff --git a/app/schemas/__pycache__/account.cpython-312.pyc b/app/schemas/__pycache__/account.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbe67c19cda5ffad1c1bd6a6ee137f36daa479fe GIT binary patch literal 1672 zcmZ8h&yU+g6rPF4Kay-VTPW4iZnn*CMdeE+E>(p}1uSA0!3GIf30VQl#M7=t9J@1) zP;Qa7NV~W6RQ?6jKZRouvMlY1Q*Vd_A@#(2V{hVQB!7PM&6_vR?|b9l8;u%)5xoEB z>5B>>f8pe6=!G#GgYlC1#HW1{QioEsEB#8SIhwMyJ`0({D5;P`;<5^?B%Cxw!=77W8{ z!5|SEIBfv@MrMHIP5aXix^XuNcz@XS;Sql8h>_vH2LP@c*=hPf?e8Bx2EHHso02o5#r*qYb`C(%UMLb#5g;@&`O`wB3Y#C)HVlv%k4F0P!s zXb1r-%ac!_y&Y%?t}9UuKW_-?m?chA;!|h{#;b*2-Xzqil{mfdtC#kw#)0oM7vFcH zH17xt5)oAdrQ1R+dt?2>l_%;dt|7d&y76tCE_Sb1=_alsU>)TV00_F2YRcN_?%w$5 z{B$BO!YSKJckV1DzLW0XTS|Pdv~M9XhF&Q&=80qW5kR@nrJJY2EmMW&XsPlp{~@pN zg1~T$kpX3E7H#o0mocQHACnc2d~c8h zJ)r@U8ObOP22a&LLZ}nfeOEV0HHtcH^`2$cqx>iISiA=@u&VMgz~36B^pfmel3SOg mHD@~Ao7i&#ulXjW)yWrg0J2IGg%j^PYlH2&jO2G_pZVB%fBVe%H_I{! zl+~%<@0WB!{=|nt$@Uv(ze3{?@rkbmB-9*D!*(Xfgu0`L)S+S4$%ckwXe2{EB)^hCza|$Xjk<_s{%$j_M-4Bi;qXD}Z=0N*1LYB6#L;}>WWFFw zXVlkjg4)j5x-4&fx^d?_R%^xVGq%@au%IsXf~uXBdDm@tA#+`6x^5WxtpM94*WGJ* zLBB^AF_+V>>opot?BPCy>;6p6a2tPLU3+J{xgFP8(AxHz&23S;&q7aZ`(EtrKuWJS z+q?h+RiMxiIS49VQ`~kO`AH(W=#$5Hc4+^zN~$q_}%&@#tIOdb_)<7Bk;2@*?iGgu#L!=}r^{ zd>WspFuaTbv*Va2$B!cQdL!mD_^48y#nuahfMdln1i)dH4>r5j%)v(YLS=vLa5;JN z=(Q7C=`JkoZzeZ7s}FCT(xr3}oIjb@ZRnZI%g;Dj2AujtbVZ3e`5`T(WY{@5^SQSZ zur**Xu!{*F6P$sDc;drLWHzXW^_XX{ZA81fg2h$4Fh(fFq5xfnfhn5{*t&vY49g|d zFd$s900FSf%pKhAUaaijIlPgq9^FDBm*)3BI9yM*jy6x}d`bjojS%_wLU5pGGLdH; z#0sF9yBTCO_zgL#2#zW}2eKki{5|P2kp|RwK5paYne}L=zd< zfr+GcLLgkh8{>8rJ20TR#M=-??DXn&Wv90~w6pcoLg%X=r;g{Aj?Lv0dcFI~;{GSe zxz57F`BS=>qQr$pQ0gFHIlB$vxhQp}r{KnprQQdoj%8bi8)tTvGcRTeTi;%M9+sB5 zS`@UxhN#M7lh;F!x7}}8+s=;zQ)f+?O2Juzxv8_pxGZ7^20Vx(ezMBRLL!poqpwfs z#Yqrw)(8YV&{)pifbd)h2Hd;3|NJvR0clfN{wQolJRUk!Dr@DC>i;DotyFSQ>G=S#R7+y{5pb z7pv<`-QVigQfE{dPSI6wAKj76-%S5T-sLN>t_DBxC4|3pP17Ee${%Fqgsk+ardgdr wk3iKksJ7fW-y=}XOhkAEu^ zvIKs=PQBQrl#oC0X7o$P!O>3;JSR4>r3P^%Ly|B~HBydj$PP8AlQz*uk zszg#`o!InyVyBt(iyS@6&;+Xht0vftU^QSf2{tR(EU>u*n-gpv*g}HMx2N>tpZF48 z;+apZfNgp|b%btd&-*QddrJmuMX=0vf@~mk(p51C-oHxxU zEvpf)$U*4yqG?*L>xC9pADE_}hgbWcG4YEC8ofnd)N0@`?NgB7P=dQ!gTlEV0opp z*}HLY=lAnVhjhiC9X()jpI^Z8_C%8<^6%t%F=B5%{F6N2}E{|MW)(d>*mTlkmJy!_+q{YC4$~WG&S+{ux#TI&2 zd9emLCVSfSSrC9^oVJD zlhc1l%TeF)v6K700xSR3ca|Ov(>B%9;^?fwgr3wbuHjU~`CZkOvm1kBZoK|gaz6ASXrEIdtM_t$Kpg^hfPGPs~C;R{4R0`Z^A!7n?ad{neJvZq9=%l$yFFx zTC;6iZdkARGB8{T+s(SW6TJyP0ltDD-YNPkz7Jw1p!}k>;>BY-;@ZvVi}kVpKIBR8 z4>ke*k|jxcNy@LtN}sH}B&8uuORGJ7NZ>M@%1H|cnIVD8a88jb2OC2Im*KQ5T|^!( Q!>lY-dLR4^aYYFCFQe@94FCWD literal 0 HcmV?d00001 diff --git a/app/schemas/account.py b/app/schemas/account.py new file mode 100644 index 0000000..333f565 --- /dev/null +++ b/app/schemas/account.py @@ -0,0 +1,42 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class AccountBase(BaseModel): + account: str + password: str + status: Optional[int] = 1 + today_updated: Optional[int] = 0 + section: Optional[str] = None + +class AccountCreate(AccountBase): + pass + +class AccountUpdate(BaseModel): + account: Optional[str] = None + password: Optional[str] = None + status: Optional[int] = None + today_updated: Optional[int] = None + section: Optional[str] = None + +class AccountResponse(AccountBase): + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + +class AccountListRequest(BaseModel): + skip: Optional[int] = 0 + limit: Optional[int] = 100 + +class AccountGetRequest(BaseModel): + account_id: int + +class AccountUpdateRequest(BaseModel): + account_id: int + account_data: AccountUpdate + +class AccountDeleteRequest(BaseModel): + account_id: int \ No newline at end of file diff --git a/app/schemas/database.py b/app/schemas/database.py new file mode 100644 index 0000000..b96a231 --- /dev/null +++ b/app/schemas/database.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel +from typing import Any, Dict, List, Optional + +class SQLExecuteRequest(BaseModel): + sql: str + +class SQLExecuteResponse(BaseModel): + success: bool + message: str + data: Optional[Any] = None + rows_affected: Optional[int] = None + +class TableDataRequest(BaseModel): + table_name: str + limit: Optional[int] = 100 + offset: Optional[int] = 0 + +class TableDataResponse(BaseModel): + success: bool + message: str + data: Optional[List[Dict[str, Any]]] = None + total_count: Optional[int] = None + +class CreateTableRequest(BaseModel): + table_name: str + columns: Dict[str, str] # 列名: 数据类型 + primary_key: Optional[str] = None + +class ImportDataRequest(BaseModel): + table_name: str + data: List[Dict[str, Any]] \ No newline at end of file diff --git a/app/schemas/task.py b/app/schemas/task.py new file mode 100644 index 0000000..c03d098 --- /dev/null +++ b/app/schemas/task.py @@ -0,0 +1,41 @@ +from pydantic import BaseModel +from typing import Optional, List, Dict, Any +from datetime import datetime + +class JobResponse(BaseModel): + id: str + name: Optional[str] = None + func: str + trigger: str + next_run_time: Optional[datetime] = None + +class AddCronJobRequest(BaseModel): + job_id: str + func_name: str + cron_expression: Optional[str] = None + year: Optional[int] = None + month: Optional[int] = None + day: Optional[int] = None + week: Optional[int] = None + day_of_week: Optional[int] = None + hour: Optional[int] = None + minute: Optional[int] = None + second: Optional[int] = None + +class AddIntervalJobRequest(BaseModel): + job_id: str + func_name: str + seconds: Optional[int] = None + minutes: Optional[int] = None + hours: Optional[int] = None + days: Optional[int] = None + +class AddDateJobRequest(BaseModel): + job_id: str + func_name: str + run_date: datetime + +class TaskResponse(BaseModel): + success: bool + message: str + data: Optional[Any] = None \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..a06d677 --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1,4 @@ +from .account import AccountService +from .database import DatabaseService + +__all__ = ["AccountService", "DatabaseService"] \ No newline at end of file diff --git a/app/services/__pycache__/__init__.cpython-312.pyc b/app/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67860a2a7da29106c124fd42345b3e17541c34e0 GIT binary patch literal 268 zcmX@j%ge<81Z8~JGOB>|V-N=hn4pZ$DnQ0`h7^Vr#vF!R#wbQc5SuB7DVI5l8OUZ% zVM%9-VyR@+WP8a7RI15%i_bARIlnZoBsjIGEHgP3D8lcOSdy5OSeyzKDq;qzEMfr? zewwVe*b^b@!J@Y~QlM(V^0(OI;}dgo;^S8`dTZlX-=wL5jRi|$XUf=K;i>4BO~Ko2Iad9st>rOFUaa$ N;L>koFX9AB0RVM5ME3vy literal 0 HcmV?d00001 diff --git a/app/services/__pycache__/account.cpython-312.pyc b/app/services/__pycache__/account.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3dde1d02e33b2f1822eed52c3b5c4021b71f7d4d GIT binary patch literal 3851 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 literal 0 HcmV?d00001 diff --git a/app/services/account.py b/app/services/account.py new file mode 100644 index 0000000..800d016 --- /dev/null +++ b/app/services/account.py @@ -0,0 +1,51 @@ +from sqlalchemy.orm import Session +from ..models.account import Account +from ..schemas.account import AccountCreate, AccountUpdate +from typing import List, Optional + +class AccountService: + @staticmethod + def create_account(db: Session, account_data: AccountCreate) -> Account: + """创建账号""" + db_account = Account(**account_data.dict()) + db.add(db_account) + db.commit() + db.refresh(db_account) + return db_account + + @staticmethod + def get_account(db: Session, account_id: int) -> Optional[Account]: + """根据ID获取账号""" + return db.query(Account).filter(Account.id == account_id).first() + + @staticmethod + def get_account_by_account(db: Session, account: str) -> Optional[Account]: + """根据账号名获取账号""" + return db.query(Account).filter(Account.account == account).first() + + @staticmethod + def get_accounts(db: Session, skip: int = 0, limit: int = 100) -> List[Account]: + """获取账号列表""" + return db.query(Account).offset(skip).limit(limit).all() + + @staticmethod + def update_account(db: Session, account_id: int, account_data: AccountUpdate) -> Optional[Account]: + """更新账号""" + db_account = db.query(Account).filter(Account.id == account_id).first() + if db_account: + update_data = account_data.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(db_account, field, value) + db.commit() + db.refresh(db_account) + return db_account + + @staticmethod + def delete_account(db: Session, account_id: int) -> bool: + """删除账号""" + db_account = db.query(Account).filter(Account.id == account_id).first() + if db_account: + db.delete(db_account) + db.commit() + return True + return False \ No newline at end of file diff --git a/app/services/database.py b/app/services/database.py new file mode 100644 index 0000000..0e2f238 --- /dev/null +++ b/app/services/database.py @@ -0,0 +1,195 @@ +from sqlalchemy.orm import Session +from sqlalchemy import text, MetaData, Table, Column, create_engine, inspect +from sqlalchemy.exc import SQLAlchemyError +from typing import List, Dict, Any, Optional +from ..core.database import engine +import pandas as pd + +class DatabaseService: + @staticmethod + def execute_sql(db: Session, sql: str) -> Dict[str, Any]: + """执行SQL语句""" + try: + result = db.execute(text(sql)) + + # 判断是否为SELECT查询 + if sql.strip().upper().startswith('SELECT'): + data = [] + columns = result.keys() + for row in result: + data.append(dict(zip(columns, row))) + + return { + "success": True, + "message": "查询成功", + "data": data + } + else: + # 非SELECT语句,提交事务 + db.commit() + return { + "success": True, + "message": "执行成功", + "rows_affected": result.rowcount + } + + except SQLAlchemyError as e: + db.rollback() + return { + "success": False, + "message": f"SQL执行失败: {str(e)}" + } + except Exception as e: + db.rollback() + return { + "success": False, + "message": f"未知错误: {str(e)}" + } + + @staticmethod + def get_table_data(db: Session, table_name: str, limit: int = 100, offset: int = 0) -> Dict[str, Any]: + """获取表数据""" + try: + # 先检查表是否存在 + inspector = inspect(engine) + if table_name not in inspector.get_table_names(): + return { + "success": False, + "message": f"表 {table_name} 不存在" + } + + # 获取总数 + count_sql = f"SELECT COUNT(*) as total FROM {table_name}" + count_result = db.execute(text(count_sql)).fetchone() + total_count = count_result.total if count_result else 0 + + # 获取数据 + sql = f"SELECT * FROM {table_name} LIMIT {limit} OFFSET {offset}" + result = db.execute(text(sql)) + + data = [] + columns = result.keys() + for row in result: + data.append(dict(zip(columns, row))) + + return { + "success": True, + "message": "获取数据成功", + "data": data, + "total_count": total_count + } + + except SQLAlchemyError as e: + return { + "success": False, + "message": f"获取表数据失败: {str(e)}" + } + except Exception as e: + return { + "success": False, + "message": f"未知错误: {str(e)}" + } + + @staticmethod + def create_table(db: Session, table_name: str, columns: Dict[str, str], primary_key: Optional[str] = None) -> Dict[str, Any]: + """创建表""" + try: + # 构建CREATE TABLE语句 + column_definitions = [] + for col_name, col_type in columns.items(): + column_definitions.append(f"{col_name} {col_type}") + + if primary_key: + column_definitions.append(f"PRIMARY KEY ({primary_key})") + + sql = f"CREATE TABLE {table_name} ({', '.join(column_definitions)})" + + db.execute(text(sql)) + db.commit() + + return { + "success": True, + "message": f"表 {table_name} 创建成功" + } + + except SQLAlchemyError as e: + db.rollback() + return { + "success": False, + "message": f"创建表失败: {str(e)}" + } + except Exception as e: + db.rollback() + return { + "success": False, + "message": f"未知错误: {str(e)}" + } + + @staticmethod + def drop_table(db: Session, table_name: str) -> Dict[str, Any]: + """删除表""" + try: + sql = f"DROP TABLE IF EXISTS {table_name}" + db.execute(text(sql)) + db.commit() + + return { + "success": True, + "message": f"表 {table_name} 删除成功" + } + + except SQLAlchemyError as e: + db.rollback() + return { + "success": False, + "message": f"删除表失败: {str(e)}" + } + except Exception as e: + db.rollback() + return { + "success": False, + "message": f"未知错误: {str(e)}" + } + + @staticmethod + def import_data(db: Session, table_name: str, data: List[Dict[str, Any]]) -> Dict[str, Any]: + """导入数据到表""" + try: + if not data: + return { + "success": False, + "message": "导入数据不能为空" + } + + # 使用pandas DataFrame来处理数据导入 + df = pd.DataFrame(data) + + # 使用pandas的to_sql方法 + df.to_sql(table_name, engine, if_exists='append', index=False) + + return { + "success": True, + "message": f"成功导入 {len(data)} 条数据到表 {table_name}" + } + + except SQLAlchemyError as e: + db.rollback() + return { + "success": False, + "message": f"导入数据失败: {str(e)}" + } + except Exception as e: + db.rollback() + return { + "success": False, + "message": f"未知错误: {str(e)}" + } + + @staticmethod + def get_table_list() -> List[str]: + """获取所有表名""" + try: + inspector = inspect(engine) + return inspector.get_table_names() + except Exception as e: + return [] \ No newline at end of file diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..3f0fb19 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1,3 @@ +from .scheduler import task_scheduler, example_task, database_cleanup_task + +__all__ = ["task_scheduler", "example_task", "database_cleanup_task"] \ No newline at end of file diff --git a/app/utils/__pycache__/__init__.cpython-312.pyc b/app/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39ce5e203f8da01a98d2482b2fd61966f5c5434f GIT binary patch literal 260 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 GfcrNdU%%`C literal 0 HcmV?d00001 diff --git a/app/utils/scheduler.py b/app/utils/scheduler.py new file mode 100644 index 0000000..de39819 --- /dev/null +++ b/app/utils/scheduler.py @@ -0,0 +1,124 @@ +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.executors.pool import ThreadPoolExecutor +from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore +from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR +import logging +from ..core.config import settings + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class TaskScheduler: + def __init__(self): + # 配置作业存储 + jobstores = { + 'default': SQLAlchemyJobStore(url=settings.DATABASE_URL) + } + + # 配置执行器 + executors = { + 'default': ThreadPoolExecutor(20) + } + + # 作业默认配置 + job_defaults = { + 'coalesce': False, + 'max_instances': 3 + } + + # 创建调度器 + self.scheduler = BackgroundScheduler( + jobstores=jobstores, + executors=executors, + job_defaults=job_defaults, + timezone='Asia/Shanghai' + ) + + # 添加事件监听器 + self.scheduler.add_listener(self._job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR) + + def _job_listener(self, event): + """作业执行监听器""" + if event.exception: + logger.error(f"Job {event.job_id} crashed: {event.exception}") + else: + logger.info(f"Job {event.job_id} executed successfully") + + def start(self): + """启动调度器""" + if not self.scheduler.running: + self.scheduler.start() + logger.info("定时任务调度器已启动") + + def shutdown(self): + """关闭调度器""" + if self.scheduler.running: + self.scheduler.shutdown() + logger.info("定时任务调度器已关闭") + + def add_cron_job(self, func, job_id: str, **kwargs): + """添加cron定时任务""" + return self.scheduler.add_job(func, 'cron', id=job_id, **kwargs) + + def add_interval_job(self, func, job_id: str, **kwargs): + """添加间隔执行任务""" + return self.scheduler.add_job(func, 'interval', id=job_id, **kwargs) + + def add_date_job(self, func, job_id: str, **kwargs): + """添加指定时间执行任务""" + return self.scheduler.add_job(func, 'date', id=job_id, **kwargs) + + def remove_job(self, job_id: str): + """移除任务""" + try: + self.scheduler.remove_job(job_id) + logger.info(f"任务 {job_id} 已移除") + return True + except Exception as e: + logger.error(f"移除任务失败: {e}") + return False + + def get_job(self, job_id: str): + """获取任务""" + return self.scheduler.get_job(job_id) + + def get_jobs(self): + """获取所有任务""" + return self.scheduler.get_jobs() + + def pause_job(self, job_id: str): + """暂停任务""" + try: + self.scheduler.pause_job(job_id) + logger.info(f"任务 {job_id} 已暂停") + return True + except Exception as e: + logger.error(f"暂停任务失败: {e}") + return False + + def resume_job(self, job_id: str): + """恢复任务""" + try: + self.scheduler.resume_job(job_id) + logger.info(f"任务 {job_id} 已恢复") + return True + except Exception as e: + logger.error(f"恢复任务失败: {e}") + return False + +# 全局调度器实例 +task_scheduler = TaskScheduler() + +# 示例定时任务函数 +def example_task(): + """示例定时任务""" + logger.info("执行示例定时任务") + # 这里可以添加具体的业务逻辑 + return "任务执行完成" + +def database_cleanup_task(): + """数据库清理任务示例""" + logger.info("执行数据库清理任务") + # 这里可以添加数据库清理逻辑 + return "数据库清理完成" \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..2a3568e --- /dev/null +++ b/main.py @@ -0,0 +1,11 @@ +import uvicorn +from app.main import app +from app.core.config import settings + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host=settings.APP_HOST, + port=settings.APP_PORT, + reload=settings.APP_DEBUG + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f0d72d9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +sqlalchemy==2.0.23 +pymysql==1.1.0 +cryptography==41.0.7 +pydantic==2.5.0 +python-dotenv==1.0.0 +apscheduler==3.10.4 +pandas==2.1.3 +python-multipart==0.0.6 \ No newline at end of file