From 0d79d4095261623e0db20f1f27b93610be9c57b4 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 10 Feb 2026 17:43:37 +0800 Subject: [PATCH 1/3] =?UTF-8?q?1=E3=80=81=E9=80=9A=E7=9F=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=88=B0=E5=88=86=E9=92=9F=E7=BA=A72=E3=80=81?= =?UTF-8?q?=E6=AF=8F=E4=B8=AA=E8=AE=A2=E9=98=85=E5=8F=AF=E4=BB=A5=E5=AE=9A?= =?UTF-8?q?=E5=88=B6=E9=80=9A=E7=9F=A5=E6=96=B9=E5=BC=8F3=E3=80=81?= =?UTF-8?q?=E9=82=AE=E7=AE=B1=E9=80=9A=E7=9F=A5=E5=A2=9E=E5=8A=A0mailgun?= =?UTF-8?q?=E6=B8=A0=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- images/add_new_sub.png | Bin 0 -> 60120 bytes images/email_cnf.png | Bin 0 -> 30575 bytes images/subscribe_list.png | Bin 0 -> 118586 bytes index_zzz_0210.js | 8836 +++++++++++++++++++++++++++++++++++++ 4 files changed, 8836 insertions(+) create mode 100644 images/add_new_sub.png create mode 100644 images/email_cnf.png create mode 100644 images/subscribe_list.png create mode 100644 index_zzz_0210.js diff --git a/images/add_new_sub.png b/images/add_new_sub.png new file mode 100644 index 0000000000000000000000000000000000000000..a773929302d183fba6dca25724e14e71cecf7632 GIT binary patch literal 60120 zcmd43byQSu95)DvgwjY2-Keybbc3W|P!hr*Eg><)kW#`hNQ!g_sDvP)w1NYO(v1u; zA{|3F`yl?__uW0ad(NKSKlU6BXYSm4pXa&Hx4)myo#?xI8l+bsS8#A}NbhK>8sgyK z<>KI6iXy@T-#A=rR>8qx!?~lXbRUMh(&*&&U?gqtZBKx$GXB7 z@7|Uf!pxJ&U5to^hFqDBm>UOHUu>cZ7N>RS^R7{kFMT4vL*X5T`lm{ zduO?iYgKWKmWmCRiY;V}A_sL3bLZdrG;qFiWn^sS=vC^)9Sryui!j6JtAgNR;?4CULC1J6-+>*Wc5cB)E>U z;#q%(4aQHnGo2hX@$+aO>g{5^o9j&U_c4MQLu4RluJtzl^J^0^DS`fdhJ^PDg@TSd zMzUe>sSxEY7CG9=c_KJVD4e?(1v@?8dV%5U!Q zf*E7`DU5f19rgsA#(*0iIpfulpB-uk?a&mY;w#Yx{@Jn|yl$We6NH8^ko=QD3nIO? z+w#-)Qr4x(vpydI|49N-ztdKuJ*I_4xa%L3?$KKnoeYT3P>d;KlZOQ{xz@(2nl>qJ z3Ta!P4e#k}$Y)gk`Fxg#@5?jfZ=FALyoMhmu5{%=yaz?J;(i=EA6;d+-%~kp7BTvZ zJ@3UwFZue|9IYW%>!+jxwhw=hp4daTzuqign;31XZ?sttC+FSaoZsl0@cHKZkQJ)Y z5q-FPL^f)8lGYPs@Ow<^I0!z4Tf72R&@{He{AnF*DKg+Bx}w5;OmP35%nwFJ#<>LhsV5FzffE&Ye^ps_0jlHR<7u=vLps?)+I?02d{>7nw6eS>krhjqzUQc&S^ zZnfS5-E_0&2$nCh|H|l~N_g2V3Z*T|L?7Va*Y7G3z?+9^6_&AbYN(k<{WSA<9xT`P zZhqKi!X5Lcv_YWS2}*LI-m4V79)jowW@b=n6D6K?h)MC`H?BdqMB57VT64{ z$ExL7YB>UdaLOGTV-}tp+Gmt>&3!|Z!{I1LI4k z2d^JJZ*8Y>YHGB93#;2yXR!zxsG%*u+6^M=;oWCF3MM;162i`(+_zHfn+ry=Y9(6t zNaY(Hi0VxCfd=upPTs?iW*uw(H9RZo-pE}g$!+g4Wt-VS*jxXhXUo{r&l%<_X8|<# zJ)%)DIxh;fq<$e&8`L4CbAE)uMl;UyX7{e{Vc=Vin73F6PwRyNE8%yOoID<=sUkYg zk_?2ri8Mg=&uJ z32ZvPyfM1P$%KYjQm<<#X3s!Oe&GkxRAxRZpYxvz74zXe@MRUk|3-caIfD&JBWAuXv}% z5?MPn4ECQiGz=jv|0uor&v+H1gXW)h)nuM8epS+~>W@RCMg!4@V;<&$Yn%L&w(pcNiS@-w#?xkk1>*dSkS1W3X4zK^y zIIg>*cuuctWH%>Eu^B^*<=ZY06zno+c7Wbb`X!77X28bRdm;MXYAU9~g z$Q*Epxe{^2<4T^kxkU%xKbv%I!A#Cc<=lB^X~%XDXI82#2xBYZlK z;Tvaga#3mHyLpw;z7i6~!aafUM<0ia7EX-I%w3OwdCql1 zE5Xn_g1Qb{^kvd`m8h|s5Q!x981QUXRrJX5(<<{0Xyw}!udP2qN!Gbo!_*3DW+}N( zP?L}7uisfhA+FQ9yX|XP=*vj)?|!rI>$B#WS7j=u8K=I+QKMfL!`|6ub~LZxd|)M= zf3l7Rze)^DHPNuS~;-o|Mk z;G-{e%)60`x}^4#V0>H#VXI2Z;J{Y=MZqH?(=!jhdaj=)UU%R$puomzot~Dqc<4DS?>)xg7Fl2~Vn~!Y5D(BJ{q9<<_AtSw55doQ%(EGUvmR zEXXy~t1^S9_`(HVei76viYaV1Kj=#n1@c8az>X-$sU8pXHNL#MoV9*~H-Ds<&Zp4t zMC`>}J5Zi<)YbBwa1y6%t^wX`R88&bnw%c8pxm>@?`n4M;-yrqL2)$Ur`#51z zGIk1&4UB&272R(lD;Es4ed9mDHDGpFTNwY6teKLjWBlP_G<0Mw$+Edpvuyf zwZ&xdRV;7wNcCAS&^C=Dy$<143+lP3It5_;K>5Cm^MypR!tvJkxU*y#?J4-0QIdjB zO+N<4@V^}`w(hkdkI8iw!x7TA6&({_uGKWy-^|jX!v0vH+m6lK7s189GPK{GeQYsb z)>!GMoQxH{_Iu1&41uJtg;=34ab>=m;i9DY{4GMi$>Xh&FuxN@v7dEL+5_13{+=P^ zmk)7ANU@X}*s*|i_LlE~*V^@>?AXTeU%bH{PGnbFFJU7;`ZXZ>CkJA-0v_F+NpF{K z>|^bPnf$6~!*E>DE+ui7!(J>z7-w5@B4D~Kta5^Ih*DS>cubeao6AS$7Am6xfNmjC})%C;VRW zN?Ga8;p%FiLepUZSc!p8RpXzf(p)KH)7-h72jo^6Bwy@Av&O4-T0Ulr^1Q{bqRL0O z-KGe`X>uy+*}~rUSCYmpFn`xtcrSyyQ!pksuuGM$%MC#hWrDHI6|?ZG^?8|x&8*+; zPH(2jzeg=8k?VId)NVpG%xz_yFD6*6XmAzx_GNMWQ6tQH1N)hAzPv?$z&8Ab-pY<< z;)~GAXOD-3op||gCX3uqjX-D~x8zkY!1w!jV|VUVc16n(NQj}`%-y=8Dfc5^K66`h zD&ib9JfECY>05Q8h~rFqD;aTzJ?-4EiUMEb59-EEM@0LVV7dBOzXK5l{B1nm)SOXD zYxks-uSiKqoX~V~^V7j?Bm@_GeO~pc(nF{<; zZB4eYhY!_CJdE!n*b#Gat>q9xeXMhw2`0vlPn+kz^nou5N4$yck&e#4%;Et^!Yswj z)l~jVj@dw(7V0J<@-Jh#PYMF+kS7f(_rF{ql;|$z>ep{=|I)g<;vm947IM z0+LT5B0B2-($66ZpaJ?idgNrt-E4@oh+9e!7e%<@+1uPNGkwvWi(~rCWX*gnX(+Z9 zN_(7{c0;Nt_k9L!`yUX6XP7_J8w(wuV@5>B9tqmrMgs{9h54 z@ps%EkHKoHbtH;;P>G7FcwkZB38JE3#WYiH=7#u<uKk^;<3CTp+r)# z+>!j3)7sj^)mk1)-G;usUf}HPi6;`r?HJ&>qGKYBN2c@aV`cz0HMZz1&FCLzRK=|(C(4^Z=A@T-bvt*wRW;=R~zIua&u5%jPp>b#XZKlO886>pHxyp zb*yJ!gYhz$DhIC_N=Qimokm{TyO5vxVyT|1*an?He)CR*aoMuBw!e%Px-4RNdy*=> zhad1=%zpR%lf={g&Rdz~%pK6OqCM7>7H2IRH0V^h^BZ7d#_#d}dKuhG;nP7i`6-ti zRkIpkeLT}hD-wQ7&Z?th$a=Ejv z@C#lbWIfcbpVjGfYg_&MihRFa@92;hU*cz$3iY0?P#krkFPe+z4?>?D5x`%3HK2>~ zkLdA={HB2AZhoCpfVLo?2bk8s9lg-{wzoSuI%VXEO~;%|sP2y~*{O~OHMKi`w?_lF zTkH)F*y4P<+ku<07S`aoIbTa|e=+vgNG;D%R)&U;k2X!5UZs;T@$+na{dWBY=S#3I zV$%;_y9VbtSK`%%D?#r{rJJRt_h=wGyk^Xsi7-&%7rKJ3&2y_Bx$WXS)wn)QX&>Ns z_BeePwL7_Z5?NuSNSOQPBZ(@jUNN6FEw-Sdy{tIx@L>i=C}6mmi0&a1UB`LdH37Jh=J@B^*q6rplAx0s zhtH9jhN)p~Gvba{#hISJ*T%fL{goGblnf+exJ1eKc{@CY#K5`Fxq*s0w1Yxc*K9Mw zS>Dr(>0Ju{f(C=>Zf>Y~Wp2It@^5J!GHp+-bUSofj6kSJ0e8Nfugg7V>no-|zjjSE z>Hd)fBOjY7I|itFMJDPUWb`;bGZ3`YW+ru_~cFVzIP&vsv~ zzK-njBxh6;M1r1~_I@?jB~nf!C04{kw(^S0#AEjzxD(y>UN*&Vr`Nn8KVD)saa<(} z#eEtQ8j*P4;4bYKuf6VBh4RR4d$Aui{uK54k9^r;+^IN;Ln#;6Sk3Wu@cm%r0l<@Z(Vh(4mGii_Yil2G9>YNOZM~Q0lKLV7E$>Wzm4(Jcw;IX zdzXjDuHM|PHP(f1_ zpec)1Xy@Rj;){t%Q8#(!o={HCSTSwQKG1YIg@%9D?*QePw=8;ihbhJ_ZP1oL{ICmM>N!zQToTRHzRj+G%dRstA>+1L&ZG$=r^ZI8}v&JtB<;CT% za2H+|2QJ>?E^Q;x(#}o$gsv<5yRLD|jm;Kf;%xa8gWBpE z0tO>;*{xz;nGjro=ouOTzi;PoN-q#`0^HuYW@2c3EU%#$Y3c;W_%yxz_Hrt;1=4F#)u4&0>p z#`WrL9ro?r#Mq}H3pUqX4+i%eqF&{YfB+6tGTA0c^-|&~vM_;8`tCJ`bdQjc90!h` zk}@@3aZ<0N;4hIHA~EFb_<&aF(6&absL)eq$SGK^#+<7*pL>>%j+grdc; zw~qGBmdLjuBWbnRl}?Jg#MGJPA<~XwKS!%I1?8;!)`j05X=YCv9!ud;5Zl=tE^8*m z2KT{%bTI*!uV-HBYd1vSD8~9yiYPIFoCO!RbdJIUV%)W?;c1m%NinU7LG7H z%024wSMhF2t#`ghXQ`3jTJ(wKh?UOdS*YPIt`8gM-l#C@O^6K~8}iZ&gbJ^lG>T|` zh*2}MB9Nen62bFUiLEHr72h0ZtzcKwh7WWEf5~bYC1)V$P$!&QXbtdW$m*7-fb-9l z#z3;^D5;Y|fUgkZi_tN#=}h$gIya<;Lwj9z=K&=1vLWwvTlV-5{Go=0Krki=o;2)% z4WQ%d_s)oQ`%x|=$S0(J>qPFl<+$}o_@)=mO=s17_N{6&ADDm0V>u=ToRd*`OCYicfh36YNl2N7rFbaq^@)?@BoFg@7LacsCj{UzD`o`0we)SuS33iwI~*`exr$#%JB4} z{1{b+krOgk)*P)Rgx|J87Pklg9D~JB*Bc+nTa<_{Rr>G>qc?)X3d#E?(j^31L^C+L zA3D_A*XZD{PW-sKpgmj+Dx+HUstWk|rQ8u~ zjy9ggtQt=Hre}Fh4fOe2OLbh%zryD;?GHV_e2|bW|C4_;!x`y%JR{qDv^#%~xcP^6oz zbWtwF*7U!4!TC4IHhQ$)&(^bbUW3r)@r1Bl6e)LCW6Rwy>L~>B%@voX^P<5p7g1zF zqj)hya76!eEv}0ptO*Br&a0{Lj5}-~P=j)d>b?xdDMK;VCK_Uiv3aYUzJ6XP-M7a$ zJiJWtx0bq*lPi`>bx|oLkAA7@JTJD`?Wl+@Y9Rcf?tKU^82-gQ%#l9noX2O#I-^9; zF22X-N1!fN?wZ-SnM6N8NI6YJ!qK`<^|zvo`1TeX)|wBh&Yaiu`goMYkGi0Qks6#l z>Y`){IaXFF_)>Uw#PMTxDlIPimxt*8t8D4-Q)BM1ahzASygWt!H9(tUWX%vJ;R?w#PGRo?eqrg5F%pF-Q|saVG)74}2emNd;aD|9?D5l18fI{uW_%l0rrSbPs7dmN7*4}q_4y%ql; z><|BQP@TRC+4E#}cA1FVgeO+lxG4CQSoZ7a2MvzbObWlPcB!vXiiEFfc==v3+}YmN zxM}37XeQe;03WG%R*w6)Z{TJNyDJUNE6}zH$y81Rs7Zp#^g=~oX#iNS@^90LnwUTm zQlwbv{^!!yrj+pL{Tq3Wx1{_^g65V;d&(!B6+%%*DMByR1$H%lPY!t9O_K75KLO|L z-*QpF%|H8vJ$(8+JmG02n*6Z}n^|OBrax(CcGl@a+TiQlnU}*gtIRTcOS;kXzJ6!9 zt$EnI!}Qv<%6c5#_x+%=q$Lyg1ayAx6XM!8?9!=?o*r0^D9&WvDfQ*4R$nKmIv&-f zshyxO&@9ml0z13HMKUSABJx6+xKd$Z^s^Baw7jPZY>C3@W3bP#R`5u9?A|!-B;do6 zE&-I$%QuV805cV^+_B{(xqJ(U&&O$ES8_@^FVl-LMxOI!W_zE_iAUDssyFX4VdurC zpQp&*R@WLn38B$8355Ko#G@bahpHXbf?C!c5_*?aKfv!PTe}OE&OJ)EGvjz6Xo{|Q zwtj#mH!4H3F;bf4dd=pTMd!C4u4?1LS)K|H$`E8w$ToX#rMogy-l0w3*;Ny>c#6w~ z-q`29Dk!9iaX-Q-SP*iyb(T+NRY8;=%jbmyW=@rGT{k2^aY*twq z*t&kfx1`G7v0jqwHscDkV=8aJ_MUDvqi-v3$SG@x-d?lV-oAqU27iBb1XgiR%;Gmg zJw9Axt?TaZ;id*cMD7e_YiX*+VHImMWxNqXp#SmU_aR0!*dQ=`(-Ao-uJ3z*a+RlK zAfM`mVaAQ0r$CzvS4YjG^gmS7@xL-%SSeYK8ivvL>eFBYHu zT3Z%#fy{y8#OA|%3z*?!to(j{;qv71??~T}_wBFjqyY_iLu<*bW&jC&Xzxta8^GLa zw=T`1L3b#t_|91<0UoGge%Vz0Tzxfj>WJ)_!{wDQ zYCdXKuOF6#PVpadE#*xee7*e9``P8F{kB=l7rz`y2dqhg$0bx>EW9v6j7$R?TJb$H z_q8F@YnHHBa&(yxbeycPaYe6Ri+X!V|ET^?U5FIAS0F+7KEtjyPC7fMMUd(;m*LS# zFYFO+!@)4+V$@GFABI%Cx*zX|Tt^$=t$$Vr_unn>L+;>l-R;9Cfmg0$c4%BbvAQ-m ztS^6$Y5RIPy!!cx(pBTYaz61*tLDD9*A`jjC#q)W-J%d5{a!l4t)8}ZXkeY@PGo!;yGO$49Ghx7M`qQA zVpF?45fnD1Urc%4Tv?&oFtDUm#8l9k0C`TTn|0*dH`LsM2GFN-xx7Qa*393oXamM` z52MnuNqOGo9TdYQqG@{1lU2Kmxg7sUjj)kj2phroo#pE%{=IL^W!P~VRCFD!a+NX% z{IW$h-}8NJFYMq8LnChg6MAm?IH_^_V|nsfFU4kmK5QL_3P!E3qEtzd&RFg7A@;$b ziC8j_u_KXUAE_(Ni~jK%^X}bm-;&mm2+LO=f}{ytB7Q8(z)D|gIuL#px9E?NLp=`0 z!rhNv9|Nxzkg7it;HK^d8+_E}v({WAD@4K7BQf?}+=JkDsq#g}d8yoDO4 zG2x3t2#7D`SlpIB7Y%H#6acALn0H0|e}XRqr;WK3{PTjWpve!!Ze5>3dO;^Bb_bnz z2rG`W`~^I24X|qJiE3y1i$er;4r*i{$7<^zc|se&`J-jFt^a`c4B&iGggEK?f5TwB zF&L}OTYyph@8MkxXwv`x^O3*x?}rS8onzzx)6SX~aDBSCwkWO$@b>@!tNi(C33!In z73!VnVKe~jt|xZ-Wuc*fEPdTMw7&mXWl_Gr zwgT?cVF?<)&*l|Y0#+44TdbZjZW**6c#8ymSQpQmMdoZa=kBiKS_Dfg%qH*7F<1wH z|FVc~8)_-`-23q*en+VXZhq|?`M&m~Rg98y=cuvX!Iqp=L*< zh`k+I3WlnroPa4so@a2QkzCO9-Vkd%`>;^g$WC#fp+#^$75zP(SV=L!$Z%QB9HsW$ z+x{M~Jp{Y1HUI6qRd&zgZV0a2)cdRarQ9Kv=QtIh0U&NW1U&y9;k?9swR`D{8ZDk& zC*lp^b?#n3Pk7o$X}O?bhLP(@zaS~j4o?jymsjk#+3efxo0%PV`+qM4t|%(7w@ zme~Nm6B(-PHm2Af%R;CcK^1HPCsEM?PsAwzZ~!tU9VD6Sy<8A_6tnGJ5OyDbAkV6n zuC#_nO8`Oz{@hWB)jQT8RHAdM0X&+7i*~CF=+DU9_A+s5$e8zO@@7l?9M>=i%dyJQ zB5p}U$<;m8j7RwZ4#d5Z(ne|s^3IK>rp8r?VO;aK4Ya{s{uoMy1RP@3b3TMk+J~aX zxR+y3f4HpTchm(N?N0NlIw19Ig!m5My236xP^!(h()7zGs!=h7+32AC1^?#Ohl+$y zvuQIa&O13@iS1p60e@0f?+;g_jg0`Rz(8mkznh_)OWjLpq>0)M76i&&{3TkH*oqs&Q=PcYW z$L(esqknN&=DvJWP+v6iN#;4}uC%D@sd6J@@B?)=(2wd~x|Oz^crUefH9+N;4;BLq z0w6HMMmy>Ufc=@QgEP)gO={^vx{29AK^#;^=3-A$ONGuJ|7H?nagi)km~~OGC?}CF z`6vu>IRF7lAT%!f?Gl8qvZ6$ul;>hMpD}Gv{eIXp?A*8i=@?ek@d-(188p4u3OW%| z+3h~d2a~tmY;u-}qEO`~&9;E>;#Wj=l0rLkU*`$a?ypg^Wq;2fT2qWZoq6WIjrP+c zFgKE5wVb8Vek&@zt0DX17O&Z<8J`7$8%_PHpKBjdAV#9G-Q8fTek1(M`5#gk61iRkL=Y53u(?CM;U^{vH|w8uFTZ z!_dE7dzBe$AMjHd+WH?#j0b|#%@6nNss6#lZHmDV)=R3i|5af$fRp%NeR3Y5*a$#C zqGzil`o}aYRs_daR<6BP{a3b$02W?5&P%obm~)pwR5r3I>gD=(LsW>Ofmoomho2j) z>TIw#I^?7H(MXV^y&KHa+C$n;=%v}uxRf!;UymCuBXr_g$9xF2*#MZ@CKZJ6W&Vij z`St0xl&rMJ+AIKYQ@a?FhX&gp3Hk^mGT-)b2V?v}{oJTYGy^d5Pwku>EbVmp` zCxtvZ^!slq&;x)`@p0qU9xy^5XF#Tc9E(xw(_wt`e?es>{Z0We;3Uy=WNnV z@ngJ3#e<$u$JpT38~`cX+#P#F#Ru?UUQY3*V>qpiPfD>!N#QKO@oq%&(;!0#5)>bR zHjJntMq3O|`H=s&L3FH*c=W{tTx5)ls(3U?Y^Y96gpdI;V%wx6pNgT~kqQ9McW*=Q zFi8Mv-;Gek*u<9*%RH99({qc)4ROW&ojyUz8L$NJ<6lg#DhKJeV@GO1ncm-F!i`j# zi&wKb+88fQjnhBf-+*;Y6;j_z9Gj4)JE!AHgqY~5o3n{q4DeY?74#^xj1kz1j>uTb z0GPKYkW1vk4cRP&i5y4)oVg~`RY%X9Osi*Km5R^)E-`K-zoO6S_n&7DL(z3XzC!C$ za?Mh=;d0GCB*nxE46i2%gT$ufR7#w~A{%b$zlY^O`&C%0iufMTx%{MC-3sA=| zob_UnuK-ShB0ERC{o5^!D?u&-+O$&%wqYFC&p}xkBsjlmKTspK%hxm~EGiTdN|2oN zC;svi*-cj!-yvpVmWdr)(L;j%#qCD0nw3!zzB>MEyJD{P_AL;Ot`212HYan~hu@80 z;g*(DjXq}xB(lixI@gCwmEEyO{Y5)4@Sa9gIu2A!>>L{c=oNVC5Yi@GO4#5zMB3GGItnj9imrDF`$3h$v|f01 z;Q6x{Z^>xF&@HK^`t1443(L#(^I85tVB00BP$fawjbzc?ldSn%rur8-YMfl!OKS@C ze9>PFsgr`STz4r##&p#4^ET}RqUHt6_74yJ8ZY&JoP3p?fcBo{Hj&OuAy|fd`D;dZ+z;QvyV7QCZC$F|$W5D?btZy47)9J@VoCAEEo`7t87=%f91U(q4h&-GR}vAry{?}RHz)g4 zK9I7Hs4*n=ZpoSut&G{XFL-Q5jFg$185!;Ms*o?9w5gEG-8V9-ecVa_B{xx2VPUVw z|6TRyukrw3Q8aCt<98Dz2nfoamu#1PiSOR?VL;I$ocv%Se^~d2!b9bB&QnR|q{&uP z^ts};Qn)fxC7Y1gsiYjQx9ia!_T}661GoHrnEY%;%FFb6GJ?+f*Idp{*Mc&VbQQr{ zIw7LvbMh=2?v>1uW%-UeF3e%Jsv6(x*cmTRd91u47LhJTJ9g+!@sCiraJ6>w2>Unk zCFz4_^f~&vG{4q?6F_9@8EO^v)=$rkGyl+VcVVXZW8f6RhdAJq+*E(9U97R@|BQ} zzWf77G>4&6d$H!C5M&!%!Dqx{b=E(N&UOmt68#5aRi^vW`6{$fqyfggi|1qD@^IT3)TQ`(Ngh9uKEKI@YIUaFjI`p$KnT+2PO&>wPRW>`qB4@yTm=$KzmwC%x?z6%~Z60UyfB)H2oj4aHOYB1feGmPZ#AVVNM9q10R)^Wr`uqlo0#_+9d z1H16?scj6Ctjwpm>nRffA?G3akpR>!uXnsF2YXlznRv91-PGi1P#xc5qTBVpjjGac z*frb!I0Cwg>KYoXuxZ>eb=m#tFdLifKsn}!aQ+;2Wc=h-1Dp`q5z+wL$V;X0AYM3v z!F8Xm6aS9!rkwg5#^5V1zwl~u1eyBL&r%_)d^qg08QW}5f4B@($=kkR-Q=WV26U2S|0~^E z^I4$Tv|P{Ca}!}miHc|4ACdv(>YQ632r{2*Ys7)G@U?6_(qt9HxELTukmS-&=(e^G zLYM?JNd*7&SsV$-%36c&{X*E4H@Bfm7WT(qMy8&+_}V(}w*nIpDxa$GYApkwB7=fH zuUVpxSp82a%CJV|7B2SoQr;`C8fb2oeY9_AY#vGV1guoS4S`1{P%gtFpMibgtw4OJ z_-Pz4YpT+c7}7@VHXNhenWTYH^Q``{yxz{pyHP1p{{H>PdEY43_2o>b8N%!Q6jAPE z4QHoE`T6-xV=A=0@fzv|c}Y@%bZLWRjk_4`a;e@=--IGi$js*ART7Ucq+`Pxc&OES zu>q;kkq5#VspsaN_t)G5ckOnhXi~4sN=eawL()mM?28MO2&33##pE;foK__h#w;t_ z-{B!r=|9uC9Zkv!Lvx0SJ}s){CVkJxTE7XjBye=u$0isy+aPJJU$d(dZv2Uhs}i^H zW<;$i*+t)Estk^h6?Fu~c{Zwx(+tCHan_5N`Gm=jDw}a+B(Q&s;=IipkMv+gnrnhs z-oPT{oBQj~+)sq+0b@Px{We8Uygovj@17l@4ZX0bkB6b?_mQZWXZ%xIl270Xr1QyP52_J`VKAWPb(>7bfz*A`?FnVpwfURv6oRCoa@q9-a=5{Z4q zlARFO26NY{<2ccigZ0q~V(VDq^V~H8w--j-!aLkK{Yx7n@a9~Ni?E(t$1roE)*(H% z!yIJ*IkByGPMroGw!SwYTPhN?V2~1tiuG0$3!JR;+Pp%>WfCN$o;1|`-X>IxWqwt< z;`yqIssUU?-Tzn}o$&dJdgJk{`{#Q`koerGp>JF?kmT>oUwUC?ujVKp{qF2UhMsPD zs#{?44LB7ZUO(Rs(sTLT8gXqH*qZowDSsY)Cj5JSWnDe++3)?I>9z5FWQrI%e&b-F z`pAr-32sfDcogFzg3g1Ky;ja2Dk;Gl5w3~CA(o@iB%&B69W>H+p3R`Vun=yh~^6vR^YlCdbU6!ohUsOk`ClJGrc`+M66PfFIK zzqe_798}nH-POYr(F5=ML`(k_dvb@=!$hfz^Rpi+l(_CvUQ`KGJND-p!i|^iB|Pr< zOa#d)DQ%B!!ZdR@K#|4p77$PS;^nXU-siv=>&^m}1*j6DTTuir71e-Eo>vKj`R8d; zu_~(KbsM0Z-X}AmibRldguGy*z(8aFLqcD$-G8hOfJ6WYDyhFT12tGZB@wHulC1|5 z!OO?8#7POk6UU^L!g>f+CYHvOPJ*HPUD$If?&GtmialB6FGpYJYx%1)7=dE-xK>a` z@rD>uxurKN(Uv((Ru+0`oA+WuX8{tb(S0%&lvR+kLaZkX%EGqao_8<(UsqD#73cTq zwM`HtDn=W^@kB|TO9m99(rwYHHGtThU{)Ea)M-j*+i;no)tz270;G@-pefBww5Sp+ zfI>l42P#3BKw>Gj{@{OKcj{UmMsZ2zShXM&mB6B}{981jwc4Inlm9DJ>iqL`CmV8O z@Fqv?Yi5CuuAf?ATjrFqJs7DBxX%ZCD!wSi*sYOQBWiuHe6qlcm&ZZlhEERknHSSO zs{bpc`7VRw>wF){{yYAEd_Lrrc%c5s7d8FzjT5K8z-T_efs)@#?(-OLZ@&N^0FDRC zl9W5?1BgdqkpdNgN-O6%*d-egp#i|Dz#&Dk|F{5Aaah^%YL_Db+^KWPuPsW`Z{AM` zFG$NeEq3_!t|(}KrFVnqJINI$x#6kt8o7n*v)p6gT0BB57!%-9{CvBf%)bV_D3B|P zXnX>ldVJKsH^dSYk0Inr{a-U0gMDJQ!WjY+M$<#MjWT)vH?A9;$}3F;+7fgEAX zO;aTgyfaMSgY4w_fT_K-6?i&pDZdrd5On@2Zio$~zOCp+aqjoz!{eAm+=@IlY09NYBnBcK5E^(>KwBq29E;kNp( z?TAWMA@67dH_OE>I42ssMgWE;1dveJZVl_YuSL{ytj#53zSjbQ$L9$2%^6>d_U=ZY zZz>p|6@EO`e}1=|TZq+G0qMkm*T&Dn;5I!UNwNc&#AI_k?-}>23JF-IQji+IAZ*$t}j#p(YQu(vy&oAC(E@$*TTMjx~OZ&J!ff^Oj zP8F=?470wX#dI~XL2h$TUm!E%H7%LD2(hC)#!er+j29WD}E-l|U%O{KvEK}x99 zQsg^n6c}?@ssWCJgDLh|M&MN1@1WDK&BHLJf*9P*;%XQFo{9BC$A+@L(ZUYk92hbh zTVW0*!>&JdV}k%I;;Z;bH30fk(hxK1Qu2K|W?`j?Ke-Zv+gpu)H%gj0@LdNnpB_BGY5!nl_A9PVvFq6alBWNqn=hT&1%qCr`rV#t zgxOKQA;=SfVr25vXzOST?m>#2|3t^m%!czZj8fQr*4>9J#_DB>3*4F5Jm@9IMM{_90YY_*=GH+iKbgROL6yA#=WKi#Qa{)i@3JNg^=mG>L7DD z;rBvZiMtptAz>y}HdTxdvjX>L3*SBbgv-eo->iptoM|6f$>mzvKg*N!I=XZiK7y1Z zw{OmrSg6FkjJ7`pYIQbpc4|He{XE^93@JNyenlVloYjU4fG&(wH%%{dqb5J_-Gug2 zXxe|~qb19pvs?MSVN$8s8mz*I^W~PTm3iEL^@le3v0(`(L~YNKd$ScChX`Q7z!v9> zcEoXIVXtl=Xr5AdROuxws`wveSW@M;u-RniBn1;VXsht5UzVH2W%L*I;{tII6{23@~|$e<>P91NJN)1L;9F(@+tCr zARfn0M>pEvh+5Cmr}ZLh{d3=eAEnrTNAQ>t+4}5=(ngcb7pQ5B&&qOutD^krDk{z_ zI{=_J`|U@x|B?xmLovYrUzQ=GKSz~ z{RejxRZq_Rd`DL6^O~y5&(55E9d0D<$gUrjnjuT3Ug!qn=ZR5Sv(Ab*RY#yW^UKuY z2&cniwhn=He@jOvj7nulGTl27X?qus2u;e7yje#)ZZ+kmI`U}qk1mr8_0RSi%RTK_ zJ4SD{*ytF5e+wq?N8-Or59Zqg#@OhMiKyc8-%7zh#SXzES|azcrc&(0(woTj?6du` z`BQq%aHqGysF>DLIfslh6%4of)OYO=Q%p5gzBKH3{D$v~Gej}g%^Tr^V-UyOadSdi z8#Ze#9D+LA{`{~?56`vv840svM$`hF+(XSSFa3vG1Awown@|U?yQTO!vz9^bM;bn7 zTgd)|9S!t1PG%wp-eXfuyuoXPT?ytfb@RT5`5hk@DjM*12j)$y1G{9lYT^~&aD~jw zff|rczb>RbEXSd~)VY!&j!Fi;h+V91k3B%WJBUsIE49aRn4W zGFUlvXw~o@=qN7lO_lho=`99%?S`G*o*r>&O}`PiG5Yzc2mB`FY~mC*py-iBbEJ-z zgDJ)fZvg>*mO@t^_^7YxBXRZR`Q)TArzfxqUD4S=C7DCpzI|)qKGKQ@{~LQ>9aUxY ztSzM=3KG)YjUe4fNJuE%NOyxYqJ#=Z8brFJLAo0brF5ro=stAwZH(W&>;C<%^?iTb zwa!9bd1LnMy=Uf~dFDAYdoWY-y<@>nvwj+H7WldiXt6Vw2=kDo;72Zp-Yh?D`f?NU z(=cew|Lae(4Y}>vn1R4@9F|{1Ly<|u}#2n!;ndL2U^b+H9 zoa-H4lygaOx5uKs$2EIDKiPf5!f^ZviN*qC8WCt~guX%#hDp!gFEjqSGB)ISp)7(_ z0$>K6pQBN3v-@w|pFcq69y-1$cc2Qjc7>0_6Zpd)&hNw7sv=&rUu5@Je5$g&qzJvP z?{k)Pz&Di%<^&;5%R`6To9#&(jbRZZ%ki8bHwiKYuE7o}61p@0Pc z?ME389G4oJXZ<_=-@pIA)(c5Fb@BcKz$5^;6TVh+WhGSC z*O-FNWf1SPM|JbC+1+O<10COVQ86q4oX~ZP@EDu3T$0+Betd5HMyZ(1`46>%yp8_c zj8Ots)(bt6HX7v;WYDp`ja6ifCp5<3%(JLV{F{BinJ#J@v}@PY)YoKzD)gK4Ka>Zc zc{|ute8v8al09F^A2tTz_{;@H>r2b6D`ugm@mrv;`>vlc<&i?J#&~$q>zknF?h6J` zuQz>3$3!RUgreXzk3ds%_++275j1>T>dw$uBj1$+^66IsL0-RK^{9RE@ws%;i&o0Y z$&Z8*h>sP;+@RvpMnTdk)|_dQL;fMWvdD17T1 zWTSPVAJFz$Z6LwfjrBDhn%pk&nj?m{^e4n(rzLr41VpK;?;MZu9E;9&?gC%}#63Rj z*rsQ*Q%U!$_MRr}KjaBo#l&i}SW=uK$Ja5!iEi;*PXgu->X(1A>Rn(;&9{~KnTulg zq{4@6JsALegy_Q42RJrwu4&Epp@GzE1^TPnb2E`vTDK@2+pqwD%z=a^$DyNWS-TKv zDLk`zAMqeutm4t5hkFe!IL`@Btg%+Va_K7eIfQ+~gdTd3(S2)>>>7Dm2}@;Kwq|Xi zzRTrrbXdGlNH~qk--<;{mHfl{J?)R{ig?K|#`n+=$DxyfF3>k_W4~iELnB?|tO{rC6fK&O4!O`B-<5i2g0 z^GOA<*ig>7>T21$Z?_ggh2xJm%i&uF z3T>1mfSm!iLU7_USPWeJ{IqudTWhA*#H%)78S}N2Et7XFj(|)s#r(6oIQ938m3yj# zqtZHs$B61&4`HOz#AT{{{s2Sd5r}die-DsO{M_s$c7BLnWl@v(h{e62#Ccrbv8SZl z4zP&qLF~KHJyFi!6SM?~>I*r&M*3SI^}N`uS~bYx{nE7;v|tGnA;dm4S+(`a1;K zU0GMZ@Kj-Vt>vr2x(OE8;H>7e4(*1Gpo#wH7|5T};nKmDD35V=x7K%j(VjySX`bA@ zM{+o6EhHBkMP*AigBogog&Ps{kRVmMUgF*pbTm~A1dF#<+%IA1_S%&`<|RM4>qva~ zPEJKHalY{fh(>keEp=*OH1t%_9A86rIz!4ZAaUH}pUKA-ddxLl@$jTzN0%o@FgVXQ z7UarTRyT9L_j|O%wnt5XK>~aw1Zw96WXvJLl`iohBECRO9?iff0Ni?s1sfjz%LK!e z=eiR5;+Y}0R%9L1^UF~VYS_{e#afbJQs}}X#FCR@J^V_nW9owDeN;}c%m@je5+edu zv8(+|j{XkakBPi~3jmF#)4?%~e!i1f|BHJ5M+As2zDw;w+Gvdzo|OJ-@bpo~T&!Gn zZwC-*e1wLPDJHv9IM(VExpd#dM6&j40u9T(5iAMO#)%Bt<7?sL>8LfW#F+ED?b$cR`sade+&Kg&cG>Gf`!w26FBck&7zCeN+23S5G z{~V~9pK^YVnMWd#IG9uzsP3Pir-PoF`^{po`O?Lo5w0*Q+6ys0S=nA&JcWhU_xg+ju3p{}% z1)#UF?Che6)MB46h0hK+*AnWf40HkmU@NAxr~ZOV-{jqubq4b*K>uezgQLar7j6>% z774+s9*N8BA43rn=mIEyr+(b~! z_z#yOK;0sOc!Ft-=Ig(%0t^swVhnr<{7Y((_x69HPc8vylL6tjZviwr;6x3V8MyNK z$qyI=s<<Mr*oeg_Zb09)s8Clp05805Dma>9CHsVT*gp1bYtHBdM^umYqP_5_DyN zHVSjQ8JGOPgK>xsnp26N{GFOmwf3Q=#$&+J=IDXlSjia)5H#!v@YVym1BK?Z&5*2g zs>!CuqogIoPZO?>xakxjDPI!F_}>gx5lI7xTXbkM=)%AVK2B_+-AvU|Z@*5WZNA^X zGu9*y0X-++iN&U+;@lj7gDjN`xyZB>j3IEW<4lL4#3{@CCN{*|a>I3Vo?;rj^MDc7 zzsE%U2_Pth{<>@cbCWZ$!RLogt6p@tHSZL`uFzn2MQoz;UmONNF|-K5-D}}l(X2*# zU*AfY^P3EEbLz?=a=77fH%60o*2<(xB9n}v2D8vwJ;*OX<#UOZz{}T`js^+LBq;zG zNhu-veG~3hN_j?|_F684;6A6WbZe99Xk?=;7LdLa{OlSb_7Rn@01HD)ZiGPH7|UUe zC74CMBJ>utA{YWtB_hV$G|F;_7}fz2v9BsWxLP7&BmwBic$WhsTKk07$muILl1(aq zY%B*&YLe1u0B?a({Tk51&?>MZd`b03*jpw98GzOKEo_zz*5(j0i9j!9NKwQBAgU-S z%YkQY@r_9#(o+5UF3o4c_|jMtBGsH$MpCLd%9$?tN7#?&p`+ozXrkk@9UdP~p?P|F zvFlbFcCJDkZ=bxx{T}U;}N-Jko3VYgze1IU2jn;n2e(f9g7q5o&x zHe}@FDS~h83=C3ck>guO+!KDV7$*w$7FF30vA;*sX$uOv+g1&JcnFyHkqFm^Dr6Nt zlJ0JVu_q~xkB+WC?;CB|Qe`szvf+`uOO?Y~59FMUl`(c(=M=9fDq^emE}u&EmzKp@ z`4MmY=F!mM(b3QNr_VKru*Z&1Tg6MbvA-lIUvHU<5dC12;1io=NZO<5&9dtbOI>ZLr8iT|JDV;~Pzd)s+EC*tNlQbp z4o*t~;F1B?m-x{*QsK(JETafHG6j`17Ff~IIeJyNS<4T_5{`H}x@hmdje(CG)T=TD zZsn@>XsndD4k(m`a2jPAC|ARA$+LzYc_7i6$m;*vTWF(1-pk3(&Q7fqEWjEp%LEve zoBqg6c#(^R zV-lq4)h~R~UUa(*K+*mDfyD0Ig9Gf#lDazLXJpuGJ3DNs@7jnK!n0C2)Wfe{kSP9H zHV;er0v;$9crEZ8_zm0SQ-ES7IKUJ5;NE&(Kg+)1lMo z&p(vqt8wS^-Q~rv%T(fnsip z%n@hkZ*D#|-^wvn1C%s9A~)z>7#MW_(}T8N-l4l;9 ztz1uYGSx7K=Bdrt^2F;BR@V~)J-sBo>f*!;>8U_^7U{l?Hex9V=BmnjB~q?t%8<8N z&`y!&zsLbCFLEGNNeM^B{nb9s9#H?dBw`nMc*9w{*5hTa?G;M!k|^m;BcuLKf`aR_ zr@Dw~(UH|$ME0syC$*%;_0HM6j}{)5hj7j*hNllSHLlY}5|JYZ6Ol`PGlTtp+ONk= zsGp-qQ^|Lq-0GllFOYr4@8+tq*(-Ts3%w&eQWo{Z%j0BIiI=dLJaq!)+$L=v9wKolkq%-XYxHrb*B-jVmsi2PzVc zL*J^NQ%appz1@G$ITVITB_S&E;l)p=AZn!q9*fr0!{Hyf@UPOLudDj@xl?U?>sgn+ z_gDLj`(_qPZ-fRNZ)$t{So9wx=D!~ZVn|`Nkq%wp%3$?CY7`(U59`+X>J#G6@>P_o z4N1H*a5Wuz51#g4%jz91TbD{U6yFlL)-|@6V|nn@;q}u1-4LT{cLKs?A~ZxB3w{TJ znsg0(DP>p7R~aRv4$h13SCR+u$_IUmeZ9TwfyvsXB{+c#WJD8|M~pLKVeHb;JVhLF zM?w{O1@nK0K>6!-?}T>r1d;Do2ncc*r{g%R6{NQI=E9GcnFAzUq_8cY1y<~swps7YFef(tc(&kMDfv> z=dkw$jQaICpXcuXXk>#~yO*&);h+UXP#Zn?`tA#zX*Y+MhEgn(uOSXy*nNDxJc0<|)76-XirH7FUtuWG%@w+Zv<;7ss91SV)=l4D@aouXtN`KGH)ol_V;dGY z+|&y#yeBV0?}m#pqW$sklT>3_Xurb*J?b{l8ES0)5+*AIwdc42OXOf$Gcl-+@MiMulkSD18aww`z&Vc^dU*B&)7ejgQpZGL&2#wF;&Q-nK z?uh8*>dLCZZ#t^eY|hi07a7fDr&Q!}xEKnS!+`s_$Kw&dv~PSD9w2Pv z`1Cvn;=Lwg2&&Q2^dc)azCu@bI`vI?us_6r+iE(h>%Q*uo%;bv04Gzgvg!Alc<>s$ zy!zU^4B|fUAD}edH&acRVe5DfKSVOxA^@4rKIFAk{*3&~(6W<*%m5Gp(%>5*q z!99G@mn~{Sp!xyV8aE65^_Rc08C^GcK6G)|J65?_E3Tt@nQo#z@{C=W&-OR zkncYjs}CO}e;J5(W`D7g%hQ&YX;B^5&Xyyg;*+EVr7PHDR`+n7Lb$dysdjo9kkWXB;3W(^ z6s>bgBfA0`d;uSEpQV2Ar{66)S!|G7UkLVXStBi}HX;am@ojKDBjC-}SGX|nB&`g{ zULm=;N?_m*eci&Ly$#JOH}1>p%;xu0#%jS-_-kFQT@XLkjhqKHbC-SOvJkz{khpqT%?oeB-OL_ zz+~0>xwf5~^HA6#mc93jTsy(k5Gn&C6rhQsdZ{9R9rw8|`^`UUL~71wkL-mdz% z8g`Rl-dZ9WO}I#O%^~CaVG>>msl}I@R|usaUIP%?#VJ@CzU7yB*F~u&<6cBuIKjxM zr@kM_@BXEc7X!QnV3sP{r|x!(H;mcW8+|)Lc9RyQTT1efG3)BzKTSTku(||m{$#u9 zVEJN~y#Z7|H`ymca!>h1VL!!%)`}hCf^aC;tfHT7w*X3p{=urmlK`W6&B% z(^%M0R@m_Lcje&OF(@=Vdqo5XU#bE6IOAX7ElaXIMK=HMRRb0+P&F740exr_meKr< zmK&fHKu-vH{`dO^#1LsIfV&E*0c6ggk}xqljJ_$AS?A@M2I|2{m6}CbO<90e)?@_n zqb|cEgu>3jUan)RiFYz3Al*L*O|?P`n27B*0d1f!biZr0KaMOnyiP-BBL96O`!F;KS)&ZcImXKpm;4S<6 z?IJ`pkIHUa85hfE4uoC~P(a3ugt*8e_>fLUU#>G1j-|u9z{pxtIbDlDrXFNB(aEew z2x(^kr>)6hy+u8jcsABDMwKndz?}}k=``<`;pG9M)2b;g%pRmHyGE7bRNYNt_gO+F~%U!_hJ5x~=6 zwA@%SSABYibRTgZtJrc+T0)zF0bl5l?5yX1fD`KdX#7!QK4 z<6$RP=^lBeq>ai&@=p#QR}hH2IpnPP^xV66Zo&^xcR#r-&?g3I(mw#Q=s+<>F0zMD zM?AsY0TSt*)`~*`i~14ltQQv|WOzguK@_57>kvH3WMG7Ue;5S)jV$Z0d59WC?@HlT z5g5DxFaupcP%ZkFntpX}-IiS2;NGvKx;4{{pOTWIA~`9C`ZNp=VgKwB%dMQ-7RVFBJR6yAei?6-U}7y$a`bRNn2H*bod@Rq39^0uq;caYRg3HZYGzm~^Cz}WmstBm_Q7fC|4 z{g2+|_PYyGLC+pwPiigRD`29T)pop|fzT@XXm008U-+Q0 zcx<#CtlCvSe6W)DyWf~4S@LKobD2ij4bkb-iXoIf#rv86iCSm7BpWzYN#v~kwapXx zqgzDHZ)m7qfj(Y&B66hoZ3O-dbKALQ=Cj`gABPb%2pMYvZcly?z)d(TWW)^A4MYH$ z-S$uMfMY&Y=5IC?<*_2Yhzu?co{~mmsz=1zc1J7kn_SABM>tRDCIYPJ|GY9t*-Np- z)%jfT7?jF?Nxa}$wZAdDJl>wx&B^HhY^6l_yM0e*o6kZNjpn_WcoaumFMuldYG4z@ z3n5n7&89=?jlTkRt=~L*l>SWAVEU4II!CFjykitrCgbHHDWsw+bpD@cK;c~;D@p@{ zyOAo}rX(~SiXXz#OY6^h%YC4f0JFm4ZVbgKJ5*DF$@eF6M~V%iMT^01Qg#&!^!c|h z;yleZN>DE2>+fI1h*^0LW*g9|fqZ-BTq-NgBOHGt>&yMDs5m;idxQzS9>T$;j%P_M z^!kV(G2@ufU)m`iKaELHAs3{521K z9xwBg_o4K22?)$v4-6xbkwZpwEaIhKE`X;>o6}xUbBjz~^^U3C`L>~UlDsXMZ}Ki z_X`p_hm+`TFYQJH9y(}O%eS@|y_s%EIif8(>RVq14NiP`8jHsH63kEiFug1q_-3n( zpG?-hqxl*jtEZBy8Z)Vf?96LBmlPL3V=6w=)Y)fGWP@o-W{@wE2SHNDAlW3hHGvH&Wlm_fi!zou=<`N%9 zM@M}?hPuvaSx#I`E(5;Uc|xAzvH9~ySe(LV$eecBh7+*fnfwlFKFf0CNFR2tciKav zc|6wLJjW|^s;*V+AlfFz2)32I!Akb?xH2!D0x0m(NgCo)$?c!4cJ5;{BtSu9Qddxy z8|WAXibr`aA%WE1JkSR+YiqTvqHdw=^FcvDA4aoYF|JuK#oUkdh|nzWIuP2D8hBJ9 z`6}IQ_LYH(4|Uql?>`mkKzcigLT>o`zpOU?Gn=Tx)~AwlV%-d)vgDZLA|%i5gn4>; z-nVkKJ+lUz!p4HNU`S{pPfMA@Mdn1f*?n%*-BhN1#x*s9fspQ4} z^dh=E5Z*4(z{A%P^lV~3_u$(r#t#n>AIrey#-u3kmbT@G;t9P8bh)#M58+Z0-3qkhWkwOQf^^4_WcEhR2x z$#p$eI5W{zn6DuUfa4}MOJk7unbR|!;o&!P2Iw#4wm(dN4Ty3O;9kXNKD*x_Yj@0)MYN8dod8kZt;TCQR{btd=YnXYoi@Bih&-gFp-H9)4Z}%tJlh| z;?EdX&l(l8U&bX3Ic)koJcc|Ncb``M2FQp`6{KuWv@;WO*rEA zQ07Fcu)mKC#cfXARye4F(*F?zbJBSYu`S_lK27}VGPX{P5Zm0_jzI6Zki(dRo0Nu= z8^|W#u=i2fO%SQ^UD>;3n!oO9B2#>CA_!pHK`}#d2s~?f7ch!q6d(qD`YFAG+o*53 z2%6})_ED-u{Y8k=P(*82?czj_Ox)K&flvGkOlIm4bUY2wV_fkMEVW)SM7*TpmO4Z4 z*ISiM_Ba?@-x&Tmyd^RlMoHtkq1tdXwkn*RoF}l#rSen)W*hsy;oLo2TO9@$A`>-F zxHxcUSzMjFN?pBiCzrRXe`W6Xw9~g<5wg0w5Kx+wmz2AArb|M6s%p)gu3iXVp5w*5 z!AAFFp6Gj5Xj)jqG}G2)G(462H5QsJU9a~v0pQ;)i`er&u~GY<^c5UelQ2M7!yWU5 zM6!NKym7~l6Jw&iZ~qxuCoRho=}?R_g(g}5K9|SVHIs0DWWOsKs_|~Pl*qm*%)Z_0 zXofX-F{mkZb*%GFSJlunNcOe3at-Tl<8yNknz{4r`TD998Z^lC>iMzx{=nL3{Jlz( zyp-I9m6>$9guaE-G%k@C?>aLc6&R(vPNhu=XJQ`QF73f2D*lHBXP3D5pWOJ9v-pWG z9F(SQA563oj>G$8&C`a`MqlcD!;qDBb^B?HH1hh%q3y}e>`ayUY4t3&?DyDhW5F!+ z+RvhUqcQu!H`5M0Y@#))@G6@&l>AW|woY5RAyv7HK7J`j#{~)I{1_AB!f69Ypd&A( zA%{)9#(Dho?v=!3Bl#Vb<2FxTg_5U8Ir?Sm&!!;>?4&*yb3T%np|r491pkG z_XA~o-U{>at&N3L8s$`fu&d{PgTnWI0V-DSy%GIxabp9DDM2cACU@p&ji8vpHV8JJ*oV^aDnwZH}dC{gAabSDMx|%PR&umqFLSMz6fr=X0K(vED>^ zzlwX_E2_~dxxjdK2>t!IaHzfRgo!_LCVc1GYPZeI%KH>I_8Fp{I0E1ia1VIQxHKVT zj&R|tE2+TD-AP`BCM+9C6LK0z)Zag07?rNG=^C4t5z!qreCj@4@KmeTvUj*NaQA*D zncFvL)7eVGjeINl$LlK7-q`i^bybTPPGUe8i~E4~{&O)RgY_cI-pdt>%ax1(s;S&& zsLzKc+bYM4j*4)Bo1CenzL8TJSx`|3yurTO?TVmQTdzSJI&GER{l?D%dBZF3MegjSM=5(M`Iuovd3E}b`rYUoHC<{yQ^|Jehl#~un=vKA;C^n2Xk_PwN z?;J;`*Kn#h^|Kz4sr=a1Veriwt6B27Se0t*wp zUIXJCxjhs7dwvQFJ4)6|m_K-28wY4c4dRDQRsG9Kr3?>H3jjL7AbwiLYxBIg0o7*7 zNO|@dPL~SId=-B;Mgtup(H9~{{HR((KPkz;=Kbd!bSer&Vw&)5DEq9wR<)b+dyL## z)DO*f(O7irYFxZb-%G3NAxow{L|w{-%SQg_W4Cg!eBb*_kAclUYnoZR9iP{GUl)0w zHoOd4EPQI{=IgjH_0wxvO*o;SSGAN55epH-dM7=AJkhSNkG)9vl;YK^Y)LU>fnc#d zB1{KO*%t=!Ue+smS2ze*6Ud0e8Y|@?WBf=}o=k{@Xk3!8rE2sM~s5*;l( z1guBk<8KD0k2k7u>fbRG3mktgWC?kT`T=~zo`P+hAZuXz?-+(^6A4wiT6-_S9^GbF z?_{-IGH0NE9;Z#q<+K*)M2p^)oisTa*65ZBYO`cSuN7%EUK(yZCvTtjTprPmq{+|U zMDp`tXE{qrJ{{fPZ@Pr9Xebt@3F-JPk4%k(9WLN_Fi`%MRaw+u+VXO1>$%_gYSVqG z&-}`z(lXi%iS0Q2ft)ergm%Fjb@i95BvR0QFaH=mz7~K^feio#a5xc@ssZk2Kv{hqQVg(+lKME zg(xMZ5k_5 zh@9E#mGS$n1#DelZo~3-6qkv3(eJ*k54T63evrGJxwoiGim4?;qF~GnUcvedeyhtDx0) + + + + +`; + +const adminPage = ` + + + + + + 订阅管理系统 + + + ${themeResources} + + +
+ +
+ +
+
+
+

订阅列表

+

使用搜索与分类快速定位订阅,开启农历显示可同步查看农历日期

+
+
+
+
+ + + + +
+
+ +
+
+ +
+ +
+
+ + +
+
+
+ +
+
+ + + + + + + + + + + + + + +
+ 名称 + + 类型 + + 到期 + + 金额 + + 提醒 + + 状态 + + 操作 +
+
+
+
+ + + + + + +`; + +const configPage = ` + + + + + + 系统配置 - 订阅管理系统 + + + ${themeResources} + + +
+ + + +
+
+

系统配置

+ +
+
+

管理员账户

+
+
+ + +
+
+ + +

留空表示不修改当前密码

+
+
+
+ +
+

显示设置

+ +
+ + +

选择系统的外观风格

+
+ +
+ +

控制是否在通知消息中包含农历日期信息

+
+
+ + +
+

时区设置

+
+ + +

选择需要使用时区,系统会按该时区计算剩余时间(提醒 Cron 仍基于 UTC,请在 Cloudflare 控制台换算触发时间)

+
+
+ + +
+

通知设置

+
+
+ + +

Comma-separated hours; empty = allow any hour (used when per-item hours are not set)

+
+
+

提示

+

Cloudflare Workers Cron 以 UTC 计算,例如北京时间 08:00 需设置 Cron 为 0 0 * * * 并在此填入 08。

+

若 Cron 已设置为每小时执行,可用该字段限制实际发送提醒的小时段。

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

调用 /api/notify/{token} 接口时需携带此令牌;留空表示禁用第三方 API 推送。

+
+ +
+

Telegram 配置

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

NotifyX 配置

+
+ +
+ + +
+

NotifyX平台 获取的 API Key

+
+
+ +
+
+ +
+

Email config

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

Webhook 通知 配置

+
+
+ + +

请填写自建服务或第三方平台提供的 Webhook 地址,例如 https://your-webhook-endpoint.com/path

+
+
+ + +
+
+ + +

JSON格式的自定义请求头,留空使用默认

+
+
+ + +

支持变量: {{title}}, {{content}}, {{timestamp}}。留空使用默认格式

+
+
+
+ +
+
+ +
+

企业微信机器人 配置

+
+
+ + +

从企业微信群聊中添加机器人获取的 Webhook URL

+
+
+ + +

选择发送的消息格式类型

+
+
+ + +

需要@的手机号,多个用逗号分隔,留空则不@任何人

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

Bark 配置

+
+
+ + +

Bark 服务器地址,默认为官方服务器,也可以使用自建服务器

+
+
+ +
+ + +
+

Bark iOS 应用 中获取的设备Key

+
+
+ + +

勾选后推送消息会保存到 Bark 的历史记录中

+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ + + + +`; + +// 管理页面 +// 与前端一致的分类切割正则,用于提取标签信息 +const CATEGORY_SEPARATOR_REGEX = /[\/,,\s]+/; + + +function dashboardPage() { + return ` + + + + + 仪表盘 - SubsTracker + + + ${themeResources} + + + + +
+
+

📊 仪表板

+

订阅费用和活动概览(统计金额已折合为 CNY)

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

最近支付

+
+ 过去7天 +
+
+
+
+
+ +
+
+
+ +

即将续费

+
+ 未来7天 +
+
+
+
+
+ +
+
+
+
+ +

按类型支出排行

+
+ 年度统计 (折合CNY) +
+
+
+
+
+ +
+
+
+ +

按分类支出统计

+
+ 年度统计 (折合CNY) +
+
+
+
+
+
+
+ + + +`; +} + +function extractTagsFromSubscriptions(subscriptions = []) { + const tagSet = new Set(); + (subscriptions || []).forEach(sub => { + if (!sub || typeof sub !== 'object') { + return; + } + if (Array.isArray(sub.tags)) { + sub.tags.forEach(tag => { + if (typeof tag === 'string' && tag.trim().length > 0) { + tagSet.add(tag.trim()); + } + }); + } + if (typeof sub.category === 'string') { + sub.category.split(CATEGORY_SEPARATOR_REGEX) + .map(tag => tag.trim()) + .filter(tag => tag.length > 0) + .forEach(tag => tagSet.add(tag)); + } + if (typeof sub.customType === 'string' && sub.customType.trim().length > 0) { + tagSet.add(sub.customType.trim()); + } + }); + return Array.from(tagSet); +} + +const admin = { + async handleRequest(request, env, ctx) { + try { + const url = new URL(request.url); + const pathname = url.pathname; + + console.log('[管理页面] 访问路径:', pathname); + + const token = getCookieValue(request.headers.get('Cookie'), 'token'); + console.log('[管理页面] Token存在:', !!token); + + const config = await getConfig(env); + const user = token ? await verifyJWT(token, config.JWT_SECRET) : null; + + console.log('[管理页面] 用户验证结果:', !!user); + + if (!user) { + console.log('[管理页面] 用户未登录,重定向到登录页面'); + return new Response('', { + status: 302, + headers: { 'Location': '/' } + }); + } + + if (pathname === '/admin/config') { + return new Response(configPage, { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }); + } + + if (pathname === '/admin/dashboard') { + return new Response(dashboardPage(), { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }); + } + + return new Response(adminPage, { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }); + } catch (error) { + console.error('[管理页面] 处理请求时出错:', error); + return new Response('服务器内部错误', { + status: 500, + headers: { 'Content-Type': 'text/plain; charset=utf-8' } + }); + } + } +}; + +// 处理API请求 +const api = { + async handleRequest(request, env, ctx) { + const url = new URL(request.url); + const path = url.pathname.slice(4); + const method = request.method; + + const config = await getConfig(env); + + if (path === '/login' && method === 'POST') { + const body = await request.json(); + + if (body.username === config.ADMIN_USERNAME && body.password === config.ADMIN_PASSWORD) { + const token = await generateJWT(body.username, config.JWT_SECRET); + + return new Response( + JSON.stringify({ success: true }), + { + headers: { + 'Content-Type': 'application/json', + 'Set-Cookie': 'token=' + token + '; HttpOnly; Path=/; SameSite=Strict; Max-Age=86400' + } + } + ); + } else { + return new Response( + JSON.stringify({ success: false, message: '用户名或密码错误' }), + { headers: { 'Content-Type': 'application/json' } } + ); + } + } + + if (path === '/logout' && (method === 'GET' || method === 'POST')) { + return new Response('', { + status: 302, + headers: { + 'Location': '/', + 'Set-Cookie': 'token=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0' + } + }); + } + + const token = getCookieValue(request.headers.get('Cookie'), 'token'); + const user = token ? await verifyJWT(token, config.JWT_SECRET) : null; + + if (!user && path !== '/login') { + return new Response( + JSON.stringify({ success: false, message: '未授权访问' }), + { status: 401, headers: { 'Content-Type': 'application/json' } } + ); + } + + if (path === '/config') { + if (method === 'GET') { + const { JWT_SECRET, ADMIN_PASSWORD, ...safeConfig } = config; + return new Response( + JSON.stringify(safeConfig), + { headers: { 'Content-Type': 'application/json' } } + ); + } + + if (method === 'POST') { + try { + const newConfig = await request.json(); + + const updatedConfig = { + ...config, + ADMIN_USERNAME: newConfig.ADMIN_USERNAME || config.ADMIN_USERNAME, + THEME_MODE: newConfig.THEME_MODE || 'system', // 保存主题配置 + TG_BOT_TOKEN: newConfig.TG_BOT_TOKEN || '', + TG_CHAT_ID: newConfig.TG_CHAT_ID || '', + NOTIFYX_API_KEY: newConfig.NOTIFYX_API_KEY || '', + WEBHOOK_URL: newConfig.WEBHOOK_URL || '', + WEBHOOK_METHOD: newConfig.WEBHOOK_METHOD || 'POST', + WEBHOOK_HEADERS: newConfig.WEBHOOK_HEADERS || '', + WEBHOOK_TEMPLATE: newConfig.WEBHOOK_TEMPLATE || '', + SHOW_LUNAR: newConfig.SHOW_LUNAR === true, + WECHATBOT_WEBHOOK: newConfig.WECHATBOT_WEBHOOK || '', + WECHATBOT_MSG_TYPE: newConfig.WECHATBOT_MSG_TYPE || 'text', + WECHATBOT_AT_MOBILES: newConfig.WECHATBOT_AT_MOBILES || '', + WECHATBOT_AT_ALL: newConfig.WECHATBOT_AT_ALL || 'false', + RESEND_API_KEY: newConfig.RESEND_API_KEY || config.RESEND_API_KEY || '', + RESEND_FROM: newConfig.RESEND_FROM || config.RESEND_FROM || '', + RESEND_FROM_NAME: newConfig.RESEND_FROM_NAME || config.RESEND_FROM_NAME || '', + RESEND_TO: newConfig.RESEND_TO || config.RESEND_TO || '', + MAILGUN_API_KEY: newConfig.MAILGUN_API_KEY || config.MAILGUN_API_KEY || '', + MAILGUN_FROM: newConfig.MAILGUN_FROM || config.MAILGUN_FROM || '', + MAILGUN_FROM_NAME: newConfig.MAILGUN_FROM_NAME || config.MAILGUN_FROM_NAME || '', + MAILGUN_TO: newConfig.MAILGUN_TO || config.MAILGUN_TO || '', + EMAIL_FROM: newConfig.EMAIL_FROM || config.EMAIL_FROM || '', + EMAIL_FROM_NAME: newConfig.EMAIL_FROM_NAME || config.EMAIL_FROM_NAME || '', + EMAIL_TO: newConfig.EMAIL_TO || config.EMAIL_TO || '', + SMTP_HOST: newConfig.SMTP_HOST || config.SMTP_HOST || '', + SMTP_PORT: newConfig.SMTP_PORT || config.SMTP_PORT || '', + SMTP_USER: newConfig.SMTP_USER || config.SMTP_USER || '', + SMTP_PASS: newConfig.SMTP_PASS || config.SMTP_PASS || '', + SMTP_FROM: newConfig.SMTP_FROM || config.SMTP_FROM || '', + SMTP_FROM_NAME: newConfig.SMTP_FROM_NAME || config.SMTP_FROM_NAME || '', + SMTP_TO: newConfig.SMTP_TO || config.SMTP_TO || '', + BARK_DEVICE_KEY: newConfig.BARK_DEVICE_KEY || '', + BARK_SERVER: newConfig.BARK_SERVER || 'https://api.day.app', + BARK_IS_ARCHIVE: newConfig.BARK_IS_ARCHIVE || 'false', + ENABLED_NOTIFIERS: newConfig.ENABLED_NOTIFIERS || ['notifyx'], + TIMEZONE: newConfig.TIMEZONE || config.TIMEZONE || 'UTC', + THIRD_PARTY_API_TOKEN: newConfig.THIRD_PARTY_API_TOKEN || '' + }; + + const rawNotificationHours = Array.isArray(newConfig.NOTIFICATION_HOURS) + ? newConfig.NOTIFICATION_HOURS + : typeof newConfig.NOTIFICATION_HOURS === 'string' + ? newConfig.NOTIFICATION_HOURS.split(',') + : []; + + const sanitizedNotificationHours = rawNotificationHours + .map(value => String(value).trim()) + .filter(value => value.length > 0) + .map(value => { + const upperValue = value.toUpperCase(); + if (upperValue === '*' || upperValue === 'ALL') { + return '*'; + } + const numeric = Number(upperValue); + if (!isNaN(numeric)) { + return String(Math.max(0, Math.min(23, Math.floor(numeric)))).padStart(2, '0'); + } + return upperValue; + }); + + updatedConfig.NOTIFICATION_HOURS = sanitizedNotificationHours; + + if (newConfig.ADMIN_PASSWORD) { + updatedConfig.ADMIN_PASSWORD = newConfig.ADMIN_PASSWORD; + } + + // 确保JWT_SECRET存在且安全 + if (!updatedConfig.JWT_SECRET || updatedConfig.JWT_SECRET === 'your-secret-key') { + updatedConfig.JWT_SECRET = generateRandomSecret(); + console.log('[安全] 生成新的JWT密钥'); + } + + await env.SUBSCRIPTIONS_KV.put('config', JSON.stringify(updatedConfig)); + + return new Response( + JSON.stringify({ success: true }), + { headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error('配置保存错误:', error); + return new Response( + JSON.stringify({ success: false, message: '更新配置失败: ' + error.message }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + } + } + + if (path === '/dashboard/stats' && method === 'GET') { + try { + const subscriptions = await getAllSubscriptions(env); + const timezone = config?.TIMEZONE || 'UTC'; + + const rates = await getDynamicRates(env); // 获取动态汇率 + const monthlyExpense = calculateMonthlyExpense(subscriptions, timezone, rates); + const yearlyExpense = calculateYearlyExpense(subscriptions, timezone, rates); + const recentPayments = getRecentPayments(subscriptions, timezone); // 不需要汇率 + const upcomingRenewals = getUpcomingRenewals(subscriptions, timezone); // 不需要汇率 + const expenseByType = getExpenseByType(subscriptions, timezone, rates); + const expenseByCategory = getExpenseByCategory(subscriptions, timezone, rates); + + const activeSubscriptions = subscriptions.filter(s => s.isActive); + const now = getCurrentTimeInTimezone(timezone); + + // 使用每个订阅自己的提醒设置来判断是否即将到期 + const expiringSoon = activeSubscriptions.filter(s => { + const expiryDate = new Date(s.expiryDate); + const diffMs = expiryDate.getTime() - now.getTime(); + const diffHours = diffMs / (1000 * 60 * 60); + const diffDays = diffMs / (1000 * 60 * 60 * 24); + + // 获取订阅的提醒设置 + const reminder = resolveReminderSetting(s, 7); + + // 根据提醒单位判断是否即将到期 + const isSoon = reminder.unit === 'minute' + ? diffMs >= 0 && diffMs <= reminder.value * 60 * 1000 + : reminder.unit === 'hour' + ? diffHours >= 0 && diffHours <= reminder.value + : diffDays >= 0 && diffDays <= reminder.value; + + return isSoon; + }).length; + + return new Response( + JSON.stringify({ + success: true, + data: { + monthlyExpense, + yearlyExpense, + activeSubscriptions: { + active: activeSubscriptions.length, + total: subscriptions.length, + expiringSoon + }, + recentPayments, + upcomingRenewals, + expenseByType, + expenseByCategory + } + }), + { headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error('获取仪表盘统计失败:', error); + return new Response( + JSON.stringify({ success: false, message: '获取统计数据失败: ' + error.message }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } + } + + if (path === '/test-notification' && method === 'POST') { + try { + const body = await request.json(); + let success = false; + let message = ''; + + if (body.type === 'telegram') { + const testConfig = { + ...config, + TG_BOT_TOKEN: body.TG_BOT_TOKEN, + TG_CHAT_ID: body.TG_CHAT_ID + }; + + const content = '*测试通知*\n\n这是一条测试通知,用于验证Telegram通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + success = await sendTelegramNotification(content, testConfig); + message = success ? 'Telegram通知发送成功' : 'Telegram通知发送失败,请检查配置'; + } else if (body.type === 'notifyx') { + const testConfig = { + ...config, + NOTIFYX_API_KEY: body.NOTIFYX_API_KEY + }; + + const title = '测试通知'; + const content = '## 这是一条测试通知\n\n用于验证NotifyX通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + const description = '测试NotifyX通知功能'; + + success = await sendNotifyXNotification(title, content, description, testConfig); + message = success ? 'NotifyX通知发送成功' : 'NotifyX通知发送失败,请检查配置'; + } else if (body.type === 'webhook') { + const testConfig = { + ...config, + WEBHOOK_URL: body.WEBHOOK_URL, + WEBHOOK_METHOD: body.WEBHOOK_METHOD, + WEBHOOK_HEADERS: body.WEBHOOK_HEADERS, + WEBHOOK_TEMPLATE: body.WEBHOOK_TEMPLATE + }; + + const title = '测试通知'; + const content = '这是一条测试通知,用于验证Webhook 通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + + success = await sendWebhookNotification(title, content, testConfig); + message = success ? 'Webhook 通知发送成功' : 'Webhook 通知发送失败,请检查配置'; + } else if (body.type === 'wechatbot') { + const testConfig = { + ...config, + WECHATBOT_WEBHOOK: body.WECHATBOT_WEBHOOK, + WECHATBOT_MSG_TYPE: body.WECHATBOT_MSG_TYPE, + WECHATBOT_AT_MOBILES: body.WECHATBOT_AT_MOBILES, + WECHATBOT_AT_ALL: body.WECHATBOT_AT_ALL + }; + + const title = '测试通知'; + const content = '这是一条测试通知,用于验证企业微信机器人功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + + success = await sendWechatBotNotification(title, content, testConfig); + message = success ? '企业微信机器人通知发送成功' : '企业微信机器人通知发送失败,请检查配置'; + } else if (body.type === 'email_resend' || body.type === 'email_smtp' || body.type === 'email_mailgun') { + const testConfig = { + ...config, + RESEND_API_KEY: body.RESEND_API_KEY, + RESEND_FROM: body.RESEND_FROM, + RESEND_FROM_NAME: body.RESEND_FROM_NAME, + RESEND_TO: body.RESEND_TO, + MAILGUN_API_KEY: body.MAILGUN_API_KEY, + MAILGUN_FROM: body.MAILGUN_FROM, + MAILGUN_FROM_NAME: body.MAILGUN_FROM_NAME, + MAILGUN_TO: body.MAILGUN_TO, + EMAIL_FROM: body.EMAIL_FROM, + EMAIL_FROM_NAME: body.EMAIL_FROM_NAME, + EMAIL_TO: body.EMAIL_TO, + SMTP_HOST: body.SMTP_HOST, + SMTP_PORT: body.SMTP_PORT, + SMTP_USER: body.SMTP_USER, + SMTP_PASS: body.SMTP_PASS, + SMTP_FROM: body.SMTP_FROM, + SMTP_FROM_NAME: body.SMTP_FROM_NAME, + SMTP_TO: body.SMTP_TO + }; + const emailProvider = body.EMAIL_PROVIDER + || (body.type === 'email_smtp' ? 'smtp' : (body.type === 'email_mailgun' ? 'mailgun' : 'resend')); + + const title = '测试通知'; + const content = '这是一条测试通知,用于验证邮件通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + + if (emailProvider === 'smtp') { + const htmlContent = '
' + content.replace(/\n/g, '
') + '
'; + const detail = await sendSmtpEmailNotificationDetailed( + title, + content, + htmlContent, + testConfig, + normalizeEmailRecipients(testConfig.SMTP_TO || testConfig.EMAIL_TO) + ); + success = detail.success; + message = success ? '邮件通知发送成功' : 'SMTP发送失败: ' + (detail.message || '未知错误'); + } else { + success = await sendEmailNotification(title, content, testConfig, { provider: emailProvider }); + message = success ? '邮件通知发送成功' : '邮件通知发送失败,请检查配置'; + } + } else if (body.type === 'bark') { + const testConfig = { + ...config, + BARK_SERVER: body.BARK_SERVER, + BARK_DEVICE_KEY: body.BARK_DEVICE_KEY, + BARK_IS_ARCHIVE: body.BARK_IS_ARCHIVE + }; + + const title = '测试通知'; + const content = '这是一条测试通知,用于验证Bark通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + + success = await sendBarkNotification(title, content, testConfig); + message = success ? 'Bark通知发送成功' : 'Bark通知发送失败,请检查配置'; + } + + return new Response( + JSON.stringify({ success, message }), + { headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error('测试通知失败:', error); + return new Response( + JSON.stringify({ success: false, message: '测试通知失败: ' + error.message }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } + } + + if (path === '/subscriptions') { + if (method === 'GET') { + const subscriptions = await getAllSubscriptions(env); + return new Response( + JSON.stringify(subscriptions), + { headers: { 'Content-Type': 'application/json' } } + ); + } + + if (method === 'POST') { + const subscription = await request.json(); + const result = await createSubscription(subscription, env); + + return new Response( + JSON.stringify(result), + { + status: result.success ? 201 : 400, + headers: { 'Content-Type': 'application/json' } + } + ); + } + } + + if (path.startsWith('/subscriptions/')) { + const parts = path.split('/'); + const id = parts[2]; + + if (parts[3] === 'toggle-status' && method === 'POST') { + const body = await request.json(); + const result = await toggleSubscriptionStatus(id, body.isActive, env); + + return new Response( + JSON.stringify(result), + { + status: result.success ? 200 : 400, + headers: { 'Content-Type': 'application/json' } + } + ); + } + + if (parts[3] === 'test-notify' && method === 'POST') { + const result = await testSingleSubscriptionNotification(id, env); + return new Response(JSON.stringify(result), { status: result.success ? 200 : 500, headers: { 'Content-Type': 'application/json' } }); + } + + if (parts[3] === 'renew' && method === 'POST') { + let options = {}; + try { + const body = await request.json(); + options = body || {}; + } catch (e) { + // 如果没有请求体,使用默认空对象 + } + const result = await manualRenewSubscription(id, env, options); + return new Response(JSON.stringify(result), { status: result.success ? 200 : 400, headers: { 'Content-Type': 'application/json' } }); + } + + if (parts[3] === 'payments' && method === 'GET') { + const subscription = await getSubscription(id, env); + if (!subscription) { + return new Response(JSON.stringify({ success: false, message: '订阅不存在' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); + } + return new Response(JSON.stringify({ success: true, payments: subscription.paymentHistory || [] }), { headers: { 'Content-Type': 'application/json' } }); + } + + if (parts[3] === 'payments' && parts[4] && method === 'DELETE') { + const paymentId = parts[4]; + const result = await deletePaymentRecord(id, paymentId, env); + return new Response(JSON.stringify(result), { status: result.success ? 200 : 400, headers: { 'Content-Type': 'application/json' } }); + } + + if (parts[3] === 'payments' && parts[4] && method === 'PUT') { + const paymentId = parts[4]; + const paymentData = await request.json(); + const result = await updatePaymentRecord(id, paymentId, paymentData, env); + return new Response(JSON.stringify(result), { status: result.success ? 200 : 400, headers: { 'Content-Type': 'application/json' } }); + } + + if (method === 'GET') { + const subscription = await getSubscription(id, env); + + return new Response( + JSON.stringify(subscription), + { headers: { 'Content-Type': 'application/json' } } + ); + } + + if (method === 'PUT') { + const subscription = await request.json(); + const result = await updateSubscription(id, subscription, env); + + return new Response( + JSON.stringify(result), + { + status: result.success ? 200 : 400, + headers: { 'Content-Type': 'application/json' } + } + ); + } + + if (method === 'DELETE') { + const result = await deleteSubscription(id, env); + + return new Response( + JSON.stringify(result), + { + status: result.success ? 200 : 400, + headers: { 'Content-Type': 'application/json' } + } + ); + } + } + + // 处理第三方通知API + if (path.startsWith('/notify/')) { + const pathSegments = path.split('/'); + // 允许通过路径、Authorization 头或查询参数三种方式传入访问令牌 + const tokenFromPath = pathSegments[2] || ''; + const tokenFromHeader = (request.headers.get('Authorization') || '').replace(/^Bearer\s+/i, '').trim(); + const tokenFromQuery = url.searchParams.get('token') || ''; + const providedToken = tokenFromPath || tokenFromHeader || tokenFromQuery; + const expectedToken = config.THIRD_PARTY_API_TOKEN || ''; + + if (!expectedToken) { + return new Response( + JSON.stringify({ message: '第三方 API 已禁用,请在后台配置访问令牌后使用' }), + { status: 403, headers: { 'Content-Type': 'application/json' } } + ); + } + + if (!providedToken || providedToken !== expectedToken) { + return new Response( + JSON.stringify({ message: '访问未授权,令牌无效或缺失' }), + { status: 401, headers: { 'Content-Type': 'application/json' } } + ); + } + + if (method === 'POST') { + try { + const body = await request.json(); + const title = body.title || '第三方通知'; + const content = body.content || ''; + + if (!content) { + return new Response( + JSON.stringify({ message: '缺少必填参数 content' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + const config = await getConfig(env); + const bodyTagsRaw = Array.isArray(body.tags) + ? body.tags + : (typeof body.tags === 'string' ? body.tags.split(/[,,\s]+/) : []); + const bodyTags = Array.isArray(bodyTagsRaw) + ? bodyTagsRaw.filter(tag => typeof tag === 'string' && tag.trim().length > 0).map(tag => tag.trim()) + : []; + + // 使用多渠道发送通知 + await sendNotificationToAllChannels(title, content, config, '[第三方API]', { + metadata: { tags: bodyTags } + }); + + return new Response( + JSON.stringify({ + message: '发送成功', + response: { + errcode: 0, + errmsg: 'ok', + msgid: 'MSGID' + Date.now() + } + }), + { headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error('[第三方API] 发送通知失败:', error); + return new Response( + JSON.stringify({ + message: '发送失败', + response: { + errcode: 1, + errmsg: error.message + } + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } + } + } + + return new Response( + JSON.stringify({ success: false, message: '未找到请求的资源' }), + { status: 404, headers: { 'Content-Type': 'application/json' } } + ); + } +}; + +// 工具函数 +function generateRandomSecret() { + // 生成一个64字符的随机密钥 + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'; + let result = ''; + for (let i = 0; i < 64; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +async function getConfig(env) { + try { + if (!env.SUBSCRIPTIONS_KV) { + console.error('[配置] KV存储未绑定'); + throw new Error('KV存储未绑定'); + } + + const data = await env.SUBSCRIPTIONS_KV.get('config'); + console.log('[配置] 从KV读取配置:', data ? '成功' : '空配置'); + + const config = data ? JSON.parse(data) : {}; + + // 确保JWT_SECRET的一致性 + let jwtSecret = config.JWT_SECRET; + if (!jwtSecret || jwtSecret === 'your-secret-key') { + jwtSecret = generateRandomSecret(); + console.log('[配置] 生成新的JWT密钥'); + + // 保存新的JWT密钥 + const updatedConfig = { ...config, JWT_SECRET: jwtSecret }; + await env.SUBSCRIPTIONS_KV.put('config', JSON.stringify(updatedConfig)); + } + + const finalConfig = { + ADMIN_USERNAME: config.ADMIN_USERNAME || 'admin', + ADMIN_PASSWORD: config.ADMIN_PASSWORD || 'password', + JWT_SECRET: jwtSecret, + TG_BOT_TOKEN: config.TG_BOT_TOKEN || '', + TG_CHAT_ID: config.TG_CHAT_ID || '', + NOTIFYX_API_KEY: config.NOTIFYX_API_KEY || '', + WEBHOOK_URL: config.WEBHOOK_URL || '', + WEBHOOK_METHOD: config.WEBHOOK_METHOD || 'POST', + WEBHOOK_HEADERS: config.WEBHOOK_HEADERS || '', + WEBHOOK_TEMPLATE: config.WEBHOOK_TEMPLATE || '', + SHOW_LUNAR: config.SHOW_LUNAR === true, + WECHATBOT_WEBHOOK: config.WECHATBOT_WEBHOOK || '', + WECHATBOT_MSG_TYPE: config.WECHATBOT_MSG_TYPE || 'text', + WECHATBOT_AT_MOBILES: config.WECHATBOT_AT_MOBILES || '', + WECHATBOT_AT_ALL: config.WECHATBOT_AT_ALL || 'false', + RESEND_API_KEY: config.RESEND_API_KEY || '', + RESEND_FROM: config.RESEND_FROM || config.EMAIL_FROM || '', + RESEND_FROM_NAME: config.RESEND_FROM_NAME || config.EMAIL_FROM_NAME || '', + RESEND_TO: config.RESEND_TO || config.EMAIL_TO || '', + MAILGUN_API_KEY: config.MAILGUN_API_KEY || '', + MAILGUN_FROM: config.MAILGUN_FROM || config.EMAIL_FROM || '', + MAILGUN_FROM_NAME: config.MAILGUN_FROM_NAME || config.EMAIL_FROM_NAME || '', + MAILGUN_TO: config.MAILGUN_TO || config.EMAIL_TO || '', + EMAIL_FROM: config.EMAIL_FROM || '', + EMAIL_FROM_NAME: config.EMAIL_FROM_NAME || '', + EMAIL_TO: config.EMAIL_TO || '', + SMTP_HOST: config.SMTP_HOST || '', + SMTP_PORT: config.SMTP_PORT || '', + SMTP_USER: config.SMTP_USER || '', + SMTP_PASS: config.SMTP_PASS || '', + SMTP_FROM: config.SMTP_FROM || config.EMAIL_FROM || '', + SMTP_FROM_NAME: config.SMTP_FROM_NAME || config.EMAIL_FROM_NAME || '', + SMTP_TO: config.SMTP_TO || config.EMAIL_TO || '', + BARK_DEVICE_KEY: config.BARK_DEVICE_KEY || '', + BARK_SERVER: config.BARK_SERVER || 'https://api.day.app', + BARK_IS_ARCHIVE: config.BARK_IS_ARCHIVE || 'false', + ENABLED_NOTIFIERS: config.ENABLED_NOTIFIERS || ['notifyx'], + THEME_MODE: config.THEME_MODE || 'system', // 默认主题为跟随系统 + TIMEZONE: config.TIMEZONE || 'UTC', // 新增时区字段 + NOTIFICATION_HOURS: Array.isArray(config.NOTIFICATION_HOURS) ? config.NOTIFICATION_HOURS : [], + THIRD_PARTY_API_TOKEN: config.THIRD_PARTY_API_TOKEN || '' + }; + + console.log('[配置] 最终配置用户名:', finalConfig.ADMIN_USERNAME); + return finalConfig; + } catch (error) { + console.error('[配置] 获取配置失败:', error); + const defaultJwtSecret = generateRandomSecret(); + + return { + ADMIN_USERNAME: 'admin', + ADMIN_PASSWORD: 'password', + JWT_SECRET: defaultJwtSecret, + TG_BOT_TOKEN: '', + TG_CHAT_ID: '', + NOTIFYX_API_KEY: '', + WEBHOOK_URL: '', + WEBHOOK_METHOD: 'POST', + WEBHOOK_HEADERS: '', + WEBHOOK_TEMPLATE: '', + SHOW_LUNAR: true, + WECHATBOT_WEBHOOK: '', + WECHATBOT_MSG_TYPE: 'text', + WECHATBOT_AT_MOBILES: '', + WECHATBOT_AT_ALL: 'false', + RESEND_API_KEY: '', + RESEND_FROM: '', + RESEND_FROM_NAME: '', + RESEND_TO: '', + MAILGUN_API_KEY: '', + MAILGUN_FROM: '', + MAILGUN_FROM_NAME: '', + MAILGUN_TO: '', + EMAIL_FROM: '', + EMAIL_FROM_NAME: '', + EMAIL_TO: '', + SMTP_HOST: '', + SMTP_PORT: '', + SMTP_USER: '', + SMTP_PASS: '', + SMTP_FROM: '', + SMTP_FROM_NAME: '', + SMTP_TO: '', + ENABLED_NOTIFIERS: ['notifyx'], + NOTIFICATION_HOURS: [], + TIMEZONE: 'UTC', // 新增时区字段 + THIRD_PARTY_API_TOKEN: '' + }; + } +} + +async function generateJWT(username, secret) { + const header = { alg: 'HS256', typ: 'JWT' }; + const payload = { username, iat: Math.floor(Date.now() / 1000) }; + + const headerBase64 = btoa(JSON.stringify(header)); + const payloadBase64 = btoa(JSON.stringify(payload)); + + const signatureInput = headerBase64 + '.' + payloadBase64; + const signature = await CryptoJS.HmacSHA256(signatureInput, secret); + + return headerBase64 + '.' + payloadBase64 + '.' + signature; +} + +async function verifyJWT(token, secret) { + try { + if (!token || !secret) { + console.log('[JWT] Token或Secret为空'); + return null; + } + + const parts = token.split('.'); + if (parts.length !== 3) { + console.log('[JWT] Token格式错误,部分数量:', parts.length); + return null; + } + + const [headerBase64, payloadBase64, signature] = parts; + const signatureInput = headerBase64 + '.' + payloadBase64; + const expectedSignature = await CryptoJS.HmacSHA256(signatureInput, secret); + + if (signature !== expectedSignature) { + console.log('[JWT] 签名验证失败'); + return null; + } + + const payload = JSON.parse(atob(payloadBase64)); + console.log('[JWT] 验证成功,用户:', payload.username); + return payload; + } catch (error) { + console.error('[JWT] 验证过程出错:', error); + return null; + } +} + +async function getAllSubscriptions(env) { + try { + const data = await env.SUBSCRIPTIONS_KV.get('subscriptions'); + return data ? JSON.parse(data) : []; + } catch (error) { + return []; + } +} + +async function getSubscription(id, env) { + const subscriptions = await getAllSubscriptions(env); + return subscriptions.find(s => s.id === id); +} + +// 2. 修改 createSubscription,支持 useLunar 字段 +async function createSubscription(subscription, env) { + try { + const subscriptions = await getAllSubscriptions(env); + + if (!subscription.name || !subscription.expiryDate) { + return { success: false, message: '缺少必填字段' }; + } + + let expiryDate = new Date(subscription.expiryDate); + const config = await getConfig(env); + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + + + let useLunar = !!subscription.useLunar; + if (useLunar) { + let lunar = lunarCalendar.solar2lunar( + expiryDate.getFullYear(), + expiryDate.getMonth() + 1, + expiryDate.getDate() + ); + + if (lunar && subscription.periodValue && subscription.periodUnit) { + // 如果到期日<=今天,自动推算到下一个周期 + while (expiryDate <= currentTime) { + lunar = lunarBiz.addLunarPeriod(lunar, subscription.periodValue, subscription.periodUnit); + const solar = lunarBiz.lunar2solar(lunar); + expiryDate = new Date(solar.year, solar.month - 1, solar.day); + } + subscription.expiryDate = expiryDate.toISOString(); + } + } else { + if (expiryDate < currentTime && subscription.periodValue && subscription.periodUnit) { + while (expiryDate < currentTime) { + if (subscription.periodUnit === 'minute') { + expiryDate.setMinutes(expiryDate.getMinutes() + subscription.periodValue); + } else if (subscription.periodUnit === 'hour') { + expiryDate.setHours(expiryDate.getHours() + subscription.periodValue); + } else if (subscription.periodUnit === 'day') { + expiryDate.setDate(expiryDate.getDate() + subscription.periodValue); + } else if (subscription.periodUnit === 'month') { + expiryDate.setMonth(expiryDate.getMonth() + subscription.periodValue); + } else if (subscription.periodUnit === 'year') { + expiryDate.setFullYear(expiryDate.getFullYear() + subscription.periodValue); + } + } + subscription.expiryDate = expiryDate.toISOString(); + } + } + + const reminderSetting = resolveReminderSetting(subscription); + const hasNotificationHours = Object.prototype.hasOwnProperty.call(subscription, 'notificationHours'); + const hasEnabledNotifiers = Object.prototype.hasOwnProperty.call(subscription, 'enabledNotifiers'); + const hasEmailTo = Object.prototype.hasOwnProperty.call(subscription, 'emailTo'); + const normalizedNotificationHours = hasNotificationHours + ? normalizeNotificationHours(subscription.notificationHours) + : undefined; + const normalizedNotifiers = hasEnabledNotifiers + ? normalizeNotifierList(subscription.enabledNotifiers) + : undefined; + const normalizedEmailTo = hasEmailTo + ? normalizeEmailRecipients(subscription.emailTo) + : undefined; + + const initialPaymentDate = subscription.startDate || currentTime.toISOString(); + const newSubscription = { + id: Date.now().toString(), // 前端使用本地时间戳 + name: subscription.name, + subscriptionMode: subscription.subscriptionMode || 'cycle', // 默认循环订阅 + customType: subscription.customType || '', + category: subscription.category ? subscription.category.trim() : '', + startDate: subscription.startDate || null, + expiryDate: subscription.expiryDate, + periodValue: subscription.periodValue || 1, + periodUnit: subscription.periodUnit || 'month', + reminderUnit: reminderSetting.unit, + reminderValue: reminderSetting.value, + reminderDays: reminderSetting.unit === 'day' ? reminderSetting.value : undefined, + reminderHours: (reminderSetting.unit === 'hour' || reminderSetting.unit === 'minute') ? reminderSetting.value : undefined, + notificationHours: normalizedNotificationHours, + enabledNotifiers: normalizedNotifiers, + emailTo: normalizedEmailTo, + notes: subscription.notes || '', + amount: subscription.amount || null, + currency: subscription.currency || 'CNY', // 使用传入的币种,默认为CNY + lastPaymentDate: initialPaymentDate, + paymentHistory: subscription.amount ? [{ + id: Date.now().toString(), + date: initialPaymentDate, + amount: subscription.amount, + type: 'initial', + note: '初始订阅', + periodStart: subscription.startDate || initialPaymentDate, + periodEnd: subscription.expiryDate + }] : [], + isActive: subscription.isActive !== false, + autoRenew: subscription.autoRenew !== false, + useLunar: useLunar, + createdAt: new Date().toISOString() + }; + + subscriptions.push(newSubscription); + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: newSubscription }; + } catch (error) { + console.error("创建订阅异常:", error && error.stack ? error.stack : error); + return { success: false, message: error && error.message ? error.message : '创建订阅失败' }; + } +} + +// 3. 修改 updateSubscription,支持 useLunar 字段 +async function updateSubscription(id, subscription, env) { + try { + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === id); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + if (!subscription.name || !subscription.expiryDate) { + return { success: false, message: '缺少必填字段' }; + } + + let expiryDate = new Date(subscription.expiryDate); + const config = await getConfig(env); + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + + let useLunar = !!subscription.useLunar; + if (useLunar) { + let lunar = lunarCalendar.solar2lunar( + expiryDate.getFullYear(), + expiryDate.getMonth() + 1, + expiryDate.getDate() + ); + if (!lunar) { + return { success: false, message: '农历日期超出支持范围(1900-2100年)' }; + } + if (lunar && expiryDate < currentTime && subscription.periodValue && subscription.periodUnit) { + // 新增:循环加周期,直到 expiryDate > currentTime + do { + lunar = lunarBiz.addLunarPeriod(lunar, subscription.periodValue, subscription.periodUnit); + const solar = lunarBiz.lunar2solar(lunar); + expiryDate = new Date(solar.year, solar.month - 1, solar.day); + } while (expiryDate < currentTime); + subscription.expiryDate = expiryDate.toISOString(); + } + } else { + if (expiryDate < currentTime && subscription.periodValue && subscription.periodUnit) { + while (expiryDate < currentTime) { + if (subscription.periodUnit === 'minute') { + expiryDate.setMinutes(expiryDate.getMinutes() + subscription.periodValue); + } else if (subscription.periodUnit === 'hour') { + expiryDate.setHours(expiryDate.getHours() + subscription.periodValue); + } else if (subscription.periodUnit === 'day') { + expiryDate.setDate(expiryDate.getDate() + subscription.periodValue); + } else if (subscription.periodUnit === 'month') { + expiryDate.setMonth(expiryDate.getMonth() + subscription.periodValue); + } else if (subscription.periodUnit === 'year') { + expiryDate.setFullYear(expiryDate.getFullYear() + subscription.periodValue); + } + } + subscription.expiryDate = expiryDate.toISOString(); + } + } + + const reminderSource = { + reminderUnit: subscription.reminderUnit !== undefined ? subscription.reminderUnit : subscriptions[index].reminderUnit, + reminderValue: subscription.reminderValue !== undefined ? subscription.reminderValue : subscriptions[index].reminderValue, + reminderHours: subscription.reminderHours !== undefined ? subscription.reminderHours : subscriptions[index].reminderHours, + reminderDays: subscription.reminderDays !== undefined ? subscription.reminderDays : subscriptions[index].reminderDays + }; + const reminderSetting = resolveReminderSetting(reminderSource); + const hasNotificationHours = Object.prototype.hasOwnProperty.call(subscription, 'notificationHours'); + const hasEnabledNotifiers = Object.prototype.hasOwnProperty.call(subscription, 'enabledNotifiers'); + const hasEmailTo = Object.prototype.hasOwnProperty.call(subscription, 'emailTo'); + const normalizedNotificationHours = hasNotificationHours + ? normalizeNotificationHours(subscription.notificationHours) + : subscriptions[index].notificationHours; + const normalizedNotifiers = hasEnabledNotifiers + ? normalizeNotifierList(subscription.enabledNotifiers) + : subscriptions[index].enabledNotifiers; + const normalizedEmailTo = hasEmailTo + ? normalizeEmailRecipients(subscription.emailTo) + : subscriptions[index].emailTo; + + const oldSubscription = subscriptions[index]; + const newAmount = subscription.amount !== undefined ? subscription.amount : oldSubscription.amount; + + let paymentHistory = oldSubscription.paymentHistory || []; + + if (newAmount !== oldSubscription.amount) { + const initialPaymentIndex = paymentHistory.findIndex(p => p.type === 'initial'); + if (initialPaymentIndex !== -1) { + paymentHistory[initialPaymentIndex] = { + ...paymentHistory[initialPaymentIndex], + amount: newAmount + }; + } + } + + subscriptions[index] = { + ...subscriptions[index], + name: subscription.name, + subscriptionMode: subscription.subscriptionMode || subscriptions[index].subscriptionMode || 'cycle', // 如果没有提供 subscriptionMode,则使用旧的 subscriptionMode + customType: subscription.customType || subscriptions[index].customType || '', + category: subscription.category !== undefined ? subscription.category.trim() : (subscriptions[index].category || ''), + startDate: subscription.startDate || subscriptions[index].startDate, + expiryDate: subscription.expiryDate, + periodValue: subscription.periodValue || subscriptions[index].periodValue || 1, + periodUnit: subscription.periodUnit || subscriptions[index].periodUnit || 'month', + reminderUnit: reminderSetting.unit, + reminderValue: reminderSetting.value, + reminderDays: reminderSetting.unit === 'day' ? reminderSetting.value : undefined, + reminderHours: (reminderSetting.unit === 'hour' || reminderSetting.unit === 'minute') ? reminderSetting.value : undefined, + notificationHours: normalizedNotificationHours, + enabledNotifiers: normalizedNotifiers, + emailTo: normalizedEmailTo, + notes: subscription.notes || '', + amount: newAmount, // 使用新的变量 + currency: subscription.currency || subscriptions[index].currency || 'CNY', // 更新币种 + lastPaymentDate: subscriptions[index].lastPaymentDate || subscriptions[index].startDate || subscriptions[index].createdAt || currentTime.toISOString(), + paymentHistory: paymentHistory, // 保存更新后的支付历史 + isActive: subscription.isActive !== undefined ? subscription.isActive : subscriptions[index].isActive, + autoRenew: subscription.autoRenew !== undefined ? subscription.autoRenew : (subscriptions[index].autoRenew !== undefined ? subscriptions[index].autoRenew : true), + useLunar: useLunar, + updatedAt: new Date().toISOString() + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: subscriptions[index] }; + } catch (error) { + return { success: false, message: '更新订阅失败' }; + } +} + +async function deleteSubscription(id, env) { + try { + const subscriptions = await getAllSubscriptions(env); + const filteredSubscriptions = subscriptions.filter(s => s.id !== id); + + if (filteredSubscriptions.length === subscriptions.length) { + return { success: false, message: '订阅不存在' }; + } + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(filteredSubscriptions)); + + return { success: true }; + } catch (error) { + return { success: false, message: '删除订阅失败' }; + } +} + +async function manualRenewSubscription(id, env, options = {}) { + try { + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === id); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + const subscription = subscriptions[index]; + + if (!subscription.periodValue || !subscription.periodUnit) { + return { success: false, message: '订阅未设置续订周期' }; + } + + const config = await getConfig(env); + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + const todayMidnight = getTimezoneMidnightTimestamp(currentTime, timezone); + + // 参数处理 + const paymentDate = options.paymentDate ? new Date(options.paymentDate) : currentTime; + const amount = options.amount !== undefined ? options.amount : subscription.amount || 0; + const periodMultiplier = options.periodMultiplier || 1; + const note = options.note || '手动续订'; + const mode = subscription.subscriptionMode || 'cycle'; // 获取订阅模式 + + let newStartDate; + let currentExpiryDate = new Date(subscription.expiryDate); + + // 1. 确定新的周期起始日 (New Start Date) + if (mode === 'reset') { + // 重置模式:忽略旧的到期日,从今天(或支付日)开始 + newStartDate = new Date(paymentDate); + } else { + // 循环模式 (Cycle) + // 如果当前还没过期,从旧的 expiryDate 接着算 (无缝衔接) + // 如果已经过期了,为了避免补交过去空窗期的费,通常从今天开始算(或者你可以选择补齐,这里采用通用逻辑:过期则从今天开始) + if (currentExpiryDate.getTime() > paymentDate.getTime()) { + newStartDate = new Date(currentExpiryDate); + } else { + newStartDate = new Date(paymentDate); + } + } + + // 2. 计算新的到期日 (New Expiry Date) + let newExpiryDate; + if (subscription.useLunar) { + // 农历逻辑 + const solarStart = { + year: newStartDate.getFullYear(), + month: newStartDate.getMonth() + 1, + day: newStartDate.getDate() + }; + let lunar = lunarCalendar.solar2lunar(solarStart.year, solarStart.month, solarStart.day); + + let nextLunar = lunar; + for (let i = 0; i < periodMultiplier; i++) { + nextLunar = lunarBiz.addLunarPeriod(nextLunar, subscription.periodValue, subscription.periodUnit); + } + const solar = lunarBiz.lunar2solar(nextLunar); + newExpiryDate = new Date(solar.year, solar.month - 1, solar.day); + } else { + // 公历逻辑 + newExpiryDate = new Date(newStartDate); + const totalPeriodValue = subscription.periodValue * periodMultiplier; + + if (subscription.periodUnit === 'day') { + newExpiryDate.setDate(newExpiryDate.getDate() + totalPeriodValue); + } else if (subscription.periodUnit === 'month') { + newExpiryDate.setMonth(newExpiryDate.getMonth() + totalPeriodValue); + } else if (subscription.periodUnit === 'year') { + newExpiryDate.setFullYear(newExpiryDate.getFullYear() + totalPeriodValue); + } + } + + const paymentRecord = { + id: Date.now().toString(), + date: paymentDate.toISOString(), + amount: amount, + type: 'manual', + note: note, + periodStart: newStartDate.toISOString(), // 记录实际的计费开始日 + periodEnd: newExpiryDate.toISOString() + }; + + const paymentHistory = subscription.paymentHistory || []; + paymentHistory.push(paymentRecord); + + subscriptions[index] = { + ...subscription, + startDate: newStartDate.toISOString(), // 关键修复:更新 startDate,这样下次编辑时,Start + Period = Expiry 成立 + expiryDate: newExpiryDate.toISOString(), + lastPaymentDate: paymentDate.toISOString(), + paymentHistory + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: subscriptions[index], message: '续订成功' }; + } catch (error) { + console.error('手动续订失败:', error); + return { success: false, message: '续订失败: ' + error.message }; + } +} + +async function deletePaymentRecord(subscriptionId, paymentId, env) { + try { + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === subscriptionId); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + const subscription = subscriptions[index]; + const paymentHistory = subscription.paymentHistory || []; + const paymentIndex = paymentHistory.findIndex(p => p.id === paymentId); + + if (paymentIndex === -1) { + return { success: false, message: '支付记录不存在' }; + } + + const deletedPayment = paymentHistory[paymentIndex]; + + // 删除支付记录 + paymentHistory.splice(paymentIndex, 1); + + // 回退订阅周期和更新 lastPaymentDate + let newExpiryDate = subscription.expiryDate; + let newLastPaymentDate = subscription.lastPaymentDate; + + if (paymentHistory.length > 0) { + // 找到剩余支付记录中 periodEnd 最晚的那条(最新的续订) + const sortedByPeriodEnd = [...paymentHistory].sort((a, b) => { + const dateA = a.periodEnd ? new Date(a.periodEnd) : new Date(0); + const dateB = b.periodEnd ? new Date(b.periodEnd) : new Date(0); + return dateB - dateA; + }); + + // 订阅的到期日期应该是最新续订的 periodEnd + if (sortedByPeriodEnd[0].periodEnd) { + newExpiryDate = sortedByPeriodEnd[0].periodEnd; + } + + // 找到最新的支付记录日期 + const sortedByDate = [...paymentHistory].sort((a, b) => new Date(b.date) - new Date(a.date)); + newLastPaymentDate = sortedByDate[0].date; + } else { + // 如果没有支付记录了,回退到初始状态 + // expiryDate 保持不变或使用 periodStart(如果删除的记录有) + if (deletedPayment.periodStart) { + newExpiryDate = deletedPayment.periodStart; + } + newLastPaymentDate = subscription.startDate || subscription.createdAt || subscription.expiryDate; + } + + subscriptions[index] = { + ...subscription, + expiryDate: newExpiryDate, + paymentHistory, + lastPaymentDate: newLastPaymentDate + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: subscriptions[index], message: '支付记录已删除' }; + } catch (error) { + console.error('删除支付记录失败:', error); + return { success: false, message: '删除失败: ' + error.message }; + } +} + +async function updatePaymentRecord(subscriptionId, paymentId, paymentData, env) { + try { + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === subscriptionId); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + const subscription = subscriptions[index]; + const paymentHistory = subscription.paymentHistory || []; + const paymentIndex = paymentHistory.findIndex(p => p.id === paymentId); + + if (paymentIndex === -1) { + return { success: false, message: '支付记录不存在' }; + } + + // 更新支付记录 + paymentHistory[paymentIndex] = { + ...paymentHistory[paymentIndex], + date: paymentData.date || paymentHistory[paymentIndex].date, + amount: paymentData.amount !== undefined ? paymentData.amount : paymentHistory[paymentIndex].amount, + note: paymentData.note !== undefined ? paymentData.note : paymentHistory[paymentIndex].note + }; + + // 更新 lastPaymentDate 为最新的支付记录日期 + const sortedPayments = [...paymentHistory].sort((a, b) => new Date(b.date) - new Date(a.date)); + const newLastPaymentDate = sortedPayments[0].date; + + subscriptions[index] = { + ...subscription, + paymentHistory, + lastPaymentDate: newLastPaymentDate + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: subscriptions[index], message: '支付记录已更新' }; + } catch (error) { + console.error('更新支付记录失败:', error); + return { success: false, message: '更新失败: ' + error.message }; + } +} + +async function toggleSubscriptionStatus(id, isActive, env) { + try { + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === id); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + subscriptions[index] = { + ...subscriptions[index], + isActive: isActive, + updatedAt: new Date().toISOString() + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: subscriptions[index] }; + } catch (error) { + return { success: false, message: '更新订阅状态失败' }; + } +} + +async function testSingleSubscriptionNotification(id, env) { + try { + const subscription = await getSubscription(id, env); + if (!subscription) { + return { success: false, message: '未找到该订阅' }; + } + const config = await getConfig(env); + + const title = `手动测试通知: ${subscription.name}`; + + // 检查是否显示农历(从配置中获取,默认不显示) + const showLunar = config.SHOW_LUNAR === true; + let lunarExpiryText = ''; + + if (showLunar) { + // 计算农历日期 + const expiryDateObj = new Date(subscription.expiryDate); + const lunarExpiry = lunarCalendar.solar2lunar(expiryDateObj.getFullYear(), expiryDateObj.getMonth() + 1, expiryDateObj.getDate()); + lunarExpiryText = lunarExpiry ? ` (农历: ${lunarExpiry.fullStr})` : ''; + } + + // 格式化到期日期(使用所选时区) + const timezone = config?.TIMEZONE || 'UTC'; + const formattedExpiryDate = formatTimeInTimezone(new Date(subscription.expiryDate), timezone, 'datetime'); + const currentTime = formatTimeInTimezone(new Date(), timezone, 'datetime'); + + // 获取日历类型和自动续期状态 + const calendarType = subscription.useLunar ? '农历' : '公历'; + const autoRenewText = subscription.autoRenew ? '是' : '否'; + const amountText = subscription.amount ? `\n金额: ¥${subscription.amount.toFixed(2)}/周期` : ''; + + const commonContent = `**订阅详情** +类型: ${subscription.customType || '其他'}${amountText} +日历类型: ${calendarType} +到期日期: ${formattedExpiryDate}${lunarExpiryText} +自动续期: ${autoRenewText} +备注: ${subscription.notes || '无'} +发送时间: ${currentTime} +当前时区: ${formatTimezoneDisplay(timezone)}`; + + // 使用多渠道发送 + const tags = extractTagsFromSubscriptions([subscription]); + await sendNotificationToAllChannels(title, commonContent, config, '[手动测试]', { + metadata: { tags }, + notifiers: subscription.enabledNotifiers, + emailTo: subscription.emailTo + }); + + return { success: true, message: '测试通知已发送到所有启用的渠道' }; + + } catch (error) { + console.error('[手动测试] 发送失败:', error); + return { success: false, message: '发送时发生错误: ' + error.message }; + } +} + +async function sendWebhookNotification(title, content, config, metadata = {}) { + try { + if (!config.WEBHOOK_URL) { + console.error('[Webhook通知] 通知未配置,缺少URL'); + return false; + } + + console.log('[Webhook通知] 开始发送通知到: ' + config.WEBHOOK_URL); + + let requestBody; + let headers = { 'Content-Type': 'application/json' }; + + // 处理自定义请求头 + if (config.WEBHOOK_HEADERS) { + try { + const customHeaders = JSON.parse(config.WEBHOOK_HEADERS); + headers = { ...headers, ...customHeaders }; + } catch (error) { + console.warn('[Webhook通知] 自定义请求头格式错误,使用默认请求头'); + } + } + + const tagsArray = Array.isArray(metadata.tags) + ? metadata.tags.filter(tag => typeof tag === 'string' && tag.trim().length > 0).map(tag => tag.trim()) + : []; + const tagsBlock = tagsArray.length ? tagsArray.map(tag => `- ${tag}`).join('\n') : ''; + const tagsLine = tagsArray.length ? '标签:' + tagsArray.join('、') : ''; + const timestamp = formatTimeInTimezone(new Date(), config?.TIMEZONE || 'UTC', 'datetime'); + const formattedMessage = [title, content, tagsLine, `发送时间:${timestamp}`] + .filter(section => section && section.trim().length > 0) + .join('\n\n'); + + const templateData = { + title, + content, + tags: tagsBlock, + tagsLine, + rawTags: tagsArray, + timestamp, + formattedMessage, + message: formattedMessage + }; + + const escapeForJson = (value) => { + if (value === null || value === undefined) { + return ''; + } + return JSON.stringify(String(value)).slice(1, -1); + }; + + const applyTemplate = (template, data) => { + const templateString = JSON.stringify(template); + const replaced = templateString.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => { + if (Object.prototype.hasOwnProperty.call(data, key)) { + return escapeForJson(data[key]); + } + return ''; + }); + return JSON.parse(replaced); + }; + + // 处理消息模板 + if (config.WEBHOOK_TEMPLATE) { + try { + const template = JSON.parse(config.WEBHOOK_TEMPLATE); + requestBody = applyTemplate(template, templateData); + } catch (error) { + console.warn('[Webhook通知] 消息模板格式错误,使用默认格式'); + requestBody = { + title, + content, + tags: tagsArray, + tagsLine, + timestamp, + message: formattedMessage + }; + } + } else { + requestBody = { + title, + content, + tags: tagsArray, + tagsLine, + timestamp, + message: formattedMessage + }; + } + + const response = await fetch(config.WEBHOOK_URL, { + method: config.WEBHOOK_METHOD || 'POST', + headers: headers, + body: JSON.stringify(requestBody) + }); + + const result = await response.text(); + console.log('[Webhook通知] 发送结果:', response.status, result); + return response.ok; + } catch (error) { + console.error('[Webhook通知] 发送通知失败:', error); + return false; + } +} + +async function sendWechatBotNotification(title, content, config) { + try { + if (!config.WECHATBOT_WEBHOOK) { + console.error('[企业微信机器人] 通知未配置,缺少Webhook URL'); + return false; + } + + console.log('[企业微信机器人] 开始发送通知到: ' + config.WECHATBOT_WEBHOOK); + + // 构建消息内容 + let messageData; + const msgType = config.WECHATBOT_MSG_TYPE || 'text'; + + if (msgType === 'markdown') { + // Markdown 消息格式 + const markdownContent = `# ${title}\n\n${content}`; + messageData = { + msgtype: 'markdown', + markdown: { + content: markdownContent + } + }; + } else { + // 文本消息格式 - 优化显示 + const textContent = `${title}\n\n${content}`; + messageData = { + msgtype: 'text', + text: { + content: textContent + } + }; + } + + // 处理@功能 + if (config.WECHATBOT_AT_ALL === 'true') { + // @所有人 + if (msgType === 'text') { + messageData.text.mentioned_list = ['@all']; + } + } else if (config.WECHATBOT_AT_MOBILES) { + // @指定手机号 + const mobiles = config.WECHATBOT_AT_MOBILES.split(',').map(m => m.trim()).filter(m => m); + if (mobiles.length > 0) { + if (msgType === 'text') { + messageData.text.mentioned_mobile_list = mobiles; + } + } + } + + console.log('[企业微信机器人] 发送消息数据:', JSON.stringify(messageData, null, 2)); + + const response = await fetch(config.WECHATBOT_WEBHOOK, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(messageData) + }); + + const responseText = await response.text(); + console.log('[企业微信机器人] 响应状态:', response.status); + console.log('[企业微信机器人] 响应内容:', responseText); + + if (response.ok) { + try { + const result = JSON.parse(responseText); + if (result.errcode === 0) { + console.log('[企业微信机器人] 通知发送成功'); + return true; + } else { + console.error('[企业微信机器人] 发送失败,错误码:', result.errcode, '错误信息:', result.errmsg); + return false; + } + } catch (parseError) { + console.error('[企业微信机器人] 解析响应失败:', parseError); + return false; + } + } else { + console.error('[企业微信机器人] HTTP请求失败,状态码:', response.status); + return false; + } + } catch (error) { + console.error('[企业微信机器人] 发送通知失败:', error); + return false; + } +} + +// 优化通知内容格式 +function resolveReminderSetting(subscription) { + const defaultDays = subscription && subscription.reminderDays !== undefined ? Number(subscription.reminderDays) : 7; + let unit = subscription && subscription.reminderUnit ? subscription.reminderUnit : 'day'; + + // 兼容旧数据:如果没有 reminderUnit 但有 reminderHours,则推断为 hour + if (!subscription.reminderUnit && subscription.reminderHours !== undefined) { + unit = 'hour'; + } + + let value; + if (unit === 'minute' || unit === 'hour') { + if (subscription && subscription.reminderValue !== undefined && subscription.reminderValue !== null && !isNaN(Number(subscription.reminderValue))) { + value = Number(subscription.reminderValue); + } else if (subscription && subscription.reminderHours !== undefined && subscription.reminderHours !== null && !isNaN(Number(subscription.reminderHours))) { + value = Number(subscription.reminderHours); + } else { + value = 0; + } + } else { + if (subscription && subscription.reminderValue !== undefined && subscription.reminderValue !== null && !isNaN(Number(subscription.reminderValue))) { + value = Number(subscription.reminderValue); + } else if (!isNaN(defaultDays)) { + value = Number(defaultDays); + } else { + value = 7; + } + } + + if (value < 0 || isNaN(value)) { + value = 0; + } + let subscriptionName = subscription.name + return { unit, value, subscriptionName }; +} + +function shouldTriggerReminder(reminder, daysDiff, hoursDiff, minutesDiff) { + console.log('shouldTriggerReminder', reminder, daysDiff, hoursDiff.toFixed(2), minutesDiff.toFixed(2)) + if (!reminder) { + return false; + } + // Cloudflare Cron 容错窗口:允许 1 分钟的延迟 + const CRON_TOLERANCE_MINUTES = 1; + if (reminder.unit === 'minute') { + if (reminder.value === 0) { + // 到期时提醒:允许在到期前1分钟到到期后x分钟内触发 + return minutesDiff >= -CRON_TOLERANCE_MINUTES && minutesDiff <= 0; + } + // 提前X分钟提醒:允许在目标时间前后x分钟内触发 + return minutesDiff >= -CRON_TOLERANCE_MINUTES && minutesDiff <= reminder.value; + } + if (reminder.unit === 'hour') { + if (reminder.value === 0) { + // 到期时提醒:允许在到期前1小时到到期后x分钟内触发 + return minutesDiff >= -CRON_TOLERANCE_MINUTES && hoursDiff <= 0; + } + // 提前X小时提醒:允许在目标时间后x分钟内触发 + return minutesDiff >= -CRON_TOLERANCE_MINUTES && hoursDiff <= reminder.value; + } + if (reminder.value === 0) { + // 到期当天提醒:允许在到期后x分钟内触发 + return daysDiff === 0 || (daysDiff === -1 && minutesDiff >= -CRON_TOLERANCE_MINUTES && minutesDiff < 0); + } + return daysDiff >= 0 && daysDiff <= reminder.value; +} + +const NOTIFIER_KEYS = ['telegram', 'notifyx', 'webhook', 'wechatbot', 'email_smtp', 'email_resend', 'email_mailgun', 'bark']; + +function normalizeNotificationHours(raw) { + const values = Array.isArray(raw) + ? raw + : typeof raw === 'string' + ? raw.split(/[,,\s]+/) + : []; + + return values + .map(value => String(value).trim()) + .filter(value => value.length > 0) + .map(value => { + const upperValue = value.toUpperCase(); + if (upperValue === '*' || upperValue === 'ALL') { + return '*'; + } + // 支持 HH:MM 格式(如 08:30, 12:15) + if (value.includes(':')) { + const parts = value.split(':'); + if (parts.length === 2) { + const hour = parseInt(parts[0]); + const minute = parseInt(parts[1]); + if (!isNaN(hour) && !isNaN(minute) && hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) { + return String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0'); + } + } + } + // 仅小时格式(如 08, 12, 20) + const numeric = Number(upperValue); + if (!isNaN(numeric)) { + return String(Math.max(0, Math.min(23, Math.floor(numeric)))).padStart(2, '0'); + } + return upperValue; + }); +} + +function normalizeNotifierList(raw) { + const values = Array.isArray(raw) + ? raw + : typeof raw === 'string' + ? raw.split(/[,,\s]+/) + : []; + + return values + .map(value => { + const normalized = String(value).trim().toLowerCase(); + if (normalized === 'email' || normalized === 'resend' || normalized === 'email_resend') { + return 'email_resend'; + } + if (normalized === 'mailgun' || normalized === 'email_mailgun') { + return 'email_mailgun'; + } + if (normalized === 'smtp' || normalized === 'email_smtp') { + return 'email_smtp'; + } + return normalized; + }) + .filter(value => value.length > 0) + .filter(value => NOTIFIER_KEYS.includes(value)); +} + +function normalizeEmailRecipients(raw) { + const values = Array.isArray(raw) + ? raw + : typeof raw === 'string' + ? raw.split(/[,,\s]+/) + : []; + + return values + .map(value => String(value).trim()) + .filter(value => value.length > 0); +} + +function resolveEmailProvider(provider) { + const normalized = String(provider || '').toLowerCase(); + if (normalized === 'smtp') { + return 'smtp'; + } + if (normalized === 'mailgun' || normalized === 'email_mailgun') { + return 'mailgun'; + } + return 'resend'; +} + +function resolveEmailConfigForProvider(provider, config) { + const legacyFrom = config?.EMAIL_FROM || ''; + const legacyFromName = config?.EMAIL_FROM_NAME || ''; + const legacyTo = config?.EMAIL_TO || ''; + if (provider === 'smtp') { + return { + apiKey: '', + from: config?.SMTP_FROM || legacyFrom || config?.SMTP_USER || '', + fromName: config?.SMTP_FROM_NAME || legacyFromName || '', + to: config?.SMTP_TO || legacyTo + }; + } + if (provider === 'mailgun') { + return { + apiKey: config?.MAILGUN_API_KEY || '', + from: config?.MAILGUN_FROM || legacyFrom || '', + fromName: config?.MAILGUN_FROM_NAME || legacyFromName || '', + to: config?.MAILGUN_TO || legacyTo + }; + } + return { + apiKey: config?.RESEND_API_KEY || '', + from: config?.RESEND_FROM || legacyFrom || '', + fromName: config?.RESEND_FROM_NAME || legacyFromName || '', + to: config?.RESEND_TO || legacyTo + }; +} + +function formatEmailFrom(address, name) { + const trimmedAddress = String(address || '').trim(); + const trimmedName = String(name || '').trim(); + if (!trimmedAddress) { + return ''; + } + return trimmedName ? `${trimmedName} <${trimmedAddress}>` : trimmedAddress; +} + +function extractEmailAddress(value) { + const raw = String(value || '').trim(); + const match = raw.match(/<([^>]+)>/); + return (match ? match[1] : raw).trim(); +} + +function extractEmailDomain(value) { + const address = extractEmailAddress(value); + const atIndex = address.lastIndexOf('@'); + return atIndex !== -1 ? address.slice(atIndex + 1).trim() : ''; +} + +function resolveSubscriptionNotificationHours(subscription, config) { + if (subscription && Object.prototype.hasOwnProperty.call(subscription, 'notificationHours')) { + return normalizeNotificationHours(subscription.notificationHours); + } + return normalizeNotificationHours(config?.NOTIFICATION_HOURS); +} + +function resolveSubscriptionNotifiers(subscription, config) { + if (subscription && Object.prototype.hasOwnProperty.call(subscription, 'enabledNotifiers')) { + return normalizeNotifierList(subscription.enabledNotifiers); + } + const fallback = normalizeNotifierList(config?.ENABLED_NOTIFIERS); + return fallback.length ? fallback : NOTIFIER_KEYS.slice(); +} + +function resolveSubscriptionEmailRecipients(subscription) { + return normalizeEmailRecipients(subscription?.emailTo); +} + +function shouldNotifyAtCurrentHour(notificationHours, currentHour, currentMinute) { + const normalized = normalizeNotificationHours(notificationHours); + if (normalized.length === 0 || normalized.includes('*')) { + return true; + } + + // 格式化当前时间为 HH:MM + const currentTimeStr = String(currentHour).padStart(2, '0') + ':' + String(currentMinute).padStart(2, '0'); + const currentHourStr = String(currentHour).padStart(2, '0'); + + // 检查是否匹配 + for (const time of normalized) { + if (time.includes(':')) { + // 对于 HH:MM 格式,允许在同一分钟内触发(考虑到定时任务可能不是精确在该分钟的0秒执行) + // 比如设置了 13:48,那么 13:48:00 到 13:48:59 都应该允许 + const [targetHour, targetMinute] = time.split(':').map(v => parseInt(v)); + const currentHourInt = parseInt(currentHour); + const currentMinuteInt = parseInt(currentMinute); + + if (targetHour === currentHourInt && targetMinute === currentMinuteInt) { + return true; + } + } else { + // 仅匹配小时(整个小时内都允许) + if (time === currentHourStr) { + return true; + } + } + } + + return false; +} + +function formatNotificationContent(subscriptions, config) { + const showLunar = config.SHOW_LUNAR === true; + const timezone = config?.TIMEZONE || 'UTC'; + let content = ''; + + for (const sub of subscriptions) { + const typeText = sub.customType || '其他'; + const periodText = (sub.periodValue && sub.periodUnit) ? `(周期: ${sub.periodValue} ${{ minute: '分钟', hour: '小时', day: '天', month: '月', year: '年' }[sub.periodUnit] || sub.periodUnit})` : ''; + const categoryText = sub.category ? sub.category : '未分类'; + const reminderSetting = resolveReminderSetting(sub); + + // 格式化到期日期(使用所选时区) + const expiryDateObj = new Date(sub.expiryDate); + const formattedExpiryDate = formatTimeInTimezone(expiryDateObj, timezone, 'datetime'); + + // 农历日期 + let lunarExpiryText = ''; + if (showLunar) { + const lunarExpiry = lunarCalendar.solar2lunar(expiryDateObj.getFullYear(), expiryDateObj.getMonth() + 1, expiryDateObj.getDate()); + lunarExpiryText = lunarExpiry ? ` +农历日期: ${lunarExpiry.fullStr}` : ''; + } + + // 状态和到期时间 + let statusText = ''; + let statusEmoji = ''; + + // 根据订阅的周期单位选择合适的显示方式 + if (sub.periodUnit === 'minute') { + // 分钟级订阅:显示分钟 + const minutesRemaining = sub.minutesRemaining !== undefined ? sub.minutesRemaining : (sub.hoursRemaining ? sub.hoursRemaining * 60 : sub.daysRemaining * 24 * 60); + if (Math.abs(minutesRemaining) < 1) { + statusEmoji = '⚠️'; + statusText = '即将到期!'; + } else if (minutesRemaining < 0) { + statusEmoji = '🚨'; + statusText = `已过期 ${Math.abs(Math.round(minutesRemaining))} 分钟`; + } else { + statusEmoji = '📅'; + statusText = `将在 ${Math.round(minutesRemaining)} 分钟后到期`; + } + } else if (sub.periodUnit === 'hour') { + // 小时级订阅:显示小时 + const hoursRemaining = sub.hoursRemaining !== undefined ? sub.hoursRemaining : sub.daysRemaining * 24; + if (Math.abs(hoursRemaining) < 1) { + statusEmoji = '⚠️'; + statusText = '即将到期!'; + } else if (hoursRemaining < 0) { + statusEmoji = '🚨'; + statusText = `已过期 ${Math.abs(Math.round(hoursRemaining))} 小时`; + } else { + statusEmoji = '📅'; + statusText = `将在 ${Math.round(hoursRemaining)} 小时后到期`; + } + } else { + // 天级订阅:显示天数 + if (sub.daysRemaining === 0) { + statusEmoji = '⚠️'; + statusText = '今天到期!'; + } else if (sub.daysRemaining < 0) { + statusEmoji = '🚨'; + statusText = `已过期 ${Math.abs(sub.daysRemaining)} 天`; + } else { + statusEmoji = '📅'; + statusText = `将在 ${sub.daysRemaining} 天后到期`; + } + } + + const reminderSuffix = reminderSetting.value === 0 + ? '(仅到期时提醒)' + : reminderSetting.unit === 'minute' + ? '(分钟级提醒)' + : reminderSetting.unit === 'hour' + ? '(小时级提醒)' + : ''; + + const reminderText = reminderSetting.unit === 'minute' + ? `提醒策略: 提前 ${reminderSetting.value} 分钟${reminderSuffix}` + : reminderSetting.unit === 'hour' + ? `提醒策略: 提前 ${reminderSetting.value} 小时${reminderSuffix}` + : `提醒策略: 提前 ${reminderSetting.value} 天${reminderSuffix}`; + + // 获取日历类型和自动续期状态 + const calendarType = sub.useLunar ? '农历' : '公历'; + const autoRenewText = sub.autoRenew ? '是' : '否'; + const amountText = sub.amount ? `\n金额: ¥${sub.amount.toFixed(2)}/周期` : ''; + + // 构建格式化的通知内容 + const subscriptionContent = `${statusEmoji} **${sub.name}** +类型: ${typeText} ${periodText} +分类: ${categoryText}${amountText} +日历类型: ${calendarType} +到期日期: ${formattedExpiryDate}${lunarExpiryText} +自动续期: ${autoRenewText} +${reminderText} +到期状态: ${statusText}`; + + // 添加备注 + let finalContent = sub.notes ? + subscriptionContent + `\n备注: ${sub.notes}` : + subscriptionContent; + + content += finalContent + '\n\n'; + } + + // 添加发送时间和时区信息 + const currentTime = formatTimeInTimezone(new Date(), timezone, 'datetime'); + content += `发送时间: ${currentTime}\n当前时区: ${formatTimezoneDisplay(timezone)}`; + + return content; +} + +async function sendNotificationToAllChannels(title, commonContent, config, logPrefix = '[定时任务]', options = {}) { + const metadata = options.metadata || {}; + const requestedNotifiers = normalizeNotifierList(options.notifiers); + const globalNotifiers = normalizeNotifierList(config.ENABLED_NOTIFIERS); + const baseNotifiers = requestedNotifiers.length ? requestedNotifiers : globalNotifiers; + const effectiveNotifiers = globalNotifiers.length + ? baseNotifiers.filter(item => globalNotifiers.includes(item)) + : baseNotifiers; + + if (!effectiveNotifiers || effectiveNotifiers.length === 0) { + console.log(`${logPrefix} 未启用任何通知渠道。`); + return; + } + + if (effectiveNotifiers.includes('notifyx')) { + const notifyxContent = `## ${title}\n\n${commonContent}`; + const success = await sendNotifyXNotification(title, notifyxContent, `订阅提醒`, config); + console.log(`${logPrefix} 发送NotifyX通知 ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('telegram')) { + const telegramContent = `*${title}*\n\n${commonContent}`; + const success = await sendTelegramNotification(telegramContent, config); + console.log(`${logPrefix} 发送Telegram通知 ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('webhook')) { + const webhookContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendWebhookNotification(title, webhookContent, config, metadata); + console.log(`${logPrefix} 发送Webhook通知 ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('wechatbot')) { + const wechatbotContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendWechatBotNotification(title, wechatbotContent, config); + console.log(`${logPrefix} 发送企业微信机器人通知 ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('email_resend')) { + const emailContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendEmailNotification(title, emailContent, config, { + provider: 'resend', + emailTo: options.emailTo + }); + console.log(`${logPrefix} 发送邮件通知(Resend) ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('email_mailgun')) { + const emailContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendEmailNotification(title, emailContent, config, { + provider: 'mailgun', + emailTo: options.emailTo + }); + console.log(`${logPrefix} 发送邮件通知(Mailgun) ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('email_smtp')) { + const emailContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendEmailNotification(title, emailContent, config, { + provider: 'smtp', + emailTo: options.emailTo + }); + console.log(`${logPrefix} 发送邮件通知(SMTP) ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('bark')) { + const barkContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendBarkNotification(title, barkContent, config); + console.log(`${logPrefix} 发送Bark通知 ${success ? '成功' : '失败'}`); + } +} + +async function sendTelegramNotification(message, config) { + try { + if (!config.TG_BOT_TOKEN || !config.TG_CHAT_ID) { + console.error('[Telegram] 通知未配置,缺少Bot Token或Chat ID'); + return false; + } + + console.log('[Telegram] 开始发送通知到 Chat ID: ' + config.TG_CHAT_ID); + + const url = 'https://api.telegram.org/bot' + config.TG_BOT_TOKEN + '/sendMessage'; + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + chat_id: config.TG_CHAT_ID, + text: message, + parse_mode: 'Markdown' + }) + }); + + const result = await response.json(); + console.log('[Telegram] 发送结果:', result); + return result.ok; + } catch (error) { + console.error('[Telegram] 发送通知失败:', error); + return false; + } +} + +async function sendNotifyXNotification(title, content, description, config) { + try { + if (!config.NOTIFYX_API_KEY) { + console.error('[NotifyX] 通知未配置,缺少API Key'); + return false; + } + + console.log('[NotifyX] 开始发送通知: ' + title); + + const url = 'https://www.notifyx.cn/api/v1/send/' + config.NOTIFYX_API_KEY; + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: title, + content: content, + description: description || '' + }) + }); + + const result = await response.json(); + console.log('[NotifyX] 发送结果:', result); + return result.status === 'queued'; + } catch (error) { + console.error('[NotifyX] 发送通知失败:', error); + return false; + } +} + +async function sendBarkNotification(title, content, config) { + try { + if (!config.BARK_DEVICE_KEY) { + console.error('[Bark] 通知未配置,缺少设备Key'); + return false; + } + + console.log('[Bark] 开始发送通知到设备: ' + config.BARK_DEVICE_KEY); + + const serverUrl = config.BARK_SERVER || 'https://api.day.app'; + const url = serverUrl + '/push'; + const payload = { + title: title, + body: content, + device_key: config.BARK_DEVICE_KEY + }; + + // 如果配置了保存推送,则添加isArchive参数 + if (config.BARK_IS_ARCHIVE === 'true') { + payload.isArchive = 1; + } + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + console.log('[Bark] 发送结果:', result); + + // Bark API返回code为200表示成功 + return result.code === 200; + } catch (error) { + console.error('[Bark] 发送通知失败:', error); + return false; + } +} + +async function sendEmailNotification(title, content, config, options = {}) { + try { + const provider = resolveEmailProvider(options.provider); + const recipients = normalizeEmailRecipients(options.emailTo); + const providerConfig = resolveEmailConfigForProvider(provider, config); + const targetRecipients = recipients.length + ? recipients + : normalizeEmailRecipients(providerConfig.to); + const fromEmail = formatEmailFrom(providerConfig.from, providerConfig.fromName); + + // 生成HTML邮件内容 + const htmlContent = ` + + + + + + ${title} + + + +
+
+

📅 ${title}

+
+
+
+ ${content.replace(/\n/g, '
')} +
+

此邮件由订阅管理系统自动发送,请及时处理相关订阅事务。

+
+ +
+ +`; + + if (provider === 'smtp') { + console.log('[Email Notification] Using SMTP provider'); + if (targetRecipients.length === 0) { + console.error('[Email Notification] Missing SMTP recipients'); + return false; + } + return await sendSmtpEmailNotification(title, content, htmlContent, config, targetRecipients); + } + + if (provider === 'mailgun') { + if (!providerConfig.apiKey || !providerConfig.from || targetRecipients.length === 0) { + console.error('[Email Notification] Missing Mailgun config or recipients'); + return false; + } + const domain = extractEmailDomain(providerConfig.from); + if (!domain) { + console.error('[Email Notification] Unable to resolve Mailgun domain from from address'); + return false; + } + console.log('[Email Notification] Sending via Mailgun to: ' + targetRecipients.join(', ')); + const auth = btoa('api:' + providerConfig.apiKey); + const response = await fetch(`https://api.mailgun.net/v3/${domain}/messages`, { + method: 'POST', + headers: { + 'Authorization': `Basic ${auth}` + }, + body: new URLSearchParams({ + from: fromEmail, + to: targetRecipients.join(', '), + subject: title, + html: htmlContent, + text: content + }) + }); + + let result; + try { + result = await response.json(); + } catch (parseError) { + result = await response.text(); + } + console.log('[Email Notification] Mailgun response', response.status, result); + return response.ok; + } + + if (!providerConfig.apiKey || !providerConfig.from || targetRecipients.length === 0) { + console.error('[Email Notification] Missing Resend config or recipients'); + return false; + } + + console.log('[Email Notification] Sending via Resend to: ' + targetRecipients.join(', ')); + + const response = await fetch('https://api.resend.com/emails', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${providerConfig.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + from: fromEmail, + to: targetRecipients, + subject: title, + html: htmlContent, + text: content + }) + }); + + const result = await response.json(); + console.log('[Email Notification] Resend response', response.status, result); + + if (response.ok && result.id) { + console.log('[Email Notification] Resend message accepted, ID:', result.id); + return true; + } + console.error('[Email Notification] Resend send failed', result); + return false; + + } catch (error) { + console.error('[邮件通知] 发送邮件失败:', error); + return false; + } +} + +async function sendSmtpEmailNotification(title, content, htmlContent, config, recipients) { + const result = await sendSmtpEmailNotificationDetailed(title, content, htmlContent, config, recipients); + return result.success; +} + +async function sendSmtpEmailNotificationDetailed(title, content, htmlContent, config, recipients) { + try { + const smtpPort = Number(config.SMTP_PORT); + const smtpFrom = config.SMTP_FROM || config.EMAIL_FROM || config.SMTP_USER || ''; + const smtpFromName = config.SMTP_FROM_NAME || config.EMAIL_FROM_NAME || ''; + if (!config.SMTP_HOST || !smtpPort || !config.SMTP_USER || !config.SMTP_PASS || !smtpFrom || !recipients.length) { + console.error('[SMTP邮件通知] 通知未配置,缺少必要参数'); + return { success: false, message: 'Missing SMTP config or recipients' }; + } + + const fromEmail = smtpFromName ? + `${smtpFromName} <${smtpFrom}>` : + smtpFrom; + + console.log('[SMTP邮件通知] 开始发送邮件到: ' + recipients.join(', ')); + + const response = await fetch('https://smtpjs.com/v3/smtpjs.aspx', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + Host: config.SMTP_HOST, + Port: smtpPort, + Username: config.SMTP_USER, + Password: config.SMTP_PASS, + To: recipients.join(','), + From: fromEmail, + Subject: title, + Body: htmlContent, + Secure: smtpPort === 465 + }) + }); + + const resultText = await response.text(); + console.log('[SMTP邮件通知] 发送结果:', response.status, resultText); + + if (response.ok && resultText && resultText.toLowerCase().includes('ok')) { + return { success: true, message: 'OK' }; + } + console.error('[SMTP邮件通知] 发送失败:', resultText); + return { + success: false, + message: 'SMTPJS response: ' + (resultText || 'empty response'), + status: response.status + }; + } catch (error) { + console.error('[SMTP邮件通知] 发送邮件失败:', error); + return { success: false, message: error.message || 'SMTP send error' }; + } +} + +async function sendNotification(title, content, description, config) { + if (config.NOTIFICATION_TYPE === 'notifyx') { + return await sendNotifyXNotification(title, content, description, config); + } else { + return await sendTelegramNotification(content, config); + } +} + +// 4. 修改定时任务 checkExpiringSubscriptions,支持农历周期自动续订和农历提醒 +async function checkExpiringSubscriptions(env) { + try { + const config = await getConfig(env); + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + + // 统一计算当天的零点时间,用于比较天数差异 + const currentMidnight = getTimezoneMidnightTimestamp(currentTime, timezone); + + console.log(`[定时任务] 开始检查 - 当前时间: ${currentTime.toISOString()} (${timezone})`); + + // --- 检查当前小时和分钟是否允许发送通知 --- + const timeFormatter = new Intl.DateTimeFormat('en-US', { + timeZone: timezone, + hour12: false, + hour: '2-digit', + minute: '2-digit' + }); + const timeParts = timeFormatter.formatToParts(currentTime); + const currentHour = timeParts.find(p => p.type === 'hour')?.value || '00'; + const currentMinute = timeParts.find(p => p.type === 'minute')?.value || '00'; + + const subscriptions = await getAllSubscriptions(env); + const expiringSubscriptions = []; + const updatedSubscriptions = []; + let hasUpdates = false; + + for (const subscription of subscriptions) { + // 1. 跳过未启用的订阅 + if (subscription.isActive === false) { + continue; + } + + const reminderSetting = resolveReminderSetting(subscription); + const subscriptionNotificationHours = resolveSubscriptionNotificationHours(subscription, config); + const shouldNotifyThisHour = shouldNotifyAtCurrentHour(subscriptionNotificationHours, currentHour, currentMinute); + + // 计算当前剩余时间(基础计算) + let expiryDate = new Date(subscription.expiryDate); + + // 为了准确计算 daysDiff,需要根据农历或公历获取"逻辑上的午夜时间" + let expiryMidnight; + if (subscription.useLunar) { + const lunar = lunarCalendar.solar2lunar(expiryDate.getFullYear(), expiryDate.getMonth() + 1, expiryDate.getDate()); + if (lunar) { + const solar = lunarBiz.lunar2solar(lunar); + const lunarDate = new Date(solar.year, solar.month - 1, solar.day); + expiryMidnight = getTimezoneMidnightTimestamp(lunarDate, timezone); + } else { + expiryMidnight = getTimezoneMidnightTimestamp(expiryDate, timezone); + } + } else { + expiryMidnight = getTimezoneMidnightTimestamp(expiryDate, timezone); + } + + let daysDiff = Math.round((expiryMidnight - currentMidnight) / MS_PER_DAY); + // 直接计算时间差(expiryDate 和 currentTime 都是 UTC 时间戳) + let diffMs = expiryDate.getTime() - currentTime.getTime(); + let diffHours = diffMs / MS_PER_HOUR; + let diffMinutes = diffMs / (1000 * 60); + + // ========================================== + // 核心逻辑:自动续费处理 + // ========================================== + // 修复:对于分钟级和小时级订阅,使用精确的时间差判断是否过期 + let isExpiredForRenewal = false; + if (subscription.periodUnit === 'minute' || subscription.periodUnit === 'hour') { + isExpiredForRenewal = diffMs < 0; // 使用精确的毫秒差 + } else { + isExpiredForRenewal = daysDiff < 0; // 使用天数差 + } + + if (isExpiredForRenewal && subscription.periodValue && subscription.periodUnit && subscription.autoRenew !== false) { + console.log(`[定时任务] 订阅 "${subscription.name}" 已过期,准备自动续费...`); + + const mode = subscription.subscriptionMode || 'cycle'; // cycle | reset + + // 1. 确定计算基准点 (Base Point) + // newStartDate 将作为新周期的"开始日期"保存到数据库,解决前端编辑时日期错乱问题 + let newStartDate; + + if (mode === 'reset') { + // 注意:为了整洁,通常从当天的 00:00 或当前时间开始,这里取 currentTime 保持精确 + newStartDate = new Date(currentTime); + } else { + // Cycle 模式:无缝接续,从"旧的到期日"开始 + newStartDate = new Date(subscription.expiryDate); + } + + // 2. 计算新的到期日 (循环补齐直到未来) + let newExpiryDate = new Date(newStartDate); // 初始化 + let periodsAdded = 0; + + // 定义增加一个周期的函数 (同时处理 newStartDate 和 newExpiryDate 的推进) + const addOnePeriod = (baseDate) => { + let targetDate; + if (subscription.useLunar) { + const solarBase = { year: baseDate.getFullYear(), month: baseDate.getMonth() + 1, day: baseDate.getDate() }; + let lunarBase = lunarCalendar.solar2lunar(solarBase.year, solarBase.month, solarBase.day); + // 农历加周期 + let nextLunar = lunarBiz.addLunarPeriod(lunarBase, subscription.periodValue, subscription.periodUnit); + const solarNext = lunarBiz.lunar2solar(nextLunar); + targetDate = new Date(solarNext.year, solarNext.month - 1, solarNext.day); + } else { + targetDate = new Date(baseDate); + if (subscription.periodUnit === 'minute') targetDate.setMinutes(targetDate.getMinutes() + subscription.periodValue); + else if (subscription.periodUnit === 'hour') targetDate.setHours(targetDate.getHours() + subscription.periodValue); + else if (subscription.periodUnit === 'day') targetDate.setDate(targetDate.getDate() + subscription.periodValue); + else if (subscription.periodUnit === 'month') targetDate.setMonth(targetDate.getMonth() + subscription.periodValue); + else if (subscription.periodUnit === 'year') targetDate.setFullYear(targetDate.getFullYear() + subscription.periodValue); + } + return targetDate; + }; + // Reset模式下 newStartDate 是今天,加一次肯定在未来,循环只会执行一次 + do { + // 在推进到期日之前,现有的 newExpiryDate 就变成了这一轮的"开始日" + // (仅在非第一次循环时有效,用于 Cycle 模式推进 start 日期) + if (periodsAdded > 0) { + newStartDate = new Date(newExpiryDate); + } + + // 计算下一个到期日 + newExpiryDate = addOnePeriod(newStartDate); + periodsAdded++; + + // 获取新到期日的午夜时间用于判断是否仍过期 + const newExpiryMidnight = getTimezoneMidnightTimestamp(newExpiryDate, timezone); + daysDiff = Math.round((newExpiryMidnight - currentMidnight) / MS_PER_DAY); + + } while (daysDiff < 0); // 只要还过期,就继续加 + + console.log(`[定时任务] 续费完成. 新开始日: ${newStartDate.toISOString()}, 新到期日: ${newExpiryDate.toISOString()}`); + // 3. 生成支付记录 + const paymentRecord = { + id: Date.now().toString() + '_' + Math.random().toString(36).substr(2, 9), + date: currentTime.toISOString(), // 实际扣款时间是现在 + amount: subscription.amount || 0, + type: 'auto', + note: `自动续订 (${mode === 'reset' ? '重置模式' : '接续模式'}${periodsAdded > 1 ? ', 补齐' + periodsAdded + '周期' : ''})`, + periodStart: newStartDate.toISOString(), // 记录准确的计费周期开始 + periodEnd: newExpiryDate.toISOString() + }; + + const paymentHistory = subscription.paymentHistory || []; + paymentHistory.push(paymentRecord); + // 4. 更新订阅对象 + const updatedSubscription = { + ...subscription, + startDate: newStartDate.toISOString(), + expiryDate: newExpiryDate.toISOString(), + lastPaymentDate: currentTime.toISOString(), + paymentHistory + }; + + updatedSubscriptions.push(updatedSubscription); + hasUpdates = true; + + // 5. 检查续费后是否需要立即提醒 (例如续费后只剩1天) + let diffMs1 = newExpiryDate.getTime() - currentTime.getTime(); + let diffHours1 = diffMs1 / MS_PER_HOUR; + let diffMinutes1 = diffMs1 / (1000 * 60); + const shouldRemindAfterRenewal = shouldTriggerReminder(reminderSetting, daysDiff, diffHours1, diffMinutes1); + + if (shouldRemindAfterRenewal && shouldNotifyThisHour) { + expiringSubscriptions.push({ + ...updatedSubscription, + daysRemaining: daysDiff, + hoursRemaining: Math.round(diffHours1) + }); + } else if (shouldRemindAfterRenewal && !shouldNotifyThisHour) { + console.log(`[Scheduler] Hour ${currentHour} not in reminder hours for "${subscription.name}", skipping notification`); + } + + // continue; // 处理下一个订阅 + } + + // ========================================== + // 普通提醒逻辑 (未过期,或过期但不自动续费) + // ========================================== + const shouldRemind = shouldTriggerReminder(reminderSetting, daysDiff, diffHours, diffMinutes); + + // 格式化剩余时间显示 + let remainingTimeStr; + if (daysDiff >= 1) { + remainingTimeStr = `${daysDiff}天`; + } else if (diffHours >= 1) { + remainingTimeStr = `${diffHours.toFixed(2)}小时`; + } else { + remainingTimeStr = `${diffMinutes.toFixed(2)}分钟`; + } + + console.log(`[定时任务] ${subscription.name} | 当前时间: ${currentHour}:${currentMinute} | 通知时间点: ${JSON.stringify(subscriptionNotificationHours)} | 时间匹配: ${shouldNotifyThisHour} | 提醒判断: ${shouldRemind} | 剩余: ${remainingTimeStr}`); + + // 修复:对于分钟级和小时级订阅,使用精确的时间差判断是否过期 + let isExpiredForNotification = false; + if (subscription.periodUnit === 'minute' || subscription.periodUnit === 'hour') { + isExpiredForNotification = diffMs < 0; + } else { + isExpiredForNotification = daysDiff < 0; + } + + if (isExpiredForNotification && subscription.autoRenew === false) { + // 已过期且不自动续费 -> 发送过期通知 + if (shouldNotifyThisHour) { + expiringSubscriptions.push({ + ...subscription, + daysRemaining: daysDiff, + hoursRemaining: Math.round(diffHours), + minutesRemaining: Math.round(diffMinutes) + }); + } else { + console.log(`[Scheduler] Hour ${currentHour} not in reminder hours for "${subscription.name}", skipping notification`); + } + } else if (shouldRemind) { + // 正常到期提醒 + if (shouldNotifyThisHour) { + expiringSubscriptions.push({ + ...subscription, + daysRemaining: daysDiff, + hoursRemaining: Math.round(diffHours), + minutesRemaining: Math.round(diffMinutes) + }); + } else { + console.log(`[Scheduler] Hour ${currentHour} not in reminder hours for "${subscription.name}", skipping notification`); + } + } + } + + // --- 保存更改 --- + if (hasUpdates) { + const mergedSubscriptions = subscriptions.map(sub => { + const updated = updatedSubscriptions.find(u => u.id === sub.id); + return updated || sub; + }); + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(mergedSubscriptions)); + console.log(`[定时任务] 已更新 ${updatedSubscriptions.length} 个自动续费订阅`); + } + + // --- 发送通知 --- + if (expiringSubscriptions.length > 0) { + console.log(`[Scheduler] Sending ${expiringSubscriptions.length} reminder notification(s)`); + // Sort by due time + expiringSubscriptions.sort((a, b) => a.daysRemaining - b.daysRemaining); + + for (const subscription of expiringSubscriptions) { + const commonContent = formatNotificationContent([subscription], config); + const metadataTags = extractTagsFromSubscriptions([subscription]); + await sendNotificationToAllChannels(`Subscription reminder: ${subscription.name}`, commonContent, config, '[Scheduler]', { + metadata: { tags: metadataTags }, + notifiers: resolveSubscriptionNotifiers(subscription, config), + emailTo: resolveSubscriptionEmailRecipients(subscription) + }); + } + } + } catch (error) { + console.error('[定时任务] 执行失败:', error); + } +} + +function getCookieValue(cookieString, key) { + if (!cookieString) return null; + + const match = cookieString.match(new RegExp('(^| )' + key + '=([^;]+)')); + return match ? match[2] : null; +} + +async function handleRequest(request, env, ctx) { + return new Response(loginPage, { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }); +} + +const CryptoJS = { + HmacSHA256: function (message, key) { + const keyData = new TextEncoder().encode(key); + const messageData = new TextEncoder().encode(message); + + return Promise.resolve().then(() => { + return crypto.subtle.importKey( + "raw", + keyData, + { name: "HMAC", hash: { name: "SHA-256" } }, + false, + ["sign"] + ); + }).then(cryptoKey => { + return crypto.subtle.sign( + "HMAC", + cryptoKey, + messageData + ); + }).then(buffer => { + const hashArray = Array.from(new Uint8Array(buffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + }); + } +}; + +function getCurrentTime(config) { + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + const formatter = new Intl.DateTimeFormat('zh-CN', { + timeZone: timezone, + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit' + }); + return { + date: currentTime, + localString: formatter.format(currentTime), + isoString: currentTime.toISOString() + }; +} + +export default { + async fetch(request, env, ctx) { + const url = new URL(request.url); + + // 添加调试页面 + if (url.pathname === '/debug') { + try { + const config = await getConfig(env); + const debugInfo = { + timestamp: new Date().toISOString(), // 使用UTC时间戳 + pathname: url.pathname, + kvBinding: !!env.SUBSCRIPTIONS_KV, + configExists: !!config, + adminUsername: config.ADMIN_USERNAME, + hasJwtSecret: !!config.JWT_SECRET, + jwtSecretLength: config.JWT_SECRET ? config.JWT_SECRET.length : 0 + }; + + return new Response(` + + + + 调试信息 + + + +

系统调试信息

+
+

基本信息

+

时间: ${debugInfo.timestamp}

+

路径: ${debugInfo.pathname}

+

KV绑定: ${debugInfo.kvBinding ? '✓' : '✗'}

+
+ +
+

配置信息

+

配置存在: ${debugInfo.configExists ? '✓' : '✗'}

+

管理员用户名: ${debugInfo.adminUsername}

+

JWT密钥: ${debugInfo.hasJwtSecret ? '✓' : '✗'} (长度: ${debugInfo.jwtSecretLength})

+
+ +
+

解决方案

+

1. 确保KV命名空间已正确绑定为 SUBSCRIPTIONS_KV

+

2. 尝试访问 / 进行登录

+

3. 如果仍有问题,请检查Cloudflare Workers日志

+
+ +`, { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }); + } catch (error) { + return new Response(`调试页面错误: ${error.message}`, { + status: 500, + headers: { 'Content-Type': 'text/plain; charset=utf-8' } + }); + } + } + + if (url.pathname.startsWith('/api')) { + return api.handleRequest(request, env, ctx); + } else if (url.pathname.startsWith('/admin')) { + return admin.handleRequest(request, env, ctx); + } else { + return handleRequest(request, env, ctx); + } + }, + + async scheduled(event, env, ctx) { + const config = await getConfig(env); + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + console.log('[Workers] 定时任务触发 UTC:', new Date().toISOString(), timezone + ':', currentTime.toLocaleString('zh-CN', { timeZone: timezone })); + await checkExpiringSubscriptions(env); + } +}; +// ==================== 仪表盘统计函数 ==================== +// 汇率配置 (以 CNY 为基准,当 API 不可用或缺少特定币种如 TWD 时使用,属于兜底汇率) +// 您可以根据需要修改此处的汇率 +const FALLBACK_RATES = { + 'CNY': 1, + 'USD': 6.98, + 'HKD': 0.90, + 'TWD': 0.22, + 'JPY': 0.044, + 'EUR': 8.16, + 'GBP': 9.40, + 'KRW': 0.0048, + 'TRY': 0.16 +}; +// 获取动态汇率 (核心逻辑:KV缓存 -> API请求 -> 兜底合并) +async function getDynamicRates(env) { + const CACHE_KEY = 'SYSTEM_EXCHANGE_RATES'; + const CACHE_TTL = 86400000; // 24小时 (毫秒) + + try { + const cached = await env.SUBSCRIPTIONS_KV.get(CACHE_KEY, { type: 'json' }); // A. 尝试从 KV 读取缓存 + if (cached && cached.ts && (Date.now() - cached.ts < CACHE_TTL)) { + return cached.rates; // console.log('[汇率] 使用 KV 缓存'); + } + const response = await fetch('https://api.frankfurter.dev/v1/latest?base=CNY'); // B. 缓存失效或不存在,请求 Frankfurter API + if (response.ok) { + const data = await response.json(); + const newRates = { // C. 合并逻辑:以 API 数据覆盖兜底数据 (保留 API 没有的币种,如 TWD) + ...FALLBACK_RATES, + ...data.rates, + 'CNY': 1 + }; + + await env.SUBSCRIPTIONS_KV.put(CACHE_KEY, JSON.stringify({ // D. 写入 KV 缓存 + ts: Date.now(), + rates: newRates + })); + + return newRates; + } else { + console.warn('[汇率] API 请求失败,使用兜底汇率'); + } + } catch (error) { + console.error('[汇率] 获取过程出错:', error); + } + return FALLBACK_RATES; // E. 发生任何错误,返回兜底汇率 +} +// 辅助函数:将金额转换为基准货币 (CNY) +function convertToCNY(amount, currency, rates) { + if (!amount || amount <= 0) return 0; + + const code = currency || 'CNY'; + if (code === 'CNY') return amount; // 如果是基准货币,直接返回 + const rate = rates[code]; // 获取汇率 + if (!rate) return amount; // 如果没有汇率,原样返回(或者你可以选择抛出错误/返回0) + return amount / rate; +} +// 修改函数签名,增加 rates 参数 +function calculateMonthlyExpense(subscriptions, timezone, rates) { + const now = getCurrentTimeInTimezone(timezone); + const parts = getTimezoneDateParts(now, timezone); + const currentYear = parts.year; + const currentMonth = parts.month; + + let amount = 0; + + // 遍历所有订阅的支付历史 + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + const paymentParts = getTimezoneDateParts(paymentDate, timezone); + if (paymentParts.year === currentYear && paymentParts.month === currentMonth) { + amount += convertToCNY(payment.amount, sub.currency, rates); // 传入 rates 参数 + } + }); + }); + // 计算上月数据用于趋势对比 + const lastMonth = currentMonth === 1 ? 12 : currentMonth - 1; + const lastMonthYear = currentMonth === 1 ? currentYear - 1 : currentYear; + let lastMonthAmount = 0; + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + const paymentParts = getTimezoneDateParts(paymentDate, timezone); + if (paymentParts.year === lastMonthYear && paymentParts.month === lastMonth) { + lastMonthAmount += convertToCNY(payment.amount, sub.currency, rates); // 使用 convertToCNY 进行汇率转换 + } + }); + }); + + let trend = 0; + let trendDirection = 'flat'; + if (lastMonthAmount > 0) { + trend = Math.round(((amount - lastMonthAmount) / lastMonthAmount) * 100); + if (trend > 0) trendDirection = 'up'; + else if (trend < 0) trendDirection = 'down'; + } else if (amount > 0) { + trend = 100; // 上月无支出,本月有支出,视为增长 + trendDirection = 'up'; + } + return { amount, trend: Math.abs(trend), trendDirection }; +} + +function calculateYearlyExpense(subscriptions, timezone, rates) { + const now = getCurrentTimeInTimezone(timezone); + const parts = getTimezoneDateParts(now, timezone); + const currentYear = parts.year; + + let amount = 0; + // 遍历所有订阅的支付历史 + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + const paymentParts = getTimezoneDateParts(paymentDate, timezone); + if (paymentParts.year === currentYear) { + amount += convertToCNY(payment.amount, sub.currency, rates); + } + }); + }); + + const monthlyAverage = amount / parts.month; + return { amount, monthlyAverage }; +} + +function getRecentPayments(subscriptions, timezone) { + const now = getCurrentTimeInTimezone(timezone); + const sevenDaysAgo = new Date(now.getTime() - 7 * MS_PER_DAY); + const recentPayments = []; + // 遍历所有订阅的支付历史 + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + if (paymentDate >= sevenDaysAgo && paymentDate <= now) { + recentPayments.push({ + name: sub.name, + amount: payment.amount, + currency: sub.currency || 'CNY', // 传递币种给前端显示 + customType: sub.customType, + paymentDate: payment.date, + note: payment.note + }); + } + }); + }); + return recentPayments.sort((a, b) => new Date(b.paymentDate) - new Date(a.paymentDate)); +} + +function getUpcomingRenewals(subscriptions, timezone) { + const now = getCurrentTimeInTimezone(timezone); + const sevenDaysLater = new Date(now.getTime() + 7 * MS_PER_DAY); + return subscriptions + .filter(sub => { + if (!sub.isActive) return false; + const renewalDate = new Date(sub.expiryDate); + return renewalDate >= now && renewalDate <= sevenDaysLater; + }) + .map(sub => { + const renewalDate = new Date(sub.expiryDate); + // 修复:计算完整的天数差,使用 Math.floor() 向下取整 + // 例如:1.9天显示为"1天后",2.1天显示为"2天后" + const diffMs = renewalDate - now; + const daysUntilRenewal = Math.max(0, Math.floor(diffMs / MS_PER_DAY)); + return { + name: sub.name, + amount: sub.amount || 0, + currency: sub.currency || 'CNY', + customType: sub.customType, + renewalDate: sub.expiryDate, + daysUntilRenewal + }; + }) + .sort((a, b) => a.daysUntilRenewal - b.daysUntilRenewal); +} + +function getExpenseByType(subscriptions, timezone, rates) { + const now = getCurrentTimeInTimezone(timezone); + const parts = getTimezoneDateParts(now, timezone); + const currentYear = parts.year; + const typeMap = {}; + let total = 0; + // 遍历所有订阅的支付历史 + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + const paymentParts = getTimezoneDateParts(paymentDate, timezone); + if (paymentParts.year === currentYear) { + const type = sub.customType || '未分类'; + const amountCNY = convertToCNY(payment.amount, sub.currency, rates); + typeMap[type] = (typeMap[type] || 0) + amountCNY; + total += amountCNY; + } + }); + }); + + return Object.entries(typeMap) + .map(([type, amount]) => ({ + type, + amount, + percentage: total > 0 ? Math.round((amount / total) * 100) : 0 + })) + .sort((a, b) => b.amount - a.amount); +} + +function getExpenseByCategory(subscriptions, timezone, rates) { + const now = getCurrentTimeInTimezone(timezone); + const parts = getTimezoneDateParts(now, timezone); + const currentYear = parts.year; + + const categoryMap = {}; + let total = 0; + // 遍历所有订阅的支付历史 + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + const paymentParts = getTimezoneDateParts(paymentDate, timezone); + if (paymentParts.year === currentYear) { + const categories = sub.category ? sub.category.split(CATEGORY_SEPARATOR_REGEX).filter(c => c.trim()) : ['未分类']; + const amountCNY = convertToCNY(payment.amount, sub.currency, rates); + + categories.forEach(category => { + const cat = category.trim() || '未分类'; + categoryMap[cat] = (categoryMap[cat] || 0) + amountCNY / categories.length; + }); + total += amountCNY; + } + }); + }); + + return Object.entries(categoryMap) + .map(([category, amount]) => ({ + category, + amount, + percentage: total > 0 ? Math.round((amount / total) * 100) : 0 + })) + .sort((a, b) => b.amount - a.amount); +} \ No newline at end of file From 183e0b320012a68170f41fa4422c68a697c74b83 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 10 Feb 2026 17:50:53 +0800 Subject: [PATCH 2/3] update by zzz --- index_zzz.js | 8836 +++++++++++++++++++++++++++++++++++++++++++++++++ wrangler.toml | 4 +- 2 files changed, 8838 insertions(+), 2 deletions(-) create mode 100644 index_zzz.js diff --git a/index_zzz.js b/index_zzz.js new file mode 100644 index 0000000..319aa0d --- /dev/null +++ b/index_zzz.js @@ -0,0 +1,8836 @@ +// 订阅续期通知网站 - 基于CloudFlare Workers (完全优化版) + +// 时区处理工具函数 +// 常量:毫秒转换为小时/天,便于全局复用 +const MS_PER_HOUR = 1000 * 60 * 60; +const MS_PER_DAY = MS_PER_HOUR * 24; + +function getCurrentTimeInTimezone(timezone = 'UTC') { + try { + // Workers 环境下 Date 始终存储 UTC 时间,这里直接返回当前时间对象 + return new Date(); + } catch (error) { + console.error(`时区转换错误: ${error.message}`); + // 如果时区无效,返回UTC时间 + return new Date(); + } +} + +function getTimestampInTimezone(timezone = 'UTC') { + return getCurrentTimeInTimezone(timezone).getTime(); +} + +function convertUTCToTimezone(utcTime, timezone = 'UTC') { + try { + // 同 getCurrentTimeInTimezone,一律返回 Date 供后续统一处理 + return new Date(utcTime); + } catch (error) { + console.error(`时区转换错误: ${error.message}`); + return new Date(utcTime); + } +} + +// 获取指定时区的年/月/日/时/分/秒,便于避免重复的 Intl 解析逻辑 +function getTimezoneDateParts(date, timezone = 'UTC') { + try { + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: timezone, + hour12: false, + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit' + }); + const parts = formatter.formatToParts(date); + const pick = (type) => { + const part = parts.find(item => item.type === type); + return part ? Number(part.value) : 0; + }; + return { + year: pick('year'), + month: pick('month'), + day: pick('day'), + hour: pick('hour'), + minute: pick('minute'), + second: pick('second') + }; + } catch (error) { + console.error(`解析时区(${timezone})失败: ${error.message}`); + return { + year: date.getUTCFullYear(), + month: date.getUTCMonth() + 1, + day: date.getUTCDate(), + hour: date.getUTCHours(), + minute: date.getUTCMinutes(), + second: date.getUTCSeconds() + }; + } +} + +// 计算指定日期在目标时区的午夜时间戳(毫秒),用于统一的“剩余天数”计算 +function getTimezoneMidnightTimestamp(date, timezone = 'UTC') { + const { year, month, day } = getTimezoneDateParts(date, timezone); + return Date.UTC(year, month - 1, day, 0, 0, 0); +} + +function formatTimeInTimezone(time, timezone = 'UTC', format = 'full') { + try { + const date = new Date(time); + + if (format === 'date') { + return date.toLocaleDateString('zh-CN', { + timeZone: timezone, + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + } else if (format === 'datetime') { + return date.toLocaleString('zh-CN', { + timeZone: timezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + } else { + // full format + return date.toLocaleString('zh-CN', { + timeZone: timezone + }); + } + } catch (error) { + console.error(`时间格式化错误: ${error.message}`); + return new Date(time).toISOString(); + } +} + +function getTimezoneOffset(timezone = 'UTC') { + try { + const now = new Date(); + const { year, month, day, hour, minute, second } = getTimezoneDateParts(now, timezone); + const zonedTimestamp = Date.UTC(year, month - 1, day, hour, minute, second); + return Math.round((zonedTimestamp - now.getTime()) / MS_PER_HOUR); + } catch (error) { + console.error(`获取时区偏移量错误: ${error.message}`); + return 0; + } +} + +// 格式化时区显示,包含UTC偏移 +function formatTimezoneDisplay(timezone = 'UTC') { + try { + const offset = getTimezoneOffset(timezone); + const offsetStr = offset >= 0 ? `+${offset}` : `${offset}`; + + // 时区中文名称映射 + const timezoneNames = { + 'UTC': '世界标准时间', + 'Asia/Shanghai': '中国标准时间', + 'Asia/Hong_Kong': '香港时间', + 'Asia/Taipei': '台北时间', + 'Asia/Singapore': '新加坡时间', + 'Asia/Tokyo': '日本时间', + 'Asia/Seoul': '韩国时间', + 'America/New_York': '美国东部时间', + 'America/Los_Angeles': '美国太平洋时间', + 'America/Chicago': '美国中部时间', + 'America/Denver': '美国山地时间', + 'Europe/London': '英国时间', + 'Europe/Paris': '巴黎时间', + 'Europe/Berlin': '柏林时间', + 'Europe/Moscow': '莫斯科时间', + 'Australia/Sydney': '悉尼时间', + 'Australia/Melbourne': '墨尔本时间', + 'Pacific/Auckland': '奥克兰时间' + }; + + const timezoneName = timezoneNames[timezone] || timezone; + return `${timezoneName} (UTC${offsetStr})`; + } catch (error) { + console.error('格式化时区显示失败:', error); + return timezone; + } +} + +// 兼容性函数 - 保持原有接口 +function formatBeijingTime(date = new Date(), format = 'full') { + return formatTimeInTimezone(date, 'Asia/Shanghai', format); +} + +// 时区处理中间件函数 +function extractTimezone(request) { + // 优先级:URL参数 > 请求头 > 默认值 + const url = new URL(request.url); + const timezoneParam = url.searchParams.get('timezone'); + + if (timezoneParam) { + return timezoneParam; + } + + // 从请求头获取时区 + const timezoneHeader = request.headers.get('X-Timezone'); + if (timezoneHeader) { + return timezoneHeader; + } + + // 从Accept-Language头推断时区(简化处理) + const acceptLanguage = request.headers.get('Accept-Language'); + if (acceptLanguage) { + // 简单的时区推断逻辑 + if (acceptLanguage.includes('zh')) { + return 'Asia/Shanghai'; + } else if (acceptLanguage.includes('en-US')) { + return 'America/New_York'; + } else if (acceptLanguage.includes('en-GB')) { + return 'Europe/London'; + } + } + + // 默认返回UTC + return 'UTC'; +} + +function isValidTimezone(timezone) { + try { + // 尝试使用该时区格式化时间 + new Date().toLocaleString('en-US', { timeZone: timezone }); + return true; + } catch (error) { + return false; + } +} + +// 农历转换工具函数 +const lunarCalendar = { + // 农历数据 (1900-2100年) + lunarInfo: [ + 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, // 1900-1909 + 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, // 1910-1919 + 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, // 1920-1929 + 0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, // 1930-1939 + 0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, // 1940-1949 + 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, // 1950-1959 + 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, // 1960-1969 + 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, // 1970-1979 + 0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, // 1980-1989 + 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0, // 1990-1999 + 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, // 2000-2009 + 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, // 2010-2019 + 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, // 2020-2029 + 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, // 2030-2039 + 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, // 2040-2049 + 0x14b63, 0x09370, 0x14a38, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x1a978, 0x16aa0, 0x0a6c0, // 2050-2059 (修正2057: 0x1a978) + 0x0aa60, 0x16d63, 0x0d260, 0x0d950, 0x0d554, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, // 2060-2069 + 0x025d0, 0x092d0, 0x0cab5, 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, // 2070-2079 + 0x15176, 0x052b0, 0x0a930, 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, // 2080-2089 + 0x0d260, 0x0ea65, 0x0d530, 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x1a4bb, 0x0a4d0, 0x0d0b0, // 2090-2099 (修正2099: 0x0d0b0) + 0x0d250 // 2100 + ], + + // 天干地支 + gan: ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'], + zhi: ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'], + + // 农历月份 + months: ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'], + + // 农历日期 + days: ['初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十', + '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十', + '廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十'], + + // 获取农历年天数 + lunarYearDays: function (year) { + let sum = 348; + for (let i = 0x8000; i > 0x8; i >>= 1) { + sum += (this.lunarInfo[year - 1900] & i) ? 1 : 0; + } + return sum + this.leapDays(year); + }, + + // 获取闰月天数 + leapDays: function (year) { + if (this.leapMonth(year)) { + return (this.lunarInfo[year - 1900] & 0x10000) ? 30 : 29; + } + return 0; + }, + + // 获取闰月月份 + leapMonth: function (year) { + return this.lunarInfo[year - 1900] & 0xf; + }, + + // 获取农历月天数 + monthDays: function (year, month) { + return (this.lunarInfo[year - 1900] & (0x10000 >> month)) ? 30 : 29; + }, + + // 公历转农历 + solar2lunar: function (year, month, day) { + if (year < 1900 || year > 2100) return null; + + const baseDate = Date.UTC(1900, 0, 31); + const objDate = Date.UTC(year, month - 1, day); + //let offset = Math.floor((objDate - baseDate) / 86400000); + let offset = Math.round((objDate - baseDate) / 86400000); + + + let temp = 0; + let lunarYear = 1900; + + for (lunarYear = 1900; lunarYear < 2101 && offset > 0; lunarYear++) { + temp = this.lunarYearDays(lunarYear); + offset -= temp; + } + + if (offset < 0) { + offset += temp; + lunarYear--; + } + + let lunarMonth = 1; + let leap = this.leapMonth(lunarYear); + let isLeap = false; + + for (lunarMonth = 1; lunarMonth < 13 && offset > 0; lunarMonth++) { + if (leap > 0 && lunarMonth === (leap + 1) && !isLeap) { + --lunarMonth; + isLeap = true; + temp = this.leapDays(lunarYear); + } else { + temp = this.monthDays(lunarYear, lunarMonth); + } + + if (isLeap && lunarMonth === (leap + 1)) isLeap = false; + offset -= temp; + } + + if (offset === 0 && leap > 0 && lunarMonth === leap + 1) { + if (isLeap) { + isLeap = false; + } else { + isLeap = true; + --lunarMonth; + } + } + + if (offset < 0) { + offset += temp; + --lunarMonth; + } + + const lunarDay = offset + 1; + + // 生成农历字符串 + const ganIndex = (lunarYear - 4) % 10; + const zhiIndex = (lunarYear - 4) % 12; + const yearStr = this.gan[ganIndex] + this.zhi[zhiIndex] + '年'; + const monthStr = (isLeap ? '闰' : '') + this.months[lunarMonth - 1] + '月'; + const dayStr = this.days[lunarDay - 1]; + + return { + year: lunarYear, + month: lunarMonth, + day: lunarDay, + isLeap: isLeap, + yearStr: yearStr, + monthStr: monthStr, + dayStr: dayStr, + fullStr: yearStr + monthStr + dayStr + }; + } +}; + +// 1. 新增 lunarBiz 工具模块,支持农历加周期、农历转公历、农历距离天数 +const lunarBiz = { + // 农历加周期,返回新的农历日期对象 + addLunarPeriod(lunar, periodValue, periodUnit) { + let { year, month, day, isLeap } = lunar; + if (periodUnit === 'year') { + year += periodValue; + const leap = lunarCalendar.leapMonth(year); + if (isLeap && leap === month) { + isLeap = true; + } else { + isLeap = false; + } + } else if (periodUnit === 'month') { + let totalMonths = (year - 1900) * 12 + (month - 1) + periodValue; + year = Math.floor(totalMonths / 12) + 1900; + month = (totalMonths % 12) + 1; + const leap = lunarCalendar.leapMonth(year); + if (isLeap && leap === month) { + isLeap = true; + } else { + isLeap = false; + } + } else if (periodUnit === 'day') { + const solar = lunarBiz.lunar2solar(lunar); + const date = new Date(solar.year, solar.month - 1, solar.day + periodValue); + return lunarCalendar.solar2lunar(date.getFullYear(), date.getMonth() + 1, date.getDate()); + } + let maxDay = isLeap + ? lunarCalendar.leapDays(year) + : lunarCalendar.monthDays(year, month); + let targetDay = Math.min(day, maxDay); + while (targetDay > 0) { + let solar = lunarBiz.lunar2solar({ year, month, day: targetDay, isLeap }); + if (solar) { + return { year, month, day: targetDay, isLeap }; + } + targetDay--; + } + return { year, month, day, isLeap }; + }, + // 农历转公历(遍历法,适用1900-2100年) + lunar2solar(lunar) { + for (let y = lunar.year - 1; y <= lunar.year + 1; y++) { + for (let m = 1; m <= 12; m++) { + for (let d = 1; d <= 31; d++) { + const date = new Date(y, m - 1, d); + if (date.getFullYear() !== y || date.getMonth() + 1 !== m || date.getDate() !== d) continue; + const l = lunarCalendar.solar2lunar(y, m, d); + if ( + l && + l.year === lunar.year && + l.month === lunar.month && + l.day === lunar.day && + l.isLeap === lunar.isLeap + ) { + return { year: y, month: m, day: d }; + } + } + } + } + return null; + }, + // 距离农历日期还有多少天 + daysToLunar(lunar) { + const solar = lunarBiz.lunar2solar(lunar); + const date = new Date(solar.year, solar.month - 1, solar.day); + const now = new Date(); + return Math.ceil((date - now) / (1000 * 60 * 60 * 24)); + } +}; + +// === 新增:主题模式公共资源 (CSS覆盖 + JS逻辑) === +const themeResources = ` + + +`; +// 定义HTML模板 +const loginPage = ` + + + + + + 订阅管理系统 + + + ${themeResources} + + + + + + + +`; + +const adminPage = ` + + + + + + 订阅管理系统 + + + ${themeResources} + + +
+ + + +
+
+
+

订阅列表

+

使用搜索与分类快速定位订阅,开启农历显示可同步查看农历日期

+
+
+
+
+ + + + +
+
+ +
+
+ +
+ +
+
+ + +
+
+
+ +
+
+ + + + + + + + + + + + + + +
+ 名称 + + 类型 + + 到期 + + 金额 + + 提醒 + + 状态 + + 操作 +
+
+
+
+ + + + + + +`; + +const configPage = ` + + + + + + 系统配置 - 订阅管理系统 + + + ${themeResources} + + +
+ + + +
+
+

系统配置

+ +
+
+

管理员账户

+
+
+ + +
+
+ + +

留空表示不修改当前密码

+
+
+
+ +
+

显示设置

+ +
+ + +

选择系统的外观风格

+
+ +
+ +

控制是否在通知消息中包含农历日期信息

+
+
+ + +
+

时区设置

+
+ + +

选择需要使用时区,系统会按该时区计算剩余时间(提醒 Cron 仍基于 UTC,请在 Cloudflare 控制台换算触发时间)

+
+
+ + +
+

通知设置

+
+
+ + +

Comma-separated hours; empty = allow any hour (used when per-item hours are not set)

+
+
+

提示

+

Cloudflare Workers Cron 以 UTC 计算,例如北京时间 08:00 需设置 Cron 为 0 0 * * * 并在此填入 08。

+

若 Cron 已设置为每小时执行,可用该字段限制实际发送提醒的小时段。

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

调用 /api/notify/{token} 接口时需携带此令牌;留空表示禁用第三方 API 推送。

+
+ +
+

Telegram 配置

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

NotifyX 配置

+
+ +
+ + +
+

NotifyX平台 获取的 API Key

+
+
+ +
+
+ +
+

Email config

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

Webhook 通知 配置

+
+
+ + +

请填写自建服务或第三方平台提供的 Webhook 地址,例如 https://your-webhook-endpoint.com/path

+
+
+ + +
+
+ + +

JSON格式的自定义请求头,留空使用默认

+
+
+ + +

支持变量: {{title}}, {{content}}, {{timestamp}}。留空使用默认格式

+
+
+
+ +
+
+ +
+

企业微信机器人 配置

+
+
+ + +

从企业微信群聊中添加机器人获取的 Webhook URL

+
+
+ + +

选择发送的消息格式类型

+
+
+ + +

需要@的手机号,多个用逗号分隔,留空则不@任何人

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

Bark 配置

+
+
+ + +

Bark 服务器地址,默认为官方服务器,也可以使用自建服务器

+
+
+ +
+ + +
+

Bark iOS 应用 中获取的设备Key

+
+
+ + +

勾选后推送消息会保存到 Bark 的历史记录中

+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ + + + +`; + +// 管理页面 +// 与前端一致的分类切割正则,用于提取标签信息 +const CATEGORY_SEPARATOR_REGEX = /[\/,,\s]+/; + + +function dashboardPage() { + return ` + + + + + 仪表盘 - SubsTracker + + + ${themeResources} + + + + +
+
+

📊 仪表板

+

订阅费用和活动概览(统计金额已折合为 CNY)

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

最近支付

+
+ 过去7天 +
+
+
+
+
+ +
+
+
+ +

即将续费

+
+ 未来7天 +
+
+
+
+
+ +
+
+
+
+ +

按类型支出排行

+
+ 年度统计 (折合CNY) +
+
+
+
+
+ +
+
+
+ +

按分类支出统计

+
+ 年度统计 (折合CNY) +
+
+
+
+
+
+
+ + + +`; +} + +function extractTagsFromSubscriptions(subscriptions = []) { + const tagSet = new Set(); + (subscriptions || []).forEach(sub => { + if (!sub || typeof sub !== 'object') { + return; + } + if (Array.isArray(sub.tags)) { + sub.tags.forEach(tag => { + if (typeof tag === 'string' && tag.trim().length > 0) { + tagSet.add(tag.trim()); + } + }); + } + if (typeof sub.category === 'string') { + sub.category.split(CATEGORY_SEPARATOR_REGEX) + .map(tag => tag.trim()) + .filter(tag => tag.length > 0) + .forEach(tag => tagSet.add(tag)); + } + if (typeof sub.customType === 'string' && sub.customType.trim().length > 0) { + tagSet.add(sub.customType.trim()); + } + }); + return Array.from(tagSet); +} + +const admin = { + async handleRequest(request, env, ctx) { + try { + const url = new URL(request.url); + const pathname = url.pathname; + + console.log('[管理页面] 访问路径:', pathname); + + const token = getCookieValue(request.headers.get('Cookie'), 'token'); + console.log('[管理页面] Token存在:', !!token); + + const config = await getConfig(env); + const user = token ? await verifyJWT(token, config.JWT_SECRET) : null; + + console.log('[管理页面] 用户验证结果:', !!user); + + if (!user) { + console.log('[管理页面] 用户未登录,重定向到登录页面'); + return new Response('', { + status: 302, + headers: { 'Location': '/' } + }); + } + + if (pathname === '/admin/config') { + return new Response(configPage, { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }); + } + + if (pathname === '/admin/dashboard') { + return new Response(dashboardPage(), { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }); + } + + return new Response(adminPage, { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }); + } catch (error) { + console.error('[管理页面] 处理请求时出错:', error); + return new Response('服务器内部错误', { + status: 500, + headers: { 'Content-Type': 'text/plain; charset=utf-8' } + }); + } + } +}; + +// 处理API请求 +const api = { + async handleRequest(request, env, ctx) { + const url = new URL(request.url); + const path = url.pathname.slice(4); + const method = request.method; + + const config = await getConfig(env); + + if (path === '/login' && method === 'POST') { + const body = await request.json(); + + if (body.username === config.ADMIN_USERNAME && body.password === config.ADMIN_PASSWORD) { + const token = await generateJWT(body.username, config.JWT_SECRET); + + return new Response( + JSON.stringify({ success: true }), + { + headers: { + 'Content-Type': 'application/json', + 'Set-Cookie': 'token=' + token + '; HttpOnly; Path=/; SameSite=Strict; Max-Age=86400' + } + } + ); + } else { + return new Response( + JSON.stringify({ success: false, message: '用户名或密码错误' }), + { headers: { 'Content-Type': 'application/json' } } + ); + } + } + + if (path === '/logout' && (method === 'GET' || method === 'POST')) { + return new Response('', { + status: 302, + headers: { + 'Location': '/', + 'Set-Cookie': 'token=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0' + } + }); + } + + const token = getCookieValue(request.headers.get('Cookie'), 'token'); + const user = token ? await verifyJWT(token, config.JWT_SECRET) : null; + + if (!user && path !== '/login') { + return new Response( + JSON.stringify({ success: false, message: '未授权访问' }), + { status: 401, headers: { 'Content-Type': 'application/json' } } + ); + } + + if (path === '/config') { + if (method === 'GET') { + const { JWT_SECRET, ADMIN_PASSWORD, ...safeConfig } = config; + return new Response( + JSON.stringify(safeConfig), + { headers: { 'Content-Type': 'application/json' } } + ); + } + + if (method === 'POST') { + try { + const newConfig = await request.json(); + + const updatedConfig = { + ...config, + ADMIN_USERNAME: newConfig.ADMIN_USERNAME || config.ADMIN_USERNAME, + THEME_MODE: newConfig.THEME_MODE || 'system', // 保存主题配置 + TG_BOT_TOKEN: newConfig.TG_BOT_TOKEN || '', + TG_CHAT_ID: newConfig.TG_CHAT_ID || '', + NOTIFYX_API_KEY: newConfig.NOTIFYX_API_KEY || '', + WEBHOOK_URL: newConfig.WEBHOOK_URL || '', + WEBHOOK_METHOD: newConfig.WEBHOOK_METHOD || 'POST', + WEBHOOK_HEADERS: newConfig.WEBHOOK_HEADERS || '', + WEBHOOK_TEMPLATE: newConfig.WEBHOOK_TEMPLATE || '', + SHOW_LUNAR: newConfig.SHOW_LUNAR === true, + WECHATBOT_WEBHOOK: newConfig.WECHATBOT_WEBHOOK || '', + WECHATBOT_MSG_TYPE: newConfig.WECHATBOT_MSG_TYPE || 'text', + WECHATBOT_AT_MOBILES: newConfig.WECHATBOT_AT_MOBILES || '', + WECHATBOT_AT_ALL: newConfig.WECHATBOT_AT_ALL || 'false', + RESEND_API_KEY: newConfig.RESEND_API_KEY || config.RESEND_API_KEY || '', + RESEND_FROM: newConfig.RESEND_FROM || config.RESEND_FROM || '', + RESEND_FROM_NAME: newConfig.RESEND_FROM_NAME || config.RESEND_FROM_NAME || '', + RESEND_TO: newConfig.RESEND_TO || config.RESEND_TO || '', + MAILGUN_API_KEY: newConfig.MAILGUN_API_KEY || config.MAILGUN_API_KEY || '', + MAILGUN_FROM: newConfig.MAILGUN_FROM || config.MAILGUN_FROM || '', + MAILGUN_FROM_NAME: newConfig.MAILGUN_FROM_NAME || config.MAILGUN_FROM_NAME || '', + MAILGUN_TO: newConfig.MAILGUN_TO || config.MAILGUN_TO || '', + EMAIL_FROM: newConfig.EMAIL_FROM || config.EMAIL_FROM || '', + EMAIL_FROM_NAME: newConfig.EMAIL_FROM_NAME || config.EMAIL_FROM_NAME || '', + EMAIL_TO: newConfig.EMAIL_TO || config.EMAIL_TO || '', + SMTP_HOST: newConfig.SMTP_HOST || config.SMTP_HOST || '', + SMTP_PORT: newConfig.SMTP_PORT || config.SMTP_PORT || '', + SMTP_USER: newConfig.SMTP_USER || config.SMTP_USER || '', + SMTP_PASS: newConfig.SMTP_PASS || config.SMTP_PASS || '', + SMTP_FROM: newConfig.SMTP_FROM || config.SMTP_FROM || '', + SMTP_FROM_NAME: newConfig.SMTP_FROM_NAME || config.SMTP_FROM_NAME || '', + SMTP_TO: newConfig.SMTP_TO || config.SMTP_TO || '', + BARK_DEVICE_KEY: newConfig.BARK_DEVICE_KEY || '', + BARK_SERVER: newConfig.BARK_SERVER || 'https://api.day.app', + BARK_IS_ARCHIVE: newConfig.BARK_IS_ARCHIVE || 'false', + ENABLED_NOTIFIERS: newConfig.ENABLED_NOTIFIERS || ['notifyx'], + TIMEZONE: newConfig.TIMEZONE || config.TIMEZONE || 'UTC', + THIRD_PARTY_API_TOKEN: newConfig.THIRD_PARTY_API_TOKEN || '' + }; + + const rawNotificationHours = Array.isArray(newConfig.NOTIFICATION_HOURS) + ? newConfig.NOTIFICATION_HOURS + : typeof newConfig.NOTIFICATION_HOURS === 'string' + ? newConfig.NOTIFICATION_HOURS.split(',') + : []; + + const sanitizedNotificationHours = rawNotificationHours + .map(value => String(value).trim()) + .filter(value => value.length > 0) + .map(value => { + const upperValue = value.toUpperCase(); + if (upperValue === '*' || upperValue === 'ALL') { + return '*'; + } + const numeric = Number(upperValue); + if (!isNaN(numeric)) { + return String(Math.max(0, Math.min(23, Math.floor(numeric)))).padStart(2, '0'); + } + return upperValue; + }); + + updatedConfig.NOTIFICATION_HOURS = sanitizedNotificationHours; + + if (newConfig.ADMIN_PASSWORD) { + updatedConfig.ADMIN_PASSWORD = newConfig.ADMIN_PASSWORD; + } + + // 确保JWT_SECRET存在且安全 + if (!updatedConfig.JWT_SECRET || updatedConfig.JWT_SECRET === 'your-secret-key') { + updatedConfig.JWT_SECRET = generateRandomSecret(); + console.log('[安全] 生成新的JWT密钥'); + } + + await env.SUBSCRIPTIONS_KV.put('config', JSON.stringify(updatedConfig)); + + return new Response( + JSON.stringify({ success: true }), + { headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error('配置保存错误:', error); + return new Response( + JSON.stringify({ success: false, message: '更新配置失败: ' + error.message }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + } + } + + if (path === '/dashboard/stats' && method === 'GET') { + try { + const subscriptions = await getAllSubscriptions(env); + const timezone = config?.TIMEZONE || 'UTC'; + + const rates = await getDynamicRates(env); // 获取动态汇率 + const monthlyExpense = calculateMonthlyExpense(subscriptions, timezone, rates); + const yearlyExpense = calculateYearlyExpense(subscriptions, timezone, rates); + const recentPayments = getRecentPayments(subscriptions, timezone); // 不需要汇率 + const upcomingRenewals = getUpcomingRenewals(subscriptions, timezone); // 不需要汇率 + const expenseByType = getExpenseByType(subscriptions, timezone, rates); + const expenseByCategory = getExpenseByCategory(subscriptions, timezone, rates); + + const activeSubscriptions = subscriptions.filter(s => s.isActive); + const now = getCurrentTimeInTimezone(timezone); + + // 使用每个订阅自己的提醒设置来判断是否即将到期 + const expiringSoon = activeSubscriptions.filter(s => { + const expiryDate = new Date(s.expiryDate); + const diffMs = expiryDate.getTime() - now.getTime(); + const diffHours = diffMs / (1000 * 60 * 60); + const diffDays = diffMs / (1000 * 60 * 60 * 24); + + // 获取订阅的提醒设置 + const reminder = resolveReminderSetting(s, 7); + + // 根据提醒单位判断是否即将到期 + const isSoon = reminder.unit === 'minute' + ? diffMs >= 0 && diffMs <= reminder.value * 60 * 1000 + : reminder.unit === 'hour' + ? diffHours >= 0 && diffHours <= reminder.value + : diffDays >= 0 && diffDays <= reminder.value; + + return isSoon; + }).length; + + return new Response( + JSON.stringify({ + success: true, + data: { + monthlyExpense, + yearlyExpense, + activeSubscriptions: { + active: activeSubscriptions.length, + total: subscriptions.length, + expiringSoon + }, + recentPayments, + upcomingRenewals, + expenseByType, + expenseByCategory + } + }), + { headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error('获取仪表盘统计失败:', error); + return new Response( + JSON.stringify({ success: false, message: '获取统计数据失败: ' + error.message }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } + } + + if (path === '/test-notification' && method === 'POST') { + try { + const body = await request.json(); + let success = false; + let message = ''; + + if (body.type === 'telegram') { + const testConfig = { + ...config, + TG_BOT_TOKEN: body.TG_BOT_TOKEN, + TG_CHAT_ID: body.TG_CHAT_ID + }; + + const content = '*测试通知*\n\n这是一条测试通知,用于验证Telegram通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + success = await sendTelegramNotification(content, testConfig); + message = success ? 'Telegram通知发送成功' : 'Telegram通知发送失败,请检查配置'; + } else if (body.type === 'notifyx') { + const testConfig = { + ...config, + NOTIFYX_API_KEY: body.NOTIFYX_API_KEY + }; + + const title = '测试通知'; + const content = '## 这是一条测试通知\n\n用于验证NotifyX通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + const description = '测试NotifyX通知功能'; + + success = await sendNotifyXNotification(title, content, description, testConfig); + message = success ? 'NotifyX通知发送成功' : 'NotifyX通知发送失败,请检查配置'; + } else if (body.type === 'webhook') { + const testConfig = { + ...config, + WEBHOOK_URL: body.WEBHOOK_URL, + WEBHOOK_METHOD: body.WEBHOOK_METHOD, + WEBHOOK_HEADERS: body.WEBHOOK_HEADERS, + WEBHOOK_TEMPLATE: body.WEBHOOK_TEMPLATE + }; + + const title = '测试通知'; + const content = '这是一条测试通知,用于验证Webhook 通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + + success = await sendWebhookNotification(title, content, testConfig); + message = success ? 'Webhook 通知发送成功' : 'Webhook 通知发送失败,请检查配置'; + } else if (body.type === 'wechatbot') { + const testConfig = { + ...config, + WECHATBOT_WEBHOOK: body.WECHATBOT_WEBHOOK, + WECHATBOT_MSG_TYPE: body.WECHATBOT_MSG_TYPE, + WECHATBOT_AT_MOBILES: body.WECHATBOT_AT_MOBILES, + WECHATBOT_AT_ALL: body.WECHATBOT_AT_ALL + }; + + const title = '测试通知'; + const content = '这是一条测试通知,用于验证企业微信机器人功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + + success = await sendWechatBotNotification(title, content, testConfig); + message = success ? '企业微信机器人通知发送成功' : '企业微信机器人通知发送失败,请检查配置'; + } else if (body.type === 'email_resend' || body.type === 'email_smtp' || body.type === 'email_mailgun') { + const testConfig = { + ...config, + RESEND_API_KEY: body.RESEND_API_KEY, + RESEND_FROM: body.RESEND_FROM, + RESEND_FROM_NAME: body.RESEND_FROM_NAME, + RESEND_TO: body.RESEND_TO, + MAILGUN_API_KEY: body.MAILGUN_API_KEY, + MAILGUN_FROM: body.MAILGUN_FROM, + MAILGUN_FROM_NAME: body.MAILGUN_FROM_NAME, + MAILGUN_TO: body.MAILGUN_TO, + EMAIL_FROM: body.EMAIL_FROM, + EMAIL_FROM_NAME: body.EMAIL_FROM_NAME, + EMAIL_TO: body.EMAIL_TO, + SMTP_HOST: body.SMTP_HOST, + SMTP_PORT: body.SMTP_PORT, + SMTP_USER: body.SMTP_USER, + SMTP_PASS: body.SMTP_PASS, + SMTP_FROM: body.SMTP_FROM, + SMTP_FROM_NAME: body.SMTP_FROM_NAME, + SMTP_TO: body.SMTP_TO + }; + const emailProvider = body.EMAIL_PROVIDER + || (body.type === 'email_smtp' ? 'smtp' : (body.type === 'email_mailgun' ? 'mailgun' : 'resend')); + + const title = '测试通知'; + const content = '这是一条测试通知,用于验证邮件通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + + if (emailProvider === 'smtp') { + const htmlContent = '
' + content.replace(/\n/g, '
') + '
'; + const detail = await sendSmtpEmailNotificationDetailed( + title, + content, + htmlContent, + testConfig, + normalizeEmailRecipients(testConfig.SMTP_TO || testConfig.EMAIL_TO) + ); + success = detail.success; + message = success ? '邮件通知发送成功' : 'SMTP发送失败: ' + (detail.message || '未知错误'); + } else { + success = await sendEmailNotification(title, content, testConfig, { provider: emailProvider }); + message = success ? '邮件通知发送成功' : '邮件通知发送失败,请检查配置'; + } + } else if (body.type === 'bark') { + const testConfig = { + ...config, + BARK_SERVER: body.BARK_SERVER, + BARK_DEVICE_KEY: body.BARK_DEVICE_KEY, + BARK_IS_ARCHIVE: body.BARK_IS_ARCHIVE + }; + + const title = '测试通知'; + const content = '这是一条测试通知,用于验证Bark通知功能是否正常工作。\n\n发送时间: ' + formatBeijingTime(); + + success = await sendBarkNotification(title, content, testConfig); + message = success ? 'Bark通知发送成功' : 'Bark通知发送失败,请检查配置'; + } + + return new Response( + JSON.stringify({ success, message }), + { headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error('测试通知失败:', error); + return new Response( + JSON.stringify({ success: false, message: '测试通知失败: ' + error.message }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } + } + + if (path === '/subscriptions') { + if (method === 'GET') { + const subscriptions = await getAllSubscriptions(env); + return new Response( + JSON.stringify(subscriptions), + { headers: { 'Content-Type': 'application/json' } } + ); + } + + if (method === 'POST') { + const subscription = await request.json(); + const result = await createSubscription(subscription, env); + + return new Response( + JSON.stringify(result), + { + status: result.success ? 201 : 400, + headers: { 'Content-Type': 'application/json' } + } + ); + } + } + + if (path.startsWith('/subscriptions/')) { + const parts = path.split('/'); + const id = parts[2]; + + if (parts[3] === 'toggle-status' && method === 'POST') { + const body = await request.json(); + const result = await toggleSubscriptionStatus(id, body.isActive, env); + + return new Response( + JSON.stringify(result), + { + status: result.success ? 200 : 400, + headers: { 'Content-Type': 'application/json' } + } + ); + } + + if (parts[3] === 'test-notify' && method === 'POST') { + const result = await testSingleSubscriptionNotification(id, env); + return new Response(JSON.stringify(result), { status: result.success ? 200 : 500, headers: { 'Content-Type': 'application/json' } }); + } + + if (parts[3] === 'renew' && method === 'POST') { + let options = {}; + try { + const body = await request.json(); + options = body || {}; + } catch (e) { + // 如果没有请求体,使用默认空对象 + } + const result = await manualRenewSubscription(id, env, options); + return new Response(JSON.stringify(result), { status: result.success ? 200 : 400, headers: { 'Content-Type': 'application/json' } }); + } + + if (parts[3] === 'payments' && method === 'GET') { + const subscription = await getSubscription(id, env); + if (!subscription) { + return new Response(JSON.stringify({ success: false, message: '订阅不存在' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); + } + return new Response(JSON.stringify({ success: true, payments: subscription.paymentHistory || [] }), { headers: { 'Content-Type': 'application/json' } }); + } + + if (parts[3] === 'payments' && parts[4] && method === 'DELETE') { + const paymentId = parts[4]; + const result = await deletePaymentRecord(id, paymentId, env); + return new Response(JSON.stringify(result), { status: result.success ? 200 : 400, headers: { 'Content-Type': 'application/json' } }); + } + + if (parts[3] === 'payments' && parts[4] && method === 'PUT') { + const paymentId = parts[4]; + const paymentData = await request.json(); + const result = await updatePaymentRecord(id, paymentId, paymentData, env); + return new Response(JSON.stringify(result), { status: result.success ? 200 : 400, headers: { 'Content-Type': 'application/json' } }); + } + + if (method === 'GET') { + const subscription = await getSubscription(id, env); + + return new Response( + JSON.stringify(subscription), + { headers: { 'Content-Type': 'application/json' } } + ); + } + + if (method === 'PUT') { + const subscription = await request.json(); + const result = await updateSubscription(id, subscription, env); + + return new Response( + JSON.stringify(result), + { + status: result.success ? 200 : 400, + headers: { 'Content-Type': 'application/json' } + } + ); + } + + if (method === 'DELETE') { + const result = await deleteSubscription(id, env); + + return new Response( + JSON.stringify(result), + { + status: result.success ? 200 : 400, + headers: { 'Content-Type': 'application/json' } + } + ); + } + } + + // 处理第三方通知API + if (path.startsWith('/notify/')) { + const pathSegments = path.split('/'); + // 允许通过路径、Authorization 头或查询参数三种方式传入访问令牌 + const tokenFromPath = pathSegments[2] || ''; + const tokenFromHeader = (request.headers.get('Authorization') || '').replace(/^Bearer\s+/i, '').trim(); + const tokenFromQuery = url.searchParams.get('token') || ''; + const providedToken = tokenFromPath || tokenFromHeader || tokenFromQuery; + const expectedToken = config.THIRD_PARTY_API_TOKEN || ''; + + if (!expectedToken) { + return new Response( + JSON.stringify({ message: '第三方 API 已禁用,请在后台配置访问令牌后使用' }), + { status: 403, headers: { 'Content-Type': 'application/json' } } + ); + } + + if (!providedToken || providedToken !== expectedToken) { + return new Response( + JSON.stringify({ message: '访问未授权,令牌无效或缺失' }), + { status: 401, headers: { 'Content-Type': 'application/json' } } + ); + } + + if (method === 'POST') { + try { + const body = await request.json(); + const title = body.title || '第三方通知'; + const content = body.content || ''; + + if (!content) { + return new Response( + JSON.stringify({ message: '缺少必填参数 content' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + const config = await getConfig(env); + const bodyTagsRaw = Array.isArray(body.tags) + ? body.tags + : (typeof body.tags === 'string' ? body.tags.split(/[,,\s]+/) : []); + const bodyTags = Array.isArray(bodyTagsRaw) + ? bodyTagsRaw.filter(tag => typeof tag === 'string' && tag.trim().length > 0).map(tag => tag.trim()) + : []; + + // 使用多渠道发送通知 + await sendNotificationToAllChannels(title, content, config, '[第三方API]', { + metadata: { tags: bodyTags } + }); + + return new Response( + JSON.stringify({ + message: '发送成功', + response: { + errcode: 0, + errmsg: 'ok', + msgid: 'MSGID' + Date.now() + } + }), + { headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error('[第三方API] 发送通知失败:', error); + return new Response( + JSON.stringify({ + message: '发送失败', + response: { + errcode: 1, + errmsg: error.message + } + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } + } + } + + return new Response( + JSON.stringify({ success: false, message: '未找到请求的资源' }), + { status: 404, headers: { 'Content-Type': 'application/json' } } + ); + } +}; + +// 工具函数 +function generateRandomSecret() { + // 生成一个64字符的随机密钥 + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'; + let result = ''; + for (let i = 0; i < 64; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +async function getConfig(env) { + try { + if (!env.SUBSCRIPTIONS_KV) { + console.error('[配置] KV存储未绑定'); + throw new Error('KV存储未绑定'); + } + + const data = await env.SUBSCRIPTIONS_KV.get('config'); + console.log('[配置] 从KV读取配置:', data ? '成功' : '空配置'); + + const config = data ? JSON.parse(data) : {}; + + // 确保JWT_SECRET的一致性 + let jwtSecret = config.JWT_SECRET; + if (!jwtSecret || jwtSecret === 'your-secret-key') { + jwtSecret = generateRandomSecret(); + console.log('[配置] 生成新的JWT密钥'); + + // 保存新的JWT密钥 + const updatedConfig = { ...config, JWT_SECRET: jwtSecret }; + await env.SUBSCRIPTIONS_KV.put('config', JSON.stringify(updatedConfig)); + } + + const finalConfig = { + ADMIN_USERNAME: config.ADMIN_USERNAME || 'admin', + ADMIN_PASSWORD: config.ADMIN_PASSWORD || 'password', + JWT_SECRET: jwtSecret, + TG_BOT_TOKEN: config.TG_BOT_TOKEN || '', + TG_CHAT_ID: config.TG_CHAT_ID || '', + NOTIFYX_API_KEY: config.NOTIFYX_API_KEY || '', + WEBHOOK_URL: config.WEBHOOK_URL || '', + WEBHOOK_METHOD: config.WEBHOOK_METHOD || 'POST', + WEBHOOK_HEADERS: config.WEBHOOK_HEADERS || '', + WEBHOOK_TEMPLATE: config.WEBHOOK_TEMPLATE || '', + SHOW_LUNAR: config.SHOW_LUNAR === true, + WECHATBOT_WEBHOOK: config.WECHATBOT_WEBHOOK || '', + WECHATBOT_MSG_TYPE: config.WECHATBOT_MSG_TYPE || 'text', + WECHATBOT_AT_MOBILES: config.WECHATBOT_AT_MOBILES || '', + WECHATBOT_AT_ALL: config.WECHATBOT_AT_ALL || 'false', + RESEND_API_KEY: config.RESEND_API_KEY || '', + RESEND_FROM: config.RESEND_FROM || config.EMAIL_FROM || '', + RESEND_FROM_NAME: config.RESEND_FROM_NAME || config.EMAIL_FROM_NAME || '', + RESEND_TO: config.RESEND_TO || config.EMAIL_TO || '', + MAILGUN_API_KEY: config.MAILGUN_API_KEY || '', + MAILGUN_FROM: config.MAILGUN_FROM || config.EMAIL_FROM || '', + MAILGUN_FROM_NAME: config.MAILGUN_FROM_NAME || config.EMAIL_FROM_NAME || '', + MAILGUN_TO: config.MAILGUN_TO || config.EMAIL_TO || '', + EMAIL_FROM: config.EMAIL_FROM || '', + EMAIL_FROM_NAME: config.EMAIL_FROM_NAME || '', + EMAIL_TO: config.EMAIL_TO || '', + SMTP_HOST: config.SMTP_HOST || '', + SMTP_PORT: config.SMTP_PORT || '', + SMTP_USER: config.SMTP_USER || '', + SMTP_PASS: config.SMTP_PASS || '', + SMTP_FROM: config.SMTP_FROM || config.EMAIL_FROM || '', + SMTP_FROM_NAME: config.SMTP_FROM_NAME || config.EMAIL_FROM_NAME || '', + SMTP_TO: config.SMTP_TO || config.EMAIL_TO || '', + BARK_DEVICE_KEY: config.BARK_DEVICE_KEY || '', + BARK_SERVER: config.BARK_SERVER || 'https://api.day.app', + BARK_IS_ARCHIVE: config.BARK_IS_ARCHIVE || 'false', + ENABLED_NOTIFIERS: config.ENABLED_NOTIFIERS || ['notifyx'], + THEME_MODE: config.THEME_MODE || 'system', // 默认主题为跟随系统 + TIMEZONE: config.TIMEZONE || 'UTC', // 新增时区字段 + NOTIFICATION_HOURS: Array.isArray(config.NOTIFICATION_HOURS) ? config.NOTIFICATION_HOURS : [], + THIRD_PARTY_API_TOKEN: config.THIRD_PARTY_API_TOKEN || '' + }; + + console.log('[配置] 最终配置用户名:', finalConfig.ADMIN_USERNAME); + return finalConfig; + } catch (error) { + console.error('[配置] 获取配置失败:', error); + const defaultJwtSecret = generateRandomSecret(); + + return { + ADMIN_USERNAME: 'admin', + ADMIN_PASSWORD: 'password', + JWT_SECRET: defaultJwtSecret, + TG_BOT_TOKEN: '', + TG_CHAT_ID: '', + NOTIFYX_API_KEY: '', + WEBHOOK_URL: '', + WEBHOOK_METHOD: 'POST', + WEBHOOK_HEADERS: '', + WEBHOOK_TEMPLATE: '', + SHOW_LUNAR: true, + WECHATBOT_WEBHOOK: '', + WECHATBOT_MSG_TYPE: 'text', + WECHATBOT_AT_MOBILES: '', + WECHATBOT_AT_ALL: 'false', + RESEND_API_KEY: '', + RESEND_FROM: '', + RESEND_FROM_NAME: '', + RESEND_TO: '', + MAILGUN_API_KEY: '', + MAILGUN_FROM: '', + MAILGUN_FROM_NAME: '', + MAILGUN_TO: '', + EMAIL_FROM: '', + EMAIL_FROM_NAME: '', + EMAIL_TO: '', + SMTP_HOST: '', + SMTP_PORT: '', + SMTP_USER: '', + SMTP_PASS: '', + SMTP_FROM: '', + SMTP_FROM_NAME: '', + SMTP_TO: '', + ENABLED_NOTIFIERS: ['notifyx'], + NOTIFICATION_HOURS: [], + TIMEZONE: 'UTC', // 新增时区字段 + THIRD_PARTY_API_TOKEN: '' + }; + } +} + +async function generateJWT(username, secret) { + const header = { alg: 'HS256', typ: 'JWT' }; + const payload = { username, iat: Math.floor(Date.now() / 1000) }; + + const headerBase64 = btoa(JSON.stringify(header)); + const payloadBase64 = btoa(JSON.stringify(payload)); + + const signatureInput = headerBase64 + '.' + payloadBase64; + const signature = await CryptoJS.HmacSHA256(signatureInput, secret); + + return headerBase64 + '.' + payloadBase64 + '.' + signature; +} + +async function verifyJWT(token, secret) { + try { + if (!token || !secret) { + console.log('[JWT] Token或Secret为空'); + return null; + } + + const parts = token.split('.'); + if (parts.length !== 3) { + console.log('[JWT] Token格式错误,部分数量:', parts.length); + return null; + } + + const [headerBase64, payloadBase64, signature] = parts; + const signatureInput = headerBase64 + '.' + payloadBase64; + const expectedSignature = await CryptoJS.HmacSHA256(signatureInput, secret); + + if (signature !== expectedSignature) { + console.log('[JWT] 签名验证失败'); + return null; + } + + const payload = JSON.parse(atob(payloadBase64)); + console.log('[JWT] 验证成功,用户:', payload.username); + return payload; + } catch (error) { + console.error('[JWT] 验证过程出错:', error); + return null; + } +} + +async function getAllSubscriptions(env) { + try { + const data = await env.SUBSCRIPTIONS_KV.get('subscriptions'); + return data ? JSON.parse(data) : []; + } catch (error) { + return []; + } +} + +async function getSubscription(id, env) { + const subscriptions = await getAllSubscriptions(env); + return subscriptions.find(s => s.id === id); +} + +// 2. 修改 createSubscription,支持 useLunar 字段 +async function createSubscription(subscription, env) { + try { + const subscriptions = await getAllSubscriptions(env); + + if (!subscription.name || !subscription.expiryDate) { + return { success: false, message: '缺少必填字段' }; + } + + let expiryDate = new Date(subscription.expiryDate); + const config = await getConfig(env); + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + + + let useLunar = !!subscription.useLunar; + if (useLunar) { + let lunar = lunarCalendar.solar2lunar( + expiryDate.getFullYear(), + expiryDate.getMonth() + 1, + expiryDate.getDate() + ); + + if (lunar && subscription.periodValue && subscription.periodUnit) { + // 如果到期日<=今天,自动推算到下一个周期 + while (expiryDate <= currentTime) { + lunar = lunarBiz.addLunarPeriod(lunar, subscription.periodValue, subscription.periodUnit); + const solar = lunarBiz.lunar2solar(lunar); + expiryDate = new Date(solar.year, solar.month - 1, solar.day); + } + subscription.expiryDate = expiryDate.toISOString(); + } + } else { + if (expiryDate < currentTime && subscription.periodValue && subscription.periodUnit) { + while (expiryDate < currentTime) { + if (subscription.periodUnit === 'minute') { + expiryDate.setMinutes(expiryDate.getMinutes() + subscription.periodValue); + } else if (subscription.periodUnit === 'hour') { + expiryDate.setHours(expiryDate.getHours() + subscription.periodValue); + } else if (subscription.periodUnit === 'day') { + expiryDate.setDate(expiryDate.getDate() + subscription.periodValue); + } else if (subscription.periodUnit === 'month') { + expiryDate.setMonth(expiryDate.getMonth() + subscription.periodValue); + } else if (subscription.periodUnit === 'year') { + expiryDate.setFullYear(expiryDate.getFullYear() + subscription.periodValue); + } + } + subscription.expiryDate = expiryDate.toISOString(); + } + } + + const reminderSetting = resolveReminderSetting(subscription); + const hasNotificationHours = Object.prototype.hasOwnProperty.call(subscription, 'notificationHours'); + const hasEnabledNotifiers = Object.prototype.hasOwnProperty.call(subscription, 'enabledNotifiers'); + const hasEmailTo = Object.prototype.hasOwnProperty.call(subscription, 'emailTo'); + const normalizedNotificationHours = hasNotificationHours + ? normalizeNotificationHours(subscription.notificationHours) + : undefined; + const normalizedNotifiers = hasEnabledNotifiers + ? normalizeNotifierList(subscription.enabledNotifiers) + : undefined; + const normalizedEmailTo = hasEmailTo + ? normalizeEmailRecipients(subscription.emailTo) + : undefined; + + const initialPaymentDate = subscription.startDate || currentTime.toISOString(); + const newSubscription = { + id: Date.now().toString(), // 前端使用本地时间戳 + name: subscription.name, + subscriptionMode: subscription.subscriptionMode || 'cycle', // 默认循环订阅 + customType: subscription.customType || '', + category: subscription.category ? subscription.category.trim() : '', + startDate: subscription.startDate || null, + expiryDate: subscription.expiryDate, + periodValue: subscription.periodValue || 1, + periodUnit: subscription.periodUnit || 'month', + reminderUnit: reminderSetting.unit, + reminderValue: reminderSetting.value, + reminderDays: reminderSetting.unit === 'day' ? reminderSetting.value : undefined, + reminderHours: (reminderSetting.unit === 'hour' || reminderSetting.unit === 'minute') ? reminderSetting.value : undefined, + notificationHours: normalizedNotificationHours, + enabledNotifiers: normalizedNotifiers, + emailTo: normalizedEmailTo, + notes: subscription.notes || '', + amount: subscription.amount || null, + currency: subscription.currency || 'CNY', // 使用传入的币种,默认为CNY + lastPaymentDate: initialPaymentDate, + paymentHistory: subscription.amount ? [{ + id: Date.now().toString(), + date: initialPaymentDate, + amount: subscription.amount, + type: 'initial', + note: '初始订阅', + periodStart: subscription.startDate || initialPaymentDate, + periodEnd: subscription.expiryDate + }] : [], + isActive: subscription.isActive !== false, + autoRenew: subscription.autoRenew !== false, + useLunar: useLunar, + createdAt: new Date().toISOString() + }; + + subscriptions.push(newSubscription); + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: newSubscription }; + } catch (error) { + console.error("创建订阅异常:", error && error.stack ? error.stack : error); + return { success: false, message: error && error.message ? error.message : '创建订阅失败' }; + } +} + +// 3. 修改 updateSubscription,支持 useLunar 字段 +async function updateSubscription(id, subscription, env) { + try { + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === id); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + if (!subscription.name || !subscription.expiryDate) { + return { success: false, message: '缺少必填字段' }; + } + + let expiryDate = new Date(subscription.expiryDate); + const config = await getConfig(env); + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + + let useLunar = !!subscription.useLunar; + if (useLunar) { + let lunar = lunarCalendar.solar2lunar( + expiryDate.getFullYear(), + expiryDate.getMonth() + 1, + expiryDate.getDate() + ); + if (!lunar) { + return { success: false, message: '农历日期超出支持范围(1900-2100年)' }; + } + if (lunar && expiryDate < currentTime && subscription.periodValue && subscription.periodUnit) { + // 新增:循环加周期,直到 expiryDate > currentTime + do { + lunar = lunarBiz.addLunarPeriod(lunar, subscription.periodValue, subscription.periodUnit); + const solar = lunarBiz.lunar2solar(lunar); + expiryDate = new Date(solar.year, solar.month - 1, solar.day); + } while (expiryDate < currentTime); + subscription.expiryDate = expiryDate.toISOString(); + } + } else { + if (expiryDate < currentTime && subscription.periodValue && subscription.periodUnit) { + while (expiryDate < currentTime) { + if (subscription.periodUnit === 'minute') { + expiryDate.setMinutes(expiryDate.getMinutes() + subscription.periodValue); + } else if (subscription.periodUnit === 'hour') { + expiryDate.setHours(expiryDate.getHours() + subscription.periodValue); + } else if (subscription.periodUnit === 'day') { + expiryDate.setDate(expiryDate.getDate() + subscription.periodValue); + } else if (subscription.periodUnit === 'month') { + expiryDate.setMonth(expiryDate.getMonth() + subscription.periodValue); + } else if (subscription.periodUnit === 'year') { + expiryDate.setFullYear(expiryDate.getFullYear() + subscription.periodValue); + } + } + subscription.expiryDate = expiryDate.toISOString(); + } + } + + const reminderSource = { + reminderUnit: subscription.reminderUnit !== undefined ? subscription.reminderUnit : subscriptions[index].reminderUnit, + reminderValue: subscription.reminderValue !== undefined ? subscription.reminderValue : subscriptions[index].reminderValue, + reminderHours: subscription.reminderHours !== undefined ? subscription.reminderHours : subscriptions[index].reminderHours, + reminderDays: subscription.reminderDays !== undefined ? subscription.reminderDays : subscriptions[index].reminderDays + }; + const reminderSetting = resolveReminderSetting(reminderSource); + const hasNotificationHours = Object.prototype.hasOwnProperty.call(subscription, 'notificationHours'); + const hasEnabledNotifiers = Object.prototype.hasOwnProperty.call(subscription, 'enabledNotifiers'); + const hasEmailTo = Object.prototype.hasOwnProperty.call(subscription, 'emailTo'); + const normalizedNotificationHours = hasNotificationHours + ? normalizeNotificationHours(subscription.notificationHours) + : subscriptions[index].notificationHours; + const normalizedNotifiers = hasEnabledNotifiers + ? normalizeNotifierList(subscription.enabledNotifiers) + : subscriptions[index].enabledNotifiers; + const normalizedEmailTo = hasEmailTo + ? normalizeEmailRecipients(subscription.emailTo) + : subscriptions[index].emailTo; + + const oldSubscription = subscriptions[index]; + const newAmount = subscription.amount !== undefined ? subscription.amount : oldSubscription.amount; + + let paymentHistory = oldSubscription.paymentHistory || []; + + if (newAmount !== oldSubscription.amount) { + const initialPaymentIndex = paymentHistory.findIndex(p => p.type === 'initial'); + if (initialPaymentIndex !== -1) { + paymentHistory[initialPaymentIndex] = { + ...paymentHistory[initialPaymentIndex], + amount: newAmount + }; + } + } + + subscriptions[index] = { + ...subscriptions[index], + name: subscription.name, + subscriptionMode: subscription.subscriptionMode || subscriptions[index].subscriptionMode || 'cycle', // 如果没有提供 subscriptionMode,则使用旧的 subscriptionMode + customType: subscription.customType || subscriptions[index].customType || '', + category: subscription.category !== undefined ? subscription.category.trim() : (subscriptions[index].category || ''), + startDate: subscription.startDate || subscriptions[index].startDate, + expiryDate: subscription.expiryDate, + periodValue: subscription.periodValue || subscriptions[index].periodValue || 1, + periodUnit: subscription.periodUnit || subscriptions[index].periodUnit || 'month', + reminderUnit: reminderSetting.unit, + reminderValue: reminderSetting.value, + reminderDays: reminderSetting.unit === 'day' ? reminderSetting.value : undefined, + reminderHours: (reminderSetting.unit === 'hour' || reminderSetting.unit === 'minute') ? reminderSetting.value : undefined, + notificationHours: normalizedNotificationHours, + enabledNotifiers: normalizedNotifiers, + emailTo: normalizedEmailTo, + notes: subscription.notes || '', + amount: newAmount, // 使用新的变量 + currency: subscription.currency || subscriptions[index].currency || 'CNY', // 更新币种 + lastPaymentDate: subscriptions[index].lastPaymentDate || subscriptions[index].startDate || subscriptions[index].createdAt || currentTime.toISOString(), + paymentHistory: paymentHistory, // 保存更新后的支付历史 + isActive: subscription.isActive !== undefined ? subscription.isActive : subscriptions[index].isActive, + autoRenew: subscription.autoRenew !== undefined ? subscription.autoRenew : (subscriptions[index].autoRenew !== undefined ? subscriptions[index].autoRenew : true), + useLunar: useLunar, + updatedAt: new Date().toISOString() + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: subscriptions[index] }; + } catch (error) { + return { success: false, message: '更新订阅失败' }; + } +} + +async function deleteSubscription(id, env) { + try { + const subscriptions = await getAllSubscriptions(env); + const filteredSubscriptions = subscriptions.filter(s => s.id !== id); + + if (filteredSubscriptions.length === subscriptions.length) { + return { success: false, message: '订阅不存在' }; + } + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(filteredSubscriptions)); + + return { success: true }; + } catch (error) { + return { success: false, message: '删除订阅失败' }; + } +} + +async function manualRenewSubscription(id, env, options = {}) { + try { + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === id); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + const subscription = subscriptions[index]; + + if (!subscription.periodValue || !subscription.periodUnit) { + return { success: false, message: '订阅未设置续订周期' }; + } + + const config = await getConfig(env); + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + const todayMidnight = getTimezoneMidnightTimestamp(currentTime, timezone); + + // 参数处理 + const paymentDate = options.paymentDate ? new Date(options.paymentDate) : currentTime; + const amount = options.amount !== undefined ? options.amount : subscription.amount || 0; + const periodMultiplier = options.periodMultiplier || 1; + const note = options.note || '手动续订'; + const mode = subscription.subscriptionMode || 'cycle'; // 获取订阅模式 + + let newStartDate; + let currentExpiryDate = new Date(subscription.expiryDate); + + // 1. 确定新的周期起始日 (New Start Date) + if (mode === 'reset') { + // 重置模式:忽略旧的到期日,从今天(或支付日)开始 + newStartDate = new Date(paymentDate); + } else { + // 循环模式 (Cycle) + // 如果当前还没过期,从旧的 expiryDate 接着算 (无缝衔接) + // 如果已经过期了,为了避免补交过去空窗期的费,通常从今天开始算(或者你可以选择补齐,这里采用通用逻辑:过期则从今天开始) + if (currentExpiryDate.getTime() > paymentDate.getTime()) { + newStartDate = new Date(currentExpiryDate); + } else { + newStartDate = new Date(paymentDate); + } + } + + // 2. 计算新的到期日 (New Expiry Date) + let newExpiryDate; + if (subscription.useLunar) { + // 农历逻辑 + const solarStart = { + year: newStartDate.getFullYear(), + month: newStartDate.getMonth() + 1, + day: newStartDate.getDate() + }; + let lunar = lunarCalendar.solar2lunar(solarStart.year, solarStart.month, solarStart.day); + + let nextLunar = lunar; + for (let i = 0; i < periodMultiplier; i++) { + nextLunar = lunarBiz.addLunarPeriod(nextLunar, subscription.periodValue, subscription.periodUnit); + } + const solar = lunarBiz.lunar2solar(nextLunar); + newExpiryDate = new Date(solar.year, solar.month - 1, solar.day); + } else { + // 公历逻辑 + newExpiryDate = new Date(newStartDate); + const totalPeriodValue = subscription.periodValue * periodMultiplier; + + if (subscription.periodUnit === 'day') { + newExpiryDate.setDate(newExpiryDate.getDate() + totalPeriodValue); + } else if (subscription.periodUnit === 'month') { + newExpiryDate.setMonth(newExpiryDate.getMonth() + totalPeriodValue); + } else if (subscription.periodUnit === 'year') { + newExpiryDate.setFullYear(newExpiryDate.getFullYear() + totalPeriodValue); + } + } + + const paymentRecord = { + id: Date.now().toString(), + date: paymentDate.toISOString(), + amount: amount, + type: 'manual', + note: note, + periodStart: newStartDate.toISOString(), // 记录实际的计费开始日 + periodEnd: newExpiryDate.toISOString() + }; + + const paymentHistory = subscription.paymentHistory || []; + paymentHistory.push(paymentRecord); + + subscriptions[index] = { + ...subscription, + startDate: newStartDate.toISOString(), // 关键修复:更新 startDate,这样下次编辑时,Start + Period = Expiry 成立 + expiryDate: newExpiryDate.toISOString(), + lastPaymentDate: paymentDate.toISOString(), + paymentHistory + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: subscriptions[index], message: '续订成功' }; + } catch (error) { + console.error('手动续订失败:', error); + return { success: false, message: '续订失败: ' + error.message }; + } +} + +async function deletePaymentRecord(subscriptionId, paymentId, env) { + try { + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === subscriptionId); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + const subscription = subscriptions[index]; + const paymentHistory = subscription.paymentHistory || []; + const paymentIndex = paymentHistory.findIndex(p => p.id === paymentId); + + if (paymentIndex === -1) { + return { success: false, message: '支付记录不存在' }; + } + + const deletedPayment = paymentHistory[paymentIndex]; + + // 删除支付记录 + paymentHistory.splice(paymentIndex, 1); + + // 回退订阅周期和更新 lastPaymentDate + let newExpiryDate = subscription.expiryDate; + let newLastPaymentDate = subscription.lastPaymentDate; + + if (paymentHistory.length > 0) { + // 找到剩余支付记录中 periodEnd 最晚的那条(最新的续订) + const sortedByPeriodEnd = [...paymentHistory].sort((a, b) => { + const dateA = a.periodEnd ? new Date(a.periodEnd) : new Date(0); + const dateB = b.periodEnd ? new Date(b.periodEnd) : new Date(0); + return dateB - dateA; + }); + + // 订阅的到期日期应该是最新续订的 periodEnd + if (sortedByPeriodEnd[0].periodEnd) { + newExpiryDate = sortedByPeriodEnd[0].periodEnd; + } + + // 找到最新的支付记录日期 + const sortedByDate = [...paymentHistory].sort((a, b) => new Date(b.date) - new Date(a.date)); + newLastPaymentDate = sortedByDate[0].date; + } else { + // 如果没有支付记录了,回退到初始状态 + // expiryDate 保持不变或使用 periodStart(如果删除的记录有) + if (deletedPayment.periodStart) { + newExpiryDate = deletedPayment.periodStart; + } + newLastPaymentDate = subscription.startDate || subscription.createdAt || subscription.expiryDate; + } + + subscriptions[index] = { + ...subscription, + expiryDate: newExpiryDate, + paymentHistory, + lastPaymentDate: newLastPaymentDate + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: subscriptions[index], message: '支付记录已删除' }; + } catch (error) { + console.error('删除支付记录失败:', error); + return { success: false, message: '删除失败: ' + error.message }; + } +} + +async function updatePaymentRecord(subscriptionId, paymentId, paymentData, env) { + try { + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === subscriptionId); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + const subscription = subscriptions[index]; + const paymentHistory = subscription.paymentHistory || []; + const paymentIndex = paymentHistory.findIndex(p => p.id === paymentId); + + if (paymentIndex === -1) { + return { success: false, message: '支付记录不存在' }; + } + + // 更新支付记录 + paymentHistory[paymentIndex] = { + ...paymentHistory[paymentIndex], + date: paymentData.date || paymentHistory[paymentIndex].date, + amount: paymentData.amount !== undefined ? paymentData.amount : paymentHistory[paymentIndex].amount, + note: paymentData.note !== undefined ? paymentData.note : paymentHistory[paymentIndex].note + }; + + // 更新 lastPaymentDate 为最新的支付记录日期 + const sortedPayments = [...paymentHistory].sort((a, b) => new Date(b.date) - new Date(a.date)); + const newLastPaymentDate = sortedPayments[0].date; + + subscriptions[index] = { + ...subscription, + paymentHistory, + lastPaymentDate: newLastPaymentDate + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: subscriptions[index], message: '支付记录已更新' }; + } catch (error) { + console.error('更新支付记录失败:', error); + return { success: false, message: '更新失败: ' + error.message }; + } +} + +async function toggleSubscriptionStatus(id, isActive, env) { + try { + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === id); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + subscriptions[index] = { + ...subscriptions[index], + isActive: isActive, + updatedAt: new Date().toISOString() + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { success: true, subscription: subscriptions[index] }; + } catch (error) { + return { success: false, message: '更新订阅状态失败' }; + } +} + +async function testSingleSubscriptionNotification(id, env) { + try { + const subscription = await getSubscription(id, env); + if (!subscription) { + return { success: false, message: '未找到该订阅' }; + } + const config = await getConfig(env); + + const title = `手动测试通知: ${subscription.name}`; + + // 检查是否显示农历(从配置中获取,默认不显示) + const showLunar = config.SHOW_LUNAR === true; + let lunarExpiryText = ''; + + if (showLunar) { + // 计算农历日期 + const expiryDateObj = new Date(subscription.expiryDate); + const lunarExpiry = lunarCalendar.solar2lunar(expiryDateObj.getFullYear(), expiryDateObj.getMonth() + 1, expiryDateObj.getDate()); + lunarExpiryText = lunarExpiry ? ` (农历: ${lunarExpiry.fullStr})` : ''; + } + + // 格式化到期日期(使用所选时区) + const timezone = config?.TIMEZONE || 'UTC'; + const formattedExpiryDate = formatTimeInTimezone(new Date(subscription.expiryDate), timezone, 'datetime'); + const currentTime = formatTimeInTimezone(new Date(), timezone, 'datetime'); + + // 获取日历类型和自动续期状态 + const calendarType = subscription.useLunar ? '农历' : '公历'; + const autoRenewText = subscription.autoRenew ? '是' : '否'; + const amountText = subscription.amount ? `\n金额: ¥${subscription.amount.toFixed(2)}/周期` : ''; + + const commonContent = `**订阅详情** +类型: ${subscription.customType || '其他'}${amountText} +日历类型: ${calendarType} +到期日期: ${formattedExpiryDate}${lunarExpiryText} +自动续期: ${autoRenewText} +备注: ${subscription.notes || '无'} +发送时间: ${currentTime} +当前时区: ${formatTimezoneDisplay(timezone)}`; + + // 使用多渠道发送 + const tags = extractTagsFromSubscriptions([subscription]); + await sendNotificationToAllChannels(title, commonContent, config, '[手动测试]', { + metadata: { tags }, + notifiers: subscription.enabledNotifiers, + emailTo: subscription.emailTo + }); + + return { success: true, message: '测试通知已发送到所有启用的渠道' }; + + } catch (error) { + console.error('[手动测试] 发送失败:', error); + return { success: false, message: '发送时发生错误: ' + error.message }; + } +} + +async function sendWebhookNotification(title, content, config, metadata = {}) { + try { + if (!config.WEBHOOK_URL) { + console.error('[Webhook通知] 通知未配置,缺少URL'); + return false; + } + + console.log('[Webhook通知] 开始发送通知到: ' + config.WEBHOOK_URL); + + let requestBody; + let headers = { 'Content-Type': 'application/json' }; + + // 处理自定义请求头 + if (config.WEBHOOK_HEADERS) { + try { + const customHeaders = JSON.parse(config.WEBHOOK_HEADERS); + headers = { ...headers, ...customHeaders }; + } catch (error) { + console.warn('[Webhook通知] 自定义请求头格式错误,使用默认请求头'); + } + } + + const tagsArray = Array.isArray(metadata.tags) + ? metadata.tags.filter(tag => typeof tag === 'string' && tag.trim().length > 0).map(tag => tag.trim()) + : []; + const tagsBlock = tagsArray.length ? tagsArray.map(tag => `- ${tag}`).join('\n') : ''; + const tagsLine = tagsArray.length ? '标签:' + tagsArray.join('、') : ''; + const timestamp = formatTimeInTimezone(new Date(), config?.TIMEZONE || 'UTC', 'datetime'); + const formattedMessage = [title, content, tagsLine, `发送时间:${timestamp}`] + .filter(section => section && section.trim().length > 0) + .join('\n\n'); + + const templateData = { + title, + content, + tags: tagsBlock, + tagsLine, + rawTags: tagsArray, + timestamp, + formattedMessage, + message: formattedMessage + }; + + const escapeForJson = (value) => { + if (value === null || value === undefined) { + return ''; + } + return JSON.stringify(String(value)).slice(1, -1); + }; + + const applyTemplate = (template, data) => { + const templateString = JSON.stringify(template); + const replaced = templateString.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => { + if (Object.prototype.hasOwnProperty.call(data, key)) { + return escapeForJson(data[key]); + } + return ''; + }); + return JSON.parse(replaced); + }; + + // 处理消息模板 + if (config.WEBHOOK_TEMPLATE) { + try { + const template = JSON.parse(config.WEBHOOK_TEMPLATE); + requestBody = applyTemplate(template, templateData); + } catch (error) { + console.warn('[Webhook通知] 消息模板格式错误,使用默认格式'); + requestBody = { + title, + content, + tags: tagsArray, + tagsLine, + timestamp, + message: formattedMessage + }; + } + } else { + requestBody = { + title, + content, + tags: tagsArray, + tagsLine, + timestamp, + message: formattedMessage + }; + } + + const response = await fetch(config.WEBHOOK_URL, { + method: config.WEBHOOK_METHOD || 'POST', + headers: headers, + body: JSON.stringify(requestBody) + }); + + const result = await response.text(); + console.log('[Webhook通知] 发送结果:', response.status, result); + return response.ok; + } catch (error) { + console.error('[Webhook通知] 发送通知失败:', error); + return false; + } +} + +async function sendWechatBotNotification(title, content, config) { + try { + if (!config.WECHATBOT_WEBHOOK) { + console.error('[企业微信机器人] 通知未配置,缺少Webhook URL'); + return false; + } + + console.log('[企业微信机器人] 开始发送通知到: ' + config.WECHATBOT_WEBHOOK); + + // 构建消息内容 + let messageData; + const msgType = config.WECHATBOT_MSG_TYPE || 'text'; + + if (msgType === 'markdown') { + // Markdown 消息格式 + const markdownContent = `# ${title}\n\n${content}`; + messageData = { + msgtype: 'markdown', + markdown: { + content: markdownContent + } + }; + } else { + // 文本消息格式 - 优化显示 + const textContent = `${title}\n\n${content}`; + messageData = { + msgtype: 'text', + text: { + content: textContent + } + }; + } + + // 处理@功能 + if (config.WECHATBOT_AT_ALL === 'true') { + // @所有人 + if (msgType === 'text') { + messageData.text.mentioned_list = ['@all']; + } + } else if (config.WECHATBOT_AT_MOBILES) { + // @指定手机号 + const mobiles = config.WECHATBOT_AT_MOBILES.split(',').map(m => m.trim()).filter(m => m); + if (mobiles.length > 0) { + if (msgType === 'text') { + messageData.text.mentioned_mobile_list = mobiles; + } + } + } + + console.log('[企业微信机器人] 发送消息数据:', JSON.stringify(messageData, null, 2)); + + const response = await fetch(config.WECHATBOT_WEBHOOK, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(messageData) + }); + + const responseText = await response.text(); + console.log('[企业微信机器人] 响应状态:', response.status); + console.log('[企业微信机器人] 响应内容:', responseText); + + if (response.ok) { + try { + const result = JSON.parse(responseText); + if (result.errcode === 0) { + console.log('[企业微信机器人] 通知发送成功'); + return true; + } else { + console.error('[企业微信机器人] 发送失败,错误码:', result.errcode, '错误信息:', result.errmsg); + return false; + } + } catch (parseError) { + console.error('[企业微信机器人] 解析响应失败:', parseError); + return false; + } + } else { + console.error('[企业微信机器人] HTTP请求失败,状态码:', response.status); + return false; + } + } catch (error) { + console.error('[企业微信机器人] 发送通知失败:', error); + return false; + } +} + +// 优化通知内容格式 +function resolveReminderSetting(subscription) { + const defaultDays = subscription && subscription.reminderDays !== undefined ? Number(subscription.reminderDays) : 7; + let unit = subscription && subscription.reminderUnit ? subscription.reminderUnit : 'day'; + + // 兼容旧数据:如果没有 reminderUnit 但有 reminderHours,则推断为 hour + if (!subscription.reminderUnit && subscription.reminderHours !== undefined) { + unit = 'hour'; + } + + let value; + if (unit === 'minute' || unit === 'hour') { + if (subscription && subscription.reminderValue !== undefined && subscription.reminderValue !== null && !isNaN(Number(subscription.reminderValue))) { + value = Number(subscription.reminderValue); + } else if (subscription && subscription.reminderHours !== undefined && subscription.reminderHours !== null && !isNaN(Number(subscription.reminderHours))) { + value = Number(subscription.reminderHours); + } else { + value = 0; + } + } else { + if (subscription && subscription.reminderValue !== undefined && subscription.reminderValue !== null && !isNaN(Number(subscription.reminderValue))) { + value = Number(subscription.reminderValue); + } else if (!isNaN(defaultDays)) { + value = Number(defaultDays); + } else { + value = 7; + } + } + + if (value < 0 || isNaN(value)) { + value = 0; + } + let subscriptionName = subscription.name + return { unit, value, subscriptionName }; +} + +function shouldTriggerReminder(reminder, daysDiff, hoursDiff, minutesDiff) { + console.log('shouldTriggerReminder', reminder, daysDiff, hoursDiff.toFixed(2), minutesDiff.toFixed(2)) + if (!reminder) { + return false; + } + // Cloudflare Cron 容错窗口:允许 1 分钟的延迟 + const CRON_TOLERANCE_MINUTES = 1; + if (reminder.unit === 'minute') { + if (reminder.value === 0) { + // 到期时提醒:允许在到期前1分钟到到期后x分钟内触发 + return minutesDiff >= -CRON_TOLERANCE_MINUTES && minutesDiff <= 0; + } + // 提前X分钟提醒:允许在目标时间前后x分钟内触发 + return minutesDiff >= -CRON_TOLERANCE_MINUTES && minutesDiff <= reminder.value; + } + if (reminder.unit === 'hour') { + if (reminder.value === 0) { + // 到期时提醒:允许在到期前1小时到到期后x分钟内触发 + return minutesDiff >= -CRON_TOLERANCE_MINUTES && hoursDiff <= 0; + } + // 提前X小时提醒:允许在目标时间后x分钟内触发 + return minutesDiff >= -CRON_TOLERANCE_MINUTES && hoursDiff <= reminder.value; + } + if (reminder.value === 0) { + // 到期当天提醒:允许在到期后x分钟内触发 + return daysDiff === 0 || (daysDiff === -1 && minutesDiff >= -CRON_TOLERANCE_MINUTES && minutesDiff < 0); + } + return daysDiff >= 0 && daysDiff <= reminder.value; +} + +const NOTIFIER_KEYS = ['telegram', 'notifyx', 'webhook', 'wechatbot', 'email_smtp', 'email_resend', 'email_mailgun', 'bark']; + +function normalizeNotificationHours(raw) { + const values = Array.isArray(raw) + ? raw + : typeof raw === 'string' + ? raw.split(/[,,\s]+/) + : []; + + return values + .map(value => String(value).trim()) + .filter(value => value.length > 0) + .map(value => { + const upperValue = value.toUpperCase(); + if (upperValue === '*' || upperValue === 'ALL') { + return '*'; + } + // 支持 HH:MM 格式(如 08:30, 12:15) + if (value.includes(':')) { + const parts = value.split(':'); + if (parts.length === 2) { + const hour = parseInt(parts[0]); + const minute = parseInt(parts[1]); + if (!isNaN(hour) && !isNaN(minute) && hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) { + return String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0'); + } + } + } + // 仅小时格式(如 08, 12, 20) + const numeric = Number(upperValue); + if (!isNaN(numeric)) { + return String(Math.max(0, Math.min(23, Math.floor(numeric)))).padStart(2, '0'); + } + return upperValue; + }); +} + +function normalizeNotifierList(raw) { + const values = Array.isArray(raw) + ? raw + : typeof raw === 'string' + ? raw.split(/[,,\s]+/) + : []; + + return values + .map(value => { + const normalized = String(value).trim().toLowerCase(); + if (normalized === 'email' || normalized === 'resend' || normalized === 'email_resend') { + return 'email_resend'; + } + if (normalized === 'mailgun' || normalized === 'email_mailgun') { + return 'email_mailgun'; + } + if (normalized === 'smtp' || normalized === 'email_smtp') { + return 'email_smtp'; + } + return normalized; + }) + .filter(value => value.length > 0) + .filter(value => NOTIFIER_KEYS.includes(value)); +} + +function normalizeEmailRecipients(raw) { + const values = Array.isArray(raw) + ? raw + : typeof raw === 'string' + ? raw.split(/[,,\s]+/) + : []; + + return values + .map(value => String(value).trim()) + .filter(value => value.length > 0); +} + +function resolveEmailProvider(provider) { + const normalized = String(provider || '').toLowerCase(); + if (normalized === 'smtp') { + return 'smtp'; + } + if (normalized === 'mailgun' || normalized === 'email_mailgun') { + return 'mailgun'; + } + return 'resend'; +} + +function resolveEmailConfigForProvider(provider, config) { + const legacyFrom = config?.EMAIL_FROM || ''; + const legacyFromName = config?.EMAIL_FROM_NAME || ''; + const legacyTo = config?.EMAIL_TO || ''; + if (provider === 'smtp') { + return { + apiKey: '', + from: config?.SMTP_FROM || legacyFrom || config?.SMTP_USER || '', + fromName: config?.SMTP_FROM_NAME || legacyFromName || '', + to: config?.SMTP_TO || legacyTo + }; + } + if (provider === 'mailgun') { + return { + apiKey: config?.MAILGUN_API_KEY || '', + from: config?.MAILGUN_FROM || legacyFrom || '', + fromName: config?.MAILGUN_FROM_NAME || legacyFromName || '', + to: config?.MAILGUN_TO || legacyTo + }; + } + return { + apiKey: config?.RESEND_API_KEY || '', + from: config?.RESEND_FROM || legacyFrom || '', + fromName: config?.RESEND_FROM_NAME || legacyFromName || '', + to: config?.RESEND_TO || legacyTo + }; +} + +function formatEmailFrom(address, name) { + const trimmedAddress = String(address || '').trim(); + const trimmedName = String(name || '').trim(); + if (!trimmedAddress) { + return ''; + } + return trimmedName ? `${trimmedName} <${trimmedAddress}>` : trimmedAddress; +} + +function extractEmailAddress(value) { + const raw = String(value || '').trim(); + const match = raw.match(/<([^>]+)>/); + return (match ? match[1] : raw).trim(); +} + +function extractEmailDomain(value) { + const address = extractEmailAddress(value); + const atIndex = address.lastIndexOf('@'); + return atIndex !== -1 ? address.slice(atIndex + 1).trim() : ''; +} + +function resolveSubscriptionNotificationHours(subscription, config) { + if (subscription && Object.prototype.hasOwnProperty.call(subscription, 'notificationHours')) { + return normalizeNotificationHours(subscription.notificationHours); + } + return normalizeNotificationHours(config?.NOTIFICATION_HOURS); +} + +function resolveSubscriptionNotifiers(subscription, config) { + if (subscription && Object.prototype.hasOwnProperty.call(subscription, 'enabledNotifiers')) { + return normalizeNotifierList(subscription.enabledNotifiers); + } + const fallback = normalizeNotifierList(config?.ENABLED_NOTIFIERS); + return fallback.length ? fallback : NOTIFIER_KEYS.slice(); +} + +function resolveSubscriptionEmailRecipients(subscription) { + return normalizeEmailRecipients(subscription?.emailTo); +} + +function shouldNotifyAtCurrentHour(notificationHours, currentHour, currentMinute) { + const normalized = normalizeNotificationHours(notificationHours); + if (normalized.length === 0 || normalized.includes('*')) { + return true; + } + + // 格式化当前时间为 HH:MM + const currentTimeStr = String(currentHour).padStart(2, '0') + ':' + String(currentMinute).padStart(2, '0'); + const currentHourStr = String(currentHour).padStart(2, '0'); + + // 检查是否匹配 + for (const time of normalized) { + if (time.includes(':')) { + // 对于 HH:MM 格式,允许在同一分钟内触发(考虑到定时任务可能不是精确在该分钟的0秒执行) + // 比如设置了 13:48,那么 13:48:00 到 13:48:59 都应该允许 + const [targetHour, targetMinute] = time.split(':').map(v => parseInt(v)); + const currentHourInt = parseInt(currentHour); + const currentMinuteInt = parseInt(currentMinute); + + if (targetHour === currentHourInt && targetMinute === currentMinuteInt) { + return true; + } + } else { + // 仅匹配小时(整个小时内都允许) + if (time === currentHourStr) { + return true; + } + } + } + + return false; +} + +function formatNotificationContent(subscriptions, config) { + const showLunar = config.SHOW_LUNAR === true; + const timezone = config?.TIMEZONE || 'UTC'; + let content = ''; + + for (const sub of subscriptions) { + const typeText = sub.customType || '其他'; + const periodText = (sub.periodValue && sub.periodUnit) ? `(周期: ${sub.periodValue} ${{ minute: '分钟', hour: '小时', day: '天', month: '月', year: '年' }[sub.periodUnit] || sub.periodUnit})` : ''; + const categoryText = sub.category ? sub.category : '未分类'; + const reminderSetting = resolveReminderSetting(sub); + + // 格式化到期日期(使用所选时区) + const expiryDateObj = new Date(sub.expiryDate); + const formattedExpiryDate = formatTimeInTimezone(expiryDateObj, timezone, 'datetime'); + + // 农历日期 + let lunarExpiryText = ''; + if (showLunar) { + const lunarExpiry = lunarCalendar.solar2lunar(expiryDateObj.getFullYear(), expiryDateObj.getMonth() + 1, expiryDateObj.getDate()); + lunarExpiryText = lunarExpiry ? ` +农历日期: ${lunarExpiry.fullStr}` : ''; + } + + // 状态和到期时间 + let statusText = ''; + let statusEmoji = ''; + + // 根据订阅的周期单位选择合适的显示方式 + if (sub.periodUnit === 'minute') { + // 分钟级订阅:显示分钟 + const minutesRemaining = sub.minutesRemaining !== undefined ? sub.minutesRemaining : (sub.hoursRemaining ? sub.hoursRemaining * 60 : sub.daysRemaining * 24 * 60); + if (Math.abs(minutesRemaining) < 1) { + statusEmoji = '⚠️'; + statusText = '即将到期!'; + } else if (minutesRemaining < 0) { + statusEmoji = '🚨'; + statusText = `已过期 ${Math.abs(Math.round(minutesRemaining))} 分钟`; + } else { + statusEmoji = '📅'; + statusText = `将在 ${Math.round(minutesRemaining)} 分钟后到期`; + } + } else if (sub.periodUnit === 'hour') { + // 小时级订阅:显示小时 + const hoursRemaining = sub.hoursRemaining !== undefined ? sub.hoursRemaining : sub.daysRemaining * 24; + if (Math.abs(hoursRemaining) < 1) { + statusEmoji = '⚠️'; + statusText = '即将到期!'; + } else if (hoursRemaining < 0) { + statusEmoji = '🚨'; + statusText = `已过期 ${Math.abs(Math.round(hoursRemaining))} 小时`; + } else { + statusEmoji = '📅'; + statusText = `将在 ${Math.round(hoursRemaining)} 小时后到期`; + } + } else { + // 天级订阅:显示天数 + if (sub.daysRemaining === 0) { + statusEmoji = '⚠️'; + statusText = '今天到期!'; + } else if (sub.daysRemaining < 0) { + statusEmoji = '🚨'; + statusText = `已过期 ${Math.abs(sub.daysRemaining)} 天`; + } else { + statusEmoji = '📅'; + statusText = `将在 ${sub.daysRemaining} 天后到期`; + } + } + + const reminderSuffix = reminderSetting.value === 0 + ? '(仅到期时提醒)' + : reminderSetting.unit === 'minute' + ? '(分钟级提醒)' + : reminderSetting.unit === 'hour' + ? '(小时级提醒)' + : ''; + + const reminderText = reminderSetting.unit === 'minute' + ? `提醒策略: 提前 ${reminderSetting.value} 分钟${reminderSuffix}` + : reminderSetting.unit === 'hour' + ? `提醒策略: 提前 ${reminderSetting.value} 小时${reminderSuffix}` + : `提醒策略: 提前 ${reminderSetting.value} 天${reminderSuffix}`; + + // 获取日历类型和自动续期状态 + const calendarType = sub.useLunar ? '农历' : '公历'; + const autoRenewText = sub.autoRenew ? '是' : '否'; + const amountText = sub.amount ? `\n金额: ¥${sub.amount.toFixed(2)}/周期` : ''; + + // 构建格式化的通知内容 + const subscriptionContent = `${statusEmoji} **${sub.name}** +类型: ${typeText} ${periodText} +分类: ${categoryText}${amountText} +日历类型: ${calendarType} +到期日期: ${formattedExpiryDate}${lunarExpiryText} +自动续期: ${autoRenewText} +${reminderText} +到期状态: ${statusText}`; + + // 添加备注 + let finalContent = sub.notes ? + subscriptionContent + `\n备注: ${sub.notes}` : + subscriptionContent; + + content += finalContent + '\n\n'; + } + + // 添加发送时间和时区信息 + const currentTime = formatTimeInTimezone(new Date(), timezone, 'datetime'); + content += `发送时间: ${currentTime}\n当前时区: ${formatTimezoneDisplay(timezone)}`; + + return content; +} + +async function sendNotificationToAllChannels(title, commonContent, config, logPrefix = '[定时任务]', options = {}) { + const metadata = options.metadata || {}; + const requestedNotifiers = normalizeNotifierList(options.notifiers); + const globalNotifiers = normalizeNotifierList(config.ENABLED_NOTIFIERS); + const baseNotifiers = requestedNotifiers.length ? requestedNotifiers : globalNotifiers; + const effectiveNotifiers = globalNotifiers.length + ? baseNotifiers.filter(item => globalNotifiers.includes(item)) + : baseNotifiers; + + if (!effectiveNotifiers || effectiveNotifiers.length === 0) { + console.log(`${logPrefix} 未启用任何通知渠道。`); + return; + } + + if (effectiveNotifiers.includes('notifyx')) { + const notifyxContent = `## ${title}\n\n${commonContent}`; + const success = await sendNotifyXNotification(title, notifyxContent, `订阅提醒`, config); + console.log(`${logPrefix} 发送NotifyX通知 ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('telegram')) { + const telegramContent = `*${title}*\n\n${commonContent}`; + const success = await sendTelegramNotification(telegramContent, config); + console.log(`${logPrefix} 发送Telegram通知 ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('webhook')) { + const webhookContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendWebhookNotification(title, webhookContent, config, metadata); + console.log(`${logPrefix} 发送Webhook通知 ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('wechatbot')) { + const wechatbotContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendWechatBotNotification(title, wechatbotContent, config); + console.log(`${logPrefix} 发送企业微信机器人通知 ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('email_resend')) { + const emailContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendEmailNotification(title, emailContent, config, { + provider: 'resend', + emailTo: options.emailTo + }); + console.log(`${logPrefix} 发送邮件通知(Resend) ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('email_mailgun')) { + const emailContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendEmailNotification(title, emailContent, config, { + provider: 'mailgun', + emailTo: options.emailTo + }); + console.log(`${logPrefix} 发送邮件通知(Mailgun) ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('email_smtp')) { + const emailContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendEmailNotification(title, emailContent, config, { + provider: 'smtp', + emailTo: options.emailTo + }); + console.log(`${logPrefix} 发送邮件通知(SMTP) ${success ? '成功' : '失败'}`); + } + if (effectiveNotifiers.includes('bark')) { + const barkContent = commonContent.replace(/(\**|\*|##|#|`)/g, ''); + const success = await sendBarkNotification(title, barkContent, config); + console.log(`${logPrefix} 发送Bark通知 ${success ? '成功' : '失败'}`); + } +} + +async function sendTelegramNotification(message, config) { + try { + if (!config.TG_BOT_TOKEN || !config.TG_CHAT_ID) { + console.error('[Telegram] 通知未配置,缺少Bot Token或Chat ID'); + return false; + } + + console.log('[Telegram] 开始发送通知到 Chat ID: ' + config.TG_CHAT_ID); + + const url = 'https://api.telegram.org/bot' + config.TG_BOT_TOKEN + '/sendMessage'; + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + chat_id: config.TG_CHAT_ID, + text: message, + parse_mode: 'Markdown' + }) + }); + + const result = await response.json(); + console.log('[Telegram] 发送结果:', result); + return result.ok; + } catch (error) { + console.error('[Telegram] 发送通知失败:', error); + return false; + } +} + +async function sendNotifyXNotification(title, content, description, config) { + try { + if (!config.NOTIFYX_API_KEY) { + console.error('[NotifyX] 通知未配置,缺少API Key'); + return false; + } + + console.log('[NotifyX] 开始发送通知: ' + title); + + const url = 'https://www.notifyx.cn/api/v1/send/' + config.NOTIFYX_API_KEY; + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: title, + content: content, + description: description || '' + }) + }); + + const result = await response.json(); + console.log('[NotifyX] 发送结果:', result); + return result.status === 'queued'; + } catch (error) { + console.error('[NotifyX] 发送通知失败:', error); + return false; + } +} + +async function sendBarkNotification(title, content, config) { + try { + if (!config.BARK_DEVICE_KEY) { + console.error('[Bark] 通知未配置,缺少设备Key'); + return false; + } + + console.log('[Bark] 开始发送通知到设备: ' + config.BARK_DEVICE_KEY); + + const serverUrl = config.BARK_SERVER || 'https://api.day.app'; + const url = serverUrl + '/push'; + const payload = { + title: title, + body: content, + device_key: config.BARK_DEVICE_KEY + }; + + // 如果配置了保存推送,则添加isArchive参数 + if (config.BARK_IS_ARCHIVE === 'true') { + payload.isArchive = 1; + } + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + console.log('[Bark] 发送结果:', result); + + // Bark API返回code为200表示成功 + return result.code === 200; + } catch (error) { + console.error('[Bark] 发送通知失败:', error); + return false; + } +} + +async function sendEmailNotification(title, content, config, options = {}) { + try { + const provider = resolveEmailProvider(options.provider); + const recipients = normalizeEmailRecipients(options.emailTo); + const providerConfig = resolveEmailConfigForProvider(provider, config); + const targetRecipients = recipients.length + ? recipients + : normalizeEmailRecipients(providerConfig.to); + const fromEmail = formatEmailFrom(providerConfig.from, providerConfig.fromName); + + // 生成HTML邮件内容 + const htmlContent = ` + + + + + + ${title} + + + +
+
+

📅 ${title}

+
+
+
+ ${content.replace(/\n/g, '
')} +
+

此邮件由订阅管理系统自动发送,请及时处理相关订阅事务。

+
+ +
+ +`; + + if (provider === 'smtp') { + console.log('[Email Notification] Using SMTP provider'); + if (targetRecipients.length === 0) { + console.error('[Email Notification] Missing SMTP recipients'); + return false; + } + return await sendSmtpEmailNotification(title, content, htmlContent, config, targetRecipients); + } + + if (provider === 'mailgun') { + if (!providerConfig.apiKey || !providerConfig.from || targetRecipients.length === 0) { + console.error('[Email Notification] Missing Mailgun config or recipients'); + return false; + } + const domain = extractEmailDomain(providerConfig.from); + if (!domain) { + console.error('[Email Notification] Unable to resolve Mailgun domain from from address'); + return false; + } + console.log('[Email Notification] Sending via Mailgun to: ' + targetRecipients.join(', ')); + const auth = btoa('api:' + providerConfig.apiKey); + const response = await fetch(`https://api.mailgun.net/v3/${domain}/messages`, { + method: 'POST', + headers: { + 'Authorization': `Basic ${auth}` + }, + body: new URLSearchParams({ + from: fromEmail, + to: targetRecipients.join(', '), + subject: title, + html: htmlContent, + text: content + }) + }); + + let result; + try { + result = await response.json(); + } catch (parseError) { + result = await response.text(); + } + console.log('[Email Notification] Mailgun response', response.status, result); + return response.ok; + } + + if (!providerConfig.apiKey || !providerConfig.from || targetRecipients.length === 0) { + console.error('[Email Notification] Missing Resend config or recipients'); + return false; + } + + console.log('[Email Notification] Sending via Resend to: ' + targetRecipients.join(', ')); + + const response = await fetch('https://api.resend.com/emails', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${providerConfig.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + from: fromEmail, + to: targetRecipients, + subject: title, + html: htmlContent, + text: content + }) + }); + + const result = await response.json(); + console.log('[Email Notification] Resend response', response.status, result); + + if (response.ok && result.id) { + console.log('[Email Notification] Resend message accepted, ID:', result.id); + return true; + } + console.error('[Email Notification] Resend send failed', result); + return false; + + } catch (error) { + console.error('[邮件通知] 发送邮件失败:', error); + return false; + } +} + +async function sendSmtpEmailNotification(title, content, htmlContent, config, recipients) { + const result = await sendSmtpEmailNotificationDetailed(title, content, htmlContent, config, recipients); + return result.success; +} + +async function sendSmtpEmailNotificationDetailed(title, content, htmlContent, config, recipients) { + try { + const smtpPort = Number(config.SMTP_PORT); + const smtpFrom = config.SMTP_FROM || config.EMAIL_FROM || config.SMTP_USER || ''; + const smtpFromName = config.SMTP_FROM_NAME || config.EMAIL_FROM_NAME || ''; + if (!config.SMTP_HOST || !smtpPort || !config.SMTP_USER || !config.SMTP_PASS || !smtpFrom || !recipients.length) { + console.error('[SMTP邮件通知] 通知未配置,缺少必要参数'); + return { success: false, message: 'Missing SMTP config or recipients' }; + } + + const fromEmail = smtpFromName ? + `${smtpFromName} <${smtpFrom}>` : + smtpFrom; + + console.log('[SMTP邮件通知] 开始发送邮件到: ' + recipients.join(', ')); + + const response = await fetch('https://smtpjs.com/v3/smtpjs.aspx', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + Host: config.SMTP_HOST, + Port: smtpPort, + Username: config.SMTP_USER, + Password: config.SMTP_PASS, + To: recipients.join(','), + From: fromEmail, + Subject: title, + Body: htmlContent, + Secure: smtpPort === 465 + }) + }); + + const resultText = await response.text(); + console.log('[SMTP邮件通知] 发送结果:', response.status, resultText); + + if (response.ok && resultText && resultText.toLowerCase().includes('ok')) { + return { success: true, message: 'OK' }; + } + console.error('[SMTP邮件通知] 发送失败:', resultText); + return { + success: false, + message: 'SMTPJS response: ' + (resultText || 'empty response'), + status: response.status + }; + } catch (error) { + console.error('[SMTP邮件通知] 发送邮件失败:', error); + return { success: false, message: error.message || 'SMTP send error' }; + } +} + +async function sendNotification(title, content, description, config) { + if (config.NOTIFICATION_TYPE === 'notifyx') { + return await sendNotifyXNotification(title, content, description, config); + } else { + return await sendTelegramNotification(content, config); + } +} + +// 4. 修改定时任务 checkExpiringSubscriptions,支持农历周期自动续订和农历提醒 +async function checkExpiringSubscriptions(env) { + try { + const config = await getConfig(env); + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + + // 统一计算当天的零点时间,用于比较天数差异 + const currentMidnight = getTimezoneMidnightTimestamp(currentTime, timezone); + + console.log(`[定时任务] 开始检查 - 当前时间: ${currentTime.toISOString()} (${timezone})`); + + // --- 检查当前小时和分钟是否允许发送通知 --- + const timeFormatter = new Intl.DateTimeFormat('en-US', { + timeZone: timezone, + hour12: false, + hour: '2-digit', + minute: '2-digit' + }); + const timeParts = timeFormatter.formatToParts(currentTime); + const currentHour = timeParts.find(p => p.type === 'hour')?.value || '00'; + const currentMinute = timeParts.find(p => p.type === 'minute')?.value || '00'; + + const subscriptions = await getAllSubscriptions(env); + const expiringSubscriptions = []; + const updatedSubscriptions = []; + let hasUpdates = false; + + for (const subscription of subscriptions) { + // 1. 跳过未启用的订阅 + if (subscription.isActive === false) { + continue; + } + + const reminderSetting = resolveReminderSetting(subscription); + const subscriptionNotificationHours = resolveSubscriptionNotificationHours(subscription, config); + const shouldNotifyThisHour = shouldNotifyAtCurrentHour(subscriptionNotificationHours, currentHour, currentMinute); + + // 计算当前剩余时间(基础计算) + let expiryDate = new Date(subscription.expiryDate); + + // 为了准确计算 daysDiff,需要根据农历或公历获取"逻辑上的午夜时间" + let expiryMidnight; + if (subscription.useLunar) { + const lunar = lunarCalendar.solar2lunar(expiryDate.getFullYear(), expiryDate.getMonth() + 1, expiryDate.getDate()); + if (lunar) { + const solar = lunarBiz.lunar2solar(lunar); + const lunarDate = new Date(solar.year, solar.month - 1, solar.day); + expiryMidnight = getTimezoneMidnightTimestamp(lunarDate, timezone); + } else { + expiryMidnight = getTimezoneMidnightTimestamp(expiryDate, timezone); + } + } else { + expiryMidnight = getTimezoneMidnightTimestamp(expiryDate, timezone); + } + + let daysDiff = Math.round((expiryMidnight - currentMidnight) / MS_PER_DAY); + // 直接计算时间差(expiryDate 和 currentTime 都是 UTC 时间戳) + let diffMs = expiryDate.getTime() - currentTime.getTime(); + let diffHours = diffMs / MS_PER_HOUR; + let diffMinutes = diffMs / (1000 * 60); + + // ========================================== + // 核心逻辑:自动续费处理 + // ========================================== + // 修复:对于分钟级和小时级订阅,使用精确的时间差判断是否过期 + let isExpiredForRenewal = false; + if (subscription.periodUnit === 'minute' || subscription.periodUnit === 'hour') { + isExpiredForRenewal = diffMs < 0; // 使用精确的毫秒差 + } else { + isExpiredForRenewal = daysDiff < 0; // 使用天数差 + } + + if (isExpiredForRenewal && subscription.periodValue && subscription.periodUnit && subscription.autoRenew !== false) { + console.log(`[定时任务] 订阅 "${subscription.name}" 已过期,准备自动续费...`); + + const mode = subscription.subscriptionMode || 'cycle'; // cycle | reset + + // 1. 确定计算基准点 (Base Point) + // newStartDate 将作为新周期的"开始日期"保存到数据库,解决前端编辑时日期错乱问题 + let newStartDate; + + if (mode === 'reset') { + // 注意:为了整洁,通常从当天的 00:00 或当前时间开始,这里取 currentTime 保持精确 + newStartDate = new Date(currentTime); + } else { + // Cycle 模式:无缝接续,从"旧的到期日"开始 + newStartDate = new Date(subscription.expiryDate); + } + + // 2. 计算新的到期日 (循环补齐直到未来) + let newExpiryDate = new Date(newStartDate); // 初始化 + let periodsAdded = 0; + + // 定义增加一个周期的函数 (同时处理 newStartDate 和 newExpiryDate 的推进) + const addOnePeriod = (baseDate) => { + let targetDate; + if (subscription.useLunar) { + const solarBase = { year: baseDate.getFullYear(), month: baseDate.getMonth() + 1, day: baseDate.getDate() }; + let lunarBase = lunarCalendar.solar2lunar(solarBase.year, solarBase.month, solarBase.day); + // 农历加周期 + let nextLunar = lunarBiz.addLunarPeriod(lunarBase, subscription.periodValue, subscription.periodUnit); + const solarNext = lunarBiz.lunar2solar(nextLunar); + targetDate = new Date(solarNext.year, solarNext.month - 1, solarNext.day); + } else { + targetDate = new Date(baseDate); + if (subscription.periodUnit === 'minute') targetDate.setMinutes(targetDate.getMinutes() + subscription.periodValue); + else if (subscription.periodUnit === 'hour') targetDate.setHours(targetDate.getHours() + subscription.periodValue); + else if (subscription.periodUnit === 'day') targetDate.setDate(targetDate.getDate() + subscription.periodValue); + else if (subscription.periodUnit === 'month') targetDate.setMonth(targetDate.getMonth() + subscription.periodValue); + else if (subscription.periodUnit === 'year') targetDate.setFullYear(targetDate.getFullYear() + subscription.periodValue); + } + return targetDate; + }; + // Reset模式下 newStartDate 是今天,加一次肯定在未来,循环只会执行一次 + do { + // 在推进到期日之前,现有的 newExpiryDate 就变成了这一轮的"开始日" + // (仅在非第一次循环时有效,用于 Cycle 模式推进 start 日期) + if (periodsAdded > 0) { + newStartDate = new Date(newExpiryDate); + } + + // 计算下一个到期日 + newExpiryDate = addOnePeriod(newStartDate); + periodsAdded++; + + // 获取新到期日的午夜时间用于判断是否仍过期 + const newExpiryMidnight = getTimezoneMidnightTimestamp(newExpiryDate, timezone); + daysDiff = Math.round((newExpiryMidnight - currentMidnight) / MS_PER_DAY); + + } while (daysDiff < 0); // 只要还过期,就继续加 + + console.log(`[定时任务] 续费完成. 新开始日: ${newStartDate.toISOString()}, 新到期日: ${newExpiryDate.toISOString()}`); + // 3. 生成支付记录 + const paymentRecord = { + id: Date.now().toString() + '_' + Math.random().toString(36).substr(2, 9), + date: currentTime.toISOString(), // 实际扣款时间是现在 + amount: subscription.amount || 0, + type: 'auto', + note: `自动续订 (${mode === 'reset' ? '重置模式' : '接续模式'}${periodsAdded > 1 ? ', 补齐' + periodsAdded + '周期' : ''})`, + periodStart: newStartDate.toISOString(), // 记录准确的计费周期开始 + periodEnd: newExpiryDate.toISOString() + }; + + const paymentHistory = subscription.paymentHistory || []; + paymentHistory.push(paymentRecord); + // 4. 更新订阅对象 + const updatedSubscription = { + ...subscription, + startDate: newStartDate.toISOString(), + expiryDate: newExpiryDate.toISOString(), + lastPaymentDate: currentTime.toISOString(), + paymentHistory + }; + + updatedSubscriptions.push(updatedSubscription); + hasUpdates = true; + + // 5. 检查续费后是否需要立即提醒 (例如续费后只剩1天) + let diffMs1 = newExpiryDate.getTime() - currentTime.getTime(); + let diffHours1 = diffMs1 / MS_PER_HOUR; + let diffMinutes1 = diffMs1 / (1000 * 60); + const shouldRemindAfterRenewal = shouldTriggerReminder(reminderSetting, daysDiff, diffHours1, diffMinutes1); + + if (shouldRemindAfterRenewal && shouldNotifyThisHour) { + expiringSubscriptions.push({ + ...updatedSubscription, + daysRemaining: daysDiff, + hoursRemaining: Math.round(diffHours1) + }); + } else if (shouldRemindAfterRenewal && !shouldNotifyThisHour) { + console.log(`[Scheduler] Hour ${currentHour} not in reminder hours for "${subscription.name}", skipping notification`); + } + + // continue; // 处理下一个订阅 + } + + // ========================================== + // 普通提醒逻辑 (未过期,或过期但不自动续费) + // ========================================== + const shouldRemind = shouldTriggerReminder(reminderSetting, daysDiff, diffHours, diffMinutes); + + // 格式化剩余时间显示 + let remainingTimeStr; + if (daysDiff >= 1) { + remainingTimeStr = `${daysDiff}天`; + } else if (diffHours >= 1) { + remainingTimeStr = `${diffHours.toFixed(2)}小时`; + } else { + remainingTimeStr = `${diffMinutes.toFixed(2)}分钟`; + } + + console.log(`[定时任务] ${subscription.name} | 当前时间: ${currentHour}:${currentMinute} | 通知时间点: ${JSON.stringify(subscriptionNotificationHours)} | 时间匹配: ${shouldNotifyThisHour} | 提醒判断: ${shouldRemind} | 剩余: ${remainingTimeStr}`); + + // 修复:对于分钟级和小时级订阅,使用精确的时间差判断是否过期 + let isExpiredForNotification = false; + if (subscription.periodUnit === 'minute' || subscription.periodUnit === 'hour') { + isExpiredForNotification = diffMs < 0; + } else { + isExpiredForNotification = daysDiff < 0; + } + + if (isExpiredForNotification && subscription.autoRenew === false) { + // 已过期且不自动续费 -> 发送过期通知 + if (shouldNotifyThisHour) { + expiringSubscriptions.push({ + ...subscription, + daysRemaining: daysDiff, + hoursRemaining: Math.round(diffHours), + minutesRemaining: Math.round(diffMinutes) + }); + } else { + console.log(`[Scheduler] Hour ${currentHour} not in reminder hours for "${subscription.name}", skipping notification`); + } + } else if (shouldRemind) { + // 正常到期提醒 + if (shouldNotifyThisHour) { + expiringSubscriptions.push({ + ...subscription, + daysRemaining: daysDiff, + hoursRemaining: Math.round(diffHours), + minutesRemaining: Math.round(diffMinutes) + }); + } else { + console.log(`[Scheduler] Hour ${currentHour} not in reminder hours for "${subscription.name}", skipping notification`); + } + } + } + + // --- 保存更改 --- + if (hasUpdates) { + const mergedSubscriptions = subscriptions.map(sub => { + const updated = updatedSubscriptions.find(u => u.id === sub.id); + return updated || sub; + }); + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(mergedSubscriptions)); + console.log(`[定时任务] 已更新 ${updatedSubscriptions.length} 个自动续费订阅`); + } + + // --- 发送通知 --- + if (expiringSubscriptions.length > 0) { + console.log(`[Scheduler] Sending ${expiringSubscriptions.length} reminder notification(s)`); + // Sort by due time + expiringSubscriptions.sort((a, b) => a.daysRemaining - b.daysRemaining); + + for (const subscription of expiringSubscriptions) { + const commonContent = formatNotificationContent([subscription], config); + const metadataTags = extractTagsFromSubscriptions([subscription]); + await sendNotificationToAllChannels(`Subscription reminder: ${subscription.name}`, commonContent, config, '[Scheduler]', { + metadata: { tags: metadataTags }, + notifiers: resolveSubscriptionNotifiers(subscription, config), + emailTo: resolveSubscriptionEmailRecipients(subscription) + }); + } + } + } catch (error) { + console.error('[定时任务] 执行失败:', error); + } +} + +function getCookieValue(cookieString, key) { + if (!cookieString) return null; + + const match = cookieString.match(new RegExp('(^| )' + key + '=([^;]+)')); + return match ? match[2] : null; +} + +async function handleRequest(request, env, ctx) { + return new Response(loginPage, { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }); +} + +const CryptoJS = { + HmacSHA256: function (message, key) { + const keyData = new TextEncoder().encode(key); + const messageData = new TextEncoder().encode(message); + + return Promise.resolve().then(() => { + return crypto.subtle.importKey( + "raw", + keyData, + { name: "HMAC", hash: { name: "SHA-256" } }, + false, + ["sign"] + ); + }).then(cryptoKey => { + return crypto.subtle.sign( + "HMAC", + cryptoKey, + messageData + ); + }).then(buffer => { + const hashArray = Array.from(new Uint8Array(buffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + }); + } +}; + +function getCurrentTime(config) { + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + const formatter = new Intl.DateTimeFormat('zh-CN', { + timeZone: timezone, + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit' + }); + return { + date: currentTime, + localString: formatter.format(currentTime), + isoString: currentTime.toISOString() + }; +} + +export default { + async fetch(request, env, ctx) { + const url = new URL(request.url); + + // 添加调试页面 + if (url.pathname === '/debug') { + try { + const config = await getConfig(env); + const debugInfo = { + timestamp: new Date().toISOString(), // 使用UTC时间戳 + pathname: url.pathname, + kvBinding: !!env.SUBSCRIPTIONS_KV, + configExists: !!config, + adminUsername: config.ADMIN_USERNAME, + hasJwtSecret: !!config.JWT_SECRET, + jwtSecretLength: config.JWT_SECRET ? config.JWT_SECRET.length : 0 + }; + + return new Response(` + + + + 调试信息 + + + +

系统调试信息

+
+

基本信息

+

时间: ${debugInfo.timestamp}

+

路径: ${debugInfo.pathname}

+

KV绑定: ${debugInfo.kvBinding ? '✓' : '✗'}

+
+ +
+

配置信息

+

配置存在: ${debugInfo.configExists ? '✓' : '✗'}

+

管理员用户名: ${debugInfo.adminUsername}

+

JWT密钥: ${debugInfo.hasJwtSecret ? '✓' : '✗'} (长度: ${debugInfo.jwtSecretLength})

+
+ +
+

解决方案

+

1. 确保KV命名空间已正确绑定为 SUBSCRIPTIONS_KV

+

2. 尝试访问 / 进行登录

+

3. 如果仍有问题,请检查Cloudflare Workers日志

+
+ +`, { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }); + } catch (error) { + return new Response(`调试页面错误: ${error.message}`, { + status: 500, + headers: { 'Content-Type': 'text/plain; charset=utf-8' } + }); + } + } + + if (url.pathname.startsWith('/api')) { + return api.handleRequest(request, env, ctx); + } else if (url.pathname.startsWith('/admin')) { + return admin.handleRequest(request, env, ctx); + } else { + return handleRequest(request, env, ctx); + } + }, + + async scheduled(event, env, ctx) { + const config = await getConfig(env); + const timezone = config?.TIMEZONE || 'UTC'; + const currentTime = getCurrentTimeInTimezone(timezone); + console.log('[Workers] 定时任务触发 UTC:', new Date().toISOString(), timezone + ':', currentTime.toLocaleString('zh-CN', { timeZone: timezone })); + await checkExpiringSubscriptions(env); + } +}; +// ==================== 仪表盘统计函数 ==================== +// 汇率配置 (以 CNY 为基准,当 API 不可用或缺少特定币种如 TWD 时使用,属于兜底汇率) +// 您可以根据需要修改此处的汇率 +const FALLBACK_RATES = { + 'CNY': 1, + 'USD': 6.98, + 'HKD': 0.90, + 'TWD': 0.22, + 'JPY': 0.044, + 'EUR': 8.16, + 'GBP': 9.40, + 'KRW': 0.0048, + 'TRY': 0.16 +}; +// 获取动态汇率 (核心逻辑:KV缓存 -> API请求 -> 兜底合并) +async function getDynamicRates(env) { + const CACHE_KEY = 'SYSTEM_EXCHANGE_RATES'; + const CACHE_TTL = 86400000; // 24小时 (毫秒) + + try { + const cached = await env.SUBSCRIPTIONS_KV.get(CACHE_KEY, { type: 'json' }); // A. 尝试从 KV 读取缓存 + if (cached && cached.ts && (Date.now() - cached.ts < CACHE_TTL)) { + return cached.rates; // console.log('[汇率] 使用 KV 缓存'); + } + const response = await fetch('https://api.frankfurter.dev/v1/latest?base=CNY'); // B. 缓存失效或不存在,请求 Frankfurter API + if (response.ok) { + const data = await response.json(); + const newRates = { // C. 合并逻辑:以 API 数据覆盖兜底数据 (保留 API 没有的币种,如 TWD) + ...FALLBACK_RATES, + ...data.rates, + 'CNY': 1 + }; + + await env.SUBSCRIPTIONS_KV.put(CACHE_KEY, JSON.stringify({ // D. 写入 KV 缓存 + ts: Date.now(), + rates: newRates + })); + + return newRates; + } else { + console.warn('[汇率] API 请求失败,使用兜底汇率'); + } + } catch (error) { + console.error('[汇率] 获取过程出错:', error); + } + return FALLBACK_RATES; // E. 发生任何错误,返回兜底汇率 +} +// 辅助函数:将金额转换为基准货币 (CNY) +function convertToCNY(amount, currency, rates) { + if (!amount || amount <= 0) return 0; + + const code = currency || 'CNY'; + if (code === 'CNY') return amount; // 如果是基准货币,直接返回 + const rate = rates[code]; // 获取汇率 + if (!rate) return amount; // 如果没有汇率,原样返回(或者你可以选择抛出错误/返回0) + return amount / rate; +} +// 修改函数签名,增加 rates 参数 +function calculateMonthlyExpense(subscriptions, timezone, rates) { + const now = getCurrentTimeInTimezone(timezone); + const parts = getTimezoneDateParts(now, timezone); + const currentYear = parts.year; + const currentMonth = parts.month; + + let amount = 0; + + // 遍历所有订阅的支付历史 + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + const paymentParts = getTimezoneDateParts(paymentDate, timezone); + if (paymentParts.year === currentYear && paymentParts.month === currentMonth) { + amount += convertToCNY(payment.amount, sub.currency, rates); // 传入 rates 参数 + } + }); + }); + // 计算上月数据用于趋势对比 + const lastMonth = currentMonth === 1 ? 12 : currentMonth - 1; + const lastMonthYear = currentMonth === 1 ? currentYear - 1 : currentYear; + let lastMonthAmount = 0; + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + const paymentParts = getTimezoneDateParts(paymentDate, timezone); + if (paymentParts.year === lastMonthYear && paymentParts.month === lastMonth) { + lastMonthAmount += convertToCNY(payment.amount, sub.currency, rates); // 使用 convertToCNY 进行汇率转换 + } + }); + }); + + let trend = 0; + let trendDirection = 'flat'; + if (lastMonthAmount > 0) { + trend = Math.round(((amount - lastMonthAmount) / lastMonthAmount) * 100); + if (trend > 0) trendDirection = 'up'; + else if (trend < 0) trendDirection = 'down'; + } else if (amount > 0) { + trend = 100; // 上月无支出,本月有支出,视为增长 + trendDirection = 'up'; + } + return { amount, trend: Math.abs(trend), trendDirection }; +} + +function calculateYearlyExpense(subscriptions, timezone, rates) { + const now = getCurrentTimeInTimezone(timezone); + const parts = getTimezoneDateParts(now, timezone); + const currentYear = parts.year; + + let amount = 0; + // 遍历所有订阅的支付历史 + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + const paymentParts = getTimezoneDateParts(paymentDate, timezone); + if (paymentParts.year === currentYear) { + amount += convertToCNY(payment.amount, sub.currency, rates); + } + }); + }); + + const monthlyAverage = amount / parts.month; + return { amount, monthlyAverage }; +} + +function getRecentPayments(subscriptions, timezone) { + const now = getCurrentTimeInTimezone(timezone); + const sevenDaysAgo = new Date(now.getTime() - 7 * MS_PER_DAY); + const recentPayments = []; + // 遍历所有订阅的支付历史 + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + if (paymentDate >= sevenDaysAgo && paymentDate <= now) { + recentPayments.push({ + name: sub.name, + amount: payment.amount, + currency: sub.currency || 'CNY', // 传递币种给前端显示 + customType: sub.customType, + paymentDate: payment.date, + note: payment.note + }); + } + }); + }); + return recentPayments.sort((a, b) => new Date(b.paymentDate) - new Date(a.paymentDate)); +} + +function getUpcomingRenewals(subscriptions, timezone) { + const now = getCurrentTimeInTimezone(timezone); + const sevenDaysLater = new Date(now.getTime() + 7 * MS_PER_DAY); + return subscriptions + .filter(sub => { + if (!sub.isActive) return false; + const renewalDate = new Date(sub.expiryDate); + return renewalDate >= now && renewalDate <= sevenDaysLater; + }) + .map(sub => { + const renewalDate = new Date(sub.expiryDate); + // 修复:计算完整的天数差,使用 Math.floor() 向下取整 + // 例如:1.9天显示为"1天后",2.1天显示为"2天后" + const diffMs = renewalDate - now; + const daysUntilRenewal = Math.max(0, Math.floor(diffMs / MS_PER_DAY)); + return { + name: sub.name, + amount: sub.amount || 0, + currency: sub.currency || 'CNY', + customType: sub.customType, + renewalDate: sub.expiryDate, + daysUntilRenewal + }; + }) + .sort((a, b) => a.daysUntilRenewal - b.daysUntilRenewal); +} + +function getExpenseByType(subscriptions, timezone, rates) { + const now = getCurrentTimeInTimezone(timezone); + const parts = getTimezoneDateParts(now, timezone); + const currentYear = parts.year; + const typeMap = {}; + let total = 0; + // 遍历所有订阅的支付历史 + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + const paymentParts = getTimezoneDateParts(paymentDate, timezone); + if (paymentParts.year === currentYear) { + const type = sub.customType || '未分类'; + const amountCNY = convertToCNY(payment.amount, sub.currency, rates); + typeMap[type] = (typeMap[type] || 0) + amountCNY; + total += amountCNY; + } + }); + }); + + return Object.entries(typeMap) + .map(([type, amount]) => ({ + type, + amount, + percentage: total > 0 ? Math.round((amount / total) * 100) : 0 + })) + .sort((a, b) => b.amount - a.amount); +} + +function getExpenseByCategory(subscriptions, timezone, rates) { + const now = getCurrentTimeInTimezone(timezone); + const parts = getTimezoneDateParts(now, timezone); + const currentYear = parts.year; + + const categoryMap = {}; + let total = 0; + // 遍历所有订阅的支付历史 + subscriptions.forEach(sub => { + const paymentHistory = sub.paymentHistory || []; + paymentHistory.forEach(payment => { + if (!payment.amount || payment.amount <= 0) return; + const paymentDate = new Date(payment.date); + const paymentParts = getTimezoneDateParts(paymentDate, timezone); + if (paymentParts.year === currentYear) { + const categories = sub.category ? sub.category.split(CATEGORY_SEPARATOR_REGEX).filter(c => c.trim()) : ['未分类']; + const amountCNY = convertToCNY(payment.amount, sub.currency, rates); + + categories.forEach(category => { + const cat = category.trim() || '未分类'; + categoryMap[cat] = (categoryMap[cat] || 0) + amountCNY / categories.length; + }); + total += amountCNY; + } + }); + }); + + return Object.entries(categoryMap) + .map(([category, amount]) => ({ + category, + amount, + percentage: total > 0 ? Math.round((amount / total) * 100) : 0 + })) + .sort((a, b) => b.amount - a.amount); +} \ No newline at end of file diff --git a/wrangler.toml b/wrangler.toml index d9a246b..60c8f51 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,5 +1,5 @@ name = "subscription-manager" -main = "index.js" +main = "index_zzz.js" compatibility_date = "2024-01-01" compatibility_flags = ["nodejs_compat"] @@ -17,7 +17,7 @@ id = "your-production-kv-namespace-id" # 定时任务配置 - 每天早上8点检查 [triggers] -crons = ["0 8 * * *"] +crons = ["* * * * *"] # 环境变量(可选,也可以在 Cloudflare Dashboard 中设置) [vars] From 7d0da18a123cb3cc5976b23766fdb96897298435 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 11 Feb 2026 10:45:03 +0800 Subject: [PATCH 3/3] =?UTF-8?q?1=E3=80=81=E4=BC=98=E5=8C=96=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E9=97=AE=E9=A2=982=E3=80=81=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index_zzz.js | 216 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 203 insertions(+), 13 deletions(-) diff --git a/index_zzz.js b/index_zzz.js index 319aa0d..d331c59 100644 --- a/index_zzz.js +++ b/index_zzz.js @@ -2797,6 +2797,12 @@ const lunarBiz = { return \`
+
+ +
@@ -2854,6 +2860,22 @@ const lunarBiz = {
+
+
+ + +
+ +
+
\${paymentsHtml}
@@ -2880,6 +2902,73 @@ const lunarBiz = { } }; + window.toggleSelectAllPayments = function(checked) { + const checkboxes = document.querySelectorAll('.payment-checkbox'); + checkboxes.forEach(cb => cb.checked = checked); + updateBatchDeleteButton(); + }; + + window.updateBatchDeleteButton = function() { + const checkboxes = document.querySelectorAll('.payment-checkbox:checked'); + const count = checkboxes.length; + const btn = document.getElementById('batchDeleteBtn'); + const countSpan = document.getElementById('selectedCount'); + const selectAllCheckbox = document.getElementById('selectAllPayments'); + + if (countSpan) countSpan.textContent = count; + if (btn) btn.disabled = count === 0; + + // 更新全选复选框状态 + if (selectAllCheckbox) { + const allCheckboxes = document.querySelectorAll('.payment-checkbox'); + selectAllCheckbox.checked = allCheckboxes.length > 0 && count === allCheckboxes.length; + selectAllCheckbox.indeterminate = count > 0 && count < allCheckboxes.length; + } + }; + + window.batchDeletePaymentRecords = async function(subscriptionId) { + const checkboxes = document.querySelectorAll('.payment-checkbox:checked'); + const paymentIds = Array.from(checkboxes).map(cb => cb.dataset.paymentId); + + if (paymentIds.length === 0) { + showToast('请选择要删除的支付记录', 'warning'); + return; + } + + if (!confirm(\`确认删除选中的 \${paymentIds.length} 条支付记录?删除后将重新计算订阅到期时间和统计数据。\`)) { + return; + } + + const btn = document.getElementById('batchDeleteBtn'); + const originalHtml = btn.innerHTML; + btn.innerHTML = '删除中...'; + btn.disabled = true; + + try { + const response = await fetch(\`/api/subscriptions/\${subscriptionId}/payments/batch\`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ paymentIds }) + }); + const result = await response.json(); + + if (result.success) { + showToast(result.message || \`已删除 \${paymentIds.length} 条支付记录\`, 'success'); + closePaymentHistoryModal(); + await loadSubscriptions(false); + } else { + showToast(result.message || '批量删除失败', 'error'); + btn.innerHTML = originalHtml; + btn.disabled = false; + } + } catch (error) { + console.error('批量删除支付记录失败:', error); + showToast('批量删除失败: ' + error.message, 'error'); + btn.innerHTML = originalHtml; + btn.disabled = false; + } + }; + window.deletePaymentRecord = async function(subscriptionId, paymentId) { if (!confirm('确认删除此支付记录?删除后将重新计算统计数据。')) { return; @@ -6408,6 +6497,12 @@ const api = { return new Response(JSON.stringify({ success: true, payments: subscription.paymentHistory || [] }), { headers: { 'Content-Type': 'application/json' } }); } + if (parts[3] === 'payments' && parts[4] === 'batch' && method === 'DELETE') { + const { paymentIds } = await request.json(); + const result = await batchDeletePaymentRecords(id, paymentIds, env); + return new Response(JSON.stringify(result), { status: result.success ? 200 : 400, headers: { 'Content-Type': 'application/json' } }); + } + if (parts[3] === 'payments' && parts[4] && method === 'DELETE') { const paymentId = parts[4]; const result = await deletePaymentRecord(id, paymentId, env); @@ -7155,6 +7250,80 @@ async function deletePaymentRecord(subscriptionId, paymentId, env) { } } +async function batchDeletePaymentRecords(subscriptionId, paymentIds, env) { + try { + if (!paymentIds || !Array.isArray(paymentIds) || paymentIds.length === 0) { + return { success: false, message: '请提供要删除的支付记录ID' }; + } + + const subscriptions = await getAllSubscriptions(env); + const index = subscriptions.findIndex(s => s.id === subscriptionId); + + if (index === -1) { + return { success: false, message: '订阅不存在' }; + } + + const subscription = subscriptions[index]; + let paymentHistory = subscription.paymentHistory || []; + + // 过滤掉要删除的支付记录 + const deletedPayments = paymentHistory.filter(p => paymentIds.includes(p.id)); + paymentHistory = paymentHistory.filter(p => !paymentIds.includes(p.id)); + + if (deletedPayments.length === 0) { + return { success: false, message: '未找到要删除的支付记录' }; + } + + // 重新计算订阅到期时间和最后支付日期 + let newExpiryDate = subscription.expiryDate; + let newLastPaymentDate = subscription.lastPaymentDate; + + if (paymentHistory.length > 0) { + // 找到剩余支付记录中 periodEnd 最晚的那条(最新的续订) + const sortedByPeriodEnd = [...paymentHistory].sort((a, b) => { + const dateA = a.periodEnd ? new Date(a.periodEnd) : new Date(0); + const dateB = b.periodEnd ? new Date(b.periodEnd) : new Date(0); + return dateB - dateA; + }); + + // 订阅的到期日期应该是最新续订的 periodEnd + if (sortedByPeriodEnd[0].periodEnd) { + newExpiryDate = sortedByPeriodEnd[0].periodEnd; + } + + // 找到最新的支付记录日期 + const sortedByDate = [...paymentHistory].sort((a, b) => new Date(b.date) - new Date(a.date)); + newLastPaymentDate = sortedByDate[0].date; + } else { + // 如果没有支付记录了,回退到初始状态 + // 使用第一条被删除记录的 periodStart(如果有) + const firstDeleted = deletedPayments.sort((a, b) => new Date(a.date) - new Date(b.date))[0]; + if (firstDeleted.periodStart) { + newExpiryDate = firstDeleted.periodStart; + } + newLastPaymentDate = subscription.startDate || subscription.createdAt || subscription.expiryDate; + } + + subscriptions[index] = { + ...subscription, + expiryDate: newExpiryDate, + paymentHistory, + lastPaymentDate: newLastPaymentDate + }; + + await env.SUBSCRIPTIONS_KV.put('subscriptions', JSON.stringify(subscriptions)); + + return { + success: true, + subscription: subscriptions[index], + message: `已删除 ${deletedPayments.length} 条支付记录` + }; + } catch (error) { + console.error('批量删除支付记录失败:', error); + return { success: false, message: '批量删除失败: ' + error.message }; + } +} + async function updatePaymentRecord(subscriptionId, paymentId, paymentData, env) { try { const subscriptions = await getAllSubscriptions(env); @@ -7687,7 +7856,7 @@ function resolveSubscriptionEmailRecipients(subscription) { return normalizeEmailRecipients(subscription?.emailTo); } -function shouldNotifyAtCurrentHour(notificationHours, currentHour, currentMinute) { +function shouldNotifyAtCurrentHour(notificationHours, currentHour, currentMinute, expiryDate = null, timezone = 'UTC') { const normalized = normalizeNotificationHours(notificationHours); if (normalized.length === 0 || normalized.includes('*')) { return true; @@ -7697,7 +7866,29 @@ function shouldNotifyAtCurrentHour(notificationHours, currentHour, currentMinute const currentTimeStr = String(currentHour).padStart(2, '0') + ':' + String(currentMinute).padStart(2, '0'); const currentHourStr = String(currentHour).padStart(2, '0'); - // 检查是否匹配 + // 如果提供了过期时间,检查当前时间是否与过期时间的 HH:MM 相等 + if (expiryDate) { + try { + const expiryTimeFormatter = new Intl.DateTimeFormat('en-US', { + timeZone: timezone, + hour12: false, + hour: '2-digit', + minute: '2-digit' + }); + const expiryTimeParts = expiryTimeFormatter.formatToParts(new Date(expiryDate)); + const expiryHour = expiryTimeParts.find(p => p.type === 'hour')?.value || '00'; + const expiryMinute = expiryTimeParts.find(p => p.type === 'minute')?.value || '00'; + const expiryTimeStr = expiryHour + ':' + expiryMinute; + // 如果当前时间与过期时间的 HH:MM 相等,直接返回 true + if (currentTimeStr === expiryTimeStr) { + return true; + } + } catch (error) { + console.error('解析过期时间失败:', error); + } + } + + // 检查是否匹配配置的通知时间 for (const time of normalized) { if (time.includes(':')) { // 对于 HH:MM 格式,允许在同一分钟内触发(考虑到定时任务可能不是精确在该分钟的0秒执行) @@ -8227,7 +8418,13 @@ async function checkExpiringSubscriptions(env) { const reminderSetting = resolveReminderSetting(subscription); const subscriptionNotificationHours = resolveSubscriptionNotificationHours(subscription, config); - const shouldNotifyThisHour = shouldNotifyAtCurrentHour(subscriptionNotificationHours, currentHour, currentMinute); + const shouldNotifyThisHour = shouldNotifyAtCurrentHour( + subscriptionNotificationHours, + currentHour, + currentMinute, + subscription.expiryDate, + timezone + ); // 计算当前剩余时间(基础计算) let expiryDate = new Date(subscription.expiryDate); @@ -8252,20 +8449,13 @@ async function checkExpiringSubscriptions(env) { let diffMs = expiryDate.getTime() - currentTime.getTime(); let diffHours = diffMs / MS_PER_HOUR; let diffMinutes = diffMs / (1000 * 60); - // ========================================== // 核心逻辑:自动续费处理 // ========================================== // 修复:对于分钟级和小时级订阅,使用精确的时间差判断是否过期 - let isExpiredForRenewal = false; - if (subscription.periodUnit === 'minute' || subscription.periodUnit === 'hour') { - isExpiredForRenewal = diffMs < 0; // 使用精确的毫秒差 - } else { - isExpiredForRenewal = daysDiff < 0; // 使用天数差 - } - + let isExpiredForRenewal = diffMs <= 0; if (isExpiredForRenewal && subscription.periodValue && subscription.periodUnit && subscription.autoRenew !== false) { - console.log(`[定时任务] 订阅 "${subscription.name}" 已过期,准备自动续费...`); + console.log(`[定时任务] 订阅:"${subscription.name}" 已过期,准备自动续费...`); const mode = subscription.subscriptionMode || 'cycle'; // cycle | reset @@ -8323,7 +8513,7 @@ async function checkExpiringSubscriptions(env) { } while (daysDiff < 0); // 只要还过期,就继续加 - console.log(`[定时任务] 续费完成. 新开始日: ${newStartDate.toISOString()}, 新到期日: ${newExpiryDate.toISOString()}`); + console.log(`[定时任务] 订阅:${subscription.name} 续费完成. 新开始日: ${newStartDate.toISOString()}, 新到期日: ${newExpiryDate.toISOString()}`); // 3. 生成支付记录 const paymentRecord = { id: Date.now().toString() + '_' + Math.random().toString(36).substr(2, 9),

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 ZzXJ-zK6m@65QhK&002ovPDHLkV1hpG9?k#& literal 0 HcmV?d00001 diff --git a/images/email_cnf.png b/images/email_cnf.png new file mode 100644 index 0000000000000000000000000000000000000000..465f89c35df89ec7ebe5433604f7568bde6d68a0 GIT binary patch literal 30575 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 F{{dOxn}PrU literal 0 HcmV?d00001 diff --git a/index_zzz_0210.js b/index_zzz_0210.js new file mode 100644 index 0000000..319aa0d --- /dev/null +++ b/index_zzz_0210.js @@ -0,0 +1,8836 @@ +// 订阅续期通知网站 - 基于CloudFlare Workers (完全优化版) + +// 时区处理工具函数 +// 常量:毫秒转换为小时/天,便于全局复用 +const MS_PER_HOUR = 1000 * 60 * 60; +const MS_PER_DAY = MS_PER_HOUR * 24; + +function getCurrentTimeInTimezone(timezone = 'UTC') { + try { + // Workers 环境下 Date 始终存储 UTC 时间,这里直接返回当前时间对象 + return new Date(); + } catch (error) { + console.error(`时区转换错误: ${error.message}`); + // 如果时区无效,返回UTC时间 + return new Date(); + } +} + +function getTimestampInTimezone(timezone = 'UTC') { + return getCurrentTimeInTimezone(timezone).getTime(); +} + +function convertUTCToTimezone(utcTime, timezone = 'UTC') { + try { + // 同 getCurrentTimeInTimezone,一律返回 Date 供后续统一处理 + return new Date(utcTime); + } catch (error) { + console.error(`时区转换错误: ${error.message}`); + return new Date(utcTime); + } +} + +// 获取指定时区的年/月/日/时/分/秒,便于避免重复的 Intl 解析逻辑 +function getTimezoneDateParts(date, timezone = 'UTC') { + try { + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: timezone, + hour12: false, + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit' + }); + const parts = formatter.formatToParts(date); + const pick = (type) => { + const part = parts.find(item => item.type === type); + return part ? Number(part.value) : 0; + }; + return { + year: pick('year'), + month: pick('month'), + day: pick('day'), + hour: pick('hour'), + minute: pick('minute'), + second: pick('second') + }; + } catch (error) { + console.error(`解析时区(${timezone})失败: ${error.message}`); + return { + year: date.getUTCFullYear(), + month: date.getUTCMonth() + 1, + day: date.getUTCDate(), + hour: date.getUTCHours(), + minute: date.getUTCMinutes(), + second: date.getUTCSeconds() + }; + } +} + +// 计算指定日期在目标时区的午夜时间戳(毫秒),用于统一的“剩余天数”计算 +function getTimezoneMidnightTimestamp(date, timezone = 'UTC') { + const { year, month, day } = getTimezoneDateParts(date, timezone); + return Date.UTC(year, month - 1, day, 0, 0, 0); +} + +function formatTimeInTimezone(time, timezone = 'UTC', format = 'full') { + try { + const date = new Date(time); + + if (format === 'date') { + return date.toLocaleDateString('zh-CN', { + timeZone: timezone, + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + } else if (format === 'datetime') { + return date.toLocaleString('zh-CN', { + timeZone: timezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + } else { + // full format + return date.toLocaleString('zh-CN', { + timeZone: timezone + }); + } + } catch (error) { + console.error(`时间格式化错误: ${error.message}`); + return new Date(time).toISOString(); + } +} + +function getTimezoneOffset(timezone = 'UTC') { + try { + const now = new Date(); + const { year, month, day, hour, minute, second } = getTimezoneDateParts(now, timezone); + const zonedTimestamp = Date.UTC(year, month - 1, day, hour, minute, second); + return Math.round((zonedTimestamp - now.getTime()) / MS_PER_HOUR); + } catch (error) { + console.error(`获取时区偏移量错误: ${error.message}`); + return 0; + } +} + +// 格式化时区显示,包含UTC偏移 +function formatTimezoneDisplay(timezone = 'UTC') { + try { + const offset = getTimezoneOffset(timezone); + const offsetStr = offset >= 0 ? `+${offset}` : `${offset}`; + + // 时区中文名称映射 + const timezoneNames = { + 'UTC': '世界标准时间', + 'Asia/Shanghai': '中国标准时间', + 'Asia/Hong_Kong': '香港时间', + 'Asia/Taipei': '台北时间', + 'Asia/Singapore': '新加坡时间', + 'Asia/Tokyo': '日本时间', + 'Asia/Seoul': '韩国时间', + 'America/New_York': '美国东部时间', + 'America/Los_Angeles': '美国太平洋时间', + 'America/Chicago': '美国中部时间', + 'America/Denver': '美国山地时间', + 'Europe/London': '英国时间', + 'Europe/Paris': '巴黎时间', + 'Europe/Berlin': '柏林时间', + 'Europe/Moscow': '莫斯科时间', + 'Australia/Sydney': '悉尼时间', + 'Australia/Melbourne': '墨尔本时间', + 'Pacific/Auckland': '奥克兰时间' + }; + + const timezoneName = timezoneNames[timezone] || timezone; + return `${timezoneName} (UTC${offsetStr})`; + } catch (error) { + console.error('格式化时区显示失败:', error); + return timezone; + } +} + +// 兼容性函数 - 保持原有接口 +function formatBeijingTime(date = new Date(), format = 'full') { + return formatTimeInTimezone(date, 'Asia/Shanghai', format); +} + +// 时区处理中间件函数 +function extractTimezone(request) { + // 优先级:URL参数 > 请求头 > 默认值 + const url = new URL(request.url); + const timezoneParam = url.searchParams.get('timezone'); + + if (timezoneParam) { + return timezoneParam; + } + + // 从请求头获取时区 + const timezoneHeader = request.headers.get('X-Timezone'); + if (timezoneHeader) { + return timezoneHeader; + } + + // 从Accept-Language头推断时区(简化处理) + const acceptLanguage = request.headers.get('Accept-Language'); + if (acceptLanguage) { + // 简单的时区推断逻辑 + if (acceptLanguage.includes('zh')) { + return 'Asia/Shanghai'; + } else if (acceptLanguage.includes('en-US')) { + return 'America/New_York'; + } else if (acceptLanguage.includes('en-GB')) { + return 'Europe/London'; + } + } + + // 默认返回UTC + return 'UTC'; +} + +function isValidTimezone(timezone) { + try { + // 尝试使用该时区格式化时间 + new Date().toLocaleString('en-US', { timeZone: timezone }); + return true; + } catch (error) { + return false; + } +} + +// 农历转换工具函数 +const lunarCalendar = { + // 农历数据 (1900-2100年) + lunarInfo: [ + 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, // 1900-1909 + 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, // 1910-1919 + 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, // 1920-1929 + 0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, // 1930-1939 + 0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, // 1940-1949 + 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, // 1950-1959 + 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, // 1960-1969 + 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, // 1970-1979 + 0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, // 1980-1989 + 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0, // 1990-1999 + 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, // 2000-2009 + 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, // 2010-2019 + 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, // 2020-2029 + 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, // 2030-2039 + 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, // 2040-2049 + 0x14b63, 0x09370, 0x14a38, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x1a978, 0x16aa0, 0x0a6c0, // 2050-2059 (修正2057: 0x1a978) + 0x0aa60, 0x16d63, 0x0d260, 0x0d950, 0x0d554, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, // 2060-2069 + 0x025d0, 0x092d0, 0x0cab5, 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, // 2070-2079 + 0x15176, 0x052b0, 0x0a930, 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, // 2080-2089 + 0x0d260, 0x0ea65, 0x0d530, 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x1a4bb, 0x0a4d0, 0x0d0b0, // 2090-2099 (修正2099: 0x0d0b0) + 0x0d250 // 2100 + ], + + // 天干地支 + gan: ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'], + zhi: ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'], + + // 农历月份 + months: ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'], + + // 农历日期 + days: ['初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十', + '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十', + '廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十'], + + // 获取农历年天数 + lunarYearDays: function (year) { + let sum = 348; + for (let i = 0x8000; i > 0x8; i >>= 1) { + sum += (this.lunarInfo[year - 1900] & i) ? 1 : 0; + } + return sum + this.leapDays(year); + }, + + // 获取闰月天数 + leapDays: function (year) { + if (this.leapMonth(year)) { + return (this.lunarInfo[year - 1900] & 0x10000) ? 30 : 29; + } + return 0; + }, + + // 获取闰月月份 + leapMonth: function (year) { + return this.lunarInfo[year - 1900] & 0xf; + }, + + // 获取农历月天数 + monthDays: function (year, month) { + return (this.lunarInfo[year - 1900] & (0x10000 >> month)) ? 30 : 29; + }, + + // 公历转农历 + solar2lunar: function (year, month, day) { + if (year < 1900 || year > 2100) return null; + + const baseDate = Date.UTC(1900, 0, 31); + const objDate = Date.UTC(year, month - 1, day); + //let offset = Math.floor((objDate - baseDate) / 86400000); + let offset = Math.round((objDate - baseDate) / 86400000); + + + let temp = 0; + let lunarYear = 1900; + + for (lunarYear = 1900; lunarYear < 2101 && offset > 0; lunarYear++) { + temp = this.lunarYearDays(lunarYear); + offset -= temp; + } + + if (offset < 0) { + offset += temp; + lunarYear--; + } + + let lunarMonth = 1; + let leap = this.leapMonth(lunarYear); + let isLeap = false; + + for (lunarMonth = 1; lunarMonth < 13 && offset > 0; lunarMonth++) { + if (leap > 0 && lunarMonth === (leap + 1) && !isLeap) { + --lunarMonth; + isLeap = true; + temp = this.leapDays(lunarYear); + } else { + temp = this.monthDays(lunarYear, lunarMonth); + } + + if (isLeap && lunarMonth === (leap + 1)) isLeap = false; + offset -= temp; + } + + if (offset === 0 && leap > 0 && lunarMonth === leap + 1) { + if (isLeap) { + isLeap = false; + } else { + isLeap = true; + --lunarMonth; + } + } + + if (offset < 0) { + offset += temp; + --lunarMonth; + } + + const lunarDay = offset + 1; + + // 生成农历字符串 + const ganIndex = (lunarYear - 4) % 10; + const zhiIndex = (lunarYear - 4) % 12; + const yearStr = this.gan[ganIndex] + this.zhi[zhiIndex] + '年'; + const monthStr = (isLeap ? '闰' : '') + this.months[lunarMonth - 1] + '月'; + const dayStr = this.days[lunarDay - 1]; + + return { + year: lunarYear, + month: lunarMonth, + day: lunarDay, + isLeap: isLeap, + yearStr: yearStr, + monthStr: monthStr, + dayStr: dayStr, + fullStr: yearStr + monthStr + dayStr + }; + } +}; + +// 1. 新增 lunarBiz 工具模块,支持农历加周期、农历转公历、农历距离天数 +const lunarBiz = { + // 农历加周期,返回新的农历日期对象 + addLunarPeriod(lunar, periodValue, periodUnit) { + let { year, month, day, isLeap } = lunar; + if (periodUnit === 'year') { + year += periodValue; + const leap = lunarCalendar.leapMonth(year); + if (isLeap && leap === month) { + isLeap = true; + } else { + isLeap = false; + } + } else if (periodUnit === 'month') { + let totalMonths = (year - 1900) * 12 + (month - 1) + periodValue; + year = Math.floor(totalMonths / 12) + 1900; + month = (totalMonths % 12) + 1; + const leap = lunarCalendar.leapMonth(year); + if (isLeap && leap === month) { + isLeap = true; + } else { + isLeap = false; + } + } else if (periodUnit === 'day') { + const solar = lunarBiz.lunar2solar(lunar); + const date = new Date(solar.year, solar.month - 1, solar.day + periodValue); + return lunarCalendar.solar2lunar(date.getFullYear(), date.getMonth() + 1, date.getDate()); + } + let maxDay = isLeap + ? lunarCalendar.leapDays(year) + : lunarCalendar.monthDays(year, month); + let targetDay = Math.min(day, maxDay); + while (targetDay > 0) { + let solar = lunarBiz.lunar2solar({ year, month, day: targetDay, isLeap }); + if (solar) { + return { year, month, day: targetDay, isLeap }; + } + targetDay--; + } + return { year, month, day, isLeap }; + }, + // 农历转公历(遍历法,适用1900-2100年) + lunar2solar(lunar) { + for (let y = lunar.year - 1; y <= lunar.year + 1; y++) { + for (let m = 1; m <= 12; m++) { + for (let d = 1; d <= 31; d++) { + const date = new Date(y, m - 1, d); + if (date.getFullYear() !== y || date.getMonth() + 1 !== m || date.getDate() !== d) continue; + const l = lunarCalendar.solar2lunar(y, m, d); + if ( + l && + l.year === lunar.year && + l.month === lunar.month && + l.day === lunar.day && + l.isLeap === lunar.isLeap + ) { + return { year: y, month: m, day: d }; + } + } + } + } + return null; + }, + // 距离农历日期还有多少天 + daysToLunar(lunar) { + const solar = lunarBiz.lunar2solar(lunar); + const date = new Date(solar.year, solar.month - 1, solar.day); + const now = new Date(); + return Math.ceil((date - now) / (1000 * 60 * 60 * 24)); + } +}; + +// === 新增:主题模式公共资源 (CSS覆盖 + JS逻辑) === +const themeResources = ` + + +`; +// 定义HTML模板 +const loginPage = ` + + + + + + 订阅管理系统 + + + ${themeResources} + + +