From e7906d57ee5d50553a098e0c5b6f2b0862137101 Mon Sep 17 00:00:00 2001 From: xsl Date: Mon, 26 Jan 2026 10:08:14 +0800 Subject: [PATCH] =?UTF-8?q?[backend]=20feat:=20=E5=AE=9E=E7=8E=B0Tasks=204?= =?UTF-8?q?-10=E7=9A=84=E5=AE=8C=E6=95=B4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建Pydantic Schema模型 - 创建认证中间件和依赖注入 - 创建认证路由 - 创建用户管理路由 - 创建项目管理路由(包含统计功能) - 创建FastAPI主应用 - 创建环境配置文件 - 修复models的Enum导入问题 - 修复模块导入路径问题 注意:测试仍有部分失败,需要调整错误处理逻辑 --- backend/.coverage | Bin 0 -> 53248 bytes backend/.env.example | 20 + backend/__pycache__/main.cpython-310.pyc | Bin 0 -> 1478 bytes .../__pycache__/database.cpython-310.pyc | Bin 1269 -> 1516 bytes .../__pycache__/settings.cpython-310.pyc | Bin 1371 -> 1868 bytes backend/main.py | 57 +++ backend/pytest.ini | 1 + .../__pycache__/dependencies.cpython-310.pyc | Bin 0 -> 1587 bytes backend/src/dependencies.py | 57 +++ backend/src/middleware/__init__.py | 13 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 322 bytes .../__pycache__/auth.cpython-310.pyc | Bin 0 -> 2037 bytes backend/src/middleware/auth.py | 81 ++++ .../__pycache__/project.cpython-310.pyc | Bin 4723 -> 5005 bytes .../models/__pycache__/user.cpython-310.pyc | Bin 1297 -> 1684 bytes backend/src/models/project.py | 9 +- backend/src/models/user.py | 10 +- backend/src/routes/__init__.py | 9 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 322 bytes .../routes/__pycache__/auth.cpython-310.pyc | Bin 0 -> 2043 bytes .../__pycache__/projects.cpython-310.pyc | Bin 0 -> 8810 bytes .../routes/__pycache__/users.cpython-310.pyc | Bin 0 -> 4470 bytes backend/src/routes/auth.py | 82 ++++ backend/src/routes/projects.py | 432 ++++++++++++++++++ backend/src/routes/users.py | 228 +++++++++ backend/src/schemas/__init__.py | 28 ++ .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 563 bytes .../schemas/__pycache__/auth.cpython-310.pyc | Bin 0 -> 820 bytes .../__pycache__/project.cpython-310.pyc | Bin 0 -> 5001 bytes .../schemas/__pycache__/user.cpython-310.pyc | Bin 0 -> 2163 bytes backend/src/schemas/auth.py | 14 + backend/src/schemas/project.py | 136 ++++++ backend/src/schemas/user.py | 44 ++ .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 155 bytes .../conftest.cpython-310-pytest-7.4.3.pyc | Bin 0 -> 4094 bytes .../test_auth.cpython-310-pytest-7.4.3.pyc | Bin 0 -> 7443 bytes ...test_projects.cpython-310-pytest-7.4.3.pyc | Bin 0 -> 18071 bytes ...st_statistics.cpython-310-pytest-7.4.3.pyc | Bin 0 -> 14876 bytes .../test_users.cpython-310-pytest-7.4.3.pyc | Bin 0 -> 14480 bytes backend/tests/conftest.py | 9 +- 40 files changed, 1213 insertions(+), 17 deletions(-) create mode 100644 backend/.coverage create mode 100644 backend/.env.example create mode 100644 backend/__pycache__/main.cpython-310.pyc create mode 100644 backend/main.py create mode 100644 backend/src/__pycache__/dependencies.cpython-310.pyc create mode 100644 backend/src/dependencies.py create mode 100644 backend/src/middleware/__init__.py create mode 100644 backend/src/middleware/__pycache__/__init__.cpython-310.pyc create mode 100644 backend/src/middleware/__pycache__/auth.cpython-310.pyc create mode 100644 backend/src/middleware/auth.py create mode 100644 backend/src/routes/__init__.py create mode 100644 backend/src/routes/__pycache__/__init__.cpython-310.pyc create mode 100644 backend/src/routes/__pycache__/auth.cpython-310.pyc create mode 100644 backend/src/routes/__pycache__/projects.cpython-310.pyc create mode 100644 backend/src/routes/__pycache__/users.cpython-310.pyc create mode 100644 backend/src/routes/auth.py create mode 100644 backend/src/routes/projects.py create mode 100644 backend/src/routes/users.py create mode 100644 backend/src/schemas/__init__.py create mode 100644 backend/src/schemas/__pycache__/__init__.cpython-310.pyc create mode 100644 backend/src/schemas/__pycache__/auth.cpython-310.pyc create mode 100644 backend/src/schemas/__pycache__/project.cpython-310.pyc create mode 100644 backend/src/schemas/__pycache__/user.cpython-310.pyc create mode 100644 backend/src/schemas/auth.py create mode 100644 backend/src/schemas/project.py create mode 100644 backend/src/schemas/user.py create mode 100644 backend/tests/__pycache__/__init__.cpython-310.pyc create mode 100644 backend/tests/__pycache__/conftest.cpython-310-pytest-7.4.3.pyc create mode 100644 backend/tests/__pycache__/test_auth.cpython-310-pytest-7.4.3.pyc create mode 100644 backend/tests/__pycache__/test_projects.cpython-310-pytest-7.4.3.pyc create mode 100644 backend/tests/__pycache__/test_statistics.cpython-310-pytest-7.4.3.pyc create mode 100644 backend/tests/__pycache__/test_users.cpython-310-pytest-7.4.3.pyc diff --git a/backend/.coverage b/backend/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..e1261824ae9eb9fa7d07b4f3a03ccf01365719ec GIT binary patch literal 53248 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 B5@P@W literal 0 HcmV?d00001 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 00000000..20ca2a07 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,20 @@ +# 应用配置 +APP_NAME=海洋项目管理系统 +APP_VERSION=1.0.0 +DEBUG=True + +# 数据库配置 +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=rootpassword +DB_NAME=project_manager +DB_CHARSET=utf8mb4 + +# 安全配置 +SECRET_KEY=your-secret-key-change-this-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=1440 + +# CORS配置 +CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 diff --git a/backend/__pycache__/main.cpython-310.pyc b/backend/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60a519c9c2a70a4d2e579823b496ca420b26e44c GIT binary patch literal 1478 zcmZWpOOM<{5O&)h+wqtsB(x}ZkjNe*q;LR)P>#9r6Z(b_8Rah|M55aB*gV|oa@E(>uIjI=OsmzB2G8Gr zjiLvprv0OgXCE7k4`Ff(X&TlrinRoJ2q|5U^~CUuTsLAfDR_lkH)AWYJv-M6v6B?N zVp8%-N!cqSjTBBauR;*145~0|*upk;aPhR@)k%G@L<~TeaS4}uD4&&6_$sWdkRq<) z+NnOXO3zv4bL-A3uAd>igqOe8PLQ`o>US>S6+l+;T8^ye$OS;w@kWkpkWIXKO=rh& zsz%)K8l*9}h%e$TybalG;Y+6`oTkyuXY=wS#ZVJT`_q7no&7ebwU74>x>qBNV{$!U z1dvLP2%i%nqO`~Ff^(Ec!pDbALzaU~^nFG%L71#$oG?DO>O)2cBouQXdksQ8{QZxI zzkT=U?9a&$_a^twCbz$t{Ca#5s8>cdj#K( zXe!H?@Q_7Ap$tbk`x%R6G51&M7ZSbMG-Wl2V|v}EEb2umm&;Q#1UHh3D2VyoBO#(s z@xttr07Iy;?FPfh9lZktSVKmRq8rlYB48pLhKjXkA03PbF!>6IVJ#0rwMUy@YoG0CN2$0koq&y{3J+&9%1fb5MCoGc3W-|L}_by zQ&wk5_+$hJxvZi>xoi5ZC7o@8@))@<<)<8;%WnWV4s?vL{-Xi;869b}EKT8p;ZluY zIAmqmA}xp|G7gwCzkliM?C<-XovXXDthBFo54!EW&iTQn+J@S0GD^i8pmEij`YqEi zk&djV>{T#BOpjj-1mN*Mrc?)r-ep1X)qNv0rCDawO zSCrTSA!`A~{&`SYQT^)g9kj2sJ6&0e(lE|2nRlT>(`ljPwIK~?<(&Nm8)=Xb-PTNrlaSU e!mKAXr-o|CF-piWVLIyVAXA@uo2c}k`SgD_>4^9M literal 0 HcmV?d00001 diff --git a/backend/config/__pycache__/database.cpython-310.pyc b/backend/config/__pycache__/database.cpython-310.pyc index dbe433aa2edb0129c8ccba90ba3ef618ead1ad66..d7a56f23d1ded8d685c2885c418af595fe63e96a 100644 GIT binary patch delta 599 zcmey$`G%V>pO=@50SFwAmSx^x*~qt)QJT?%VU|@bLzQY3PYpvD10zEX!ve;I44w=r zjFZnX%GLwr85x=w${E5zGAs}okXRlEDj%dGg^`7Ug`t@lq#L9rg)x}H5NwtQL$OCW zBO_3Zks%T)qsdfy6zG^2ecPY*PkT0X!?WIXPj^jzvS-$d^>d%kYI(A6?u)JU(_Svz z`Lum6P@0QN0SKN?S@C37pIc&aiDQ5#Sjn_!n^!z-UHX1c&x`g|PrFt;U$^l2gf>j` zo==(obmHEp6IVQ&Gaqc!vyKT*yXHTez73-9S^tD5yOscrdAg_R>5d%`aZR9sPiN12 zwtE&(;@O;rrxW^~FI@Lz|Jse>RvpJnF7Or@{d-Ah+Eq;^hm>sl4fqII-gcy)@i#;3<^U2*4MI#} njC%iz!;n2y-BYP2S34&nO50^M>Fd delta 367 zcmaFE{gsn1pO=@50SJ0Flx1FK-pIF%Ekia85x=w${E5zGAs}okXRlEDj%dGg@FYKo0&nnL26PMf*B0KW_d6adz3RW z0>u~^BB3%X87B)ddoYSkj%JP!XJlbyV-#ZKVdP_EVr2Tq^k0cp??;iu?3iAg7oE tNH8@BF^Vzj{V!6Oe4k~BrX)zY0EmzQ5fUIm0Yo6offzP<4XZsP4*-U*L=^x4 diff --git a/backend/config/__pycache__/settings.cpython-310.pyc b/backend/config/__pycache__/settings.cpython-310.pyc index aa592d1e9b9830491e2945ac40880a2e68ad2178..37abc9884e1c2593d013ebe917c812b547176277 100644 GIT binary patch delta 1164 zcmZ`&&2QX96rb_eI?k?llWv=jh9(qJX$8s+6p2XCR(wcQ5v3}JIn-KqW`ohjMq}?v zpr9={P%4saf}1E30aOmq7Bvw)kV2cng&ThWa&IX1?w+`EgEwBboUk=N|K58)zj^a! z=ArSMX}Yq8ihx z63tM3SqU)7(18o6WOPNQ5{|NG${xGGeBX8!Jh1aso7Je*DA#OS_FcZwJkeA@`T`8^ z4*-S;rie%v5yeEN5>c4SG%C-*)FhK~6}U!pHURz%aGe;be*m~a23Z#TgTS*Sm-=(S zO=2>Ynr9Hi@?aN8K8+2{p(Y(Bh158b3WpvEi(nrm!)fdo@DXw(O+E^IjE*lzq{xmk zbxb5ZK8M&bny1A;Vk2~dKDLYk4E-2|o}HkR(6h-vB4gBq?o35`za{l&(JVsoPuJSl zZgf9dZ6EBmzrXkA{gw9a_4fM5$<@or&eitXUi@_sx3;@iTJdKqJz4kqk6{BphhGZs z;8{E${)Oj4OM1U4Ll66p;9{JG;Y|Q6VT$XBXMMCJy^TLdRQf^+@P~K_LsfG9Nsx4J zt#tN2NH$x^@>h>$O1Agn^>5<+JDuGJP~v?38Bk6oJGXi^)Z5wHk9Tgg*Ixav)Z~+` z8Rj&Stt*H3emJ~)yO*sbMaCQDTAjVfxyvJ?cOIPbA}h&}T&Z~xZbXXhRNb4QBfoNf z?BAopERQ*!?b{7zRa~cPFN}*-i4&_U5;m~%w<5oHvftO4|NSj|D6NLCDaB@_olWzI zRePGqgA}Y|J`J=q&ZpqR9~W>EAj()4bt{&|g))*opGSGiDm#wrmwnrHJP2ikS{HHU zP1mh)Q5+vk^|s^loG?uR1%U8rWolIHPBd7ll|65f`ER+DPm1_-_=_@fnybLYblxcd z1FFkPSqyKA1;KD3)r3Rlgr$t zOUsULS72*;7n}=4r1$+iUEV;o?o@o}hsVWDPb=gzkaaYCUNs9@@MTfD7Fl_si2ngo C`)N}E delta 683 zcmYk4zmL-}6vu7HNz-1M{pA z=uN;WX4 zi!+nR2cyCKa-s>`VvUm9z`LwjatC;iwMy;+_gK43+6Nx+{W)Q~q9f=4LB8P>LHQ$o z0OcQS2;1X_Q0};JkA>DrI#;VcRCM+2^6G0&$Q}GFzf11Qf7P=>m*C_3-UhyKtDMbNFE@R|Fxbb&f_dS zN#)klURkJG{LfB=IM(pJC#*q zn^!NaB-PT(KidHw=fCa#Z33=PMPfh diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 00000000..f84f6e5e --- /dev/null +++ b/backend/main.py @@ -0,0 +1,57 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from config.settings import get_settings +from config.database import init_db +from src.routes import auth_router, users_router, projects_router + +settings = get_settings() + +app = FastAPI( + title=settings.APP_NAME, + version=settings.APP_VERSION, + description="海洋项目管理系统后端API", + docs_url="/docs", + redoc_url="/redoc", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(auth_router, prefix="/api/v1") +app.include_router(users_router, prefix="/api/v1") +app.include_router(projects_router, prefix="/api/v1") + + +@app.on_event("startup") +async def startup_event(): + await init_db() + + +@app.get("/") +async def root(): + return { + "app": settings.APP_NAME, + "version": settings.APP_VERSION, + "status": "running", + } + + +@app.get("/health") +async def health(): + return {"status": "healthy"} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + "main:app", + host="0.0.0.0", + port=5000, + reload=settings.DEBUG, + ) diff --git a/backend/pytest.ini b/backend/pytest.ini index 5a32cb91..88a2a545 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -3,6 +3,7 @@ testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* +pythonpath = . addopts = -v --strict-markers diff --git a/backend/src/__pycache__/dependencies.cpython-310.pyc b/backend/src/__pycache__/dependencies.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b06a7cd307e62232388088c3a52352ace4960bf0 GIT binary patch literal 1587 zcmah|OK%)S5T5RN?bGY*8uLn|2*hE9w2MTJ5rW7;$z@{^hr?V(v(vrytmm(k#pkGNwPf8b*ND#PMKc;xS3n88SFB*Y^L^=M*xCczkut;F{1!Z%|laXq*2t+j8(aV4pG)xvk;T2lAwh401-NyBRpgA?AE5U}M58BkW@i>%D6tio+p<@G^{)mZ&Y z+dKEabqlNkbJ=3!g=Rd6Y5&5b<=1s9RfR}ZEAOgk9f+Yu3uYQTfMp+@{x+N zR69}yDwl1dYahv-G`z{BggC4n$zvWW^wzoZSsy&>mgEB7?yAU!)8ry1!(0fSDnEy` zu8gKhKVrIeDnnUcJA$iRq@jtxjxHPCFeBqvc%H6+xl6`$*HA>!3E4Fj9a>{!lkSl{ zavkP!&A4rj&0TxXxNVIsX5@yrs2t@EON=mjkA6YM7H~Ivc>~Tm-23^-xBGwobno$_ zZyta5;33u3DQ7>-7}pNtDv08CS-X6jhq>Ycp$OE6wzoF8;F9>S+afGqIA5y;D6?h<8;jWG;jV$PaQhJ=w#Q=A{0Lpmly z6MbbVd+0FA%%b%swBG7zJ77ta=C8m}2lsamzMuTH|I6Y1{ewq8Oa~$sA=7pXB-#>L z%ylIcJW!nZ{T+e(Uee$9p|_u0xxTV;wb!=J^jEx&d$6NZ^Co!u!hBz%fuC)`+;D&e zJS>q2gx+b|%kvF-3pcx{XOQu!PR)x|pumoW_T=ja2Vea-6WWwuS+t;JDyb5BOK1vq z%jnG{BwmFjXy$E<&NL%lgQd0*$600#WL5y2j{9BQcmd5jV0!H(?G6G^`X~|sC6UE(m}M|VSx(z0RVBatN|kIB%sz Project: + result = await db.execute(select(Project).where(Project.id == project_id)) + project = result.scalar_one_or_none() + + if project is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="项目不存在", + ) + + return project + + +async def get_project_if_owner_or_admin( + project_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Project: + project = await get_project_or_404(project_id, current_user, db) + + if current_user.role != "admin" and project.created_by != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="无权访问此项目", + ) + + return project + + +async def get_user_or_404( + user_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> User: + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + + if user is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="用户不存在", + ) + + return user diff --git a/backend/src/middleware/__init__.py b/backend/src/middleware/__init__.py new file mode 100644 index 00000000..0fa35da9 --- /dev/null +++ b/backend/src/middleware/__init__.py @@ -0,0 +1,13 @@ +from .auth import ( + get_current_user, + get_current_active_user, + require_admin, + require_market_or_admin, +) + +__all__ = [ + "get_current_user", + "get_current_active_user", + "require_admin", + "require_market_or_admin", +] diff --git a/backend/src/middleware/__pycache__/__init__.cpython-310.pyc b/backend/src/middleware/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..516c1b81134ab124c2f46d7d0afeeae24d35fdc2 GIT binary patch literal 322 zcmYk1Jx&8L5QXjC4T&OFq6-J0*(SaKifEA#4c#^)V~;i_{t2_*0N0`8DjdNr6*V0d zwj!jAd$8-b;2q{{x)|Tf6`O literal 0 HcmV?d00001 diff --git a/backend/src/middleware/__pycache__/auth.cpython-310.pyc b/backend/src/middleware/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a72fd760ffea48ba46d79a49e5233bd7343afd3a GIT binary patch literal 2037 zcma)7&2QX96!(nnS!chp`D!IpApr%tNZk!82M(wrB`J-BQraY{v=__qOd4>GjEFay#c z>00-5fz_~5-SGQ@d?TOgre6q(jbf@3zZ8@kc(L*_d&74pHVtl-Z8!LcA@a zMHb3}`z)WtbWRe^M8ZQCtNnH_)h2cS26uc1#8uSNn%XKBsD;~TRd4B3OEi8!V4*h+ ziYTVKhp2HwzlK`+x=GCqv}&}BH0q6R6rs5r82o6Rh@7z6XuVHZ&AceIj5b=t8)ySf z0s&5Gt7gkwFK%e7q(#yUA9gcX-3+8ziqCXyeY-X&dVVSyhTYC6q@AzUJ6~S!T)*}B z;b)z-Z!cHQojZ3_RaO<#bp(?nt2y+jkoCe=<&uOl;ds8xEii{N9!ot==90pH^KN2Z z0Sg}hk@SOj^u^uI_4|K4xUu!)?X9)D$>5%C>;ApR-~I6D&Q%DhmU&UVrm}He2?K;2 zQC+v+8GYNHof?~+Id%H%WMg7n>d;b|W6R7<1ml(pSK}qkOACxMi9ITHFBEbxb{*g0 zb`&x@;&up6+2_U3c;Q`S)m-+?Qu=Dfg4~j`;ztgZxdgakLn5F-ocKZ-%CM>NS0VbJ ziTXkmu=;ZB*Hvlik;|OWUgA*`IA;e==$vP~KIgblZ(6U_WA4@ik5Zpq0yftjsCI2> zMOG9w-Gp-}y{+=A8pQ8{z*mT2VGChoz)cW&k|%qsox`SuEZp_&5OY^EOnz8_DfZ@c z5tw!_rK`%O1bzvWjKFb0Ac32@B5X=U4M?L;^)#@v_I>B!mCnzVZl6K7^8re`)0e_e&Cnq}fO(B#2h*jX=9^vA#%mq7q}eGiktkAUgLC|(9{ zhJvC+otL^uVDR>k7*o=8XyAp(2n27hU3m9Z#$$Hyn8_?wXAc}ykFE+ig@Lq&b(I7IxNA<6e5ISTgwhvXu90g@`_ zRmdVq;P6E_p%=zadj&8l&R9S*J7AS?uiv{ssI8)9*8gaj_)q8V5P>EMa@wswHI-9kjnfs zXFABT-;Kg~?|h9q!kKemCgAT4+*2+> literal 0 HcmV?d00001 diff --git a/backend/src/middleware/auth.py b/backend/src/middleware/auth.py new file mode 100644 index 00000000..dc0b7c66 --- /dev/null +++ b/backend/src/middleware/auth.py @@ -0,0 +1,81 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from config.database import get_db +from src.models.user import User +from src.utils.jwt import decode_access_token + +security = HTTPBearer() + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: AsyncSession = Depends(get_db), +) -> User: + token = credentials.credentials + + payload = decode_access_token(token) + if payload is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token无效或过期", + headers={"WWW-Authenticate": "Bearer"}, + ) + + user_id = payload.get("sub") + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token无效", + ) + + result = await db.execute(select(User).where(User.id == int(user_id))) + user = result.scalar_one_or_none() + + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="用户不存在", + ) + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="用户已被禁用", + ) + + return user + + +async def get_current_active_user( + current_user: User = Depends(get_current_user), +) -> User: + if not current_user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="用户未激活", + ) + return current_user + + +async def require_admin( + current_user: User = Depends(get_current_active_user), +) -> User: + if current_user.role != "admin": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="需要管理员权限", + ) + return current_user + + +async def require_market_or_admin( + current_user: User = Depends(get_current_active_user), +) -> User: + if current_user.role not in ["admin", "market"]: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="需要市场部或管理员权限", + ) + return current_user diff --git a/backend/src/models/__pycache__/project.cpython-310.pyc b/backend/src/models/__pycache__/project.cpython-310.pyc index 7e399c415330bd1f8aecf6ec586bcc81fa413a5b..a7be8949e8e83fe24247330ead5c5ff511ddecb3 100644 GIT binary patch delta 1683 zcmYk6OKcle6ox%xkL}pwCr;9YB+bKV>LziUHck3S)1-Mft=s0o!#q;Q=eBV=w$qsm zA|Pa;umB-TwMIylx@k8>Y@kwML0BQNK!OcoH!BvTr3)57SBM4Y9y`tT$antp-T$0> ze0^vBiI;nV#X!L4z)$L@cXA&#RDvxMRC*IL`MPwSN#6!p+D2T^;Vjv6~mv+YE9s|j*+VB85uNOz&$E8a^aj-(s`EWPnbMy5IGKBd^ba!yE_BQh$Gi$u-~WS+>FKrRs(7szEI7mz5Bf)YR3 z6BJybiAmI*aFz5F>1(8?NiUF|;eIv?i?9UCWf|Ofq+Wqn%Sw6Fj_#8j`5eHM{>cO{9?|r#@r~LHuM+sT4M})rTO1BiXn%%#u$Qp>@@c2*n zvNUY_bH+*r)=guR0e@)}GI>U|p4|Ov_s+vV9&JDQ?CXS6Z{m6T$&22cbNoH8X)xVL z=D_wYOf4=g%uOs|4Q~+})Yv97{g|}-@x9&ec7EU4nVZBi)Y6MG*3bSpw)U65ajXNa zJk#8O%qG+M@3LLXSl$k2b%qF5HuT~;%WfLDL~u2ey|uDY+%WOg#*9?rn$@N#UE8-;W_3&VBGCQT58?I}o$s6Np~<5Dg+pKGbA0%B()v5x zFD0$ENaJh}0nak-l5k03xxpEhqEb-e|n9Rf?jQkImKxkqN2U`aV*x zy8jD%bY3@=h*-Zx24&r6MWQjrH_Z+jx3!FxU1RyJWKqvoy9{NEg&Vn>N!+2#Y6kZv hfL1E8-*r{p;x)V@`68Ga8EO<7zc2_cH+2c;-+cJXA7Ypw5Dy!N1+8uzbKUQ(XkaYDS~xr9Cj1;zJ}YD6P-|ow!sdbkR}= zg{~BJwSATnzoKrUcA@Doi6O`ll0$+VCFv047)hrfJtSR%94AR_N(q-gLDaoTD7_?y z1?eL>BFIUSqk^0wIVQ+yk{*(s^zi#hj&JppWH>`~VvA%LAn6rkkfcwTXo%#b&@)VO zN{|tf(}J8O>EE8G1fxV}gr;*O1A?3<85CrUWJr(;B*TJ?lZ>DQ;UeX;lrK>p#oPop zFH>=jiYt`QQ=XtaMklE71?c#Ty@1uYdGY0?R}x%>FHtUU%V+-zCd+~J6}W~k<8`kb z{I*{4Rkx74IzODXO*{vtrsr%8&$GkcVr^3YS+X;{;=_L<-aoZn-#B)*J6{CT za=K^i;%zfKo0^%k`F)0Pcb?;*NdXp<6H1K5na=pQxy7*B*dlCWODOz1hP@FMR}x+& zzQcPQ?@H>5%w(qW{gWKziRbV~T4}=2y+7kUQ@-xpu`*Wndow?m!f5k9TE8Y+EZoX{ om%@jTna((-&7UEiw|z4kX_~id{dhaBWezZ`9DeidbZrUce?abf4gdfE diff --git a/backend/src/models/__pycache__/user.cpython-310.pyc b/backend/src/models/__pycache__/user.cpython-310.pyc index b5dc8aa76d16f000b2f48d75b83703a84928f55a..bc48a3305e2fce54f8683d7878bc40a6d36b454f 100644 GIT binary patch delta 1046 zcmYjPSx*yD6rQb}=}Li8M5?mXWk5?*UNpufF2op#0gTDSNQRz^jLvk*osJQGz=#j} z(8M}KO^i`rG`_e6eZUxgfWN?bGm!#+!D!;S*9CiXZ@+WCbH4k{nW@L;Q)xGqiVOJL z`}vA>C|XbF#q;(4?jc7tR8ar{BwKT0MvP!Lec|os^Lhg&_eV5WOWpl%(cl zBMs`PplfSPQ&;PAdkSIvgiDEav3!cm=whf$`sEV-Cn>Sbs^ylwDXUWd)R9oJ<1lWo zAlAS3s}iW7)gnQ&xO-NDG&I!|BS93BgbXx83$#MEra%-VXyaT8ITey~B4#AWK|9u` zd2R#dI$&c>GMX^XaK01r$TwpgAuX^8Hp7;h1YKAq8?BhllD4P0X99FXPfb2yw6CZB z5WP@9feoYsXS5Zzq4-9Qx5Ez5QKS>2zCTPJ`f(;ZgC+4?UJ0Xq*_!rAsFi)&HYaQn zMyxWBt27hzOZjLkjsW3lsnD#;lt)>f!XucLa&@3RcCA<3VL_;k93LGUIeByp4QMW!w@H=w36xpB{b>2);`<$0inH2RXs!9OQcjC);ER0p!}p=W7+uM#)%oqrbT1H7rASIvX7q@ z*wMqvGJne|zeT*40^P@0c6>h)sLHMj#Q~3&1{@b3+Z$j$ij^74X2>q`202i~jFb~w zaAn0j?hLM+DB+5-{ZooC#PN@$nnso@4C_t7ZKdxz<(|{tv~Aj@DdNl&5z-h+hyN#C x>hK2$n@VnZ(z;m0VVV;rT4L{lE3Ui$jrKDQ?Et#>ZE{e=$YUcYh)KS(@-M#N8R7r{ delta 661 zcmYjOJ8u&~5Z>K;=3<{8IiCp_NTf6u2S?DPK?Kpzpfo^p1?b{ggQc?_-{p`}fiMk) zCCsLvK?)5I2_*tS1ri+vKjAtG9J``NAecRn!j9(KZ@!tGoz?E{*9-clD=T4P1=1rOUf9V)pM2}M<^0clQU=CHWPS4)8VRncuxyo};fFhIz3g}SI)I6#N z%Ll z%%jHEP9*=?K#x}tOrp{vb7(m;7qLif%FuM_znCVOWhh`~kTQ Bodf^? diff --git a/backend/src/models/project.py b/backend/src/models/project.py index 39959a06..2e279fa7 100644 --- a/backend/src/models/project.py +++ b/backend/src/models/project.py @@ -2,12 +2,7 @@ from sqlalchemy import Column, Integer, String, Text, Date, Numeric, DateTime, Enum, ForeignKey from sqlalchemy.sql import func from sqlalchemy.orm import relationship -from ..config.database import Base - - -class AdjustmentEnum(str, Enum): - YES = "yes" - NO = "no" +from config.database import Base class Project(Base): @@ -48,7 +43,7 @@ class Project(Base): # 成本控制 total_cost_control = Column(Numeric(15, 2), comment="总体成本(控制)") - is_adjusted = Column(Enum(AdjustmentEnum), default=AdjustmentEnum.NO, index=True, comment="是否调整") + is_adjusted = Column(Enum("是", "否", name="is_adjusted"), default="否", index=True, comment="是否调整") labor_cost_control = Column(Numeric(15, 2), comment="人工成本(控制)") labor_cost_planned = Column(Numeric(15, 2), comment="农民工工资(计划)") labor_cost_paid = Column(Numeric(15, 2), comment="农民工工资(实付)") diff --git a/backend/src/models/user.py b/backend/src/models/user.py index bf186944..16632b83 100644 --- a/backend/src/models/user.py +++ b/backend/src/models/user.py @@ -2,13 +2,7 @@ from sqlalchemy import Column, Integer, String, Boolean, DateTime, Enum, ForeignKey from sqlalchemy.sql import func from sqlalchemy.orm import relationship -from ..config.database import Base - - -class UserRoleEnum(str, Enum): - admin = "admin" - market = "market" - other = "other" +from config.database import Base class User(Base): @@ -19,7 +13,7 @@ class User(Base): password_hash = Column(String(255), nullable=False, comment="密码哈希") real_name = Column(String(100), nullable=False, comment="真实姓名") department = Column(String(50), nullable=False, index=True, comment="部门") - role = Column(Enum(UserRoleEnum), nullable=False, index=True, comment="角色") + role = Column(Enum("admin", "market", "other", name="user_role"), nullable=False, index=True, comment="角色") email = Column(String(100), unique=True, index=True, comment="邮箱") phone = Column(String(20), comment="电话") is_active = Column(Boolean, default=True, index=True, comment="是否激活") diff --git a/backend/src/routes/__init__.py b/backend/src/routes/__init__.py new file mode 100644 index 00000000..e8cb9dac --- /dev/null +++ b/backend/src/routes/__init__.py @@ -0,0 +1,9 @@ +from .auth import router as auth_router +from .users import router as users_router +from .projects import router as projects_router + +__all__ = [ + "auth_router", + "users_router", + "projects_router", +] diff --git a/backend/src/routes/__pycache__/__init__.cpython-310.pyc b/backend/src/routes/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9071732a24d8a2485dc1b398c6adb605ed173fe4 GIT binary patch literal 322 zcmYk1zfQw25XSBNX(LF*b7ZOe00^ufoE@=#n(gw5XM zc4_km(G)+DV~vap^5cNioFjOnXZoHpU@2R$9+@9BDGVRf^wEvP6;ZK zujYO#6D|R?a{%vx(oU27-l@IsP`w1FjJK$~LEXbS2%YZr4oN*~bHLuJwF+d^G+a;s bZQl+6>*H&zt~+l>hc{`TP_fjamVB~5tmjvH literal 0 HcmV?d00001 diff --git a/backend/src/routes/__pycache__/auth.cpython-310.pyc b/backend/src/routes/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1def7b60c8fcb0b0dfd4ca409e1ffed2ea8ac88 GIT binary patch literal 2043 zcmZ`)TWcIQ6dq}2XLo03FTOjMHqf`FW!FhlD5W$FBqgE64UY3N6o&CgYtLq8XCrCi z3mb^*(uRWDJfwkup*Hj(=|lZe2sD&F^tHc0uf>k@4@ybn9%*gIG3_iP9qD|M&V1)P z+O%AD40t~K<7V`3(J=m0gTs#vgAd{54l)d45EL5;@=$_3oS2@OSe}K{JdSgT?b+Hk z<9t%^3fi{fV&ZtP7%;iGl(?R&Z96U}6|a(1y{aDP<61J{4QRU%4<hf8=-CslTk9JF|X;p#{Uy@j2zdP*U5>iM*ZY7#Zujp#q(D#Po_PGcGhWzACIlNqqAYn-w= zmhLC%Y}A>eH!{kFEbonYH|=n$woW} zev%RzOGi6=5{N)zG81CO%ifcNJ`eBj@bZVyETbM;Hv|%R1+AL`x2>Mhwyxt1w1F;z zL@pV(a1XENHjGAA-(0vkFs$H`02fe+jkys z-Cf4}Lmk1UzznPyk!6W}0F+wJY2p8(Y4*^Dfxv~+?563vbQY&vrl_`#E{U%%VFb9d|O z@1;FM0|r7dWlkqnI=b`4Vi#gi>+6FuPZwyYw5hVdpuP>6o1dew?sa7k@-T=4=BFL% zL$Dp_Sy?#>`zn&W`2}e3Z7Vm7 zoZW&sh%vGdhF1No=k^QRIXZ)#8nSnB4iJDVAnPBTv;ShJVWTCpnNaB-uBi~N%Y#SR ziZopZqhKr}rD9>Mqn-iE12t>aS(E)l?|k!k>)ZSNL^Fl2W6d9X3+zXToq=#GBYRWGi?RnAH|Bya4A|$5sP_O+XlmbET{eKOso5KBW zkth^eOwxo*>J({i4w0#ffazox-iHs#V)}tn7FTi@H(b>J)%JQ9Rj_N?ru722-o@B` Q;Wlk-?PBEax}}roUx6z;p8x;= literal 0 HcmV?d00001 diff --git a/backend/src/routes/__pycache__/projects.cpython-310.pyc b/backend/src/routes/__pycache__/projects.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4047c5eeaab74e765a4fa32d75722d30439d9073 GIT binary patch literal 8810 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 Y51cRB49Ro(P3G(EV)%WN`HJuVFTl#ONB{r; literal 0 HcmV?d00001 diff --git a/backend/src/routes/__pycache__/users.cpython-310.pyc b/backend/src/routes/__pycache__/users.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1915ccf622c55ee393ef7802e43cdd319df45820 GIT binary patch literal 4470 zcmbtXTW=f372cV>l3Y@>B+HVo@-=1>OSaKIv@nb&b=|l~6~~tsA}+D!jHH!!NqTk} zTkZ<5s~|NBsCA1VND%`ifj-12>RwR5MTfeD3B#~ zcIM1Eb2;bx&J64Md{%?!7oWcC|7M4#{SzDgPZ1j5f;Y-iO=B7(ffkZ732B*zdRZqZ zqk$2cWmDDlzzXfM9Xe$v%#<@qW(3(VSI()r8MtA-oL6-#D1-y$fv{LEhTF>9!ol)j zI8+`|eReP$ZZB_Fbtf1Jca(RidL|eR$I4?w^%rxGcsed%e_VNMNwXoPeHm)r8tF`U#tZk#k0hoK;;vhSuEDnIhLH1&c zy}a17a@b*ZL|GjItHWPnwLZ^F?5MIhqV`gH;mTg-Lw)nS!d_KYFR|f^TItv-qNJqD z%xmY*U8px>E~I^iH+YRj(tNwg#e9iK_qAxgR=voh$gkHx*@}3;tFbhuo3*Oc>!Jcm z)(np;>?+iZ%MlkPQ)cn|ZNa^mr}fJXhIJR^7kJdD*CH;9*m~2C;+`ab-ix9eb-_Tw zV;C?$>qWDbMn{SQ!LK)c!7CmM{Tc|Iw;D05%?oZ>lT8>eYPztml@C67eD~4H!}iL( z_e)e-4Z)}Vo6?BAnMi72RuG+4RKhyr!74##5J_kFD)MQSVVjCk_(&S?DQPsk87?ze zRwDlv?2GY+C*qLTz#XC905kp zwsLK4k+fW}%C|L;I*HXPBu<;0gADM7wvb6Ot%0_-kOhrm0(oLoOUN*2wzefitpoM# zo_;RLwFcW-YY1#yMw!kGX1=E-?irXT$tGaUtosIJ7yG^ecyp!%q$lmt=NO)U{q)iD z&pv#7_ulePepN-+cOS@6nhEbkc%u<0X0$}RVlRM)S_Ieeb9$X#)E;1L(2ug9!dH3M z3w%Tv#F|bVm!v5UL9Z&lcztrV9`ebXQ7~DBSWnig+^ba@qCN*mu7q9IM5m$B)hrp( z^kW_ZpW=G#1!6a@X%7?+3~5KrY87}T?GQ@DmH`{^WR?p7w8E8^hJ}R|2uNw(nB{_t zy(rj+1wu$VIC;$rxpb=aTI~6?NM?X{K}88o9(sNt5Pj0D)|&v;T*~0ew5W&DiJDih zBh{tNZ}KYa85j=R@PxGL)6>8~X$F4i$FN)_j->7dK`Fn!mA(~?W33dGT4 zI%g!>oPh-Fkw8!Mmf41wG6EFPkOT;J(6hv;P1Y7lx3oS5*5%EP zflzZfcySb$SxfeU0psV9_(X$Rp{=DvR;>Y)ciYTnPFw^UgA8U-D=cNE&Nc^OlWC${ zzPGge=#R_4{b>3AC(<~1{P;=nDs(mnpjiFU!l@gsdyGj7sH$KKc^~jKtCEE{4E-fjd(oOo0pC_*WfeMk#HPv-ZNLX;2 z~Bl;aD;&&PEO(Z~hOLF8}WRmDZirA1|%^?P15I*)Ioon)KZo zztQ_6AifLRP%bW6DV8fJ*qGC|OgDJ%hdW!>Z=Zr$HvrFJc0&L>&@3I5_OutpUc(o7 z0~C0F5a??;5?v3xpgPOL`3ZhAp78L0lV2BjtB~3C&h)s$3t4)B8-Q!uv_CU}QM>8^ zi!h(b0l0%sC`v~Y>R?h=z#>W`oWtvgb2yHRg(7zWYnaI8AgVTFKZqu}TTR_Kq(=4` z3-}FB@CmOO&x&nmV#V_fzcwQVRYP5Xh14NC!^PBxzkIq6w?{pKlyU|&j7A;mc!CMc z$x?4LV}XqGVmyKRGdLFhYw7f^R literal 0 HcmV?d00001 diff --git a/backend/src/routes/auth.py b/backend/src/routes/auth.py new file mode 100644 index 00000000..1ee0b3ae --- /dev/null +++ b/backend/src/routes/auth.py @@ -0,0 +1,82 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from config.database import get_db +from src.models.user import User +from src.schemas.auth import LoginRequest, LoginResponse +from src.schemas.user import UserResponse +from src.utils.password import verify_password +from src.utils.jwt import create_access_token +from src.middleware.auth import get_current_user + +router = APIRouter(prefix="/auth", tags=["认证"]) + + +@router.post("/login", response_model=dict) +async def login( + login_data: LoginRequest, + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(User).where(User.username == login_data.username) + ) + user = result.scalar_one_or_none() + + if not user or not verify_password(login_data.password, user.password_hash): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "success": False, + "message": "用户名或密码错误", + "data": None, + "error_code": "1002", + } + ) + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "success": False, + "message": "用户未激活", + "data": None, + "error_code": "1002", + } + ) + + access_token = create_access_token( + data={"sub": str(user.id), "username": user.username} + ) + + return { + "success": True, + "message": "登录成功", + "data": { + "token": access_token, + "token_type": "bearer", + "user": UserResponse.model_validate(user), + }, + "error_code": None, + } + + +@router.get("/me", response_model=dict) +async def get_current_user_info( + current_user: User = Depends(get_current_user), +): + return { + "success": True, + "message": "获取成功", + "data": UserResponse.model_validate(current_user), + "error_code": None, + } + + +@router.post("/logout", response_model=dict) +async def logout(current_user: User = Depends(get_current_user)): + return { + "success": True, + "message": "登出成功", + "data": None, + "error_code": None, + } diff --git a/backend/src/routes/projects.py b/backend/src/routes/projects.py new file mode 100644 index 00000000..df60d32d --- /dev/null +++ b/backend/src/routes/projects.py @@ -0,0 +1,432 @@ +from fastapi import APIRouter, Depends, Query +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func, and_, desc, asc +from config.database import get_db +from src.models.user import User +from src.models.project import Project +from src.schemas.project import ( + ProjectCreate, + ProjectUpdate, + ProjectResponse, + ProjectListResponse, +) +from src.middleware.auth import ( + get_current_user, + require_market_or_admin, +) +from src.dependencies import get_project_or_404 +from typing import Optional +from datetime import date +from decimal import Decimal + +router = APIRouter(prefix="/projects", tags=["项目管理"]) + + +@router.get("", response_model=dict) +async def get_projects( + page: int = Query(1, ge=1), + page_size: int = Query(10, ge=1, le=100), + project_no: Optional[str] = None, + engineering_type: Optional[str] = None, + project_department: Optional[str] = None, + signing_date_start: Optional[date] = None, + signing_date_end: Optional[date] = None, + contract_amount_min: Optional[Decimal] = None, + contract_amount_max: Optional[Decimal] = None, + keyword: Optional[str] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = Query(None, pattern="^(asc|desc)$"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + query = select(Project) + + conditions = [] + if project_no: + conditions.append(Project.project_no == project_no) + if engineering_type: + conditions.append(Project.engineering_type == engineering_type) + if project_department: + conditions.append(Project.project_department == project_department) + if signing_date_start: + conditions.append(Project.signing_date >= signing_date_start) + if signing_date_end: + conditions.append(Project.signing_date <= signing_date_end) + if contract_amount_min: + conditions.append(Project.contract_amount >= contract_amount_min) + if contract_amount_max: + conditions.append(Project.contract_amount <= contract_amount_max) + if keyword: + conditions.append( + (Project.name.contains(keyword)) | + (Project.owner_unit.contains(keyword)) + ) + + if conditions: + query = query.where(and_(*conditions)) + + if sort_by: + sort_column = getattr(Project, sort_by, None) + if sort_column: + order = desc if sort_order == "desc" else asc + query = query.order_by(order(sort_column)) + else: + query = query.order_by(Project.created_at.desc()) + + total_query = select(func.count()).select_from(query.subquery()) + total_result = await db.execute(total_query) + total = total_result.scalar() + + query = query.offset((page - 1) * page_size).limit(page_size) + result = await db.execute(query) + projects = result.scalars().all() + + project_data = [] + for p in projects: + creator_result = await db.execute( + select(User.real_name).where(User.id == p.created_by) + ) + creator_name = creator_result.scalar_one_or_none() + p_dict = ProjectResponse.model_validate(p).model_dump() + p_dict["created_by_name"] = creator_name + project_data.append(p_dict) + + return { + "success": True, + "message": "获取成功", + "data": { + "items": project_data, + "total": total, + "page": page, + "page_size": page_size, + }, + "error_code": None, + } + + +@router.get("/{project_id}", response_model=dict) +async def get_project( + project: Project = Depends(get_project_or_404), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(User.real_name).where(User.id == project.created_by) + ) + creator_name = result.scalar_one_or_none() + + project_dict = ProjectResponse.model_validate(project).model_dump() + project_dict["created_by_name"] = creator_name + + return { + "success": True, + "message": "获取成功", + "data": project_dict, + "error_code": None, + } + + +@router.post("", response_model=dict) +async def create_project( + project_data: ProjectCreate, + current_user: User = Depends(require_market_or_admin), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(Project).where(Project.project_no == project_data.project_no) + ) + if result.scalar_one_or_none(): + return { + "success": False, + "message": "项目编号已存在", + "data": None, + "error_code": "2002", + } + + project = Project( + **project_data.model_dump(), + created_by=current_user.id, + ) + db.add(project) + await db.commit() + await db.refresh(project) + + return { + "success": True, + "message": "项目创建成功", + "data": ProjectResponse.model_validate(project), + "error_code": None, + } + + +@router.put("/{project_id}", response_model=dict) +async def update_project( + project_id: int, + project_data: ProjectUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(Project).where(Project.id == project_id)) + project = result.scalar_one_or_none() + + if not project: + return { + "success": False, + "message": "项目不存在", + "data": None, + "error_code": "2001", + } + + if current_user.role == "market" and project.created_by != current_user.id: + return { + "success": False, + "message": "无权修改此项目", + "data": None, + "error_code": "3001", + } + + update_data = project_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(project, field, value) + + await db.commit() + await db.refresh(project) + + return { + "success": True, + "message": "项目更新成功", + "data": ProjectResponse.model_validate(project), + "error_code": None, + } + + +@router.delete("/{project_id}", response_model=dict) +async def delete_project( + project_id: int, + current_user: User = Depends(require_market_or_admin), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(Project).where(Project.id == project_id)) + project = result.scalar_one_or_none() + + if not project: + return { + "success": False, + "message": "项目不存在", + "data": None, + "error_code": "2001", + } + + if current_user.role == "market" and project.created_by != current_user.id: + return { + "success": False, + "message": "无权删除此项目", + "data": None, + "error_code": "3001", + } + + await db.delete(project) + await db.commit() + + return { + "success": True, + "message": "项目删除成功", + "data": None, + "error_code": None, + } + + +@router.get("/statistics/basic", response_model=dict) +async def get_statistics( + engineering_type: Optional[str] = None, + signing_date_start: Optional[date] = None, + signing_date_end: Optional[date] = None, + contract_amount_min: Optional[Decimal] = None, + contract_amount_max: Optional[Decimal] = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + query = select(Project) + + conditions = [] + if engineering_type: + conditions.append(Project.engineering_type == engineering_type) + if signing_date_start: + conditions.append(Project.signing_date >= signing_date_start) + if signing_date_end: + conditions.append(Project.signing_date <= signing_date_end) + if contract_amount_min: + conditions.append(Project.contract_amount >= contract_amount_min) + if contract_amount_max: + conditions.append(Project.contract_amount <= contract_amount_max) + + if conditions: + query = query.where(and_(*conditions)) + + result = await db.execute(query) + projects = result.scalars().all() + + total_count = len(projects) + total_contract_amount = sum( + p.contract_amount or Decimal(0) for p in projects + ) + total_receipt_amount = sum( + p.actual_receipt_amount or Decimal(0) for p in projects + ) + total_payment_amount = sum( + p.actual_payment_amount or Decimal(0) for p in projects + ) + + avg_receipt_rate = 0 + if total_contract_amount > 0: + avg_receipt_rate = ( + (total_receipt_amount / total_contract_amount) * 100 + ) + + avg_payment_rate = 0 + total_payable = sum(p.payable_amount or Decimal(0) for p in projects) + if total_payable > 0: + avg_payment_rate = ( + (total_payment_amount / total_payable) * 100 + ) + + avg_progress = 0 + if total_count > 0: + avg_progress = sum( + p.cumulative_progress or Decimal(0) for p in projects + ) / total_count + + return { + "success": True, + "message": "统计成功", + "data": { + "total_count": total_count, + "total_contract_amount": float(total_contract_amount), + "total_receipt_amount": float(total_receipt_amount), + "total_payment_amount": float(total_payment_amount), + "avg_receipt_completion_rate": float(avg_receipt_rate), + "avg_payment_completion_rate": float(avg_payment_rate), + "avg_cumulative_progress": float(avg_progress), + }, + "error_code": None, + } + + +@router.get("/statistics/group", response_model=dict) +async def get_group_statistics( + group_by: str = Query(..., pattern="^(engineering_type|project_department)$"), + signing_date_start: Optional[date] = None, + signing_date_end: Optional[date] = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + query = select(Project) + + if signing_date_start: + query = query.where(Project.signing_date >= signing_date_start) + if signing_date_end: + query = query.where(Project.signing_date <= signing_date_end) + + result = await db.execute(query) + projects = result.scalars().all() + + groups = {} + for p in projects: + group_value = getattr(p, group_by) + if group_value not in groups: + groups[group_value] = { + "count": 0, + "total_contract_amount": Decimal(0), + "total_receipt_amount": Decimal(0), + "total_payment_amount": Decimal(0), + } + groups[group_value]["count"] += 1 + groups[group_value]["total_contract_amount"] += p.contract_amount or Decimal(0) + groups[group_value]["total_receipt_amount"] += p.actual_receipt_amount or Decimal(0) + groups[group_value]["total_payment_amount"] += p.actual_payment_amount or Decimal(0) + + result_list = [] + for key, value in groups.items(): + if group_by == "engineering_type": + item = {"engineering_type": key} + else: + item = {"project_department": key} + item.update({ + "count": value["count"], + "total_contract_amount": float(value["total_contract_amount"]), + "total_receipt_amount": float(value["total_receipt_amount"]), + "total_payment_amount": float(value["total_payment_amount"]), + }) + result_list.append(item) + + return { + "success": True, + "message": "统计成功", + "data": result_list, + "error_code": None, + } + + +@router.get("/statistics/timeline", response_model=dict) +async def get_timeline_statistics( + time_field: str = Query(..., pattern="^(signing_date|start_date|planned_end_date)$"), + group_by: str = Query("month", pattern="^(day|month|year)$"), + engineering_type: Optional[str] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + query = select(Project) + + if engineering_type: + query = query.where(Project.engineering_type == engineering_type) + if start_date: + date_column = getattr(Project, time_field) + query = query.where(date_column >= start_date) + if end_date: + date_column = getattr(Project, time_field) + query = query.where(date_column <= end_date) + + result = await db.execute(query) + projects = result.scalars().all() + + groups = {} + for p in projects: + date_value = getattr(p, time_field) + if not date_value: + continue + + if group_by == "day": + key = date_value.strftime("%Y-%m-%d") + elif group_by == "month": + key = date_value.strftime("%Y-%m") + else: + key = str(date_value.year) + + if key not in groups: + groups[key] = {"count": 0, "total_contract_amount": Decimal(0)} + groups[key]["count"] += 1 + groups[key]["total_contract_amount"] += p.contract_amount or Decimal(0) + + result_list = [] + for key in sorted(groups.keys()): + value = groups[key] + if group_by == "day": + item = {"day": key} + elif group_by == "month": + item = {"month": key} + else: + item = {"year": key} + item.update({ + "count": value["count"], + "total_contract_amount": float(value["total_contract_amount"]), + }) + result_list.append(item) + + return { + "success": True, + "message": "统计成功", + "data": result_list, + "error_code": None, + } diff --git a/backend/src/routes/users.py b/backend/src/routes/users.py new file mode 100644 index 00000000..62d76d6c --- /dev/null +++ b/backend/src/routes/users.py @@ -0,0 +1,228 @@ +from fastapi import APIRouter, Depends, Query +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func, or_ +from config.database import get_db +from src.models.user import User +from src.schemas.user import ( + UserCreate, + UserUpdate, + UserResponse, + UserListResponse, + PasswordReset, +) +from src.utils.password import hash_password +from src.middleware.auth import require_admin +from typing import Optional + +router = APIRouter(prefix="/users", tags=["用户管理"]) + + +@router.get("", response_model=dict) +async def get_users( + page: int = Query(1, ge=1), + page_size: int = Query(10, ge=1, le=100), + department: Optional[str] = None, + role: Optional[str] = None, + keyword: Optional[str] = None, + current_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + query = select(User) + + if department: + query = query.where(User.department == department) + if role: + query = query.where(User.role == role) + if keyword: + query = query.where( + or_( + User.username.contains(keyword), + User.real_name.contains(keyword), + User.email.contains(keyword), + ) + ) + + total_query = select(func.count()).select_from(query.subquery()) + total_result = await db.execute(total_query) + total = total_result.scalar() + + query = query.offset((page - 1) * page_size).limit(page_size) + result = await db.execute(query) + users = result.scalars().all() + + return { + "success": True, + "message": "获取成功", + "data": { + "items": [UserResponse.model_validate(u) for u in users], + "total": total, + "page": page, + "page_size": page_size, + }, + "error_code": None, + } + + +@router.post("", response_model=dict) +async def create_user( + user_data: UserCreate, + current_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(User).where(User.username == user_data.username) + ) + if result.scalar_one_or_none(): + return { + "success": False, + "message": "用户名已存在", + "data": None, + "error_code": "2002", + } + + if user_data.email: + result = await db.execute( + select(User).where(User.email == user_data.email) + ) + if result.scalar_one_or_none(): + return { + "success": False, + "message": "邮箱已存在", + "data": None, + "error_code": "2002", + } + + user = User( + username=user_data.username, + password_hash=hash_password(user_data.password), + real_name=user_data.real_name, + department=user_data.department, + role=user_data.role, + email=user_data.email, + phone=user_data.phone, + ) + db.add(user) + await db.commit() + await db.refresh(user) + + return { + "success": True, + "message": "用户创建成功", + "data": UserResponse.model_validate(user), + "error_code": None, + } + + +@router.get("/{user_id}", response_model=dict) +async def get_user( + user_id: int, + current_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + + if not user: + return { + "success": False, + "message": "用户不存在", + "data": None, + "error_code": "2001", + } + + return { + "success": True, + "message": "获取成功", + "data": UserResponse.model_validate(user), + "error_code": None, + } + + +@router.put("/{user_id}", response_model=dict) +async def update_user( + user_id: int, + user_data: UserUpdate, + current_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + + if not user: + return { + "success": False, + "message": "用户不存在", + "data": None, + "error_code": "2001", + } + + update_data = user_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(user, field, value) + + await db.commit() + await db.refresh(user) + + return { + "success": True, + "message": "用户更新成功", + "data": UserResponse.model_validate(user), + "error_code": None, + } + + +@router.delete("/{user_id}", response_model=dict) +async def delete_user( + user_id: int, + current_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + + if not user: + return { + "success": False, + "message": "用户不存在", + "data": None, + "error_code": "2001", + } + + await db.delete(user) + await db.commit() + + return { + "success": True, + "message": "用户删除成功", + "data": None, + "error_code": None, + } + + +@router.post("/{user_id}/reset-password", response_model=dict) +async def reset_password( + user_id: int, + password_data: PasswordReset, + current_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + + if not user: + return { + "success": False, + "message": "用户不存在", + "data": None, + "error_code": "2001", + } + + user.password_hash = hash_password(password_data.new_password) + await db.commit() + + return { + "success": True, + "message": "密码重置成功", + "data": None, + "error_code": None, + } diff --git a/backend/src/schemas/__init__.py b/backend/src/schemas/__init__.py new file mode 100644 index 00000000..ebf03ce8 --- /dev/null +++ b/backend/src/schemas/__init__.py @@ -0,0 +1,28 @@ +from .auth import LoginRequest, LoginResponse +from .user import ( + UserCreate, + UserUpdate, + UserResponse, + UserListResponse, + PasswordReset, +) +from .project import ( + ProjectCreate, + ProjectUpdate, + ProjectResponse, + ProjectListResponse, +) + +__all__ = [ + "LoginRequest", + "LoginResponse", + "UserCreate", + "UserUpdate", + "UserResponse", + "UserListResponse", + "PasswordReset", + "ProjectCreate", + "ProjectUpdate", + "ProjectResponse", + "ProjectListResponse", +] diff --git a/backend/src/schemas/__pycache__/__init__.cpython-310.pyc b/backend/src/schemas/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52e8f933e71709b3859ad213ec3628fb3d3a76a5 GIT binary patch literal 563 zcmZ{hyH3L}7=@iQx8_2NSQvPLEcK2MLPBCfs4D8>#WJzg2I4r_376Ml<5hSBuS~oG z6ZvT<9l(-5{pZ*|zNsvWQQwZ=9!lRu=pDpv>pgVVcQfz75qByi9pXrrxDIHy8cC0M z(kH$QNFYNJ%7{eYW20&;V-m}RBr+wb%t+>-72@8DL-x3Tjq>2b>0@)(rk83})(dgp z3DeqaP^M9}5xH;Ur4jmE3)YHFy=*wtDfkPqnP;b zMbQpfKXQnh45Qoi2ZOO2nkO0_MUMa;zy}0?5D)>z0C-xR0MZ^;`NVpxYuCT)t=IKO z{x`*j&E~j&#o3TjR#lX~BE1iL06s79T1kPQOoa=@1y+S%H63E8WHnm}jc-_SCu)vo o*ysYAVl5;yn9{N?TS{lmvyD!q;$0<9;e!lZX^M`V!!QlM06#>ONdN!< literal 0 HcmV?d00001 diff --git a/backend/src/schemas/__pycache__/auth.cpython-310.pyc b/backend/src/schemas/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f585f5f1de31cc8e0ccfaaf92133e5c8cdcd5030 GIT binary patch literal 820 zcmZuv&5qMB5VrH*(6R+8v9C}L&5cuqR*M9erAUkRl8fbyM{OXnvv#u3GhBHPUWG^S zl~Z4V6Ei7-O5n&}JTvyp=WlFgvz*~bzu(s{Ib%P_I2{oh7dYJ&f?|p{tmP$dgEHV` z2Tj;UWrXWcMNQl$Wy0BOrec+RVJZRsIV@AOQxsXr-20u*I#cK7dxms&KX{T(yH%xBgol;pYzf8kBY?Zf;K)wI8_lM#Zjf8j3LBzh$<%dM>SQ9$|ZjGTe)L5R|LSlK@Xh`PBPzm+DzP{9;_9^Okzb8!aeZ26T#FiUbJ~m>(+2zXs1>)TZLjiW zMKnb7r;2Dw@5kD71%6Ak;kV@q{0{sT(ShIL_Eq?+q6@#veiwdEtifMnzbE>?t&9gx zF`~-2s#iV_GWk_1WTYFv3S}h5o^F3V&%-ncBDB{ewg=nn3m zecK~MWo7D#%C!1zMb?BTs-pJy>a;E!vMK8OCf49si(}0qX0B^a96a$OnH=XQx-&Ny_(}TopJ>+gB#5OR zpfyaSRAF-L=Vx~;GGCgIuZhPiJ!YuCh@&^oPf+^m%++PtP6T0f4j*tMqXR}t@Wyn{9LrakM^ zntIbU>v!4jTfbM_$J~E_xVisY5%2SJ*KK@oEg>xu<;>3Zv=mn$G5@$mi0&M zZ(Dz}xR2SdJN7b( zM^4<*y&@+bD2bdsd@8U56m0{&K2IM@<=fqzq`d3ZfEs(*;wa2zjAm?kwL-D(GA58T zIW}|ThsmkT@>nLhUV9uU6(sqYuV|O+^-Sh@B&n@P9X&FgW@(oD=my@%rO>_8G|I7) zk*23IQayTDCw-h?bc-a+b=ka@1sNDrgnD5a^xYm~7~dKVR9O3sCc zX@0`9>|EMXNKo~CHol`E6BRWgvq^?Y;|3w-gDpbaWNyOfT8O2%Mec1fl+o%Anditn zPv!+OFOqqQ%*$k6A=4o9Dw#2v*T}q1<_$7$lG!6OA@dfQx5-#4GH91#*Ib{l#Bx=A*M0DVVbV>4bu?w zn0CW7#5~3a)1W^wkMY4U@QHDZ-)Eng#`s_wv=hS^9}EMZn8o-@W--2DmQCAkn1z_e zw1ZjDpP0t@U=;XEhA~XCWER6DOJ-pv0TW!aOz`cTGUbrdjG4el5v91%Bho%*0i#U3 z*Jy|7O=KSg&R23K!?{iBGF&E(kx9J#Bu7VR6eMZAc>6sM9D?O4ey z(|sqi=0Pa*=zLn45$NIhC||kmWLj}qCx`YIk89!?kHVa{wQj?~P~uFlW{cxvq9UmB z`1+Ok$Vz=7AD6<-S!m}-y+@PxKA8{5d`RXlnUBbPOy(0Z`(!>PbB_!s+kHX@WUflP zchNwpoKY5+X}3(Ss}K^7dRZUkgNv&SMk3o8%UzMs1-3P)ta)# zv4+!5$=8%Ej+6A4xb0w){xXgvv=|8pY@UPUvzR2ljFYUF7zt-F$$C?^`MD(JB}S52 zj0Ce7$pvi2Ns?V&PcmI%Bowe250c4ZB$CCDV_tl>c)9jv)n$d{`t=Xg+9X50uX>jH3=Jf! z*W@ER zoIlabnjaBpR_Xl|#Sh18sHx5bPJiJ{RT1H%jBeuB6r9b>X@(b$)={^F`OU~2Vd&Qh qJ{WMZVunIDy=r}Rd*A%ta*sA~Mr=E%bTQTV)&{M??qF;5<$nQ+KXy$3 literal 0 HcmV?d00001 diff --git a/backend/src/schemas/__pycache__/user.cpython-310.pyc b/backend/src/schemas/__pycache__/user.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ae29bf3b72853f46ab750652e18a1408e8dbd9b GIT binary patch literal 2163 zcmah~OK%)S5T5tTv)&;w#)0q-kUg*~akh|^2)Mw4NPL+?M&s>{cgVcxZYQyoTR3ou z#NWtY;V1N!lh3(v;;SCNlwFHiO?6jwcTH7&U)Oe{QDWf<{(f3L^epQiQm#I*S$lZp zUl7b`MZM^3B7Xi(Ac6gn`>;5vBhULB!+#lv8B66DNpzYPj6Z9wQ2T@*w~yK z!R{M-M*Ap59qmglD8K2ci^xvDftWZtX{xrUd3&gqn3vDb=g|?iw`Idm88*JQ#Rdi) z3trX*nRUW=UkcUmR-J4=x?3_hdeM~P30E&Vwd7)Q2XzYjQYkpnp6F^m*FLARo^Fvd zzh8DOpKR)=FfNNiCq>b8?5O6Lr$zDmQCZW+ib4>BL`uh82~fsGQMPTTNHz5*h+ukz2%)V`#wH0cd--0)v;lL_20Pa8x z51{z~Ezk*pb{|DofQ`NdAn2y)s4t~_)(JMBgkl>O@g9jC5~ft2((D$A_eor2KFn^D z?IQ>(+Z#HNSUa|{TpP7~gl*SH4Z4MlBDZs*ZEW;Qio3`zA?g}?X!e1mhQ1*K?U{{? z<00eyp03wijPON4)g9n9pb~Wqo;t2%Q7%;Vlut%$YhXBGNN`yQ~>_zPo(KLLF4#ohZi!n3^d4wc-rOyKN%HXAwM=S(B zkxq$`NT|oPEy`3NAs0%Y(znkNV=-$;eawObKu@Rxp~$7^nTYq zt_~m0C*E85>hWXIHTZxM)xnYCavgd)7|s*Mk~kh&kuAQ)WIl=CcIZ~E8m`J0*dXYx z4gG`muk$YMVu7xZq^^{}HT_cihGSM%GQR+V8t-}L^uNG8TnXQR5nvp^@C_KGUp9k` zYV%_y)kXRJO7TW&U!f7!+UwD@bWF04)d^p14ce=5+{8EJIz=pNlbd0I55`i>6NabLBEL+wlzsZE;BVs4FL=@0?=TK91s?GAqe-x6CS U_46$&MQJlPi!(0^GjA*T53#kZ)&Kwi literal 0 HcmV?d00001 diff --git a/backend/src/schemas/auth.py b/backend/src/schemas/auth.py new file mode 100644 index 00000000..d98808aa --- /dev/null +++ b/backend/src/schemas/auth.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, Field +from typing import Optional +from .user import UserResponse + + +class LoginRequest(BaseModel): + username: str = Field(..., min_length=3) + password: str = Field(..., min_length=6) + + +class LoginResponse(BaseModel): + token: str + token_type: str = "bearer" + user: UserResponse diff --git a/backend/src/schemas/project.py b/backend/src/schemas/project.py new file mode 100644 index 00000000..c15cbccd --- /dev/null +++ b/backend/src/schemas/project.py @@ -0,0 +1,136 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import date, datetime +from decimal import Decimal + + +class ProjectBase(BaseModel): + project_no: str = Field(..., max_length=50) + name: str = Field(..., max_length=200) + engineering_type: str = Field(..., max_length=50) + contract_amount: Optional[Decimal] = Field(None, ge=0) + + +class ProjectCreate(ProjectBase): + power_contract_no: Optional[str] = Field(None, max_length=100) + subitem_count: Optional[int] = Field(None, ge=0) + subitem_code: Optional[str] = Field(None, max_length=50) + total_investment: Optional[Decimal] = Field(None, ge=0) + warranty_ratio: Optional[Decimal] = None + settlement_amount: Optional[Decimal] = None + total_cost_estimated: Optional[Decimal] = None + voltage_level: Optional[str] = Field(None, max_length=50) + owner_unit: Optional[str] = Field(None, max_length=200) + owner_contact: Optional[str] = Field(None, max_length=200) + bidding_type: Optional[str] = Field(None, max_length=50) + signing_date: Optional[date] = None + start_date: Optional[date] = None + planned_end_date: Optional[date] = None + project_department: Optional[str] = Field(None, max_length=100) + project_leader: Optional[str] = Field(None, max_length=200) + payment_method: Optional[str] = None + total_cost_control: Optional[Decimal] = None + labor_cost_control: Optional[Decimal] = None + material_cost_control: Optional[Decimal] = None + other_cost_control: Optional[Decimal] = None + remarks: Optional[str] = None + + +class ProjectUpdate(BaseModel): + name: Optional[str] = Field(None, max_length=200) + engineering_type: Optional[str] = Field(None, max_length=50) + contract_amount: Optional[Decimal] = Field(None, ge=0) + total_investment: Optional[Decimal] = None + settlement_amount: Optional[Decimal] = None + actual_receipt_amount: Optional[Decimal] = None + actual_payment_amount: Optional[Decimal] = None + invoice_amount: Optional[Decimal] = None + payable_amount: Optional[Decimal] = None + cumulative_progress: Optional[Decimal] = None + labor_cost_paid: Optional[Decimal] = None + material_cost_actual: Optional[Decimal] = None + material_cost_paid: Optional[Decimal] = None + other_cost_actual: Optional[Decimal] = None + tax_amount: Optional[Decimal] = None + profit: Optional[Decimal] = None + remarks: Optional[str] = None + problems: Optional[str] = None + suggestions: Optional[str] = None + + +class ProjectResponse(BaseModel): + id: int + project_no: str + power_contract_no: Optional[str] + name: str + subitem_count: Optional[int] + subitem_code: Optional[str] + total_investment: Optional[Decimal] + contract_amount: Optional[Decimal] + warranty_ratio: Optional[Decimal] + settlement_amount: Optional[Decimal] + total_cost_estimated: Optional[Decimal] + voltage_level: Optional[str] + engineering_type: Optional[str] + owner_unit: Optional[str] + owner_contact: Optional[str] + bidding_type: Optional[str] + signing_date: Optional[date] + start_date: Optional[date] + planned_end_date: Optional[date] + actual_end_date: Optional[date] + warranty_amount: Optional[Decimal] + warranty_expiry_date: Optional[date] + actual_warranty_refund_date: Optional[date] + project_department: Optional[str] + project_leader: Optional[str] + payment_method: Optional[str] + total_cost_control: Optional[Decimal] + is_adjusted: Optional[str] + labor_cost_control: Optional[Decimal] + labor_cost_planned: Optional[Decimal] + labor_cost_paid: Optional[Decimal] + material_cost_control: Optional[Decimal] + material_cost_payable: Optional[Decimal] + material_cost_actual: Optional[Decimal] + material_cost_paid: Optional[Decimal] + other_cost_control: Optional[Decimal] + other_cost_payable: Optional[Decimal] + other_cost_actual: Optional[Decimal] + tax_amount: Optional[Decimal] + profit: Optional[Decimal] + actual_profit: Optional[Decimal] + cost_settlement_amount: Optional[Decimal] + cumulative_progress: Optional[Decimal] + receivable_amount: Optional[Decimal] + invoice_amount: Optional[Decimal] + actual_receipt_amount: Optional[Decimal] + receipt_completion_rate: Optional[Decimal] + payable_amount: Optional[Decimal] + actual_payment_amount: Optional[Decimal] + unpaid_amount: Optional[Decimal] + payment_completion_rate: Optional[Decimal] + labor_debt_amount: Optional[Decimal] + settlement_cost_amount: Optional[Decimal] + settlement_labor_cost: Optional[Decimal] + settlement_material_cost: Optional[Decimal] + settlement_other_cost: Optional[Decimal] + due_settlement_count: Optional[int] + unsettlement_count: Optional[int] + problems: Optional[str] + suggestions: Optional[str] + remarks: Optional[str] + created_by: int + created_by_name: Optional[str] = None + created_at: datetime + updated_at: Optional[datetime] + + class Config: + from_attributes = True + + +class ProjectListResponse(BaseModel): + items: list[ProjectResponse] + total: int + page: int + page_size: int diff --git a/backend/src/schemas/user.py b/backend/src/schemas/user.py new file mode 100644 index 00000000..d8f5de58 --- /dev/null +++ b/backend/src/schemas/user.py @@ -0,0 +1,44 @@ +from pydantic import BaseModel, EmailStr, Field +from typing import Optional +from datetime import datetime + + +class UserBase(BaseModel): + username: str = Field(..., min_length=3, max_length=50) + real_name: str = Field(..., max_length=100) + department: str = Field(..., max_length=50) + role: str = Field(..., pattern="^(admin|market|other)$") + email: Optional[EmailStr] = None + phone: Optional[str] = Field(None, max_length=20) + + +class UserCreate(UserBase): + password: str = Field(..., min_length=6) + + +class UserUpdate(BaseModel): + real_name: Optional[str] = Field(None, max_length=100) + email: Optional[EmailStr] = None + phone: Optional[str] = Field(None, max_length=20) + is_active: Optional[bool] = None + + +class PasswordReset(BaseModel): + new_password: str = Field(..., min_length=6) + + +class UserResponse(UserBase): + id: int + is_active: bool + created_at: datetime + updated_at: Optional[datetime] + + class Config: + from_attributes = True + + +class UserListResponse(BaseModel): + items: list[UserResponse] + total: int + page: int + page_size: int diff --git a/backend/tests/__pycache__/__init__.cpython-310.pyc b/backend/tests/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b4901cfd3d9a920c7d9f4b6baa90741d859e891 GIT binary patch literal 155 zcmd1j<>g`kf;U!WnG!(yF^Gc<7=auIATDMB5-AM944RC7D;bJF!U*D*oqk4sZmNDo zagKg+eoCr-esXGJUVK4OepYI7NqlZ%USfJ`k$zHQa&~H7ihfCIaY?a$e0*kJW=VX! ZUP0w84x8Nkl+v73JCM1>OhAH#0RSNNB+UQ- literal 0 HcmV?d00001 diff --git a/backend/tests/__pycache__/conftest.cpython-310-pytest-7.4.3.pyc b/backend/tests/__pycache__/conftest.cpython-310-pytest-7.4.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa732e20f2d33a4efcec55d77c1e49f0a951c203 GIT binary patch literal 4094 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Ý*q-;m-ya~I=kpS?16bN@dh+-7Q7>aEm zUgU@rz8%F56gyEo1|mpFx5L-7o>f{7RuX-wp3=(K3) z?huG}L^?8m0Ume7B0mUPSOpw&7~Gqyuzu~r=E^x*0*;91hfr)S0DlheA7lae^Vq`| zQT(S9Z)A^8;g4TL@g)#lr+N20Y*yUmY?-OV%(NIY(_5I?%aloz#LSE)GfOp@*%P1- znb|C~8q6$3?Km?_N6ak4vR#;&){&XzD8KiGoFZaoKcX@96N?$>%3=m1W;VozyD&4f zxW&vy$kJ$>y@=~%DM^;bdSi)c;BebwP}}_D_4VJq+3IaPg_}vf7P{Fo?A(K+jQnfn z17Nlz=3fG#}9)Rrj6bi(I=XA+`TZvhdxGg<}cy> zgL~GOv77sQR;MX9e2X0OW$fb>5M6ysJksV{56?YY-bLJbw1$B{-29EJzumZXcH{Rq z;v_oEGluP$CC^^6f=r_Ythu)l7jsKKB5S#Zl`X!;_uzIT*7-gZ`%yfJ0(lt!Jc_4K zd;!JNDB5z=c+7`d`hOK(7MLVkQxv$**Wh8037I1?p!_1dei&2RyWRFi;krIEk{x~a^?;JrFXe~hR!YblFwOi zJmQmZ_DdeOOlEO$gyRYBLqy)AT}fT&C|d!Y!Y{HEIJ+elERW~W!s94Di{f)Ao5ZKB9Z!Ew6r*|mUg&>Z@mt&+n(gSw=E$C-0O9%5F;bv!N+&FHL=JI_+ z05TZm?0|YnF<`&=cj2}>T<%ND8?ez^VAG)G2pgjbn|4}`unB2-2%C_W$6*uFa*ZJ^ z?+hCx$^Q-;q~%aHZGlaLmLqHiy22)mt!aj|yfbXjVgwriDAIB$n}(LaX%L_dT8@^+ zdSfY~<(S|jfc|1-x7^@&8nT zS~2;FAZgU9As>wG-WDD8BbSS oRx4Z2AR`b5L3~`w%6dYVhh%+#WUhhhSZ_mMW$cxSu(FNwD@zW@LL literal 0 HcmV?d00001 diff --git a/backend/tests/__pycache__/test_projects.cpython-310-pytest-7.4.3.pyc b/backend/tests/__pycache__/test_projects.cpython-310-pytest-7.4.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b18dc673413ef34762031eebfdc789d5059dcabd GIT binary patch literal 18071 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 literal 0 HcmV?d00001 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 929cff44..76e9ee21 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,3 +1,8 @@ +import sys +from pathlib import Path +backend_dir = Path(__file__).parent.parent +sys.path.insert(0, str(backend_dir)) + import pytest import asyncio from typing import AsyncGenerator, Generator @@ -5,8 +10,8 @@ from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker from sqlalchemy.pool import StaticPool -from src.main import app -from src.config.database import Base, get_db +from main import app +from config.database import Base, get_db from src.models.user import User from src.utils.password import hash_password