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
+2
View File
@@ -63,6 +63,8 @@
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<configuration> <configuration>
<addResources>true</addResources> <addResources>true</addResources>
<jvmArguments>-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Duser.language=zh -Duser.country=CN</jvmArguments>
<arguments>-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Duser.language=zh -Duser.country=CN</arguments>
</configuration> </configuration>
<executions> <executions>
<execution> <execution>
@@ -3,12 +3,14 @@ package com.ruoyi;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
import org.springframework.scheduling.annotation.EnableAsync;
/** /**
* 启动程序 * 启动程序
* *
* @author ruoyi * @author ruoyi
*/ */
@EnableAsync
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class RuoYiApplication public class RuoYiApplication
{ {
@@ -0,0 +1,124 @@
package com.ruoyi.web.controller.data;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.BmsRegion;
import com.ruoyi.system.service.IBmsRegionService;
import com.ruoyi.system.service.IBmsRegionSyncService;
/**
* 区域管理 信息操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/data/region")
public class BmsRegionController extends BaseController
{
@Autowired
private IBmsRegionService regionService;
@Autowired
private IBmsRegionSyncService syncService;
/**
* 获取区域列表
*/
@PreAuthorize("@ss.hasPermi('data:region:list')")
@GetMapping("/list")
public TableDataInfo list(BmsRegion region)
{
startPage();
List<BmsRegion> list = regionService.selectRegionList(region);
return getDataTable(list);
}
/**
* 导出区域列表
*/
@PreAuthorize("@ss.hasPermi('data:region:list')")
@Log(title = "区域管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, BmsRegion region)
{
List<BmsRegion> list = regionService.selectRegionList(region);
ExcelUtil<BmsRegion> util = new ExcelUtil<BmsRegion>(BmsRegion.class);
util.exportExcel(response, list, "区域数据");
}
/**
* 获取区域详细信息
*/
@PreAuthorize("@ss.hasPermi('data:region:query')")
@GetMapping(value = "/{regionId}")
public AjaxResult getInfo(@PathVariable Long regionId)
{
return success(regionService.selectRegionById(regionId));
}
/**
* 修改区域
*/
@PreAuthorize("@ss.hasPermi('data:region:edit')")
@Log(title = "区域管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody BmsRegion region)
{
region.setUpdateBy(getUsername());
return toAjax(regionService.updateRegion(region));
}
/**
* 删除区域
*/
@PreAuthorize("@ss.hasPermi('data:region:remove')")
@Log(title = "区域管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{regionIds}")
public AjaxResult remove(@PathVariable Long[] regionIds)
{
return toAjax(regionService.deleteRegionByIds(regionIds));
}
/**
* 修改区域状态(启用/停用)
*/
@PreAuthorize("@ss.hasPermi('data:region:edit')")
@Log(title = "区域管理", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody BmsRegion region)
{
region.setUpdateBy(getUsername());
return toAjax(regionService.updateRegionStatus(region));
}
/**
* 同步国家数据(异步)
*/
@PreAuthorize("@ss.hasPermi('data:region:sync')")
@Log(title = "区域管理", businessType = BusinessType.OTHER)
@PostMapping("/sync")
public AjaxResult sync()
{
Long logId = syncService.syncRegion();
AjaxResult ajax = AjaxResult.success("同步任务已创建,正在后台执行");
ajax.put("logId", logId);
return ajax;
}
}
@@ -0,0 +1,63 @@
package com.ruoyi.web.controller.data;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.BmsRegionSyncLog;
import com.ruoyi.system.mapper.BmsRegionSyncLogMapper;
/**
* 区域同步日志 信息操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/data/region/log")
public class BmsRegionSyncLogController extends BaseController
{
@Autowired
private BmsRegionSyncLogMapper syncLogMapper;
/**
* 获取同步日志列表
*/
@PreAuthorize("@ss.hasPermi('data:region:log:list')")
@GetMapping("/list")
public TableDataInfo list(BmsRegionSyncLog syncLog)
{
startPage();
List<BmsRegionSyncLog> list = syncLogMapper.selectSyncLogList(syncLog);
return getDataTable(list);
}
/**
* 获取同步日志详细信息
*/
@PreAuthorize("@ss.hasPermi('data:region:log:list')")
@GetMapping(value = "/{logId}")
public AjaxResult getInfo(@PathVariable Long logId)
{
return success(syncLogMapper.selectSyncLogById(logId));
}
/**
* 删除同步日志
*/
@PreAuthorize("@ss.hasPermi('data:region:log:remove')")
@Log(title = "同步日志", businessType = BusinessType.DELETE)
@DeleteMapping("/{logIds}")
public AjaxResult remove(@PathVariable Long[] logIds)
{
return toAjax(syncLogMapper.deleteSyncLogByIds(logIds));
}
}
@@ -6,9 +6,10 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&connectionCollation=utf8mb4_unicode_ci&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root username: root
password: password password: password
connectionInitSqls: SET NAMES utf8mb4
# 从库数据源 # 从库数据源
slave: slave:
# 从数据源开关/默认关闭 # 从数据源开关/默认关闭
@@ -0,0 +1,107 @@
[
{"name":{"common":"China","official":"People's Republic of China"},"cca2":"CN","cca3":"CHN","region":"Asia","subregion":"Eastern Asia","currencies":{"CNY":{"name":"Chinese yuan","symbol":"¥"}},"idd":{"root":"+86","suffixes":[""]},"flag":"🇨🇳","flags":{"png":"https://flagcdn.com/w320/cn.png"},"independent":true},
{"name":{"common":"United States","official":"United States of America"},"cca2":"US","cca3":"USA","region":"Americas","subregion":"North America","currencies":{"USD":{"name":"United States dollar","symbol":"$"}},"idd":{"root":"+1","suffixes":[""]},"flag":"🇺🇸","flags":{"png":"https://flagcdn.com/w320/us.png"},"independent":true},
{"name":{"common":"Germany","official":"Federal Republic of Germany"},"cca2":"DE","cca3":"DEU","region":"Europe","subregion":"Western Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+49","suffixes":[""]},"flag":"🇩🇪","flags":{"png":"https://flagcdn.com/w320/de.png"},"independent":true},
{"name":{"common":"Japan","official":"Japan"},"cca2":"JP","cca3":"JPN","region":"Asia","subregion":"Eastern Asia","currencies":{"JPY":{"name":"Japanese yen","symbol":"¥"}},"idd":{"root":"+81","suffixes":[""]},"flag":"🇯🇵","flags":{"png":"https://flagcdn.com/w320/jp.png"},"independent":true},
{"name":{"common":"South Korea","official":"Republic of Korea"},"cca2":"KR","cca3":"KOR","region":"Asia","subregion":"Eastern Asia","currencies":{"KRW":{"name":"South Korean won","symbol":"₩"}},"idd":{"root":"+82","suffixes":[""]},"flag":"🇰🇷","flags":{"png":"https://flagcdn.com/w320/kr.png"},"independent":true},
{"name":{"common":"United Kingdom","official":"United Kingdom of Great Britain and Northern Ireland"},"cca2":"GB","cca3":"GBR","region":"Europe","subregion":"Northern Europe","currencies":{"GBP":{"name":"British pound","symbol":"£"}},"idd":{"root":"+44","suffixes":[""]},"flag":"🇬🇧","flags":{"png":"https://flagcdn.com/w320/gb.png"},"independent":true},
{"name":{"common":"France","official":"French Republic"},"cca2":"FR","cca3":"FRA","region":"Europe","subregion":"Western Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+33","suffixes":[""]},"flag":"🇫🇷","flags":{"png":"https://flagcdn.com/w320/fr.png"},"independent":true},
{"name":{"common":"Italy","official":"Italian Republic"},"cca2":"IT","cca3":"ITA","region":"Europe","subregion":"Southern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+39","suffixes":[""]},"flag":"🇮🇹","flags":{"png":"https://flagcdn.com/w320/it.png"},"independent":true},
{"name":{"common":"Spain","official":"Kingdom of Spain"},"cca2":"ES","cca3":"ESP","region":"Europe","subregion":"Southern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+34","suffixes":[""]},"flag":"🇪🇸","flags":{"png":"https://flagcdn.com/w320/es.png"},"independent":true},
{"name":{"common":"Netherlands","official":"Kingdom of the Netherlands"},"cca2":"NL","cca3":"NLD","region":"Europe","subregion":"Western Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+31","suffixes":[""]},"flag":"🇳🇱","flags":{"png":"https://flagcdn.com/w320/nl.png"},"independent":true},
{"name":{"common":"Belgium","official":"Kingdom of Belgium"},"cca2":"BE","cca3":"BEL","region":"Europe","subregion":"Western Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+32","suffixes":[""]},"flag":"🇧🇪","flags":{"png":"https://flagcdn.com/w320/be.png"},"independent":true},
{"name":{"common":"Switzerland","official":"Swiss Confederation"},"cca2":"CH","cca3":"CHE","region":"Europe","subregion":"Western Europe","currencies":{"CHF":{"name":"Swiss franc","symbol":"Fr."}},"idd":{"root":"+41","suffixes":[""]},"flag":"🇨🇭","flags":{"png":"https://flagcdn.com/w320/ch.png"},"independent":true},
{"name":{"common":"Austria","official":"Republic of Austria"},"cca2":"AT","cca3":"AUT","region":"Europe","subregion":"Western Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+43","suffixes":[""]},"flag":"🇦🇹","flags":{"png":"https://flagcdn.com/w320/at.png"},"independent":true},
{"name":{"common":"Sweden","official":"Kingdom of Sweden"},"cca2":"SE","cca3":"SWE","region":"Europe","subregion":"Northern Europe","currencies":{"SEK":{"name":"Swedish krona","symbol":"kr"}},"idd":{"root":"+46","suffixes":[""]},"flag":"🇸🇪","flags":{"png":"https://flagcdn.com/w320/se.png"},"independent":true},
{"name":{"common":"Norway","official":"Kingdom of Norway"},"cca2":"NO","cca3":"NOR","region":"Europe","subregion":"Northern Europe","currencies":{"NOK":{"name":"Norwegian krone","symbol":"kr"}},"idd":{"root":"+47","suffixes":[""]},"flag":"🇳🇴","flags":{"png":"https://flagcdn.com/w320/no.png"},"independent":true},
{"name":{"common":"Denmark","official":"Kingdom of Denmark"},"cca2":"DK","cca3":"DNK","region":"Europe","subregion":"Northern Europe","currencies":{"DKK":{"name":"Danish krone","symbol":"kr"}},"idd":{"root":"+45","suffixes":[""]},"flag":"🇩🇰","flags":{"png":"https://flagcdn.com/w320/dk.png"},"independent":true},
{"name":{"common":"Finland","official":"Republic of Finland"},"cca2":"FI","cca3":"FIN","region":"Europe","subregion":"Northern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+358","suffixes":[""]},"flag":"🇫🇮","flags":{"png":"https://flagcdn.com/w320/fi.png"},"independent":true},
{"name":{"common":"Poland","official":"Republic of Poland"},"cca2":"PL","cca3":"POL","region":"Europe","subregion":"Central Europe","currencies":{"PLN":{"name":"Polish zloty","symbol":"zł"}},"idd":{"root":"+48","suffixes":[""]},"flag":"🇵🇱","flags":{"png":"https://flagcdn.com/w320/pl.png"},"independent":true},
{"name":{"common":"Russia","official":"Russian Federation"},"cca2":"RU","cca3":"RUS","region":"Europe","subregion":"Eastern Europe","currencies":{"RUB":{"name":"Russian ruble","symbol":"₽"}},"idd":{"root":"+7","suffixes":[""]},"flag":"🇷🇺","flags":{"png":"https://flagcdn.com/w320/ru.png"},"independent":true},
{"name":{"common":"India","official":"Republic of India"},"cca2":"IN","cca3":"IND","region":"Asia","subregion":"Southern Asia","currencies":{"INR":{"name":"Indian rupee","symbol":"₹"}},"idd":{"root":"+91","suffixes":[""]},"flag":"🇮🇳","flags":{"png":"https://flagcdn.com/w320/in.png"},"independent":true},
{"name":{"common":"Brazil","official":"Federative Republic of Brazil"},"cca2":"BR","cca3":"BRA","region":"Americas","subregion":"South America","currencies":{"BRL":{"name":"Brazilian real","symbol":"R$"}},"idd":{"root":"+55","suffixes":[""]},"flag":"🇧🇷","flags":{"png":"https://flagcdn.com/w320/br.png"},"independent":true},
{"name":{"common":"Canada","official":"Canada"},"cca2":"CA","cca3":"CAN","region":"Americas","subregion":"North America","currencies":{"CAD":{"name":"Canadian dollar","symbol":"$"}},"idd":{"root":"+1","suffixes":[""]},"flag":"🇨🇦","flags":{"png":"https://flagcdn.com/w320/ca.png"},"independent":true},
{"name":{"common":"Australia","official":"Commonwealth of Australia"},"cca2":"AU","cca3":"AUS","region":"Oceania","subregion":"Australia and New Zealand","currencies":{"AUD":{"name":"Australian dollar","symbol":"$"}},"idd":{"root":"+61","suffixes":[""]},"flag":"🇦🇺","flags":{"png":"https://flagcdn.com/w320/au.png"},"independent":true},
{"name":{"common":"Mexico","official":"United Mexican States"},"cca2":"MX","cca3":"MEX","region":"Americas","subregion":"North America","currencies":{"MXN":{"name":"Mexican peso","symbol":"$"}},"idd":{"root":"+52","suffixes":[""]},"flag":"🇲🇽","flags":{"png":"https://flagcdn.com/w320/mx.png"},"independent":true},
{"name":{"common":"Indonesia","official":"Republic of Indonesia"},"cca2":"ID","cca3":"IDN","region":"Asia","subregion":"South-Eastern Asia","currencies":{"IDR":{"name":"Indonesian rupiah","symbol":"Rp"}},"idd":{"root":"+62","suffixes":[""]},"flag":"🇮🇩","flags":{"png":"https://flagcdn.com/w320/id.png"},"independent":true},
{"name":{"common":"Thailand","official":"Kingdom of Thailand"},"cca2":"TH","cca3":"THA","region":"Asia","subregion":"South-Eastern Asia","currencies":{"THB":{"name":"Thai baht","symbol":"฿"}},"idd":{"root":"+66","suffixes":[""]},"flag":"🇹🇭","flags":{"png":"https://flagcdn.com/w320/th.png"},"independent":true},
{"name":{"common":"Vietnam","official":"Socialist Republic of Vietnam"},"cca2":"VN","cca3":"VNM","region":"Asia","subregion":"South-Eastern Asia","currencies":{"VND":{"name":"Vietnamese đồng","symbol":"₫"}},"idd":{"root":"+84","suffixes":[""]},"flag":"🇻🇳","flags":{"png":"https://flagcdn.com/w320/vn.png"},"independent":true},
{"name":{"common":"Malaysia","official":"Malaysia"},"cca2":"MY","cca3":"MYS","region":"Asia","subregion":"South-Eastern Asia","currencies":{"MYR":{"name":"Malaysian ringgit","symbol":"RM"}},"idd":{"root":"+60","suffixes":[""]},"flag":"🇲🇾","flags":{"png":"https://flagcdn.com/w320/my.png"},"independent":true},
{"name":{"common":"Singapore","official":"Republic of Singapore"},"cca2":"SG","cca3":"SGP","region":"Asia","subregion":"South-Eastern Asia","currencies":{"SGD":{"name":"Singapore dollar","symbol":"$"}},"idd":{"root":"+65","suffixes":[""]},"flag":"🇸🇬","flags":{"png":"https://flagcdn.com/w320/sg.png"},"independent":true},
{"name":{"common":"Philippines","official":"Republic of the Philippines"},"cca2":"PH","cca3":"PHL","region":"Asia","subregion":"South-Eastern Asia","currencies":{"PHP":{"name":"Philippine peso","symbol":"₱"}},"idd":{"root":"+63","suffixes":[""]},"flag":"🇵🇭","flags":{"png":"https://flagcdn.com/w320/ph.png"},"independent":true},
{"name":{"common":"Turkey","official":"Republic of Türkiye"},"cca2":"TR","cca3":"TUR","region":"Asia","subregion":"Western Asia","currencies":{"TRY":{"name":"Turkish lira","symbol":"₺"}},"idd":{"root":"+90","suffixes":[""]},"flag":"🇹🇷","flags":{"png":"https://flagcdn.com/w320/tr.png"},"independent":true},
{"name":{"common":"Saudi Arabia","official":"Kingdom of Saudi Arabia"},"cca2":"SA","cca3":"SAU","region":"Asia","subregion":"Western Asia","currencies":{"SAR":{"name":"Saudi riyal","symbol":"ر.س"}},"idd":{"root":"+966","suffixes":[""]},"flag":"🇸🇦","flags":{"png":"https://flagcdn.com/w320/sa.png"},"independent":true},
{"name":{"common":"United Arab Emirates","official":"United Arab Emirates"},"cca2":"AE","cca3":"ARE","region":"Asia","subregion":"Western Asia","currencies":{"AED":{"name":"United Arab Emirates dirham","symbol":"د.إ"}},"idd":{"root":"+971","suffixes":[""]},"flag":"🇦🇪","flags":{"png":"https://flagcdn.com/w320/ae.png"},"independent":true},
{"name":{"common":"Israel","official":"State of Israel"},"cca2":"IL","cca3":"ISR","region":"Asia","subregion":"Western Asia","currencies":{"ILS":{"name":"Israeli new shekel","symbol":"₪"}},"idd":{"root":"+972","suffixes":[""]},"flag":"🇮🇱","flags":{"png":"https://flagcdn.com/w320/il.png"},"independent":true},
{"name":{"common":"Egypt","official":"Arab Republic of Egypt"},"cca2":"EG","cca3":"EGY","region":"Africa","subregion":"Northern Africa","currencies":{"EGP":{"name":"Egyptian pound","symbol":"£"}},"idd":{"root":"+20","suffixes":[""]},"flag":"🇪🇬","flags":{"png":"https://flagcdn.com/w320/eg.png"},"independent":true},
{"name":{"common":"South Africa","official":"Republic of South Africa"},"cca2":"ZA","cca3":"ZAF","region":"Africa","subregion":"Southern Africa","currencies":{"ZAR":{"name":"South African rand","symbol":"R"}},"idd":{"root":"+27","suffixes":[""]},"flag":"🇿🇦","flags":{"png":"https://flagcdn.com/w320/za.png"},"independent":true},
{"name":{"common":"Nigeria","official":"Federal Republic of Nigeria"},"cca2":"NG","cca3":"NGA","region":"Africa","subregion":"Western Africa","currencies":{"NGN":{"name":"Nigerian naira","symbol":"₦"}},"idd":{"root":"+234","suffixes":[""]},"flag":"🇳🇬","flags":{"png":"https://flagcdn.com/w320/ng.png"},"independent":true},
{"name":{"common":"Argentina","official":"Argentine Republic"},"cca2":"AR","cca3":"ARG","region":"Americas","subregion":"South America","currencies":{"ARS":{"name":"Argentine peso","symbol":"$"}},"idd":{"root":"+54","suffixes":[""]},"flag":"🇦🇷","flags":{"png":"https://flagcdn.com/w320/ar.png"},"independent":true},
{"name":{"common":"Chile","official":"Republic of Chile"},"cca2":"CL","cca3":"CHL","region":"Americas","subregion":"South America","currencies":{"CLP":{"name":"Chilean peso","symbol":"$"}},"idd":{"root":"+56","suffixes":[""]},"flag":"🇨🇱","flags":{"png":"https://flagcdn.com/w320/cl.png"},"independent":true},
{"name":{"common":"Colombia","official":"Republic of Colombia"},"cca2":"CO","cca3":"COL","region":"Americas","subregion":"South America","currencies":{"COP":{"name":"Colombian peso","symbol":"$"}},"idd":{"root":"+57","suffixes":[""]},"flag":"🇨🇴","flags":{"png":"https://flagcdn.com/w320/co.png"},"independent":true},
{"name":{"common":"Peru","official":"Republic of Peru"},"cca2":"PE","cca3":"PER","region":"Americas","subregion":"South America","currencies":{"PEN":{"name":"Peruvian sol","symbol":"S/"}},"idd":{"root":"+51","suffixes":[""]},"flag":"🇵🇪","flags":{"png":"https://flagcdn.com/w320/pe.png"},"independent":true},
{"name":{"common":"New Zealand","official":"New Zealand"},"cca2":"NZ","cca3":"NZL","region":"Oceania","subregion":"Australia and New Zealand","currencies":{"NZD":{"name":"New Zealand dollar","symbol":"$"}},"idd":{"root":"+64","suffixes":[""]},"flag":"🇳🇿","flags":{"png":"https://flagcdn.com/w320/nz.png"},"independent":true},
{"name":{"common":"Ireland","official":"Republic of Ireland"},"cca2":"IE","cca3":"IRL","region":"Europe","subregion":"Northern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+353","suffixes":[""]},"flag":"🇮🇪","flags":{"png":"https://flagcdn.com/w320/ie.png"},"independent":true},
{"name":{"common":"Portugal","official":"Portuguese Republic"},"cca2":"PT","cca3":"PRT","region":"Europe","subregion":"Southern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+351","suffixes":[""]},"flag":"🇵🇹","flags":{"png":"https://flagcdn.com/w320/pt.png"},"independent":true},
{"name":{"common":"Greece","official":"Hellenic Republic"},"cca2":"GR","cca3":"GRC","region":"Europe","subregion":"Southern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+30","suffixes":[""]},"flag":"🇬🇷","flags":{"png":"https://flagcdn.com/w320/gr.png"},"independent":true},
{"name":{"common":"Czechia","official":"Czech Republic"},"cca2":"CZ","cca3":"CZE","region":"Europe","subregion":"Central Europe","currencies":{"CZK":{"name":"Czech koruna","symbol":"Kč"}},"idd":{"root":"+420","suffixes":[""]},"flag":"🇨🇿","flags":{"png":"https://flagcdn.com/w320/cz.png"},"independent":true},
{"name":{"common":"Hungary","official":"Hungary"},"cca2":"HU","cca3":"HUN","region":"Europe","subregion":"Central Europe","currencies":{"HUF":{"name":"Hungarian forint","symbol":"Ft"}},"idd":{"root":"+36","suffixes":[""]},"flag":"🇭🇺","flags":{"png":"https://flagcdn.com/w320/hu.png"},"independent":true},
{"name":{"common":"Romania","official":"Romania"},"cca2":"RO","cca3":"ROU","region":"Europe","subregion":"Central Europe","currencies":{"RON":{"name":"Romanian leu","symbol":"lei"}},"idd":{"root":"+40","suffixes":[""]},"flag":"🇷🇴","flags":{"png":"https://flagcdn.com/w320/ro.png"},"independent":true},
{"name":{"common":"Bulgaria","official":"Republic of Bulgaria"},"cca2":"BG","cca3":"BGR","region":"Europe","subregion":"Southeast Europe","currencies":{"BGN":{"name":"Bulgarian lev","symbol":"лв"}},"idd":{"root":"+359","suffixes":[""]},"flag":"🇧🇬","flags":{"png":"https://flagcdn.com/w320/bg.png"},"independent":true},
{"name":{"common":"Croatia","official":"Republic of Croatia"},"cca2":"HR","cca3":"HRV","region":"Europe","subregion":"Southeast Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+385","suffixes":[""]},"flag":"🇭🇷","flags":{"png":"https://flagcdn.com/w320/hr.png"},"independent":true},
{"name":{"common":"Slovakia","official":"Slovak Republic"},"cca2":"SK","cca3":"SVK","region":"Europe","subregion":"Central Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+421","suffixes":[""]},"flag":"🇸🇰","flags":{"png":"https://flagcdn.com/w320/sk.png"},"independent":true},
{"name":{"common":"Slovenia","official":"Republic of Slovenia"},"cca2":"SI","cca3":"SVN","region":"Europe","subregion":"Central Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+386","suffixes":[""]},"flag":"🇸🇮","flags":{"png":"https://flagcdn.com/w320/si.png"},"independent":true},
{"name":{"common":"Lithuania","official":"Republic of Lithuania"},"cca2":"LT","cca3":"LTU","region":"Europe","subregion":"Northern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+370","suffixes":[""]},"flag":"🇱🇹","flags":{"png":"https://flagcdn.com/w320/lt.png"},"independent":true},
{"name":{"common":"Latvia","official":"Republic of Latvia"},"cca2":"LV","cca3":"LVA","region":"Europe","subregion":"Northern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+371","suffixes":[""]},"flag":"🇱🇻","flags":{"png":"https://flagcdn.com/w320/lv.png"},"independent":true},
{"name":{"common":"Estonia","official":"Republic of Estonia"},"cca2":"EE","cca3":"EST","region":"Europe","subregion":"Northern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+372","suffixes":[""]},"flag":"🇪🇪","flags":{"png":"https://flagcdn.com/w320/ee.png"},"independent":true},
{"name":{"common":"Ukraine","official":"Ukraine"},"cca2":"UA","cca3":"UKR","region":"Europe","subregion":"Eastern Europe","currencies":{"UAH":{"name":"Ukrainian hryvnia","symbol":"₴"}},"idd":{"root":"+380","suffixes":[""]},"flag":"🇺🇦","flags":{"png":"https://flagcdn.com/w320/ua.png"},"independent":true},
{"name":{"common":"Morocco","official":"Kingdom of Morocco"},"cca2":"MA","cca3":"MAR","region":"Africa","subregion":"Northern Africa","currencies":{"MAD":{"name":"Moroccan dirham","symbol":"د.م."}},"idd":{"root":"+212","suffixes":[""]},"flag":"🇲🇦","flags":{"png":"https://flagcdn.com/w320/ma.png"},"independent":true},
{"name":{"common":"Kenya","official":"Republic of Kenya"},"cca2":"KE","cca3":"KEN","region":"Africa","subregion":"Eastern Africa","currencies":{"KES":{"name":"Kenyan shilling","symbol":"KSh"}},"idd":{"root":"+254","suffixes":[""]},"flag":"🇰🇪","flags":{"png":"https://flagcdn.com/w320/ke.png"},"independent":true},
{"name":{"common":"Pakistan","official":"Islamic Republic of Pakistan"},"cca2":"PK","cca3":"PAK","region":"Asia","subregion":"Southern Asia","currencies":{"PKR":{"name":"Pakistani rupee","symbol":"₨"}},"idd":{"root":"+92","suffixes":[""]},"flag":"🇵🇰","flags":{"png":"https://flagcdn.com/w320/pk.png"},"independent":true},
{"name":{"common":"Bangladesh","official":"People's Republic of Bangladesh"},"cca2":"BD","cca3":"BGD","region":"Asia","subregion":"Southern Asia","currencies":{"BDT":{"name":"Bangladeshi taka","symbol":"৳"}},"idd":{"root":"+880","suffixes":[""]},"flag":"🇧🇩","flags":{"png":"https://flagcdn.com/w320/bd.png"},"independent":true},
{"name":{"common":"Iran","official":"Islamic Republic of Iran"},"cca2":"IR","cca3":"IRN","region":"Asia","subregion":"Southern Asia","currencies":{"IRR":{"name":"Iranian rial","symbol":"﷼"}},"idd":{"root":"+98","suffixes":[""]},"flag":"🇮🇷","flags":{"png":"https://flagcdn.com/w320/ir.png"},"independent":true},
{"name":{"common":"Iraq","official":"Republic of Iraq"},"cca2":"IQ","cca3":"IRQ","region":"Asia","subregion":"Western Asia","currencies":{"IQD":{"name":"Iraqi dinar","symbol":"د.ع"}},"idd":{"root":"+964","suffixes":[""]},"flag":"🇮🇶","flags":{"png":"https://flagcdn.com/w320/iq.png"},"independent":true},
{"name":{"common":"Kuwait","official":"State of Kuwait"},"cca2":"KW","cca3":"KWT","region":"Asia","subregion":"Western Asia","currencies":{"KWD":{"name":"Kuwaiti dinar","symbol":"د.ك"}},"idd":{"root":"+965","suffixes":[""]},"flag":"🇰🇼","flags":{"png":"https://flagcdn.com/w320/kw.png"},"independent":true},
{"name":{"common":"Qatar","official":"State of Qatar"},"cca2":"QA","cca3":"QAT","region":"Asia","subregion":"Western Asia","currencies":{"QAR":{"name":"Qatari riyal","symbol":"ر.ق"}},"idd":{"root":"+974","suffixes":[""]},"flag":"🇶🇦","flags":{"png":"https://flagcdn.com/w320/qa.png"},"independent":true},
{"name":{"common":"Hong Kong","official":"Hong Kong Special Administrative Region of China"},"cca2":"HK","cca3":"HKG","region":"Asia","subregion":"Eastern Asia","currencies":{"HKD":{"name":"Hong Kong dollar","symbol":"$"}},"idd":{"root":"+852","suffixes":[""]},"flag":"🇭🇰","flags":{"png":"https://flagcdn.com/w320/hk.png"},"independent":false},
{"name":{"common":"Taiwan","official":"Republic of China"},"cca2":"TW","cca3":"TWN","region":"Asia","subregion":"Eastern Asia","currencies":{"TWD":{"name":"New Taiwan dollar","symbol":"$"}},"idd":{"root":"+886","suffixes":[""]},"flag":"🇹🇼","flags":{"png":"https://flagcdn.com/w320/tw.png"},"independent":false},
{"name":{"common":"Iceland","official":"Iceland"},"cca2":"IS","cca3":"ISL","region":"Europe","subregion":"Northern Europe","currencies":{"ISK":{"name":"Icelandic króna","symbol":"kr"}},"idd":{"root":"+354","suffixes":[""]},"flag":"🇮🇸","flags":{"png":"https://flagcdn.com/w320/is.png"},"independent":true},
{"name":{"common":"Luxembourg","official":"Grand Duchy of Luxembourg"},"cca2":"LU","cca3":"LUX","region":"Europe","subregion":"Western Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+352","suffixes":[""]},"flag":"🇱🇺","flags":{"png":"https://flagcdn.com/w320/lu.png"},"independent":true},
{"name":{"common":"Malta","official":"Republic of Malta"},"cca2":"MT","cca3":"MLT","region":"Europe","subregion":"Southern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+356","suffixes":[""]},"flag":"🇲🇹","flags":{"png":"https://flagcdn.com/w320/mt.png"},"independent":true},
{"name":{"common":"Cyprus","official":"Republic of Cyprus"},"cca2":"CY","cca3":"CYP","region":"Europe","subregion":"Southern Europe","currencies":{"EUR":{"name":"Euro","symbol":"€"}},"idd":{"root":"+357","suffixes":[""]},"flag":"🇨🇾","flags":{"png":"https://flagcdn.com/w320/cy.png"},"independent":true},
{"name":{"common":"Albania","official":"Republic of Albania"},"cca2":"AL","cca3":"ALB","region":"Europe","subregion":"Southeast Europe","currencies":{"ALL":{"name":"Albanian lek","symbol":"L"}},"idd":{"root":"+355","suffixes":[""]},"flag":"🇦🇱","flags":{"png":"https://flagcdn.com/w320/al.png"},"independent":true},
{"name":{"common":"Serbia","official":"Republic of Serbia"},"cca2":"RS","cca3":"SRB","region":"Europe","subregion":"Southeast Europe","currencies":{"RSD":{"name":"Serbian dinar","symbol":"дин"}},"idd":{"root":"+381","suffixes":[""]},"flag":"🇷🇸","flags":{"png":"https://flagcdn.com/w320/rs.png"},"independent":true},
{"name":{"common":"Belarus","official":"Republic of Belarus"},"cca2":"BY","cca3":"BLR","region":"Europe","subregion":"Eastern Europe","currencies":{"BYN":{"name":"Belarusian ruble","symbol":"Br"}},"idd":{"root":"+375","suffixes":[""]},"flag":"🇧🇾","flags":{"png":"https://flagcdn.com/w320/by.png"},"independent":true},
{"name":{"common":"Kazakhstan","official":"Republic of Kazakhstan"},"cca2":"KZ","cca3":"KAZ","region":"Asia","subregion":"Central Asia","currencies":{"KZT":{"name":"Kazakhstani tenge","symbol":"₸"}},"idd":{"root":"+7","suffixes":[""]},"flag":"🇰🇿","flags":{"png":"https://flagcdn.com/w320/kz.png"},"independent":true},
{"name":{"common":"Uzbekistan","official":"Republic of Uzbekistan"},"cca2":"UZ","cca3":"UZB","region":"Asia","subregion":"Central Asia","currencies":{"UZS":{"name":"Uzbekistani soʻm","symbol":"so'm"}},"idd":{"root":"+998","suffixes":[""]},"flag":"🇺🇿","flags":{"png":"https://flagcdn.com/w320/uz.png"},"independent":true},
{"name":{"common":"Sri Lanka","official":"Democratic Socialist Republic of Sri Lanka"},"cca2":"LK","cca3":"LKA","region":"Asia","subregion":"Southern Asia","currencies":{"LKR":{"name":"Sri Lankan rupee","symbol":"Rs"}},"idd":{"root":"+94","suffixes":[""]},"flag":"🇱🇰","flags":{"png":"https://flagcdn.com/w320/lk.png"},"independent":true},
{"name":{"common":"Cambodia","official":"Kingdom of Cambodia"},"cca2":"KH","cca3":"KHM","region":"Asia","subregion":"South-Eastern Asia","currencies":{"KHR":{"name":"Cambodian riel","symbol":"៛"}},"idd":{"root":"+855","suffixes":[""]},"flag":"🇰🇭","flags":{"png":"https://flagcdn.com/w320/kh.png"},"independent":true},
{"name":{"common":"Myanmar","official":"Republic of the Union of Myanmar"},"cca2":"MM","cca3":"MMR","region":"Asia","subregion":"South-Eastern Asia","currencies":{"MMK":{"name":"Burmese kyat","symbol":"K"}},"idd":{"root":"+95","suffixes":[""]},"flag":"🇲🇲","flags":{"png":"https://flagcdn.com/w320/mm.png"},"independent":true},
{"name":{"common":"Laos","official":"Lao People's Democratic Republic"},"cca2":"LA","cca3":"LAO","region":"Asia","subregion":"South-Eastern Asia","currencies":{"LAK":{"name":"Lao kip","symbol":"₭"}},"idd":{"root":"+856","suffixes":[""]},"flag":"🇱🇦","flags":{"png":"https://flagcdn.com/w320/la.png"},"independent":true},
{"name":{"common":"Nepal","official":"Federal Democratic Republic of Nepal"},"cca2":"NP","cca3":"NPL","region":"Asia","subregion":"Southern Asia","currencies":{"NPR":{"name":"Nepalese rupee","symbol":"रू"}},"idd":{"root":"+977","suffixes":[""]},"flag":"🇳🇵","flags":{"png":"https://flagcdn.com/w320/np.png"},"independent":true},
{"name":{"common":"Afghanistan","official":"Islamic Republic of Afghanistan"},"cca2":"AF","cca3":"AFG","region":"Asia","subregion":"Southern Asia","currencies":{"AFN":{"name":"Afghan afghani","symbol":"؋"}},"idd":{"root":"+93","suffixes":[""]},"flag":"🇦🇫","flags":{"png":"https://flagcdn.com/w320/af.png"},"independent":true},
{"name":{"common":"Jordan","official":"Hashemite Kingdom of Jordan"},"cca2":"JO","cca3":"JOR","region":"Asia","subregion":"Western Asia","currencies":{"JOD":{"name":"Jordanian dinar","symbol":"د.ا"}},"idd":{"root":"+962","suffixes":[""]},"flag":"🇯🇴","flags":{"png":"https://flagcdn.com/w320/jo.png"},"independent":true},
{"name":{"common":"Lebanon","official":"Lebanese Republic"},"cca2":"LB","cca3":"LBN","region":"Asia","subregion":"Western Asia","currencies":{"LBP":{"name":"Lebanese pound","symbol":"ل.ل"}},"idd":{"root":"+961","suffixes":[""]},"flag":"🇱🇧","flags":{"png":"https://flagcdn.com/w320/lb.png"},"independent":true},
{"name":{"common":"Oman","official":"Sultanate of Oman"},"cca2":"OM","cca3":"OMN","region":"Asia","subregion":"Western Asia","currencies":{"OMR":{"name":"Omani rial","symbol":"ر.ع."}},"idd":{"root":"+968","suffixes":[""]},"flag":"🇴🇲","flags":{"png":"https://flagcdn.com/w320/om.png"},"independent":true},
{"name":{"common":"Bahrain","official":"Kingdom of Bahrain"},"cca2":"BH","cca3":"BHR","region":"Asia","subregion":"Western Asia","currencies":{"BHD":{"name":"Bahraini dinar","symbol":"د.ب"}},"idd":{"root":"+973","suffixes":[""]},"flag":"🇧🇭","flags":{"png":"https://flagcdn.com/w320/bh.png"},"independent":true},
{"name":{"common":"Algeria","official":"People's Democratic Republic of Algeria"},"cca2":"DZ","cca3":"DZA","region":"Africa","subregion":"Northern Africa","currencies":{"DZD":{"name":"Algerian dinar","symbol":"دج"}},"idd":{"root":"+213","suffixes":[""]},"flag":"🇩🇿","flags":{"png":"https://flagcdn.com/w320/dz.png"},"independent":true},
{"name":{"common":"Tunisia","official":"Republic of Tunisia"},"cca2":"TN","cca3":"TUN","region":"Africa","subregion":"Northern Africa","currencies":{"TND":{"name":"Tunisian dinar","symbol":"د.ت"}},"idd":{"root":"+216","suffixes":[""]},"flag":"🇹🇳","flags":{"png":"https://flagcdn.com/w320/tn.png"},"independent":true},
{"name":{"common":"Ghana","official":"Republic of Ghana"},"cca2":"GH","cca3":"GHA","region":"Africa","subregion":"Western Africa","currencies":{"GHS":{"name":"Ghanaian cedi","symbol":"₵"}},"idd":{"root":"+233","suffixes":[""]},"flag":"🇬🇭","flags":{"png":"https://flagcdn.com/w320/gh.png"},"independent":true},
{"name":{"common":"Ethiopia","official":"Federal Democratic Republic of Ethiopia"},"cca2":"ET","cca3":"ETH","region":"Africa","subregion":"Eastern Africa","currencies":{"ETB":{"name":"Ethiopian birr","symbol":"Br"}},"idd":{"root":"+251","suffixes":[""]},"flag":"🇪🇹","flags":{"png":"https://flagcdn.com/w320/et.png"},"independent":true},
{"name":{"common":"Tanzania","official":"United Republic of Tanzania"},"cca2":"TZ","cca3":"TZA","region":"Africa","subregion":"Eastern Africa","currencies":{"TZS":{"name":"Tanzanian shilling","symbol":"Sh"}},"idd":{"root":"+255","suffixes":[""]},"flag":"🇹🇿","flags":{"png":"https://flagcdn.com/w320/tz.png"},"independent":true},
{"name":{"common":"Uganda","official":"Republic of Uganda"},"cca2":"UG","cca3":"UGA","region":"Africa","subregion":"Eastern Africa","currencies":{"UGX":{"name":"Ugandan shilling","symbol":"Sh"}},"idd":{"root":"+256","suffixes":[""]},"flag":"🇺🇬","flags":{"png":"https://flagcdn.com/w320/ug.png"},"independent":true},
{"name":{"common":"Cameroon","official":"Republic of Cameroon"},"cca2":"CM","cca3":"CMR","region":"Africa","subregion":"Middle Africa","currencies":{"XAF":{"name":"Central African CFA franc","symbol":"FCFA"}},"idd":{"root":"+237","suffixes":[""]},"flag":"🇨🇲","flags":{"png":"https://flagcdn.com/w320/cm.png"},"independent":true},
{"name":{"common":"Angola","official":"Republic of Angola"},"cca2":"AO","cca3":"AGO","region":"Africa","subregion":"Middle Africa","currencies":{"AOA":{"name":"Angolan kwanza","symbol":"Kz"}},"idd":{"root":"+244","suffixes":[""]},"flag":"🇦🇴","flags":{"png":"https://flagcdn.com/w320/ao.png"},"independent":true},
{"name":{"common":"Senegal","official":"Republic of Senegal"},"cca2":"SN","cca3":"SEN","region":"Africa","subregion":"Western Africa","currencies":{"XOF":{"name":"West African CFA franc","symbol":"FCFA"}},"idd":{"root":"+221","suffixes":[""]},"flag":"🇸🇳","flags":{"png":"https://flagcdn.com/w320/sn.png"},"independent":true},
{"name":{"common":"Zimbabwe","official":"Republic of Zimbabwe"},"cca2":"ZW","cca3":"ZWE","region":"Africa","subregion":"Eastern Africa","currencies":{"ZWL":{"name":"Zimbabwean dollar","symbol":"$"}},"idd":{"root":"+263","suffixes":[""]},"flag":"🇿🇼","flags":{"png":"https://flagcdn.com/w320/zw.png"},"independent":true},
{"name":{"common":"Bolivia","official":"Plurinational State of Bolivia"},"cca2":"BO","cca3":"BOL","region":"Americas","subregion":"South America","currencies":{"BOB":{"name":"Bolivian boliviano","symbol":"Bs"}},"idd":{"root":"+591","suffixes":[""]},"flag":"🇧🇴","flags":{"png":"https://flagcdn.com/w320/bo.png"},"independent":true},
{"name":{"common":"Ecuador","official":"Republic of Ecuador"},"cca2":"EC","cca3":"ECU","region":"Americas","subregion":"South America","currencies":{"USD":{"name":"United States dollar","symbol":"$"}},"idd":{"root":"+593","suffixes":[""]},"flag":"🇪🇨","flags":{"png":"https://flagcdn.com/w320/ec.png"},"independent":true},
{"name":{"common":"Uruguay","official":"Oriental Republic of Uruguay"},"cca2":"UY","cca3":"URY","region":"Americas","subregion":"South America","currencies":{"UYU":{"name":"Uruguayan peso","symbol":"$"}},"idd":{"root":"+598","suffixes":[""]},"flag":"🇺🇾","flags":{"png":"https://flagcdn.com/w320/uy.png"},"independent":true},
{"name":{"common":"Paraguay","official":"Republic of Paraguay"},"cca2":"PY","cca3":"PRY","region":"Americas","subregion":"South America","currencies":{"PYG":{"name":"Paraguayan guaraní","symbol":"₲"}},"idd":{"root":"+595","suffixes":[""]},"flag":"🇵🇾","flags":{"png":"https://flagcdn.com/w320/py.png"},"independent":true},
{"name":{"common":"Venezuela","official":"Bolivarian Republic of Venezuela"},"cca2":"VE","cca3":"VEN","region":"Americas","subregion":"South America","currencies":{"VES":{"name":"Venezuelan bolívar soberano","symbol":"Bs.S"}},"idd":{"root":"+58","suffixes":[""]},"flag":"🇻🇪","flags":{"png":"https://flagcdn.com/w320/ve.png"},"independent":true},
{"name":{"common":"Dominican Republic","official":"Dominican Republic"},"cca2":"DO","cca3":"DOM","region":"Americas","subregion":"Caribbean","currencies":{"DOP":{"name":"Dominican peso","symbol":"$"}},"idd":{"root":"+1","suffixes":["809"]},"flag":"🇩🇴","flags":{"png":"https://flagcdn.com/w320/do.png"},"independent":true},
{"name":{"common":"Guatemala","official":"Republic of Guatemala"},"cca2":"GT","cca3":"GTM","region":"Americas","subregion":"Central America","currencies":{"GTQ":{"name":"Guatemalan quetzal","symbol":"Q"}},"idd":{"root":"+502","suffixes":[""]},"flag":"🇬🇹","flags":{"png":"https://flagcdn.com/w320/gt.png"},"independent":true},
{"name":{"common":"Panama","official":"Republic of Panama"},"cca2":"PA","cca3":"PAN","region":"Americas","subregion":"Central America","currencies":{"USD":{"name":"United States dollar","symbol":"$"}},"idd":{"root":"+507","suffixes":[""]},"flag":"🇵🇦","flags":{"png":"https://flagcdn.com/w320/pa.png"},"independent":true},
{"name":{"common":"Costa Rica","official":"Republic of Costa Rica"},"cca2":"CR","cca3":"CRI","region":"Americas","subregion":"Central America","currencies":{"CRC":{"name":"Costa Rican colón","symbol":"₡"}},"idd":{"root":"+506","suffixes":[""]},"flag":"🇨🇷","flags":{"png":"https://flagcdn.com/w320/cr.png"},"independent":true},
{"name":{"common":"Vietnam","official":"Socialist Republic of Vietnam"},"cca2":"VN","cca3":"VNM","region":"Asia","subregion":"South-Eastern Asia","currencies":{"VND":{"name":"Vietnamese đồng","symbol":"₫"}},"idd":{"root":"+84","suffixes":[""]},"flag":"🇻🇳","flags":{"png":"https://flagcdn.com/w320/vn.png"},"independent":true}
]
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<!-- 日志存放路径 --> <!-- 日志存放路径 -->
<property name="log.path" value="/home/ruoyi/logs" /> <property name="log.path" value="${user.dir}/logs" />
<!-- 日志输出格式 --> <!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" /> <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
@@ -0,0 +1,25 @@
package com.ruoyi.framework.config;
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate配置
*
* @author ruoyi
*/
@Configuration
public class RestTemplateConfig
{
@Bean
public RestTemplate restTemplate()
{
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(10000);
factory.setReadTimeout(30000);
return new RestTemplate(factory);
}
}
@@ -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;
}
}
@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.BmsRegionMapper">
<resultMap type="BmsRegion" id="BmsRegionResult">
<id property="regionId" column="region_id" />
<result property="countryNameCn" column="country_name_cn" />
<result property="countryNameEn" column="country_name_en" />
<result property="countryCode2" column="country_code_2" />
<result property="countryCode3" column="country_code_3" />
<result property="continent" column="continent" />
<result property="subRegion" column="sub_region" />
<result property="currencyCode" column="currency_code" />
<result property="currencyName" column="currency_name" />
<result property="currencySymbol" column="currency_symbol" />
<result property="phonePrefix" column="phone_prefix" />
<result property="flagEmoji" column="flag_emoji" />
<result property="flagUrl" column="flag_url" />
<result property="isIndependent" column="is_independent" />
<result property="isEu" column="is_eu" />
<result property="status" column="status" />
<result property="delFlag" column="del_flag" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectRegionVo">
select region_id, country_name_cn, country_name_en, country_code_2, country_code_3,
continent, sub_region, currency_code, currency_name, currency_symbol,
phone_prefix, flag_emoji, flag_url, is_independent, is_eu,
status, del_flag, create_by, create_time, update_by, update_time, remark
from bms_region
</sql>
<select id="selectRegionById" parameterType="Long" resultMap="BmsRegionResult">
<include refid="selectRegionVo"/>
where region_id = #{regionId}
</select>
<select id="selectRegionByCode2" parameterType="String" resultMap="BmsRegionResult">
<include refid="selectRegionVo"/>
where country_code_2 = #{countryCode2} limit 1
</select>
<select id="selectRegionList" parameterType="BmsRegion" resultMap="BmsRegionResult">
<include refid="selectRegionVo"/>
<where>
del_flag = '0'
<if test="countryNameCn != null and countryNameCn != ''">
AND country_name_cn like concat('%', #{countryNameCn}, '%')
</if>
<if test="countryNameEn != null and countryNameEn != ''">
AND country_name_en like concat('%', #{countryNameEn}, '%')
</if>
<if test="countryCode2 != null and countryCode2 != ''">
AND country_code_2 like concat('%', #{countryCode2}, '%')
</if>
<if test="continent != null and continent != ''">
AND continent = #{continent}
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
</where>
order by region_id
</select>
<insert id="insertRegion" parameterType="BmsRegion" useGeneratedKeys="true" keyProperty="regionId">
insert into bms_region (
<if test="countryNameCn != null">country_name_cn,</if>
<if test="countryNameEn != null and countryNameEn != ''">country_name_en,</if>
<if test="countryCode2 != null and countryCode2 != ''">country_code_2,</if>
<if test="countryCode3 != null">country_code_3,</if>
<if test="continent != null">continent,</if>
<if test="subRegion != null">sub_region,</if>
<if test="currencyCode != null">currency_code,</if>
<if test="currencyName != null">currency_name,</if>
<if test="currencySymbol != null">currency_symbol,</if>
<if test="phonePrefix != null">phone_prefix,</if>
<if test="flagEmoji != null">flag_emoji,</if>
<if test="flagUrl != null">flag_url,</if>
<if test="isIndependent != null and isIndependent != ''">is_independent,</if>
<if test="isEu != null and isEu != ''">is_eu,</if>
<if test="status != null and status != ''">status,</if>
<if test="delFlag != null and delFlag != ''">del_flag,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="remark != null">remark,</if>
create_time
) values (
<if test="countryNameCn != null">#{countryNameCn},</if>
<if test="countryNameEn != null and countryNameEn != ''">#{countryNameEn},</if>
<if test="countryCode2 != null and countryCode2 != ''">#{countryCode2},</if>
<if test="countryCode3 != null">#{countryCode3},</if>
<if test="continent != null">#{continent},</if>
<if test="subRegion != null">#{subRegion},</if>
<if test="currencyCode != null">#{currencyCode},</if>
<if test="currencyName != null">#{currencyName},</if>
<if test="currencySymbol != null">#{currencySymbol},</if>
<if test="phonePrefix != null">#{phonePrefix},</if>
<if test="flagEmoji != null">#{flagEmoji},</if>
<if test="flagUrl != null">#{flagUrl},</if>
<if test="isIndependent != null and isIndependent != ''">#{isIndependent},</if>
<if test="isEu != null and isEu != ''">#{isEu},</if>
<if test="status != null and status != ''">#{status},</if>
<if test="delFlag != null and delFlag != ''">#{delFlag},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="remark != null">#{remark},</if>
sysdate()
)
</insert>
<update id="updateRegion" parameterType="BmsRegion">
update bms_region
<set>
<if test="countryNameCn != null">country_name_cn = #{countryNameCn},</if>
<if test="countryNameEn != null and countryNameEn != ''">country_name_en = #{countryNameEn},</if>
<if test="countryCode2 != null and countryCode2 != ''">country_code_2 = #{countryCode2},</if>
<if test="countryCode3 != null">country_code_3 = #{countryCode3},</if>
<if test="continent != null">continent = #{continent},</if>
<if test="subRegion != null">sub_region = #{subRegion},</if>
<if test="currencyCode != null">currency_code = #{currencyCode},</if>
<if test="currencyName != null">currency_name = #{currencyName},</if>
<if test="currencySymbol != null">currency_symbol = #{currencySymbol},</if>
<if test="phonePrefix != null">phone_prefix = #{phonePrefix},</if>
<if test="flagEmoji != null">flag_emoji = #{flagEmoji},</if>
<if test="flagUrl != null">flag_url = #{flagUrl},</if>
<if test="isIndependent != null and isIndependent != ''">is_independent = #{isIndependent},</if>
<if test="isEu != null and isEu != ''">is_eu = #{isEu},</if>
<if test="status != null and status != ''">status = #{status},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="remark != null">remark = #{remark},</if>
update_time = sysdate()
</set>
where region_id = #{regionId}
</update>
<delete id="deleteRegionByIds" parameterType="Long">
delete from bms_region where region_id in
<foreach item="regionId" collection="array" open="(" separator="," close=")">
#{regionId}
</foreach>
</delete>
<insert id="upsertByCode2" parameterType="BmsRegion">
insert into bms_region (
country_name_cn, country_name_en, country_code_2, country_code_3,
continent, sub_region, currency_code, currency_name, currency_symbol,
phone_prefix, flag_emoji, flag_url, is_independent, is_eu,
status, del_flag, create_by, create_time
) values (
#{countryNameCn}, #{countryNameEn}, #{countryCode2}, #{countryCode3},
#{continent}, #{subRegion}, #{currencyCode}, #{currencyName}, #{currencySymbol},
#{phonePrefix}, #{flagEmoji}, #{flagUrl}, #{isIndependent}, #{isEu},
#{status}, '0', #{createBy}, sysdate()
)
on duplicate key update
country_name_cn = values(country_name_cn),
country_name_en = values(country_name_en),
country_code_3 = values(country_code_3),
continent = values(continent),
sub_region = values(sub_region),
currency_code = values(currency_code),
currency_name = values(currency_name),
currency_symbol = values(currency_symbol),
phone_prefix = values(phone_prefix),
flag_emoji = values(flag_emoji),
flag_url = values(flag_url),
is_independent = values(is_independent),
is_eu = values(is_eu),
update_by = #{updateBy},
update_time = sysdate()
</insert>
</mapper>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.BmsRegionSyncLogMapper">
<resultMap type="BmsRegionSyncLog" id="BmsRegionSyncLogResult">
<id property="logId" column="log_id" />
<result property="syncType" column="sync_type" />
<result property="status" column="status" />
<result property="totalCount" column="total_count" />
<result property="insertCount" column="insert_count" />
<result property="updateCount" column="update_count" />
<result property="failCount" column="fail_count" />
<result property="durationMs" column="duration_ms" />
<result property="errorMsg" column="error_msg" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectSyncLogVo">
select log_id, sync_type, status, total_count, insert_count, update_count,
fail_count, duration_ms, error_msg, create_by, create_time, update_by, update_time
from bms_region_sync_log
</sql>
<select id="selectSyncLogById" parameterType="Long" resultMap="BmsRegionSyncLogResult">
<include refid="selectSyncLogVo"/>
where log_id = #{logId}
</select>
<select id="selectSyncLogList" parameterType="BmsRegionSyncLog" resultMap="BmsRegionSyncLogResult">
<include refid="selectSyncLogVo"/>
<where>
<if test="syncType != null and syncType != ''">
AND sync_type = #{syncType}
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="params.beginTime != null and params.beginTime != ''">
and date_format(create_time,'%Y%m%d') &gt;= date_format(#{params.beginTime},'%Y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''">
and date_format(create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')
</if>
</where>
order by log_id desc
</select>
<insert id="insertSyncLog" parameterType="BmsRegionSyncLog" useGeneratedKeys="true" keyProperty="logId">
insert into bms_region_sync_log (
<if test="syncType != null and syncType != ''">sync_type,</if>
<if test="status != null and status != ''">status,</if>
<if test="totalCount != null">total_count,</if>
<if test="insertCount != null">insert_count,</if>
<if test="updateCount != null">update_count,</if>
<if test="failCount != null">fail_count,</if>
<if test="durationMs != null">duration_ms,</if>
<if test="errorMsg != null">error_msg,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
) values (
<if test="syncType != null and syncType != ''">#{syncType},</if>
<if test="status != null and status != ''">#{status},</if>
<if test="totalCount != null">#{totalCount},</if>
<if test="insertCount != null">#{insertCount},</if>
<if test="updateCount != null">#{updateCount},</if>
<if test="failCount != null">#{failCount},</if>
<if test="durationMs != null">#{durationMs},</if>
<if test="errorMsg != null">#{errorMsg},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)
</insert>
<update id="updateSyncLog" parameterType="BmsRegionSyncLog">
update bms_region_sync_log
<set>
<if test="status != null and status != ''">status = #{status},</if>
<if test="totalCount != null">total_count = #{totalCount},</if>
<if test="insertCount != null">insert_count = #{insertCount},</if>
<if test="updateCount != null">update_count = #{updateCount},</if>
<if test="failCount != null">fail_count = #{failCount},</if>
<if test="durationMs != null">duration_ms = #{durationMs},</if>
<if test="errorMsg != null">error_msg = #{errorMsg},</if>
update_time = sysdate()
</set>
where log_id = #{logId}
</update>
<delete id="deleteSyncLogByIds" parameterType="Long">
delete from bms_region_sync_log where log_id in
<foreach item="logId" collection="array" open="(" separator="," close=")">
#{logId}
</foreach>
</delete>
</mapper>
+90
View File
@@ -0,0 +1,90 @@
-- ----------------------------
-- 1、国家/区域表
-- ----------------------------
DROP TABLE IF EXISTS bms_region;
CREATE TABLE bms_region (
region_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '区域ID',
country_name_cn VARCHAR(100) DEFAULT NULL COMMENT '中文名',
country_name_en VARCHAR(100) NOT NULL COMMENT '英文名',
country_code_2 VARCHAR(2) NOT NULL COMMENT 'ISO两位码',
country_code_3 VARCHAR(3) DEFAULT NULL COMMENT 'ISO三位码',
continent VARCHAR(50) DEFAULT NULL COMMENT '洲/大区',
sub_region VARCHAR(50) DEFAULT NULL COMMENT '子区域',
currency_code VARCHAR(10) DEFAULT NULL COMMENT '货币代码',
currency_name VARCHAR(50) DEFAULT NULL COMMENT '货币名称',
currency_symbol VARCHAR(10) DEFAULT NULL COMMENT '货币符号',
phone_prefix VARCHAR(10) DEFAULT NULL COMMENT '电话前缀',
flag_emoji VARCHAR(10) DEFAULT NULL COMMENT '国旗emoji',
flag_url VARCHAR(500) DEFAULT NULL COMMENT '国旗图片URL',
is_independent CHAR(1) NOT NULL DEFAULT '1' COMMENT '是否独立(1是 0否)',
is_eu CHAR(1) NOT NULL DEFAULT '0' COMMENT '是否欧盟成员(1是 0否)',
status CHAR(1) NOT NULL DEFAULT '0' COMMENT '状态(0正常 1停用)',
del_flag CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (region_id),
UNIQUE INDEX idx_country_code_2 (country_code_2),
INDEX idx_country_name_cn (country_name_cn),
INDEX idx_country_name_en (country_name_en),
INDEX idx_continent (continent)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='国家/区域表';
-- ----------------------------
-- 2、区域数据同步日志表
-- ----------------------------
DROP TABLE IF EXISTS bms_region_sync_log;
CREATE TABLE bms_region_sync_log (
log_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
sync_type VARCHAR(20) NOT NULL DEFAULT 'ALL' COMMENT '同步类型(ALL全量)',
status VARCHAR(20) NOT NULL DEFAULT 'PROCESSING' COMMENT '状态(PENDING/PROCESSING/SUCCESS/FAILED)',
total_count INT(11) DEFAULT 0 COMMENT 'API返回总数',
insert_count INT(11) DEFAULT 0 COMMENT '新增数',
update_count INT(11) DEFAULT 0 COMMENT '更新数',
fail_count INT(11) DEFAULT 0 COMMENT '失败数',
duration_ms BIGINT(20) DEFAULT 0 COMMENT '耗时(毫秒)',
error_msg TEXT DEFAULT NULL COMMENT '错误信息',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (log_id),
INDEX idx_status (status),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='区域数据同步日志表';
-- ----------------------------
-- 3、菜单数据
-- ----------------------------
-- 一级菜单: 数据管理
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES ('数据管理', 0, 6, 'data', NULL, 1, 0, 'M', '0', '0', '', 'chart', 'admin', sysdate(), '数据管理目录');
SET @parentId = LAST_INSERT_ID();
-- 二级菜单: 区域管理
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES ('区域管理', @parentId, 1, 'region', 'data/region/index', 1, 0, 'C', '0', '0', 'data:region:list', 'international', 'admin', sysdate(), '');
SET @regionMenuId = LAST_INSERT_ID();
-- 二级菜单: 同步日志
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES ('同步日志', @parentId, 2, 'region/sync-log', 'data/region/sync-log/index', 1, 0, 'C', '0', '0', 'data:region:log:list', 'log', 'admin', sysdate(), '');
-- 按钮权限 (区域管理)
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES ('区域查询', @regionMenuId, 1, '', NULL, 1, 0, 'F', '0', '0', 'data:region:query', '#', 'admin', sysdate(), '');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES ('区域编辑', @regionMenuId, 2, '', NULL, 1, 0, 'F', '0', '0', 'data:region:edit', '#', 'admin', sysdate(), '');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES ('区域删除', @regionMenuId, 3, '', NULL, 1, 0, 'F', '0', '0', 'data:region:remove', '#', 'admin', sysdate(), '');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES ('数据同步', @regionMenuId, 4, '', NULL, 1, 0, 'F', '0', '0', 'data:region:sync', '#', 'admin', sysdate(), '');
-- 按钮权限 (同步日志)
SET @logMenuId = @regionMenuId + 1;
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES ('日志删除', @logMenuId, 1, '', NULL, 1, 0, 'F', '0', '0', 'data:region:log:remove', '#', 'admin', sysdate(), '');
+5
View File
@@ -49,5 +49,10 @@
}, },
"resolutions": { "resolutions": {
"quill": "2.0.2" "quill": "2.0.2"
},
"allowScripts": {
"esbuild@0.25.12": true,
"@parcel/watcher@2.6.0": true,
"vue-demi@0.14.10": true
} }
} }
+26
View File
@@ -0,0 +1,26 @@
import request from '@/utils/request'
// 查询同步日志列表
export function listSyncLog(query) {
return request({
url: '/data/region/log/list',
method: 'get',
params: query
})
}
// 查询同步日志详细
export function getSyncLog(logId) {
return request({
url: '/data/region/log/' + logId,
method: 'get'
})
}
// 删除同步日志
export function delSyncLog(logIds) {
return request({
url: '/data/region/log/' + logIds,
method: 'delete'
})
}
+56
View File
@@ -0,0 +1,56 @@
import request from '@/utils/request'
// 查询区域列表
export function listRegion(query) {
return request({
url: '/data/region/list',
method: 'get',
params: query
})
}
// 查询区域详细
export function getRegion(regionId) {
return request({
url: '/data/region/' + regionId,
method: 'get'
})
}
// 修改区域
export function updateRegion(data) {
return request({
url: '/data/region',
method: 'put',
data: data
})
}
// 删除区域
export function delRegion(regionIds) {
return request({
url: '/data/region/' + regionIds,
method: 'delete'
})
}
// 修改区域状态
export function changeRegionStatus(regionId, status) {
const data = {
regionId,
status
}
return request({
url: '/data/region/changeStatus',
method: 'put',
data: data
})
}
// 同步国家数据
export function syncRegion() {
return request({
url: '/data/region/sync',
method: 'post'
})
}
@@ -91,6 +91,8 @@
* colProps: {} // 透传给 el-col 的属性 * colProps: {} // 透传给 el-col 的属性
* } * }
*/ */
import { ref, computed, reactive, isRef, watch, nextTick, getCurrentInstance } from "vue"
import FormControl from "./FormControl.vue"
defineOptions({ name: 'BasicForm' }) defineOptions({ name: 'BasicForm' })
+430
View File
@@ -0,0 +1,430 @@
<template>
<div class="app-container">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="regionId"
>
<template #toolbar="{ selection, ids }">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Refresh"
:loading="syncLoading"
@click="handleSync"
v-hasPermi="['data:region:sync']"
>同步国家数据</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="List"
@click="openSyncLogDialog"
v-hasPermi="['data:region:log:list']"
>同步日志</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="selection.length !== 1"
@click="handleUpdate(selection[0])"
v-hasPermi="['data:region:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="!selection.length"
@click="handleDelete(ids)"
v-hasPermi="['data:region:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['data:region:list']"
>导出</el-button>
</el-col>
</template>
<template #col-status="{ row }">
<el-switch
v-model="row.status"
active-value="0"
inactive-value="1"
@change="handleStatusChange(row)"
v-hasPermi="['data:region:edit']"
/>
</template>
<template #col-flagEmoji="{ row }">
<span style="font-size: 18px">{{ row.flagEmoji }}</span>
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['data:region:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(row.regionId)" v-hasPermi="['data:region:remove']">删除</el-button>
</template>
</basic-table>
<!-- 修改区域对话框 -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form ref="regionRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="中文名" prop="countryNameCn">
<el-input v-model="form.countryNameCn" placeholder="请输入中文名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="英文名" prop="countryNameEn">
<el-input v-model="form.countryNameEn" placeholder="请输入英文名" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="ISO两位码" prop="countryCode2">
<el-input v-model="form.countryCode2" placeholder="如: DE" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="ISO三位码" prop="countryCode3">
<el-input v-model="form.countryCode3" placeholder="如: DEU" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="洲/大区" prop="continent">
<el-input v-model="form.continent" placeholder="如: Europe" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="子区域" prop="subRegion">
<el-input v-model="form.subRegion" placeholder="如: Western Europe" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="货币代码" prop="currencyCode">
<el-input v-model="form.currencyCode" placeholder="如: EUR" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电话前缀" prop="phonePrefix">
<el-input v-model="form.phonePrefix" placeholder="如: +49" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="是否独立" prop="isIndependent">
<el-radio-group v-model="form.isIndependent">
<el-radio value="1"></el-radio>
<el-radio value="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否欧盟" prop="isEu">
<el-radio-group v-model="form.isEu">
<el-radio value="1"></el-radio>
<el-radio value="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<!-- 同步日志对话框 -->
<el-dialog title="同步日志" v-model="logDialogVisible" width="1200px" append-to-body top="8vh">
<basic-table
ref="logTableRef"
:columns="logColumns"
:search-columns="logSearchColumns"
:query="logQueryList"
row-key="logId"
:show-search="true"
>
<template #toolbar="{ selection, ids }">
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="!selection.length"
@click="handleLogDelete(ids)"
v-hasPermi="['data:region:log:remove']"
>删除</el-button>
</el-col>
</template>
<template #col-status="{ row }">
<el-tag :type="logStatusType(row.status)">
{{ logStatusText(row.status) }}
</el-tag>
</template>
</basic-table>
</el-dialog>
</div>
</template>
<script setup name="Region">
import { listRegion, getRegion, updateRegion, delRegion, changeRegionStatus, syncRegion } from "@/api/data/region"
import { getSyncLog, listSyncLog, delSyncLog } from "@/api/data/region-log"
import { nextTick, onBeforeUnmount } from "vue"
const { proxy } = getCurrentInstance()
const tableRef = ref()
const open = ref(false)
const title = ref("")
const syncLoading = ref(false)
const syncLogId = ref(null)
const syncTimer = ref(null)
// ====== 同步日志对话框 ======
const logDialogVisible = ref(false)
const logTableRef = ref()
const logSearchColumns = [
{ label: "状态", prop: "status", component: "select", options: [
{ label: "待处理", value: "PENDING" },
{ label: "处理中", value: "PROCESSING" },
{ label: "成功", value: "SUCCESS" },
{ label: "失败", value: "FAILED" }
], placeholder: "状态", width: "160px" },
{ label: "创建时间", prop: "dateRange", component: "daterange", width: "320px" }
]
const logColumns = [
{ label: "日志ID", prop: "logId", align: "center", width: 80 },
{ label: "同步类型", prop: "syncType", align: "center", width: 100 },
{ label: "状态", prop: "status", align: "center", width: 100 },
{ label: "总数", prop: "totalCount", align: "center", width: 80 },
{ label: "新增数", prop: "insertCount", align: "center", width: 80 },
{ label: "更新数", prop: "updateCount", align: "center", width: 80 },
{ label: "失败数", prop: "failCount", align: "center", width: 80 },
{ label: "耗时(ms)", prop: "durationMs", align: "center", width: 100 },
{ label: "错误信息", prop: "errorMsg", align: "center", showOverflowTooltip: true },
{ label: "创建时间", prop: "createTime", align: "center", width: 180, parseTime: true }
]
function logQueryList(params) {
const p = proxy.addDateRange({ ...params }, params.dateRange)
delete p.dateRange
return listSyncLog(p)
}
function logStatusText(status) {
const map = { PENDING: "待处理", PROCESSING: "处理中", SUCCESS: "成功", FAILED: "失败" }
return map[status] || status
}
function logStatusType(status) {
const map = { PENDING: "info", PROCESSING: "warning", SUCCESS: "success", FAILED: "danger" }
return map[status] || "info"
}
function handleLogDelete(logIds) {
proxy.$modal.confirm('是否确认删除日志编号为"' + logIds + '"的数据项?').then(function () {
return delSyncLog(logIds)
}).then(() => {
logTableRef.value.refresh()
proxy.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
function openSyncLogDialog() {
logDialogVisible.value = true
nextTick(() => { logTableRef.value && logTableRef.value.reload() })
}
// 搜索项配置
const searchColumns = [
{ label: "中文名", prop: "countryNameCn", component: "input", placeholder: "请输入中文名", width: "200px" },
{ label: "英文名", prop: "countryNameEn", component: "input", placeholder: "请输入英文名", width: "200px" },
{ label: "国家代码", prop: "countryCode2", component: "input", placeholder: "如: DE", width: "160px" },
{ label: "洲/大区", prop: "continent", component: "input", placeholder: "如: Europe", width: "200px" },
{ label: "状态", prop: "status", component: "select", options: [{ label: "正常", value: "0" }, { label: "停用", value: "1" }], placeholder: "状态", width: "140px" }
]
// 表格列配置
const columns = [
{ label: "区域ID", prop: "regionId", align: "center", width: 80 },
{ label: "国旗", prop: "flagEmoji", align: "center", width: 60 },
{ label: "中文名", prop: "countryNameCn", align: "center" },
{ label: "英文名", prop: "countryNameEn", align: "center" },
{ label: "国家代码", prop: "countryCode2", align: "center", width: 80 },
{ label: "ISO三位码", prop: "countryCode3", align: "center", width: 90 },
{ label: "洲/大区", prop: "continent", align: "center", width: 120 },
{ label: "子区域", prop: "subRegion", align: "center", width: 140 },
{ label: "货币", prop: "currencyCode", align: "center", width: 80 },
{ label: "电话前缀", prop: "phonePrefix", align: "center", width: 90 },
{ label: "状态", prop: "status", align: "center", width: 80 },
{ label: "操作", prop: "action", align: "center", width: 150, showInToolbar: false }
]
// 查询函数
function queryList(params) {
return listRegion(params)
}
const data = reactive({
form: {},
rules: {
countryNameEn: [{ required: true, message: "英文名不能为空", trigger: "blur" }],
countryCode2: [{ required: true, message: "ISO两位码不能为空", trigger: "blur" }]
}
})
const { form, rules } = toRefs(data)
/** 取消按钮 */
function cancel() {
open.value = false
reset()
}
/** 表单重置 */
function reset() {
form.value = {
regionId: undefined,
countryNameCn: undefined,
countryNameEn: undefined,
countryCode2: undefined,
countryCode3: undefined,
continent: undefined,
subRegion: undefined,
currencyCode: undefined,
phonePrefix: undefined,
isIndependent: "1",
isEu: "0",
remark: undefined
}
proxy.resetForm("regionRef")
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset()
const regionId = row.regionId
getRegion(regionId).then(response => {
form.value = response.data
open.value = true
title.value = "修改区域"
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["regionRef"].validate(valid => {
if (valid) {
updateRegion(form.value).then(() => {
proxy.$modal.msgSuccess("修改成功")
open.value = false
tableRef.value.refresh()
})
}
})
}
/** 删除按钮操作 */
function handleDelete(regionIds) {
proxy.$modal.confirm('是否确认删除区域编号为"' + regionIds + '"的数据项?').then(function () {
return delRegion(regionIds)
}).then(() => {
tableRef.value.refresh()
proxy.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
/** 修改状态 */
function handleStatusChange(row) {
// 优先显示:中文名 > 英文名 > 国家代码
const displayName = row.countryNameCn || row.countryNameEn || row.countryCode2 || '';
if (row.regionId === undefined || row.regionId === null || row.regionId === '' || !displayName) {
nextTick(() => {
row.status = row.status === "0" ? "1" : "0"
})
return
}
if (row.__originalStatus !== undefined && row.__originalStatus === row.status) {
return
}
row.__originalStatus = row.status === "0" ? "1" : "0"
const text = row.status === "0" ? "启用" : "停用"
proxy.$modal.confirm(`确认要${text}${displayName}」国家/区域吗?`).then(function () {
return changeRegionStatus(row.regionId, row.status)
}).then(() => {
row.__originalStatus = row.status
proxy.$modal.msgSuccess(text + "成功")
}).catch(function () {
const restored = row.status === "0" ? "1" : "0"
row.__originalStatus = restored
nextTick(() => { row.status = restored })
})
}
/** 同步国家数据 */
function handleSync() {
proxy.$modal.confirm('确认从第三方API同步国家数据?').then(function () {
syncLoading.value = true
return syncRegion()
}).then(res => {
syncLogId.value = res.logId
proxy.$modal.msgSuccess("同步任务已创建,正在后台执行...")
// 轮询同步状态
syncTimer.value = setInterval(() => {
getSyncLog(syncLogId.value).then(response => {
const log = response.data
if (log.status === "SUCCESS") {
clearInterval(syncTimer.value)
syncTimer.value = null
syncLoading.value = false
tableRef.value.refresh()
proxy.$modal.msgSuccess(`同步完成!总计${log.totalCount}条,新增${log.insertCount}条,更新${log.updateCount}条,失败${log.failCount}`)
} else if (log.status === "FAILED") {
clearInterval(syncTimer.value)
syncTimer.value = null
syncLoading.value = false
proxy.$modal.msgError("同步失败:" + log.errorMsg)
}
})
}, 3000)
}).catch(() => {
syncLoading.value = false
})
}
/** 导出按钮操作 */
function handleExport() {
proxy.download("data/region/export", {
...tableRef.value.getQueryParams()
}, `region_${new Date().getTime()}.xlsx`)
}
/** 组件卸载前清理定时器 */
onBeforeUnmount(() => {
if (syncTimer.value) {
clearInterval(syncTimer.value)
syncTimer.value = null
}
})
</script>
@@ -0,0 +1,91 @@
<template>
<div class="app-container">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="logId"
>
<template #toolbar="{ selection, ids }">
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="!selection.length"
@click="handleDelete(ids)"
v-hasPermi="['data:region:log:remove']"
>删除</el-button>
</el-col>
</template>
<template #col-status="{ row }">
<el-tag :type="statusType(row.status)">
{{ statusText(row.status) }}
</el-tag>
</template>
</basic-table>
</div>
</template>
<script setup name="RegionSyncLog">
import { listSyncLog, delSyncLog } from "@/api/data/region-log"
const { proxy } = getCurrentInstance()
const tableRef = ref()
// 搜索项配置
const searchColumns = [
{ label: "状态", prop: "status", component: "select", options: [
{ label: "待处理", value: "PENDING" },
{ label: "处理中", value: "PROCESSING" },
{ label: "成功", value: "SUCCESS" },
{ label: "失败", value: "FAILED" }
], placeholder: "状态", width: "160px" },
{ label: "创建时间", prop: "dateRange", component: "daterange", width: "320px" }
]
// 表格列配置
const columns = [
{ label: "日志ID", prop: "logId", align: "center", width: 80 },
{ label: "同步类型", prop: "syncType", align: "center", width: 100 },
{ label: "状态", prop: "status", align: "center", width: 100 },
{ label: "总数", prop: "totalCount", align: "center", width: 80 },
{ label: "新增数", prop: "insertCount", align: "center", width: 80 },
{ label: "更新数", prop: "updateCount", align: "center", width: 80 },
{ label: "失败数", prop: "failCount", align: "center", width: 80 },
{ label: "耗时(ms)", prop: "durationMs", align: "center", width: 100 },
{ label: "错误信息", prop: "errorMsg", align: "center", showOverflowTooltip: true },
{ label: "创建时间", prop: "createTime", align: "center", width: 180, parseTime: true }
]
// 查询函数
function queryList(params) {
const p = proxy.addDateRange({ ...params }, params.dateRange)
delete p.dateRange
return listSyncLog(p)
}
/** 状态文本 */
function statusText(status) {
const map = { PENDING: "待处理", PROCESSING: "处理中", SUCCESS: "成功", FAILED: "失败" }
return map[status] || status
}
/** 状态标签类型 */
function statusType(status) {
const map = { PENDING: "info", PROCESSING: "warning", SUCCESS: "success", FAILED: "danger" }
return map[status] || "info"
}
/** 删除按钮操作 */
function handleDelete(logIds) {
proxy.$modal.confirm('是否确认删除日志编号为"' + logIds + '"的数据项?').then(function () {
return delSyncLog(logIds)
}).then(() => {
tableRef.value.refresh()
proxy.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
</script>
File diff suppressed because it is too large Load Diff
+19 -3
View File
@@ -179,6 +179,7 @@
<script setup name="Role"> <script setup name="Role">
import { addRole, changeRoleStatus, dataScope, delRole, getRole, listRole, updateRole, deptTreeSelect } from "@/api/system/role" import { addRole, changeRoleStatus, dataScope, delRole, getRole, listRole, updateRole, deptTreeSelect } from "@/api/system/role"
import { roleMenuTreeselect, treeselect as menuTreeselect } from "@/api/system/menu" import { roleMenuTreeselect, treeselect as menuTreeselect } from "@/api/system/menu"
import { nextTick } from "vue"
const router = useRouter() const router = useRouter()
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
@@ -263,13 +264,28 @@ function handleExport() {
/** 角色状态修改 */ /** 角色状态修改 */
function handleStatusChange(row) { function handleStatusChange(row) {
let text = row.status === "0" ? "启用" : "停用" if (row.roleId === undefined || row.roleId === null || row.roleId === '' ||
proxy.$modal.confirm('确认要"' + text + '""' + row.roleName + '"角色吗?').then(function () { row.roleName === undefined || row.roleName === null || row.roleName === '') {
nextTick(() => {
row.status = row.status === "0" ? "1" : "0"
})
return
}
if (row.__originalStatus !== undefined && row.__originalStatus === row.status) {
return
}
row.__originalStatus = row.status === "0" ? "1" : "0"
const text = row.status === "0" ? "启用" : "停用"
proxy.$modal.confirm(`确认要${text}${row.roleName}」角色吗?`).then(function () {
return changeRoleStatus(row.roleId, row.status) return changeRoleStatus(row.roleId, row.status)
}).then(() => { }).then(() => {
row.__originalStatus = row.status
proxy.$modal.msgSuccess(text + "成功") proxy.$modal.msgSuccess(text + "成功")
}).catch(function () { }).catch(function () {
row.status = row.status === "0" ? "1" : "0" const restored = row.status === "0" ? "1" : "0"
row.__originalStatus = restored
nextTick(() => { row.status = restored })
}) })
} }
+24 -3
View File
@@ -161,6 +161,7 @@ import ExcelImportDialog from "@/components/ExcelImportDialog"
import UserViewDrawer from "./view" import UserViewDrawer from "./view"
import { usePasswordRule } from "@/utils/passwordRule" import { usePasswordRule } from "@/utils/passwordRule"
import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect } from "@/api/system/user" import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect } from "@/api/system/user"
import { nextTick } from "vue"
const router = useRouter() const router = useRouter()
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
@@ -265,13 +266,33 @@ function handleExport() {
/** 用户状态修改 */ /** 用户状态修改 */
function handleStatusChange(row) { function handleStatusChange(row) {
let text = row.status === "0" ? "启用" : "停用" // 守卫:页面初始化/数据加载过程中 el-switch 可能发生赋值抖动,
proxy.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?').then(function () { // userId 或 userName 缺失时不弹确认,只把状态反向重置防止显示偏差
if (row.userId === undefined || row.userId === null || row.userId === '' ||
row.userName === undefined || row.userName === null || row.userName === '') {
nextTick(() => {
row.status = row.status === "0" ? "1" : "0"
})
return
}
// 与原始记录中的 status 对比:status 只能是字符串 "0"/"1",若不一致(如 loading 中被改)则回滚
if (row.__originalStatus !== undefined && row.__originalStatus === row.status) {
return
}
// 为当前行记录一次原始值,后面 catch 时与回滚同步,避免重复触发 change
row.__originalStatus = row.status === "0" ? "1" : "0"
const text = row.status === "0" ? "启用" : "停用"
proxy.$modal.confirm(`确认要${text}${row.userName}」用户吗?`).then(function () {
return changeUserStatus(row.userId, row.status) return changeUserStatus(row.userId, row.status)
}).then(() => { }).then(() => {
row.__originalStatus = row.status
proxy.$modal.msgSuccess(text + "成功") proxy.$modal.msgSuccess(text + "成功")
}).catch(function () { }).catch(function () {
row.status = row.status === "0" ? "1" : "0" const restored = row.status === "0" ? "1" : "0"
row.__originalStatus = restored
// 回滚赋值时短暂忽略 change,防止递归弹框
nextTick(() => { row.status = restored })
}) })
} }