@@ -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 | |||
); |