feat: 新增数据管理模块(区域管理)及修复前端已知问题

后端(bms-back):
- 新增区域管理完整后端:Domain/Mapper/Service/Controller + MyBatis XML
- 新增 RestTemplateConfig 配置类,用于第三方API调用
- 新增 countries.json 本地国家数据源(105个国家)
- RuoYiApplication 加 @EnableAsync 支持异步任务
- application-druid.yml 修复 Druid 连接池字符集配置(SET NAMES utf8mb4)
- logback.xml 日志路径改为 ${user.dir}/logs 避免沙箱写入受限
- pom.xml 增加 JVM 编码参数(-Dfile.encoding=UTF-8)
- 新增 sql/bms_region.sql 建表脚本+菜单权限

前端(bms-front):
- 新增区域管理页面(列表/编辑/状态切换/同步国家数据/导出)
- 新增同步日志功能(内嵌为区域管理弹窗,移除独立菜单)
- 新增 API 层: region.js / region-log.js
- 修复 BasicForm/index.vue 缺失 import FormControl 导致搜索栏控件不渲染
- 修复 user/role/region 三个页面 el-switch 初始化时弹 undefined 提示
This commit is contained in:
wuweihua
2026-08-31 11:01:25 +08:00
parent b003b73b77
commit 44d2b690de
28 changed files with 2318 additions and 1105 deletions
@@ -0,0 +1,270 @@
package com.ruoyi.system.domain;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 国家/区域 bms_region
*
* @author ruoyi
*/
public class BmsRegion extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 区域ID */
@Excel(name = "区域ID")
private Long regionId;
/** 中文名 */
@Excel(name = "中文名")
private String countryNameCn;
/** 英文名 */
@Excel(name = "英文名")
private String countryNameEn;
/** ISO两位码 */
@Excel(name = "国家代码")
private String countryCode2;
/** ISO三位码 */
@Excel(name = "ISO三位码")
private String countryCode3;
/** 洲/大区 */
@Excel(name = "洲/大区")
private String continent;
/** 子区域 */
@Excel(name = "子区域")
private String subRegion;
/** 货币代码 */
@Excel(name = "货币代码")
private String currencyCode;
/** 货币名称 */
@Excel(name = "货币名称")
private String currencyName;
/** 货币符号 */
private String currencySymbol;
/** 电话前缀 */
@Excel(name = "电话前缀")
private String phonePrefix;
/** 国旗emoji */
private String flagEmoji;
/** 国旗图片URL */
private String flagUrl;
/** 是否独立(1是 0否) */
@Excel(name = "是否独立", readConverterExp = "1=是,0=否")
private String isIndependent;
/** 是否欧盟成员(1是 0否) */
@Excel(name = "是否欧盟", readConverterExp = "1=是,0=否")
private String isEu;
/** 状态(0正常 1停用) */
@Excel(name = "状态", readConverterExp = "0=正常,1=停用")
private String status;
/** 删除标志(0存在 2删除) */
private String delFlag;
public Long getRegionId()
{
return regionId;
}
public void setRegionId(Long regionId)
{
this.regionId = regionId;
}
public String getCountryNameCn()
{
return countryNameCn;
}
public void setCountryNameCn(String countryNameCn)
{
this.countryNameCn = countryNameCn;
}
public String getCountryNameEn()
{
return countryNameEn;
}
public void setCountryNameEn(String countryNameEn)
{
this.countryNameEn = countryNameEn;
}
public String getCountryCode2()
{
return countryCode2;
}
public void setCountryCode2(String countryCode2)
{
this.countryCode2 = countryCode2;
}
public String getCountryCode3()
{
return countryCode3;
}
public void setCountryCode3(String countryCode3)
{
this.countryCode3 = countryCode3;
}
public String getContinent()
{
return continent;
}
public void setContinent(String continent)
{
this.continent = continent;
}
public String getSubRegion()
{
return subRegion;
}
public void setSubRegion(String subRegion)
{
this.subRegion = subRegion;
}
public String getCurrencyCode()
{
return currencyCode;
}
public void setCurrencyCode(String currencyCode)
{
this.currencyCode = currencyCode;
}
public String getCurrencyName()
{
return currencyName;
}
public void setCurrencyName(String currencyName)
{
this.currencyName = currencyName;
}
public String getCurrencySymbol()
{
return currencySymbol;
}
public void setCurrencySymbol(String currencySymbol)
{
this.currencySymbol = currencySymbol;
}
public String getPhonePrefix()
{
return phonePrefix;
}
public void setPhonePrefix(String phonePrefix)
{
this.phonePrefix = phonePrefix;
}
public String getFlagEmoji()
{
return flagEmoji;
}
public void setFlagEmoji(String flagEmoji)
{
this.flagEmoji = flagEmoji;
}
public String getFlagUrl()
{
return flagUrl;
}
public void setFlagUrl(String flagUrl)
{
this.flagUrl = flagUrl;
}
public String getIsIndependent()
{
return isIndependent;
}
public void setIsIndependent(String isIndependent)
{
this.isIndependent = isIndependent;
}
public String getIsEu()
{
return isEu;
}
public void setIsEu(String isEu)
{
this.isEu = isEu;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public String getDelFlag()
{
return delFlag;
}
public void setDelFlag(String delFlag)
{
this.delFlag = delFlag;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("regionId", getRegionId())
.append("countryNameCn", getCountryNameCn())
.append("countryNameEn", getCountryNameEn())
.append("countryCode2", getCountryCode2())
.append("countryCode3", getCountryCode3())
.append("continent", getContinent())
.append("subRegion", getSubRegion())
.append("status", getStatus())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.toString();
}
}
@@ -0,0 +1,160 @@
package com.ruoyi.system.domain;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 区域数据同步日志 bms_region_sync_log
*
* @author ruoyi
*/
public class BmsRegionSyncLog extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 日志ID */
@Excel(name = "日志ID")
private Long logId;
/** 同步类型 */
@Excel(name = "同步类型")
private String syncType;
/** 状态 */
@Excel(name = "状态", readConverterExp = "PENDING=待处理,PROCESSING=处理中,SUCCESS=成功,FAILED=失败")
private String status;
/** API返回总数 */
@Excel(name = "总数")
private Integer totalCount;
/** 新增数 */
@Excel(name = "新增数")
private Integer insertCount;
/** 更新数 */
@Excel(name = "更新数")
private Integer updateCount;
/** 失败数 */
@Excel(name = "失败数")
private Integer failCount;
/** 耗时(毫秒) */
@Excel(name = "耗时(ms)")
private Long durationMs;
/** 错误信息 */
private String errorMsg;
public Long getLogId()
{
return logId;
}
public void setLogId(Long logId)
{
this.logId = logId;
}
public String getSyncType()
{
return syncType;
}
public void setSyncType(String syncType)
{
this.syncType = syncType;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public Integer getTotalCount()
{
return totalCount;
}
public void setTotalCount(Integer totalCount)
{
this.totalCount = totalCount;
}
public Integer getInsertCount()
{
return insertCount;
}
public void setInsertCount(Integer insertCount)
{
this.insertCount = insertCount;
}
public Integer getUpdateCount()
{
return updateCount;
}
public void setUpdateCount(Integer updateCount)
{
this.updateCount = updateCount;
}
public Integer getFailCount()
{
return failCount;
}
public void setFailCount(Integer failCount)
{
this.failCount = failCount;
}
public Long getDurationMs()
{
return durationMs;
}
public void setDurationMs(Long durationMs)
{
this.durationMs = durationMs;
}
public String getErrorMsg()
{
return errorMsg;
}
public void setErrorMsg(String errorMsg)
{
this.errorMsg = errorMsg;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("logId", getLogId())
.append("syncType", getSyncType())
.append("status", getStatus())
.append("totalCount", getTotalCount())
.append("insertCount", getInsertCount())
.append("updateCount", getUpdateCount())
.append("failCount", getFailCount())
.append("durationMs", getDurationMs())
.append("errorMsg", getErrorMsg())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateTime", getUpdateTime())
.toString();
}
}
@@ -0,0 +1,68 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.BmsRegion;
/**
* 国家/区域 数据层
*
* @author ruoyi
*/
public interface BmsRegionMapper
{
/**
* 查询区域信息
*
* @param regionId 区域ID
* @return 区域信息
*/
public BmsRegion selectRegionById(Long regionId);
/**
* 查询区域列表
*
* @param region 区域信息
* @return 区域集合
*/
public List<BmsRegion> selectRegionList(BmsRegion region);
/**
* 根据ISO两位码查询区域
*
* @param countryCode2 ISO两位码
* @return 区域信息
*/
public BmsRegion selectRegionByCode2(String countryCode2);
/**
* 新增区域
*
* @param region 区域信息
* @return 结果
*/
public int insertRegion(BmsRegion region);
/**
* 修改区域
*
* @param region 区域信息
* @return 结果
*/
public int updateRegion(BmsRegion region);
/**
* 批量删除区域
*
* @param regionIds 需要删除的区域ID
* @return 结果
*/
public int deleteRegionByIds(Long[] regionIds);
/**
* 根据ISO两位码做upsert(存在则更新,不存在则新增)
*
* @param region 区域信息
* @return 结果
*/
public int upsertByCode2(BmsRegion region);
}
@@ -0,0 +1,52 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.BmsRegionSyncLog;
/**
* 区域数据同步日志 数据层
*
* @author ruoyi
*/
public interface BmsRegionSyncLogMapper
{
/**
* 查询同步日志
*
* @param logId 日志ID
* @return 同步日志
*/
public BmsRegionSyncLog selectSyncLogById(Long logId);
/**
* 查询同步日志列表
*
* @param syncLog 同步日志
* @return 同步日志集合
*/
public List<BmsRegionSyncLog> selectSyncLogList(BmsRegionSyncLog syncLog);
/**
* 新增同步日志
*
* @param syncLog 同步日志
* @return 结果
*/
public int insertSyncLog(BmsRegionSyncLog syncLog);
/**
* 修改同步日志
*
* @param syncLog 同步日志
* @return 结果
*/
public int updateSyncLog(BmsRegionSyncLog syncLog);
/**
* 批量删除同步日志
*
* @param logIds 需要删除的日志ID
* @return 结果
*/
public int deleteSyncLogByIds(Long[] logIds);
}
@@ -0,0 +1,60 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.system.domain.BmsRegion;
/**
* 国家/区域 服务层
*
* @author ruoyi
*/
public interface IBmsRegionService
{
/**
* 查询区域信息
*
* @param regionId 区域ID
* @return 区域信息
*/
public BmsRegion selectRegionById(Long regionId);
/**
* 查询区域列表
*
* @param region 区域信息
* @return 区域集合
*/
public List<BmsRegion> selectRegionList(BmsRegion region);
/**
* 新增区域
*
* @param region 区域信息
* @return 结果
*/
public int insertRegion(BmsRegion region);
/**
* 修改区域
*
* @param region 区域信息
* @return 结果
*/
public int updateRegion(BmsRegion region);
/**
* 批量删除区域
*
* @param regionIds 需要删除的区域ID
* @return 结果
*/
public int deleteRegionByIds(Long[] regionIds);
/**
* 修改区域状态
*
* @param region 区域信息
* @return 结果
*/
public int updateRegionStatus(BmsRegion region);
}
@@ -0,0 +1,16 @@
package com.ruoyi.system.service;
/**
* 区域数据同步 服务层
*
* @author ruoyi
*/
public interface IBmsRegionSyncService
{
/**
* 触发同步国家数据(异步)
*
* @return 日志ID
*/
public Long syncRegion();
}
@@ -0,0 +1,93 @@
package com.ruoyi.system.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.BmsRegion;
import com.ruoyi.system.mapper.BmsRegionMapper;
import com.ruoyi.system.service.IBmsRegionService;
/**
* 国家/区域 服务层实现
*
* @author ruoyi
*/
@Service
public class BmsRegionServiceImpl implements IBmsRegionService
{
@Autowired
private BmsRegionMapper regionMapper;
/**
* 查询区域信息
*
* @param regionId 区域ID
* @return 区域信息
*/
@Override
public BmsRegion selectRegionById(Long regionId)
{
return regionMapper.selectRegionById(regionId);
}
/**
* 查询区域列表
*
* @param region 区域信息
* @return 区域集合
*/
@Override
public List<BmsRegion> selectRegionList(BmsRegion region)
{
return regionMapper.selectRegionList(region);
}
/**
* 新增区域
*
* @param region 区域信息
* @return 结果
*/
@Override
public int insertRegion(BmsRegion region)
{
return regionMapper.insertRegion(region);
}
/**
* 修改区域
*
* @param region 区域信息
* @return 结果
*/
@Override
public int updateRegion(BmsRegion region)
{
return regionMapper.updateRegion(region);
}
/**
* 批量删除区域
*
* @param regionIds 需要删除的区域ID
* @return 结果
*/
@Override
public int deleteRegionByIds(Long[] regionIds)
{
return regionMapper.deleteRegionByIds(regionIds);
}
/**
* 修改区域状态
*
* @param region 区域信息
* @return 结果
*/
@Override
public int updateRegionStatus(BmsRegion region)
{
return regionMapper.updateRegion(region);
}
}
@@ -0,0 +1,249 @@
package com.ruoyi.system.service.impl;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.BmsRegion;
import com.ruoyi.system.domain.BmsRegionSyncLog;
import com.ruoyi.system.mapper.BmsRegionMapper;
import com.ruoyi.system.mapper.BmsRegionSyncLogMapper;
import com.ruoyi.system.service.IBmsRegionSyncService;
/**
* 区域数据同步 服务层实现
* 从本地 countries.json 文件加载国家数据
*
* @author ruoyi
*/
@Service
public class BmsRegionSyncServiceImpl implements IBmsRegionSyncService
{
private static final Logger log = LoggerFactory.getLogger(BmsRegionSyncServiceImpl.class);
private static final String DATA_FILE = "countries.json";
@Autowired
private BmsRegionMapper regionMapper;
@Autowired
private BmsRegionSyncLogMapper syncLogMapper;
/**
* 触发同步国家数据(异步)
*
* @return 日志ID
*/
@Override
public Long syncRegion()
{
// 创建同步日志记录
BmsRegionSyncLog syncLog = new BmsRegionSyncLog();
syncLog.setSyncType("ALL");
syncLog.setStatus("PROCESSING");
syncLog.setTotalCount(0);
syncLog.setInsertCount(0);
syncLog.setUpdateCount(0);
syncLog.setFailCount(0);
syncLog.setDurationMs(0L);
syncLogMapper.insertSyncLog(syncLog);
// 异步执行同步
asyncSync(syncLog.getLogId());
return syncLog.getLogId();
}
/**
* 异步同步国家数据
*
* @param logId 日志ID
*/
@Async("threadPoolTaskExecutor")
@Transactional(rollbackFor = Exception.class)
public void asyncSync(Long logId)
{
long startTime = System.currentTimeMillis();
BmsRegionSyncLog syncLog = syncLogMapper.selectSyncLogById(logId);
try
{
log.info("开始同步国家数据,读取本地文件: {}", DATA_FILE);
// 从classpath读取国家数据文件
ClassPathResource resource = new ClassPathResource(DATA_FILE);
InputStream inputStream = resource.getInputStream();
byte[] bytes = inputStream.readAllBytes();
inputStream.close();
String jsonData = new String(bytes, StandardCharsets.UTF_8);
if (StringUtils.isEmpty(jsonData))
{
throw new RuntimeException("国家数据文件为空");
}
// 解析JSON
ObjectMapper objectMapper = new ObjectMapper();
List<Object> countryList = objectMapper.readValue(jsonData, List.class);
int insertCount = 0;
int updateCount = 0;
int failCount = 0;
for (Object obj : countryList)
{
try
{
@SuppressWarnings("unchecked")
Map<String, Object> country = (Map<String, Object>) obj;
BmsRegion region = parseCountry(country);
if (region == null || StringUtils.isEmpty(region.getCountryCode2()))
{
failCount++;
continue;
}
// 检查是否已存在
BmsRegion existing = regionMapper.selectRegionByCode2(region.getCountryCode2());
if (existing != null)
{
region.setRegionId(existing.getRegionId());
regionMapper.updateRegion(region);
updateCount++;
}
else
{
regionMapper.insertRegion(region);
insertCount++;
}
}
catch (Exception e)
{
failCount++;
log.warn("解析国家数据失败: {}", e.getMessage());
}
}
// 更新同步日志
long duration = System.currentTimeMillis() - startTime;
syncLog.setStatus("SUCCESS");
syncLog.setTotalCount(countryList.size());
syncLog.setInsertCount(insertCount);
syncLog.setUpdateCount(updateCount);
syncLog.setFailCount(failCount);
syncLog.setDurationMs(duration);
syncLogMapper.updateSyncLog(syncLog);
log.info("同步国家数据完成: 总数={}, 新增={}, 更新={}, 失败={}, 耗时={}ms",
countryList.size(), insertCount, updateCount, failCount, duration);
}
catch (Exception e)
{
long duration = System.currentTimeMillis() - startTime;
syncLog.setStatus("FAILED");
syncLog.setDurationMs(duration);
syncLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
syncLogMapper.updateSyncLog(syncLog);
log.error("同步国家数据失败: {}", e.getMessage(), e);
}
}
/**
* 解析国家数据
*
* @param country 国家Map
* @return 区域实体
*/
@SuppressWarnings("unchecked")
private BmsRegion parseCountry(Map<String, Object> country)
{
BmsRegion region = new BmsRegion();
region.setStatus("0");
region.setIsIndependent("1");
region.setIsEu("0");
// 英文名 name.common
Map<String, Object> name = (Map<String, Object>) country.get("name");
if (name != null)
{
region.setCountryNameEn((String) name.get("common"));
}
// 中文名 translations.cmn.common,如无则用英文名
Map<String, Object> translations = (Map<String, Object>) country.get("translations");
if (translations != null)
{
Map<String, Object> cmn = (Map<String, Object>) translations.get("cmn");
if (cmn != null)
{
region.setCountryNameCn((String) cmn.get("common"));
}
}
if (StringUtils.isEmpty(region.getCountryNameCn()))
{
region.setCountryNameCn(region.getCountryNameEn());
}
// ISO代码
region.setCountryCode2((String) country.get("cca2"));
region.setCountryCode3((String) country.get("cca3"));
// 大洲/子区域
region.setContinent((String) country.get("region"));
region.setSubRegion((String) country.get("subregion"));
// 货币
Map<String, Object> currencies = (Map<String, Object>) country.get("currencies");
if (currencies != null && !currencies.isEmpty())
{
Map.Entry<String, Object> entry = currencies.entrySet().iterator().next();
region.setCurrencyCode(entry.getKey());
Map<String, Object> currency = (Map<String, Object>) entry.getValue();
if (currency != null)
{
region.setCurrencyName((String) currency.get("name"));
region.setCurrencySymbol((String) currency.get("symbol"));
}
}
// 电话前缀 idd.root + idd.suffixes
Map<String, Object> idd = (Map<String, Object>) country.get("idd");
if (idd != null)
{
String root = (String) idd.get("root");
List<String> suffixes = (List<String>) idd.get("suffixes");
if (StringUtils.isNotEmpty(root) && suffixes != null && !suffixes.isEmpty())
{
region.setPhonePrefix(root + suffixes.get(0));
}
}
// 国旗
region.setFlagEmoji((String) country.get("flag"));
Map<String, Object> flags = (Map<String, Object>) country.get("flags");
if (flags != null)
{
region.setFlagUrl((String) flags.get("png"));
}
// 是否独立
Boolean independent = (Boolean) country.get("independent");
if (independent != null)
{
region.setIsIndependent(independent ? "1" : "0");
}
return region;
}
}