diff --git a/core/core-backend/src/main/java/io/dataease/config/DeMvcConfig.java b/core/core-backend/src/main/java/io/dataease/config/DeMvcConfig.java index 3d77809d14..a21c988dc5 100644 --- a/core/core-backend/src/main/java/io/dataease/config/DeMvcConfig.java +++ b/core/core-backend/src/main/java/io/dataease/config/DeMvcConfig.java @@ -21,12 +21,16 @@ public class DeMvcConfig implements WebMvcConfigurer { public void addResourceHandlers(ResourceHandlerRegistry registry) { String workDir = FILE_PROTOCOL + ensureSuffix(WORK_DIR, FILE_SEPARATOR); String uploadUrlPattern = ensureBoth(URL_SEPARATOR + UPLOAD_URL_PREFIX, AuthConstant.DE_API_PREFIX, URL_SEPARATOR) + "**"; - registry.addResourceHandler(uploadUrlPattern) - .addResourceLocations(workDir); + registry.addResourceHandler(uploadUrlPattern).addResourceLocations(workDir); + // map String mapDir = FILE_PROTOCOL + ensureSuffix(MAP_DIR, FILE_SEPARATOR); String mapUrlPattern = ensureBoth(MAP_URL, AuthConstant.DE_API_PREFIX, URL_SEPARATOR) + "**"; - registry.addResourceHandler(mapUrlPattern) - .addResourceLocations(mapDir); + registry.addResourceHandler(mapUrlPattern).addResourceLocations(mapDir); + + String geoDir = FILE_PROTOCOL + ensureSuffix(CUSTOM_MAP_DIR, FILE_SEPARATOR); + String geoUrlPattern = ensureBoth(GEO_URL, AuthConstant.DE_API_PREFIX, URL_SEPARATOR) + "**"; + registry.addResourceHandler(geoUrlPattern).addResourceLocations(geoDir); + } } diff --git a/core/core-backend/src/main/java/io/dataease/map/bo/AreaBO.java b/core/core-backend/src/main/java/io/dataease/map/bo/AreaBO.java new file mode 100644 index 0000000000..c9620c84a3 --- /dev/null +++ b/core/core-backend/src/main/java/io/dataease/map/bo/AreaBO.java @@ -0,0 +1,13 @@ +package io.dataease.map.bo; + +import io.dataease.map.dao.auto.entity.Area; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +@EqualsAndHashCode(callSuper = true) +@Data +public class AreaBO extends Area implements Serializable { + private boolean custom = false; +} diff --git a/core/core-backend/src/main/java/io/dataease/map/dao/ext/entity/CoreAreaCustom.java b/core/core-backend/src/main/java/io/dataease/map/dao/ext/entity/CoreAreaCustom.java new file mode 100644 index 0000000000..808d7eef37 --- /dev/null +++ b/core/core-backend/src/main/java/io/dataease/map/dao/ext/entity/CoreAreaCustom.java @@ -0,0 +1,15 @@ +package io.dataease.map.dao.ext.entity; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class CoreAreaCustom implements Serializable { + + private String id; + + private String pid; + + private String name; +} diff --git a/core/core-backend/src/main/java/io/dataease/map/dao/ext/mapper/CoreAreaCustomMapper.java b/core/core-backend/src/main/java/io/dataease/map/dao/ext/mapper/CoreAreaCustomMapper.java new file mode 100644 index 0000000000..90605915f5 --- /dev/null +++ b/core/core-backend/src/main/java/io/dataease/map/dao/ext/mapper/CoreAreaCustomMapper.java @@ -0,0 +1,9 @@ +package io.dataease.map.dao.ext.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import io.dataease.map.dao.ext.entity.CoreAreaCustom; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface CoreAreaCustomMapper extends BaseMapper { +} diff --git a/core/core-backend/src/main/java/io/dataease/map/manage/MapManage.java b/core/core-backend/src/main/java/io/dataease/map/manage/MapManage.java index 98ddcc2e26..448339da25 100644 --- a/core/core-backend/src/main/java/io/dataease/map/manage/MapManage.java +++ b/core/core-backend/src/main/java/io/dataease/map/manage/MapManage.java @@ -1,17 +1,34 @@ package io.dataease.map.manage; -import cn.hutool.core.collection.CollectionUtil; +import io.dataease.api.map.dto.GeometryNodeCreator; import io.dataease.api.map.vo.AreaNode; +import io.dataease.constant.StaticResourceConstants; +import io.dataease.exception.DEException; +import io.dataease.map.bo.AreaBO; import io.dataease.map.dao.auto.entity.Area; import io.dataease.map.dao.auto.mapper.AreaMapper; +import io.dataease.map.dao.ext.entity.CoreAreaCustom; +import io.dataease.map.dao.ext.mapper.CoreAreaCustomMapper; import io.dataease.utils.BeanUtils; +import io.dataease.utils.CommonBeanFactory; +import io.dataease.utils.LogUtil; import jakarta.annotation.Resource; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import static io.dataease.constant.CacheConstant.CommonCacheConstant.WORLD_MAP_CACHE; @@ -19,6 +36,8 @@ import static io.dataease.constant.CacheConstant.CommonCacheConstant.WORLD_MAP_C public class MapManage { private final static AreaNode WORLD; + private static final String GEO_PREFIX = "geo_"; + static { WORLD = AreaNode.builder() .id("000") @@ -30,13 +49,34 @@ public class MapManage { @Resource private AreaMapper areaMapper; + @Resource + private CoreAreaCustomMapper coreAreaCustomMapper; + + public List defaultArea() { + return areaMapper.selectList(null); + } + + private MapManage proxy() { + return CommonBeanFactory.getBean(MapManage.class); + } + @Cacheable(value = WORLD_MAP_CACHE, key = "'world_map'") public AreaNode getWorldTree() { - List areas = areaMapper.selectList(null); + List areas = proxy().defaultArea(); + List areaBOS = areas.stream().map(item -> BeanUtils.copyBean(new AreaBO(), item)).collect(Collectors.toList()); + List coreAreaCustoms = coreAreaCustomMapper.selectList(null); + if (CollectionUtils.isNotEmpty(coreAreaCustoms)) { + List customBoList = coreAreaCustoms.stream().map(item -> { + AreaBO areaBO = BeanUtils.copyBean(new AreaBO(), item); + areaBO.setCustom(true); + return areaBO; + }).toList(); + areaBOS.addAll(customBoList); + } WORLD.setChildren(new ArrayList<>()); var areaNodeMap = new HashMap(); areaNodeMap.put(WORLD.getId(), WORLD); - areas.forEach(area -> { + areaBOS.forEach(area -> { var node = areaNodeMap.get(area.getId()); if (node == null) { node = AreaNode.builder().build(); @@ -64,5 +104,80 @@ public class MapManage { return WORLD; } + @CacheEvict(cacheNames = WORLD_MAP_CACHE, key = "'world_map'") + @Transactional + public void saveMapGeo(GeometryNodeCreator request, MultipartFile file) { + List areas = proxy().defaultArea(); + String code = getBusiGeoCode(request.getCode()); + + AtomicReference atomicReference = new AtomicReference<>(); + if (areas.stream().anyMatch(area -> { + boolean exist = area.getId().equals(code); + if (exist) { + atomicReference.set(area.getName()); + } + return exist; + })) { + DEException.throwException(String.format("Area code [%s] is already exists for [%s]", code, atomicReference.get())); + } + + CoreAreaCustom originData = null; + if (ObjectUtils.isNotEmpty(originData = coreAreaCustomMapper.selectById(getDaoGeoCode(code)))) { + DEException.throwException(String.format("Area code [%s] is already exists for [%s]", code, originData.getName())); + } + + CoreAreaCustom coreAreaCustom = new CoreAreaCustom(); + coreAreaCustom.setId(getDaoGeoCode(code)); + coreAreaCustom.setPid(request.getPid()); + coreAreaCustom.setName(request.getName()); + coreAreaCustomMapper.insert(coreAreaCustom); + + File geoFile = buildGeoFile(code); + try { + file.transferTo(geoFile); + } catch (IOException e) { + LogUtil.error(e.getMessage()); + DEException.throwException(e); + } + } + + @CacheEvict(cacheNames = WORLD_MAP_CACHE, key = "'world_map'") + @Transactional + public void deleteGeo(String code) { + if (!StringUtils.startsWith(code, GEO_PREFIX)) { + DEException.throwException("内置Geometry,禁止删除"); + } + coreAreaCustomMapper.deleteById(code); + File file = buildGeoFile(code); + if (file.exists()) { + file.delete(); + } + } + + private String getDaoGeoCode(String code) { + return StringUtils.startsWith(code, GEO_PREFIX) ? code : (GEO_PREFIX + code); + } + + private String getBusiGeoCode(String code) { + return StringUtils.startsWith(code, GEO_PREFIX) ? code.substring(GEO_PREFIX.length()) : code; + } + + private File buildGeoFile(String code) { + String id = getBusiGeoCode(code); + String customMapDir = StaticResourceConstants.CUSTOM_MAP_DIR; + String countryCode = countryCode(id); + String fileDirPath = customMapDir + "/" + countryCode + "/"; + File dir = new File(fileDirPath); + if (!dir.exists()) { + dir.mkdirs(); + } + String filePath = fileDirPath + id + ".json"; + return new File(filePath); + } + + private String countryCode(String code) { + return code.substring(0, 3); + } + } diff --git a/core/core-backend/src/main/java/io/dataease/map/server/GeoServer.java b/core/core-backend/src/main/java/io/dataease/map/server/GeoServer.java new file mode 100644 index 0000000000..4d4c0ebbe3 --- /dev/null +++ b/core/core-backend/src/main/java/io/dataease/map/server/GeoServer.java @@ -0,0 +1,26 @@ +package io.dataease.map.server; + +import io.dataease.api.map.GeoApi; +import io.dataease.api.map.dto.GeometryNodeCreator; +import io.dataease.map.manage.MapManage; +import jakarta.annotation.Resource; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/geometry") +public class GeoServer implements GeoApi { + + @Resource + private MapManage mapManage; + @Override + public void saveMapGeo(GeometryNodeCreator request, MultipartFile file) { + mapManage.saveMapGeo(request, file); + } + + @Override + public void deleteGeo(String id) { + mapManage.deleteGeo(id); + } +} diff --git a/core/core-backend/src/main/resources/db/migration/V2.1__ddl.sql b/core/core-backend/src/main/resources/db/migration/V2.1__ddl.sql index bc45bca231..dd0e6efcd2 100644 --- a/core/core-backend/src/main/resources/db/migration/V2.1__ddl.sql +++ b/core/core-backend/src/main/resources/db/migration/V2.1__ddl.sql @@ -26,12 +26,25 @@ VALUES (20, 15, 2, 'template-setting', 'system/template-setting', 4, 'icon_templ COMMIT; DROP TABLE IF EXISTS `visualization_template_extend_data`; -CREATE TABLE `visualization_template_extend_data` ( - `id` bigint NOT NULL, - `dv_id` bigint DEFAULT NULL, - `view_id` bigint DEFAULT NULL, - `view_details` longtext, - `copy_from` varchar(255) DEFAULT NULL, - `copy_id` varchar(255) DEFAULT NULL, - PRIMARY KEY (`id`) +CREATE TABLE `visualization_template_extend_data` +( + `id` bigint NOT NULL, + `dv_id` bigint DEFAULT NULL, + `view_id` bigint DEFAULT NULL, + `view_details` longtext, + `copy_from` varchar(255) DEFAULT NULL, + `copy_id` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +); + +-- ---------------------------- +-- Table structure for core_area_custom +-- ---------------------------- +DROP TABLE IF EXISTS `core_area_custom`; +CREATE TABLE `core_area_custom` +( + `id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `pid` varchar(255) NOT NULL, + PRIMARY KEY (`id`) ); diff --git a/core/core-frontend/src/api/map.ts b/core/core-frontend/src/api/map.ts index 66afbf439b..7023c28c49 100644 --- a/core/core-frontend/src/api/map.ts +++ b/core/core-frontend/src/api/map.ts @@ -5,9 +5,21 @@ export const getWorldTree = (): Promise> => { return request.get({ url: '/map/worldTree' }) } -export const getGeoJson = ( - country: string, - areaId: string -): Promise> => { - return request.get({ url: `/map/${country}/${areaId}.json` }) +export const getGeoJson = (areaId: string): Promise> => { + let prefix = '/map' + let areaCode = areaId + if (isCustomGeo(areaId)) { + prefix = '/geo' + areaCode = getBusiGeoCode(areaId) + } + const realCountry = areaCode.substring(0, 3) + const url = `${prefix}/${realCountry}/${areaCode}.json` + return request.get({ url }) +} + +const isCustomGeo = (id: string) => { + return id.startsWith('geo_') +} +const getBusiGeoCode = (id: string) => { + return id.substring(4) } diff --git a/core/core-frontend/src/config/axios/service.ts b/core/core-frontend/src/config/axios/service.ts index ee50af4d99..c066786dd7 100644 --- a/core/core-frontend/src/config/axios/service.ts +++ b/core/core-frontend/src/config/axios/service.ts @@ -130,7 +130,7 @@ service.interceptors.response.use( return response } else if (response.data.code === result_code || response.data.code === 50002) { return response.data - } else if (response.config.url.match(/^\/map\/\d{3}\/\d+\.json$/)) { + } else if (response.config.url.match(/^\/map|geo\/\d{3}\/\d+\.json$/)) { // TODO 处理静态文件 return response } else { diff --git a/core/core-frontend/src/views/chart/components/js/util.ts b/core/core-frontend/src/views/chart/components/js/util.ts index 59ed01a9b3..7810c1d310 100644 --- a/core/core-frontend/src/views/chart/components/js/util.ts +++ b/core/core-frontend/src/views/chart/components/js/util.ts @@ -421,10 +421,8 @@ export const getGeoJsonFile = async (areaId: string): Promise const mapStore = useMapStoreWithOut() let geoJson = mapStore.mapCache[areaId] if (!geoJson) { - const country = areaId.slice(0, 3) - geoJson = await getGeoJson(country, areaId).then(result => { - return result.data - }) + const res = await getGeoJson(areaId) + geoJson = res.data mapStore.setMap({ id: areaId, geoJson }) } return toRaw(geoJson) diff --git a/core/core-frontend/src/views/system/parameter/map/Geometry.vue b/core/core-frontend/src/views/system/parameter/map/Geometry.vue index 68768887d2..67244cba94 100644 --- a/core/core-frontend/src/views/system/parameter/map/Geometry.vue +++ b/core/core-frontend/src/views/system/parameter/map/Geometry.vue @@ -43,6 +43,19 @@ :title="data.name" v-html="data.colorName && keyword ? data.colorName : data.name" /> + + + + + + + @@ -83,7 +96,7 @@ - + diff --git a/core/core-frontend/src/views/system/parameter/map/GeometryEdit.vue b/core/core-frontend/src/views/system/parameter/map/GeometryEdit.vue index cb4d663e2f..8c8dd53dc7 100644 --- a/core/core-frontend/src/views/system/parameter/map/GeometryEdit.vue +++ b/core/core-frontend/src/views/system/parameter/map/GeometryEdit.vue @@ -1,5 +1,5 @@