refactor(ui): 优化表格数据加载和异步单元格更新逻辑

修复表格高度调整时频繁触发数据加载的问题,增加调整阈值判断
改进异步单元格更新逻辑,增加取消机制和状态检查
重构常量命名,使用更清晰的KEY_SYNC_BY前缀
添加日志记录和注释说明
This commit is contained in:
2025-08-27 14:43:55 +08:00
parent 9ff84ebe8a
commit fb28bac53a
11 changed files with 145 additions and 37 deletions

View File

@@ -10,7 +10,7 @@
</parent> </parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId> <artifactId>Contract-Manager</artifactId>
<version>0.0.46-SNAPSHOT</version> <version>0.0.47-SNAPSHOT</version>
<name>Contract-Manager</name> <name>Contract-Manager</name>
<description>Contract-Manager</description> <description>Contract-Manager</description>
<url/> <url/>

View File

@@ -37,7 +37,7 @@ public class ContractSyncTask extends AbstContractRepairTasker {
holder.error("无法获取 YongYouU8Repository " + e.getMessage()); holder.error("无法获取 YongYouU8Repository " + e.getMessage());
return; return;
} }
if (getConfService().getBoolean(ContractCtx.KEY_CONTRACT_SYNC_USE_LATEST_ID)) { if (getConfService().getBoolean(ContractCtx.KEY_SYNC_USE_LATEST_ID)) {
syncByLatestId(holder); syncByLatestId(holder);
} else { } else {
syncByLatestDate(holder); syncByLatestDate(holder);
@@ -45,7 +45,7 @@ public class ContractSyncTask extends AbstContractRepairTasker {
} }
private void syncByLatestId(MessageHolder holder) { private void syncByLatestId(MessageHolder holder) {
int latestId = getConfService().getInt(ContractCtx.CONTRACT_LATEST_ID); int latestId = getConfService().getInt(ContractCtx.KEY_SYNC_BY_LATEST_ID);
updateTitle("用友U8系统-同步合同,从 " + latestId + " 开始"); updateTitle("用友U8系统-同步合同,从 " + latestId + " 开始");
Long total = repository.countAllContracts(latestId); Long total = repository.countAllContracts(latestId);
@@ -81,11 +81,11 @@ public class ContractSyncTask extends AbstContractRepairTasker {
updateProgress(counter.incrementAndGet(), total); updateProgress(counter.incrementAndGet(), total);
}); });
} }
getConfService().set(ContractCtx.CONTRACT_LATEST_ID, String.valueOf(reference.get())); getConfService().set(ContractCtx.KEY_SYNC_BY_LATEST_ID, String.valueOf(reference.get()));
} }
private void syncByLatestDate(MessageHolder holder) { private void syncByLatestDate(MessageHolder holder) {
String strDateTime = getConfService().getString(ContractCtx.CONTRACT_LATEST_DATE); String strDateTime = getConfService().getString(ContractCtx.KEY_SYNC_BY_LATEST_DATE);
LocalDateTime latestDateTime = null; LocalDateTime latestDateTime = null;
if (StringUtils.hasText(strDateTime)) { if (StringUtils.hasText(strDateTime)) {
try { try {
@@ -135,7 +135,7 @@ public class ContractSyncTask extends AbstContractRepairTasker {
updateProgress(counter.incrementAndGet(), total); updateProgress(counter.incrementAndGet(), total);
}); });
} }
getConfService().set(ContractCtx.CONTRACT_LATEST_DATE, String.valueOf(reference.get())); getConfService().set(ContractCtx.KEY_SYNC_BY_LATEST_DATE, String.valueOf(reference.get()));
} }
} }

View File

@@ -42,11 +42,11 @@ public class YongYouU8ConfigWindowController extends BaseController {
config1.setPicker(auto_create_company_after); config1.setPicker(auto_create_company_after);
config1.initialize(); config1.initialize();
LocalDateConfig config2 = new LocalDateConfig(ContractCtx.CONTRACT_LATEST_DATE); LocalDateConfig config2 = new LocalDateConfig(ContractCtx.KEY_SYNC_BY_LATEST_DATE);
config2.setPicker(contract_latest_date); config2.setPicker(contract_latest_date);
config2.initialize(); config2.initialize();
StringConfig config3 = new StringConfig(ContractCtx.CONTRACT_LATEST_ID); StringConfig config3 = new StringConfig(ContractCtx.KEY_SYNC_BY_LATEST_ID);
config3.setTextField(contract_latest_id); config3.setTextField(contract_latest_id);
config3.initialize(); config3.initialize();

View File

@@ -44,11 +44,23 @@ import java.util.stream.Stream;
import static com.ecep.contract.manager.SpringApp.getBean; import static com.ecep.contract.manager.SpringApp.getBean;
/**
* 合同上下文
*/
public class ContractCtx extends AbstractYongYouU8Ctx { public class ContractCtx extends AbstractYongYouU8Ctx {
public static final String CONTRACT_LATEST_DATE = "cloud.u8.contract.latestDate"; public static final String KEY_PREFIX = "cloud.u8.contract.";
public static final String CONTRACT_LATEST_ID = "cloud.u8.contract.latestId"; /**
public static final String KEY_CONTRACT_SYNC_USE_LATEST_ID = "cloud.u8.contract.sync.use-latest-id"; * 合同同步后的最后一个日期
*/
public static final String KEY_SYNC_BY_LATEST_DATE = KEY_PREFIX + "latestDate";
/**
* 合同同步后的最后一个合同ID
*/
public static final String KEY_SYNC_BY_LATEST_ID = KEY_PREFIX + "latestId";
/**
* 合同同步时是否使用最后更新的Id来判断更新范围否则使用最后更新的合同日期来判断更新范围
*/
public static final String KEY_SYNC_USE_LATEST_ID = KEY_PREFIX + "sync.use-latest-id";
@Setter @Setter
private ContractService contractService; private ContractService contractService;
@@ -78,7 +90,6 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
@Setter @Setter
private int customerEntityUpdateDelayDays = 1; private int customerEntityUpdateDelayDays = 1;
public ContractService getContractService() { public ContractService getContractService() {
if (contractService == null) { if (contractService == null) {
contractService = getBean(ContractService.class); contractService = getBean(ContractService.class);

View File

@@ -19,6 +19,9 @@ import org.springframework.util.StringUtils;
import java.util.List; import java.util.List;
/**
* 合同支付计划服务
*/
@Lazy @Lazy
@Service @Service
@CacheConfig(cacheNames = "contract-pay-plan") @CacheConfig(cacheNames = "contract-pay-plan")

View File

@@ -150,10 +150,10 @@ public class ContractService implements ViewModelService<Contract, ContractViewM
*/ */
public void updateLatestIdAndDate(Integer latestId, LocalDateTime latestDate) { public void updateLatestIdAndDate(Integer latestId, LocalDateTime latestDate) {
if (latestId != null) { if (latestId != null) {
confService.set(ContractCtx.CONTRACT_LATEST_ID, String.valueOf(latestId)); confService.set(ContractCtx.KEY_SYNC_BY_LATEST_ID, String.valueOf(latestId));
} }
if (latestDate != null) { if (latestDate != null) {
confService.set(ContractCtx.CONTRACT_LATEST_DATE, latestDate.toString()); confService.set(ContractCtx.KEY_SYNC_BY_LATEST_DATE, latestDate.toString());
} }
} }

View File

@@ -195,10 +195,15 @@ public abstract class AbstEntityManagerSkin<T extends IdentityEntity, TV extends
loadTableDataSet(true); loadTableDataSet(true);
} }
private boolean allowResize = true;
/** /**
* 根据表格高度重新计算分页的页大小 * 根据表格高度重新计算分页的页大小
*/ */
private void resizeTable(Object observable, Bounds old, Bounds newBounds) { private void resizeTable(Object observable, Bounds old, Bounds newBounds) {
if (!allowResize) {
return;
}
double tableHeight = newBounds.getHeight(); double tableHeight = newBounds.getHeight();
if (tableHeight <= 0) { if (tableHeight <= 0) {
return; return;
@@ -208,8 +213,13 @@ public abstract class AbstEntityManagerSkin<T extends IdentityEntity, TV extends
if (lookup != null) { if (lookup != null) {
double rowHeight = lookup.prefHeight(-1); double rowHeight = lookup.prefHeight(-1);
int rows = (int) Math.round(table.getHeight() / rowHeight) - 1; int rows = (int) Math.round(table.getHeight() / rowHeight) - 1;
int pageNumber = (int) Math.abs(currentPageable.getOffset() / rows); // 只有当行数变化超过一定阈值时才重新加载数据
int currentRows = currentPageable.getPageSize();
if (Math.abs(rows - currentRows) <= 2) {
return; // 避免微小变化导致频繁刷新
}
int pageNumber = (int) Math.abs(currentPageable.getOffset() / rows);
if (currentPageable.getPageNumber() == pageNumber && currentPageable.getPageSize() == rows) { if (currentPageable.getPageNumber() == pageNumber && currentPageable.getPageSize() == rows) {
return; return;
} }
@@ -359,6 +369,7 @@ public abstract class AbstEntityManagerSkin<T extends IdentityEntity, TV extends
private CompletableFuture<Void> _reloadTableData() { private CompletableFuture<Void> _reloadTableData() {
CompletableFuture<Void> future = new CompletableFuture<>(); CompletableFuture<Void> future = new CompletableFuture<>();
Platform.runLater(() -> { Platform.runLater(() -> {
allowResize = false; // 禁用调整
dataSet.clear(); dataSet.clear();
runAsync(() -> { runAsync(() -> {
controller.setStatus("载入中..."); controller.setStatus("载入中...");
@@ -366,8 +377,10 @@ public abstract class AbstEntityManagerSkin<T extends IdentityEntity, TV extends
Platform.runLater(() -> { Platform.runLater(() -> {
try { try {
updateTableDataSet(models); updateTableDataSet(models);
allowResize = true; // 恢复调整
future.complete(null); future.complete(null);
} catch (Exception e) { } catch (Exception e) {
allowResize = true; // 恢复调整
future.completeExceptionally(e); future.completeExceptionally(e);
} }
}); });
@@ -381,7 +394,23 @@ public abstract class AbstEntityManagerSkin<T extends IdentityEntity, TV extends
protected void updateTableDataSet(List<TV> models) { protected void updateTableDataSet(List<TV> models) {
long timeMillis = System.currentTimeMillis(); long timeMillis = System.currentTimeMillis();
dataSet.setAll(models); // 清除所有选择状态,避免选择状态混乱
if (getTableView() != null && getTableView().getSelectionModel() != null) {
getTableView().getSelectionModel().clearSelection();
}
// 先清空再设置新数据,避免数据叠加
dataSet.clear();
if (models != null) {
dataSet.addAll(models);
}
// 强制刷新表格布局
if (getTableView() != null) {
getTableView().requestLayout();
getTableView().refresh();
}
long timeCost = System.currentTimeMillis() - timeMillis; long timeCost = System.currentTimeMillis() - timeMillis;
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("update table dataSet cost: {} ms", timeCost); logger.debug("update table dataSet cost: {} ms", timeCost);

View File

@@ -6,6 +6,13 @@ import com.ecep.contract.manager.ds.other.model.IdentityEntity;
import com.ecep.contract.manager.ds.other.service.IEntityService; import com.ecep.contract.manager.ds.other.service.IEntityService;
import com.ecep.contract.manager.ds.other.vo.IdentityViewModel; import com.ecep.contract.manager.ds.other.vo.IdentityViewModel;
/**
* 视图模型服务接口
*
* @param <T> 实体类型
* @param <TV> 视图模型类型
* @author 2025-08-02
*/
public interface ViewModelService<T extends IdentityEntity, TV extends IdentityViewModel<T>> public interface ViewModelService<T extends IdentityEntity, TV extends IdentityViewModel<T>>
extends IEntityService<T> { extends IEntityService<T> {

View File

@@ -3,6 +3,8 @@ package com.ecep.contract.manager.ui.table.cell;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecep.contract.manager.Desktop; import com.ecep.contract.manager.Desktop;
import com.ecep.contract.manager.ds.other.model.IdentityEntity; import com.ecep.contract.manager.ds.other.model.IdentityEntity;
@@ -18,6 +20,8 @@ import javafx.application.Platform;
* @param <T> * @param <T>
*/ */
public class AsyncUpdateTableCell<V, T extends IdentityEntity> extends javafx.scene.control.TableCell<V, T> { public class AsyncUpdateTableCell<V, T extends IdentityEntity> extends javafx.scene.control.TableCell<V, T> {
private static final Logger logger = LoggerFactory.getLogger(AsyncUpdateTableCell.class);
/** /**
* 转换为文本 * 转换为文本
*/ */
@@ -32,6 +36,20 @@ public class AsyncUpdateTableCell<V, T extends IdentityEntity> extends javafx.sc
} }
private IEntityService<T> service; private IEntityService<T> service;
Future<?> syncFuture;
/**
* 通过 #setService 设置服务类,或者子类实现, 提供服务类
*
* @return
*/
protected IEntityService<T> getServiceBean() {
return null;
}
public String format(T entity) {
return toString(entity);
}
public void setService(IEntityService<T> service) { public void setService(IEntityService<T> service) {
this.service = service; this.service = service;
@@ -44,13 +62,15 @@ public class AsyncUpdateTableCell<V, T extends IdentityEntity> extends javafx.sc
return service; return service;
} }
protected IEntityService<T> getServiceBean() {
return null;
}
@Override @Override
protected void updateItem(T item, boolean empty) { protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
// 取消之前的异步任务
if (syncFuture != null) {
syncFuture.cancel(true);
syncFuture = null;
}
if (empty || item == null) { if (empty || item == null) {
setText(null); setText(null);
return; return;
@@ -59,23 +79,45 @@ public class AsyncUpdateTableCell<V, T extends IdentityEntity> extends javafx.sc
setText(format(item)); setText(format(item));
return; return;
} }
setText("# " + item.getId()); setText("# " + item.getId());
submit(this::asyncLoadAndUpdate); syncFuture = submit(this::asyncLoadAndUpdate);
} }
/**
* 检查实体是否已初始化
*
* @param proxy
* @return
*/
protected boolean isInitialized(T proxy) { protected boolean isInitialized(T proxy) {
return Hibernate.isInitialized(proxy); return Hibernate.isInitialized(proxy);
} }
/**
* 初始化实体
*
* @return
*/
protected T initialize() {
return getService().findById(getItem().getId());
}
/**
* 提交异步任务
*
* @param var1 异步任务
* @return
*/
Future<?> submit(Runnable var1) { Future<?> submit(Runnable var1) {
return Desktop.instance.getExecutorService().submit(var1); return Desktop.instance.getExecutorService().submit(var1);
} }
public String format(T entity) { protected void asyncLoadAndUpdate() {
return toString(entity); if (getItem() == null) {
return;
} }
protected void asyncLoadAndUpdate() {
T entity = initialize(); T entity = initialize();
String formated; String formated;
try { try {
@@ -84,14 +126,26 @@ public class AsyncUpdateTableCell<V, T extends IdentityEntity> extends javafx.sc
formated = e.getMessage(); formated = e.getMessage();
} }
String texted = formated; String texted = formated;
// 保存当前需要更新的项目信息,避免闭包引用导致的问题
final T updatedEntity = entity;
final String updatedText = texted;
final T finalCurrentLoadingItem = getItem();
Platform.runLater(() -> { Platform.runLater(() -> {
setText(texted); // 检查单元格是否仍然显示相同的项目
setItem(entity); // 如果单元格已被重用或当前项目已改变,则不更新
if (isEmpty() || getItem() == null) {
logger.debug("Skipping async update - cell reused ");
return;
}
if (getItem().getId() != finalCurrentLoadingItem.getId()) {
logger.debug("Skipping async update - cell item changed");
return;
}
setItem(updatedEntity);
setText(updatedText);
}); });
} }
protected T initialize() {
return getService().findById(getItem().getId());
}
} }

View File

@@ -1,11 +1,14 @@
package com.ecep.contract.manager.ui.table.cell; package com.ecep.contract.manager.ui.table.cell;
import com.ecep.contract.manager.ds.company.model.Company;
import com.ecep.contract.manager.ds.company.service.CompanyService;
import lombok.NoArgsConstructor;
import static com.ecep.contract.manager.SpringApp.getBean; import static com.ecep.contract.manager.SpringApp.getBean;
import com.ecep.contract.manager.ds.company.model.Company;
import com.ecep.contract.manager.ds.company.service.CompanyService;
import lombok.NoArgsConstructor;
/**
* 公司单元格
*/
@NoArgsConstructor @NoArgsConstructor
public class CompanyTableCell<V> extends AsyncUpdateTableCell<V, Company> { public class CompanyTableCell<V> extends AsyncUpdateTableCell<V, Company> {

View File

@@ -29,6 +29,7 @@
<logger name="com.ecep.contract.manager.ui.service" level="info"/> <logger name="com.ecep.contract.manager.ui.service" level="info"/>
<logger name="com.ecep.contract.manager.ui.table.AbstEntityTableTabSkin" level="debug"/> <logger name="com.ecep.contract.manager.ui.table.AbstEntityTableTabSkin" level="debug"/>
<logger name="com.ecep.contract.manager.ui.table.cell.AsyncUpdateTableCell" level="debug"/>
<logger name="com.ecep.contract.manager.ui.AbstEntityManagerSkin" level="debug"/> <logger name="com.ecep.contract.manager.ui.AbstEntityManagerSkin" level="debug"/>
<logger name="com.ecep.contract.manager.ui.BaseController" level="debug"/> <logger name="com.ecep.contract.manager.ui.BaseController" level="debug"/>
<logger name="com.ecep.contract.manager.ui.controller" level="info"/> <logger name="com.ecep.contract.manager.ui.controller" level="info"/>