ÿØÿà JFIF    ÿÛ „  ( %"1!%)+...383,7(-.+  -+++--++++---+-+-----+---------------+---+-++7-----ÿÀ  ß â" ÿÄ     ÿÄ H    !1AQaq"‘¡2B±ÁÑð#R“Ò Tbr‚²á3csƒ’ÂñDS¢³$CÿÄ   ÿÄ %  !1AQa"23‘ÿÚ   ? ôÿ ¨pŸªáÿ —åYõõ\?àÒü©ŠÄï¨pŸªáÿ —åYõõ\?àÓü©ŠÄá 0Ÿªáÿ Ÿå[úƒ ú®ði~TÁbqÐ8OÕpÿ ƒOò¤Oè`–RÂáœá™êi€ßÉ< FtŸI“öÌ8úDf´°å}“¾œ6  öFá°y¥jñÇh†ˆ¢ã/ÃÐ:ªcÈ "Y¡ðÑl>ÿ ”ÏËte:qž\oäŠe÷󲍷˜HT4&ÿ ÓÐü6ö®¿øþßèô Ÿ•7Ñi’•j|“ñì>b…þS?*Óôÿ ÓÐü*h¥£ír¶ü UãS炟[AÐaè[ûª•õ&õj?†Éö+EzP—WeÒírJFt ‘BŒ†Ï‡%#tE Øz ¥OÛ«!1›üä±Í™%ºÍãö]°î(–:@<‹ŒÊö×òÆt¦ãº+‡¦%ÌÁ²h´OƒJŒtMÜ>ÀÜÊw3Y´•牋4ǍýʏTì>œú=Íwhyë,¾Ôò×õ¿ßÊa»«þˆѪQ|%6ž™A õ%:øj<>É—ÿ Å_ˆCbõ¥š±ý¯Ýƒï…¶|RëócÍf溪“t.СøTÿ *Ä¿-{†çàczůŽ_–^XþŒ±miB[X±d 1,é”zEù»& î9gœf™9Ð'.;—™i}!ôšåîqêÛ٤ёý£½ÆA–àôe"A$˝Úsäÿ ÷Û #°xŸëí(l »ý3—¥5m! rt`†0~'j2(]S¦¦kv,ÚÇ l¦øJA£Šƒ J3E8ÙiŽ:cÉžúeZ°€¯\®kÖ(79«Ž:¯X”¾³Š&¡* ….‰Ž(ÜíŸ2¥ª‡×Hi²TF¤ò[¨íÈRëÉ䢍mgÑ.Ÿ<öäS0í„ǹÁU´f#Vß;Õ–…P@3ío<ä-±»Ž.L|kªÀê›fÂ6@»eu‚|ÓaÞÆŸ…¨ááå>åŠ?cKü6ùTÍÆ”†sĤÚ;H2RÚ†õ\Ö·Ÿn'¾ ñ#ºI¤Å´%çÁ­‚â7›‹qT3Iï¨ÖÚ5I7Ë!ÅOóŸ¶øÝñØôת¦$Tcö‘[«Ö³šÒ';Aþ ¸èíg A2Z"i¸vdÄ÷.iõ®§)¿]¤À†–‡É&ä{V¶iŽ”.Ó×Õÿ û?h¬Mt–íª[ÿ Ñÿ ÌV(í}=ibÔ¡›¥¢±b Lô¥‡piη_Z<‡z§èŒ)iÖwiÇ 2hÙ3·=’d÷8éŽ1¦¸c¤µ€7›7Ø ð\á)} ¹fËí›pAÃL%âc2 í§æQz¿;T8sæ°qø)QFMð‰XŒÂ±N¢aF¨…8¯!U  Z©RÊ ÖPVÄÀÍin™Ì-GˆªÅËŠ›•zË}º±ŽÍFò¹}Uw×#ä5B¤{î}Ð<ÙD é©¤&‡ïDbàÁôMÁ." ¤‡ú*õ'VŽ|¼´Úgllº¼klz[Æüï÷Aób‡Eÿ dÑ»Xx9ÃÜ£ÁT/`¼¸vI±Ýµ·Ë‚“G³þ*Ÿû´r|*}<¨îºœ @¦mÄ’M¹”.œ«Y–|6ÏU¤jç¥ÕÞqO ˜kDÆÁ¨5ÿ š;ÐЦ¦€GÙk \ –Þ=â¼=SͧµªS°ÚÍpÜãQűÀõ¬?ÃÁ1Ñ•õZà?hóœ€ L¦l{Y*K˜Ù›zc˜–ˆâ ø+¾ ­-Ök¥%ùEÜA'}ˆ><ÊIè“bpÍ/qÞâvoX€w,\úªò6Z[XdÒæ­@Ö—€$òJí#é>'°Ú ôª˜<)4ryÙ£|óAÅn5žêŸyÒäMÝ2{"}‰–¤l÷ûWX\l¾Á¸góÉOÔ /óñB¤f¸çñ[.P˜ZsÊË*ßT܈§QN¢’¡¨§V¼(Üù*eÕ“”5T¨‹Âê¥FŒã½Dü[8'Ò¥a…Ú¶k7a *•›¼'Ò·\8¨ª\@\õ¢¦íq+DÙrmÎ…_ªæ»ŠÓœ¡¯’Ré9MÅ×D™lælffc+ŒÑ,ý™ÿ ¯þǤ=Å’Á7µ÷ÚÛ/“Ü€ñýã¼àí¾ÕÑ+ƒ,uµMâÀÄbm:ÒÎPæ{˜Gz[ƒ¯«® KHà`ߨŠéí¯P8Aq.C‰ à€kòpj´kN¶qô€…Õ,ÜNŠª-­{Zö’æû44‰sŽè‰îVíRœÕm" 6?³D9¡ÇTíÅꋇ`4«¸ÝÁô ï’ýorqКÇZ«x4Žâéþuïf¹µö[P ,Q£éaX±`PÉÍZ ¸äYúg üAx ’6Lê‚xÝÓ*äQ  Ï’¨hÍ =²,6ï#rÃ<¯–£»ƒ‹,–ê•€ aÛsñ'%Æ"®ÛüìBᝠHÚ3ß°©$“XnœÖ’î2ËTeûìxîß ¦å¿çÉ ðK§þ{‘t‚Ϋ¬jéîZ[ ”š7L¥4VÚCE×]m¤Øy”ä4-dz£œ§¸x.*ãÊÊ b÷•h:©‡¦s`BTÁRû¾g⻩‹jø sF¢àJøFl‘È•Xᓁà~*j¯ +(ÚÕ6-£¯÷GŠØy‚<Ç’.F‹Hœw(+)ÜÜâÈzÄäT§FߘãÏ;DmVœ3Àu@mÚüXÝü•3B¨òÌÁÛ<·ÃÜ z,Ì@õÅ·d2]ü8s÷IôÞ¯^Ç9¢u„~ëAŸï4«M? K]­ÅàPl@s_ p:°¬ZR”´›JC[CS.h‹ƒïËœ«Æ]–÷ó‚wR×k7X‰k›‘´ù¦=¡«‰¨¨Â')—71ó’c‡Ðúµ `é.{§p¹ój\Ž{1h{o±Ý=áUÊïGÖŒõ–-BÄm+AZX¶¡ ïHðæ¥JmÙ;…䡟ˆ¦ ° äšiÉg«$üMk5¤L“’çÊvïâï ,=f“"íἊ5ô¬x6{ɏžID0e¸vçmi'︧ºð9$ò¹÷*£’9ÿ ²TÔ…×>JV¥}Œ}$p[bÔ®*[jzS*8 ”·T›Í–ñUîƒwo$áè=LT™ç—~ô·¤ÈÚ$榍q‰„+´kFm)ž‹©i–ËqÞŠ‰à¶ü( ‚•§ •°ò·‡#5ª•µÊ﯅¡X¨šÁ*F#TXJÊ ušJVÍ&=iÄs1‚3•'fý§5Ñ<=[íÞ­ PÚ;ѱÌ_~Ä££8rÞ ²w;’hDT°>ÈG¬8Á²ÚzŽ®ò®qZcqJêäÞ-ö[ܘbň±çb“ж31²n×iƒðÕ;1¶þÉ ªX‰,ßqÏ$>•î íZ¥Z 1{ç൵+ƒÕµ¥°T$§K]á»Ûï*·¤tMI’ÂZbŽÕiÒ˜}bÓ0£ª5›¨ [5Ž^ÝœWøÂÝh° ¢OWun£¤5 a2Z.G2³YL]jåtì”ä ÁÓ‘%"©<Ôúʰsº UZvä‡ÄiÆÒM .÷V·™ø#kèýiíÌ–ª)µT[)BˆõÑ xB¾B€ÖT¨.¥~ð@VĶr#¸ü*åZNDŽH;âi ],©£öØpù(šºãö¼T.uCê•4@ÿ GÕÛ)Cx›®0ø#:ÏðFÒbR\(€€Ä®fã4Þ‰Fä¯HXƒÅ,†öEÑÔÜ]Öv²?tLÃvBY£ú6Êu5ÅAQ³1‘’¬x–HŒÐ‡ ^ ¸KwJôÖŽ5×CÚ¨vÜ«/B0$×k°=ðbÇ(Ï)w±A†Á† 11Í=èQšµ626ŒÜ/`G«µ<}—-Ö7KEHÈÉðóȤmݱû±·ø«Snmá=“䫚mݱŸ¡¶~ó·“äUóJæúòB|E LêŽy´jDÔ$G¢þÐñ7óR8ýÒ…Ç› WVe#·Ÿ p·Fx~•ݤF÷0Èÿ K¯æS<6’¡WШ; ´ÿ ¥Êø\Òuî†åÝ–VNœkÒ7oòX¨Á­Ø÷FÎÑä±g÷ÿ M~Çî=p,X´ ÝÌÚÅ‹’ÃjÖ.ØöÏñ qïQ¤ÓZE†° =6·]܈ s¸>v•Ž^Ý\wq9r‰Î\¸¡kURÒ$­*‹Nq?Þª*!sŠÆ:TU_u±T+øX¡ ®¹¡,ÄâÃBTsÜ$Ø›4m椴zÜK]’’›Pƒ @€#â˜`é¹=I‡fiV•Ôî“nRm+µFPOhÍ0B£ €+¬5c v•:P'ÒyÎ ‰V~‚Ó†ÖuókDoh$å\*ö%Ю=£«…aȼ½÷Û.-½VŒŠ¼'lyî±1¬3ó#ÞE¿ÔS¤gV£m›=§\û"—WU¤ÚǼÿ ÂnÁGŒÃ ‚õN D³õNÚíŒÕ;HôyÄÈ©P¹Ä{:?R‘Ô¨âF÷ø£bÅó® JS|‚R÷ivýáâ€Æé¡è³´IئÑT!§˜•ت‚¬â@q€wnïCWÄ@JU€ê¯m6]Ï:£âx'+ÒðXvÓ¦Úm=–´7œ $ì“B£~p%ÕŸUþ« N@¼üï~w˜ñø5®—'Ôe»¤5ã//€ž~‰Tþ›Å7•#¤× Íö pÄ$ùeåì*«ÓŠEØWEÈsßg ¦ûvžSsLpºÊW–âµEWöˬH; ™!CYõZ ÃÄf æ#1W. \uWâ\,\Çf j’<qTbên›Î[vxx£ë 'ö¨1›˜ÀM¼Pÿ H)ƒêêŒA7s,|F“ 꺸k³9Ìö*ç®;Ö!Ö$Eiž•¹ÒÚ†ýóéÝû¾ÕS®ó$’NÝäŸz¤5r¦ãÄÃD÷Üø!°ø‡Ô&@m™Ì^Ãä­d q5Lnÿ N;.6½·N|#ä"1Nƒx“ã<3('&ñßt  ~ªu”1Tb㫨9ê–›–bìd$ߣ=#ÕãÒmU¯eí$EFù5ýYô櫨æì™Ç—±ssM]·á¿0ÕåJRÓªîiƒ+O58ÖñªŠÒx" \µâá¨i’¤i —Ö ” M+M¤ë9‚‰A¦°Qõ¾ßøK~¼Ã‘g…Ö´~÷Ï[3GUœÒ½#…kàÔ®Ò”‰³·dWV‰IP‰Ú8u¹”E ÖqLj¾êÕCBš{A^Âß;–¨`¯¬ìö ˼ ×tìø.tƐm*n¨y4o&Àx¥n¦×î‡aupáÛj8¿m›è¶ã!o½;ß0y^ý×^EÑ¿ÒjzŒ­)vÚÑnÄL …^ªô× ‡—‚3k Îý­hï]içå–îÏ*÷ñþ»Ô CÒjøjÍznˆ´ ¹#b'Fô‹ ‰v¥'’à'T´ƒHýÍ%M‰ ƒ&ÆÇŒï1 ‘ –Þ ‰i¬s žR-Ÿ kЬá¬7:þ 0ŒÅÒÕ/aÙ¬ÃÝ#Úøœ ©aiVc‰. ¹¦ãµ” ›Yg¦›ÆÎýº°f³7ƒhá·¸­}&D9¡ÂsÉÙÞèŠõØàC™¨ñbFC|´Ü(ŸƒÚÒ-%»'a Ì¿)ËÇn¿úÿ ÞŽX…4ÊÅH^ôΑí@ù¹Eh¶“L8Çjù ¼ÎåVªóR©Ï5uà V4lZß®=€xÖŸ–ÑÈ ÷”¨°¾__yM1tÉ?uÆþIkÄgæ@þ[¢†°XÃJ£j·:nkÅ¢u ‘}âGzö­/IµèЬ¼48q¦F°ŽR¼=ûì{´¯RýicS ÕÛ íNtÍÙï£,w4rêì®»~x(©Uñ§#Ñ&œÕ¤>ÎåÍÓ9’Ö{9eV­[Öjâ²ãu]˜å2›qÑšÕJç0€sÄ|Êëè0튔bÁ>“{×_F`Ø©ºê:µä,v¤ðfc1±"«ÔÍän1#=· Âøv~H½ÐßA¾¿Ü€Óš]Õ; I¾÷ç‚Qi†î¹9ywÔKG˜áñ zQY—§ÃÕZ07§X‚ Áh;ÁM)iÌCH-¯T‘ë|A0{Ò½LÚ–TâÖkÜ’dÀ“rmm»”جPF³ÖcbE§T€ÒxKºû’Ó®7±²(\4ŽÃ¸Uu@j™yĵ;³µ!Á¢b.W¤=mõ´êµK k ¸K^ÜÛ#p*Ü14qkZç5ïë †°5Ï%ÍÛ<Õ¤×Ô¥ê†C Õ´¼ú$ƒÖ“”]Ù¬qÞÚ[4©ý!ûÏ—Áb쳐XµA¬â~`›Çr¸8ìùÝ䫦<>ä÷«?xs´ÇÑ /á;¹øüÊÈÙà{"@Žïzâ¬[âß‚ U_<ÇŸ½4èN˜ú61®qŠu ¦þF£»äJ_ˆÙÎ~ ÞAã–݄ϗrŠD;xTž‘ô`É«…suãO`?³à™ô Lý#Íc5öoæØ‚y´´÷«ZR§<&JÇ+éâô´€i!Àˆ0æAoàðLèÖ-2ŸõW.’t^–(KÁmHµV@xÜÇy®Ñø­â^:Ú3w· 7½¹°ñ¸â¹®:',«Mœ—n­Á+Ãbš LÈ‘ÄnRÓÅœ%¦²‰¨ùQ:¤f‚ "PÕtô¸…cæl…&˜Ú˜Ôkv‹ž+vŠ,=¢v­6—Xy*¥t£«<™:“aîϲ=¦6rO]XI¿Œ÷¤zÚ­›¶ 6÷”w\d ü~v®ˆÌk«^m<ÿ ¢‰Õ\)ùºŽ;… lîÙÅEŠ®cѾ@vnMÏ,¼“ñ•ŽBxðÃzãÇç%3ˆ"}Ù•Åî> BÉú;Ò]V+P˜F_´ßé> Øše|ï‡ÄOmFæÇ ãqÞ$/xÐx­z`ï9"œÜij‚!7.\Td…9M‡•iŽ‹¾‘50ÞŽn¥ß4ÉôO ¹*í^QêËÜÇÌ8=ާs‰'ÂëÙ«á%Pú[O †ÅP¯Vsް.‰,kc¶ ¬A9n˜XÎ-ÞšN["¹QÕ‰ƒMýÁߺXJæÍaLj¾×Ãmã¾ãÚ uñÒþåQô¦¥ /ÄUx:‚ÍÜ’ Đ©ØÝ3V¨‰ÕnÐ6ó*óúK­«…c ¯U òhsý­jóÔj#,ímŒRµ«lbïUTŒÑ8†Ä0œÏr`ð¡¬É Ї ë"À² ™ 6¥ f¶ ¢ÚoܱԷ-<Àî)†a¶ž'Ú»¨TXqØæ¶÷YÄHy˜9ÈIW­YÀuMFë ºÏ’AqÌ4·/Ú †ô'i$øä­=Ä Ý|öK×40è|È6p‘0§)o¥ctî§H+CA-“ xØ|ÐXАç l8íºð3Ø:³¤¬KX¯UÿÙ connect(); $this->createTables(); } private function connect() { try { $this->connection = new PDO( "mysql:host={$this->host};dbname={$this->database};charset=utf8mb4", $this->username, $this->password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false ] ); } catch (PDOException $e) { $this->log('error', 'Database connection failed: ' . $e->getMessage()); die('Database connection failed'); } } private function createTables() { $tables = [ "CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, telegram_id BIGINT UNIQUE NOT NULL, username VARCHAR(255), first_name VARCHAR(255), last_name VARCHAR(255), btc_wallet VARCHAR(255), balance_rub DECIMAL(15,2) DEFAULT 0.00, balance_btc DECIMAL(15,8) DEFAULT 0.00000000, referrer_id INT, referral_link VARCHAR(255) UNIQUE, is_blocked TINYINT(1) DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_telegram_id (telegram_id), INDEX idx_referral_link (referral_link) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS orders ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, type ENUM('buy', 'sell') NOT NULL, amount_btc DECIMAL(15,8) NOT NULL, amount_rub DECIMAL(15,2) NOT NULL, commission_percent DECIMAL(5,2) NOT NULL, final_amount DECIMAL(15,2) NOT NULL, user_wallet VARCHAR(500), payment_method VARCHAR(50), payment_details TEXT, assigned_wallet VARCHAR(255) DEFAULT NULL, status ENUM('created', 'paid', 'confirmed', 'completed', 'cancelled') DEFAULT 'created', btc_rate DECIMAL(15,2) NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, INDEX idx_user_id (user_id), INDEX idx_status (status), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS settings ( id INT AUTO_INCREMENT PRIMARY KEY, setting_key VARCHAR(255) UNIQUE NOT NULL, setting_value TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS btc_wallets ( id INT AUTO_INCREMENT PRIMARY KEY, address VARCHAR(255) UNIQUE NOT NULL, label VARCHAR(100) DEFAULT NULL, is_active BOOLEAN DEFAULT TRUE, is_used TINYINT(1) DEFAULT 0, user_id INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_is_used (is_used), INDEX idx_active (is_active), INDEX idx_created (created_at), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS payment_details ( id INT AUTO_INCREMENT PRIMARY KEY, type ENUM('card', 'sbp') NOT NULL, details TEXT NOT NULL, is_active TINYINT(1) DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS referral_earnings ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, referral_id INT NOT NULL, order_id INT NOT NULL, amount DECIMAL(15,2) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (referral_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS logs ( id INT AUTO_INCREMENT PRIMARY KEY, level ENUM('info', 'warning', 'error', 'debug') NOT NULL, message TEXT NOT NULL, context JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_level (level), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", // ✅ НОВЫЕ ТАБЛИЦЫ ДЛЯ РЕЗЕРВА BTC "CREATE TABLE IF NOT EXISTS btc_reserve ( id INT AUTO_INCREMENT PRIMARY KEY, total_btc DECIMAL(15,8) NOT NULL DEFAULT 0.00000000 COMMENT 'Общий резерв BTC', reserved_btc DECIMAL(15,8) NOT NULL DEFAULT 0.00000000 COMMENT 'Зарезервированный BTC', available_btc DECIMAL(15,8) GENERATED ALWAYS AS (total_btc - reserved_btc) STORED COMMENT 'Доступный BTC', last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_by VARCHAR(100) DEFAULT 'system' COMMENT 'Кто обновил', notes TEXT COMMENT 'Примечания к изменению' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS btc_reserve_history ( id INT AUTO_INCREMENT PRIMARY KEY, operation_type ENUM('add', 'subtract', 'reserve', 'unreserve', 'manual_set') NOT NULL, amount DECIMAL(15,8) NOT NULL, old_total DECIMAL(15,8) NOT NULL, new_total DECIMAL(15,8) NOT NULL, old_reserved DECIMAL(15,8) NOT NULL, new_reserved DECIMAL(15,8) NOT NULL, order_id INT NULL COMMENT 'Связанный заказ', admin_id VARCHAR(100) NULL COMMENT 'ID админа если ручное изменение', description TEXT COMMENT 'Описание операции', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL, INDEX idx_operation_type (operation_type), INDEX idx_created_at (created_at), INDEX idx_order_id (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ]; foreach ($tables as $sql) { try { $this->connection->exec($sql); } catch (PDOException $e) { $this->log('error', 'Table creation failed: ' . $e->getMessage(), ['sql' => $sql]); } } // Добавляем недостающие колонки если таблица уже существует $this->addMissingColumns(); $this->insertDefaultSettings(); } private function addMissingColumns() { try { // Проверяем структуру таблицы orders $stmt = $this->connection->query("DESCRIBE orders"); $columns = $stmt->fetchAll(PDO::FETCH_COLUMN); // Добавляем payment_details если её нет if (!in_array('payment_details', $columns)) { $this->connection->exec("ALTER TABLE orders ADD COLUMN payment_details TEXT AFTER user_wallet"); $this->log('info', 'Added payment_details column to orders table'); } // Добавляем assigned_wallet если её нет if (!in_array('assigned_wallet', $columns)) { $this->connection->exec("ALTER TABLE orders ADD COLUMN assigned_wallet VARCHAR(255) DEFAULT NULL AFTER payment_details"); $this->log('info', 'Added assigned_wallet column to orders table'); } // Проверяем и добавляем колонку btc_rate if (!in_array('btc_rate', $columns)) { $this->connection->exec("ALTER TABLE orders ADD COLUMN btc_rate DECIMAL(15,2) NOT NULL DEFAULT 0 AFTER status"); $this->log('info', 'Added btc_rate column to orders table'); } // Увеличиваем размер поля user_wallet для больших данных $this->connection->exec("ALTER TABLE orders MODIFY COLUMN user_wallet VARCHAR(500)"); // Проверяем структуру таблицы btc_wallets и добавляем недостающие колонки $btcStmt = $this->connection->query("DESCRIBE btc_wallets"); $btcColumns = $btcStmt->fetchAll(PDO::FETCH_COLUMN); if (!in_array('label', $btcColumns)) { $this->connection->exec("ALTER TABLE btc_wallets ADD COLUMN label VARCHAR(100) DEFAULT NULL AFTER address"); $this->log('info', 'Added label column to btc_wallets table'); } if (!in_array('is_active', $btcColumns)) { $this->connection->exec("ALTER TABLE btc_wallets ADD COLUMN is_active BOOLEAN DEFAULT TRUE AFTER label"); $this->log('info', 'Added is_active column to btc_wallets table'); } if (!in_array('updated_at', $btcColumns)) { $this->connection->exec("ALTER TABLE btc_wallets ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at"); $this->log('info', 'Added updated_at column to btc_wallets table'); } } catch (PDOException $e) { $this->log('error', 'Failed to add missing columns: ' . $e->getMessage()); } } private function insertDefaultSettings() { $defaultSettings = [ ['buy_commission', '3.00'], ['sell_commission', '2.00'], ['min_amount', '2500'], ['max_amount', '1000000'], ['support_username', '@DarkMoneyFastSupport_bot'], ['admin_chat_id', '-1002720394905'], ['referral_percent', '1.00'], ['min_withdrawal', '2500'], ['system_btc_wallet', 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh'], ['usd_rub_fallback', '100.00'], ['rate_update_interval', '300'], ['binance_api_enabled', '1'], ['order_hash_secret', 'your_secret_key_change_this_123456'], ['btc_wallet_rotation_index', '0'], ['user_wallet_rotation_index', '0'], // ✅ НОВЫЕ НАСТРОЙКИ ДЛЯ РЕЗЕРВА ['btc_reserve_display_enabled', '1'], ['btc_reserve_auto_management', '1'], ['btc_reserve_min_threshold', '0.1'] ]; foreach ($defaultSettings as [$key, $value]) { $this->setSetting($key, $value); } // ✅ НОВОЕ: Инициализируем резерв если его нет $this->initializeBtcReserve(); } /** * ✅ НОВОЕ: Инициализация BTC резерва */ private function initializeBtcReserve() { try { $stmt = $this->connection->query("SELECT COUNT(*) as count FROM btc_reserve"); $count = $stmt->fetch()['count']; if ($count == 0) { // Создаем начальную запись резерва $stmt = $this->connection->prepare(" INSERT INTO btc_reserve (total_btc, reserved_btc, updated_by, notes) VALUES (0.00000000, 0.00000000, 'system', 'Initial reserve setup') "); $stmt->execute(); $this->log('info', 'BTC reserve initialized', ['initial_amount' => 0]); } } catch (PDOException $e) { $this->log('error', 'Failed to initialize BTC reserve: ' . $e->getMessage()); } } public function getConnection() { return $this->connection; } public function log($level, $message, $context = []) { try { $stmt = $this->connection->prepare( "INSERT INTO logs (level, message, context) VALUES (?, ?, ?)" ); $stmt->execute([$level, $message, json_encode($context)]); } catch (PDOException $e) { error_log("Logging failed: " . $e->getMessage()); } } // ✅ ИСПРАВЛЕННЫЙ метод createUser с правильной логикой назначения кошельков public function createUser($telegramId, $username = null, $firstName = null, $lastName = null, $referrerId = null) { try { // ✅ ИЗМЕНЕНО: Используем метод назначения уникального кошелька $personalWallet = $this->assignUniquePersonalWallet(); $referralLink = $this->generateReferralLink(); $stmt = $this->connection->prepare( "INSERT INTO users (telegram_id, username, first_name, last_name, btc_wallet, referrer_id, referral_link) VALUES (?, ?, ?, ?, ?, ?, ?)" ); $stmt->execute([$telegramId, $username, $firstName, $lastName, $personalWallet, $referrerId, $referralLink]); $userId = $this->connection->lastInsertId(); $this->log('info', 'User created with personal wallet', [ 'user_id' => $userId, 'telegram_id' => $telegramId, 'personal_wallet' => $personalWallet ]); return $userId; } catch (PDOException $e) { $this->log('error', 'User creation failed: ' . $e->getMessage(), ['telegram_id' => $telegramId]); return false; } } // ✅ НОВЫЙ метод для назначения уникального персонального кошелька private function assignUniquePersonalWallet() { try { // Получаем все доступные кошельки из базы $availableWallets = $this->getBtcWallets(); if (empty($availableWallets)) { // Fallback на системный кошелек из настроек $fallbackWallet = $this->getSetting('system_btc_wallet'); $this->log('warning', 'No wallets available for user assignment, using fallback', [ 'fallback_wallet' => $fallbackWallet ]); return $fallbackWallet; } // Ищем неиспользованный кошелек foreach ($availableWallets as $wallet) { // Проверяем, не назначен ли уже этот кошелек другому пользователю $stmt = $this->connection->prepare("SELECT COUNT(*) as count FROM users WHERE btc_wallet = ?"); $stmt->execute([$wallet['address']]); $isUsed = $stmt->fetch()['count'] > 0; if (!$isUsed) { // Нашли свободный кошелек $this->log('info', 'Assigned unused wallet to user', [ 'wallet_address' => $wallet['address'], 'wallet_label' => $wallet['label'] ?? 'No label', 'wallet_id' => $wallet['id'] ]); return $wallet['address']; } } // Все кошельки использованы, начинаем выдавать по кругу $this->log('info', 'All wallets are used, starting rotation assignment'); // Получаем текущий индекс ротации для переназначения $currentIndex = (int)$this->getSetting('user_wallet_rotation_index', 0); // Проверяем что индекс в допустимых пределах if ($currentIndex >= count($availableWallets)) { $currentIndex = 0; } $selectedWallet = $availableWallets[$currentIndex]; // Обновляем индекс для следующего пользователя $nextIndex = ($currentIndex + 1) % count($availableWallets); $this->setSetting('user_wallet_rotation_index', $nextIndex); $this->log('info', 'Assigned wallet from rotation (reuse)', [ 'wallet_address' => $selectedWallet['address'], 'wallet_label' => $selectedWallet['label'] ?? 'No label', 'current_index' => $currentIndex, 'next_index' => $nextIndex, 'total_wallets' => count($availableWallets), 'mode' => 'reuse_rotation' ]); return $selectedWallet['address']; } catch (Exception $e) { $this->log('error', 'Failed to assign unique personal wallet: ' . $e->getMessage()); // Fallback на системный кошелек return $this->getSetting('system_btc_wallet', 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh'); } } public function getUserByTelegramId($telegramId) { try { $stmt = $this->connection->prepare("SELECT * FROM users WHERE telegram_id = ?"); $stmt->execute([$telegramId]); return $stmt->fetch(); } catch (PDOException $e) { $this->log('error', 'Get user failed: ' . $e->getMessage(), ['telegram_id' => $telegramId]); return false; } } // ✅ НОВЫЙ метод getUserById для системы уведомлений public function getUserById($userId) { try { $stmt = $this->connection->prepare("SELECT * FROM users WHERE id = ?"); $stmt->execute([$userId]); $user = $stmt->fetch(); $this->log('debug', 'getUserById called', [ 'user_id' => $userId, 'found' => $user !== false, 'telegram_id' => $user['telegram_id'] ?? null ]); return $user; } catch (PDOException $e) { $this->log('error', 'Get user by ID failed: ' . $e->getMessage(), ['user_id' => $userId]); return false; } } public function updateUserBalance($userId, $balanceRub = null, $balanceBtc = null) { try { $updates = []; $params = []; if ($balanceRub !== null) { $updates[] = "balance_rub = balance_rub + ?"; $params[] = $balanceRub; } if ($balanceBtc !== null) { $updates[] = "balance_btc = balance_btc + ?"; $params[] = $balanceBtc; } if (empty($updates)) return false; $params[] = $userId; $stmt = $this->connection->prepare( "UPDATE users SET " . implode(', ', $updates) . " WHERE id = ?" ); return $stmt->execute($params); } catch (PDOException $e) { $this->log('error', 'Update balance failed: ' . $e->getMessage(), ['user_id' => $userId]); return false; } } // ✅ ИСПРАВЛЕННЫЙ МЕТОД createOrder с правильным управлением транзакциями public function createOrder($userId, $type, $amountBtc, $amountRub, $commission, $finalAmount, $userWallet, $paymentMethod, $btcRate) { try { $this->connection->beginTransaction(); // Подробное логирование входящих параметров $this->log('debug', 'CreateOrder - Starting with parameters', [ 'user_id' => $userId, 'type' => $type, 'amount_btc' => $amountBtc, 'amount_rub' => $amountRub, 'commission' => $commission, 'final_amount' => $finalAmount, 'user_wallet' => substr($userWallet, 0, 50) . '...', // Обрезаем для логов 'payment_method' => $paymentMethod, 'btc_rate' => $btcRate ]); // Проверяем что пользователь существует $userCheck = $this->connection->prepare("SELECT id FROM users WHERE id = ?"); $userCheck->execute([$userId]); if (!$userCheck->fetch()) { $this->log('error', 'User not found for order creation', ['user_id' => $userId]); throw new Exception("User not found"); } // ✅ ИСПРАВЛЕНО: Для покупки BTC проверяем резерв БЕЗ резервирования if ($type === 'buy') { $reserveEnabled = $this->getSetting('btc_reserve_auto_management', '1'); if ($reserveEnabled && !$this->checkReserveSufficiency($amountBtc)) { throw new Exception("Недостаточно BTC в резерве обменника"); } } // Определяем куда записывать данные в зависимости от типа заказа if ($type === 'sell') { // Для продажи: userWallet содержит реквизиты получателя $paymentDetails = $userWallet; $btcWallet = null; } else { // Для покупки: userWallet содержит BTC адрес пользователя $paymentDetails = null; $btcWallet = $userWallet; } // Создаем заказ с правильным разделением данных $sql = "INSERT INTO orders ( user_id, type, amount_btc, amount_rub, commission_percent, final_amount, user_wallet, payment_details, payment_method, btc_rate, status, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created', NOW())"; $stmt = $this->connection->prepare($sql); $params = [ $userId, $type, $amountBtc, $amountRub, $commission, $finalAmount, $btcWallet, // BTC кошелек для покупок, NULL для продаж $paymentDetails, // Реквизиты для продаж, NULL для покупок $paymentMethod, $btcRate ]; $result = $stmt->execute($params); if (!$result) { throw new Exception("Failed to execute INSERT statement"); } $orderId = $this->connection->lastInsertId(); if (!$orderId || $orderId == 0) { throw new Exception("Order creation failed - no ID returned"); } // ✅ ИСПРАВЛЕНО: Резервируем BTC ПОСЛЕ создания заказа внутри той же транзакции if ($type === 'buy' && $this->getSetting('btc_reserve_auto_management', '1')) { $this->reserveBtcInternal($orderId, $amountBtc); } $this->connection->commit(); $this->log('info', 'Order created successfully', [ 'order_id' => $orderId, 'user_id' => $userId, 'type' => $type, 'amount_btc' => $amountBtc, 'amount_rub' => $amountRub, 'reserve_applied' => ($type === 'buy') ]); return $orderId; } catch (Exception $e) { $this->connection->rollBack(); $this->log('error', 'Order creation failed: ' . $e->getMessage(), [ 'user_id' => $userId, 'type' => $type, 'amount_btc' => $amountBtc ]); return false; } } public function getOrder($orderId) { try { $this->log('debug', 'Getting order by ID', ['order_id' => $orderId]); $stmt = $this->connection->prepare("SELECT * FROM orders WHERE id = ?"); $stmt->execute([$orderId]); $order = $stmt->fetch(); $this->log('debug', 'Order retrieval result', [ 'order_id' => $orderId, 'found' => $order !== false, 'order_data' => $order ? [ 'id' => $order['id'], 'user_id' => $order['user_id'], 'type' => $order['type'], 'status' => $order['status'] ] : null ]); return $order; } catch (PDOException $e) { $this->log('error', 'Get order failed: ' . $e->getMessage(), ['order_id' => $orderId]); return false; } } // ✅ ИСПРАВЛЕННЫЙ метод updateOrderStatus с правильным управлением транзакциями public function updateOrderStatus($orderId, $status) { try { $this->connection->beginTransaction(); // Получаем данные заказа ДО обновления $order = $this->getOrder($orderId); if (!$order) { throw new Exception("Order not found"); } $oldStatus = $order['status']; $amountBtc = $order['amount_btc']; $orderType = $order['type']; // Обновляем статус заказа $stmt = $this->connection->prepare("UPDATE orders SET status = ? WHERE id = ?"); $result = $stmt->execute([$status, $orderId]); if (!$result) { throw new Exception("Failed to update order status"); } // ✅ ИСПРАВЛЕНО: Управляем резервом через внутренние методы без транзакций $reserveEnabled = $this->getSetting('btc_reserve_auto_management', '1'); if ($reserveEnabled) { if ($orderType === 'buy') { // Для покупки BTC if ($status === 'completed' && in_array($oldStatus, ['created', 'paid', 'confirmed'])) { // Заказ завершен - списываем с резерва $this->deductBtcFromReserveInternal($orderId, $amountBtc); } elseif ($status === 'cancelled' && in_array($oldStatus, ['created', 'paid', 'confirmed'])) { // Заказ отменен - снимаем резервирование $this->unreserveBtcInternal($orderId, $amountBtc); } } elseif ($orderType === 'sell' && $status === 'completed') { // Для продажи BTC - добавляем в резерв при завершении $this->addBtcToReserveInternal($orderId, $amountBtc); } } $this->connection->commit(); $this->log('info', 'Order status updated with reserve management', [ 'order_id' => $orderId, 'old_status' => $oldStatus, 'new_status' => $status, 'order_type' => $orderType, 'amount_btc' => $amountBtc, 'reserve_enabled' => $reserveEnabled, 'affected_rows' => $stmt->rowCount() ]); return true; } catch (Exception $e) { $this->connection->rollBack(); $this->log('error', 'Update order status failed: ' . $e->getMessage(), [ 'order_id' => $orderId, 'status' => $status ]); return false; } } /** * ✅ НОВЫЕ внутренние методы для работы с резервом БЕЗ транзакций */ private function unreserveBtcInternal($orderId, $amountBtc) { try { $currentReserve = $this->getBtcReserve(); $newReserved = max(0, $currentReserve['reserved_btc'] - $amountBtc); $stmt = $this->connection->prepare(" UPDATE btc_reserve SET reserved_btc = ?, updated_by = 'system', notes = ? ORDER BY id DESC LIMIT 1 "); $stmt->execute([$newReserved, "Снятие резервирования для заказа #$orderId"]); $this->addReserveHistoryInternal( 'unreserve', $amountBtc, $currentReserve['total_btc'], $currentReserve['total_btc'], $currentReserve['reserved_btc'], $newReserved, $orderId, 'system', "Снятие резервирования BTC для заказа #$orderId" ); $this->log('info', 'BTC unreserved for order (internal)', [ 'order_id' => $orderId, 'amount' => $amountBtc, 'new_reserved' => $newReserved ]); return true; } catch (Exception $e) { $this->log('error', 'Failed to unreserve BTC (internal): ' . $e->getMessage(), [ 'order_id' => $orderId, 'amount' => $amountBtc ]); throw $e; } } private function deductBtcFromReserveInternal($orderId, $amountBtc) { try { $currentReserve = $this->getBtcReserve(); $newTotal = max(0, $currentReserve['total_btc'] - $amountBtc); $newReserved = max(0, $currentReserve['reserved_btc'] - $amountBtc); $stmt = $this->connection->prepare(" UPDATE btc_reserve SET total_btc = ?, reserved_btc = ?, updated_by = 'system', notes = ? ORDER BY id DESC LIMIT 1 "); $stmt->execute([$newTotal, $newReserved, "Списание BTC по заказу #$orderId"]); $this->addReserveHistoryInternal( 'subtract', $amountBtc, $currentReserve['total_btc'], $newTotal, $currentReserve['reserved_btc'], $newReserved, $orderId, 'system', "Списание BTC по завершенному заказу #$orderId" ); $this->log('info', 'BTC deducted from reserve (internal)', [ 'order_id' => $orderId, 'amount' => $amountBtc, 'new_total' => $newTotal, 'new_reserved' => $newReserved ]); return true; } catch (Exception $e) { $this->log('error', 'Failed to deduct BTC from reserve (internal): ' . $e->getMessage(), [ 'order_id' => $orderId, 'amount' => $amountBtc ]); throw $e; } } private function addBtcToReserveInternal($orderId, $amountBtc) { try { $currentReserve = $this->getBtcReserve(); $newTotal = $currentReserve['total_btc'] + $amountBtc; $stmt = $this->connection->prepare(" UPDATE btc_reserve SET total_btc = ?, updated_by = 'system', notes = ? ORDER BY id DESC LIMIT 1 "); $stmt->execute([$newTotal, "Пополнение BTC по заказу #$orderId"]); $this->addReserveHistoryInternal( 'add', $amountBtc, $currentReserve['total_btc'], $newTotal, $currentReserve['reserved_btc'], $currentReserve['reserved_btc'], $orderId, 'system', "Пополнение BTC по завершенному заказу продажи #$orderId" ); $this->log('info', 'BTC added to reserve (internal)', [ 'order_id' => $orderId, 'amount' => $amountBtc, 'new_total' => $newTotal ]); return true; } catch (Exception $e) { $this->log('error', 'Failed to add BTC to reserve (internal): ' . $e->getMessage(), [ 'order_id' => $orderId, 'amount' => $amountBtc ]); throw $e; } } public function getUserOrders($userId, $status = null) { try { if ($status) { $stmt = $this->connection->prepare( "SELECT * FROM orders WHERE user_id = ? AND status = ? ORDER BY created_at DESC" ); $stmt->execute([$userId, $status]); } else { $stmt = $this->connection->prepare( "SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC" ); $stmt->execute([$userId]); } return $stmt->fetchAll(); } catch (PDOException $e) { $this->log('error', 'Get user orders failed: ' . $e->getMessage(), ['user_id' => $userId]); return []; } } // ✅ НОВЫЕ МЕТОДЫ ДЛЯ УПРАВЛЕНИЯ РЕЗЕРВОМ BTC /** * Получить текущий резерв BTC */ public function getBtcReserve() { try { $stmt = $this->connection->query(" SELECT total_btc, reserved_btc, available_btc, last_updated FROM btc_reserve ORDER BY id DESC LIMIT 1 "); $result = $stmt->fetch(); if (!$result) { // Если резерва нет, создаем его $this->initializeBtcReserve(); return [ 'total_btc' => 0.00000000, 'reserved_btc' => 0.00000000, 'available_btc' => 0.00000000, 'last_updated' => date('Y-m-d H:i:s') ]; } return $result; } catch (PDOException $e) { $this->log('error', 'Failed to get BTC reserve: ' . $e->getMessage()); return [ 'total_btc' => 0.00000000, 'reserved_btc' => 0.00000000, 'available_btc' => 0.00000000, 'last_updated' => date('Y-m-d H:i:s') ]; } } /** * Установить общий резерв BTC (только админ) */ public function setBtcReserve($totalBtc, $adminId = 'admin', $notes = '') { try { $this->connection->beginTransaction(); // Получаем текущий резерв $currentReserve = $this->getBtcReserve(); $currentTotal = $currentReserve['total_btc']; $currentReserved = $currentReserve['reserved_btc']; // Проверяем что новый total не меньше зарезервированного if ($totalBtc < $currentReserved) { throw new Exception("Общий резерв ($totalBtc) не может быть меньше зарезервированного ($currentReserved)"); } // Обновляем резерв $stmt = $this->connection->prepare(" UPDATE btc_reserve SET total_btc = ?, updated_by = ?, notes = ? ORDER BY id DESC LIMIT 1 "); $stmt->execute([$totalBtc, $adminId, $notes]); // Записываем в историю $this->addReserveHistory( 'manual_set', $totalBtc - $currentTotal, $currentTotal, $totalBtc, $currentReserved, $currentReserved, null, $adminId, "Ручная установка резерва. " . $notes ); $this->connection->commit(); $this->log('info', 'BTC reserve manually set', [ 'old_total' => $currentTotal, 'new_total' => $totalBtc, 'admin_id' => $adminId, 'notes' => $notes ]); return true; } catch (Exception $e) { $this->connection->rollBack(); $this->log('error', 'Failed to set BTC reserve: ' . $e->getMessage(), [ 'total_btc' => $totalBtc, 'admin_id' => $adminId ]); return false; } } /** * Зарезервировать BTC для заказа */ public function reserveBtc($orderId, $amountBtc) { try { $this->connection->beginTransaction(); $currentReserve = $this->getBtcReserve(); $availableBtc = $currentReserve['available_btc']; // Проверяем достаточность резерва if ($availableBtc < $amountBtc) { throw new Exception("Недостаточно BTC в резерве. Доступно: $availableBtc, требуется: $amountBtc"); } $newReserved = $currentReserve['reserved_btc'] + $amountBtc; // Обновляем резерв $stmt = $this->connection->prepare(" UPDATE btc_reserve SET reserved_btc = ?, updated_by = 'system', notes = ? ORDER BY id DESC LIMIT 1 "); $stmt->execute([$newReserved, "Резервирование для заказа #$orderId"]); // Записываем в историю $this->addReserveHistory( 'reserve', $amountBtc, $currentReserve['total_btc'], $currentReserve['total_btc'], $currentReserve['reserved_btc'], $newReserved, $orderId, 'system', "Резервирование BTC для заказа #$orderId" ); $this->connection->commit(); $this->log('info', 'BTC reserved for order', [ 'order_id' => $orderId, 'amount' => $amountBtc, 'new_reserved' => $newReserved, 'available_after' => $availableBtc - $amountBtc ]); return true; } catch (Exception $e) { $this->connection->rollBack(); $this->log('error', 'Failed to reserve BTC: ' . $e->getMessage(), [ 'order_id' => $orderId, 'amount' => $amountBtc ]); return false; } } /** * Снять резервирование BTC (отмена заказа) */ public function unreserveBtc($orderId, $amountBtc) { try { $this->connection->beginTransaction(); $currentReserve = $this->getBtcReserve(); $newReserved = max(0, $currentReserve['reserved_btc'] - $amountBtc); // Обновляем резерв $stmt = $this->connection->prepare(" UPDATE btc_reserve SET reserved_btc = ?, updated_by = 'system', notes = ? ORDER BY id DESC LIMIT 1 "); $stmt->execute([$newReserved, "Снятие резервирования для заказа #$orderId"]); // Записываем в историю $this->addReserveHistory( 'unreserve', $amountBtc, $currentReserve['total_btc'], $currentReserve['total_btc'], $currentReserve['reserved_btc'], $newReserved, $orderId, 'system', "Снятие резервирования BTC для заказа #$orderId" ); $this->connection->commit(); $this->log('info', 'BTC unreserved for order', [ 'order_id' => $orderId, 'amount' => $amountBtc, 'new_reserved' => $newReserved ]); return true; } catch (Exception $e) { $this->connection->rollBack(); $this->log('error', 'Failed to unreserve BTC: ' . $e->getMessage(), [ 'order_id' => $orderId, 'amount' => $amountBtc ]); return false; } } /** * Списать BTC с резерва (успешная покупка) */ public function deductBtcFromReserve($orderId, $amountBtc) { try { $this->connection->beginTransaction(); $currentReserve = $this->getBtcReserve(); $newTotal = max(0, $currentReserve['total_btc'] - $amountBtc); $newReserved = max(0, $currentReserve['reserved_btc'] - $amountBtc); // Обновляем резерв $stmt = $this->connection->prepare(" UPDATE btc_reserve SET total_btc = ?, reserved_btc = ?, updated_by = 'system', notes = ? ORDER BY id DESC LIMIT 1 "); $stmt->execute([$newTotal, $newReserved, "Списание BTC по заказу #$orderId"]); // Записываем в историю $this->addReserveHistory( 'subtract', $amountBtc, $currentReserve['total_btc'], $newTotal, $currentReserve['reserved_btc'], $newReserved, $orderId, 'system', "Списание BTC по завершенному заказу #$orderId" ); $this->connection->commit(); $this->log('info', 'BTC deducted from reserve', [ 'order_id' => $orderId, 'amount' => $amountBtc, 'new_total' => $newTotal, 'new_reserved' => $newReserved ]); return true; } catch (Exception $e) { $this->connection->rollBack(); $this->log('error', 'Failed to deduct BTC from reserve: ' . $e->getMessage(), [ 'order_id' => $orderId, 'amount' => $amountBtc ]); return false; } } /** * Добавить BTC в резерв (успешная продажа) */ public function addBtcToReserve($orderId, $amountBtc) { try { $this->connection->beginTransaction(); $currentReserve = $this->getBtcReserve(); $newTotal = $currentReserve['total_btc'] + $amountBtc; // Обновляем резерв $stmt = $this->connection->prepare(" UPDATE btc_reserve SET total_btc = ?, updated_by = 'system', notes = ? ORDER BY id DESC LIMIT 1 "); $stmt->execute([$newTotal, "Пополнение BTC по заказу #$orderId"]); // Записываем в историю $this->addReserveHistory( 'add', $amountBtc, $currentReserve['total_btc'], $newTotal, $currentReserve['reserved_btc'], $currentReserve['reserved_btc'], $orderId, 'system', "Пополнение BTC по завершенному заказу продажи #$orderId" ); $this->connection->commit(); $this->log('info', 'BTC added to reserve', [ 'order_id' => $orderId, 'amount' => $amountBtc, 'new_total' => $newTotal ]); return true; } catch (Exception $e) { $this->connection->rollBack(); $this->log('error', 'Failed to add BTC to reserve: ' . $e->getMessage(), [ 'order_id' => $orderId, 'amount' => $amountBtc ]); return false; } } /** * Запись в историю изменений резерва */ private function addReserveHistory($operationType, $amount, $oldTotal, $newTotal, $oldReserved, $newReserved, $orderId = null, $adminId = null, $description = '') { try { $stmt = $this->connection->prepare(" INSERT INTO btc_reserve_history (operation_type, amount, old_total, new_total, old_reserved, new_reserved, order_id, admin_id, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) "); $stmt->execute([ $operationType, $amount, $oldTotal, $newTotal, $oldReserved, $newReserved, $orderId, $adminId, $description ]); } catch (PDOException $e) { $this->log('error', 'Failed to add reserve history: ' . $e->getMessage()); } } /** * Получить историю изменений резерва */ public function getReserveHistory($limit = 50, $offset = 0) { try { $stmt = $this->connection->prepare(" SELECT rh.*, o.type as order_type, o.user_id, u.username FROM btc_reserve_history rh LEFT JOIN orders o ON rh.order_id = o.id LEFT JOIN users u ON o.user_id = u.id ORDER BY rh.created_at DESC LIMIT ? OFFSET ? "); $stmt->execute([$limit, $offset]); return $stmt->fetchAll(); } catch (PDOException $e) { $this->log('error', 'Failed to get reserve history: ' . $e->getMessage()); return []; } } /** * Проверить достаточность резерва для заказа */ public function checkReserveSufficiency($amountBtc) { $reserve = $this->getBtcReserve(); return $reserve['available_btc'] >= $amountBtc; } // ✅ НОВЫЕ МЕТОДЫ ДЛЯ РОТАЦИИ BTC КОШЕЛЬКОВ /** * Получить все активные BTC кошельки для ротации */ public function getBtcWallets() { try { $stmt = $this->connection->prepare(" SELECT id, address, label FROM btc_wallets WHERE is_active = TRUE ORDER BY id ASC "); $stmt->execute(); $result = $stmt->fetchAll(PDO::FETCH_ASSOC); $this->log('debug', 'BTC wallets retrieved', [ 'count' => count($result), 'wallets' => $result ]); return $result; } catch (PDOException $e) { $this->log('error', 'Failed to get BTC wallets: ' . $e->getMessage()); return []; } } /** * Обновить назначенный кошелек для заказа */ public function updateOrderWallet($orderId, $walletAddress) { try { $stmt = $this->connection->prepare(" UPDATE orders SET assigned_wallet = ? WHERE id = ? "); $result = $stmt->execute([$walletAddress, $orderId]); $this->log('debug', 'Order wallet updated', [ 'order_id' => $orderId, 'wallet_address' => $walletAddress ]); return $result; } catch (PDOException $e) { $this->log('error', 'Failed to update order wallet: ' . $e->getMessage()); return false; } } // ✅ ОСТАВЛЯЕМ старые методы для совместимости /** * @deprecated Используйте getBtcWallets() вместо этого метода */ public function addBtcWallet($address) { try { $stmt = $this->connection->prepare("INSERT INTO btc_wallets (address, is_active) VALUES (?, TRUE)"); return $stmt->execute([$address]); } catch (PDOException $e) { $this->log('error', 'Add BTC wallet failed: ' . $e->getMessage(), ['address' => $address]); return false; } } /** * @deprecated Метод больше не используется в новой логике */ public function getAvailableBtcWallet() { try { $stmt = $this->connection->prepare("SELECT address FROM btc_wallets WHERE is_used = 0 LIMIT 1"); $stmt->execute(); $wallet = $stmt->fetch(); return $wallet ? $wallet['address'] : $this->generateRandomBtcWallet(); } catch (PDOException $e) { $this->log('error', 'Get available BTC wallet failed: ' . $e->getMessage()); return $this->generateRandomBtcWallet(); } } private function generateRandomBtcWallet() { // Генерируем случайный BTC адрес для демо (в продакшене используйте реальные кошельки) return 'bc1q' . bin2hex(random_bytes(20)); } /** * @deprecated Метод больше не используется в новой логике */ public function markWalletAsUsed($address, $userId) { try { $stmt = $this->connection->prepare("UPDATE btc_wallets SET is_used = 1, user_id = ? WHERE address = ?"); return $stmt->execute([$userId, $address]); } catch (PDOException $e) { $this->log('error', 'Mark wallet as used failed: ' . $e->getMessage(), ['address' => $address]); return false; } } /** * ✅ ИСПРАВЛЕННАЯ версия reserveBtc без вложенных транзакций */ private function reserveBtcInternal($orderId, $amountBtc) { try { $currentReserve = $this->getBtcReserve(); $availableBtc = $currentReserve['available_btc']; // Проверяем достаточность резерва if ($availableBtc < $amountBtc) { throw new Exception("Недостаточно BTC в резерве. Доступно: $availableBtc, требуется: $amountBtc"); } $newReserved = $currentReserve['reserved_btc'] + $amountBtc; // Обновляем резерв БЕЗ собственной транзакции $stmt = $this->connection->prepare(" UPDATE btc_reserve SET reserved_btc = ?, updated_by = 'system', notes = ? ORDER BY id DESC LIMIT 1 "); $stmt->execute([$newReserved, "Резервирование для заказа #$orderId"]); // Записываем в историю БЕЗ собственной транзакции $this->addReserveHistoryInternal( 'reserve', $amountBtc, $currentReserve['total_btc'], $currentReserve['total_btc'], $currentReserve['reserved_btc'], $newReserved, $orderId, 'system', "Резервирование BTC для заказа #$orderId" ); $this->log('info', 'BTC reserved for order (internal)', [ 'order_id' => $orderId, 'amount' => $amountBtc, 'new_reserved' => $newReserved, 'available_after' => $availableBtc - $amountBtc ]); return true; } catch (Exception $e) { $this->log('error', 'Failed to reserve BTC (internal): ' . $e->getMessage(), [ 'order_id' => $orderId, 'amount' => $amountBtc ]); throw $e; // Перебрасываем исключение для обработки в родительской транзакции } } /** * ✅ ИСПРАВЛЕННАЯ версия addReserveHistory без транзакций */ private function addReserveHistoryInternal($operationType, $amount, $oldTotal, $newTotal, $oldReserved, $newReserved, $orderId = null, $adminId = null, $description = '') { try { $stmt = $this->connection->prepare(" INSERT INTO btc_reserve_history (operation_type, amount, old_total, new_total, old_reserved, new_reserved, order_id, admin_id, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) "); $stmt->execute([ $operationType, $amount, $oldTotal, $newTotal, $oldReserved, $newReserved, $orderId, $adminId, $description ]); } catch (PDOException $e) { $this->log('error', 'Failed to add reserve history (internal): ' . $e->getMessage()); throw $e; } } // Настройки public function setSetting($key, $value) { try { $stmt = $this->connection->prepare( "INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)" ); return $stmt->execute([$key, $value]); } catch (PDOException $e) { $this->log('error', 'Set setting failed: ' . $e->getMessage(), ['key' => $key]); return false; } } public function getSetting($key, $default = null) { try { $stmt = $this->connection->prepare("SELECT setting_value FROM settings WHERE setting_key = ?"); $stmt->execute([$key]); $result = $stmt->fetch(); return $result ? $result['setting_value'] : $default; } catch (PDOException $e) { $this->log('error', 'Get setting failed: ' . $e->getMessage(), ['key' => $key]); return $default; } } // Реферальная система public function generateReferralLink() { return 'ref_' . bin2hex(random_bytes(8)); } public function getUserByReferralLink($link) { try { $stmt = $this->connection->prepare("SELECT id FROM users WHERE referral_link = ?"); $stmt->execute([$link]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } catch (PDOException $e) { $this->log('error', 'Get user by referral link failed: ' . $e->getMessage(), ['link' => $link]); return null; } } public function addReferralEarning($userId, $referralId, $orderId, $amount) { try { $stmt = $this->connection->prepare( "INSERT INTO referral_earnings (user_id, referral_id, order_id, amount) VALUES (?, ?, ?, ?)" ); $result = $stmt->execute([$userId, $referralId, $orderId, $amount]); if ($result) { $this->updateUserBalance($userId, $amount); $this->log('info', 'Referral earning added', [ 'user_id' => $userId, 'referral_id' => $referralId, 'amount' => $amount ]); } return $result; } catch (PDOException $e) { $this->log('error', 'Add referral earning failed: ' . $e->getMessage()); return false; } } public function getReferralEarnings($userId) { try { $stmt = $this->connection->prepare( "SELECT re.*, u.username as referral_username FROM referral_earnings re JOIN users u ON re.referral_id = u.id WHERE re.user_id = ? ORDER BY re.created_at DESC" ); $stmt->execute([$userId]); return $stmt->fetchAll(); } catch (PDOException $e) { $this->log('error', 'Get referral earnings failed: ' . $e->getMessage(), ['user_id' => $userId]); return []; } } // Платежные реквизиты public function addPaymentDetails($type, $details) { try { $stmt = $this->connection->prepare("INSERT INTO payment_details (type, details) VALUES (?, ?)"); return $stmt->execute([$type, $details]); } catch (PDOException $e) { $this->log('error', 'Add payment details failed: ' . $e->getMessage(), ['type' => $type]); return false; } } public function getActivePaymentDetails($type) { try { $stmt = $this->connection->prepare( "SELECT * FROM payment_details WHERE type = ? AND is_active = 1 ORDER BY RAND() LIMIT 1" ); $stmt->execute([$type]); return $stmt->fetch(); } catch (PDOException $e) { $this->log('error', 'Get payment details failed: ' . $e->getMessage(), ['type' => $type]); return false; } } // Статистика public function getStatistics() { try { $stats = []; // Общее количество пользователей $stmt = $this->connection->query("SELECT COUNT(*) as total_users FROM users"); $stats['total_users'] = $stmt->fetch()['total_users']; // Общий оборот $stmt = $this->connection->query("SELECT SUM(final_amount) as total_turnover FROM orders WHERE status = 'completed'"); $stats['total_turnover'] = $stmt->fetch()['total_turnover'] ?: 0; // Количество завершенных заказов $stmt = $this->connection->query("SELECT COUNT(*) as completed_orders FROM orders WHERE status = 'completed'"); $stats['completed_orders'] = $stmt->fetch()['completed_orders']; // Активные заказы $stmt = $this->connection->query("SELECT COUNT(*) as active_orders FROM orders WHERE status IN ('created', 'paid', 'confirmed')"); $stats['active_orders'] = $stmt->fetch()['active_orders']; return $stats; } catch (PDOException $e) { $this->log('error', 'Get statistics failed: ' . $e->getMessage()); return []; } } } // Singleton экземпляр $database = new Database(); ?>