| @@ -1,2 +1,2 @@ | |||
| #Wed Aug 06 17:08:34 HKT 2025 | |||
| gradle.version=8.9 | |||
| #Mon Aug 11 14:31:25 HKT 2025 | |||
| gradle.version=8.8 | |||
| @@ -0,0 +1,26 @@ | |||
| --liquibase formatted sql | |||
| --changeset terence:77-create-embedding-table | |||
| --comment: Insert F&B related permissions and user authorities | |||
| --liquibase formatted sql | |||
| --changeset yourname:001-create-menu_item-table | |||
| --liquibase formatted sql | |||
| --changeset yourname:002-create-embedding-table | |||
| --comment: Create embedding table similar to i18n for multilingual vector storage | |||
| CREATE TABLE embedding ( | |||
| id INT AUTO_INCREMENT PRIMARY KEY, | |||
| table_name VARCHAR(50), | |||
| field_name VARCHAR(50), | |||
| record_id INT, | |||
| embedding_text TEXT, | |||
| embedding_vector JSON, | |||
| language VARCHAR(10), | |||
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |||
| modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |||
| created_by INT DEFAULT NULL, | |||
| modified_by INT DEFAULT NULL, | |||
| deleted BIT(1) DEFAULT b'0', | |||
| version INT DEFAULT 0 | |||
| ); | |||
| @@ -0,0 +1,52 @@ | |||
| package com.ffii.fhsmsc.modules.embedding.entity; | |||
| import com.ffii.core.entity.BaseEntity; | |||
| import jakarta.persistence.Column; | |||
| import jakarta.persistence.Entity; | |||
| import jakarta.persistence.Table; | |||
| import java.util.Date; | |||
| @Entity | |||
| @Table(name = "embedding") | |||
| public class embedding extends BaseEntity<Long> { | |||
| @Column(name = "table_name") | |||
| private String table_name; | |||
| @Column(name = "record_id") | |||
| private Long record_id; | |||
| @Column(name = "embedding_text") | |||
| private String embedding_text; | |||
| @Column(name = "embedding_vector") | |||
| private String embedding_vector; | |||
| @Column(name = "language") | |||
| private String language; | |||
| public String getTable_name() { | |||
| return table_name; | |||
| } | |||
| public void setTable_name(String table_name) { | |||
| this.table_name = table_name; | |||
| } | |||
| public Long getRecord_id() { | |||
| return record_id; | |||
| } | |||
| public void setRecord_id(Long record_id) { | |||
| this.record_id = record_id; | |||
| } | |||
| public String getEmbedding_text() { | |||
| return embedding_text; | |||
| } | |||
| public void setEmbedding_text(String embedding_text) { | |||
| this.embedding_text = embedding_text; | |||
| } | |||
| public String getEmbedding_vector() { | |||
| return embedding_vector; | |||
| } | |||
| public void setEmbedding_vector(String embedding_vector) { | |||
| this.embedding_vector = embedding_vector; | |||
| } | |||
| public String getLanguage() { | |||
| return language; | |||
| } | |||
| public void setLanguage(String language) { | |||
| this.language = language; | |||
| } | |||
| } | |||
| @@ -0,0 +1,6 @@ | |||
| package com.ffii.fhsmsc.modules.embedding.entity; | |||
| import com.ffii.core.support.AbstractRepository; | |||
| public interface embeddingRepository extends AbstractRepository<embedding, Long> { | |||
| } | |||
| @@ -0,0 +1,47 @@ | |||
| package com.ffii.fhsmsc.modules.embedding.req; | |||
| public class embeddingReq { | |||
| private Long id; | |||
| private String table_name; | |||
| private Long record_id; | |||
| private String embedding_text; | |||
| private String embedding_vector; | |||
| private String language; | |||
| public Long getId() { | |||
| return id; | |||
| } | |||
| public void setId(Long id) { | |||
| this.id = id; | |||
| } | |||
| public String getTable_name() { | |||
| return table_name; | |||
| } | |||
| public void setTable_name(String table_name) { | |||
| this.table_name = table_name; | |||
| } | |||
| public Long getRecord_id() { | |||
| return record_id; | |||
| } | |||
| public void setRecord_id(Long record_id) { | |||
| this.record_id = record_id; | |||
| } | |||
| public String getEmbedding_text() { | |||
| return embedding_text; | |||
| } | |||
| public void setEmbedding_text(String embedding_text) { | |||
| this.embedding_text = embedding_text; | |||
| } | |||
| public String getEmbedding_vector() { | |||
| return embedding_vector; | |||
| } | |||
| public void setEmbedding_vector(String embedding_vector) { | |||
| this.embedding_vector = embedding_vector; | |||
| } | |||
| public String getLanguage() { | |||
| return language; | |||
| } | |||
| public void setLanguage(String language) { | |||
| this.language = language; | |||
| } | |||
| } | |||
| @@ -0,0 +1,107 @@ | |||
| package com.ffii.fhsmsc.modules.embedding.service; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.transaction.annotation.Transactional; | |||
| import com.ffii.core.exception.InternalServerErrorException; | |||
| import com.ffii.core.support.AbstractBaseEntityService; | |||
| import com.ffii.core.support.JdbcDao; | |||
| import com.ffii.core.utils.BeanUtils; | |||
| import com.ffii.fhsmsc.modules.embedding.entity.embedding; | |||
| import com.ffii.fhsmsc.modules.embedding.entity.embeddingRepository; | |||
| import com.ffii.fhsmsc.modules.embedding.req.embeddingReq; | |||
| import jakarta.validation.Valid; | |||
| @Service | |||
| public class embeddingService extends AbstractBaseEntityService<embedding, Long, embeddingRepository>{ | |||
| private static final Logger logger = LoggerFactory.getLogger(embeddingService.class); | |||
| public embeddingService(JdbcDao jdbcDao, embeddingRepository repository) { | |||
| super(jdbcDao, repository); | |||
| } | |||
| public List<Map<String, Object>> search(Map<String, Object> args) { | |||
| logger.info("Search called with args: {}", args); | |||
| StringBuilder sql = new StringBuilder(); | |||
| // Check if we need to fetch records for embedding processing | |||
| if (args != null && args.containsKey("embedding_text")) { | |||
| sql.append("SELECT pi.id,pi.record_id, pi.embedding_text"); | |||
| } else if (args != null && args.containsKey("embedding_vector")) { | |||
| sql.append("SELECT pi.id, pi.record_id, pi.embedding_vector"); | |||
| } else { | |||
| // Normal case - return all fields except embeddings | |||
| sql.append("SELECT" | |||
| + " pi.id," | |||
| + " pi.table_name," | |||
| + " pi.record_id," | |||
| + " pi.embedding_text," | |||
| + " pi.embedding_vector," | |||
| + " pi.language"); | |||
| } | |||
| sql.append(" FROM embedding pi") | |||
| .append(" WHERE pi.deleted = 0"); // 使用 0,因为是 tinyint(1) | |||
| if (args != null) { | |||
| if (args.containsKey("table_name")) { | |||
| sql.append(" AND pi.table_name = :table_name"); | |||
| logger.info("Added table_name filter: {}", args.get("table_name")); | |||
| } | |||
| if (args.containsKey("id")) { | |||
| sql.append(" AND pi.id = :id"); | |||
| logger.info("Added id filter: {}", args.get("id")); | |||
| } | |||
| if (args.containsKey("language")) { | |||
| sql.append(" AND pi.language = :language"); | |||
| logger.info("Added language filter: {}", args.get("language")); | |||
| } | |||
| } | |||
| return jdbcDao.queryForList(sql.toString(), args); | |||
| } | |||
| @Transactional(rollbackFor = Exception.class) | |||
| public embedding saveOrUpdate(@Valid embeddingReq req) { | |||
| embedding instance; | |||
| if (req.getId() != null && req.getId() > 0) { | |||
| // 更新现有记录 | |||
| instance = find(req.getId()).orElseThrow(InternalServerErrorException::new); | |||
| // 只更新非空字段 | |||
| if (req.getTable_name() != null) { | |||
| instance.setTable_name(req.getTable_name()); | |||
| } | |||
| if (req.getRecord_id() != null) { | |||
| instance.setRecord_id(req.getRecord_id()); | |||
| } | |||
| if (req.getEmbedding_text() != null) { | |||
| instance.setEmbedding_text(req.getEmbedding_text()); | |||
| } | |||
| if (req.getEmbedding_vector() != null) { | |||
| instance.setEmbedding_vector(req.getEmbedding_vector()); | |||
| } | |||
| if (req.getLanguage() != null) { | |||
| instance.setLanguage(req.getLanguage()); | |||
| } | |||
| } else { | |||
| // 创建新记录 | |||
| instance = new embedding(); | |||
| BeanUtils.copyProperties(req, instance); | |||
| } | |||
| saveAndFlush(instance); | |||
| return instance; | |||
| } | |||
| } | |||
| @@ -0,0 +1,56 @@ | |||
| package com.ffii.fhsmsc.modules.embedding.web; | |||
| import java.util.Map; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import org.springframework.web.bind.ServletRequestBindingException; | |||
| import org.springframework.web.bind.annotation.GetMapping; | |||
| import org.springframework.web.bind.annotation.PostMapping; | |||
| import org.springframework.web.bind.annotation.RequestBody; | |||
| import org.springframework.web.bind.annotation.RequestMapping; | |||
| import org.springframework.web.bind.annotation.RestController; | |||
| import com.ffii.core.response.IdRes; | |||
| import com.ffii.core.response.RecordsRes; | |||
| import com.ffii.core.utils.CriteriaArgsBuilder; | |||
| import com.ffii.fhsmsc.modules.embedding.req.embeddingReq; | |||
| import com.ffii.fhsmsc.modules.embedding.service.embeddingService; | |||
| import jakarta.servlet.http.HttpServletRequest; | |||
| import jakarta.validation.Valid; | |||
| @RestController | |||
| @RequestMapping("/embedding") | |||
| public class embeddingController { | |||
| private static final Logger logger = LoggerFactory.getLogger(embeddingController.class); | |||
| private embeddingService embeddingService; | |||
| public embeddingController( | |||
| embeddingService embeddingService | |||
| ) { | |||
| this.embeddingService = embeddingService; | |||
| } | |||
| @GetMapping("/list") | |||
| public RecordsRes<Map<String, Object>> listJson(HttpServletRequest request) throws ServletRequestBindingException { | |||
| logger.info("Received list request with parameters: {}", request.getParameterMap()); | |||
| Map<String, Object> args = CriteriaArgsBuilder.withRequest(request) | |||
| .addString("table_name") | |||
| .addBoolean("embedding_text") // Add embedding_text parameter | |||
| .addBoolean("embedding_vector") | |||
| .addLong("record_id") | |||
| .addInteger("id") | |||
| .addString("language") | |||
| .build(); | |||
| logger.info("Built args: {}", args); | |||
| return new RecordsRes<>(embeddingService.search(args)); | |||
| } | |||
| @PostMapping("/save") | |||
| public IdRes saveOrUpdate(@RequestBody @Valid embeddingReq req) { | |||
| return new IdRes(embeddingService.saveOrUpdate(req).getId()); | |||
| } | |||
| } | |||
| @@ -55,10 +55,6 @@ public class food_nutrients extends BaseEntity<Long> { | |||
| private String target_age_category; | |||
| @Column(name = "pet") | |||
| private String pet; | |||
| @Column(name = "embedding_text") | |||
| private String embedding_text; | |||
| @Column(name = "embedding_vector") | |||
| private String embedding_vector; | |||
| @Column(name = "description") | |||
| private String description; | |||
| @@ -200,18 +196,7 @@ public class food_nutrients extends BaseEntity<Long> { | |||
| public void setPet(String pet) { | |||
| this.pet = pet; | |||
| } | |||
| public String getEmbedding_text() { | |||
| return embedding_text; | |||
| } | |||
| public void setEmbedding_text(String embedding_text) { | |||
| this.embedding_text = embedding_text; | |||
| } | |||
| public String getEmbedding_vector() { | |||
| return embedding_vector; | |||
| } | |||
| public void setEmbedding_vector(String embedding_vector) { | |||
| this.embedding_vector = embedding_vector; | |||
| } | |||
| public String getDescription() { | |||
| return description; | |||
| } | |||
| @@ -6,8 +6,12 @@ public class food_nutrientsReq { | |||
| private Long id; | |||
| @Column(name = "food_name") | |||
| private String food_name; | |||
| @Column | |||
| private String food_name_zh; | |||
| @Column(name = "brand") | |||
| private String brand; | |||
| @Column | |||
| private String brand_zh; | |||
| @Column(name = "size_kg") | |||
| private Double size_kg; | |||
| @Column(name = "protein_percent") | |||
| @@ -46,16 +50,30 @@ public class food_nutrientsReq { | |||
| private Integer calories_kcal_kg; | |||
| @Column(name = "food_type") | |||
| private String food_type; | |||
| @Column(name = "food_type_zh") | |||
| private String food_type_zh; | |||
| @Column(name = "target_age_category") | |||
| private String target_age_category; | |||
| @Column(name = "target_age_category_zh") | |||
| private String target_age_category_zh; | |||
| @Column(name = "pet") | |||
| private String pet; | |||
| @Column(name = "pet_zh") | |||
| private String pet_zh; | |||
| @Column(name = "embedding_text") | |||
| private String embedding_text; | |||
| @Column(name = "embedding_vector") | |||
| private String embedding_vector; | |||
| @Column(name = "description") | |||
| private String description; | |||
| @Column(name = "description_zh") | |||
| private String description_zh; | |||
| @Column(name = "embedding_text_zh") | |||
| private String embedding_text_zh; | |||
| @Column(name = "embedding_vector_zh") | |||
| private String embedding_vector_zh; | |||
| @Column(name = "language") | |||
| private String language; | |||
| public Long getId() { | |||
| return id; | |||
| @@ -219,4 +237,58 @@ public class food_nutrientsReq { | |||
| public void setDescription(String description) { | |||
| this.description = description; | |||
| } | |||
| public String getFood_name_zh() { | |||
| return food_name_zh; | |||
| } | |||
| public void setFood_name_zh(String food_name_zh) { | |||
| this.food_name_zh = food_name_zh; | |||
| } | |||
| public String getBrand_zh() { | |||
| return brand_zh; | |||
| } | |||
| public void setBrand_zh(String brand_zh) { | |||
| this.brand_zh = brand_zh; | |||
| } | |||
| public String getFood_type_zh() { | |||
| return food_type_zh; | |||
| } | |||
| public void setFood_type_zh(String food_type_zh) { | |||
| this.food_type_zh = food_type_zh; | |||
| } | |||
| public String getTarget_age_category_zh() { | |||
| return target_age_category_zh; | |||
| } | |||
| public void setTarget_age_category_zh(String target_age_category_zh) { | |||
| this.target_age_category_zh = target_age_category_zh; | |||
| } | |||
| public String getPet_zh() { | |||
| return pet_zh; | |||
| } | |||
| public void setPet_zh(String pet_zh) { | |||
| this.pet_zh = pet_zh; | |||
| } | |||
| public String getDescription_zh() { | |||
| return description_zh; | |||
| } | |||
| public void setDescription_zh(String description_zh) { | |||
| this.description_zh = description_zh; | |||
| } | |||
| public String getEmbedding_text_zh() { | |||
| return embedding_text_zh; | |||
| } | |||
| public void setEmbedding_text_zh(String embedding_text_zh) { | |||
| this.embedding_text_zh = embedding_text_zh; | |||
| } | |||
| public String getEmbedding_vector_zh() { | |||
| return embedding_vector_zh; | |||
| } | |||
| public void setEmbedding_vector_zh(String embedding_vector_zh) { | |||
| this.embedding_vector_zh = embedding_vector_zh; | |||
| } | |||
| public String getLanguage() { | |||
| return language; | |||
| } | |||
| public void setLanguage(String language) { | |||
| this.language = language; | |||
| } | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| package com.ffii.fhsmsc.modules.food_nutrients.service; | |||
| import java.util.HashMap; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| @@ -12,31 +13,45 @@ import com.ffii.core.exception.InternalServerErrorException; | |||
| import com.ffii.core.support.AbstractBaseEntityService; | |||
| import com.ffii.core.support.JdbcDao; | |||
| import com.ffii.core.utils.BeanUtils; | |||
| import com.ffii.fhsmsc.modules.embedding.req.embeddingReq; | |||
| import com.ffii.fhsmsc.modules.embedding.service.embeddingService; | |||
| import com.ffii.fhsmsc.modules.food_nutrients.entity.food_nutrients; | |||
| import com.ffii.fhsmsc.modules.food_nutrients.entity.food_nutrientsRepository; | |||
| import com.ffii.fhsmsc.modules.food_nutrients.req.food_nutrientsReq; | |||
| import com.ffii.fhsmsc.modules.i18n.req.i18nReq; | |||
| import com.ffii.fhsmsc.modules.i18n.service.i18nService; | |||
| import jakarta.validation.Valid; | |||
| @Service | |||
| public class food_nutrientsService extends AbstractBaseEntityService<food_nutrients, Long, food_nutrientsRepository>{ | |||
| public class food_nutrientsService extends AbstractBaseEntityService<food_nutrients, Long, food_nutrientsRepository> { | |||
| private static final Logger logger = LoggerFactory.getLogger(food_nutrientsService.class); | |||
| public food_nutrientsService(JdbcDao jdbcDao, food_nutrientsRepository repository) { | |||
| super(jdbcDao, repository); | |||
| } | |||
| private final i18nService i18nService; | |||
| private final embeddingService embeddingService; | |||
| public food_nutrientsService(JdbcDao jdbcDao, food_nutrientsRepository repository, i18nService i18nService, embeddingService embeddingService) { | |||
| super(jdbcDao, repository); | |||
| this.i18nService = i18nService; | |||
| this.embeddingService = embeddingService; | |||
| } | |||
| public List<Map<String, Object>> search(Map<String, Object> args) { | |||
| logger.info("Search called with args: {}", args); | |||
| StringBuilder sql = new StringBuilder(); | |||
| // Check if we need to fetch records for embedding processing | |||
| boolean useEmbeddingTable = false; | |||
| // Check if we need to fetch specific fields from the embedding table | |||
| if (args != null && args.containsKey("embedding_text")) { | |||
| sql.append("SELECT pi.id, pi.embedding_text"); | |||
| sql.append("SELECT pi.id, e.embedding_text"); | |||
| useEmbeddingTable = true; | |||
| } else if (args != null && args.containsKey("embedding_vector")) { | |||
| sql.append("SELECT pi.id, pi.embedding_vector"); | |||
| sql.append("SELECT pi.id, e.embedding_vector"); | |||
| useEmbeddingTable = true; | |||
| } else if (args != null && args.containsKey("embedding")) { | |||
| sql.append("SELECT pi.id, e.embedding_text, e.embedding_vector"); | |||
| useEmbeddingTable = true; | |||
| } else { | |||
| // Normal case - return all fields except embeddings | |||
| // Normal case - return all fields from food_nutrients | |||
| sql.append("SELECT" | |||
| + " pi.id," | |||
| + " pi.food_name," | |||
| @@ -64,10 +79,16 @@ public class food_nutrientsService extends AbstractBaseEntityService<food_nutrie | |||
| + " pi.pet," | |||
| + " pi.description"); | |||
| } | |||
| sql.append(" FROM food_nutrients pi") | |||
| .append(" WHERE pi.deleted = 0"); // 使用 0,因为是 tinyint(1) | |||
| sql.append(" FROM food_nutrients pi"); | |||
| if (useEmbeddingTable) { | |||
| sql.append(" LEFT JOIN embedding e ON e.table_name = 'food_nutrients' AND e.record_id = pi.id AND e.deleted = 0"); | |||
| if (args != null && args.containsKey("language")) { | |||
| sql.append(" AND e.language = :language"); | |||
| } | |||
| } | |||
| sql.append(" WHERE pi.deleted = 0"); | |||
| if (args != null) { | |||
| if (args.containsKey("food_name")) { | |||
| sql.append(" AND pi.food_name = :food_name"); | |||
| @@ -78,109 +99,399 @@ public class food_nutrientsService extends AbstractBaseEntityService<food_nutrie | |||
| logger.info("Added id filter: {}", args.get("id")); | |||
| } | |||
| } | |||
| sql.append(" ORDER BY pi.id"); | |||
| logger.info("Final SQL query: {}", sql.toString()); | |||
| List<Map<String, Object>> result = jdbcDao.queryForList(sql.toString(), args); | |||
| logger.info("Query returned {} results", result.size()); | |||
| // Apply i18n for food_nutrients fields if no embedding parameters are specified | |||
| if (args != null && args.containsKey("language") && args.get("language") != null && !useEmbeddingTable) { | |||
| String language = (String) args.get("language"); | |||
| logger.info("Adding i18n support for language: {}", language); | |||
| String[] translatableFields = { | |||
| "food_name", "brand", "food_type", "target_age_category", "pet", "description" | |||
| }; | |||
| for (Map<String, Object> record : result) { | |||
| Object idObj = record.get("id"); | |||
| Long id = null; | |||
| if (idObj instanceof Integer) { | |||
| id = ((Integer) idObj).longValue(); | |||
| } else if (idObj instanceof Long) { | |||
| id = (Long) idObj; | |||
| } | |||
| if (id != null) { | |||
| Map<String, Object> i18nArgs = new HashMap<>(); | |||
| i18nArgs.put("table_name", "food_nutrients"); | |||
| i18nArgs.put("record_id", id); | |||
| i18nArgs.put("language", language); | |||
| for (String field : translatableFields) { | |||
| i18nArgs.put("field_name", field); | |||
| List<Map<String, Object>> translations = i18nService.search(i18nArgs); | |||
| if (translations != null && !translations.isEmpty()) { | |||
| String translated = (String) translations.get(0).get("value"); | |||
| if (translated != null) { | |||
| record.put(field, translated); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| @Transactional(rollbackFor = Exception.class) | |||
| public food_nutrients saveOrUpdate(@Valid food_nutrientsReq req) { | |||
| food_nutrients instance; | |||
| if (req.getId() != null && req.getId() > 0) { | |||
| // 更新现有记录 | |||
| instance = find(req.getId()).orElseThrow(InternalServerErrorException::new); | |||
| // 只更新非空字段 | |||
| if (req.getFood_name() != null) { | |||
| instance.setFood_name(req.getFood_name()); | |||
| } | |||
| if (req.getBrand() != null) { | |||
| instance.setBrand(req.getBrand()); | |||
| } | |||
| if (req.getSize_kg() != null) { | |||
| instance.setSize_kg(req.getSize_kg()); | |||
| } | |||
| if (req.getProtein_percent() != null) { | |||
| instance.setProtein_percent(req.getProtein_percent()); | |||
| } | |||
| if (req.getFat_percent() != null) { | |||
| instance.setFat_percent(req.getFat_percent()); | |||
| } | |||
| if (req.getFibre_percent() != null) { | |||
| instance.setFibre_percent(req.getFibre_percent()); | |||
| } | |||
| if (req.getAsh_percent() != null) { | |||
| instance.setAsh_percent(req.getAsh_percent()); | |||
| } | |||
| if (req.getTaurine_percent() != null) { | |||
| instance.setTaurine_percent(req.getTaurine_percent()); | |||
| } | |||
| if (req.getVitamin_a_iu_kg() != null) { | |||
| instance.setVitamin_a_iu_kg(req.getVitamin_a_iu_kg()); | |||
| } | |||
| if (req.getVitamin_d_iu_kg() != null) { | |||
| instance.setVitamin_d_iu_kg(req.getVitamin_d_iu_kg()); | |||
| } | |||
| if (req.getVitamin_e_mg_kg() != null) { | |||
| instance.setVitamin_e_mg_kg(req.getVitamin_e_mg_kg()); | |||
| } | |||
| if (req.getPhosphorus_percent() != null) { | |||
| instance.setPhosphorus_percent(req.getPhosphorus_percent()); | |||
| } | |||
| if (req.getCalcium_percent() != null) { | |||
| instance.setCalcium_percent(req.getCalcium_percent()); | |||
| } | |||
| if (req.getMagnesium_percent() != null) { | |||
| instance.setMagnesium_percent(req.getMagnesium_percent()); | |||
| } | |||
| if (req.getMethionine_percent() != null) { | |||
| instance.setMethionine_percent(req.getMethionine_percent()); | |||
| } | |||
| if (req.getMoisture_percent() != null) { | |||
| instance.setMoisture_percent(req.getMoisture_percent()); | |||
| } | |||
| if (req.getOmega_3_mg_kg() != null) { | |||
| instance.setOmega_3_mg_kg(req.getOmega_3_mg_kg()); | |||
| } | |||
| if (req.getOmega_6_mg_kg() != null) { | |||
| instance.setOmega_6_mg_kg(req.getOmega_6_mg_kg()); | |||
| } | |||
| if (req.getVitamin_c_mg_kg() != null) { | |||
| instance.setVitamin_c_mg_kg(req.getVitamin_c_mg_kg()); | |||
| } | |||
| if (req.getCalories_kcal_kg() != null) { | |||
| instance.setCalories_kcal_kg(req.getCalories_kcal_kg()); | |||
| } | |||
| if (req.getFood_type() != null) { | |||
| instance.setFood_type(req.getFood_type()); | |||
| } | |||
| if (req.getTarget_age_category() != null) { | |||
| instance.setTarget_age_category(req.getTarget_age_category()); | |||
| } | |||
| if (req.getPet() != null) { | |||
| instance.setPet(req.getPet()); | |||
| } | |||
| if (req.getEmbedding_text() != null) { | |||
| instance.setEmbedding_text(req.getEmbedding_text()); | |||
| } | |||
| if (req.getEmbedding_vector() != null) { | |||
| instance.setEmbedding_vector(req.getEmbedding_vector()); | |||
| } | |||
| if (req.getDescription() != null) { | |||
| instance.setDescription(req.getDescription()); | |||
| } | |||
| } else { | |||
| // 创建新记录 | |||
| instance = new food_nutrients(); | |||
| BeanUtils.copyProperties(req, instance); | |||
| } | |||
| saveAndFlush(instance); | |||
| return instance; | |||
| } | |||
| } | |||
| public food_nutrients saveOrUpdate(@Valid food_nutrientsReq req) { | |||
| food_nutrients instance; | |||
| String targetLanguage = req.getLanguage(); // 新增:获取目标语言 | |||
| if (req.getId() != null && req.getId() > 0) { | |||
| instance = find(req.getId()).orElseThrow(InternalServerErrorException::new); | |||
| // 检查是否需要语言参数 | |||
| boolean needsLanguage = req.getFood_name() != null || req.getBrand() != null || | |||
| req.getFood_type() != null || req.getTarget_age_category() != null || | |||
| req.getPet() != null || req.getDescription() != null; | |||
| if (needsLanguage && (targetLanguage == null || targetLanguage.trim().isEmpty())) { | |||
| throw new IllegalArgumentException("Language parameter is required when updating translatable fields (food_name, brand, food_type, target_age_category, pet, description)"); | |||
| } | |||
| // 记录原始值用于检测变化 | |||
| String originalFoodName = instance.getFood_name(); | |||
| String originalBrand = instance.getBrand(); | |||
| if (req.getFood_name() != null) { | |||
| instance.setFood_name(req.getFood_name()); | |||
| } | |||
| if (req.getBrand() != null) { | |||
| instance.setBrand(req.getBrand()); | |||
| } | |||
| if (req.getSize_kg() != null) { | |||
| instance.setSize_kg(req.getSize_kg()); | |||
| } | |||
| if (req.getProtein_percent() != null) { | |||
| instance.setProtein_percent(req.getProtein_percent()); | |||
| } | |||
| if (req.getFat_percent() != null) { | |||
| instance.setFat_percent(req.getFat_percent()); | |||
| } | |||
| if (req.getFibre_percent() != null) { | |||
| instance.setFibre_percent(req.getFibre_percent()); | |||
| } | |||
| if (req.getAsh_percent() != null) { | |||
| instance.setAsh_percent(req.getAsh_percent()); | |||
| } | |||
| if (req.getTaurine_percent() != null) { | |||
| instance.setTaurine_percent(req.getTaurine_percent()); | |||
| } | |||
| if (req.getVitamin_a_iu_kg() != null) { | |||
| instance.setVitamin_a_iu_kg(req.getVitamin_a_iu_kg()); | |||
| } | |||
| if (req.getVitamin_d_iu_kg() != null) { | |||
| instance.setVitamin_d_iu_kg(req.getVitamin_d_iu_kg()); | |||
| } | |||
| if (req.getVitamin_e_mg_kg() != null) { | |||
| instance.setVitamin_e_mg_kg(req.getVitamin_e_mg_kg()); | |||
| } | |||
| if (req.getPhosphorus_percent() != null) { | |||
| instance.setPhosphorus_percent(req.getPhosphorus_percent()); | |||
| } | |||
| if (req.getCalcium_percent() != null) { | |||
| instance.setCalcium_percent(req.getCalcium_percent()); | |||
| } | |||
| if (req.getMagnesium_percent() != null) { | |||
| instance.setMagnesium_percent(req.getMagnesium_percent()); | |||
| } | |||
| if (req.getMethionine_percent() != null) { | |||
| instance.setMethionine_percent(req.getMethionine_percent()); | |||
| } | |||
| if (req.getMoisture_percent() != null) { | |||
| instance.setMoisture_percent(req.getMoisture_percent()); | |||
| } | |||
| if (req.getOmega_3_mg_kg() != null) { | |||
| instance.setOmega_3_mg_kg(req.getOmega_3_mg_kg()); | |||
| } | |||
| if (req.getOmega_6_mg_kg() != null) { | |||
| instance.setOmega_6_mg_kg(req.getOmega_6_mg_kg()); | |||
| } | |||
| if (req.getVitamin_c_mg_kg() != null) { | |||
| instance.setVitamin_c_mg_kg(req.getVitamin_c_mg_kg()); | |||
| } | |||
| if (req.getCalories_kcal_kg() != null) { | |||
| instance.setCalories_kcal_kg(req.getCalories_kcal_kg()); | |||
| } | |||
| if (req.getFood_type() != null) { | |||
| instance.setFood_type(req.getFood_type()); | |||
| } | |||
| if (req.getTarget_age_category() != null) { | |||
| instance.setTarget_age_category(req.getTarget_age_category()); | |||
| } | |||
| if (req.getPet() != null) { | |||
| instance.setPet(req.getPet()); | |||
| } | |||
| if (req.getDescription() != null) { | |||
| instance.setDescription(req.getDescription()); | |||
| } | |||
| saveAndFlush(instance); | |||
| Long recordId = instance.getId(); | |||
| final String table = "food_nutrients"; | |||
| // 更新i18n表 - 使用指定的语言 | |||
| if (targetLanguage != null) { | |||
| if (req.getFood_name() != null) upsertI18n(table, recordId, "food_name", targetLanguage, req.getFood_name()); | |||
| if (req.getBrand() != null) upsertI18n(table, recordId, "brand", targetLanguage, req.getBrand()); | |||
| if (req.getFood_type() != null) upsertI18n(table, recordId, "food_type", targetLanguage, req.getFood_type()); | |||
| if (req.getTarget_age_category() != null) upsertI18n(table, recordId, "target_age_category", targetLanguage, req.getTarget_age_category()); | |||
| if (req.getPet() != null) upsertI18n(table, recordId, "pet", targetLanguage, req.getPet()); | |||
| if (req.getDescription() != null) upsertI18n(table, recordId, "description", targetLanguage, req.getDescription()); | |||
| } | |||
| // 当food_name或brand有更新时,更新embedding | |||
| if (req.getFood_name() != null || req.getBrand() != null) { | |||
| String currentFoodName = req.getFood_name() != null ? req.getFood_name() : originalFoodName; | |||
| String currentBrand = req.getBrand() != null ? req.getBrand() : originalBrand; | |||
| updateEmbeddingForFoodChange(recordId, currentFoodName, currentBrand, targetLanguage); | |||
| } | |||
| } else { | |||
| // 新建记录 | |||
| if (targetLanguage == null || targetLanguage.trim().isEmpty()) { | |||
| throw new IllegalArgumentException("Language parameter is required when creating new food nutrient record"); | |||
| } | |||
| instance = new food_nutrients(); | |||
| BeanUtils.copyProperties(req, instance); | |||
| saveAndFlush(instance); | |||
| Long recordId = instance.getId(); | |||
| final String table = "food_nutrients"; | |||
| // 保存i18n翻译 | |||
| if (req.getFood_name() != null) upsertI18n(table, recordId, "food_name", targetLanguage, req.getFood_name()); | |||
| if (req.getBrand() != null) upsertI18n(table, recordId, "brand", targetLanguage, req.getBrand()); | |||
| if (req.getFood_type() != null) upsertI18n(table, recordId, "food_type", targetLanguage, req.getFood_type()); | |||
| if (req.getTarget_age_category() != null) upsertI18n(table, recordId, "target_age_category", targetLanguage, req.getTarget_age_category()); | |||
| if (req.getPet() != null) upsertI18n(table, recordId, "pet", targetLanguage, req.getPet()); | |||
| if (req.getDescription() != null) upsertI18n(table, recordId, "description", targetLanguage, req.getDescription()); | |||
| // 为新建记录创建embedding | |||
| if (req.getFood_name() != null || req.getBrand() != null) { | |||
| updateEmbeddingForFoodChange(recordId, req.getFood_name(), req.getBrand(), targetLanguage); | |||
| } | |||
| } | |||
| return instance; | |||
| } | |||
| /** | |||
| * 当food_name或brand发生变化时更新embedding表 | |||
| */ | |||
| private void updateEmbeddingForFoodChange(Long recordId, String foodName, String brand, String language) { | |||
| try { | |||
| logger.info("Starting updateEmbeddingForFoodChange - recordId: {}, foodName: {}, brand: {}, language: {}", | |||
| recordId, foodName, brand, language); | |||
| // 构建embedding文本 | |||
| StringBuilder embeddingText = new StringBuilder(); | |||
| // 添加food_name(如果传入的话) | |||
| if (foodName != null && !foodName.trim().isEmpty()) { | |||
| embeddingText.append(foodName); | |||
| logger.info("Added foodName to embedding text: '{}'", foodName); | |||
| } | |||
| // 从i18n表获取对应语言的brand值 | |||
| String brandFromI18n = getBrandFromI18n(recordId, language); | |||
| logger.info("Retrieved brand from i18n: '{}'", brandFromI18n); | |||
| if (brandFromI18n != null && !brandFromI18n.trim().isEmpty()) { | |||
| if (embeddingText.length() > 0) { | |||
| embeddingText.append(" "); | |||
| } | |||
| embeddingText.append(brandFromI18n); | |||
| logger.info("Added brandFromI18n to embedding text: '{}'", brandFromI18n); | |||
| } | |||
| // 移除这部分:不再添加传入的brand,避免重复 | |||
| // if (brand != null && !brand.trim().isEmpty()) { | |||
| // if (embeddingText.length() > 0) { | |||
| // embeddingText.append(" "); | |||
| // } | |||
| // embeddingText.append(brand); | |||
| // logger.info("Added brand to embedding text: '{}'", brand); | |||
| // } | |||
| logger.info("Final embedding text: '{}'", embeddingText.toString()); | |||
| if (embeddingText.length() > 0) { | |||
| // 查找现有的embedding记录 | |||
| Map<String, Object> eArgs = new HashMap<>(); | |||
| eArgs.put("table_name", "food_nutrients"); | |||
| eArgs.put("record_id", recordId); | |||
| eArgs.put("language", language); | |||
| List<Map<String, Object>> existing = embeddingService.search(eArgs); | |||
| logger.info("Found {} existing embedding records", existing.size()); | |||
| embeddingReq eReq = new embeddingReq(); | |||
| if (!existing.isEmpty()) { | |||
| Object idObj = existing.get(0).get("id"); | |||
| Long id = null; | |||
| if (idObj instanceof Integer) id = ((Integer) idObj).longValue(); | |||
| else if (idObj instanceof Long) id = (Long) idObj; | |||
| if (id != null) { | |||
| eReq.setId(id); | |||
| logger.info("Updating existing embedding record with ID: {}", id); | |||
| } | |||
| } else { | |||
| logger.info("Creating new embedding record"); | |||
| } | |||
| eReq.setTable_name("food_nutrients"); | |||
| eReq.setRecord_id(recordId); | |||
| eReq.setEmbedding_text(embeddingText.toString()); | |||
| eReq.setLanguage(language); | |||
| logger.info("Saving embedding with text: '{}'", eReq.getEmbedding_text()); | |||
| embeddingService.saveOrUpdate(eReq); | |||
| logger.info("Successfully updated embedding for record ID: {} with text: '{}' in language: {}", | |||
| recordId, embeddingText.toString(), language); | |||
| } else { | |||
| logger.warn("No embedding text generated for record ID: {}", recordId); | |||
| } | |||
| } catch (Exception e) { | |||
| logger.error("Error updating embedding for record ID: " + recordId, e); | |||
| // 不抛出异常,避免影响主流程 | |||
| } | |||
| } | |||
| /** | |||
| * 从i18n表获取指定记录的brand翻译值 | |||
| */ | |||
| private String getBrandFromI18n(Long recordId, String language) { | |||
| try { | |||
| logger.info("Getting brand from i18n for recordId: {}, language: {}", recordId, language); | |||
| Map<String, Object> i18nArgs = new HashMap<>(); | |||
| i18nArgs.put("table_name", "food_nutrients"); | |||
| i18nArgs.put("field_name", "brand"); | |||
| i18nArgs.put("record_id", recordId); | |||
| i18nArgs.put("language", language); | |||
| List<Map<String, Object>> translations = i18nService.search(i18nArgs); | |||
| logger.info("Found {} brand translations", translations.size()); | |||
| if (translations != null && !translations.isEmpty()) { | |||
| String translated = (String) translations.get(0).get("value"); | |||
| logger.info("Brand translation value: '{}'", translated); | |||
| return translated; | |||
| } else { | |||
| logger.info("No brand translation found"); | |||
| } | |||
| } catch (Exception e) { | |||
| logger.error("Error getting brand from i18n for record ID: " + recordId + ", language: " + language, e); | |||
| } | |||
| return null; | |||
| } | |||
| private void upsertI18n(String tableName, Long recordId, String fieldName, String language, String value) { | |||
| if (recordId == null || value == null) return; | |||
| Map<String, Object> findArgs = new HashMap<>(); | |||
| findArgs.put("table_name", tableName); | |||
| findArgs.put("field_name", fieldName); | |||
| findArgs.put("record_id", recordId); | |||
| findArgs.put("language", language); | |||
| List<Map<String, Object>> rows = i18nService.search(findArgs); | |||
| i18nReq ireq = new i18nReq(); | |||
| if (rows != null && !rows.isEmpty()) { | |||
| Object idObj = rows.get(0).get("id"); | |||
| Long id = null; | |||
| if (idObj instanceof Integer) id = ((Integer) idObj).longValue(); | |||
| else if (idObj instanceof Long) id = (Long) idObj; | |||
| if (id != null) ireq.setId(id); | |||
| } | |||
| ireq.setTable_name(tableName); | |||
| ireq.setField_name(fieldName); | |||
| ireq.setRecord_id(recordId); | |||
| ireq.setLanguage(language); | |||
| ireq.setValue(value); | |||
| i18nService.saveOrUpdate(ireq); | |||
| } | |||
| public List<Map<String, Object>> checkEmbeddingStatus(Map<String, Object> args) { | |||
| logger.info("Check embedding status called with args: {}", args); | |||
| StringBuilder sql = new StringBuilder(); | |||
| sql.append("SELECT" | |||
| + " fn.id," | |||
| + " e_en.embedding_text as embedding_text_en," | |||
| + " e_en.embedding_vector as embedding_vector_en," | |||
| + " e_zh.embedding_text as embedding_text_zh," | |||
| + " e_zh.embedding_vector as embedding_vector_zh"); | |||
| sql.append(" FROM food_nutrients fn") | |||
| .append(" LEFT JOIN embedding e_en ON e_en.table_name = 'food_nutrients' AND e_en.record_id = fn.id AND e_en.language = 'en' AND e_en.deleted = 0") | |||
| .append(" LEFT JOIN embedding e_zh ON e_zh.table_name = 'food_nutrients' AND e_zh.record_id = fn.id AND e_zh.language = 'zh' AND e_zh.deleted = 0") | |||
| .append(" WHERE fn.deleted = 0"); | |||
| if (args != null) { | |||
| if (args.containsKey("language")) { | |||
| String language = (String) args.get("language"); | |||
| if ("zh".equals(language)) { | |||
| sql = new StringBuilder(); | |||
| sql.append("SELECT" | |||
| + " fn.id," | |||
| + " e_zh.embedding_text as embedding_text_zh," | |||
| + " e_zh.embedding_vector as embedding_vector_zh"); | |||
| sql.append(" FROM food_nutrients fn") | |||
| .append(" LEFT JOIN embedding e_zh ON e_zh.table_name = 'food_nutrients' AND e_zh.record_id = fn.id AND e_zh.language = 'zh' AND e_zh.deleted = 0") | |||
| .append(" WHERE fn.deleted = 0"); | |||
| } else if ("en".equals(language)) { | |||
| sql = new StringBuilder(); | |||
| sql.append("SELECT" | |||
| + " fn.id," | |||
| + " e_en.embedding_text as embedding_text_en," | |||
| + " e_en.embedding_vector as embedding_vector_en"); | |||
| sql.append(" FROM food_nutrients fn") | |||
| .append(" LEFT JOIN embedding e_en ON e_en.table_name = 'food_nutrients' AND e_en.record_id = fn.id AND e_en.language = 'en' AND e_en.deleted = 0") | |||
| .append(" WHERE fn.deleted = 0"); | |||
| } | |||
| } | |||
| if (args.containsKey("id")) { | |||
| sql.append(" AND fn.id = :id"); | |||
| logger.info("Added id filter: {}", args.get("id")); | |||
| } | |||
| } | |||
| sql.append(" ORDER BY fn.id"); | |||
| logger.info("Final SQL query: {}", sql.toString()); | |||
| List<Map<String, Object>> result = jdbcDao.queryForList(sql.toString(), args); | |||
| logger.info("Query returned {} results", result.size()); | |||
| // Convert null values to false and non-null values to true | |||
| for (Map<String, Object> record : result) { | |||
| for (String key : record.keySet()) { | |||
| if (key.startsWith("embedding_")) { | |||
| Object value = record.get(key); | |||
| record.put(key, value != null && !value.toString().trim().isEmpty()); | |||
| } | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| } | |||
| @@ -37,9 +37,12 @@ public class food_nutrientsController { | |||
| logger.info("Received list request with parameters: {}", request.getParameterMap()); | |||
| Map<String, Object> args = CriteriaArgsBuilder.withRequest(request) | |||
| .addString("food_name") | |||
| .addBoolean("embedding") | |||
| .addBoolean("embedding_text") // Add embedding_text parameter | |||
| .addBoolean("embedding_vector") | |||
| .addInteger("id") | |||
| .addString("language") | |||
| .build(); | |||
| logger.info("Built args: {}", args); | |||
| return new RecordsRes<>(food_nutrientsService.search(args)); | |||
| @@ -50,5 +53,14 @@ public class food_nutrientsController { | |||
| return new IdRes(food_nutrientsService.saveOrUpdate(req).getId()); | |||
| } | |||
| @GetMapping("/embedding-status") | |||
| public RecordsRes<Map<String, Object>> checkEmbeddingStatus(HttpServletRequest request) throws ServletRequestBindingException { | |||
| logger.info("Received embedding status request with parameters: {}", request.getParameterMap()); | |||
| Map<String, Object> args = CriteriaArgsBuilder.withRequest(request) | |||
| .addString("language") | |||
| .addInteger("id") | |||
| .build(); | |||
| logger.info("Built args: {}", args); | |||
| return new RecordsRes<>(food_nutrientsService.checkEmbeddingStatus(args)); | |||
| } | |||
| } | |||
| @@ -0,0 +1,63 @@ | |||
| package com.ffii.fhsmsc.modules.i18n.enity; | |||
| import com.ffii.core.entity.BaseEntity; | |||
| import jakarta.persistence.Column; | |||
| import jakarta.persistence.Entity; | |||
| import jakarta.persistence.Table; | |||
| @Entity | |||
| @Table(name = "i18n") | |||
| public class i18n extends BaseEntity<Long> { | |||
| @Column(name = "table_name") | |||
| private String table_name; | |||
| @Column(name = "field_name") | |||
| private String field_name; | |||
| @Column(name = "record_id") | |||
| private Long record_id; | |||
| @Column(name = "language") | |||
| private String language; | |||
| @Column(name = "value") | |||
| private String value; | |||
| public String getTable_name() { | |||
| return table_name; | |||
| } | |||
| public void setTable_name(String table_name) { | |||
| this.table_name = table_name; | |||
| } | |||
| public String getField_name() { | |||
| return field_name; | |||
| } | |||
| public void setField_name(String field_name) { | |||
| this.field_name = field_name; | |||
| } | |||
| public Long getRecord_id() { | |||
| return record_id; | |||
| } | |||
| public void setRecord_id(Long record_id) { | |||
| this.record_id = record_id; | |||
| } | |||
| public String getLanguage() { | |||
| return language; | |||
| } | |||
| public void setLanguage(String language) { | |||
| this.language = language; | |||
| } | |||
| public String getValue() { | |||
| return value; | |||
| } | |||
| public void setValue(String value) { | |||
| this.value = value; | |||
| } | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| package com.ffii.fhsmsc.modules.i18n.enity; | |||
| import com.ffii.core.support.AbstractRepository; | |||
| public interface i18nRepository extends AbstractRepository<i18n, Long> { | |||
| } | |||
| @@ -0,0 +1,59 @@ | |||
| package com.ffii.fhsmsc.modules.i18n.req; | |||
| public class i18nReq { | |||
| private Long id; | |||
| private String table_name; | |||
| private String field_name; | |||
| private Long record_id; | |||
| private String language; | |||
| private String value; | |||
| public Long getId() { | |||
| return id; | |||
| } | |||
| public void setId(Long id) { | |||
| this.id = id; | |||
| } | |||
| public String getTable_name() { | |||
| return table_name; | |||
| } | |||
| public void setTable_name(String table_name) { | |||
| this.table_name = table_name; | |||
| } | |||
| public String getField_name() { | |||
| return field_name; | |||
| } | |||
| public void setField_name(String field_name) { | |||
| this.field_name = field_name; | |||
| } | |||
| public Long getRecord_id() { | |||
| return record_id; | |||
| } | |||
| public void setRecord_id(Long record_id) { | |||
| this.record_id = record_id; | |||
| } | |||
| public String getLanguage() { | |||
| return language; | |||
| } | |||
| public void setLanguage(String language) { | |||
| this.language = language; | |||
| } | |||
| public String getValue() { | |||
| return value; | |||
| } | |||
| public void setValue(String value) { | |||
| this.value = value; | |||
| } | |||
| } | |||
| @@ -0,0 +1,115 @@ | |||
| package com.ffii.fhsmsc.modules.i18n.service; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.transaction.annotation.Transactional; | |||
| import com.ffii.core.exception.InternalServerErrorException; | |||
| import com.ffii.core.support.AbstractBaseEntityService; | |||
| import com.ffii.core.support.JdbcDao; | |||
| import com.ffii.core.utils.BeanUtils; | |||
| import com.ffii.fhsmsc.modules.i18n.enity.i18n; | |||
| import com.ffii.fhsmsc.modules.i18n.enity.i18nRepository; | |||
| import com.ffii.fhsmsc.modules.i18n.req.i18nReq; | |||
| import jakarta.validation.Valid; | |||
| @Service | |||
| public class i18nService extends AbstractBaseEntityService<i18n, Long, i18nRepository>{ | |||
| private static final Logger logger = LoggerFactory.getLogger(i18nService.class); | |||
| public i18nService(JdbcDao jdbcDao, i18nRepository repository) { | |||
| super(jdbcDao, repository); | |||
| } | |||
| public List<Map<String, Object>> search(Map<String, Object> args) { | |||
| logger.info("Search called with args: {}", args); | |||
| StringBuilder sql = new StringBuilder(); | |||
| // Always select all fields needed for translation | |||
| sql.append("SELECT" | |||
| + " pi.id," | |||
| + " pi.table_name," | |||
| + " pi.field_name," | |||
| + " pi.record_id," | |||
| + " pi.language," | |||
| + " pi.value"); | |||
| sql.append(" FROM i18n pi") | |||
| .append(" WHERE pi.deleted = 0"); // 使用 0,因为是 tinyint(1) | |||
| if (args != null) { | |||
| if (args.containsKey("table_name")) { | |||
| sql.append(" AND pi.table_name = :table_name"); | |||
| logger.info("Added table_name filter: {}", args.get("table_name")); | |||
| } | |||
| if (args.containsKey("id")) { | |||
| sql.append(" AND pi.id = :id"); | |||
| logger.info("Added id filter: {}", args.get("id")); | |||
| } | |||
| if (args.containsKey("field_name")) { | |||
| sql.append(" AND pi.field_name = :field_name"); | |||
| logger.info("Added field_name filter: {}", args.get("field_name")); | |||
| } | |||
| if (args.containsKey("record_id")) { | |||
| sql.append(" AND pi.record_id = :record_id"); | |||
| logger.info("Added record_id filter: {}", args.get("record_id")); | |||
| } | |||
| if (args.containsKey("language")) { | |||
| sql.append(" AND pi.language = :language"); | |||
| logger.info("Added language filter: {}", args.get("language")); | |||
| } | |||
| if (args.containsKey("value")) { | |||
| sql.append(" AND pi.value = :value"); | |||
| logger.info("Added value filter: {}", args.get("value")); | |||
| } | |||
| } | |||
| sql.append(" ORDER BY pi.id"); | |||
| logger.info("Final SQL query: {}", sql.toString()); | |||
| List<Map<String, Object>> result = jdbcDao.queryForList(sql.toString(), args); | |||
| logger.info("Query returned {} results", result.size()); | |||
| return result; | |||
| } | |||
| @Transactional(rollbackFor = Exception.class) | |||
| public i18n saveOrUpdate(@Valid i18nReq req) { | |||
| i18n instance; | |||
| if (req.getId() != null && req.getId() > 0) { | |||
| // 更新现有记录 | |||
| instance = find(req.getId()).orElseThrow(InternalServerErrorException::new); | |||
| // 只更新非空字段 | |||
| if (req.getTable_name() != null) { | |||
| instance.setTable_name(req.getTable_name()); | |||
| } | |||
| if (req.getField_name() != null) { | |||
| instance.setField_name(req.getField_name()); | |||
| } | |||
| if (req.getRecord_id() != null) { | |||
| instance.setRecord_id(req.getRecord_id()); | |||
| } | |||
| if (req.getLanguage() != null) { | |||
| instance.setLanguage(req.getLanguage()); | |||
| } | |||
| if (req.getValue() != null) { | |||
| instance.setValue(req.getValue()); | |||
| } | |||
| } else { | |||
| // 创建新记录 | |||
| instance = new i18n(); | |||
| BeanUtils.copyProperties(req, instance); | |||
| } | |||
| saveAndFlush(instance); | |||
| return instance; | |||
| } | |||
| } | |||
| @@ -0,0 +1,55 @@ | |||
| package com.ffii.fhsmsc.modules.i18n.web; | |||
| import java.util.Map; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import org.springframework.web.bind.ServletRequestBindingException; | |||
| import org.springframework.web.bind.annotation.GetMapping; | |||
| import org.springframework.web.bind.annotation.PostMapping; | |||
| import org.springframework.web.bind.annotation.RequestBody; | |||
| import org.springframework.web.bind.annotation.RequestMapping; | |||
| import org.springframework.web.bind.annotation.RestController; | |||
| import com.ffii.core.response.IdRes; | |||
| import com.ffii.core.response.RecordsRes; | |||
| import com.ffii.core.utils.CriteriaArgsBuilder; | |||
| import com.ffii.fhsmsc.modules.i18n.req.i18nReq; | |||
| import com.ffii.fhsmsc.modules.i18n.service.i18nService; | |||
| import jakarta.servlet.http.HttpServletRequest; | |||
| import jakarta.validation.Valid; | |||
| @RestController | |||
| @RequestMapping("/i18n") | |||
| public class i18nController { | |||
| private static final Logger logger = LoggerFactory.getLogger(i18nController.class); | |||
| private i18nService i18nService; | |||
| public i18nController( | |||
| i18nService i18nService | |||
| ) { | |||
| this.i18nService = i18nService; | |||
| } | |||
| @GetMapping("/list") | |||
| public RecordsRes<Map<String, Object>> listJson(HttpServletRequest request) throws ServletRequestBindingException { | |||
| logger.info("Received list request with parameters: {}", request.getParameterMap()); | |||
| Map<String, Object> args = CriteriaArgsBuilder.withRequest(request) | |||
| .addString("table_name") | |||
| .addString("field_name") | |||
| .addLong("record_id") | |||
| .addString("language") | |||
| .addInteger("id") | |||
| .build(); | |||
| logger.info("Built args: {}", args); | |||
| return new RecordsRes<>(i18nService.search(args)); | |||
| } | |||
| @PostMapping("/save") | |||
| public IdRes saveOrUpdate(@RequestBody @Valid i18nReq req) { | |||
| return new IdRes(i18nService.saveOrUpdate(req).getId()); | |||
| } | |||
| } | |||
| @@ -37,6 +37,26 @@ public class pet_informationReq { | |||
| private String health_goal; | |||
| @Column(name = "type") | |||
| private String type; | |||
| @Column(name = "pet_name_zh") | |||
| private String pet_name_zh; | |||
| @Column(name = "gender_zh") | |||
| private String gender_zh; | |||
| @Column(name = "age_category_zh") | |||
| private String age_category_zh; | |||
| @Column(name = "breed_zh") | |||
| private String breed_zh; | |||
| @Column(name = "sterilization_zh") | |||
| private String sterilization_zh; | |||
| @Column(name = "activity_level_zh") | |||
| private String activity_level_zh; | |||
| @Column(name = "health_issues_zh") | |||
| private String health_issues_zh; | |||
| @Column(name = "family_health_history_zh") | |||
| private String family_health_history_zh; | |||
| @Column(name = "health_goal_zh") | |||
| private String health_goal_zh; | |||
| @Column(name = "type_zh") | |||
| private String type_zh; | |||
| public Long getId() { | |||
| return id; | |||
| @@ -150,4 +170,83 @@ public class pet_informationReq { | |||
| this.type = type; | |||
| } | |||
| public String getPet_name_zh() { | |||
| return pet_name_zh; | |||
| } | |||
| public void setPet_name_zh(String pet_name_zh) { | |||
| this.pet_name_zh = pet_name_zh; | |||
| } | |||
| public String getGender_zh() { | |||
| return gender_zh; | |||
| } | |||
| public void setGender_zh(String gender_zh) { | |||
| this.gender_zh = gender_zh; | |||
| } | |||
| public String getAge_category_zh() { | |||
| return age_category_zh; | |||
| } | |||
| public void setAge_category_zh(String age_category_zh) { | |||
| this.age_category_zh = age_category_zh; | |||
| } | |||
| public String getBreed_zh() { | |||
| return breed_zh; | |||
| } | |||
| public void setBreed_zh(String breed_zh) { | |||
| this.breed_zh = breed_zh; | |||
| } | |||
| public String getSterilization_zh() { | |||
| return sterilization_zh; | |||
| } | |||
| public void setSterilization_zh(String sterilization_zh) { | |||
| this.sterilization_zh = sterilization_zh; | |||
| } | |||
| public String getActivity_level_zh() { | |||
| return activity_level_zh; | |||
| } | |||
| public void setActivity_level_zh(String activity_level_zh) { | |||
| this.activity_level_zh = activity_level_zh; | |||
| } | |||
| public String getHealth_issues_zh() { | |||
| return health_issues_zh; | |||
| } | |||
| public void setHealth_issues_zh(String health_issues_zh) { | |||
| this.health_issues_zh = health_issues_zh; | |||
| } | |||
| public String getFamily_health_history_zh() { | |||
| return family_health_history_zh; | |||
| } | |||
| public void setFamily_health_history_zh(String family_health_history_zh) { | |||
| this.family_health_history_zh = family_health_history_zh; | |||
| } | |||
| public String getHealth_goal_zh() { | |||
| return health_goal_zh; | |||
| } | |||
| public void setHealth_goal_zh(String health_goal_zh) { | |||
| this.health_goal_zh = health_goal_zh; | |||
| } | |||
| public String getType_zh() { | |||
| return type_zh; | |||
| } | |||
| public void setType_zh(String type_zh) { | |||
| this.type_zh = type_zh; | |||
| } | |||
| } | |||
| @@ -15,51 +15,98 @@ import com.ffii.core.utils.BeanUtils; | |||
| import com.ffii.fhsmsc.modules.pet_information.enity.pet_information; | |||
| import com.ffii.fhsmsc.modules.pet_information.enity.pet_informationRepository; | |||
| import com.ffii.fhsmsc.modules.pet_information.req.pet_informationReq; | |||
| import com.ffii.fhsmsc.modules.i18n.service.i18nService; | |||
| import com.ffii.fhsmsc.modules.i18n.req.i18nReq; | |||
| import jakarta.validation.Valid; | |||
| import java.util.HashMap; | |||
| @Service | |||
| public class pet_informationService extends AbstractBaseEntityService<pet_information, Long, pet_informationRepository>{ | |||
| private static final Logger logger = LoggerFactory.getLogger(pet_informationService.class); | |||
| private final i18nService i18nService; | |||
| public pet_informationService(JdbcDao jdbcDao, pet_informationRepository repository) { | |||
| public pet_informationService(JdbcDao jdbcDao, pet_informationRepository repository, i18nService i18nService) { | |||
| super(jdbcDao, repository); | |||
| this.i18nService = i18nService; | |||
| } | |||
| public List<Map<String, Object>> search(Map<String, Object> args) { | |||
| logger.info("Search called with args: {}", args); | |||
| StringBuilder sql = new StringBuilder("SELECT" | |||
| + " pi.id," | |||
| + " pi.owner_id," | |||
| + " pi.pet_name," | |||
| + " pi.gender," | |||
| + " pi.age_category," | |||
| + " pi.birth_date," | |||
| + " pi.breed," | |||
| + " pi.weight_kg," | |||
| + " pi.sterilization," | |||
| + " pi.activity_level," | |||
| + " pi.health_issues," | |||
| + " pi.family_health_history," | |||
| + " pi.health_goal," | |||
| + " pi.type" | |||
| + " FROM pet_information pi" | |||
| + " WHERE pi.deleted = 0" // 使用 0,因为是 tinyint(1) | |||
| ); | |||
| if (args != null) { | |||
| if (args.containsKey("owner_id")) { | |||
| sql.append(" AND pi.owner_id = :owner_id"); | |||
| logger.info("Added owner_id filter: {}", args.get("owner_id")); | |||
| } | |||
| } | |||
| sql.append(" ORDER BY pi.id"); | |||
| logger.info("Final SQL query: {}", sql.toString()); | |||
| List<Map<String, Object>> result = jdbcDao.queryForList(sql.toString(), args); | |||
| logger.info("Query returned {} results", result.size()); | |||
| return result; | |||
| } | |||
| logger.info("Search called with args: {}", args); | |||
| StringBuilder sql = new StringBuilder("SELECT" | |||
| + " pi.id," | |||
| + " pi.owner_id," | |||
| + " pi.pet_name," | |||
| + " pi.gender," | |||
| + " pi.age_category," | |||
| + " pi.birth_date," | |||
| + " pi.breed," | |||
| + " pi.weight_kg," | |||
| + " pi.sterilization," | |||
| + " pi.activity_level," | |||
| + " pi.health_issues," | |||
| + " pi.family_health_history," | |||
| + " pi.health_goal," | |||
| + " pi.type" | |||
| + " FROM pet_information pi" | |||
| + " WHERE pi.deleted = 0"); // 使用 0,因为是 tinyint(1) | |||
| if (args != null) { | |||
| if (args.containsKey("owner_id")) { | |||
| sql.append(" AND pi.owner_id = :owner_id"); | |||
| logger.info("Added owner_id filter: {}", args.get("owner_id")); | |||
| } | |||
| } | |||
| sql.append(" ORDER BY pi.id"); | |||
| logger.info("Final SQL query: {}", sql.toString()); | |||
| List<Map<String, Object>> result = jdbcDao.queryForList(sql.toString(), args); | |||
| logger.info("Query returned {} results", result.size()); | |||
| // 如果指定了语言参数,则添加国际化支持 | |||
| if (args != null && args.containsKey("language") && args.get("language") != null) { | |||
| String language = (String) args.get("language"); | |||
| logger.info("Adding i18n support for language: {}", language); | |||
| String[] translatableFields = { | |||
| "pet_name", "gender", "age_category", "breed", "sterilization", | |||
| "activity_level", "health_issues", "family_health_history", "health_goal", "type" | |||
| }; | |||
| for (Map<String, Object> record : result) { | |||
| Object idObj = record.get("id"); | |||
| Long id = null; | |||
| if (idObj instanceof Integer) { | |||
| id = ((Integer) idObj).longValue(); | |||
| } else if (idObj instanceof Long) { | |||
| id = (Long) idObj; | |||
| } | |||
| if (id != null) { | |||
| // Get translations for each translatable field | |||
| Map<String, Object> i18nArgs = new HashMap<>(); | |||
| i18nArgs.put("table_name", "pet_information"); | |||
| i18nArgs.put("record_id", id); | |||
| i18nArgs.put("language", language); | |||
| for (String field : translatableFields) { | |||
| i18nArgs.put("field_name", field); | |||
| logger.info("i18nArgs for {}: {}", field, i18nArgs); | |||
| List<Map<String, Object>> translations = i18nService.search(i18nArgs); | |||
| logger.info("{} translations: {}", field, translations); | |||
| if (!translations.isEmpty()) { | |||
| String translatedValue = (String) translations.get(0).get("value"); | |||
| if (translatedValue != null) { | |||
| record.put(field, translatedValue); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| @Transactional(rollbackFor = Exception.class) | |||
| public pet_information saveOrUpdate(@Valid pet_informationReq req) { | |||
| @@ -126,6 +173,64 @@ public class pet_informationService extends AbstractBaseEntityService<pet_inform | |||
| } | |||
| saveAndFlush(instance); | |||
| Long recordId = instance.getId(); | |||
| final String table = "pet_information"; | |||
| // 英文(原始字段 -> en) | |||
| if (req.getPet_name() != null) upsertI18n(table, recordId, "pet_name", "en", req.getPet_name()); | |||
| if (req.getGender() != null) upsertI18n(table, recordId, "gender", "en", req.getGender()); | |||
| if (req.getAge_category() != null) upsertI18n(table, recordId, "age_category", "en", req.getAge_category()); | |||
| if (req.getBirth_date() != null) upsertI18n(table, recordId, "birth_date", "en", String.valueOf(req.getBirth_date().getTime())); | |||
| if (req.getBreed() != null) upsertI18n(table, recordId, "breed", "en", req.getBreed()); | |||
| if (req.getWeight_kg() != null) upsertI18n(table, recordId, "weight_kg", "en", String.valueOf(req.getWeight_kg())); | |||
| if (req.getSterilization() != null) upsertI18n(table, recordId, "sterilization", "en", req.getSterilization()); | |||
| if (req.getActivity_level() != null) upsertI18n(table, recordId, "activity_level", "en", req.getActivity_level()); | |||
| if (req.getHealth_issues() != null) upsertI18n(table, recordId, "health_issues", "en", req.getHealth_issues()); | |||
| if (req.getFamily_health_history() != null) upsertI18n(table, recordId, "family_health_history", "en", req.getFamily_health_history()); | |||
| if (req.getHealth_goal() != null) upsertI18n(table, recordId, "health_goal", "en", req.getHealth_goal()); | |||
| if (req.getType() != null) upsertI18n(table, recordId, "type", "en", req.getType()); | |||
| // 如需支持中文,请在 `pet_informationReq` 中新增同名 `_zh` 字段,并参照 food_nutrients 的写法 upsert 为语言 "zh" | |||
| if (req.getPet_name_zh() != null) upsertI18n(table, recordId, "pet_name", "zh", req.getPet_name_zh()); | |||
| if (req.getGender_zh() != null) upsertI18n(table, recordId, "gender", "zh", req.getGender_zh()); | |||
| if (req.getAge_category_zh() != null) upsertI18n(table, recordId, "age_category", "zh", req.getAge_category_zh()); | |||
| if (req.getBirth_date() != null) upsertI18n(table, recordId, "birth_date", "zh", String.valueOf(req.getBirth_date().getTime())); | |||
| if (req.getBreed_zh() != null) upsertI18n(table, recordId, "breed", "zh", req.getBreed_zh()); | |||
| if (req.getWeight_kg() != null) upsertI18n(table, recordId, "weight_kg", "zh", String.valueOf(req.getWeight_kg())); | |||
| if (req.getSterilization_zh() != null) upsertI18n(table, recordId, "sterilization", "zh", req.getSterilization_zh()); | |||
| if (req.getActivity_level_zh() != null) upsertI18n(table, recordId, "activity_level", "zh", req.getActivity_level_zh()); | |||
| if (req.getHealth_issues_zh() != null) upsertI18n(table, recordId, "health_issues", "zh", req.getHealth_issues_zh()); | |||
| if (req.getFamily_health_history_zh() != null) upsertI18n(table, recordId, "family_health_history", "zh", req.getFamily_health_history_zh()); | |||
| if (req.getHealth_goal_zh() != null) upsertI18n(table, recordId, "health_goal", "zh", req.getHealth_goal_zh()); | |||
| if (req.getType_zh() != null) upsertI18n(table, recordId, "type", "zh", req.getType_zh()); | |||
| return instance; | |||
| } | |||
| } | |||
| private void upsertI18n(String tableName, Long recordId, String fieldName, String language, String value) { | |||
| if (recordId == null || value == null) return; | |||
| Map<String, Object> findArgs = new HashMap<>(); | |||
| findArgs.put("table_name", tableName); | |||
| findArgs.put("field_name", fieldName); | |||
| findArgs.put("record_id", recordId); | |||
| findArgs.put("language", language); | |||
| List<Map<String, Object>> rows = i18nService.search(findArgs); | |||
| i18nReq ireq = new i18nReq(); | |||
| if (rows != null && !rows.isEmpty()) { | |||
| Object idObj = rows.get(0).get("id"); | |||
| Long id = null; | |||
| if (idObj instanceof Integer) id = ((Integer) idObj).longValue(); | |||
| else if (idObj instanceof Long) id = (Long) idObj; | |||
| if (id != null) ireq.setId(id); | |||
| } | |||
| ireq.setTable_name(tableName); | |||
| ireq.setField_name(fieldName); | |||
| ireq.setRecord_id(recordId); | |||
| ireq.setLanguage(language); | |||
| ireq.setValue(value); | |||
| i18nService.saveOrUpdate(ireq); | |||
| } | |||
| } | |||
| @@ -16,10 +16,10 @@ import com.ffii.core.response.RecordsRes; | |||
| import com.ffii.core.utils.CriteriaArgsBuilder; | |||
| import com.ffii.fhsmsc.modules.pet_information.req.pet_informationReq; | |||
| import com.ffii.fhsmsc.modules.pet_information.service.pet_informationService; | |||
| import jakarta.servlet.http.HttpServletRequest; | |||
| import jakarta.validation.Valid; | |||
| @RestController | |||
| @RequestMapping("/pet_information") | |||
| public class pet_informationController { | |||
| @@ -37,6 +37,7 @@ public class pet_informationController { | |||
| logger.info("Received list request with parameters: {}", request.getParameterMap()); | |||
| Map<String, Object> args = CriteriaArgsBuilder.withRequest(request) | |||
| .addInteger("owner_id") | |||
| .addString("language") | |||
| .build(); | |||
| logger.info("Built args: {}", args); | |||
| return new RecordsRes<>(pet_informationService.search(args)); | |||
| @@ -46,4 +47,6 @@ public class pet_informationController { | |||
| public IdRes saveOrUpdate(@RequestBody @Valid pet_informationReq req) { | |||
| return new IdRes(pet_informationService.saveOrUpdate(req).getId()); | |||
| } | |||
| } | |||
| @@ -0,0 +1,26 @@ | |||
| --liquibase formatted sql | |||
| --changeset terence:77-create-embedding-table | |||
| --comment: Insert F&B related permissions and user authorities | |||
| --liquibase formatted sql | |||
| --changeset yourname:001-create-menu_item-table | |||
| --liquibase formatted sql | |||
| --changeset yourname:002-create-embedding-table | |||
| --comment: Create embedding table similar to i18n for multilingual vector storage | |||
| CREATE TABLE embedding ( | |||
| id INT AUTO_INCREMENT PRIMARY KEY, | |||
| table_name VARCHAR(50), | |||
| field_name VARCHAR(50), | |||
| record_id INT, | |||
| embedding_text TEXT, | |||
| embedding_vector JSON, | |||
| language VARCHAR(10), | |||
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |||
| modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |||
| created_by INT DEFAULT NULL, | |||
| modified_by INT DEFAULT NULL, | |||
| deleted BIT(1) DEFAULT b'0', | |||
| version INT DEFAULT 0 | |||
| ); | |||