From bfaab857661413023f1004c2b3503fc79a6ab04a Mon Sep 17 00:00:00 2001 From: Amnesiac1745 <1426688201@qq.com> Date: Fri, 10 Oct 2025 20:05:35 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__pycache__/main_app.cpython-311.pyc | Bin 0 -> 38564 bytes src/__pycache__/question_bank.cpython-311.pyc | Bin 0 -> 2076 bytes .../question_generator.cpython-311.pyc | Bin 0 -> 17506 bytes src/__pycache__/quiz.cpython-311.pyc | Bin 0 -> 4055 bytes src/__pycache__/user_manager.cpython-311.pyc | Bin 0 -> 17301 bytes src/main.py | 23 + src/main_app.py | 632 ++++++++++++++++++ src/question_bank.py | 45 ++ src/question_generator.py | 390 +++++++++++ src/quiz.py | 91 +++ src/user_manager.py | 381 +++++++++++ src/users.json | 8 + 12 files changed, 1570 insertions(+) create mode 100644 src/__pycache__/main_app.cpython-311.pyc create mode 100644 src/__pycache__/question_bank.cpython-311.pyc create mode 100644 src/__pycache__/question_generator.cpython-311.pyc create mode 100644 src/__pycache__/quiz.cpython-311.pyc create mode 100644 src/__pycache__/user_manager.cpython-311.pyc create mode 100644 src/main.py create mode 100644 src/main_app.py create mode 100644 src/question_bank.py create mode 100644 src/question_generator.py create mode 100644 src/quiz.py create mode 100644 src/user_manager.py create mode 100644 src/users.json diff --git a/src/__pycache__/main_app.cpython-311.pyc b/src/__pycache__/main_app.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..badc7e1011cbabda901dba0ca663f99f2d2deaac GIT binary patch literal 38564 zcmeHwYjhJ=nxJG`_O&Hj!m{Nj_<`RTW8Pri#=HWUHvvO1#w8nrA5fBbxJ`$2rxTMV z4c$!|Cm|it;}9n};NIzn6G%dDXJ+{^Gp9=D?vYpLu$^-j89X~Xp*NkKoIZPY_WN#0 zS5=aX9g@t<*`0Fv)2&;#?yK(op7(zBc5-r(4vzoy*~7!thjhCCh8MvjE)e+qc?ewA z3AzTIpci7=^$jub8{4k$h;4}Nh--+`EAfT~1H{L*$9E(&By^Al!r~3>i5*D|Nh}=S zZtO5Mm{>TWJ-H*LA%%rWySc;CVA1PhbnA3N;`2Ho=~|3VcOCwfqhX2=vqM*IbWMTl zr2E6S{2yF;aA{z4;Hv+P%SmIHsJ=YLm(*c*IGS4Rhq_MrVx7*T5EZwn&EfQsEytW~ zU7bzs<$9lSm%}b@Zt8?|B3zobx$F+SS=-ck6t3g8x!StnH)+kOVP1Ta}J0!$H1b~EIX3@I&va0-N{2xhpODp=UvDG*~7QXwWyumVgM((E>S zMpm#W9psP>X)*;H+|3d)0A>rB0CR*afVo08z^OtGz-dA*z&v3pzXnZ07G19>vjxefjw%fR}YW`W{^)Tq;M8?np4GbL)9f6%h2-ApSbPr`KtoQJ2J@ z5@H_vOz_R}^cWCgg;?!a)a#syp(NKJc%5HD8~FQoCi7@rM5=_~JtvOnz;T4FP@;>l&?{)`D;9D`5MpixB3L_O%0YS#KGIP@+$*e zrSYqHxJnOR_2}HTU|9XD9d|fK+!^Zo!EkswAr9tyf@>SxXY&`Qxxe=f|Jy(GpZ}S9 zj`O=bu<}@w*wm4a^Svg2=h0F3`w!l|?7#3AJb??_r{IhSx4wV>efNXEy!GJLo4$kv z)zzowS68pC_vu8OzvY=C4jZ4r+2(Aw`$((3tHbUTPx}%9FSm6!9cs6WIFEcL7hv?p z4h5x)h-7`q#{*cs@erVOUySpp&#)HiW%n7|yIR{i8(Y{#ifC^I`T$QW*QpM>v+-Dy z!*Q}p6asPPjwVRoVefPWA}q%N12#fR*ReoM5{pr?FhS*9?asiR^yb4&ovrrpJktbw zyWMGTY-(=qa&-n1o0?ssXopM8&X*j#YPX-TxBF6fi`XN+lxET11X-MH6I$&~htJ|T z+;y^1)i@-64tskG>qG~3Xny_|d*Nqw&3+_-j{URe%~`Nuf1TZN)Y)}x|LE28V{iOo z^o{R5yz$ZghacX&f9p5?-@b*Y{O0Y4H$J%k;g$QtukC+u>!<#k?~h%5|H11w_B+Jp z{j7JIjvcEycG^c88{0bDoQ;j$X(~!l6^zYaIgM~LZs;sx_d zW+}5$CNn9SDUq2h(o3>EB)f01Oo}NfmPm0RHpfG9`qsGT%cPu=a*33G5wDwC_Uh&< zn|tFZ$+|bS{M7?j4q!MN?&MCpvdCT6zd_EaqB&IwcsvPxx*OwGfX?2&2gHwW${CVL5XfJ~-B$r70go#G`^ zJ!Gm(@}SOA@cbP5T8Dkj4Z>6dvO$g7$ON>+gNtf9rz>ui!*~ zaO-)P;rv#8xlu$CA|icYC_}{QDk2{#W+TW!kc(g{f@uiy0KB6YOW?d}K0I-N{Iia#$$xn5 zIpt}1F6;?k+@UV#;hjjOTH4y%eew2F$C^5Y`tqd74m_XnSX1-S#+EJ-2%s+xFUn)t zSQIOv{yu$+1N#|R7d9$=NsL0WnswXMgEJJr0n$6V004zb(OENSYN5Lh5c6;vUA%Q9 z)kafG2I6ShvSH`#XW8X+_fA^8;*lXHb(Q{+E;ccjf!;3HmcK@ANpJA2Q*3>?R@)cs&cHtdB zu1@>uAa1yDt_6!{8Tb;KyE;G;6IiM-EX0K}_~>5__+L0b`m-UW z%3omZA#Uu_*@wTn25qP>Phn`^mvXYLv!$!C`EXmi0J4xTK>+QX*masQCqBcbrbG62 zaSlA~Gpy-EVQ`8f^4LKRX>SDK38K=JC@#k-vGKJor?acmXJB`IR)+P%Z6UQS ze5BJSPGz8X?T9J$Lj6nivMHCEawT?xER$$FPcEj>DFp*5(z5Lm*&&l1lGKZ2mUb4_b77lNd z$U>Q{pk#$aRs`f5I&;~b?RT4{qV00gc3QNZ$vG_h0xv1?kdlE_5-E{M6(v;?slx0N zFBSpu9mpJ>ce~~;v~{aYwo+kJn;JfB*9jF210kp|P{K9)9#=jwNBz06x3)>cg8iT`0diy!qRQ zAN|z-gSQkk1enkNlk*C4t7mCPdoTK5dQ(BJ46P~=z?5!>$9=KLX&L|_wH*_8K!k`d z*_Vh|!!C+lqKGI~+=Y<@BN3PR(il<+(;@{pRUbj!RVON@NDyCXxK@@~_Y^f=VJPJ| zl;Icx0LX;s(lV*F+-sfbvCfpOv#52J*E-*0oiAG#QtQHAG6I~ewbE;?_E@WB>l|vG z(@Q=zXFSryBwG7Uv7dV;8#U#-Go=~pWzz;~+8~)W02b<9`>D}Pjk#W9p~qP0UNo>r zHZGvX1^7%EdnV6K2C8J!Vrp6}nHGHsi~eC&HS9^Ck4 z4P&k!{^+;<3qMnG0ybTI3R7+Z;KISn z704^|gk5ZFIUV_kh}{vZN2^!gogut|BE~Eic}cm4ln)#S zo>L~XDS?M)t1@5wct&5bHPD$W@5V?)8|9*nv}ogGIq_nta@idxU3W-2(k_t>nRHOn z;U%Jnh|;NUiHI`!4kh1_$ahrr`F0x1B!ggm1gX7KLN2|Fk}fZCdVss{?g0j0CTA!) zBat(l(=Q&VN7-O6dpUyga=fI?L)xUSV-jhT$#F`cYmciq{TUPL=@6MS=4I2rAx?i% z^24_$4M5IX9$(Z-9GqB3lLk~=egX6?XXX7Demr(IgtvISxDQf|&)h3`vR>SesZg=O zF`xgp|3S&~v)h0Xe>?i-TcekLQ=TZcLL%`Q1P2ftMu2ZvM5Q=m@O{Y~nIQ5L55kS0 z=J^nWeFld5w0wLU+~_XUprxqT@J&ePxB-AO_Q1@~^;qZ1*7?*r-)mjsu`ZFV%cyl3 zF!q)-YAN?xW_m0$Wy>sTndP<2_gLo3mW9-^u-CwueAncd{0fh;VqpGInQUA_jY|*< zZqf(2e5v9o*|d?GHcG+sQ7qn9Qbn@$d}f^Z_n85&6OSlza^=^TlVP?&4_}9WI$Wlcd;VPN&3`goxzYW8YCjacSO!NoUOzDlb& zdjYND;U{brmo+tSOW&*S#dWpT=LtxfX_L|lq)#v|qL*ZYa#4t5$E3@O@~+P5++4;F z=7+!Pc14hp43KIwT_{j-QlUt-YAhcn+wzowl>m$ts>xRrt_oG*Ntm8homVtD*oE&f zAnxX?|F(bVg8zkA{qJ2Ky@`5YwkGlNXSdJ!;#$R~(~7{o4s%-%z=g_%2Ul-A zywM-9hBZS3KafMjrJA2t0eB?_5U<`1p~&N$PTS2pz|>X`>&;DSx>p{cV!EeKqPe&6(QN3hFQIj?egjQxGrhK@9@|pcww&6QPhPAq z8~CmSq9SRKNdqMfUef3xjgq}pB8@UROvz!19Oja4*+BR38hIAjuxb^vnwPBekac&r zNo1W&wotM~B3o3cPY>^wNS#dTD5>+34IZ-LE?Ag0$mD5Co|eedD6%p`Uzs$^E*$~k z^td9NddUe7IU)6cTzf($-=*Zc68Uam=^~v`d$&;9v0Ey9S}uH=7Cw!leIC;rMhQL;%Qo9>mA^&cK^(phWo9+IAJkV^K+ zCHrW}KFohQ%YV9;lzB+mK$AqkG*(SXwM42hez`q-;jr==yrjcJIwS|!^gt=$qQoT; zmr8)2#p?rAf>RMw)VvMSEhfOXJdv0JMm5!v$;oh7XJUI|zzi1)W;jlB)r)S%HN(L( zN3o``MsM6F@C4rt>tgZcm|Z&sMhvQai|qNYufX;Z-JXO%KaJ;qYWSQ;`7MkG>9 zgXJD$`9SedrfjUCM$nzcBo>7Pn~>6g2{r{DQ-N%nPEFIL;E7^Q5qm09iGbsd&CU|& zoEO15sODEDGuyry>!7;SuP4tyPJ9N2cy4h@^!O0}+Nl@f&%|ivBn}sLXlj?x6Z5)G zAaBHi9WYkQ@sGm;Cp{5oY6K2gFPM-67Lo^3t^tO*9>Ov_6s#luf(-Teg@Ms|U^6Sp z@-I-*6joB|zd%V=cxPXouYa=sONX*J?>}i7!uCY(Dg(+g1>ZT0jLgBTNsWvsrGdo4 z?Rmi*@mD?CwX8Hym~4<(a$Kd5jMEQAVu8(w4=-HtzZ|iS4#yn78CCwk_KaS6_raTi zRU=Fw86Et^!;gPdng3^f0mvK+A;}n~Q_OA{LgC`QYh%CYgN-tX@A-k~EF>u~6^)zU zR0qPhI6~86>U~MDa|kvgIh(}OutqPPasJ^OlEfL14_lD0hz(*7TzAjW&=Odk0$Zju zJDVcKh5wEDp!p9N>bpC=n_E4bTcyKJd9#adcAZtEg)!u&fV`myCT>Kq27oXAu)VGI zu#<^+uyySI@O$5UU&ME`2?Fc_dl5_hrrP{}AhXCeACos9r<;!l+RR!tcD?UgX;s`| z=(umTO*-IhBiqy|Z|pSZ>DjaX7e7$=9>o~F8DDX% zXcyiIfAOLVZsBsy9QYN-6&ceo1HR#nnNb*Pv=igA*{F`bbuJ#3KXtjBz7*{$O=b)Z z3X&*8`jRly#*iiEV34tDg1g=M6HSJHgj9|^6efYgQkH90cx{V4w#Bk-3AHif$mEu5 z77uKcmhX_rPMPeaWT%(x^^m<%BS=eoWpap;LlQak>9SRHSv_60PfG8)xRa)r(Db=9 zeX#^*Q&(>imk7b`SvS-TKsoO&W0~xwWUrST@W5(FT=;lES@;MmAh{4$GH^gzk9J9G0_P+iRaf}V zPP#^bXRw~RSkGLnXYLXkb4Be8J#L z*bZe4Fr(G)#ELIK)5Y@$eguF~H)Sunl4Dc`a4MwaiUlPdJTvynV`koER)TYZ1$4v9|WhTs28Aojye-@^(H43(Y2M3g6kh3%%9cP6?8D% z>jvme!#v7BnMUYqc7RrtquO8`{$7Ls>pTKvR;V?!9QRlU4rqBbkZoRHjdnOzNN^%T z3LHJL+IRB0?v1dm70MX(7_kTEc`B(KW+&<2+=IE7kqp$ShE?{&PblHU_s0Wwc1hr})pUb}i z2?KU&)Z~|&eX(t@j|&$=Lo20eFnw3Mk3G1r?-b8#jB8C*#2RT zXN#eu9CZKy6Ox2P>%FuL+%PoTn>pW;IbY6PNHZ78X^UvuqTV$lY3Y6GQd$w59tPco z%C8c4&yr0oMeaS)tgW(T8?|hcEZasbwu|wP47&8crX{(GyER+)9bGv}?E6Z^Z_dW6eXC9gK= zes4;ui;w-it+-Af`-$EFVa@(&eDw_Ny$cgryQ#zh&brvHCU{TEF&Wgy70H-u#1THJ zVc)%a;SE>Ssvp!ogM0dwnh9u9;t5szvtn_X3vlOgYS&Vlv>atbAcZ(UC{uW*T2#mT zp$AaJV0;+YuR_V8=Y@npqAdeZHBRcl5snzo#7$hffQxJ377!(5Hre4U2!?=og(>4YylPMGoweNVhbZcOb-00I;X=|Ux{M@53Rf1u^oF!gZsw~;2K z3%0=w?Z^bb1$)&cxpCuj#35vA%Y<>t+XI9ZHL>haw@v%R8PY@C(d(5J`9>h0V3sO> z9zym}h6BXen$l4V#*Q8${43muOU5M=!_9xYFO~=~povqHpE=&PT-PqZ4qd$BH zsx^AGw+0p~ErV!g`Y4a{=~wymwLbl7pT4d;rYb*R8Ss%4P2imXe75-vCyF1S zV{A`REo4w0>x*ydY(Cs2G8YA>#1)t;mZ;N>u|-|4?~a{5jknLqx?S^73rPDk|QMxbW>ugDt;}L04=+ zw$Nt_j{(PLf*rh$LO+Rvm?cg$aVrAWz;_|!vv5YWi?N;#Y_J2i4*L>!z@FmH*4<5_ zFG-1Ng#Pj+g4ekY#@hOf+na>8u0x6sgIHKBrg&;%`tm8@sq{o!m&?H!U|+Jsb*KYe zu?6x^?gTdw{H{tJkf@z8qQ#{`} z@I~bZ55+~o07P}?L{}#;u6!vBJ;!cfu#kP$!QKXQKJt}T#>6s(!lf>+ZJx(APqriNY+j` z|8_O3kE2_g=+>k1rgpliU5V}`0j0#HJol*~kmN!1uM4f?f`v`6j%zywB?H+GJbG5H zlv^oh&ZL<$dy_*?o|YCyDkVTs07?l|6oAGcl=jTaQ{PO?PmWl~Q`y_f9pkR8%qutIArkD1w_;C7g%lk8K&$zo882jXXdQjv81Wi7`KkRGt>;u;eN2KEpsrx(9zD9Xp z58c-zu@f4Vn0(%>IK1nFrF8)ZchIKH>`SNIas6gFqnc(^_nJmXnwMmGNLC-bfGn95 zf^R+W+NJDWr^dNMO&;Sy$+!@!z2Rc}Kn}_}u-JZ=Y}!RlyS%0bkEuaA*dldwNu~zb zbc~vgNv30pHKNjeV0b5828;N@oKQ4LhmSBn6h|pJDv_hSxL-F97txxHv}TvQU^iW` zTiV+oFKF->8zf_crV3jn(>B?(jheQ3O}jm&-O~O;Qrl6qw<~6ulu=R!tMZgnF;&Ei;JH%l)YvMxwDb>5Nz|i1tOrF0 zr&PE=%!%kXaA-LWvBAl$3Xv)iK_8&-YsXP&fSp2{A1sgq6;WWRTPAoqQ{^740oJI8 z$wT2(bTV}rTHEQIOghjhaES_dMUW(w18pneb#`)&7wQ(Klo1R8?yqFL)~I=tV9UZ6 z+C5QAnLOcgojQIigjJ~8c z2dMX*z#`ejxA4X%0DSQxcw6e~@Fn0c=*@hwO@iP{aCNqUW4sQyI^^&fMDQ{z0EdLW z&=VYoT`eu`_Hu*b%qT_M3em%I?P_TO9>Zq}(D_Du)PWO`k#vQg{}o(nT^@z=H@F7H z(E7L?A#dPWlFs)~vp5KUs2d$w*qx<&8ht=uR23$@u|wnv}K(w#8d*+C0Ae!W0n~ zk?%ct>co$iTw3C`daOl~wJ4DPTLyRM&ACjv?S0!kDbu8sX(J}xF}?7b2PSq+h@mNyWpF`^z3RE*y2IV0X0%!eJz0> zF5rOLUHof^&~E1ZTZrk-(g+D^N#-YrQT_~RqKOEfre|Gh^rn}2(#z!Z3YuQgn>1q1 zVhZw*Ho8_et)`~cl4-RfR>UPG*6Hu1XZO{}xa_6`y{}n7+wC>Y^q6K2w8*CU)HGi* z%?Eyzi6mZgrN>+;o2#g~3fz4ruhPRdY_FxnV=0j>Wz;h$C`7cUmH>f3Y|>I^%*fL#=d*jwNuAUFT)78Lz4*zY&HR z6da|_J+#n66U*^Hh=F(xu?lB7LM248@*!5?YAj$Txot3#nGC3c;+R(hKI!id2lN&^ zrd}L{BH3()x&7eQh0(s7qgT%QFa9W?9!UD0ca;FKB%3f8vVk%kDH`0jcnNw)6|&$NM}e)e-sm!oPIBBT3@%ERY9{V zfb64-g95K*w#PDi2qxHKYFR9?^UHfyT!5DhPX+-bz8HscW;z;@;&m{Q(RUfTHIR*! z)L8jQM-tcR$1`LFXE-&N4W!ZX8d|=Mnzu{l1D{Sf!Pm%^wbZg!3Z9>%lKEQsDvR!S zmZVivV}F;c2N->J7XJI}gw6^c+F7j3g@7}2TvEzM|NY^B7Ldo(my-zw{NF$ZY`{ku z(|E6Ys+#abXw^Q!aXk#_q7k2eg>hZwF;>aOYH9?p0Q$t~;|^<}9Mf3}@iVBrW)LER zWF(>=5Ow$l8^s<83+|!a%JFSjO4SSc-BHPfc1VC`Ld8yoq;hpV5vdNwh1pP|5(8#^ zgZ5qLZ3GL-#QDQE8-ctYCnUl~o1{sWWCZJGSV@qJ=_oSkfNzE5GqIg%5R<}V%n)OQ z7z>Y?0x>Z{Dp+$TtRrYqtWRDuwMIfndtRrO!PRdpw5m}P%WJ1)ck9B4ueLuxmJiWZ z%|?$eS3h11@MWgD3CU;~q3Fh9(86@By<2*$xe z?K=t^|LW4GL)mDMVTVxkb;^M4Jt{kFFi*C^IoaF!LvcLC*n*8qAs@W}n))$Mvgj-B6Gu8kh#%bNZ-S^<_ zvjzDNZoU2B)_eCqeq;3fa|L5)=wX7}!*e(NZ+%#h|M25J{{{Gl&RhQHUW05$U;h5{%|}YS|0TEo_6N$RlKelt zRscpXG~tOu05qr^(a2o*{~YVO5w?7wy#GuIGwFjmwxy)LhC&-sKV2BcTjPA zvrvfHt-GuTb9E5f5!51h6~Su=UPo{RfGZw=^1fJ5Q5iNvBtMX*vRz(_An?j#c6p7DJ$C5CZg?2F zw-T#hgi1A?dSlnD$@XW!2}BGi867A@etA~sF7g&G^%O2;77n>^D=pl5R_z7i zBP<#{CNXQN_&W%drz%|5G>olBumyn644x*MUG2CG&jBh$dzb|jmf5*L9oY?@lLzsc ze?;&J0+fmrj}V0zYC!@msjbv&Ti~%RkZp^oZ4vTT zkJ$?ZoFTfJ3J5W>WiGYM?KOasCg{+;aA4YmJA}{>#GFDDO66;ZivjTSE_g!+ZH$OCaj*d%RlG}pn+d3yVlAZkv*nCAG-J;A=ECBc;j7ORFIGy$ zy9SP;J#Q1O+6))UIg-6q;^&?QRL<1$0~}I5OUbhmc~-UFyIdv}lvGH;Gg9M=sEIN3zX4WS ztMPRs;)po3%r#%;2;)EU(5C(W8OXoz??x}g%b*@P{$0|tvf zqh8ys);0x}Dr(9dCx7!3zV&c^Z)*)CAKyj4)v!P%*2i|ZZz~yn`Nd%_=u=Q_>M;Z8*O)&LS zczY89Tv3L*xNw6+h&2l1gcJwbsqlTCFt=E~)K2@!@VM-to0!OZ$~G@^*gokH0~P5T z`=?N^?ve<0F)RmV1s4Gd;8+6yxFBW!bG6sD!ed(@+g4HAstI@=6?ZdZs7hM5Un0-Q zp*dP}+(831X(=Zut zSCIdm1&nifijt?iWQzy9bM1oKZBc3m)eJ1XqElu_a}G-#@ZA{jCXL^X@siUXa{7@@ zw=xEORh|B0q_3*4TRUk^T?C^AZp47m3alx+S5Y}wHZ+gU-7M{TS~}1qRUDEl4$+E3 z*a4+%TUx1?RC-9|Pyu{iPA2mxnJ1BX3flqS)Tta=jvQIk58i+mvM!mNqU02K;=*oV zJJ~9wxpj91THh*lfqQK5^es{%ddW!-d>7{oFe)dbF)DLpGMAFMQt*sqR1kdw`O$rT z1=y0W&Zw~O-*6^nGOP_VG-HYEnA8)nFItS_?z%W?2cpdp?xfsxjrU~aM!0YKdpptH zb*btT?2jlWtKj!`!c5X?9}yf!Yb?_KtJnQ6ygGXBEZCOOz2c2u*SiX!AdWM`{Bngk z-%5p~3T{=3;@{wjgOQ?`r41O0M}RL><#j6o;|^k66M*2hE|VJB`b=t6>Pw4;V8ix& zVXoSht6?a!7?I@zD3OVbu&FAe%9}CAlQBomm`5|_fye=yVI$)5rqjIT-n?2*Uag#0 zNAqCr=YDF=VFqhA?3RT+ThhXfkGEfcYRr&wW_gUWB;%}*{TMzSwuPFuNTw|TcV3F8 zu1b%wa$x`PQrWnk8rNgS{3AY6#d6uSf|^!HrWMSP@D;z}gHw@B=YtUV>eHEho19N) z$bui?GDpA`gxUu_`o>faN_b>FtYXJ?8R8`b#R$0iB;aEW?{ufC>7n9Xqy}H-Y`Do@ zXJRsZzS&FiJtUtmEd@*)^}EZFcn(>8)-mS+qQGv+7jGs`5~G*Ry3vtRP7swR&gN@+Rww|^yNh5a;l~7ORWHb zYVL7z$DNIU#;?P_a)gQDC@Qc6>}}|tEhKKG!B|TV+ou+NuGnCIVTSh2KBkmRX06Er zYfai<_N3-w)cnDc!o{^<8}N7hUIH;I2gI!0Xkr%l_UB*a3>UM|>ysj8U4)Q&lMwUz zq-@h!4!2ymb?4!G*Ze9gCfjlS1E`qP*1e|W_|^up-S@cs9H;O}*hz1puBU6c=i zqW@*aFTe)z(U)(0cKbO+{;I{?3NW{uzYPdp%v8g_PoOXgP>N`WR4Pv#tMRf9!CC}d z)`O**K}XI?TbWW@{4pl_2mxxlT{y<^qe{Q&ZH5p(0AVU>eau)<>xa;4bYw2*;YD!D zrX3vc5@PDd1jq|I6J=$ep{d0QpA^wjE48wJp_*DT-L=*cmLj~Vj3@!6|1(I^Z~+Bq zwc+#Cr$2u7gJ0jFXsWwL>i4X`7LyNIs4TPqcAkqfub!Yz}Pm@6VP zhfPC}`Fcv$OJx1M;?n*-sO(>NSCsbblZyAt#rtXTe$2m+38aNyQtBb410cwj%48NL zvm`Q0rI%kUXVlP)nsN2=0hPJ34d*fLy5fhoy9K#qB`sMgm8?|c*Gll9uA4Uugnj#+E%4?P+0AJ&^ff;OqE=LD5i3x*fb`0~ zgOli*3y*QhAm9`F{ojn9d;8(l^8pH^&?H8vcuGaAMO!I%WfDet>ctdz?<{}F)_>zQ zFy*UhsQ)5-Qs7Na0!J^qNB{Db(Vz4Kkwqf6AF_5W!2H>FuyKtxBfzjM0LY`Vgy8;% zAH(Mp9^QOQDSGV1mqCz*Wbj38(E^17u1`&r+{thQ~5P zw#=lKnO@6Wk7cfGnNKbAdkyT+-hlJkheO|ok$`S=F26ncv-e>n!8lRYp!k1q z+kfNz(X(&E2P}A2@L>rNX=a4ri$kD0TTMu{#29z`IdUOUKmb-g(;aO;VSV{@A8BOn z!x|fXNsWyiU4pCK4q;OxY&dReN5^7tmC)EIbTv0NGG(|gX6I3{3@cWIAOpcX1dQ+5 zg`wRD+7KK?@GgR52)=`$2|+J{s|W@V7!W)_@K*@_7Qx>k_!|U&j{x=GB2y7DodD|^ z9M&TC$2$T3rxX(*lgNM-0*)^r%~}2j#*!E&1c_dcj#b*5n*#HirBDF&z~2PLx%kQWDs^f`ukSla-oNyX0Mpu&KK za*&c|fR=Djt2Y~9Dsxa2hqaFcY@)Mq0IObdU=;WgoJV1o9kA%QLQBE1aTr2`=Zklq zJ_g(Pm7$2_Q$e^$D4DA&5k+8M96Hbwk^1>kgWG5t16$`{RSS|mW&BOWp;?ZXI40ra zFUs6nF5-%DI2AMtYY+7LM0*_6u!~g&Q literal 0 HcmV?d00001 diff --git a/src/__pycache__/question_bank.cpython-311.pyc b/src/__pycache__/question_bank.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd940b3b21e8b2caaa0f33a662d99bedceac5b03 GIT binary patch literal 2076 zcmah~Uu;uV7(e%KZ))pC){Ce;2{Q16f z&iChcZf|VbCkm8|Q)?L&scuI)W5_1S!H4i_k52EeSdm@of}vq@RVct9t#4asOg@deRuryQ9^9Vro-G3HKF3Lp2WoWMpRK^ zYO|v4XmoI>X*y#0W1(Ypu<|l=@<>HN27+Z3Cdeu52nzBFrwR(Mii)5*RH?&WdqPyi z5gvkaKu{8K*N=HUoH&{812dN�s*eutOEV8c zOAl@@&i-ut@4D3A(()i6;1}&Hr|6||2 zN4|YUUvJ6Rn>{-3ZhP$RdgSi9@?O!sqvYOEFrOzaTME(^{2Uy7Jm8}DEfJ2ZvP@jE z98W6g811`dNK`nss&UA&l8neQ_Rv^4O~-_bCUi`BiYcM7AK)f3H#HN2>Hn1pZs=(5 z1Dv&gHI@}5@ESmp1h}L&cs)YLG*?5=+&OGEcl&?cS~_&h?EfcN=h7n~BK#t>MAT2G zqKQGvMV`)h>eZ9&`dYp>e*G-ZyJ5vzG=EEntjxTEcY=amcI`8O8pkk9O=1}yq5+T) h^LznqYkua@slxv~HE}!BQA5q}C!PE2UlgW;{{RBhSIz(c literal 0 HcmV?d00001 diff --git a/src/__pycache__/question_generator.cpython-311.pyc b/src/__pycache__/question_generator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b40930b338f06c394a6280c077853fdb69d0c7a2 GIT binary patch literal 17506 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 literal 0 HcmV?d00001 diff --git a/src/__pycache__/quiz.cpython-311.pyc b/src/__pycache__/quiz.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fb1b35156f7b97e7b86cb220f4bd9c37d91fed7 GIT binary patch literal 4055 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 0: + tk.Button(button_frame, text="上一题", command=self.previous_question).pack(side="left", padx=10) + + tk.Button(button_frame, text="提交答案", command=self.submit_answer).pack(side="left", padx=10) + + if self.current_quiz.current_question_index < len(self.current_quiz.questions) - 1: + tk.Button(button_frame, text="下一题", command=self.next_question).pack(side="left", padx=10) + + tk.Button(self.quiz_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15).pack(pady=10) + + self.show_frame(self.quiz_frame) + + def generate_options(self, correct_answer) -> List[float]: + """ + 为题目生成选项 + + @param correct_answer: 正确答案 + @return: 选项列表 + """ + # 生成4个选项,其中一个是正确答案 + options = {correct_answer} + + # 添加一些干扰项 + if isinstance(correct_answer, int): + while len(options) < 4: + offset = random.randint(-10, 10) + if offset != 0: + options.add(correct_answer + offset) + else: + # 浮点数情况 + while len(options) < 4: + offset = random.uniform(-10, 10) + if abs(offset) > 0.1: # 避免太接近正确答案 + options.add(round(correct_answer + offset, 2)) + + # 如果选项不足4个,补充一些随机数 + while len(options) < 4: + options.add(round(random.uniform(correct_answer - 20, correct_answer + 20), 2)) + + options_list = list(options) + random.shuffle(options_list) + return options_list[:4] + + def submit_answer(self): + """ + 提交答案 + """ + if not self.current_quiz: + return + + answer_str = self.answer_var.get() + if not answer_str: + messagebox.showerror("错误", "请选择一个答案") + return + + try: + answer = float(answer_str) + self.current_quiz.answer_question(answer) + + # 如果是最后一题,显示结果 + if self.current_quiz.is_finished(): + self.show_result_frame() + else: + messagebox.showinfo("提示", "答案已提交") + except ValueError: + messagebox.showerror("错误", "请选择有效答案") + + def next_question(self): + """ + 下一题 + """ + if not self.current_quiz: + return + + # 保存当前答案(如果有选择) + answer_str = self.answer_var.get() + if answer_str: + try: + answer = float(answer_str) + self.current_quiz.answer_question(answer) + except ValueError: + pass # 如果答案无效,保持为None + + if self.current_quiz.next_question(): + self.show_quiz_frame() + else: + # 已经是最后一题 + if self.current_quiz.answers[self.current_quiz.current_question_index] is not None: + # 如果最后一题已答题,显示结果 + self.show_result_frame() + else: + messagebox.showinfo("提示", "已经是最后一题") + + def previous_question(self): + """ + 上一题 + """ + if not self.current_quiz: + return + + # 保存当前答案(如果有选择) + answer_str = self.answer_var.get() + if answer_str: + try: + answer = float(answer_str) + self.current_quiz.answer_question(answer) + except ValueError: + pass # 如果答案无效,保持为None + + if self.current_quiz.previous_question(): + self.show_quiz_frame() + + def show_result_frame(self): + """ + 显示结果界面 + """ + if not self.current_quiz: + return + + # 清除之前的内容 + for widget in self.result_frame.winfo_children(): + widget.destroy() + + # 计算得分 + score = self.current_quiz.calculate_score() + + # 创建结果界面 + tk.Label(self.result_frame, text="测验结果", font=("Arial", 18, "bold")).pack(pady=20) + + tk.Label(self.result_frame, text=f"您的得分: {score:.1f}%", font=("Arial", 16)).pack(pady=10) + + # 根据得分显示评语 + if score >= 90: + comment = "优秀! 继续保持!" + elif score >= 80: + comment = "良好! 还可以做得更好!" + elif score >= 60: + comment = "及格了,需要继续努力!" + else: + comment = "需要加强练习哦!" + + tk.Label(self.result_frame, text=comment, font=("Arial", 14)).pack(pady=10) + + # 显示答题详情 + correct_count = sum(1 for q, a in zip(self.current_quiz.questions, self.current_quiz.answers) + if a is not None and abs(q.answer - a) < 1e-6) + total_count = len(self.current_quiz.questions) + tk.Label(self.result_frame, text=f"答对: {correct_count}/{total_count}", font=("Arial", 12)).pack(pady=5) + + # 按钮 + button_frame = tk.Frame(self.result_frame) + button_frame.pack(pady=20) + + level_names = {"elementary": "小学", "middle": "初中", "high": "高中"} + tk.Button(button_frame, text=f"继续{level_names[self.current_level]}题目", + command=lambda: self.show_quiz_setup_frame(self.current_level), width=20).pack(side="left", padx=10) + + tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15).pack(side="left", padx=10) + + self.show_frame(self.result_frame) + + def show_change_password_frame(self): + """ + 显示修改密码界面 + """ + # 清除之前的内容 + for widget in self.change_password_frame.winfo_children(): + widget.destroy() + + # 创建修改密码界面 + tk.Label(self.change_password_frame, text="修改密码", font=("Arial", 18, "bold")).pack(pady=20) + + tk.Label(self.change_password_frame, text="原密码:").pack(pady=5) + self.old_password_entry = tk.Entry(self.change_password_frame, width=30, show="*") + self.old_password_entry.pack(pady=5) + + tk.Label(self.change_password_frame, text="新密码:", fg="blue").pack(pady=(10, 5)) + tk.Label(self.change_password_frame, text="(6-10位,必须包含大小写字母和数字)", fg="gray").pack(pady=5) + self.new_password_entry = tk.Entry(self.change_password_frame, width=30, show="*") + self.new_password_entry.pack(pady=5) + + tk.Label(self.change_password_frame, text="确认新密码:").pack(pady=5) + self.confirm_new_password_entry = tk.Entry(self.change_password_frame, width=30, show="*") + self.confirm_new_password_entry.pack(pady=5) + + tk.Button(self.change_password_frame, text="修改密码", command=self.change_password, width=20).pack(pady=10) + tk.Button(self.change_password_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20).pack(pady=5) + + self.show_frame(self.change_password_frame) + + def change_password(self): + """ + 修改密码 + """ + old_password = self.old_password_entry.get() + new_password = self.new_password_entry.get() + confirm_new_password = self.confirm_new_password_entry.get() + + if not old_password or not new_password or not confirm_new_password: + messagebox.showerror("错误", "请填写所有字段") + return + + if new_password != confirm_new_password: + messagebox.showerror("错误", "新密码和确认密码不一致") + return + + if self.user_manager.change_password(old_password, new_password): + messagebox.showinfo("成功", "密码修改成功") + self.show_main_menu_frame() + # 错误信息在user_manager.change_password中已经显示 + + def logout(self): + """ + 退出登录 + """ + self.user_manager.logout() + self.show_login_frame() + + def show_delete_account_frame(self): + """ + 显示注销账户界面 + """ + # 清除之前的内容 + for widget in self.delete_account_frame.winfo_children(): + widget.destroy() + + # 创建注销账户界面 + tk.Label(self.delete_account_frame, text="注销账户", font=("Arial", 18, "bold"), fg="red").pack(pady=20) + + tk.Label(self.delete_account_frame, text="警告:此操作将永久删除您的账户和所有数据!", fg="red").pack(pady=5) + tk.Label(self.delete_account_frame, text="请确认您的邮箱和密码:", fg="red").pack(pady=5) + + tk.Label(self.delete_account_frame, text="邮箱:").pack(pady=10) + self.delete_email_entry = tk.Entry(self.delete_account_frame, width=30) + self.delete_email_entry.pack(pady=5) + + tk.Label(self.delete_account_frame, text="密码:").pack(pady=5) + self.delete_password_entry = tk.Entry(self.delete_account_frame, width=30, show="*") + self.delete_password_entry.pack(pady=5) + + # 按钮框架 + button_frame = tk.Frame(self.delete_account_frame) + button_frame.pack(pady=20) + + tk.Button(button_frame, text="确认注销", command=self.confirm_delete_account, + width=15, fg="red").pack(side="left", padx=10) + tk.Button(button_frame, text="取消", command=lambda: self.root.after(100, self.show_login_frame), + width=15).pack(side="left", padx=10) + + self.show_frame(self.delete_account_frame) + + def confirm_delete_account(self): + """ + 确认并执行账户删除操作 + """ + email = self.delete_email_entry.get().strip() + password = self.delete_password_entry.get() + + if not email or not password: + messagebox.showerror("错误", "请输入邮箱和密码") + return + + # 确认操作 + if messagebox.askyesno("确认注销", "确定要注销账户吗?此操作无法撤销!"): + if self.user_manager.delete_account(email, password): + messagebox.showinfo("成功", "账户已注销,您可以使用该邮箱重新注册") + self.root.after(100, self.show_login_frame) # 延迟执行以避免界面冲突 + # 错误信息在user_manager.delete_account中已经显示 + + def delete_account(self): + """ + 注销账户(从主菜单调用的旧方法,保持兼容性) + """ + self.show_delete_account_frame() diff --git a/src/question_bank.py b/src/question_bank.py new file mode 100644 index 0000000..20925a9 --- /dev/null +++ b/src/question_bank.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +题库模块 +""" + +from typing import List, Dict +from question_generator import Expression, ElementaryQuestionGenerator, MiddleQuestionGenerator, HighQuestionGenerator + + +class QuestionBank: + """ + 题库类,管理不同难度的题目生成器 + """ + + def __init__(self): + """ + 初始化题库 + """ + self.generators = { + "elementary": ElementaryQuestionGenerator(), + "middle": MiddleQuestionGenerator(), + "high": HighQuestionGenerator() + } + + def generate_questions(self, level: str, count: int) -> List[Expression]: + """ + 生成指定数量和难度的题目 + + @param level: 题目难度(elementary, middle, high) + @param count: 题目数量 + @return: 题目列表 + """ + if level not in self.generators: + raise ValueError("无效的题目难度") + + generator = self.generators[level] + questions = [] + + for _ in range(count): + question = generator.generate_question() + questions.append(question) + + return questions \ No newline at end of file diff --git a/src/question_generator.py b/src/question_generator.py new file mode 100644 index 0000000..a9f0f04 --- /dev/null +++ b/src/question_generator.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +题目生成器模块 +""" + +import random +import math +import re +from abc import ABC, abstractmethod +from typing import List, Set, Optional + + +class Expression: + """ + 数学表达式类 + """ + + def __init__(self, expression_str: str, answer: float): + """ + 初始化表达式 + + @param expression_str: 表达式字符串 + @param answer: 表达式答案 + """ + self.expression_str = expression_str + self.answer = answer + + def __str__(self) -> str: + """ + 返回表达式的字符串表示 + + @return: 表达式字符串 + """ + return self.expression_str + + def __eq__(self, other) -> bool: + """ + 比较两个表达式是否相等 + + @param other: 另一个表达式 + @return: 是否相等 + """ + if isinstance(other, Expression): + return self.expression_str == other.expression_str + return False + + def __hash__(self) -> int: + """ + 计算表达式的哈希值 + + @return: 哈希值 + """ + return hash(self.expression_str) + + +class QuestionGenerator(ABC): + """ + 题目生成器抽象基类 + """ + + def __init__(self): + """ + 初始化题目生成器 + """ + self.generated_questions: Set[str] = set() + self.load_existing_questions() + + def load_existing_questions(self) -> None: + """ + 加载已存在的题目用于查重 + """ + # 在实际应用中,可以从文件或其他存储中加载已存在的题目 + pass + + @abstractmethod + def generate_question(self) -> Expression: + """ + 生成题目(抽象方法) + + @return: 数学表达式 + """ + pass + + +class ElementaryQuestionGenerator(QuestionGenerator): + """ + 小学题目生成器(+, -, *, /, 括号) + """ + + def generate_question(self) -> Expression: + """ + 生成小学题目 + + @return: 数学表达式 + """ + max_attempts = 100 + for _ in range(max_attempts): + # 随机生成操作数数量(2-5个) + num_operands = random.randint(2, 5) + operands = [random.randint(1, 50) for _ in range(num_operands)] + operators = [random.choice(['+', '-', '*']) if random.random() < 0.8 else '/' + for _ in range(num_operands - 1)] # 减少减法和除法概率以避免负数 + + # 随机添加括号 + expression_parts = [] + for i in range(num_operands): + expression_parts.append(str(operands[i])) + if i < len(operators): + expression_parts.append(operators[i]) + + # 随机添加括号 + if num_operands >= 3 and random.random() < 0.3: + # 在随机位置添加括号 + open_pos = random.randint(0, len(expression_parts) - 3) + # 确保括号内至少有两个操作数 + close_pos = min(open_pos + 2 + random.randint(1, 2) * 2, len(expression_parts)) + + # 确保括号位置是操作数位置 + if open_pos % 2 == 0 and close_pos % 2 == 0: + expression_parts.insert(open_pos, '(') + expression_parts.insert(close_pos + 1, ')') + + expression_str = ''.join(expression_parts) + + # 验证表达式是否有效 + try: + # 替换除法符号以便计算 + eval_expr = expression_str.replace('/', '/') + result = eval(eval_expr) + + # 确保结果是非负数且是合理的(整数或有限小数) + if isinstance(result, (int, float)) and result >= 0 and abs(result) < 1000: + # 格式化结果,保留合适的小数位数 + if isinstance(result, float) and result.is_integer(): + result = int(result) + + expr = Expression(expression_str, result) + # 检查是否已生成过相同题目 + if str(expr) not in self.generated_questions: + self.generated_questions.add(str(expr)) + return expr + except: + continue + + # 如果无法生成有效题目,返回默认题目 + expr = Expression("1+1", 2) + self.generated_questions.add(str(expr)) + return expr + + +class MiddleQuestionGenerator(QuestionGenerator): + """ + 初中题目生成器(包含平方或开根号) + """ + + def generate_question(self) -> Expression: + """ + 生成初中题目 + + @return: 数学表达式 + """ + max_attempts = 100 + for _ in range(max_attempts): + expression_parts = [] + + # 确保至少有一个平方或开根号 + has_square_or_sqrt = True # 确保至少有一个平方或开根号 + + # 生成操作数数量(2-5个) + num_operands = random.randint(2, 5) + + # 标记是否已添加特殊操作 + special_added = False + + for i in range(num_operands): + # 确保至少添加一个平方或开根号 + if not special_added and i == num_operands - 1: + # 如果还没添加特殊操作,强制在最后一个操作数添加 + choice = random.choice([0, 1]) # 提高开根号概率 + if choice == 0: + # 添加平方 + base = random.randint(1, 12) + expression_parts.append(f"{base}²") + else: + # 添加开根号 + square = random.randint(1, 12) + value = square * square + expression_parts.append(f"√{value}") + special_added = True + else: + rand_val = random.random() + # 修改这里的概率,增加开根号和平方的出现频率 + if not special_added and rand_val < 0.6: # 提高特殊操作概率从0.4到0.6 + # 添加特殊操作(平方或开根号) + choice = random.choice([0, 1]) if random.random() < 0.6 else 1 # 提高开根号概率 + if choice == 0: + # 添加平方 + base = random.randint(1, 12) + expression_parts.append(f"{base}²") + else: + # 添加开根号 + square = random.randint(1, 12) + value = square * square + expression_parts.append(f"√{value}") + special_added = True + else: + # 普通操作数 + expression_parts.append(str(random.randint(1, 50))) + + # 添加运算符(除了最后一个操作数) + if i < num_operands - 1: + expression_parts.append(random.choice(['+', '-', '*', '/'])) + + expression_str = ''.join(expression_parts) + + # 处理可能的语法问题 + expression_str = self.fix_expression_syntax(expression_str) + + # 计算结果 + try: + # 替换表达式中的函数以便计算 + eval_expr = expression_str.replace('²', '**2').replace('√', 'math.sqrt') + result = eval(eval_expr) + + # 确保结果是合理的 + if isinstance(result, (int, float)) and abs(result) < 1000 and not math.isnan(result): + # 格式化结果 + if isinstance(result, float) and abs(result - round(result)) < 1e-10: + result = int(round(result)) + + expr = Expression(expression_str, result) + # 检查是否已生成过相同题目 + if str(expr) not in self.generated_questions: + self.generated_questions.add(str(expr)) + return expr + except: + continue + + # 如果无法生成有效题目,返回默认题目 + expr = Expression("√4+2²", 6) + self.generated_questions.add(str(expr)) + return expr + + def fix_expression_syntax(self, expression: str) -> str: + """ + 修复表达式语法问题 + + @param expression: 原始表达式 + @return: 修复后的表达式 + """ + # 确保函数调用之间有运算符 + expression = re.sub(r'(\d)([√])', r'\1*\2', expression) + expression = re.sub(r'(²)([√])', r'\1*\2', expression) + expression = re.sub(r'(\))(\d)', r'\1*\2', expression) + expression = re.sub(r'(\d)(\()', r'\1*\2', expression) + # 修复根号表达式显示,确保根号符号正确显示 + expression = re.sub(r'√(\d+)', r'√\1', expression) + return expression + + +class HighQuestionGenerator(QuestionGenerator): + """ + 高中题目生成器(包含三角函数) + """ + + def generate_question(self) -> Expression: + """ + 生成高中题目 + + @return: 数学表达式 + """ + max_attempts = 100 + for _ in range(max_attempts): + expression_parts = [] + + # 确保至少有一个三角函数 + has_trig_function = random.random() < 0.8 + + # 生成操作数数量(2-4个) + num_operands = random.randint(2, 4) + + for i in range(num_operands): + if has_trig_function and i == 0: + # 第一个操作数有较高概率是三角函数 + if random.random() < 0.33: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"sin({angle}°)") + elif random.random() < 0.5: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"cos({angle}°)") + else: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"tan({angle}°)") + else: + # 其他操作数 + rand_val = random.random() + if rand_val < 0.1 and has_trig_function: + # 添加三角函数 + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"sin({angle}°)") + elif rand_val < 0.2 and has_trig_function: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"cos({angle}°)") + elif rand_val < 0.3 and has_trig_function: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"tan({angle}°)") + elif rand_val < 0.55: + # 普通数字 + expression_parts.append(str(random.randint(1, 20))) + elif rand_val < 0.7: + # 平方 + base = random.randint(1, 10) + expression_parts.append(f"{base}²") + else: + # 开根号 + square = random.randint(1, 10) + value = square * square + expression_parts.append(f"√{value}") + + # 添加运算符(除了最后一个操作数) + if i < num_operands - 1: + expression_parts.append(random.choice(['+', '-', '*', '/'])) + + expression_str = ''.join(expression_parts) + + # 处理可能的语法问题 + expression_str = self.fix_expression_syntax(expression_str) + + # 计算结果 + try: + # 替换表达式中的函数以便计算 + eval_expr = expression_str.replace('²', '**2').replace('√', 'math.sqrt') + # 修复:确保正确的替换顺序,先替换角度制三角函数 + eval_expr = re.sub(r'sin\((\d+)°\)', r'math.sin(math.radians(\1))', eval_expr) + eval_expr = re.sub(r'cos\((\d+)°\)', r'math.cos(math.radians(\1))', eval_expr) + eval_expr = re.sub(r'tan\((\d+)°\)', r'math.tan(math.radians(\1))', eval_expr) + + result = eval(eval_expr) + + # 确保结果是合理的 + if isinstance(result, (int, float)) and abs(result) < 1000 and not math.isnan(result) and not math.isinf(result): + # 格式化结果 + if isinstance(result, float) and abs(result - round(result, 10)) < 1e-10: + result = int(round(result)) + # 特殊处理常见的三角函数值,使其更加准确 + elif isinstance(result, float): + # 对于常见的三角函数值进行舍入处理 + if abs(result - 0.5) < 1e-10: # sin(30°) = 0.5 + result = 0.5 + elif abs(result - 0.7071067811865476) < 1e-10: # sin(45°) = cos(45°) ≈ 0.707 + result = round(result, 10) + elif abs(result - 0.8660254037844386) < 1e-10: # sin(60°) ≈ 0.866 + result = round(result, 10) + elif abs(result - 0.5773502691896257) < 1e-10: # tan(30°) ≈ 0.577 + result = round(result, 10) + elif abs(result - 1.7320508075688772) < 1e-10: # tan(60°) ≈ 1.732 + result = round(result, 10) + elif abs(result - 1.0) < 1e-10: # tan(45°) = 1, sin(90°) = 1 + result = 1.0 + + expr = Expression(expression_str, result) + # 检查是否已生成过相同题目 + if str(expr) not in self.generated_questions: + self.generated_questions.add(str(expr)) + return expr + except: + continue + + # 如果无法生成有效题目,返回默认题目 + expr = Expression("sin(30°)", 0.5) + self.generated_questions.add(str(expr)) + return expr + + def fix_expression_syntax(self, expression: str) -> str: + """ + 修复表达式语法问题 + + @param expression: 原始表达式 + @return: 修复后的表达式 + """ + # 确保函数调用之间有运算符 + expression = re.sub(r'(\d)([sincostan√])', r'\1*\2', expression) + expression = re.sub(r'(²)([sincostan√])', r'\1*\2', expression) + expression = re.sub(r'(sqrt\(\d+\))(\d)', r'\1*\2', expression) + expression = re.sub(r'(\))(\d)', r'\1*\2', expression) + expression = re.sub(r'(\d)(\()', r'\1*\2', expression) + expression = re.sub(r'°\)(\d)', r'°)*\1', expression) + return expression \ No newline at end of file diff --git a/src/quiz.py b/src/quiz.py new file mode 100644 index 0000000..80ae9a1 --- /dev/null +++ b/src/quiz.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测验模块 +""" + +from typing import List, Optional +from question_generator import Expression + + +class Quiz: + """ + 测验类,管理一次答题过程 + """ + + def __init__(self, questions: List[Expression]): + """ + 初始化测验 + + @param questions: 题目列表 + """ + self.questions = questions + self.answers: List[Optional[float]] = [None] * len(questions) + self.current_question_index = 0 + self.score = 0 + + def answer_question(self, answer: float) -> None: + """ + 回答当前题目 + + @param answer: 用户答案 + """ + if 0 <= self.current_question_index < len(self.questions): + self.answers[self.current_question_index] = answer + + def next_question(self) -> bool: + """ + 跳转到下一题 + + @return: 是否有下一题 + """ + if self.current_question_index < len(self.questions) - 1: + self.current_question_index += 1 + return True + return False + + def previous_question(self) -> bool: + """ + 返回上一题 + + @return: 是否有上一题 + """ + if self.current_question_index > 0: + self.current_question_index -= 1 + return True + return False + + def get_current_question(self) -> Optional[Expression]: + """ + 获取当前题目 + + @return: 当前题目 + """ + if 0 <= self.current_question_index < len(self.questions): + return self.questions[self.current_question_index] + return None + + def calculate_score(self) -> float: + """ + 计算得分 + + @return: 得分(百分比) + """ + correct_count = 0 + for i, (question, answer) in enumerate(zip(self.questions, self.answers)): + if answer is not None: + # 允许一定的浮点数误差 + if abs(question.answer - answer) < 1e-6: + correct_count += 1 + + self.score = (correct_count / len(self.questions)) * 100 if self.questions else 0 + return self.score + + def is_finished(self) -> bool: + """ + 检查测验是否已完成 + + @return: 是否已完成 + """ + return self.current_question_index == len(self.questions) - 1 and self.answers[self.current_question_index] is not None \ No newline at end of file diff --git a/src/user_manager.py b/src/user_manager.py new file mode 100644 index 0000000..a214d7e --- /dev/null +++ b/src/user_manager.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +用户管理模块 +""" + +import json +import os +import re +import random +import hashlib +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from typing import Dict, Optional +from tkinter import messagebox + + +class User: + """ + 用户类,存储用户信息 + """ + + def __init__(self, email: str, username: str = "", password_hash: str = "", registration_code: str = ""): + """ + 初始化用户对象 + + @param email: 用户邮箱 + @param username: 用户名 + @param password_hash: 密码哈希值 + @param registration_code: 注册码 + """ + self.email = email + self.username = username + self.password_hash = password_hash + self.registration_code = registration_code + self.is_registered = bool(password_hash) + + +class UserManager: + """ + 用户管理类,负责处理用户注册、登录和密码管理 + """ + + def __init__(self, data_file: str = "users.json"): + """ + 初始化用户管理器 + + @param data_file: 存储用户数据的文件路径 + """ + self.data_file = data_file + self.users: Dict[str, User] = {} + self.current_user: Optional[User] = None + # 邮件配置 + self.smtp_server = "smtp.qq.com" # 可根据需要修改SMTP服务器 + self.smtp_port = 465 + self.sender_email = "3257534544@qq.com" # 需要替换为实际邮箱 + self.sender_password = "pmfyurbkwfpkdbed" # 需要替换为实际应用密码 + self.load_users() + + def load_users(self) -> None: + """ + 从文件加载用户数据 + """ + if os.path.exists(self.data_file): + try: + with open(self.data_file, 'r', encoding='utf-8') as f: + data = json.load(f) + for email, user_data in data.items(): + user = User( + email=email, + username=user_data.get('username', ''), + password_hash=user_data.get('password_hash', ''), + registration_code=user_data.get('registration_code', '') + ) + user.is_registered = user_data.get('is_registered', False) + self.users[email] = user + except (json.JSONDecodeError, FileNotFoundError): + self.users = {} + + def save_users(self) -> None: + """ + 保存用户数据到文件 + """ + data = {} + for email, user in self.users.items(): + data[email] = { + 'username': user.username, + 'password_hash': user.password_hash, + 'registration_code': user.registration_code, + 'is_registered': user.is_registered + } + + try: + with open(self.data_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except IOError as e: + messagebox.showerror("错误", f"保存用户数据失败: {str(e)}") + + def is_valid_email(self, email: str) -> bool: + """ + 验证邮箱格式 + + @param email: 邮箱地址 + @return: 邮箱格式是否有效 + """ + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return re.match(pattern, email) is not None + + def is_valid_username(self, username: str) -> bool: + """ + 验证用户名是否符合要求 + + @param username: 用户名 + @return: 用户名是否有效 + """ + # 用户名长度应在3-20个字符之间 + if not (3 <= len(username) <= 20): + return False + # 用户名只能包含字母、数字和下划线 + pattern = r'^[a-zA-Z0-9_]+$' + return re.match(pattern, username) is not None + + def generate_registration_code(self) -> str: + """ + 生成注册码 + + @return: 6位数字注册码 + """ + return str(random.randint(100000, 999999)) + + def hash_password(self, password: str) -> str: + """ + 对密码进行哈希处理 + + @param password: 原始密码 + @return: 哈希后的密码 + """ + return hashlib.sha256(password.encode('utf-8')).hexdigest() + + def is_valid_password(self, password: str) -> bool: + """ + 验证密码是否符合要求 + + @param password: 密码 + @return: 密码是否有效 + """ + if not (6 <= len(password) <= 10): + return False + + has_lower = any(c.islower() for c in password) + has_upper = any(c.isupper() for c in password) + has_digit = any(c.isdigit() for c in password) + + return has_lower and has_upper and has_digit + + def register_user(self, email: str, username: str) -> bool: + """ + 注册新用户(发送注册码) + + @param email: 用户邮箱 + @param username: 用户名 + @return: 是否成功发送注册码 + """ + if not self.is_valid_email(email): + messagebox.showerror("错误", "邮箱格式不正确") + return False + + if not self.is_valid_username(username): + messagebox.showerror("错误", "用户名应为3-20位,只能包含字母、数字和下划线") + return False + + # 检查邮箱是否已被注册 + if email in self.users and self.users[email].is_registered: + messagebox.showerror("错误", "该邮箱已注册") + return False + + # 检查用户名是否已被使用 + for user in self.users.values(): + if user.username == username and user.is_registered: + messagebox.showerror("错误", "该用户名已存在") + return False + + registration_code = self.generate_registration_code() + user = User(email=email, username=username, registration_code=registration_code) + self.users[email] = user + + # 尝试发送邮件 + if self.send_registration_code_via_email(email, registration_code): + self.save_users() + messagebox.showinfo("成功", "注册码已发送到您的邮箱,请查收") + return True + else: + messagebox.showerror("错误", "无法发送注册码到邮箱,请检查网络连接或邮箱地址") + # 即使邮件发送失败,也保存用户信息以便重试 + self.save_users() + return False + + def send_registration_code_via_email(self, email: str, code: str) -> bool: + """ + 通过电子邮件发送注册码 + + @param email: 接收邮箱 + @param code: 注册码 + @return: 是否发送成功 + """ + try: + # 创建邮件内容 + message = MIMEMultipart() + message["From"] = self.sender_email + message["To"] = email + message["Subject"] = "数学练习系统注册码" + + body = f""" + 您好! + + 欢迎使用数学练习系统! + + 您的注册码是: {code} + + 请在注册界面输入此注册码完成注册。 + + 如果您没有请求此注册码,请忽略此邮件。 + + 祝学习愉快! + 数学练习系统团队 + """ + + message.attach(MIMEText(body, "plain", "utf-8")) + + # 使用SMTP_SSL连接QQ邮箱服务器 + server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) + server.login(self.sender_email, self.sender_password) + text = message.as_string() + server.sendmail(self.sender_email, email, text) + server.quit() + + return True + except Exception as e: + print(f"邮件发送失败: {str(e)}") + return False + + def verify_registration_code(self, email: str, code: str) -> bool: + """ + 验证注册码 + + @param email: 用户邮箱 + @param code: 用户输入的注册码 + @return: 注册码是否正确 + """ + if email not in self.users: + messagebox.showerror("错误", "请先获取注册码") + return False + + user = self.users[email] + if user.registration_code != code: + messagebox.showerror("错误", "注册码不正确") + return False + + return True + + def set_password(self, email: str, password: str) -> bool: + """ + 设置用户密码 + + @param email: 用户邮箱 + @param password: 用户密码 + @return: 是否设置成功 + """ + if not self.is_valid_password(password): + messagebox.showerror("错误", "密码必须为6-10位,且包含大小写字母和数字") + return False + + if email not in self.users: + messagebox.showerror("错误", "用户不存在") + return False + + user = self.users[email] + user.password_hash = self.hash_password(password) + user.is_registered = True + self.save_users() + return True + + def login(self, username: str, password: str) -> bool: + """ + 用户登录 + + @param username: 用户名 + @param password: 用户密码 + @return: 是否登录成功 + """ + # 根据用户名查找用户 + user = None + for u in self.users.values(): + if u.username == username: + user = u + break + + if user is None: + messagebox.showerror("错误", "用户不存在") + return False + + if not user.is_registered: + messagebox.showerror("错误", "请先完成注册") + return False + + if user.password_hash != self.hash_password(password): + messagebox.showerror("错误", "密码不正确") + return False + + self.current_user = user + return True + + def change_password(self, old_password: str, new_password: str) -> bool: + """ + 修改用户密码 + + @param old_password: 原密码 + @param new_password: 新密码 + @return: 是否修改成功 + """ + if not self.current_user: + messagebox.showerror("错误", "用户未登录") + return False + + if self.current_user.password_hash != self.hash_password(old_password): + messagebox.showerror("错误", "原密码不正确") + return False + + if not self.is_valid_password(new_password): + messagebox.showerror("错误", "新密码必须为6-10位,且包含大小写字母和数字") + return False + + self.current_user.password_hash = self.hash_password(new_password) + self.save_users() + return True + + def logout(self) -> None: + """ + 用户登出 + """ + self.current_user = None + + def delete_account(self, email: str, password: str) -> bool: + """ + 注销账户 + + @param email: 用户邮箱 + @param password: 用户密码 + @return: 是否注销成功 + """ + if not self.is_valid_email(email): + messagebox.showerror("错误", "邮箱格式不正确") + return False + + # 检查用户是否存在 + if email not in self.users: + messagebox.showerror("错误", "该邮箱未注册") + return False + + user = self.users[email] + + # 检查用户是否已完成注册 + if not user.is_registered: + messagebox.showerror("错误", "该账户未完成注册") + return False + + # 验证密码 + if user.password_hash != self.hash_password(password): + messagebox.showerror("错误", "密码不正确") + return False + + # 删除用户 + del self.users[email] + # 如果当前用户是被删除的用户,则登出 + if self.current_user and self.current_user.email == email: + self.current_user = None + + self.save_users() + return True diff --git a/src/users.json b/src/users.json new file mode 100644 index 0000000..ffcea7e --- /dev/null +++ b/src/users.json @@ -0,0 +1,8 @@ +{ + "1426688201@qq.com": { + "username": "111", + "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", + "registration_code": "825880", + "is_registered": true + } +} \ No newline at end of file -- 2.34.1 From 39f8b9ad59e0110e045b57091eb6e9995e6c2e98 Mon Sep 17 00:00:00 2001 From: Amnesiac1745 <1426688201@qq.com> Date: Fri, 10 Oct 2025 21:07:13 +0800 Subject: [PATCH 02/12] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__pycache__/main_app.cpython-311.pyc | Bin 38564 -> 38384 bytes .../question_generator.cpython-311.pyc | Bin 17506 -> 18700 bytes src/main_app.py | 9 ++--- src/question_generator.py | 32 ++++++++++++++---- src/users.json | 9 +---- 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/__pycache__/main_app.cpython-311.pyc b/src/__pycache__/main_app.cpython-311.pyc index badc7e1011cbabda901dba0ca663f99f2d2deaac..f8332c659d2b957abb39e7b3b6a993853e92eca2 100644 GIT binary patch delta 889 zcmZ`&U2IH26u#%~-rc>sd)-_8zg@Z|+wKasXthn%Uz(EIG^*10DN%HTMCrC@WGfV{ z6l?i3h?h!*CR!@_xKz1nhJTc(J5_=)tMU(vs4$29h zK1&9-fQJ`(F8Kr1J8QP^3V)l-Jp=Eb2jEj`Qi8$`%fy^tE*3T1%3a~q= z3(eHbzB?}%se_$$RT=1d=ZM=(Pw~n7g6^`%+3$?>oZZQ-G0{(CEci@g91}64=pGRp zE}Bd9kvSI4Fq54vE0HD2WaTRs+Ng+yEBnmUz^XTv8wptBcd*Bsj~VD1TjL)T=>f~$ zzRyhGSl8|{A_MzcHY5W{+8tw zgx*@CajESSQg2P@PDS<<7gn|=qM$ep-gI5kD+Es(8*6ouB@mOh%rgm&wmPGW`#8~- zMox6K&6Ah%Bh;60=zP^CO1%w{JRe3*REY-&VLb;IGb`~vIUjK$50#)WluNTv7n;d$ zF$pgv4YfHI(^dngvS2tg6z{PGR4zG9Lf(pV{$G@(W9Y5Y+$x delta 1128 zcmZ`&ZA@EL7(VZ9Z_91(-My}bF+Rtz#m)jFDljN8GGb&iM5Iol>$K1oX)u#pj5@2d z;hQm-eVJ_Whh4VJ=wO+i5RD`5ho3);X|>7N;@2-TKQe1(;u4K|?rq?gzIo5{p7);j zJm)!Y&dpxn&aH6L8SCD~;`uX8*BP3{9|2YG^g=PIE^t;ey4nCr<4L)avKd5cE z3xK12Y8k*)8jqL+$fMIyx5l&@{{o;1$H%tFN7C%t!asL72g@ zsh~JbLJ-(Ko)@q$7ooq&iDlDv;|xiPtjl&RZ!YDFdx)!q4oy3tyq~ps)0j$PW8+o_ zgOi}^t#~K^GbGH!3>yy+;l7vk7l2fT`D z=P`^`HR7@3E_fJMkKbq5Gs2!=wH;x;Kn&|Io*hAwR@SisF-pRsmTHDK?<_P04D8wh z9++P*R26nTm!xiGLG`f$^{@sutY4@yYyD8~0DNr033iMxRGIoTeb@2g!qHLBx@^nH zJFd#LU&^(Y_)E{H<-HksZ$ko0C8 zO=+b$qco>BJZm-xo-EOTV+=Zt-mQME{~N}R&;5@zm^|?@j9}=MpP01n#N)Ea@5z!) zD4~}V^I&B>GM@wVE;^wYUtD|!O7WM)LfDD?$>p}70In?gZ~RUF{`>!|>gxUlE1OE4 diff --git a/src/__pycache__/question_generator.cpython-311.pyc b/src/__pycache__/question_generator.cpython-311.pyc index b40930b338f06c394a6280c077853fdb69d0c7a2..d0555e05bb0b0a8c1520ec70a7704fc886d3f4ee 100644 GIT binary patch delta 2575 zcmb`IZA?>F7{~9q*LH%m^p=<2wioFOE4@;X0z(R-qu^$Q;vC{cV5M@4pa^(cre<}x z88haHAv=39do@eu%=U$?OSb4fFio7BS!}yI>&T2uKWrvjR-#MxWy#LD)U31`zj)LB z&biNdp68x(&+|X`#w3_}4X8g>sj?}2eZt%ttZe>F{aX%Lhvp4-hVDHVHmm9wb`heYxDm5Id7B#16iux4) z7cZPj-_0(y9a0C)cgHCzqz>hzjpv4P`)NRRP}B6(*H#KpU8MKT0j7)E4^m@AEz)WG zY)efdotdtK67s@_v%Y>#2t4&6n=P;GXp*Z7Y&Na4hIuvC~B(@|$IkWT&lrwy@@C{(g zSLx^ZRdK#5##hZ$f5k-kwGzKJ&bP$)mf4Pr)+paD@$Kk1XJK;KaunuXX_1Usz`Oq>_F7+k?fums;kA02$%dJ z=lTM)LvOcT<18m1J8h0~4vBM2s;=hpk>z|qg_nQ9*izA|rZ1`K);x4Vzr(stQFO9C zLE+}UKm~^H>3<$UzZwMK53ewuTBe)^P#H9$36lX`F3tkC(a*)D;5q`P+qgC+%$tD< zjX8B_*vz66PMxgNQo77B8NuJ&stIw z5@UKoQI|R;*8``NnIWXbRi#Hin!np97Zu!umjK-atr+9WF>Xx0T~k(d?(E+hKZy2|z;G1ANKSI8bf zD%lg(l?XJ*M@_a3iUo?4>8rMn9$uNYm)d(snbx1)7v-E1=bTi{^R_r&5#uXP2WDEL zyi4L;alR?WH_cjSgHgUs;@cw2^*AVEt(oyfxjKofTTxXLGz)|#$P(=fPE(`A#j}l7>frdbK@RJ_FdlWLdi_E=XQ6QvEP#JuIMqTbo zT=cx59X(mLm!@;DPD&X?@9yw?*;yq+LxA3|Dg}R{3snNRiV~RT!-nd!G!E*k-Hb+^ z9Ag&+bj#K)$)qOBG@E zk#3qSwb&-Bve~8sc)1Pu$hh~X#V0G$7rx?J!2ml}WGTvO+{qZ(^6*IG8St<~+2|#L zY?xu9T#dxl5J8s4`PDIg^$at!HOkjZe0`jE$N0r>6z`FE&;Nq#k8<@ASHDD%myDYV zJ8S8mYU$1f1w0sjd3^;aejcxTJRif}BMSCzyczwYfuL^$ADz7d3?b?9O64K)zFt7t zO}uV`bYH><&=@AuT^%bK#L6Sm8N}36_YkWWpDs(cRO{& zUC_W{5-K0ra?Kn@C*3Y3IT7GdblqK){W|XA{h|UrNZEX5q8R|3^4^SCsu$#;JE;zB r_)GwX1a)slR<2%}H;zNlXUIWK)DMUywU@GCgFAe8T+)pVo_! delta 1624 zcmai!e@t6d6vyvh9z?SrYGR?B63F;pjr!N`@>gN7%KU^Z3#m)VrSxg+sVoD^MEHUw(wPVaQ^x#9=8;%w#T;`egkx*UMltOcUpTnL|8C-pZS?o%1~fpJ^V zcGH+Wkr6fUZNITMI$Mcwl?bhpv_;WDz1IPH`$l)IwC=6iB;mt<0=n&2h2M3&Mhf+) z9rC+t*05_aJ1o@Lz~R!TEj<7Zo(Mb5k)htN`1Is6%vc+*;ckj&?p8|P7Gt|GV2BR&boYe^ zy26o2PlWA(8%5siy|}Iu(ZvxpbHLG{44aBQX=l(gCStqNS}FFcRXtv;2-FPcN<&hT zU~@!edRcSsD#4tk=3LHh%HLciT&of`*Qu|1{-%}@+*06hIvJ{hq417)p}sg5?uFJ1 ze-gO-@M#BnSs}uN&@#hlA>JVHWpkc$7rozM9Ft@Phv0(u5gLXA_AOBFJ1E>shmU*; z^{YSmW_1!}mtqe1&}qwzxbACziZT_>KdR6(FwZ#&=c^PoP<2A!t^cmNO);igFC{#w z)=P=n_nB}`M4X@#P| z7qkRXK5UP+I6Sz2pohiKF^ph{C<6a{*aLFH?!mEOV_GKVKW|3Tq|~WNsZ;$cm?6?4 zE~OIiG(0QjnhVwA4Ks9^nvOOGtxIb9*8YUt9uTevgq>7kBdXHmqx2B$4&~9q@O;Rx zdjU^y1U?AKmKV`{nd9GLgP%iwo@{D9mOqLa4@hhbeTX#pqB-AnAf+%IlVk;Vi}1DA zZ^3-U#2mO*yVP2# str: """ @@ -364,14 +375,21 @@ class HighQuestionGenerator(QuestionGenerator): # 检查是否已生成过相同题目 if str(expr) not in self.generated_questions: self.generated_questions.add(str(expr)) - return expr + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, result) + return readable_expr except: continue # 如果无法生成有效题目,返回默认题目 expr = Expression("sin(30°)", 0.5) self.generated_questions.add(str(expr)) - return expr + readable_expr = Expression("sin(30°)", 0.5) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = "sin(30°)".replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, 0.5) + return readable_expr def fix_expression_syntax(self, expression: str) -> str: """ diff --git a/src/users.json b/src/users.json index ffcea7e..9e26dfe 100644 --- a/src/users.json +++ b/src/users.json @@ -1,8 +1 @@ -{ - "1426688201@qq.com": { - "username": "111", - "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", - "registration_code": "825880", - "is_registered": true - } -} \ No newline at end of file +{} \ No newline at end of file -- 2.34.1 From 3ce07fcfce54bf4a96d9b1c4f0283634f37525e7 Mon Sep 17 00:00:00 2001 From: Amnesiac1745 <1426688201@qq.com> Date: Sat, 11 Oct 2025 19:01:56 +0800 Subject: [PATCH 03/12] =?UTF-8?q?=E7=BE=8E=E5=8C=96=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__pycache__/main_app.cpython-311.pyc | Bin 38384 -> 49470 bytes src/main_app.py | 356 ++++++++++++++++------- src/users.json | 9 +- 3 files changed, 257 insertions(+), 108 deletions(-) diff --git a/src/__pycache__/main_app.cpython-311.pyc b/src/__pycache__/main_app.cpython-311.pyc index f8332c659d2b957abb39e7b3b6a993853e92eca2..e3f5ecfef21a85b6e050a7a848d52bacf3006e9f 100644 GIT binary patch literal 49470 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

tevF9`U5}r?aQDMOrK*9azpp$k{uFLc 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 EUnRAAI{*Lx diff --git a/src/main_app.py b/src/main_app.py index 1146541..99591b2 100644 --- a/src/main_app.py +++ b/src/main_app.py @@ -29,23 +29,27 @@ class MathQuizApp: """ self.root = root self.root.title("数学练习系统") - self.root.geometry("600x500") - self.root.resizable(False, False) + self.root.geometry("700x600") + self.root.resizable(True, True) + self.root.configure(bg="#f0f0f0") # 初始化系统组件 self.user_manager = UserManager() self.question_bank = QuestionBank() + # 设置样式 + self.setup_styles() + # 创建不同的界面框架 - self.login_frame = tk.Frame(self.root) - self.register_frame = tk.Frame(self.root) - self.set_password_frame = tk.Frame(self.root) - self.main_menu_frame = tk.Frame(self.root) - self.quiz_setup_frame = tk.Frame(self.root) - self.quiz_frame = tk.Frame(self.root) - self.result_frame = tk.Frame(self.root) - self.change_password_frame = tk.Frame(self.root) - self.delete_account_frame = tk.Frame(self.root) # 添加删除账户框架 + self.login_frame = tk.Frame(self.root, bg="#f0f0f0") + self.register_frame = tk.Frame(self.root, bg="#f0f0f0") + self.set_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.main_menu_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_setup_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_frame = tk.Frame(self.root, bg="#f0f0f0") + self.result_frame = tk.Frame(self.root, bg="#f0f0f0") + self.change_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.delete_account_frame = tk.Frame(self.root, bg="#f0f0f0") # 添加删除账户框架 # 初始化应用状态 self.current_quiz: Optional[Quiz] = None @@ -56,20 +60,39 @@ class MathQuizApp: self.create_widgets() self.show_login_frame() + def setup_styles(self): + """ + 设置界面样式 + """ + self.title_font = ("Arial", 20, "bold") + self.header_font = ("Arial", 16, "bold") + self.normal_font = ("Arial", 12) + self.button_font = ("Arial", 11, "bold") + + # 定义颜色方案 + self.primary_color = "#4a6fa5" + self.secondary_color = "#6b8cbc" + self.accent_color = "#ff6b6b" + self.success_color = "#4caf50" + self.warning_color = "#ffc107" + self.danger_color = "#f44336" + self.light_bg = "#f8f9fa" + self.dark_text = "#333333" + def create_widgets(self): """ 创建界面组件 """ # 创建不同的界面框架 - self.login_frame = tk.Frame(self.root) - self.register_frame = tk.Frame(self.root) - self.set_password_frame = tk.Frame(self.root) - self.main_menu_frame = tk.Frame(self.root) - self.quiz_setup_frame = tk.Frame(self.root) - self.quiz_frame = tk.Frame(self.root) - self.result_frame = tk.Frame(self.root) - self.change_password_frame = tk.Frame(self.root) - self.delete_account_frame = tk.Frame(self.root) # 添加删除账户框架 + self.login_frame = tk.Frame(self.root, bg="#f0f0f0") + self.register_frame = tk.Frame(self.root, bg="#f0f0f0") + self.set_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.main_menu_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_setup_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_frame = tk.Frame(self.root, bg="#f0f0f0") + self.result_frame = tk.Frame(self.root, bg="#f0f0f0") + self.change_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.delete_account_frame = tk.Frame(self.root, bg="#f0f0f0") # 添加删除账户框架 def show_frame(self, frame: tk.Frame): """ @@ -95,19 +118,33 @@ class MathQuizApp: widget.destroy() # 创建登录界面 - tk.Label(self.login_frame, text="用户登录", font=("Arial", 18, "bold")).pack(pady=20) + main_frame = tk.Frame(self.login_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=50, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 30)) + tk.Label(title_frame, text="用户登录", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) - tk.Label(self.login_frame, text="用户名:").pack(pady=5) - self.login_email_entry = tk.Entry(self.login_frame, width=30) + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="用户名:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.login_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") self.login_email_entry.pack(pady=5) - tk.Label(self.login_frame, text="密码:").pack(pady=5) - self.login_password_entry = tk.Entry(self.login_frame, width=30, show="*") + tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.login_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") self.login_password_entry.pack(pady=5) - tk.Button(self.login_frame, text="登录", command=self.login, width=20).pack(pady=10) - tk.Button(self.login_frame, text="注册新用户", command=self.show_register_frame, width=20).pack(pady=5) - tk.Button(self.login_frame, text="注销账户", command=self.show_delete_account_frame, width=20).pack(pady=5) + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="登录", command=self.login, width=20, bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="注册新用户", command=self.show_register_frame, width=20, bg=self.secondary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + tk.Button(button_frame, text="注销账户", command=self.show_delete_account_frame, width=20, bg=self.danger_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) self.show_frame(self.login_frame) @@ -136,24 +173,38 @@ class MathQuizApp: widget.destroy() # 创建注册界面 - tk.Label(self.register_frame, text="用户注册", font=("Arial", 18, "bold")).pack(pady=20) + main_frame = tk.Frame(self.register_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="用户注册", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) - tk.Label(self.register_frame, text="用户名:").pack(pady=5) - self.register_username_entry = tk.Entry(self.register_frame, width=30) + tk.Label(form_frame, text="用户名:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.register_username_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") self.register_username_entry.pack(pady=5) - tk.Label(self.register_frame, text="邮箱:").pack(pady=5) - self.register_email_entry = tk.Entry(self.register_frame, width=30) + tk.Label(form_frame, text="邮箱:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.register_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") self.register_email_entry.pack(pady=5) - tk.Button(self.register_frame, text="获取注册码", command=self.send_registration_code, width=20).pack(pady=10) + tk.Button(form_frame, text="获取注册码", command=self.send_registration_code, width=20, bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) - tk.Label(self.register_frame, text="注册码:").pack(pady=5) - self.registration_code_entry = tk.Entry(self.register_frame, width=30) + tk.Label(form_frame, text="注册码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.registration_code_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") self.registration_code_entry.pack(pady=5) - tk.Button(self.register_frame, text="验证注册码", command=self.verify_registration_code, width=20).pack(pady=10) - tk.Button(self.register_frame, text="返回登录", command=self.show_login_frame, width=20).pack(pady=5) + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="验证注册码", command=self.verify_registration_code, width=20, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回登录", command=self.show_login_frame, width=20, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) self.show_frame(self.register_frame) @@ -197,21 +248,34 @@ class MathQuizApp: widget.destroy() # 创建设置密码界面 - tk.Label(self.set_password_frame, text="设置密码", font=("Arial", 18, "bold")).pack(pady=20) + main_frame = tk.Frame(self.set_password_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) - tk.Label(self.set_password_frame, text="邮箱: " + email).pack(pady=5) + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="设置密码", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) - tk.Label(self.set_password_frame, text="密码:", fg="blue").pack(pady=(10, 5)) - tk.Label(self.set_password_frame, text="(6-10位,必须包含大小写字母和数字)", fg="gray").pack(pady=5) - self.set_password_entry = tk.Entry(self.set_password_frame, width=30, show="*") + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="邮箱: " + email, font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + + tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff", fg=self.primary_color).pack(pady=(10, 5), anchor="w") + tk.Label(form_frame, text="(6-10位,必须包含大小写字母和数字)", font=("Arial", 10), bg="#ffffff", fg="gray").pack(pady=5, anchor="w") + self.set_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") self.set_password_entry.pack(pady=5) - tk.Label(self.set_password_frame, text="确认密码:").pack(pady=5) - self.confirm_password_entry = tk.Entry(self.set_password_frame, width=30, show="*") + tk.Label(form_frame, text="确认密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.confirm_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") self.confirm_password_entry.pack(pady=5) - tk.Button(self.set_password_frame, text="设置密码", command=lambda: self.set_password(email), width=20).pack(pady=10) - tk.Button(self.set_password_frame, text="返回登录", command=self.show_login_frame, width=20).pack(pady=5) + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="设置密码", command=lambda: self.set_password(email), width=20, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回登录", command=self.show_login_frame, width=20, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) self.show_frame(self.set_password_frame) @@ -245,23 +309,30 @@ class MathQuizApp: widget.destroy() # 创建主菜单界面 - tk.Label(self.main_menu_frame, text="主菜单", font=("Arial", 18, "bold")).pack(pady=20) + main_frame = tk.Frame(self.main_menu_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="主菜单", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) user_email = self.user_manager.current_user.email if self.user_manager.current_user else "未知用户" - tk.Label(self.main_menu_frame, text=f"欢迎, {user_email}!", font=("Arial", 12)).pack(pady=10) + tk.Label(main_frame, text=f"欢迎, {user_email}!", font=self.header_font, bg="#ffffff").pack(pady=10) - tk.Button(self.main_menu_frame, text="小学题目", command=lambda: self.show_quiz_setup_frame("elementary"), - width=20, height=2).pack(pady=10) - tk.Button(self.main_menu_frame, text="初中题目", command=lambda: self.show_quiz_setup_frame("middle"), - width=20, height=2).pack(pady=10) - tk.Button(self.main_menu_frame, text="高中题目", command=lambda: self.show_quiz_setup_frame("high"), - width=20, height=2).pack(pady=10) + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="小学题目", command=lambda: self.show_quiz_setup_frame("elementary"), + width=20, height=2, bg="#4CAF50", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack(pady=10) + tk.Button(button_frame, text="初中题目", command=lambda: self.show_quiz_setup_frame("middle"), + width=20, height=2, bg="#2196F3", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack(pady=10) + tk.Button(button_frame, text="高中题目", command=lambda: self.show_quiz_setup_frame("high"), + width=20, height=2, bg="#FF9800", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack(pady=10) - tk.Button(self.main_menu_frame, text="修改密码", command=self.show_change_password_frame, - width=20).pack(pady=10) - tk.Button(self.main_menu_frame, text="退出登录", command=self.logout, width=20).pack(pady=5) - tk.Button(self.main_menu_frame, text="注销账户", command=self.delete_account, - width=20, fg="red").pack(pady=5) + tk.Button(button_frame, text="修改密码", command=self.show_change_password_frame, + width=20, bg=self.secondary_color, fg="white", font=self.button_font, relief=tk.FLAT, bd=0, pady=5).pack(pady=10) + tk.Button(button_frame, text="退出登录", command=self.logout, width=20, bg=self.danger_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, pady=5).pack(pady=5) self.show_frame(self.main_menu_frame) @@ -278,16 +349,31 @@ class MathQuizApp: widget.destroy() # 创建测验设置界面 + main_frame = tk.Frame(self.quiz_setup_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + level_names = {"elementary": "小学", "middle": "初中", "high": "高中"} - tk.Label(self.quiz_setup_frame, text=f"{level_names[level]}数学题目", font=("Arial", 18, "bold")).pack(pady=20) + level_colors = {"elementary": "#4CAF50", "middle": "#2196F3", "high": "#FF9800"} + + title_frame = tk.Frame(main_frame, bg=level_colors[level]) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text=f"{level_names[level]}数学题目", font=self.title_font, bg=level_colors[level], fg="white").pack(pady=20) - tk.Label(self.quiz_setup_frame, text="请输入题目数量 (1-20):").pack(pady=10) - self.question_count_entry = tk.Entry(self.quiz_setup_frame, width=20) + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=20) + + tk.Label(form_frame, text="请输入题目数量 (1-20):", font=self.normal_font, bg="#ffffff").pack(pady=10) + self.question_count_entry = tk.Entry(form_frame, width=20, font=self.normal_font, justify="center", relief=tk.FLAT, bd=5, bg="#f0f0f0") self.question_count_entry.pack(pady=5) self.question_count_entry.insert(0, "10") # 默认10题 - tk.Button(self.quiz_setup_frame, text="开始答题", command=self.start_quiz, width=20).pack(pady=10) - tk.Button(self.quiz_setup_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20).pack(pady=5) + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="开始答题", command=self.start_quiz, width=20, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) self.show_frame(self.quiz_setup_frame) @@ -330,42 +416,57 @@ class MathQuizApp: if not current_question: return + main_frame = tk.Frame(self.quiz_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=20, padx=30, fill="both", expand=True) + # 显示题目进度 progress = f"题目 {self.current_quiz.current_question_index + 1}/{len(self.current_quiz.questions)}" - tk.Label(self.quiz_frame, text=progress, font=("Arial", 12)).pack(pady=10) + progress_frame = tk.Frame(main_frame, bg=self.primary_color) + progress_frame.pack(fill="x", pady=(0, 20)) + tk.Label(progress_frame, text=progress, font=self.header_font, bg=self.primary_color, fg="white").pack(pady=10) # 显示题目 + question_frame = tk.Frame(main_frame, bg="#ffffff") + question_frame.pack(pady=10) + + tk.Label(question_frame, text="题目:", font=self.header_font, bg="#ffffff").pack(pady=(10, 5)) question_text = str(current_question) - tk.Label(self.quiz_frame, text="题目:", font=("Arial", 14, "bold")).pack(pady=(10, 5)) - tk.Label(self.quiz_frame, text=question_text, font=("Arial", 16), wraplength=500).pack(pady=10) + tk.Label(question_frame, text=question_text, font=("Arial", 16, "bold"), bg="#ffffff", wraplength=500).pack(pady=10) # 计算选项 options = self.generate_options(current_question.answer) # 显示选项 - tk.Label(self.quiz_frame, text="请选择答案:", font=("Arial", 12)).pack(pady=(20, 10)) + tk.Label(main_frame, text="请选择答案:", font=self.normal_font, bg="#ffffff").pack(pady=(20, 10)) self.answer_var = tk.StringVar() - for i, option in enumerate(options): - tk.Radiobutton(self.quiz_frame, text=f"{['A', 'B', 'C', 'D'][i]}. {option}", - variable=self.answer_var, value=str(option), font=("Arial", 12)).pack(anchor="w", padx=50, pady=5) + self.answer_var.set(" ") # 明确设置初始值为空字符串,确保不选中任何选项 + + options_frame = tk.Frame(main_frame, bg="#ffffff") + options_frame.pack(pady=10) - # 默认不选择任何选项 - self.answer_var.set("") + for i, option in enumerate(options): + tk.Radiobutton(options_frame, text=f"{['A', 'B', 'C', 'D'][i]}. {option}", + variable=self.answer_var, value=str(option), font=self.normal_font, + bg="#ffffff", selectcolor="#e0e0e0", activebackground="#f0f0f0").pack(anchor="w", padx=50, pady=5) # 按钮框架 - button_frame = tk.Frame(self.quiz_frame) + button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=20) if self.current_quiz.current_question_index > 0: - tk.Button(button_frame, text="上一题", command=self.previous_question).pack(side="left", padx=10) + tk.Button(button_frame, text="上一题", command=self.previous_question, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) - tk.Button(button_frame, text="提交答案", command=self.submit_answer).pack(side="left", padx=10) + tk.Button(button_frame, text="提交答案", command=self.submit_answer, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) if self.current_quiz.current_question_index < len(self.current_quiz.questions) - 1: - tk.Button(button_frame, text="下一题", command=self.next_question).pack(side="left", padx=10) + tk.Button(button_frame, text="下一题", command=self.next_question, bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) - tk.Button(self.quiz_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15).pack(pady=10) + tk.Button(main_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15, bg=self.danger_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) self.show_frame(self.quiz_frame) @@ -484,37 +585,54 @@ class MathQuizApp: score = self.current_quiz.calculate_score() # 创建结果界面 - tk.Label(self.result_frame, text="测验结果", font=("Arial", 18, "bold")).pack(pady=20) + main_frame = tk.Frame(self.result_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="测验结果", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + result_frame = tk.Frame(main_frame, bg="#ffffff") + result_frame.pack(pady=20) - tk.Label(self.result_frame, text=f"您的得分: {score:.1f}%", font=("Arial", 16)).pack(pady=10) + # 根据得分显示不同颜色的分数 + score_color = self.danger_color if score < 60 else self.warning_color if score < 80 else self.success_color + + tk.Label(result_frame, text=f"您的得分: {score:.1f}%", font=("Arial", 20, "bold"), fg=score_color, bg="#ffffff").pack(pady=10) # 根据得分显示评语 if score >= 90: comment = "优秀! 继续保持!" + comment_color = self.success_color elif score >= 80: comment = "良好! 还可以做得更好!" + comment_color = self.success_color elif score >= 60: comment = "及格了,需要继续努力!" + comment_color = self.warning_color else: comment = "需要加强练习哦!" + comment_color = self.danger_color - tk.Label(self.result_frame, text=comment, font=("Arial", 14)).pack(pady=10) + tk.Label(result_frame, text=comment, font=self.header_font, fg=comment_color, bg="#ffffff").pack(pady=10) # 显示答题详情 correct_count = sum(1 for q, a in zip(self.current_quiz.questions, self.current_quiz.answers) if a is not None and abs(q.answer - a) < 1e-6) total_count = len(self.current_quiz.questions) - tk.Label(self.result_frame, text=f"答对: {correct_count}/{total_count}", font=("Arial", 12)).pack(pady=5) + tk.Label(result_frame, text=f"答对: {correct_count}/{total_count}", font=self.normal_font, bg="#ffffff").pack(pady=5) # 按钮 - button_frame = tk.Frame(self.result_frame) - button_frame.pack(pady=20) + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=30) level_names = {"elementary": "小学", "middle": "初中", "high": "高中"} tk.Button(button_frame, text=f"继续{level_names[self.current_level]}题目", - command=lambda: self.show_quiz_setup_frame(self.current_level), width=20).pack(side="left", padx=10) + command=lambda: self.show_quiz_setup_frame(self.current_level), width=20, bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) - tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15).pack(side="left", padx=10) + tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) self.show_frame(self.result_frame) @@ -527,23 +645,36 @@ class MathQuizApp: widget.destroy() # 创建修改密码界面 - tk.Label(self.change_password_frame, text="修改密码", font=("Arial", 18, "bold")).pack(pady=20) + main_frame = tk.Frame(self.change_password_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="修改密码", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) - tk.Label(self.change_password_frame, text="原密码:").pack(pady=5) - self.old_password_entry = tk.Entry(self.change_password_frame, width=30, show="*") + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="原密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.old_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") self.old_password_entry.pack(pady=5) - tk.Label(self.change_password_frame, text="新密码:", fg="blue").pack(pady=(10, 5)) - tk.Label(self.change_password_frame, text="(6-10位,必须包含大小写字母和数字)", fg="gray").pack(pady=5) - self.new_password_entry = tk.Entry(self.change_password_frame, width=30, show="*") + tk.Label(form_frame, text="新密码:", font=self.normal_font, bg="#ffffff", fg=self.primary_color).pack(pady=(10, 5), anchor="w") + tk.Label(form_frame, text="(6-10位,必须包含大小写字母和数字)", font=("Arial", 10), bg="#ffffff", fg="gray").pack(pady=5, anchor="w") + self.new_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") self.new_password_entry.pack(pady=5) - tk.Label(self.change_password_frame, text="确认新密码:").pack(pady=5) - self.confirm_new_password_entry = tk.Entry(self.change_password_frame, width=30, show="*") + tk.Label(form_frame, text="确认新密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.confirm_new_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") self.confirm_new_password_entry.pack(pady=5) - tk.Button(self.change_password_frame, text="修改密码", command=self.change_password, width=20).pack(pady=10) - tk.Button(self.change_password_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20).pack(pady=5) + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="修改密码", command=self.change_password, width=20, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) self.show_frame(self.change_password_frame) @@ -584,27 +715,38 @@ class MathQuizApp: widget.destroy() # 创建注销账户界面 - tk.Label(self.delete_account_frame, text="注销账户", font=("Arial", 18, "bold"), fg="red").pack(pady=20) + main_frame = tk.Frame(self.delete_account_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.danger_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="注销账户", font=self.title_font, bg=self.danger_color, fg="white").pack(pady=20) + + warning_frame = tk.Frame(main_frame, bg="#ffffff") + warning_frame.pack(pady=10) + + tk.Label(warning_frame, text="警告:此操作将永久删除您的账户和所有数据!", font=self.normal_font, fg=self.danger_color, bg="#ffffff").pack(pady=5) + tk.Label(warning_frame, text="请确认您的邮箱和密码:", font=self.normal_font, fg=self.danger_color, bg="#ffffff").pack(pady=5) - tk.Label(self.delete_account_frame, text="警告:此操作将永久删除您的账户和所有数据!", fg="red").pack(pady=5) - tk.Label(self.delete_account_frame, text="请确认您的邮箱和密码:", fg="red").pack(pady=5) + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) - tk.Label(self.delete_account_frame, text="邮箱:").pack(pady=10) - self.delete_email_entry = tk.Entry(self.delete_account_frame, width=30) + tk.Label(form_frame, text="邮箱:", font=self.normal_font, bg="#ffffff").pack(pady=10, anchor="w") + self.delete_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") self.delete_email_entry.pack(pady=5) - tk.Label(self.delete_account_frame, text="密码:").pack(pady=5) - self.delete_password_entry = tk.Entry(self.delete_account_frame, width=30, show="*") + tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.delete_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") self.delete_password_entry.pack(pady=5) # 按钮框架 - button_frame = tk.Frame(self.delete_account_frame) + button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=20) tk.Button(button_frame, text="确认注销", command=self.confirm_delete_account, - width=15, fg="red").pack(side="left", padx=10) + width=15, bg=self.danger_color, fg="white", font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) tk.Button(button_frame, text="取消", command=self.show_login_frame, - width=15).pack(side="left", padx=10) + width=15, bg="#cccccc", fg=self.dark_text, font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) self.show_frame(self.delete_account_frame) diff --git a/src/users.json b/src/users.json index 9e26dfe..0e0979a 100644 --- a/src/users.json +++ b/src/users.json @@ -1 +1,8 @@ -{} \ No newline at end of file +{ + "1426688201@qq.com": { + "username": "111", + "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", + "registration_code": "939598", + "is_registered": true + } +} \ No newline at end of file -- 2.34.1 From 9ffa9557e3f11ab0274600be0b11f9212d889e95 Mon Sep 17 00:00:00 2001 From: Wzw <3257534544@qq.com> Date: Sat, 11 Oct 2025 20:36:46 +0800 Subject: [PATCH 04/12] FIX1 --- src/__pycache__/main_app.cpython-311.pyc | Bin 0 -> 49470 bytes src/__pycache__/main_app.cpython-313.pyc | Bin 0 -> 47742 bytes src/__pycache__/question_bank.cpython-311.pyc | Bin 0 -> 2076 bytes src/__pycache__/question_bank.cpython-313.pyc | Bin 0 -> 1831 bytes .../question_generator.cpython-311.pyc | Bin 0 -> 18700 bytes .../question_generator.cpython-313.pyc | Bin 0 -> 16303 bytes src/__pycache__/quiz.cpython-311.pyc | Bin 0 -> 4055 bytes src/__pycache__/quiz.cpython-313.pyc | Bin 0 -> 3752 bytes src/__pycache__/user_manager.cpython-311.pyc | Bin 0 -> 17301 bytes src/__pycache__/user_manager.cpython-313.pyc | Bin 0 -> 15642 bytes src/main.py | 23 + src/main_app.py | 835 ++++++++++++++++++ src/question_bank.py | 45 + src/question_generator.py | 409 +++++++++ src/quiz.py | 91 ++ src/user_manager.py | 381 ++++++++ src/users.json | 14 + 17 files changed, 1798 insertions(+) create mode 100644 src/__pycache__/main_app.cpython-311.pyc create mode 100644 src/__pycache__/main_app.cpython-313.pyc create mode 100644 src/__pycache__/question_bank.cpython-311.pyc create mode 100644 src/__pycache__/question_bank.cpython-313.pyc create mode 100644 src/__pycache__/question_generator.cpython-311.pyc create mode 100644 src/__pycache__/question_generator.cpython-313.pyc create mode 100644 src/__pycache__/quiz.cpython-311.pyc create mode 100644 src/__pycache__/quiz.cpython-313.pyc create mode 100644 src/__pycache__/user_manager.cpython-311.pyc create mode 100644 src/__pycache__/user_manager.cpython-313.pyc create mode 100644 src/main.py create mode 100644 src/main_app.py create mode 100644 src/question_bank.py create mode 100644 src/question_generator.py create mode 100644 src/quiz.py create mode 100644 src/user_manager.py create mode 100644 src/users.json diff --git a/src/__pycache__/main_app.cpython-311.pyc b/src/__pycache__/main_app.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3f5ecfef21a85b6e050a7a848d52bacf3006e9f GIT binary patch literal 49470 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 z z6GLhaa7CDADgqQ%a_I;Vm6F(2Uqjb%1TP`@4uXF{@EU?Q5g>n3(q_0Z7G-A;kclq z8;*pk;rh!VKrV)x2k^M=cOfB0=2IrmXzQI0no(pd8Dwwi;OEnQ`Z#0m zK+Y{4{CqYq-&pkNcKtl#!okv8I{5jlPM=`hrvI$y0lm@wX?4haqira6$n)0xTRL!l zwp(vCuGfE7oMe1J|Jh<%mi2n${LdZ=S!i7RS))D{GM{IJfKkQ9QYb;RvEYkbgV8Qk z7OQBFTHg%lO2Cg{y+?Zh$(0%(cS(r{345Mok(tB2PBfso zWiA4pvGOruhu7i&C`FnuG;LZmbMI?zxQtsGW2?AM_yB)8cQF7s$*0%<&J?0Ie9q|f n2_Nfn{<|*nV_nv`F84RO{NLyjextMhKCIKCw|}NXNK^cOX+N9q literal 0 HcmV?d00001 diff --git a/src/__pycache__/question_bank.cpython-311.pyc b/src/__pycache__/question_bank.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd940b3b21e8b2caaa0f33a662d99bedceac5b03 GIT binary patch literal 2076 zcmah~Uu;uV7(e%KZ))pC){Ce;2{Q16f z&iChcZf|VbCkm8|Q)?L&scuI)W5_1S!H4i_k52EeSdm@of}vq@RVct9t#4asOg@deRuryQ9^9Vro-G3HKF3Lp2WoWMpRK^ zYO|v4XmoI>X*y#0W1(Ypu<|l=@<>HN27+Z3Cdeu52nzBFrwR(Mii)5*RH?&WdqPyi z5gvkaKu{8K*N=HUoH&{812dN�s*eutOEV8c zOAl@@&i-ut@4D3A(()i6;1}&Hr|6||2 zN4|YUUvJ6Rn>{-3ZhP$RdgSi9@?O!sqvYOEFrOzaTME(^{2Uy7Jm8}DEfJ2ZvP@jE z98W6g811`dNK`nss&UA&l8neQ_Rv^4O~-_bCUi`BiYcM7AK)f3H#HN2>Hn1pZs=(5 z1Dv&gHI@}5@ESmp1h}L&cs)YLG*?5=+&OGEcl&?cS~_&h?EfcN=h7n~BK#t>MAT2G zqKQGvMV`)h>eZ9&`dYp>e*G-ZyJ5vzG=EEntjxTEcY=amcI`8O8pkk9O=1}yq5+T) h^LznqYkua@slxv~HE}!BQA5q}C!PE2UlgW;{{RBhSIz(c literal 0 HcmV?d00001 diff --git a/src/__pycache__/question_bank.cpython-313.pyc b/src/__pycache__/question_bank.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..430b106adb5c77155a4fbebd0fdc4dfd1a77be9d GIT binary patch literal 1831 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 literal 0 HcmV?d00001 diff --git a/src/__pycache__/question_generator.cpython-313.pyc b/src/__pycache__/question_generator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1914aac7c83244fecd5ae46f97397ffbfaa269f GIT binary patch literal 16303 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 U4=EcI@Y`7NCwN;<^Mv~U03ua^_W%F@ literal 0 HcmV?d00001 diff --git a/src/__pycache__/quiz.cpython-311.pyc b/src/__pycache__/quiz.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fb1b35156f7b97e7b86cb220f4bd9c37d91fed7 GIT binary patch literal 4055 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 literal 0 HcmV?d00001 diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..1524dbd --- /dev/null +++ b/src/main.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +数学练习系统 - 面向小学、初中和高中学生的数学题目练习应用 +支持用户注册、登录、密码管理、题目生成和答题功能 +""" + +import tkinter as tk +from main_app import MathQuizApp + + +def main(): + """ + 主函数 + """ + root = tk.Tk() + app = MathQuizApp(root) + root.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/main_app.py b/src/main_app.py new file mode 100644 index 0000000..d253f17 --- /dev/null +++ b/src/main_app.py @@ -0,0 +1,835 @@ + +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +主应用模块 +""" + +import tkinter as tk +from tkinter import messagebox, ttk +import random +from typing import List, Optional + +from user_manager import UserManager +from question_bank import QuestionBank +from quiz import Quiz +from question_generator import Expression + + +class MathQuizApp: + """ + 数学测验应用程序主类 + """ + + def __init__(self, root: tk.Tk): + """ + 初始化应用程序 + + @param root: Tk根窗口 + """ + self.root = root + self.root.title("数学练习系统") + self.root.geometry("700x600") + self.root.resizable(True, True) + self.root.configure(bg="#f0f0f0") + + # 初始化系统组件 + self.user_manager = UserManager() + self.question_bank = QuestionBank() + + # 设置样式 + self.setup_styles() + + # 创建不同的界面框架 + self.login_frame = tk.Frame(self.root, bg="#f0f0f0") + self.register_frame = tk.Frame(self.root, bg="#f0f0f0") + self.set_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.main_menu_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_setup_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_frame = tk.Frame(self.root, bg="#f0f0f0") + self.result_frame = tk.Frame(self.root, bg="#f0f0f0") + self.change_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.delete_account_frame = tk.Frame(self.root, bg="#f0f0f0") # 添加删除账户框架 + + # 初始化应用状态 + self.current_quiz: Optional[Quiz] = None + self.current_level = "" + self.question_count = 0 + + # 创建界面 + self.create_widgets() + self.show_login_frame() + + def setup_styles(self): + """ + 设置界面样式 + """ + self.title_font = ("Arial", 20, "bold") + self.header_font = ("Arial", 16, "bold") + self.normal_font = ("Arial", 12) + self.button_font = ("Arial", 11, "bold") + + # 定义颜色方案 + self.primary_color = "#4a6fa5" + self.secondary_color = "#6b8cbc" + self.accent_color = "#ff6b6b" + self.success_color = "#4caf50" + self.warning_color = "#ffc107" + self.danger_color = "#f44336" + self.light_bg = "#f8f9fa" + self.dark_text = "#333333" + + def create_widgets(self): + """ + 创建界面组件 + """ + # 创建不同的界面框架 + self.login_frame = tk.Frame(self.root, bg="#f0f0f0") + self.register_frame = tk.Frame(self.root, bg="#f0f0f0") + self.set_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.main_menu_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_setup_frame = tk.Frame(self.root, bg="#f0f0f0") + self.quiz_frame = tk.Frame(self.root, bg="#f0f0f0") + self.result_frame = tk.Frame(self.root, bg="#f0f0f0") + self.change_password_frame = tk.Frame(self.root, bg="#f0f0f0") + self.delete_account_frame = tk.Frame(self.root, bg="#f0f0f0") # 添加删除账户框架 + + def show_frame(self, frame: tk.Frame): + """ + 显示指定的界面框架 + + @param frame: 要显示的框架 + """ + # 隐藏所有框架 + for f in [self.login_frame, self.register_frame, self.set_password_frame, + self.main_menu_frame, self.quiz_setup_frame, self.quiz_frame, + self.result_frame, self.change_password_frame, self.delete_account_frame]: + f.pack_forget() + + # 显示指定框架 + frame.pack(fill="both", expand=True) + + def show_login_frame(self): + """ + 显示登录界面 + """ + # 清除之前的内容 + for widget in self.login_frame.winfo_children(): + widget.destroy() + + # 创建登录界面 + main_frame = tk.Frame(self.login_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=50, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 30)) + tk.Label(title_frame, text="用户登录", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="用户名:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.login_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, + bg="#f0f0f0") + self.login_email_entry.pack(pady=5) + + tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.login_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, + bd=5, bg="#f0f0f0") + self.login_password_entry.pack(pady=5) + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="登录", command=self.login, width=20, bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="注册新用户", command=self.show_register_frame, width=20, bg=self.secondary_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + tk.Button(button_frame, text="注销账户", command=self.show_delete_account_frame, width=20, bg=self.danger_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + + self.show_frame(self.login_frame) + + def login(self): + """ + 处理登录逻辑 + """ + username = self.login_email_entry.get().strip() + password = self.login_password_entry.get() + + if not username or not password: + messagebox.showerror("错误", "请输入用户名和密码") + return + + if self.user_manager.login(username, password): + messagebox.showinfo("成功", "登录成功") + self.show_main_menu_frame() + # 错误信息在user_manager.login中已经显示 + + def show_register_frame(self): + """ + 显示注册界面 + """ + # 清除之前的内容 + for widget in self.register_frame.winfo_children(): + widget.destroy() + + # 创建注册界面 + main_frame = tk.Frame(self.register_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="用户注册", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="用户名:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.register_username_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, + bg="#f0f0f0") + self.register_username_entry.pack(pady=5) + + tk.Label(form_frame, text="邮箱:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.register_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, + bg="#f0f0f0") + self.register_email_entry.pack(pady=5) + + tk.Button(form_frame, text="获取注册码", command=self.send_registration_code, width=20, bg=self.primary_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + + tk.Label(form_frame, text="注册码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.registration_code_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, + bg="#f0f0f0") + self.registration_code_entry.pack(pady=5) + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="验证注册码", command=self.verify_registration_code, width=20, + bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回登录", command=self.show_login_frame, width=20, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + + self.show_frame(self.register_frame) + + def send_registration_code(self): + """ + 发送注册码 + """ + username = self.register_username_entry.get().strip() + email = self.register_email_entry.get().strip() + + if not username or not email: + messagebox.showerror("错误", "请输入用户名和邮箱") + return + + # 注意:register_user 方法内部已经包含了消息提示,不需要额外的消息框 + self.user_manager.register_user(email, username) + + def verify_registration_code(self): + """ + 验证注册码 + """ + email = self.register_email_entry.get().strip() + code = self.registration_code_entry.get().strip() + + if not email or not code: + messagebox.showerror("错误", "请输入邮箱和注册码") + return + + if self.user_manager.verify_registration_code(email, code): + messagebox.showinfo("成功", "注册码验证成功,请设置密码") + self.show_set_password_frame(email) + + def show_set_password_frame(self, email: str): + """ + 显示设置密码界面 + + @param email: 用户邮箱 + """ + # 清除之前的内容 + for widget in self.set_password_frame.winfo_children(): + widget.destroy() + + # 创建设置密码界面 + main_frame = tk.Frame(self.set_password_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="设置密码", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="邮箱: " + email, font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + + tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff", fg=self.primary_color).pack( + pady=(10, 5), anchor="w") + tk.Label(form_frame, text="(6-10位,必须包含大小写字母和数字)", font=("Arial", 10), bg="#ffffff", + fg="gray").pack(pady=5, anchor="w") + self.set_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, + bg="#f0f0f0") + self.set_password_entry.pack(pady=5) + + tk.Label(form_frame, text="确认密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.confirm_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, + bd=5, bg="#f0f0f0") + self.confirm_password_entry.pack(pady=5) + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="设置密码", command=lambda: self.set_password(email), width=20, + bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回登录", command=self.show_login_frame, width=20, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + + self.show_frame(self.set_password_frame) + + def set_password(self, email: str): + """ + 设置密码 + + @param email: 用户邮箱 + """ + password = self.set_password_entry.get() + confirm_password = self.confirm_password_entry.get() + + if not password or not confirm_password: + messagebox.showerror("错误", "请输入密码和确认密码") + return + + if password != confirm_password: + messagebox.showerror("错误", "两次输入的密码不一致") + return + + if self.user_manager.set_password(email, password): + messagebox.showinfo("成功", "密码设置成功,请登录") + self.show_login_frame() + + def show_main_menu_frame(self): + """ + 显示主菜单界面 + """ + # 清除之前的内容 + for widget in self.main_menu_frame.winfo_children(): + widget.destroy() + + # 创建主菜单界面 + main_frame = tk.Frame(self.main_menu_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="主菜单", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + user_email = self.user_manager.current_user.email if self.user_manager.current_user else "未知用户" + tk.Label(main_frame, text=f"欢迎, {user_email}!", font=self.header_font, bg="#ffffff").pack(pady=10) + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="小学题目", command=lambda: self.show_quiz_setup_frame("elementary"), + width=20, height=2, bg="#4CAF50", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack( + pady=10) + tk.Button(button_frame, text="初中题目", command=lambda: self.show_quiz_setup_frame("middle"), + width=20, height=2, bg="#2196F3", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack( + pady=10) + tk.Button(button_frame, text="高中题目", command=lambda: self.show_quiz_setup_frame("high"), + width=20, height=2, bg="#FF9800", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack( + pady=10) + + tk.Button(button_frame, text="修改密码", command=self.show_change_password_frame, + width=20, bg=self.secondary_color, fg="white", font=self.button_font, relief=tk.FLAT, bd=0, + pady=5).pack(pady=10) + tk.Button(button_frame, text="退出登录", command=self.logout, width=20, bg=self.danger_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, pady=5).pack(pady=5) + + self.show_frame(self.main_menu_frame) + + def show_quiz_setup_frame(self, level: str): + """ + 显示测验设置界面 + + @param level: 题目难度 + """ + self.current_level = level + + # 清除之前的内容 + for widget in self.quiz_setup_frame.winfo_children(): + widget.destroy() + + # 创建测验设置界面 + main_frame = tk.Frame(self.quiz_setup_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + level_names = {"elementary": "小学", "middle": "初中", "high": "高中"} + level_colors = {"elementary": "#4CAF50", "middle": "#2196F3", "high": "#FF9800"} + + title_frame = tk.Frame(main_frame, bg=level_colors[level]) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text=f"{level_names[level]}数学题目", font=self.title_font, bg=level_colors[level], + fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=20) + + tk.Label(form_frame, text="请输入题目数量 (1-20):", font=self.normal_font, bg="#ffffff").pack(pady=10) + self.question_count_entry = tk.Entry(form_frame, width=20, font=self.normal_font, justify="center", + relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.question_count_entry.pack(pady=5) + self.question_count_entry.insert(0, "10") # 默认10题 + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="开始答题", command=self.start_quiz, width=20, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + + self.show_frame(self.quiz_setup_frame) + + def start_quiz(self): + """ + 开始测验 + """ + try: + count = int(self.question_count_entry.get()) + if not (1 <= count <= 20): + messagebox.showerror("错误", "题目数量必须在1-20之间") + return + except ValueError: + messagebox.showerror("错误", "请输入有效的数字") + return + + self.question_count = count + + # 生成题目 + try: + questions = self.question_bank.generate_questions(self.current_level, self.question_count) + self.current_quiz = Quiz(questions) + self.show_quiz_frame() + except Exception as e: + messagebox.showerror("错误", f"生成题目时出错: {str(e)}") + + def show_quiz_frame(self): + """ + 显示答题界面 + """ + if not self.current_quiz: + return + + # 清除之前的内容 + for widget in self.quiz_frame.winfo_children(): + widget.destroy() + + # 创建答题界面 + current_question = self.current_quiz.get_current_question() + if not current_question: + return + + main_frame = tk.Frame(self.quiz_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=20, padx=30, fill="both", expand=True) + + # 显示题目进度 + progress = f"题目 {self.current_quiz.current_question_index + 1}/{len(self.current_quiz.questions)}" + progress_frame = tk.Frame(main_frame, bg=self.primary_color) + progress_frame.pack(fill="x", pady=(0, 20)) + tk.Label(progress_frame, text=progress, font=self.header_font, bg=self.primary_color, fg="white").pack(pady=10) + + # 显示题目 + question_frame = tk.Frame(main_frame, bg="#ffffff") + question_frame.pack(pady=10) + + tk.Label(question_frame, text="题目:", font=self.header_font, bg="#ffffff").pack(pady=(10, 5)) + question_text = str(current_question) + tk.Label(question_frame, text=question_text, font=("Arial", 16, "bold"), bg="#ffffff", wraplength=500).pack( + pady=10) + + # 计算选项 + options = self.generate_options(current_question.answer) + + # 显示选项 + tk.Label(main_frame, text="请选择答案:", font=self.normal_font, bg="#ffffff").pack(pady=(20, 10)) + + self.answer_var = tk.StringVar() + self.answer_var.set(" ") # 明确设置初始值为空字符串,确保不选中任何选项 + + options_frame = tk.Frame(main_frame, bg="#ffffff") + options_frame.pack(pady=10) + + for i, option in enumerate(options): + tk.Radiobutton(options_frame, text=f"{['A', 'B', 'C', 'D'][i]}. {option}", + variable=self.answer_var, value=str(option), font=self.normal_font, + bg="#ffffff", selectcolor="#e0e0e0", activebackground="#f0f0f0").pack(anchor="w", padx=50, + pady=5) + + # 按钮框架 + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + if self.current_quiz.current_question_index > 0: + tk.Button(button_frame, text="上一题", command=self.previous_question, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + tk.Button(button_frame, text="提交答案", command=self.submit_answer, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + if self.current_quiz.current_question_index < len(self.current_quiz.questions) - 1: + tk.Button(button_frame, text="下一题", command=self.next_question, bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + tk.Button(main_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15, bg=self.danger_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + + self.show_frame(self.quiz_frame) + + def generate_options(self, correct_answer) -> List[float]: + """ + 为题目生成选项 + + @param correct_answer: 正确答案 + @return: 选项列表 + """ + # 生成4个选项,其中一个是正确答案 + options = {correct_answer} + + # 添加一些干扰项 + if isinstance(correct_answer, int): + while len(options) < 4: + # 生成不同长度的干扰项,避免正确答案总是最长的 + if random.random() < 0.5: + # 生成1-2位数的干扰项 + options.add(random.randint(0, 99)) + else: + # 生成2-3位数的干扰项 + options.add(random.randint(10, 999)) + else: + # 浮点数情况 + while len(options) < 4: + # 随机生成不同长度的浮点数选项 + if random.random() < 0.33: + # 生成1位小数的数 + options.add(round(random.uniform(0, 100), 1)) + elif random.random() < 0.66: + # 生成2位小数的数 + options.add(round(random.uniform(0, 100), 2)) + else: + # 生成整数 + options.add(random.randint(0, 100)) + + # 如果选项不足4个,补充一些随机数 + while len(options) < 4: + if random.random() < 0.5: + options.add(random.randint(0, 100)) + else: + options.add(round(random.uniform(0, 100), random.choice([0, 1, 2]))) + + options_list = list(options) + random.shuffle(options_list) + return options_list[:4] + + def submit_answer(self): + """ + 提交答案 + """ + if not self.current_quiz: + return + + answer_str = self.answer_var.get() + if not answer_str: + messagebox.showerror("错误", "请选择一个答案") + return + + try: + answer = float(answer_str) + self.current_quiz.answer_question(answer) + + # 如果是最后一题,显示结果 + if self.current_quiz.is_finished(): + self.show_result_frame() + else: + messagebox.showinfo("提示", "答案已提交") + except ValueError: + messagebox.showerror("错误", "请选择有效答案") + + def next_question(self): + """ + 下一题 + """ + if not self.current_quiz: + return + + # 保存当前答案(如果有选择) + answer_str = self.answer_var.get() + if answer_str: + try: + answer = float(answer_str) + self.current_quiz.answer_question(answer) + except ValueError: + pass # 如果答案无效,保持为None + + if self.current_quiz.next_question(): + self.show_quiz_frame() + else: + # 已经是最后一题 + if self.current_quiz.answers[self.current_quiz.current_question_index] is not None: + # 如果最后一题已答题,显示结果 + self.show_result_frame() + else: + messagebox.showinfo("提示", "已经是最后一题") + + def previous_question(self): + """ + 上一题 + """ + if not self.current_quiz: + return + + # 保存当前答案(如果有选择) + answer_str = self.answer_var.get() + if answer_str: + try: + answer = float(answer_str) + self.current_quiz.answer_question(answer) + except ValueError: + pass # 如果答案无效,保持为None + + if self.current_quiz.previous_question(): + self.show_quiz_frame() + + def show_result_frame(self): + """ + 显示结果界面 + """ + if not self.current_quiz: + return + + # 清除之前的内容 + for widget in self.result_frame.winfo_children(): + widget.destroy() + + # 计算得分 + score = self.current_quiz.calculate_score() + + # 创建结果界面 + main_frame = tk.Frame(self.result_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="测验结果", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + result_frame = tk.Frame(main_frame, bg="#ffffff") + result_frame.pack(pady=20) + + # 根据得分显示不同颜色的分数 + score_color = self.danger_color if score < 60 else self.warning_color if score < 80 else self.success_color + + tk.Label(result_frame, text=f"您的得分: {score:.1f}%", font=("Arial", 20, "bold"), fg=score_color, + bg="#ffffff").pack(pady=10) + + # 根据得分显示评语 + if score >= 90: + comment = "优秀! 继续保持!" + comment_color = self.success_color + elif score >= 80: + comment = "良好! 还可以做得更好!" + comment_color = self.success_color + elif score >= 60: + comment = "及格了,需要继续努力!" + comment_color = self.warning_color + else: + comment = "需要加强练习哦!" + comment_color = self.danger_color + + tk.Label(result_frame, text=comment, font=self.header_font, fg=comment_color, bg="#ffffff").pack(pady=10) + + # 显示答题详情 + correct_count = sum(1 for q, a in zip(self.current_quiz.questions, self.current_quiz.answers) + if a is not None and abs(q.answer - a) < 1e-6) + total_count = len(self.current_quiz.questions) + tk.Label(result_frame, text=f"答对: {correct_count}/{total_count}", font=self.normal_font, bg="#ffffff").pack( + pady=5) + + # 按钮 + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=30) + + level_names = {"elementary": "小学", "middle": "初中", "high": "高中"} + tk.Button(button_frame, text=f"继续{level_names[self.current_level]}题目", + command=lambda: self.show_quiz_setup_frame(self.current_level), width=20, bg=self.primary_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + self.show_frame(self.result_frame) + + def show_change_password_frame(self): + """ + 显示修改密码界面 + """ + # 清除之前的内容 + for widget in self.change_password_frame.winfo_children(): + widget.destroy() + + # 创建修改密码界面 + main_frame = tk.Frame(self.change_password_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.primary_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="修改密码", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="原密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.old_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, + bg="#f0f0f0") + self.old_password_entry.pack(pady=5) + + tk.Label(form_frame, text="新密码:", font=self.normal_font, bg="#ffffff", fg=self.primary_color).pack( + pady=(10, 5), anchor="w") + tk.Label(form_frame, text="(6-10位,必须包含大小写字母和数字)", font=("Arial", 10), bg="#ffffff", + fg="gray").pack(pady=5, anchor="w") + self.new_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, + bg="#f0f0f0") + self.new_password_entry.pack(pady=5) + + tk.Label(form_frame, text="确认新密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.confirm_new_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", + relief=tk.FLAT, bd=5, bg="#f0f0f0") + self.confirm_new_password_entry.pack(pady=5) + + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="修改密码", command=self.change_password, width=20, bg=self.success_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) + + self.show_frame(self.change_password_frame) + + def change_password(self): + """ + 修改密码 + """ + old_password = self.old_password_entry.get() + new_password = self.new_password_entry.get() + confirm_new_password = self.confirm_new_password_entry.get() + + if not old_password or not new_password or not confirm_new_password: + messagebox.showerror("错误", "请填写所有字段") + return + + if new_password != confirm_new_password: + messagebox.showerror("错误", "新密码和确认密码不一致") + return + + if self.user_manager.change_password(old_password, new_password): + messagebox.showinfo("成功", "密码修改成功") + self.show_main_menu_frame() + # 错误信息在user_manager.change_password中已经显示 + + def logout(self): + """ + 退出登录 + """ + self.user_manager.logout() + self.show_login_frame() + + def show_delete_account_frame(self): + """ + 显示注销账户界面 + """ + # 清除之前的内容 + for widget in self.delete_account_frame.winfo_children(): + widget.destroy() + + # 创建注销账户界面 + main_frame = tk.Frame(self.delete_account_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame.pack(pady=30, padx=50, fill="both", expand=True) + + title_frame = tk.Frame(main_frame, bg=self.danger_color) + title_frame.pack(fill="x", pady=(0, 20)) + tk.Label(title_frame, text="注销账户", font=self.title_font, bg=self.danger_color, fg="white").pack(pady=20) + + warning_frame = tk.Frame(main_frame, bg="#ffffff") + warning_frame.pack(pady=10) + + tk.Label(warning_frame, text="警告:此操作将永久删除您的账户和所有数据!", font=self.normal_font, + fg=self.danger_color, bg="#ffffff").pack(pady=5) + tk.Label(warning_frame, text="请确认您的邮箱和密码:", font=self.normal_font, fg=self.danger_color, + bg="#ffffff").pack(pady=5) + + form_frame = tk.Frame(main_frame, bg="#ffffff") + form_frame.pack(pady=10) + + tk.Label(form_frame, text="邮箱:", font=self.normal_font, bg="#ffffff").pack(pady=10, anchor="w") + self.delete_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, + bg="#f0f0f0") + self.delete_email_entry.pack(pady=5) + + tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") + self.delete_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, + bd=5, bg="#f0f0f0") + self.delete_password_entry.pack(pady=5) + + # 按钮框架 + button_frame = tk.Frame(main_frame, bg="#ffffff") + button_frame.pack(pady=20) + + tk.Button(button_frame, text="确认注销", command=self.confirm_delete_account, + width=15, bg=self.danger_color, fg="white", font=self.button_font, relief=tk.FLAT, bd=0, padx=10, + pady=5).pack(side="left", padx=10) + tk.Button(button_frame, text="取消", command=self.show_login_frame, + width=15, bg="#cccccc", fg=self.dark_text, font=self.button_font, relief=tk.FLAT, bd=0, padx=10, + pady=5).pack(side="left", padx=10) + + self.show_frame(self.delete_account_frame) + + def confirm_delete_account(self): + """ + 确认并执行账户删除操作 + """ + email = self.delete_email_entry.get().strip() + password = self.delete_password_entry.get() + + if not email or not password: + messagebox.showerror("错误", "请输入邮箱和密码") + return + + # 确认操作 + if messagebox.askyesno("确认注销", "确定要注销账户吗?此操作无法撤销!"): + if self.user_manager.delete_account(email, password): + messagebox.showinfo("成功", "账户已注销,您可以使用该邮箱重新注册") + self.show_login_frame() + # 错误信息在user_manager.delete_account中已经显示 + + def delete_account(self): + """ + 注销账户(从主菜单调用的旧方法,保持兼容性) + """ + self.show_delete_account_frame() + + diff --git a/src/question_bank.py b/src/question_bank.py new file mode 100644 index 0000000..20925a9 --- /dev/null +++ b/src/question_bank.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +题库模块 +""" + +from typing import List, Dict +from question_generator import Expression, ElementaryQuestionGenerator, MiddleQuestionGenerator, HighQuestionGenerator + + +class QuestionBank: + """ + 题库类,管理不同难度的题目生成器 + """ + + def __init__(self): + """ + 初始化题库 + """ + self.generators = { + "elementary": ElementaryQuestionGenerator(), + "middle": MiddleQuestionGenerator(), + "high": HighQuestionGenerator() + } + + def generate_questions(self, level: str, count: int) -> List[Expression]: + """ + 生成指定数量和难度的题目 + + @param level: 题目难度(elementary, middle, high) + @param count: 题目数量 + @return: 题目列表 + """ + if level not in self.generators: + raise ValueError("无效的题目难度") + + generator = self.generators[level] + questions = [] + + for _ in range(count): + question = generator.generate_question() + questions.append(question) + + return questions \ No newline at end of file diff --git a/src/question_generator.py b/src/question_generator.py new file mode 100644 index 0000000..eaf6202 --- /dev/null +++ b/src/question_generator.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +题目生成器模块 +""" + +import random +import math +import re +from abc import ABC, abstractmethod +from typing import List, Set, Optional + + +class Expression: + """ + 数学表达式类 + """ + + def __init__(self, expression_str: str, answer: float): + """ + 初始化表达式 + + @param expression_str: 表达式字符串 + @param answer: 表达式答案 + """ + self.expression_str = expression_str + self.answer = answer + + def __str__(self) -> str: + """ + 返回表达式的字符串表示 + + @return: 表达式字符串 + """ + return self.expression_str + + def __eq__(self, other) -> bool: + """ + 比较两个表达式是否相等 + + @param other: 另一个表达式 + @return: 是否相等 + """ + if isinstance(other, Expression): + return self.expression_str == other.expression_str + return False + + def __hash__(self) -> int: + """ + 计算表达式的哈希值 + + @return: 哈希值 + """ + return hash(self.expression_str) + + +class QuestionGenerator(ABC): + """ + 题目生成器抽象基类 + """ + + def __init__(self): + """ + 初始化题目生成器 + """ + self.generated_questions: Set[str] = set() + self.load_existing_questions() + + def load_existing_questions(self) -> None: + """ + 加载已存在的题目用于查重 + """ + # 在实际应用中,可以从文件或其他存储中加载已存在的题目 + pass + + @abstractmethod + def generate_question(self) -> Expression: + """ + 生成题目(抽象方法) + + @return: 数学表达式 + """ + pass + + +class ElementaryQuestionGenerator(QuestionGenerator): + """ + 小学题目生成器(+, -, *, /, 括号) + """ + + def generate_question(self) -> Expression: + """ + 生成小学题目 + + @return: 数学表达式 + """ + max_attempts = 100 + for _ in range(max_attempts): + # 随机生成操作数数量(2-5个) + num_operands = random.randint(2, 5) + operands = [random.randint(1, 50) for _ in range(num_operands)] + operators = [random.choice(['+', '-', '*']) if random.random() < 0.8 else '/' + for _ in range(num_operands - 1)] # 减少减法和除法概率以避免负数 + + # 随机添加括号 + expression_parts = [] + for i in range(num_operands): + expression_parts.append(str(operands[i])) + if i < len(operators): + expression_parts.append(operators[i]) + + # 随机添加括号 + if num_operands >= 3 and random.random() < 0.3: + # 在随机位置添加括号 + open_pos = random.randint(0, len(expression_parts) - 3) + # 确保括号内至少有两个操作数 + close_pos = min(open_pos + 2 + random.randint(1, 2) * 2, len(expression_parts)) + + # 确保括号位置是操作数位置 + if open_pos % 2 == 0 and close_pos % 2 == 0: + expression_parts.insert(open_pos, '(') + expression_parts.insert(close_pos + 1, ')') + + expression_str = ''.join(expression_parts) + + # 验证表达式是否有效 + try: + # 替换除法符号以便计算 + eval_expr = expression_str.replace('/', '/').replace('*', '*') + result = eval(eval_expr) + + # 确保结果是非负数且是合理的(整数或有限小数) + if isinstance(result, (int, float)) and result >= 0 and abs(result) < 1000: + # 格式化结果,保留合适的小数位数 + if isinstance(result, float) and result.is_integer(): + result = int(result) + + expr = Expression(expression_str, result) + # 检查是否已生成过相同题目 + if str(expr) not in self.generated_questions: + self.generated_questions.add(str(expr)) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, result) + return readable_expr + except: + continue + + # 如果无法生成有效题目,返回默认题目 + expr = Expression("1+1", 2) + self.generated_questions.add(str(expr)) + readable_expr = Expression("1+1", 2) + return readable_expr + + +class MiddleQuestionGenerator(QuestionGenerator): + """ + 初中题目生成器(包含平方或开根号) + """ + + def generate_question(self) -> Expression: + """ + 生成初中题目 + + @return: 数学表达式 + """ + max_attempts = 100 + for _ in range(max_attempts): + expression_parts = [] + + # 确保至少有一个平方或开根号 + has_square_or_sqrt = True # 确保至少有一个平方或开根号 + + # 生成操作数数量(2-5个) + num_operands = random.randint(2, 5) + + # 标记是否已添加特殊操作 + special_added = False + + for i in range(num_operands): + # 确保至少添加一个平方或开根号 + if not special_added and i == num_operands - 1: + # 如果还没添加特殊操作,强制在最后一个操作数添加 + choice = random.choice([0, 1]) # 提高开根号概率 + if choice == 0: + # 添加平方 + base = random.randint(1, 12) + expression_parts.append(f"{base}²") + else: + # 添加开根号 + square = random.randint(1, 12) + value = square * square + expression_parts.append(f"√{value}") + special_added = True + else: + rand_val = random.random() + # 修改这里的概率,增加开根号和平方的出现频率 + if not special_added and rand_val < 0.6: # 提高特殊操作概率从0.4到0.6 + # 添加特殊操作(平方或开根号) + choice = random.choice([0, 1]) if random.random() < 0.6 else 1 # 提高开根号概率 + if choice == 0: + # 添加平方 + base = random.randint(1, 12) + expression_parts.append(f"{base}²") + else: + # 添加开根号 + square = random.randint(1, 12) + value = square * square + expression_parts.append(f"√{value}") + special_added = True + else: + # 普通操作数 + expression_parts.append(str(random.randint(1, 50))) + + # 添加运算符(除了最后一个操作数) + if i < num_operands - 1: + expression_parts.append(random.choice(['+', '-', '*', '/'])) + + expression_str = ''.join(expression_parts) + + # 处理可能的语法问题 + expression_str = self.fix_expression_syntax(expression_str) + + # 计算结果 + try: + # 替换表达式中的函数以便计算 + eval_expr = expression_str.replace('²', '**2').replace('√', 'math.sqrt') + result = eval(eval_expr) + + # 确保结果是合理的 + if isinstance(result, (int, float)) and abs(result) < 1000 and not math.isnan(result): + # 格式化结果 + if isinstance(result, float) and abs(result - round(result)) < 1e-10: + result = int(round(result)) + + expr = Expression(expression_str, result) + # 检查是否已生成过相同题目 + if str(expr) not in self.generated_questions: + self.generated_questions.add(str(expr)) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, result) + return readable_expr + except: + continue + + # 如果无法生成有效题目,返回默认题目 + expr = Expression("√4+2²", 6) + self.generated_questions.add(str(expr)) + readable_expr = Expression("√4+2²", 6) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = "√4+2²".replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, 6) + return readable_expr + + def fix_expression_syntax(self, expression: str) -> str: + """ + 修复表达式语法问题 + + @param expression: 原始表达式 + @return: 修复后的表达式 + """ + # 确保函数调用之间有运算符 + expression = re.sub(r'(\d)([√])', r'\1*\2', expression) + expression = re.sub(r'(²)([√])', r'\1*\2', expression) + expression = re.sub(r'(\))(\d)', r'\1*\2', expression) + expression = re.sub(r'(\d)(\()', r'\1*\2', expression) + # 修复根号表达式显示,确保根号符号正确显示 + expression = re.sub(r'√(\d+)', r'√\1', expression) + return expression + + +class HighQuestionGenerator(QuestionGenerator): + """ + 高中题目生成器(包含三角函数) + """ + + def generate_question(self) -> Expression: + """ + 生成高中题目 + + @return: 数学表达式 + """ + max_attempts = 100 + for _ in range(max_attempts): + expression_parts = [] + + # 确保至少有一个三角函数 + has_trig_function = random.random() < 0.8 + + # 生成操作数数量(2-4个) + num_operands = random.randint(2, 4) + + for i in range(num_operands): + if has_trig_function and i == 0: + # 第一个操作数有较高概率是三角函数 + if random.random() < 0.33: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"sin({angle}°)") + elif random.random() < 0.5: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"cos({angle}°)") + else: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"tan({angle}°)") + else: + # 其他操作数 + rand_val = random.random() + if rand_val < 0.1 and has_trig_function: + # 添加三角函数 + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"sin({angle}°)") + elif rand_val < 0.2 and has_trig_function: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"cos({angle}°)") + elif rand_val < 0.3 and has_trig_function: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"tan({angle}°)") + elif rand_val < 0.55: + # 普通数字 + expression_parts.append(str(random.randint(1, 20))) + elif rand_val < 0.7: + # 平方 + base = random.randint(1, 10) + expression_parts.append(f"{base}²") + else: + # 开根号 + square = random.randint(1, 10) + value = square * square + expression_parts.append(f"√{value}") + + # 添加运算符(除了最后一个操作数) + if i < num_operands - 1: + expression_parts.append(random.choice(['+', '-', '*', '/'])) + + expression_str = ''.join(expression_parts) + + # 处理可能的语法问题 + expression_str = self.fix_expression_syntax(expression_str) + + # 计算结果 + try: + # 替换表达式中的函数以便计算 + eval_expr = expression_str.replace('²', '**2').replace('√', 'math.sqrt') + # 修复:确保正确的替换顺序,先替换角度制三角函数 + eval_expr = re.sub(r'sin\((\d+)°\)', r'math.sin(math.radians(\1))', eval_expr) + eval_expr = re.sub(r'cos\((\d+)°\)', r'math.cos(math.radians(\1))', eval_expr) + eval_expr = re.sub(r'tan\((\d+)°\)', r'math.tan(math.radians(\1))', eval_expr) + + result = eval(eval_expr) + + # 确保结果是合理的 + if isinstance(result, (int, float)) and abs(result) < 1000 and not math.isnan(result) and not math.isinf(result): + # 格式化结果,保留两位小数 + if isinstance(result, float): + # 特殊处理常见的三角函数值,使其更加准确 + if abs(result - 0.5) < 1e-10: # sin(30°) = 0.5 + result = 0.5 + elif abs(result - 0.7071067811865476) < 1e-10: # sin(45°) = cos(45°) ≈ 0.707 + result = round(result, 2) + elif abs(result - 0.8660254037844386) < 1e-10: # sin(60°) ≈ 0.866 + result = round(result, 2) + elif abs(result - 0.5773502691896257) < 1e-10: # tan(30°) ≈ 0.577 + result = round(result, 2) + elif abs(result - 1.7320508075688772) < 1e-10: # tan(60°) ≈ 1.732 + result = round(result, 2) + elif abs(result - 1.0) < 1e-10: # tan(45°) = 1, sin(90°) = 1 + result = 1.0 + else: + # 保留两位小数 + result = round(result, 2) + # 整数保持不变 + + expr = Expression(expression_str, result) + # 检查是否已生成过相同题目 + if str(expr) not in self.generated_questions: + self.generated_questions.add(str(expr)) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, result) + return readable_expr + except: + continue + + # 如果无法生成有效题目,返回默认题目 + expr = Expression("sin(30°)", 0.5) + self.generated_questions.add(str(expr)) + readable_expr = Expression("sin(30°)", 0.5) + # 将表达式中的乘号和除号替换为更易读的形式 + readable_expr_str = "sin(30°)".replace('*', '×').replace('/', '÷') + readable_expr = Expression(readable_expr_str, 0.5) + return readable_expr + + def fix_expression_syntax(self, expression: str) -> str: + """ + 修复表达式语法问题 + + @param expression: 原始表达式 + @return: 修复后的表达式 + """ + # 确保函数调用之间有运算符 + expression = re.sub(r'(\d)([sincostan√])', r'\1*\2', expression) + expression = re.sub(r'(²)([sincostan√])', r'\1*\2', expression) + expression = re.sub(r'(sqrt\(\d+\))(\d)', r'\1*\2', expression) + expression = re.sub(r'(\))(\d)', r'\1*\2', expression) + expression = re.sub(r'(\d)(\()', r'\1*\2', expression) + expression = re.sub(r'°\)(\d)', r'°)*\1', expression) + return expression \ No newline at end of file diff --git a/src/quiz.py b/src/quiz.py new file mode 100644 index 0000000..80ae9a1 --- /dev/null +++ b/src/quiz.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测验模块 +""" + +from typing import List, Optional +from question_generator import Expression + + +class Quiz: + """ + 测验类,管理一次答题过程 + """ + + def __init__(self, questions: List[Expression]): + """ + 初始化测验 + + @param questions: 题目列表 + """ + self.questions = questions + self.answers: List[Optional[float]] = [None] * len(questions) + self.current_question_index = 0 + self.score = 0 + + def answer_question(self, answer: float) -> None: + """ + 回答当前题目 + + @param answer: 用户答案 + """ + if 0 <= self.current_question_index < len(self.questions): + self.answers[self.current_question_index] = answer + + def next_question(self) -> bool: + """ + 跳转到下一题 + + @return: 是否有下一题 + """ + if self.current_question_index < len(self.questions) - 1: + self.current_question_index += 1 + return True + return False + + def previous_question(self) -> bool: + """ + 返回上一题 + + @return: 是否有上一题 + """ + if self.current_question_index > 0: + self.current_question_index -= 1 + return True + return False + + def get_current_question(self) -> Optional[Expression]: + """ + 获取当前题目 + + @return: 当前题目 + """ + if 0 <= self.current_question_index < len(self.questions): + return self.questions[self.current_question_index] + return None + + def calculate_score(self) -> float: + """ + 计算得分 + + @return: 得分(百分比) + """ + correct_count = 0 + for i, (question, answer) in enumerate(zip(self.questions, self.answers)): + if answer is not None: + # 允许一定的浮点数误差 + if abs(question.answer - answer) < 1e-6: + correct_count += 1 + + self.score = (correct_count / len(self.questions)) * 100 if self.questions else 0 + return self.score + + def is_finished(self) -> bool: + """ + 检查测验是否已完成 + + @return: 是否已完成 + """ + return self.current_question_index == len(self.questions) - 1 and self.answers[self.current_question_index] is not None \ No newline at end of file diff --git a/src/user_manager.py b/src/user_manager.py new file mode 100644 index 0000000..a214d7e --- /dev/null +++ b/src/user_manager.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +用户管理模块 +""" + +import json +import os +import re +import random +import hashlib +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from typing import Dict, Optional +from tkinter import messagebox + + +class User: + """ + 用户类,存储用户信息 + """ + + def __init__(self, email: str, username: str = "", password_hash: str = "", registration_code: str = ""): + """ + 初始化用户对象 + + @param email: 用户邮箱 + @param username: 用户名 + @param password_hash: 密码哈希值 + @param registration_code: 注册码 + """ + self.email = email + self.username = username + self.password_hash = password_hash + self.registration_code = registration_code + self.is_registered = bool(password_hash) + + +class UserManager: + """ + 用户管理类,负责处理用户注册、登录和密码管理 + """ + + def __init__(self, data_file: str = "users.json"): + """ + 初始化用户管理器 + + @param data_file: 存储用户数据的文件路径 + """ + self.data_file = data_file + self.users: Dict[str, User] = {} + self.current_user: Optional[User] = None + # 邮件配置 + self.smtp_server = "smtp.qq.com" # 可根据需要修改SMTP服务器 + self.smtp_port = 465 + self.sender_email = "3257534544@qq.com" # 需要替换为实际邮箱 + self.sender_password = "pmfyurbkwfpkdbed" # 需要替换为实际应用密码 + self.load_users() + + def load_users(self) -> None: + """ + 从文件加载用户数据 + """ + if os.path.exists(self.data_file): + try: + with open(self.data_file, 'r', encoding='utf-8') as f: + data = json.load(f) + for email, user_data in data.items(): + user = User( + email=email, + username=user_data.get('username', ''), + password_hash=user_data.get('password_hash', ''), + registration_code=user_data.get('registration_code', '') + ) + user.is_registered = user_data.get('is_registered', False) + self.users[email] = user + except (json.JSONDecodeError, FileNotFoundError): + self.users = {} + + def save_users(self) -> None: + """ + 保存用户数据到文件 + """ + data = {} + for email, user in self.users.items(): + data[email] = { + 'username': user.username, + 'password_hash': user.password_hash, + 'registration_code': user.registration_code, + 'is_registered': user.is_registered + } + + try: + with open(self.data_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except IOError as e: + messagebox.showerror("错误", f"保存用户数据失败: {str(e)}") + + def is_valid_email(self, email: str) -> bool: + """ + 验证邮箱格式 + + @param email: 邮箱地址 + @return: 邮箱格式是否有效 + """ + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return re.match(pattern, email) is not None + + def is_valid_username(self, username: str) -> bool: + """ + 验证用户名是否符合要求 + + @param username: 用户名 + @return: 用户名是否有效 + """ + # 用户名长度应在3-20个字符之间 + if not (3 <= len(username) <= 20): + return False + # 用户名只能包含字母、数字和下划线 + pattern = r'^[a-zA-Z0-9_]+$' + return re.match(pattern, username) is not None + + def generate_registration_code(self) -> str: + """ + 生成注册码 + + @return: 6位数字注册码 + """ + return str(random.randint(100000, 999999)) + + def hash_password(self, password: str) -> str: + """ + 对密码进行哈希处理 + + @param password: 原始密码 + @return: 哈希后的密码 + """ + return hashlib.sha256(password.encode('utf-8')).hexdigest() + + def is_valid_password(self, password: str) -> bool: + """ + 验证密码是否符合要求 + + @param password: 密码 + @return: 密码是否有效 + """ + if not (6 <= len(password) <= 10): + return False + + has_lower = any(c.islower() for c in password) + has_upper = any(c.isupper() for c in password) + has_digit = any(c.isdigit() for c in password) + + return has_lower and has_upper and has_digit + + def register_user(self, email: str, username: str) -> bool: + """ + 注册新用户(发送注册码) + + @param email: 用户邮箱 + @param username: 用户名 + @return: 是否成功发送注册码 + """ + if not self.is_valid_email(email): + messagebox.showerror("错误", "邮箱格式不正确") + return False + + if not self.is_valid_username(username): + messagebox.showerror("错误", "用户名应为3-20位,只能包含字母、数字和下划线") + return False + + # 检查邮箱是否已被注册 + if email in self.users and self.users[email].is_registered: + messagebox.showerror("错误", "该邮箱已注册") + return False + + # 检查用户名是否已被使用 + for user in self.users.values(): + if user.username == username and user.is_registered: + messagebox.showerror("错误", "该用户名已存在") + return False + + registration_code = self.generate_registration_code() + user = User(email=email, username=username, registration_code=registration_code) + self.users[email] = user + + # 尝试发送邮件 + if self.send_registration_code_via_email(email, registration_code): + self.save_users() + messagebox.showinfo("成功", "注册码已发送到您的邮箱,请查收") + return True + else: + messagebox.showerror("错误", "无法发送注册码到邮箱,请检查网络连接或邮箱地址") + # 即使邮件发送失败,也保存用户信息以便重试 + self.save_users() + return False + + def send_registration_code_via_email(self, email: str, code: str) -> bool: + """ + 通过电子邮件发送注册码 + + @param email: 接收邮箱 + @param code: 注册码 + @return: 是否发送成功 + """ + try: + # 创建邮件内容 + message = MIMEMultipart() + message["From"] = self.sender_email + message["To"] = email + message["Subject"] = "数学练习系统注册码" + + body = f""" + 您好! + + 欢迎使用数学练习系统! + + 您的注册码是: {code} + + 请在注册界面输入此注册码完成注册。 + + 如果您没有请求此注册码,请忽略此邮件。 + + 祝学习愉快! + 数学练习系统团队 + """ + + message.attach(MIMEText(body, "plain", "utf-8")) + + # 使用SMTP_SSL连接QQ邮箱服务器 + server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) + server.login(self.sender_email, self.sender_password) + text = message.as_string() + server.sendmail(self.sender_email, email, text) + server.quit() + + return True + except Exception as e: + print(f"邮件发送失败: {str(e)}") + return False + + def verify_registration_code(self, email: str, code: str) -> bool: + """ + 验证注册码 + + @param email: 用户邮箱 + @param code: 用户输入的注册码 + @return: 注册码是否正确 + """ + if email not in self.users: + messagebox.showerror("错误", "请先获取注册码") + return False + + user = self.users[email] + if user.registration_code != code: + messagebox.showerror("错误", "注册码不正确") + return False + + return True + + def set_password(self, email: str, password: str) -> bool: + """ + 设置用户密码 + + @param email: 用户邮箱 + @param password: 用户密码 + @return: 是否设置成功 + """ + if not self.is_valid_password(password): + messagebox.showerror("错误", "密码必须为6-10位,且包含大小写字母和数字") + return False + + if email not in self.users: + messagebox.showerror("错误", "用户不存在") + return False + + user = self.users[email] + user.password_hash = self.hash_password(password) + user.is_registered = True + self.save_users() + return True + + def login(self, username: str, password: str) -> bool: + """ + 用户登录 + + @param username: 用户名 + @param password: 用户密码 + @return: 是否登录成功 + """ + # 根据用户名查找用户 + user = None + for u in self.users.values(): + if u.username == username: + user = u + break + + if user is None: + messagebox.showerror("错误", "用户不存在") + return False + + if not user.is_registered: + messagebox.showerror("错误", "请先完成注册") + return False + + if user.password_hash != self.hash_password(password): + messagebox.showerror("错误", "密码不正确") + return False + + self.current_user = user + return True + + def change_password(self, old_password: str, new_password: str) -> bool: + """ + 修改用户密码 + + @param old_password: 原密码 + @param new_password: 新密码 + @return: 是否修改成功 + """ + if not self.current_user: + messagebox.showerror("错误", "用户未登录") + return False + + if self.current_user.password_hash != self.hash_password(old_password): + messagebox.showerror("错误", "原密码不正确") + return False + + if not self.is_valid_password(new_password): + messagebox.showerror("错误", "新密码必须为6-10位,且包含大小写字母和数字") + return False + + self.current_user.password_hash = self.hash_password(new_password) + self.save_users() + return True + + def logout(self) -> None: + """ + 用户登出 + """ + self.current_user = None + + def delete_account(self, email: str, password: str) -> bool: + """ + 注销账户 + + @param email: 用户邮箱 + @param password: 用户密码 + @return: 是否注销成功 + """ + if not self.is_valid_email(email): + messagebox.showerror("错误", "邮箱格式不正确") + return False + + # 检查用户是否存在 + if email not in self.users: + messagebox.showerror("错误", "该邮箱未注册") + return False + + user = self.users[email] + + # 检查用户是否已完成注册 + if not user.is_registered: + messagebox.showerror("错误", "该账户未完成注册") + return False + + # 验证密码 + if user.password_hash != self.hash_password(password): + messagebox.showerror("错误", "密码不正确") + return False + + # 删除用户 + del self.users[email] + # 如果当前用户是被删除的用户,则登出 + if self.current_user and self.current_user.email == email: + self.current_user = None + + self.save_users() + return True diff --git a/src/users.json b/src/users.json new file mode 100644 index 0000000..1d27da5 --- /dev/null +++ b/src/users.json @@ -0,0 +1,14 @@ +{ + "1426688201@qq.com": { + "username": "111", + "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", + "registration_code": "939598", + "is_registered": true + }, + "3154420541@qq.com": { + "username": "123", + "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", + "registration_code": "926322", + "is_registered": true + } +} \ No newline at end of file -- 2.34.1 From d7c6f2305daa1eb36c3412b8f8b5cd14f596bc5c Mon Sep 17 00:00:00 2001 From: Wzw <3257534544@qq.com> Date: Sat, 11 Oct 2025 20:55:11 +0800 Subject: [PATCH 05/12] FIX2 --- src/__pycache__/main_app.cpython-313.pyc | Bin 47742 -> 47744 bytes src/__pycache__/user_manager.cpython-313.pyc | Bin 15642 -> 15507 bytes src/main_app.py | 6 +++--- src/user_manager.py | 7 +++---- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/__pycache__/main_app.cpython-313.pyc b/src/__pycache__/main_app.cpython-313.pyc index fe635a1ee00d58ae3fc1a95228aaf90d0c578bb6..0819c518bfd6b205b626dd1c6578d2c5304336f6 100644 GIT binary patch delta 345 zcmezOg{k2y6YpnUUM>b85D0peA-s{dKax>y^4v&iW&>U0&ATGIndF>Kh+OxKyyzKu zAujzQN5*xI+>0E!7m7-2qHlyehpg@r=h^Pb+Rv_ZhX68I^M#cx5{qwsx zOb8X!U)SA+V9RKax>)^4v&iCS9Y=yCb`qB&=0sG5O_mC>;o%< zsOo0B#9drWMRzy<%Kpa4_;m8eTw%t?n_2U`85tjL4$SZ3WV}21PKBh@U68$@K*CRx z`xaMbab{j|Nn&1dYSF#T{FM(_81HZXP@BiheIKNZ1?bYEhnq7Sw=zKln7{&yHCwnC f?{3!Wi)3Pay18Y-E_KGolkIoN0xgQ&q09sTQJ-=@ diff --git a/src/__pycache__/user_manager.cpython-313.pyc b/src/__pycache__/user_manager.cpython-313.pyc index 10f1acb549b7470aac53b419bf0763a98deb1e92..273925cf962736fe372ef12280899774f83cbfc6 100644 GIT binary patch delta 295 zcmbPLHMx@aGcPX}0}vPly~<$P$g9fC=)BpOIfI|kV)7gzOO|vNP1ecW!g-P*nvA!Y zb5ip(nTmvf%8Ph_L@^VPn7moojzby9XkhptF!_{}4D$ni;mvv?{EUn>o9#sF85vtP z?-Mg;Vw^tto5UK%8Iu=FPGX!n*-WaPv3K)6DQ^}vC7|Z#Q&v3d*#2}v@5`zCpYB@r zbk~$8J9ZiC8W}vCe|J+w3df#>hSwWUUS3WF1TG$*hWUEdEz`0ye8FCbBUe z-`uG#!p!)2^E|D`OpI)sn{>698D%!FHMqjeXtlY?M3Ip(X!8=&MrOu{&1#mv8HKr7 kMOG+(U;t7ZRK9?y4^o>=tgkcj2eUGYd}08RMPfkf0DN{_s1~F4@XBYw}sicE-NVp;F!~E)GCrpHErwtYiDr z3B6BuO?k3om$9yq!IQnc@AvdP?O*kxdGFJn)~6FzKV3Wh+4|nc4UNyHZUC~MPU?BG zqx)&cr02W#Z$2j1#>hBtvaf str: """ @@ -168,7 +167,7 @@ class UserManager: return False if not self.is_valid_username(username): - messagebox.showerror("错误", "用户名应为3-20位,只能包含字母、数字和下划线") + messagebox.showerror("错误", "用户名长度应为3-20个字符") return False # 检查邮箱是否已被注册 -- 2.34.1 From 9839c0e6814601efb609d629583a6c80b41a604d Mon Sep 17 00:00:00 2001 From: Wzw <3257534544@qq.com> Date: Sun, 12 Oct 2025 01:49:09 +0800 Subject: [PATCH 06/12] FIX3 --- src/__pycache__/main_app.cpython-313.pyc | Bin 47744 -> 47855 bytes src/__pycache__/quiz.cpython-313.pyc | Bin 3752 -> 6934 bytes src/main_app.py | 50 +++++++++---- src/quiz.py | 89 +++++++++++++++++++++-- src/users.json | 2 +- 5 files changed, 118 insertions(+), 23 deletions(-) diff --git a/src/__pycache__/main_app.cpython-313.pyc b/src/__pycache__/main_app.cpython-313.pyc index 0819c518bfd6b205b626dd1c6578d2c5304336f6..bd55c36342b9012fbe4826aefd5eff7a48c84767 100644 GIT binary patch delta 2077 zcma)7Z%k8H6upvyKh%oF_julj$f!EDe zwAp4B4W*dYLdb(^W=*@F(HTV}oZMrF$)+UGwWQgfLudOvUYXHPc< zk&v+pizb@hj0CsHnNdhbGcju~!HJ`Gc)!KZu0+U$Ah_3;$cLF2FY;P?KKfz;FXW1F zvo8(0qlXiDp+H2d7rw?U$71S$y-jwxt?jrjoZ1rUs^7Rpk|MFL`u5FT zt?jW+>1k4b)F5<8t+AFD8l+KUL#%UaM5=$4;KV8135#P}u^GPiU8%5YN5Eq;RN%W@%7A}_n@vcn+-!xLt#SnlS8%p% zgbT|b?BK9gW|NMVfhF6$v?2^xli+xFAuQW@8)rezuAJqQq=dAenrzZVQVumY=`?|& zTNFK{=1*#T)Tg7UpPF1Uo#J?SmruWpa=c79=DS(me%E3jXdAJ3uUl-Y#j9Ans-;M= z6vh1`mPNApE#H68dpZAKNimN|J+580JD`7w*53VpYQxa(D2~e+Z?g${!4osodEupA zH&I?HYp5QOk#mxWEeKFNN%L@U~ z4E=p+mTBSVhjbz;!9oFy@AJa3XtBW~xP&=EVc0~qZV!^#+%Q9UKE6Vg3!z#lYPI89ct2Y5WD5RK3bMT%p6#FO_n;Q+MRm0IwULM#2ptb{qe@(T454PU zNase)IIlz&_b&H3y>r89%53QcWjO^mi#{GBu$2{ zF#Z}nb5(Dwv9nigm{|GJ;e`f#NuE5`&1NsBO44+{f}%=lG-V&8hjmg@1*cDmxC$Pe z^5AiK*6E#?(d*=6XY@%pB%l81BxZP>T=_B2;?*F2y2BzoN_l{W*TS7oMJz&i$cM}2 zh9Lptbue_!jaR|7b2%1G;|uA=9BL}Tcs^6FrKpAC^O+`%7JVZ~%jNp>iFm$tKY28s z)M)pUb{}ZB(l>Nx6#v0WlE^D}`X5T5C=I^5Ah;s5!dyD1 gJu;fK@RjzaEt?{Xr8X%4yf}F&kMkxFwc4h?0dJ2)!~g&Q delta 2086 zcma)7drVVT96sk>TKc#^rBnoYw7kos6a^=Ws2lUuu_%|TOh8wllul^tZDEeg2fiOD z=r1fTZax;Xn9K)w=kS%qMYp(roE>Ckja!`Y54Y?gpmS!6+c_6ex5Q*8>37fL_xR4^ z_w~2?%!d0+7f;JLOWod-*XivY@$>V)NTZF-APJo1_ zoUkf{v{3HMWjTS9)9h)g#T@+HbRvx7^8`56X^>94)EINp#W%iU<8)4k+acQhj+Gv- z-{o=pLQ1>a_ohRHL+(WQ+}(yFpv2Pvy`JQ-2$B&7J;h8~7+AL$q%>~_j?D~2XI{^m z9?Y5@$eKM6J!gCPJtb0Wq2_Qh46Tld9#U~#Q9Gi7s>uP>dA_28Yh9+&L{8f$LDc%v6pgwJ z-!R1QpD|#_*gkKF(*?Q20GAl#@&a7mvBp8JNNQQnVVM9E3(c&4j6~sM0v4FiaSNN^ zUPt<(M8*EIUA2j9M{c|^9#yBpuRD_AHS+R}J7eYYShQ0U zQ}o!Rh%lhdt*$mdF}z!%V%!=yxIJ}(Nl=q2ND(xo3c@N;MK%O_^en@tg0(Rgr@`HA z`K(sZ%Fn!ampV3`&)_rpEDP)As^!Y2!7FlMc#T1+<;?EWhJ3q75B4WG-$^J#ouu^e_HZz03{V#bL68!=qVTM4H^nNtBj1uUHtaV2!P z49|%b`7{>sD~82gR;3X+v3aVrfA=h0R{!`kC}~6$U@G~IdI_W=BkDl{(xV0BeanIo zEim=un###GtDpD@7!Fs(tQVL(GCh8{Jf0DaPy;n4D#9~us7GN$J#IFJ}%v_@cK|yhAdYnGkEQ^U2j?Wjl?deWRtM*;8+C_LW+m*t|oZRWb4V5p6(s5l*n-ud^LT(hi%3l8vJ* zN=&*@P}sYqzpAh567H)RD0uNwWuRc;1G@6=Tw?mr#}V(1>S?$uei}YOzHn#HF#Z5S zU1_l8q#n3#9ZrPA?iD&;YfFf2cX`Fz#6!%+@P4piV*zqpMq ze@nnewe5A|Gfm#2m=4SLrf3=|^(CZS|9}ZzgLGi;r+5}oxwwR3c^k)5k2M4>vjdjd zLCeB`W#QGrpsglgs|nf~0=9<1Y{#fd2|D2Mi`0vP^rRGB>l-Fn0d%Z zpPY(NU;(atyiT|DNyy#QeHm1o5pWe8JCliXr0ZvRY^o&|q4=lQ=e0CAnjPf-z+Nw( z(JUzIPuIyRQbI$dQ&qbm2?d>!46YhqVxK?n3^2tT5lLU~&}3-n8D24|bPbV3(&RE%6mOu%f{Wx;Ki3*Niis!ghW zrAsswM-3z~aY@p+rbaPoMv|K6AGPgtrfgQi&Yg6|X5CWQWm8?nSjD|hOlavE_+Kpe%<+o(obpc@&1 zX=DYqQ6VVIn9gidHmU@b8Rem3#IXg4Q>>=t83c8g(xUR{p{1F4`{>l`$0qzk;TH!q zQ@G#4O6+#0=$6zwd)>|+-sY0n2b^}dq-gQy_EI_>g|avtB{pP+pj zwIhLoP0}1CFdQwg9Mi67S1O>dipCrIE-R=w1<(}jYD|sxC`nHm=^=C&ts*oHr*2ns z3NEdR6~ znk7qOcX^zLrgX4PPbp+kzD-{K?dMmIOI? z_*9ZAN|ti$9zk&M?s%Wo$#*yoND9&3BRDLy#EK4ACs{+p>zU2c!<(wxTSZu=ebs%d z*RF1Vz#)Fm-P7AHx;-6uCh?%?c63+tI{2=G9)6##$GTUr@%DY@vXz#ULG7YoZ{H7c zt?WH0sjaZK(`~g%=~&d2v1TSLD^^2x1VwbHVDT}1sA8pm%Wt@m{I?ztlyCB~$8@2x z^6|3mfwJwvvL@0lUFv18s~3l|bG+;`nmh0{l9YnO?Gbo;RG^raPy{bDPNNlwr$Dfz zYi4FdTUQ{&gD9Kh%8?k+N;k*FouI~4hi6);xs;`qiH$I$9iRkSWJax239?Y0EijAb z=E%DfeMctF^vQysJau-$+aG@6-SEZdr;hYZ4ZQN?Bi^>;-9cz(^x3E9qi7<02pw(;6WIIz@a;Gb91P5eWu7zC;$+{3`b? z{BZ+hM2dL@UR@}!@T~rf-n%uFQ!t)W9>^*8@4jR`Zw=rzr_`8utkaRoJ=c8_l8Q zRI$?}=9uR!V8%P(JUc&{XUv=TJg=IZxBRQl+m;?XDx4f^NlJo~r^R;7X=1g+{&(8$ z!Ef#VaDCkz>whyDB%LO4hI#mlWdJ;$XT(qMDS`i1~v?%+#2^yqzLlDNS%^a zATE7Z+=tHQ^E@2DJi=UHjObxjq*ox31`vjriD%m|PLb5g#!m4Ny#oLWLnyD{S=dc> zY)qnTRm4V{QLBQZIfg|gFs~W@JllyZdJo2a47|yZ8wXq2BZFrY9Co)=Mo)m=H(r?> zx(IQ{#E>@u$TL8g`0A!93_o4+c5ZB#vO{eON1M(`2IxgU|HrP8g<~70Ixrn$O_UVg zFa^9o7KY3sEP=X!Lm-P5T$+vV=;@X;PSMGWZg8^>iE;970h^!1*g85S6&?a4Ng?!j zcxZcgCj-f1zORyKU9Z3wNaJHEYd`=Dt&zbcSl^@(nEgh9HElpF^o$0TvNJOR_5 zhT94Tv()r;CBab@fK5amIe@~bmYQoe7zI!x(uw;M$bhl1@}1_^mA7uacOmQ@zBzIf zZf78d#$h;9>|1lwA+oM%$_eV!t$7V%-_$Wuz^9S0J(7AcFccm{1R3>Mcy zCNnr=Tw5B@mR>OWTP{6*{_&utW@LZBQXkaThjba^x}~4!miku)bme2Zr6FT($Y7ey zU#!DK?;nE7SMisls!xH=&&M;m0z%jsJY%|8i^5Y0(`s&ga0*oS=E&3YF)Y@Z@CYqi zER@VOd{s*3R3rGlSY?Uz_nz|frYwG=3$|ssQR~K9{kXQ@Z zycQ4kf5Tt|6F21z)SamFm5dkH2HnmoHo?~BV0-+snuH6<{EzoXFSA4(a8FHvV;q>w5|1%i2C?^!yjjwT_WT5< zNZe1q3XX|zGQDZCcxq16_}Fpt{Q>wHYesc7Ap`Wy77)Hy65MbDfMI{hQ*CkFz84

>Ys43sGDAMhbC0dEbN`C8eY|KORqG^pAOEjwXHp z&e00*ni%nUY6q;hqbB-jF{e+D->o5nXr<#Lzqak9aAe_1>>V{{ zC2cxdEkTP=8}`Q}sgMoK!HkN~j;9goM72y2>ZIb>3h#s$2g2_UguPECYaAg!1TgvB z`_MCS=~UeJcRlmZw|#eiH(V!&-6gPy1zr^$yr-MIuSv`yXKx}VfRKByNFoAB(dp{3 zxh2iRHkZe-SrFjT0DMCNK7vIdN7?DIDBBL9xm%)~!a_U*M_eK{9~@gp{hh&XbJ;yE z{G>;o=AuDG3iTNN@^#1v#N-!v(?45Mc|jbmx>9$!?!&5UH6PUkS3LCTl3fAaB5y;$ zu&7@R!DrC0C_*bU^Ka%B46Zx7;kgYLT8G#CdQV_Mbujn-k&a;Q#!%i8e`X+W+4JeQ z8I-q)x}Anfn<;o8-%Nd*y4G~9slWSGyT9a8<@w5B>H0v<`VsDv?E2B&kHTY|20-ih zp}s?->ijRKwJ5Ln4x%&j|7yyMD4_hei1p&#Wg9K@#}>`TwTh3|P>|34NGJQ0xiS7; zb`;aEXk4=uK}3_Rp>QMohTz80+bIAy#)E?l&BQB5CeHpU`kGHdoA8x4!xxXilUmeV7M?`KeU1((g8)|&#Rs2=b&wHJXPCwf%L0aFNulG14S~wd z0mJ5T!{OhT{JH!O<)e?ZjXxp;;MXXQ{_yZ<DVS-kmkJy4_SS4VJAFE_l&A|_| zysg^-53?GpwY#Uo-0@iY#A?fAP?xxSbU-Whn}L+0zJ$NSRVmPY<@-MYxprk9XZ< zNfRD{Qc}7P_B#14nTLcc7!?HUf~Bn3yKL-T7XXH^nH>HxJPx8vBwtC?HL~35ggrpR nzDHaPS%je|>b9Du*t=>(tq7vZzaYz9YRWr-l( delta 1243 zcmZ8fO>7%Q6rR~XuO09D|0iw&+ayimmgc_=X;DZ^e?&@B(T(8}6uFyiVsO`C))vqM z;RF#X5@tl9CoC02A|zC)9E!x1iUSBmKoyO|4Iz~Sa!94z5O2l_sLbJe@0)q=&6{uD zjpOT`=8j>65UiewKbF4^*3ImRCxLy4Y($WaZRrA@mWVVh6WQWizMxF2M72;Bbs<~H zAzM8r@qC1cR$uOnNlbaJMmI|mW zi^c#1Q|PkvY(gbL*kE$m^;n?j`LDTz{wd{6zU%X+SQdN1p@(GunZv-|Ld`JBbze8P z21nPgeC6Mrxbx=bz}YqBdU&h1Z%z4G@7_wK*Od1{I}nYS)+)&~;#OUw%JGx$?t==RtziSe!E!N&8O z$=(lUwu7km5^jd+U}jJv{No;{pJsl@@Mp}D4B(^Mu4TJ$fqs!a-@&!bD`Fm6za+*( zyd+K&?PrejK1mFKsaMLCVwGgMuUxKF%H%PAc94U@p@+i&hhh3jd*^9hTfB~Nkp=L% z^F4sK(Y7S1dQ%UonMP0J+Gf|tLj-;^9Z;WcxJ?A+wnDpeL#w8mYos0`FilHSr=*YP zb`iHk)aRvV^8qprmTA>DR!ZIy;lm+%V)1>|7nWR)e%8B#-;IO{PC}wX#?% pyOU%R4)_%L7V1N&DPxScbqOoGI>KGQqWrFQ5#uT8FC>g8@ehGo4x0b~ diff --git a/src/main_app.py b/src/main_app.py index 74b5804..238d6a9 100644 --- a/src/main_app.py +++ b/src/main_app.py @@ -458,21 +458,29 @@ class MathQuizApp: tk.Label(question_frame, text=question_text, font=("Arial", 16, "bold"), bg="#ffffff", wraplength=500).pack( pady=10) - # 计算选项 - options = self.generate_options(current_question.answer) + # 获取固定选项 + options = self.current_quiz.get_current_options() # 显示选项 tk.Label(main_frame, text="请选择答案:", font=self.normal_font, bg="#ffffff").pack(pady=(20, 10)) self.answer_var = tk.StringVar() - self.answer_var.set(" ") # 明确设置初始值为空字符串,确保不选中任何选项 + # 设置默认选项为用户之前的选择(如果有) + current_answer = self.current_quiz.answers[self.current_quiz.current_question_index] + # print(current_answer) + # print(self.current_quiz.current_question_index) + # print(self.current_quiz.answers[:5]) 调试功能 + if current_answer is not None: + self.answer_var.set(current_answer) + else: + self.answer_var.set(" ") # 没有选择时设为空字符串 options_frame = tk.Frame(main_frame, bg="#ffffff") options_frame.pack(pady=10) for i, option in enumerate(options): tk.Radiobutton(options_frame, text=f"{['A', 'B', 'C', 'D'][i]}. {option}", - variable=self.answer_var, value=str(option), font=self.normal_font, + variable=self.answer_var, value=option, font=self.normal_font, bg="#ffffff", selectcolor="#e0e0e0", activebackground="#f0f0f0").pack(anchor="w", padx=50, pady=5) @@ -550,13 +558,14 @@ class MathQuizApp: return answer_str = self.answer_var.get() - if not answer_str: + if not answer_str: # 检查字符串是否为空 messagebox.showerror("错误", "请选择一个答案") return try: - answer = float(answer_str) - self.current_quiz.answer_question(answer) + # 验证答案是否为有效数字 + float(answer_str) + self.current_quiz.answer_question(answer_str) # 如果是最后一题,显示结果 if self.current_quiz.is_finished(): @@ -575,10 +584,11 @@ class MathQuizApp: # 保存当前答案(如果有选择) answer_str = self.answer_var.get() - if answer_str: + if answer_str: # 检查字符串是否非空 try: - answer = float(answer_str) - self.current_quiz.answer_question(answer) + # 验证答案是否为有效数字 + float(answer_str) + self.current_quiz.answer_question(answer_str) except ValueError: pass # 如果答案无效,保持为None @@ -601,10 +611,11 @@ class MathQuizApp: # 保存当前答案(如果有选择) answer_str = self.answer_var.get() - if answer_str: + if answer_str: # 检查字符串是否非空 try: - answer = float(answer_str) - self.current_quiz.answer_question(answer) + # 验证答案是否为有效数字 + float(answer_str) + self.current_quiz.answer_question(answer_str) except ValueError: pass # 如果答案无效,保持为None @@ -659,8 +670,17 @@ class MathQuizApp: tk.Label(result_frame, text=comment, font=self.header_font, fg=comment_color, bg="#ffffff").pack(pady=10) # 显示答题详情 - correct_count = sum(1 for q, a in zip(self.current_quiz.questions, self.current_quiz.answers) - if a is not None and abs(q.answer - a) < 1e-6) + correct_count = 0 + for q, a in zip(self.current_quiz.questions, self.current_quiz.answers): + if a is not None: + try: + # 将字符串答案转换为浮点数进行比较 + if abs(q.answer - float(a)) < 1e-6: + correct_count += 1 + except ValueError: + # 如果转换失败,说明答案无效,不计入正确答案 + pass + total_count = len(self.current_quiz.questions) tk.Label(result_frame, text=f"答对: {correct_count}/{total_count}", font=self.normal_font, bg="#ffffff").pack( pady=5) diff --git a/src/quiz.py b/src/quiz.py index 80ae9a1..cf09164 100644 --- a/src/quiz.py +++ b/src/quiz.py @@ -5,7 +5,7 @@ 测验模块 """ -from typing import List, Optional +from typing import List, Optional, Dict, Tuple from question_generator import Expression @@ -21,15 +21,76 @@ class Quiz: @param questions: 题目列表 """ self.questions = questions - self.answers: List[Optional[float]] = [None] * len(questions) + self.answers: List[Optional[str]] = [None] * len(questions) # 改为字符串类型存储 + self.options: List[List[str]] = self._generate_options_for_questions() # 为每道题生成固定选项 self.current_question_index = 0 self.score = 0 - def answer_question(self, answer: float) -> None: + def _generate_options_for_questions(self) -> List[List[str]]: + """ + 为所有题目生成固定选项 + + @return: 每道题的选项列表 + """ + options_list = [] + for question in self.questions: + options = self._generate_options(question.answer) + options_list.append(options) + return options_list + + def _generate_options(self, correct_answer) -> List[str]: + """ + 为题目生成选项 + + @param correct_answer: 正确答案 + @return: 选项列表 + """ + import random + + # 生成4个选项,其中一个是正确答案 + options = {correct_answer} + + # 添加一些干扰项 + if isinstance(correct_answer, int): + while len(options) < 4: + # 生成不同长度的干扰项,避免正确答案总是最长的 + if random.random() < 0.5: + # 生成1-2位数的干扰项 + options.add(random.randint(0, 99)) + else: + # 生成2-3位数的干扰项 + options.add(random.randint(10, 999)) + else: + # 浮点数情况 + while len(options) < 4: + # 随机生成不同长度的浮点数选项 + if random.random() < 0.33: + # 生成1位小数的数 + options.add(round(random.uniform(0, 100), 1)) + elif random.random() < 0.66: + # 生成2位小数的数 + options.add(round(random.uniform(0, 100), 2)) + else: + # 生成整数 + options.add(random.randint(0, 100)) + + # 如果选项不足4个,补充一些随机数 + while len(options) < 4: + if random.random() < 0.5: + options.add(random.randint(0, 100)) + else: + options.add(round(random.uniform(0, 100), random.choice([0, 1, 2]))) + + # 将所有选项转换为字符串 + options_list = [str(opt) for opt in options] + random.shuffle(options_list) + return options_list[:4] + + def answer_question(self, answer: str) -> None: """ 回答当前题目 - @param answer: 用户答案 + @param answer: 用户答案(字符串形式) """ if 0 <= self.current_question_index < len(self.questions): self.answers[self.current_question_index] = answer @@ -66,6 +127,16 @@ class Quiz: return self.questions[self.current_question_index] return None + def get_current_options(self) -> List[str]: + """ + 获取当前题目的选项 + + @return: 当前题目的选项列表 + """ + if 0 <= self.current_question_index < len(self.questions): + return self.options[self.current_question_index] + return [] + def calculate_score(self) -> float: """ 计算得分 @@ -75,9 +146,13 @@ class Quiz: correct_count = 0 for i, (question, answer) in enumerate(zip(self.questions, self.answers)): if answer is not None: - # 允许一定的浮点数误差 - if abs(question.answer - answer) < 1e-6: - correct_count += 1 + try: + # 将字符串答案转换为浮点数进行比较 + if abs(question.answer - float(answer)) < 1e-6: + correct_count += 1 + except ValueError: + # 如果转换失败,说明答案无效,不计入正确答案 + pass self.score = (correct_count / len(self.questions)) * 100 if self.questions else 0 return self.score diff --git a/src/users.json b/src/users.json index 1d27da5..672adb6 100644 --- a/src/users.json +++ b/src/users.json @@ -7,7 +7,7 @@ }, "3154420541@qq.com": { "username": "123", - "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", + "password_hash": "b17e1e0450dac425ea318253f6f852972d69731d6c7499c001468b695b6da219", "registration_code": "926322", "is_registered": true } -- 2.34.1 From 5a89d0ab47b35a0bf04b8f50d0273e44f2eef4c2 Mon Sep 17 00:00:00 2001 From: Wzw <3257534544@qq.com> Date: Sun, 12 Oct 2025 02:08:09 +0800 Subject: [PATCH 07/12] FIX4 --- .../question_generator.cpython-313.pyc | Bin 16303 -> 16146 bytes src/question_generator.py | 57 ++++++++++-------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/__pycache__/question_generator.cpython-313.pyc b/src/__pycache__/question_generator.cpython-313.pyc index b1914aac7c83244fecd5ae46f97397ffbfaa269f..4e941737dffdf55e5d4e656f9df9371811bff66a 100644 GIT binary patch delta 3046 zcmbVOYfN0n6`r~K0QSMM@AnJ#Y1r^w;w)Yk92*?sHG!LLXbHv}-jco8ccE@^?TwN) zb=t(SnYMATDiiv{IH~GQP$Z;_U(GBbiWFZEF4l@7U4D$GL&O8=`%cq zPc6J-Fk*a#Tr~zuok>wJj)*{p{h#Gh_ z^JpXQ+rGflx)9}QQC340Coek0d!EfE7-QJ?rzDU_{8W#$s^nZ z_c)#W;^)7(`O%$keH_k`_vSUCRGKWN@W+!g4V888y}cr)1HXpb2b4~LCjhz>fZYHd zfUl6BI?Sf0KL2H* zL>=7^3-aXQp@FgZ!T$K*;LsrV3}|$by-pth2NL6jO^Dny8`=6%h>i9vi2d^t#5K9@lzGNlEph<1yD ze?rHpvWo|N&`8w{W=gRqC38cddrMU60bEXbD&a5>`nL1pIO_tIEe#Mp}(}k&FgXO|F`1)$1$r zo=*ZI8vRo8x>4`d#7aw1J+mI_{F%`Z3<~HqWulGwyIhX7_NSBAt$|$cGND4(y|s7*wHZdR}oFa_SBPHGY!^?kU|^RLXm?o`h|SAVx#g( z0AL4&DPeLf;hUuAg5FE}{UV;jP17Mq0DeGW+a<}1(0*W|&GC=z@!cCGh!N$~mRlJ0*fp7IS z;F$h_3Qm^`3pfmq0`NurMUcJ+uxUAmg>L|Slfo_07~N;s%T2)yY|GUG@a10nK)EM} z8ugFywOum7jg4$5?7X#jV&vcu_bkZL;mO26|G?<@*uW5X9E_d-pcfc%kkB0e2w;+0 za>}VAx7Bj;=SE+Vme#@K6(RZ5Y3p&iugR=yVq_^l-#pVi@0{<-n5$3jT@@pf{ba|o z)^v8qO|9=V{#@^zjW0T{S2krlP0QAb^ZRG^&$rGW$yjSow=LtJLvun&-vps$Iqos@A-q-J?~4i zZ$7l_sk)GuOQgFNamEumD_t^s=A9XHjg+tnG@&a=ChELArx_17)+>A}U| zqW4|(wT9g{ENx3x-+XJv8n{RPpl)}@)pj4dVV%DGUu}L!ZMyxhNyrbk|D>o7Bb|Ts zz(VAaIn1Po$!DQss$K@E+-pw@P?_m$b3@O>tRn8Y zg+Dd5Vf<}U6}IB! z0(%7{fHF8+_{Y6~{>Ct(;??3~SY(|Its&@Fr^wHm4SFE@%Q!bE(aU`=S6?KbG(VDk nwY@?t5i6b}p$J=k7-Q2lTlE?OVQpCO1VhJ`z&{WOybJ#U?jL{w delta 2907 zcmai0eQZ&Uwy< z-DTqZ@q6cI5Y)LG7( zV5jpDJ~Q`Yr;GCs;F+2r{M%(PpJGfhi2A6NvBU|+P#zFOJ-lfMz$Z?VI?hGy-2ElZ zT=gtY-tV%*Xs80tl{)#4<}Q_<6>Rm4#Z(;)3N|UY0if&_e;uCk4Cgf{^k?|U<8Js8 z2KT9|re@uz`NN5kJ$0!A@`(Hp3U8hN*?YhH!_)7#$PeO3?jYDo&t?hC*cpG}bWg6Na@W|;9^^F%kj z75ZuDeE6L$m!t6?*k5&@anJJE$n?SA`kF3EUzr;(7X`yR`UgUtjl#Q)LT594R9T_> z7(Kqsx*1$@mBIR|DQK_q6>G&ji8rcn3Mt7YUde(wxL!3COY2bbr=t^rp8nckT9X;{ zL}r9}K^~Y_rP7bj2$>x|hjS?b!RwXH_>rGLm=ARf_4n@^08fiw%QJ$3NSSB=i!M~qdArj~k{$2phPLE#Q{tt{) z2(zS$N=PdTQCu6|2-L6d#$_@jX&M)K8;c3;UquV8qz)|3CVq~ks1-F){EdmXGNq^! zwMu1%;X<$q77ThJnFps`4u?+4Gchs0LAB)3%VIo!h1S-I1*_ZWSG9Q&JT(>Bt_`cI z?V=Oo(n&hesCdU(&kFtX@zt&#pApSXih1LTIV5U>S?dQ^2{Zm1G&JAC(6>t9@e^oR zs2JiwRnG6^x&nx=^{FhP72>5Ox_AOrY*8y1U4D&4ENT?scx{*G$ zS-!g0fZ6SW>tR!~Ni3jzEPE&QE#g?iK^wDo8pIa^egYF z-4yjW^$v_%wjlDe^9K;XOAKMP8`}U^-htwm9WMFXO`$Ru!TV#IOCby%~;^;=3 zyos?eTPPygZiW8Zwm@$R`2c}TkRqT?E{_nTq3pizYN?6K2jR{8n#Qu4Hwx4F2ozUC zJq8tY$oKB$N}I}b@)yYMF@i@4=*VUFbQ(TMa0p>uGlc8x?w6k;2`wu}2$Y_0UA5<1 zsOOK!Lxa7$)5*S(q5Vmvec#t6Papocb}!yxZPK1J=&>xFl1 zrO?zA$a7$OOPy204}GOu?`oOP|Elqf@fGuI#U*d!MS~kfYsx2ECt9a{)7vkXhL1jU zv$*7Y8{aDqjPvWZ%4y4V;x+qChx^3nOQSG0U3A$Ig3p>NELWYIfA8{4x+mNxJ<|=B zU3DZ?qx2^Hr>VRY8T9O}o*bPRJvlbse$C;Ts(`~+YnqjLBhFuQ`KGp39 za)(6kwQ}H!88!gP+CNOK&-7mr~qAByQ?ClOIBwPszQiDc* zZmzv~E9Xbx_(nH>9$w#Azu}vdoTCWJ8|CfUo*+I_f@v*E@aNa`MC5Og#UEVXW27Ps6EJv0Uc7 Date: Sun, 12 Oct 2025 02:15:43 +0800 Subject: [PATCH 08/12] README --- doc/README.md | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 doc/README.md diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..3a931dd --- /dev/null +++ b/doc/README.md @@ -0,0 +1,132 @@ +软1_[彭云昊]_[王祖旺]_结对项目 +中小学数学学习软件 - 结对编程项目 +项目简介 +本项目是一个面向中小学学生的数学学习桌面应用程序,提供个性化的数学题目练习和评估功能。系统根据学生所在学段(小学、初中、高中)生成相应难度的数学题目,通过图形化界面提供友好的学习体验。 +功能特性 +用户管理 +• ✅ 用户注册:通过邮箱验证码完成注册 +• ✅ 密码设置:6-10位,必须包含大小写字母和数字 +• ✅ 用户登录:安全的身份验证机制 +• ✅ 密码修改:支持原密码验证 +• ✅ 账户注销:永久删除账户及所有数据 +题目生成 +• ✅ 小学题目:加减乘除四则运算,支持括号 +• ✅ 初中题目:平方、开根运算 +• ✅ 高中题目:三角函数计算 +• ✅ 智能防重复:确保同一试卷无重复题目 +学习流程 +• ✅ 难度选择:小学/初中/高中三级难度 +• ✅ 题目数量:用户自定义题目数量(10-30题) +• ✅ 选择题形式:每题4个选项,单选作答 +• ✅ 实时评分:提交后立即显示得分情况 +• ✅ 学习延续:支持连续练习或退出选择 +• ✅ 答题进度:支持上一题/下一题导航 +技术栈 +• 编程语言:Python 3.x +• GUI框架:Tkinter(内置Python GUI库) +• 数据存储:JSON文件(无需数据库) +• 邮件服务:SMTP协议(QQ邮箱) +• 架构模式:MVC(模型-视图-控制器) +项目结构 +text +复制 +下载 +软1_[彭云昊]_[王祖旺]_结对项目/ +├── main.py # 程序入口 +├── main_app.py # 主应用模块,界面控制器 +├── user_manager.py # 用户管理模块 +├── question_bank.py # 题库管理模块 +├── question_generator.py # 题目生成器模块 +├── quiz.py # 测验管理模块 +├── users.json # 用户数据文件(运行时生成) +└── README.md # 项目说明文档 +安装与运行 +环境要求 +• Python 3.7 或更高版本 +• 网络连接(用于邮箱验证码发送) +运行步骤 +1. 下载项目文件到本地 +2. 确保所有Python文件在同一目录下 +3. 运行主程序: +bash +复制 +下载 +python main.py +4. 按照界面提示进行注册和登录 +预设测试账号 +系统支持新用户注册,也可使用以下测试账号: +• 邮箱:任意有效邮箱(接收验证码) +• 密码:符合规范的密码(如:Abc123) +分支管理 +本项目遵循Git分支管理规范: +• main分支:稳定版本,存放经过测试的代码 +• develop分支:开发主线,集成最新功能 +• 个人分支:每位开发者的功能分支(如:zhangsan_branch) +代码提交规则 +• 源代码:必须通过个人分支 + Pull Request +• 文档:直接推送到develop分支 +开发规范 +代码规范 +• 遵循PEP 8 Python编码规范 +• 使用类型注解提高代码可读性 +• 模块化设计,高内聚低耦合 +提交信息规范 +• feat: 新功能 +• fix: 修复bug +• docs: 文档更新 +• style: 代码格式调整 +• refactor: 代码重构 +功能模块详解 +1. 用户认证模块 (user_manager.py) +• 邮箱格式验证 +• 密码强度校验 +• 验证码发送与验证 +• 用户数据持久化(JSON文件) +2. 题目生成模块 (question_generator.py) +• 小学题目:2-5个数字的加减乘除运算,确保结果非负 +• 初中题目:平方运算(1-12)、开根运算(完全平方数) +• 高中题目:三角函数(sin/cos/tan)特殊角度计算 +• 选项生成:智能生成4个合理选项,包含正确答案 +3. 界面模块 (main_app.py) +• 响应式图形界面设计 +• 实时输入验证 +• 友好的用户交互反馈 +• 多框架界面切换 +数据存储 +项目使用JSON文件存储用户数据,文件结构如下: +json +复制 +下载 +{ + "user@example.com": { + "username": "张三", + "password_hash": "加密密码", + "registration_code": "注册码", + "is_registered": true + } +} +配置说明 +在 user_manager.py 中配置以下参数: +• 邮箱服务配置(SMTP服务器、端口、授权码) +• 用户数据文件路径 +测试用例 +功能测试 +• 用户注册流程测试 +• 登录验证测试 +• 密码修改测试 +• 题目生成测试(各学段) +• 答题评分测试 +边界测试 +• 密码格式边界测试 +• 题目数量边界测试 +• 邮箱格式验证测试 +已知限制 +• 邮箱服务:依赖QQ邮箱SMTP服务,需配置正确的授权码 +• 题目数量:建议10-30题,过多可能影响性能 +• 网络要求:发送验证码需要网络连接 +• 平台兼容:主要支持Windows,其他平台可能需调整 +开发团队 +• 班级:软1 +• 组长:[彭云昊](202326010111) +• 组员:[王祖旺](202326010117) + -- 2.34.1 From a426194d1f95bdcffe49f14c51edd2bd81d5bdba Mon Sep 17 00:00:00 2001 From: Amnesiac1745 <1426688201@qq.com> Date: Sun, 12 Oct 2025 11:48:28 +0800 Subject: [PATCH 09/12] FIX5 --- doc/README.md | 132 +++ .../backend_service.cpython-311.pyc | Bin 0 -> 9128 bytes src/__pycache__/main_app.cpython-311.pyc | Bin 49470 -> 49025 bytes src/__pycache__/main_app.cpython-313.pyc | Bin 0 -> 47855 bytes src/__pycache__/question_bank.cpython-311.pyc | Bin 2076 -> 2076 bytes src/__pycache__/question_bank.cpython-313.pyc | Bin 0 -> 1831 bytes .../question_generator.cpython-311.pyc | Bin 18700 -> 27957 bytes .../question_generator.cpython-313.pyc | Bin 0 -> 16146 bytes src/__pycache__/quiz.cpython-311.pyc | Bin 4055 -> 7974 bytes src/__pycache__/quiz.cpython-313.pyc | Bin 0 -> 6934 bytes src/__pycache__/user_manager.cpython-311.pyc | Bin 17301 -> 20702 bytes src/__pycache__/user_manager.cpython-313.pyc | Bin 0 -> 15507 bytes src/backend_service.py | 228 +++++ src/main_app.py | 826 +++++++++++------- src/question_generator.py | 597 +++++++++---- src/quiz.py | 98 ++- src/user_manager.py | 220 +++-- src/users.json | 8 +- 18 files changed, 1525 insertions(+), 584 deletions(-) create mode 100644 doc/README.md create mode 100644 src/__pycache__/backend_service.cpython-311.pyc create mode 100644 src/__pycache__/main_app.cpython-313.pyc create mode 100644 src/__pycache__/question_bank.cpython-313.pyc create mode 100644 src/__pycache__/question_generator.cpython-313.pyc create mode 100644 src/__pycache__/quiz.cpython-313.pyc create mode 100644 src/__pycache__/user_manager.cpython-313.pyc create mode 100644 src/backend_service.py diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..e6108d0 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,132 @@ +软1_[彭云昊]_[王祖旺]_结对项目 +中小学数学学习软件 - 结对编程项目 +项目简介 +本项目是一个面向中小学学生的数学学习桌面应用程序,提供个性化的数学题目练习和评估功能。系统根据学生所在学段(小学、初中、高中)生成相应难度的数学题目,通过图形化界面提供友好的学习体验。 +功能特性 +用户管理 +• ✅ 用户注册:通过邮箱验证码完成注册 +• ✅ 密码设置:6-10位,必须包含大小写字母和数字 +• ✅ 用户登录:安全的身份验证机制 +• ✅ 密码修改:支持原密码验证 +• ✅ 账户注销:永久删除账户及所有数据 +题目生成 +• ✅ 小学题目:加减乘除四则运算,支持括号 +• ✅ 初中题目:平方、开根运算 +• ✅ 高中题目:三角函数计算 +• ✅ 智能防重复:确保同一试卷无重复题目 +学习流程 +• ✅ 难度选择:小学/初中/高中三级难度 +• ✅ 题目数量:用户自定义题目数量(10-30题) +• ✅ 选择题形式:每题4个选项,单选作答 +• ✅ 实时评分:提交后立即显示得分情况 +• ✅ 学习延续:支持连续练习或退出选择 +• ✅ 答题进度:支持上一题/下一题导航 +技术栈 +• 编程语言:Python 3.x +• GUI框架:Tkinter(内置Python GUI库) +• 数据存储:JSON文件(无需数据库) +• 邮件服务:SMTP协议(QQ邮箱) +• 架构模式:MVC(模型-视图-控制器) +项目结构 +text +复制 +下载 +软1_[彭云昊]_[王祖旺]_结对项目/ +├── main.py # 程序入口 +├── main_app.py # 主应用模块,界面控制器 +├── user_manager.py # 用户管理模块 +├── question_bank.py # 题库管理模块 +├── question_generator.py # 题目生成器模块 +├── quiz.py # 测验管理模块 +├── users.json # 用户数据文件(运行时生成) +└── README.md # 项目说明文档 +安装与运行 +环境要求 +• Python 3.11.9 或更高版本 +• 网络连接(用于邮箱验证码发送) +运行步骤 +1. 下载项目文件到本地 +2. 确保所有Python文件在同一目录下 +3. 运行主程序: +bash +复制 +下载 +python main.py +4. 按照界面提示进行注册和登录 +预设测试账号 +系统支持新用户注册,也可使用以下测试账号: +• 邮箱:任意有效邮箱(接收验证码) +• 密码:符合规范的密码(如:Abc123) +分支管理 +本项目遵循Git分支管理规范: +• main分支:稳定版本,存放经过测试的代码 +• develop分支:开发主线,集成最新功能 +• 个人分支:每位开发者的功能分支(如:zhangsan_branch) +代码提交规则 +• 源代码:必须通过个人分支 + Pull Request +• 文档:直接推送到develop分支 +开发规范 +代码规范 +• 遵循PEP 8 Python编码规范 +• 使用类型注解提高代码可读性 +• 模块化设计,高内聚低耦合 +提交信息规范 +• feat: 新功能 +• fix: 修复bug +• docs: 文档更新 +• style: 代码格式调整 +• refactor: 代码重构 +功能模块详解 +1. 用户认证模块 (user_manager.py) +• 邮箱格式验证 +• 密码强度校验 +• 验证码发送与验证 +• 用户数据持久化(JSON文件) +2. 题目生成模块 (question_generator.py) +• 小学题目:2-5个数字的加减乘除运算,确保结果非负 +• 初中题目:平方运算(1-12)、开根运算(完全平方数) +• 高中题目:三角函数(sin/cos/tan)特殊角度计算 +• 选项生成:智能生成4个合理选项,包含正确答案 +3. 界面模块 (main_app.py) +• 响应式图形界面设计 +• 实时输入验证 +• 友好的用户交互反馈 +• 多框架界面切换 +数据存储 +项目使用JSON文件存储用户数据,文件结构如下: +json +复制 +下载 +{ + "user@example.com": { + "username": "张三", + "password_hash": "加密密码", + "registration_code": "注册码", + "is_registered": true + } +} +配置说明 +在 user_manager.py 中配置以下参数: +• 邮箱服务配置(SMTP服务器、端口、授权码) +• 用户数据文件路径 +测试用例 +功能测试 +• 用户注册流程测试 +• 登录验证测试 +• 密码修改测试 +• 题目生成测试(各学段) +• 答题评分测试 +边界测试 +• 密码格式边界测试 +• 题目数量边界测试 +• 邮箱格式验证测试 +已知限制 +• 邮箱服务:依赖QQ邮箱SMTP服务,需配置正确的授权码 +• 题目数量:建议10-30题,过多可能影响性能 +• 网络要求:发送验证码需要网络连接 +• 平台兼容:主要支持Windows,其他平台可能需调整 +开发团队 +• 班级:软1 +• 组长:[彭云昊](202326010111) +• 组员:[王祖旺](202326010117) + diff --git a/src/__pycache__/backend_service.cpython-311.pyc b/src/__pycache__/backend_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4839076971a437dca919309289729b199b66f5da GIT binary patch literal 9128 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 literal 0 HcmV?d00001 diff --git a/src/__pycache__/main_app.cpython-311.pyc b/src/__pycache__/main_app.cpython-311.pyc index e3f5ecfef21a85b6e050a7a848d52bacf3006e9f..7d1fa1f38bd12b954d45d98bbdd6d5dc827737a4 100644 GIT binary patch literal 49025 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 literal 0 HcmV?d00001 diff --git a/src/__pycache__/question_bank.cpython-311.pyc b/src/__pycache__/question_bank.cpython-311.pyc index bd940b3b21e8b2caaa0f33a662d99bedceac5b03..c213e92fabbcb374e7e9bea2b2a8d1d34dcc66db 100644 GIT binary patch delta 20 acmbOuFh_uUIWI340}zBuz23+z$N>N_Zvz1^AF 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 literal 18700 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 diff --git a/src/__pycache__/question_generator.cpython-313.pyc b/src/__pycache__/question_generator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e941737dffdf55e5d4e656f9df9371811bff66a GIT binary patch literal 16146 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 axf81@7YMvr=u_UUfM;XH?+Lu&TKZq+!gt94 literal 0 HcmV?d00001 diff --git a/src/__pycache__/quiz.cpython-311.pyc b/src/__pycache__/quiz.cpython-311.pyc index 7fb1b35156f7b97e7b86cb220f4bd9c37d91fed7..7e32e5be47a48015b6c5cc8a3ea3bb869c11bede 100644 GIT binary patch literal 7974 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 TBk=a_fBR18`~CqW{Qmz3%^hnW delta 1517 zcmaJ=U2Gdg5Z=8X`|Q|0JNDU$(>6|O9BhJs+2@ggW5Wv;`%Nk!9Qis z5h!)*riw%!B9Uk-(FYzPBub)y6r>`;3*rG)JODaZ7FhRG2_e)MFls9yo|ru+K?$OJ zd*9B?&dkov%-%ZwbGzpgx7&$e{PN{*Z+_)i@w5yL0rvq?Q3|P8<+3=(rMMiQ;xXg- ztdJ8^B1RlKfmC4usp2gTp$+&OBV}93xBEC#I6bWwO=;{>ak`LCXID0&`1*>`J!w}( z)ix!l_5~p&siNkX;#3J}=QiyCx(R5PCbsVQM4;mYxf|qmpqqg9Z0oy#mQ~vs6nE1N zoWgHV8}~v$7%fB4b@#J ze0Wh<^3-~ctxbKQZw-8Y_4d_Das*_qTC8hPs7i-xN)TA*B76_nIi{`Q&9o7%I7{-Q z^bZ`QKPqE1;qArabkh6sGtS8NAS_*@>jH7XN?87_ut*b7agKPHpcz1CdXWf`=_79f zAD|K6>)1zo0G)rces);2g#uGj!?T;!m2Rm21~s3R0+-OHW&$) zaM4ZcN|b)s;tb@milNr_T^DG^PVcl7a_olpRe-o;I<)+3P9y1}X7Y2>m;Oz*$)_*s zR;n9Sfb&pXv{GlfGX+AlOz}ddFqPA$R=h4=%rZqr4WZgnBCe-m1TMv+u6I-|+X>0v)U4 zl|XFymA~%2c=Fp(Lpi^k`#7@}{WMvL4OC+TmEgeEnTj%Gz&v-Jbze>PFU=WJE7?~A z`h1`XTI7ciOTs8o0QXk#>$c7lud;Ca0j!Sq2C8!6v&?4ncCwNjt|o`S8HSF(D~}j* z`Mc=%-NxC;>RD1fr5l&a#;LL)m90bLUfO-&Sed1QcmYgtDqBbwNs!UP#X=!V+SuMf z1_Fb222V0b0IVa+4Uw6E7109rEOhp)uI+wxjuWLiVjwxh&}wvbuG)FDj)1PCHVbzl zXcq@pwK{@jQ=sDqvlAXMysFd@Ec+p16Nk8)6LrMa$73T>7UXE$PLj0ukV3}}DNkFM zw_-oSF2HJG-RE!Vxm-b=&1%ObO K424|bPbV3(&RE%6mOu%f{Wx;Ki3*Niis!ghW zrAsswM-3z~aY@p+rbaPoMv|K6AGPgtrfgQi&Yg6|X5CWQWm8?nSjD|hOlavE_+Kpe%<+o(obpc@&1 zX=DYqQ6VVIn9gidHmU@b8Rem3#IXg4Q>>=t83c8g(xUR{p{1F4`{>l`$0qzk;TH!q zQ@G#4O6+#0=$6zwd)>|+-sY0n2b^}dq-gQy_EI_>g|avtB{pP+pj zwIhLoP0}1CFdQwg9Mi67S1O>dipCrIE-R=w1<(}jYD|sxC`nHm=^=C&ts*oHr*2ns z3NEdR6~ znk7qOcX^zLrgX4PPbp+kzD-{K?dMmIOI? z_*9ZAN|ti$9zk&M?s%Wo$#*yoND9&3BRDLy#EK4ACs{+p>zU2c!<(wxTSZu=ebs%d z*RF1Vz#)Fm-P7AHx;-6uCh?%?c63+tI{2=G9)6##$GTUr@%DY@vXz#ULG7YoZ{H7c zt?WH0sjaZK(`~g%=~&d2v1TSLD^^2x1VwbHVDT}1sA8pm%Wt@m{I?ztlyCB~$8@2x z^6|3mfwJwvvL@0lUFv18s~3l|bG+;`nmh0{l9YnO?Gbo;RG^raPy{bDPNNlwr$Dfz zYi4FdTUQ{&gD9Kh%8?k+N;k*FouI~4hi6);xs;`qiH$I$9iRkSWJax239?Y0EijAb z=E%DfeMctF^vQysJau-$+aG@6-SEZdr;hYZ4ZQN?Bi^>;-9cz(^x3E9qi7<02pw(;6WIIz@a;Gb91P5eWu7zC;$+{3`b? z{BZ+hM2dL@UR@}!@T~rf-n%uFQ!t)W9>^*8@4jR`Zw=rzr_`8utkaRoJ=c8_l8Q zRI$?}=9uR!V8%P(JUc&{XUv=TJg=IZxBRQl+m;?XDx4f^NlJo~r^R;7X=1g+{&(8$ z!Ef#VaDCkz>whyDB%LO4hI#mlWdJ;$XT(qMDS`i1~v?%+#2^yqzLlDNS%^a zATE7Z+=tHQ^E@2DJi=UHjObxjq*ox31`vjriD%m|PLb5g#!m4Ny#oLWLnyD{S=dc> zY)qnTRm4V{QLBQZIfg|gFs~W@JllyZdJo2a47|yZ8wXq2BZFrY9Co)=Mo)m=H(r?> zx(IQ{#E>@u$TL8g`0A!93_o4+c5ZB#vO{eON1M(`2IxgU|HrP8g<~70Ixrn$O_UVg zFa^9o7KY3sEP=X!Lm-P5T$+vV=;@X;PSMGWZg8^>iE;970h^!1*g85S6&?a4Ng?!j zcxZcgCj-f1zORyKU9Z3wNaJHEYd`=Dt&zbcSl^@(nEgh9HElpF^o$0TvNJOR_5 zhT94Tv()r;CBab@fK5amIe@~bmYQoe7zI!x(uw;M$bhl1@}1_^mA7uacOmQ@zBzIf zZf78d#$h;9>|1lwA+oM%$_eV!t$7V%-_$Wuz^9S0J(7AcFccm{1R3>Mcy zCNnr=Tw5B@mR>OWTP{6*{_&utW@LZBQXkaThjba^x}~4!miku)bme2Zr6FT($Y7ey zU#!DK?;nE7SMisls!xH=&&M;m0z%jsJY%|8i^5Y0(`s&ga0*oS=E&3YF)Y@Z@CYqi zER@VOd{s*3R3rGlSY?Uz_nz|frYwG=3$|ssQR~K9{kXQ@Z zycQ4kf5Tt|6F21z)SamFm5dkH2HnmoHo?~BV0-+snuH6<{EzoXFSA4(a8FHvV;q>w5|1%i2C?^!yjjwT_WT5< zNZe1q3XX|zGQDZCcxq16_}Fpt{Q>wHYesc7Ap`Wy77)Hy65MbDfMI{hQ*CkFz84

>Ys43sGDAMhbC0dEbN`C8eY|KORqG^pAOEjwXHp z&e00*ni%nUY6q;hqbB-jF{e+D->o5nXr<#Lzqak9aAe_1>>V{{ zC2cxdEkTP=8}`Q}sgMoK!HkN~j;9goM72y2>ZIb>3h#s$2g2_UguPECYaAg!1TgvB z`_MCS=~UeJcRlmZw|#eiH(V!&-6gPy1zr^$yr-MIuSv`yXKx}VfRKByNFoAB(dp{3 zxh2iRHkZe-SrFjT0DMCNK7vIdN7?DIDBBL9xm%)~!a_U*M_eK{9~@gp{hh&XbJ;yE z{G>;o=AuDG3iTNN@^#1v#N-!v(?45Mc|jbmx>9$!?!&5UH6PUkS3LCTl3fAaB5y;$ zu&7@R!DrC0C_*bU^Ka%B46Zx7;kgYLT8G#CdQV_Mbujn-k&a;Q#!%i8e`X+W+4JeQ z8I-q)x}Anfn<;o8-%Nd*y4G~9slWSGyT9a8<@w5B>H0v<`VsDv?E2B&kHTY|20-ih zp}s?->ijRKwJ5Ln4x%&j|7yyMD4_hei1p&#Wg9K@#}>`TwTh3|P>|34NGJQ0xiS7; zb`;aEXk4=uK}3_Rp>QMohTz80+bIAy#)E?l&BQB5CeHpU`kGHdoA8x4!xxXilUmeV7M?`KeU1((g8)|&#Rs2=b&wHJXPCwf%L0aFNulG14S~wd z0mJ5T!{OhT{JH!O<)e?ZjXxp;;MXXQ{_yZ<DVS-kmkJy4_SS4VJAFE_l&A|_| zysg^-53?GpwY#Uo-0@iY#A?fAP?xxSbU-Whn}L+0zJ$NSRVmPY<@-MYxprk9XZ< zNfRD{Qc}7P_B#14nTLcc7!?HUf~Bn3yKL-T7XXH^nH>HxJPx8vBwtC?HL~35ggrpR nzDHaPS%je|>b9Du*t=>(tq7vZzaYz9YRWr-l( literal 0 HcmV?d00001 diff --git a/src/__pycache__/user_manager.cpython-311.pyc b/src/__pycache__/user_manager.cpython-311.pyc index 04ceb360f267619f5c0fb582a2d91623b5740e8f..e5767b96ab071b2f6428952dd46d4292e3a2448a 100644 GIT binary patch delta 7912 zcmb7J4Nz29mVU4QyXilm>86`*X!*4fK}6(F6v2s;j6tJhlTb0F+kzkA=g=iYnnednBe?(xKr*)LABC1-Uy4FgZT<99oMS;sKH#h2(Kk5ukY9V=<^ z^{Cs`EW@#!tXtEQ)}BV|@@{R9u3bm#if(<6q2179Y&X((Wp{c{MtcU!NSO5ur#i@R z>N66C`4E2M)1FCNG|-a9tIkNF4Sta}6K&H%n~pa@TV`To7AM)lxb(+<<1|Y`(zVSk zA-#K>$1Auyy*<2lXCHU$zqXi{!AK_=o@r+}rd`6Z?NT+PW+pbXzh#X&XcP1Z1_UGd zo^;WC;vT7AXM=W6Ex>*zY$9p0#xyPV5P@uw)jG}aevT0=NX|k?ivqrRwvrEJRv)us zWw7OYVX0nT8D;=RI%zSsg%&|)0c{F4HpN+JOA3gy-~^rUi#^In?Y@LfT{AD^B)r_m zNqIRZ;}zQ#ocy5d4Df##>)oytnK(s^M>&s272#3Ec+~TF)WC!D#CbFk9$$Pi4KSqv zlQuSxZr(s?z?04y=FzqDgfPzI(Q$gvjf_y{+K5Ix>){38)9zmP4qoVQ1VJ=TcxDe@ zz4!6yyT`|7{g>{JpT0Z%`rVH|`T7?}X2##XbNNsb{q2LZZ@fSA=6kfC5L2CyLg2mq zLT_hGX<=;Ge+J+0D*#S{xqvK^egm$5c4BPi^&_)yT@(jz6&yfxNd*%W@lie48|0&^ z6w(R&jxLXv7drZ36$6VB*A!V&c@hgM{e3k6sH=ie>-;jRGTTD3&OVNR98sa~mmeIt zcj162@7>V@3rJcBT*;DVlk?@-nt`&YgyeL2p|2X)gaQP#j*x1%+vC~WCvcA;M+S7b zH~#6}tK;H!&0KgrCd#?QO)CM$WKpyp-rIpw`EafQXH+Cqgy^P4S&*Y11&m4g7_y=9 zM>OOopUhl6k-S0Ftx&1zlD?(9yKhHV?|^k7acI+;xE$oW>N4NLx`Z2Z=i2z}@yn0i zn58&UIY9Ep7&>>ldw1}3Z|K$(EF?>miImqVee2>fL9MkDWr{uv%a^=4KfWSv&N-Iq zxd;11bXx(7?(6ps*cK8(YcOZWuDL4{p)cIPIgg7S=oIq%G>4j`;up;0oA><&)5bOx}p@e*dVuVtJGK5Nm z6$mR4&aHmmmii@<9PdE8^z3^OvsqkDwG20$%s<9TzHtC_ob?^L6 zz6Dkl=X%5f#8EF+iG#0*^V3>VClr=a#Seko-wD6iQ-$svpZjf~!O2ktmj{QQY`cV$ z9hA4rIVT5)XS;%Pk`40beCcpv$$32-yBR<=aCk(DG!@V?fo7yMJn`f_(o8^0PoZT2 zEhD9`8EBc5rjPZt0L=uaD~^@Lo2^W{C^f@umvPz9XrYbq4pyLLb2jLZ6C1=1G%GB_ z0i`Wvf?S~Ga0)oo7ERPDrdYOts8ugn=n>Wcb9pehiyU5pnY^a5vM-Pk)$?i^9XX+K z;*Y8^$r9=#X$&RCU(n!^#Sg(qR0dQbll*I%4W{`j&0)m2CnVj$dxcDFT1_n4QUlH% zVz-B6oZIUbbYzp(>7!v7x~>6pqSNp0b93|{^>jg-=Ky@l{meHiMyo$k^XBTqt49h? zl~1R+0%@)=V^llC5`)fpE6a-Hv4TnER94e;R#PCWDa>foE3dD;m6La>ZuFU-tU9%7 zcwMk;*@dS*cMPR$bgYCuJ+@9xwo5wZdteV*nl?jL5=z zro86D)(^H$t_e0Y0b_YH3!H!>vOsp}?XqPfvQwJT>gjCPRJJReg+t3>ekufoITYss zLYx+>tw<$4eE2ZdnA%1g9)4pZH+7ZDJlq1tIAPE`Bo6w)V&97He~8^HtjF*0ee$Gn zqiZ!*Hz6#*D?E)2s3Aft`OKKP21AsP+OxB7FE0pv0`-_Kg%}Gxi)=K^dJb!C$jb5_ zG(iZA;1RM&W_k(hn`lUXmt}Qim&r=*XZ+Dl)et*|l)K$th?;uiKA)tSD%igw4W?2B z)kQt_(li4zMrmG_OxN4Un8_&v-IkMYO>a5cTx>|%&G)(_qFA`Bn0}MrWK}CpA*G6_ z&C3;rOutvKkmt?6DnaKDv7x({>+1r(b((rm3}Ex3Y-W7e-&Hj51AG_k)VS97X>JeTug z{$IdIAiuK9)4zK+Z1E6I9~}hh!hX_cuTJ|1tR6r}?Z$QO=<+_ITfwj5-Jk{=vt7s*QNSV>FK0ER(&f;9k;tkcoNFC4eC&D(P*z$%mXt2?h2*=t-Cey@ z%fUUN0t%=xAq8lcyK|?2H{|Gwslwv=LdqWAV*!-mbph4!+0T)jd$0=&d?@k7WE87}C=X0PZV;$#fp8I9}mb25q?k+Y-NWSaHY@ z%yvLUc1Ra=)I&u*t+It>I-@qMVRG}sX;@>T;FpDS8MX0E{bBuyS4U-|JI3}-TWSIp zQ_ZxhHlV7VQq=|xX1|`EjrjGHWAqSIlT8)2OmNc93CZ&eX}pIV!(%?{>sho&W6`guuSi&pygS~=7^rQ|bHOVxh? z28|iR?0frXfBL?7iI^R^Ff)AS?nh^4hW(Ly>R+D*djm^*fplc43LeD}8%@|s>hkl7 z^I?X_y=3_4mnW{xTsbpy<@lY+D^*LEFS|4O!OZ!SFbg@AU+HUese~QCF5p=pyo`V< zEzpAm5BQK`kGs2{_s|0c>@Vit#S;b(2M?+h?x1*GF6`*s$#?FeXMBo%b%Q_K*45qR z-8Zm|T3M}_5h#D0E-dJ~=l3ube31-NQMggFaay}6pxrd3-9-F_N7F?24-Ap5e+9Na z&z5Q#9>_v%VFsB|Hmb07LsD-v%?7gRb7~eN;1& zoNR1hfvP~Vf2B9B0c)ZoO-hjDv3~~N^50+{v@XVIx@oArVW^$Vx~`fwJQ*-N8D>~8 zxyf3hbO?$%)d&9K7F<@_;Qdw^qGDnKW76*%K!cx{)H^%@#tY)n9%WjiZMFntn zyj)bdGHA>W8lRh!OLQ+lD2E7l3q%#8OMi3YVNf1NKUS4b`e^{F9!)^NpBQMdc?o1w?IX^OUYV3)AykN$XG(ev{|K8ox<85Fm)P&+@ zg&!fwehPsgZeEi8(2MFoVdCEHA>-v1$tsroV|hM#uFgpQyS$bLP@=r-22k`|Vqaz; z#+n?`vC>TJ4edm>tc!f>)D`?m6+(pro$)LiWw#PjWs6TZ3u3hie*@T`3m~R^#3azH ze|YBbPa!{d@4}_KH0k0}3x{zT@nzCg??D<3RMCZq3iUqJ&L#Sf5^PHKAK;7VC4C?} zaRU+)MH|r#OhVt7>J;5D6pikjHdF)*6=6oEw%*d316n6lfDNbstyBS8sR95h7zkDE z%eU3vQe^~GIV0bnDt=;GwLYL)Kc!k9jC&CYOcH(Y6vf~D7XeN(WS725o<1ZQl#tuY zvuY)skn`Qb9Lb2J8eapZm{Xn|y*zs~nm`*q3hsXP(5IpvfoHdp-!CfiMZ7hQp=SMW zwf6P$vHq=K%XC*y!>FPgj-=ku219IrJif+vbI10sUh#(2v27pq!oBVuejq<_LsNL3 zhf@)mcH-Wyg;8K{^-PBOxb0?o{*Cnf>GZ-tI;J_)Rto;LL4)zgz{vBk^HYYRpe1`) zh5y1*##nqizJt`9STm^SQu&Z{P`aO_*JKlGm5r5>Kd)Fso~^R!Q2FUYz1e3CE#z92 zomG(ks5+-WvlGsezg$u2Qwks9OI*>YW>FoZ+O`H-qvl0@(}L;+BVe#Fke;|1H1Frd zrN0AhF|{haVJID|oi?lp7*_ZdvfF6}|0?oyosGOzz1?U*VU{A41BiAwv3zBo1k7l2 zZGRn#lOSwdPHHaH!!84sq)$h4QY5?1p*V`d{4;5&+f$sZ&KpskpP}mf3{_`9s?NWz ztAtC(cXjO>>C9C)HVnduXEIH64P>M&8uET;aUyhKuxb_KSmc)lR95h@@R#5aC$82D z67ge95!&~2x82ZF8dBK!;?AE6kb6ak#& z>d1je^$oS<|Hu`>|JX4CLL2-%pObgi)R|0jxV0xk8M(KnK0i%v8iSl36c@psphyK9 zS=Yq-a^;qb(l7(XBxKH@2rjM;OQEDM3DCr5$*mVV!weMHGimWpFebdELb+XTnuMvL z2-ehxrC6?Mp(Q0Iz$#ac+=@v~C@{$h1tvM6NQHXxU6adak()442?Zu9p};&P6sfRW z1FQEyP6ilXy)OItYqXjQ+0YX+m{4E_6AH{=E;L*=aI}P(d?+z(2?eGtp-6=l`fBZ|fA$=hJj@$Utw05V8|0PB~W|% zyS-hz-GY~1QbP*wzTI8DI|L;T0eaN}ms0O8NJBs-fVwdnc43y23YC(A{#LcBr;qFJ z=2r`sfCX*YvxX%nntxy}m|_fnU3g(ddAl|f@ Z9g?%Kp9|wuM$sH?dLS~G=kZZ5^1of3syYAw delta 4835 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 UXq6MN=a1tZCcpO+qb#ca58yw6hX4Qo diff --git a/src/__pycache__/user_manager.cpython-313.pyc b/src/__pycache__/user_manager.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..273925cf962736fe372ef12280899774f83cbfc6 GIT binary patch literal 15507 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 literal 0 HcmV?d00001 diff --git a/src/backend_service.py b/src/backend_service.py new file mode 100644 index 0000000..3268f29 --- /dev/null +++ b/src/backend_service.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +后端服务模块,作为前端与后端之间的通信中介 +""" + +from typing import List, Optional, Dict, Any + +from user_manager import UserManager +from question_bank import QuestionBank +from quiz import Quiz + + +class BackendService: + """ + 后端服务类,作为前端与实际业务逻辑模块之间的通信中介 + """ + + def __init__(self): + """ + 初始化后端服务 + """ + self.user_manager = UserManager() + self.question_bank = QuestionBank() + self.current_quiz: Optional[Quiz] = None + + def login(self, username: str, password: str) -> bool: + """ + 用户登录 + + @param username: 用户名 + @param password: 密码 + @return: 登录是否成功 + """ + return self.user_manager.login(username, password) + + def register_user(self, email: str, username: str) -> bool: + """ + 注册新用户 + + @param email: 邮箱 + @param username: 用户名 + @return: 是否成功发送注册码 + """ + return self.user_manager.register_user(email, username) + + def verify_registration_code(self, email: str, code: str) -> bool: + """ + 验证注册码 + + @param email: 邮箱 + @param code: 注册码 + @return: 验证是否成功 + """ + return self.user_manager.verify_registration_code(email, code) + + def set_password(self, email: str, password: str) -> bool: + """ + 设置密码 + + @param email: 邮箱 + @param password: 密码 + @return: 是否设置成功 + """ + return self.user_manager.set_password(email, password) + + def change_password(self, old_password: str, new_password: str) -> bool: + """ + 修改密码 + + @param old_password: 原密码 + @param new_password: 新密码 + @return: 是否修改成功 + """ + return self.user_manager.change_password(old_password, new_password) + + def logout(self): + """ + 退出登录 + """ + self.user_manager.logout() + + def delete_account(self, email: str, password: str) -> bool: + """ + 删除账户 + + @param email: 邮箱 + @param password: 密码 + @return: 是否删除成功 + """ + return self.user_manager.delete_account(email, password) + + def start_quiz(self, level: str, question_count: int) -> bool: + """ + 开始测验 + + @param level: 题目难度等级 + @param question_count: 题目数量 + @return: 是否成功开始测验 + """ + try: + questions = self.question_bank.generate_questions(level, question_count) + self.current_quiz = Quiz(questions) + return True + except Exception: + return False + + def get_current_question(self): + """ + 获取当前题目 + + @return: 当前题目 + """ + if self.current_quiz: + return self.current_quiz.get_current_question() + return None + + def get_current_options(self) -> List[str]: + """ + 获取当前题目的选项 + + @return: 选项列表 + """ + if self.current_quiz: + return self.current_quiz.get_current_options() + return [] + + def answer_question(self, answer: str) -> bool: + """ + 回答当前题目 + + @param answer: 答案 + @return: 是否回答成功 + """ + if self.current_quiz: + try: + self.current_quiz.answer_question(answer) + return True + except Exception: + return False + return False + + def next_question(self) -> bool: + """ + 跳转到下一题 + + @return: 是否成功跳转 + """ + if self.current_quiz: + return self.current_quiz.next_question() + return False + + def previous_question(self) -> bool: + """ + 跳转到上一题 + + @return: 是否成功跳转 + """ + if self.current_quiz: + return self.current_quiz.previous_question() + return False + + def is_quiz_finished(self) -> bool: + """ + 检查测验是否已完成 + + @return: 测验是否已完成 + """ + if self.current_quiz: + return self.current_quiz.is_finished() + return True + + def calculate_score(self) -> float: + """ + 计算测验得分 + + @return: 得分百分比 + """ + if self.current_quiz: + return self.current_quiz.calculate_score() + return 0.0 + + def get_quiz_progress(self) -> Dict[str, Any]: + """ + 获取测验进度信息 + + @return: 进度信息字典 + """ + if self.current_quiz: + return { + "current_index": self.current_quiz.current_question_index, + "total_questions": len(self.current_quiz.questions), + "current_answer": self.current_quiz.answers[ + self.current_quiz.current_question_index] if self.current_quiz.answers else None + } + return { + "current_index": 0, + "total_questions": 0, + "current_answer": None + } + + def get_quiz_result_details(self) -> Dict[str, Any]: + """ + 获取测验结果详情 + + @return: 结果详情字典 + """ + if not self.current_quiz: + return {} + + correct_count = 0 + for q, a in zip(self.current_quiz.questions, self.current_quiz.answers): + if a is not None: + try: + # 将字符串答案转换为浮点数进行比较 + if abs(q.answer - float(a)) < 1e-6: + correct_count += 1 + except ValueError: + # 如果转换失败,说明答案无效,不计入正确答案 + pass + + total_count = len(self.current_quiz.questions) + + return { + "correct_count": correct_count, + "total_count": total_count + } \ No newline at end of file diff --git a/src/main_app.py b/src/main_app.py index 99591b2..69f39a1 100644 --- a/src/main_app.py +++ b/src/main_app.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +# !/usr/bin/env python3 # -*- coding: utf-8 -*- """ @@ -10,11 +10,9 @@ from tkinter import messagebox, ttk import random from typing import List, Optional -from user_manager import UserManager -from question_bank import QuestionBank -from quiz import Quiz +from backend_service import BackendService from question_generator import Expression - +from quiz import Quiz class MathQuizApp: """ @@ -24,7 +22,7 @@ class MathQuizApp: def __init__(self, root: tk.Tk): """ 初始化应用程序 - + @param root: Tk根窗口 """ self.root = root @@ -32,14 +30,13 @@ class MathQuizApp: self.root.geometry("700x600") self.root.resizable(True, True) self.root.configure(bg="#f0f0f0") - + # 初始化系统组件 - self.user_manager = UserManager() - self.question_bank = QuestionBank() - + self.backend_service = BackendService() + # 设置样式 self.setup_styles() - + # 创建不同的界面框架 self.login_frame = tk.Frame(self.root, bg="#f0f0f0") self.register_frame = tk.Frame(self.root, bg="#f0f0f0") @@ -49,13 +46,13 @@ class MathQuizApp: self.quiz_frame = tk.Frame(self.root, bg="#f0f0f0") self.result_frame = tk.Frame(self.root, bg="#f0f0f0") self.change_password_frame = tk.Frame(self.root, bg="#f0f0f0") - self.delete_account_frame = tk.Frame(self.root, bg="#f0f0f0") # 添加删除账户框架 - + self.delete_account_frame = tk.Frame(self.root, bg="#f0f0f0") + # 初始化应用状态 self.current_quiz: Optional[Quiz] = None self.current_level = "" self.question_count = 0 - + # 创建界面 self.create_widgets() self.show_login_frame() @@ -68,7 +65,7 @@ class MathQuizApp: self.header_font = ("Arial", 16, "bold") self.normal_font = ("Arial", 12) self.button_font = ("Arial", 11, "bold") - + # 定义颜色方案 self.primary_color = "#4a6fa5" self.secondary_color = "#6b8cbc" @@ -92,20 +89,23 @@ class MathQuizApp: self.quiz_frame = tk.Frame(self.root, bg="#f0f0f0") self.result_frame = tk.Frame(self.root, bg="#f0f0f0") self.change_password_frame = tk.Frame(self.root, bg="#f0f0f0") - self.delete_account_frame = tk.Frame(self.root, bg="#f0f0f0") # 添加删除账户框架 + self.delete_account_frame = tk.Frame(self.root, bg="#f0f0f0") def show_frame(self, frame: tk.Frame): """ 显示指定的界面框架 - + @param frame: 要显示的框架 """ # 隐藏所有框架 - for f in [self.login_frame, self.register_frame, self.set_password_frame, - self.main_menu_frame, self.quiz_setup_frame, self.quiz_frame, - self.result_frame, self.change_password_frame, self.delete_account_frame]: + for f in [self.login_frame, self.register_frame, + self.set_password_frame, + self.main_menu_frame, self.quiz_setup_frame, + self.quiz_frame, + self.result_frame, self.change_password_frame, + self.delete_account_frame]: f.pack_forget() - + # 显示指定框架 frame.pack(fill="both", expand=True) @@ -116,36 +116,56 @@ class MathQuizApp: # 清除之前的内容 for widget in self.login_frame.winfo_children(): widget.destroy() - + # 创建登录界面 - main_frame = tk.Frame(self.login_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame = tk.Frame(self.login_frame, bg="#ffffff", + relief=tk.RAISED, bd=2) main_frame.pack(pady=50, padx=50, fill="both", expand=True) - + title_frame = tk.Frame(main_frame, bg=self.primary_color) title_frame.pack(fill="x", pady=(0, 30)) - tk.Label(title_frame, text="用户登录", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) - + tk.Label(title_frame, text="用户登录", font=self.title_font, + bg=self.primary_color, fg="white").pack(pady=20) + form_frame = tk.Frame(main_frame, bg="#ffffff") form_frame.pack(pady=10) - - tk.Label(form_frame, text="用户名:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") - self.login_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="用户名:", font=self.normal_font, + bg="#ffffff").pack(pady=5, anchor="w") + self.login_email_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + relief=tk.FLAT, bd=5, + bg="#f0f0f0") self.login_email_entry.pack(pady=5) - - tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") - self.login_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="密码:", font=self.normal_font, + bg="#ffffff").pack(pady=5, anchor="w") + self.login_password_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + show="*", relief=tk.FLAT, + bd=5, bg="#f0f0f0") self.login_password_entry.pack(pady=5) - + button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=20) - - tk.Button(button_frame, text="登录", command=self.login, width=20, bg=self.primary_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) - tk.Button(button_frame, text="注册新用户", command=self.show_register_frame, width=20, bg=self.secondary_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) - tk.Button(button_frame, text="注销账户", command=self.show_delete_account_frame, width=20, bg=self.danger_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) - + + tk.Button(button_frame, text="登录", command=self.login, width=20, + bg=self.primary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="注册新用户", + command=self.show_register_frame, width=20, + bg=self.secondary_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=5) + tk.Button(button_frame, text="注销账户", + command=self.show_delete_account_frame, width=20, + bg=self.danger_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=5) + self.show_frame(self.login_frame) def login(self): @@ -154,15 +174,15 @@ class MathQuizApp: """ username = self.login_email_entry.get().strip() password = self.login_password_entry.get() - + if not username or not password: messagebox.showerror("错误", "请输入用户名和密码") return - - if self.user_manager.login(username, password): + + if self.backend_service.login(username, password): messagebox.showinfo("成功", "登录成功") self.show_main_menu_frame() - # 错误信息在user_manager.login中已经显示 + # 错误信息在backend_service.login中已经显示 def show_register_frame(self): """ @@ -171,41 +191,65 @@ class MathQuizApp: # 清除之前的内容 for widget in self.register_frame.winfo_children(): widget.destroy() - + # 创建注册界面 - main_frame = tk.Frame(self.register_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame = tk.Frame(self.register_frame, bg="#ffffff", + relief=tk.RAISED, bd=2) main_frame.pack(pady=30, padx=50, fill="both", expand=True) - + title_frame = tk.Frame(main_frame, bg=self.primary_color) title_frame.pack(fill="x", pady=(0, 20)) - tk.Label(title_frame, text="用户注册", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) - + tk.Label(title_frame, text="用户注册", font=self.title_font, + bg=self.primary_color, fg="white").pack(pady=20) + form_frame = tk.Frame(main_frame, bg="#ffffff") form_frame.pack(pady=10) - - tk.Label(form_frame, text="用户名:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") - self.register_username_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="用户名:", font=self.normal_font, + bg="#ffffff").pack(pady=5, anchor="w") + self.register_username_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + relief=tk.FLAT, bd=5, + bg="#f0f0f0") self.register_username_entry.pack(pady=5) - - tk.Label(form_frame, text="邮箱:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") - self.register_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="邮箱:", font=self.normal_font, + bg="#ffffff").pack(pady=5, anchor="w") + self.register_email_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + relief=tk.FLAT, bd=5, + bg="#f0f0f0") self.register_email_entry.pack(pady=5) - - tk.Button(form_frame, text="获取注册码", command=self.send_registration_code, width=20, bg=self.primary_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) - - tk.Label(form_frame, text="注册码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") - self.registration_code_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Button(form_frame, text="获取注册码", + command=self.send_registration_code, width=20, + bg=self.primary_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=10) + + tk.Label(form_frame, text="注册码:", font=self.normal_font, + bg="#ffffff").pack(pady=5, anchor="w") + self.registration_code_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + relief=tk.FLAT, bd=5, + bg="#f0f0f0") self.registration_code_entry.pack(pady=5) - + button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=20) - - tk.Button(button_frame, text="验证注册码", command=self.verify_registration_code, width=20, bg=self.success_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) - tk.Button(button_frame, text="返回登录", command=self.show_login_frame, width=20, bg="#cccccc", fg=self.dark_text, - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) - + + tk.Button(button_frame, text="验证注册码", + command=self.verify_registration_code, width=20, + bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回登录", command=self.show_login_frame, + width=20, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=5) + self.show_frame(self.register_frame) def send_registration_code(self): @@ -214,13 +258,13 @@ class MathQuizApp: """ username = self.register_username_entry.get().strip() email = self.register_email_entry.get().strip() - + if not username or not email: messagebox.showerror("错误", "请输入用户名和邮箱") return - - if self.user_manager.register_user(email, username): - messagebox.showinfo("成功", "注册码已发送,请查收") + + # 注意:register_user 方法内部已经包含了消息提示,不需要额外的消息框 + self.backend_service.register_user(email, username) def verify_registration_code(self): """ @@ -228,76 +272,95 @@ class MathQuizApp: """ email = self.register_email_entry.get().strip() code = self.registration_code_entry.get().strip() - + if not email or not code: messagebox.showerror("错误", "请输入邮箱和注册码") return - - if self.user_manager.verify_registration_code(email, code): + + if self.backend_service.verify_registration_code(email, code): messagebox.showinfo("成功", "注册码验证成功,请设置密码") self.show_set_password_frame(email) def show_set_password_frame(self, email: str): """ 显示设置密码界面 - + @param email: 用户邮箱 """ # 清除之前的内容 for widget in self.set_password_frame.winfo_children(): widget.destroy() - + # 创建设置密码界面 - main_frame = tk.Frame(self.set_password_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame = tk.Frame(self.set_password_frame, bg="#ffffff", + relief=tk.RAISED, bd=2) main_frame.pack(pady=30, padx=50, fill="both", expand=True) - + title_frame = tk.Frame(main_frame, bg=self.primary_color) title_frame.pack(fill="x", pady=(0, 20)) - tk.Label(title_frame, text="设置密码", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) - + tk.Label(title_frame, text="设置密码", font=self.title_font, + bg=self.primary_color, fg="white").pack(pady=20) + form_frame = tk.Frame(main_frame, bg="#ffffff") form_frame.pack(pady=10) - - tk.Label(form_frame, text="邮箱: " + email, font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") - - tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff", fg=self.primary_color).pack(pady=(10, 5), anchor="w") - tk.Label(form_frame, text="(6-10位,必须包含大小写字母和数字)", font=("Arial", 10), bg="#ffffff", fg="gray").pack(pady=5, anchor="w") - self.set_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="邮箱: " + email, font=self.normal_font, + bg="#ffffff").pack(pady=5, anchor="w") + + tk.Label(form_frame, text="密码:", font=self.normal_font, + bg="#ffffff", fg=self.primary_color).pack( + pady=(10, 5), anchor="w") + tk.Label(form_frame, text="(6-10位,必须包含大小写字母和数字)", + font=("Arial", 10), bg="#ffffff", + fg="gray").pack(pady=5, anchor="w") + self.set_password_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + show="*", relief=tk.FLAT, bd=5, + bg="#f0f0f0") self.set_password_entry.pack(pady=5) - - tk.Label(form_frame, text="确认密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") - self.confirm_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="确认密码:", font=self.normal_font, + bg="#ffffff").pack(pady=5, anchor="w") + self.confirm_password_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + show="*", relief=tk.FLAT, + bd=5, bg="#f0f0f0") self.confirm_password_entry.pack(pady=5) - + button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=20) - - tk.Button(button_frame, text="设置密码", command=lambda: self.set_password(email), width=20, bg=self.success_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) - tk.Button(button_frame, text="返回登录", command=self.show_login_frame, width=20, bg="#cccccc", fg=self.dark_text, - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) - + + tk.Button(button_frame, text="设置密码", + command=lambda: self.set_password(email), width=20, + bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回登录", command=self.show_login_frame, + width=20, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=5) + self.show_frame(self.set_password_frame) def set_password(self, email: str): """ 设置密码 - + @param email: 用户邮箱 """ password = self.set_password_entry.get() confirm_password = self.confirm_password_entry.get() - + if not password or not confirm_password: messagebox.showerror("错误", "请输入密码和确认密码") return - + if password != confirm_password: messagebox.showerror("错误", "两次输入的密码不一致") return - - if self.user_manager.set_password(email, password): - messagebox.showinfo("成功", "密码设置成功,请登录") + + if self.backend_service.set_password(email, password): self.show_login_frame() def show_main_menu_frame(self): @@ -307,74 +370,106 @@ class MathQuizApp: # 清除之前的内容 for widget in self.main_menu_frame.winfo_children(): widget.destroy() - + # 创建主菜单界面 - main_frame = tk.Frame(self.main_menu_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame = tk.Frame(self.main_menu_frame, bg="#ffffff", + relief=tk.RAISED, bd=2) main_frame.pack(pady=30, padx=50, fill="both", expand=True) - + title_frame = tk.Frame(main_frame, bg=self.primary_color) title_frame.pack(fill="x", pady=(0, 20)) - tk.Label(title_frame, text="主菜单", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) - - user_email = self.user_manager.current_user.email if self.user_manager.current_user else "未知用户" - tk.Label(main_frame, text=f"欢迎, {user_email}!", font=self.header_font, bg="#ffffff").pack(pady=10) - + tk.Label(title_frame, text="主菜单", font=self.title_font, + bg=self.primary_color, fg="white").pack(pady=20) + + username = (self.backend_service.user_manager.current_user.username + if self.backend_service.user_manager.current_user else "未知用户") + tk.Label(main_frame, text=f"欢迎, {username}!", + font=self.header_font, bg="#ffffff").pack(pady=10) + button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=20) - - tk.Button(button_frame, text="小学题目", command=lambda: self.show_quiz_setup_frame("elementary"), - width=20, height=2, bg="#4CAF50", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack(pady=10) - tk.Button(button_frame, text="初中题目", command=lambda: self.show_quiz_setup_frame("middle"), - width=20, height=2, bg="#2196F3", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack(pady=10) - tk.Button(button_frame, text="高中题目", command=lambda: self.show_quiz_setup_frame("high"), - width=20, height=2, bg="#FF9800", fg="white", font=self.button_font, relief=tk.FLAT, bd=0).pack(pady=10) - - tk.Button(button_frame, text="修改密码", command=self.show_change_password_frame, - width=20, bg=self.secondary_color, fg="white", font=self.button_font, relief=tk.FLAT, bd=0, pady=5).pack(pady=10) - tk.Button(button_frame, text="退出登录", command=self.logout, width=20, bg=self.danger_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, pady=5).pack(pady=5) - + + tk.Button(button_frame, text="小学题目", + command=lambda: self.show_quiz_setup_frame("elementary"), + width=20, height=2, bg="#4CAF50", fg="white", + font=self.button_font, relief=tk.FLAT, bd=0).pack( + pady=10) + tk.Button(button_frame, text="初中题目", + command=lambda: self.show_quiz_setup_frame("middle"), + width=20, height=2, bg="#2196F3", fg="white", + font=self.button_font, relief=tk.FLAT, bd=0).pack( + pady=10) + tk.Button(button_frame, text="高中题目", + command=lambda: self.show_quiz_setup_frame("high"), + width=20, height=2, bg="#FF9800", fg="white", + font=self.button_font, relief=tk.FLAT, bd=0).pack( + pady=10) + + tk.Button(button_frame, text="修改密码", + command=self.show_change_password_frame, + width=20, bg=self.secondary_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + pady=5).pack(pady=10) + tk.Button(button_frame, text="退出登录", command=self.logout, + width=20, bg=self.danger_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + pady=5).pack(pady=5) + self.show_frame(self.main_menu_frame) def show_quiz_setup_frame(self, level: str): """ 显示测验设置界面 - + @param level: 题目难度 """ self.current_level = level - + # 清除之前的内容 for widget in self.quiz_setup_frame.winfo_children(): widget.destroy() - + # 创建测验设置界面 - main_frame = tk.Frame(self.quiz_setup_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame = tk.Frame(self.quiz_setup_frame, bg="#ffffff", + relief=tk.RAISED, bd=2) main_frame.pack(pady=30, padx=50, fill="both", expand=True) - + level_names = {"elementary": "小学", "middle": "初中", "high": "高中"} - level_colors = {"elementary": "#4CAF50", "middle": "#2196F3", "high": "#FF9800"} - + level_colors = {"elementary": "#4CAF50", "middle": "#2196F3", + "high": "#FF9800"} + title_frame = tk.Frame(main_frame, bg=level_colors[level]) title_frame.pack(fill="x", pady=(0, 20)) - tk.Label(title_frame, text=f"{level_names[level]}数学题目", font=self.title_font, bg=level_colors[level], fg="white").pack(pady=20) - + tk.Label(title_frame, text=f"{level_names[level]}数学题目", + font=self.title_font, bg=level_colors[level], + fg="white").pack(pady=20) + form_frame = tk.Frame(main_frame, bg="#ffffff") form_frame.pack(pady=20) - - tk.Label(form_frame, text="请输入题目数量 (1-20):", font=self.normal_font, bg="#ffffff").pack(pady=10) - self.question_count_entry = tk.Entry(form_frame, width=20, font=self.normal_font, justify="center", relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="请输入题目数量 (10-30):", + font=self.normal_font, bg="#ffffff").pack(pady=10) + self.question_count_entry = tk.Entry(form_frame, width=20, + font=self.normal_font, + justify="center", + relief=tk.FLAT, bd=5, + bg="#f0f0f0") self.question_count_entry.pack(pady=5) self.question_count_entry.insert(0, "10") # 默认10题 - + button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=20) - - tk.Button(button_frame, text="开始答题", command=self.start_quiz, width=20, bg=self.success_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) - tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20, bg="#cccccc", fg=self.dark_text, - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) - + + tk.Button(button_frame, text="开始答题", command=self.start_quiz, + width=20, bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回主菜单", + command=self.show_main_menu_frame, width=20, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=5) + self.show_frame(self.quiz_setup_frame) def start_quiz(self): @@ -383,120 +478,159 @@ class MathQuizApp: """ try: count = int(self.question_count_entry.get()) - if not (1 <= count <= 20): - messagebox.showerror("错误", "题目数量必须在1-20之间") + if not (10 <= count <= 30): + messagebox.showerror("错误", "题目数量必须在10-30之间") return except ValueError: messagebox.showerror("错误", "请输入有效的数字") return - + self.question_count = count - + # 生成题目 - try: - questions = self.question_bank.generate_questions(self.current_level, self.question_count) - self.current_quiz = Quiz(questions) + if self.backend_service.start_quiz(self.current_level, self.question_count): self.show_quiz_frame() - except Exception as e: - messagebox.showerror("错误", f"生成题目时出错: {str(e)}") + else: + messagebox.showerror("错误", "生成题目时出错") def show_quiz_frame(self): """ 显示答题界面 """ - if not self.current_quiz: + current_question = self.backend_service.get_current_question() + if not current_question: return - + # 清除之前的内容 for widget in self.quiz_frame.winfo_children(): widget.destroy() - + # 创建答题界面 - current_question = self.current_quiz.get_current_question() - if not current_question: - return - - main_frame = tk.Frame(self.quiz_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame = tk.Frame(self.quiz_frame, bg="#ffffff", + relief=tk.RAISED, bd=2) main_frame.pack(pady=20, padx=30, fill="both", expand=True) - + # 显示题目进度 - progress = f"题目 {self.current_quiz.current_question_index + 1}/{len(self.current_quiz.questions)}" + progress_info = self.backend_service.get_quiz_progress() + progress = f"题目 {progress_info['current_index'] + 1}/{progress_info['total_questions']}" progress_frame = tk.Frame(main_frame, bg=self.primary_color) progress_frame.pack(fill="x", pady=(0, 20)) - tk.Label(progress_frame, text=progress, font=self.header_font, bg=self.primary_color, fg="white").pack(pady=10) - + tk.Label(progress_frame, text=progress, font=self.header_font, + bg=self.primary_color, fg="white").pack(pady=10) + # 显示题目 question_frame = tk.Frame(main_frame, bg="#ffffff") question_frame.pack(pady=10) - - tk.Label(question_frame, text="题目:", font=self.header_font, bg="#ffffff").pack(pady=(10, 5)) + + tk.Label(question_frame, text="题目:", font=self.header_font, + bg="#ffffff").pack(pady=(10, 5)) question_text = str(current_question) - tk.Label(question_frame, text=question_text, font=("Arial", 16, "bold"), bg="#ffffff", wraplength=500).pack(pady=10) - - # 计算选项 - options = self.generate_options(current_question.answer) - + tk.Label(question_frame, text=question_text, + font=("Arial", 16, "bold"), bg="#ffffff", + wraplength=500).pack( + pady=10) + + # 获取固定选项 + options = self.backend_service.get_current_options() + # 显示选项 - tk.Label(main_frame, text="请选择答案:", font=self.normal_font, bg="#ffffff").pack(pady=(20, 10)) - + tk.Label(main_frame, text="请选择答案:", font=self.normal_font, + bg="#ffffff").pack(pady=(20, 10)) + self.answer_var = tk.StringVar() - self.answer_var.set(" ") # 明确设置初始值为空字符串,确保不选中任何选项 - + # 设置默认选项为用户之前的选择(如果有) + current_answer = progress_info['current_answer'] + if current_answer is not None: + self.answer_var.set(current_answer) + else: + self.answer_var.set(" ") # 没有选择时设为空字符串 + options_frame = tk.Frame(main_frame, bg="#ffffff") options_frame.pack(pady=10) - + for i, option in enumerate(options): - tk.Radiobutton(options_frame, text=f"{['A', 'B', 'C', 'D'][i]}. {option}", - variable=self.answer_var, value=str(option), font=self.normal_font, - bg="#ffffff", selectcolor="#e0e0e0", activebackground="#f0f0f0").pack(anchor="w", padx=50, pady=5) - + tk.Radiobutton(options_frame, + text=f"{['A', 'B', 'C', 'D'][i]}. {option}", + variable=self.answer_var, value=option, + font=self.normal_font, + bg="#ffffff", selectcolor="#e0e0e0", + activebackground="#f0f0f0").pack( + anchor="w", padx=50, pady=5) + # 按钮框架 button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=20) - - if self.current_quiz.current_question_index > 0: - tk.Button(button_frame, text="上一题", command=self.previous_question, bg="#cccccc", fg=self.dark_text, - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) - - tk.Button(button_frame, text="提交答案", command=self.submit_answer, bg=self.success_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) - - if self.current_quiz.current_question_index < len(self.current_quiz.questions) - 1: - tk.Button(button_frame, text="下一题", command=self.next_question, bg=self.primary_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) - - tk.Button(main_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15, bg=self.danger_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) - + + if progress_info['current_index'] > 0: + tk.Button(button_frame, text="上一题", + command=self.previous_question, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(side="left", padx=10) + + tk.Button(button_frame, text="提交答案", command=self.submit_answer, + bg=self.success_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(side="left", padx=10) + + if progress_info['current_index'] < progress_info['total_questions'] - 1: + tk.Button(button_frame, text="下一题", + command=self.next_question, bg=self.primary_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(side="left", padx=10) + + tk.Button(main_frame, text="返回主菜单", + command=self.show_main_menu_frame, width=15, + bg=self.danger_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=10) + self.show_frame(self.quiz_frame) def generate_options(self, correct_answer) -> List[float]: """ 为题目生成选项 - + @param correct_answer: 正确答案 @return: 选项列表 """ # 生成4个选项,其中一个是正确答案 options = {correct_answer} - + # 添加一些干扰项 if isinstance(correct_answer, int): while len(options) < 4: - offset = random.randint(-10, 10) - if offset != 0: - options.add(correct_answer + offset) + # 生成不同长度的干扰项,避免正确答案总是最长的 + if random.random() < 0.5: + # 生成1-2位数的干扰项 + options.add(random.randint(0, 99)) + else: + # 生成2-3位数的干扰项 + options.add(random.randint(10, 999)) else: # 浮点数情况 while len(options) < 4: - offset = random.uniform(-10, 10) - if abs(offset) > 0.1: # 避免太接近正确答案 - options.add(round(correct_answer + offset, 2)) - + # 随机生成不同长度的浮点数选项 + if random.random() < 0.33: + # 生成1位小数的数 + options.add(round(random.uniform(0, 100), 1)) + elif random.random() < 0.66: + # 生成2位小数的数 + options.add(round(random.uniform(0, 100), 2)) + else: + # 生成整数 + options.add(random.randint(0, 100)) + # 如果选项不足4个,补充一些随机数 while len(options) < 4: - options.add(round(random.uniform(correct_answer - 20, correct_answer + 20), 2)) - + if random.random() < 0.5: + options.add(random.randint(0, 100)) + else: + options.add(round(random.uniform(0, 100), + random.choice([0, 1, 2]))) + options_list = list(options) random.shuffle(options_list) return options_list[:4] @@ -505,20 +639,20 @@ class MathQuizApp: """ 提交答案 """ - if not self.current_quiz: - return - answer_str = self.answer_var.get() - if not answer_str: + if not answer_str: # 检查字符串是否为空 messagebox.showerror("错误", "请选择一个答案") return - + try: - answer = float(answer_str) - self.current_quiz.answer_question(answer) - + # 验证答案是否为有效数字 + float(answer_str) + if not self.backend_service.answer_question(answer_str): + messagebox.showerror("错误", "提交答案失败") + return + # 如果是最后一题,显示结果 - if self.current_quiz.is_finished(): + if self.backend_service.is_quiz_finished(): self.show_result_frame() else: messagebox.showinfo("提示", "答案已提交") @@ -529,23 +663,22 @@ class MathQuizApp: """ 下一题 """ - if not self.current_quiz: - return - # 保存当前答案(如果有选择) answer_str = self.answer_var.get() - if answer_str: + if answer_str: # 检查字符串是否非空 try: - answer = float(answer_str) - self.current_quiz.answer_question(answer) + # 验证答案是否为有效数字 + float(answer_str) + self.backend_service.answer_question(answer_str) except ValueError: pass # 如果答案无效,保持为None - - if self.current_quiz.next_question(): + + if self.backend_service.next_question(): self.show_quiz_frame() else: # 已经是最后一题 - if self.current_quiz.answers[self.current_quiz.current_question_index] is not None: + progress_info = self.backend_service.get_quiz_progress() + if progress_info['current_answer'] is not None: # 如果最后一题已答题,显示结果 self.show_result_frame() else: @@ -555,51 +688,52 @@ class MathQuizApp: """ 上一题 """ - if not self.current_quiz: - return - # 保存当前答案(如果有选择) answer_str = self.answer_var.get() - if answer_str: + if answer_str: # 检查字符串是否非空 try: - answer = float(answer_str) - self.current_quiz.answer_question(answer) + # 验证答案是否为有效数字 + float(answer_str) + self.backend_service.answer_question(answer_str) except ValueError: pass # 如果答案无效,保持为None - - if self.current_quiz.previous_question(): + + if self.backend_service.previous_question(): self.show_quiz_frame() def show_result_frame(self): """ 显示结果界面 """ - if not self.current_quiz: - return - # 清除之前的内容 for widget in self.result_frame.winfo_children(): widget.destroy() - + # 计算得分 - score = self.current_quiz.calculate_score() - + score = self.backend_service.calculate_score() + # 创建结果界面 - main_frame = tk.Frame(self.result_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame = tk.Frame(self.result_frame, bg="#ffffff", + relief=tk.RAISED, bd=2) main_frame.pack(pady=30, padx=50, fill="both", expand=True) - + title_frame = tk.Frame(main_frame, bg=self.primary_color) title_frame.pack(fill="x", pady=(0, 20)) - tk.Label(title_frame, text="测验结果", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) - + tk.Label(title_frame, text="测验结果", font=self.title_font, + bg=self.primary_color, fg="white").pack(pady=20) + result_frame = tk.Frame(main_frame, bg="#ffffff") result_frame.pack(pady=20) - + # 根据得分显示不同颜色的分数 - score_color = self.danger_color if score < 60 else self.warning_color if score < 80 else self.success_color - - tk.Label(result_frame, text=f"您的得分: {score:.1f}%", font=("Arial", 20, "bold"), fg=score_color, bg="#ffffff").pack(pady=10) - + score_color = (self.danger_color if score < 60 else + self.warning_color if score < 80 else + self.success_color) + + tk.Label(result_frame, text=f"您的得分: {score:.1f}%", + font=("Arial", 20, "bold"), fg=score_color, + bg="#ffffff").pack(pady=10) + # 根据得分显示评语 if score >= 90: comment = "优秀! 继续保持!" @@ -613,27 +747,38 @@ class MathQuizApp: else: comment = "需要加强练习哦!" comment_color = self.danger_color - - tk.Label(result_frame, text=comment, font=self.header_font, fg=comment_color, bg="#ffffff").pack(pady=10) - + + tk.Label(result_frame, text=comment, font=self.header_font, + fg=comment_color, bg="#ffffff").pack(pady=10) + # 显示答题详情 - correct_count = sum(1 for q, a in zip(self.current_quiz.questions, self.current_quiz.answers) - if a is not None and abs(q.answer - a) < 1e-6) - total_count = len(self.current_quiz.questions) - tk.Label(result_frame, text=f"答对: {correct_count}/{total_count}", font=self.normal_font, bg="#ffffff").pack(pady=5) + result_details = self.backend_service.get_quiz_result_details() + correct_count = result_details.get("correct_count", 0) + total_count = result_details.get("total_count", 0) + tk.Label(result_frame, text=f"答对: {correct_count}/{total_count}", + font=self.normal_font, bg="#ffffff").pack( + pady=5) + # 按钮 button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=30) - + level_names = {"elementary": "小学", "middle": "初中", "high": "高中"} - tk.Button(button_frame, text=f"继续{level_names[self.current_level]}题目", - command=lambda: self.show_quiz_setup_frame(self.current_level), width=20, bg=self.primary_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) - - tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=15, bg="#cccccc", fg=self.dark_text, - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) - + tk.Button(button_frame, + text=f"继续{level_names[self.current_level]}题目", + command=lambda: self.show_quiz_setup_frame( + self.current_level), width=20, bg=self.primary_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(side="left", padx=10) + + tk.Button(button_frame, text="返回主菜单", + command=self.show_main_menu_frame, width=15, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(side="left", padx=10) + self.show_frame(self.result_frame) def show_change_password_frame(self): @@ -643,39 +788,63 @@ class MathQuizApp: # 清除之前的内容 for widget in self.change_password_frame.winfo_children(): widget.destroy() - + # 创建修改密码界面 - main_frame = tk.Frame(self.change_password_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame = tk.Frame(self.change_password_frame, bg="#ffffff", + relief=tk.RAISED, bd=2) main_frame.pack(pady=30, padx=50, fill="both", expand=True) - + title_frame = tk.Frame(main_frame, bg=self.primary_color) title_frame.pack(fill="x", pady=(0, 20)) - tk.Label(title_frame, text="修改密码", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) - + tk.Label(title_frame, text="修改密码", font=self.title_font, + bg=self.primary_color, fg="white").pack(pady=20) + form_frame = tk.Frame(main_frame, bg="#ffffff") form_frame.pack(pady=10) - - tk.Label(form_frame, text="原密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") - self.old_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="原密码:", font=self.normal_font, + bg="#ffffff").pack(pady=5, anchor="w") + self.old_password_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + show="*", relief=tk.FLAT, bd=5, + bg="#f0f0f0") self.old_password_entry.pack(pady=5) - - tk.Label(form_frame, text="新密码:", font=self.normal_font, bg="#ffffff", fg=self.primary_color).pack(pady=(10, 5), anchor="w") - tk.Label(form_frame, text="(6-10位,必须包含大小写字母和数字)", font=("Arial", 10), bg="#ffffff", fg="gray").pack(pady=5, anchor="w") - self.new_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="新密码:", font=self.normal_font, + bg="#ffffff", fg=self.primary_color).pack( + pady=(10, 5), anchor="w") + tk.Label(form_frame, text="(6-10位,必须包含大小写字母和数字)", + font=("Arial", 10), bg="#ffffff", + fg="gray").pack(pady=5, anchor="w") + self.new_password_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + show="*", relief=tk.FLAT, bd=5, + bg="#f0f0f0") self.new_password_entry.pack(pady=5) - - tk.Label(form_frame, text="确认新密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") - self.confirm_new_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="确认新密码:", font=self.normal_font, + bg="#ffffff").pack(pady=5, anchor="w") + self.confirm_new_password_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + show="*", + relief=tk.FLAT, bd=5, + bg="#f0f0f0") self.confirm_new_password_entry.pack(pady=5) - + button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=20) - - tk.Button(button_frame, text="修改密码", command=self.change_password, width=20, bg=self.success_color, fg="white", - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=10) - tk.Button(button_frame, text="返回主菜单", command=self.show_main_menu_frame, width=20, bg="#cccccc", fg=self.dark_text, - font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(pady=5) - + + tk.Button(button_frame, text="修改密码", command=self.change_password, + width=20, bg=self.success_color, + fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=10) + tk.Button(button_frame, text="返回主菜单", + command=self.show_main_menu_frame, width=20, bg="#cccccc", + fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, pady=5).pack(pady=5) + self.show_frame(self.change_password_frame) def change_password(self): @@ -685,25 +854,25 @@ class MathQuizApp: old_password = self.old_password_entry.get() new_password = self.new_password_entry.get() confirm_new_password = self.confirm_new_password_entry.get() - + if not old_password or not new_password or not confirm_new_password: messagebox.showerror("错误", "请填写所有字段") return - + if new_password != confirm_new_password: messagebox.showerror("错误", "新密码和确认密码不一致") return - - if self.user_manager.change_password(old_password, new_password): + + if self.backend_service.change_password(old_password, new_password): messagebox.showinfo("成功", "密码修改成功") self.show_main_menu_frame() - # 错误信息在user_manager.change_password中已经显示 + # 错误信息在backend_service.change_password中已经显示 def logout(self): """ 退出登录 """ - self.user_manager.logout() + self.backend_service.logout() self.show_login_frame() def show_delete_account_frame(self): @@ -713,41 +882,63 @@ class MathQuizApp: # 清除之前的内容 for widget in self.delete_account_frame.winfo_children(): widget.destroy() - + # 创建注销账户界面 - main_frame = tk.Frame(self.delete_account_frame, bg="#ffffff", relief=tk.RAISED, bd=2) + main_frame = tk.Frame(self.delete_account_frame, bg="#ffffff", + relief=tk.RAISED, bd=2) main_frame.pack(pady=30, padx=50, fill="both", expand=True) - + title_frame = tk.Frame(main_frame, bg=self.danger_color) title_frame.pack(fill="x", pady=(0, 20)) - tk.Label(title_frame, text="注销账户", font=self.title_font, bg=self.danger_color, fg="white").pack(pady=20) - + tk.Label(title_frame, text="注销账户", font=self.title_font, + bg=self.danger_color, fg="white").pack(pady=20) + warning_frame = tk.Frame(main_frame, bg="#ffffff") warning_frame.pack(pady=10) - - tk.Label(warning_frame, text="警告:此操作将永久删除您的账户和所有数据!", font=self.normal_font, fg=self.danger_color, bg="#ffffff").pack(pady=5) - tk.Label(warning_frame, text="请确认您的邮箱和密码:", font=self.normal_font, fg=self.danger_color, bg="#ffffff").pack(pady=5) - + + tk.Label(warning_frame, + text="警告:此操作将永久删除您的账户和所有数据!", + font=self.normal_font, + fg=self.danger_color, bg="#ffffff").pack(pady=5) + tk.Label(warning_frame, text="请确认您的邮箱和密码:", + font=self.normal_font, fg=self.danger_color, + bg="#ffffff").pack(pady=5) + form_frame = tk.Frame(main_frame, bg="#ffffff") form_frame.pack(pady=10) - - tk.Label(form_frame, text="邮箱:", font=self.normal_font, bg="#ffffff").pack(pady=10, anchor="w") - self.delete_email_entry = tk.Entry(form_frame, width=30, font=self.normal_font, relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="邮箱:", font=self.normal_font, + bg="#ffffff").pack(pady=10, anchor="w") + self.delete_email_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + relief=tk.FLAT, bd=5, + bg="#f0f0f0") self.delete_email_entry.pack(pady=5) - - tk.Label(form_frame, text="密码:", font=self.normal_font, bg="#ffffff").pack(pady=5, anchor="w") - self.delete_password_entry = tk.Entry(form_frame, width=30, font=self.normal_font, show="*", relief=tk.FLAT, bd=5, bg="#f0f0f0") + + tk.Label(form_frame, text="密码:", font=self.normal_font, + bg="#ffffff").pack(pady=5, anchor="w") + self.delete_password_entry = tk.Entry(form_frame, width=30, + font=self.normal_font, + show="*", relief=tk.FLAT, + bd=5, bg="#f0f0f0") self.delete_password_entry.pack(pady=5) - + # 按钮框架 button_frame = tk.Frame(main_frame, bg="#ffffff") button_frame.pack(pady=20) - - tk.Button(button_frame, text="确认注销", command=self.confirm_delete_account, - width=15, bg=self.danger_color, fg="white", font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) + + tk.Button(button_frame, text="确认注销", + command=self.confirm_delete_account, + width=15, bg=self.danger_color, fg="white", + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, + pady=5).pack(side="left", padx=10) tk.Button(button_frame, text="取消", command=self.show_login_frame, - width=15, bg="#cccccc", fg=self.dark_text, font=self.button_font, relief=tk.FLAT, bd=0, padx=10, pady=5).pack(side="left", padx=10) - + width=15, bg="#cccccc", fg=self.dark_text, + font=self.button_font, relief=tk.FLAT, bd=0, + padx=10, + pady=5).pack(side="left", padx=10) + self.show_frame(self.delete_account_frame) def confirm_delete_account(self): @@ -756,17 +947,18 @@ class MathQuizApp: """ email = self.delete_email_entry.get().strip() password = self.delete_password_entry.get() - + if not email or not password: messagebox.showerror("错误", "请输入邮箱和密码") return - + # 确认操作 if messagebox.askyesno("确认注销", "确定要注销账户吗?此操作无法撤销!"): - if self.user_manager.delete_account(email, password): - messagebox.showinfo("成功", "账户已注销,您可以使用该邮箱重新注册") + if self.backend_service.delete_account(email, password): + messagebox.showinfo("成功", + "账户已注销,您可以使用该邮箱重新注册") self.show_login_frame() - # 错误信息在user_manager.delete_account中已经显示 + # 错误信息在backend_service.delete_account中已经显示 def delete_account(self): """ diff --git a/src/question_generator.py b/src/question_generator.py index ed473d3..54bdca4 100644 --- a/src/question_generator.py +++ b/src/question_generator.py @@ -14,7 +14,7 @@ from typing import List, Set, Optional class Expression: """ - 数学表达式类 + 数学表达式类(辅助类) """ def __init__(self, expression_str: str, answer: float): @@ -97,42 +97,16 @@ class ElementaryQuestionGenerator(QuestionGenerator): """ max_attempts = 100 for _ in range(max_attempts): - # 随机生成操作数数量(2-5个) - num_operands = random.randint(2, 5) - operands = [random.randint(1, 50) for _ in range(num_operands)] - operators = [random.choice(['+', '-', '*']) if random.random() < 0.8 else '/' - for _ in range(num_operands - 1)] # 减少减法和除法概率以避免负数 - - # 随机添加括号 - expression_parts = [] - for i in range(num_operands): - expression_parts.append(str(operands[i])) - if i < len(operators): - expression_parts.append(operators[i]) - - # 随机添加括号 - if num_operands >= 3 and random.random() < 0.3: - # 在随机位置添加括号 - open_pos = random.randint(0, len(expression_parts) - 3) - # 确保括号内至少有两个操作数 - close_pos = min(open_pos + 2 + random.randint(1, 2) * 2, len(expression_parts)) - - # 确保括号位置是操作数位置 - if open_pos % 2 == 0 and close_pos % 2 == 0: - expression_parts.insert(open_pos, '(') - expression_parts.insert(close_pos + 1, ')') - - expression_str = ''.join(expression_parts) - + expression_str = self._generate_elementary_expression() # 验证表达式是否有效 try: # 替换除法符号以便计算 eval_expr = expression_str.replace('/', '/').replace('*', '*') result = eval(eval_expr) - # 确保结果是非负数且是合理的(整数或有限小数) - if isinstance(result, (int, float)) and result >= 0 and abs(result) < 1000: - # 格式化结果,保留合适的小数位数 + if (isinstance(result, (int, float)) and + result >= 0 and + abs(result) < 1000): if isinstance(result, float) and result.is_integer(): result = int(result) @@ -141,7 +115,8 @@ class ElementaryQuestionGenerator(QuestionGenerator): if str(expr) not in self.generated_questions: self.generated_questions.add(str(expr)) # 将表达式中的乘号和除号替换为更易读的形式 - readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr_str = expression_str.replace('*', '×') + readable_expr_str = readable_expr_str.replace('/', '÷') readable_expr = Expression(readable_expr_str, result) return readable_expr except: @@ -153,6 +128,65 @@ class ElementaryQuestionGenerator(QuestionGenerator): readable_expr = Expression("1+1", 2) return readable_expr + def _generate_elementary_expression(self) -> str: + """ + 生成小学题目表达式字符串 + + @return: 表达式字符串 + """ + # 随机生成操作数数量(2-5个) + num_operands = random.randint(2, 5) + # 修改操作数范围从1-50到1-100 + operands = [random.randint(1, 100) for _ in range(num_operands)] + operators = [random.choice(['+', '-', '*']) if random.random() < 0.8 else '/' + for _ in range(num_operands - 1)] # 减少减法和除法概率以避免负数 + + # 构建表达式部分 + expression_parts = self._build_expression_parts(operands, operators) + + # 随机添加括号 + expression_parts = self._add_parentheses(expression_parts, num_operands) + + return ''.join(expression_parts) + + def _build_expression_parts(self, operands: List[int], + operators: List[str]) -> List[str]: + """ + 构建表达式部分 + + @param operands: 操作数列表 + @param operators: 操作符列表 + @return: 表达式部分列表 + """ + expression_parts = [] + for i in range(len(operands)): + expression_parts.append(str(operands[i])) + if i < len(operators): + expression_parts.append(operators[i]) + return expression_parts + + def _add_parentheses(self, expression_parts: List[str], + num_operands: int) -> List[str]: + """ + 随机添加括号到表达式 + + @param expression_parts: 表达式部分列表 + @param num_operands: 操作数数量 + @return: 添加括号后的表达式部分列表 + """ + if num_operands >= 3 and random.random() < 0.3: + # 在随机位置添加括号 + open_pos = random.randint(0, len(expression_parts) - 3) + # 确保括号内至少有两个操作数 + close_pos = min(open_pos + 2 + random.randint(1, 2) * 2, + len(expression_parts)) + + # 确保括号位置是操作数位置 + if open_pos % 2 == 0 and close_pos % 2 == 0: + expression_parts.insert(open_pos, '(') + expression_parts.insert(close_pos + 1, ')') + return expression_parts + class MiddleQuestionGenerator(QuestionGenerator): """ @@ -167,93 +201,161 @@ class MiddleQuestionGenerator(QuestionGenerator): """ max_attempts = 100 for _ in range(max_attempts): - expression_parts = [] - - # 确保至少有一个平方或开根号 - has_square_or_sqrt = True # 确保至少有一个平方或开根号 - - # 生成操作数数量(2-5个) - num_operands = random.randint(2, 5) - - # 标记是否已添加特殊操作 - special_added = False - - for i in range(num_operands): - # 确保至少添加一个平方或开根号 - if not special_added and i == num_operands - 1: - # 如果还没添加特殊操作,强制在最后一个操作数添加 - choice = random.choice([0, 1]) # 提高开根号概率 - if choice == 0: - # 添加平方 - base = random.randint(1, 12) - expression_parts.append(f"{base}²") - else: - # 添加开根号 - square = random.randint(1, 12) - value = square * square - expression_parts.append(f"√{value}") - special_added = True - else: - rand_val = random.random() - # 修改这里的概率,增加开根号和平方的出现频率 - if not special_added and rand_val < 0.6: # 提高特殊操作概率从0.4到0.6 - # 添加特殊操作(平方或开根号) - choice = random.choice([0, 1]) if random.random() < 0.6 else 1 # 提高开根号概率 - if choice == 0: - # 添加平方 - base = random.randint(1, 12) - expression_parts.append(f"{base}²") - else: - # 添加开根号 - square = random.randint(1, 12) - value = square * square - expression_parts.append(f"√{value}") - special_added = True - else: - # 普通操作数 - expression_parts.append(str(random.randint(1, 50))) - - # 添加运算符(除了最后一个操作数) - if i < num_operands - 1: - expression_parts.append(random.choice(['+', '-', '*', '/'])) - - expression_str = ''.join(expression_parts) - - # 处理可能的语法问题 + expression_str = self._generate_middle_expression() expression_str = self.fix_expression_syntax(expression_str) - # 计算结果 try: # 替换表达式中的函数以便计算 - eval_expr = expression_str.replace('²', '**2').replace('√', 'math.sqrt') + eval_expr = self._prepare_middle_expression_for_eval(expression_str) result = eval(eval_expr) - # 确保结果是合理的 - if isinstance(result, (int, float)) and abs(result) < 1000 and not math.isnan(result): + if (isinstance(result, (int, float)) and + abs(result) < 1000 and + not math.isnan(result)): # 格式化结果 if isinstance(result, float) and abs(result - round(result)) < 1e-10: result = int(round(result)) - expr = Expression(expression_str, result) # 检查是否已生成过相同题目 if str(expr) not in self.generated_questions: self.generated_questions.add(str(expr)) - # 将表达式中的乘号和除号替换为更易读的形式 - readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr_str = expression_str.replace('*', '×') + readable_expr_str = readable_expr_str.replace('/', '÷') readable_expr = Expression(readable_expr_str, result) return readable_expr except: continue - - # 如果无法生成有效题目,返回默认题目 expr = Expression("√4+2²", 6) self.generated_questions.add(str(expr)) readable_expr = Expression("√4+2²", 6) - # 将表达式中的乘号和除号替换为更易读的形式 readable_expr_str = "√4+2²".replace('*', '×').replace('/', '÷') readable_expr = Expression(readable_expr_str, 6) return readable_expr + def _generate_middle_expression(self) -> str: + """ + 生成初中题目表达式字符串 + + @return: 表达式字符串 + """ + expression_parts = [] + + # 生成操作数数量(2-5个) + num_operands = random.randint(2, 5) + + # 标记是否已添加特殊操作 + special_added = False + + # 生成表达式部分 + expression_parts, special_added = self._build_middle_expression_parts( + expression_parts, num_operands, special_added) + + return ''.join(expression_parts) + + def _build_middle_expression_parts(self, expression_parts: List[str], + num_operands: int, + special_added: bool) -> tuple: + """ + 构建初中表达式部分 + + @param expression_parts: 表达式部分列表 + @param num_operands: 操作数数量 + @param special_added: 是否已添加特殊操作标记 + @return: (表达式部分列表, 是否已添加特殊操作标记) + """ + for i in range(num_operands): + # 确保至少添加一个平方或开根号 + if not special_added and i == num_operands - 1: + # 如果还没添加特殊操作,强制在最后一个操作数添加 + expression_parts, special_added = self._add_forced_special_operation( + expression_parts) + else: + expression_parts, special_added = self._add_middle_operand_or_operation( + expression_parts, special_added, i, num_operands) + + # 添加运算符(除了最后一个操作数) + if i < num_operands - 1: + expression_parts.append(random.choice(['+', '-', '*', '/'])) + + return expression_parts, special_added + + def _add_forced_special_operation(self, expression_parts: List[str]) -> tuple: + """ + 强制添加特殊操作(平方或开根号) + + @param expression_parts: 表达式部分列表 + @return: (表达式部分列表, 是否已添加特殊操作标记) + """ + choice = random.choice([0, 1]) # 0为平方,1为开根号 + if choice == 0: + # 添加平方 + base = random.randint(1, 100) + expression_parts.append(f"{base}²") + else: + # 添加开根号(确保至少有一个开根号) + square = random.randint(1, 10) + value = square * square + expression_parts.append(f"√{value}") + return expression_parts, True + + def _add_middle_operand_or_operation(self, expression_parts: List[str], + special_added: bool, i: int, + num_operands: int) -> tuple: + """ + 添加初中操作数或操作 + + @param expression_parts: 表达式部分列表 + @param special_added: 是否已添加特殊操作标记 + @param i: 当前索引 + @param num_operands: 操作数总数 + @return: (表达式部分列表, 是否已添加特殊操作标记) + """ + rand_val = random.random() + # 修改这里的概率,增加开根号和平方的出现频率 + if not special_added and rand_val < 0.6: + # 添加特殊操作(平方或开根号) + expression_parts, special_added = self._add_middle_special_operation() + else: + # 普通操作数 + expression_parts.append(str(random.randint(1, 100))) + return expression_parts, special_added + + return expression_parts, special_added + + def _add_middle_special_operation(self) -> tuple: + """ + 添加初中特殊操作(平方或开根号) + + @return: (表达式部分列表, 是否已添加特殊操作标记) + """ + choice = random.choice([0, 1]) # 平方和开根号的概率相等 + expression_parts = [] + if choice == 0: + # 添加平方 + base = random.randint(1, 100) + expression_parts.append(f"{base}²") + else: + # 添加开根号 + square = random.randint(1, 10) + value = square * square + expression_parts.append(f"√{value}") + return expression_parts, True + + def _prepare_middle_expression_for_eval(self, expression_str: str) -> str: + """ + 准备初中题目表达式用于eval计算 + + @param expression_str: 原始表达式字符串 + @return: 可用于eval计算的表达式字符串 + """ + # 替换表达式中的函数以便计算 + eval_expr = expression_str.replace('²', '**2') + # 使用正则表达式正确处理平方根 + eval_expr = re.sub(r'√(\d+)', + r'math.sqrt(\1)', + eval_expr) + return eval_expr + def fix_expression_syntax(self, expression: str) -> str: """ 修复表达式语法问题 @@ -266,8 +368,6 @@ class MiddleQuestionGenerator(QuestionGenerator): expression = re.sub(r'(²)([√])', r'\1*\2', expression) expression = re.sub(r'(\))(\d)', r'\1*\2', expression) expression = re.sub(r'(\d)(\()', r'\1*\2', expression) - # 修复根号表达式显示,确保根号符号正确显示 - expression = re.sub(r'√(\d+)', r'√\1', expression) return expression @@ -279,110 +379,34 @@ class HighQuestionGenerator(QuestionGenerator): def generate_question(self) -> Expression: """ 生成高中题目 - @return: 数学表达式 """ max_attempts = 100 for _ in range(max_attempts): - expression_parts = [] - - # 确保至少有一个三角函数 - has_trig_function = random.random() < 0.8 - - # 生成操作数数量(2-4个) - num_operands = random.randint(2, 4) - - for i in range(num_operands): - if has_trig_function and i == 0: - # 第一个操作数有较高概率是三角函数 - if random.random() < 0.33: - angle = random.choice([0, 30, 45, 60, 90]) - expression_parts.append(f"sin({angle}°)") - elif random.random() < 0.5: - angle = random.choice([0, 30, 45, 60, 90]) - expression_parts.append(f"cos({angle}°)") - else: - angle = random.choice([0, 30, 45, 60, 90]) - expression_parts.append(f"tan({angle}°)") - else: - # 其他操作数 - rand_val = random.random() - if rand_val < 0.1 and has_trig_function: - # 添加三角函数 - angle = random.choice([0, 30, 45, 60, 90]) - expression_parts.append(f"sin({angle}°)") - elif rand_val < 0.2 and has_trig_function: - angle = random.choice([0, 30, 45, 60, 90]) - expression_parts.append(f"cos({angle}°)") - elif rand_val < 0.3 and has_trig_function: - angle = random.choice([0, 30, 45, 60, 90]) - expression_parts.append(f"tan({angle}°)") - elif rand_val < 0.55: - # 普通数字 - expression_parts.append(str(random.randint(1, 20))) - elif rand_val < 0.7: - # 平方 - base = random.randint(1, 10) - expression_parts.append(f"{base}²") - else: - # 开根号 - square = random.randint(1, 10) - value = square * square - expression_parts.append(f"√{value}") - - # 添加运算符(除了最后一个操作数) - if i < num_operands - 1: - expression_parts.append(random.choice(['+', '-', '*', '/'])) - - expression_str = ''.join(expression_parts) - - # 处理可能的语法问题 + expression_str = self._generate_high_expression() expression_str = self.fix_expression_syntax(expression_str) - # 计算结果 try: # 替换表达式中的函数以便计算 - eval_expr = expression_str.replace('²', '**2').replace('√', 'math.sqrt') - # 修复:确保正确的替换顺序,先替换角度制三角函数 - eval_expr = re.sub(r'sin\((\d+)°\)', r'math.sin(math.radians(\1))', eval_expr) - eval_expr = re.sub(r'cos\((\d+)°\)', r'math.cos(math.radians(\1))', eval_expr) - eval_expr = re.sub(r'tan\((\d+)°\)', r'math.tan(math.radians(\1))', eval_expr) - + eval_expr = self._prepare_high_expression_for_eval(expression_str) result = eval(eval_expr) - # 确保结果是合理的 - if isinstance(result, (int, float)) and abs(result) < 1000 and not math.isnan(result) and not math.isinf(result): - # 格式化结果 - if isinstance(result, float) and abs(result - round(result, 10)) < 1e-10: - result = int(round(result)) - # 特殊处理常见的三角函数值,使其更加准确 - elif isinstance(result, float): - # 对于常见的三角函数值进行舍入处理 - if abs(result - 0.5) < 1e-10: # sin(30°) = 0.5 - result = 0.5 - elif abs(result - 0.7071067811865476) < 1e-10: # sin(45°) = cos(45°) ≈ 0.707 - result = round(result, 10) - elif abs(result - 0.8660254037844386) < 1e-10: # sin(60°) ≈ 0.866 - result = round(result, 10) - elif abs(result - 0.5773502691896257) < 1e-10: # tan(30°) ≈ 0.577 - result = round(result, 10) - elif abs(result - 1.7320508075688772) < 1e-10: # tan(60°) ≈ 1.732 - result = round(result, 10) - elif abs(result - 1.0) < 1e-10: # tan(45°) = 1, sin(90°) = 1 - result = 1.0 - + if (isinstance(result, (int, float)) and + abs(result) < 1000 and + not math.isnan(result) and + not math.isinf(result)): + # 格式化结果,保留两位小数 + result = self._format_high_result(result) expr = Expression(expression_str, result) # 检查是否已生成过相同题目 if str(expr) not in self.generated_questions: self.generated_questions.add(str(expr)) - # 将表达式中的乘号和除号替换为更易读的形式 - readable_expr_str = expression_str.replace('*', '×').replace('/', '÷') + readable_expr_str = expression_str.replace('*', '×') + readable_expr_str = readable_expr_str.replace('/', '÷') readable_expr = Expression(readable_expr_str, result) return readable_expr except: continue - - # 如果无法生成有效题目,返回默认题目 expr = Expression("sin(30°)", 0.5) self.generated_questions.add(str(expr)) readable_expr = Expression("sin(30°)", 0.5) @@ -391,6 +415,189 @@ class HighQuestionGenerator(QuestionGenerator): readable_expr = Expression(readable_expr_str, 0.5) return readable_expr + def _generate_high_expression(self) -> str: + """ + 生成高中题目表达式字符串 + + @return: 表达式字符串 + """ + expression_parts = [] + + # 生成操作数数量(2-4个) + num_operands = random.randint(2, 5) + + # 标记是否已添加三角函数 + trig_added = False + + # 生成表达式部分 + expression_parts, trig_added = self._build_high_expression_parts( + expression_parts, num_operands, trig_added) + + return ''.join(expression_parts) + + def _build_high_expression_parts(self, expression_parts: List[str], + num_operands: int, + trig_added: bool) -> tuple: + """ + 构建高中表达式部分 + + @param expression_parts: 表达式部分列表 + @param num_operands: 操作数数量 + @param trig_added: 是否已添加三角函数标记 + @return: (表达式部分列表, 是否已添加三角函数标记) + """ + for i in range(num_operands): + # 确保至少添加一个三角函数 + if not trig_added and i == num_operands - 1: + # 如果还没添加三角函数,强制在最后一个操作数添加 + expression_parts, trig_added = self._add_forced_trig_function( + expression_parts) + else: + # 其他操作数 + expression_parts, trig_added = self._add_high_operand_or_operation( + expression_parts, trig_added) + + # 添加运算符(除了最后一个操作数) + if i < num_operands - 1: + expression_parts.append(random.choice(['+', '-', '*', '/'])) + + return expression_parts, trig_added + + def _add_forced_trig_function(self, expression_parts: List[str]) -> tuple: + """ + 强制添加三角函数 + + @param expression_parts: 表达式部分列表 + @return: (表达式部分列表, 是否已添加三角函数标记) + """ + choice = random.randint(0, 2) + if choice == 0: + angle = random.choice([0, 30, 45, 60]) # 不包含90度 + expression_parts.append(f"sin({angle}°)") + elif choice == 1: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"cos({angle}°)") + else: + angle = random.choice([0, 30, 45, 60]) # 不包含90度 + expression_parts.append(f"tan({angle}°)") + return expression_parts, True + + def _add_high_operand_or_operation(self, expression_parts: List[str], + trig_added: bool) -> tuple: + """ + 添加高中操作数或操作 + + @param expression_parts: 表达式部分列表 + @param trig_added: 是否已添加三角函数标记 + @return: (表达式部分列表, 是否已添加三角函数标记) + """ + rand_val = random.random() + if not trig_added and rand_val < 0.4: + # 添加三角函数 + return self._add_trig_function() + elif rand_val < 0.65: + # 普通数字 + expression_parts.append(str(random.randint(1, 20))) + return expression_parts, trig_added + elif rand_val < 0.8: + # 平方 + base = random.randint(1, 10) + expression_parts.append(f"{base}²") + return expression_parts, trig_added + else: + # 开根号 + square = random.randint(1, 10) + value = square * square + expression_parts.append(f"√{value}") + return expression_parts, trig_added + + def _add_trig_function(self) -> tuple: + """ + 添加三角函数 + + @return: (表达式部分列表, 是否已添加三角函数标记) + """ + expression_parts = [] + func_choice = random.randint(0, 2) + if func_choice == 0: + angle = random.choice([0, 30, 45, 60]) # 不包含90度 + expression_parts.append(f"sin({angle}°)") + elif func_choice == 1: + angle = random.choice([0, 30, 45, 60, 90]) + expression_parts.append(f"cos({angle}°)") + else: + angle = random.choice([0, 30, 45, 60]) # 不包含90度 + expression_parts.append(f"tan({angle}°)") + return expression_parts, True + + def _prepare_high_expression_for_eval(self, expression_str: str) -> str: + """ + 准备高中题目表达式用于eval计算 + + @param expression_str: 原始表达式字符串 + @return: 可用于eval计算的表达式字符串 + """ + eval_expr = self._replace_powers_and_roots(expression_str) + # 修复:确保正确的替换顺序,先替换角度制三角函数 + eval_expr = self._replace_trig_functions(eval_expr) + return eval_expr + + def _replace_powers_and_roots(self, expression_str: str) -> str: + """ + 替换幂运算和开根号符号 + + @param expression_str: 原始表达式字符串 + @return: 替换后的表达式字符串 + """ + eval_expr = expression_str.replace('²', '**2') + eval_expr = eval_expr.replace('√', 'math.sqrt') + return eval_expr + + def _replace_trig_functions(self, eval_expr: str) -> str: + """ + 替换三角函数 + + @param eval_expr: 表达式字符串 + @return: 替换三角函数后的表达式字符串 + """ + eval_expr = re.sub(r'sin\((\d+)°\)', + r'math.sin(math.radians(\1))', + eval_expr) + eval_expr = re.sub(r'cos\((\d+)°\)', + r'math.cos(math.radians(\1))', + eval_expr) + eval_expr = re.sub(r'tan\((\d+)°\)', + r'math.tan(math.radians(\1))', + eval_expr) + return eval_expr + + def _format_high_result(self, result: float) -> float: + """ + 格式化高中题目计算结果 + + @param result: 计算结果 + @return: 格式化后的结果 + """ + if isinstance(result, float): + # 特殊处理常见的三角函数值,使其更加准确 + if abs(result - 0.5) < 1e-10: # sin(30°) = 0.5 + result = 0.5 + elif abs(result - 0.7071067811865476) < 1e-10: # sin(45°) = cos(45°) ≈ 0.707 + result = round(result, 2) + elif abs(result - 0.8660254037844386) < 1e-10: # sin(60°) ≈ 0.866 + result = round(result, 2) + elif abs(result - 0.5773502691896257) < 1e-10: # tan(30°) ≈ 0.577 + result = round(result, 2) + elif abs(result - 1.7320508075688772) < 1e-10: # tan(60°) ≈ 1.732 + result = round(result, 2) + elif abs(result - 1.0) < 1e-10: # tan(45°) = 1, sin(90°) = 1 + result = 1.0 + else: + # 保留两位小数 + result = round(result, 2) + # 整数保持不变 + return result + def fix_expression_syntax(self, expression: str) -> str: """ 修复表达式语法问题 @@ -399,10 +606,22 @@ class HighQuestionGenerator(QuestionGenerator): @return: 修复后的表达式 """ # 确保函数调用之间有运算符 - expression = re.sub(r'(\d)([sincostan√])', r'\1*\2', expression) - expression = re.sub(r'(²)([sincostan√])', r'\1*\2', expression) - expression = re.sub(r'(sqrt\(\d+\))(\d)', r'\1*\2', expression) - expression = re.sub(r'(\))(\d)', r'\1*\2', expression) - expression = re.sub(r'(\d)(\()', r'\1*\2', expression) - expression = re.sub(r'°\)(\d)', r'°)*\1', expression) + expression = re.sub(r'(\d)([sincostan√])', + r'\1*\2', + expression) + expression = re.sub(r'(²)([sincostan√])', + r'\1*\2', + expression) + expression = re.sub(r'(sqrt\(\d+\))(\d)', + r'\1*\2', + expression) + expression = re.sub(r'(\))(\d)', + r'\1*\2', + expression) + expression = re.sub(r'(\d)(\()', + r'\1*\2', + expression) + expression = re.sub(r'°\)(\d)', + r'°)*\1', + expression) return expression \ No newline at end of file diff --git a/src/quiz.py b/src/quiz.py index 80ae9a1..6500f32 100644 --- a/src/quiz.py +++ b/src/quiz.py @@ -5,7 +5,7 @@ 测验模块 """ -from typing import List, Optional +from typing import List, Optional, Dict, Tuple from question_generator import Expression @@ -21,15 +21,77 @@ class Quiz: @param questions: 题目列表 """ self.questions = questions - self.answers: List[Optional[float]] = [None] * len(questions) + self.answers: List[Optional[str]] = [None] * len(questions) # 改为字符串类型存储 + self.options: List[List[str]] = self._generate_options_for_questions() self.current_question_index = 0 self.score = 0 - def answer_question(self, answer: float) -> None: + def _generate_options_for_questions(self) -> List[List[str]]: + """ + 为所有题目生成固定选项 + + @return: 每道题的选项列表 + """ + options_list = [] + for question in self.questions: + options = self._generate_options(question.answer) + options_list.append(options) + return options_list + + def _generate_options(self, correct_answer) -> List[str]: + """ + 为题目生成选项 + + @param correct_answer: 正确答案 + @return: 选项列表 + """ + import random + + # 生成4个选项,其中一个是正确答案 + options = {correct_answer} + + # 添加一些干扰项 + if isinstance(correct_answer, int): + while len(options) < 4: + # 生成不同长度的干扰项,避免正确答案总是最长的 + if random.random() < 0.5: + # 生成1-2位数的干扰项 + options.add(random.randint(0, 99)) + else: + # 生成2-3位数的干扰项 + options.add(random.randint(10, 999)) + else: + # 浮点数情况 + while len(options) < 4: + # 随机生成不同长度的浮点数选项 + if random.random() < 0.33: + # 生成1位小数的数 + options.add(round(random.uniform(0, 100), 1)) + elif random.random() < 0.66: + # 生成2位小数的数 + options.add(round(random.uniform(0, 100), 2)) + else: + # 生成整数 + options.add(random.randint(0, 100)) + + # 如果选项不足4个,补充一些随机数 + while len(options) < 4: + if random.random() < 0.5: + options.add(random.randint(0, 100)) + else: + options.add(round(random.uniform(0, 100), + random.choice([0, 1, 2]))) + + # 将所有选项转换为字符串 + options_list = [str(opt) for opt in options] + random.shuffle(options_list) + return options_list[:4] + + def answer_question(self, answer: str) -> None: """ 回答当前题目 - @param answer: 用户答案 + @param answer: 用户答案(字符串形式) """ if 0 <= self.current_question_index < len(self.questions): self.answers[self.current_question_index] = answer @@ -66,6 +128,16 @@ class Quiz: return self.questions[self.current_question_index] return None + def get_current_options(self) -> List[str]: + """ + 获取当前题目的选项 + + @return: 当前题目的选项列表 + """ + if 0 <= self.current_question_index < len(self.questions): + return self.options[self.current_question_index] + return [] + def calculate_score(self) -> float: """ 计算得分 @@ -75,11 +147,18 @@ class Quiz: correct_count = 0 for i, (question, answer) in enumerate(zip(self.questions, self.answers)): if answer is not None: - # 允许一定的浮点数误差 - if abs(question.answer - answer) < 1e-6: - correct_count += 1 + try: + # 将字符串答案转换为浮点数进行比较 + if abs(question.answer - float(answer)) < 1e-6: + correct_count += 1 + except ValueError: + # 如果转换失败,说明答案无效,不计入正确答案 + pass - self.score = (correct_count / len(self.questions)) * 100 if self.questions else 0 + if self.questions: + self.score = (correct_count / len(self.questions)) * 100 + else: + self.score = 0 return self.score def is_finished(self) -> bool: @@ -88,4 +167,5 @@ class Quiz: @return: 是否已完成 """ - return self.current_question_index == len(self.questions) - 1 and self.answers[self.current_question_index] is not None \ No newline at end of file + return (self.current_question_index == len(self.questions) - 1 and + self.answers[self.current_question_index] is not None) \ No newline at end of file diff --git a/src/user_manager.py b/src/user_manager.py index a214d7e..e1ce150 100644 --- a/src/user_manager.py +++ b/src/user_manager.py @@ -11,6 +11,7 @@ import re import random import hashlib import smtplib +from abc import ABC, abstractmethod from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from typing import Dict, Optional @@ -22,7 +23,8 @@ class User: 用户类,存储用户信息 """ - def __init__(self, email: str, username: str = "", password_hash: str = "", registration_code: str = ""): + def __init__(self, email: str, username: str = "", + password_hash: str = "", registration_code: str = ""): """ 初始化用户对象 @@ -38,7 +40,48 @@ class User: self.is_registered = bool(password_hash) -class UserManager: +class AbstractUserManager(ABC): + """ + 抽象用户管理类,定义用户管理的接口 + """ + + @abstractmethod + def register_user(self, email: str, username: str) -> bool: + """注册新用户""" + pass + + @abstractmethod + def verify_registration_code(self, email: str, code: str) -> bool: + """验证注册码""" + pass + + @abstractmethod + def set_password(self, email: str, password: str) -> bool: + """设置用户密码""" + pass + + @abstractmethod + def login(self, username: str, password: str) -> bool: + """用户登录""" + pass + + @abstractmethod + def change_password(self, old_password: str, new_password: str) -> bool: + """修改用户密码""" + pass + + @abstractmethod + def logout(self) -> None: + """用户登出""" + pass + + @abstractmethod + def delete_account(self, email: str, password: str) -> bool: + """注销账户""" + pass + + +class UserManager(AbstractUserManager): """ 用户管理类,负责处理用户注册、登录和密码管理 """ @@ -72,9 +115,11 @@ class UserManager: email=email, username=user_data.get('username', ''), password_hash=user_data.get('password_hash', ''), - registration_code=user_data.get('registration_code', '') + registration_code=user_data.get( + 'registration_code', '') ) - user.is_registered = user_data.get('is_registered', False) + user.is_registered = user_data.get('is_registered', + False) self.users[email] = user except (json.JSONDecodeError, FileNotFoundError): self.users = {} @@ -118,9 +163,8 @@ class UserManager: # 用户名长度应在3-20个字符之间 if not (3 <= len(username) <= 20): return False - # 用户名只能包含字母、数字和下划线 - pattern = r'^[a-zA-Z0-9_]+$' - return re.match(pattern, username) is not None + # 不再限制用户名只能包含字母、数字和下划线,允许使用各种字符包括中文 + return True def generate_registration_code(self) -> str: """ @@ -155,20 +199,76 @@ class UserManager: return has_lower and has_upper and has_digit - def register_user(self, email: str, username: str) -> bool: + def _create_registration_email(self, email: str, code: str + ) -> MIMEMultipart: """ - 注册新用户(发送注册码) + 创建注册邮件内容 + + @param email: 接收邮箱 + @param code: 注册码 + @return: 邮件对象 + """ + message = MIMEMultipart() + message["From"] = self.sender_email + message["To"] = email + message["Subject"] = "数学练习系统注册码" + + body = f""" + 您好! + + 欢迎使用数学练习系统! + + 您的注册码是: {code} + + 请在注册界面输入此注册码完成注册。 + + 如果您没有请求此注册码,请忽略此邮件。 + + 祝学习愉快! + 数学练习系统团队 + """ + + message.attach(MIMEText(body, "plain", "utf-8")) + return message + + def send_registration_code_via_email(self, email: str, code: str) -> bool: + """ + 通过电子邮件发送注册码 + + @param email: 接收邮箱 + @param code: 注册码 + @return: 是否发送成功 + """ + try: + message = self._create_registration_email(email, code) + + # 使用SMTP_SSL连接QQ邮箱服务器 + server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) + server.login(self.sender_email, self.sender_password) + text = message.as_string() + server.sendmail(self.sender_email, email, text) + server.quit() + + return True + except Exception as e: + print(f"邮件发送失败: {str(e)}") + return False + + def _check_user_registration_eligibility(self, email: str, + username: str) -> bool: + """ + 检查用户是否符合注册条件 @param email: 用户邮箱 @param username: 用户名 - @return: 是否成功发送注册码 + @return: 是否符合注册条件 """ if not self.is_valid_email(email): messagebox.showerror("错误", "邮箱格式不正确") return False if not self.is_valid_username(username): - messagebox.showerror("错误", "用户名应为3-20位,只能包含字母、数字和下划线") + messagebox.showerror("错误", "用户名长度应为3-20个字符") return False # 检查邮箱是否已被注册 @@ -181,9 +281,23 @@ class UserManager: if user.username == username and user.is_registered: messagebox.showerror("错误", "该用户名已存在") return False + + return True + + def register_user(self, email: str, username: str) -> bool: + """ + 注册新用户(发送注册码) + + @param email: 用户邮箱 + @param username: 用户名 + @return: 是否成功发送注册码 + """ + if not self._check_user_registration_eligibility(email, username): + return False registration_code = self.generate_registration_code() - user = User(email=email, username=username, registration_code=registration_code) + user = User(email=email, username=username, + registration_code=registration_code) self.users[email] = user # 尝试发送邮件 @@ -192,65 +306,23 @@ class UserManager: messagebox.showinfo("成功", "注册码已发送到您的邮箱,请查收") return True else: - messagebox.showerror("错误", "无法发送注册码到邮箱,请检查网络连接或邮箱地址") + messagebox.showerror( + "错误", + "无法发送注册码到邮箱,请检查网络连接或邮箱地址") # 即使邮件发送失败,也保存用户信息以便重试 self.save_users() return False - def send_registration_code_via_email(self, email: str, code: str) -> bool: - """ - 通过电子邮件发送注册码 - - @param email: 接收邮箱 - @param code: 注册码 - @return: 是否发送成功 - """ - try: - # 创建邮件内容 - message = MIMEMultipart() - message["From"] = self.sender_email - message["To"] = email - message["Subject"] = "数学练习系统注册码" - - body = f""" - 您好! - - 欢迎使用数学练习系统! - - 您的注册码是: {code} - - 请在注册界面输入此注册码完成注册。 - - 如果您没有请求此注册码,请忽略此邮件。 - - 祝学习愉快! - 数学练习系统团队 - """ - - message.attach(MIMEText(body, "plain", "utf-8")) - - # 使用SMTP_SSL连接QQ邮箱服务器 - server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) - server.login(self.sender_email, self.sender_password) - text = message.as_string() - server.sendmail(self.sender_email, email, text) - server.quit() - - return True - except Exception as e: - print(f"邮件发送失败: {str(e)}") - return False - def verify_registration_code(self, email: str, code: str) -> bool: """ 验证注册码 @param email: 用户邮箱 - @param code: 用户输入的注册码 + @param code: 注册码 @return: 注册码是否正确 """ if email not in self.users: - messagebox.showerror("错误", "请先获取注册码") + messagebox.showerror("错误", "用户不存在") return False user = self.users[email] @@ -269,7 +341,9 @@ class UserManager: @return: 是否设置成功 """ if not self.is_valid_password(password): - messagebox.showerror("错误", "密码必须为6-10位,且包含大小写字母和数字") + messagebox.showerror( + "错误", + "密码必须为6-10位,且包含大小写字母和数字") return False if email not in self.users: @@ -280,8 +354,21 @@ class UserManager: user.password_hash = self.hash_password(password) user.is_registered = True self.save_users() + messagebox.showinfo("成功", "密码设置成功,请登录") return True + def _find_user_by_username(self, username: str) -> Optional[User]: + """ + 根据用户名查找用户 + + @param username: 用户名 + @return: 用户对象或None + """ + for u in self.users.values(): + if u.username == username: + return u + return None + def login(self, username: str, password: str) -> bool: """ 用户登录 @@ -290,12 +377,7 @@ class UserManager: @param password: 用户密码 @return: 是否登录成功 """ - # 根据用户名查找用户 - user = None - for u in self.users.values(): - if u.username == username: - user = u - break + user = self._find_user_by_username(username) if user is None: messagebox.showerror("错误", "用户不存在") @@ -329,7 +411,9 @@ class UserManager: return False if not self.is_valid_password(new_password): - messagebox.showerror("错误", "新密码必须为6-10位,且包含大小写字母和数字") + messagebox.showerror( + "错误", + "新密码必须为6-10位,且包含大小写字母和数字") return False self.current_user.password_hash = self.hash_password(new_password) @@ -378,4 +462,4 @@ class UserManager: self.current_user = None self.save_users() - return True + return True \ No newline at end of file diff --git a/src/users.json b/src/users.json index 0e0979a..91d824c 100644 --- a/src/users.json +++ b/src/users.json @@ -1,8 +1,14 @@ { + "3154420541@qq.com": { + "username": "123", + "password_hash": "b17e1e0450dac425ea318253f6f852972d69731d6c7499c001468b695b6da219", + "registration_code": "926322", + "is_registered": true + }, "1426688201@qq.com": { "username": "111", "password_hash": "c4318372f98f4c46ed3a32c16ee4d7a76c832886d887631c0294b3314f34edf1", - "registration_code": "939598", + "registration_code": "769790", "is_registered": true } } \ No newline at end of file -- 2.34.1 From cb25220382a549c9873358ff79d271c65bbde8af Mon Sep 17 00:00:00 2001 From: Amnesiac1745 <1426688201@qq.com> Date: Sun, 12 Oct 2025 13:53:08 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend_service.cpython-311.pyc | Bin 9128 -> 9521 bytes src/__pycache__/main_app.cpython-311.pyc | Bin 49025 -> 46620 bytes src/__pycache__/question_bank.cpython-311.pyc | Bin 2076 -> 2076 bytes src/backend_service.py | 12 ++++- src/main_app.py | 50 +----------------- 5 files changed, 12 insertions(+), 50 deletions(-) diff --git a/src/__pycache__/backend_service.cpython-311.pyc b/src/__pycache__/backend_service.cpython-311.pyc index 4839076971a437dca919309289729b199b66f5da..fa4694df018373703ce6d90a96f767a4ed08d44c 100644 GIT binary patch delta 484 zcmZ4CzR`Hyb!>k=s|CV%I-z%Rqm*zmoX4dA1*JSdL!Ql9^?d$Ph@>WS5id~a7Ef|% zQBi7MNqlK>YEh9WP^d@%NECzIq);RTQX-O`S`v?}EH5!Pb+VqItUX8%i1Q4jLk&nY zFnr));1RgOEpvri^8*7Tr}YhC@hieASA-3(2wPp?vAV%8+fi2}0#xd!DL2_cel=53 z_~aMza*R_pvnv!c8Jlsi$}LF#zyPFh5@78`++Z_!i=rn_QgV@s2l61|ASeIgu*uC& fDa}c>D@vODL8;O{o|zG79(M90So{kPS+Jo1DIJJ- delta 165 zcmdn!wZffmIWI340}u!+yv~r8*vO~M$mp=yh|z(ORW6u8Q+{(Z(=i!lKTX-mD-~8x zj#QLptlV6xSj;5BAj!)00Ynsu0Cg8}0|`G(-lB-f(kd>DF_RNiBpBl-SE^LnhcYt) Q6=Ekpg2lh!kOi9z0E_7*3IG5A diff --git a/src/__pycache__/main_app.cpython-311.pyc b/src/__pycache__/main_app.cpython-311.pyc index 7d1fa1f38bd12b954d45d98bbdd6d5dc827737a4..7db10b052206597a5ff1f2ea316c7d37f701e46c 100644 GIT binary patch delta 1561 zcma)+e@s(X6vy9r?JI4eP%ZsM3oYf>7^0E=Vv6AWL$V zNZnipo^1c%ge?)5>74HV12mLnVPi{J;uypX91^xAEqw5Ke&9+o#$HgEAE|1Oos05t%A== zEQ~f{Wwa9;V;r$FI!GL&lQ2ALbT zYj4HkHEUF&VP(H960rn@N?;q)5+f0bjYKn5G?v-SIBL_WiTx6jy_|x(FS{R+ITV@m zp)CIY$`TZr>!Hm37+L1pCKFjx#+9atk$LM@y+<0>5IgZ)N?K{zur^6D{BvX{F*0Z#zzUe};A%E{FvKw&k2=TPQZ@t&2d&%d@}Y>MPuo~ zCRwhiZ7gkSsE}*R{1y0i;W>>T1fju(oA%x?&zP*^rnKv(oH0|*_om!&Q*KcE6Mep@ zR|g@S*)PLKw5TLfg0HZ&JRL^xRQc14;c^H3NGHn+qhOeBsOtcjz`^<)&3CMBvjaDm zSTvO~t2R#^uT7~j=75C`9o;X&Ep!HaFp94SlHfLW1rpeEHV`nYv=dBNATJ7dz18)~ zE#}B=97`O0GxFaYqa5=bypa40$1;bCBgl}hlGC{@upP<^%xd7b+-|r0BwwD#!nOpc zr1fnF#4V36GsokVGWU5J{>rUnnL~Zh1II8kcm+f{9n99c#&~o&$3c!kjxfg%#~1ia z=a=G`i1#`-8E^8H8IEC&S(<%D0;8Tc%sv8w9N??ppckFuvVcc05jyBQ_-7;x^Xze>{bgy=ElE)?D`MzTI=q~l8!Eu_@e;yzeza4M^;%@^Pkcj3`75s|FLbmJ? z9l(ca{SVc8z2X|og zV8|IGXE4EJMB|aF+Qtf5w(}gyq|^fL48I7O_-@#y&tgIBCo~M<)$kU)8!pJpkB0OG S;otd(vVQkJ?#dfjrhfo z@BF|2|NFoHdmo3s7Bzk=QXW?*5()Gy%KmDz?e!LAD%jC7!(1S8kXDigbeu!tjJL)+ zrB*2*5KlT1oQc*%ftEOAPPtVs(D4q1Q)yMU)a!w&<)AlFjANqbo4>;h(D@U)&Yqe! zDnVE!qEq(7S<`6U9>SVVry|tTX$UjubcC6-9$^-pfzUu_A~e!j2&d5ogeKaEFq@wC z0%0+655WluNa2o4HF2PryCi=}4AyY@>d!@BBbTZn6G0u9mDxpt6Wn{IwTa*om;A&j z5`4>Dp7)d(D7b&;?@0iwxyBMR02$n|Ql11>xO3SJ(T5}q`Kq{gpWO(a*&>TpzC|9T z*pwz>i_kbMnapB~B5KP3IvJKer%%&(k@N#EYvAQ>1?=oe)RDB#PgDWaD=Dp}MRXdiL_ySWajI646bqGpQV*L~;!wML zEXXHA@W~wW$(oXnLGUq-`AqxYK4OwK!IdiwK&VFbooeXpljiYyvPn7z)l9{zRU=Lt z<6`TXECQ8%GU*r~h!f~<^Z`T@45sN||H@2qIvRx;P_*h*HQt}WL%$m8>=|n99rV8g z{j0tNO`LNz1;8)h%WG}`2sb|eide^H!T$9oc&kK}qeDJnxyNN^%bhmHR&8g)@=6cO z+8K8_ma_Yh0i09zO9_LryXL}cWq+9+kr1gFye^N|74o{J5i!tOLGv>_mCIB4JhgBA|?`&2DlH_9!mhffiG@g!BOtk1{n!Hgm-Kfa1~OU79f0XlOEjQ?3>Eu z;0pH+(+t3;aCu!Z_!#c3GlB?rtnNl!aah9I-5!>y6pgk(zyyT&uzhnYjv(3yFW3wk zMDAFd2qbYMdw8otm;aiZ9QX8!CN`7?FA7R_r#pV!YKs^GNs62njVUc&5iG4wQBJTS z;SVa(;Xa2tj>(-|e{Y+h-!PQ|0y~*IQ?X+|n^449q48sd7DNk)9Yca}$HUilYg5Iv z^cZT%iC7!y1c8T*jx;s1ZL+9||4&p|ENZMTh6%xAyFo`w!X;RzviQJfQ?X(@CghuZ zFQ-yd9P^{^?Wa0ge6*jiJ4Yw4CTRI#Fgjvr_)_qAESpNrfeyE3eEuGt(grcw#5Ijt zpLpW*0>Ue%6*Sl@8vo3oTS4hzQLIGc*z@Iui^GT8hu%Cr)Y>`dZyw&=G~E5`#F)CY z&c@oD#>!fB_$%GzHpaEx&K4O5PaPTRK0S1*b+FqXwe=ZGM-O9>aWu++zwNKxy-~Gs z7@bCe?fx5Tj)97s8F-RwWGP+nW-uBl~Q<=DlA7lm8N&h%lW zy4udzS)1ElK4w*)vC8J&T;{2%UtCu=`%&%ls0*%i1iOidnxIU!OyD){?Fl`ZT#gO>V!B(zmTRu%btD zRN1>VU^u_Nh(D!F+^lViW5y{u1} z6;x*3RB87&H2L=Xx-@+%b5LczuGY254#+x|9a`Nxx3@BsF^|uf7gEpX)$@bG+dpQo zv}gL!*@2|M4@0Ix-c)$glza4vKmz`SOhvraRCHeqGK(J&2<{UgX%V857DX_OBs^Be zh!TA++n;S|Xl?MzZ%UQ>mCfFe)XYoGL8?!uEKK_59ZNm7{Pqqw(sjEJP4qM=;PDgZpQ)3AORG3;Ip zN@U35uRN>5)JK@Qhv8!k0~nI90)IwXKLT{}@QGkGSb85(F02yf9*gfmi#H1##!q0b^BxmqSj>A>nm*6YDc_J-kobcF2cEOc_Ou#+t~IsY0ky&LKiKVnu$T^)2le7 z(9}H0bgTn;@PiH`Sj!D`90HO{Sebi3O{HO~KKZ-1f}^&&*5j_`&UL0pfgb+xxKsND zHpjhTof!Ht+~!vG8USE8=Ua^=m^Wj$mlQa2mr^48qosm=cCWh7)FyQ}#UFkpsfB*JF)dF&MgouFyhPWlhS)?&w4tUPx zMF>Pb3L5~_oVz4evrog&IlY(#0Ny#bJi8Ppjb;0nwAfiodmQ$s*`-K str: + """ + 获取当前登录用户的用户名 + + @return: 用户名 + """ + if self.user_manager.current_user: + return self.user_manager.current_user.username + return "未知用户" diff --git a/src/main_app.py b/src/main_app.py index 69f39a1..644a768 100644 --- a/src/main_app.py +++ b/src/main_app.py @@ -7,7 +7,6 @@ import tkinter as tk from tkinter import messagebox, ttk -import random from typing import List, Optional from backend_service import BackendService @@ -381,8 +380,7 @@ class MathQuizApp: tk.Label(title_frame, text="主菜单", font=self.title_font, bg=self.primary_color, fg="white").pack(pady=20) - username = (self.backend_service.user_manager.current_user.username - if self.backend_service.user_manager.current_user else "未知用户") + username = self.backend_service.get_current_username() tk.Label(main_frame, text=f"欢迎, {username}!", font=self.header_font, bg="#ffffff").pack(pady=10) @@ -589,52 +587,6 @@ class MathQuizApp: self.show_frame(self.quiz_frame) - def generate_options(self, correct_answer) -> List[float]: - """ - 为题目生成选项 - - @param correct_answer: 正确答案 - @return: 选项列表 - """ - # 生成4个选项,其中一个是正确答案 - options = {correct_answer} - - # 添加一些干扰项 - if isinstance(correct_answer, int): - while len(options) < 4: - # 生成不同长度的干扰项,避免正确答案总是最长的 - if random.random() < 0.5: - # 生成1-2位数的干扰项 - options.add(random.randint(0, 99)) - else: - # 生成2-3位数的干扰项 - options.add(random.randint(10, 999)) - else: - # 浮点数情况 - while len(options) < 4: - # 随机生成不同长度的浮点数选项 - if random.random() < 0.33: - # 生成1位小数的数 - options.add(round(random.uniform(0, 100), 1)) - elif random.random() < 0.66: - # 生成2位小数的数 - options.add(round(random.uniform(0, 100), 2)) - else: - # 生成整数 - options.add(random.randint(0, 100)) - - # 如果选项不足4个,补充一些随机数 - while len(options) < 4: - if random.random() < 0.5: - options.add(random.randint(0, 100)) - else: - options.add(round(random.uniform(0, 100), - random.choice([0, 1, 2]))) - - options_list = list(options) - random.shuffle(options_list) - return options_list[:4] - def submit_answer(self): """ 提交答案 -- 2.34.1 From 829cfb95fb07cbd52bf61e6f9ea22c1ebeb5b93c Mon Sep 17 00:00:00 2001 From: Amnesiac1745 <1426688201@qq.com> Date: Sun, 12 Oct 2025 14:09:08 +0800 Subject: [PATCH 11/12] FIX7 --- doc/README.md | 317 ++++++++++++++++++++++++++--------------- src/backend_service.py | 32 +++++ src/main_app.py | 30 ++-- 3 files changed, 243 insertions(+), 136 deletions(-) diff --git a/doc/README.md b/doc/README.md index e6108d0..fe3d53d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,102 +1,178 @@ 软1_[彭云昊]_[王祖旺]_结对项目 中小学数学学习软件 - 结对编程项目 -项目简介 + +## 项目简介 + 本项目是一个面向中小学学生的数学学习桌面应用程序,提供个性化的数学题目练习和评估功能。系统根据学生所在学段(小学、初中、高中)生成相应难度的数学题目,通过图形化界面提供友好的学习体验。 -功能特性 -用户管理 -• ✅ 用户注册:通过邮箱验证码完成注册 -• ✅ 密码设置:6-10位,必须包含大小写字母和数字 -• ✅ 用户登录:安全的身份验证机制 -• ✅ 密码修改:支持原密码验证 -• ✅ 账户注销:永久删除账户及所有数据 -题目生成 -• ✅ 小学题目:加减乘除四则运算,支持括号 -• ✅ 初中题目:平方、开根运算 -• ✅ 高中题目:三角函数计算 -• ✅ 智能防重复:确保同一试卷无重复题目 -学习流程 -• ✅ 难度选择:小学/初中/高中三级难度 -• ✅ 题目数量:用户自定义题目数量(10-30题) -• ✅ 选择题形式:每题4个选项,单选作答 -• ✅ 实时评分:提交后立即显示得分情况 -• ✅ 学习延续:支持连续练习或退出选择 -• ✅ 答题进度:支持上一题/下一题导航 -技术栈 -• 编程语言:Python 3.x -• GUI框架:Tkinter(内置Python GUI库) -• 数据存储:JSON文件(无需数据库) -• 邮件服务:SMTP协议(QQ邮箱) -• 架构模式:MVC(模型-视图-控制器) -项目结构 -text -复制 -下载 + +## 功能特性 + +### 用户管理 +- ✅ 用户注册:通过邮箱验证码完成注册 +- ✅ 密码设置:6-10位,必须包含大小写字母和数字 +- ✅ 用户登录:安全的身份验证机制 +- ✅ 密码修改:支持原密码验证 +- ✅ 账户注销:永久删除账户及所有数据 + +### 题目生成 +- ✅ 小学题目:加减乘除四则运算,支持括号 +- ✅ 初中题目:平方、开根运算 +- ✅ 高中题目:三角函数计算(sin, cos, tan) +- ✅ 智能防重复:确保同一试卷无重复题目 + +### 学习流程 +- ✅ 难度选择:小学/初中/高中三级难度 +- ✅ 题目数量:用户自定义题目数量(10-30题) +- ✅ 选择题形式:每题4个选项,单选作答 +- ✅ 实时评分:提交后立即显示得分情况 +- ✅ 学习延续:支持连续练习或退出选择 +- ✅ 答题进度:支持上一题/下一题导航 +- ✅ 成绩反馈:根据得分提供个性化评语 + +## 技术栈 +- 编程语言:Python 3.x +- GUI框架:Tkinter(内置Python GUI库) +- 数据存储:JSON文件(无需数据库) +- 邮件服务:SMTP协议(QQ邮箱) +- 架构模式:前后端分离架构 + +## 项目结构 + +``` 软1_[彭云昊]_[王祖旺]_结对项目/ -├── main.py # 程序入口 -├── main_app.py # 主应用模块,界面控制器 -├── user_manager.py # 用户管理模块 -├── question_bank.py # 题库管理模块 -├── question_generator.py # 题目生成器模块 -├── quiz.py # 测验管理模块 -├── users.json # 用户数据文件(运行时生成) -└── README.md # 项目说明文档 -安装与运行 -环境要求 -• Python 3.11.9 或更高版本 -• 网络连接(用于邮箱验证码发送) -运行步骤 -1. 下载项目文件到本地 -2. 确保所有Python文件在同一目录下 -3. 运行主程序: -bash -复制 -下载 +├── src/ +│ ├── main.py # 程序入口 +│ ├── main_app.py # 主应用模块,界面控制器 +│ ├── backend_service.py # 后端服务模块,前后端通信中介 +│ ├── user_manager.py # 用户管理模块 +│ ├── question_bank.py # 题库管理模块 +│ ├── question_generator.py # 题目生成器模块 +│ ├── quiz.py # 测验管理模块 +│ └── users.json # 用户数据文件(运行时生成) +└── doc/ + └── README.md # 项目说明文档 +``` + +## 架构设计 + +本项目采用前后端分离的架构设计,确保代码的可维护性和可扩展性: + +- **前端层** (`main_app.py`):负责UI展示和用户交互,不包含业务逻辑 +- **服务层** (`backend_service.py`):作为前后端通信中介,转发请求到业务模块 +- **业务逻辑层** (`user_manager.py`, `question_bank.py`, `quiz.py`):处理具体业务逻辑 +- **数据层** (`users.json`):持久化存储用户数据 + +### 设计原则 +1. 严格的前后端分离,前端仅负责界面展示和用户交互 +2. 业务逻辑完全封装在后端模块中 +3. 模块间通过定义良好的接口进行通信 +4. 高内聚低耦合,便于独立测试和维护 + +## 安装与运行 + +### 环境要求 +- Python 3.11.9或更高版本 +- 网络连接(用于邮箱验证码发送) +- Windows 操作系统 + +### 运行步骤 +1. 下载项目文件到本地 +2. 确保所有Python文件在同一目录下 +3. 运行主程序: +```bash python main.py -4. 按照界面提示进行注册和登录 -预设测试账号 -系统支持新用户注册,也可使用以下测试账号: -• 邮箱:任意有效邮箱(接收验证码) -• 密码:符合规范的密码(如:Abc123) -分支管理 +``` +4. 按照界面提示进行注册和登录 + +## 使用说明 + +### 用户注册流程 +1. 点击"注册新用户" +2. 输入用户名和邮箱 +3. 点击"获取注册码",系统将发送验证码到邮箱 +4. 输入收到的注册码 +5. 设置符合要求的密码(6-10位,包含大小写字母和数字) +6. 完成注册并登录 + +### 学习流程 +1. 登录系统后进入主菜单 +2. 选择题目难度(小学/初中/高中) +3. 输入题目数量(10-30题) +4. 开始答题,选择正确答案 +5. 提交答案或切换题目 +6. 完成所有题目后查看成绩和评语 + +## 预设测试账号 + +系统支持新用户注册,也可使用以下方式测试: +- 邮箱:任意有效邮箱(接收验证码) +- 密码:符合规范的密码(如:Abc123) + +## 分支管理 + 本项目遵循Git分支管理规范: -• main分支:稳定版本,存放经过测试的代码 -• develop分支:开发主线,集成最新功能 -• 个人分支:每位开发者的功能分支(如:zhangsan_branch) -代码提交规则 -• 源代码:必须通过个人分支 + Pull Request -• 文档:直接推送到develop分支 -开发规范 -代码规范 -• 遵循PEP 8 Python编码规范 -• 使用类型注解提高代码可读性 -• 模块化设计,高内聚低耦合 -提交信息规范 -• feat: 新功能 -• fix: 修复bug -• docs: 文档更新 -• style: 代码格式调整 -• refactor: 代码重构 -功能模块详解 -1. 用户认证模块 (user_manager.py) -• 邮箱格式验证 -• 密码强度校验 -• 验证码发送与验证 -• 用户数据持久化(JSON文件) -2. 题目生成模块 (question_generator.py) -• 小学题目:2-5个数字的加减乘除运算,确保结果非负 -• 初中题目:平方运算(1-12)、开根运算(完全平方数) -• 高中题目:三角函数(sin/cos/tan)特殊角度计算 -• 选项生成:智能生成4个合理选项,包含正确答案 -3. 界面模块 (main_app.py) -• 响应式图形界面设计 -• 实时输入验证 -• 友好的用户交互反馈 -• 多框架界面切换 -数据存储 +- main分支:稳定版本,存放经过测试的代码 +- develop分支:开发主线,集成最新功能 +- 个人分支:每位开发者的功能分支(如:zhangsan_branch) + +## 代码提交规则 + +- 源代码:必须通过个人分支 + Pull Request +- 文档:直接推送到develop分支 + +## 开发规范 + +### 代码规范 +- 遵循PEP 8 Python编码规范 +- 使用类型注解提高代码可读性 +- 模块化设计,高内聚低耦合 +- 严格遵循前后端分离原则 + +### 提交信息规范 +- feat: 新功能 +- fix: 修复bug +- docs: 文档更新 +- style: 代码格式调整 +- refactor: 代码重构 + +## 功能模块详解 + +### 1. 用户认证模块 (user_manager.py) +- 邮箱格式验证 +- 密码强度校验(6-10位,包含大小写字母和数字) +- 验证码发送与验证(通过QQ邮箱SMTP服务) +- 用户数据持久化(JSON文件) +- 用户会话管理 + +### 2. 题目生成模块 (question_generator.py) +- 小学题目:2-5个数字的加减乘除运算,支持括号,确保结果非负 +- 初中题目:平方运算、开根运算(完全平方数) +- 高中题目:三角函数(sin/cos/tan)特殊角度(0°, 30°, 45°, 60°, 90°)计算 +- 智能选项生成:为每道题生成4个合理选项,包含正确答案和干扰项 + +### 3. 测验模块 (quiz.py) +- 管理一次答题会话的所有题目 +- 跟踪用户答题进度和答案 +- 计算最终得分和正确率 +- 提供题目导航功能 + +### 4. 界面模块 (main_app.py) +- 响应式图形界面设计 +- 实时输入验证 +- 友好的用户交互反馈 +- 多框架界面切换 +- 美观的UI主题和颜色方案 + +### 5. 后端服务模块 (backend_service.py) +- 作为前端与业务逻辑模块之间的通信中介 +- 封装所有业务规则和逻辑判断 +- 提供统一的API接口给前端调用 + +## 数据存储 + 项目使用JSON文件存储用户数据,文件结构如下: -json -复制 -下载 + +```json { "user@example.com": { "username": "张三", @@ -105,28 +181,37 @@ json "is_registered": true } } -配置说明 -在 user_manager.py 中配置以下参数: -• 邮箱服务配置(SMTP服务器、端口、授权码) -• 用户数据文件路径 -测试用例 -功能测试 -• 用户注册流程测试 -• 登录验证测试 -• 密码修改测试 -• 题目生成测试(各学段) -• 答题评分测试 -边界测试 -• 密码格式边界测试 -• 题目数量边界测试 -• 邮箱格式验证测试 -已知限制 -• 邮箱服务:依赖QQ邮箱SMTP服务,需配置正确的授权码 -• 题目数量:建议10-30题,过多可能影响性能 -• 网络要求:发送验证码需要网络连接 -• 平台兼容:主要支持Windows,其他平台可能需调整 -开发团队 -• 班级:软1 -• 组长:[彭云昊](202326010111) -• 组员:[王祖旺](202326010117) +``` + +## 配置说明 + +在 `user_manager.py` 中配置以下参数: +- 邮箱服务配置(SMTP服务器、端口、授权码) +- 用户数据文件路径 + +## 测试用例 + +### 功能测试 +- 用户注册流程测试 +- 登录验证测试 +- 密码修改测试 +- 题目生成测试(各学段) +- 答题评分测试 + +### 边界测试 +- 密码格式边界测试 +- 题目数量边界测试(10-30题) +- 邮箱格式验证测试 + +## 已知限制 + +- 邮箱服务:依赖QQ邮箱SMTP服务,需配置正确的授权码 +- 题目数量:建议10-30题,过多可能影响性能 +- 网络要求:发送验证码需要网络连接 +- 平台兼容:主要支持Windows,其他平台可能需调整 + +## 开发团队 +- 班级:软1 +- 组长:[彭云昊](202326010111) +- 组员:[王祖旺](202326010117) \ No newline at end of file diff --git a/src/backend_service.py b/src/backend_service.py index f8e2f43..bce17ed 100644 --- a/src/backend_service.py +++ b/src/backend_service.py @@ -227,6 +227,38 @@ class BackendService: "total_count": total_count } + def get_score_grade(self, score: float) -> Dict[str, str]: + """ + 根据分数获取评分等级和评语 + + @param score: 分数 + @return: 包含等级、颜色和评语的字典 + """ + if score >= 90: + return { + "grade": "优秀", + "comment": "优秀! 继续保持!", + "color": "success" + } + elif score >= 80: + return { + "grade": "良好", + "comment": "良好! 还可以做得更好!", + "color": "success" + } + elif score >= 60: + return { + "grade": "及格", + "comment": "及格了,需要继续努力!", + "color": "warning" + } + else: + return { + "grade": "不及格", + "comment": "需要加强练习哦!", + "color": "danger" + } + def get_current_username(self) -> str: """ 获取当前登录用户的用户名 diff --git a/src/main_app.py b/src/main_app.py index 644a768..ae3dd42 100644 --- a/src/main_app.py +++ b/src/main_app.py @@ -663,6 +663,7 @@ class MathQuizApp: # 计算得分 score = self.backend_service.calculate_score() + grade_info = self.backend_service.get_score_grade(score) # 创建结果界面 main_frame = tk.Frame(self.result_frame, bg="#ffffff", @@ -677,31 +678,20 @@ class MathQuizApp: result_frame = tk.Frame(main_frame, bg="#ffffff") result_frame.pack(pady=20) - # 根据得分显示不同颜色的分数 - score_color = (self.danger_color if score < 60 else - self.warning_color if score < 80 else - self.success_color) + # 根据后端返回的颜色标识获取对应颜色 + color_map = { + "success": self.success_color, + "warning": self.warning_color, + "danger": self.danger_color + } + score_color = color_map.get(grade_info["color"], self.dark_text) tk.Label(result_frame, text=f"您的得分: {score:.1f}%", font=("Arial", 20, "bold"), fg=score_color, bg="#ffffff").pack(pady=10) - # 根据得分显示评语 - if score >= 90: - comment = "优秀! 继续保持!" - comment_color = self.success_color - elif score >= 80: - comment = "良好! 还可以做得更好!" - comment_color = self.success_color - elif score >= 60: - comment = "及格了,需要继续努力!" - comment_color = self.warning_color - else: - comment = "需要加强练习哦!" - comment_color = self.danger_color - - tk.Label(result_frame, text=comment, font=self.header_font, - fg=comment_color, bg="#ffffff").pack(pady=10) + tk.Label(result_frame, text=grade_info["comment"], font=self.header_font, + fg=score_color, bg="#ffffff").pack(pady=10) # 显示答题详情 result_details = self.backend_service.get_quiz_result_details() -- 2.34.1 From 256a0cc934a3718c4506438b9e4470e2c8a00749 Mon Sep 17 00:00:00 2001 From: Amnesiac1745 <1426688201@qq.com> Date: Sun, 12 Oct 2025 15:26:23 +0800 Subject: [PATCH 12/12] TEST1 --- doc/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index fe3d53d..75fb071 100644 --- a/doc/README.md +++ b/doc/README.md @@ -2,7 +2,6 @@ 中小学数学学习软件 - 结对编程项目 ## 项目简介 - 本项目是一个面向中小学学生的数学学习桌面应用程序,提供个性化的数学题目练习和评估功能。系统根据学生所在学段(小学、初中、高中)生成相应难度的数学题目,通过图形化界面提供友好的学习体验。 ## 功能特性 -- 2.34.1