#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Smart Save Service für Intelectra WebShop Speichert nur echte Änderungen - keine DELETE * mehr! """ import json import sys import pymysql import logging from configparser import ConfigParser import os from datetime import datetime # Logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('/var/www/vhosts/intelectra.de/httpdocs/logs/item_save_service.log'), logging.StreamHandler() ] ) logger = logging.getLogger('item_save_service') class ItemSaveService: """Intelligenter Save-Service mit Change Detection""" def __init__(self): self.conn = self._connect_db() self.cursor = self.conn.cursor(pymysql.cursors.DictCursor) def _connect_db(self): """DB-Connection aus config.ini""" config_file = os.path.join(os.path.dirname(__file__), 'db_config.ini') config = ConfigParser() config.read(config_file) return pymysql.connect( host=config.get('database', 'host', fallback='localhost'), user=config.get('database', 'user', fallback='tbapy'), password=config.get('database', 'password', fallback='9%%0H32ryj_N9%%0H32ryj'), database=config.get('database', 'database', fallback='webshop-sql'), charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor ) def save_alternative_items(self, item_id, new_items): """ Speichert Alternativartikel - NUR Änderungen! Type 3 = Alternativartikel """ try: # 1. Aktuelle Items laden self.cursor.execute( "SELECT item_child_id FROM item_item_assign WHERE item_parent_id = %s AND type = 3 ORDER BY position", (item_id,) ) current_items = [row['item_child_id'] for row in self.cursor.fetchall()] # 2. Änderungen erkennen new_items = [int(x) for x in new_items if x] # Clean input to_delete = set(current_items) - set(new_items) to_add = set(new_items) - set(current_items) # Reihenfolge geändert? order_changed = (current_items != new_items) and not to_delete and not to_add changes_made = False # 3. Nur löschen was weg muss if to_delete: placeholders = ','.join(['%s'] * len(to_delete)) self.cursor.execute( f"DELETE FROM item_item_assign WHERE item_parent_id = %s AND item_child_id IN ({placeholders}) AND type = 3", [item_id] + list(to_delete) ) logger.info(f"Deleted {len(to_delete)} alternative items from item {item_id}") changes_made = True # 4. Nur hinzufügen was neu ist if to_add: # Position berechnen - bei erster Zuweisung start bei 1 if not current_items: start_pos = 1 else: start_pos = len(current_items) - len(to_delete) + 1 values = [] for idx, child_id in enumerate(to_add): values.append((item_id, child_id, 3, start_pos + idx)) self.cursor.executemany( "INSERT INTO item_item_assign (item_parent_id, item_child_id, type, position) VALUES (%s, %s, %s, %s)", values ) logger.info(f"Added {len(to_add)} alternative items to item {item_id}") changes_made = True # 5. Reihenfolge updaten wenn nötig if order_changed or to_add or to_delete: for idx, child_id in enumerate(new_items): self.cursor.execute( "UPDATE item_item_assign SET position = %s WHERE item_parent_id = %s AND item_child_id = %s AND type = 3", (idx + 1, item_id, child_id) ) logger.info(f"Updated positions for item {item_id}") changes_made = True self.conn.commit() return { 'success': True, 'changes': { 'deleted': len(to_delete), 'added': len(to_add), 'reordered': order_changed }, 'message': 'Changes saved' if changes_made else 'No changes detected' } except Exception as e: self.conn.rollback() logger.error(f"Error saving alternative items: {str(e)}") return { 'success': False, 'error': str(e) } def save_accessory_items(self, item_id, new_items): """Type 1 = Zubehör - gleiche Logik""" return self._save_related_items(item_id, new_items, 1) def save_spare_parts(self, item_id, new_items): """Type 2 = Ersatzteile - gleiche Logik""" return self._save_related_items(item_id, new_items, 2) def _save_related_items(self, item_id, new_items, item_type): """Generische Methode für alle Verknüpfungstypen""" type_names = {1: 'accessory', 2: 'spare_part', 3: 'alternative'} try: # Gleiche Logik wie save_alternative_items self.cursor.execute( "SELECT item_child_id FROM item_item_assign WHERE item_parent_id = %s AND type = %s ORDER BY position", (item_id, item_type) ) current_items = [row['item_child_id'] for row in self.cursor.fetchall()] new_items = [int(x) for x in new_items if x] to_delete = set(current_items) - set(new_items) to_add = set(new_items) - set(current_items) if to_delete: placeholders = ','.join(['%s'] * len(to_delete)) self.cursor.execute( f"DELETE FROM item_item_assign WHERE item_parent_id = %s AND item_child_id IN ({placeholders}) AND type = %s", [item_id] + list(to_delete) + [item_type] ) if to_add: # Position berechnen - bei erster Zuweisung start bei 1 if not current_items: start_pos = 1 else: start_pos = len(current_items) - len(to_delete) + 1 values = [(item_id, child_id, item_type, start_pos + idx) for idx, child_id in enumerate(to_add)] self.cursor.executemany( "INSERT INTO item_item_assign (item_parent_id, item_child_id, type, position) VALUES (%s, %s, %s, %s)", values ) # Update positions for idx, child_id in enumerate(new_items): self.cursor.execute( "UPDATE item_item_assign SET position = %s WHERE item_parent_id = %s AND item_child_id = %s AND type = %s", (idx + 1, item_id, child_id, item_type) ) self.conn.commit() logger.info(f"Saved {type_names[item_type]} items: {len(to_delete)} deleted, {len(to_add)} added") return { 'success': True, 'type': type_names[item_type], 'changes': { 'deleted': len(to_delete), 'added': len(to_add) } } except Exception as e: self.conn.rollback() logger.error(f"Error saving {type_names[item_type]} items: {str(e)}") return {'success': False, 'error': str(e)} def close(self): """Cleanup""" if self.cursor: self.cursor.close() if self.conn: self.conn.close() def main(): """CLI Entry Point für PHP shell_exec()""" if len(sys.argv) < 2: print(json.dumps({'error': 'No data provided'})) sys.exit(1) try: # Parse input data = json.loads(sys.argv[1]) item_id = data.get('item_id') action = data.get('action', 'save_all') service = ItemSaveService() if action == 'save_alternatives': items = data.get('items', []) result = service.save_alternative_items(item_id, items) elif action == 'save_accessories': items = data.get('items', []) result = service.save_accessory_items(item_id, items) elif action == 'save_spare_parts': items = data.get('items', []) result = service.save_spare_parts(item_id, items) elif action == 'save_all': # Alle 3 Typen speichern results = {} if 'alternatives' in data: results['alternatives'] = service.save_alternative_items(item_id, data['alternatives']) if 'accessories' in data: results['accessories'] = service.save_accessory_items(item_id, data['accessories']) if 'spare_parts' in data: results['spare_parts'] = service.save_spare_parts(item_id, data['spare_parts']) result = {'success': True, 'results': results} else: result = {'error': f'Unknown action: {action}'} service.close() # Return JSON print(json.dumps(result)) except Exception as e: logger.error(f"Main error: {str(e)}") print(json.dumps({'error': str(e)})) sys.exit(1) if __name__ == '__main__': main()