support pinyin search

This commit is contained in:
Timi
2026-08-21 13:51:26 +08:00
parent a523df6a07
commit acc3f05f12
10 changed files with 248 additions and 0 deletions
+5
View File
@@ -205,6 +205,11 @@
<artifactId>timi-utils</artifactId>
<version>0.0.13</version>
</dependency>
<dependency>
<groupId>me.majiajie</groupId>
<artifactId>tinypinyin</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>com.imyeyu.font.icon</groupId>
<artifactId>timi-icon-font</artifactId>
@@ -1,6 +1,8 @@
package com.imyeyu.api.modules.common.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.imyeyu.java.bean.Language;
import com.imyeyu.spring.annotation.table.Column;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -15,6 +17,18 @@ public class Multilingual extends UUIDEntity {
protected String zhCN;
@Column("zh_cn_pinyin_full")
@JsonIgnore
protected String zhCNPinyinFull;
@Column("zh_cn_pinyin_initial")
@JsonIgnore
protected String zhCNPinyinInitial;
@Column("zh_cn_pinyin_mixed")
@JsonIgnore
protected String zhCNPinyinMixed;
protected String zhTW;
protected String enUS;
@@ -3,6 +3,7 @@ package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.api.modules.common.mapper.MultilingualMapper;
import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.api.util.PinyinUtil;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.spring.util.Redis;
@@ -34,6 +35,18 @@ public class MultilingualServiceImplement extends AbstractEntityService<Multilin
return mapper;
}
@Override
public void create(Multilingual multilingual) {
fillPinyin(multilingual);
super.create(multilingual);
}
@Override
public void update(Multilingual multilingual) {
fillPinyin(multilingual);
super.update(multilingual);
}
@Override
public Multilingual get(String id) {
Multilingual result = redisLanguage.get(getKey(id));
@@ -81,4 +94,14 @@ public class MultilingualServiceImplement extends AbstractEntityService<Multilin
private String getKey(String id) {
return "%s%s".formatted(KEY_PREFIX, id);
}
private void fillPinyin(Multilingual multilingual) {
if (multilingual == null) {
return;
}
String zhCN = multilingual.getZhCN();
multilingual.setZhCNPinyinFull(PinyinUtil.full(zhCN));
multilingual.setZhCNPinyinInitial(PinyinUtil.initial(zhCN));
multilingual.setZhCNPinyinMixed(PinyinUtil.mixed(zhCN));
}
}
@@ -6,7 +6,9 @@ import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.TagApply;
import com.imyeyu.api.modules.user.bean.Gender;
import com.imyeyu.api.modules.user.entity.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.annotation.table.Column;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
@@ -59,6 +61,18 @@ public class GaoCustomer extends UUIDEntity {
/// 姓名
private String name;
@Column("zh_cn_pinyin_full")
@JsonIgnore
private String namePinyinFull;
@Column("zh_cn_pinyin_initial")
@JsonIgnore
private String namePinyinInitial;
@Column("zh_cn_pinyin_mixed")
@JsonIgnore
private String namePinyinMixed;
/// 性别
private Gender gender;
@@ -21,6 +21,7 @@ import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStateReq;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.util.PinyinUtil;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
@@ -91,6 +92,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
public void create(GaoCustomer customer) {
TimiException.required(customer.getName(), "not found customer.name");
TimiException.required(customer.getStoreId(), "not found customer.storeId");
fillPinyin(customer);
Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_CREATE");
try {
@@ -169,6 +171,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
}
}
dbCustomer.setName(customer.getName());
fillPinyin(dbCustomer);
dbCustomer.setGender(customer.getGender());
dbCustomer.setAge(customer.getAge());
dbCustomer.setBirthdateCalendar(customer.getBirthdateCalendar());
@@ -424,4 +427,11 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
customer.setInvitedCount(customer.getInvitedCount() + delta);
mapper.update(customer);
}
private void fillPinyin(GaoCustomer customer) {
String name = customer.getName();
customer.setNamePinyinFull(PinyinUtil.full(name));
customer.setNamePinyinInitial(PinyinUtil.initial(name));
customer.setNamePinyinMixed(PinyinUtil.mixed(name));
}
}
@@ -0,0 +1,63 @@
package com.imyeyu.api.util;
import me.majiajie.tinypinyin.Pinyin;
import java.util.Locale;
/// 拼音搜索工具
///
/// @author Codex
/// @since 2026-08-21
public final class PinyinUtil {
private PinyinUtil() {
}
/// 转换为无声调全拼
///
/// @param value 原始文字
/// @return 无声调全拼
public static String full(String value) {
return convert(value, false, false);
}
/// 转换为拼音首字母
///
/// @param value 原始文字
/// @return 拼音首字母
public static String initial(String value) {
return convert(value, true, false);
}
/// 转换为首字全拼、后续字首字母的混合拼音
///
/// @param value 原始文字
/// @return 混合拼音
public static String mixed(String value) {
return convert(value, false, true);
}
private static String convert(String value, boolean initial, boolean mixed) {
if (value == null) {
return null;
}
StringBuilder result = new StringBuilder(value.length());
boolean first = true;
for (char character : value.toCharArray()) {
if (Character.isWhitespace(character)) {
continue;
}
String pinyin = Pinyin.toPinyin(character).toLowerCase(Locale.ROOT);
if (pinyin.isEmpty()) {
continue;
}
if (initial || (mixed && !first)) {
result.append(pinyin.charAt(0));
} else {
result.append(pinyin);
}
first = false;
}
return result.toString();
}
}
@@ -0,0 +1,98 @@
package db.migration.timiserver;
import com.imyeyu.api.util.PinyinUtil;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/// 回填拼音搜索字段
///
/// @author Codex
/// @since 2026-08-21
public class V37__backfill_pinyin_search extends BaseJavaMigration {
private static final int BATCH_SIZE = 500;
@Override
public void migrate(Context context) throws Exception {
backfillMultilingual(context.getConnection());
backfillCustomer(context.getConnection());
}
private void backfillMultilingual(Connection connection) throws Exception {
try (PreparedStatement select = connection.prepareStatement("""
SELECT `id`, `zh_cn`
FROM `multilingual`
WHERE `zh_cn` IS NOT NULL
AND (`zh_cn_pinyin_full` IS NULL OR `zh_cn_pinyin_initial` IS NULL OR `zh_cn_pinyin_mixed` IS NULL)
""");
PreparedStatement update = connection.prepareStatement("""
UPDATE `multilingual`
SET `zh_cn_pinyin_full` = ?, `zh_cn_pinyin_initial` = ?, `zh_cn_pinyin_mixed` = ?
WHERE `id` = ?
""")) {
try (ResultSet resultSet = select.executeQuery()) {
int batchSize = 0;
while (resultSet.next()) {
addMultilingualBatch(update, resultSet.getString("id"), resultSet.getString("zh_cn"));
batchSize++;
if (batchSize >= BATCH_SIZE) {
update.executeBatch();
batchSize = 0;
}
}
if (batchSize > 0) {
update.executeBatch();
}
}
}
}
private void backfillCustomer(Connection connection) throws Exception {
try (PreparedStatement select = connection.prepareStatement("""
SELECT `id`, `name`
FROM `gao_customer`
WHERE `name` IS NOT NULL
AND (`name_pinyin_full` IS NULL OR `name_pinyin_initial` IS NULL OR `name_pinyin_mixed` IS NULL)
""");
PreparedStatement update = connection.prepareStatement("""
UPDATE `gao_customer`
SET `name_pinyin_full` = ?, `name_pinyin_initial` = ?, `name_pinyin_mixed` = ?
WHERE `id` = ?
""")) {
try (ResultSet resultSet = select.executeQuery()) {
int batchSize = 0;
while (resultSet.next()) {
addCustomerBatch(update, resultSet.getString("id"), resultSet.getString("name"));
batchSize++;
if (batchSize >= BATCH_SIZE) {
update.executeBatch();
batchSize = 0;
}
}
if (batchSize > 0) {
update.executeBatch();
}
}
}
}
private void addMultilingualBatch(PreparedStatement update, String id, String value) throws Exception {
update.setString(1, PinyinUtil.full(value));
update.setString(2, PinyinUtil.initial(value));
update.setString(3, PinyinUtil.mixed(value));
update.setString(4, id);
update.addBatch();
}
private void addCustomerBatch(PreparedStatement update, String id, String value) throws Exception {
update.setString(1, PinyinUtil.full(value));
update.setString(2, PinyinUtil.initial(value));
update.setString(3, PinyinUtil.mixed(value));
update.setString(4, id);
update.addBatch();
}
}
@@ -0,0 +1,15 @@
ALTER TABLE `multilingual`
ADD COLUMN `zh_cn_pinyin_full` TEXT NULL COMMENT '简体中文全拼' AFTER `zh_cn`,
ADD COLUMN `zh_cn_pinyin_initial` TEXT NULL COMMENT '简体中文拼音首字母' AFTER `zh_cn_pinyin_full`,
ADD COLUMN `zh_cn_pinyin_mixed` TEXT NULL COMMENT '简体中文混合拼音' AFTER `zh_cn_pinyin_initial`,
ADD KEY `idx_multilingual_zh_cn_pinyin_full` (`zh_cn_pinyin_full`(128)),
ADD KEY `idx_multilingual_zh_cn_pinyin_initial` (`zh_cn_pinyin_initial`(128)),
ADD KEY `idx_multilingual_zh_cn_pinyin_mixed` (`zh_cn_pinyin_mixed`(128));
ALTER TABLE `gao_customer`
ADD COLUMN `name_pinyin_full` VARCHAR(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '姓名全拼' AFTER `name`,
ADD COLUMN `name_pinyin_initial` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '姓名拼音首字母' AFTER `name_pinyin_full`,
ADD COLUMN `name_pinyin_mixed` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '姓名混合拼音' AFTER `name_pinyin_initial`,
ADD KEY `idx_gao_customer_store_name_pinyin_full` (`store_id`, `deleted_at`, `name_pinyin_full`(128)),
ADD KEY `idx_gao_customer_store_name_pinyin_initial` (`store_id`, `deleted_at`, `name_pinyin_initial`),
ADD KEY `idx_gao_customer_store_name_pinyin_mixed` (`store_id`, `deleted_at`, `name_pinyin_mixed`);
@@ -32,6 +32,9 @@
OR `m`.`ja_jp` LIKE CONCAT('%', #{keyword}, '%')
OR `m`.`de_de` LIKE CONCAT('%', #{keyword}, '%')
OR `m`.`ko_kr` LIKE CONCAT('%', #{keyword}, '%')
OR `m`.`zh_cn_pinyin_full` LIKE CONCAT(#{keyword}, '%')
OR `m`.`zh_cn_pinyin_initial` LIKE CONCAT(#{keyword}, '%')
OR `m`.`zh_cn_pinyin_mixed` LIKE CONCAT(#{keyword}, '%')
)
</if>
AND `t`.`deleted_at` IS NULL
@@ -29,6 +29,9 @@
`c`.`code` LIKE CONCAT('%', #{keyword}, '%')
OR `c`.`name` LIKE CONCAT('%', #{keyword}, '%')
OR `c`.`telephone` LIKE CONCAT('%', #{keyword}, '%')
OR `c`.`name_pinyin_full` LIKE CONCAT(#{keyword}, '%')
OR `c`.`name_pinyin_initial` LIKE CONCAT(#{keyword}, '%')
OR `c`.`name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%')
)
</if>
<if test="tagIdList != null and !tagIdList.isEmpty()">