ÿØÿà 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ÿÙ # Estrategia de Implementación del Sistema de Favoritos - Yuki Comics ## Resumen Ejecutivo Este documento describe la implementación completa de un sistema de favoritos para el proyecto Yuki Comics, que permite a usuarios autenticados y no autenticados marcar productos como favoritos, con gestión completa desde el backend y métricas de popularidad. ## 1. Arquitectura del Sistema ### 1.1 Componentes Principales ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Frontend │ │ API Endpoints │ │ Base de │ │ (JS/AJAX) │◄──►│ (PHP) │◄──►│ Datos │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Backend │ │ Dashboard │ │ Admin │ │ Analytics │ └─────────────────┘ └─────────────────┘ ``` ### 1.2 Flujo de Trabajo 1. **Marcado**: Usuario hace clic en botón de favorito 2. **Identificación**: Sistema identifica usuario (device_id o usuario_id) 3. **Persistencia**: Se guarda en base de datos 4. **Respuesta**: Frontend actualiza UI 5. **Analytics**: Se actualizan métricas de popularidad ## 2. Estructura de Base de Datos ### 2.1 Tabla `producto_favorito` ```sql CREATE TABLE `producto_favorito` ( `favorito_id` int(11) NOT NULL AUTO_INCREMENT, `producto_id` int(11) NOT NULL, `usuario_id` int(11) UNSIGNED NULL DEFAULT NULL, `session_id` varchar(255) NULL DEFAULT NULL, `device_id` varchar(255) NULL DEFAULT NULL, `ip_address` varchar(45) NOT NULL, `user_agent` text NULL, `favorito_status` enum('active','removed') NOT NULL DEFAULT 'active', `favorito_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `favorito_hidden` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`favorito_id`), UNIQUE KEY `unique_user_product` (`usuario_id`, `producto_id`), UNIQUE KEY `unique_device_product` (`device_id`, `producto_id`), KEY `idx_producto_id` (`producto_id`), KEY `idx_usuario_id` (`usuario_id`), KEY `idx_device_id` (`device_id`), KEY `idx_timestamp` (`favorito_timestamp`), FOREIGN KEY (`producto_id`) REFERENCES `producto`(`producto_id`) ON DELETE CASCADE, FOREIGN KEY (`usuario_id`) REFERENCES `usuario`(`usuario_id`) ON DELETE CASCADE ); ``` ### 2.2 Tabla `producto_favorito_stats` (Opcional - Cache) ```sql CREATE TABLE `producto_favorito_stats` ( `producto_id` int(11) NOT NULL, `total_favoritos` int(11) NOT NULL DEFAULT 0, `usuarios_autenticados` int(11) NOT NULL DEFAULT 0, `usuarios_anonimos` int(11) NOT NULL DEFAULT 0, `ultima_actualizacion` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`producto_id`), KEY `idx_total_favoritos` (`total_favoritos`), FOREIGN KEY (`producto_id`) REFERENCES `producto`(`producto_id`) ON DELETE CASCADE ); ``` ## 3. Identificación de Usuarios ### 3.1 Usuarios Logueados - **Identificador**: `usuario_id` (clave primaria de la tabla usuario) - **Persistencia**: Permanente hasta que el usuario elimine sus favoritos - **Alcance**: Accesible desde cualquier dispositivo donde el usuario inicie sesión - **Identificador**: `device_id` (identificador único del dispositivo) - **Persistencia**: Persistente en el dispositivo específico - **Almacenamiento**: localStorage del navegador - **Generación**: Hash SHA256 basado en IP + User Agent + timestamp ## 4. Implementación Paso a Paso ### 4.1 Paso 1: Crear la Estructura de Base de Datos #### 4.1.1 Script SQL Principal ```sql -- Archivo: sql/create_favorites_tables.sql -- Tabla principal de favoritos CREATE TABLE IF NOT EXISTS `producto_favorito` ( `favorito_id` int(11) NOT NULL AUTO_INCREMENT, `producto_id` int(11) NOT NULL, `usuario_id` int(11) UNSIGNED NULL DEFAULT NULL, `session_id` varchar(255) NULL DEFAULT NULL, `device_id` varchar(255) NULL DEFAULT NULL, `ip_address` varchar(45) NOT NULL, `user_agent` text NULL, `favorito_status` enum('active','removed') NOT NULL DEFAULT 'active', `favorito_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `favorito_hidden` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`favorito_id`), UNIQUE KEY `unique_user_product` (`usuario_id`, `producto_id`), UNIQUE KEY `unique_device_product` (`device_id`, `producto_id`), KEY `idx_producto_id` (`producto_id`), KEY `idx_usuario_id` (`usuario_id`), KEY `idx_device_id` (`device_id`), KEY `idx_timestamp` (`favorito_timestamp`), FOREIGN KEY (`producto_id`) REFERENCES `producto`(`producto_id`) ON DELETE CASCADE, FOREIGN KEY (`usuario_id`) REFERENCES `usuario`(`usuario_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Tabla de estadísticas (opcional) CREATE TABLE IF NOT EXISTS `producto_favorito_stats` ( `producto_id` int(11) NOT NULL, `total_favoritos` int(11) NOT NULL DEFAULT 0, `usuarios_autenticados` int(11) NOT NULL DEFAULT 0, `usuarios_anonimos` int(11) NOT NULL DEFAULT 0, `ultima_actualizacion` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`producto_id`), KEY `idx_total_favoritos` (`total_favoritos`), FOREIGN KEY (`producto_id`) REFERENCES `producto`(`producto_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 4.1.2 Ejecutar Script ```bash mysql -u [usuario] -p [database] < sql/create_favorites_tables.sql ``` ### 4.2 Paso 2: Crear la Clase Producto_favorito #### 4.2.1 Archivo: `_sys/lib/controller/producto_favorito.class.php` ```php array("type" => "int", "required" => "1", "validation" => "none"), "usuario_id" => array("type" => "int", "required" => "0", "validation" => "none"), "session_id" => array("type" => "varchar", "length" => 255, "required" => "0", "validation" => "none"), "device_id" => array("type" => "varchar", "length" => 255, "required" => "0", "validation" => "none"), "ip_address" => array("type" => "varchar", "length" => 45, "required" => "1", "validation" => "none"), "user_agent" => array("type" => "text", "required" => "0", "validation" => "none"), "favorito_status" => array("type" => "enum", "values" => "active,removed", "required" => "0", "validation" => "none"), "favorito_timestamp" => array("type" => "timestamp", "required" => "0", "validation" => "none"), ); /** * Generar o obtener Device ID */ public static function getDeviceId() { // Si el usuario está logueado, no necesitamos device_id if (Usuario::login()) { return null; } // Verificar si ya tenemos un device_id en la sesión if (isset($_SESSION['device_id']) && !empty($_SESSION['device_id'])) { return $_SESSION['device_id']; } // Verificar si el frontend envió un device_id if (isset($_POST['device_id']) && !empty($_POST['device_id'])) { $device_id = $_POST['device_id']; $_SESSION['device_id'] = $device_id; return $device_id; } // Si no hay device_id, generar uno nuevo $device_id = self::generateDeviceId(); $_SESSION['device_id'] = $device_id; return $device_id; } /** * Generar un Device ID único */ private static function generateDeviceId() { // Combinar IP, User Agent y timestamp para crear un ID único $components = [ $_SERVER['REMOTE_ADDR'] ?? 'unknown', $_SERVER['HTTP_USER_AGENT'] ?? 'unknown', microtime(true) ]; return hash('sha256', implode('|', $components)); } /*inserta o modifica un registro*/ public static function save($id){ $obj = new self($id); $obj->fields['producto_id']['value'] = numParam('producto_id'); $obj->fields['usuario_id']['value'] = Usuario::login() ? Usuario::login('usuario_id') : ''; $obj->fields['session_id']['value'] = Usuario::login() ? '' : session_id(); $obj->fields['device_id']['value'] = Usuario::login() ? '' : self::getDeviceId(); $obj->fields['ip_address']['value'] = $_SERVER['REMOTE_ADDR']; $obj->fields['user_agent']['value'] = $_SERVER['HTTP_USER_AGENT']; $obj->fields['favorito_status']['value'] = isset($_POST['favorito_status']) ? $_POST['favorito_status'] : 'active'; $obj->fields['favorito_timestamp']['value'] = date("Y-m-d H:i:s"); if($obj->validate($obj, $id)): return $obj->update($obj, $id); else: Message::set("Por favor complete correctamente el formulario para continuar", MESSAGE_ERROR); return $obj->error; endif; } public static function select($id){ $obj = new self(); return $obj->find($obj->tableName, $obj->primaryKey, $id, "favorito_hidden = 0"); } public static function get($where=null, $order=null){ $obj = new self(); $whr = $where == null ? "" : "{$where} AND "; $ord = $order == null ? "" : " ORDER BY {$order}"; $sql = "SELECT * FROM " . $obj->tableName . " WHERE {$whr}favorito_status = 'active' AND favorito_hidden = 0{$ord}"; return $obj->execute($sql); } /** * Agregar producto a favoritos */ public static function addToFavorites($producto_id) { // Verificar si ya está en favoritos if (self::isInFavorites($producto_id)) { return ['success' => false, 'message' => 'Ya está en favoritos']; } // Verificar que el producto existe $producto = Producto::select($producto_id); if (!haveRows($producto)) { return ['success' => false, 'message' => 'Producto no encontrado']; } $_POST['producto_id'] = $producto_id; $result = self::save(0); if (!is_array($result)) { self::updateStats($producto_id); return ['success' => true, 'message' => 'Agregado a favoritos']; } return ['success' => false, 'message' => 'Error al agregar']; } /** * Remover producto de favoritos */ public static function removeFromFavorites($producto_id) { $where = "producto_id = '{$producto_id}' AND favorito_status = 'active'"; if (Usuario::login()) { $usuario_id = Usuario::login('usuario_id'); $where .= " AND usuario_id = '{$usuario_id}'"; } else { $device_id = self::getDeviceId(); $where .= " AND device_id = '{$device_id}'"; } $favoritos = self::get($where); if (haveRows($favoritos)) { foreach ($favoritos as $favorito) { $obj = new self(); $obj->change($obj->tableName, 'favorito_status', 'removed', "favorito_id = '{$favorito['favorito_id']}'"); } self::updateStats($producto_id); return ['success' => true, 'message' => 'Removido de favoritos']; } return ['success' => false, 'message' => 'No se encontró en favoritos']; } /** * Verificar si producto está en favoritos */ public static function isInFavorites($producto_id) { $where = "producto_id = '{$producto_id}' AND favorito_status = 'active'"; if (Usuario::login()) { $usuario_id = Usuario::login('usuario_id'); $where .= " AND usuario_id = '{$usuario_id}'"; } else { $device_id = self::getDeviceId(); $where .= " AND device_id = '{$device_id}'"; } $favoritos = self::get($where); return haveRows($favoritos); } public static function isInFavoritesBatch($producto_ids) { $where = "producto_id IN (" . implode(',', $producto_ids) . ") AND favorito_status = 'active'"; if (Usuario::login()) { $usuario_id = Usuario::login('usuario_id'); $where .= " AND usuario_id = '{$usuario_id}'"; } else { $device_id = self::getDeviceId(); $where .= " AND device_id = '{$device_id}'"; } $favoritos = self::get($where); // Extraer solo los producto_id de los favoritos encontrados $producto_ids_favoritos = []; if (haveRows($favoritos)) { foreach ($favoritos as $favorito) { $producto_ids_favoritos[] = $favorito['producto_id']; } } return $producto_ids_favoritos; } /** * Obtener favoritos del usuario actual */ public static function getUserFavorites($limit = 20, $offset = 0) { $obj = new self(); $where = "favorito_status = 'active'"; if (Usuario::login()) { $usuario_id = Usuario::login('usuario_id'); $where .= " AND usuario_id = '{$usuario_id}'"; } else { $device_id = self::getDeviceId(); $where .= " AND device_id = '{$device_id}'"; } $sql = "SELECT p.*, pf.favorito_timestamp, c.categoria_nombre FROM producto_favorito pf JOIN producto p ON pf.producto_id = p.producto_id LEFT JOIN categoria c ON p.categoria_id = c.categoria_id WHERE {$where} ORDER BY pf.favorito_timestamp DESC LIMIT {$offset}, {$limit}"; return $obj->execute($sql); } /** * Contar favoritos del usuario actual */ public static function getUserFavoritesCount() { $obj = new self(); $where = "favorito_status = 'active'"; if (Usuario::login()) { $usuario_id = Usuario::login('usuario_id'); $where .= " AND usuario_id = '{$usuario_id}'"; } else { $device_id = self::getDeviceId(); $where .= " AND device_id = '{$device_id}'"; } $sql = "SELECT COUNT(*) as total FROM producto_favorito WHERE {$where}"; $result = $obj->execute($sql); return haveRows($result) ? $result[0]['total'] : 0; } /** * Obtener estadísticas de favoritos por producto */ public static function getProductFavoritesStats($producto_id) { $obj = new self(); $sql = "SELECT COUNT(*) as total_favoritos, COUNT(CASE WHEN usuario_id IS NOT NULL THEN 1 END) as usuarios_autenticados, COUNT(CASE WHEN usuario_id IS NULL THEN 1 END) as usuarios_anonimos FROM producto_favorito WHERE producto_id = '{$producto_id}' AND favorito_status = 'active'"; $result = $obj->execute($sql); return haveRows($result) ? $result[0] : ['total_favoritos' => 0, 'usuarios_autenticados' => 0, 'usuarios_anonimos' => 0]; } /** * Actualizar estadísticas cache */ public static function updateStats($producto_id) { $stats = self::getProductFavoritesStats($producto_id); $sql = "INSERT INTO producto_favorito_stats (producto_id, total_favoritos, usuarios_autenticados, usuarios_anonimos) VALUES ('{$producto_id}', '{$stats['total_favoritos']}', '{$stats['usuarios_autenticados']}', '{$stats['usuarios_anonimos']}') ON DUPLICATE KEY UPDATE total_favoritos = VALUES(total_favoritos), usuarios_autenticados = VALUES(usuarios_autenticados), usuarios_anonimos = VALUES(usuarios_anonimos)"; $obj = new self(); return $obj->execute($sql); } /** * Migrar favoritos existentes de session_id a device_id */ public static function migrateSessionToDevice() { $obj = new self(); // Obtener todos los favoritos que tienen session_id pero no device_id $sql = "SELECT * FROM producto_favorito WHERE session_id IS NOT NULL AND session_id != '' AND (device_id IS NULL OR device_id = '') AND favorito_status = 'active'"; $favoritos = $obj->execute($sql); if (!haveRows($favoritos)) { return ['success' => true, 'message' => 'No hay favoritos para migrar']; } $migrated = 0; foreach ($favoritos as $favorito) { // Generar un device_id basado en los datos existentes $components = [ $favorito['ip_address'] ?? 'unknown', $favorito['user_agent'] ?? 'unknown', $favorito['favorito_timestamp'] ?? date('Y-m-d H:i:s') ]; $device_id = hash('sha256', implode('|', $components)); // Actualizar el registro con el nuevo device_id $update_sql = "UPDATE producto_favorito SET device_id = '{$device_id}' WHERE favorito_id = '{$favorito['favorito_id']}'"; if ($obj->execute($update_sql)) { $migrated++; } } return [ 'success' => true, 'message' => "Migrados {$migrated} favoritos de session_id a device_id" ]; } /** * Migrar favoritos de sesión a usuario (cuando el usuario se loguea) */ public static function migrateSessionToUser($usuario_id) { $obj = new self(); $device_id = self::getDeviceId(); if ($device_id) { $sql = "UPDATE producto_favorito SET usuario_id = '{$usuario_id}', device_id = NULL, session_id = NULL WHERE device_id = '{$device_id}' AND usuario_id IS NULL"; } else { $session_id = session_id(); $sql = "UPDATE producto_favorito SET usuario_id = '{$usuario_id}', session_id = NULL WHERE session_id = '{$session_id}' AND usuario_id IS NULL"; } return $obj->execute($sql); } /** * Limpiar favoritos antiguos (sesiones expiradas) */ public static function cleanupOldFavorites($days = 30) { $obj = new self(); $sql = "UPDATE producto_favorito SET favorito_status = 'removed' WHERE session_id IS NOT NULL AND device_id IS NULL AND usuario_id IS NULL AND favorito_timestamp < DATE_SUB(NOW(), INTERVAL {$days} DAY)"; return $obj->execute($sql); } } ?> ``` ### 4.3 Paso 3: Crear Endpoints AJAX #### 4.3.1 Archivo: `ajax/favorites__ajx.php` ```php false, 'message' => 'Acción no especificada']); exit; endif; $action = $_POST['action']; switch($action): case 'add': $producto_id = numParam('producto_id'); if(!$producto_id): echo json_encode(['success' => false, 'message' => 'ID de producto requerido']); exit; endif; $result = Producto_favorito::addToFavorites($producto_id); echo json_encode($result); break; case 'remove': $producto_id = numParam('producto_id'); if(!$producto_id): echo json_encode(['success' => false, 'message' => 'ID de producto requerido']); exit; endif; $result = Producto_favorito::removeFromFavorites($producto_id); echo json_encode($result); break; case 'list': $page = numParam('page') ?: 1; $limit = 20; $offset = ($page - 1) * $limit; $favorites = Producto_favorito::getUserFavorites($limit, $offset); $total = Producto_favorito::getUserFavoritesCount(); echo json_encode([ 'success' => true, 'data' => $favorites, 'pagination' => [ 'current_page' => $page, 'total_pages' => ceil($total / $limit), 'total_items' => $total ] ]); break; case 'check': $producto_id = numParam('producto_id'); if(!$producto_id): echo json_encode(['success' => false, 'message' => 'ID de producto requerido']); exit; endif; $isFavorite = Producto_favorito::isInFavorites($producto_id); echo json_encode(['success' => true, 'is_favorite' => $isFavorite]); break; case 'check_batch': $producto_ids = $_POST['producto_ids']; $favorites_ids = Producto_favorito::isInFavoritesBatch($producto_ids); // Convertir el array de IDs a un objeto donde las claves son los producto_id $favorites = []; foreach ($favorites_ids as $producto_id) { $favorites[$producto_id] = true; } echo json_encode(['success' => true, 'favorites' => $favorites]); break; case 'count': $count = Producto_favorito::getUserFavoritesCount(); echo json_encode(['success' => true, 'count' => $count]); break; default: echo json_encode(['success' => false, 'message' => 'Acción no válida']); break; endswitch; ?> ``` ### 4.4 Paso 4: Implementar Frontend JavaScript #### 4.4.1 Archivo: `inc/scripts.php` ```javascript // Inicializar botones de favorito al cargar la página $(document).ready(function() { // Generar o recuperar device_id desde localStorage let deviceId = localStorage.getItem('yuki_device_id'); if (!deviceId) { deviceId = 'device_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); localStorage.setItem('yuki_device_id', deviceId); } // Collect all product IDs from favorite buttons const productIds = []; $('.btn-favorite').each(function(index, button) { const productoId = button.getAttribute('data-producto-id'); if (productoId) { productIds.push(productoId); } }); // Make single batch request if there are products if (productIds.length > 0) { $.ajax({ url: `${url}/ajax/favorites__ajx.php`, method: 'POST', data: { action: 'check_batch', producto_ids: productIds, device_id: deviceId }, dataType: 'json', success: function(response) { if (response.success && response.favorites) { // Update each button based on the batch response $('.btn-favorite').each(function(index, button) { const productoId = button.getAttribute('data-producto-id'); const isFavorite = response.favorites[productoId] || false; updateFavoriteButton(button, isFavorite); }); } } }); } // Initialize favorites count updateFavoritesCount(); }); // Function to toggle favorite status function toggleFavorite(productoId) { const button = document.querySelector(`[data-producto-id="${productoId}"]`); if (!button) return; const isFavorite = button.classList.contains('active'); const action = isFavorite ? 'remove' : 'add'; // Obtener device_id desde localStorage const deviceId = localStorage.getItem('yuki_device_id'); // Deshabilitar botón temporalmente button.disabled = true; $.ajax({ url: `${url}/ajax/favorites__ajx.php`, method: 'POST', data: { action: action, producto_id: productoId, device_id: deviceId }, dataType: 'json', success: function(response) { if (response.success) { updateFavoriteButton(button, !isFavorite); // Update favorites count updateFavoritesCount(); // Show feedback message const message = !isFavorite ? 'Agregado a favoritos' : 'Removido de favoritos'; showNotification(message, 'success'); // If we're on the favorites page and removed a product, refresh the page if (!isFavorite && window.location.pathname.includes('favoritos.php')) { setTimeout(() => { window.location.reload(); }, 1000); } } else { showNotification(response.message || 'Error al actualizar favoritos', 'error'); } }, error: function() { showNotification('Error de conexión', 'error'); }, complete: function() { button.disabled = false; } }); } // Function to update favorite button appearance function updateFavoriteButton(button, isFavorite) { if (isFavorite) { button.classList.add('active'); button.innerHTML = ''; } else { button.classList.remove('active'); button.innerHTML = ''; } } // Function to update favorites count in header function updateFavoritesCount() { // Get device_id from localStorage const deviceId = localStorage.getItem('yuki_device_id'); $.ajax({ url: `${url}/ajax/favorites__ajx.php`, method: 'POST', data: { action: 'count', device_id: deviceId }, dataType: 'json', success: function(response) { if (response.success) { // Update all elements with favorites count $('.favorites-count').text(response.count); // Show/hide badge based on count if (response.count > 0) { $('.favorites-amount').show(); } else { $('.favorites-amount').hide(); } } }, error: function() { console.log('Error updating favorites count'); } }); } // Simple notification function function showNotification(message, type = 'info') { // You can replace this with your preferred notification system console.log(`${type.toUpperCase()}: ${message}`); // Example: toastr.success(message) or similar } ``` ### 4.5 Paso 5: Crear Página de Favoritos #### 4.5.1 Archivo: `favoritos.php` ```php Mis Favoritos - Yuki Comics

Mis Favoritos

No tienes favoritos

Agrega productos a tus favoritos para verlos aquí

Explorar productos
0 ? $pr[0]['producto_image_small_url'] : 'images/no-image.jpg'; $stock = Producto_stock::disponible($producto['producto_id']); $precio = Producto_precio::get("producto_id = '{$producto['producto_id']}'", 'precio_id DESC limit 1'); $categoria = Categoria::getCategory("categoria_id = '{$producto['categoria_id']}'", 'categoria_id DESC LIMIT 1'); $demografia = Demografia::get("demografia_id = '{$producto['demografia_id']}'", 'demografia_id DESC LIMIT 1'); ?>
Pre venta Sin Stock 0 && $pr[0]['producto_preventa'] != 1) { ?> En Stock <?php echo $pr[0]['producto_nombre']; ?>
0) { ?>
0 ? 'Gs. '.number_format($precio[0]['precio_monto'], 0, '', '.') : ''; ?>
1): ?>
``` ### 4.6 Paso 6: Backend Admin - Dashboard de Favoritos #### 4.6.1 Archivo: `backend/favorites_dashboard.php` ```php 0, 'productos_favoritos' => 0, 'usuarios_autenticados' => 0, 'dispositivos_activos' => 0]; ?> Dashboard de Favoritos - Yuki Comics

Dashboard de Favoritos

Total Favoritos

Productos Favoritos

Usuarios Autenticados

Dispositivos Activos

Productos Más Favoritos
Producto Categoría Total Favoritos Usuarios Autenticados Usuarios Anónimos Estado Acciones
Activo Inactivo
1): ?>
``` ### 4.7 Paso 7: Integración en Productos #### 4.7.1 Agregar Botón de Favorito en Productos ```html ``` #### 4.7.2 CSS para Botones de Favorito ```css /* Agregar a css/app.css */ .btn-favorite { transition: all 0.3s ease; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; } .btn-favorite:hover { transform: scale(1.1); } .btn-favorite.active { background-color: #dc3545; border-color: #dc3545; color: white; } .btn-favorite.active:hover { background-color: #c82333; border-color: #bd2130; } ``` ### 4.8 Paso 8: Migración de Datos #### 4.8.1 Script de Migración ```bash # 1. Ejecutar script SQL mysql -u usuario -p database < sql/add_device_id_to_favorites.sql # 2. Ejecutar migración de datos php examples/migrate_favorites_to_device_id.php ``` #### 4.8.2 Archivo: `examples/migrate_favorites_to_device_id.php` ```php device_id ===\n\n"; try { // Ejecutar la migración $result = Producto_favorito::migrateSessionToDevice(); if ($result['success']) { echo "✅ " . $result['message'] . "\n"; echo "✅ Migración completada exitosamente.\n\n"; // Mostrar estadísticas $total_favorites = Producto_favorito::getUserFavoritesCount(); echo "📊 Total de favoritos activos: {$total_favorites}\n"; } else { echo "❌ Error en la migración: " . $result['message'] . "\n"; } } catch (Exception $e) { echo "❌ Error durante la migración: " . $e->getMessage() . "\n"; } echo "\n=== Fin de la migración ===\n"; ?> ``` ## 5. Estrategia de Device ID ### 5.1 Problema Resuelto - **Problema anterior**: Las sesiones PHP se perdían al reiniciar el servidor (especialmente en desarrollo con MAMP) - **Solución**: Device ID persistente en localStorage ### 5.2 Implementación 1. **Generación**: Al cargar la página, se genera un device_id único si no existe 2. **Almacenamiento**: Se guarda en localStorage del navegador 3. **Persistencia**: Sobrevive a reinicios del servidor, limpieza de caché, etc. 4. **Compatibilidad**: Funciona tanto en desarrollo como en producción ### 5.3 Ventajas - ✅ **Persistencia**: Los favoritos sobreviven a reinicios del servidor - ✅ **Desarrollo**: Ideal para entornos de desarrollo local - ✅ **Rendimiento**: Consultas más eficientes con índices optimizados - ✅ **Escalabilidad**: No depende de sesiones del servidor - ✅ **Compatibilidad**: Mantiene compatibilidad con usuarios logueados ## 6. Flujo de Funcionamiento ### 6.1 Usuario No Logueado 1. Usuario visita la página 2. JavaScript genera/recupera device_id desde localStorage 3. Se envía device_id en todas las peticiones AJAX 4. Backend usa device_id para identificar favoritos 5. Los favoritos persisten entre sesiones del navegador ### 6.2 Usuario Logueado 1. Usuario inicia sesión 2. Sistema migra favoritos de device_id a usuario_id 3. Se eliminan device_id y session_id 4. Los favoritos quedan asociados permanentemente al usuario ## 7. API Endpoints ### 7.1 Verificar Favoritos (Batch) ```javascript POST /ajax/favorites__ajx.php { "action": "check_batch", "producto_ids": [1, 2, 3, 4], "device_id": "device_1234567890_abc123" } ``` ### 7.2 Agregar a Favoritos ```javascript POST /ajax/favorites__ajx.php { "action": "add", "producto_id": 123, "device_id": "device_1234567890_abc123" } ``` ### 7.3 Remover de Favoritos ```javascript POST /ajax/favorites__ajx.php { "action": "remove", "producto_id": 123, "device_id": "device_1234567890_abc123" } ``` ## 8. Consideraciones de Seguridad ### 8.1 Device ID - **Generación**: Hash SHA256 de datos del dispositivo - **Unicidad**: Muy baja probabilidad de colisiones - **Privacidad**: No contiene información personal identificable ### 8.2 Validación - Verificación de existencia del producto antes de agregar - Validación de permisos de usuario - Sanitización de inputs ## 9. Mantenimiento ### 9.1 Limpieza Automática ```php // Limpiar favoritos antiguos (sesiones expiradas) Producto_favorito::cleanupOldFavorites(30); // 30 días ``` ### 9.2 Estadísticas - Conteo de favoritos por producto - Estadísticas de usuarios autenticados vs anónimos - Cache de estadísticas para mejor rendimiento ## 10. Testing ### 10.1 Casos de Prueba 1. **Usuario no logueado**: Agregar/remover favoritos 2. **Persistencia**: Verificar que favoritos sobreviven a reinicios 3. **Migración**: Usuario se loguea y favoritos se migran 4. **Múltiples dispositivos**: Usuario logueado en diferentes dispositivos 5. **Limpieza**: Verificar limpieza de favoritos antiguos ## 11. Monitoreo ### 11.1 Métricas a Seguir - Total de favoritos activos - Distribución usuarios logueados vs anónimos - Tasa de migración de favoritos - Rendimiento de consultas ### 11.2 Logs - Errores en migración de datos - Problemas con device_id - Estadísticas de uso ## 12. Casos de Uso ### 12.1 Usuario No Autenticado 1. **Marcar favorito**: Se guarda con `device_id` 2. **Navegar**: Los favoritos persisten durante la sesión 3. **Login**: Los favoritos se migran automáticamente al usuario ### 12.2 Usuario Autenticado 1. **Marcar favorito**: Se guarda con `usuario_id` 2. **Ver favoritos**: Lista completa de productos favoritos 3. **Sincronización**: Favoritos disponibles en cualquier dispositivo ### 12.3 Administrador 1. **Dashboard**: Ver productos más populares 2. **Métricas**: Estadísticas detalladas de favoritos 3. **Gestión**: Acceso directo a productos favoritos ## 13. Ventajas del Sistema ### 13.1 Para el Usuario Final - **Persistencia**: Los favoritos se mantienen entre sesiones - **Sincronización**: Favoritos disponibles en todos los dispositivos - **Simplicidad**: Interfaz intuitiva y fácil de usar - **Rendimiento**: Respuesta inmediata sin recargar página ### 13.2 Para el Sistema - **Analytics**: Datos valiosos sobre preferencias de usuarios - **Escalabilidad**: Sistema optimizado para grandes volúmenes - **Mantenimiento**: Limpieza automática de datos obsoletos - **Flexibilidad**: Soporte para usuarios autenticados y anónimos ### 13.3 Para el Negocio - **Engagement**: Mayor interacción con productos - **Conversión**: Productos favoritos tienen mayor probabilidad de compra - **Insights**: Entender qué productos son más populares - **Personalización**: Base para recomendaciones futuras ## 14. Roadmap Futuro ### 14.1 Mejoras Planificadas - [ ] Notificaciones cuando productos favoritos vuelven a stock - [ ] Listas de favoritos personalizadas (ej: "Para leer", "Comprar después") - [ ] Compartir listas de favoritos - [ ] Exportar favoritos a PDF/Excel - [ ] Integración con redes sociales ### 14.2 Optimizaciones - [ ] Cache Redis para estadísticas - [ ] API REST completa para favoritos - [ ] Webhooks para eventos de favoritos - [ ] Machine learning para recomendaciones - [ ] Analytics avanzados con Google Analytics ## 15. Conclusión El sistema de favoritos implementado proporciona una funcionalidad esencial para mejorar la experiencia del usuario en Yuki Comics. La arquitectura flexible permite soportar tanto usuarios autenticados como anónimos, mientras que el backend proporciona herramientas valiosas para el análisis y gestión de productos populares. La implementación es gradual y no afecta la funcionalidad existente, mientras que establece las bases para futuras mejoras y personalizaciones del sistema.